From 2fda0501a1aae2722ba0eff147b843d5c6fb1a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E4=BF=8A=E6=B1=9F?= Date: Fri, 7 Aug 2026 01:23:29 +0800 Subject: [PATCH 1/2] feat(gui): show public combo model name with one-click copy Clarify Combo ID vs client-facing model name, preview the effective public id under the alias field, and route header/preview copy through shared useCopyFeedback (including clipboard unavailable). --- .../components/combo-workspace-add-modal.tsx | 10 ++--- .../components/combo-workspace-controls.tsx | 38 +++++++++++++++++++ .../combo-workspace-detail-panel.tsx | 38 +++++++++++-------- gui/src/i18n/de.ts | 10 +++-- gui/src/i18n/en.ts | 10 +++-- gui/src/i18n/ja.ts | 10 +++-- gui/src/i18n/ko.ts | 10 +++-- gui/src/i18n/ru.ts | 10 +++-- gui/src/i18n/zh.ts | 10 +++-- gui/src/styles-combos-workspace.css | 28 ++++++++++++++ 10 files changed, 134 insertions(+), 40 deletions(-) diff --git a/gui/src/components/combo-workspace-add-modal.tsx b/gui/src/components/combo-workspace-add-modal.tsx index cff828c5c..e0049a8ad 100644 --- a/gui/src/components/combo-workspace-add-modal.tsx +++ b/gui/src/components/combo-workspace-add-modal.tsx @@ -10,7 +10,7 @@ import { IconX } from "../icons"; import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; -import { EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { EffortSelect, PublicModelPreview, StrategySeg, TargetEditor } from "./combo-workspace-controls"; import { clampedNumberInput } from "./combo-workspace-utils"; export function AddComboModal({ @@ -139,11 +139,9 @@ export function AddComboModal({

{t("cws.field.aliasHint")}

-

- {t("cws.field.idHint", { - model: draft.id.trim() ? comboPublicModelId(draft.id, draft.alias) : "…", - })} -

+
{t("cws.strategy")} diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 8a7277fd4..1b60f1894 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -6,6 +6,7 @@ import { useT } from "../i18n/shared"; import { formatProviderDisplayName } from "../provider-icons"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; import { clampedNumberInput, enabledProviders, modelsForProvider } from "./combo-workspace-utils"; +import { useCopyFeedback } from "./use-copy-feedback"; export function StrategySeg({ value, @@ -262,3 +263,40 @@ export function TargetEditor({
); } + +/** Effective public model id clients will request — mono value + copy. */ +export function PublicModelPreview({ model }: { model: string }) { + const t = useT(); + const { outcomeFor, copy } = useCopyFeedback(); + const canCopy = model.trim().length > 0 && model !== "…"; + const outcome = outcomeFor(model); + const copyLabel = outcome === "copied" + ? t("cws.copiedPublicModel") + : outcome === "unavailable" + ? t("cws.copyUnavailable") + : t("cws.copyPublicModel"); + // Split around a sentinel so the model token stays mono in any locale word order. + const sentinel = "\u0001"; + const [before, after = ""] = t("cws.field.publicModelPreview", { model: sentinel }).split(sentinel); + + return ( +
+

+ {before} + {model} + {after} +

+ +
+ ); +} diff --git a/gui/src/components/combo-workspace-detail-panel.tsx b/gui/src/components/combo-workspace-detail-panel.tsx index 1c6f9e059..4110a8f49 100644 --- a/gui/src/components/combo-workspace-detail-panel.tsx +++ b/gui/src/components/combo-workspace-detail-panel.tsx @@ -12,8 +12,9 @@ import { IconChevron, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; -import { EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { EffortSelect, PublicModelPreview, StrategySeg, TargetEditor } from "./combo-workspace-controls"; import { clampedNumberInput } from "./combo-workspace-utils"; +import { useCopyFeedback } from "./use-copy-feedback"; type DetailTab = "config" | "about"; @@ -57,6 +58,7 @@ export function DetailPanel({ onDirtyChange: (dirty: boolean) => void; }) { const t = useT(); + const { outcomeFor, copy } = useCopyFeedback(); const [tab, setTab] = useState("config"); /* @@ -79,7 +81,6 @@ export function DetailPanel({ const [draft, setDraft] = useState(baseline); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); - const [copied, setCopied] = useState(false); const dirty = !draftEquals(draft, baseline); const baselineSyncKey = `${baseline.id}:${baseline.alias ?? ""}:${baseline.nativeAlias}:${baseline.displayName ?? ""}:${baseline.strategy}:${baseline.stickyLimit}:${baseline.defaultEffort}:${baseline.targets.map((t) => `${t.provider}/${t.model}:${t.weight ?? 1}`).join(",")}`; const effortMap = useMemo(() => { @@ -111,15 +112,6 @@ export function DetailPanel({ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: key captures baseline payload }, [baselineSyncKey]); - const copyModel = async () => { - try { - await navigator.clipboard.writeText(baseline.model); - setCopied(true); - window.setTimeout(() => setCopied(false), 1200); - } catch { - /* ignore */ - } - }; const save = async () => { const code = validateComboDraft(draft, { @@ -163,6 +155,14 @@ export function DetailPanel({ const headerModel = isCreate ? (draft.id.trim() ? comboPublicModelId(draft.id, draft.alias) : t("cws.addTitle")) : baseline.model; + // Public model id clients request — same string PublicModelPreview copies. + const copyModelId = baseline.model; + const copyOutcome = outcomeFor(copyModelId); + const copyLabel = copyOutcome === "copied" + ? t("cws.copied") + : copyOutcome === "unavailable" + ? t("cws.copyUnavailable") + : t("cws.copyModel"); return (
@@ -175,8 +175,13 @@ export function DetailPanel({ )}

{headerModel}

{!isCreate && ( - )}
@@ -251,9 +256,7 @@ export function DetailPanel({ }))} />

- {isCreate - ? t("cws.field.idInternalHint") - : t("cws.field.idHintEdit", { model: comboPublicModelId(draft.id, draft.alias) })} + {isCreate ? t("cws.field.idInternalHint") : t("cws.field.idHintEdit")}

@@ -269,6 +272,9 @@ export function DetailPanel({

{t("cws.field.aliasHint")}

+
diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 1b60f1894..c5e619e0e 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import type { ComboEffort, ComboStrategy, ComboTarget } from "../combo-workspace-data"; +import { canCopyPublicModelId } from "../combo-public-model"; import { COMBO_EFFORTS, newComboTarget } from "../combo-workspace-data"; import { IconArrowDown, IconArrowUp, IconGrip, IconPlus, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; @@ -268,7 +269,7 @@ export function TargetEditor({ export function PublicModelPreview({ model }: { model: string }) { const t = useT(); const { outcomeFor, copy } = useCopyFeedback(); - const canCopy = model.trim().length > 0 && model !== "…"; + const canCopy = canCopyPublicModelId(model); const outcome = outcomeFor(model); const copyLabel = outcome === "copied" ? t("cws.copiedPublicModel") diff --git a/gui/src/components/combo-workspace-detail-panel.tsx b/gui/src/components/combo-workspace-detail-panel.tsx index 4110a8f49..47070eeb1 100644 --- a/gui/src/components/combo-workspace-detail-panel.tsx +++ b/gui/src/components/combo-workspace-detail-panel.tsx @@ -8,6 +8,7 @@ import { updateComboAliasDraft, validateComboDraft, } from "../combo-workspace-data"; +import { PUBLIC_MODEL_PREVIEW_PLACEHOLDER } from "../combo-public-model"; import { IconChevron, IconTrash } from "../icons"; import { useT } from "../i18n/shared"; import { Notice } from "../ui"; @@ -273,7 +274,7 @@ export function DetailPanel({ {t("cws.field.aliasHint")}

diff --git a/tests/combo-public-model-preview.test.ts b/tests/combo-public-model-preview.test.ts new file mode 100644 index 000000000..6dd0b6892 --- /dev/null +++ b/tests/combo-public-model-preview.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { + PUBLIC_MODEL_PREVIEW_PLACEHOLDER, + canCopyPublicModelId, +} from "../gui/src/combo-public-model"; + +describe("public model preview copy guard", () => { + test("rejects empty and the draft placeholder", () => { + expect(canCopyPublicModelId("")).toBe(false); + expect(canCopyPublicModelId(" ")).toBe(false); + expect(canCopyPublicModelId(PUBLIC_MODEL_PREVIEW_PLACEHOLDER)).toBe(false); + }); + + test("allows real client-facing model ids", () => { + expect(canCopyPublicModelId("combo/main")).toBe(true); + expect(canCopyPublicModelId("team/balanced")).toBe(true); + }); +});