dsh-working-activity 0.2.6 → 0.3.0

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.
@@ -5,7 +5,8 @@
5
5
  * no timers, no cordis — deterministic given the event stream and a clock.
6
6
  * @module @deepseek-ai/dsh-working-activity/status
7
7
  */
8
- import { actionFor, fmtDuration, isGitTool, isNight, pickPhrase, thinkingPhrase, WAITING_PHRASES, DONE_PHRASES, FAIL_PHRASES, } from './phrases.js';
8
+ import { actionFor, donePhrase, failPhrase, fmtDuration, isGitTool, isNight, thinkingPhrase, waitingPhrase, } from './phrases.js';
9
+ import { t } from './lang.js';
9
10
  /** Format one tool into its display fragment (`跑个命令 npm test`). */
10
11
  function toolFragment(tool) {
11
12
  return tool.detail.length === 0 ? tool.action : `${tool.action} ${tool.detail}`;
@@ -94,7 +95,7 @@ export class ActivityTracker {
94
95
  /** Total tokens reported across the turn's assistant messages. */
95
96
  turnTokens = 0;
96
97
  /** Completion prefix drawn ONCE at turn end so the done line stays stable. */
97
- donePrefix = '搞定 ✓';
98
+ donePrefix = t('done-prefix');
98
99
  /**
99
100
  * @param config - Behavioral knobs.
100
101
  * @param now - Wall-clock supplier (injectable for tests).
@@ -231,13 +232,15 @@ export class ActivityTracker {
231
232
  this.thinkingMs += Math.max(0, at - this.thinkingStartedAt);
232
233
  }
233
234
  // Draw the completion prefix ONCE so repeated renders of the done line
234
- // stay stable (a fresh random per render would make it flicker).
235
+ // stay stable (a fresh random per render would make it flicker). The
236
+ // pools are language-aware, so the line matches the language the turn
237
+ // ended in.
235
238
  const lastTool = this.doneQueue.at(-1);
236
239
  if (this.config.phrases) {
237
- this.donePrefix = lastTool?.failed ? pickPhrase(FAIL_PHRASES) : pickPhrase(DONE_PHRASES);
240
+ this.donePrefix = lastTool?.failed ? failPhrase() : donePhrase();
238
241
  }
239
242
  else {
240
- this.donePrefix = '搞定 ✓';
243
+ this.donePrefix = t('done-prefix');
241
244
  }
242
245
  this.setPhase('done', at);
243
246
  return;
@@ -314,11 +317,12 @@ export class ActivityTracker {
314
317
  ? 0
315
318
  : this.thinkingMs + Math.max(0, nowMs - this.thinkingStartedAt);
316
319
  const elapsed = fmtDuration(this.turnElapsedMs(nowMs));
320
+ const elapsedLine = t('line-elapsed', { elapsed });
317
321
  const narration = this.freshNarration(nowMs);
318
322
  if (narration !== null) {
319
323
  return {
320
324
  phase: this.phase,
321
- line: `⏵ ${narration} · 总${elapsed}`,
325
+ line: `⏵ ${narration} · ${elapsedLine}`,
322
326
  phrase: narration,
323
327
  toolCount: this.toolCount,
324
328
  turnElapsedMs: this.turnElapsedMs(nowMs),
@@ -328,28 +332,29 @@ export class ActivityTracker {
328
332
  if (this.config.phrases) {
329
333
  if (nowMs - this.phraseChangedAt >= PHRASE_ROTATE_MS) {
330
334
  // Waiting (pre-first-token) draws from the waiting pool; thinking
331
- // rotates the playful copy pool with night mixing.
335
+ // rotates the playful copy pool with night mixing. Both pools are
336
+ // language-aware, so a `/lang` switch shows on the next rotation.
332
337
  this.previousPhrase = this.phase === 'waiting'
333
- ? pickPhrase(WAITING_PHRASES, this.previousPhrase)
338
+ ? waitingPhrase(this.previousPhrase)
334
339
  : thinkingPhrase(thinkingMs, this.previousPhrase, isNight(new Date(nowMs).getHours()));
335
340
  this.phraseChangedAt = nowMs;
336
341
  }
337
342
  const phrase = this.previousPhrase ?? (this.phase === 'waiting'
338
- ? pickPhrase(WAITING_PHRASES)
343
+ ? waitingPhrase()
339
344
  : thinkingPhrase(thinkingMs, undefined, isNight(new Date(nowMs).getHours())));
340
345
  return {
341
346
  phase: this.phase,
342
- line: `${phrase} · 总${elapsed}`,
347
+ line: `${phrase} · ${elapsedLine}`,
343
348
  phrase,
344
349
  toolCount: this.toolCount,
345
350
  turnElapsedMs: this.turnElapsedMs(nowMs),
346
351
  phaseStartedAt: this.phaseStartedAt,
347
352
  };
348
353
  }
349
- const label = this.phase === 'waiting' ? '等待模型响应' : '思考中';
354
+ const label = this.phase === 'waiting' ? t('waiting-label') : t('thinking-label');
350
355
  return {
351
356
  phase: this.phase,
352
- line: `${label} · 总${elapsed}`,
357
+ line: `${label} · ${elapsedLine}`,
353
358
  label,
354
359
  toolCount: this.toolCount,
355
360
  turnElapsedMs: this.turnElapsedMs(nowMs),
@@ -359,16 +364,21 @@ export class ActivityTracker {
359
364
  doneSummary(nowMs) {
360
365
  const { thinkingMs, toolMs, toolCount } = this.stats();
361
366
  const tokens = this.turnTokens > 0 ? ` · 🔥 ${fmtTokens(this.turnTokens)}` : '';
362
- const base = `${this.donePrefix} · ${toolCount} 工具 · 想${fmtDuration(thinkingMs)} 干${fmtDuration(toolMs)}${tokens}`;
367
+ const tools = t(toolCount === 1 ? 'tool-count-one' : 'tool-count-many', { count: toolCount });
368
+ const summary = t('done-summary', {
369
+ tools,
370
+ thinking: fmtDuration(thinkingMs),
371
+ tooling: fmtDuration(toolMs),
372
+ });
363
373
  if (!this.config.phrases) {
364
- return { line: `搞定 ✓ · ${toolCount} 工具 · 想${fmtDuration(thinkingMs)} 干${fmtDuration(toolMs)}${tokens}` };
374
+ return { line: `${t('done-prefix')} · ${summary}${tokens}` };
365
375
  }
366
376
  const last = this.doneQueue.at(-1);
367
377
  if (last !== undefined && nowMs - last.endedAt < DONE_FRAGMENT_MS) {
368
378
  const fragment = toolFragment(last);
369
- return { line: `${this.donePrefix} · ${fragment} · ${toolCount} 工具${tokens}`, phrase: this.donePrefix };
379
+ return { line: `${this.donePrefix} · ${fragment} · ${tools}${tokens}`, phrase: this.donePrefix };
370
380
  }
371
- return { line: base, ...(this.donePrefix === '搞定 ✓' ? {} : { phrase: this.donePrefix }) };
381
+ return { line: `${this.donePrefix} · ${summary}${tokens}`, phrase: this.donePrefix };
372
382
  }
373
383
  /** The fresh self-narration line, or null once the stream has been quiet. */
374
384
  freshNarration(nowMs) {
package/package.json CHANGED
@@ -1,117 +1,119 @@
1
- {
2
- "name": "dsh-working-activity",
3
- "description": "Live model working-status line: playful copy, running tool, turn elapsed — for TUI prompt and Web UI",
4
- "version": "0.2.6",
5
- "type": "module",
6
- "main": "lib/types/index.js",
7
- "types": "lib/types/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/types/index.d.ts",
11
- "default": "./lib/types/index.js"
12
- },
13
- "./client": {
14
- "types": "./lib/types/client/index.d.ts",
15
- "default": "./lib/client.js"
16
- },
17
- "./events": {
18
- "types": "./lib/types/events.d.ts",
19
- "default": "./lib/types/events.js"
20
- },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/types/invariant.js"
24
- },
25
- "./status": {
26
- "types": "./lib/types/status.d.ts",
27
- "default": "./lib/types/status.js"
28
- },
29
- "./cordis.patch.yml": "./cordis.patch.yml",
30
- "./src/*": "./src/*",
31
- "./package.json": "./package.json"
32
- },
33
- "files": [
34
- "lib",
35
- "src",
36
- "cordis.patch.yml"
37
- ],
38
- "engines": {
39
- "node": "^22.19 || >=24"
40
- },
41
- "scripts": {
42
- "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
43
- "build:client": "tsdown -c tsdown.config.ts",
44
- "verify:deps": "node scripts/verify-manifest-deps.mjs",
45
- "verify:lockfiles": "node scripts/verify-lockfiles.mjs",
46
- "prepublishOnly": "npm run verify:deps && npm run verify:lockfiles && npm run build && npm run build:client"
47
- },
48
- "license": "BSD-3-Clause",
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/ccch1mneyyy/working-activity.git"
52
- },
53
- "bugs": {
54
- "url": "https://github.com/ccch1mneyyy/working-activity/issues"
55
- },
56
- "homepage": "https://github.com/ccch1mneyyy/working-activity#readme",
57
- "dsh": {
58
- "bundle": {
59
- "patch": "./cordis.patch.yml"
60
- },
61
- "client": {
62
- "platform": "web",
63
- "inject": [
64
- "@deepseek-ai/dsh-client-runtime",
65
- "@deepseek-ai/dsh-client-ui-conversation",
66
- "@deepseek-ai/dsh-client-ui-slots"
67
- ]
68
- }
69
- },
70
- "peerDependencies": {
71
- "@deepseek-ai/cordis": "^4.0.1",
72
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
73
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
74
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
75
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
76
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
77
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
78
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
79
- "@deepseek-ai/schemastery": "^3.18.1",
80
- "react": "^18.2.0"
81
- },
82
- "devDependencies": {
83
- "@deepseek-ai/cordis": "^4.0.1",
84
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
85
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
86
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.0-rc.6",
87
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
88
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
89
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
90
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
91
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
92
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
93
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
94
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
95
- "@deepseek-ai/schemastery": "^3.18.1",
96
- "@types/node": "^22.0.0",
97
- "@types/react": "~18.3.1",
98
- "lightningcss": "^1.32.0",
99
- "react": "^18.2.0",
100
- "tsdown": "^0.22.2",
101
- "typescript": "^6.0.3"
102
- },
103
- "peerDependenciesMeta": {
104
- "@deepseek-ai/dsh-client-runtime": {
105
- "optional": true
106
- },
107
- "@deepseek-ai/dsh-client-ui-conversation": {
108
- "optional": true
109
- },
110
- "@deepseek-ai/dsh-client-ui-slots": {
111
- "optional": true
112
- },
113
- "react": {
114
- "optional": true
115
- }
116
- }
117
- }
1
+ {
2
+ "name": "dsh-working-activity",
3
+ "description": "Live model working-status line: playful copy, running tool, turn elapsed — for TUI prompt and Web UI",
4
+ "version": "0.3.0",
5
+ "type": "module",
6
+ "main": "lib/types/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/types/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./lib/types/client/index.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./events": {
18
+ "types": "./lib/types/events.d.ts",
19
+ "default": "./lib/types/events.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/types/invariant.js"
24
+ },
25
+ "./status": {
26
+ "types": "./lib/types/status.d.ts",
27
+ "default": "./lib/types/status.js"
28
+ },
29
+ "./cordis.patch.yml": "./cordis.patch.yml",
30
+ "./src/*": "./src/*",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "lib",
35
+ "src",
36
+ "cordis.patch.yml"
37
+ ],
38
+ "engines": {
39
+ "node": "^22.19 || >=24"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
43
+ "build:client": "tsdown -c tsdown.config.ts",
44
+ "test": "vitest run",
45
+ "verify:deps": "node scripts/verify-manifest-deps.mjs",
46
+ "verify:lockfiles": "node scripts/verify-lockfiles.mjs",
47
+ "prepublishOnly": "npm run verify:deps && npm run verify:lockfiles && npm run build && npm run build:client"
48
+ },
49
+ "license": "BSD-3-Clause",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/ccch1mneyyy/working-activity.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/ccch1mneyyy/working-activity/issues"
56
+ },
57
+ "homepage": "https://github.com/ccch1mneyyy/working-activity#readme",
58
+ "dsh": {
59
+ "bundle": {
60
+ "patch": "./cordis.patch.yml"
61
+ },
62
+ "client": {
63
+ "platform": "web",
64
+ "inject": [
65
+ "@deepseek-ai/dsh-client-runtime",
66
+ "@deepseek-ai/dsh-client-ui-conversation",
67
+ "@deepseek-ai/dsh-client-ui-slots"
68
+ ]
69
+ }
70
+ },
71
+ "peerDependencies": {
72
+ "@deepseek-ai/cordis": "^4.0.1",
73
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
80
+ "@deepseek-ai/schemastery": "^3.18.1",
81
+ "react": "^18.2.0"
82
+ },
83
+ "devDependencies": {
84
+ "@deepseek-ai/cordis": "^4.0.1",
85
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
86
+ "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
87
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.0-rc.6",
88
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
90
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
91
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
92
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
93
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
94
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
95
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
96
+ "@deepseek-ai/schemastery": "^3.18.1",
97
+ "@types/node": "^22.0.0",
98
+ "@types/react": "~18.3.1",
99
+ "lightningcss": "^1.32.0",
100
+ "react": "^18.2.0",
101
+ "tsdown": "^0.22.2",
102
+ "typescript": "^6.0.3",
103
+ "vitest": "^3.2.4"
104
+ },
105
+ "peerDependenciesMeta": {
106
+ "@deepseek-ai/dsh-client-runtime": {
107
+ "optional": true
108
+ },
109
+ "@deepseek-ai/dsh-client-ui-conversation": {
110
+ "optional": true
111
+ },
112
+ "@deepseek-ai/dsh-client-ui-slots": {
113
+ "optional": true
114
+ },
115
+ "react": {
116
+ "optional": true
117
+ }
118
+ }
119
+ }
@@ -1,74 +1,74 @@
1
- /* Working line row: one dim line with a phase-colored breathing marker and
2
- the turn's tool-count badge, sized to the composer dock inset like the
3
- queue strip. Mirrors the pre-slots patch's WorkingLine visuals, re-targeted
4
- at the rc.6 design tokens. */
5
-
6
- .line {
7
- box-sizing: border-box;
8
- display: flex;
9
- align-items: center;
10
- gap: 8px;
11
- width: 100%;
12
- max-width: var(--dsh-composer-card-max-width);
13
- margin: 0 auto;
14
- padding: 4px 16px 0;
15
- font-family: var(--dsw-font-family);
16
- font-size: 13px;
17
- line-height: 18px;
18
- color: var(--dsw-alias-label-secondary);
19
- }
20
-
21
- .marker {
22
- flex: none;
23
- width: 7px;
24
- height: 7px;
25
- border-radius: 50%;
26
- background: var(--dsw-alias-label-tertiary);
27
- animation: working-pulse 1.6s ease-in-out infinite;
28
- }
29
-
30
- /* The model's turn is done: steady marker in the brand tone, no pulse. */
31
- .line[data-activity-phase='done'] .marker {
32
- background: var(--dsw-alias-state-business-primary);
33
- animation: none;
34
- }
35
-
36
- /* A tool is actually executing: brand-tone pulse. */
37
- .line[data-activity-phase='tool'] .marker {
38
- background: var(--dsw-alias-state-business-primary);
39
- }
40
-
41
- /* Waiting for the first token: muted pulse. */
42
- .line[data-activity-phase='waiting'] .marker {
43
- background: var(--dsw-alias-label-tertiary);
44
- opacity: 0.6;
45
- }
46
-
47
- .text {
48
- overflow: hidden;
49
- text-overflow: ellipsis;
50
- white-space: nowrap;
51
- }
52
-
53
- .tools {
54
- flex: none;
55
- min-width: 18px;
56
- padding: 0 5px;
57
- border-radius: 9px;
58
- background: var(--dsw-alias-surface-tertiary);
59
- color: var(--dsw-alias-label-secondary);
60
- font-size: 11px;
61
- line-height: 18px;
62
- text-align: center;
63
- }
64
-
65
- @keyframes working-pulse {
66
- 0%,
67
- 100% {
68
- opacity: 1;
69
- }
70
-
71
- 50% {
72
- opacity: 0.35;
73
- }
74
- }
1
+ /* Working line row: one dim line with a phase-colored breathing marker and
2
+ the turn's tool-count badge, sized to the composer dock inset like the
3
+ queue strip. Mirrors the pre-slots patch's WorkingLine visuals, re-targeted
4
+ at the rc.6 design tokens. */
5
+
6
+ .line {
7
+ box-sizing: border-box;
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 8px;
11
+ width: 100%;
12
+ max-width: var(--dsh-composer-card-max-width);
13
+ margin: 0 auto;
14
+ padding: 4px 16px 0;
15
+ font-family: var(--dsw-font-family);
16
+ font-size: 13px;
17
+ line-height: 18px;
18
+ color: var(--dsw-alias-label-secondary);
19
+ }
20
+
21
+ .marker {
22
+ flex: none;
23
+ width: 7px;
24
+ height: 7px;
25
+ border-radius: 50%;
26
+ background: var(--dsw-alias-label-tertiary);
27
+ animation: working-pulse 1.6s ease-in-out infinite;
28
+ }
29
+
30
+ /* The model's turn is done: steady marker in the brand tone, no pulse. */
31
+ .line[data-activity-phase='done'] .marker {
32
+ background: var(--dsw-alias-state-business-primary);
33
+ animation: none;
34
+ }
35
+
36
+ /* A tool is actually executing: brand-tone pulse. */
37
+ .line[data-activity-phase='tool'] .marker {
38
+ background: var(--dsw-alias-state-business-primary);
39
+ }
40
+
41
+ /* Waiting for the first token: muted pulse. */
42
+ .line[data-activity-phase='waiting'] .marker {
43
+ background: var(--dsw-alias-label-tertiary);
44
+ opacity: 0.6;
45
+ }
46
+
47
+ .text {
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .tools {
54
+ flex: none;
55
+ min-width: 18px;
56
+ padding: 0 5px;
57
+ border-radius: 9px;
58
+ background: var(--dsw-alias-surface-tertiary);
59
+ color: var(--dsw-alias-label-secondary);
60
+ font-size: 11px;
61
+ line-height: 18px;
62
+ text-align: center;
63
+ }
64
+
65
+ @keyframes working-pulse {
66
+ 0%,
67
+ 100% {
68
+ opacity: 1;
69
+ }
70
+
71
+ 50% {
72
+ opacity: 0.35;
73
+ }
74
+ }
@@ -1,42 +1,42 @@
1
- // Working-line dock entry: one dim full-width row above the composer card
2
- // showing the live working-activity snapshot — phase-colored breathing
3
- // marker, the host-composed status line, and the turn's tool count badge.
4
- // Renders for every live phase (waiting/thinking/tool) and the done summary;
5
- // hides while idle or before the first publish.
6
- //
7
- // The 'conversation.input.dock' SlotMap declaration lives in
8
- // @deepseek-ai/dsh-client-ui-conversation/client (contract/slots.ts); this
9
- // entry contributes into it without owning it.
10
- import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
11
- import css from './WorkingLine.module.css'
12
-
13
- /** Full props of the dock entry: the input-zone runtime share (session standard kit). */
14
- export type WorkingLineProps = PropsRuntime<'conversation.input.dock'>
15
-
16
- /** Tool-count badge copy (no locale seat: the line text itself is host-composed). */
17
- const TOOLS_LABEL = 'tools this turn'
18
-
19
- /**
20
- * Working-line dock entry: reads the latest activity snapshot off the
21
- * conversation snapshot and renders the row, or nothing when idle/absent.
22
- */
23
- export function WorkingLine({ useSession }: WorkingLineProps) {
24
- // Defensive read: the runtime patch that puts `activity` onto the
25
- // conversation snapshot ships separately — on an unpatched host the field
26
- // is absent (undefined), not null, and this component must render nothing
27
- // instead of crashing into the slot error boundary. Same never-throw
28
- // discipline as the node side's registration.js.
29
- const activity = useSession(s => s.activity ?? null)
30
- if (activity === null || activity.phase === 'idle' || activity.line === '') return null
31
- return (
32
- <div className={css.line} data-activity-phase={activity.phase}>
33
- <span className={css.marker} aria-hidden="true" />
34
- <span className={css.text}>{activity.line}</span>
35
- {activity.toolCount > 0 && (
36
- <span className={css.tools} title={`${activity.toolCount} ${TOOLS_LABEL}`}>
37
- {activity.toolCount}
38
- </span>
39
- )}
40
- </div>
41
- )
42
- }
1
+ // Working-line dock entry: one dim full-width row above the composer card
2
+ // showing the live working-activity snapshot — phase-colored breathing
3
+ // marker, the host-composed status line, and the turn's tool count badge.
4
+ // Renders for every live phase (waiting/thinking/tool) and the done summary;
5
+ // hides while idle or before the first publish.
6
+ //
7
+ // The 'conversation.input.dock' SlotMap declaration lives in
8
+ // @deepseek-ai/dsh-client-ui-conversation/client (contract/slots.ts); this
9
+ // entry contributes into it without owning it.
10
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
11
+ import css from './WorkingLine.module.css'
12
+
13
+ /** Full props of the dock entry: the input-zone runtime share (session standard kit). */
14
+ export type WorkingLineProps = PropsRuntime<'conversation.input.dock'>
15
+
16
+ /** Tool-count badge copy (no locale seat: the line text itself is host-composed). */
17
+ const TOOLS_LABEL = 'tools this turn'
18
+
19
+ /**
20
+ * Working-line dock entry: reads the latest activity snapshot off the
21
+ * conversation snapshot and renders the row, or nothing when idle/absent.
22
+ */
23
+ export function WorkingLine({ useSession }: WorkingLineProps) {
24
+ // Defensive read: the runtime patch that puts `activity` onto the
25
+ // conversation snapshot ships separately — on an unpatched host the field
26
+ // is absent (undefined), not null, and this component must render nothing
27
+ // instead of crashing into the slot error boundary. Same never-throw
28
+ // discipline as the node side's registration.js.
29
+ const activity = useSession(s => s.activity ?? null)
30
+ if (activity === null || activity.phase === 'idle' || activity.line === '') return null
31
+ return (
32
+ <div className={css.line} data-activity-phase={activity.phase}>
33
+ <span className={css.marker} aria-hidden="true" />
34
+ <span className={css.text}>{activity.line}</span>
35
+ {activity.toolCount > 0 && (
36
+ <span className={css.tools} title={`${activity.toolCount} ${TOOLS_LABEL}`}>
37
+ {activity.toolCount}
38
+ </span>
39
+ )}
40
+ </div>
41
+ )
42
+ }