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/src/index.ts
CHANGED
|
@@ -1,228 +1,278 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* working-activity — a live "working line" for DeepSeek Harness agents.
|
|
3
|
-
*
|
|
4
|
-
* Folds the durable session stream (turn/step/tool/stream events) plus
|
|
5
|
-
* `agent/status` into a playful real-time status line, then publishes it two
|
|
6
|
-
* ways, both optional:
|
|
7
|
-
*
|
|
8
|
-
* - TUI: registers the `${activity}` prompt slot on `ctx.tuiPrompt` when the
|
|
9
|
-
* TUI is composed; add `${activity}` to `theme.leftPrompt` to see it.
|
|
10
|
-
* - Session log: appends log-only `activity/status` events (never surface
|
|
11
|
-
* events) for Web and other UI consumers; replay ignores them.
|
|
12
|
-
*
|
|
13
|
-
* The state machine itself lives in `./status.ts` (pure, clock-injected); this
|
|
14
|
-
* module only wires events, the render tick, and the two sinks.
|
|
15
|
-
* @module @deepseek-ai/dsh-working-activity
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
19
|
-
import z from '@deepseek-ai/schemastery'
|
|
20
|
-
import type { Session } from '@deepseek-ai/dsh-session'
|
|
21
|
-
// Type-only: resolves the agent/status cordis event declaration.
|
|
22
|
-
import type {} from '@deepseek-ai/dsh-agent'
|
|
23
|
-
// Type-only: resolves ctx.systemPrompt for the narration section injection.
|
|
24
|
-
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
25
|
-
import { ActivityTracker } from './status.js'
|
|
26
|
-
import { registerActivityEventType } from './registration.js'
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export type
|
|
32
|
-
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1
|
+
/**
|
|
2
|
+
* working-activity — a live "working line" for DeepSeek Harness agents.
|
|
3
|
+
*
|
|
4
|
+
* Folds the durable session stream (turn/step/tool/stream events) plus
|
|
5
|
+
* `agent/status` into a playful real-time status line, then publishes it two
|
|
6
|
+
* ways, both optional:
|
|
7
|
+
*
|
|
8
|
+
* - TUI: registers the `${activity}` prompt slot on `ctx.tuiPrompt` when the
|
|
9
|
+
* TUI is composed; add `${activity}` to `theme.leftPrompt` to see it.
|
|
10
|
+
* - Session log: appends log-only `activity/status` events (never surface
|
|
11
|
+
* events) for Web and other UI consumers; replay ignores them.
|
|
12
|
+
*
|
|
13
|
+
* The state machine itself lives in `./status.ts` (pure, clock-injected); this
|
|
14
|
+
* module only wires events, the render tick, and the two sinks.
|
|
15
|
+
* @module @deepseek-ai/dsh-working-activity
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
19
|
+
import z from '@deepseek-ai/schemastery'
|
|
20
|
+
import type { Session } from '@deepseek-ai/dsh-session'
|
|
21
|
+
// Type-only: resolves the agent/status cordis event declaration.
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-agent'
|
|
23
|
+
// Type-only: resolves ctx.systemPrompt for the narration section injection.
|
|
24
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
25
|
+
import { ActivityTracker } from './status.js'
|
|
26
|
+
import { registerActivityEventType } from './registration.js'
|
|
27
|
+
import { setLangOverride, t } from './lang.js'
|
|
28
|
+
import { DEFAULT_PRESET } from './frames.js'
|
|
29
|
+
import type { ActivityState } from './status.js'
|
|
30
|
+
import type { ActivityStatusEvent } from './events.js'
|
|
31
|
+
// Re-export the event type + SessionEventMap merge: the package root must carry
|
|
32
|
+
// the declare-module side effect for consumers resolving the built d.ts.
|
|
33
|
+
export type * from './events.js'
|
|
34
|
+
|
|
35
|
+
export const name = 'working-activity'
|
|
36
|
+
|
|
37
|
+
/** Configurable knobs; every key has a sane default. */
|
|
38
|
+
export type Config = {
|
|
39
|
+
/** Playful copy pool; false renders plain functional labels. */
|
|
40
|
+
phrases?: boolean
|
|
41
|
+
/** Append `activity/status` session events for UI consumers. Default OFF:
|
|
42
|
+
* dsh-session's append() cannot mark events ignorable, and the resume
|
|
43
|
+
* read path refuses logs containing unknown non-ignorable types — every
|
|
44
|
+
* appended snapshot makes the whole session unresumable. Re-enable only
|
|
45
|
+
* for a log-replaying consumer on a harness that supports ignorable
|
|
46
|
+
* appends. The live status line (prompt slot / session events) is
|
|
47
|
+
* unaffected by this flag. */
|
|
48
|
+
publish?: boolean
|
|
49
|
+
/** Status render tick interval in ms. */
|
|
50
|
+
tickMs?: number
|
|
51
|
+
/** Minimum interval in ms between published events while the line is stable. */
|
|
52
|
+
publishIntervalMs?: number
|
|
53
|
+
/** Maximum displayed detail length (paths/commands/patterns). */
|
|
54
|
+
detailLimit?: number
|
|
55
|
+
/** Exact tool-name → action-copy pools (case-insensitive match). */
|
|
56
|
+
customActions?: Record<string, string[]>
|
|
57
|
+
/** Inject the `⏵` self-narration contract into the system prompt and surface it. */
|
|
58
|
+
narrate?: boolean
|
|
59
|
+
/** UI language: `auto` follows `DSH_TUI_LANG` → `~/.dsh-tui/lang.json` →
|
|
60
|
+
* OS locale → zh; `zh`/`en` pin the copy directly. */
|
|
61
|
+
lang?: 'auto' | 'zh' | 'en'
|
|
62
|
+
/** Default frame preset name (informational for UI consumers; the TUI
|
|
63
|
+
* resolves the persisted `frames` choice itself). */
|
|
64
|
+
frames?: string
|
|
65
|
+
/** lively: full flourish (default) / minimal: functional labels only. */
|
|
66
|
+
mode?: 'lively' | 'minimal'
|
|
67
|
+
/** Per-feature switches; explicit values override `mode` defaults. */
|
|
68
|
+
features?: Record<string, boolean>
|
|
69
|
+
/** Extra thinking phrases appended to the base pool. */
|
|
70
|
+
customPhrases?: string[]
|
|
71
|
+
/** Show an estimated tokens/s prefix while streaming. */
|
|
72
|
+
showTokPerSec?: boolean
|
|
73
|
+
/** Work reminder after this many turn-hours (0 = off). */
|
|
74
|
+
workRemindAt?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Explicit annotation: the inferred z.dict output references cosmokit's
|
|
78
|
+
// Dict through a pnpm-virtual path, which is not portable in declaration
|
|
79
|
+
// emit (TS2883) when the dependency graph shifts. The global `Schemastery`
|
|
80
|
+
// interface comes from schemastery's own d.ts (declare global).
|
|
81
|
+
export const Config: Schemastery<Config> = z.object({
|
|
82
|
+
phrases: z.boolean().default(true),
|
|
83
|
+
publish: z.boolean().default(false),
|
|
84
|
+
tickMs: z.number().step(50).min(100).max(5000).default(500),
|
|
85
|
+
publishIntervalMs: z.number().step(500).min(500).max(30_000).default(2000),
|
|
86
|
+
detailLimit: z.number().step(1).min(8).max(120).default(40),
|
|
87
|
+
customActions: z.dict(z.array(z.string())).default({}),
|
|
88
|
+
narrate: z.boolean().default(true),
|
|
89
|
+
lang: z.union(['auto', 'zh', 'en']).default('auto'),
|
|
90
|
+
frames: z.string().default(DEFAULT_PRESET),
|
|
91
|
+
mode: z.union(['lively', 'minimal']).default('lively'),
|
|
92
|
+
features: z.dict(z.boolean()).default({}),
|
|
93
|
+
customPhrases: z.array(z.string()).default([]),
|
|
94
|
+
showTokPerSec: z.boolean().default(false),
|
|
95
|
+
workRemindAt: z.number().min(0).max(24).default(0),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** Structural view of the TUI prompt service; the real type lives in dsh-tui. */
|
|
99
|
+
interface TuiPromptLike {
|
|
100
|
+
register(name: string, initialValue?: string): {
|
|
101
|
+
set(value: string | undefined): void
|
|
102
|
+
dispose(): void
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Resolved plugin configuration after schema defaults. */
|
|
107
|
+
interface ResolvedConfig {
|
|
108
|
+
phrases: boolean
|
|
109
|
+
publish: boolean
|
|
110
|
+
tickMs: number
|
|
111
|
+
publishIntervalMs: number
|
|
112
|
+
detailLimit: number
|
|
113
|
+
customActions: Record<string, string[]>
|
|
114
|
+
narrate: boolean
|
|
115
|
+
lang: 'auto' | 'zh' | 'en'
|
|
116
|
+
frames: string
|
|
117
|
+
mode: 'lively' | 'minimal'
|
|
118
|
+
features: Record<string, boolean>
|
|
119
|
+
customPhrases: string[]
|
|
120
|
+
showTokPerSec: boolean
|
|
121
|
+
workRemindAt: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wire the working-activity plugin.
|
|
126
|
+
* @param ctx - Cordis context (agent loop + session services composed).
|
|
127
|
+
* @param config - Validated plugin config (schema defaults applied).
|
|
128
|
+
*/
|
|
129
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
130
|
+
// Register the event type BEFORE anything can publish or validate: the
|
|
131
|
+
// strict read paths (resume seed validation, persistence load) refuse
|
|
132
|
+
// logs with unknown non-ignorable types. Registration is unconditional —
|
|
133
|
+
// it also protects READING logs written by an earlier publish:true era
|
|
134
|
+
// in processes where publishing itself is off. See registration.ts.
|
|
135
|
+
registerActivityEventType()
|
|
136
|
+
const resolved: ResolvedConfig = {
|
|
137
|
+
// `mode: minimal` renders functional labels only (pi extension parity).
|
|
138
|
+
phrases: config.phrases ?? config.mode !== 'minimal',
|
|
139
|
+
publish: config.publish ?? false,
|
|
140
|
+
tickMs: config.tickMs ?? 500,
|
|
141
|
+
publishIntervalMs: config.publishIntervalMs ?? 2000,
|
|
142
|
+
detailLimit: config.detailLimit ?? 40,
|
|
143
|
+
narrate: config.narrate ?? true,
|
|
144
|
+
lang: config.lang ?? 'auto',
|
|
145
|
+
customActions: config.customActions ?? {},
|
|
146
|
+
frames: config.frames ?? DEFAULT_PRESET,
|
|
147
|
+
mode: config.mode ?? 'lively',
|
|
148
|
+
features: config.features ?? {},
|
|
149
|
+
customPhrases: config.customPhrases ?? [],
|
|
150
|
+
showTokPerSec: config.showTokPerSec ?? false,
|
|
151
|
+
workRemindAt: config.workRemindAt ?? 0,
|
|
152
|
+
}
|
|
153
|
+
// A pinned plugin-level language beats the env/file chain; releasing it on
|
|
154
|
+
// dispose restores `auto` for any other composition in the process.
|
|
155
|
+
setLangOverride(resolved.lang)
|
|
156
|
+
ctx.effect(() => () => setLangOverride('auto'), 'working-activity lang override')
|
|
157
|
+
const trackers = new Map<Session, ActivityTracker>()
|
|
158
|
+
let activeSession: Session | undefined
|
|
159
|
+
let lastPublishedLine: string | undefined
|
|
160
|
+
let lastPublishedPhase: string | undefined
|
|
161
|
+
let lastPublishAt = 0
|
|
162
|
+
|
|
163
|
+
// Optional TUI seam: no TUI composed -> no slot, no error. The register()
|
|
164
|
+
// call is itself effect-owned, so fiber disposal unregisters the slot.
|
|
165
|
+
const prompt = ctx.get('tuiPrompt', false) as TuiPromptLike | undefined
|
|
166
|
+
const promptHandle = prompt?.register('activity', undefined)
|
|
167
|
+
|
|
168
|
+
// The `⏵` self-narration contract rides the stable system-prompt sections:
|
|
169
|
+
// injected when the systemPrompt service is composed (agent assemblies
|
|
170
|
+
// always mount it), removed with this fiber. The text is resolved at every
|
|
171
|
+
// assembly in the live language, so a `/lang` switch applies to the next
|
|
172
|
+
// turn without rebuilding the agent.
|
|
173
|
+
if (resolved.narrate) {
|
|
174
|
+
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
175
|
+
promptCtx.systemPrompt.section({
|
|
176
|
+
name: 'working-activity:narrate',
|
|
177
|
+
order: 60,
|
|
178
|
+
text: () => t('narrate-instruction'),
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const trackerFor = (session: Session): ActivityTracker => {
|
|
184
|
+
let tracker = trackers.get(session)
|
|
185
|
+
if (tracker === undefined) {
|
|
186
|
+
tracker = new ActivityTracker(
|
|
187
|
+
{
|
|
188
|
+
phrases: resolved.phrases,
|
|
189
|
+
detailLimit: resolved.detailLimit,
|
|
190
|
+
showIdle: false,
|
|
191
|
+
features: resolved.features,
|
|
192
|
+
customPhrases: resolved.customPhrases,
|
|
193
|
+
showTokPerSec: resolved.showTokPerSec,
|
|
194
|
+
workRemindAt: resolved.workRemindAt,
|
|
195
|
+
},
|
|
196
|
+
Date.now,
|
|
197
|
+
resolved.customActions,
|
|
198
|
+
)
|
|
199
|
+
trackers.set(session, tracker)
|
|
200
|
+
}
|
|
201
|
+
return tracker
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Publish one rendered snapshot: TUI slot update + throttled session event.
|
|
206
|
+
* Callers snapshot the tracker state at event time and hand it here, so a
|
|
207
|
+
* burst of fast events (e.g. a synchronous tool call+result) cannot lose an
|
|
208
|
+
* intermediate phase; the append itself runs inside a microtask because the
|
|
209
|
+
* session's appending guard is still set while session/event callbacks run.
|
|
210
|
+
*/
|
|
211
|
+
const publish = (session: Session, state: ActivityState): void => {
|
|
212
|
+
queueMicrotask(() => {
|
|
213
|
+
const line = state.phase === 'idle' ? undefined : state.line
|
|
214
|
+
promptHandle?.set(line)
|
|
215
|
+
if (!resolved.publish) return
|
|
216
|
+
const nowMs = Date.now()
|
|
217
|
+
const lineChanged = state.line !== lastPublishedLine
|
|
218
|
+
const phaseChanged = state.phase !== lastPublishedPhase
|
|
219
|
+
// Live phases republish on a throttle so elapsed times stay current;
|
|
220
|
+
// settled phases (idle/done) publish only when the line itself changes.
|
|
221
|
+
const liveThrottle = state.phase !== 'idle' && state.phase !== 'done'
|
|
222
|
+
&& nowMs - lastPublishAt >= resolved.publishIntervalMs
|
|
223
|
+
if (!lineChanged && !phaseChanged && !liveThrottle) return
|
|
224
|
+
// Optional fields must be omitted (not undefined): session append rejects
|
|
225
|
+
// data JSON would discard, and `activity/status` is a lossless-JSON event.
|
|
226
|
+
const payload: ActivityStatusEvent = {
|
|
227
|
+
phase: state.phase,
|
|
228
|
+
line: state.line,
|
|
229
|
+
toolCount: state.toolCount,
|
|
230
|
+
turnElapsedMs: state.turnElapsedMs,
|
|
231
|
+
phaseStartedAt: state.phaseStartedAt,
|
|
232
|
+
...(state.label === undefined ? {} : { label: state.label }),
|
|
233
|
+
...(state.detail === undefined ? {} : { detail: state.detail }),
|
|
234
|
+
...(state.phrase === undefined ? {} : { phrase: state.phrase }),
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
session.append('activity/status', payload)
|
|
238
|
+
lastPublishedLine = state.line
|
|
239
|
+
lastPublishedPhase = state.phase
|
|
240
|
+
lastPublishAt = nowMs
|
|
241
|
+
} catch {
|
|
242
|
+
// Session closed or the append guard still held: drop this snapshot;
|
|
243
|
+
// the next tick retries the same line.
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ctx.on('session/event', (session, event) => {
|
|
249
|
+
const tracker = trackerFor(session)
|
|
250
|
+
tracker.onSessionEvent(event)
|
|
251
|
+
activeSession = session
|
|
252
|
+
publish(session, tracker.render())
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
ctx.on('session/disposed', (session) => {
|
|
256
|
+
trackers.delete(session)
|
|
257
|
+
if (activeSession === session) activeSession = undefined
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
261
|
+
const session = agent.session
|
|
262
|
+
const tracker = trackerFor(session)
|
|
263
|
+
tracker.onAgentStatus(status)
|
|
264
|
+
activeSession = session
|
|
265
|
+
publish(session, tracker.render())
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Continuous tick: elapsed times and the phrase rotation move on their own.
|
|
269
|
+
// A manual timer keeps this plugin free of the @cordisjs/plugin-timer mixin;
|
|
270
|
+
// the effect disposer clears it when this fiber unloads.
|
|
271
|
+
const tickTimer = setInterval(() => {
|
|
272
|
+
if (activeSession === undefined) return
|
|
273
|
+
const tracker = trackers.get(activeSession)
|
|
274
|
+
if (tracker === undefined) return
|
|
275
|
+
publish(activeSession, tracker.render())
|
|
276
|
+
}, resolved.tickMs)
|
|
277
|
+
ctx.effect(() => () => { clearInterval(tickTimer) }, 'working-activity tick timer')
|
|
278
|
+
}
|