dsh-better-display 0.3.0 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.en.md +13 -15
- package/README.md +13 -15
- package/compat/harness-rc2.json +58 -18
- package/docs/official-rendering-bridge.md +1 -1
- package/lib/client.js +2227 -4059
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +4 -2
- package/lib/types/client/OfficialContent.d.ts.map +1 -1
- package/lib/types/client/OfficialContent.js +16 -5
- package/lib/types/client/OfficialContent.js.map +1 -1
- package/lib/types/client/Reader.d.ts.map +1 -1
- package/lib/types/client/Reader.js +6 -7
- package/lib/types/client/Reader.js.map +1 -1
- package/lib/types/client/ToolActivity.js +3 -3
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/markdown/render.d.ts.map +1 -1
- package/lib/types/client/markdown/render.js +2 -1
- package/lib/types/client/markdown/render.js.map +1 -1
- package/lib/types/client/native/ContextInjectionRow.d.ts +2 -2
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -1
- package/lib/types/client/native/ContextInjectionRow.js +16 -4
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -1
- package/lib/types/client/native/ReferenceIcon.js +3 -3
- package/lib/types/client/native/ReferenceIcon.js.map +1 -1
- package/lib/types/client/native/tool-call-model.js +1 -1
- package/lib/types/client/native/tool-call-model.js.map +1 -1
- package/lib/types/client/native/tool-update.d.ts +23 -0
- package/lib/types/client/native/tool-update.d.ts.map +1 -0
- package/lib/types/client/native/tool-update.js +22 -0
- package/lib/types/client/native/tool-update.js.map +1 -0
- package/lib/types/client/official-slots.js +1 -1
- package/lib/types/client/official-slots.js.map +1 -1
- package/lib/types/client/platform-media.d.ts +1 -1
- package/lib/types/client/platform-media.d.ts.map +1 -1
- package/lib/types/client/platform-media.js +9 -4
- package/lib/types/client/platform-media.js.map +1 -1
- package/lib/types/client/primitive-labels.d.ts.map +1 -1
- package/lib/types/client/primitive-labels.js +3 -2
- package/lib/types/client/primitive-labels.js.map +1 -1
- package/lib/types/client/tool-activity.js +2 -2
- package/lib/types/client/tool-activity.js.map +1 -1
- package/package.json +22 -18
- package/scripts/link-harness-dependencies.mjs +2 -0
- package/scripts/verify-official-bridge.mjs +6 -4
- package/scripts/verify-tool-shimmer.mjs +62 -0
- package/src/client/OfficialContent.tsx +19 -7
- package/src/client/Reader.module.css +17 -2
- package/src/client/Reader.tsx +8 -9
- package/src/client/ToolActivity.tsx +3 -3
- package/src/client/index.tsx +2 -0
- package/src/client/markdown/render.tsx +2 -1
- package/src/client/native/ContextInjectionRow.module.css +4 -0
- package/src/client/native/ContextInjectionRow.tsx +32 -10
- package/src/client/native/ReferenceIcon.tsx +3 -3
- package/src/client/native/tool-call-model.ts +1 -1
- package/src/client/native/tool-update.ts +32 -0
- package/src/client/official-slots.tsx +1 -1
- package/src/client/platform-media.ts +9 -3
- package/src/client/primitive-labels.ts +3 -2
- package/src/client/tool-activity.ts +2 -2
- package/tests/browser-auto-fold.tsx +1 -1
- package/tests/browser-official-bridge.tsx +17 -9
- package/tests/context-tool-update.test.ts +33 -0
- package/tests/platform-media.test.ts +6 -0
- package/tests/stock-install.test.ts +17 -0
- package/tsdown.config.ts +7 -5
|
@@ -22,6 +22,7 @@ const paths = {
|
|
|
22
22
|
feedback: 'ui-message-feedback/src/client/index.ts', 'read-row': 'ui-tool/src/client/tool/toolviews/read-row.tsx',
|
|
23
23
|
'read-image-row': 'ui-tool/src/client/tool/toolviews/read-image-row.tsx', 'tool-locale': 'ui-conversation/src/client/locales.ts',
|
|
24
24
|
deliverables: 'ui-deliverables/src/client/Deliverables.tsx', 'deliverables-locale': 'ui-deliverables/src/client/locales.ts',
|
|
25
|
+
'open-in-app-action': 'ui-open-in-app/src/client/FileRouteAction.tsx', 'open-in-app-locale': 'ui-open-in-app/src/client/locales.ts',
|
|
25
26
|
};
|
|
26
27
|
const webRequire = createRequire(join(harness, 'packages/client/web/package.json'));
|
|
27
28
|
await build({
|
|
@@ -39,6 +40,7 @@ const pixel = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQ
|
|
|
39
40
|
const server = createServer(async (req, res) => {
|
|
40
41
|
if (req.url === '/') { res.setHeader('Content-Type', 'text/html'); res.end('<!doctype html><html lang="zh"><meta charset="utf-8"><link rel="stylesheet" href="/fixture.css"><style>body{font-family:system-ui;margin:24px}button{cursor:pointer}</style><div id="app"></div><script type="module" src="/fixture.js"></script></html>'); return; }
|
|
41
42
|
if (req.url?.startsWith('/api/file?') || req.url === '/pixel.png') { res.setHeader('Content-Type', 'image/png'); res.end(pixel); return; }
|
|
43
|
+
if (req.url?.startsWith('/api/present.open')) { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify([{ id: 'preview-app', name: 'Preview App', default: true, icon: null }, { id: 'other-app', name: 'Other App', default: false, icon: null }])); return; }
|
|
42
44
|
const file = req.url === '/fixture.js' ? 'fixture.js' : req.url === '/fixture.css' ? 'fixture.css' : null;
|
|
43
45
|
if (!file) { res.writeHead(404); res.end(); return; }
|
|
44
46
|
res.setHeader('Content-Type', file.endsWith('.js') ? 'text/javascript' : 'text/css');
|
|
@@ -90,11 +92,11 @@ try {
|
|
|
90
92
|
await tool.getByText('export const value = 1;', { exact: true }).waitFor();
|
|
91
93
|
assert.equal(await page.locator('[data-presented-file]').count(), 1);
|
|
92
94
|
assert.equal(await page.locator('[data-produced-files-row]').count(), 0);
|
|
93
|
-
await page.getByRole('button', { name: '
|
|
95
|
+
await page.getByRole('button', { name: '在侧边栏预览 /fixture/report.pdf', exact: true }).click();
|
|
94
96
|
assert.equal(await page.evaluate(() => window.fixture.observations.opens.at(-1).path), '/fixture/report.pdf');
|
|
95
|
-
await page.getByRole('button', { name: '
|
|
96
|
-
await page.getByRole('menuitem', { name: '
|
|
97
|
-
assert.deepEqual(await page.evaluate(() => window.fixture.observations.opens.at(-1)), ['presented', 'session-a', 42, 0, 'open']);
|
|
97
|
+
await page.getByRole('button', { name: '更多打开方式', exact: true }).click();
|
|
98
|
+
await page.getByRole('menuitem', { name: 'Other App', exact: true }).click();
|
|
99
|
+
assert.deepEqual(await page.evaluate(() => window.fixture.observations.opens.at(-1)), ['presented', 'session-a', 42, 0, 'open', 'other-app']);
|
|
98
100
|
await page.evaluate(() => window.fixture.language());
|
|
99
101
|
await page.getByRole('button', { name: 'Good response', exact: true }).waitFor();
|
|
100
102
|
const image = page.locator('img[alt="本地样例"]');
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Offline pixel regression for the shipped tool-summary CSS; no Host or model. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const root = resolve(import.meta.dirname, '..');
|
|
9
|
+
const bundle = resolve(process.argv[2] ?? join(root, 'lib/client.js'));
|
|
10
|
+
const out = join(root, '.evidence/tool-shimmer');
|
|
11
|
+
await mkdir(out, { recursive: true });
|
|
12
|
+
const js = await readFile(bundle, 'utf8');
|
|
13
|
+
const line = js.split('\n').find(line => /const css\$\d+ = "\.\w+_root\{/.test(line));
|
|
14
|
+
assert.ok(line, 'Reader CSS must be present in the shipped bundle');
|
|
15
|
+
const css = JSON.parse(line.slice(line.indexOf('=') + 1).trim().replace(/;$/, ''));
|
|
16
|
+
const summary = css.match(/\.(\w+_nativeToolSummary)\[data-running\]/)?.[1];
|
|
17
|
+
assert.ok(summary);
|
|
18
|
+
const { launchPinnedChromium } = await import(pathToFileURL(join(homedir(), '.codex/playwright-runtime/runtime.mjs')).href);
|
|
19
|
+
const browser = await launchPinnedChromium();
|
|
20
|
+
const results = [];
|
|
21
|
+
try {
|
|
22
|
+
for (const theme of ['dark', 'light']) for (const width of [180, 800]) {
|
|
23
|
+
const page = await browser.newPage({ viewport: { width: 900, height: 120 }, deviceScaleFactor: 2 });
|
|
24
|
+
await page.setContent(`<style>${css}body{margin:20px;background:${theme === 'dark' ? '#141414' : '#fff'};--dsw-alias-label-tertiary:${theme === 'dark' ? '#aaa' : '#666'};font:14px system-ui}.row{display:flex;width:${width}px}</style><div class="row" data-motion="on"><span data-running class="${summary}">Re-run acceptance criteria verification</span></div>`);
|
|
25
|
+
const target = page.locator(`.${summary}`);
|
|
26
|
+
const capture = async time => {
|
|
27
|
+
await target.evaluate((el, time) => {
|
|
28
|
+
const animations = el.getAnimations();
|
|
29
|
+
if (animations.length !== 1) throw new Error(`Expected one animation, found ${animations.length}`);
|
|
30
|
+
animations[0].pause(); animations[0].currentTime = time;
|
|
31
|
+
}, time);
|
|
32
|
+
return target.screenshot();
|
|
33
|
+
};
|
|
34
|
+
const timing = await target.evaluate(el => el.getAnimations()[0].effect.getTiming().duration);
|
|
35
|
+
const first = await capture(0);
|
|
36
|
+
const middle = await capture(timing * .25);
|
|
37
|
+
const beforeWrap = await capture(timing - 1);
|
|
38
|
+
const afterWrap = await capture(timing);
|
|
39
|
+
assert.notDeepEqual(middle, first, 'The highlight must visibly travel across the text');
|
|
40
|
+
assert.deepEqual(beforeWrap, first, 'The sweep must fully leave the text before wrapping');
|
|
41
|
+
assert.deepEqual(afterWrap, first, 'The cycle boundary must preserve identical text pixels');
|
|
42
|
+
await writeFile(join(out, `${theme}-${width}-start.png`), first);
|
|
43
|
+
await writeFile(join(out, `${theme}-${width}-middle.png`), middle);
|
|
44
|
+
await writeFile(join(out, `${theme}-${width}-end.png`), beforeWrap);
|
|
45
|
+
const runningStyle = await target.evaluate(el => ({ repeat: getComputedStyle(el).backgroundRepeat, animationCount: el.getAnimations().length }));
|
|
46
|
+
assert.equal(runningStyle.repeat, 'no-repeat');
|
|
47
|
+
await page.locator('[data-motion]').evaluate(el => { el.dataset.motion = 'off'; });
|
|
48
|
+
assert.equal(await target.evaluate(el => el.getAnimations().length), 0, 'Reader motion-off must stop shimmer');
|
|
49
|
+
await page.locator('[data-motion]').evaluate(el => { el.dataset.motion = 'on'; });
|
|
50
|
+
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
51
|
+
const reduced = await target.evaluate(el => ({ count: el.getAnimations().length, fill: getComputedStyle(el).webkitTextFillColor }));
|
|
52
|
+
assert.equal(reduced.count, 0, 'System reduced motion must stop shimmer');
|
|
53
|
+
assert.notEqual(reduced.fill, 'rgba(0, 0, 0, 0)', 'Stopped animation must keep text readable');
|
|
54
|
+
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
55
|
+
await target.evaluate(el => el.removeAttribute('data-running'));
|
|
56
|
+
assert.equal(await target.evaluate(el => el.getAnimations().length), 0, 'Settled tools must not shimmer');
|
|
57
|
+
results.push({ theme, width, loopPixelsContinuous: true, ...runningStyle, motionOff: true, reducedMotion: true, settled: true });
|
|
58
|
+
await page.close();
|
|
59
|
+
}
|
|
60
|
+
await writeFile(join(out, 'result.json'), JSON.stringify({ bundle, results }, null, 2));
|
|
61
|
+
console.log(JSON.stringify({ passed: true, results }));
|
|
62
|
+
} finally { await browser.close(); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useSyncExternalStore, type ReactNode } from 'react';
|
|
2
|
-
import type { ChatConversationViewNode, ChatNodeOwnerProps, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
1
|
+
import { useCallback, useState, useSyncExternalStore, type ReactNode } from 'react';
|
|
2
|
+
import type { ChatConversationViewNode, ChatNodeOwnerProps, TurnTailOwnerProps, UseDisclosure } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
3
|
+
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
3
4
|
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
5
|
import type { AssistantActionOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
5
6
|
type MessageId = AssistantActionOwnerProps['messageId'];
|
|
@@ -13,26 +14,37 @@ export function OfficialActions({ official, messageId }: Pick<BlockRenderProps,
|
|
|
13
14
|
</span>;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Each atomic Tool view owns its disclosure state through this stable Hook prop. */
|
|
18
|
+
const useToolDisclosure: UseDisclosure = () => {
|
|
19
|
+
const [expanded, setExpanded] = useState(false);
|
|
20
|
+
const toggle = useCallback(() => setExpanded(previous => !previous), []);
|
|
21
|
+
return { expanded, setExpanded, toggle };
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
export function OfficialTool({ official, block, toolName, cwd, openFile, fallback }: BlockRenderProps & {
|
|
17
25
|
official: NonNullable<BlockRenderProps['official']>; block: ToolCallBlock; toolName: string; fallback: ReactNode;
|
|
18
26
|
}) {
|
|
19
27
|
const home = useSyncExternalStore(official.officialHost.subscribe, official.officialHost.getSnapshot).home;
|
|
20
28
|
return <div data-reader-tool-official>
|
|
21
|
-
{official.renderSlot(OFFICIAL_SEATS.tools, {
|
|
22
|
-
callId: block.callId, toolName,
|
|
29
|
+
{(official.renderSlot as (key: string, owner: object, options: object) => ReactNode)(OFFICIAL_SEATS.tools, {
|
|
30
|
+
callId: block.callId, toolName,
|
|
31
|
+
phase: 'kind' in block ? 'result' : block.phase, block, cwd, home,
|
|
23
32
|
openFile: openFile ?? (() => {}),
|
|
24
33
|
loadImage: official.officialImageLoader,
|
|
34
|
+
useDisclosure: useToolDisclosure,
|
|
25
35
|
inspect: () => official.openView('trajectory', block.callId),
|
|
26
|
-
}, { entryKey: toolName, fallback })}
|
|
36
|
+
}, { entryKey: toolName, hookContext: { callId: block.callId, assistant: undefined }, fallback })}
|
|
27
37
|
</div>;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & { node: ChatConversationViewNode; fallback: ReactNode }) {
|
|
31
41
|
const official = render.official;
|
|
42
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
32
43
|
if (!official) return fallback;
|
|
33
44
|
const owner: ChatNodeOwnerProps = {
|
|
34
45
|
cwd: render.cwd,
|
|
35
46
|
openFile: render.openFile ?? (() => {}),
|
|
47
|
+
openSkill: () => {},
|
|
36
48
|
forkAt: render.forkAt ?? (() => {}),
|
|
37
49
|
inspectCall: callId => official.openView('trajectory', callId),
|
|
38
50
|
loadImage: official.officialImageLoader,
|
|
@@ -45,13 +57,13 @@ export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & {
|
|
|
45
57
|
// The runtime node domain is open; the public SlotMap enumerates the known kinds.
|
|
46
58
|
const renderNode = official.renderSlot as unknown as (key: string, owner: object, options: object) => ReactNode;
|
|
47
59
|
return <div data-reader-official-node={node.kind} data-chat-anchor-key={node.key} data-chat-flow-kind={node.kind}>
|
|
48
|
-
{renderNode(OFFICIAL_SEATS.nodes, { ...owner, node }, { entryKey: node.kind, hookContext: turn?.data, fallback })}
|
|
60
|
+
{renderNode(OFFICIAL_SEATS.nodes, { ...owner, node }, { entryKey: node.kind, hookContext: { turnData: turn?.data, disclosureReset }, fallback })}
|
|
49
61
|
</div>;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
export function OfficialTail({ official, owner, produced }: Pick<BlockRenderProps, 'official'> & { owner?: TurnTailOwnerProps; produced: readonly string[] }) {
|
|
53
65
|
const tailOwner = owner && official ? { ...owner, openFile: official.officialPreviewFile, readerProducedPaths: produced } : undefined;
|
|
54
66
|
return official && tailOwner ? <div data-reader-official-tail style={{ display: 'contents' }}>
|
|
55
|
-
{official.
|
|
67
|
+
{official.renderSlot(OFFICIAL_SEATS.tail, tailOwner)}
|
|
56
68
|
</div> : null;
|
|
57
69
|
}
|
|
@@ -250,13 +250,16 @@
|
|
|
250
250
|
currentColor 0%, currentColor 35%,
|
|
251
251
|
color-mix(in srgb, currentColor 35%, transparent) 50%,
|
|
252
252
|
currentColor 65%, currentColor 100%);
|
|
253
|
-
|
|
253
|
+
/* Keep the travelling band outside the text at both loop endpoints. A single
|
|
254
|
+
tile covers the row throughout, so wrapping cannot start a second sweep. */
|
|
255
|
+
background-size: 300% 100%;
|
|
256
|
+
background-repeat: no-repeat;
|
|
254
257
|
background-position: 100% 0;
|
|
255
258
|
-webkit-background-clip: text; background-clip: text;
|
|
256
259
|
-webkit-text-fill-color: transparent;
|
|
257
260
|
animation: readerStepShimmer 1.6s linear infinite;
|
|
258
261
|
}
|
|
259
|
-
@keyframes readerStepShimmer { to { background-position:
|
|
262
|
+
@keyframes readerStepShimmer { to { background-position: 0% 0; } }
|
|
260
263
|
/* The duration stays on a step that ran long, so a slow command keeps its number;
|
|
261
264
|
a fast one shows nothing once it returns. */
|
|
262
265
|
.toolElapsed[data-slow] { opacity: 1; font-weight: 500;
|
|
@@ -538,8 +541,19 @@
|
|
|
538
541
|
pinned straight on top of it. Give each lane its own band instead, stacked
|
|
539
542
|
from the measured top: status first, then the summary, then the fold body. */
|
|
540
543
|
.turnProcessSticky { box-sizing: border-box; width: calc(100% - var(--reader-control-width, 148px)); top: max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)); min-height: 40px; padding-right: 0; z-index: 6; border-bottom: 1px solid transparent; background-clip: padding-box; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
544
|
+
/* A pinned lane's in-flow gap stays visible as an uncovered scrollport strip
|
|
545
|
+
while the lane itself paints opaque. Cast each plate upward over its strip
|
|
546
|
+
(the .turn 14px gap / the prior cell's 16px flowCellInner padding) so the
|
|
547
|
+
lanes read as one band instead of solid plates with notches between them.
|
|
548
|
+
The status lane reserves --reader-control-width on its right for the
|
|
549
|
+
toolbar's control group, which leaves the same kind of strip there; paint
|
|
550
|
+
it too. The toolbar is z-index 7, above the lane's 6, so its buttons still
|
|
551
|
+
render on top of the extension. */
|
|
552
|
+
.turnProcessSticky { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base), var(--reader-control-width, 148px) 0 0 var(--dsw-alias-bg-base); }
|
|
541
553
|
/* The same summary row stays in flow until it reaches the measured top lane. */
|
|
542
554
|
.flowCell[data-flow-summary], .closedProcessSummary { position: sticky; top: calc(max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)) + var(--reader-sticky-lane, 40px)); z-index: 5; overflow: visible; box-sizing: border-box; min-height: 40px; padding-block: 4px; background: var(--dsw-alias-bg-base); border-bottom: 0; }
|
|
555
|
+
.flowCell[data-flow-summary] { box-shadow: 0 -16px 0 var(--dsw-alias-bg-base); }
|
|
556
|
+
.closedProcessSummary { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base); }
|
|
543
557
|
.foldControl { display: inline-flex; align-items: center; gap: 0; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-module-platform); }
|
|
544
558
|
.foldStop { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); padding: 2px 8px; border-radius: 6px; white-space: nowrap; font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }
|
|
545
559
|
.foldStop[data-on] { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }
|
|
@@ -573,6 +587,7 @@
|
|
|
573
587
|
a glass reader keeps exactly the same footer precedence as an opaque one. */
|
|
574
588
|
.root[data-reader-glass] .flowCell[data-flow-summary],
|
|
575
589
|
.root[data-reader-glass] .closedProcessSummary {
|
|
590
|
+
box-shadow: none;
|
|
576
591
|
position: static;
|
|
577
592
|
z-index: auto;
|
|
578
593
|
min-height: 32px;
|
package/src/client/Reader.tsx
CHANGED
|
@@ -272,8 +272,7 @@ function subagentCount(snapshot: { nodes: { get(key: string): ChatConversationVi
|
|
|
272
272
|
return 0;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
function GroupStatus({ group, sessionId, useChat,
|
|
276
|
-
const pending = useSessionPendingInteraction(snapshot => snapshot.get(sessionId));
|
|
275
|
+
function GroupStatus({ group, sessionId, useChat, pending, motion }: Pick<ReaderProps, 'sessionId' | 'useChat'> & { group: ReaderGroup; motion: boolean; pending: { kind?: string } | undefined }) {
|
|
277
276
|
const text = useChat(snapshot => {
|
|
278
277
|
const turn = group.turn === null ? undefined : snapshot.timeline.turns.get(group.turn);
|
|
279
278
|
if (turn?.status === 'closed') {
|
|
@@ -477,7 +476,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
477
476
|
const snapshot = props.useChat(snapshot => snapshot);
|
|
478
477
|
const nodes = snapshot.nodes;
|
|
479
478
|
const turn = group.turn === null ? undefined : snapshot.timeline.turns.get(group.turn);
|
|
480
|
-
const interaction = props.
|
|
479
|
+
const interaction = props.useSessionStatus(snapshot => snapshot.get(props.sessionId)?.pendingInteraction);
|
|
481
480
|
const sessionRunning = props.useSession(snapshot => snapshot.running);
|
|
482
481
|
const boundary = useMemo(() => boundaryOf(turn), [turn]);
|
|
483
482
|
const choiceKey = processChoiceKey(group.key, boundary);
|
|
@@ -538,8 +537,8 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
538
537
|
const metrics = useMemo(() => ({
|
|
539
538
|
usage: tailData?.tokenUsage,
|
|
540
539
|
runMs,
|
|
541
|
-
tokensPerSecond: tailData?.tokensPerSecond,
|
|
542
|
-
ttftMs: tailData?.ttftMs,
|
|
540
|
+
tokensPerSecond: (tailData as { tokensPerSecond?: number } | undefined)?.tokensPerSecond,
|
|
541
|
+
ttftMs: (tailData as { ttftMs?: number } | undefined)?.ttftMs,
|
|
543
542
|
endedAt: tailData?.closing?.time ?? turn?.end?.time,
|
|
544
543
|
}), [tailData, runMs, turn?.end?.time]);
|
|
545
544
|
const forkSeq = forkAnchorSeq([tailData?.closing?.finalNode]);
|
|
@@ -594,7 +593,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
594
593
|
{startsWithUser && <BlockBoundary><MainNode {...shared} useChat={props.useChat} boundary={boundary} nodeKey={group.keys[0]} /></BlockBoundary>}
|
|
595
594
|
{hasProcess && !isAwaitingModel && <StickyLane kind="status" className={css.turnProcessSticky}>
|
|
596
595
|
<Disclosure open={expanded} onChange={setExpanded} controls={flowId} buttonRef={processButton}
|
|
597
|
-
label={<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat}
|
|
596
|
+
label={<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />} />
|
|
598
597
|
</StickyLane>}
|
|
599
598
|
{boundary.status === 'closed' && hasProcess && autoFold && <ClosedProcessSummary open={expanded} onChange={setExpanded} controls={flowId}
|
|
600
599
|
steps={steps.filter(step => {
|
|
@@ -610,7 +609,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
610
609
|
onOpenChange={(key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }} renderStep={renderStep} />
|
|
611
610
|
{/* 状态指示永远排在流程之后:AI 的响应永远出现在最新消息(含补充消息)的下方 */}
|
|
612
611
|
{!hasProcess && boundary.status === 'open' && !isAwaitingModel && <div className={css.disclosure} data-reader-status-only>
|
|
613
|
-
<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat}
|
|
612
|
+
<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />
|
|
614
613
|
</div>}
|
|
615
614
|
{showDeliverablesRow(boundary.status, deliverables) && <DeliverablesRow deliverables={deliverables} openFile={props.openFile} revealFile={props.revealFile} openMode={openMode} />}
|
|
616
615
|
{boundary.status === 'closed' && <BlockBoundary><OfficialTail official={official} owner={tailOwner} produced={deliverables} /></BlockBoundary>}
|
|
@@ -625,7 +624,7 @@ export function Reader(props: ReaderProps) {
|
|
|
625
624
|
const nodes = props.useChat(snapshot => snapshot.nodes);
|
|
626
625
|
const timeline = props.useChat(snapshot => snapshot.timeline);
|
|
627
626
|
const running = props.useSession(snapshot => snapshot.running);
|
|
628
|
-
const pending = props.
|
|
627
|
+
const pending = props.useSessionStatus(snapshot => snapshot.get(props.sessionId)?.pendingInteraction);
|
|
629
628
|
const openError = props.useSession(snapshot => snapshot.openError);
|
|
630
629
|
const loading = props.useSession(snapshot => snapshot.openState === 'loading');
|
|
631
630
|
const hasMore = props.useSession(snapshot => snapshot.hasMore);
|
|
@@ -816,7 +815,7 @@ export function Reader(props: ReaderProps) {
|
|
|
816
815
|
|
|
817
816
|
// ChatView publishes data-chat-flow="" on its column. Skins treat a
|
|
818
817
|
// scrollport without that hook as inspect-only and hide [data-composer-seat].
|
|
819
|
-
return <StreamMotionContext.Provider value={streamMotion}><div ref={root} className={css.root} data-reader-build="0.3.
|
|
818
|
+
return <StreamMotionContext.Provider value={streamMotion}><div ref={root} className={css.root} data-reader-build="0.3.4" data-dsh-better-display="0.3.4" data-reader-wait-clock-version="input-v1" data-reader-wait-start={waitAnchor.time ?? undefined} data-motion={motion ? 'on' : 'off'} data-reader-glass={frostedGlass || undefined} data-reader-auto-fold={autoFold ? 'on' : 'off'}>
|
|
820
819
|
<TimelineRail items={timelineItems} activeTurn={activeTurn} busyTurn={busyTurn} onNavigate={onNavigateTurn} />
|
|
821
820
|
<div className={css.column} data-chat-flow="">
|
|
822
821
|
<StickyLane kind="toolbar" className={css.toolbar}>
|
|
@@ -2,7 +2,7 @@ import { memo, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import type { ToolCallBlock, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
3
3
|
import type { DiffHunk, ReadBlockLine, SearchFileGroup } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { DiffBlock, DisclosureRow, JsonTree, ReadBlock, SearchBlock, TerminalBlock, WebBlock,
|
|
5
|
-
|
|
5
|
+
IconApiOutlineRegular, IconBrowseOutlineRegular, IconEditOutlineRegular, IconSearchOutlineRegular, IconSkillOutlineRegular, IconSparkleRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
6
6
|
import { Blocks, contentBlocks } from './Blocks.js';
|
|
7
7
|
import { OfficialTool } from './OfficialContent.js';
|
|
8
8
|
import { ProcessFragment } from './motion.js';
|
|
@@ -15,7 +15,7 @@ import { diffBlockLabels, jsonTreeLabels, readBlockLabels, searchBlockLabels, te
|
|
|
15
15
|
import css from './Reader.module.css';
|
|
16
16
|
|
|
17
17
|
const LABEL: Record<ToolPhase, string> = { preparing: '输入生成中', running: '执行中', returned: '已返回', succeeded: '已完成', failed: '失败', interrupted: '已中断' };
|
|
18
|
-
const ICONS = { write:
|
|
18
|
+
const ICONS = { write: IconEditOutlineRegular, read: IconBrowseOutlineRegular, terminal: IconApiOutlineRegular, search: IconSearchOutlineRegular, web: IconSearchOutlineRegular, code: IconApiOutlineRegular, other: IconSparkleRegular } satisfies Record<ToolCategory, unknown>;
|
|
19
19
|
const number = new Intl.NumberFormat('zh-CN');
|
|
20
20
|
const language = (path: string | undefined) => path?.split('.').at(-1);
|
|
21
21
|
const duration = (ms: number) => ms < 1000 ? `${Math.round(ms)} 毫秒` : `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} 秒`;
|
|
@@ -179,7 +179,7 @@ export const ToolActivity = memo(function ToolActivityView({ entry, motion, turn
|
|
|
179
179
|
return () => document.removeEventListener('selectionchange', track);
|
|
180
180
|
}, []);
|
|
181
181
|
const facts = executionFacts(entry.block);
|
|
182
|
-
const Icon = model.name === 'skill' ?
|
|
182
|
+
const Icon = model.name === 'skill' ? IconSkillOutlineRegular : ICONS[model.category];
|
|
183
183
|
const block = entry.block;
|
|
184
184
|
const native = block ? toolRowModel(model.name, block) : null;
|
|
185
185
|
const skillName = typeof model.args?.name === 'string' ? model.args.name.split('\n')[0] : model.raw.split('\n')[0];
|
package/src/client/index.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client';
|
|
2
3
|
import type {} from '@deepseek-ai/dsh-api-session-controller/client';
|
|
4
|
+
import type {} from '@deepseek-ai/dsh-client-connection/client';
|
|
3
5
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
6
|
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
|
|
5
7
|
import * as workspacePathPkg from '@deepseek-ai/dsh-util-workspace-path';
|
|
@@ -524,7 +524,8 @@ function renderImage(url: string, alt: string, key: Key, context: MarkdownRender
|
|
|
524
524
|
let imageSrc = remote
|
|
525
525
|
if (rewritten !== undefined) {
|
|
526
526
|
try {
|
|
527
|
-
|
|
527
|
+
const parsed = new URL(rewritten)
|
|
528
|
+
if (['http:', 'https:', 'blob:', 'data:'].includes(parsed.protocol) || rewritten.startsWith('dsh-app://app/api/file?')) imageSrc = rewritten
|
|
528
529
|
} catch { /* A vocabulary result must be an absolute URL. */ }
|
|
529
530
|
}
|
|
530
531
|
if (imageSrc === undefined) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import type { ContextMessageNode } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
3
3
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
4
|
-
import { DisclosureRow,
|
|
4
|
+
import { DisclosureRow, IconBrowseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
5
|
import { ReferenceIcon } from './ReferenceIcon.js'
|
|
6
6
|
import { contextBody } from './ContextBody.js'
|
|
7
|
+
import { toolUpdateRows } from './tool-update.js'
|
|
7
8
|
import css from './ContextInjectionRow.module.css'
|
|
8
9
|
|
|
9
10
|
/** Props for the logged non-user message presentation. */
|
|
@@ -18,9 +19,9 @@ export interface ContextInjectionRowProps {
|
|
|
18
19
|
* optional here and resolved below, so the row renders on either host instead
|
|
19
20
|
* of throwing on `undefined.role` and degrading its whole block boundary.
|
|
20
21
|
*/
|
|
21
|
-
provenance?: ContextMessageNode['
|
|
22
|
+
provenance?: ContextMessageNode['producer'] | null
|
|
22
23
|
/** Alpha-generation spelling of {@link provenance}. */
|
|
23
|
-
producer?: ContextMessageNode['
|
|
24
|
+
producer?: ContextMessageNode['producer'] | null
|
|
24
25
|
/** Producer-declared information form; null renders the opaque body. */
|
|
25
26
|
form: ContextMessageNode['form']
|
|
26
27
|
/** The owning view's locale seat, passed down as a plain prop. */
|
|
@@ -46,16 +47,32 @@ export function ContextInjectionRow({ content, source, provenance, producer, for
|
|
|
46
47
|
// Resolved rather than declared: a form whose fields are unreadable renders
|
|
47
48
|
// the opaque body, and the marker must say what the row actually shows.
|
|
48
49
|
const { rendered, summary, body } = contextBody(form, { content, source, t })
|
|
50
|
+
const tools = toolUpdateRows(content)
|
|
51
|
+
const toolSummary = tools === null || tools.single !== null ? null
|
|
52
|
+
: tools.added.length > 0 && tools.removed.length > 0
|
|
53
|
+
? t('message.toolsChanged', { added: tools.added.length, removed: tools.removed.length })
|
|
54
|
+
: tools.added.length > 0
|
|
55
|
+
? t('message.toolsAddedCount', { count: tools.added.length })
|
|
56
|
+
: t('message.toolsRemovedCount', { count: tools.removed.length })
|
|
49
57
|
|
|
50
58
|
return (
|
|
51
59
|
<DisclosureRow
|
|
52
60
|
className={css.root}
|
|
53
|
-
icon={view.role === 'recall'
|
|
61
|
+
icon={view.role === 'recall' && tools === null
|
|
54
62
|
? <span data-context-recall-icon><ReferenceIcon kind="session" /></span>
|
|
55
|
-
: <
|
|
63
|
+
: <IconBrowseOutlineRegular size={14} />}
|
|
56
64
|
chevronClassName={css.chevron}
|
|
57
|
-
title={
|
|
58
|
-
|
|
65
|
+
title={tools?.single !== undefined && tools.single !== null
|
|
66
|
+
? t(tools.single.type === 'tool-addition' ? 'message.toolAdded' : 'message.toolRemoved', { name: tools.single.toolName })
|
|
67
|
+
: tools !== null
|
|
68
|
+
? t('message.toolsUpdated')
|
|
69
|
+
: t(view.role === 'recall' ? 'message.contextRecall' : 'message.contextInjection')}
|
|
70
|
+
collapsedContent={toolSummary !== null ? (
|
|
71
|
+
<>
|
|
72
|
+
<span className={css.sep} aria-hidden />
|
|
73
|
+
<span className={css.summary}>{toolSummary}</span>
|
|
74
|
+
</>
|
|
75
|
+
) : tools !== null || view.label === null ? undefined : (
|
|
59
76
|
/* ToolRow's separator shape: an aria-hidden dot, so the accessible name
|
|
60
77
|
stays the two readable parts and the two disclosure rows expose one
|
|
61
78
|
name shape. A source that names no producer drops the dot with it. */
|
|
@@ -71,13 +88,18 @@ export function ContextInjectionRow({ content, source, provenance, producer, for
|
|
|
71
88
|
</>
|
|
72
89
|
)}
|
|
73
90
|
keepContentWhenOpen
|
|
74
|
-
open={open}
|
|
75
|
-
expandable
|
|
91
|
+
open={open && tools?.single == null}
|
|
92
|
+
expandable={tools?.single == null}
|
|
76
93
|
expandOnRowClick
|
|
77
94
|
onToggle={() => { setOpen(value => !value) }}
|
|
78
95
|
>
|
|
79
96
|
<div className={css.body} data-context-injection-body data-context-form={rendered ?? undefined}>
|
|
80
|
-
{body
|
|
97
|
+
{tools === null ? body : (
|
|
98
|
+
<div className={css.toolChanges}>
|
|
99
|
+
{tools.added.length > 0 && <div>{t('message.toolsAdded', { names: tools.added.join(', ') })}</div>}
|
|
100
|
+
{tools.removed.length > 0 && <div>{t('message.toolsRemoved', { names: tools.removed.join(', ') })}</div>}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
81
103
|
</div>
|
|
82
104
|
</DisclosureRow>
|
|
83
105
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
IconBrowseOutlineRegular, IconFolderCloseRegular,
|
|
4
4
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
5
|
|
|
6
6
|
/** Reference domains with distinct composer and transcript glyphs. */
|
|
@@ -29,7 +29,7 @@ export function ReferenceIcon({ kind, size = 16, className }: ReferenceIconProps
|
|
|
29
29
|
/>
|
|
30
30
|
</svg>
|
|
31
31
|
)
|
|
32
|
-
case 'file': return <
|
|
33
|
-
case 'folder': return <
|
|
32
|
+
case 'file': return <IconBrowseOutlineRegular size={size} className={className} />
|
|
33
|
+
case 'folder': return <IconFolderCloseRegular size={size} className={className} />
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -217,7 +217,7 @@ function deriveBody(variant: ToolRowVariant, argsRaw: string): string | null {
|
|
|
217
217
|
export function toolRowModel(toolName: string, block: ToolCallBlock, cwd?: string, home?: string): ToolRowModel {
|
|
218
218
|
const variant = classifyTool(toolName)
|
|
219
219
|
const done = 'kind' in block
|
|
220
|
-
const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? ''
|
|
220
|
+
const argsRaw = (done ? block.call?.argsRaw : ('argsRaw' in block ? block.argsRaw : '')) ?? ''
|
|
221
221
|
const state: ToolRowState = !done ? 'running'
|
|
222
222
|
: block.error?.code === 'interrupted' ? 'stopped'
|
|
223
223
|
: block.isError ? 'error' : 'ok'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One developer tool-change block the 0.1.7-rc.2 context row names in place. */
|
|
2
|
+
export interface ToolChangeBlock {
|
|
3
|
+
type: 'tool-addition' | 'tool-removal'
|
|
4
|
+
toolName: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Names collected from a context whose every block is a tool change. */
|
|
8
|
+
export interface ToolUpdateRows {
|
|
9
|
+
added: readonly string[]
|
|
10
|
+
removed: readonly string[]
|
|
11
|
+
/** The only block, when the row is a single add or removal. */
|
|
12
|
+
single: ToolChangeBlock | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Classify a context as the rc.2 tool-update row.
|
|
17
|
+
* Mixed or empty content stays on the generic injection presentation.
|
|
18
|
+
* @param content - durable context blocks.
|
|
19
|
+
* @returns collected names, or null when any block is not a tool change.
|
|
20
|
+
*/
|
|
21
|
+
export function toolUpdateRows(content: readonly { readonly type: string; readonly toolName?: string }[]): ToolUpdateRows | null {
|
|
22
|
+
if (content.length === 0) return null
|
|
23
|
+
const rows: ToolChangeBlock[] = []
|
|
24
|
+
for (const block of content) {
|
|
25
|
+
if (block.type !== 'tool-addition' && block.type !== 'tool-removal') return null
|
|
26
|
+
if (typeof block.toolName !== 'string') return null
|
|
27
|
+
rows.push({ type: block.type, toolName: block.toolName })
|
|
28
|
+
}
|
|
29
|
+
const added = rows.flatMap(row => row.type === 'tool-addition' ? [row.toolName] : [])
|
|
30
|
+
const removed = rows.flatMap(row => row.type === 'tool-removal' ? [row.toolName] : [])
|
|
31
|
+
return { added, removed, single: rows.length === 1 ? rows[0]! : null }
|
|
32
|
+
}
|
|
@@ -62,7 +62,7 @@ const READER_NODES = new Set(['user', 'steering', 'assistant-step', 'tool-call',
|
|
|
62
62
|
const EXPECTED: Record<OfficialFamily, Spec> = {
|
|
63
63
|
actions: { kind: 'list', scope: 'session' },
|
|
64
64
|
tools: { kind: 'keyed', scope: 'session' },
|
|
65
|
-
tail: { kind: '
|
|
65
|
+
tail: { kind: 'list', scope: 'session' },
|
|
66
66
|
nodes: { kind: 'keyed', scope: 'session' },
|
|
67
67
|
images: { kind: 'single', scope: 'session' },
|
|
68
68
|
};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Web or Desktop file route. Authorization stays Host-side. */
|
|
2
2
|
export function localPathMediaUrl(protocol: string, origin: string, value: string): string | undefined {
|
|
3
|
-
if (protocol !== 'http:' && protocol !== 'https:') return undefined;
|
|
4
3
|
if (!value.startsWith('/') || value.startsWith('//')) return undefined;
|
|
5
|
-
|
|
4
|
+
if (protocol === 'http:' || protocol === 'https:') {
|
|
5
|
+
return `${origin}/api/file?path=${encodeURIComponent(value)}`;
|
|
6
|
+
}
|
|
7
|
+
// Official fileMediaUrl accepts the Desktop application base dsh-app://app/.
|
|
8
|
+
if (protocol === 'dsh-app:' && origin === 'dsh-app://app') {
|
|
9
|
+
return `dsh-app://app/api/file?path=${encodeURIComponent(value)}`;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
export const readerPathImages = {
|
|
@@ -14,22 +14,23 @@ export const markdownLabels: MarkdownLabels = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const readBlockLabels: ReadBlockLabels = {
|
|
17
|
+
codeLabel: '代码', wrapLabel: '自动换行', unwrapLabel: '不换行',
|
|
17
18
|
window: (shown, total) => `显示 ${shown} / ${total} 行`,
|
|
18
19
|
copy: '复制', copied: '已复制', collapseAria: '收起文件内容',
|
|
19
20
|
expandAria: hidden => `展开其余 ${hidden} 行`, collapse: '收起', expand: hidden => `展开其余 ${hidden} 行`,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const terminalBlockLabels: TerminalBlockLabels = {
|
|
23
|
-
signal: signal => `信号 ${signal}`, exitCode: code => `退出码 ${code}`,
|
|
24
|
+
signal: signal => `信号 ${signal}`, exitCode: code => `退出码 ${code}`, noExitCode: '无退出码',
|
|
24
25
|
running: '执行中', failed: '失败', done: '已完成', copy: '复制', copied: '已复制',
|
|
25
26
|
noOutput: '没有输出', collapseAria: '收起命令输出', collapse: '收起',
|
|
26
27
|
expandAria: hidden => `展开其余 ${hidden} 行`, expand: hidden => `展开其余 ${hidden} 行`,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
export const diffBlockLabels: DiffBlockLabels = {
|
|
31
|
+
codeLabel: '代码', wrapLabel: '自动换行', unwrapLabel: '不换行',
|
|
30
32
|
copy: '复制', copied: '已复制', collapseAria: '收起差异', collapse: '收起',
|
|
31
33
|
expandAria: hidden => `展开其余 ${hidden} 行`, expand: hidden => `展开其余 ${hidden} 行`,
|
|
32
|
-
files: count => `${count} 个文件`,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
export const searchBlockLabels: SearchBlockLabels = {
|
|
@@ -95,13 +95,13 @@ export function toolIdentity(entry: Pick<ToolActivityEntry, 'block' | 'draft'>)
|
|
|
95
95
|
const block = entry.block;
|
|
96
96
|
return {
|
|
97
97
|
name: block ? 'kind' in block ? block.call?.name ?? entry.draft?.name ?? '工具调用' : block.name : entry.draft?.name ?? '工具调用',
|
|
98
|
-
raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : block.argsRaw : entry.draft?.argsRaw ?? '',
|
|
98
|
+
raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : ('argsRaw' in block ? block.argsRaw : '') : entry.draft?.argsRaw ?? '',
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Name and raw arguments of any call block, whether it has landed or is pending. */
|
|
103
103
|
function childIdentity(block: ToolCallBlock): { name?: string; raw: string } {
|
|
104
|
-
if (!block || !('kind' in block)) return { name: block?.name, raw: block
|
|
104
|
+
if (!block || !('kind' in block)) return { name: block?.name, raw: block && 'argsRaw' in block ? block.argsRaw : '' };
|
|
105
105
|
return { name: block.call?.name, raw: block.call?.argsRaw ?? '' };
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -43,7 +43,7 @@ const useReaderStore: ReaderProps['useStore'] = selector => selector(useSyncExte
|
|
|
43
43
|
const props = {
|
|
44
44
|
sessionId: 'auto-fold-fixture', useChat: selector => selector(chat),
|
|
45
45
|
useSession: selector => selector(sessionSnapshot), useSessions: selector => selector(sessionsSnapshot),
|
|
46
|
-
|
|
46
|
+
useSessionStatus: selector => selector(new Map([['auto-fold-fixture', { running: true, pendingInteraction: pending.get('auto-fold-fixture'), completionUnread: false }]])),
|
|
47
47
|
useStore: useReaderStore, actions: session.actions, openPrefs: prefs,
|
|
48
48
|
t: key => key, renderSlot: (_name, _owner, options) => options?.fallback ?? null,
|
|
49
49
|
renderSlotChain: (_name, _owner, options) => options?.fallback ?? null,
|