From 7b86b84c2f7415fec73c120845d514d4b2ce3e72 Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:37:07 +0800 Subject: [PATCH 01/14] =?UTF-8?q?fix(web):=20=E4=BF=AE=E5=A4=8D=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=A8=A1=E5=9E=8B=E8=AE=BE=E7=BD=AE=E7=9A=84=20namesp?= =?UTF-8?q?ace=20=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 +- .../client/ui-settings-models/README.zh.md | 2 +- .../src/client/ProviderEditor.tsx | 1 + .../tests/components.client.spec.tsx | 52 +++++++++++++++++-- .../tests/store.client.spec.ts | 14 ++++- 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 79acecbefe..9af1829753 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -30,8 +30,8 @@ en: 589397a35f82244e zh: 342fbceee2c173a2 /deepseek-ai-dsh-client-ui-settings-models/use-this-package/extension-slots: - en: 6979796f0332268f - zh: 00896a005cc4f4e2 + en: 1edb9186d1ab73c2 + zh: c4895e03947c1235 /deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation: en: a87afbec5e339b36 zh: 6348c81af7e650e1 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 549177fbbb..b34be2679f 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -61,7 +61,7 @@ After the versioned notice step completes, the DeepSeek step projects first-run The section declares two seats for plugins distributed outside this repository, typed in [`src/client/slot-contract.ts`](src/client/slot-contract.ts) and exported from `./client`. `settings.models.provider-card` (keyed) renders inside every card that shows a directory row — a saved row's card, its first-run setup posture, and the add-provider draft — dispatched with `entryKey = settingsNs` and owner props carrying the row's `ConfigurableProviderView`, its configured state, and its confirmed api-key credential state, so one registration under an adapter family's namespace receives every card of that family, hand-declared routes included; the hand-declared draft card has no directory row yet and dispatches nothing until saved. `settings.models.footer` (list) renders after the rows and the add controls. A registrant activates through `ctx.slots.inject` with a type-only import of this package's `/client` entry; without registrants both seats render nothing. -The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. +The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs, and saves that catalog under the account route's own `llm-deepseek-account` section, so an account edit never rewrites the `llm-deepseek` section the official route reads. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. ----- diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index e9bd2122c3..d4029f3a19 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -61,7 +61,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求 本分区为仓库外分发的插件声明两个席位,类型定义在 [`src/client/slot-contract.ts`](src/client/slot-contract.ts) 并从 `./client` 导出。`settings.models.provider-card`(keyed)渲染在每张展示目录行的卡片内部——已保存行的卡片、其首次运行 setup 形态、以及「添加提供商」草稿卡——以 `entryKey = settingsNs` 分发,owner props 携带该行的 `ConfigurableProviderView`、其 configured 状态与已确认的 api-key 凭据状态,因此以某适配器家族的 namespace 注册一次即可收到该家族的全部卡片,含手工声明的路由;手工声明的草稿卡尚无目录行,保存之前不分发。`settings.models.footer`(list)渲染在行列表与新增控件之后。注册方通过 `ctx.slots.inject` 激活,并以 type-only import 引入本包 `/client` 入口;没有注册方时两个席位均不渲染任何内容。 -Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 +Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框;目录保存到账号路由自己的 `llm-deepseek-account` 段,因此账号侧的编辑不会改写 official 路由读取的 `llm-deepseek` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 ----- diff --git a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx index a50fe2933f..08056cc965 100644 --- a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx @@ -138,6 +138,7 @@ export function pathOps( /** The editor layout the owning namespace selects. */ function layoutOf(ns: string): EditorLayout { if (ns === 'llm-deepseek') return 'deepseek' + if (ns === 'llm-deepseek-account') return 'deepseek' if (ns === 'llm-pi-ai') return 'pi-ai' return 'unknown' } diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 1a2398bd2a..671cc0b5ca 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -137,9 +137,29 @@ function wireNamespaces(): SettingsNamespaceView[] { secrets: [], revision: 4, }, + { + ns: 'llm-deepseek-account', + schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue, + value: { + baseURL: 'https://base', + defaultContextWindow: 1_000_000, + maxTokens: 256_000, + models: DEFAULT_DEEPSEEK_MODELS, + }, + base: { defaultContextWindow: 1_000_000, maxTokens: 256_000, models: DEFAULT_DEEPSEEK_MODELS }, + user: {}, + autoGenerate: true, applies: 'live', + secrets: [], + revision: 0, + }, ] } +/** The account provider's dedicated settings namespace. */ +function accountNamespace(): SettingsNamespaceView { + return wireNamespaces().find(view => view.ns === 'llm-deepseek-account')! +} + /** Credentials answers over the Remote carrier, which has no envelope. */ function remoteOk(value: T) { return { ok: true as const, value } @@ -1887,15 +1907,41 @@ it.each([en, zh])('edits the account model catalog without credential or endpoin const scripted = scriptedFace({}) const ops = operationsWith(scripted.face) const describe = vi.spyOn(ops, 'describeCredential') + const namespace = accountNamespace() + const mutate = vi.spyOn(scripted.face.settings, 'mutate').mockResolvedValue(remoteOk(accountNamespace())) + const onClose = vi.fn() render( copy[key]} readOnly={false} onClose={() => {}} />) + namespace={namespace} settingsPath={[]} schema={settingsSchema} + operations={ops} t={key => copy[key]} readOnly={false} onClose={onClose} />) expect(screen.queryByLabelText(copy.keyInput)).toBeNull() expect(screen.queryByLabelText(copy.baseUrl)).toBeNull() expect(describe).not.toHaveBeenCalled() expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy() + expect(screen.queryByText(content => content.includes(copy.advancedHint))).toBeNull() await expect(`${document.body.textContent}\n`) .toMatchFileSnapshot(`./expected/deepseek-account-${copy === en ? 'en' : 'zh'}.txt`) + const set = vi.spyOn(ops, 'storeCredential') + fireEvent.change(screen.getAllByLabelText(new RegExp(copy.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } }) + fireEvent.click(screen.getByText(copy.apply)) + await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) + expect(mutate.mock.calls[0]).toEqual([ + 'llm-deepseek-account', + [{ + op: 'set', + path: ['models'], + value: [ + { + id: 'deepseek-v4-mini', + name: 'DeepSeek-V4-Flash', + description: 'Preserved hidden detail', + contextWindow: 1_000_000, + }, + { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 }, + ], + }], + 0, + ]) + expect(set).not.toHaveBeenCalled() }) it('renders the localized account row and supports catalogs without capacity defaults', async () => { @@ -1907,7 +1953,7 @@ it('renders the localized account row and supports catalogs without capacity def }) }) expect(screen.getByText(en.deepSeekAccount)).toBeTruthy() view.unmount() - const namespace = wireNamespaces()[0]! + const namespace = accountNamespace() render( {}} />) diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index 79905b4441..007ff68099 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -62,6 +62,15 @@ const NAMESPACES = [ secrets: [], revision: 0, }, + { + ns: 'llm-deepseek-account', + schema: {}, + value: { baseURL: 'https://base' }, + base: { baseURL: 'https://base' }, + autoGenerate: true, applies: 'live' as const, + secrets: [], + revision: 0, + }, { ns: 'llm-pi-ai', schema: {}, @@ -346,7 +355,7 @@ describe('edge joins', () => { it.each([false, true])('uses account availability without asking for an API key: %s', async (accountAvailable) => { const { ctx, mirror, seenRefs } = api({ accountAvailable, providers: async () => ok({ providers: [{ - provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true, + provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true, }] }) }) const store = new ModelsSettingsStore(ctx, settingsSchema, mirror) await store.load() @@ -356,12 +365,13 @@ it.each([false, true])('uses account availability without asking for an API key: expect(rows[0]).toMatchObject({ accountAvailable: true, apiKeyEnv: undefined, credential: undefined }) expect(providerUsable(rows[0]!)).toBe(true) } + expect(store.store.getSnapshot().namespaces.get('llm-deepseek-account')?.ns).toBe('llm-deepseek-account') expect(seenRefs).toEqual([]) }) it('removes the account row after sign-out and restores it after sign-in', async () => { const overrides = { accountAvailable: true, providers: async () => ok({ providers: [{ - provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true, + provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true, }, ...DIRECTORY] }) } const { ctx, mirror } = api(overrides) const store = new ModelsSettingsStore(ctx, settingsSchema, mirror) From d183c8a8c0e1ed7e718a6c1e791478005511d856 Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:54:58 +0800 Subject: [PATCH 02/14] =?UTF-8?q?fix(web):=20=E6=8C=89=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=20provider=20=E8=AF=86=E5=88=AB=E6=A8=A1=E5=9E=8B=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 +- .../client/ui-settings-models/README.zh.md | 2 +- .../src/client/ProviderEditor.tsx | 4 +- .../tests/components.client.spec.tsx | 69 +++++++++++++++++-- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 9af1829753..3e3c096cab 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -30,8 +30,8 @@ en: 589397a35f82244e zh: 342fbceee2c173a2 /deepseek-ai-dsh-client-ui-settings-models/use-this-package/extension-slots: - en: 1edb9186d1ab73c2 - zh: c4895e03947c1235 + en: 65e9d8822b67b6c2 + zh: 7e83be9defda28ee /deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation: en: a87afbec5e339b36 zh: 6348c81af7e650e1 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index b34be2679f..ead2111e2b 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -61,7 +61,7 @@ After the versioned notice step completes, the DeepSeek step projects first-run The section declares two seats for plugins distributed outside this repository, typed in [`src/client/slot-contract.ts`](src/client/slot-contract.ts) and exported from `./client`. `settings.models.provider-card` (keyed) renders inside every card that shows a directory row — a saved row's card, its first-run setup posture, and the add-provider draft — dispatched with `entryKey = settingsNs` and owner props carrying the row's `ConfigurableProviderView`, its configured state, and its confirmed api-key credential state, so one registration under an adapter family's namespace receives every card of that family, hand-declared routes included; the hand-declared draft card has no directory row yet and dispatches nothing until saved. `settings.models.footer` (list) renders after the rows and the add controls. A registrant activates through `ctx.slots.inject` with a type-only import of this package's `/client` entry; without registrants both seats render nothing. -The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs, and saves that catalog under the account route's own `llm-deepseek-account` section, so an account edit never rewrites the `llm-deepseek` section the official route reads. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. +The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs, and saves that catalog under the account route's own settings section (`llm-deepseek-account` by default), so an account edit never rewrites the `llm-deepseek` section the official route reads. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available. ----- diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index d4029f3a19..8f8e0720fb 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -61,7 +61,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求 本分区为仓库外分发的插件声明两个席位,类型定义在 [`src/client/slot-contract.ts`](src/client/slot-contract.ts) 并从 `./client` 导出。`settings.models.provider-card`(keyed)渲染在每张展示目录行的卡片内部——已保存行的卡片、其首次运行 setup 形态、以及「添加提供商」草稿卡——以 `entryKey = settingsNs` 分发,owner props 携带该行的 `ConfigurableProviderView`、其 configured 状态与已确认的 api-key 凭据状态,因此以某适配器家族的 namespace 注册一次即可收到该家族的全部卡片,含手工声明的路由;手工声明的草稿卡尚无目录行,保存之前不分发。`settings.models.footer`(list)渲染在行列表与新增控件之后。注册方通过 `ctx.slots.inject` 激活,并以 type-only import 引入本包 `/client` 入口;没有注册方时两个席位均不渲染任何内容。 -Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框;目录保存到账号路由自己的 `llm-deepseek-account` 段,因此账号侧的编辑不会改写 official 路由读取的 `llm-deepseek` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 +Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框;目录保存到账号路由自己的设置段(默认为 `llm-deepseek-account`),因此账号侧的编辑不会改写 official 路由读取的 `llm-deepseek` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。 ----- diff --git a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx index 08056cc965..41dac376d1 100644 --- a/packages/client/ui-settings-models/src/client/ProviderEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ProviderEditor.tsx @@ -138,7 +138,6 @@ export function pathOps( /** The editor layout the owning namespace selects. */ function layoutOf(ns: string): EditorLayout { if (ns === 'llm-deepseek') return 'deepseek' - if (ns === 'llm-deepseek-account') return 'deepseek' if (ns === 'llm-pi-ai') return 'pi-ai' return 'unknown' } @@ -183,8 +182,9 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode { const node = useMemo(() => schema.nodeAtPath(root, settingsPath), [root, schema, settingsPath]) const fallback = schema.getPath(namespace.value, settingsPath) const disabled = props.readOnly || busy - const layout = layoutOf(namespace.ns) const accountProvider = props.provider === 'deepseek-account' + // Account settings use a configurable Cordis entry id. + const layout = accountProvider ? 'deepseek' : layoutOf(namespace.ns) const keyRef = refFor(schema, namespace, settingsPath, props.provider) // The same schema read the create card makes, so the choices offered here // and there cannot drift apart: both come from the adapter's own `Config`. diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index 671cc0b5ca..9396ff7f42 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -155,9 +155,9 @@ function wireNamespaces(): SettingsNamespaceView[] { ] } -/** The account provider's dedicated settings namespace. */ -function accountNamespace(): SettingsNamespaceView { - return wireNamespaces().find(view => view.ns === 'llm-deepseek-account')! +/** The account provider's settings namespace, under a composition's entry id. */ +function accountNamespace(ns = 'llm-deepseek-account'): SettingsNamespaceView { + return { ...wireNamespaces().find(view => view.ns === 'llm-deepseek-account')!, ns } } /** Credentials answers over the Remote carrier, which has no envelope. */ @@ -1904,11 +1904,11 @@ describe('apiKeyFailure', () => { }) it.each([en, zh])('edits the account model catalog without credential or endpoint fields', async (copy) => { - const scripted = scriptedFace({}) + const mutate = vi.fn(() => Promise.resolve(remoteOk(accountNamespace()))) + const scripted = scriptedFace({ mutate }) const ops = operationsWith(scripted.face) const describe = vi.spyOn(ops, 'describeCredential') const namespace = accountNamespace() - const mutate = vi.spyOn(scripted.face.settings, 'mutate').mockResolvedValue(remoteOk(accountNamespace())) const onClose = vi.fn() render( { + const namespace = accountNamespace('team-account-entry') + const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace))) + const ops = operationsWith(scriptedFace({ mutate }).face) + const set = vi.spyOn(ops, 'storeCredential') + const onClose = vi.fn() + render() + expect(screen.queryByText(content => content.includes(en.advancedHint))).toBeNull() + expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy() + fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } }) + fireEvent.click(screen.getByText(en.apply)) + await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) }) + expect(mutate).toHaveBeenCalledWith('team-account-entry', expect.any(Array), 0) + expect(set).not.toHaveBeenCalled() +}) + +it('opens the account row from the section and saves to its own namespace', async () => { + const namespace = accountNamespace() + const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace))) + const { controller, set } = await mountSection({ mutate }) + const row = controller.store.getSnapshot().rows[0]! + await act(async () => { controller.store.update((state) => { + state.rows = [{ + ...row, + accountAvailable: true, + entry: { + provider: 'deepseek-account', displayName: en.deepSeekAccount, settingsNs: namespace.ns, settingsPath: [], active: true, + }, + }, ...state.rows] + }) }) + fireEvent.click(screen.getByRole('button', { + name: providerCopy(en.editProvider, { provider: 'deepseek-account', displayName: en.deepSeekAccount }), + })) + expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy() + fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } }) + fireEvent.click(screen.getByText(en.apply)) + await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) }) + expect(mutate.mock.calls[0]).toEqual([ + 'llm-deepseek-account', + [{ + op: 'set', + path: ['models'], + value: [ + { + id: 'deepseek-v4-mini', + name: 'DeepSeek-V4-Flash', + description: 'Preserved hidden detail', + contextWindow: 1_000_000, + }, + { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 }, + ], + }], + 0, + ]) + expect(set).not.toHaveBeenCalled() +}) + it('renders the localized account row and supports catalogs without capacity defaults', async () => { const scripted = scriptedFace({}) const { controller, view } = await mountFace(scripted) From 955a706d336f5e977fb079fd0c0433bd064c71b0 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:59:56 -0700 Subject: [PATCH 03/14] fix(ui-schedule): clamp saved prompts in task run records to two lines Each record rendered its whole saved prompt, so a long instruction on a recurring task filled the records tab with the same text once per run. A prompt longer than two lines now shows two lines and an Expand toggle; the toggle appears only while the clamp hides text, and a collapsed prompt re-measures when the panel width changes. Also drop the stale comment about the removed message-id anchor. --- packages/client/ui-schedule/README.i18n.yaml | 4 +- packages/client/ui-schedule/README.md | 2 +- packages/client/ui-schedule/README.zh.md | 2 +- .../src/client/DeliveryHistory.tsx | 39 +++++++- .../src/client/TaskManagerPage.module.css | 42 ++++++++- .../src/client/task-manager-locales.ts | 4 + .../tests/delivery-history.client.spec.tsx | 90 +++++++++++++++++++ 7 files changed, 174 insertions(+), 9 deletions(-) diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index b7d6d89b78..8ac72ffbd6 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: 6ae804e65293c141 - zh: ebdbe1b5b81ea52d + en: 8e2b1f368fd2ede0 + zh: 39938b60e4351977 /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 6bd6882dbbbefcb0 zh: 03aa15f1a65d13dc diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index fc60b7c3f1..0a32e39210 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -39,7 +39,7 @@ Rules shows the rule in a Run time card whose rows edit a local draft: Repeat is The rows are disabled while a save is in flight, while the catalog is loading, during a deletion, and for an inactive task. A save bar appears only while the local draft differs from the stored task: it shows Unsaved changes with Cancel and Save changes, and Save changes becomes Saving… while the save is in flight. Cancel restores the stored values. A catalog refresh becomes the shown values only while the draft has no unsaved edit, so an unsaved draft survives an authoritative refresh. A failed save keeps the draft and shows a localized message; after a conflict, retry against the refreshed record. If the task disappears from the catalog, Delete is disabled until the task returns. A successful update refreshes the authoritative catalog. A rejected or unconfirmed update is distinct from a catalog-read failure: Retry reloads the catalog without presenting the update as failed. If the update itself cannot be confirmed, check the task before retrying. Late responses after switching tasks or closing details do not replace the current view, and no failure rolls back a Host write already begun. -Opening Delivery records lazily requests the newest 20 saved records. Load older records adds another page in newest-first append order, independent of wall-clock rollback. The tab strip is the whole header of that view: the editable name and next-run line belong to Rules. Each Saved delivery record leads with a clock glyph and its occurrence time as the locale's month name and clock field pair in the task's zone, or the browser's zone for a one-shot record, and shows the actual saved prompt when present. A legacy receipt without a prompt never substitutes the current instruction. +Opening Delivery records lazily requests the newest 20 saved records. Load older records adds another page in newest-first append order, independent of wall-clock rollback. The tab strip is the whole header of that view: the editable name and next-run line belong to Rules. Each Saved delivery record leads with a clock glyph and its occurrence time as the locale's month name and clock field pair in the task's zone, or the browser's zone for a one-shot record, and shows the actual saved prompt when present. A prompt longer than two lines shows its first two lines and an Expand toggle, which Collapse reverses; a collapsed prompt rechecks its length when the panel width changes. A legacy receipt without a prompt never substitutes the current instruction. After the last saved page loads, a nonempty history with confirmed pruning shows a borderless notice at the bottom left of the detail panel. Its info button expands or collapses the current Host retention rules. Empty history, legacy unavailability without confirmed pruning, incomplete pagination, and failed reads show no cleanup notice. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 2ff1533fae..188720d31a 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -39,7 +39,7 @@ dsh web --patch apps/cli/config/examples/schedule/cordis.yml 行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 -打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。旧回执没有提示文本时,绝不以当前指令代替。 +打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。 最后一页加载完成后,非空且确认裁剪过的历史在详情面板左下角显示无分隔线的清理提示,信息图标可展开或收起当前宿主的保留规则。空历史、未确认裁剪的旧格式历史、尚未加载完的分页和读取失败均不显示清理提示。 diff --git a/packages/client/ui-schedule/src/client/DeliveryHistory.tsx b/packages/client/ui-schedule/src/client/DeliveryHistory.tsx index 89f71adc6c..2e6c1f3a36 100644 --- a/packages/client/ui-schedule/src/client/DeliveryHistory.tsx +++ b/packages/client/ui-schedule/src/client/DeliveryHistory.tsx @@ -1,8 +1,11 @@ /** Lazy saved delivery pages owned by the selected task's mounted records view. */ -import { useCallback, useEffect, useId, useRef, useState } from 'react' +import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' -import { Button, IconClockOutlineRegular, IconInfoOutlineRegular, IconWarningOutlineRegular, StateDot, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' +import { + Button, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, IconClockOutlineRegular, IconInfoOutlineRegular, + IconWarningOutlineRegular, StateDot, Tooltip, +} from '@deepseek-ai/dsh-client-ui-primitives' import type { ScheduleDeliveryHistoryRequest, ScheduleDeliveryHistoryResult } from '@deepseek-ai/dsh-schedule/client' import { formatScheduleNextRun } from './schedule-format.ts' import css from './TaskManagerPage.module.css' @@ -28,6 +31,36 @@ type Props = DeliveryHistoryInjected & PropsLocale<'schedule.manager'> & { timeZone?: string | undefined } +/** + * Render one saved prompt clamped to two lines; the toggle appears only while the clamp hides text. + * Width changes re-measure a collapsed prompt; an expanded prompt keeps its toggle until collapsed. + * @param props - Saved prompt text and locale. + * @returns The prompt paragraph and, when its text exceeds two lines, the expand or collapse toggle. + */ +function SavedPrompt({ prompt, t }: { prompt: string } & PropsLocale<'schedule.manager'>) { + const ref = useRef(null) + const id = useId() + const [expanded, setExpanded] = useState(false) + const [clamped, setClamped] = useState(false) + useLayoutEffect(() => { + if (expanded) return + const paragraph = ref.current as HTMLParagraphElement + const measure = () => { setClamped(paragraph.scrollHeight > paragraph.clientHeight) } + measure() + const observer = new ResizeObserver(measure) + observer.observe(paragraph) + return () => { observer.disconnect() } + }, [expanded, prompt]) + return <> +

{prompt}

+ {clamped && } + +} + /** * Render immutable saved deliveries; the parent keys this view by task and Session. * Refreshes supersede pending older pages, and unmount ignores both late outcomes. @@ -121,7 +154,7 @@ export function DeliveryHistory({ id, sessionId, latestMessageId, timeZone, load
- {record.prompt !== undefined &&

{record.prompt}

} + {record.prompt !== undefined && } )} {page?.nextBefore !== undefined && failure === undefined &&