From f92f984d3dad43ae138a4c2b0b5ef2733d97fb11 Mon Sep 17 00:00:00 2001 From: creatixchu Date: Wed, 16 Sep 2026 19:48:48 +0800 Subject: [PATCH 01/17] docs(notes): propose one card per delivered changed file --- ...09-16-delivered-file-single-card.i18n.yaml | 6 +++ .../2026-09-16-delivered-file-single-card.md | 41 +++++++++++++++++++ ...026-09-16-delivered-file-single-card.zh.md | 41 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 .agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.i18n.yaml create mode 100644 .agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.md create mode 100644 .agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.zh.md diff --git a/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.i18n.yaml b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.i18n.yaml new file mode 100644 index 0000000000..cc205c7391 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.md +2026-09-16-delivered-file-single-card.md: 65edb58a97a3c67550df37a4172863faa9d2eae4 +2026-09-16-delivered-file-single-card.zh.md: c21857071184fc3a5f4d97eb524606f465c82ae1 diff --git a/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.md b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.md new file mode 100644 index 0000000000..65edb58a97 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.md @@ -0,0 +1,41 @@ +# Agent Note: One card per delivered changed file + +Status: proposed + +English | [中文](2026-09-16-delivered-file-single-card.zh.md) + +## Problem + +A finished turn ends with two file surfaces: the [changed-files card](../../implemented/feature/2026-09-11-turn-changed-files-card.md) lists every file the turn changed with its line counts and opens the [review tab](../../implemented/feature/2026-09-15-changed-file-diff-preview.md) on that file, and a delivery card for every file the model declared through [present](../../implemented/feature/2026-09-08-present-workspace-source-files.md) previews it in the Sidebar and opens it natively. A file the turn both changed and delivered appears in both, and the second appearance tells the user nothing new; the recorded `changed-files-turn` scenario shows four files as four rows and four cards. + +The two surfaces differ in what they offer and how long they live. The row carries counts and the comparison; the card carries the model's description, the preview, and the native actions. The card's content lives only in the Host process, so a conversation reopened after a Host restart has no changed-files card, while `deliverables/presented` events persist in the Session log and their cards come back. + +## Proposal + +Render a file the turn both changed and delivered once, on its delivery card. The changed-files card drops that file's row and keeps its header, which still names the turn's complete file count and summed counts and still opens the review on the turn; when every changed file was delivered only the header remains. The delivery card gains the file's added and deleted lines, or its binary or oversized label, beside the file name, and that label opens the review tab on the file, so the comparison, the preview, the native actions, and the description stay available from one place. Files only changed or only delivered render as before, and turns never share data. + +Matching is lexical, on the Client, at render time: separators normalized to `/`, `.` segments dropped, `..` folded, and an absolute path under the Session working directory stated relative to it. Repeated deliveries of one file in a turn merge under the same key, keeping the latest description. Two spellings that meet only through a symbolic link stay distinct. + +Nothing durable changes: no Session event, no Host route, and no recorder change; the merge lives in `packages/client/ui-deliverables` beside the card derivation. After a Host restart the changed-files card is absent and the delivery cards render as today, without a special case. + +## Alternatives considered + +**Keeping the row and dropping the card** costs less code, but turns a deliverable's default entry from the preview into the comparison: a report or SVG becomes one diff row, and a binary deliverable becomes a `binary` label with no preview at all. + +**Asking the model not to `present` files the changed-files card will list** cannot work: the `workspace/changes` event is never model-visible and its coverage depends on git, ignore rules, the file cap, and temporary-directory exclusion; `present` is shared by the `standard`, `ptc`, and `cordis` presets whose headless and SDK consumers have no changed-files card; and after a Host restart an undelivered file has no entry at all. The tool description also requires delivering every requested output, which the [delivery decision](../../implemented/feature/2026-09-08-present-workspace-source-files.md) settled. + +**One combined card** that lists every changed file with delivery actions on the delivered rows would merge two surfaces with different lifetimes into one and reopen the card design; the proposal keeps both surfaces and moves only the duplicate. + +## Acceptance criteria + +- A file both changed and delivered in one turn renders one delivery card and no changed-files row; the header count and totals still cover it. +- The delivery card shows the file's counts, and that label opens the turn's review tab on the file; preview, native actions, and the description remain. +- Repeated deliveries of one file in a turn render one card; files with the same basename in different directories render separately. +- Reloading the page renders the same result as the first render; after a Host restart the delivery cards render without the changed-files card. +- The `changed-files-turn` and `present-svg` recorded scenarios and the package unit tests cover the merge. + +## Risks + +- A delivery spelled through a symbolic link, or with an absolute path outside the working directory that the recorder spelled differently, is not matched and renders twice; the README lists this as a known limitation. +- A changed-files card reduced to its header may read as incomplete; the header keeps the complete count so the review still lists every file. +- The counts label on a 60px delivery card competes for space with long file names; the design of that label is open until a design is agreed. diff --git a/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.zh.md b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.zh.md new file mode 100644 index 0000000000..c218570711 --- /dev/null +++ b/.agents/notes/proposed/feature/2026-09-16-delivered-file-single-card.zh.md @@ -0,0 +1,41 @@ +# Agent Note: 交付的改动文件只显示一张卡片 + +Status: proposed + +[English](2026-09-16-delivered-file-single-card.md) | 中文 + +## 问题 + +一轮结束时有两个文件区块:[改动文件卡片](../../implemented/feature/2026-09-11-turn-changed-files-card.zh.md)列出本轮改动的每个文件及其增删行数,并在该文件上打开 [review tab](../../implemented/feature/2026-09-15-changed-file-diff-preview.zh.md);模型通过 [present](../../implemented/feature/2026-09-08-present-workspace-source-files.zh.md) 声明的每个文件有一张交付卡片,在 Sidebar 中预览并用原生应用打开。本轮既改动又交付的文件在两处都出现,第二次出现没有给用户任何新信息;录制的 `changed-files-turn` 场景里四个文件显示为四行加四张卡片。 + +两个区块提供的东西和寿命都不同。行有行数和对比;卡片有模型写的说明、预览和原生操作。卡片的内容只活在 Host 进程里,Host 重启后重新打开的会话没有改动文件卡片,而 `deliverables/presented` 事件持久化在 Session log 里,它们的卡片会回来。 + +## 提议 + +本轮既改动又交付的文件只显示一次,显示在交付卡片上。改动文件卡片去掉该文件的行,保留头部;头部仍写本轮完整的文件数和汇总行数,仍在本轮上打开 review;所有改动文件都被交付时只剩头部。交付卡片在文件名旁补上该文件的增删行数,或二进制、过大标签,点击该标签在该文件上打开 review tab,这样对比、预览、原生操作和说明都从同一处可达。只改动或只交付的文件照旧显示,不同轮次之间不共享数据。 + +匹配在 Client 渲染时按词法进行:分隔符统一为 `/`,去掉 `.` 段,折叠 `..`,落在 Session 工作目录下的绝对路径改写为相对路径。同一轮内重复交付同一文件按同一个键合并,保留最新的说明。只经软链才相同的两种拼写保持不同。 + +持久化的东西都不变:没有新的 Session 事件、没有 Host 路由、recorder 不改;合并逻辑放在 `packages/client/ui-deliverables` 里卡片推导的旁边。Host 重启后改动文件卡片不存在,交付卡片按现在的方式渲染,不需要特殊处理。 + +## 考虑过的替代方案 + +**保留行、去掉卡片**代码更少,但交付物的默认入口从预览变成对比:一份报告或 SVG 变成一行 diff,二进制交付物变成一个「二进制」标签,完全没有预览。 + +**让模型不要 `present` 改动文件卡片会列出的文件**行不通:`workspace/changes` 事件从不进模型上下文,它的覆盖范围取决于 git、ignore 规则、文件数上限和临时目录排除;`present` 由 `standard`、`ptc`、`cordis` 三个 preset 共用,它们的 headless 和 SDK 消费者没有改动文件卡片;Host 重启后没交付的文件就没有任何入口。工具描述也要求交付用户要的每个产出,这是[交付决定](../../implemented/feature/2026-09-08-present-workspace-source-files.zh.md)定下的。 + +**一张合并卡片**,列出每个改动文件并在被交付的行上放交付操作,会把两个寿命不同的区块并成一个,并重开卡片设计;本提议保留两个区块,只挪走重复的那份。 + +## 验收标准 + +- 同一轮既改动又交付的文件只显示一张交付卡片,没有改动文件行;头部的数量和汇总仍包含它。 +- 交付卡片显示该文件的行数,点击该标签在该文件上打开本轮的 review tab;预览、原生操作和说明都保留。 +- 同一轮重复交付同一文件只显示一张卡片;不同目录下同名的文件分别显示。 +- 重新加载页面后的渲染结果与首次一致;Host 重启后交付卡片照常渲染,没有改动文件卡片。 +- `changed-files-turn` 和 `present-svg` 录制场景以及本包单测覆盖这次合并。 + +## 风险 + +- 经软链拼写的交付,或 recorder 拼法不同的工作目录外绝对路径,不会被匹配到,仍显示两次;README 把它列为已知限制。 +- 只剩头部的改动文件卡片可能显得不完整;头部保留完整数量,review 仍列出每个文件。 +- 60px 高的交付卡片上,行数标签会和长文件名争空间;该标签的样式在设计定下之前保持开放。 From 9ce43f925c94b9cb988b16f92bffd92a54405670 Mon Sep 17 00:00:00 2001 From: Kaige-Gao Date: Thu, 17 Sep 2026 16:40:13 +0800 Subject: [PATCH 02/17] feat(web): add feedback to session header menu --- ...8-feedback-dialog-and-categories.i18n.yaml | 4 +-- ...26-09-08-feedback-dialog-and-categories.md | 2 ++ ...09-08-feedback-dialog-and-categories.zh.md | 2 ++ apps/web/tests/message-feedback.e2e.ts | 36 +++++++++++++++++++ .../ui-message-feedback/README.i18n.yaml | 4 +-- packages/client/ui-message-feedback/README.md | 4 +-- .../client/ui-message-feedback/README.zh.md | 4 +-- .../ui-message-feedback/src/client/index.ts | 22 +++++++++++- .../tests/browser-plugin.client.spec.tsx | 14 ++++++++ .../session-log-export/README.i18n.yaml | 4 +-- .../session-log-export/README.md | 2 ++ .../session-log-export/README.zh.md | 2 ++ .../session-log-export/package.json | 1 + .../src/client/HeaderAction.tsx | 33 +++++++++++++---- .../session-log-export/src/client/index.ts | 17 ++++++--- .../session-log-export/src/client/locales.ts | 2 ++ .../tests/client-apply.client.spec.tsx | 21 +++++++++++ .../tests/header-action.client.spec.tsx | 32 ++++++++++++++--- .../session-log-export/tsconfig.client.json | 1 + pnpm-lock.yaml | 3 ++ scripts/gen-cordis-catalog.ts | 1 + 21 files changed, 184 insertions(+), 27 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.i18n.yaml b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.i18n.yaml index 16c17f3405..ad7aa6aa6e 100644 --- a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.md -2026-09-08-feedback-dialog-and-categories.md: ee656946a9e5d2921a02a1b642041e5965661f31 -2026-09-08-feedback-dialog-and-categories.zh.md: ef2b2d4670aad12398125925dd1e1a7e7c10a5f0 +2026-09-08-feedback-dialog-and-categories.md: 271eb080dcca866ddbb5fee9df2d72ef9a1300ee +2026-09-08-feedback-dialog-and-categories.zh.md: 10f662e7d020c283bc250ffc75d71f757ddd6516 diff --git a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.md b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.md index ee656946a9..271eb080dc 100644 --- a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.md +++ b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.md @@ -14,6 +14,8 @@ The Web client had two disconnected feedback paths with no visible outcome. `/fe `ui-message-feedback` becomes the Web feedback surface. A per-session `FeedbackSurface` owns the message-feedback controller, a `FeedbackDialogController` for the draft, the submission, and the toast sequence, and the routing between them: a message target puts its selected judgment with the dialog's category and note through the message controller, while the Session target records through `ctx.remote.sessionFeedback`. A `FeedbackDialog` entry of `conversation.input.overlay` renders the Modal and Toast primitives from the dialog store. A decoration on the Host's `feedback` command opens the dialog for the Session from a menu pick or a bare Enter while `/feedback ` still reaches the Host; it uses the `action` kind in `CommandUiSpec`, which consumes the trigger token and runs a client callback without submitting anything. The later [symmetric message feedback submission](2026-09-10-symmetric-message-feedback-submission.md) decision owns the rating entry rule: either unrecorded rating opens the dialog, while clicking the recorded rating retracts it. The note popover, `clearNote`, and `clear` remain absent because the dialog is the only note editor. +The Session Header’s more-actions menu also opens the Session dialog through the `feedbackUi.openSession` Client service. The menu remains owned by `session-log-export`, while the existing per-session feedback controller owns the draft and submission. An observable tracks the optional feedback plugin’s availability, so unloading feedback removes only its menu row and leaves export usable. Opening or dismissing the form writes no feedback event; Submit records one Session remark. + The dialog is the shared Modal card at the design's width; the design's checkbox for including the conversation log is not built, because the log travels with every feedback event and is not optional. An oversized description still fails on submit with `note-too-large`; the dialog stays open with its draft and a warning toast presents the localized failure. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.zh.md b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.zh.md index ef2b2d4670..10f662e7d0 100644 --- a/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.zh.md +++ b/.agents/notes/implemented/feature/2026-09-08-feedback-dialog-and-categories.zh.md @@ -14,6 +14,8 @@ Web 客户端有两条互不相连的反馈路径,且都没有可见结果。` `ui-message-feedback` 成为 Web 反馈界面。每个 Session 一个 `FeedbackSurface`,拥有消息反馈控制器、负责草稿、提交与 toast 序号的 `FeedbackDialogController`,以及两者之间的路由:消息目标经消息控制器 put 一条带弹窗分类与备注的所选评分,Session 目标经 `ctx.remote.sessionFeedback` 记录。`conversation.input.overlay` 的 `FeedbackDialog` 条目从弹窗 store 渲染 Modal 与 Toast 基元。宿主 `feedback` 命令上的装饰让菜单选中或不带参数的回车为 Session 打开弹窗,而 `/feedback ` 仍到达宿主;它使用 `CommandUiSpec` 中的 `action` 种类:裸调用消费触发 token 后运行一个客户端回调,不提交任何内容。后续的[对称消息反馈提交](2026-09-10-symmetric-message-feedback-submission.zh.md)决策拥有评分入口规则:任一未记录的评分都会打开弹窗,再次点击已记录的评分则撤回。备注浮层、`clearNote` 与 `clear` 继续保持移除,因为弹窗是唯一的备注编辑器。 +会话标题栏的更多操作菜单也通过 `feedbackUi.openSession` Client 服务打开 Session 弹窗。菜单仍由 `session-log-export` 持有,现有的逐 Session 反馈控制器持有草稿与提交。一个可观察值跟踪可选反馈插件的可用状态,因此卸载反馈插件只移除其菜单行,导出仍然可用。打开或关闭表单不写反馈事件;点击提交才记录一条 Session 反馈。 + 弹窗是共用的 Modal 卡片,宽度按设计稿;设计稿里「包括当前对话的日志」复选框不做,因为日志随每个反馈事件一起投递,不是可选项。超长描述仍在提交时以 `note-too-large` 失败;弹窗保留草稿并通过警告 toast 展示本地化错误。 ## 考虑过的替代方案 diff --git a/apps/web/tests/message-feedback.e2e.ts b/apps/web/tests/message-feedback.e2e.ts index df89180c18..f26202d4b8 100644 --- a/apps/web/tests/message-feedback.e2e.ts +++ b/apps/web/tests/message-feedback.e2e.ts @@ -145,4 +145,40 @@ describe('web e2e: durable per-message feedback', () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }) + + it.skipIf(MODE === 'record')('opens the shared dialog from the header and records Session feedback only on submit', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-header-feedback')) + await openSeededSession() + await page.getByText('DONE', { exact: true }).waitFor({ timeout: 30_000 }) + const agent = scaffold.ctx.agents.get(SessionId(SEED_ID)) + if (agent === undefined) throw new Error('seeded session did not attach an agent') + const feedbackEvents = () => agent.session.snapshotEvents().filter(event => event.type.startsWith('feedback/')) + const before = feedbackEvents().length + const dialog = page.getByRole('dialog', { name: 'Submit feedback' }) + + await page.getByRole('button', { name: 'More actions', exact: true }).click() + await page.getByRole('menuitem', { name: 'Download session log' }).waitFor() + await page.getByRole('menuitem', { name: 'Feedback', exact: true }).click() + await dialog.waitFor() + expect(await page.getByRole('menu').count()).toBe(0) + expect(feedbackEvents()).toHaveLength(before) + await dialog.getByRole('textbox', { name: 'Feedback details' }).fill('discarded draft') + await dialog.getByRole('button', { name: 'Close', exact: true }).click() + expect(feedbackEvents()).toHaveLength(before) + + await page.getByRole('button', { name: 'More actions', exact: true }).click() + await page.getByRole('menuitem', { name: 'Feedback', exact: true }).click() + await dialog.waitFor() + expect(await dialog.getByRole('textbox', { name: 'Feedback details' }).inputValue()).toBe('') + await dialog.getByRole('button', { name: 'Product features and interaction', exact: true }).click() + await dialog.getByRole('textbox', { name: 'Feedback details' }).fill('Feedback from the Session menu.') + await dialog.getByRole('button', { name: 'Submit', exact: true }).click() + await expect.poll(() => dialog.count()).toBe(0) + await page.getByRole('alert').filter({ hasText: 'Thanks for your feedback' }).waitFor() + expect(feedbackEvents().slice(before)).toMatchObject([ + { type: 'feedback/record', data: { category: 'product-interaction', text: 'Feedback from the Session menu.' } }, + ]) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 60_000) }) diff --git a/packages/client/ui-message-feedback/README.i18n.yaml b/packages/client/ui-message-feedback/README.i18n.yaml index bdd5f1e664..642559b51f 100644 --- a/packages/client/ui-message-feedback/README.i18n.yaml +++ b/packages/client/ui-message-feedback/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-message-feedback/README.md -README.md: b37278e8d115a2ad61bb17ef8d884b0d888cd6a7 -README.zh.md: cd233061640f5e52ea9ca54ffe973bff3b957197 +README.md: bc33b02c6966bfa851614ef616c1ff9c583ae67d +README.zh.md: 88c1ba476c1a9c2a2df4b6cad471d78af65b83c9 diff --git a/packages/client/ui-message-feedback/README.md b/packages/client/ui-message-feedback/README.md index b37278e8d1..bc33b02c69 100644 --- a/packages/client/ui-message-feedback/README.md +++ b/packages/client/ui-message-feedback/README.md @@ -25,7 +25,7 @@ This package is the Web GUI's feedback surface: the Like/Dislike pair in the fin ## Use this package -Mount this plugin alongside `ui-conversation` and `ui-commands`; the Like/Dislike pair then appears in the action row of each turn's closing assistant message, between copy and branch, and the Feedback row of the composer menu opens the dialog. A recorded rating shows the filled glyph and stays visible without hover. Like and Dislike both open the dialog: seven category chips and a detail box are optional, and Submit records the selected judgment with whatever was filled in before the toast thanks the user; the conversation log travels with every feedback event. Clicking the recorded rating retracts it without opening the dialog. A bare `/feedback`, picked from the menu or typed and sent without text, opens the same dialog for the Session; `/feedback ` keeps the Host command path and its acknowledgement row. +Mount this plugin alongside `ui-conversation` and `ui-commands`; the Like/Dislike pair then appears in the action row of each turn's closing assistant message, between copy and branch, and Feedback in either the composer menu or the Session Header’s more-actions menu opens the same dialog. A recorded rating shows the filled glyph and stays visible without hover. Like and Dislike both open the dialog: seven category chips and a detail box are optional, and Submit records the selected judgment with whatever was filled in before the toast thanks the user; the conversation log travels with every feedback event. Clicking the recorded rating retracts it without opening the dialog. A bare `/feedback`, picked from the menu or typed and sent without text, opens the same dialog for the Session; `/feedback ` keeps the Host command path and its acknowledgement row. ### Failures @@ -39,7 +39,7 @@ A rating or list-load failure shows inline in the row; a submission failure show
Implementation internals — click to expand -The package contributes the `feedback` entry (order 10) of `conversation.chat.assistant-actions`, declared by ui-conversation and rendered inside the finalized assistant message's IconActions row, and the `feedback-dialog` entry (order 2) of `conversation.input.overlay`, which renders the Modal and Toast primitives through body portals and centers the toast over the composer card it mounts inside. The `/feedback` decoration is an `action` registered through `ctx.commandUi.decorate`, so a menu pick or a bare Enter consumes the trigger token and opens the dialog while an argued line still reaches the Host command. +The package contributes the `feedback` entry (order 10) of `conversation.chat.assistant-actions`, declared by ui-conversation and rendered inside the finalized assistant message's IconActions row, and the `feedback-dialog` entry (order 2) of `conversation.input.overlay`, which renders the Modal and Toast primitives through body portals and centers the toast over the composer card it mounts inside. The `feedbackUi.openSession(sessionId)` Client service opens that same Session-scoped dialog without recording feedback; the Header menu and command decoration both use it. The `/feedback` decoration is an `action` registered through `ctx.commandUi.decorate`, so a menu pick or a bare Enter consumes the trigger token and opens the dialog while an argued line still reaches the Host command. Per Session, one `MessageFeedbackController` backs every message control and one `FeedbackDialogController` owns the dialog draft, the submission, and the toast sequence. The message controller reads `messageFeedback.list` once, deferred to the first hover or focus rather than fired on mount, and serializes mutations so each carries the version last observed; a `version-conflict` reply carries the authoritative item and reconciles the view without refetching. Before either rating action proceeds, the row checks the committed item: the matching rating calls `retract`, which rechecks the rating in the serialized queue and becomes a no-op after a concurrent change, while any other state opens the dialog with the requested rating. The dialog controller submits by target: a message target puts that rating with the dialog's note and category through the message controller, and the Session target records through `ctx.remote.sessionFeedback`. Success closes the draft and raises the acknowledgement toast; a late success from a superseded draft raises that toast without closing the new draft; a failure keeps the draft open and raises a longer-lived warning toast. diff --git a/packages/client/ui-message-feedback/README.zh.md b/packages/client/ui-message-feedback/README.zh.md index cd23306164..88c1ba476c 100644 --- a/packages/client/ui-message-feedback/README.zh.md +++ b/packages/client/ui-message-feedback/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -与 `ui-conversation`、`ui-commands` 一起挂载本插件;Like/Dislike 对随即出现在每个轮次收尾助手消息的动作行中,位于复制与分支之间,输入框菜单里的「反馈」行则打开弹窗。已记录的评分显示实心图标,不需要悬停也一直可见。点赞和点踩都会打开弹窗:七个分类标签和一个详情框都可不填;提交后才会记录带所填内容的对应评分并弹出感谢 toast,对话日志随每个反馈事件一起投递。再次点击已记录的评分会直接撤回,不打开弹窗。不带文本的 `/feedback`,无论是从菜单选中还是直接输入后发送,都会为 Session 打开同一个弹窗;`/feedback ` 仍走宿主命令路径并显示确认行。 +与 `ui-conversation`、`ui-commands` 一起挂载本插件;Like/Dislike 对随即出现在每个轮次收尾助手消息的动作行中,位于复制与分支之间,输入框菜单或会话标题栏更多操作菜单里的「反馈」会打开同一个弹窗。已记录的评分显示实心图标,不需要悬停也一直可见。点赞和点踩都会打开弹窗:七个分类标签和一个详情框都可不填;提交后才会记录带所填内容的对应评分并弹出感谢 toast,对话日志随每个反馈事件一起投递。再次点击已记录的评分会直接撤回,不打开弹窗。不带文本的 `/feedback`,无论是从菜单选中还是直接输入后发送,都会为 Session 打开同一个弹窗;`/feedback ` 仍走宿主命令路径并显示确认行。 ### 失败 @@ -39,7 +39,7 @@ kind: "package-reference"
实现细节——点击展开 -本包贡献 `conversation.chat.assistant-actions` 的 `feedback` 条目(order 10),由 ui-conversation 声明并渲染在已定稿助手消息的 IconActions 行内;同时贡献 `conversation.input.overlay` 的 `feedback-dialog` 条目(order 2),它通过 body portal 渲染 Modal 与 Toast 基元,并让 toast 以其所在的输入框卡片为中心。`/feedback` 装饰是经 `ctx.commandUi.decorate` 注册的 `action`,因此菜单选中或不带参数的回车会消费触发 token 并打开弹窗,而带参数的命令行仍到达宿主命令。 +本包贡献 `conversation.chat.assistant-actions` 的 `feedback` 条目(order 10),由 ui-conversation 声明并渲染在已定稿助手消息的 IconActions 行内;同时贡献 `conversation.input.overlay` 的 `feedback-dialog` 条目(order 2),它通过 body portal 渲染 Modal 与 Toast 基元,并让 toast 以其所在的输入框卡片为中心。`feedbackUi.openSession(sessionId)` Client 服务打开同一个 Session 级弹窗,不记录反馈;标题栏菜单和命令装饰共用此方法。`/feedback` 装饰是经 `ctx.commandUi.decorate` 注册的 `action`,因此菜单选中或不带参数的回车会消费触发 token 并打开弹窗,而带参数的命令行仍到达宿主命令。 每个 Session 有一个 `MessageFeedbackController` 支撑所有消息控件,以及一个 `FeedbackDialogController` 拥有弹窗草稿、提交与 toast 序号。消息控制器只读取一次 `messageFeedback.list`,且延迟到首次 hover 或 focus 才发起,而非挂载时触发;变更串行执行,每次都携带最后观察到的版本,`version-conflict` 响应带回权威条目,据此对账视图而不重新拉取。任一评分操作执行前,该行都会检查已提交条目:评分相同则调用 `retract`,它会在串行队列内重新检查评分并在并发变更后变为无操作;其他状态则携带所选评分打开弹窗。弹窗控制器按目标提交:消息目标通过消息控制器 put 一条带弹窗备注与分类的对应评分,Session 目标通过 `ctx.remote.sessionFeedback` 记录。成功会关闭草稿并弹出确认 toast;被替换的旧草稿迟到的成功只弹确认 toast、不关闭新草稿;失败会保留弹窗并弹出停留时间更长的警告 toast。 diff --git a/packages/client/ui-message-feedback/src/client/index.ts b/packages/client/ui-message-feedback/src/client/index.ts index 91e6718f62..d421bf4d72 100644 --- a/packages/client/ui-message-feedback/src/client/index.ts +++ b/packages/client/ui-message-feedback/src/client/index.ts @@ -37,6 +37,21 @@ export type { } from './slots.ts' export type { MessageFeedbackKey } from './locales.ts' +/** Opens the existing Session feedback form without submitting feedback. */ +export interface FeedbackUi { + /** + * Open the Session feedback draft without recording feedback. + * @param sessionId - Session whose feedback draft to open. + */ + openSession(sessionId: SessionId): void +} + +declare module '@deepseek-ai/cordis' { + interface Context { + feedbackUi: FeedbackUi + } +} + /** Dictionary namespace owned by this plugin. */ const NS = 'feedback' @@ -65,6 +80,11 @@ export function apply(ctx: ClientContext): void { surfaces.clear() }, 'ui-message-feedback: per-session surfaces') + const feedbackUi: FeedbackUi = { + openSession: (sessionId) => { surfaceFor(sessionId).dialog.open({ kind: 'session' }) }, + } + ctx.provide('feedbackUi', feedbackUi) + // A reconnect can only invalidate what was already read; a cold Session // stays cold until something asks for it. ctx.on('connection/reset', () => { @@ -114,7 +134,7 @@ export function apply(ctx: ClientContext): void { scope.effect(() => scope.commandUi.decorate({ name: 'feedback', available: () => true, - ui: { kind: 'action', run: (session) => { surfaceFor(session.sessionId).dialog.open({ kind: 'session' }) } }, + ui: { kind: 'action', run: (session) => { feedbackUi.openSession(session.sessionId) } }, }), 'ui-message-feedback: /feedback decoration') }) } diff --git a/packages/client/ui-message-feedback/tests/browser-plugin.client.spec.tsx b/packages/client/ui-message-feedback/tests/browser-plugin.client.spec.tsx index 2bad94b036..e41e7d599a 100644 --- a/packages/client/ui-message-feedback/tests/browser-plugin.client.spec.tsx +++ b/packages/client/ui-message-feedback/tests/browser-plugin.client.spec.tsx @@ -164,6 +164,20 @@ describe('ui-message-feedback browser plugin', () => { expect(b.dialogEntry()!.inject!(sid('s2')).hooks.dialog.getSnapshot()).toMatchObject({ target: null, toast: 0 }) }) + it('opens the shared Session dialog through feedbackUi without recording anything', async () => { + const b = await bench() + await b.fiber.await() + const dialog = b.dialogEntry()!.inject!(sid('s1')) + b.ctx.feedbackUi.openSession(sid('s1')) + expect(dialog.hooks.dialog.getSnapshot().target).toEqual({ kind: 'session' }) + expect(b.dialogEntry()!.inject!(sid('s2')).hooks.dialog.getSnapshot().target).toBeNull() + expect(b.calls).toEqual([]) + dialog.dismiss() + expect(b.calls).toEqual([]) + await b.fiber.dispose() + expect(b.ctx.get('feedbackUi')).toBeUndefined() + }) + it('records a Session remark through the sessionFeedback Remote and a message judgment through put', async () => { const b = await bench() await b.fiber.await() diff --git a/packages/session-query/session-log-export/README.i18n.yaml b/packages/session-query/session-log-export/README.i18n.yaml index ae845c1725..3e072029dd 100644 --- a/packages/session-query/session-log-export/README.i18n.yaml +++ b/packages/session-query/session-log-export/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-query/session-log-export/README.md -README.md: 8697b0c2c105c928f5325f953b7fb663ae68e18e -README.zh.md: fe60c6ab8cc8d2d5e7515416bb72624584c11b9d +README.md: 23388e10b9c64071a666e9842a669e37d6b9545d +README.zh.md: 2af96f8621745fd6393f03905e4c6a978dbaf400 diff --git a/packages/session-query/session-log-export/README.md b/packages/session-query/session-log-export/README.md index 8697b0c2c1..23388e10b9 100644 --- a/packages/session-query/session-log-export/README.md +++ b/packages/session-query/session-log-export/README.md @@ -27,6 +27,8 @@ English | [中文](README.zh.md) Use this package when the Web bundle should let users export a session log. It requires Connection, the command registry, Session query and persistence, and attachments. Mount the plugin, then choose `Download session log` from the Session Header's more-actions menu or type `/export`; the browser downloads `dsh-session-.zip`. +When `ui-message-feedback` is mounted, the same menu also offers `Feedback`, which opens its existing Session feedback dialog. Opening or dismissing that form does not export the Session or submit feedback. The feedback row follows the feedback plugin’s availability; export remains available independently. + ### When to choose it Choose it for a Web deployment that needs user-facing session export with a visible download dialog. Avoid it when a programmatic or Host-side export is needed: this package produces a browser download, not a Host path write. The logs are serialized from persistence read handles, so any mounted backend is supported. diff --git a/packages/session-query/session-log-export/README.zh.md b/packages/session-query/session-log-export/README.zh.md index fe60c6ab8c..2af96f8621 100644 --- a/packages/session-query/session-log-export/README.zh.md +++ b/packages/session-query/session-log-export/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" 当 Web bundle 需要让用户导出会话日志时使用本包。它需要 Connection、命令注册表、Session 查询与持久化以及附件服务。挂载插件,然后在 Session Header 的更多操作菜单中选择 `下载 Session 日志` 或输入 `/export`;浏览器会下载 `dsh-session-.zip`。 +挂载 `ui-message-feedback` 时,同一菜单还提供“反馈”,打开已有的 Session 反馈弹窗。打开或关闭该弹窗不会导出 Session 或提交反馈。反馈入口随反馈插件的可用状态显示;导出功能保持独立可用。 + ### 何时选择 为需要带可见下载弹窗的面向用户的会话导出的 Web 部署选择它。需要程序化或 Host 侧导出时避免使用:本包产生的是浏览器下载,而非 Host 路径写入。日志从持久化读句柄序列化而来,因此任何已挂载后端都受支持。 diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index 75d743253d..5edbe45d8e 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -56,6 +56,7 @@ "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-client-ui-commands": "workspace:^", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", + "@deepseek-ai/dsh-client-ui-message-feedback": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", "@deepseek-ai/dsh-client-ui-session": "workspace:^", diff --git a/packages/session-query/session-log-export/src/client/HeaderAction.tsx b/packages/session-query/session-log-export/src/client/HeaderAction.tsx index cb20cb9f9d..5e5b472659 100644 --- a/packages/session-query/session-log-export/src/client/HeaderAction.tsx +++ b/packages/session-query/session-log-export/src/client/HeaderAction.tsx @@ -1,16 +1,31 @@ import { useState } from 'react' import type { ReactNode } from 'react' -import { IconDownloadOutline16, IconEllipsisOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { IconDownloadOutline16, IconEllipsisOutline16, IconPaperPlaneOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives' import { SessionLogDownloadDialog, type SessionLogDownloadDialogProps } from './Dialog.tsx' +import type { SessionLogDownloadDialogInjected } from './Dialog.tsx' import css from './HeaderAction.module.css' +/** The optional feedback plugin controls whether the Header offers its form. */ +export interface SessionLogDownloadHeaderInjected extends SessionLogDownloadDialogInjected { + hooks: SessionLogDownloadDialogInjected['hooks'] & { feedbackAvailable: ObservableSnapshot } + /** @param sessionId - Session whose existing feedback form to open. */ + openFeedback: (sessionId: SessionId) => void +} + +/** Session download props plus the optional feedback action. */ +export type SessionLogDownloadHeaderProps = SessionLogDownloadDialogProps & InjectFace + /** - * Render the Session Header more-actions icon button, its download menu, and the shared result dialog. + * Render the Session Header menu with download and optional feedback actions. * @param props - Session runtime, download controller, and localized copy. * @returns the persistent Header action and Session-scoped dialog. */ -export function SessionLogDownloadHeaderAction(props: SessionLogDownloadDialogProps): ReactNode { - const { sessionId, useSessionLogDownload, request, t } = props +export function SessionLogDownloadHeaderAction(props: SessionLogDownloadHeaderProps): ReactNode { + const { sessionId, useSessionLogDownload, useFeedbackAvailable, request, openFeedback, t } = props + const feedbackAvailable = useFeedbackAvailable(value => value) const entry = useSessionLogDownload(state => state.bySession[String(sessionId)]) const busy = entry?.status === 'downloading' const [open, setOpen] = useState(false) @@ -22,10 +37,14 @@ export function SessionLogDownloadHeaderAction(props: SessionLogDownloadDialogPr align="end" dense onClose={() => { setOpen(false) }} - items={[{ id: 'download', label: t('menu.download'), icon: , disabled: busy }]} - onSelect={() => { + items={[ + { id: 'download', label: t('menu.download'), icon: , disabled: busy }, + ...feedbackAvailable ? [{ id: 'feedback', label: t('menu.feedback'), icon: }] : [], + ]} + onSelect={(id) => { setOpen(false) - void request(sessionId) + if (id === 'feedback') openFeedback(sessionId) + else void request(sessionId) }} anchor={( @@ -275,7 +310,7 @@ function DetailTop({ crumbLabel, crumbText, onBack, icon, actions }: { readonly crumbLabel: string readonly crumbText: string readonly onBack: () => void - readonly icon?: ReactNode + readonly icon: ReactNode readonly actions?: ReactNode }): ReactNode { return ( @@ -285,7 +320,7 @@ function DetailTop({ crumbLabel, crumbText, onBack, icon, actions }: { {crumbText}
- + {actions}
@@ -314,6 +349,7 @@ function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: { title={title} t={t} onOpen={onOpen} + icon={packageArtwork(pkg.name)} tags={( <> {beta ? {t('statusBeta')} : null} @@ -339,7 +375,7 @@ function ItemCard({ item, t, onOpen, renderSlot }: { }): ReactNode { return (
  • - +
  • ) } @@ -353,7 +389,7 @@ function ItemDetail({ item, t, onBack, renderSlot }: { }): ReactNode { return (
    - +

    {item.label}

    @@ -382,7 +418,7 @@ function RowDetail({ pkg, row, t, onBack, renderSlot }: { const key = rowConfigKey(pkg.name, row.rowId) return (
    - } /> + } />

    {row.rowId}

    @@ -431,6 +467,7 @@ function PackageDetail({ crumbLabel={t('backToList')} crumbText={t('crumbRoot')} onBack={onBack} + icon={packageArtwork(pkg.name)} actions={(
    {pkg.installed @@ -918,31 +955,21 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { - +
    ) : null} - {state.status === 'loading' ? ( + {showsCards && state.status === 'loading' ? (

    {t('loading')}

    ) : null} - {state.status === 'unavailable' ? ( + {showsCards && state.status === 'unavailable' ? (

    {t('unavailable')}

    ) : null} - {state.status === 'error' - ? ( -
    -

    - {t('error')} -

    - -
    - ) - : null} {state.notice === null || noticeLine === null ? null : ( @@ -986,12 +1013,24 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode { ? { setView({ kind: 'list' }) }} /> : null} {loaded && showsCards - ? officialCards.length === 0 && mine.length === 0 + ? officialCards.length === 0 && mine.length === 0 && state.status !== 'error' ?

    {t('empty')}

    : ( <> {renderGroup('official', t('officialTitle'), officialCards)} {renderGroup('bundles', t('bundlesTitle'), mine.map(packageCard))} + {/* A failed package read trails the groups it left incomplete: right under Official on a + first-load failure, and after the kept cards when a refresh fails over stale data. */} + {state.status === 'error' + ? ( +
    +

    + {t('error')} +

    + +
    + ) + : null} ) : null} diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 63071a5cb7..f8f666a219 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -6,7 +6,7 @@ export const zh = { title: '插件', intro: '添加和管理插件', loading: '正在读取插件…', - error: '暂时无法读取插件。', + error: '可能由于网络问题,无法读取全部插件', unavailable: '本部署没有可管理的 profile,无法安装或启停插件。', retry: '重试', refresh: '刷新', @@ -17,9 +17,9 @@ export const zh = { bundlesTitle: '已安装', officialTitle: '官方', builtinAgentTeamTitle: '智能体团队', - builtinAgentTeamDescription: '启用智能体团队协作与团队工具。', + builtinAgentTeamDescription: '把一个任务交给一支团队:主智能体按需创建队友,成员之间发消息沟通、共同维护一块任务看板,各自认领工作;中途退出也不丢进度。', builtinAgentTeamWebTitle: '智能体团队 Web 界面', - builtinAgentTeamWebDescription: '在浏览器中查看团队成员、任务看板和成员会话。', + builtinAgentTeamWebDescription: '在浏览器里看团队干活:谁在忙、谁空闲一目了然,任务看板实时更新,点开任意成员就能进入它的会话。', builtinAutoReviewTitle: '自动授权审查', builtinAutoReviewDescription: '提供自动审查权限模式,由模型在每次工具调用前判断是否授权。', statusProblem: '异常', @@ -168,7 +168,7 @@ export const en = { title: 'Plugins', intro: 'Add and manage plugins', loading: 'Reading plugins…', - error: 'Plugins are temporarily unavailable.', + error: 'Could not read all plugins, possibly due to a network problem', unavailable: 'This deployment runs without a manageable profile, so plugins cannot be installed or switched here.', retry: 'Retry', refresh: 'Refresh', @@ -179,9 +179,9 @@ export const en = { bundlesTitle: 'Installed', officialTitle: 'Official', builtinAgentTeamTitle: 'Agent Teams', - builtinAgentTeamDescription: 'Enable agent team collaboration and team tools.', + builtinAgentTeamDescription: 'Hand a task to a team: the lead agent creates teammates as needed, members message each other and claim work on a shared task board, and progress survives even if someone drops off mid-way.', builtinAgentTeamWebTitle: 'Agent Teams Web UI', - builtinAgentTeamWebDescription: 'View team members, the task board, and teammate sessions in the browser.', + builtinAgentTeamWebDescription: 'Watch the team at work in the browser: see who is busy and who is idle at a glance, follow the task board live, and open any member\'s own session with a click.', builtinAutoReviewTitle: 'Auto Authorization Review', builtinAutoReviewDescription: 'Add an Auto review permission mode that uses the model to assess authorization before each tool call.', statusProblem: 'Problem', diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index 3e84437d9f..1e8fc33423 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -80,3 +80,6 @@ export type { MarkdownCodeLabels, MarkdownFileMentions, MarkdownLabels, Markdown export { extractMarkdownPlainText } from './markdown/plain-text.ts' export type { MarkdownPlainTextMode, MarkdownPlainTextOptions } from './markdown/plain-text.ts' export * from './icons/index.tsx' +export { + PluginArtworkTeam, PluginArtworkTerminal, PluginArtworkLoop, PluginArtworkSubagent, PluginArtworkSearch, PluginArtworkDefault, +} from './plugin-artwork.tsx' diff --git a/packages/client/ui-primitives/src/plugin-artwork.tsx b/packages/client/ui-primitives/src/plugin-artwork.tsx new file mode 100644 index 0000000000..2e70e6a71e --- /dev/null +++ b/packages/client/ui-primitives/src/plugin-artwork.tsx @@ -0,0 +1,107 @@ +/** + * Fixed-palette plugin artwork for the plugin management surfaces. Unlike the + * ic_ds_* set these glyphs carry their own brand colors and gradients instead + * of riding currentColor; all draw on a 36×36 viewBox and take {size, className}. + */ +import type { IconProps } from './icons/props.ts' + +/** Agent-team plugin artwork (four interlocking quadrants). */ +export const PluginArtworkTeam = ({ size = 36, className }: IconProps) => ( + + + + + + + + + + + + +) + +/** Terminal plugin artwork (prompt chevron and cursor bar). */ +export const PluginArtworkTerminal = ({ size = 36, className }: IconProps) => ( + + + + +) + +/** Agent-loop plugin artwork (four leaves circling a center). */ +export const PluginArtworkLoop = ({ size = 36, className }: IconProps) => ( + + + + + + + + + + + + +) + +/** Subagent plugin artwork (two stacked rounded squares); also marks one row inside a bundle. */ +export const PluginArtworkSubagent = ({ size = 36, className }: IconProps) => ( + + + + + + + + + + + + + + +) + +/** + * Web-search plugin artwork (conic-gradient ring and handle). SVG has no + * native conic gradient, so the ring clips an HTML div painted with CSS + * `conic-gradient` — the same emulation Figma exports; it renders inline in + * the browser UI but would stay empty in an `` or mask context. + */ +export const PluginArtworkSearch = ({ size = 36, className }: IconProps) => ( + + + + + +
    + + + + + + + + + +) + +/** Default plugin artwork for plugins without one of their own (connector blocks and a node). */ +export const PluginArtworkDefault = ({ size = 36, className }: IconProps) => ( + + + + + + + + + +) diff --git a/packages/client/ui-settings-plugins/src/client/locales.ts b/packages/client/ui-settings-plugins/src/client/locales.ts index e45dbb5cd0..562ed2d1e8 100644 --- a/packages/client/ui-settings-plugins/src/client/locales.ts +++ b/packages/client/ui-settings-plugins/src/client/locales.ts @@ -42,17 +42,17 @@ export const en: Record = { saveFailed: 'The deployment did not accept these values; they were left for you to correct.', invalidNumber: 'Enter a number, or leave blank to use the default.', bashTitle: 'Shell', - bashDescription: 'Limits every command the agent runs.', + bashDescription: 'Limit how long each command may run and how much it may output.', bashTimeoutMs: 'Command timeout (ms)', bashTimeoutMsHint: 'How long one command may run before it is terminated.', bashMaxOutputBytes: 'Output cap per stream (bytes)', bashMaxOutputBytesHint: 'Output beyond this spills to a temporary file rather than being lost.', agentLoopTitle: 'Agent loop', - agentLoopDescription: 'How the agent dispatches tool calls.', + agentLoopDescription: 'Control how the Agent dispatches tool calls.', agentLoopMaxParallel: 'Parallel tool calls', agentLoopMaxParallelHint: 'Upper bound on parallel-safe calls running at once within one step.', webSearchTitle: 'Web search', - webSearchDescription: 'The DeepSeek search provider.', + webSearchDescription: 'Set up the DeepSeek search provider.', webSearchApiKey: 'API key', webSearchApiKeyHint: 'Stored outside the settings file. Leave blank to keep the current key.', webSearchApiKeySet: 'A key is configured.', @@ -107,17 +107,17 @@ export const zh: Record = { saveFailed: '本部署没有接受这些值,已保留供你修改。', invalidNumber: '请填数字;留空表示使用默认值。', bashTitle: '终端', - bashDescription: '限制 agent 运行的每一条命令。', + bashDescription: '限制每条命令最多能跑多久、最多输出多少内容。', bashTimeoutMs: '命令超时(毫秒)', bashTimeoutMsHint: '单条命令允许运行多久,超时即终止。', bashMaxOutputBytes: '单流输出上限(字节)', bashMaxOutputBytesHint: '超出部分会转存到临时文件,而不是被丢弃。', agentLoopTitle: 'Agent 循环', - agentLoopDescription: 'Agent 如何派发工具调用。', + agentLoopDescription: '控制 Agent 派发工具调用的方式。', agentLoopMaxParallel: '并行工具调用数', agentLoopMaxParallelHint: '同一步内最多同时运行多少个可并行的调用。', webSearchTitle: '网页搜索', - webSearchDescription: 'DeepSeek 搜索提供方。', + webSearchDescription: '设置 DeepSeek 的搜索提供方。', webSearchApiKey: 'API Key', webSearchApiKeyHint: '不写入设置文件。留空表示保持当前密钥。', webSearchApiKeySet: '已配置密钥。', From bd6447479dbd4a409f43b141508d564b0b050cb5 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 18 Sep 2026 14:00:50 +0800 Subject: [PATCH 04/17] refine(plugin-manager): polish install dialog guide, input, and wizard states --- .../src/client/PluginManagerPage.module.css | 75 ++++++++++++------- .../src/client/PluginManagerPage.tsx | 30 ++++---- .../ui-plugin-manager/src/client/locales.ts | 12 +-- .../tests/components.client.spec.tsx | 6 +- .../client/ui-primitives/src/icons/index.tsx | 17 +++++ 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css index 63f55bd302..ba90b04f93 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -397,7 +397,7 @@ /* Wider than the default dialog: pnpm's lines are long, and the terminal keeps them unwrapped. The card never outgrows the viewport; the running screens scroll inside it so the head's controls stay reachable. */ .installDialog { - width: min(540px, 100%); + width: min(560px, 100%); max-height: calc(100vh - 48px); } @@ -425,16 +425,22 @@ } .installField input[type='text'] { - height: 36px; - padding: 0 12px; + height: 40px; + padding: 0 14px; border: 0.5px solid var(--dsw-alias-border-l4); - border-radius: 10px; + border-radius: 14px; + outline: none; background: var(--dsw-alias-bg-layer-3); font: inherit; font-size: 13px; color: var(--dsw-alias-label-primary); } +.installField input[type='text']:focus { + border-color: var(--dsw-alias-brand-primary); + box-shadow: inset 0 0 0 0.5px var(--dsw-alias-brand-primary); +} + .installField input[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); } @@ -445,6 +451,7 @@ align-items: center; align-self: flex-start; gap: 4px; + margin-top: 4px; padding: 0; border: 0; background: none; @@ -470,13 +477,12 @@ display: flex; flex-direction: column; gap: 10px; - padding: 12px 14px; + padding: 8px 14px 14px; border: 0.5px solid var(--dsw-alias-border-l4); - border-radius: 12px; + border-radius: 14px; background: var(--dsw-alias-bg-layer-1); } -.guideIntro, .guideHint { margin: 0; font-size: 12px; @@ -488,10 +494,10 @@ .guideSafety { display: flex; align-items: flex-start; - gap: 6px; + gap: 8px; margin: 0; - padding: 8px 10px; - border-radius: 8px; + padding: 10px 12px; + border-radius: 10px; background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent); font-size: 12px; line-height: 18px; @@ -503,19 +509,10 @@ margin-top: 2px; } -.guideNote { - margin: 0; - padding: 8px 10px; - border-radius: 8px; - background: var(--dsw-alias-bg-layer-3); - font-size: 12px; - line-height: 18px; - color: var(--dsw-alias-label-secondary); -} - .guideList { display: flex; flex-direction: column; + gap: 2px; margin: 0; padding: 0; list-style: none; @@ -526,38 +523,43 @@ align-items: flex-start; gap: 10px; padding: 8px 0; - border-top: 0.5px solid var(--dsw-alias-border-l4); + border-radius: 10px; + background: var(--dsw-alias-bg-layer-2); } .guideIndex { + display: inline-flex; flex: none; + align-items: center; + justify-content: center; width: 20px; height: 20px; border-radius: 999px; corner-shape: round; - background: var(--dsw-alias-bg-layer-3); + background: var(--dsw-alias-bg-module-platform); font-size: 11px; - line-height: 20px; - text-align: center; - color: var(--dsw-alias-label-tertiary); + font-weight: 500; + color: var(--dsw-alias-label-secondary); } .guideMain { display: flex; flex: 1; flex-direction: column; - gap: 2px; + gap: 0; min-width: 0; } .guideTitle { - font-size: 12.5px; - font-weight: 600; + font-size: 13px; + font-weight: 500; + line-height: 20px; color: var(--dsw-alias-label-primary); } .guideExample { font-size: 12px; + line-height: 20px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; } @@ -581,7 +583,9 @@ /* The footer's only action spans the dialog, as the primary action of a short form does. */ .wide { width: 100%; + height: 40px; justify-content: center; + border-radius: 14px; } /* The installing, installed, and failed screens: a head row, the centred @@ -593,7 +597,7 @@ gap: 16px; min-width: 0; min-height: 0; - padding: 16px 20px 20px; + padding: 16px 20px 0; } /* Everything under the head: it scrolls when the runs' output makes the screen taller than the card. */ @@ -664,6 +668,14 @@ color: var(--dsw-alias-label-secondary); } +.wizardIcon[data-state='done'] { + color: var(--dsw-alias-state-success-secondary); +} + +.wizardIcon[data-state='error'] { + color: var(--dsw-alias-state-warn-label); +} + .wizardTitle { margin: 0; font-size: 18px; @@ -714,6 +726,11 @@ gap: 12px; } +.footAction { + height: 32px; + border-radius: 16px; +} + .detailsToggle { display: inline-flex; align-items: center; diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index f27252c775..2e1fee9ea9 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -12,8 +12,8 @@ import { useEffect, useId, useState, type ReactNode } from 'react' import type { PluginInstallFailureKind } from '@deepseek-ai/dsh-api-remotes/client' import { - Button, IconChevronDownOutlineRegular, IconChevronLeftOutlineRegular, - IconChevronRightOutlineRegular, IconCloseOutlineRegular, + Button, IconCheckCircleFillRegular, IconChevronDownOutlineRegular, IconChevronLeftOutlineMedium, + IconChevronRightOutlineRegular, IconCloseOutlineMedium, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular, IconWarningOutlineRegular, Input, Modal, PluginArtworkDefault, PluginArtworkLoop, PluginArtworkSearch, PluginArtworkSubagent, PluginArtworkTeam, PluginArtworkTerminal, @@ -672,19 +672,19 @@ function InstallDialog({ )} >
    -