mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
revert(client): return the fork seam to master
ui-chat goes back to origin/master as a whole package: the footer branch forks
through ctx.sessions.fork with its own atSeq and analytics again, and Workspace
navigation takes no ForkSessionOptions. The package is identical to
origin/master, which drops the served-page branch assertions and README lines
added on this branch; they pinned behavior master already has.
ui-workspace restores master's forkSession signature and implementation
(allowMigration only, increaseTitle: true, selection untouched) and drops the
ForkSessionOptions type. uiSession leaves the inject list because the removed
session/rename probe was its only consumer. The row fork keeps passing
{ allowMigration: false }, which master already does, so forking from a shared
page never starts source-format migration.
Regenerate the client API catalog, restore the README fork contract (fork does
not navigate; allowMigration carries migration permission), re-record the
translation pairing, and return the ui-chat unknown-cast baseline count to 2.
This commit is contained in:
1 parent
58785b975e
commit
d9c012551a
16 files changed
+54
-262
No files matched your search
@@ -12,11 +12,11 @@
|
||||
en: 4f2d50107db6c4f8
|
||||
zh: ea7ccfcce8f1849e
|
||||
/deepseek-ai-dsh-client-ui-chat/table-of-contents:
|
||||
en: 5adb39e9973415c5
|
||||
zh: 24417762432b803b
|
||||
en: 41cd21266011d571
|
||||
zh: 3172d1a2acbb27b7
|
||||
/deepseek-ai-dsh-client-ui-chat/reference-previews:
|
||||
en: dbc28d281081c29e
|
||||
zh: ad5080c4cdefaaf8
|
||||
en: a05d85ff4949b526
|
||||
zh: 7261ffc98665e9e7
|
||||
/deepseek-ai-dsh-client-ui-chat/hidden-chat-rows:
|
||||
en: cbf1f0a2205046a5
|
||||
zh: 2e59d0815351349f
|
||||
@@ -36,11 +36,8 @@
|
||||
en: a14c74ed7de0ef9c
|
||||
zh: d5e618b2ef809150
|
||||
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
|
||||
en: 97a4d98a72c34263
|
||||
zh: 9df3d197d9986a54
|
||||
/deepseek-ai-dsh-client-ui-chat/write-actions:
|
||||
en: e18fccec7be6b240
|
||||
zh: c40747bf36c443b2
|
||||
en: 009ecb4fecd4ee9e
|
||||
zh: ec2714337a1102c5
|
||||
/deepseek-ai-dsh-client-ui-chat/model-experience:
|
||||
en: c17b4dd43a61c569
|
||||
zh: f7414309810c3a08
|
||||
|
||||
@@ -24,7 +24,6 @@ File-mention providers receive the viewed Session ID with the closing-turn owner
|
||||
- [Turn Process Folding](#turn-process-folding)
|
||||
- [Grouped rendering](#grouped-rendering)
|
||||
- [Scroll ownership](#scroll-ownership)
|
||||
- [Write actions](#write-actions)
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
- [Dev Note](#dev-note)
|
||||
@@ -42,13 +41,13 @@ Sent generic-file attachments retain the draft's [file-card radius](../../../doc
|
||||
|
||||
Native message images use separate user and output positions. The `conversation.chat.user.content` Factory declares `conversation.chat.user.images`, which `UserContent` renders; the Assistant node registration declares `conversation.chat.message.images`, which `AssistantMarkdown` renders. Both receive image sources, the authorized `loadImage`, and layout options through explicit props. Their official `MessageImages` entries call the shared root-scoped `conversation.images.content` Factory. Each position can be replaced independently without duplicating the gallery implementation.
|
||||
|
||||
The session-scoped `conversation.chat.user.content` Factory renders only the user content stack: text, extra blocks, image/file attachments in input order, and reference summaries. Its [`UserContentInput`](src/client/contract/slots.ts) carries prepared `content`, transform `facts`, display props, and explicit `loadImage`; node-owned injections provide reference actions. It calls the shared `conversation.user.text` Factory for text and its own image Slot for images. Direct callers need a Session binding and supply their own row wrapper. Reference rendering is independent of the Session's access answers; a withdrawn `session/fork` answer removes only the branch action, without replacing content or resetting Turn disclosures.
|
||||
The session-scoped `conversation.chat.user.content` Factory renders only the user content stack: text, extra blocks, image/file attachments in input order, and reference summaries. Its [`UserContentInput`](src/client/contract/slots.ts) carries prepared `content`, transform `facts`, display props, and explicit `loadImage`; node-owned injections provide reference actions. It calls the shared `conversation.user.text` Factory for text and its own image Slot for images. Direct callers need a Session binding and supply their own row wrapper.
|
||||
|
||||
`UserStyleBubble` and the [source-specific wrappers](<src/client/chat/MessageItem.tsx#L157-L309>) retain the user row, keys, pending/echo markers, timestamps, copy controls, and other message actions. The content Factory reads neither source-specific models nor private Context, and is not a global user-content override entry point.
|
||||
|
||||
Standalone Markdown images show contained previews and open the shared image lightbox; local paths resolve against the viewed workspace after settlement. Image file links keep their sidebar activation and show a thumbnail after hover dwell or keyboard focus. Escape dismisses the thumbnail. Failed images retain a localized status and their description; no duplicate-image filtering is applied.
|
||||
|
||||
Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting follows Keyboard shortcuts and is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on the Host for authenticated browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references. The page's tab-type registry decides how a file address opens. A page that lists workspace paths without serving file contents provides its own tab response; Chat delegates navigation without inferring file-read permission.
|
||||
Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting follows Keyboard shortcuts and is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on the Host for authenticated browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references.
|
||||
|
||||
<a id="system-prompt-row"></a>
|
||||
## Hidden Chat rows
|
||||
@@ -175,11 +174,6 @@ Active-Turn highlighting is approximate: `readVisibleTurn` binary-searches the f
|
||||
|
||||
</details>
|
||||
|
||||
<a id="write-actions"></a>
|
||||
## Write actions
|
||||
|
||||
The Turn Tail branch action reads the Session-scope `useAccess('session/fork')` Hook and disappears while the calling Peer's access snapshot excludes that endpoint; the view always receives the `forkAt` handler. A branch always names its child, because whoever may fork also holds `session/rename` on the child it creates, and it opens that child immediately. The Session tree's branch verb uses the same naming rule but leaves the selection alone. These answers never replace Host authorization.
|
||||
|
||||
-----
|
||||
|
||||
The Chat view detects selection and context-menu gestures on business nodes marked with `data-text-interaction-source`. Both selection endpoints must resolve to the same source container. Displayed user content and settled Assistant text or reasoning participate; tools, injected context, streaming or interrupted Assistant text, and Markdown controls do not.
|
||||
|
||||
@@ -24,7 +24,6 @@ kind: "package-reference"
|
||||
- [轮次过程折叠](#turn-process-folding)
|
||||
- [分组渲染](#grouped-rendering)
|
||||
- [滚动归属](#scroll-ownership)
|
||||
- [写操作](#write-actions)
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
|
||||
- [开发备注](#dev-note)
|
||||
@@ -42,13 +41,13 @@ kind: "package-reference"
|
||||
|
||||
原生消息图片使用独立的用户与输出位置。`conversation.chat.user.content` Factory 声明 `conversation.chat.user.images`,由 `UserContent` 渲染;Assistant 节点注册声明 `conversation.chat.message.images`,由 `AssistantMarkdown` 渲染。两者通过显式 props 接收图片来源、授权 `loadImage` 和布局参数。官方 `MessageImages` 条目调用共享的 root 作用域 `conversation.images.content` Factory。每个位置均可独立替换,无需复制画廊实现。
|
||||
|
||||
Session 作用域的 `conversation.chat.user.content` Factory 只渲染用户内容区域:文本、额外内容块、按输入顺序排列的图片/文件附件及引用摘要。[`UserContentInput`](src/client/contract/slots.ts) 携带预处理的 `content`、变换 `facts`、显示 props 和显式 `loadImage`;引用动作由节点自己的 Injection 提供。正文调用共享 `conversation.user.text` Factory,图片调用自身拥有的图片 Slot。直接调用需要 Session 绑定,行包装层由调用方提供。引用渲染独立于 Session 的访问答案;撤回 `session/fork` 答案只移除分支操作,不替换内容或重置轮次开合状态。
|
||||
Session 作用域的 `conversation.chat.user.content` Factory 只渲染用户内容区域:文本、额外内容块、按输入顺序排列的图片/文件附件及引用摘要。[`UserContentInput`](src/client/contract/slots.ts) 携带预处理的 `content`、变换 `facts`、显示 props 和显式 `loadImage`;引用动作由节点自己的 Injection 提供。正文调用共享 `conversation.user.text` Factory,图片调用自身拥有的图片 Slot。直接调用需要 Session 绑定,行包装层由调用方提供。
|
||||
|
||||
`UserStyleBubble` 和[各来源包装层](<src/client/chat/MessageItem.tsx#L157-L309>) 保留用户行、key、pending/echo 标记、时间、复制控件及其他消息操作。内容 Factory 不读取来源专属模型或私有 Context,也不是全局用户内容替换入口。
|
||||
|
||||
独立的 Markdown 图片以内嵌预览显示,点击打开共享图片浮层;本地路径在消息落定后基于当前查看的工作区解析。图片文件链接保持点击打开侧栏,鼠标停留或键盘聚焦时显示缩略图,Esc 关闭缩略图。图片加载失败时保留本地化状态与图片说明;不执行重复图片过滤。
|
||||
|
||||
设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项位于快捷键之后,仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。`ui-chat.linkOpening` 偏好在已认证浏览器的 Host 上持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。页面的标签类型注册表决定如何打开文件地址。只列出工作区路径而不提供文件内容的页面提供自己的标签反馈;Chat 只委派导航,不推断文件读取权限。
|
||||
设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项位于快捷键之后,仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。已认证浏览器的 `ui-chat.linkOpening` 偏好在 Host 上持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。
|
||||
|
||||
<a id="system-prompt-row"></a>
|
||||
## Chat 隐藏的行
|
||||
@@ -175,11 +174,6 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且
|
||||
|
||||
</details>
|
||||
|
||||
<a id="write-actions"></a>
|
||||
## 写操作
|
||||
|
||||
轮次末尾的分支操作读取会话作用域的 `useAccess('session/fork')` 钩子,调用方 Peer 的访问快照不含该端点时不显示;视图始终收到 `forkAt` 处理函数。分支总会为子会话命名,因为能 fork 的页面同时拥有对其创建的子会话的 `session/rename`,并会立即打开该子会话。会话树的分支动词使用同一命名规则,但不改变当前选中。这些界面判断不替代 Host 授权。
|
||||
|
||||
-----
|
||||
|
||||
Chat view 在标记了 `data-text-interaction-source` 的业务节点上检测选区和右键手势。选区两端必须定位到同一个来源容器。已展示的用户内容和已完成的 Assistant 文本或 reasoning 参与交互;Tool、注入上下文、流式或中断的 Assistant 文本以及 Markdown 控件不参与。
|
||||
|
||||
@@ -297,18 +297,12 @@ export function apply(ctx: Context): void {
|
||||
read: () => chatScrollPositions.get(sessionId) ?? null,
|
||||
},
|
||||
forkAt: (seq) => {
|
||||
// The Workspace navigation owns the branch policy: it names the child and opens it
|
||||
// immediately for `selected`. This page supplies the branch point and its analytics.
|
||||
const turn = [...chat.getSnapshot().timeline.turns.values()].find(turn => turn.end?.seq === seq)
|
||||
const messageId = turn?.data.get('turn-tail')?.closing?.finalNode.messageId
|
||||
void ctx.uiWorkspace.forkSession(sessionId, (childId) => {
|
||||
ctx.get('productAnalytics')?.track('branch_session_click', {
|
||||
session_id: childId,
|
||||
parent_session_id: sessionId,
|
||||
...messageId === undefined ? {} : { parent_message_id: messageId },
|
||||
click_position: 'footer',
|
||||
})
|
||||
}, { atSeq: seq, present: 'selected' })
|
||||
ctx.sessions.fork({ sessionId, atSeq: seq, increaseTitle: true, onCreated: (childId) => {
|
||||
ctx.get('productAnalytics')?.track('branch_session_click', { session_id: childId, parent_session_id: sessionId, ...messageId === undefined ? {} : { parent_message_id: messageId }, click_position: 'footer' })
|
||||
} })
|
||||
.then((childId) => { ctx.uiWorkspace.openSession(childId) })
|
||||
.catch(() => {
|
||||
// Fork or child-title failure leaves the source view unchanged.
|
||||
})
|
||||
|
||||
@@ -69,12 +69,9 @@ async function bench(initialSettings?: ChatSettings, withBrowserRegistry = true,
|
||||
openTab: vi.fn<(kind: string, options?: unknown) => void>(),
|
||||
}
|
||||
runtime.ctx.provide('sidebarRight', sidebarRight as never)
|
||||
// Pages open files through the right Sidebar's tab-type registry, whose own claim decides whether
|
||||
// an address opens at all; the spy records whether this call site ever asks it.
|
||||
const browserAvailable = createSnapshotStore(true)
|
||||
const sidebarRightTabs = {
|
||||
get: vi.fn<(kind: string) => object | undefined>(() => browserAvailable.getSnapshot() ? {} : undefined),
|
||||
candidates: vi.fn<(address: string) => readonly object[]>(() => []),
|
||||
subscribe: (listener: () => void) => browserAvailable.subscribe(listener),
|
||||
register: vi.fn(() => () => {}),
|
||||
}
|
||||
@@ -85,23 +82,13 @@ async function bench(initialSettings?: ChatSettings, withBrowserRegistry = true,
|
||||
)
|
||||
runtime.remote.provideNamespaces({ session: { openWorkspacePath } })
|
||||
const openSession = vi.fn<(id: SessionId) => void>()
|
||||
const uiWorkspace = {
|
||||
runtime.ctx.provide('uiWorkspace', {
|
||||
openWorkspace: vi.fn(async (_workspaceId: WorkspaceId, beforeOpen: (id: SessionId) => void) => {
|
||||
beforeOpen(ROOT)
|
||||
openSession(ROOT)
|
||||
}),
|
||||
openSession,
|
||||
// The branch surface delegates naming and presentation to Workspace navigation.
|
||||
forkSession: vi.fn(async (
|
||||
_sessionId: SessionId,
|
||||
onCreated?: (childId: SessionId) => void,
|
||||
_options?: { readonly atSeq?: number; readonly present?: 'selected' | 'listed' },
|
||||
) => {
|
||||
onCreated?.(ROOT)
|
||||
return ROOT
|
||||
}),
|
||||
}
|
||||
runtime.ctx.provide('uiWorkspace', uiWorkspace as never)
|
||||
} as never)
|
||||
const session = sessionFakeFor()
|
||||
await runtime.sessions.add({
|
||||
id: ROOT,
|
||||
@@ -148,7 +135,6 @@ async function bench(initialSettings?: ChatSettings, withBrowserRegistry = true,
|
||||
},
|
||||
runtime, chat, chatSettings, browserAvailable,
|
||||
layout, openWorkspacePath, sidebarRight, sidebarRightTabs, session, chatViewApi, chatFlowApi, rootReference, openSession,
|
||||
uiWorkspace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +245,7 @@ describe('Chat inject API', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('delegates the branch point, its analytics, and the child presentation to Workspace navigation', async () => {
|
||||
it('loads older history and forks through the Session Controller', async () => {
|
||||
const b = await bench()
|
||||
const { injected } = b.chatViewApi(b.rootReference)
|
||||
injected.loadOlder()
|
||||
@@ -270,11 +256,8 @@ describe('Chat inject API', () => {
|
||||
|
||||
const track = vi.fn()
|
||||
b.runtime.ctx.provide('productAnalytics', { track } as never)
|
||||
const forkCreated = vi.spyOn(b.runtime.sessions, 'fork').mockImplementation(async (input) => { input.onCreated?.(ROOT); return ROOT })
|
||||
injected.forkAt(17)
|
||||
// The page owns only its branch point and analytics; naming and presentation belong to Workspace.
|
||||
expect(b.uiWorkspace.forkSession).toHaveBeenCalledWith(ROOT, expect.any(Function) as (childId: SessionId) => void, {
|
||||
atSeq: 17, present: 'selected',
|
||||
})
|
||||
expect(track).toHaveBeenCalledWith('branch_session_click', { session_id: ROOT, parent_session_id: ROOT, click_position: 'footer' })
|
||||
await b.runtime.sessions.replaceEvents(ROOT, [
|
||||
{ type: 'event', event: { type: 'turn/start', seq: SessionSeq(10), time: 10, data: { turn: 1 } } },
|
||||
@@ -286,12 +269,20 @@ describe('Chat inject API', () => {
|
||||
])
|
||||
injected.forkAt(17)
|
||||
expect(track).toHaveBeenLastCalledWith('branch_session_click', { session_id: ROOT, parent_session_id: ROOT, parent_message_id: 'reply', click_position: 'footer' })
|
||||
// A rejected branch leaves the source view unchanged, as on master; the page adds no retry.
|
||||
vi.spyOn(b.uiWorkspace, 'forkSession').mockRejectedValueOnce(new Error('fork failed'))
|
||||
forkCreated.mockRestore()
|
||||
injected.forkAt(17)
|
||||
await vi.waitFor(() => {
|
||||
expect(b.openSession).toHaveBeenCalledWith(ROOT)
|
||||
})
|
||||
expect(b.runtime.sessions.calls).toContainEqual({
|
||||
method: 'fork', args: [{ sessionId: ROOT, atSeq: 17, increaseTitle: true, onCreated: expect.any(Function) as (childId: SessionId) => void }],
|
||||
})
|
||||
|
||||
const fork = vi.spyOn(b.runtime.sessions, 'fork').mockRejectedValueOnce(new Error('fork failed'))
|
||||
injected.forkAt(18)
|
||||
await vi.waitFor(() => {
|
||||
expect(b.uiWorkspace.forkSession).toHaveBeenLastCalledWith(ROOT, expect.any(Function) as (childId: SessionId) => void, {
|
||||
atSeq: 18, present: 'selected',
|
||||
expect(fork).toHaveBeenCalledWith({
|
||||
sessionId: ROOT, atSeq: 18, increaseTitle: true, onCreated: expect.any(Function) as (childId: SessionId) => void,
|
||||
})
|
||||
})
|
||||
await b.runtime.dispose()
|
||||
@@ -317,10 +308,6 @@ describe('Chat inject API', () => {
|
||||
// A line travels as the `file` type's navigation parameter, not in the address.
|
||||
await injected.openFile('src/a.ts', { line: 7 })
|
||||
expect(b.sidebarRight.openResource).toHaveBeenLastCalledWith('dsh-resource://file/session/root-1/src/a.ts', { params: { line: 7 } })
|
||||
// Which type claims the address is the Sidebar registry's decision: the call site hands the
|
||||
// address over without asking whether this page carries a type that would open it. A page with
|
||||
// none — a reader's share — is answered by its own registered type, or by the Sidebar's refusal.
|
||||
expect(b.sidebarRightTabs.candidates).not.toHaveBeenCalled()
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
@@ -503,7 +490,10 @@ describe('Chat inject API', () => {
|
||||
it('fails loud when a Chat View inject resolves no Session', async () => {
|
||||
const b = await bench()
|
||||
const entry = b.runtime.slots.entries('conversation.view')[0]!
|
||||
const injectView = entry.inject! as (...args: [SessionId, ChatActions]) => Record<string, unknown>
|
||||
const injectView = entry.inject as unknown as (
|
||||
sessionId: SessionId,
|
||||
actions: ChatActions,
|
||||
) => ChatViewInjected
|
||||
expect(() => injectView('never-listed' as SessionId, {} as ChatActions))
|
||||
.toThrow(/unknown session/)
|
||||
const flowEntry = b.runtime.slots.entries('conversation.chat.flow')[0]!
|
||||
@@ -519,7 +509,6 @@ describe('Chat inject API', () => {
|
||||
|
||||
const flow = b.chatFlowApi(b.rootReference)
|
||||
expect(flow.instance).toBe(b.chatViewApi(b.rootReference).instance)
|
||||
expect(flow.injected.hooks.presentation).toBe(injected.hooks.presentation)
|
||||
expect(flow.injected.keyedHooks.chatNode('missing')).toBe(injected.keyedHooks.chatNode('missing'))
|
||||
expect(flow.injected.keyedHooks.chatNodeProcess('missing')).toBe(injected.keyedHooks.chatNodeProcess('missing'))
|
||||
expect(flow.injected.keyedHooks.chatNodeBottom('missing')).toBe(injected.keyedHooks.chatNodeBottom('missing'))
|
||||
|
||||
@@ -1926,43 +1926,6 @@ describe('ChatView', () => {
|
||||
expect(h.forkAt).toHaveBeenCalledWith(3)
|
||||
}))
|
||||
|
||||
it('keeps next-input folding independent from a live session/fork answer in the same flow', () => {
|
||||
const h = makeCompletionHarness('detailed', 'next-input')
|
||||
const fork = createSnapshotStore(true)
|
||||
const useFork = bindSnapshotSelector(fork)
|
||||
// Chat Node renderers read the harness's standard props object, so the override goes there.
|
||||
Object.assign(h.props, {
|
||||
useAccess: (endpoint: string) => {
|
||||
const allowed = useFork(value => value)
|
||||
return endpoint === 'session/fork' ? allowed : true
|
||||
},
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
const root = view.container.querySelector<HTMLElement>('[data-chat-group-key][data-chat-turn="2"]')!
|
||||
act(() => { h.complete() })
|
||||
const control = view.container.querySelector<HTMLButtonElement>('[data-turn-process="2"]')!
|
||||
expect(control.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(root.closest('[hidden]')).toBeNull()
|
||||
expect(view.getAllByRole('button', { name: '在新对话中分支' })).toHaveLength(2)
|
||||
|
||||
act(() => { fork.set(false) })
|
||||
expect(view.queryAllByRole('button', { name: '在新对话中分支' })).toHaveLength(0)
|
||||
expect(view.container.querySelector('[data-chat-group-key][data-chat-turn="2"]')).toBe(root)
|
||||
expect(control.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(root.closest('[hidden]')).toBeNull()
|
||||
|
||||
act(() => { h.sendNext() })
|
||||
expect(view.getByText('next question').closest('[data-submission-echo]')).not.toBeNull()
|
||||
expect(control.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(root.hasAttribute('hidden')).toBe(true)
|
||||
act(() => { fork.set(true) })
|
||||
expect(view.getAllByRole('button', { name: '在新对话中分支' })).toHaveLength(2)
|
||||
expect(view.container.querySelector('[data-chat-group-key][data-chat-turn="2"]')).toBe(root)
|
||||
expect(control.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(root.hasAttribute('hidden')).toBe(true)
|
||||
expect(view.getByText('current answer').closest('[hidden]')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps pending file mentions non-interactive until admission enables the reference injector', () => {
|
||||
const pending: InboxState['next-step'][number] = {
|
||||
id: MessageId('pending-file-mention'), role: 'user', source: { kind: 'user' },
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 9fb88260380bf093
|
||||
zh: 3bace2b96ecd4a82
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers:
|
||||
en: 0701b99279062282
|
||||
zh: 29255dc290eb177b
|
||||
en: 2168316f604893a5
|
||||
zh: fe11347009339a9a
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation:
|
||||
en: 96a37e74555a36b7
|
||||
zh: d03941523509a5cf
|
||||
|
||||
@@ -73,7 +73,7 @@ Grouped and flat Session rows show a clock mark when the Session has active sche
|
||||
|
||||
-----
|
||||
|
||||
`ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?, options?)` opens a Workspace blank only if no later navigation has superseded the request; New Session opens the same blank as Workspace reopening, so it returns to that blank's unsent draft. `forkSession(id, onCreated?, options?)` creates the child and applies `ForkSessionOptions`: `atSeq` sets the branch boundary, `allowMigration` carries the caller's migration permission, and `present` selects the landing. A branch always names its child: whoever may fork a Session also holds `session/rename` on the child it creates. `selected` opens the child immediately, while `listed` (the default) leaves the selection alone. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation.
|
||||
`ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?, options?)` opens a Workspace blank only if no later navigation has superseded the request; New Session opens the same blank as Workspace reopening, so it returns to that blank's unsent draft. `forkSession(id, onCreated?, options?)` creates the child without navigating or superseding a pending navigation; `allowMigration` carries the caller's migration permission. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation.
|
||||
|
||||
Session and Workspace rows derive their affordances from the browser root's access Hooks: the global `useRemoteAccess('session/create')` gates every New Session entry, `useWorkspaceAccess` gates a Workspace's create, rename, delete, and reorder verbs, and `useSessionAccess` gates a Session row's rename, fork, drag, pin, archive, and unarchive verbs plus the search result's restore control. A refused verb is not rendered, while every Host request remains authoritative. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel.
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
|
||||
|
||||
-----
|
||||
|
||||
`ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?, options?)` 仅在请求未被后续导航替代时打开工作区空白 Session;新会话与重新打开工作区打开同一个空白会话,因此会回到该空白会话中未发送的草稿。`forkSession(id, onCreated?, options?)` 创建子会话并应用 `ForkSessionOptions`:`atSeq` 指定分支点,`allowMigration` 携带调用方的迁移许可,`present` 选择落地方式。分支总会为子会话命名:能 fork 的页面同时拥有对其创建的子会话的 `session/rename`。`selected` 立即打开子会话,`listed`(默认)保持当前选中。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。
|
||||
`ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?, options?)` 仅在请求未被后续导航替代时打开工作区空白 Session;新会话与重新打开工作区打开同一个空白会话,因此会回到该空白会话中未发送的草稿。`forkSession(id, onCreated?, options?)` 创建子会话,不导航,也不替代尚未完成的导航;`allowMigration` 携带调用方的迁移许可。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。
|
||||
|
||||
Session 行与 Workspace 行的可用操作来自浏览器根的访问 Hook:全局 `useRemoteAccess('session/create')` 门控所有新建会话入口,`useWorkspaceAccess` 门控 Workspace 的新建、重命名、删除与排序动作,`useSessionAccess` 门控 Session 行的重命名、分叉、拖拽、置顶、归档与取消归档动作,以及搜索结果的恢复控件。被拒绝的动作不渲染,但每个发往 Host 的请求仍以其授权为准。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ import { SessionIdMenuItem } from './session-actions/SessionId.tsx'
|
||||
import { WorkspacePicker } from './WorkspacePicker.tsx'
|
||||
import { en, ja, zh, type WorkspaceKey } from './locales.ts'
|
||||
|
||||
export type { ForkSessionOptions, StartSessionOptions, UiWorkspace } from './navigation.ts'
|
||||
export type { StartSessionOptions, UiWorkspace } from './navigation.ts'
|
||||
export type {
|
||||
DirectoryFlowOwnerProps, DirectoryFlowSlotName, DirectoryPickingHooks, DirectoryPickingInjected,
|
||||
MenuOpenState, RowToast, SessionRenameTarget, SessionRowOwnerProps, UseMenuOpenState, WorkspaceBrowserInjected,
|
||||
@@ -101,7 +101,6 @@ const NS = 'workspace'
|
||||
*/
|
||||
export const inject = [
|
||||
'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', 'layout', 'shortcuts', 'remoteAccess',
|
||||
'uiSession',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,20 +59,16 @@ export interface UiWorkspace {
|
||||
options?: Pick<StartSessionOptions, 'agentPreset'>,
|
||||
): Promise<void>
|
||||
/**
|
||||
* Fork a Session and apply its branch presentation policy. The child always receives the
|
||||
* incremented title: whoever may fork a Session also holds `session/rename` on the child it
|
||||
* creates, so the source's own rename answer does not matter.
|
||||
* Fork a Session without changing the current selection.
|
||||
* @param sessionId - source Session.
|
||||
* @param onCreated - observer after creation, before the child-title update.
|
||||
* @param options - branch boundary, migration permission, and how the child is presented.
|
||||
* @param onCreated - observer before the optional child-title update.
|
||||
* @param options - optional permission to start source migration; omission preserves the Host default.
|
||||
* @returns the child SessionId after creation and inherited-title increment.
|
||||
* @throws when the Host refuses or fails the fork; the caller keeps its current view and decides
|
||||
* whether to report the failure.
|
||||
*/
|
||||
forkSession(
|
||||
sessionId: SessionId,
|
||||
onCreated?: (childId: SessionId) => void,
|
||||
options?: ForkSessionOptions,
|
||||
options?: Pick<Parameters<ISessions['fork']>[0], 'allowMigration'>,
|
||||
): Promise<SessionId>
|
||||
/**
|
||||
* Resolve the Session for a new conversation in a Workspace: navigation names the first reusable
|
||||
@@ -156,15 +152,6 @@ declare module '@deepseek-ai/cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** What one branch fork asks of the Workspace navigation service. Every fork names its child. */
|
||||
export interface ForkSessionOptions extends Pick<Parameters<ISessions['fork']>[0], 'atSeq' | 'allowMigration'> {
|
||||
/**
|
||||
* How the caller presents the child: `selected` opens it immediately, while `listed` (the
|
||||
* default, the row-action path) leaves the selection alone.
|
||||
*/
|
||||
readonly present?: 'selected' | 'listed'
|
||||
}
|
||||
|
||||
/** Structured directory failure exposed to directory UI consumers. */
|
||||
export class DirectoryBrowseError extends Error {
|
||||
override readonly name = 'DirectoryBrowseError'
|
||||
@@ -337,19 +324,14 @@ class UiWorkspaceService extends Service implements UiWorkspace {
|
||||
async forkSession(
|
||||
sessionId: SessionId,
|
||||
onCreated?: (childId: SessionId) => void,
|
||||
options: ForkSessionOptions = {},
|
||||
options?: Pick<Parameters<ISessions['fork']>[0], 'allowMigration'>,
|
||||
): Promise<SessionId> {
|
||||
// `increaseTitle` renames the child, not the source: whoever may fork a Session also holds
|
||||
// `session/rename` on the Session it creates, so the source's own access answer does not matter.
|
||||
const childId = await this.sessions.fork({
|
||||
return this.sessions.fork({
|
||||
sessionId,
|
||||
...options.atSeq === undefined ? {} : { atSeq: options.atSeq },
|
||||
increaseTitle: true,
|
||||
...onCreated === undefined ? {} : { onCreated },
|
||||
...options.allowMigration === undefined ? {} : { allowMigration: options.allowMigration },
|
||||
...options?.allowMigration === undefined ? {} : { allowMigration: options.allowMigration },
|
||||
})
|
||||
if (options.present === 'selected') this.openSession(childId)
|
||||
return childId
|
||||
}
|
||||
|
||||
startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions): void {
|
||||
|
||||
@@ -73,11 +73,6 @@ async function bench(access: 'fake' | 'static' = 'fake') {
|
||||
const whenResourceAccess = vi.fn((): () => void => stopGuard)
|
||||
if (access === 'static') new StaticRemoteAccess(ctx)
|
||||
else ctx.provide('remoteAccess', { source: accessSource, whenAccess: vi.fn(() => () => {}), whenResourceAccess })
|
||||
// The Session kind's source owner: navigation reads `session/rename` through this service.
|
||||
ctx.provide('uiSession', {
|
||||
access: (sessionId: string, action: string) =>
|
||||
ctx.get('remoteAccess')?.source('session', sessionId, action).getSnapshot() ?? true,
|
||||
} as never)
|
||||
const create = vi.fn(async (input: { name: string } | { path: string }) => ({
|
||||
workspaceId: 'ws-new' as never,
|
||||
path: 'name' in input ? `/projects/${input.name}` : input.path,
|
||||
@@ -129,19 +124,7 @@ async function bench(access: 'fake' | 'static' = 'fake') {
|
||||
// keeps its identity between reads.
|
||||
let workspaceSnapshot = workspaceState([])
|
||||
let sessionSnapshot = sessionState([])
|
||||
const listeners = new Set<() => void>()
|
||||
const subscribe = (listener: () => void): (() => void) => {
|
||||
listeners.add(listener)
|
||||
return () => { listeners.delete(listener) }
|
||||
}
|
||||
const setSummary = (sessionId: string, facts: Partial<SessionSummary>): void => {
|
||||
const byId = {
|
||||
...sessionSnapshot.byId,
|
||||
[sessionId]: { ...summary(sessionId, 0), ...sessionSnapshot.byId[sessionId as SessionId], ...facts },
|
||||
}
|
||||
sessionSnapshot = { ...sessionSnapshot, ids: Object.keys(byId) as SessionId[], byId }
|
||||
for (const listener of [...listeners]) listener()
|
||||
}
|
||||
const subscribe = () => () => {}
|
||||
const workspacesSubscribe = vi.fn(subscribe)
|
||||
const initializeDefault = vi.fn(async (): Promise<WorkspaceView | undefined> => undefined)
|
||||
ctx.provide('workspaces', {
|
||||
@@ -184,7 +167,7 @@ async function bench(access: 'fake' | 'static' = 'fake') {
|
||||
return {
|
||||
ctx, slots: ctx.get('slots') as SlotRegistry, locale, create, rename, commands, accessSource, whenResourceAccess, stopGuard,
|
||||
retain, using, selectPanel, search, renameSession, binding, fork, pickDirectory, pinSession, unpinSession,
|
||||
workspacesSubscribe, setSummary, initializeDefault, history, scope, sessionOf,
|
||||
workspacesSubscribe, initializeDefault, history, scope, sessionOf,
|
||||
setWorkspaces: (snapshot: WorkspaceSnapshot): void => { workspaceSnapshot = snapshot },
|
||||
setSessions: (snapshot: SessionListState): void => { sessionSnapshot = snapshot },
|
||||
}
|
||||
@@ -240,7 +223,6 @@ describe('ui-workspace apply', () => {
|
||||
it('declares the services it drives', () => {
|
||||
expect(inject).toEqual([
|
||||
'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', 'layout', 'shortcuts', 'remoteAccess',
|
||||
'uiSession',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -349,8 +331,7 @@ describe('ui-workspace apply', () => {
|
||||
expect(rowToastEntry.store).toBeUndefined()
|
||||
expect(b.slots.entries('workspace.toast').map(row => row.options.key)).toEqual([
|
||||
'sessionIdCopied', 'sessionIdCopyFailed', 'pinFailed', 'unpinFailed', 'archivedNotOpenable',
|
||||
'defaultWorkspaceFailed', 'shortcutForkFailed', 'createFailed', 'forkRequiresOpen',
|
||||
'archived', 'stoppedAndArchived',
|
||||
'defaultWorkspaceFailed', 'shortcutForkFailed', 'createFailed', 'forkRequiresOpen', 'archived', 'stoppedAndArchived',
|
||||
])
|
||||
for (const registration of b.slots.entries('workspace.toast')) {
|
||||
expect(registration.locale).toBe('workspace')
|
||||
@@ -875,97 +856,6 @@ describe('ui-workspace apply', () => {
|
||||
expect(b.create).toHaveBeenCalledWith({ path: '/tmp/project' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Answer as a standalone reader page's access snapshot does: it may branch the one Session the
|
||||
* share grants and may not rename that source Session.
|
||||
* @param b - Benchmark whose access source answers.
|
||||
*/
|
||||
function provideReaderAnswers(b: Awaited<ReturnType<typeof bench>>): void {
|
||||
b.accessSource.mockImplementation((_kind, id, endpoint) =>
|
||||
createSnapshotStore(endpoint === 'session/fork' && id === 'session'))
|
||||
}
|
||||
|
||||
it('names a reader page\'s branch and opens it immediately when the caller selects it', async () => {
|
||||
const b = await bench()
|
||||
onTestFinished(() => b.ctx.fiber.dispose())
|
||||
provideReaderAnswers(b)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
await b.ctx.uiWorkspace.forkSession(sid('session'), undefined, { present: 'selected' })
|
||||
|
||||
// The child inherits a title from the fork itself; the reader's answer for the source says nothing
|
||||
// about the child it now owns.
|
||||
expect(b.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: sid('session'), increaseTitle: true })
|
||||
expect(b.retain).toHaveBeenCalledExactlyOnceWith('forked', { source: 'mainView' })
|
||||
expect(b.selectPanel).toHaveBeenCalledExactlyOnceWith(null)
|
||||
})
|
||||
|
||||
it('leaves the selection alone when the caller presents the branch as listed', async () => {
|
||||
const b = await bench()
|
||||
onTestFinished(() => b.ctx.fiber.dispose())
|
||||
provideReaderAnswers(b)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
b.ctx.uiWorkspace.openSession(sid('source'))
|
||||
b.retain.mockClear()
|
||||
b.selectPanel.mockClear()
|
||||
|
||||
await b.ctx.uiWorkspace.forkSession(sid('session'), undefined, { present: 'listed' })
|
||||
|
||||
expect(b.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: sid('session'), increaseTitle: true })
|
||||
expect(b.retain).not.toHaveBeenCalled()
|
||||
expect(b.selectPanel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forks the reader main Session by shortcut with a child title and leaves the selection alone', async () => {
|
||||
const b = await bench()
|
||||
onTestFinished(() => b.ctx.fiber.dispose())
|
||||
provideReaderAnswers(b)
|
||||
b.setSessions(sessionState([{ ...summary('session', 1), retainedBy: { mainView: 1 } }]))
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const resolution = b.commands.get('session.fork')!.resolve({ region: 'page', modal: null, target: null })
|
||||
if (resolution.status !== 'handled') throw new Error('the reader may branch its main Session')
|
||||
resolution.run()
|
||||
await vi.waitFor(() => {
|
||||
expect(b.fork).toHaveBeenCalledExactlyOnceWith({ sessionId: sid('session'), increaseTitle: true })
|
||||
})
|
||||
expect(b.retain).not.toHaveBeenCalled()
|
||||
expect(b.selectPanel).not.toHaveBeenCalled()
|
||||
expect(b.renameSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not open or retry a reader fork refused for migration until its source notice is used', async () => {
|
||||
const b = await bench()
|
||||
onTestFinished(() => b.ctx.fiber.dispose())
|
||||
provideReaderAnswers(b)
|
||||
declare(b.slots, 'sidebar.workspaces', 'shell.overlay')
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
b.fork.mockRejectedValueOnce(new SessionForkError(
|
||||
new RemoteError('session/migration-required', 'migration required', { sessionId: sid('session') }),
|
||||
sid('session'),
|
||||
))
|
||||
const fork = faceOf(entry(b.slots, MENU_ITEM, 'fork')) as ForkSessionInjected
|
||||
const toast = faceOf(entry(b.slots, 'shell.overlay', 'workspace.row-toast')) as RowToastInjected
|
||||
|
||||
fork.forkSession(sid('session'))
|
||||
await vi.waitFor(() => {
|
||||
expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'forkRequiresOpen', sessionId: 'session', seq: 1 })
|
||||
})
|
||||
|
||||
expect(b.fork).toHaveBeenCalledExactlyOnceWith({
|
||||
sessionId: 'session', increaseTitle: true, allowMigration: false, onCreated: b.fork.mock.calls[0]?.[0].onCreated,
|
||||
})
|
||||
expect(b.fork.mock.calls[0]?.[0].onCreated).toBeTypeOf('function')
|
||||
expect(b.retain).not.toHaveBeenCalled()
|
||||
expect(b.renameSession).not.toHaveBeenCalled()
|
||||
expect(b.selectPanel).not.toHaveBeenCalled()
|
||||
toast.dismissToast()
|
||||
;(faceOf(b.slots.entries('workspace.toast').find(row => row.options.key === 'forkRequiresOpen')!) as ToastDisplayInjected<'forkRequiresOpen'>)
|
||||
.display({ kind: 'forkRequiresOpen', sessionId: sid('session'), seq: 1 }, b.locale.bind('workspace'), toast.dismissToast).actions![0]!.onClick()
|
||||
expect(b.retain).toHaveBeenCalledExactlyOnceWith('session', { source: 'mainView' })
|
||||
expect(b.fork).toHaveBeenCalledOnce()
|
||||
expect(b.renameSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('declares the browser child slots and reports directory-flow occupancy per surface', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots, 'sidebar.workspaces', 'conversation.hero.workspace')
|
||||
|
||||
@@ -285,11 +285,6 @@ function bench(options: BenchOptions = {}) {
|
||||
locale.setLocale('en')
|
||||
ctx.effect(() => locale.register('workspace', { en, zh }))
|
||||
ctx.provide('locale', locale)
|
||||
// The owner's access snapshot allows every endpoint, so forks name their child.
|
||||
const allowed = createSnapshotStore(true)
|
||||
ctx.provide('remoteAccess', { source: () => allowed })
|
||||
// ui-session owns the Session kind's source; navigation asks it for the rename answer.
|
||||
ctx.provide('uiSession', { access: () => allowed.getSnapshot() } as never)
|
||||
const requestDraftInitialization = vi.fn<SessionInputResolver['requestDraftInitialization']>(() => 'applied')
|
||||
if (options.conversation !== false) ctx.provide('conversation', { input: { requestDraftInitialization } })
|
||||
const layout = new LayoutController({
|
||||
|
||||
@@ -348,11 +348,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
throws: ['on failure; a refused creation is also shown through the Workspace notice unless a later navigation or disposal superseded the request.'],
|
||||
},
|
||||
{
|
||||
signature: 'forkSession( sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: ForkSessionOptions, ): Promise<SessionId>',
|
||||
description: 'Fork a Session and apply its branch presentation policy. The child always receives the incremented title: whoever may fork a Session also holds `session/rename` on the child it creates, so the source\'s own rename answer does not matter.',
|
||||
parameters: [{ name: 'sessionId', description: 'source Session.' }, { name: 'onCreated', description: 'observer after creation, before the child-title update.' }, { name: 'options', description: 'branch boundary, migration permission, and how the child is presented.' }],
|
||||
signature: 'forkSession( sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: Pick<Parameters<ISessions[\'fork\']>[0], \'allowMigration\'>, ): Promise<SessionId>',
|
||||
description: 'Fork a Session without changing the current selection.',
|
||||
parameters: [{ name: 'sessionId', description: 'source Session.' }, { name: 'onCreated', description: 'observer before the optional child-title update.' }, { name: 'options', description: 'optional permission to start source migration; omission preserves the Host default.' }],
|
||||
returns: 'the child SessionId after creation and inherited-title increment.',
|
||||
throws: ['when the Host refuses or fails the fork; the caller keeps its current view and decides whether to report the failure.'],
|
||||
},
|
||||
{
|
||||
signature: 'connectWorkspace(workspaceId: WorkspaceId, options?: Pick<StartSessionOptions, \'agentPreset\'>): Promise<SessionId>',
|
||||
@@ -632,10 +631,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'FontSizes',
|
||||
declaration: 'export type FontSizes = Readonly<Record<FontRole, number>>;',
|
||||
},
|
||||
{
|
||||
name: 'ForkSessionOptions',
|
||||
declaration: 'export interface ForkSessionOptions extends Pick<Parameters<ISessions[\'fork\']>[0], \'atSeq\' | \'allowMigration\'> {\n readonly present?: \'selected\' | \'listed\';\n}',
|
||||
},
|
||||
{
|
||||
name: 'GlobalStandardProps',
|
||||
declaration: 'export interface GlobalStandardProps {\n}',
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Client Cordis inspect catalog', () => {
|
||||
.toEqual([
|
||||
'openSession(target: SessionTarget): void',
|
||||
'openWorkspace( workspaceId: WorkspaceId, beforeOpen?: (sessionId: SessionId) => void, options?: Pick<StartSessionOptions, \'agentPreset\'>, ): Promise<void>',
|
||||
'forkSession( sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: ForkSessionOptions, ): Promise<SessionId>',
|
||||
'forkSession( sessionId: SessionId, onCreated?: (childId: SessionId) => void, options?: Pick<Parameters<ISessions[\'fork\']>[0], \'allowMigration\'>, ): Promise<SessionId>',
|
||||
'connectWorkspace(workspaceId: WorkspaceId, options?: Pick<StartSessionOptions, \'agentPreset\'>): Promise<SessionId>',
|
||||
'startSession(workspaceId?: WorkspaceId, options?: StartSessionOptions): void',
|
||||
'archiveSession(sessionId: SessionId, options?: { readonly stopActivity?: boolean }): Promise<void>',
|
||||
|
||||
@@ -429,7 +429,7 @@
|
||||
"a7d8cc8d42e37ad5cc4882241492ac572ed8163c7fb8663180e893c7178e906f": 1
|
||||
},
|
||||
"packages/client/ui-chat/tests/apply-inject.client.spec.tsx": {
|
||||
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 1
|
||||
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 2
|
||||
},
|
||||
"packages/client/ui-chat/tests/approval-command.client.spec.tsx": {
|
||||
"84e41ecee24d2aa283a30e2cd2c08fb3ec72999d7db5bbac68a0fa87021a91b7": 1
|
||||
|
||||
Reference in new issue
Block a user