From 14e315856276a0e6c16f71bb42f89908ee681272 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 22 Oct 2026 12:23:53 +0900 Subject: [PATCH 1/7] feat(extension): mark shared commands with unpushed local edits in command list Show a CloudUpload icon with a badge on the share button when a shared command has local edits not yet pushed to the Hub, and push the edits on click. Commands without a content fingerprint count as unchanged. Closes #507 Co-Authored-By: Claude Opus 5.5 --- .../public/_locales/de/messages.json | 3 ++ .../public/_locales/en/messages.json | 4 ++ .../public/_locales/es/messages.json | 3 ++ .../public/_locales/fr/messages.json | 3 ++ .../public/_locales/hi/messages.json | 3 ++ .../public/_locales/id/messages.json | 3 ++ .../public/_locales/it/messages.json | 3 ++ .../public/_locales/ja/messages.json | 3 ++ .../public/_locales/ko/messages.json | 3 ++ .../public/_locales/ms/messages.json | 3 ++ .../public/_locales/pt_BR/messages.json | 3 ++ .../public/_locales/pt_PT/messages.json | 3 ++ .../public/_locales/ru/messages.json | 3 ++ .../public/_locales/zh_CN/messages.json | 3 ++ .../src/components/option/ShareButton.tsx | 40 ++++++++++++++++--- .../option/__tests__/ShareButton.test.tsx | 28 ++++++++++++- .../option/editor/CommandTreeRenderer.tsx | 5 +++ .../hub/__tests__/commandContent.test.ts | 17 ++++++++ .../src/services/hub/commandContent.ts | 11 +++++ packages/extension/src/testIds.ts | 1 + 20 files changed, 139 insertions(+), 6 deletions(-) diff --git a/packages/extension/public/_locales/de/messages.json b/packages/extension/public/_locales/de/messages.json index f8b65c428..da7191941 100644 --- a/packages/extension/public/_locales/de/messages.json +++ b/packages/extension/public/_locales/de/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Bereits geteilt" }, + "Option_shareButton_modified_tooltip": { + "message": "Lokale Änderungen nicht mit dem Hub synchronisiert. Zum Aktualisieren klicken" + }, "Option_remove_title": { "message": "Dies löschen?" }, diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 43e5a44d1..584127163 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -833,6 +833,10 @@ "message": "Already shared", "description": "Tooltip for the share button when the command has already been shared to the Hub. Clicking opens the hub dashboard for this command." }, + "Option_shareButton_modified_tooltip": { + "message": "Local changes not synced to Hub. Click to update", + "description": "Tooltip for the share button when the command has already been shared to the Hub but has local edits not yet pushed. Clicking pushes the edits to the Hub." + }, "Option_remove_title": { "message": "Delete this?" }, diff --git a/packages/extension/public/_locales/es/messages.json b/packages/extension/public/_locales/es/messages.json index 390643087..d9234f9ea 100644 --- a/packages/extension/public/_locales/es/messages.json +++ b/packages/extension/public/_locales/es/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Ya compartido" }, + "Option_shareButton_modified_tooltip": { + "message": "Hay cambios locales sin sincronizar con el Hub. Haz clic para actualizar" + }, "Option_remove_title": { "message": "¿Eliminar esto?" }, diff --git a/packages/extension/public/_locales/fr/messages.json b/packages/extension/public/_locales/fr/messages.json index c99e1ce6b..d6e484ae6 100644 --- a/packages/extension/public/_locales/fr/messages.json +++ b/packages/extension/public/_locales/fr/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Déjà partagé" }, + "Option_shareButton_modified_tooltip": { + "message": "Modifications locales non synchronisées avec le Hub. Cliquez pour mettre à jour" + }, "Option_remove_title": { "message": "Supprimer ceci ?" }, diff --git a/packages/extension/public/_locales/hi/messages.json b/packages/extension/public/_locales/hi/messages.json index ce51b24fa..94166e3b8 100644 --- a/packages/extension/public/_locales/hi/messages.json +++ b/packages/extension/public/_locales/hi/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "पहले से साझा किया गया" }, + "Option_shareButton_modified_tooltip": { + "message": "स्थानीय बदलाव Hub से सिंक नहीं हुए हैं। अपडेट करने के लिए क्लिक करें" + }, "Option_remove_title": { "message": "क्या आप वाकई इसे हटाना चाहते हैं?" }, diff --git a/packages/extension/public/_locales/id/messages.json b/packages/extension/public/_locales/id/messages.json index 550e05ef5..a88675f53 100644 --- a/packages/extension/public/_locales/id/messages.json +++ b/packages/extension/public/_locales/id/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Sudah dibagikan" }, + "Option_shareButton_modified_tooltip": { + "message": "Perubahan lokal belum disinkronkan ke Hub. Klik untuk memperbarui" + }, "Option_remove_title": { "message": "Hapus ini?" }, diff --git a/packages/extension/public/_locales/it/messages.json b/packages/extension/public/_locales/it/messages.json index 9cdc5020e..817976f22 100644 --- a/packages/extension/public/_locales/it/messages.json +++ b/packages/extension/public/_locales/it/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Già condiviso" }, + "Option_shareButton_modified_tooltip": { + "message": "Modifiche locali non sincronizzate con l'Hub. Fai clic per aggiornare" + }, "Option_remove_title": { "message": "Sei sicuro di voler eliminare questo?" }, diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index 4c6161727..fb495a52d 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "共有済み" }, + "Option_shareButton_modified_tooltip": { + "message": "Hubに未反映の変更があります。クリックして反映" + }, "Option_remove_title": { "message": "削除しますか?" }, diff --git a/packages/extension/public/_locales/ko/messages.json b/packages/extension/public/_locales/ko/messages.json index 6d044dec7..061122d83 100644 --- a/packages/extension/public/_locales/ko/messages.json +++ b/packages/extension/public/_locales/ko/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "이미 공유됨" }, + "Option_shareButton_modified_tooltip": { + "message": "Hub에 반영되지 않은 변경 사항이 있습니다. 클릭하여 반영" + }, "Option_remove_title": { "message": "이것을 삭제하시겠습니까?" }, diff --git a/packages/extension/public/_locales/ms/messages.json b/packages/extension/public/_locales/ms/messages.json index e97dea4e0..a3fb8c9cb 100644 --- a/packages/extension/public/_locales/ms/messages.json +++ b/packages/extension/public/_locales/ms/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Sudah dikongsi" }, + "Option_shareButton_modified_tooltip": { + "message": "Perubahan setempat belum disegerakkan ke Hub. Klik untuk mengemas kini" + }, "Option_remove_title": { "message": "Padam ini?" }, diff --git a/packages/extension/public/_locales/pt_BR/messages.json b/packages/extension/public/_locales/pt_BR/messages.json index 835c43434..ebcb748f2 100644 --- a/packages/extension/public/_locales/pt_BR/messages.json +++ b/packages/extension/public/_locales/pt_BR/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Já compartilhado" }, + "Option_shareButton_modified_tooltip": { + "message": "Alterações locais não sincronizadas com o Hub. Clique para atualizar" + }, "Option_remove_title": { "message": "Excluir isto?" }, diff --git a/packages/extension/public/_locales/pt_PT/messages.json b/packages/extension/public/_locales/pt_PT/messages.json index d40ef417b..54e8f5e1d 100644 --- a/packages/extension/public/_locales/pt_PT/messages.json +++ b/packages/extension/public/_locales/pt_PT/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Já partilhado" }, + "Option_shareButton_modified_tooltip": { + "message": "Alterações locais não sincronizadas com o Hub. Clique para atualizar" + }, "Option_remove_title": { "message": "Eliminar isto?" }, diff --git a/packages/extension/public/_locales/ru/messages.json b/packages/extension/public/_locales/ru/messages.json index 87769d655..ad4025376 100644 --- a/packages/extension/public/_locales/ru/messages.json +++ b/packages/extension/public/_locales/ru/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "Уже опубликовано" }, + "Option_shareButton_modified_tooltip": { + "message": "Локальные изменения не синхронизированы с Hub. Нажмите, чтобы обновить" + }, "Option_remove_title": { "message": "Вы уверены, что хотите удалить это?" }, diff --git a/packages/extension/public/_locales/zh_CN/messages.json b/packages/extension/public/_locales/zh_CN/messages.json index 7e637a291..ddd710de8 100644 --- a/packages/extension/public/_locales/zh_CN/messages.json +++ b/packages/extension/public/_locales/zh_CN/messages.json @@ -827,6 +827,9 @@ "Option_shareButton_shared_tooltip": { "message": "已共享" }, + "Option_shareButton_modified_tooltip": { + "message": "本地更改尚未同步到 Hub。点击以更新" + }, "Option_remove_title": { "message": "您确定要删除吗?" }, diff --git a/packages/extension/src/components/option/ShareButton.tsx b/packages/extension/src/components/option/ShareButton.tsx index 4a5ff5f21..ab7a715e7 100644 --- a/packages/extension/src/components/option/ShareButton.tsx +++ b/packages/extension/src/components/option/ShareButton.tsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react" -import { Share, CloudCheck } from "lucide-react" +import { Share, CloudCheck, CloudUpload } from "lucide-react" import { Tooltip } from "@/components/Tooltip" import { cn, isUUIDv7, generateId } from "@/lib/utils" import { t } from "@/services/i18n" @@ -8,6 +8,7 @@ import { getHubLocale, isHubShareable, isHubRegistered, + pushEditToHub, } from "@/services/hubShare" import { sendEvent, @@ -23,12 +24,15 @@ type Props = { command: SelectionCommand onCommandIdChange?: (newId: string) => void isShared?: boolean + // Shared command with local edits not yet pushed to the Hub + isModified?: boolean } export const ShareButton = ({ command, onCommandIdChange, isShared, + isModified, }: Props) => { const buttonRef = useRef(null) const [status, setStatus] = useState<"idle" | "pending" | "sent" | "error">( @@ -38,6 +42,15 @@ export const ShareButton = ({ const handleClick = async (e: React.MouseEvent) => { e.stopPropagation() + if (isShared && isModified) { + // Push the local edits to the Hub + setStatus("pending") + const ok = pushEditToHub(command) + setStatus(ok ? "sent" : "error") + setTimeout(() => setStatus("idle"), 2000) + return + } + if (isShared) { // Open the hub dashboard page for the shared command const locale = getHubLocale() @@ -106,7 +119,11 @@ export const ShareButton = ({ onClick={handleClick} ref={buttonRef} data-testid={ - isShared ? TEST_IDS.sharedCommandButton : TEST_IDS.shareCommandButton + !isShared + ? TEST_IDS.shareCommandButton + : isModified + ? TEST_IDS.modifiedSharedCommandButton + : TEST_IDS.sharedCommandButton } > {!isShared ? ( @@ -117,6 +134,17 @@ export const ShareButton = ({ )} size={16} /> + ) : isModified ? ( + + + + ) : ( diff --git a/packages/extension/src/components/option/__tests__/ShareButton.test.tsx b/packages/extension/src/components/option/__tests__/ShareButton.test.tsx index a37a09287..f31dbe7eb 100644 --- a/packages/extension/src/components/option/__tests__/ShareButton.test.tsx +++ b/packages/extension/src/components/option/__tests__/ShareButton.test.tsx @@ -2,7 +2,11 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { render, screen, fireEvent, waitFor } from "@testing-library/react" import { ShareButton } from "../ShareButton" import { sendEvent, sendHubLinkClick } from "@/services/analytics" -import { shareCommandToHub, isHubRegistered } from "@/services/hubShare" +import { + shareCommandToHub, + isHubRegistered, + pushEditToHub, +} from "@/services/hubShare" import { OPEN_MODE } from "@/const" import { TEST_IDS } from "@/testIds" import type { SearchCommand } from "@/types" @@ -20,6 +24,7 @@ vi.mock("@/services/hubShare", () => ({ getHubLocale: () => "en", isHubShareable: () => true, isHubRegistered: vi.fn(), + pushEditToHub: vi.fn(), })) vi.mock("@/services/analytics", () => ({ @@ -39,6 +44,7 @@ const mockSendEvent = vi.mocked(sendEvent) const mockSendHubLinkClick = vi.mocked(sendHubLinkClick) const mockShareCommandToHub = vi.mocked(shareCommandToHub) const mockIsHubRegistered = vi.mocked(isHubRegistered) +const mockPushEditToHub = vi.mocked(pushEditToHub) // A UUIDv7 id, so that sharing does not regenerate the command id. const command: SearchCommand = { @@ -111,4 +117,24 @@ describe("ShareButton", () => { expect(mockSendHubLinkClick).not.toHaveBeenCalled() expect(mockSendEvent).not.toHaveBeenCalled() }) + + it("SB-05: pushes local edits to the Hub for a modified shared command", () => { + mockPushEditToHub.mockReturnValue(true) + render() + + fireEvent.click(screen.getByTestId(TEST_IDS.modifiedSharedCommandButton)) + + expect(mockPushEditToHub).toHaveBeenCalledWith(command) + expect(chrome.tabs.create).not.toHaveBeenCalled() + expect(mockShareCommandToHub).not.toHaveBeenCalled() + }) + + it("SB-06: shows the shared state when isModified is set on an unshared command", () => { + render() + + expect(screen.getByTestId(TEST_IDS.shareCommandButton)).toBeTruthy() + expect( + screen.queryByTestId(TEST_IDS.modifiedSharedCommandButton), + ).toBeNull() + }) }) diff --git a/packages/extension/src/components/option/editor/CommandTreeRenderer.tsx b/packages/extension/src/components/option/editor/CommandTreeRenderer.tsx index 6f4ce7327..0fa81c8e3 100644 --- a/packages/extension/src/components/option/editor/CommandTreeRenderer.tsx +++ b/packages/extension/src/components/option/editor/CommandTreeRenderer.tsx @@ -13,6 +13,7 @@ import { isPageActionCommand, } from "@/services/option/commandUtils" import { calcLevel } from "@/services/option/commandTree" +import { hasUnpushedEdits } from "@/services/hub/commandContent" interface Props { nodes: FlattenNode[] @@ -80,6 +81,10 @@ export const CommandTreeRenderer: React.FC = ({ onUpdateCommandId?.(field.content.id, newId) } isShared={sharedCommandIds?.has(field.content.id)} + isModified={ + sharedCommandIds?.has(field.content.id) && + hasUnpushedEdits(field.content) + } /> )} {isPageActionCommand(field.content) && ( diff --git a/packages/extension/src/services/hub/__tests__/commandContent.test.ts b/packages/extension/src/services/hub/__tests__/commandContent.test.ts index 8e00a2882..dd4db160a 100644 --- a/packages/extension/src/services/hub/__tests__/commandContent.test.ts +++ b/packages/extension/src/services/hub/__tests__/commandContent.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest" import { applyHubContent, calcContentHash, + hasUnpushedEdits, isLocallyModified, nextContentUpdatedAt, parseContentUpdatedAt, @@ -90,6 +91,22 @@ describe("isLocallyModified", () => { }) }) +describe("hasUnpushedEdits", () => { + it("CC-07a: false when the fingerprint matches", () => { + const cmd = { ...base, contentHash: calcContentHash(base) } + expect(hasUnpushedEdits(cmd)).toBe(false) + }) + + it("CC-07b: true when the content differs from the fingerprint", () => { + const cmd = { ...base, contentHash: calcContentHash(base), title: "Edit" } + expect(hasUnpushedEdits(cmd)).toBe(true) + }) + + it("CC-07c: false when there is no fingerprint", () => { + expect(hasUnpushedEdits(base)).toBe(false) + }) +}) + describe("parseContentUpdatedAt", () => { it("CC-08: normalizes valid dates to ISO 8601", () => { expect(parseContentUpdatedAt("2026-01-02T03:04:05Z")).toBe( diff --git a/packages/extension/src/services/hub/commandContent.ts b/packages/extension/src/services/hub/commandContent.ts index 981b0bd6c..1801da5eb 100644 --- a/packages/extension/src/services/hub/commandContent.ts +++ b/packages/extension/src/services/hub/commandContent.ts @@ -96,6 +96,17 @@ export function isLocallyModified(cmd: SelectionCommand): boolean { return cmd.contentHash !== calcContentHash(cmd) } +/** + * Whether a shared command has local edits not yet pushed to the Hub. + * Unlike isLocallyModified, a command without a fingerprint (shared before + * fingerprints existed) counts as unchanged, to avoid flagging every such + * command. The fingerprint is saved on the next push, enabling detection. + */ +export function hasUnpushedEdits(cmd: SelectionCommand): boolean { + if (!cmd.contentHash) return false + return cmd.contentHash !== calcContentHash(cmd) +} + /** Returns the value as ISO 8601 if it is a valid date string. */ export function parseContentUpdatedAt(value: unknown): string | undefined { if (typeof value !== "string") return undefined diff --git a/packages/extension/src/testIds.ts b/packages/extension/src/testIds.ts index 75c427fca..082891c96 100644 --- a/packages/extension/src/testIds.ts +++ b/packages/extension/src/testIds.ts @@ -17,6 +17,7 @@ export const TEST_IDS = { submitCommandButton: "submit-command-button", shareCommandButton: "share-command-button", sharedCommandButton: "shared-command-button", + modifiedSharedCommandButton: "modified-shared-command-button", commandType: (type: string) => `command-type-${type}`, recButton: "rec-button", pageActionCompleteButton: "page-action-complete-button", From c1b16de6ae3f7d8a1572e38378bb1eb781f8eda5 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 22 Oct 2026 12:30:21 +0900 Subject: [PATCH 2/7] feat(extension): use CloudUpload icon for sharing and highlight unpushed edits Use the CloudUpload icon for the new share action too, and give the button a light red background and a red badge when a shared command has unpushed local edits. Co-Authored-By: Claude Opus 5.5 --- packages/extension/src/components/option/ShareButton.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/components/option/ShareButton.tsx b/packages/extension/src/components/option/ShareButton.tsx index ab7a715e7..5e2712db7 100644 --- a/packages/extension/src/components/option/ShareButton.tsx +++ b/packages/extension/src/components/option/ShareButton.tsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react" -import { Share, CloudCheck, CloudUpload } from "lucide-react" +import { CloudCheck, CloudUpload } from "lucide-react" import { Tooltip } from "@/components/Tooltip" import { cn, isUUIDv7, generateId } from "@/lib/utils" import { t } from "@/services/i18n" @@ -115,6 +115,7 @@ export const ShareButton = ({ className={cn( "outline-gray-200 p-2 rounded-md transition hover:bg-green-100 hover:scale-125 group/share-btn", "disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none", + isShared && isModified && "bg-red-50 hover:bg-red-100", )} onClick={handleClick} ref={buttonRef} @@ -127,7 +128,7 @@ export const ShareButton = ({ } > {!isShared ? ( - - + ) : ( Date: Thu, 22 Oct 2026 12:33:06 +0900 Subject: [PATCH 3/7] style(extension): use orange for the unpushed-edits share button Use an orange badge, background and hover stroke instead of red, so the state is not confused with an error. Co-Authored-By: Claude Opus 5.5 --- packages/extension/src/components/option/ShareButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/components/option/ShareButton.tsx b/packages/extension/src/components/option/ShareButton.tsx index 5e2712db7..4f35d12d7 100644 --- a/packages/extension/src/components/option/ShareButton.tsx +++ b/packages/extension/src/components/option/ShareButton.tsx @@ -115,7 +115,7 @@ export const ShareButton = ({ className={cn( "outline-gray-200 p-2 rounded-md transition hover:bg-green-100 hover:scale-125 group/share-btn", "disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none", - isShared && isModified && "bg-red-50 hover:bg-red-100", + isShared && isModified && "bg-orange-50 hover:bg-orange-100", )} onClick={handleClick} ref={buttonRef} @@ -139,12 +139,12 @@ export const ShareButton = ({ - + ) : ( Date: Thu, 22 Oct 2026 12:34:03 +0900 Subject: [PATCH 4/7] style(extension): use CloudUpload icon for share buttons in edit dialog Co-Authored-By: Claude Opus 5.5 --- .../src/components/option/editor/CommandEditDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/components/option/editor/CommandEditDialog.tsx b/packages/extension/src/components/option/editor/CommandEditDialog.tsx index 651281de2..a4e3545e7 100644 --- a/packages/extension/src/components/option/editor/CommandEditDialog.tsx +++ b/packages/extension/src/components/option/editor/CommandEditDialog.tsx @@ -8,7 +8,7 @@ import { SquareTerminal, ChevronsUpDown, ChevronsDownUp, - Share, + CloudUpload, } from "lucide-react" import { @@ -908,7 +908,7 @@ const CommandEditDialogInner = ({ (err) => console.error(err), )} > - + {t("labelShare")} )} @@ -927,7 +927,7 @@ const CommandEditDialogInner = ({ )} data-testid={TEST_IDS.submitCommandButton} > - {isHubEdit ? : } + {isHubEdit ? : } {isHubEdit ? t("labelShare") : isEdit From e1b902d87188e9708d8d23898dbbb5605f6aa7cd Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 22 Oct 2026 12:40:12 +0900 Subject: [PATCH 5/7] i18n(extension): simplify unsynced changes tooltip Co-Authored-By: Claude Opus 5.5 --- packages/extension/public/_locales/de/messages.json | 2 +- packages/extension/public/_locales/en/messages.json | 2 +- packages/extension/public/_locales/es/messages.json | 2 +- packages/extension/public/_locales/fr/messages.json | 2 +- packages/extension/public/_locales/hi/messages.json | 2 +- packages/extension/public/_locales/id/messages.json | 2 +- packages/extension/public/_locales/it/messages.json | 2 +- packages/extension/public/_locales/ja/messages.json | 2 +- packages/extension/public/_locales/ko/messages.json | 2 +- packages/extension/public/_locales/ms/messages.json | 2 +- packages/extension/public/_locales/pt_BR/messages.json | 2 +- packages/extension/public/_locales/pt_PT/messages.json | 2 +- packages/extension/public/_locales/ru/messages.json | 2 +- packages/extension/public/_locales/zh_CN/messages.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/extension/public/_locales/de/messages.json b/packages/extension/public/_locales/de/messages.json index da7191941..8df859b69 100644 --- a/packages/extension/public/_locales/de/messages.json +++ b/packages/extension/public/_locales/de/messages.json @@ -828,7 +828,7 @@ "message": "Bereits geteilt" }, "Option_shareButton_modified_tooltip": { - "message": "Lokale Änderungen nicht mit dem Hub synchronisiert. Zum Aktualisieren klicken" + "message": "Nicht synchronisierte Änderungen. Zum Synchronisieren klicken" }, "Option_remove_title": { "message": "Dies löschen?" diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 584127163..70d03db77 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -834,7 +834,7 @@ "description": "Tooltip for the share button when the command has already been shared to the Hub. Clicking opens the hub dashboard for this command." }, "Option_shareButton_modified_tooltip": { - "message": "Local changes not synced to Hub. Click to update", + "message": "Unsynced changes. Click to sync", "description": "Tooltip for the share button when the command has already been shared to the Hub but has local edits not yet pushed. Clicking pushes the edits to the Hub." }, "Option_remove_title": { diff --git a/packages/extension/public/_locales/es/messages.json b/packages/extension/public/_locales/es/messages.json index d9234f9ea..c225f26de 100644 --- a/packages/extension/public/_locales/es/messages.json +++ b/packages/extension/public/_locales/es/messages.json @@ -828,7 +828,7 @@ "message": "Ya compartido" }, "Option_shareButton_modified_tooltip": { - "message": "Hay cambios locales sin sincronizar con el Hub. Haz clic para actualizar" + "message": "Cambios sin sincronizar. Haz clic para sincronizar" }, "Option_remove_title": { "message": "¿Eliminar esto?" diff --git a/packages/extension/public/_locales/fr/messages.json b/packages/extension/public/_locales/fr/messages.json index d6e484ae6..92c02331e 100644 --- a/packages/extension/public/_locales/fr/messages.json +++ b/packages/extension/public/_locales/fr/messages.json @@ -828,7 +828,7 @@ "message": "Déjà partagé" }, "Option_shareButton_modified_tooltip": { - "message": "Modifications locales non synchronisées avec le Hub. Cliquez pour mettre à jour" + "message": "Modifications non synchronisées. Cliquez pour synchroniser" }, "Option_remove_title": { "message": "Supprimer ceci ?" diff --git a/packages/extension/public/_locales/hi/messages.json b/packages/extension/public/_locales/hi/messages.json index 94166e3b8..75aff289e 100644 --- a/packages/extension/public/_locales/hi/messages.json +++ b/packages/extension/public/_locales/hi/messages.json @@ -828,7 +828,7 @@ "message": "पहले से साझा किया गया" }, "Option_shareButton_modified_tooltip": { - "message": "स्थानीय बदलाव Hub से सिंक नहीं हुए हैं। अपडेट करने के लिए क्लिक करें" + "message": "बदलाव सिंक नहीं हुए। सिंक करने के लिए क्लिक करें" }, "Option_remove_title": { "message": "क्या आप वाकई इसे हटाना चाहते हैं?" diff --git a/packages/extension/public/_locales/id/messages.json b/packages/extension/public/_locales/id/messages.json index a88675f53..c9e4d9c2f 100644 --- a/packages/extension/public/_locales/id/messages.json +++ b/packages/extension/public/_locales/id/messages.json @@ -828,7 +828,7 @@ "message": "Sudah dibagikan" }, "Option_shareButton_modified_tooltip": { - "message": "Perubahan lokal belum disinkronkan ke Hub. Klik untuk memperbarui" + "message": "Ada perubahan yang belum disinkronkan. Klik untuk menyinkronkan" }, "Option_remove_title": { "message": "Hapus ini?" diff --git a/packages/extension/public/_locales/it/messages.json b/packages/extension/public/_locales/it/messages.json index 817976f22..33f0df9ee 100644 --- a/packages/extension/public/_locales/it/messages.json +++ b/packages/extension/public/_locales/it/messages.json @@ -828,7 +828,7 @@ "message": "Già condiviso" }, "Option_shareButton_modified_tooltip": { - "message": "Modifiche locali non sincronizzate con l'Hub. Fai clic per aggiornare" + "message": "Modifiche non sincronizzate. Fai clic per sincronizzare" }, "Option_remove_title": { "message": "Sei sicuro di voler eliminare questo?" diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index fb495a52d..eea758b52 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -828,7 +828,7 @@ "message": "共有済み" }, "Option_shareButton_modified_tooltip": { - "message": "Hubに未反映の変更があります。クリックして反映" + "message": "未反映の変更有り。クリックして反映" }, "Option_remove_title": { "message": "削除しますか?" diff --git a/packages/extension/public/_locales/ko/messages.json b/packages/extension/public/_locales/ko/messages.json index 061122d83..78659e8ba 100644 --- a/packages/extension/public/_locales/ko/messages.json +++ b/packages/extension/public/_locales/ko/messages.json @@ -828,7 +828,7 @@ "message": "이미 공유됨" }, "Option_shareButton_modified_tooltip": { - "message": "Hub에 반영되지 않은 변경 사항이 있습니다. 클릭하여 반영" + "message": "반영되지 않은 변경 사항 있음. 클릭하여 반영" }, "Option_remove_title": { "message": "이것을 삭제하시겠습니까?" diff --git a/packages/extension/public/_locales/ms/messages.json b/packages/extension/public/_locales/ms/messages.json index a3fb8c9cb..0b206156f 100644 --- a/packages/extension/public/_locales/ms/messages.json +++ b/packages/extension/public/_locales/ms/messages.json @@ -828,7 +828,7 @@ "message": "Sudah dikongsi" }, "Option_shareButton_modified_tooltip": { - "message": "Perubahan setempat belum disegerakkan ke Hub. Klik untuk mengemas kini" + "message": "Ada perubahan belum disegerakkan. Klik untuk segerakkan" }, "Option_remove_title": { "message": "Padam ini?" diff --git a/packages/extension/public/_locales/pt_BR/messages.json b/packages/extension/public/_locales/pt_BR/messages.json index ebcb748f2..df63a6be8 100644 --- a/packages/extension/public/_locales/pt_BR/messages.json +++ b/packages/extension/public/_locales/pt_BR/messages.json @@ -828,7 +828,7 @@ "message": "Já compartilhado" }, "Option_shareButton_modified_tooltip": { - "message": "Alterações locais não sincronizadas com o Hub. Clique para atualizar" + "message": "Alterações não sincronizadas. Clique para sincronizar" }, "Option_remove_title": { "message": "Excluir isto?" diff --git a/packages/extension/public/_locales/pt_PT/messages.json b/packages/extension/public/_locales/pt_PT/messages.json index 54e8f5e1d..ea1bb181b 100644 --- a/packages/extension/public/_locales/pt_PT/messages.json +++ b/packages/extension/public/_locales/pt_PT/messages.json @@ -828,7 +828,7 @@ "message": "Já partilhado" }, "Option_shareButton_modified_tooltip": { - "message": "Alterações locais não sincronizadas com o Hub. Clique para atualizar" + "message": "Alterações não sincronizadas. Clique para sincronizar" }, "Option_remove_title": { "message": "Eliminar isto?" diff --git a/packages/extension/public/_locales/ru/messages.json b/packages/extension/public/_locales/ru/messages.json index ad4025376..7443a8950 100644 --- a/packages/extension/public/_locales/ru/messages.json +++ b/packages/extension/public/_locales/ru/messages.json @@ -828,7 +828,7 @@ "message": "Уже опубликовано" }, "Option_shareButton_modified_tooltip": { - "message": "Локальные изменения не синхронизированы с Hub. Нажмите, чтобы обновить" + "message": "Есть несинхронизированные изменения. Нажмите, чтобы синхронизировать" }, "Option_remove_title": { "message": "Вы уверены, что хотите удалить это?" diff --git a/packages/extension/public/_locales/zh_CN/messages.json b/packages/extension/public/_locales/zh_CN/messages.json index ddd710de8..22e1c6100 100644 --- a/packages/extension/public/_locales/zh_CN/messages.json +++ b/packages/extension/public/_locales/zh_CN/messages.json @@ -828,7 +828,7 @@ "message": "已共享" }, "Option_shareButton_modified_tooltip": { - "message": "本地更改尚未同步到 Hub。点击以更新" + "message": "有未同步的更改。点击同步" }, "Option_remove_title": { "message": "您确定要删除吗?" From b6cd49c297a95a05ddd33352f6c8f88d941c3a6e Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 22 Oct 2026 12:41:57 +0900 Subject: [PATCH 6/7] i18n(extension): use kana in Japanese unsynced changes tooltip Co-Authored-By: Claude Opus 5.5 --- packages/extension/public/_locales/ja/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index eea758b52..c78308b7c 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -828,7 +828,7 @@ "message": "共有済み" }, "Option_shareButton_modified_tooltip": { - "message": "未反映の変更有り。クリックして反映" + "message": "未反映の変更あり。クリックして反映" }, "Option_remove_title": { "message": "削除しますか?" From 82f92407fba9f4d14f81e556bf1e01a4463f067f Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 22 Oct 2026 12:45:33 +0900 Subject: [PATCH 7/7] refactor(extension): address review comments on share button - Derive the share state once and reuse it for icon, test id and tooltip - Add aria-label to the share button and hide the badge from screen readers - Drop the unused pending status in the push flow - Add tests for push failure, repeated clicks and fingerprint stability through the form schema; fix the SB-06 test name Co-Authored-By: Claude Opus 5.5 --- .../src/components/option/ShareButton.tsx | 94 ++++++++++--------- .../option/__tests__/ShareButton.test.tsx | 27 +++++- .../hub/__tests__/commandContent.test.ts | 53 +++++++++++ 3 files changed, 127 insertions(+), 47 deletions(-) diff --git a/packages/extension/src/components/option/ShareButton.tsx b/packages/extension/src/components/option/ShareButton.tsx index 4f35d12d7..65f9cecdc 100644 --- a/packages/extension/src/components/option/ShareButton.tsx +++ b/packages/extension/src/components/option/ShareButton.tsx @@ -39,19 +39,25 @@ export const ShareButton = ({ "idle", ) + const shareState: "unshared" | "shared" | "modified" = !isShared + ? "unshared" + : isModified + ? "modified" + : "shared" + const handleClick = async (e: React.MouseEvent) => { e.stopPropagation() - if (isShared && isModified) { - // Push the local edits to the Hub - setStatus("pending") + if (shareState === "modified") { + // Push the local edits to the Hub. The mark clears once the Hub + // acknowledges and the new content hash is saved. const ok = pushEditToHub(command) setStatus(ok ? "sent" : "error") setTimeout(() => setStatus("idle"), 2000) return } - if (isShared) { + if (shareState === "shared") { // Open the hub dashboard page for the shared command const locale = getHubLocale() const url = `${NEW_HUB_URL}/${locale}/dashboard/mycommands?id=${encodeURIComponent(command.id)}` @@ -107,6 +113,29 @@ export const ShareButton = ({ return null } + const iconClassName = cn( + "stroke-gray-500", + shareState === "modified" + ? "group-hover/share-btn:stroke-orange-600" + : "group-hover/share-btn:stroke-green-600", + status === "error" && "stroke-red-500", + ) + + const { testId, tooltip } = { + unshared: { + testId: TEST_IDS.shareCommandButton, + tooltip: t("Option_shareButton_tooltip"), + }, + shared: { + testId: TEST_IDS.sharedCommandButton, + tooltip: t("Option_shareButton_shared_tooltip"), + }, + modified: { + testId: TEST_IDS.modifiedSharedCommandButton, + tooltip: t("Option_shareButton_modified_tooltip"), + }, + }[shareState] + return ( <> - + ) } diff --git a/packages/extension/src/components/option/__tests__/ShareButton.test.tsx b/packages/extension/src/components/option/__tests__/ShareButton.test.tsx index f31dbe7eb..be6ae07ec 100644 --- a/packages/extension/src/components/option/__tests__/ShareButton.test.tsx +++ b/packages/extension/src/components/option/__tests__/ShareButton.test.tsx @@ -129,7 +129,7 @@ describe("ShareButton", () => { expect(mockShareCommandToHub).not.toHaveBeenCalled() }) - it("SB-06: shows the shared state when isModified is set on an unshared command", () => { + it("SB-06: keeps the unshared state when isModified is set without isShared", () => { render() expect(screen.getByTestId(TEST_IDS.shareCommandButton)).toBeTruthy() @@ -137,4 +137,29 @@ describe("ShareButton", () => { screen.queryByTestId(TEST_IDS.modifiedSharedCommandButton), ).toBeNull() }) + + it("SB-07: shows the error state when pushing the edits fails", () => { + mockPushEditToHub.mockReturnValue(false) + render() + + const button = screen.getByTestId(TEST_IDS.modifiedSharedCommandButton) + fireEvent.click(button) + + expect(mockPushEditToHub).toHaveBeenCalledWith(command) + expect((button as HTMLButtonElement).disabled).toBe(true) + expect(button.querySelector("svg")?.getAttribute("class")).toContain( + "stroke-red-500", + ) + }) + + it("SB-08: blocks repeated pushes while the result is shown", () => { + mockPushEditToHub.mockReturnValue(true) + render() + + const button = screen.getByTestId(TEST_IDS.modifiedSharedCommandButton) + fireEvent.click(button) + fireEvent.click(button) + + expect(mockPushEditToHub).toHaveBeenCalledOnce() + }) }) diff --git a/packages/extension/src/services/hub/__tests__/commandContent.test.ts b/packages/extension/src/services/hub/__tests__/commandContent.test.ts index dd4db160a..2fae8d783 100644 --- a/packages/extension/src/services/hub/__tests__/commandContent.test.ts +++ b/packages/extension/src/services/hub/__tests__/commandContent.test.ts @@ -9,6 +9,7 @@ import { pickHubContent, stampForHub, } from "../commandContent" +import { commandSchema } from "@/types/schema" import type { SelectionCommand } from "@/types" const base = { @@ -105,6 +106,58 @@ describe("hasUnpushedEdits", () => { it("CC-07c: false when there is no fingerprint", () => { expect(hasUnpushedEdits(base)).toBe(false) }) + + // Saving from the edit dialog passes the command through the zod schema. + // A command already saved once must keep its fingerprint through it, + // otherwise saving without changes would mark it as edited. + it("CC-07d: false after saving a saved command again through the form schema", () => { + const commands = [ + { + id: "s", + title: "Search", + iconUrl: "https://a.com/i.png", + openMode: "popup", + openModeSecondary: "tab", + spaceEncoding: "plus", + searchUrl: "https://a.com/?q=%s", + }, + { + id: "a", + title: "AI", + iconUrl: "https://a.com/i.png", + openMode: "aiPrompt", + aiPromptOption: { + serviceId: "gemini", + prompt: "{{SelectedText}}", + openMode: "popup", + }, + }, + { + id: "p", + title: "Page Action", + iconUrl: "https://a.com/i.png", + openMode: "pageAction", + pageActionOption: { + startUrl: "https://a.com", + openMode: "popup", + steps: [ + { + id: "1", + // Set by the recorder but not part of the schema + timestamp: 1, + param: { type: "start", label: "Start" }, + }, + ], + }, + }, + ] + for (const raw of commands) { + const saved = commandSchema.parse(raw) as SelectionCommand + const stamped = { ...saved, contentHash: calcContentHash(saved) } + const resaved = commandSchema.parse(stamped) as SelectionCommand + expect(hasUnpushedEdits(resaved)).toBe(false) + } + }) }) describe("parseContentUpdatedAt", () => {