dsh-working-activity 0.3.0 → 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/lib/client.js +3 -3
- 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 +13 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +24 -2
- package/lib/types/lang.d.ts +10 -0
- package/lib/types/lang.d.ts.map +1 -1
- package/lib/types/lang.js +4 -0
- package/lib/types/phrases.d.ts +86 -1
- package/lib/types/phrases.d.ts.map +1 -1
- package/lib/types/phrases.js +247 -16
- package/lib/types/status.d.ts +62 -0
- package/lib/types/status.d.ts.map +1 -1
- package/lib/types/status.js +183 -13
- package/package.json +127 -119
- package/src/config.ts +174 -0
- package/src/frames.ts +138 -0
- package/src/index.ts +43 -2
- package/src/lang.ts +4 -0
- package/src/phrases.ts +294 -16
- package/src/status.ts +209 -14
package/src/status.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
10
10
|
import {
|
|
11
|
-
actionFor,
|
|
12
|
-
|
|
11
|
+
actionFor, compactPhrase, continuePhrase, donePhrase, failPhrase, fmtDuration,
|
|
12
|
+
holidayPhrase, isGitTool, isNight, isWeekend, modelQuip, overflowPhrase, rarePhrase,
|
|
13
|
+
RARE_CHANCE, RARE_PHRASES, EN_RARE_PHRASES, thinkingPhrase, weekendPhrase, waitingPhrase,
|
|
13
14
|
} from './phrases.js'
|
|
14
15
|
import { t } from './lang.js'
|
|
15
16
|
|
|
@@ -46,6 +47,18 @@ export interface TurnStats {
|
|
|
46
47
|
readonly toolCount: number
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** Easter-egg toggles for the thinking phrase (pi extension parity). */
|
|
51
|
+
export interface TrackerFeatures {
|
|
52
|
+
/** Rare 1/150 easter eggs. */
|
|
53
|
+
readonly rareEggs?: boolean
|
|
54
|
+
/** Weekend greetings on Sat/Sun. */
|
|
55
|
+
readonly weekend?: boolean
|
|
56
|
+
/** Date-matched holiday / Lunar New Year copy. */
|
|
57
|
+
readonly holidays?: boolean
|
|
58
|
+
/** Night-owl copy between 00:00 and 06:00. */
|
|
59
|
+
readonly nightPhrases?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/** Configuration knobs for the state machine (subset of plugin Config). */
|
|
50
63
|
export interface TrackerConfig {
|
|
51
64
|
/** Playful copy pool on/off; false renders plain functional labels. */
|
|
@@ -54,6 +67,14 @@ export interface TrackerConfig {
|
|
|
54
67
|
readonly detailLimit: number
|
|
55
68
|
/** Hide the status line while idle. */
|
|
56
69
|
readonly showIdle: boolean
|
|
70
|
+
/** Easter-egg toggles; absent flags default to on. */
|
|
71
|
+
readonly features?: TrackerFeatures
|
|
72
|
+
/** User custom phrases appended to the base thinking pool. */
|
|
73
|
+
readonly customPhrases?: readonly string[]
|
|
74
|
+
/** Show an estimated tokens/s prefix while streaming (pi parity). */
|
|
75
|
+
readonly showTokPerSec?: boolean
|
|
76
|
+
/** Work reminder after this many turn-hours (0 = off). */
|
|
77
|
+
readonly workRemindAt?: number
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
/** A tool execution in flight. */
|
|
@@ -157,6 +178,27 @@ export class ActivityTracker {
|
|
|
157
178
|
private turnTokens = 0
|
|
158
179
|
/** Completion prefix drawn ONCE at turn end so the done line stays stable. */
|
|
159
180
|
private donePrefix = t('done-prefix')
|
|
181
|
+
/** Easter eggs shown once per turn (holiday / rare / weekend). */
|
|
182
|
+
private holidayShown = false
|
|
183
|
+
private rareShown = false
|
|
184
|
+
private weekendShown = false
|
|
185
|
+
/** One-off copy pinned by an external event (interrupt / model switch /
|
|
186
|
+
* compaction / work reminder), shown until it expires. */
|
|
187
|
+
private pendingPhrase: string | null = null
|
|
188
|
+
private pendingUntil = 0
|
|
189
|
+
/** Git branch of the session cwd (fed by the host, best-effort). */
|
|
190
|
+
private gitBranch: string | undefined
|
|
191
|
+
/** Consecutive fast tool streak (combo). */
|
|
192
|
+
private streak = 0
|
|
193
|
+
private lastToolEndAt = 0
|
|
194
|
+
private maxStreak = 0
|
|
195
|
+
/** Subagent (agent/task) calls in the current turn. */
|
|
196
|
+
private subagentCount = 0
|
|
197
|
+
/** Work-reminder fired once per turn. */
|
|
198
|
+
private reminded = false
|
|
199
|
+
/** Streaming token estimate for the tps prefix. */
|
|
200
|
+
private tokBuf = 0
|
|
201
|
+
private tokWindowStart = 0
|
|
160
202
|
|
|
161
203
|
/**
|
|
162
204
|
* @param config - Behavioral knobs.
|
|
@@ -184,6 +226,35 @@ export class ActivityTracker {
|
|
|
184
226
|
}
|
|
185
227
|
}
|
|
186
228
|
|
|
229
|
+
/** The user interrupted the running turn: show a comeback quip next. */
|
|
230
|
+
onInterrupted(): void {
|
|
231
|
+
if (!this.config.phrases) return
|
|
232
|
+
this.pendingPhrase = continuePhrase()
|
|
233
|
+
this.pendingUntil = this.now() + PENDING_MS
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The model was switched: quip for the new model id. */
|
|
237
|
+
onModelSwitch(modelId: string): void {
|
|
238
|
+
if (!this.config.phrases) return
|
|
239
|
+
const quip = modelQuip(modelId)
|
|
240
|
+
if (quip !== null) {
|
|
241
|
+
this.pendingPhrase = quip
|
|
242
|
+
this.pendingUntil = this.now() + PENDING_MS
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A context compaction finished (or overflowed): quip about it. */
|
|
247
|
+
onCompact(kind: 'done' | 'overflow'): void {
|
|
248
|
+
if (!this.config.phrases) return
|
|
249
|
+
this.pendingPhrase = kind === 'overflow' ? overflowPhrase() : compactPhrase()
|
|
250
|
+
this.pendingUntil = this.now() + PENDING_MS
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Feed the session cwd's git branch (best-effort, host-resolved). */
|
|
254
|
+
onGitBranch(branch: string | undefined): void {
|
|
255
|
+
this.gitBranch = branch
|
|
256
|
+
}
|
|
257
|
+
|
|
187
258
|
/** Consume one durable session event (turn/step/tool/stream). */
|
|
188
259
|
onSessionEvent(event: SessionEvent): void {
|
|
189
260
|
switch (event.type) {
|
|
@@ -201,6 +272,18 @@ export class ActivityTracker {
|
|
|
201
272
|
this.narratedText = null
|
|
202
273
|
this.lastChunkAt = 0
|
|
203
274
|
this.recentStream = ''
|
|
275
|
+
// Easter eggs are once-per-turn: a fresh turn can roll them again.
|
|
276
|
+
this.holidayShown = false
|
|
277
|
+
this.rareShown = false
|
|
278
|
+
this.weekendShown = false
|
|
279
|
+
// Per-turn stats reset; the pending quip (interrupt/model/compact)
|
|
280
|
+
// survives across the turn boundary so it shows on the next think.
|
|
281
|
+
this.streak = 0
|
|
282
|
+
this.maxStreak = 0
|
|
283
|
+
this.subagentCount = 0
|
|
284
|
+
this.reminded = false
|
|
285
|
+
this.tokBuf = 0
|
|
286
|
+
this.tokWindowStart = at
|
|
204
287
|
this.setPhase('waiting', at)
|
|
205
288
|
return
|
|
206
289
|
}
|
|
@@ -221,6 +304,8 @@ export class ActivityTracker {
|
|
|
221
304
|
this.recentStream = (this.recentStream + chunk.text).slice(-STREAM_BUFFER_CHARS)
|
|
222
305
|
const narration = extractNarration(this.recentStream)
|
|
223
306
|
if (narration !== null) this.narratedText = narration
|
|
307
|
+
// Streaming token estimate for the tps prefix (pi parity).
|
|
308
|
+
this.tokBuf += estimateTokens(chunk.text)
|
|
224
309
|
}
|
|
225
310
|
return
|
|
226
311
|
}
|
|
@@ -237,6 +322,12 @@ export class ActivityTracker {
|
|
|
237
322
|
if (this.phase === 'thinking' || this.phase === 'waiting') {
|
|
238
323
|
this.thinkingMs += at - this.thinkingStartedAt
|
|
239
324
|
}
|
|
325
|
+
// Combo streak: consecutive tools within COMBO_GAP_MS count up.
|
|
326
|
+
this.streak = (this.lastToolEndAt > 0 && at - this.lastToolEndAt <= COMBO_GAP_MS)
|
|
327
|
+
? this.streak + 1
|
|
328
|
+
: 1
|
|
329
|
+
if (this.streak > this.maxStreak) this.maxStreak = this.streak
|
|
330
|
+
if (/^(?:subagent|agent|task)$/i.test(event.data.name)) this.subagentCount += 1
|
|
240
331
|
const parsed = parseArguments(event.data.arguments)
|
|
241
332
|
const action = this.config.phrases ? actionFor(event.data.name, this.customActions) : event.data.name
|
|
242
333
|
const detail = detailFor(event.data.name, parsed, this.config.detailLimit)
|
|
@@ -264,6 +355,7 @@ export class ActivityTracker {
|
|
|
264
355
|
active.endedAt = at
|
|
265
356
|
this.toolMs += at - active.startedAt
|
|
266
357
|
this.toolCount += 1
|
|
358
|
+
this.lastToolEndAt = at
|
|
267
359
|
this.doneQueue.push({
|
|
268
360
|
action: active.action,
|
|
269
361
|
detail: active.detail,
|
|
@@ -337,11 +429,14 @@ export class ActivityTracker {
|
|
|
337
429
|
}
|
|
338
430
|
const fragment = toolFragment(tool)
|
|
339
431
|
const elapsed = fmtDuration(Math.max(0, nowMs - tool.startedAt))
|
|
340
|
-
const git = tool.isGit
|
|
432
|
+
const git = tool.isGit
|
|
433
|
+
? (this.gitBranch !== undefined ? ` · git ${this.gitBranch}` : ' · git')
|
|
434
|
+
: ''
|
|
435
|
+
const combo = this.streak >= COMBO_SHOW_AT ? ` · 🔥x${this.streak}` : ''
|
|
341
436
|
const narration = this.freshNarration(nowMs)
|
|
342
437
|
const line = narration === null
|
|
343
|
-
? `${fragment} · ${elapsed}${git}`
|
|
344
|
-
: `⏵ ${narration} · ${fragment} · ${elapsed}${git}`
|
|
438
|
+
? `${fragment} · ${elapsed}${git}${combo}`
|
|
439
|
+
: `⏵ ${narration} · ${fragment} · ${elapsed}${git}${combo}`
|
|
345
440
|
return {
|
|
346
441
|
phase: 'tool',
|
|
347
442
|
line,
|
|
@@ -391,21 +486,33 @@ export class ActivityTracker {
|
|
|
391
486
|
}
|
|
392
487
|
}
|
|
393
488
|
if (this.config.phrases) {
|
|
394
|
-
|
|
489
|
+
const pending = this.pendingPhraseAt(nowMs)
|
|
490
|
+
// Rare eggs linger longer (pi RARE_PHRASE_TICKS ≈ 7.5s).
|
|
491
|
+
const rotateMs = this.previousPhrase !== undefined && this.isRarePhrase(this.previousPhrase)
|
|
492
|
+
? RARE_ROTATE_MS
|
|
493
|
+
: PHRASE_ROTATE_MS
|
|
494
|
+
if (pending !== null) {
|
|
495
|
+
this.previousPhrase = pending
|
|
496
|
+
this.phraseChangedAt = nowMs
|
|
497
|
+
} else if (nowMs - this.phraseChangedAt >= rotateMs) {
|
|
395
498
|
// Waiting (pre-first-token) draws from the waiting pool; thinking
|
|
396
|
-
// rotates the
|
|
397
|
-
// language-aware, so a `/lang` switch shows
|
|
499
|
+
// rotates the egg-aware lively pool (holiday / rare / weekend /
|
|
500
|
+
// night). Both pools are language-aware, so a `/lang` switch shows
|
|
501
|
+
// on the next rotation.
|
|
398
502
|
this.previousPhrase = this.phase === 'waiting'
|
|
399
503
|
? waitingPhrase(this.previousPhrase)
|
|
400
|
-
:
|
|
504
|
+
: this.livelyPhrase(thinkingMs, nowMs)
|
|
401
505
|
this.phraseChangedAt = nowMs
|
|
402
506
|
}
|
|
403
507
|
const phrase = this.previousPhrase ?? (this.phase === 'waiting'
|
|
404
508
|
? waitingPhrase()
|
|
405
|
-
:
|
|
509
|
+
: this.livelyPhrase(thinkingMs, nowMs))
|
|
510
|
+
// Ellipsis breathing (pi DOT_FRAMES) + optional estimated tps prefix.
|
|
511
|
+
const dots = DOT_FRAMES[Math.floor(nowMs / TICK_MS) % DOT_FRAMES.length]
|
|
512
|
+
const tps = this.tpsPrefix(nowMs)
|
|
406
513
|
return {
|
|
407
514
|
phase: this.phase,
|
|
408
|
-
line: `${phrase} · ${elapsedLine}`,
|
|
515
|
+
line: `${tps}${phrase}${dots} · ${elapsedLine}`,
|
|
409
516
|
phrase,
|
|
410
517
|
toolCount: this.toolCount,
|
|
411
518
|
turnElapsedMs: this.turnElapsedMs(nowMs),
|
|
@@ -423,9 +530,75 @@ export class ActivityTracker {
|
|
|
423
530
|
}
|
|
424
531
|
}
|
|
425
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Pick the next thinking phrase with the pi extension's egg order:
|
|
535
|
+
* holiday (once per turn) → rare 1/150 (once per turn) → weekend greeting
|
|
536
|
+
* (once per turn) → elapsed-time tiers with night mixing. Every egg is
|
|
537
|
+
* gated by `config.features` (absent flags default to on).
|
|
538
|
+
*/
|
|
539
|
+
private livelyPhrase(thinkingMs: number, nowMs: number): string {
|
|
540
|
+
const features = this.config.features ?? {}
|
|
541
|
+
const now = new Date(nowMs)
|
|
542
|
+
if (features.holidays !== false && !this.holidayShown) {
|
|
543
|
+
const holiday = holidayPhrase(now)
|
|
544
|
+
if (holiday !== null) {
|
|
545
|
+
this.holidayShown = true
|
|
546
|
+
return holiday
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (features.rareEggs !== false && !this.rareShown && Math.random() < RARE_CHANCE) {
|
|
550
|
+
this.rareShown = true
|
|
551
|
+
return rarePhrase(this.previousPhrase)
|
|
552
|
+
}
|
|
553
|
+
if (features.weekend !== false && !this.weekendShown && isWeekend(now)) {
|
|
554
|
+
this.weekendShown = true
|
|
555
|
+
return weekendPhrase(this.previousPhrase)
|
|
556
|
+
}
|
|
557
|
+
return thinkingPhrase(
|
|
558
|
+
thinkingMs,
|
|
559
|
+
this.previousPhrase,
|
|
560
|
+
features.nightPhrases !== false && isNight(now.getHours()),
|
|
561
|
+
this.config.customPhrases,
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The pending one-off quip (interrupt / model / compact / work reminder)
|
|
566
|
+
* while it is still fresh; expired pending is cleared here. */
|
|
567
|
+
private pendingPhraseAt(nowMs: number): string | null {
|
|
568
|
+
if (this.pendingPhrase !== null) {
|
|
569
|
+
if (nowMs < this.pendingUntil) return this.pendingPhrase
|
|
570
|
+
this.pendingPhrase = null
|
|
571
|
+
}
|
|
572
|
+
const remindAt = this.config.workRemindAt ?? 0
|
|
573
|
+
if (!this.reminded && remindAt > 0) {
|
|
574
|
+
const hours = this.turnElapsedMs(nowMs) / 3_600_000
|
|
575
|
+
if (hours >= remindAt) {
|
|
576
|
+
this.reminded = true
|
|
577
|
+
return t('work-remind', { hours: Math.floor(hours) })
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Whether a phrase comes from the rare pool (longer display window). */
|
|
584
|
+
private isRarePhrase(phrase: string): boolean {
|
|
585
|
+
return RARE_PHRASES.includes(phrase) || EN_RARE_PHRASES.includes(phrase)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Estimated tokens/s while the stream is fresh (pi parity, opt-in). */
|
|
589
|
+
private tpsPrefix(nowMs: number): string {
|
|
590
|
+
if (!this.config.showTokPerSec || this.tokBuf <= 0) return ''
|
|
591
|
+
if (nowMs - this.lastChunkAt > TPS_WINDOW_MS) return ''
|
|
592
|
+
const windowSec = Math.max(1, (nowMs - this.tokWindowStart) / 1000)
|
|
593
|
+
const tps = Math.round(this.tokBuf / windowSec)
|
|
594
|
+
return tps > 0 ? `~${tps} tok/s · ` : ''
|
|
595
|
+
}
|
|
596
|
+
|
|
426
597
|
private doneSummary(nowMs: number): { line: string; phrase?: string } {
|
|
427
598
|
const { thinkingMs, toolMs, toolCount } = this.stats()
|
|
428
599
|
const tokens = this.turnTokens > 0 ? ` · 🔥 ${fmtTokens(this.turnTokens)}` : ''
|
|
600
|
+
const sub = this.subagentCount > 0 ? ` · ${t('subagent-count', { count: this.subagentCount })}` : ''
|
|
601
|
+
const combo = this.maxStreak >= COMBO_SHOW_AT ? ` · 🔥x${this.maxStreak}` : ''
|
|
429
602
|
const tools = t(toolCount === 1 ? 'tool-count-one' : 'tool-count-many', { count: toolCount })
|
|
430
603
|
const summary = t('done-summary', {
|
|
431
604
|
tools,
|
|
@@ -433,14 +606,14 @@ export class ActivityTracker {
|
|
|
433
606
|
tooling: fmtDuration(toolMs),
|
|
434
607
|
})
|
|
435
608
|
if (!this.config.phrases) {
|
|
436
|
-
return { line: `${t('done-prefix')} · ${summary}${tokens}` }
|
|
609
|
+
return { line: `${t('done-prefix')} · ${summary}${sub}${combo}${tokens}` }
|
|
437
610
|
}
|
|
438
611
|
const last = this.doneQueue.at(-1)
|
|
439
612
|
if (last !== undefined && nowMs - last.endedAt < DONE_FRAGMENT_MS) {
|
|
440
613
|
const fragment = toolFragment(last)
|
|
441
|
-
return { line: `${this.donePrefix} · ${fragment} · ${tools}${tokens}`, phrase: this.donePrefix }
|
|
614
|
+
return { line: `${this.donePrefix} · ${fragment} · ${tools}${sub}${combo}${tokens}`, phrase: this.donePrefix }
|
|
442
615
|
}
|
|
443
|
-
return { line: `${this.donePrefix} · ${summary}${tokens}`, phrase: this.donePrefix }
|
|
616
|
+
return { line: `${this.donePrefix} · ${summary}${sub}${combo}${tokens}`, phrase: this.donePrefix }
|
|
444
617
|
}
|
|
445
618
|
|
|
446
619
|
/** The fresh self-narration line, or null once the stream has been quiet. */
|
|
@@ -470,6 +643,20 @@ export class ActivityTracker {
|
|
|
470
643
|
|
|
471
644
|
/** Rotate the thinking phrase every N render ticks (render cadence ≈ 500ms → ~4s). */
|
|
472
645
|
const PHRASE_ROTATE_MS = 4000
|
|
646
|
+
/** Rare easter-egg phrases linger this long before rotation (pi ≈ 7.5s). */
|
|
647
|
+
const RARE_ROTATE_MS = 7500
|
|
648
|
+
/** One-off quips (interrupt / model / compact) display window. */
|
|
649
|
+
const PENDING_MS = 6000
|
|
650
|
+
/** Tools closer than this count as one combo streak. */
|
|
651
|
+
const COMBO_GAP_MS = 10_000
|
|
652
|
+
/** Streak at which the combo badge shows. */
|
|
653
|
+
const COMBO_SHOW_AT = 2
|
|
654
|
+
/** The tps estimate stays fresh this long after the last chunk. */
|
|
655
|
+
const TPS_WINDOW_MS = 3500
|
|
656
|
+
/** Render tick cadence (matches the TUI's 500ms activity timer). */
|
|
657
|
+
const TICK_MS = 500
|
|
658
|
+
/** Ellipsis breathing frames appended to thinking lines (pi parity). */
|
|
659
|
+
const DOT_FRAMES = ['', ' ·', ' ··', ' ···', ' ··', ' ·']
|
|
473
660
|
/** Cap on replayed done cards; older entries drop. */
|
|
474
661
|
const DONE_QUEUE_MAX = 6
|
|
475
662
|
/** Show the last tool's fragment in the done line for this long after it ends. */
|
|
@@ -496,6 +683,14 @@ function fmtTokens(tokens: number): string {
|
|
|
496
683
|
return String(tokens)
|
|
497
684
|
}
|
|
498
685
|
|
|
686
|
+
/** Coarse streaming token estimate (pi parity: CJK ×1.5, others ÷4). */
|
|
687
|
+
function estimateTokens(text: string): number {
|
|
688
|
+
const compact = text.replace(/\s/g, '')
|
|
689
|
+
if (compact.length === 0) return 0
|
|
690
|
+
const cjkCount = (compact.match(/[\u3400-\u9fff]/g) ?? []).length
|
|
691
|
+
return Math.max(1, Math.ceil(cjkCount * 1.5 + (compact.length - cjkCount) / 4))
|
|
692
|
+
}
|
|
693
|
+
|
|
499
694
|
/** Parse a tool call's raw arguments JSON defensively. */
|
|
500
695
|
function parseArguments(raw: string): Readonly<Record<string, unknown>> | undefined {
|
|
501
696
|
if (raw.trim().length === 0) return undefined
|