dsh-better-display 0.3.6 → 0.3.8
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 +16 -0
- package/docs/official-rendering-bridge.md +16 -0
- package/lib/client.js +564 -394
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +139 -48
- package/lib/types/client/OfficialContent.d.ts.map +1 -1
- package/lib/types/client/OfficialContent.js +2 -1
- 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 +96 -20
- package/lib/types/client/Reader.js.map +1 -1
- package/lib/types/client/ToolActivity.d.ts.map +1 -1
- package/lib/types/client/ToolActivity.js +10 -5
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/chat-seat.d.ts +3 -0
- package/lib/types/client/chat-seat.d.ts.map +1 -0
- package/lib/types/client/chat-seat.js +9 -0
- package/lib/types/client/chat-seat.js.map +1 -0
- package/lib/types/client/deliverables.d.ts.map +1 -1
- package/lib/types/client/deliverables.js +2 -0
- package/lib/types/client/deliverables.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +9 -1
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/motion.d.ts.map +1 -1
- package/lib/types/client/motion.js +9 -1
- package/lib/types/client/motion.js.map +1 -1
- package/lib/types/client/official-slots.d.ts.map +1 -1
- package/lib/types/client/official-slots.js +6 -3
- package/lib/types/client/official-slots.js.map +1 -1
- package/lib/types/client/open-file.d.ts.map +1 -1
- package/lib/types/client/open-file.js +1 -6
- package/lib/types/client/open-file.js.map +1 -1
- package/lib/types/client/styles.d.ts +3 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +25 -0
- package/lib/types/client/styles.js.map +1 -0
- package/lib/types/client/tool-activity.d.ts +2 -2
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +12 -7
- package/lib/types/client/tool-activity.js.map +1 -1
- package/lib/types/dsh-better-display.d.ts.map +1 -1
- package/lib/types/dsh-better-display.js +2 -39
- package/lib/types/dsh-better-display.js.map +1 -1
- package/lib/types/reveal.d.ts +12 -0
- package/lib/types/reveal.d.ts.map +1 -0
- package/lib/types/reveal.js +147 -0
- package/lib/types/reveal.js.map +1 -0
- package/lib/types/workspace-path.d.ts +3 -0
- package/lib/types/workspace-path.d.ts.map +1 -0
- package/lib/types/workspace-path.js +5 -0
- package/lib/types/workspace-path.js.map +1 -0
- package/package.json +1 -1
- package/src/client/OfficialContent.tsx +2 -1
- package/src/client/Reader.module.css +41 -9
- package/src/client/Reader.tsx +108 -32
- package/src/client/ToolActivity.tsx +9 -5
- package/src/client/chat-seat.ts +10 -0
- package/src/client/deliverables.ts +1 -0
- package/src/client/index.tsx +10 -1
- package/src/client/motion.tsx +8 -1
- package/src/client/official-slots.tsx +6 -3
- package/src/client/open-file.ts +2 -8
- package/src/client/styles.ts +22 -0
- package/src/client/tool-activity.ts +18 -7
- package/src/dsh-better-display.ts +2 -38
- package/src/reveal.ts +90 -0
- package/src/workspace-path.ts +4 -0
- package/tests/chat-flow-hook.test.ts +13 -0
- package/tests/fixtures/reader-official-users.tsx +170 -0
- package/tests/footer-precedence.test.ts +8 -0
- package/tests/reader-glass-css.test.ts +28 -0
- package/tests/reader-official-users.test.ts +81 -0
- package/tests/retiring-content.test.ts +137 -0
- package/tests/reveal.test.ts +128 -0
- package/tests/stock-install.test.ts +8 -0
- package/tests/styles.test.ts +48 -0
- package/tests/terminal-input-preview.test.ts +113 -0
- package/tsdown.config.ts +55 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { spawn } from 'node:child_process';
|
|
54
|
+
import { existsSync } from 'node:fs';
|
|
55
|
+
import { posix, win32 } from 'node:path';
|
|
56
|
+
import { isAbsoluteWorkspacePath } from './workspace-path.js';
|
|
57
|
+
export async function revealRoots(ctx, sessionId) {
|
|
58
|
+
if (sessionId !== undefined) {
|
|
59
|
+
const sessions = ctx.get?.('sessions');
|
|
60
|
+
const cwd = sessions?.get(sessionId)?.header.cwd;
|
|
61
|
+
if (cwd)
|
|
62
|
+
return [cwd];
|
|
63
|
+
const query = ctx.get?.('sessionQuery');
|
|
64
|
+
if (query) {
|
|
65
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
66
|
+
try {
|
|
67
|
+
const observation = __addDisposableResource(env_1, await query.observeSession(sessionId, { projectionMode: 'none' }), false);
|
|
68
|
+
if (observation.header.cwd)
|
|
69
|
+
return [observation.header.cwd];
|
|
70
|
+
}
|
|
71
|
+
catch (e_1) {
|
|
72
|
+
env_1.error = e_1;
|
|
73
|
+
env_1.hasError = true;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
__disposeResources(env_1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const registry = ctx.get?.('workspaceRegistry');
|
|
81
|
+
return (registry?.list() ?? [])
|
|
82
|
+
.filter(workspace => sessionId === undefined || workspace.sessionIds.includes(sessionId))
|
|
83
|
+
.map(workspace => workspace.path);
|
|
84
|
+
}
|
|
85
|
+
export function resolveRevealPath(path, roots, exists = existsSync) {
|
|
86
|
+
if (isAbsoluteWorkspacePath(path))
|
|
87
|
+
return path;
|
|
88
|
+
// Drive-relative and root-relative Windows paths do not identify a workspace.
|
|
89
|
+
if (/^[A-Za-z]:|^\\/.test(path))
|
|
90
|
+
throw new Error('Path is not workspace-relative');
|
|
91
|
+
const matches = new Set();
|
|
92
|
+
for (const root of roots) {
|
|
93
|
+
if (!isAbsoluteWorkspacePath(root))
|
|
94
|
+
continue;
|
|
95
|
+
const paths = /^[A-Za-z]:[/\\]|^\\\\/.test(root) ? win32 : posix;
|
|
96
|
+
const candidate = paths.resolve(root, path);
|
|
97
|
+
if (exists(candidate))
|
|
98
|
+
matches.add(candidate);
|
|
99
|
+
}
|
|
100
|
+
if (matches.size !== 1)
|
|
101
|
+
throw new Error(matches.size === 0 ? 'Workspace path not found' : 'Workspace path is ambiguous');
|
|
102
|
+
return [...matches][0];
|
|
103
|
+
}
|
|
104
|
+
export function revealPath(path, platform = process.platform, spawnProcess = spawn) {
|
|
105
|
+
const options = { detached: true, stdio: 'ignore' };
|
|
106
|
+
if (platform === 'darwin') {
|
|
107
|
+
spawnProcess('open', ['-R', path], options);
|
|
108
|
+
}
|
|
109
|
+
else if (platform === 'win32') {
|
|
110
|
+
spawnProcess('explorer.exe', [`/select,"${path}"`], { ...options, windowsVerbatimArguments: true });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
spawnProcess('xdg-open', [path], options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function createRevealHandler({ roots, reveal = revealPath }) {
|
|
117
|
+
return (req, res) => {
|
|
118
|
+
if (req.method !== 'POST') {
|
|
119
|
+
res.statusCode = 405;
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let body = '';
|
|
124
|
+
req.on('data', chunk => { body += chunk; });
|
|
125
|
+
req.on('end', async () => {
|
|
126
|
+
res.setHeader('Content-Type', 'application/json');
|
|
127
|
+
try {
|
|
128
|
+
const data = JSON.parse(body);
|
|
129
|
+
const path = typeof data === 'object' && data !== null && 'path' in data && typeof data.path === 'string'
|
|
130
|
+
? data.path.trim() : '';
|
|
131
|
+
if (!path)
|
|
132
|
+
throw new Error('Empty path');
|
|
133
|
+
const sessionId = typeof data === 'object' && data !== null && 'sessionId' in data && typeof data.sessionId === 'string'
|
|
134
|
+
? data.sessionId : undefined;
|
|
135
|
+
const targetPath = resolveRevealPath(path, isAbsoluteWorkspacePath(path) ? [] : await roots(sessionId));
|
|
136
|
+
reveal(targetPath);
|
|
137
|
+
res.statusCode = 200;
|
|
138
|
+
res.end(JSON.stringify({ ok: true }));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
res.statusCode = 400;
|
|
142
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=reveal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reveal.js","sourceRoot":"","sources":["../../src/reveal.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAY,EAAE,SAA6B;IAC3E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,UAAU,CAAkF,CAAC;QACxH,MAAM,GAAG,GAAG,QAAQ,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC;QACjD,IAAI,GAAG;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,cAAc,CAEzB,CAAC;QACd,IAAI,KAAK,EAAE,CAAC;;;gBACV,MAAM,WAAW,kCAAG,MAAM,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,QAAA,CAAC;gBACtF,IAAI,WAAW,CAAC,MAAM,CAAC,GAAG;oBAAE,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;;;;;;;;;SAC7D;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAEjC,CAAC;IACd,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,KAAK,SAAS,IAAI,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;SACxF,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAwB,EAAE,MAAM,GAAG,UAAU;IAC3F,IAAI,uBAAuB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,8EAA8E;IAC9E,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACnF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7C,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;QACjE,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,SAAS,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC;IACzH,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAC3B,eAAoF,KAAK;IAEzF,MAAM,OAAO,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAyB,CAAC;IAC3E,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;SAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,YAAY,CAAC,cAAc,EAAE,CAAC,YAAY,IAAI,GAAG,CAAC,EAAE,EAAE,GAAG,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,CAAC,CAAC;IACtG,CAAC;SAAM,CAAC;QACN,YAAY,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,EAG/D;IACC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAClB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;YACrB,GAAG,CAAC,GAAG,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;YACvB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YAClD,IAAI,CAAC;gBACH,MAAM,IAAI,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;oBACvG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1B,IAAI,CAAC,IAAI;oBAAE,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;gBACzC,MAAM,SAAS,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;oBACtH,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC/B,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;gBACxG,MAAM,CAAC,UAAU,CAAC,CAAC;gBACnB,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;gBACrB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACxC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;gBACrB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-path.d.ts","sourceRoot":"","sources":["../../src/workspace-path.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-path.js","sourceRoot":"","sources":["../../src/workspace-path.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,OAAO,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC"}
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import type { AssistantActionOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/
|
|
|
6
6
|
type MessageId = AssistantActionOwnerProps['messageId'];
|
|
7
7
|
import { OFFICIAL_SEATS } from './official-slots.js';
|
|
8
8
|
import type { BlockRenderProps } from './types.js';
|
|
9
|
+
import { chatSeatProps } from './chat-seat.js';
|
|
9
10
|
|
|
10
11
|
export function OfficialActions({ official, messageId }: Pick<BlockRenderProps, 'official'> & { messageId?: MessageId }) {
|
|
11
12
|
if (!official || !messageId) return null;
|
|
@@ -56,7 +57,7 @@ export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & {
|
|
|
56
57
|
const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined;
|
|
57
58
|
// The runtime node domain is open; the public SlotMap enumerates the known kinds.
|
|
58
59
|
const renderNode = official.renderSlot as unknown as (key: string, owner: object, options: object) => ReactNode;
|
|
59
|
-
return <div data-reader-official-node={node.kind}
|
|
60
|
+
return <div data-reader-official-node={node.kind} {...(node.kind === 'user' || node.kind === 'steering' ? chatSeatProps(node.kind, node.key) : {})}>
|
|
60
61
|
{renderNode(OFFICIAL_SEATS.nodes, { ...owner, node }, { entryKey: node.kind, hookContext: { turnData: turn?.data, disclosureReset }, fallback })}
|
|
61
62
|
</div>;
|
|
62
63
|
}
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
container-type: inline-size;
|
|
11
11
|
}
|
|
12
12
|
.column { max-width: var(--dsh-chat-content-width, 748px); width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 22px; min-width: 0; }
|
|
13
|
-
.toolbar { position: sticky; top: 0; display: flex; justify-content: flex-end; align-items: center; gap: 6px; min-height: 32px; padding: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px);
|
|
13
|
+
.toolbar { position: sticky; top: 0; display: flex; justify-content: flex-end; align-items: center; gap: 6px; min-height: 32px; padding: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); }
|
|
14
14
|
/* 吸顶带第二车道:左「正在处理」+ 右「自动折叠」共享同一条 40px 水平基线。
|
|
15
15
|
top / z-index 由下方 measured-flow 段统一定义:这一道永远钉在工具栏车道
|
|
16
16
|
之下、且永远低于宿主输入卡(composerSeat z-index 7)。 */
|
|
17
|
-
.turnProcessSticky { position: sticky; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px);
|
|
17
|
+
.turnProcessSticky { position: sticky; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); min-height: 32px; display: flex; align-items: center; }
|
|
18
18
|
.turnProcessSticky .disclosureButton { min-height: 32px; }
|
|
19
19
|
/* 第二车道:折叠胶囊吸附在第一车道正下方,互不重叠 */
|
|
20
20
|
.textButton, .historyButton, .iconButton, .jump, .disclosureButton, .imageOpen, .dialogClose { font: inherit; cursor: pointer; }
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
.textButton:hover, .iconButton:hover, .disclosureButton:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
23
23
|
.root button:focus-visible, .root summary:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 3px; }
|
|
24
24
|
.turn { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
25
|
+
/* Keep pinned process lanes inside the process/answer block, before turn-tail UI. */
|
|
26
|
+
.turnStickyRegion { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
27
|
+
.turnStickyLanes { position: sticky; top: max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)); z-index: 6; display: flex; flex-direction: column; min-width: 0; }
|
|
25
28
|
.turn + .turn { margin-top: 12px; }
|
|
26
29
|
.userCluster { display: flex; flex-direction: column; align-self: flex-end; align-items: flex-end; max-width: min(525px, 82%); gap: 8px; min-width: 0; margin-bottom: 4px; }
|
|
27
30
|
.userImages { display: flex; flex-direction: column; align-items: flex-end; max-width: 100%; min-width: 0; }
|
|
@@ -473,7 +476,7 @@
|
|
|
473
476
|
.morphDrawer { overflow: hidden; height: 0; transform-origin: top center; will-change: height, opacity, transform; }
|
|
474
477
|
.liveFoldImpact { animation: liveFoldElastic 180ms cubic-bezier(0.2, 0, 0, 1) both; }
|
|
475
478
|
@keyframes liveFoldElastic { 0% { transform: scale(1); } 50% { transform: scale(1.015); } 100% { transform: scale(1); } }
|
|
476
|
-
.liveFoldContainer { position: sticky; top: calc(max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)) + var(--reader-sticky-lane, 40px) * 2); z-index: 4; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px);
|
|
479
|
+
.liveFoldContainer { position: sticky; top: calc(max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)) + var(--reader-sticky-lane, 40px) * 2); z-index: 4; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); padding: 4px 0; margin-bottom: 6px; animation: liveFoldPulse 320ms cubic-bezier(0.16, 1, 0.3, 1) both; transform-origin: left center; will-change: transform, opacity; }
|
|
477
480
|
@keyframes liveFoldPulse { 0% { transform: translateY(-4px) scale(0.98); opacity: 0.8; } 50% { transform: translateY(0) scale(1.02); opacity: 1; } 100% { transform: translateY(0) scale(1); opacity: 1; } }
|
|
478
481
|
[data-motion=off] .liveFoldContainer { animation: none !important; }
|
|
479
482
|
@media (prefers-reduced-motion: reduce) { .liveFoldContainer { animation: none !important; } }
|
|
@@ -532,7 +535,7 @@
|
|
|
532
535
|
input card. Only the top toolbar lane may use 7: it pins to the scrollport's
|
|
533
536
|
first lane and its containing block always ends above the seat, so it can
|
|
534
537
|
never reach the band, and 7 keeps it above the host's code-block banners. */
|
|
535
|
-
.toolbar { width: 100%; max-width: 100%; align-self: stretch; box-sizing: border-box; min-height: 40px; z-index: 7; border-bottom: 0; backdrop-filter: none;
|
|
538
|
+
.toolbar { width: 100%; max-width: 100%; align-self: stretch; box-sizing: border-box; min-height: 40px; z-index: 7; border-bottom: 0; backdrop-filter: none; }
|
|
536
539
|
/* Every sticky lane used to pin to the SAME top: the status row, the folded
|
|
537
540
|
summary row and the live-fold container all sat at
|
|
538
541
|
max(toolbar-height, status-height) with z-index 6. Three siblings competing
|
|
@@ -540,7 +543,7 @@
|
|
|
540
543
|
内容被盖住、看不见" was: the disclosure opened, and the row that followed
|
|
541
544
|
pinned straight on top of it. Give each lane its own band instead, stacked
|
|
542
545
|
from the measured top: status first, then the summary, then the fold body. */
|
|
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;
|
|
546
|
+
.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; }
|
|
544
547
|
/* A pinned lane's in-flow gap stays visible as an uncovered scrollport strip
|
|
545
548
|
while the lane itself paints opaque. Cast each plate upward over its strip
|
|
546
549
|
(the .turn 14px gap / the prior cell's 16px flowCellInner padding) so the
|
|
@@ -550,8 +553,14 @@
|
|
|
550
553
|
it too. The toolbar is z-index 7, above the lane's 6, so its buttons still
|
|
551
554
|
render on top of the extension. */
|
|
552
555
|
.turnProcessSticky { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base), var(--reader-control-width, 148px) 0 0 var(--dsw-alias-bg-base); }
|
|
556
|
+
.turnStickyLanes .turnProcessSticky, .turnStickyLanes .closedProcessSummary { position: static; top: auto; z-index: auto; }
|
|
553
557
|
/* The same summary row stays in flow until it reaches the measured top lane. */
|
|
554
558
|
.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; }
|
|
559
|
+
/* Keep the official CodeBlock banner's internal z-index inside its wrapper so Reader lanes stay above it. */
|
|
560
|
+
.root :global(.md-code-block) { z-index: 0; }
|
|
561
|
+
/* Keep native code actions below the pinned Reader lanes. */
|
|
562
|
+
.turnStickyRegion:has(> .turnStickyLanes > .closedProcessSummary), .turnStickyRegion:has(.flowCell[data-flow-summary]) { --reader-code-summary-height: var(--reader-sticky-lane, 40px); }
|
|
563
|
+
.root :global(.md-code-block > :has(+ [data-code-block-content])) { top: calc(max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 0px)) + var(--reader-status-height, 0px) + var(--reader-code-summary-height, 0px)); }
|
|
555
564
|
.flowCell[data-flow-summary] { box-shadow: 0 -16px 0 var(--dsw-alias-bg-base); }
|
|
556
565
|
.closedProcessSummary { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base); }
|
|
557
566
|
.foldControl { display: inline-flex; align-items: center; gap: 0; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-module-platform); }
|
|
@@ -578,11 +587,34 @@
|
|
|
578
587
|
.root[data-reader-glass] .liveFoldContainer {
|
|
579
588
|
background: transparent;
|
|
580
589
|
backdrop-filter: blur(16px);
|
|
581
|
-
-webkit-backdrop-filter: blur(16px);
|
|
582
590
|
border: 0;
|
|
583
591
|
border-radius: 0;
|
|
584
592
|
box-shadow: none;
|
|
585
593
|
}
|
|
594
|
+
/* Keep the skin visible between controls without changing the sticky lanes. */
|
|
595
|
+
.root[data-reader-glass] .toolbar,
|
|
596
|
+
.root[data-reader-glass] .turnProcessSticky {
|
|
597
|
+
backdrop-filter: none;
|
|
598
|
+
}
|
|
599
|
+
.root[data-reader-glass] .toolbar {
|
|
600
|
+
pointer-events: none;
|
|
601
|
+
}
|
|
602
|
+
.root[data-reader-glass] .toolbar > .textButton,
|
|
603
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton {
|
|
604
|
+
pointer-events: auto;
|
|
605
|
+
padding: 2px 10px;
|
|
606
|
+
border-radius: 999px;
|
|
607
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, .7)) 80%, transparent);
|
|
608
|
+
backdrop-filter: blur(12px);
|
|
609
|
+
}
|
|
610
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton {
|
|
611
|
+
margin-left: 0;
|
|
612
|
+
max-width: 100%;
|
|
613
|
+
}
|
|
614
|
+
.root[data-reader-glass] .toolbar > .textButton:hover,
|
|
615
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton:hover {
|
|
616
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
617
|
+
}
|
|
586
618
|
/* The z-index ladder is mode-independent: skin mode only drops the plate, so
|
|
587
619
|
a glass reader keeps exactly the same footer precedence as an opaque one. */
|
|
588
620
|
.root[data-reader-glass] .flowCell[data-flow-summary],
|
|
@@ -597,11 +629,13 @@
|
|
|
597
629
|
padding-left: 10px;
|
|
598
630
|
border-radius: 0;
|
|
599
631
|
}
|
|
632
|
+
.root[data-reader-glass] .turnStickyLanes { display: contents; position: static; top: auto; z-index: auto; }
|
|
633
|
+
.root[data-reader-glass] .turnStickyRegion { --reader-code-summary-height: 0px; }
|
|
634
|
+
.root[data-reader-glass] .turnStickyLanes .turnProcessSticky { position: sticky; top: max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 40px)); z-index: 6; }
|
|
600
635
|
/* User bubble becomes a sleek translucent frosted pill in skin mode */
|
|
601
636
|
.root[data-reader-glass] .user {
|
|
602
637
|
background: color-mix(in srgb, var(--dsw-specific-bubble, rgba(0,0,0,.08)) 25%, transparent);
|
|
603
638
|
backdrop-filter: blur(12px);
|
|
604
|
-
-webkit-backdrop-filter: blur(12px);
|
|
605
639
|
border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 30%, transparent);
|
|
606
640
|
}
|
|
607
641
|
/* Skin mode paints no plates of the reader's own: cards, panels and frames go
|
|
@@ -625,7 +659,6 @@
|
|
|
625
659
|
.root[data-reader-glass] .diffOverlay {
|
|
626
660
|
background: color-mix(in srgb, var(--dsw-alias-bg-module-platform, rgba(127,127,127,.1)) 20%, transparent);
|
|
627
661
|
backdrop-filter: blur(16px);
|
|
628
|
-
-webkit-backdrop-filter: blur(16px);
|
|
629
662
|
}
|
|
630
663
|
.root[data-reader-glass] .diffScrollArea {
|
|
631
664
|
background: transparent;
|
|
@@ -642,7 +675,6 @@
|
|
|
642
675
|
background: color-mix(in srgb, var(--dsw-alias-markdown-code-block, rgba(0,0,0,.06)) 25%, transparent) !important;
|
|
643
676
|
border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 30%, transparent);
|
|
644
677
|
backdrop-filter: blur(12px);
|
|
645
|
-
-webkit-backdrop-filter: blur(12px);
|
|
646
678
|
}
|
|
647
679
|
/* The fold control rides a sticky row: it must not paint a plate of its own. */
|
|
648
680
|
.root[data-reader-glass] .foldControl {
|
package/src/client/Reader.tsx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type {} from '@deepseek-ai/dsh-session-turn-outline/types';
|
|
2
|
-
import {
|
|
2
|
+
import type {} from '@deepseek-ai/dsh-agent/types';
|
|
3
|
+
import { Fragment, memo, useCallback, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
4
|
import type { ReactNode, RefObject } from 'react';
|
|
4
|
-
import type { ChatConversationViewNode, ChatNode, ChatNodeKind } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
5
|
+
import type { ChatConversationViewNode, ChatNode, ChatNodeKind, ChatNodeOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
6
|
+
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
5
7
|
import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
6
8
|
import { BlockBoundary, Blocks, contentBlocks, CopyAnswer, UserMessageActions } from './Blocks.js';
|
|
7
9
|
import { ReasoningCard } from './ReasoningCard.js';
|
|
8
10
|
import { ToolActivity, ToolMedia } from './ToolActivity.js';
|
|
9
11
|
import { OfficialActions, OfficialNode, OfficialTail } from './OfficialContent.js';
|
|
12
|
+
import { OFFICIAL_SEATS } from './official-slots.js';
|
|
10
13
|
import { preparingLabel, readerFlow } from './tool-activity.js';
|
|
11
14
|
import { Disclosure, ProcessFragment, RetiringContent, StatusText, useMotionAllowed, usePinnedSelection, useReadingScroll } from './motion.js';
|
|
12
15
|
import { StreamMotionContext } from './streaming.js';
|
|
@@ -30,11 +33,34 @@ import type { ReaderGroup, TurnBoundary } from './projection.js';
|
|
|
30
33
|
import type { BlockRenderProps, ReaderProps } from './types.js';
|
|
31
34
|
import css from './Reader.module.css';
|
|
32
35
|
import { markdownLabels, truncatedJsonLabel } from './primitive-labels.js';
|
|
36
|
+
import { chatSeatProps } from './chat-seat.js';
|
|
37
|
+
import { ensureStyles } from './styles.js';
|
|
33
38
|
|
|
34
39
|
function isNode<K extends ChatNodeKind>(node: ChatConversationViewNode, kind: K): node is ChatNode<K> {
|
|
35
40
|
return node.kind === kind;
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
function observedInputIds(order: readonly string[], getNode: (key: string) => ChatConversationViewNode | undefined): Set<string> {
|
|
44
|
+
const ids = new Set<string>();
|
|
45
|
+
for (const key of order) {
|
|
46
|
+
const node = getNode(key);
|
|
47
|
+
if (!node || (node.kind !== 'user' && node.kind !== 'steering' && node.kind !== 'turn-trigger')) continue;
|
|
48
|
+
const source = (node.data as { source?: { kind?: string; rpcId?: unknown } }).source;
|
|
49
|
+
if (source?.kind === 'user' && typeof source.rpcId === 'string') ids.add(source.rpcId);
|
|
50
|
+
}
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inboxText(item: { content?: readonly unknown[] }): string {
|
|
55
|
+
return (item.content ?? []).filter((block): block is { type: 'text'; text: string } => {
|
|
56
|
+
return typeof block === 'object' && block !== null && (block as { type?: unknown }).type === 'text' && typeof (block as { text?: unknown }).text === 'string';
|
|
57
|
+
}).map(block => block.text).join('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inboxAttachmentCount(item: { content?: readonly unknown[] }): number {
|
|
61
|
+
return (item.content ?? []).filter(block => typeof block === 'object' && block !== null && (block as { type?: unknown }).type !== 'text').length;
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
function cleanErrorMessage(raw: string | undefined): string {
|
|
39
65
|
if (!raw) return '模型服务暂时无响应或连接中断,请稍后重试。';
|
|
40
66
|
let str = raw.trim();
|
|
@@ -51,7 +77,7 @@ function cleanErrorMessage(raw: string | undefined): string {
|
|
|
51
77
|
return str;
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat'> & {
|
|
80
|
+
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat' | 'sessionId'> & {
|
|
55
81
|
nodeKey: string; boundary: TurnBoundary; pinned?: boolean; processOpen?: boolean;
|
|
56
82
|
};
|
|
57
83
|
|
|
@@ -191,6 +217,7 @@ const AssistantNode = memo(function AssistantNode({ useChat, nodeKey, boundary,
|
|
|
191
217
|
|
|
192
218
|
const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, processOpen = false, ...render }: SeatProps) {
|
|
193
219
|
const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
|
|
220
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
194
221
|
if (!node || node.visibility === 'hidden') return null;
|
|
195
222
|
if (isNode(node, 'user') || isNode(node, 'steering')) {
|
|
196
223
|
const blocks = contentBlocks(node.data.content);
|
|
@@ -198,7 +225,7 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
198
225
|
const otherBlocks = blocks.filter(b => b.kind !== 'image');
|
|
199
226
|
const text = otherBlocks.filter((block): block is Extract<typeof block, { kind: 'text' }> => block.kind === 'text').map(block => block.text).join('\n\n');
|
|
200
227
|
const time = node.data.time;
|
|
201
|
-
|
|
228
|
+
const fallback = <>
|
|
202
229
|
{node.kind === 'steering' && <p className={css.meta}>补充消息</p>}
|
|
203
230
|
{imageBlocks.length > 0 && <div className={css.userImages}>
|
|
204
231
|
<Blocks {...render} blocks={imageBlocks} source="user" />
|
|
@@ -207,7 +234,30 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
207
234
|
<Blocks {...render} blocks={otherBlocks} source="user" />
|
|
208
235
|
</div>}
|
|
209
236
|
<UserMessageActions text={text} time={time} />
|
|
210
|
-
|
|
237
|
+
</>;
|
|
238
|
+
const official = render.official;
|
|
239
|
+
let rendered: ReactNode = fallback;
|
|
240
|
+
if (official) {
|
|
241
|
+
const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined;
|
|
242
|
+
const owner: ChatNodeOwnerProps & Pick<ReaderProps, 'sessionId'> = {
|
|
243
|
+
cwd: render.cwd,
|
|
244
|
+
openFile: render.openFile ?? (() => {}),
|
|
245
|
+
openSkill: () => {},
|
|
246
|
+
forkAt: render.forkAt ?? (() => {}),
|
|
247
|
+
inspectCall: callId => official.openView('trajectory', callId),
|
|
248
|
+
loadImage: official.officialImageLoader,
|
|
249
|
+
renderMessageImages: images => official.renderSlot(OFFICIAL_SEATS.images, {
|
|
250
|
+
...images, loadImage: official.officialImageLoader,
|
|
251
|
+
}),
|
|
252
|
+
fileMentions: official.officialFileMentions,
|
|
253
|
+
sessionId: render.sessionId,
|
|
254
|
+
};
|
|
255
|
+
const options = { hookContext: { turnData: turn?.data, disclosureReset }, fallback };
|
|
256
|
+
rendered = node.kind === 'user'
|
|
257
|
+
? official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'user' })
|
|
258
|
+
: official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'steering' });
|
|
259
|
+
}
|
|
260
|
+
return <div className={css.userCluster} data-reader-anchor data-reader-key={nodeKey} {...chatSeatProps(node.kind, nodeKey)}>{rendered}</div>;
|
|
211
261
|
}
|
|
212
262
|
if (isNode(node, 'assistant-step')) return null;
|
|
213
263
|
if (isNode(node, 'tool-call')) return <ToolMedia {...render} block={node.data.root} />;
|
|
@@ -497,6 +547,8 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
497
547
|
() => 'external',
|
|
498
548
|
));
|
|
499
549
|
const hasProcess = flow.some(item => item.kind === 'tool' || hasProcessContent(nodes.get(item.nodeKey), boundary));
|
|
550
|
+
const showStatusLane = hasProcess && !isAwaitingModel;
|
|
551
|
+
const showClosedSummary = boundary.status === 'closed' && hasProcess && autoFold;
|
|
500
552
|
// Only a real, still-active text selection delays folding. Merely clicking,
|
|
501
553
|
// focusing or scrolling the live card does not create a permanent override.
|
|
502
554
|
const holdingSelection = selectedProcessKeys.some(key =>
|
|
@@ -563,6 +615,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
563
615
|
fileMentions,
|
|
564
616
|
metrics,
|
|
565
617
|
official,
|
|
618
|
+
sessionId: props.sessionId,
|
|
566
619
|
cwd,
|
|
567
620
|
};
|
|
568
621
|
const terminal = terminalLabel(boundary.reason);
|
|
@@ -591,22 +644,26 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
591
644
|
};
|
|
592
645
|
return <section className={css.turn} data-reader-turn={group.turn ?? 'unresolved'} data-reader-turn-state={boundary.status} data-reader-turn-result={boundary.reason ?? undefined}>
|
|
593
646
|
{startsWithUser && <BlockBoundary><MainNode {...shared} useChat={props.useChat} boundary={boundary} nodeKey={group.keys[0]} /></BlockBoundary>}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
647
|
+
<div className={css.turnStickyRegion} data-reader-sticky-region>
|
|
648
|
+
{(showStatusLane || showClosedSummary) && <div className={css.turnStickyLanes}>
|
|
649
|
+
{showStatusLane && <StickyLane kind="status" className={css.turnProcessSticky}>
|
|
650
|
+
<Disclosure open={expanded} onChange={setExpanded} controls={flowId} buttonRef={processButton}
|
|
651
|
+
label={<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />} />
|
|
652
|
+
</StickyLane>}
|
|
653
|
+
{showClosedSummary && <ClosedProcessSummary open={expanded} onChange={setExpanded} controls={flowId}
|
|
654
|
+
steps={steps.filter(step => {
|
|
655
|
+
if (step.kind === 'user') return false;
|
|
656
|
+
if (step.kind !== 'body') return true;
|
|
657
|
+
const node = nodes.get(step.nodeKey);
|
|
658
|
+
return !!node && isNode(node, 'assistant-step') && (isEarlierNarration(node.data, boundary)
|
|
659
|
+
|| node.data.blocks.some(block => block.kind === 'tool-call')
|
|
660
|
+
|| (boundary.latestStep > 0 && node.data.step < boundary.latestStep));
|
|
661
|
+
})} />}
|
|
662
|
+
</div>}
|
|
663
|
+
<ChoreographedFlow id={flowId} frame={presentation} motion={motion} enabled={autoFold && boundary.status === 'open' && !holdingSelection}
|
|
664
|
+
urgent={hasTurnError || interaction !== undefined || !sessionRunning} open={foldOpenByKey} processOpen={expanded}
|
|
665
|
+
onOpenChange={(key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }} renderStep={renderStep} />
|
|
666
|
+
</div>
|
|
610
667
|
{/* 状态指示永远排在流程之后:AI 的响应永远出现在最新消息(含补充消息)的下方 */}
|
|
611
668
|
{!hasProcess && boundary.status === 'open' && !isAwaitingModel && <div className={css.disclosure} data-reader-status-only>
|
|
612
669
|
<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />
|
|
@@ -618,6 +675,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
618
675
|
});
|
|
619
676
|
|
|
620
677
|
export function Reader(props: ReaderProps) {
|
|
678
|
+
useInsertionEffect(() => { ensureStyles(); }, []);
|
|
621
679
|
const root = useRef<HTMLDivElement>(null);
|
|
622
680
|
const activatedAt = useRef(Date.now());
|
|
623
681
|
const order = props.useChat(snapshot => snapshot.order);
|
|
@@ -631,6 +689,25 @@ export function Reader(props: ReaderProps) {
|
|
|
631
689
|
const loadingOlder = props.useSession(snapshot => snapshot.loadingOlder);
|
|
632
690
|
const pendingSubmissions = props.useSession(snapshot => snapshot.pendingSubmissions);
|
|
633
691
|
const pendingList = asReadonlyArray<PendingSubmissionEcho>(pendingSubmissions);
|
|
692
|
+
const inbox = props.useProjection?.('inbox');
|
|
693
|
+
type PendingInboxItem = { source?: { kind?: string; rpcId?: unknown }; content?: readonly unknown[] };
|
|
694
|
+
const inboxSteering = asReadonlyArray<PendingInboxItem>(inbox?.['next-step']).filter(item => item.source?.kind === 'user');
|
|
695
|
+
const observed = useMemo(() => observedInputIds(order, key => nodes.get(key)), [order, nodes]);
|
|
696
|
+
const visibleSubmissions = useMemo(() => pendingList.filter(sub => sub.placement !== 'queued' && !observed.has(sub.requestId)), [pendingList, observed]);
|
|
697
|
+
const pendingInputs = useMemo(() => {
|
|
698
|
+
const local = new Map<string, PendingSubmissionEcho>(visibleSubmissions.map(sub => [sub.requestId, sub]));
|
|
699
|
+
const localIds = new Set(pendingList.filter(sub => sub.placement !== 'queued').map(sub => sub.requestId));
|
|
700
|
+
const rows: Array<PendingSubmissionEcho | (typeof inboxSteering)[number]> = [];
|
|
701
|
+
for (const item of inboxSteering) {
|
|
702
|
+
const rpcId = typeof item.source?.rpcId === 'string' ? item.source.rpcId : undefined;
|
|
703
|
+
if (!rpcId) { rows.push(item); continue; }
|
|
704
|
+
const submission = local.get(rpcId);
|
|
705
|
+
if (submission) { local.delete(rpcId); rows.push(submission); }
|
|
706
|
+
else if (!localIds.has(rpcId)) rows.push(item);
|
|
707
|
+
}
|
|
708
|
+
rows.push(...local.values());
|
|
709
|
+
return rows;
|
|
710
|
+
}, [inboxSteering, pendingList, visibleSubmissions]);
|
|
634
711
|
const waitAnchor = waitingAnchor(order, key => nodes.get(key), pendingList);
|
|
635
712
|
const motionPreference = props.useStore(state => state.motion);
|
|
636
713
|
const motion = useMotionAllowed(motionPreference);
|
|
@@ -808,11 +885,6 @@ export function Reader(props: ReaderProps) {
|
|
|
808
885
|
}
|
|
809
886
|
}, [lastKey, lastNode?.kind, lastSubmissionId, scroll]);
|
|
810
887
|
|
|
811
|
-
const visibleSubmissions = useMemo(() => {
|
|
812
|
-
if (pendingList.length === 0) return [];
|
|
813
|
-
return pendingList.filter(sub => sub.placement !== 'queued');
|
|
814
|
-
}, [pendingList]);
|
|
815
|
-
|
|
816
888
|
// ChatView publishes data-chat-flow="" on its column. Skins treat a
|
|
817
889
|
// scrollport without that hook as inspect-only and hide [data-composer-seat].
|
|
818
890
|
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'}>
|
|
@@ -840,10 +912,14 @@ export function Reader(props: ReaderProps) {
|
|
|
840
912
|
{openError && <div className={css.error} role="alert">会话暂时无法读取:{openError.message}</div>}
|
|
841
913
|
{loading && groups.length === 0 && <p className={css.empty} role="status">正在读取会话…</p>}
|
|
842
914
|
{groups.map(group => <TurnGroup key={group.key} {...props} group={group} motion={motion} autoFold={autoFold} pinnedKeys={pinnedKeys} selectedProcessKeys={selectedProcessKeys} isAwaitingModel={isAwaitingModel && group.key === groups.at(-1)?.key} />)}
|
|
843
|
-
{
|
|
844
|
-
|
|
915
|
+
{pendingInputs.map((item, index) => {
|
|
916
|
+
const submission = 'requestId' in item ? item : undefined;
|
|
917
|
+
const text = submission?.text ?? inboxText(item as PendingInboxItem);
|
|
918
|
+
const images = submission ? pendingSubmissionImages(submission) : [];
|
|
919
|
+
const attachmentCount = images.length;
|
|
920
|
+
const requestId = submission?.requestId ?? `inbox-${index}`;
|
|
845
921
|
return (
|
|
846
|
-
<div key={
|
|
922
|
+
<div key={requestId} className={css.userCluster} data-reader-pending-submission data-pending-steering={submission ? undefined : ''} data-pending-attachment-count={attachmentCount}>
|
|
847
923
|
{images.length > 0 && (
|
|
848
924
|
<div className={css.userImages}>
|
|
849
925
|
{images.map((item, idx) => (
|
|
@@ -855,12 +931,12 @@ export function Reader(props: ReaderProps) {
|
|
|
855
931
|
))}
|
|
856
932
|
</div>
|
|
857
933
|
)}
|
|
858
|
-
{
|
|
934
|
+
{text ? (
|
|
859
935
|
<div className={css.user}>
|
|
860
|
-
<div className={css.blocks}>{
|
|
936
|
+
<div className={css.blocks}>{text}</div>
|
|
861
937
|
</div>
|
|
862
938
|
) : null}
|
|
863
|
-
<UserMessageActions text={
|
|
939
|
+
<UserMessageActions text={text} time={submission?.time} />
|
|
864
940
|
</div>
|
|
865
941
|
);
|
|
866
942
|
})}
|