mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(web): date @ session rows by last activity, not creation
The Host session list already carries each session's `updatedAt`, which is the number its own rows show; reading it there keeps the two surfaces from disagreeing and avoids making a context capability depend on the BFF assembly that owns the `sessionListMetadata` projection. A session the list does not carry falls back to the candidate's creation time. Refs #3154
This commit is contained in:
+2
-2
@@ -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-08-27-web-at-mention-discovery-and-row-content.md
|
||||
2026-08-27-web-at-mention-discovery-and-row-content.md: bb2bdfd19a453262d6b2311e17412e4e8761606e
|
||||
2026-08-27-web-at-mention-discovery-and-row-content.zh.md: 8f45da994973c21f42f6cba1e8c8402238719260
|
||||
2026-08-27-web-at-mention-discovery-and-row-content.md: 49587a35411952ddb270e2dc2687dcc98b3bebe5
|
||||
2026-08-27-web-at-mention-discovery-and-row-content.zh.md: 0a26f5ddc2cb1469c2b69c160a134e36cd84586e
|
||||
|
||||
+3
-1
@@ -24,7 +24,7 @@ The cache is optional, and without it the previous fold path stands unchanged, i
|
||||
|
||||
**An invalidated file index keeps answering while its replacement builds.** `invalidate()` bumps a counter instead of discarding the traversal. A bare query serves the settled entries and starts a background rebuild that swaps in atomically; only a workspace's first bare query ever waits. A failed refresh leaves the stale entries and the counter behind, so the next query retries. `DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES` grows from two names to sixteen — version-control and dependency stores plus the build-output basenames of the ecosystems this harness runs in — and `DEFAULT_FILE_SEARCH_MAX_ENTRIES` rises to 50 000. Both remain `excludedDirectories` and `maxEntries` config fields a deployment overrides.
|
||||
|
||||
**Rows carry only what distinguishes them.** A file names its parent directory and nothing at the workspace root. A drilled directory listing names no parent, because its breadcrumb does. A session names its workspace only when `SessionReferenceCandidate.sameWorkspace` is false — the host computes that, since it already holds both working directories for ranking — and is dated with the relative-time bucket the session list uses, so one session reads the same age on both surfaces. `relativeTime` moves from `ui-workspace`'s `tree.ts` to `ui-primitives`; the words stay in each plugin's own dictionary, per locale-owned copy. The session id leaves the row: it is already the label a session without a title falls back to.
|
||||
**Rows carry only what distinguishes them.** A file names its parent directory and nothing at the workspace root. A drilled directory listing names no parent, because its breadcrumb does. A session names its workspace only when `SessionReferenceCandidate.sameWorkspace` is false — the host computes that, since it already holds both working directories for ranking — and is dated from the Host session list's `updatedAt` through the relative-time bucket that list uses, so one session reads the same age on both surfaces. A session the list does not carry falls back to the candidate's `createdAt`. `relativeTime` moves from `ui-workspace`'s `tree.ts` to `ui-primitives`; the words stay in each plugin's own dictionary, per locale-owned copy. The session id leaves the row: it is already the label a session without a title falls back to.
|
||||
|
||||
**A drill publishes a breadcrumb; typing a path does not.** `InputTriggerSource` gains an optional synchronous `header(session, req)` hook returning crumbs, re-polled on every hit with the live query and a pipeline-owned `drilled` flag that says whether a drill or typing produced it. `CandidateRequest` carries the same flag. Crumbs ride their own snapshot store beside the menu store, so the frozen menu reducer stays unaware of them, and a crumb pick routes through `onPick` with `action: 'drill'` — returning to a step and descending into one are one outcome. `MenuView` renders the header above its scrolling viewport and moves `role="listbox"` onto that viewport, because a breadcrumb is not an option and a listbox may not carry one.
|
||||
|
||||
@@ -40,6 +40,8 @@ The zh composer placeholder says `文件或对话`, matching the `对话` sectio
|
||||
|
||||
**Read `.gitignore` to bound the index.** Rejected for now: it adds an ignore-file parser and a git dependency to a path that must stay synchronous and cheap. A basename list covers the measured 41% and stays a config field. A workspace that keeps sources under one of those basenames must override `excludedDirectories`.
|
||||
|
||||
**Read the session's last activity on the host, from the `sessionListMetadata` projection.** Rejected: that projection key is declared by `api-session-controller`, so reading it would make a `packages/context` capability depend on the BFF assembly — a direction with no precedent in this repository. The client already holds the same number in `ctx.sessions.list`, which is also what makes the two surfaces agree by construction rather than by coincidence.
|
||||
|
||||
**Let `MenuView` recognize the `@` trigger and draw the breadcrumb itself.** Rejected: `MenuView` is shared with `/`, and hardcoding file-reference semantics there crosses the package boundary the source registry exists to hold.
|
||||
|
||||
**Add a `drilled` flag to `CandidateRequest` as optional.** Rejected: the pipeline always knows it, and an optional field invites a source to read `undefined` as "not drilled" for a request that simply predates the field. Required, with every call site updated, matches the pre-release stance.
|
||||
|
||||
+3
-1
@@ -24,7 +24,7 @@ Web e2e 看不到这一切:它的 scaffold 固定使用只含两个会话的
|
||||
|
||||
**失效的文件索引在替代品构建期间继续作答。** `invalidate()` 递增一个计数器而不是丢弃遍历。裸查询由已完成的条目作答,并启动一次后台重建、完成后原子替换;只有一个工作区的首次裸查询会等待。失败的刷新保留陈旧条目与计数器,下一次查询因此重试。`DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES` 从两个名字增至十六个——版本控制与依赖目录,加上本 harness 运行的各生态的构建产物基名——`DEFAULT_FILE_SEARCH_MAX_ENTRIES` 提高到 50 000。两者仍是部署方可覆盖的 `excludedDirectories` 与 `maxEntries` 配置字段。
|
||||
|
||||
**每一行只承载能区分它的信息。** 文件显示其父目录,位于工作区根目录时不显示。下钻后的目录列表不显示父目录,因为面包屑已经在显示。会话仅在 `SessionReferenceCandidate.sameWorkspace` 为 false 时显示其工作区——由宿主计算,因为排序时它本就同时握有两个工作目录——并使用会话列表所用的相对时间分档标注时间,因此同一个会话在两处读到的时长一致。`relativeTime` 从 `ui-workspace` 的 `tree.ts` 移到 `ui-primitives`;按 locale-owned 文案的规则,词句仍留在各插件自己的字典里。session id 离开行内:它本就是无标题会话回落到的标签。
|
||||
**每一行只承载能区分它的信息。** 文件显示其父目录,位于工作区根目录时不显示。下钻后的目录列表不显示父目录,因为面包屑已经在显示。会话仅在 `SessionReferenceCandidate.sameWorkspace` 为 false 时显示其工作区——由宿主计算,因为排序时它本就同时握有两个工作目录——并用宿主会话列表的 `updatedAt` 经该列表所用的相对时间分档标注时间,因此同一个会话在两处读到的时长一致。列表中没有的会话回落到候选自带的 `createdAt`。`relativeTime` 从 `ui-workspace` 的 `tree.ts` 移到 `ui-primitives`;按 locale-owned 文案的规则,词句仍留在各插件自己的字典里。session id 离开行内:它本就是无标题会话回落到的标签。
|
||||
|
||||
**下钻会发布面包屑,键入路径不会。** `InputTriggerSource` 增加可选的同步 `header(session, req)` 钩子返回面包屑,在每次命中时以实时查询与管线持有的 `drilled` 标记重新询问,后者说明该查询由下钻还是键入产生。`CandidateRequest` 携带同一个标记。面包屑走菜单 store 之外的独立快照 store,冻结的菜单归约器因此对它一无所知;点击面包屑经 `onPick` 以 `action: 'drill'` 路由——「回到某一步」与「进入某一层」是同一个结果。`MenuView` 把头部渲染在其滚动视口之上,并把 `role="listbox"` 移到该视口上,因为面包屑不是选项,listbox 也不得承载它。
|
||||
|
||||
@@ -40,6 +40,8 @@ Web e2e 看不到这一切:它的 scaffold 固定使用只含两个会话的
|
||||
|
||||
**读 `.gitignore` 来约束索引。** 暂时否决:这会给一条必须保持同步且廉价的路径引入 ignore 文件解析器与 git 依赖。基名列表覆盖了实测的 41%,且本就是配置字段。把源码放在其中某个基名下的工作区需覆盖 `excludedDirectories`。
|
||||
|
||||
**在宿主侧从 `sessionListMetadata` 投影读取会话最近活动时间。** 否决:该投影键由 `api-session-controller` 声明,读取它会让 `packages/context` 的能力依赖 BFF 装配层——本仓库没有这个方向的先例。客户端的 `ctx.sessions.list` 里本就有同一个数字,而这也正是让两处界面「由构造而非由巧合」保持一致的原因。
|
||||
|
||||
**让 `MenuView` 识别 `@` 触发符并自行绘制面包屑。** 否决:`MenuView` 与 `/` 共用,把文件引用语义硬编码进去,越过了 source 注册表本就用来守住的包边界。
|
||||
|
||||
**把 `drilled` 作为可选字段加进 `CandidateRequest`。** 否决:管线始终知道它,而可选字段会诱使 source 把「请求早于该字段」读成「未下钻」。改为必填并更新每一处调用点,符合预发布阶段的取舍。
|
||||
|
||||
@@ -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 docs/module-graph.md
|
||||
module-graph.md: 902a0e51e9fa0af603eb0a0588d19110d31c84c4
|
||||
module-graph.zh.md: 5a71e69a7f6bf8c9d4959afda06f9afe8cf3868e
|
||||
module-graph.md: 41b05c95f7e45c2fc044ce7bf62dfc8c7d31c45e
|
||||
module-graph.zh.md: 1fb86d8745ce909d97ada49565bf200c0eb868b2
|
||||
|
||||
@@ -1546,6 +1546,7 @@ flowchart TD
|
||||
pkg_client_ui_commands --> pkg_invariants
|
||||
pkg_client_ui_commands --> pkg_session
|
||||
pkg_client_ui_reference --> pkg_api_remotes
|
||||
pkg_client_ui_reference --> pkg_api_session_controller
|
||||
pkg_client_ui_reference --> pkg_client_connection
|
||||
pkg_client_ui_reference --> pkg_client_locale
|
||||
pkg_client_ui_reference --> pkg_client_ui_input_trigger
|
||||
@@ -1926,7 +1927,7 @@ flowchart TD
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
@@ -1548,6 +1548,7 @@ flowchart TD
|
||||
pkg_client_ui_commands --> pkg_invariants
|
||||
pkg_client_ui_commands --> pkg_session
|
||||
pkg_client_ui_reference --> pkg_api_remotes
|
||||
pkg_client_ui_reference --> pkg_api_session_controller
|
||||
pkg_client_ui_reference --> pkg_client_connection
|
||||
pkg_client_ui_reference --> pkg_client_locale
|
||||
pkg_client_ui_reference --> pkg_client_ui_input_trigger
|
||||
@@ -1928,7 +1929,7 @@ flowchart TD
|
||||
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
@@ -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-reference/README.md
|
||||
README.md: 9c2655fcac96803ba47864c2ab99ed123ca8e201
|
||||
README.zh.md: 15f61ba1a692539e87bfe551225b346b78a203e7
|
||||
README.md: 1f82f4bb30f2ad5d194a70279f9e80885d6a048e
|
||||
README.zh.md: 03a79ec4ff34431a63d8a7b0de115e3ec63b9392
|
||||
|
||||
@@ -49,7 +49,7 @@ The source keeps candidate encoding internal to the registration effect: the `/c
|
||||
|
||||
### Candidate flow
|
||||
|
||||
For an unquoted token, the browser starts the `fileReferences/list` and `sessionReferenceResolver/candidates` Remote calls together, then deterministically orders files before sessions with locale-registered folder/file/session labels. Rows render under non-selectable file and session section headings without a redundant raw `reference` source title. A session row is dated with the same relative-time bucket the session list uses, so one session reads the same age on both surfaces. A drilled query publishes a breadcrumb from the workspace root to the directory being listed; each crumb carries the drill payload a folder row would, so returning to a step and descending into one are one outcome.
|
||||
For an unquoted token, the browser starts the `fileReferences/list` and `sessionReferenceResolver/candidates` Remote calls together, then deterministically orders files before sessions with locale-registered folder/file/session labels. Rows render under non-selectable file and session section headings without a redundant raw `reference` source title. A session row is dated from the Host session list's `updatedAt` through the same relative-time bucket that list uses, so one session reads the same age on both surfaces; a session the list does not carry falls back to the candidate's creation time. A drilled query publishes a breadcrumb from the workspace root to the directory being listed; each crumb carries the drill payload a folder row would, so returning to a step and descending into one are one outcome.
|
||||
|
||||
### Serialization
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-reference"
|
||||
|
||||
### 候选流程
|
||||
|
||||
对于未加引号的 token,浏览器会同时启动 `fileReferences/list` 与 `sessionReferenceResolver/candidates` Remote 调用,再以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的文件夹、文件与会话标签。各行分别渲染在不可选择的文件与会话分组标题下,不显示重复的原始 `reference` source 标题。会话行使用与会话列表相同的相对时间分档标注时间,因此同一个会话在两处读到的时长一致。下钻后的查询会发布一条从工作区根目录到当前所列目录的面包屑;每一节携带的下钻载荷与文件夹行相同,因此「回到某一步」与「进入某一层」是同一个结果。
|
||||
对于未加引号的 token,浏览器会同时启动 `fileReferences/list` 与 `sessionReferenceResolver/candidates` Remote 调用,再以确定性顺序把文件排在会话之前,并使用注册在 locale 字典中的文件夹、文件与会话标签。各行分别渲染在不可选择的文件与会话分组标题下,不显示重复的原始 `reference` source 标题。会话行用宿主会话列表的 `updatedAt` 经该列表相同的相对时间分档标注时间,因此同一个会话在两处读到的时长一致;列表中没有的会话回落到候选自带的创建时间。下钻后的查询会发布一条从工作区根目录到当前所列目录的面包屑;每一节携带的下钻载荷与文件夹行相同,因此「回到某一步」与「进入某一层」是同一个结果。
|
||||
|
||||
### 序列化
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-api-remotes",
|
||||
"@deepseek-ai/dsh-api-session-controller",
|
||||
"@deepseek-ai/dsh-client-connection",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger"
|
||||
@@ -47,6 +48,7 @@
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
@@ -59,6 +61,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* Rows carry only what distinguishes them: a file names its parent directory
|
||||
* (nothing at the workspace root), a directory listing names none because its
|
||||
* breadcrumb already does, and a session names its workspace only when that
|
||||
* workspace is not the current one.
|
||||
* workspace is not the current one. A session is dated from the Host session
|
||||
* list, so the `@` menu and the session list never disagree about its age.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-client-ui-reference/client
|
||||
*/
|
||||
@@ -15,6 +16,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { relativeTime } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type {
|
||||
@@ -28,7 +30,7 @@ import { en, NS, zh, type ReferenceKey } from './locales.ts'
|
||||
|
||||
/** Required services: the trigger registry, the Remote namespaces, and the copy. */
|
||||
export const inject = [
|
||||
'inputTriggers', 'locale', 'connection', 'remote', 'remote.fileReferences',
|
||||
'inputTriggers', 'locale', 'connection', 'sessions', 'remote', 'remote.fileReferences',
|
||||
'remote.sessionReferenceResolver',
|
||||
]
|
||||
|
||||
@@ -40,31 +42,39 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-reference: dictionaries')
|
||||
const t = ctx.locale.bind(NS)
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const sessions = ctx.get('sessions') as ISessions
|
||||
const source: InputTriggerSource = {
|
||||
trigger: '@',
|
||||
name: 'reference',
|
||||
showGroupTitle: false,
|
||||
async candidates(session: ClientSessionContext, { query, quoted, drilled, signal }) {
|
||||
const files = ctx.remote.fileReferences.list(session.sessionId, query, signal).then(
|
||||
const fileLookup = ctx.remote.fileReferences.list(session.sessionId, query, signal).then(
|
||||
result => result.ok ? result.value : [],
|
||||
() => [],
|
||||
)
|
||||
const sessions = quoted === true
|
||||
const sessionLookup = quoted === true
|
||||
? Promise.resolve([] as SessionReferenceMentionCandidate[])
|
||||
: ctx.remote.sessionReferenceResolver.candidates(session.sessionId, query, signal).then(
|
||||
result => result.ok ? result.value : [],
|
||||
() => [],
|
||||
)
|
||||
const [fileItems, sessionItems] = await Promise.all([files, sessions])
|
||||
const [fileItems, sessionItems] = await Promise.all([fileLookup, sessionLookup])
|
||||
if (signal.aborted) return []
|
||||
// The header already names the directory being listed; rows repeat it only
|
||||
// when there is no header to carry it.
|
||||
const withLocation = crumbsFor(query, quoted === true, drilled, t) === undefined
|
||||
const now = Date.now()
|
||||
const home = connection.hostDescription.getSnapshot()?.home
|
||||
const listed = sessions.list.getSnapshot().byId
|
||||
return [
|
||||
...fileItems.flatMap(candidate => fileCandidate(candidate, quoted === true, withLocation, t)),
|
||||
...sessionItems.map(candidate => sessionCandidate(candidate, now, home, t)),
|
||||
...sessionItems.map(candidate => sessionCandidate(
|
||||
candidate,
|
||||
listed[candidate.sessionId]?.updatedAt ?? candidate.createdAt,
|
||||
now,
|
||||
home,
|
||||
t,
|
||||
)),
|
||||
]
|
||||
},
|
||||
header(_session: ClientSessionContext, req) {
|
||||
@@ -198,11 +208,12 @@ function fileCandidate(
|
||||
|
||||
function sessionCandidate(
|
||||
candidate: SessionReferenceMentionCandidate,
|
||||
updatedAt: number,
|
||||
now: number,
|
||||
home: string | undefined,
|
||||
t: Translate,
|
||||
) {
|
||||
const { unit, n } = relativeTime(candidate.createdAt, now)
|
||||
const { unit, n } = relativeTime(updatedAt, now)
|
||||
const age = unit === 'now' ? t('time.now') : t(`time.${unit}`, { n })
|
||||
// Candidates are ranked by workspace affinity, so the location only tells
|
||||
// the user something when it is not the workspace they are already in.
|
||||
|
||||
@@ -22,6 +22,8 @@ const HOME = '/Users/dev'
|
||||
const CREATED_AT = 1_700_000_000_000
|
||||
/** Three days after every fixture's createdAt, so age copy is one fixed bucket. */
|
||||
const NOW = CREATED_AT + 3 * 86_400_000
|
||||
/** The Host session list dates a row; only a session missing from it falls back to createdAt. */
|
||||
const UPDATED_AT = NOW - 3_600_000
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
@@ -71,6 +73,7 @@ async function bench(
|
||||
mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
||||
}],
|
||||
})),
|
||||
listed: Record<string, { updatedAt: number }> = {},
|
||||
): Promise<{ ctx: Context; fiber: ReturnType<Context['plugin']>; source: InputTriggerSource }> {
|
||||
const ctx = new Context()
|
||||
let source: InputTriggerSource | undefined
|
||||
@@ -90,6 +93,7 @@ async function bench(
|
||||
ctx.provide('remote.sessionReferenceResolver', { candidates: sessions })
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
ctx.provide('connection', { hostDescription: { getSnapshot: () => ({ home: HOME }) } })
|
||||
ctx.provide('sessions', { list: { getSnapshot: () => ({ byId: listed }) } })
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
if (source === undefined) throw new Error('reference source was not registered')
|
||||
@@ -99,7 +103,7 @@ async function bench(
|
||||
describe('apply', () => {
|
||||
it('declares its services and releases the @ reference registration on disposal', async () => {
|
||||
expect(inject).toEqual([
|
||||
'inputTriggers', 'locale', 'connection', 'remote', 'remote.fileReferences',
|
||||
'inputTriggers', 'locale', 'connection', 'sessions', 'remote', 'remote.fileReferences',
|
||||
'remote.sessionReferenceResolver',
|
||||
])
|
||||
const { fiber } = await bench()
|
||||
@@ -121,6 +125,7 @@ describe('apply', () => {
|
||||
ctx.provide('remote.sessionReferenceResolver', { candidates: () => Promise.resolve({ ok: true, value: [] }) })
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
ctx.provide('connection', { hostDescription: { getSnapshot: () => undefined } })
|
||||
ctx.provide('sessions', { list: { getSnapshot: () => ({ byId: {} }) } })
|
||||
const ownFiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await ownFiber.await()
|
||||
expect(registered).toMatchObject({ trigger: '@', name: 'reference', showGroupTitle: false })
|
||||
@@ -177,7 +182,7 @@ describe('candidates', () => {
|
||||
})
|
||||
}
|
||||
}))
|
||||
const { source } = await bench(files, sessions)
|
||||
const { source } = await bench(files, sessions, { source: { updatedAt: UPDATED_AT } })
|
||||
const pending = source.candidates(session, request('re'))
|
||||
expect(files).toHaveBeenCalledTimes(1)
|
||||
expect(sessions).toHaveBeenCalledTimes(1)
|
||||
@@ -199,7 +204,7 @@ describe('candidates', () => {
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Research',
|
||||
description: '~/project · 3d',
|
||||
description: '~/project · 1h',
|
||||
icon: 'session',
|
||||
section: 'Sessions',
|
||||
}),
|
||||
@@ -299,6 +304,26 @@ describe('candidates', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to the candidate createdAt for a session the Host list does not carry', async () => {
|
||||
const files = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
|
||||
const sessions = vi.fn(() => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: [{
|
||||
sessionId: sid('unlisted'),
|
||||
label: 'Unlisted run',
|
||||
cwd: `${HOME}/project`,
|
||||
sameWorkspace: true,
|
||||
createdAt: CREATED_AT,
|
||||
mention: '@[Unlisted run](dsh-session:InVubGlzdGVkIg)',
|
||||
}],
|
||||
}))
|
||||
// A row absent from the list has no durable activity time to read.
|
||||
const { source } = await bench(files, sessions, { other: { updatedAt: UPDATED_AT } })
|
||||
await expect(source.candidates(session, request('unlisted'))).resolves.toEqual([
|
||||
expect.objectContaining({ name: 'Unlisted run', description: '3d' }),
|
||||
])
|
||||
})
|
||||
|
||||
it('reads a session opened moments ago as the present, not a zero distance', async () => {
|
||||
const files = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
|
||||
const sessions = vi.fn(() => Promise.resolve({
|
||||
@@ -312,7 +337,7 @@ describe('candidates', () => {
|
||||
mention: '@[Just now](dsh-session:Imp1c3Qtbm93Ig)',
|
||||
}],
|
||||
}))
|
||||
const { source } = await bench(files, sessions)
|
||||
const { source } = await bench(files, sessions, { 'just-now': { updatedAt: NOW - 1_000 } })
|
||||
await expect(source.candidates(session, request('just'))).resolves.toEqual([
|
||||
expect.objectContaining({ name: 'Just now', description: 'now' }),
|
||||
])
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
{
|
||||
"path": "../../api/remotes/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api/session-controller/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../../context/file-reference"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -2850,6 +2850,9 @@ importers:
|
||||
'@deepseek-ai/dsh-api-remotes':
|
||||
specifier: workspace:^
|
||||
version: link:../../api/remotes
|
||||
'@deepseek-ai/dsh-api-session-controller':
|
||||
specifier: workspace:^
|
||||
version: link:../../api/session-controller
|
||||
'@deepseek-ai/dsh-client-connection':
|
||||
specifier: workspace:^
|
||||
version: link:../connection
|
||||
|
||||
Reference in New Issue
Block a user