Merge remote-tracking branch 'origin/master' into worktree/system-prompt-order-bands

This commit is contained in:
Yichen Jiang
2026-08-25 12:07:56 +08:00
57 changed files with 723 additions and 252 deletions
@@ -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/architecture/2026-08-20-client-session-conversation-ownership.md
2026-08-20-client-session-conversation-ownership.md: 8e5521ff1981d83ab72db00dea556b4b2acc97fa
2026-08-20-client-session-conversation-ownership.zh.md: a007a42b3d10ceeced8a2a64696521a96382f4e5
2026-08-20-client-session-conversation-ownership.md: 12e137209bb43d21c3437d2ce5e9d2f9180bbc77
2026-08-20-client-session-conversation-ownership.zh.md: f0d9861eeafc07481c536b85f5ba9667573a66ef
@@ -82,7 +82,7 @@ Adding a target does not add a branch to the renderer or Session Controller. The
| `client/ui-session` | Session scope, standard sources, `SessionProvider`, and pending-interaction aggregation | Session transport, Conversation assembly, Approval/Question results |
| `client/ui-workspace` | Workspace hook, browser UI, and cross-Controller navigation policy | Workspace transport, copies of Session data |
| `client/ui-conversation` | Conversation core, registries, bindings, shell, input, composer, queue, and View navigation | Session transport, Chat/Trajectory snapshots |
| `client/ui-chat` | Chat target, Node definitions, renderers, selection, details, locale, and historical images | Session lifecycle, generic View navigation, Trajectory |
| `client/ui-chat` | Chat target, Node definitions, renderers, selection, details, and locale | Session lifecycle, generic View navigation, Trajectory, historical-image cache |
| `client/ui-trajectory` | Trajectory target, event-record projection, and inspection view | Session snapshots, Chat snapshots |
| `client/ui-approval` | Pending Approval, Remote listener, composer, and approval UI | Session control, generic composer election |
| `client/ui-user-questions` | Pending Question, Remote listener, composer, and question UI | Session control, generic composer election |
@@ -296,13 +296,13 @@ Draft and input state belong to Conversation UI and do not enter the Session sna
### Chat owner
`client/ui-chat` registers target id `chat` and owns the Chat snapshot builder, Conversation Node definitions, keyed node renderers, selection, details, statistics, locale, Tool-inspection collaboration, and historical-image cache.
`client/ui-chat` registers target id `chat` and owns the Chat snapshot builder, Conversation Node definitions, keyed node renderers, selection, details, statistics, locale, and Tool-inspection collaboration.
It registers the `chat` target source through `ctx.uiSession.provide()`. `ChatNodeSeat` and internal Chat consumers use `useChat` instead of passing `useConversation(snapshot => snapshot.views.get('chat'))`.
Only visible non-command Chat Nodes activate Chat. Ordinary command-only history keeps the Hero visible; the `/goal` `command-input` Node activates a fresh Conversation.
The historical-image cache's Session key, pending promise, generation guard, blob URL, and disposer all belong to `ui-chat`; draft images remain part of Conversation input.
The historical-image cache moved to `ui-conversation` (`ctx.uiConversation.imageUrl`), so Chat and Trajectory share one authorized read and one browser URL per session attachment ([Trajectory durable image attachments](../feature/2026-08-24-trajectory-image-attachments.md)); draft images remain part of Conversation input.
### Trajectory owner
@@ -82,7 +82,7 @@ UI 层可以同时读取多个 Controller 做一次导航决定,但不得把
| `client/ui-session` | Session scope、标准 source、`SessionProvider`、pending interaction 聚合 | Session transport、Conversation 组装、Approval/Question 结果 |
| `client/ui-workspace` | Workspace hook、浏览器 UI 和跨 Controller 导航策略 | Workspace transport、Session 数据副本 |
| `client/ui-conversation` | Conversation core、registry、binding、shell、input、composer、queue 和 View 导航 | Session transport、Chat/Trajectory snapshot |
| `client/ui-chat` | Chat target、Node definitions、renderer、selection、detailslocale 和历史图片 | Session 生命周期、通用 View 导航、Trajectory |
| `client/ui-chat` | Chat target、Node definitions、renderer、selection、detailslocale | Session 生命周期、通用 View 导航、Trajectory、历史图片 cache |
| `client/ui-trajectory` | Trajectory target、事件记录投影和检查视图 | Session snapshot、Chat snapshot |
| `client/ui-approval` | Pending Approval、Remote listener、composer 和审批 UI | Session control、通用 composer election |
| `client/ui-user-questions` | Pending Question、Remote listener、composer 和问题 UI | Session control、通用 composer election |
@@ -296,13 +296,13 @@ Draft 与输入状态属于 Conversation UI,不进入 Session snapshot。Queue
### Chat owner
`client/ui-chat` 注册 target id `chat`,并拥有 Chat snapshot builder、Conversation Node definitions、keyed node renderers、selection、details、stats、localetool inspection 协作和历史图片 cache
`client/ui-chat` 注册 target id `chat`,并拥有 Chat snapshot builder、Conversation Node definitions、keyed node renderers、selection、details、stats、localetool inspection 协作。
它通过 `ctx.uiSession.provide()` 注册 `chat` target source。`ChatNodeSeat` 和 Chat 内部消费者使用 `useChat`,不再传递 `useConversation(snapshot => snapshot.views.get('chat'))`
Chat activity 只由可见且非 command 的 Chat Node 激活。普通 command-only history 保持 Hero`/goal``command-input` Node 激活 fresh Conversation。
历史图片 cache 的 Session key、pending promise、generation guard、blob URL 和 disposer 同属 `ui-chat`Draft 图片仍属于 Conversation input。
历史图片 cache 已移入 `ui-conversation``ctx.uiConversation.imageUrl`),Chat 与 Trajectory 对同一会话附件共享一次授权读取和一个浏览器 URL([Trajectory 持久化图片附件](../feature/2026-08-24-trajectory-image-attachments.zh.md)Draft 图片仍属于 Conversation input。
### Trajectory owner
@@ -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/implemented/feature/2026-08-24-trajectory-image-attachments.md
2026-08-24-trajectory-image-attachments.md: 6f89840a7d4686e45012ea2ae25f4cc939a5ca0c
2026-08-24-trajectory-image-attachments.zh.md: f1e8634639e42adc094021c03a339b8f8223bfff
@@ -0,0 +1,33 @@
# Agent Note: Trajectory durable image attachments
Status: implemented
English | [中文](2026-08-24-trajectory-image-attachments.zh.md)
## Problem
Trajectory did not display session images. A durable `{ type: 'image', attachment: ImageAttachmentRef }` block rendered as pretty-printed JSON in the details panel, and an image-only user message produced an empty ledger row. The only image path Trajectory knew was `imageSrc` sniffing over inline wire fields (`url`, `image_url`, base64 `data`), which no production event carries: every producer commits a durable `ImageAttachmentRef` before its event is appended. Users could not confirm from the execution ledger which image the model saw ([issue #2986](https://github.com/deepseek-harness/deepseek-harness/issues/2986)), while Chat already displayed the same attachments.
## Decision
- `ui-conversation` owns the per-session durable image URL cache. `HistoricalImageCache` moved from `ui-chat` into `packages/client/ui-conversation/src/client/conversation/historical-images.ts` and is served as `ctx.uiConversation.imageUrl(sessionId, attachment)`. Chat and Trajectory resolve through the same instance, so one session attachment costs one `session.attachment` read and one browser URL, revoked when the Session binding is released. This partially supersedes the `ui-chat` cache ownership recorded in [client Session/Conversation ownership](../architecture/2026-08-20-client-session-conversation-ownership.md).
- The gallery owner contract (`MessageImagesOwnerProps`, `RenderMessageImages`) moved to the `ui-conversation` client contract. `ui-chat` keeps its `conversation.message.images` SlotMap row over the shared owner type; `ui-trajectory` declares its own child slot `conversation.trajectory.images` with the same owner type; `ui-attachment` registers the one `MessageImages` gallery component into both keys, so loading, retry, and lightbox behavior is identical in both views.
- `TrajectorySourceBlock` carries `attachment?: ImageAttachmentRef` instead of `imageSrc`/`imageAlt`. The inline-source sniffing (`sourceImage`, `safeImageSource`) and the Trajectory-local `PanelImage` renderer are removed: no producer writes inline image bytes or URLs into the session log, so those paths were dead code, and the issue explicitly excludes upload-time transient paths.
- A record whose content has images but no text labels its ledger row with the locale-owned `layout.imageOnly` count; tool results with only images use the same label for their result summary instead of a JSON dump.
- Neither the storage nor the BFF changes: `session.attachment` already authorizes by session-log reference (missing, corrupt, and unreferenced attachments fail loud into the gallery's retry state), and sha256 content addressing already stores each image once.
## Alternatives considered
**Keep Trajectory's own `<img>` rendering and feed it resolved URLs.** This duplicates the loading placeholder, retry control, and lightbox that `ui-attachment` already owns, and contradicts [slot-based attachment ownership](../architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md), which rejected cross-plugin component imports.
**Lift the `conversation.message.images` declaration to a shared parent so both views render one key.** `renderSlot` is typed to the declaring entry's own children table, so a sibling `conversation.view` entry cannot render another entry's child key; the slot registry also rejects a second declaration of the same key. A second key sharing the owner type is the supported composition and lets a theme replace either gallery independently.
**Keep the inline `imageSrc` sniffing beside the durable path.** All producers (host prompt admission, `read_image`, MCP projection, ACP ingress) commit durable refs before their events append, so the sniffing matched nothing; keeping it would preserve a non-durable rendering path the acceptance criteria exclude.
**A Trajectory-owned image cache.** A second cache per view issues duplicate `session.attachment` RPCs and duplicate blob URLs for the same session attachment, violating the "Chat and Trajectory reference the same session attachment" requirement for no benefit.
## Consequences
- Both views present one gallery implementation, so image behavior (sizing, retry, lightbox, labels) cannot drift between Chat and Trajectory, and a session attachment is read once regardless of how many views show it.
- `TrajectoryTable` threads a required `renderImages` prop through its detail components; `ui-trajectory` gains a type-only dependency on `dsh-attachment`, and `ui-attachment` gains a type-only dependency on `ui-trajectory` for the new SlotMap row.
- The keyless assembled snapshot `apps/web/tests/trajectory-image-display.snapshot.ts` pins the shared-cache fact directly: the details-panel image URL is string-identical to the Chat gallery's URL for the same fixture attachment.
@@ -0,0 +1,33 @@
# Agent Note: Trajectory 持久化图片附件
Status: implemented
[English](2026-08-24-trajectory-image-attachments.md) | 中文
## Problem
Trajectory 不展示会话图片。持久化的 `{ type: 'image', attachment: ImageAttachmentRef }` 块在详情面板里渲染成格式化 JSON,纯图片的用户消息在记录表中是一个空行。Trajectory 唯一认识的图片路径是对内联 wire 字段(`url``image_url`、base64 `data`)的 `imageSrc` 嗅探,而生产事件从不携带这些字段:每个生产方都在事件追加前提交持久化的 `ImageAttachmentRef`。用户无法从执行记录确认模型看到了哪张图([issue #2986](https://github.com/deepseek-harness/deepseek-harness/issues/2986)),而 Chat 已经能展示同样的附件。
## Decision
- `ui-conversation` 拥有按会话的持久化图片 URL 缓存。`HistoricalImageCache``ui-chat` 移入 `packages/client/ui-conversation/src/client/conversation/historical-images.ts`,以 `ctx.uiConversation.imageUrl(sessionId, attachment)` 提供。Chat 与 Trajectory 通过同一实例解析,因此一个会话附件只产生一次 `session.attachment` 读取和一个浏览器 URL,并随 Session binding 释放而撤销。这部分取代了 [client Session/Conversation 所有权](../architecture/2026-08-20-client-session-conversation-ownership.zh.md)中记录的 `ui-chat` 缓存归属。
- 画廊 owner 契约(`MessageImagesOwnerProps``RenderMessageImages`)移入 `ui-conversation` 客户端契约。`ui-chat``conversation.message.images` SlotMap 行沿用共享 owner 类型;`ui-trajectory` 以同一 owner 类型声明自己的子槽位 `conversation.trajectory.images``ui-attachment` 把同一个 `MessageImages` 画廊组件注册进两个键,因此加载、重试与灯箱行为在两个视图中完全一致。
- `TrajectorySourceBlock``attachment?: ImageAttachmentRef` 取代 `imageSrc`/`imageAlt`。内联来源嗅探(`sourceImage``safeImageSource`)与 Trajectory 本地的 `PanelImage` 渲染器一并删除:没有生产方向会话日志写入内联图片字节或 URL,这些路径是死代码,且 issue 明确排除上传来源的临时路径。
- 内容含图片但没有文本的记录,其记录表行以 locale 持有的 `layout.imageOnly` 计数标注;只含图片的工具结果的摘要也使用同一标签,而不是 JSON 转储。
- 存储与 BFF 均不改动:`session.attachment` 已按会话日志引用授权(缺失、损坏与未被引用的附件显式失败并进入画廊的重试态),sha256 内容寻址已保证每张图片只存一份。
## Alternatives considered
**保留 Trajectory 自己的 `<img>` 渲染并喂给它解析好的 URL。** 这会重复 `ui-attachment` 已拥有的加载占位、重试控件和灯箱,并与[基于 slot 的附件所有权](../architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md)相抵触,该决定已拒绝跨插件直接 import 组件。
**把 `conversation.message.images` 的声明上提到共享父级,让两个视图渲染同一个键。** `renderSlot` 的类型限定在声明入口自己的 children 表内,同级的 `conversation.view` 入口无法渲染另一个入口的子键;slot registry 也拒绝对同一键的第二次声明。共享 owner 类型的第二个键是受支持的组合方式,且允许主题独立替换任一画廊。
**在持久化路径之外保留内联 `imageSrc` 嗅探。** 所有生产方(宿主 prompt admission、`read_image`、MCP 投影、ACP 入口)都在事件追加前提交持久化引用,嗅探不会命中任何东西;保留它等于保留验收标准明确排除的非持久化渲染路径。
**Trajectory 自有的图片缓存。** 每个视图一份缓存会对同一会话附件发出重复的 `session.attachment` RPC 和重复的 blob URL,违背"Chat 与 Trajectory 引用同一会话附件"的要求,且没有任何收益。
## Consequences
- 两个视图共用一个画廊实现,图片行为(尺寸、重试、灯箱、文案)不会在 Chat 与 Trajectory 之间漂移,且无论多少个视图展示,一个会话附件只读取一次。
- `TrajectoryTable` 需要把必填的 `renderImages` prop 逐层传入详情组件;`ui-trajectory` 新增对 `dsh-attachment` 的仅类型依赖,`ui-attachment` 为新的 SlotMap 行新增对 `ui-trajectory` 的仅类型依赖。
- keyless 组装快照 `apps/web/tests/trajectory-image-display.snapshot.ts` 直接钉住共享缓存这一事实:详情面板中的图片 URL 与 Chat 画廊对同一 fixture 附件的 URL 字符串相同。
@@ -0,0 +1,93 @@
// @vitest-environment jsdom
// Trajectory image surfaces over the BUILT client graph (the code-mode-fixture
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
// Opens the fixture history session whose turn 73 carries an image in BOTH a
// user message and an assistant message, and pins the Trajectory surfaces:
// selecting the ledger record renders the shared ui-attachment gallery from
// the durable session-log reference, and the browser URL is the SAME object
// URL Chat resolved — one sessions.attachment read per session attachment.
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import { expect, it, vi } from 'vitest'
import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
installAssembledBootEnv()
/** Open the fixture history session and wait for the Chat gallery to load. */
async function openFixtureSession(): Promise<void> {
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
const group = (await within(tree).findAllByText('fixture'))
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
.find(el => el?.getAttribute('aria-expanded') !== null)
if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
if (group.getAttribute('aria-expanded') === 'false') {
fireEvent.click(within(group).getByText('fixture'))
await waitFor(() => {
expect(group.getAttribute('aria-expanded')).toBe('true')
})
}
const session = await within(tree).findByText('Fixture 历史会话')
fireEvent.click(session)
await waitFor(() => {
expect(document.querySelectorAll('[data-align] img').length).toBeGreaterThan(0)
}, { timeout: 10_000 })
}
/** Scroll the virtual ledger until the row whose text contains `needle` mounts. */
async function scrollRowIntoWindow(needle: string): Promise<HTMLElement> {
await waitFor(() => {
if (document.querySelectorAll('tr[data-trajectory-row-key]').length === 0) {
throw new Error('trajectory rows not mounted')
}
}, { timeout: 10_000 })
const pane = document.querySelector('[data-trajectory-scroll] table')?.parentElement
if (!(pane instanceof HTMLElement)) throw new Error('trajectory scroll pane missing')
for (let top = 0; top <= 40_000; top += 1_000) {
pane.scrollTop = top
fireEvent.scroll(pane)
// Let the virtualizer publish the new window before probing.
await new Promise(resolve => setTimeout(resolve, 25))
const hit = [...document.querySelectorAll<HTMLElement>('tr[data-trajectory-row-key]')]
.find(row => row.textContent?.includes(needle))
if (hit !== undefined) return hit
}
throw new Error(`trajectory row containing ${JSON.stringify(needle)} never mounted`)
}
it('renders durable record images in the Trajectory details panel from the shared cache', async () => {
// The virtual ledger needs a measurable viewport; jsdom reports zero
// heights, so pin one and neutralize the imperative tail scroll.
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600)
Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
configurable: true,
value: () => {},
})
mountAssembledApp()
await openFixtureSession()
const chatSrc = document.querySelector('[data-align="end"] img')?.getAttribute('src')
if (chatSrc === null || chatSrc === undefined) throw new Error('chat gallery image missing')
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
const userRow = await scrollRowIntoWindow('历史用户图片')
fireEvent.click(userRow)
// Selecting the record opens the details panel; the ui-attachment gallery
// resolves the durable reference through the SAME per-session cache Chat
// used, so the object URL is identical — no second attachment read.
const panel = await screen.findByRole('tabpanel')
await waitFor(() => {
expect(within(panel).getAllByRole('img').length).toBeGreaterThan(0)
}, { timeout: 10_000 })
expect(within(panel).getAllByRole('img').map(img => ({
alt: img.getAttribute('alt'),
scheme: img.getAttribute('src')?.split(':')[0],
sharedWithChat: img.getAttribute('src') === chatSrc,
}))).toMatchInlineSnapshot(`
[
{
"alt": "fixture-image.png",
"scheme": "blob",
"sharedWithChat": true,
},
]
`)
})
+2 -2
View File
@@ -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: e2827ade8bb65421cd246928c02722ba5035ac9a
module-graph.zh.md: 8babd2669b8103b323e72d338e8ef4c32b5e2dbf
module-graph.md: 3fce90069441b683f449b4b16def95d8d9acc323
module-graph.zh.md: f7431bd5a417ff6c5de4efe22a1d711611b1ffee
+5 -4
View File
@@ -1455,6 +1455,7 @@ flowchart TD
pkg_client_ui_settings_general --> pkg_settings
pkg_client_ui_trajectory --> pkg_agent
pkg_client_ui_trajectory --> pkg_api_session_controller
pkg_client_ui_trajectory --> pkg_attachment
pkg_client_ui_trajectory --> pkg_client_locale
pkg_client_ui_trajectory --> pkg_client_ui_conversation
pkg_client_ui_trajectory --> pkg_client_ui_renderer
@@ -1495,7 +1496,6 @@ flowchart TD
pkg_client_ui_chat --> pkg_session_stats
pkg_client_ui_chat --> pkg_token_meter
pkg_client_ui_chat --> pkg_tools
pkg_client_ui_chat --> pkg_util_crypto
pkg_client_ui_chat --> pkg_util_workspace_path
pkg_client_ui_commands --> pkg_api_remotes
pkg_client_ui_commands --> pkg_api_session_controller
@@ -1542,6 +1542,7 @@ flowchart TD
pkg_client_ui_attachment --> pkg_client_ui_chat
pkg_client_ui_attachment --> pkg_client_ui_conversation
pkg_client_ui_attachment --> pkg_client_ui_renderer
pkg_client_ui_attachment --> pkg_client_ui_trajectory
pkg_client_ui_attachment --> pkg_invariants
pkg_client_ui_deliverables --> pkg_client_connection
pkg_client_ui_deliverables --> pkg_client_locale
@@ -1877,15 +1878,15 @@ flowchart TD
| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session) |
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
| [`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-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path) |
| [`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-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) |
| [`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) |
| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-trajectory`](../packages/client/ui-trajectory), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
+5 -4
View File
@@ -1457,6 +1457,7 @@ flowchart TD
pkg_client_ui_settings_general --> pkg_settings
pkg_client_ui_trajectory --> pkg_agent
pkg_client_ui_trajectory --> pkg_api_session_controller
pkg_client_ui_trajectory --> pkg_attachment
pkg_client_ui_trajectory --> pkg_client_locale
pkg_client_ui_trajectory --> pkg_client_ui_conversation
pkg_client_ui_trajectory --> pkg_client_ui_renderer
@@ -1497,7 +1498,6 @@ flowchart TD
pkg_client_ui_chat --> pkg_session_stats
pkg_client_ui_chat --> pkg_token_meter
pkg_client_ui_chat --> pkg_tools
pkg_client_ui_chat --> pkg_util_crypto
pkg_client_ui_chat --> pkg_util_workspace_path
pkg_client_ui_commands --> pkg_api_remotes
pkg_client_ui_commands --> pkg_api_session_controller
@@ -1544,6 +1544,7 @@ flowchart TD
pkg_client_ui_attachment --> pkg_client_ui_chat
pkg_client_ui_attachment --> pkg_client_ui_conversation
pkg_client_ui_attachment --> pkg_client_ui_renderer
pkg_client_ui_attachment --> pkg_client_ui_trajectory
pkg_client_ui_attachment --> pkg_invariants
pkg_client_ui_deliverables --> pkg_client_connection
pkg_client_ui_deliverables --> pkg_client_locale
@@ -1879,15 +1880,15 @@ flowchart TD
| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session) |
| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) |
| [`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-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path) |
| [`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-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) |
| [`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) |
| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-trajectory`](../packages/client/ui-trajectory), [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
+2 -2
View File
@@ -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/subsystems/slots.md
slots.md: d201223c9e630f16f310d8ff90318ab8c43211e5
slots.zh.md: 23277e94e2a9e85be7f1745a172dfd9cb8a5be37
slots.md: 6eb61780ca2f06ebc38a0fcf2638a7fafcd5ceee
slots.zh.md: e5a25763d382c228525f3da24694e41dd09737e0
+2 -1
View File
@@ -134,7 +134,8 @@ root
│ │ │ ├─ conversation.chat.turnTail
│ │ │ └─ tool.call.toolview
│ │ │ └─ tool.view.cordis
│ │ ─ conversation.message.images
│ │ ─ conversation.message.images
│ │ └─ conversation.trajectory.images
│ ├─ conversation.session.header
│ │ ├─ conversation.session.header.lineage
│ │ ├─ conversation.session.header.actions
+2 -1
View File
@@ -134,7 +134,8 @@ root
│ │ │ ├─ conversation.chat.turnTail
│ │ │ └─ tool.call.toolview
│ │ │ └─ tool.view.cordis
│ │ ─ conversation.message.images
│ │ ─ conversation.message.images
│ │ └─ conversation.trajectory.images
│ ├─ conversation.session.header
│ │ ├─ conversation.session.header.lineage
│ │ ├─ conversation.session.header.actions
@@ -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-attachment/README.md
README.md: 925187da23e9acec9a69959bfc29ce7ec62f0096
README.zh.md: 2872dac19e36143923ba39e4b3efbd6e8bb27cac
README.md: ee9b604d0227b5ae52f833f97f80429e4ab93bea
README.zh.md: 8d8cd43c32587f258de3e6a26fa7e0d0127dbf57
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Dynamic attachment presentation plugin for the conversation UI. It waits for the conversation package's `conversation.input.attachments` and `conversation.message.images` declarations through `ctx.slots.inject`, then registers the composer draft-image rail, document drop target, chat-history image gallery, and original-image lightbox. The conversation slot owner supplies attachment data, image loading, callbacks, and its namespace translator; presentation components remain pure props and are not exported from the package entry.
Dynamic attachment presentation plugin for the conversation UI. It waits for the `conversation.input.attachments`, `conversation.message.images`, and `conversation.trajectory.images` declarations through `ctx.slots.inject`, then registers the composer draft-image rail, document drop target, the history image gallery serving both the Chat transcript and the Trajectory inspector, and the original-image lightbox. The conversation slot owner supplies attachment data, image loading, callbacks, and its namespace translator; presentation components remain pure props and are not exported from the package entry.
## Attachment rail
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
对话 UI 的动态附件呈现插件。它通过 `ctx.slots.inject` 等待 conversation 包声明 `conversation.input.attachments``conversation.message.images`,随后注册输入框草稿图片栏、文档拖放目标、聊天历史图片画廊原图灯箱。conversation slot 持有方提供附件数据、图片加载、回调及其命名空间翻译器;呈现组件保持纯 props,且不从包入口导出。
对话 UI 的动态附件呈现插件。它通过 `ctx.slots.inject` 等待 `conversation.input.attachments``conversation.message.images``conversation.trajectory.images` 声明,随后注册输入框草稿图片栏、文档拖放目标、同时服务 Chat 会话记录与 Trajectory 检查器的历史图片画廊,以及原图灯箱。conversation slot 持有方提供附件数据、图片加载、回调及其命名空间翻译器;呈现组件保持纯 props,且不从包入口导出。
## 附件栏
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Dynamic attachment presentation plugin for conversation input and message-image slots",
"description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots",
"version": "0.1.1-rc.2",
"publishConfig": {
"access": "public"
@@ -34,7 +34,8 @@
"inject": [
"@deepseek-ai/dsh-client-ui-chat",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-renderer"
"@deepseek-ai/dsh-client-ui-renderer",
"@deepseek-ai/dsh-client-ui-trajectory"
],
"platform": "web"
}
@@ -54,6 +55,7 @@
"@types/react-dom": "~18.3.0",
"@deepseek-ai/dsh-client-ui-chat": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-trajectory": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
@@ -72,6 +74,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-client-ui-chat": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-trajectory": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-attachment": "workspace:^"
}
@@ -3,6 +3,7 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import type {} from '@deepseek-ai/dsh-client-ui-trajectory/client'
import { ComposerAttachments } from './ComposerAttachments.tsx'
import { MessageImages } from './MessageImages.tsx'
@@ -19,4 +20,8 @@ export function apply(ctx: ClientContext): void {
name: 'conversation.message.images',
locale: 'conversation',
}, MessageImages))
ctx.slots.inject('conversation.trajectory.images', () => ctx.slots.register({
name: 'conversation.trajectory.images',
locale: 'conversation',
}, MessageImages))
}
@@ -14,6 +14,7 @@ async function bench() {
children: {
'conversation.input.attachments': { kind: 'single', scope: 'session-maybe' },
'conversation.message.images': { kind: 'single', scope: 'session' },
'conversation.trajectory.images': { kind: 'single', scope: 'session' },
},
} as never, () => null)
const fiber = ctx.plugin({ inject: [...inject], apply })
@@ -26,7 +27,7 @@ describe('attachment plugin', () => {
expect(() => { applyHost() }).not.toThrow()
})
it('registers both entries and removes them with the plugin fiber', async () => {
it('registers all entries and removes them with the plugin fiber', async () => {
const { ctx, fiber } = await bench()
expect(inject).toEqual(['slots'])
expect(ctx.slots.entries('conversation.input.attachments')).toMatchObject([{
@@ -37,10 +38,15 @@ describe('attachment plugin', () => {
locale: 'conversation',
component: MessageImages,
}])
expect(ctx.slots.entries('conversation.trajectory.images')).toMatchObject([{
locale: 'conversation',
component: MessageImages,
}])
await fiber.dispose()
expect(ctx.slots.entries('conversation.input.attachments')).toHaveLength(0)
expect(ctx.slots.entries('conversation.message.images')).toHaveLength(0)
expect(ctx.slots.entries('conversation.trajectory.images')).toHaveLength(0)
})
})
@@ -23,6 +23,9 @@
{
"path": "../ui-conversation"
},
{
"path": "../ui-trajectory"
},
{
"path": "../ui-slots"
},
+2 -2
View File
@@ -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-chat/README.md
README.md: 56bb20ab7b4d9b5c0c95142b311a07ad9b8a1fd3
README.zh.md: 40ee1ee710e2f86e802ffaac4dc0bb10852f128f
README.md: 5253cb95b0e5c0b89c32646e2ae2915936d35288
README.zh.md: 8cd2d0d581a0493892aed23f42ebc0c229a0bc17
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The browser Chat target for Conversation assembly. It registers Chat event definitions and snapshot construction, supplies `useChat`, renders transcript nodes and details, and owns Chat-specific stores, actions, localization, historical images, and scroll restoration.
The browser Chat target for Conversation assembly. It registers Chat event definitions and snapshot construction, supplies `useChat`, renders transcript nodes and details, and owns Chat-specific stores, actions, localization, and scroll restoration; historical image URLs resolve through the Conversation-owned per-session cache (`ctx.uiConversation.imageUrl`).
## Model Experience
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
Conversation 组装的浏览器 Chat target。本包注册 Chat event definition 与 snapshot 构造、提供 `useChat`、渲染 transcript node 和详情,并拥有 Chat 专属 store、action、本地化、历史图片与滚动位置恢复。
Conversation 组装的浏览器 Chat target。本包注册 Chat event definition 与 snapshot 构造、提供 `useChat`、渲染 transcript node 和详情,并拥有 Chat 专属 store、action、本地化与滚动位置恢复;历史图片 URL 通过 Conversation 持有的按会话缓存(`ctx.uiConversation.imageUrl`)解析
## 模型体验
-2
View File
@@ -72,7 +72,6 @@
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-util-crypto": "workspace:^",
"@deepseek-ai/dsh-util-workspace-path": "workspace:^"
},
"devDependencies": {
@@ -101,7 +100,6 @@
"@deepseek-ai/dsh-session-stats": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-util-crypto": "workspace:^",
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@types/react": "~18.3.1",
"react": "^18.2.0"
+1 -3
View File
@@ -23,7 +23,6 @@ import { registerChatNodeRenderers } from './chat/register-node-renderers.ts'
import { StatsLine } from './chat/StatsLine.tsx'
import { registerConversationNodes } from './conversation-nodes/register.ts'
import { DetailsPanel } from './details/DetailsPanel.tsx'
import { HistoricalImageCache } from './historical-images.ts'
import { en, NS, zh } from './locale.ts'
import { createChatStore } from './stores.ts'
@@ -74,7 +73,6 @@ export function apply(ctx: Context): void {
const t = ctx.locale.bind(NS)
const chatStore = createChatStore()
const chatScrollPositions = new Map<SessionId, ChatScrollPosition>()
const images = new HistoricalImageCache(ctx)
ctx.slots.inject('conversation.view', () => {
const disposeView = ctx.slots.register({
@@ -102,7 +100,7 @@ export function apply(ctx: Context): void {
return ctx.uiWorkspace.openPath(resolveWorkspacePath(cwd, path))
},
loadOlder: () => { void session.loadOlder() },
loadImage: attachment => images.resolve(sessionId, attachment),
loadImage: attachment => ctx.uiConversation.imageUrl(sessionId, attachment),
chatScroll: {
save: (position) => {
if (position === null) chatScrollPositions.delete(sessionId)
@@ -2,9 +2,11 @@
// otherwise this view owns it. Each row subscribes to one stable node key.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import type { ConversationTimelineSnapshot } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {
ConversationTimelineSnapshot, RenderMessageImages,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import { Button, IconChevronDownOutline14, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps, RenderMessageImages } from '../contract/slots.ts'
import type { ChatViewSlotProps } from '../contract/slots.ts'
import { PendingSteeringBubble } from './MessageItem.tsx'
import { ChatNodeSeat } from './ChatNodeSeat.tsx'
import { formatRunDuration } from './message-chrome.ts'
@@ -1,9 +1,8 @@
/** Chat-owned Slot declarations and composed component props. */
import type { ReactNode } from 'react'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type {
ConversationTurnDataMap, TurnLocation,
ConversationTurnDataMap, MessageImagesOwnerProps, RenderMessageImages, TurnLocation,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {
InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, SlotHookFactory,
@@ -19,16 +18,6 @@ import type { ChatSnapshot, CommandNode, CompactionSummaryNode, ToolCallBlock }
/** Selector hook over the current Conversation binding's Chat target. */
export type UseChat = SnapshotSelectorHook<ChatSnapshot>
/** Historical image group handed to the optional attachment presentation plugin. */
export interface MessageImagesOwnerProps {
images: readonly { readonly attachment: ImageAttachmentRef }[]
loadImage: (attachment: ImageAttachmentRef) => Promise<string>
align: 'start' | 'end'
}
/** Slot-backed renderer used by Chat nodes without importing an attachment implementation. */
export type RenderMessageImages = (owner: Omit<MessageImagesOwnerProps, 'loadImage'>) => ReactNode
/** Owner currency of the completed-Turn extension chain. */
export interface TurnTailOwnerProps {
turn: TurnLocation
+1 -2
View File
@@ -28,8 +28,7 @@ export type {
AssistantActionOwnerProps, ChatFileMentions, ChatNodeOwnerProps, ChatNodeTurnDataInjected,
ChatNodeViewProps, ChatScrollPosition, ChatStore, ChatViewInjected, ChatViewSlotProps,
CommandRowOwnerProps, CommandRowProps, DetailsInjected, DetailsSlotProps,
DetailsToolOwnerProps, MessageImagesOwnerProps, MessageImagesProps, RenderMessageImages,
TurnTailOwnerProps, UseChat, UseChatNodeTurnData,
DetailsToolOwnerProps, MessageImagesProps, TurnTailOwnerProps, UseChat, UseChatNodeTurnData,
} from './contract/slots.ts'
export type { ChatKey } from './locale.ts'
export type { ConversationContext, ConversationContextOriginKind } from './model/conversation-context.ts'
@@ -5,8 +5,8 @@ import { cleanup, render } from '@testing-library/react'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import type { RenderMessageImages } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
import type { RenderMessageImages } from '../src/client/contract/slots.ts'
import { zh } from '../src/client/locale.ts'
afterEach(cleanup)
-3
View File
@@ -50,9 +50,6 @@
{
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/crypto"
},
{
"path": "../../util/workspace-path"
},
@@ -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-conversation/README.md
README.md: 08d1408bea404a80da491078aa013989835fccaf
README.zh.md: 874aea8865e6f74a64da2c249087bdad032d98d5
README.md: 8506cc9e2d2935151003ad11e34d056dda70e0ff
README.zh.md: bf6643112878f6537d3db1d1686db046202fada5
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
`ui-conversation` owns target-neutral Conversation assembly and the shared browser shell. It consumes Session Controller event feeds, exposes React-free registries and per-Session bindings through `ctx.uiConversation`, and contributes the `useConversation`, `useInput`, and `inputActions` standard props through `ctx.uiSession`. Concrete targets such as Chat are separate packages that register their own Definitions, snapshot builders, Views, and renderers.
`ui-conversation` owns target-neutral Conversation assembly and the shared browser shell. It consumes Session Controller event feeds, exposes React-free registries and per-Session bindings through `ctx.uiConversation`, and contributes the `useConversation`, `useInput`, and `inputActions` standard props through `ctx.uiSession`. It also owns the per-session durable image URL cache: `ctx.uiConversation.imageUrl(sessionId, attachment)` resolves one session-authorized browser URL per attachment and revokes it with the Session binding, so every Conversation target shares one `session.attachment` read. Concrete targets such as Chat are separate packages that register their own Definitions, snapshot builders, Views, and renderers.
## Conversation assembly
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
`ui-conversation` 拥有与 target 无关的 Conversation 组装和共享浏览器 shell。它消费 Session Controller event feed,通过 `ctx.uiConversation` 暴露不依赖 React 的 registry 与逐 Session binding,并通过 `ctx.uiSession` 提供 `useConversation``useInput``inputActions` 标准 props。Chat 等具体 target 位于独立 package,由各自 package 注册 Definition、snapshot builder、View 和 renderer。
`ui-conversation` 拥有与 target 无关的 Conversation 组装和共享浏览器 shell。它消费 Session Controller event feed,通过 `ctx.uiConversation` 暴露不依赖 React 的 registry 与逐 Session binding,并通过 `ctx.uiSession` 提供 `useConversation``useInput``inputActions` 标准 props。它还拥有按会话的持久化图片 URL 缓存:`ctx.uiConversation.imageUrl(sessionId, attachment)` 为每个附件解析一个经会话授权的浏览器 URL,并随 Session binding 释放而撤销,因此所有 Conversation target 共享一次 `session.attachment` 读取。Chat 等具体 target 位于独立 package,由各自 package 注册 Definition、snapshot builder、View 和 renderer。
## Conversation 组装
@@ -1,5 +1,6 @@
/** Target-neutral Conversation slot declarations and composed component props. */
import type { ReactNode, RefObject } from 'react'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { SessionSnapshot } from '@deepseek-ai/dsh-api-session-controller/client'
import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
import type {
@@ -43,6 +44,19 @@ export interface ComposerAttachmentsOwnerProps {
dropLimits?: { readonly count: number; readonly size: string } | undefined
}
/** Durable image group handed to the optional attachment presentation plugin. */
export interface MessageImagesOwnerProps {
/** Durable image references in source order. */
images: readonly { readonly attachment: ImageAttachmentRef }[]
/** Session-authorized image URL loader. */
loadImage: (attachment: ImageAttachmentRef) => Promise<string>
/** Horizontal placement inside the owning record. */
align: 'start' | 'end'
}
/** Slot-backed renderer used by Conversation targets without importing an attachment implementation. */
export type RenderMessageImages = (owner: Omit<MessageImagesOwnerProps, 'loadImage'>) => ReactNode
/** Selector hook over the current Session's assembled Conversation. */
export type UseConversation = SnapshotSelectorHook<ConversationSnapshot>
/** Selector hook over the registered Conversation View roster. */
@@ -1,5 +1,6 @@
/** Per-Session target-neutral Conversation assembly. */
import { Service, type Context } from '@deepseek-ai/cordis'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
ISessions, SessionBinding, SessionEventSource, SessionEventWindow,
} from '@deepseek-ai/dsh-api-session-controller/client'
@@ -15,6 +16,7 @@ import type {
import type { ConversationSnapshot } from '../contract/snapshot.ts'
import { ConversationNodeAssembler } from './assembler.ts'
import { ConversationEventRegistry } from './event-registry.ts'
import { HistoricalImageCache } from './historical-images.ts'
import { ConversationViewRegistry } from './view-registry.ts'
/** Observable faces published for one Session's Conversation assembly. */
@@ -144,6 +146,7 @@ export class UiConversation extends Service {
/** Registry of target View definitions. */
readonly views: ConversationViewRegistry
private readonly bindings = new Map<SessionId, BindingRecord>()
private readonly images: HistoricalImageCache
/**
* @param ctx - owning Client context.
@@ -153,6 +156,7 @@ export class UiConversation extends Service {
super(ctx, 'uiConversation')
this.events = new ConversationEventRegistry(ctx)
this.views = new ConversationViewRegistry(ctx)
this.images = new HistoricalImageCache(ctx, sessions)
const rebuild = (): void => {
for (const record of this.bindings.values()) record.binding.rebuild()
}
@@ -202,6 +206,17 @@ export class UiConversation extends Service {
return binding
}
/**
* Resolve one session-authorized durable image URL, cached per Session so
* every Conversation target shares one read and one browser URL.
* @param sessionId - Session authorization and lifetime scope.
* @param attachment - Durable image reference from a session event.
* @returns browser URL valid until the Session binding is released.
*/
imageUrl(sessionId: SessionId, attachment: ImageAttachmentRef): Promise<string> {
return this.images.resolve(sessionId, attachment)
}
private drop(record: BindingRecord, releaseScope: boolean): void {
if (this.bindings.get(record.source.sessionId) !== record) return
this.bindings.delete(record.source.sessionId)
@@ -1,4 +1,4 @@
/** Session-scoped historical image URL cache owned by the Chat plugin. */
/** Session-scoped durable image URL cache shared by Conversation targets. */
import type { Context } from '@deepseek-ai/cordis'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
@@ -11,9 +11,8 @@ interface ImageUrlEntry {
readonly pending: Promise<string>
}
/** Resolve durable Chat images and release their browser URLs with Session scope. */
/** Resolve durable Conversation images and release their browser URLs with Session scope. */
export class HistoricalImageCache {
private readonly sessions: ISessions
private readonly entries = new Map<string, ImageUrlEntry>()
private readonly generations = new Map<SessionId, number>()
private readonly scopeDisposers = new Map<SessionId, () => void>()
@@ -21,11 +20,11 @@ export class HistoricalImageCache {
private disposed = false
/**
* @param ctx - Owning ui-chat fiber.
* @param ctx - Owning ui-conversation fiber.
* @param sessions - Session Controller object layer.
*/
constructor(ctx: Context) {
this.sessions = ctx.sessions
ctx.effect(() => () => { this.dispose() }, 'ui-chat historical image cache')
constructor(ctx: Context, private readonly sessions: ISessions) {
ctx.effect(() => () => { this.dispose() }, 'ui-conversation historical image cache')
}
/**
@@ -35,22 +34,22 @@ export class HistoricalImageCache {
* @returns browser URL valid until the Session binding is released.
*/
resolve(sessionId: SessionId, attachment: ImageAttachmentRef): Promise<string> {
if (this.disposed) return Promise.reject(new Error('ui-chat image cache is disposed'))
if (this.disposed) return Promise.reject(new Error('ui-conversation image cache is disposed'))
const key = `${sessionId}:${attachment.attachmentId}`
const cached = this.entries.get(key)
if (cached !== undefined) return cached.pending
const binding = this.sessions.binding(sessionId)
if (binding === undefined) {
return Promise.reject(new Error(`ui-chat: unknown session "${sessionId}"`))
return Promise.reject(new Error(`ui-conversation: unknown session "${sessionId}"`))
}
this.bindScope(sessionId, binding.ctx)
const generation = this.generations.get(sessionId) ?? 0
const pending = binding.session.readAttachment(attachment.attachmentId)
.then((result) => {
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`)
if (this.disposed) throw new Error('ui-chat image cache was disposed before loading completed')
if (this.disposed) throw new Error('ui-conversation image cache was disposed before loading completed')
if ((this.generations.get(sessionId) ?? 0) !== generation) {
throw new Error('ui-chat image scope was released before loading completed')
throw new Error('ui-conversation image scope was released before loading completed')
}
if (typeof URL.createObjectURL !== 'function') {
return `data:${result.value.attachment.mediaType};base64,${bytesToBase64(result.value.data)}`
@@ -73,7 +72,7 @@ export class HistoricalImageCache {
const dispose = scope.effect(() => () => {
this.scopeDisposers.delete(sessionId)
this.release(sessionId)
}, 'ui-chat historical image scope')
}, 'ui-conversation historical image scope')
this.scopeDisposers.set(sessionId, () => { void dispose() })
}
@@ -51,7 +51,7 @@ export type {
ConversationSessionInjected, ConversationSessionSlotProps, ConversationSlotProps,
ConversationStore, ConvViewOwnerProps, ConvViewProps, EmptyWorkspaceOwnerProps,
HeroAgentPresetOwnerProps, HeroBrandMarkOwnerProps, InputControlOwnerProps, InputZone,
UseConversation, UseConversationViews,
MessageImagesOwnerProps, RenderMessageImages, UseConversation, UseConversationViews,
} from './contract/slots.ts'
export type {
ArbitrateKey, ArbitrateOutcome, BeginCommandRequest, CommandClaim, ConsumeTokenRequest,
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client'
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
import { HistoricalImageCache } from '../src/client/historical-images.ts'
import { HistoricalImageCache } from '../src/client/conversation/historical-images.ts'
describe('HistoricalImageCache', () => {
it('invalidates a pending image load when its Session binding is released', async () => {
@@ -13,7 +13,7 @@ describe('HistoricalImageCache', () => {
id: 's1',
session: { readAttachment: () => read.promise },
})
const cache = new HistoricalImageCache(runtime.ctx)
const cache = new HistoricalImageCache(runtime.ctx, runtime.ctx.sessions)
const attachment = {
attachmentId: AttachmentId('image-1'), mediaType: 'image/png', bytes: 1, width: 1, height: 1,
} as const
@@ -22,7 +22,7 @@ describe('HistoricalImageCache', () => {
await runtime.sessions.remove(sessionId)
read.resolve({ ok: true, value: { attachment, data: Uint8Array.of(1) } })
await expect(pending).rejects.toThrow('ui-chat image scope was released before loading completed')
await expect(pending).rejects.toThrow('ui-conversation image scope was released before loading completed')
await runtime.dispose()
})
})
@@ -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-trajectory/README.md
README.md: 97badd562bbf132c9766c763ff604306d1a6c08d
README.zh.md: 5a17ee811047e8ffd15be849595d87adfe4ddf00
README.md: c623105b9bb84edbd8ff6a91244271f6fc92d943
README.zh.md: 007f4451be9ac2f19c7fdd8e6eefe410813c7eb7
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Trajectory renders a turn-aware event ledger with selectable User, Assistant, Tool, and nested Subtool records. Thick rules mark Turn boundaries, compact inline markers identify Steps, and the main ledger keeps only index, event, and content; selection opens a local inspector for token usage, duration, Input, Output, and Timing. Scrollable Summary regions keep their scrollbar thumbs transparent until the region is hovered or contains keyboard focus, without changing the reserved scroll geometry. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn. Long ledgers open at the current tail, load one older page when the user reaches the loaded range's top, and mount only the visible row window plus a small overscan; request-only separators share the next measurable virtual item, while semantic row keys and ARIA indexes survive prepends. Selection, timeline navigation, folding, search, and Request totals cover the currently loaded window. The ledger covers records with an explicit loading row until the initial tail is positioned. While an older prefix remains unloaded, a first-row control precedes the loaded records, loads one earlier page on click, and changes in place to a disabled loading status while that page is pending. A fixed Overview above the ledger projects real record start/duration timing from left to right; when earlier records remain unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control identifies the omitted prefix and loads one earlier page without assigning unknown history fabricated duration. Assistant spans divide recorded TTFT from decoding, and a 500 ms hover reveals exact clock and duration details. Dragging an interval focuses the ledger on every record active at any point in that inclusive range, while clearing the selection restores the full loaded ledger. Wheel gestures zoom the time domain. A right-button click clears the selected interval, while a right-button drag pans an already zoomed viewport without changing it. The initial view and streaming updates stay at the tail; scrolling upward suspends following so new records do not interrupt inspection of earlier rows. Content-only stream frames preserve virtual row keys and heights, reuse measurements, and do not issue repeated tail-scroll writes. Completed replies retain assembled blocks, timing, and usage in Trajectory target State, while the shared Session window keeps the raw Events. Trajectory asks the conversation shell to float the composer over the full-height ledger, while its responsive vertical scrollers reserve the composer's live height so final rows remain reachable. Trajectory-owned Definitions assemble business records, including durable cancellation-finalized prefixes, chunk-only interruption fallbacks, and interrupted Tool records, from the shared Session window, so Trajectory neither reads nor changes the Chat conversation snapshot. The package provides no service and declares no Context merge; it registers target-specific Event Definitions, a Trajectory view builder, and one tab in the conversation's `'conversation.view'` slot ring. Its typed `trajectory` locale namespace owns every product-authored ledger, timeline, inspector, tooltip, and accessibility phrase; event content, tool names, identifiers, and provider diagnostics remain verbatim data.
Trajectory renders a turn-aware event ledger with selectable User, Assistant, Tool, and nested Subtool records. Thick rules mark Turn boundaries, compact inline markers identify Steps, and the main ledger keeps only index, event, and content; selection opens a local inspector for token usage, duration, Input, Output, and Timing. Durable image attachments in user input, assistant output, and tool results render through the `conversation.trajectory.images` gallery slot: a record without text labels its row with the image count, the inspector shows each image with the shared loading, retry, and lightbox behavior, and image URLs come from the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. Scrollable Summary regions keep their scrollbar thumbs transparent until the region is hovered or contains keyboard focus, without changing the reserved scroll geometry. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn. Long ledgers open at the current tail, load one older page when the user reaches the loaded range's top, and mount only the visible row window plus a small overscan; request-only separators share the next measurable virtual item, while semantic row keys and ARIA indexes survive prepends. Selection, timeline navigation, folding, search, and Request totals cover the currently loaded window. The ledger covers records with an explicit loading row until the initial tail is positioned. While an older prefix remains unloaded, a first-row control precedes the loaded records, loads one earlier page on click, and changes in place to a disabled loading status while that page is pending. A fixed Overview above the ledger projects real record start/duration timing from left to right; when earlier records remain unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control identifies the omitted prefix and loads one earlier page without assigning unknown history fabricated duration. Assistant spans divide recorded TTFT from decoding, and a 500 ms hover reveals exact clock and duration details. Dragging an interval focuses the ledger on every record active at any point in that inclusive range, while clearing the selection restores the full loaded ledger. Wheel gestures zoom the time domain. A right-button click clears the selected interval, while a right-button drag pans an already zoomed viewport without changing it. The initial view and streaming updates stay at the tail; scrolling upward suspends following so new records do not interrupt inspection of earlier rows. Content-only stream frames preserve virtual row keys and heights, reuse measurements, and do not issue repeated tail-scroll writes. Completed replies retain assembled blocks, timing, and usage in Trajectory target State, while the shared Session window keeps the raw Events. Trajectory asks the conversation shell to float the composer over the full-height ledger, while its responsive vertical scrollers reserve the composer's live height so final rows remain reachable. Trajectory-owned Definitions assemble business records, including durable cancellation-finalized prefixes, chunk-only interruption fallbacks, and interrupted Tool records, from the shared Session window, so Trajectory neither reads nor changes the Chat conversation snapshot. The package provides no service and declares no Context merge; it registers target-specific Event Definitions, a Trajectory view builder, and one tab in the conversation's `'conversation.view'` slot ring. Its typed `trajectory` locale namespace owns every product-authored ledger, timeline, inspector, tooltip, and accessibility phrase; event content, tool names, identifiers, and provider diagnostics remain verbatim data.
## Model Experience
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。可滚动的概述区域默认保持滚动条滑块透明,直到鼠标悬停该区域或其中包含键盘焦点时才显示,同时不改变滚动条预留的几何空间。独立运行的压缩(compaction)请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。长记录表打开时定位于当前尾部,用户到达已加载范围顶部时加载一页更早的历史,并且只挂载可见行窗口和少量额外缓冲行;仅含请求的分隔行并入下一个具备可测高度的虚拟项,语义行键和 ARIA 索引在向前补页后保持不变。选择、时间线导航、折叠、搜索和请求汇总只覆盖当前已加载的窗口。初始尾部完成定位前,记录表会用明确的加载行遮住真实记录。更早的前缀仍未加载时,已加载记录前会始终保留首行控件;单击它会加载一页更早的历史,页面加载期间则会原地变为禁用的加载状态。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;仍有更早记录未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会标识被省略的前缀,并可加载一页更早历史,而不会为未知部分虚构耗时。助手时间条会区分记录到的 TTFT 与解码时间,悬停 500 ms 后可查看精确时刻和耗时详情。拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整的已加载记录表。滚轮手势用于缩放时间域。右键单击会清除所选区间;在已放大的 viewport 上按住右键拖动则只会平移视图,不会改变该区间。初始视图和流式更新都会停留在尾部;向上滚动会暂停跟随,因此新记录不会打断对旧记录的检查。仅含内容更新的流式帧会保持虚拟行的键和高度不变、复用测量结果,并且不会重复写入末尾滚动位置。已完成的回复会在 Trajectory target State 中保留组装后的 blocks、计时与用量,共享 Session 窗口则保留原始 Event。Trajectory 要求会话壳将 composer 作为浮层置于全高记录表上方;其响应式纵向滚动容器会预留 composer 的实时高度,确保仍可滚动到最后几行。Trajectory 自有的 Definition 从共享 Session 窗口组装业务记录,其中包括持久化的取消定稿前缀、只能从分片恢复的打断前缀和被打断的工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包不提供 service,也不声明 Context 合并;它会注册 target 专属 Event Definition、Trajectory view builder,以及会话 `'conversation.view'` slot 环中的一个视图标签页。其 typed `trajectory` locale namespace 持有 ledger、时间线、检查器、tooltip 与无障碍短语中的全部产品编写文案;事件内容、工具名称、标识符与提供方诊断仍作为数据原样呈现。
Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。用户输入、助手输出和工具结果中的持久化图片附件通过 `conversation.trajectory.images` 画廊 slot 渲染:没有文本的记录行以图片数量标注,检查器内展示每张图片并复用共享的加载、重试与灯箱行为,图片 URL 来自 Conversation 持有的按会话缓存,因此 Chat 与 Trajectory 对同一附件共享一次经会话授权的读取。可滚动的概述区域默认保持滚动条滑块透明,直到鼠标悬停该区域或其中包含键盘焦点时才显示,同时不改变滚动条预留的几何空间。独立运行的压缩(compaction)请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。长记录表打开时定位于当前尾部,用户到达已加载范围顶部时加载一页更早的历史,并且只挂载可见行窗口和少量额外缓冲行;仅含请求的分隔行并入下一个具备可测高度的虚拟项,语义行键和 ARIA 索引在向前补页后保持不变。选择、时间线导航、折叠、搜索和请求汇总只覆盖当前已加载的窗口。初始尾部完成定位前,记录表会用明确的加载行遮住真实记录。更早的前缀仍未加载时,已加载记录前会始终保留首行控件;单击它会加载一页更早的历史,页面加载期间则会原地变为禁用的加载状态。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;仍有更早记录未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会标识被省略的前缀,并可加载一页更早历史,而不会为未知部分虚构耗时。助手时间条会区分记录到的 TTFT 与解码时间,悬停 500 ms 后可查看精确时刻和耗时详情。拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整的已加载记录表。滚轮手势用于缩放时间域。右键单击会清除所选区间;在已放大的 viewport 上按住右键拖动则只会平移视图,不会改变该区间。初始视图和流式更新都会停留在尾部;向上滚动会暂停跟随,因此新记录不会打断对旧记录的检查。仅含内容更新的流式帧会保持虚拟行的键和高度不变、复用测量结果,并且不会重复写入末尾滚动位置。已完成的回复会在 Trajectory target State 中保留组装后的 blocks、计时与用量,共享 Session 窗口则保留原始 Event。Trajectory 要求会话壳将 composer 作为浮层置于全高记录表上方;其响应式纵向滚动容器会预留 composer 的实时高度,确保仍可滚动到最后几行。Trajectory 自有的 Definition 从共享 Session 窗口组装业务记录,其中包括持久化的取消定稿前缀、只能从分片恢复的打断前缀和被打断的工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包不提供 service,也不声明 Context 合并;它会注册 target 专属 Event Definition、Trajectory view builder,以及会话 `'conversation.view'` slot 环中的一个视图标签页。其 typed `trajectory` locale namespace 持有 ledger、时间线、检查器、tooltip 与无障碍短语中的全部产品编写文案;事件内容、工具名称、标识符与提供方诊断仍作为数据原样呈现。
## 模型体验
+4 -2
View File
@@ -62,7 +62,8 @@
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^"
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-attachment": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -85,7 +86,8 @@
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^"
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-attachment": "workspace:^"
},
"files": [
"lib/index.js",
@@ -1545,35 +1545,6 @@
white-space: pre-wrap;
}
.panelImageLink {
display: block;
width: auto;
max-width: 100%;
overflow: hidden;
border-radius: 4px;
cursor: zoom-in;
}
.panelImageLinkPreview {
max-height: 140px;
}
.panelImage {
display: block;
width: auto;
max-width: 100%;
height: auto;
max-height: 320px;
margin: 0;
border-radius: inherit;
background: var(--dsw-alias-bg-base);
object-fit: contain;
}
.panelImageLinkPreview .panelImage {
max-height: 140px;
}
.messageImages {
display: flex;
flex-direction: column;
@@ -14,8 +14,9 @@ import {
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { JsonTreeLabels, MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives'
import { structuredPatch } from 'diff'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
AssistantRequestConfig, ConversationPromptSnapshot,
AssistantRequestConfig, ConversationPromptSnapshot, RenderMessageImages,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {
AssistantMetricDetail, TrajectoryCellKind, TrajectoryCellProps, TrajectorySourceBlock,
@@ -377,6 +378,8 @@ function AssistantTimingPanel({
export interface TrajectoryTableProps {
/** Trajectory locale seat. */
t: TrajectoryTranslate
/** Slot-backed durable image renderer shared with the Chat gallery. */
renderImages: RenderMessageImages
/** Session-global request numbers for the request groups visible in this context. */
requestNumbers?: readonly TrajectoryRequestNumber[]
/** Grouped records in display order. */
@@ -1121,10 +1124,12 @@ function MarkdownFragment({
function SourceBlocks({
blocks,
onOpenCall,
renderImages,
t,
}: {
blocks: readonly TrajectorySourceBlock[]
onOpenCall: (callId: string) => void
renderImages: RenderMessageImages
t: TrajectoryTranslate
}) {
return (
@@ -1155,8 +1160,10 @@ function SourceBlocks({
</span>
</div>
)}
{block.imageSrc !== undefined
? <PanelImage block={block} t={t} />
{/* The Raw view keeps model block order and granularity: one
gallery per image block, unlike the aggregated record gallery. */}
{block.attachment !== undefined
? renderImages({ images: [{ attachment: block.attachment }], align: 'start' })
: <pre className={css.sourceBlockContent}>{block.content}</pre>}
</section>
))}
@@ -1164,47 +1171,27 @@ function SourceBlocks({
)
}
function PanelImage({
block,
preview = false,
t,
}: {
block: TrajectorySourceBlock
preview?: boolean
t: TrajectoryTranslate
}) {
if (block.imageSrc === undefined) return null
return (
<a
className={preview ? `${css.panelImageLink} ${css.panelImageLinkPreview}` : css.panelImageLink}
href={block.imageSrc}
target="_blank"
rel="noopener noreferrer"
title={t('block.openImage')}
>
<img
className={css.panelImage}
src={block.imageSrc}
alt={block.imageAlt ?? ''}
/>
</a>
)
function recordImages(
blocks: readonly TrajectorySourceBlock[] | undefined,
): { readonly attachment: ImageAttachmentRef }[] {
return (blocks ?? []).flatMap(block =>
block.attachment !== undefined ? [{ attachment: block.attachment }] : [])
}
function MessageImages({
blocks,
preview,
t,
renderImages,
}: {
blocks: readonly TrajectorySourceBlock[] | undefined
preview: boolean
t: TrajectoryTranslate
renderImages: RenderMessageImages
}) {
const images = blocks?.filter(block => block.imageSrc !== undefined) ?? []
const images = recordImages(blocks)
if (images.length === 0) return null
return (
<div className={preview ? `${css.messageImages} ${css.messageImagesPreview}` : css.messageImages}>
{images.map((block, index) => <PanelImage block={block} preview={preview} t={t} key={index} />)}
{renderImages({ images, align: 'start' })}
</div>
)
}
@@ -1403,13 +1390,16 @@ function SystemPromptDiff({
function ToolOutputBlocks({
blocks,
error,
errorDetail,
preview,
t,
renderImages,
}: {
blocks: readonly TrajectorySourceBlock[]
error: boolean
/** Failure name and code preserved beside image-only error content. */
errorDetail?: string | undefined
preview: boolean
t: TrajectoryTranslate
renderImages: RenderMessageImages
}) {
return (
<div className={[
@@ -1418,9 +1408,15 @@ function ToolOutputBlocks({
error ? css.errorPayload : undefined,
].filter((value): value is string => value !== undefined).join(' ')}
>
{error && errorDetail !== undefined && errorDetail !== ''
&& <pre className={css.resultBlockText}>{errorDetail}</pre>}
{blocks.map((block, index) => (
block.imageSrc !== undefined
? <PanelImage block={block} preview={preview} t={t} key={index} />
block.attachment !== undefined
? (
<div className={css.messageImages} key={index}>
{renderImages({ images: [{ attachment: block.attachment }], align: 'start' })}
</div>
)
: block.content !== ''
? <pre className={css.resultBlockText} key={index}>{block.content}</pre>
: null
@@ -1436,6 +1432,7 @@ function MarkdownRecordContent({
thinkingExpanded,
onThinkingExpandedChange,
onOpenCall,
renderImages,
t,
}: {
record: TableRecord
@@ -1444,10 +1441,18 @@ function MarkdownRecordContent({
thinkingExpanded: boolean
onThinkingExpandedChange: (expanded: boolean) => void
onOpenCall: (callId: string) => void
renderImages: RenderMessageImages
t: TrajectoryTranslate
}) {
if (!rendered && record.cell.sourceBlocks && record.cell.sourceBlocks.length > 0) {
return <SourceBlocks blocks={record.cell.sourceBlocks} onOpenCall={onOpenCall} t={t} />
return (
<SourceBlocks
blocks={record.cell.sourceBlocks}
onOpenCall={onOpenCall}
renderImages={renderImages}
t={t}
/>
)
}
if (record.cell.thinkingDetail) {
if (!rendered) {
@@ -1502,13 +1507,13 @@ function MarkdownRecordContent({
<MessageImages
blocks={record.cell.sourceBlocks}
preview={preview}
t={t}
renderImages={renderImages}
/>
</div>
)
}
const source = markdownSource(record)
const hasImages = record.cell.sourceBlocks?.some(block => block.imageSrc !== undefined) === true
const hasImages = record.cell.sourceBlocks?.some(block => block.attachment !== undefined) === true
const hasToolCalls = record.cell.kind === 'message'
&& record.cell.sourceBlocks?.some(block => block.type === 'tool-call') === true
if (!source && !hasImages && !hasToolCalls) {
@@ -1531,7 +1536,7 @@ function MarkdownRecordContent({
t={t}
/>
)}
<MessageImages blocks={record.cell.sourceBlocks} preview={preview} t={t} />
<MessageImages blocks={record.cell.sourceBlocks} preview={preview} renderImages={renderImages} />
</div>
)
}
@@ -1590,11 +1595,13 @@ function RecordPayload({
record,
direction,
preview = false,
renderImages,
t,
}: {
record: TableRecord
direction: 'input' | 'output'
preview?: boolean
renderImages: RenderMessageImages
t: TrajectoryTranslate
}) {
const value = direction === 'input' ? record.cell.inputDetail : record.cell.outputDetail
@@ -1624,14 +1631,15 @@ function RecordPayload({
if (
direction === 'output'
&& record.cell.outputBlocks?.some(block =>
block.imageSrc !== undefined || block.content !== '') === true
block.attachment !== undefined || block.content !== '') === true
) {
return (
<ToolOutputBlocks
blocks={record.cell.outputBlocks}
error={error}
errorDetail={error ? value : undefined}
preview={preview}
t={t}
renderImages={renderImages}
/>
)
}
@@ -1791,6 +1799,7 @@ function OverviewSection({
*/
export function TrajectoryTable({
t,
renderImages,
requestNumbers: sessionRequestNumbers,
turns,
streamingCells = [],
@@ -2990,6 +2999,7 @@ export function TrajectoryTable({
>
<MarkdownRecordContent
record={selected}
renderImages={renderImages}
rendered
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
@@ -3103,6 +3113,7 @@ export function TrajectoryTable({
<OverviewSection label={t('tab.preview')} onOpen={() => { activateTab('rendered') }}>
<MarkdownRecordContent
record={selected}
renderImages={renderImages}
rendered
preview
thinkingExpanded={thinkingExpanded}
@@ -3117,12 +3128,12 @@ export function TrajectoryTable({
<>
{selected.cell.inputDetail && (
<OverviewSection label={t('tab.payload')} onOpen={() => { activateTab('input') }}>
<RecordPayload record={selected} direction="input" preview t={t} />
<RecordPayload record={selected} direction="input" preview renderImages={renderImages} t={t} />
</OverviewSection>
)}
{selected.cell.outputDetail && (
<OverviewSection label={t('tab.result')} onOpen={() => { activateTab('output') }}>
<RecordPayload record={selected} direction="output" preview t={t} />
<RecordPayload record={selected} direction="output" preview renderImages={renderImages} t={t} />
</OverviewSection>
)}
<OverviewSection label={t('tab.schema')} onOpen={() => { activateTab('schema') }}>
@@ -3151,6 +3162,7 @@ export function TrajectoryTable({
{!promptSelected && selected !== undefined && activeTab === 'rendered' && (
<MarkdownRecordContent
record={selected}
renderImages={renderImages}
rendered
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
@@ -3161,6 +3173,7 @@ export function TrajectoryTable({
{!promptSelected && selected !== undefined && activeTab === 'raw' && (
<MarkdownRecordContent
record={selected}
renderImages={renderImages}
rendered={false}
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
@@ -3172,10 +3185,10 @@ export function TrajectoryTable({
<MessageSource record={selected} t={t} />
)}
{!promptSelected && selected !== undefined && activeTab === 'input' && (
<RecordPayload record={selected} direction="input" t={t} />
<RecordPayload record={selected} direction="input" renderImages={renderImages} t={t} />
)}
{!promptSelected && selected !== undefined && activeTab === 'output' && (
<RecordPayload record={selected} direction="output" t={t} />
<RecordPayload record={selected} direction="output" renderImages={renderImages} t={t} />
)}
{!promptSelected && selected !== undefined && activeTab === 'schema' && (
<RecordSchema record={selected} t={t} />
@@ -1,10 +1,11 @@
/** Trajectory view: compact summary over a turn-aware event ledger. */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
AssistantBlock, AssistantMessageNode, ConvViewProps,
AssistantBlock, AssistantMessageNode, ConvViewProps, RenderMessageImages,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { InjectFace, PropsLocale, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
import {
TrajectoryTable,
@@ -69,6 +70,7 @@ export interface TrajectoryViewInjected {
duration: SnapshotStore<boolean>
}
loadOlder: () => Promise<boolean>
loadImage: (attachment: ImageAttachmentRef) => Promise<string>
setActualDuration: (actualDuration: boolean) => void
}
@@ -117,10 +119,17 @@ function addUsage(
}
export function TrajectoryView({
useSession, useTrajectory, useDuration, loadOlder, setActualDuration,
viewRequest, completeViewRequest, t,
}: ConvViewProps & InjectFace<TrajectoryViewInjected> & PropsLocale<'trajectory'>) {
useSession, useTrajectory, useDuration, loadOlder, loadImage, setActualDuration,
viewRequest, completeViewRequest, renderSlot, t,
}: ConvViewProps
& PropsRenderSlots<'conversation.trajectory.images'>
& InjectFace<TrajectoryViewInjected>
& PropsLocale<'trajectory'>) {
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(EMPTY_TURN_IDS)
const renderImages = useCallback<RenderMessageImages>(
owner => renderSlot('conversation.trajectory.images', { ...owner, loadImage }),
[loadImage, renderSlot],
)
const [collapsedAssistants, setCollapsedAssistants] =
useState<ReadonlySet<string>>(EMPTY_RECORD_IDS)
const [timelineSelection, setTimelineSelection] = useState<TrajectoryTimeRange | null>(null)
@@ -481,6 +490,7 @@ export function TrajectoryView({
<div className={css.ledger}>
<TrajectoryTable
t={t}
renderImages={renderImages}
requestNumbers={requestNumbers}
turns={timelineTurns}
streamingCells={streamingCells}
@@ -79,6 +79,9 @@ export function apply(ctx: Context): void {
order: 10,
locale: NS,
label: () => t('view.trajectory'),
children: {
'conversation.trajectory.images': { kind: 'single', scope: 'session' },
},
inject: (sessionId: SessionId): TrajectoryViewInjected => {
const session = ctx.sessions.binding(sessionId)?.session
if (session === undefined) {
@@ -92,6 +95,7 @@ export function apply(ctx: Context): void {
await session.loadOlder()
return trajectory.getSnapshot() !== before
},
loadImage: attachment => ctx.uiConversation.imageUrl(sessionId, attachment),
setActualDuration: (value) => { duration.set(value) },
}
},
@@ -12,6 +12,7 @@ import type {
ToolCallBlock,
ToolResultNode,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type {
TrajectoryCellProps,
TrajectorySourceBlock,
@@ -108,7 +109,7 @@ function layoutEntryOrder(entry: OrderedLayoutEntry): number {
: entry.seq
}
function inputCellDetail(node: InputNode): Pick<
function inputCellDetail(node: InputNode, t: TrajectoryTranslate): Pick<
TrajectoryCellProps,
| 'text'
| 'previewMarkdown'
@@ -119,9 +120,15 @@ function inputCellDetail(node: InputNode): Pick<
| 'timeSeconds'
| 'startedAt'
> {
const previewMarkdown = previewContent(node.content)
// An empty text block yields an empty preview; treat it as absent so an
// image-bearing record still labels its row instead of rendering blank.
const preview = previewContent(node.content)
const previewMarkdown = preview === '' ? undefined : preview
const images = imageBlockCount(node.content)
return {
text: '',
text: previewMarkdown === undefined && images > 0
? t('layout.imageOnly', { count: images })
: '',
...(previewMarkdown === undefined ? {} : { previewMarkdown }),
sourceSeq: node.seq,
messageSource: node.source,
@@ -368,7 +375,7 @@ export function deriveTrajectoryLayout(
cell: {
index: ++index,
kind: 'user',
...inputCellDetail(node),
...inputCellDetail(node, t),
opensTurn: true,
},
})
@@ -387,7 +394,7 @@ export function deriveTrajectoryLayout(
cell: {
index: ++index,
kind: 'user' as const,
...inputCellDetail(node),
...inputCellDetail(node, t),
},
}
if (placement.step === undefined) pushMessage(placement.turn, laid)
@@ -415,7 +422,7 @@ export function deriveTrajectoryLayout(
cell: {
index: ++index,
kind: 'context',
...inputCellDetail(node),
...inputCellDetail(node, t),
},
})
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
@@ -788,6 +795,8 @@ function summarizeAssistantActivity(
if (tools.size > 0) {
return t('layout.toolCallOnly')
}
const images = blocks.filter(block => block.kind === 'image').length
if (images > 0) return t('layout.imageOnly', { count: images })
return ''
}
@@ -808,12 +817,7 @@ function assistantSourceBlock(block: AssistantBlock): TrajectorySourceBlock {
callId: block.callId,
toolName: block.name,
}
// Attachment refs carry no fetchable bytes, so the record shows the
// durable metadata instead of an inline preview.
case 'image': return {
type: 'image',
content: stringifySourceValue(block.attachment),
}
case 'image': return { type: 'image', content: '', attachment: block.attachment }
case 'other': return sourceBlock(block.block)
}
}
@@ -827,47 +831,21 @@ function sourceBlock(value: unknown): TrajectorySourceBlock {
if (typeof block.text === 'string') {
return { type: type === 'reasoning' ? 'thinking' : type, content: block.text }
}
const imageSrc = sourceImage(block)
const imageAlt = typeof block.alt === 'string' ? block.alt : undefined
return {
type,
content: imageSrc === undefined ? stringifySourceValue(value) : '',
...(imageSrc !== undefined ? { imageSrc } : {}),
...(imageAlt !== undefined ? { imageAlt } : {}),
if (
type === 'image'
&& typeof block.attachment === 'object' && block.attachment !== null
&& typeof (block.attachment as Record<string, unknown>).attachmentId === 'string'
) {
// Session-log content is validated into core ContentBlocks by the
// Conversation node assembly; the `attachmentId` guard only keeps
// wire-shaped 'other' blocks with an unrelated `attachment` member out.
return { type, content: '', attachment: block.attachment as ImageAttachmentRef }
}
return { type, content: stringifySourceValue(value) }
}
function sourceImage(block: Record<string, unknown>): string | undefined {
if (typeof block.type !== 'string' || !block.type.toLowerCase().includes('image')) return undefined
for (const candidate of [block.url, block.image_url]) {
if (typeof candidate === 'string') return safeImageSource(candidate)
}
if (typeof block.data === 'string') {
const mediaType = [block.mimeType, block.mediaType, block.media_type]
.find((candidate): candidate is string => typeof candidate === 'string')
?? 'image/png'
return safeImageSource(
block.data.startsWith('data:')
? block.data
: `data:${mediaType};base64,${block.data}`,
)
}
if (typeof block.source !== 'object' || block.source === null) return undefined
const source = block.source as Record<string, unknown>
if (typeof source.url === 'string') return safeImageSource(source.url)
if (typeof source.data !== 'string') return undefined
const mediaType = typeof source.media_type === 'string' ? source.media_type : 'image/png'
return safeImageSource(`data:${mediaType};base64,${source.data}`)
}
function safeImageSource(value: string): string | undefined {
if (value.startsWith('data:image/') || value.startsWith('blob:')) return value
try {
const protocol = new URL(value).protocol
return protocol === 'http:' || protocol === 'https:' ? value : undefined
} catch {
return undefined
}
function imageBlockCount(content: readonly { type: string }[]): number {
return content.filter(block => block.type === 'image').length
}
function stringifySourceValue(value: unknown): string {
@@ -1087,6 +1065,8 @@ function summarizeResult(
return { result: '', resultPreviewMarkdown: block.text }
}
}
const images = imageBlockCount(node.content)
if (images > 0) return { result: t('layout.imageOnly', { count: images }) }
return { result: t('record.noOutput') }
}
@@ -1112,6 +1092,8 @@ function detailResult(node: ToolResultNode, t: TrajectoryTranslate): string {
.map(block => block.type === 'text' ? block.text : '')
.join('\n')
if (text !== '') return text
const images = imageBlockCount(node.content)
if (images > 0) return t('layout.imageOnly', { count: images })
if (
node.content.length === 0
|| node.content.every(block =>
@@ -121,7 +121,6 @@ export const zh = {
'block.openSummary': '打开第 {index} 个块的工具调用概述',
'block.openSummaryTitle': '打开工具调用概述',
'block.label': '块 #{index} {type}',
'block.openImage': '打开图片',
'history.loadingTrajectory': '正在加载轨迹…',
'history.loadingEarlier': '正在加载更早的历史…',
'history.loadingEarlierAria': '正在加载更早的历史…',
@@ -174,6 +173,7 @@ export const zh = {
'layout.compactionFailed': '上下文压缩失败',
'layout.compacted': '上下文已压缩',
'layout.toolCallOnly': '仅工具调用',
'layout.imageOnly': '图片 ×{count}',
'layout.initialSystemPrompt': '初始系统提示词',
'layout.systemPromptUpdated': '系统提示词已更新',
'layout.toolsUpdated': '工具已更新',
@@ -313,7 +313,6 @@ export const en: Record<TrajectoryKey, string> = {
'block.openSummary': 'Open Block #{index} tool call summary',
'block.openSummaryTitle': 'Open tool call summary',
'block.label': 'Block #{index} {type}',
'block.openImage': 'Open image',
'history.loadingTrajectory': 'Loading trajectory…',
'history.loadingEarlier': 'Loading earlier history…',
'history.loadingEarlierAria': 'Loading earlier history…',
@@ -366,6 +365,7 @@ export const en: Record<TrajectoryKey, string> = {
'layout.compactionFailed': 'Compaction failed',
'layout.compacted': 'Context compacted',
'layout.toolCallOnly': 'Tool call only',
'layout.imageOnly': 'Images ×{count}',
'layout.initialSystemPrompt': 'Initial System Prompt',
'layout.systemPromptUpdated': 'System Prompt Updated',
'layout.toolsUpdated': 'Tools Updated',
@@ -1,7 +1,7 @@
import type {
AssistantMessageNode, ConversationLocation, ConversationNode, ConversationPromptSnapshot,
ConversationViewNode, PartialAssistant, RequestPromptChange, RequestView, RunningToolCall,
ToolCallBlock,
ConversationViewNode, MessageImagesOwnerProps, PartialAssistant, RequestPromptChange,
RequestView, RunningToolCall, ToolCallBlock,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
@@ -84,4 +84,14 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
/** Selector hook over the current Conversation binding's Trajectory target. */
useTrajectory: UseTrajectory
}
interface SlotMap {
/**
* Renderer for one group of durable record images in the Trajectory
* ledger. The owner supplies image references, an authorized loader, and
* alignment. A registration replaces the shipped gallery; without one,
* images are omitted.
*/
'conversation.trajectory.images': { kind: 'single'; scope: 'session'; owner: MessageImagesOwnerProps }
}
}
@@ -1,6 +1,7 @@
/** Shared trajectory record data and formatting contracts. */
import type { HTMLAttributes } from 'react'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { ConversationPromptSnapshot } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { TrajectoryTranslate } from './locales.ts'
@@ -28,8 +29,7 @@ export interface AssistantMetricDetail {
export interface TrajectorySourceBlock {
type: string
content: string
imageSrc?: string
imageAlt?: string
attachment?: ImageAttachmentRef
callId?: string
toolName?: string
}
@@ -64,7 +64,7 @@ function recordSources(
block.content,
block.callId ?? '',
block.toolName ?? '',
block.imageAlt ?? '',
block.attachment?.name ?? '',
]),
searchableJson(cell.messageSource),
searchableJson(cell.promptDetail),
@@ -566,3 +566,103 @@ describe('run_code sub-dispatch cells', () => {
expect(cells.map(cell => cell.index)).toEqual([1, 2, 3, 4])
})
})
describe('durable image attachments', () => {
const attachment = {
attachmentId: `sha256:${'a'.repeat(64)}`,
mediaType: 'image/png',
bytes: 68,
width: 640,
height: 320,
name: 'screenshot.png',
}
it('carries user image refs into sourceBlocks and labels an image-only record', () => {
const nodes = [
{
kind: 'user', seq: 1, time: 1_000, source: null,
content: [{ type: 'image', attachment }, { type: 'image', attachment }],
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const user = turns[0]?.groups[0]?.cells[0]
expect(user?.text).toBe('Images ×2')
expect(user?.previewMarkdown).toBeUndefined()
expect(user?.sourceBlocks).toEqual([
{ type: 'image', content: '', attachment },
{ type: 'image', content: '', attachment },
])
})
it('labels a record whose only text block is empty as image-only', () => {
const nodes = [
{
kind: 'user', seq: 1, time: 1_000, source: null,
content: [{ type: 'text', text: '' }, { type: 'image', attachment }],
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const user = turns[0]?.groups[0]?.cells[0]
expect(user?.text).toBe('Images ×1')
expect(user?.previewMarkdown).toBeUndefined()
})
it('keeps the text preview when a user message mixes text and images', () => {
const nodes = [
{
kind: 'user', seq: 1, time: 1_000, source: null,
content: [{ type: 'text', text: 'look at this' }, { type: 'image', attachment }],
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const user = turns[0]?.groups[0]?.cells[0]
expect(user?.text).toBe('')
expect(user?.previewMarkdown).toBe('look at this')
expect(user?.sourceBlocks?.[1]).toEqual({ type: 'image', content: '', attachment })
})
it('maps assistant image blocks to attachment source blocks and labels image-only output', () => {
const nodes = [
{
kind: 'assistant', seq: 1, time: 1_000, turn: 1, step: 0,
blocks: [{ kind: 'image', attachment }],
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
expect(message?.text).toBe('Images ×1')
expect(message?.sourceBlocks).toEqual([{ type: 'image', content: '', attachment }])
})
it('carries tool-result image refs into outputBlocks and labels the result', () => {
const nodes = [
{
kind: 'assistant', seq: 1, time: 1_000, turn: 1, step: 1,
blocks: [{ kind: 'tool-call', callId: 'c1', name: 'read_image', argsRaw: '{}' }],
},
{
kind: 'tool-result', seq: 2, time: 2_000, callId: 'c1',
call: { name: 'read_image', argsRaw: '{}' }, callTime: 1_200,
content: [{ type: 'image', attachment }], isError: false,
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const tool = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'tool')
expect(tool?.result).toBe('Images ×1')
expect(tool?.outputDetail).toBe('Images ×1')
expect(tool?.outputBlocks).toEqual([{ type: 'image', content: '', attachment }])
})
it('shows wire-shaped blocks without an attachment as JSON, not as an image', () => {
const nodes = [
{
kind: 'user', seq: 1, time: 1_000, source: null,
content: [{ type: 'image', url: 'https://example.com/a.png' }],
},
] as unknown as LegacyConversationSlice['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const block = turns[0]?.groups[0]?.cells[0]?.sourceBlocks?.[0]
expect(block?.attachment).toBeUndefined()
expect(block?.content).toContain('https://example.com/a.png')
})
})
@@ -4,12 +4,24 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { ComponentProps } from 'react'
import type { RenderMessageImages } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { TrajectoryTable as LocalizedTrajectoryTable } from '../src/client/TrajectoryTable.tsx'
import type { TrajectoryTurnModel } from '../src/client/layout.ts'
import { trajectoryRecordId } from '../src/client/trajectory-record.ts'
import { t, tZh } from './locale.client.ts'
function TrajectoryTable(props: Omit<ComponentProps<typeof LocalizedTrajectoryTable>, 't'>) {
const renderImagesStub: RenderMessageImages = ({ images }) => (
<div data-testid="record-images" data-count={images.length}>
{images.map((image, index) => (
<span key={index} data-attachment-id={image.attachment.attachmentId} />
))}
</div>
)
function TrajectoryTable(
props: Omit<ComponentProps<typeof LocalizedTrajectoryTable>, 't' | 'renderImages'>
& { renderImages?: RenderMessageImages },
) {
const inferred: Array<NonNullable<typeof props.requestNumbers>[number] & { firstIndex: number }> = []
for (const turn of props.turns) {
for (const group of turn.groups) {
@@ -40,7 +52,14 @@ function TrajectoryTable(props: Omit<ComponentProps<typeof LocalizedTrajectoryTa
const requestNumbers = props.requestNumbers ?? inferred
.sort((left, right) => left.firstIndex - right.firstIndex)
.map(({ firstIndex: _firstIndex, ...request }, index) => ({ ...request, number: index + 1 }))
return <LocalizedTrajectoryTable {...props} requestNumbers={requestNumbers} t={t} />
return (
<LocalizedTrajectoryTable
renderImages={renderImagesStub}
{...props}
requestNumbers={requestNumbers}
t={t}
/>
)
}
afterEach(() => {
@@ -129,6 +148,7 @@ describe('TrajectoryTable', () => {
render(
<LocalizedTrajectoryTable
t={tZh}
renderImages={renderImagesStub}
turns={TURNS}
collapsedTurns={new Set<number>()}
onToggleTurn={() => {}}
@@ -937,6 +957,120 @@ describe('TrajectoryTable', () => {
expect(screen.getByText('value:')).toBeTruthy()
})
it('renders user image attachments through the shared gallery in the details panel', () => {
const attachment = {
attachmentId: `sha256:${'a'.repeat(64)}`,
mediaType: 'image/png',
bytes: 68,
width: 640,
height: 320,
name: 'screenshot.png',
} as unknown as NonNullable<
NonNullable<TrajectoryTurnModel['groups'][number]['cells'][number]['sourceBlocks']>[number]['attachment']
>
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
groups: [{
title: 'Message',
cells: [{
index: 1,
kind: 'user',
text: 'Images ×2',
sourceBlocks: [
{ type: 'image', content: '', attachment },
{ type: 'image', content: '', attachment },
],
timeSeconds: 0,
}],
}],
}]
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
fireEvent.click(screen.getByRole('row', { name: /USER/ }))
const preview = screen.getAllByTestId('record-images')
expect(preview.length).toBeGreaterThan(0)
expect(preview[0]?.getAttribute('data-count')).toBe('2')
fireEvent.click(screen.getByRole('tab', { name: 'Raw' }))
const rawGalleries = screen.getAllByTestId('record-images')
expect(rawGalleries).toHaveLength(2)
expect(rawGalleries[0]?.querySelector('[data-attachment-id]')?.getAttribute('data-attachment-id'))
.toBe(String(attachment.attachmentId))
})
it('renders a tool-result image through the shared gallery in the Result tab', () => {
const attachment = {
attachmentId: `sha256:${'b'.repeat(64)}`,
mediaType: 'image/png',
bytes: 68,
width: 320,
height: 640,
name: 'capture.png',
} as unknown as NonNullable<
NonNullable<TrajectoryTurnModel['groups'][number]['cells'][number]['outputBlocks']>[number]['attachment']
>
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
groups: [{
title: 'Step 1',
cells: [{
index: 1,
kind: 'tool',
text: 'read_image {"path":"a.png"}',
outputDetail: 'Images ×1',
outputBlocks: [{ type: 'image', content: '', attachment }],
timeSeconds: 0.1,
}],
}],
}]
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
fireEvent.click(screen.getByRole('row', { name: /TOOL/ }))
fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
const gallery = screen.getAllByTestId('record-images').at(-1)
expect(gallery?.getAttribute('data-count')).toBe('1')
expect(gallery?.querySelector('[data-attachment-id]')?.getAttribute('data-attachment-id'))
.toBe(String(attachment.attachmentId))
})
it('keeps the failure name beside an image-only error result', () => {
const attachment = {
attachmentId: `sha256:${'c'.repeat(64)}`,
mediaType: 'image/png',
bytes: 68,
width: 320,
height: 320,
name: 'failed.png',
} as unknown as NonNullable<
NonNullable<TrajectoryTurnModel['groups'][number]['cells'][number]['outputBlocks']>[number]['attachment']
>
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
groups: [{
title: 'Step 1',
cells: [{
index: 1,
kind: 'tool',
text: 'render {"target":"chart"}',
outputDetail: 'ToolError: RENDER_TRUNCATED',
outputBlocks: [{ type: 'image', content: '', attachment }],
isError: true,
timeSeconds: 0.1,
}],
}],
}]
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
fireEvent.click(screen.getByRole('row', { name: /TOOL/ }))
fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
expect(screen.getByText('ToolError: RENDER_TRUNCATED')).toBeTruthy()
const gallery = screen.getAllByTestId('record-images').at(-1)
expect(gallery?.getAttribute('data-count')).toBe('1')
})
it('keeps the first row and a compact summary when a turn is collapsed', () => {
render(
<TrajectoryTable
@@ -228,6 +228,10 @@ function standaloneProps(
viewRequest: null,
openView: () => {},
completeViewRequest: () => {},
// Image seats the outlet would bake: standalone renders omit the gallery.
renderSlot: () => null,
SessionProvider: ({ children }) => <>{children}</>,
loadImage: () => Promise.reject(new Error('standalone views load no images')),
// The locale seat the outlet would inject for the declared namespace.
t: tZh,
}
@@ -49,6 +49,9 @@
},
{
"path": "../../llm/llm"
},
{
"path": "../../attachment/attachment"
}
]
}
@@ -204,7 +204,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.assistant-actions\', () => ctx.slots.register(\n { name: \'conversation.chat.assistant-actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:197',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:186',
},
{
key: 'conversation.chat.commandview',
@@ -249,7 +249,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.commandview\', () => ctx.slots.register(\n { name: \'conversation.chat.commandview\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:185',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:174',
},
{
key: 'conversation.chat.node',
@@ -310,7 +310,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.node\', () => ctx.slots.register(\n { name: \'conversation.chat.node\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:166',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:155',
},
{
key: 'conversation.chat.turnTail',
@@ -355,7 +355,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.turnTail\', () => ctx.slots.register(\n { name: \'conversation.chat.turnTail\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:191',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:180',
},
{
key: 'conversation.composer',
@@ -404,7 +404,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer\', () => ctx.slots.register(\n { name: \'conversation.composer\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:78',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:92',
},
{
key: 'conversation.composer.bar',
@@ -440,7 +440,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.bar\', () => ctx.slots.register(\n { name: \'conversation.composer.bar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:96',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:110',
},
{
key: 'conversation.composer.dock',
@@ -498,7 +498,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.dock\', () => ctx.slots.register(\n { name: \'conversation.composer.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:90',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:104',
},
{
key: 'conversation.details.tool',
@@ -534,7 +534,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.details.tool\', () => ctx.slots.register(\n { name: \'conversation.details.tool\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:203',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:192',
},
{
key: 'conversation.hero.agentPreset',
@@ -562,7 +562,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.agentPreset\', () => ctx.slots.register(\n { name: \'conversation.hero.agentPreset\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:84',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:98',
},
{
key: 'conversation.hero.brand.mark',
@@ -590,7 +590,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.brand.mark\', () => ctx.slots.register(\n { name: \'conversation.hero.brand.mark\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:82',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:96',
},
{
key: 'conversation.hero.workspace',
@@ -620,7 +620,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.workspace\', () => ctx.slots.register(\n { name: \'conversation.hero.workspace\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:80',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:94',
},
{
key: 'conversation.hero.workspace.directoryFlow',
@@ -686,7 +686,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.attachments\', () => ctx.slots.register(\n { name: \'conversation.input.attachments\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:98',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:112',
},
{
key: 'conversation.input.dock',
@@ -746,7 +746,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.dock\', () => ctx.slots.register(\n { name: \'conversation.input.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:86',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:100',
},
{
key: 'conversation.input.left',
@@ -802,7 +802,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.left\', () => ctx.slots.register(\n { name: \'conversation.input.left\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:92',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:106',
},
{
key: 'conversation.input.model',
@@ -838,7 +838,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.model\', () => ctx.slots.register(\n { name: \'conversation.input.model\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:106',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:120',
},
{
key: 'conversation.input.overlay',
@@ -892,7 +892,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.overlay\', () => ctx.slots.register(\n { name: \'conversation.input.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:88',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:102',
},
{
key: 'conversation.input.plan',
@@ -928,7 +928,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.plan\', () => ctx.slots.register(\n { name: \'conversation.input.plan\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:104',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:118',
},
{
key: 'conversation.input.right',
@@ -984,7 +984,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.input.right\', () => ctx.slots.register(\n { name: \'conversation.input.right\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:94',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:108',
},
{
key: 'conversation.message.images',
@@ -994,7 +994,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
doc: 'Renderer for one consecutive group of durable message images. The owner\nsupplies image references, an authorized loader, and alignment. A\nregistration replaces the shipped gallery; without one, images are omitted.',
registerOptions: [],
ownerProps: [
'/** Historical image group handed to the optional attachment presentation plugin. */\nexport interface MessageImagesOwnerProps {\n images: readonly { readonly attachment: ImageAttachmentRef }[]\n loadImage: (attachment: ImageAttachmentRef) => Promise<string>\n align: \'start\' | \'end\'\n}',
'/** Durable image group handed to the optional attachment presentation plugin. */\nexport interface MessageImagesOwnerProps {\n /** Durable image references in source order. */\n images: readonly { readonly attachment: ImageAttachmentRef }[]\n /** Session-authorized image URL loader. */\n loadImage: (attachment: ImageAttachmentRef) => Promise<string>\n /** Horizontal placement inside the owning record. */\n align: \'start\' | \'end\'\n}',
],
ownerPropsReferences: [
'ImageAttachmentRef',
@@ -1022,7 +1022,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.message.images\', () => ctx.slots.register(\n { name: \'conversation.message.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:179',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:168',
},
{
key: 'conversation.session',
@@ -1056,7 +1056,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session\', () => ctx.slots.register(\n { name: \'conversation.session\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:54',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:68',
},
{
key: 'conversation.session.header',
@@ -1090,7 +1090,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header\', () => ctx.slots.register(\n { name: \'conversation.session.header\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:56',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:70',
},
{
key: 'conversation.session.header.actions',
@@ -1146,7 +1146,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.actions\', () => ctx.slots.register(\n { name: \'conversation.session.header.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:64',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:78',
},
{
key: 'conversation.session.header.lineage',
@@ -1184,7 +1184,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.lineage\', () => ctx.slots.register(\n { name: \'conversation.session.header.lineage\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:58',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:72',
},
{
key: 'conversation.session.header.utilities',
@@ -1239,7 +1239,45 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.utilities\', () => ctx.slots.register(\n { name: \'conversation.session.header.utilities\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:70',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:84',
},
{
key: 'conversation.trajectory.images',
kind: 'single',
scope: 'session',
summary: 'Renderer for one group of durable record images in the Trajectory ledger.',
doc: 'Renderer for one group of durable record images in the Trajectory\nledger. The owner supplies image references, an authorized loader, and\nalignment. A registration replaces the shipped gallery; without one,\nimages are omitted.',
registerOptions: [],
ownerProps: [
'/** Durable image group handed to the optional attachment presentation plugin. */\nexport interface MessageImagesOwnerProps {\n /** Durable image references in source order. */\n images: readonly { readonly attachment: ImageAttachmentRef }[]\n /** Session-authorized image URL loader. */\n loadImage: (attachment: ImageAttachmentRef) => Promise<string>\n /** Horizontal placement inside the owning record. */\n align: \'start\' | \'end\'\n}',
],
ownerPropsReferences: [
'ImageAttachmentRef',
],
standardProps: [
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'useSessions: UseSessions',
'useSessionPendingInteraction: UseSessionPendingInteraction',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'useChat: UseChat',
'useConversation: UseConversation',
'useInput: SnapshotSelectorHook<InputState>',
'inputActions: InputActions',
'useSession: SessionSnapshotSelector',
'sessionId: SessionId',
'useProjection: UseProjection',
'useTrajectory: UseTrajectory',
],
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'conversation.view\' (client-ui-trajectory), so it exists while that entry is mounted',
occupants: [
'client-ui-attachment MessageImages',
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.trajectory.images\', () => ctx.slots.register(\n { name: \'conversation.trajectory.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-trajectory/src/client/trajectory-contract.ts:95',
},
{
key: 'conversation.view',
@@ -1297,7 +1335,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.view\', () => ctx.slots.register(\n { name: \'conversation.view\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:76',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:90',
},
{
key: 'details',
+6 -3
View File
@@ -1835,6 +1835,9 @@ importers:
'@deepseek-ai/dsh-client-ui-slots':
specifier: workspace:^
version: link:../ui-slots
'@deepseek-ai/dsh-client-ui-trajectory':
specifier: workspace:^
version: link:../ui-trajectory
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
@@ -1961,9 +1964,6 @@ importers:
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
'@deepseek-ai/dsh-util-crypto':
specifier: workspace:^
version: link:../../util/crypto
'@deepseek-ai/dsh-util-workspace-path':
specifier: workspace:^
version: link:../../util/workspace-path
@@ -3379,6 +3379,9 @@ importers:
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:^
version: link:../../api/session-controller
'@deepseek-ai/dsh-attachment':
specifier: workspace:^
version: link:../../attachment/attachment
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../locale