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.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/official-rendering-bridge.md +16 -0
  3. package/lib/client.js +564 -394
  4. package/lib/client.js.map +1 -1
  5. package/lib/dsh-better-display.js +139 -48
  6. package/lib/types/client/OfficialContent.d.ts.map +1 -1
  7. package/lib/types/client/OfficialContent.js +2 -1
  8. package/lib/types/client/OfficialContent.js.map +1 -1
  9. package/lib/types/client/Reader.d.ts.map +1 -1
  10. package/lib/types/client/Reader.js +96 -20
  11. package/lib/types/client/Reader.js.map +1 -1
  12. package/lib/types/client/ToolActivity.d.ts.map +1 -1
  13. package/lib/types/client/ToolActivity.js +10 -5
  14. package/lib/types/client/ToolActivity.js.map +1 -1
  15. package/lib/types/client/chat-seat.d.ts +3 -0
  16. package/lib/types/client/chat-seat.d.ts.map +1 -0
  17. package/lib/types/client/chat-seat.js +9 -0
  18. package/lib/types/client/chat-seat.js.map +1 -0
  19. package/lib/types/client/deliverables.d.ts.map +1 -1
  20. package/lib/types/client/deliverables.js +2 -0
  21. package/lib/types/client/deliverables.js.map +1 -1
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/client/index.js +9 -1
  24. package/lib/types/client/index.js.map +1 -1
  25. package/lib/types/client/motion.d.ts.map +1 -1
  26. package/lib/types/client/motion.js +9 -1
  27. package/lib/types/client/motion.js.map +1 -1
  28. package/lib/types/client/official-slots.d.ts.map +1 -1
  29. package/lib/types/client/official-slots.js +6 -3
  30. package/lib/types/client/official-slots.js.map +1 -1
  31. package/lib/types/client/open-file.d.ts.map +1 -1
  32. package/lib/types/client/open-file.js +1 -6
  33. package/lib/types/client/open-file.js.map +1 -1
  34. package/lib/types/client/styles.d.ts +3 -0
  35. package/lib/types/client/styles.d.ts.map +1 -0
  36. package/lib/types/client/styles.js +25 -0
  37. package/lib/types/client/styles.js.map +1 -0
  38. package/lib/types/client/tool-activity.d.ts +2 -2
  39. package/lib/types/client/tool-activity.d.ts.map +1 -1
  40. package/lib/types/client/tool-activity.js +12 -7
  41. package/lib/types/client/tool-activity.js.map +1 -1
  42. package/lib/types/dsh-better-display.d.ts.map +1 -1
  43. package/lib/types/dsh-better-display.js +2 -39
  44. package/lib/types/dsh-better-display.js.map +1 -1
  45. package/lib/types/reveal.d.ts +12 -0
  46. package/lib/types/reveal.d.ts.map +1 -0
  47. package/lib/types/reveal.js +147 -0
  48. package/lib/types/reveal.js.map +1 -0
  49. package/lib/types/workspace-path.d.ts +3 -0
  50. package/lib/types/workspace-path.d.ts.map +1 -0
  51. package/lib/types/workspace-path.js +5 -0
  52. package/lib/types/workspace-path.js.map +1 -0
  53. package/package.json +1 -1
  54. package/src/client/OfficialContent.tsx +2 -1
  55. package/src/client/Reader.module.css +41 -9
  56. package/src/client/Reader.tsx +108 -32
  57. package/src/client/ToolActivity.tsx +9 -5
  58. package/src/client/chat-seat.ts +10 -0
  59. package/src/client/deliverables.ts +1 -0
  60. package/src/client/index.tsx +10 -1
  61. package/src/client/motion.tsx +8 -1
  62. package/src/client/official-slots.tsx +6 -3
  63. package/src/client/open-file.ts +2 -8
  64. package/src/client/styles.ts +22 -0
  65. package/src/client/tool-activity.ts +18 -7
  66. package/src/dsh-better-display.ts +2 -38
  67. package/src/reveal.ts +90 -0
  68. package/src/workspace-path.ts +4 -0
  69. package/tests/chat-flow-hook.test.ts +13 -0
  70. package/tests/fixtures/reader-official-users.tsx +170 -0
  71. package/tests/footer-precedence.test.ts +8 -0
  72. package/tests/reader-glass-css.test.ts +28 -0
  73. package/tests/reader-official-users.test.ts +81 -0
  74. package/tests/retiring-content.test.ts +137 -0
  75. package/tests/reveal.test.ts +128 -0
  76. package/tests/stock-install.test.ts +8 -0
  77. package/tests/styles.test.ts +48 -0
  78. package/tests/terminal-input-preview.test.ts +113 -0
  79. 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":"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"}
@@ -0,0 +1,3 @@
1
+ /** Absolute paths accepted by both POSIX and Windows hosts. */
2
+ export declare function isAbsoluteWorkspacePath(path: string): boolean;
3
+ //# sourceMappingURL=workspace-path.d.ts.map
@@ -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,5 @@
1
+ /** Absolute paths accepted by both POSIX and Windows hosts. */
2
+ export function isAbsoluteWorkspacePath(path) {
3
+ return /^(?:\/|[A-Za-z]:[/\\]|\\\\)/.test(path);
4
+ }
5
+ //# sourceMappingURL=workspace-path.js.map
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-display",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "A calmer reading view for DeepSeek Harness: native process details, live reasoning, and clean final answers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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} data-chat-anchor-key={node.key} data-chat-flow-kind={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); -webkit-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); -webkit-backdrop-filter: blur(12px); min-height: 32px; display: flex; align-items: center; }
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); -webkit-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; }
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; -webkit-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; -webkit-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 {
@@ -1,12 +1,15 @@
1
1
  import type {} from '@deepseek-ai/dsh-session-turn-outline/types';
2
- import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
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
- return <div className={css.userCluster} data-reader-anchor data-reader-key={nodeKey}>
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
- </div>;
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
- {hasProcess && !isAwaitingModel && <StickyLane kind="status" className={css.turnProcessSticky}>
595
- <Disclosure open={expanded} onChange={setExpanded} controls={flowId} buttonRef={processButton}
596
- label={<GroupStatus group={group} sessionId={props.sessionId} useChat={props.useChat} pending={interaction} motion={motion} />} />
597
- </StickyLane>}
598
- {boundary.status === 'closed' && hasProcess && autoFold && <ClosedProcessSummary open={expanded} onChange={setExpanded} controls={flowId}
599
- steps={steps.filter(step => {
600
- if (step.kind === 'user') return false;
601
- if (step.kind !== 'body') return true;
602
- const node = nodes.get(step.nodeKey);
603
- return !!node && isNode(node, 'assistant-step') && (isEarlierNarration(node.data, boundary)
604
- || node.data.blocks.some(block => block.kind === 'tool-call')
605
- || (boundary.latestStep > 0 && node.data.step < boundary.latestStep));
606
- })} />}
607
- <ChoreographedFlow id={flowId} frame={presentation} motion={motion} enabled={autoFold && boundary.status === 'open' && !holdingSelection}
608
- urgent={hasTurnError || interaction !== undefined || !sessionRunning} open={foldOpenByKey} processOpen={expanded}
609
- onOpenChange={(key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }} renderStep={renderStep} />
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
- {visibleSubmissions.map(submission => {
844
- const images = pendingSubmissionImages(submission);
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={submission.requestId} className={css.userCluster} data-reader-pending-submission>
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
- {submission.text ? (
934
+ {text ? (
859
935
  <div className={css.user}>
860
- <div className={css.blocks}>{submission.text}</div>
936
+ <div className={css.blocks}>{text}</div>
861
937
  </div>
862
938
  ) : null}
863
- <UserMessageActions text={submission.text ?? ''} time={submission.time} />
939
+ <UserMessageActions text={text} time={submission?.time} />
864
940
  </div>
865
941
  );
866
942
  })}