dsh-better-display 0.3.7 → 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 +11 -1
- package/docs/official-rendering-bridge.md +12 -0
- package/lib/client.js +221 -105
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +297 -53
- package/lib/types/client/Reader.d.ts.map +1 -1
- package/lib/types/client/Reader.js +43 -12
- 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/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/Reader.module.css +41 -9
- package/src/client/Reader.tsx +56 -21
- package/src/client/ToolActivity.tsx +9 -5
- 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/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 +49 -1
|
@@ -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
|
@@ -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,13 +1,15 @@
|
|
|
1
1
|
import type {} from '@deepseek-ai/dsh-session-turn-outline/types';
|
|
2
2
|
import type {} from '@deepseek-ai/dsh-agent/types';
|
|
3
|
-
import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Fragment, memo, useCallback, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import type { ReactNode, RefObject } from 'react';
|
|
5
|
-
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';
|
|
6
7
|
import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
7
8
|
import { BlockBoundary, Blocks, contentBlocks, CopyAnswer, UserMessageActions } from './Blocks.js';
|
|
8
9
|
import { ReasoningCard } from './ReasoningCard.js';
|
|
9
10
|
import { ToolActivity, ToolMedia } from './ToolActivity.js';
|
|
10
11
|
import { OfficialActions, OfficialNode, OfficialTail } from './OfficialContent.js';
|
|
12
|
+
import { OFFICIAL_SEATS } from './official-slots.js';
|
|
11
13
|
import { preparingLabel, readerFlow } from './tool-activity.js';
|
|
12
14
|
import { Disclosure, ProcessFragment, RetiringContent, StatusText, useMotionAllowed, usePinnedSelection, useReadingScroll } from './motion.js';
|
|
13
15
|
import { StreamMotionContext } from './streaming.js';
|
|
@@ -32,6 +34,7 @@ import type { BlockRenderProps, ReaderProps } from './types.js';
|
|
|
32
34
|
import css from './Reader.module.css';
|
|
33
35
|
import { markdownLabels, truncatedJsonLabel } from './primitive-labels.js';
|
|
34
36
|
import { chatSeatProps } from './chat-seat.js';
|
|
37
|
+
import { ensureStyles } from './styles.js';
|
|
35
38
|
|
|
36
39
|
function isNode<K extends ChatNodeKind>(node: ChatConversationViewNode, kind: K): node is ChatNode<K> {
|
|
37
40
|
return node.kind === kind;
|
|
@@ -74,7 +77,7 @@ function cleanErrorMessage(raw: string | undefined): string {
|
|
|
74
77
|
return str;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat'> & {
|
|
80
|
+
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat' | 'sessionId'> & {
|
|
78
81
|
nodeKey: string; boundary: TurnBoundary; pinned?: boolean; processOpen?: boolean;
|
|
79
82
|
};
|
|
80
83
|
|
|
@@ -214,6 +217,7 @@ const AssistantNode = memo(function AssistantNode({ useChat, nodeKey, boundary,
|
|
|
214
217
|
|
|
215
218
|
const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, processOpen = false, ...render }: SeatProps) {
|
|
216
219
|
const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
|
|
220
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
217
221
|
if (!node || node.visibility === 'hidden') return null;
|
|
218
222
|
if (isNode(node, 'user') || isNode(node, 'steering')) {
|
|
219
223
|
const blocks = contentBlocks(node.data.content);
|
|
@@ -221,7 +225,7 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
221
225
|
const otherBlocks = blocks.filter(b => b.kind !== 'image');
|
|
222
226
|
const text = otherBlocks.filter((block): block is Extract<typeof block, { kind: 'text' }> => block.kind === 'text').map(block => block.text).join('\n\n');
|
|
223
227
|
const time = node.data.time;
|
|
224
|
-
|
|
228
|
+
const fallback = <>
|
|
225
229
|
{node.kind === 'steering' && <p className={css.meta}>补充消息</p>}
|
|
226
230
|
{imageBlocks.length > 0 && <div className={css.userImages}>
|
|
227
231
|
<Blocks {...render} blocks={imageBlocks} source="user" />
|
|
@@ -230,7 +234,30 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
230
234
|
<Blocks {...render} blocks={otherBlocks} source="user" />
|
|
231
235
|
</div>}
|
|
232
236
|
<UserMessageActions text={text} time={time} />
|
|
233
|
-
|
|
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>;
|
|
234
261
|
}
|
|
235
262
|
if (isNode(node, 'assistant-step')) return null;
|
|
236
263
|
if (isNode(node, 'tool-call')) return <ToolMedia {...render} block={node.data.root} />;
|
|
@@ -520,6 +547,8 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
520
547
|
() => 'external',
|
|
521
548
|
));
|
|
522
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;
|
|
523
552
|
// Only a real, still-active text selection delays folding. Merely clicking,
|
|
524
553
|
// focusing or scrolling the live card does not create a permanent override.
|
|
525
554
|
const holdingSelection = selectedProcessKeys.some(key =>
|
|
@@ -586,6 +615,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
586
615
|
fileMentions,
|
|
587
616
|
metrics,
|
|
588
617
|
official,
|
|
618
|
+
sessionId: props.sessionId,
|
|
589
619
|
cwd,
|
|
590
620
|
};
|
|
591
621
|
const terminal = terminalLabel(boundary.reason);
|
|
@@ -614,22 +644,26 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
614
644
|
};
|
|
615
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}>
|
|
616
646
|
{startsWithUser && <BlockBoundary><MainNode {...shared} useChat={props.useChat} boundary={boundary} nodeKey={group.keys[0]} /></BlockBoundary>}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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>
|
|
633
667
|
{/* 状态指示永远排在流程之后:AI 的响应永远出现在最新消息(含补充消息)的下方 */}
|
|
634
668
|
{!hasProcess && boundary.status === 'open' && !isAwaitingModel && <div className={css.disclosure} data-reader-status-only>
|
|
635
669
|
<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />
|
|
@@ -641,6 +675,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
641
675
|
});
|
|
642
676
|
|
|
643
677
|
export function Reader(props: ReaderProps) {
|
|
678
|
+
useInsertionEffect(() => { ensureStyles(); }, []);
|
|
644
679
|
const root = useRef<HTMLDivElement>(null);
|
|
645
680
|
const activatedAt = useRef(Date.now());
|
|
646
681
|
const order = props.useChat(snapshot => snapshot.order);
|
|
@@ -17,6 +17,7 @@ import css from './Reader.module.css';
|
|
|
17
17
|
const LABEL: Record<ToolPhase, string> = { preparing: '输入生成中', running: '执行中', returned: '已返回', succeeded: '已完成', failed: '失败', interrupted: '已中断' };
|
|
18
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
|
+
const preparingTerminalLabels = { ...terminalBlockLabels, running: '正在生成命令' };
|
|
20
21
|
const language = (path: string | undefined) => path?.split('.').at(-1);
|
|
21
22
|
const duration = (ms: number) => ms < 1000 ? `${Math.round(ms)} 毫秒` : `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} 秒`;
|
|
22
23
|
|
|
@@ -28,14 +29,17 @@ function generatedInput(content: string, target: string | undefined, preparing:
|
|
|
28
29
|
</div>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function InputView({ model,
|
|
32
|
+
function InputView({ model, phase, fillComposer }: { model: ReturnType<typeof activitySummary>; phase: ToolPhase; fillComposer: BlockRenderProps['fillComposer'] }) {
|
|
33
|
+
const preparing = phase === 'preparing';
|
|
34
|
+
const inProgress = preparing || phase === 'running';
|
|
32
35
|
if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
|
|
33
36
|
return preparing
|
|
34
37
|
? <StreamingMcpAppPlaceholder title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} />
|
|
35
38
|
: <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={fillComposer} />;
|
|
36
39
|
}
|
|
37
40
|
if (model.content) return generatedInput(model.content, model.target, preparing);
|
|
38
|
-
if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} labels={terminalBlockLabels} /></div>;
|
|
41
|
+
if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} running={inProgress} labels={preparing ? preparingTerminalLabels : terminalBlockLabels} /></div>;
|
|
42
|
+
if (model.category === 'terminal' && preparing && model.argsState === 'partial') return <p className={css.toolDetailNote} data-reader-tool-command-pending>命令仍在生成,收到可识别的命令文本后会显示预览。</p>;
|
|
39
43
|
return <JsonTree data={model.args} label={preparing ? '已收到的输入字段' : '工具输入'} labels={jsonTreeLabels} />;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -95,11 +99,11 @@ function ResultFallback({ entry, model, phase, ...render }: BlockRenderProps & {
|
|
|
95
99
|
const block = entry.block;
|
|
96
100
|
if (!block || !('kind' in block)) return <>
|
|
97
101
|
<p className={css.toolDetailNote}>{phase === 'interrupted' ? '已中断,没有工具结果。已生成的输入仍可查看。' : phase === 'preparing' ? '模型正在生成工具输入,工具还未开始执行。' : '工具已开始执行,正在等待结果。'}</p>
|
|
98
|
-
<InputView model={model}
|
|
102
|
+
<InputView model={model} phase={phase} fillComposer={render.fillComposer} />
|
|
99
103
|
</>;
|
|
100
104
|
const meta = objectValue(block.meta);
|
|
101
105
|
const text = block.content.filter(item => item.type === 'text').map(item => item.text).join('\n');
|
|
102
|
-
if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model}
|
|
106
|
+
if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model} phase={phase} fillComposer={render.fillComposer} /><pre className={css.toolRaw}>{text}</pre></>;
|
|
103
107
|
|
|
104
108
|
if (model.category === 'terminal') {
|
|
105
109
|
const facts = executionFacts(block);
|
|
@@ -247,7 +251,7 @@ export const ToolActivity = memo(function ToolActivityView({ entry, motion, turn
|
|
|
247
251
|
<div ref={panel} id={`${detailId}-panel`} className={css.toolPanel} role="tabpanel" aria-labelledby={`${detailId}-${tab}`} tabIndex={0}>
|
|
248
252
|
{selected && <p className={css.toolDetailNote}>为保留选区,预览暂停更新;当前状态见卡片标题。</p>}
|
|
249
253
|
{tab === 'result' && <ResultView {...render} {...preview} />}
|
|
250
|
-
{tab === 'input' && <><InputView model={preview.model}
|
|
254
|
+
{tab === 'input' && <><InputView model={preview.model} phase={preview.phase} fillComposer={render.fillComposer} /><details className={css.detail}><summary>全部输入字段</summary><JsonTree data={preview.model.args} label="输入字段" labels={jsonTreeLabels} /></details></>}
|
|
251
255
|
{tab === 'raw' && <><p className={css.toolDetailNote}>完整记录 · 只读 · 不执行其中的代码</p><h4 className={css.toolRawLabel}>工具输入</h4><pre className={css.toolRaw}>{preview.model.raw || '输入尚未到达'}</pre>{rawResult && <><h4 className={css.toolRawLabel}>工具结果</h4><pre className={css.toolRaw}>{rawResult}</pre></>}</>}
|
|
252
256
|
</div>
|
|
253
257
|
</div>
|
|
@@ -23,6 +23,7 @@ export function basename(path: string): string {
|
|
|
23
23
|
export function dirname(path: string): string {
|
|
24
24
|
const normalized = path.replace(/[/\\]+$/, '');
|
|
25
25
|
const at = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
|
|
26
|
+
if (at === 0 || (at === 2 && /^[A-Za-z]:/.test(normalized))) return normalized.slice(0, at + 1);
|
|
26
27
|
return at === -1 ? '.' : normalized.slice(0, at) || '.';
|
|
27
28
|
}
|
|
28
29
|
|
package/src/client/index.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
|
6
6
|
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
|
|
7
7
|
import * as workspacePathPkg from '@deepseek-ai/dsh-util-workspace-path';
|
|
8
8
|
import { dirname } from './deliverables.js';
|
|
9
|
+
import { isAbsoluteWorkspacePath } from '../workspace-path.js';
|
|
10
|
+
import { ensureStyles } from './styles.js';
|
|
9
11
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
10
12
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
|
|
11
13
|
import { Reader } from './Reader.js';
|
|
@@ -57,6 +59,9 @@ export { McpAppFrame } from './McpAppFrame.js';
|
|
|
57
59
|
export const name = 'dsh-better-display-client';
|
|
58
60
|
export const inject = ['slots', 'sessions', 'conversation', 'uiConversation', 'remote', 'remote.session'];
|
|
59
61
|
|
|
62
|
+
// All CSS imports have injected their nodes before this module's body runs.
|
|
63
|
+
ensureStyles();
|
|
64
|
+
|
|
60
65
|
export function apply(ctx: Context): void {
|
|
61
66
|
const store = createReaderStore();
|
|
62
67
|
// conversation.view is session-scoped, so session persist keys are
|
|
@@ -138,7 +143,7 @@ export function apply(ctx: Context): void {
|
|
|
138
143
|
const res = await fetch('/better-display/reveal', {
|
|
139
144
|
method: 'POST',
|
|
140
145
|
headers: { 'Content-Type': 'application/json' },
|
|
141
|
-
body: JSON.stringify({ path: targetPath }),
|
|
146
|
+
body: JSON.stringify({ path: targetPath, sessionId }),
|
|
142
147
|
});
|
|
143
148
|
if (res.ok) {
|
|
144
149
|
const data = await res.json();
|
|
@@ -150,6 +155,10 @@ export function apply(ctx: Context): void {
|
|
|
150
155
|
|
|
151
156
|
// 2. Fallback to official opener with parent directory
|
|
152
157
|
const parentDir = dirname(targetPath);
|
|
158
|
+
if (!isAbsoluteWorkspacePath(parentDir)) {
|
|
159
|
+
console.warn('[dsh-better-display] reveal failed; refusing to open a relative parent:', targetPath);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
153
162
|
const remote = ctx.remote as unknown as { session?: { openWorkspacePath: (arg: { path: string }) => Promise<{ ok: boolean; error?: { message: string } }> } } | undefined;
|
|
154
163
|
const remoteSession = remote?.session
|
|
155
164
|
?? (ctx.get?.('remote.session') as unknown as { openWorkspacePath: (arg: { path: string }) => Promise<{ ok: boolean; error?: { message: string } }> } | undefined);
|
package/src/client/motion.tsx
CHANGED
|
@@ -191,7 +191,14 @@ export function RetiringContent({ visible, children }: { visible: boolean; child
|
|
|
191
191
|
if (!enabled || from < 1) { setPresent(false); return; }
|
|
192
192
|
const next = element.animate([{ height: `${from}px`, opacity: 1 }, { height: '0px', opacity: 0 }], { duration: 220, easing: EASING, fill: 'both' });
|
|
193
193
|
animation.current = next;
|
|
194
|
-
|
|
194
|
+
const settle = () => { if (animation.current === next) { animation.current = null; next.cancel(); setPresent(false); } };
|
|
195
|
+
next.onfinish = settle;
|
|
196
|
+
const deadline = window.setTimeout(settle, 220 + 240);
|
|
197
|
+
return () => {
|
|
198
|
+
window.clearTimeout(deadline);
|
|
199
|
+
if (animation.current === next) animation.current = null;
|
|
200
|
+
next.cancel();
|
|
201
|
+
};
|
|
195
202
|
}, [visible, enabled, focusHeld]);
|
|
196
203
|
useEffect(() => () => animation.current?.cancel(), []);
|
|
197
204
|
if (!visible && !present) return null;
|
|
@@ -57,8 +57,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Existing Reader presentations own these rows. All other node kinds, including
|
|
60
|
-
// future registrations, reach the official renderer through the
|
|
61
|
-
|
|
60
|
+
// future registrations, reach the official renderer through the mirror seat.
|
|
61
|
+
// 'user' and 'steering' are mirrored too, so third-party renderers registered
|
|
62
|
+
// on the official node slot (e.g. message-recall buttons) keep working inside
|
|
63
|
+
// the Reader instead of being silently dropped.
|
|
64
|
+
const READER_OWNED_NODES = new Set(['assistant-step', 'tool-call', 'turn-tail', 'turn-process']);
|
|
62
65
|
const EXPECTED: Record<OfficialFamily, Spec> = {
|
|
63
66
|
actions: { kind: 'list', scope: 'session' },
|
|
64
67
|
tools: { kind: 'keyed', scope: 'session' },
|
|
@@ -198,7 +201,7 @@ export function installOfficialSlots(ctx: Context): () => void {
|
|
|
198
201
|
throw new Error(`Official slot contract changed: ${source}`);
|
|
199
202
|
}
|
|
200
203
|
return mirrorOfficialSlot(slots, source, target, `dsh-better-display.official.${family}`,
|
|
201
|
-
family === 'nodes' ? entry => !
|
|
204
|
+
family === 'nodes' ? entry => !READER_OWNED_NODES.has(entry.options.key ?? '') : undefined);
|
|
202
205
|
}));
|
|
203
206
|
}
|
|
204
207
|
} catch (error) {
|