dsh-working-activity 0.2.6 → 0.3.2
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/LICENSE +28 -28
- package/README.i18n.yaml +6 -6
- package/README.md +122 -122
- package/README.zh.md +113 -113
- package/cordis.patch.yml +19 -19
- package/lib/client.js +6 -6
- package/lib/client.js.map +1 -1
- package/lib/types/config.d.ts +68 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +141 -0
- package/lib/types/frames.d.ts +37 -0
- package/lib/types/frames.d.ts.map +1 -0
- package/lib/types/frames.js +126 -0
- package/lib/types/index.d.ts +16 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +35 -6
- package/lib/types/lang.d.ts +100 -0
- package/lib/types/lang.d.ts.map +1 -0
- package/lib/types/lang.js +132 -0
- package/lib/types/phrases.d.ts +125 -6
- package/lib/types/phrases.d.ts.map +1 -1
- package/lib/types/phrases.js +362 -27
- package/lib/types/status.d.ts +62 -0
- package/lib/types/status.d.ts.map +1 -1
- package/lib/types/status.js +202 -22
- package/package.json +12 -2
- package/src/client/WorkingLine.module.css +74 -74
- package/src/client/WorkingLine.tsx +42 -42
- package/src/client/activity.ts +50 -50
- package/src/client/css-modules.d.ts +6 -6
- package/src/client/index.ts +42 -42
- package/src/config.ts +174 -0
- package/src/events.ts +45 -45
- package/src/frames.ts +138 -0
- package/src/index.ts +278 -228
- package/src/invariant.ts +69 -69
- package/src/lang.ts +145 -0
- package/src/phrases.ts +584 -180
- package/src/registration.ts +99 -99
- package/src/status.ts +706 -501
package/lib/types/status.js
CHANGED
|
@@ -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,
|
|
8
|
+
import { actionFor, compactPhrase, continuePhrase, donePhrase, failPhrase, fmtDuration, holidayPhrase, isGitTool, isNight, isWeekend, modelQuip, overflowPhrase, rarePhrase, RARE_CHANCE, RARE_PHRASES, EN_RARE_PHRASES, thinkingPhrase, weekendPhrase, 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,28 @@ 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');
|
|
99
|
+
/** Easter eggs shown once per turn (holiday / rare / weekend). */
|
|
100
|
+
holidayShown = false;
|
|
101
|
+
rareShown = false;
|
|
102
|
+
weekendShown = false;
|
|
103
|
+
/** One-off copy pinned by an external event (interrupt / model switch /
|
|
104
|
+
* compaction / work reminder), shown until it expires. */
|
|
105
|
+
pendingPhrase = null;
|
|
106
|
+
pendingUntil = 0;
|
|
107
|
+
/** Git branch of the session cwd (fed by the host, best-effort). */
|
|
108
|
+
gitBranch;
|
|
109
|
+
/** Consecutive fast tool streak (combo). */
|
|
110
|
+
streak = 0;
|
|
111
|
+
lastToolEndAt = 0;
|
|
112
|
+
maxStreak = 0;
|
|
113
|
+
/** Subagent (agent/task) calls in the current turn. */
|
|
114
|
+
subagentCount = 0;
|
|
115
|
+
/** Work-reminder fired once per turn. */
|
|
116
|
+
reminded = false;
|
|
117
|
+
/** Streaming token estimate for the tps prefix. */
|
|
118
|
+
tokBuf = 0;
|
|
119
|
+
tokWindowStart = 0;
|
|
98
120
|
/**
|
|
99
121
|
* @param config - Behavioral knobs.
|
|
100
122
|
* @param now - Wall-clock supplier (injectable for tests).
|
|
@@ -120,6 +142,34 @@ export class ActivityTracker {
|
|
|
120
142
|
this.waitingFirstToken = true;
|
|
121
143
|
}
|
|
122
144
|
}
|
|
145
|
+
/** The user interrupted the running turn: show a comeback quip next. */
|
|
146
|
+
onInterrupted() {
|
|
147
|
+
if (!this.config.phrases)
|
|
148
|
+
return;
|
|
149
|
+
this.pendingPhrase = continuePhrase();
|
|
150
|
+
this.pendingUntil = this.now() + PENDING_MS;
|
|
151
|
+
}
|
|
152
|
+
/** The model was switched: quip for the new model id. */
|
|
153
|
+
onModelSwitch(modelId) {
|
|
154
|
+
if (!this.config.phrases)
|
|
155
|
+
return;
|
|
156
|
+
const quip = modelQuip(modelId);
|
|
157
|
+
if (quip !== null) {
|
|
158
|
+
this.pendingPhrase = quip;
|
|
159
|
+
this.pendingUntil = this.now() + PENDING_MS;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** A context compaction finished (or overflowed): quip about it. */
|
|
163
|
+
onCompact(kind) {
|
|
164
|
+
if (!this.config.phrases)
|
|
165
|
+
return;
|
|
166
|
+
this.pendingPhrase = kind === 'overflow' ? overflowPhrase() : compactPhrase();
|
|
167
|
+
this.pendingUntil = this.now() + PENDING_MS;
|
|
168
|
+
}
|
|
169
|
+
/** Feed the session cwd's git branch (best-effort, host-resolved). */
|
|
170
|
+
onGitBranch(branch) {
|
|
171
|
+
this.gitBranch = branch;
|
|
172
|
+
}
|
|
123
173
|
/** Consume one durable session event (turn/step/tool/stream). */
|
|
124
174
|
onSessionEvent(event) {
|
|
125
175
|
switch (event.type) {
|
|
@@ -137,6 +187,18 @@ export class ActivityTracker {
|
|
|
137
187
|
this.narratedText = null;
|
|
138
188
|
this.lastChunkAt = 0;
|
|
139
189
|
this.recentStream = '';
|
|
190
|
+
// Easter eggs are once-per-turn: a fresh turn can roll them again.
|
|
191
|
+
this.holidayShown = false;
|
|
192
|
+
this.rareShown = false;
|
|
193
|
+
this.weekendShown = false;
|
|
194
|
+
// Per-turn stats reset; the pending quip (interrupt/model/compact)
|
|
195
|
+
// survives across the turn boundary so it shows on the next think.
|
|
196
|
+
this.streak = 0;
|
|
197
|
+
this.maxStreak = 0;
|
|
198
|
+
this.subagentCount = 0;
|
|
199
|
+
this.reminded = false;
|
|
200
|
+
this.tokBuf = 0;
|
|
201
|
+
this.tokWindowStart = at;
|
|
140
202
|
this.setPhase('waiting', at);
|
|
141
203
|
return;
|
|
142
204
|
}
|
|
@@ -158,6 +220,8 @@ export class ActivityTracker {
|
|
|
158
220
|
const narration = extractNarration(this.recentStream);
|
|
159
221
|
if (narration !== null)
|
|
160
222
|
this.narratedText = narration;
|
|
223
|
+
// Streaming token estimate for the tps prefix (pi parity).
|
|
224
|
+
this.tokBuf += estimateTokens(chunk.text);
|
|
161
225
|
}
|
|
162
226
|
return;
|
|
163
227
|
}
|
|
@@ -174,6 +238,14 @@ export class ActivityTracker {
|
|
|
174
238
|
if (this.phase === 'thinking' || this.phase === 'waiting') {
|
|
175
239
|
this.thinkingMs += at - this.thinkingStartedAt;
|
|
176
240
|
}
|
|
241
|
+
// Combo streak: consecutive tools within COMBO_GAP_MS count up.
|
|
242
|
+
this.streak = (this.lastToolEndAt > 0 && at - this.lastToolEndAt <= COMBO_GAP_MS)
|
|
243
|
+
? this.streak + 1
|
|
244
|
+
: 1;
|
|
245
|
+
if (this.streak > this.maxStreak)
|
|
246
|
+
this.maxStreak = this.streak;
|
|
247
|
+
if (/^(?:subagent|agent|task)$/i.test(event.data.name))
|
|
248
|
+
this.subagentCount += 1;
|
|
177
249
|
const parsed = parseArguments(event.data.arguments);
|
|
178
250
|
const action = this.config.phrases ? actionFor(event.data.name, this.customActions) : event.data.name;
|
|
179
251
|
const detail = detailFor(event.data.name, parsed, this.config.detailLimit);
|
|
@@ -202,6 +274,7 @@ export class ActivityTracker {
|
|
|
202
274
|
active.endedAt = at;
|
|
203
275
|
this.toolMs += at - active.startedAt;
|
|
204
276
|
this.toolCount += 1;
|
|
277
|
+
this.lastToolEndAt = at;
|
|
205
278
|
this.doneQueue.push({
|
|
206
279
|
action: active.action,
|
|
207
280
|
detail: active.detail,
|
|
@@ -231,13 +304,15 @@ export class ActivityTracker {
|
|
|
231
304
|
this.thinkingMs += Math.max(0, at - this.thinkingStartedAt);
|
|
232
305
|
}
|
|
233
306
|
// Draw the completion prefix ONCE so repeated renders of the done line
|
|
234
|
-
// stay stable (a fresh random per render would make it flicker).
|
|
307
|
+
// stay stable (a fresh random per render would make it flicker). The
|
|
308
|
+
// pools are language-aware, so the line matches the language the turn
|
|
309
|
+
// ended in.
|
|
235
310
|
const lastTool = this.doneQueue.at(-1);
|
|
236
311
|
if (this.config.phrases) {
|
|
237
|
-
this.donePrefix = lastTool?.failed ?
|
|
312
|
+
this.donePrefix = lastTool?.failed ? failPhrase() : donePhrase();
|
|
238
313
|
}
|
|
239
314
|
else {
|
|
240
|
-
this.donePrefix = '
|
|
315
|
+
this.donePrefix = t('done-prefix');
|
|
241
316
|
}
|
|
242
317
|
this.setPhase('done', at);
|
|
243
318
|
return;
|
|
@@ -275,11 +350,14 @@ export class ActivityTracker {
|
|
|
275
350
|
}
|
|
276
351
|
const fragment = toolFragment(tool);
|
|
277
352
|
const elapsed = fmtDuration(Math.max(0, nowMs - tool.startedAt));
|
|
278
|
-
const git = tool.isGit
|
|
353
|
+
const git = tool.isGit
|
|
354
|
+
? (this.gitBranch !== undefined ? ` · git ${this.gitBranch}` : ' · git')
|
|
355
|
+
: '';
|
|
356
|
+
const combo = this.streak >= COMBO_SHOW_AT ? ` · 🔥x${this.streak}` : '';
|
|
279
357
|
const narration = this.freshNarration(nowMs);
|
|
280
358
|
const line = narration === null
|
|
281
|
-
? `${fragment} · ${elapsed}${git}`
|
|
282
|
-
: `⏵ ${narration} · ${fragment} · ${elapsed}${git}`;
|
|
359
|
+
? `${fragment} · ${elapsed}${git}${combo}`
|
|
360
|
+
: `⏵ ${narration} · ${fragment} · ${elapsed}${git}${combo}`;
|
|
283
361
|
return {
|
|
284
362
|
phase: 'tool',
|
|
285
363
|
line,
|
|
@@ -314,11 +392,12 @@ export class ActivityTracker {
|
|
|
314
392
|
? 0
|
|
315
393
|
: this.thinkingMs + Math.max(0, nowMs - this.thinkingStartedAt);
|
|
316
394
|
const elapsed = fmtDuration(this.turnElapsedMs(nowMs));
|
|
395
|
+
const elapsedLine = t('line-elapsed', { elapsed });
|
|
317
396
|
const narration = this.freshNarration(nowMs);
|
|
318
397
|
if (narration !== null) {
|
|
319
398
|
return {
|
|
320
399
|
phase: this.phase,
|
|
321
|
-
line: `⏵ ${narration} ·
|
|
400
|
+
line: `⏵ ${narration} · ${elapsedLine}`,
|
|
322
401
|
phrase: narration,
|
|
323
402
|
toolCount: this.toolCount,
|
|
324
403
|
turnElapsedMs: this.turnElapsedMs(nowMs),
|
|
@@ -326,49 +405,128 @@ export class ActivityTracker {
|
|
|
326
405
|
};
|
|
327
406
|
}
|
|
328
407
|
if (this.config.phrases) {
|
|
329
|
-
|
|
408
|
+
const pending = this.pendingPhraseAt(nowMs);
|
|
409
|
+
// Rare eggs linger longer (pi RARE_PHRASE_TICKS ≈ 7.5s).
|
|
410
|
+
const rotateMs = this.previousPhrase !== undefined && this.isRarePhrase(this.previousPhrase)
|
|
411
|
+
? RARE_ROTATE_MS
|
|
412
|
+
: PHRASE_ROTATE_MS;
|
|
413
|
+
if (pending !== null) {
|
|
414
|
+
this.previousPhrase = pending;
|
|
415
|
+
this.phraseChangedAt = nowMs;
|
|
416
|
+
}
|
|
417
|
+
else if (nowMs - this.phraseChangedAt >= rotateMs) {
|
|
330
418
|
// Waiting (pre-first-token) draws from the waiting pool; thinking
|
|
331
|
-
// rotates the
|
|
419
|
+
// rotates the egg-aware lively pool (holiday / rare / weekend /
|
|
420
|
+
// night). Both pools are language-aware, so a `/lang` switch shows
|
|
421
|
+
// on the next rotation.
|
|
332
422
|
this.previousPhrase = this.phase === 'waiting'
|
|
333
|
-
?
|
|
334
|
-
:
|
|
423
|
+
? waitingPhrase(this.previousPhrase)
|
|
424
|
+
: this.livelyPhrase(thinkingMs, nowMs);
|
|
335
425
|
this.phraseChangedAt = nowMs;
|
|
336
426
|
}
|
|
337
427
|
const phrase = this.previousPhrase ?? (this.phase === 'waiting'
|
|
338
|
-
?
|
|
339
|
-
:
|
|
428
|
+
? waitingPhrase()
|
|
429
|
+
: this.livelyPhrase(thinkingMs, nowMs));
|
|
430
|
+
// Ellipsis breathing (pi DOT_FRAMES) + optional estimated tps prefix.
|
|
431
|
+
const dots = DOT_FRAMES[Math.floor(nowMs / TICK_MS) % DOT_FRAMES.length];
|
|
432
|
+
const tps = this.tpsPrefix(nowMs);
|
|
340
433
|
return {
|
|
341
434
|
phase: this.phase,
|
|
342
|
-
line: `${phrase} ·
|
|
435
|
+
line: `${tps}${phrase}${dots} · ${elapsedLine}`,
|
|
343
436
|
phrase,
|
|
344
437
|
toolCount: this.toolCount,
|
|
345
438
|
turnElapsedMs: this.turnElapsedMs(nowMs),
|
|
346
439
|
phaseStartedAt: this.phaseStartedAt,
|
|
347
440
|
};
|
|
348
441
|
}
|
|
349
|
-
const label = this.phase === 'waiting' ? '
|
|
442
|
+
const label = this.phase === 'waiting' ? t('waiting-label') : t('thinking-label');
|
|
350
443
|
return {
|
|
351
444
|
phase: this.phase,
|
|
352
|
-
line: `${label} ·
|
|
445
|
+
line: `${label} · ${elapsedLine}`,
|
|
353
446
|
label,
|
|
354
447
|
toolCount: this.toolCount,
|
|
355
448
|
turnElapsedMs: this.turnElapsedMs(nowMs),
|
|
356
449
|
phaseStartedAt: this.phaseStartedAt,
|
|
357
450
|
};
|
|
358
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Pick the next thinking phrase with the pi extension's egg order:
|
|
454
|
+
* holiday (once per turn) → rare 1/150 (once per turn) → weekend greeting
|
|
455
|
+
* (once per turn) → elapsed-time tiers with night mixing. Every egg is
|
|
456
|
+
* gated by `config.features` (absent flags default to on).
|
|
457
|
+
*/
|
|
458
|
+
livelyPhrase(thinkingMs, nowMs) {
|
|
459
|
+
const features = this.config.features ?? {};
|
|
460
|
+
const now = new Date(nowMs);
|
|
461
|
+
if (features.holidays !== false && !this.holidayShown) {
|
|
462
|
+
const holiday = holidayPhrase(now);
|
|
463
|
+
if (holiday !== null) {
|
|
464
|
+
this.holidayShown = true;
|
|
465
|
+
return holiday;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (features.rareEggs !== false && !this.rareShown && Math.random() < RARE_CHANCE) {
|
|
469
|
+
this.rareShown = true;
|
|
470
|
+
return rarePhrase(this.previousPhrase);
|
|
471
|
+
}
|
|
472
|
+
if (features.weekend !== false && !this.weekendShown && isWeekend(now)) {
|
|
473
|
+
this.weekendShown = true;
|
|
474
|
+
return weekendPhrase(this.previousPhrase);
|
|
475
|
+
}
|
|
476
|
+
return thinkingPhrase(thinkingMs, this.previousPhrase, features.nightPhrases !== false && isNight(now.getHours()), this.config.customPhrases);
|
|
477
|
+
}
|
|
478
|
+
/** The pending one-off quip (interrupt / model / compact / work reminder)
|
|
479
|
+
* while it is still fresh; expired pending is cleared here. */
|
|
480
|
+
pendingPhraseAt(nowMs) {
|
|
481
|
+
if (this.pendingPhrase !== null) {
|
|
482
|
+
if (nowMs < this.pendingUntil)
|
|
483
|
+
return this.pendingPhrase;
|
|
484
|
+
this.pendingPhrase = null;
|
|
485
|
+
}
|
|
486
|
+
const remindAt = this.config.workRemindAt ?? 0;
|
|
487
|
+
if (!this.reminded && remindAt > 0) {
|
|
488
|
+
const hours = this.turnElapsedMs(nowMs) / 3_600_000;
|
|
489
|
+
if (hours >= remindAt) {
|
|
490
|
+
this.reminded = true;
|
|
491
|
+
return t('work-remind', { hours: Math.floor(hours) });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/** Whether a phrase comes from the rare pool (longer display window). */
|
|
497
|
+
isRarePhrase(phrase) {
|
|
498
|
+
return RARE_PHRASES.includes(phrase) || EN_RARE_PHRASES.includes(phrase);
|
|
499
|
+
}
|
|
500
|
+
/** Estimated tokens/s while the stream is fresh (pi parity, opt-in). */
|
|
501
|
+
tpsPrefix(nowMs) {
|
|
502
|
+
if (!this.config.showTokPerSec || this.tokBuf <= 0)
|
|
503
|
+
return '';
|
|
504
|
+
if (nowMs - this.lastChunkAt > TPS_WINDOW_MS)
|
|
505
|
+
return '';
|
|
506
|
+
const windowSec = Math.max(1, (nowMs - this.tokWindowStart) / 1000);
|
|
507
|
+
const tps = Math.round(this.tokBuf / windowSec);
|
|
508
|
+
return tps > 0 ? `~${tps} tok/s · ` : '';
|
|
509
|
+
}
|
|
359
510
|
doneSummary(nowMs) {
|
|
360
511
|
const { thinkingMs, toolMs, toolCount } = this.stats();
|
|
361
512
|
const tokens = this.turnTokens > 0 ? ` · 🔥 ${fmtTokens(this.turnTokens)}` : '';
|
|
362
|
-
const
|
|
513
|
+
const sub = this.subagentCount > 0 ? ` · ${t('subagent-count', { count: this.subagentCount })}` : '';
|
|
514
|
+
const combo = this.maxStreak >= COMBO_SHOW_AT ? ` · 🔥x${this.maxStreak}` : '';
|
|
515
|
+
const tools = t(toolCount === 1 ? 'tool-count-one' : 'tool-count-many', { count: toolCount });
|
|
516
|
+
const summary = t('done-summary', {
|
|
517
|
+
tools,
|
|
518
|
+
thinking: fmtDuration(thinkingMs),
|
|
519
|
+
tooling: fmtDuration(toolMs),
|
|
520
|
+
});
|
|
363
521
|
if (!this.config.phrases) {
|
|
364
|
-
return { line:
|
|
522
|
+
return { line: `${t('done-prefix')} · ${summary}${sub}${combo}${tokens}` };
|
|
365
523
|
}
|
|
366
524
|
const last = this.doneQueue.at(-1);
|
|
367
525
|
if (last !== undefined && nowMs - last.endedAt < DONE_FRAGMENT_MS) {
|
|
368
526
|
const fragment = toolFragment(last);
|
|
369
|
-
return { line: `${this.donePrefix} · ${fragment} · ${
|
|
527
|
+
return { line: `${this.donePrefix} · ${fragment} · ${tools}${sub}${combo}${tokens}`, phrase: this.donePrefix };
|
|
370
528
|
}
|
|
371
|
-
return { line:
|
|
529
|
+
return { line: `${this.donePrefix} · ${summary}${sub}${combo}${tokens}`, phrase: this.donePrefix };
|
|
372
530
|
}
|
|
373
531
|
/** The fresh self-narration line, or null once the stream has been quiet. */
|
|
374
532
|
freshNarration(nowMs) {
|
|
@@ -396,6 +554,20 @@ export class ActivityTracker {
|
|
|
396
554
|
}
|
|
397
555
|
/** Rotate the thinking phrase every N render ticks (render cadence ≈ 500ms → ~4s). */
|
|
398
556
|
const PHRASE_ROTATE_MS = 4000;
|
|
557
|
+
/** Rare easter-egg phrases linger this long before rotation (pi ≈ 7.5s). */
|
|
558
|
+
const RARE_ROTATE_MS = 7500;
|
|
559
|
+
/** One-off quips (interrupt / model / compact) display window. */
|
|
560
|
+
const PENDING_MS = 6000;
|
|
561
|
+
/** Tools closer than this count as one combo streak. */
|
|
562
|
+
const COMBO_GAP_MS = 10_000;
|
|
563
|
+
/** Streak at which the combo badge shows. */
|
|
564
|
+
const COMBO_SHOW_AT = 2;
|
|
565
|
+
/** The tps estimate stays fresh this long after the last chunk. */
|
|
566
|
+
const TPS_WINDOW_MS = 3500;
|
|
567
|
+
/** Render tick cadence (matches the TUI's 500ms activity timer). */
|
|
568
|
+
const TICK_MS = 500;
|
|
569
|
+
/** Ellipsis breathing frames appended to thinking lines (pi parity). */
|
|
570
|
+
const DOT_FRAMES = ['', ' ·', ' ··', ' ···', ' ··', ' ·'];
|
|
399
571
|
/** Cap on replayed done cards; older entries drop. */
|
|
400
572
|
const DONE_QUEUE_MAX = 6;
|
|
401
573
|
/** Show the last tool's fragment in the done line for this long after it ends. */
|
|
@@ -423,6 +595,14 @@ function fmtTokens(tokens) {
|
|
|
423
595
|
return `${(tokens / 1000).toFixed(1)}k`;
|
|
424
596
|
return String(tokens);
|
|
425
597
|
}
|
|
598
|
+
/** Coarse streaming token estimate (pi parity: CJK ×1.5, others ÷4). */
|
|
599
|
+
function estimateTokens(text) {
|
|
600
|
+
const compact = text.replace(/\s/g, '');
|
|
601
|
+
if (compact.length === 0)
|
|
602
|
+
return 0;
|
|
603
|
+
const cjkCount = (compact.match(/[\u3400-\u9fff]/g) ?? []).length;
|
|
604
|
+
return Math.max(1, Math.ceil(cjkCount * 1.5 + (compact.length - cjkCount) / 4));
|
|
605
|
+
}
|
|
426
606
|
/** Parse a tool call's raw arguments JSON defensively. */
|
|
427
607
|
function parseArguments(raw) {
|
|
428
608
|
if (raw.trim().length === 0)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-working-activity",
|
|
3
3
|
"description": "Live model working-status line: playful copy, running tool, turn elapsed — for TUI prompt and Web UI",
|
|
4
|
-
"version": "0.2
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/types/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -14,10 +14,18 @@
|
|
|
14
14
|
"types": "./lib/types/client/index.d.ts",
|
|
15
15
|
"default": "./lib/client.js"
|
|
16
16
|
},
|
|
17
|
+
"./config": {
|
|
18
|
+
"types": "./lib/types/config.d.ts",
|
|
19
|
+
"default": "./lib/types/config.js"
|
|
20
|
+
},
|
|
17
21
|
"./events": {
|
|
18
22
|
"types": "./lib/types/events.d.ts",
|
|
19
23
|
"default": "./lib/types/events.js"
|
|
20
24
|
},
|
|
25
|
+
"./frames": {
|
|
26
|
+
"types": "./lib/types/frames.d.ts",
|
|
27
|
+
"default": "./lib/types/frames.js"
|
|
28
|
+
},
|
|
21
29
|
"./invariant": {
|
|
22
30
|
"types": "./lib/types/invariant.d.ts",
|
|
23
31
|
"default": "./lib/types/invariant.js"
|
|
@@ -41,6 +49,7 @@
|
|
|
41
49
|
"scripts": {
|
|
42
50
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
43
51
|
"build:client": "tsdown -c tsdown.config.ts",
|
|
52
|
+
"test": "vitest run",
|
|
44
53
|
"verify:deps": "node scripts/verify-manifest-deps.mjs",
|
|
45
54
|
"verify:lockfiles": "node scripts/verify-lockfiles.mjs",
|
|
46
55
|
"prepublishOnly": "npm run verify:deps && npm run verify:lockfiles && npm run build && npm run build:client"
|
|
@@ -98,7 +107,8 @@
|
|
|
98
107
|
"lightningcss": "^1.32.0",
|
|
99
108
|
"react": "^18.2.0",
|
|
100
109
|
"tsdown": "^0.22.2",
|
|
101
|
-
"typescript": "^6.0.3"
|
|
110
|
+
"typescript": "^6.0.3",
|
|
111
|
+
"vitest": "^3.2.4"
|
|
102
112
|
},
|
|
103
113
|
"peerDependenciesMeta": {
|
|
104
114
|
"@deepseek-ai/dsh-client-runtime": {
|
|
@@ -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
|
+
}
|