dsh-working-activity 0.2.1 → 0.2.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.md +122 -120
- package/README.zh.md +3 -1
- package/cordis.patch.yml +19 -19
- package/lib/client.js +7 -7
- package/lib/client.js.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -2
- package/package.json +106 -106
- package/src/client/WorkingLine.module.css +74 -74
- package/src/client/WorkingLine.tsx +37 -37
- 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/events.ts +45 -45
- package/src/index.ts +217 -211
- package/src/invariant.ts +69 -69
- package/src/status.ts +501 -501
package/src/events.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `activity/status` session event — a log-only, non-surface snapshot of the
|
|
3
|
-
* model's current working activity, published by this plugin for any UI
|
|
4
|
-
* consumer (Web client, telemetry, …). It never enters derived model history
|
|
5
|
-
* (no `surfaceOp`), so it cannot leak into prompts; UIs render it like
|
|
6
|
-
* `todo/write` or `plan/mode`.
|
|
7
|
-
* @module @deepseek-ai/dsh-working-activity/events
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ActivityPhase } from './status.js'
|
|
11
|
-
|
|
12
|
-
/** Durable payload of one `activity/status` snapshot. */
|
|
13
|
-
export interface ActivityStatusEvent {
|
|
14
|
-
/** Which activity phase the model is in. */
|
|
15
|
-
readonly phase: ActivityPhase
|
|
16
|
-
/** Human-readable status line (plain text, no ANSI). */
|
|
17
|
-
readonly line: string
|
|
18
|
-
/** Short label of the current work, when any. */
|
|
19
|
-
readonly label?: string
|
|
20
|
-
/** Detail fragment (path / command / pattern), when any. */
|
|
21
|
-
readonly detail?: string
|
|
22
|
-
/** The playful phrase currently shown, when the copy pool is on. */
|
|
23
|
-
readonly phrase?: string
|
|
24
|
-
/** Tools completed in the current turn. */
|
|
25
|
-
readonly toolCount: number
|
|
26
|
-
/** Milliseconds since the current turn started (0 when idle). */
|
|
27
|
-
readonly turnElapsedMs: number
|
|
28
|
-
/** Wall-clock time (epoch ms) the current phase started, for animations. */
|
|
29
|
-
readonly phaseStartedAt: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The `activity/status` phase vocabulary, exported for wire consumers. */
|
|
33
|
-
export type { ActivityPhase }
|
|
34
|
-
|
|
35
|
-
declare module '@deepseek-ai/dsh-session/types' {
|
|
36
|
-
interface SessionEventMap {
|
|
37
|
-
/**
|
|
38
|
-
* Log-only UI snapshot of the model's working activity (thinking copy,
|
|
39
|
-
* running tool, turn elapsed). Never a surface event: UIs render it, the
|
|
40
|
-
* model never sees it.
|
|
41
|
-
* @param data - The rendered status snapshot.
|
|
42
|
-
*/
|
|
43
|
-
'activity/status': ActivityStatusEvent
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `activity/status` session event — a log-only, non-surface snapshot of the
|
|
3
|
+
* model's current working activity, published by this plugin for any UI
|
|
4
|
+
* consumer (Web client, telemetry, …). It never enters derived model history
|
|
5
|
+
* (no `surfaceOp`), so it cannot leak into prompts; UIs render it like
|
|
6
|
+
* `todo/write` or `plan/mode`.
|
|
7
|
+
* @module @deepseek-ai/dsh-working-activity/events
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ActivityPhase } from './status.js'
|
|
11
|
+
|
|
12
|
+
/** Durable payload of one `activity/status` snapshot. */
|
|
13
|
+
export interface ActivityStatusEvent {
|
|
14
|
+
/** Which activity phase the model is in. */
|
|
15
|
+
readonly phase: ActivityPhase
|
|
16
|
+
/** Human-readable status line (plain text, no ANSI). */
|
|
17
|
+
readonly line: string
|
|
18
|
+
/** Short label of the current work, when any. */
|
|
19
|
+
readonly label?: string
|
|
20
|
+
/** Detail fragment (path / command / pattern), when any. */
|
|
21
|
+
readonly detail?: string
|
|
22
|
+
/** The playful phrase currently shown, when the copy pool is on. */
|
|
23
|
+
readonly phrase?: string
|
|
24
|
+
/** Tools completed in the current turn. */
|
|
25
|
+
readonly toolCount: number
|
|
26
|
+
/** Milliseconds since the current turn started (0 when idle). */
|
|
27
|
+
readonly turnElapsedMs: number
|
|
28
|
+
/** Wall-clock time (epoch ms) the current phase started, for animations. */
|
|
29
|
+
readonly phaseStartedAt: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The `activity/status` phase vocabulary, exported for wire consumers. */
|
|
33
|
+
export type { ActivityPhase }
|
|
34
|
+
|
|
35
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
36
|
+
interface SessionEventMap {
|
|
37
|
+
/**
|
|
38
|
+
* Log-only UI snapshot of the model's working activity (thinking copy,
|
|
39
|
+
* running tool, turn elapsed). Never a surface event: UIs render it, the
|
|
40
|
+
* model never sees it.
|
|
41
|
+
* @param data - The rendered status snapshot.
|
|
42
|
+
*/
|
|
43
|
+
'activity/status': ActivityStatusEvent
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,211 +1,217 @@
|
|
|
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 type { ActivityState } from './status.js'
|
|
27
|
-
import type { ActivityStatusEvent } from './events.js'
|
|
28
|
-
// Re-export the event type + SessionEventMap merge: the package root must carry
|
|
29
|
-
// the declare-module side effect for consumers resolving the built d.ts.
|
|
30
|
-
export type * from './events.js'
|
|
31
|
-
|
|
32
|
-
export const name = 'working-activity'
|
|
33
|
-
|
|
34
|
-
/** Configurable knobs; every key has a sane default. */
|
|
35
|
-
export type Config = {
|
|
36
|
-
/** Playful copy pool; false renders plain functional labels. */
|
|
37
|
-
phrases?: boolean
|
|
38
|
-
/** Append `activity/status` session events for UI consumers.
|
|
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
|
-
|
|
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 type { ActivityState } from './status.js'
|
|
27
|
+
import type { ActivityStatusEvent } from './events.js'
|
|
28
|
+
// Re-export the event type + SessionEventMap merge: the package root must carry
|
|
29
|
+
// the declare-module side effect for consumers resolving the built d.ts.
|
|
30
|
+
export type * from './events.js'
|
|
31
|
+
|
|
32
|
+
export const name = 'working-activity'
|
|
33
|
+
|
|
34
|
+
/** Configurable knobs; every key has a sane default. */
|
|
35
|
+
export type Config = {
|
|
36
|
+
/** Playful copy pool; false renders plain functional labels. */
|
|
37
|
+
phrases?: boolean
|
|
38
|
+
/** Append `activity/status` session events for UI consumers. Default OFF:
|
|
39
|
+
* dsh-session's append() cannot mark events ignorable, and the resume
|
|
40
|
+
* read path refuses logs containing unknown non-ignorable types — every
|
|
41
|
+
* appended snapshot makes the whole session unresumable. Re-enable only
|
|
42
|
+
* for a log-replaying consumer on a harness that supports ignorable
|
|
43
|
+
* appends. The live status line (prompt slot / session events) is
|
|
44
|
+
* unaffected by this flag. */
|
|
45
|
+
publish?: boolean
|
|
46
|
+
/** Status render tick interval in ms. */
|
|
47
|
+
tickMs?: number
|
|
48
|
+
/** Minimum interval in ms between published events while the line is stable. */
|
|
49
|
+
publishIntervalMs?: number
|
|
50
|
+
/** Maximum displayed detail length (paths/commands/patterns). */
|
|
51
|
+
detailLimit?: number
|
|
52
|
+
/** Exact tool-name → action-copy pools (case-insensitive match). */
|
|
53
|
+
customActions?: Record<string, string[]>
|
|
54
|
+
/** Inject the `⏵` self-narration contract into the system prompt and surface it. */
|
|
55
|
+
narrate?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Config = z.object({
|
|
59
|
+
phrases: z.boolean().default(true),
|
|
60
|
+
publish: z.boolean().default(false),
|
|
61
|
+
tickMs: z.number().step(50).min(100).max(5000).default(500),
|
|
62
|
+
publishIntervalMs: z.number().step(500).min(500).max(30_000).default(2000),
|
|
63
|
+
detailLimit: z.number().step(1).min(8).max(120).default(40),
|
|
64
|
+
customActions: z.dict(z.array(z.string())).default({}),
|
|
65
|
+
narrate: z.boolean().default(true),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/** Structural view of the TUI prompt service; the real type lives in dsh-tui. */
|
|
69
|
+
interface TuiPromptLike {
|
|
70
|
+
register(name: string, initialValue?: string): {
|
|
71
|
+
set(value: string | undefined): void
|
|
72
|
+
dispose(): void
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Resolved plugin configuration after schema defaults. */
|
|
77
|
+
interface ResolvedConfig {
|
|
78
|
+
phrases: boolean
|
|
79
|
+
publish: boolean
|
|
80
|
+
tickMs: number
|
|
81
|
+
publishIntervalMs: number
|
|
82
|
+
detailLimit: number
|
|
83
|
+
customActions: Record<string, string[]>
|
|
84
|
+
narrate: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The self-narration contract injected into the system prompt (narrate on). */
|
|
88
|
+
const NARRATE_INSTRUCTION =
|
|
89
|
+
'[状态栏] 你有一个状态栏展示给用户。【必须】在每个步骤/子任务开始时(不只是调用工具前),在回复正文的最前面单独写一行:⏵ 你在做的具体事情(不超过20字),然后换行继续正常回复。整轮回复只写一行 ⏵,不要重复。信息为主——让人一眼知道你在干什么,风格自然、可以带点俏皮。例:⏵ 修复登录页样式、⏵ 查一下报错原因、⏵ 给补丁跑个验证。切换任务时必须更新。'
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wire the working-activity plugin.
|
|
93
|
+
* @param ctx - Cordis context (agent loop + session services composed).
|
|
94
|
+
* @param config - Validated plugin config (schema defaults applied).
|
|
95
|
+
*/
|
|
96
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
97
|
+
const resolved: ResolvedConfig = {
|
|
98
|
+
phrases: config.phrases ?? true,
|
|
99
|
+
publish: config.publish ?? false,
|
|
100
|
+
tickMs: config.tickMs ?? 500,
|
|
101
|
+
publishIntervalMs: config.publishIntervalMs ?? 2000,
|
|
102
|
+
detailLimit: config.detailLimit ?? 40,
|
|
103
|
+
narrate: config.narrate ?? true,
|
|
104
|
+
customActions: config.customActions ?? {},
|
|
105
|
+
}
|
|
106
|
+
const trackers = new Map<Session, ActivityTracker>()
|
|
107
|
+
let activeSession: Session | undefined
|
|
108
|
+
let lastPublishedLine: string | undefined
|
|
109
|
+
let lastPublishedPhase: string | undefined
|
|
110
|
+
let lastPublishAt = 0
|
|
111
|
+
|
|
112
|
+
// Optional TUI seam: no TUI composed -> no slot, no error. The register()
|
|
113
|
+
// call is itself effect-owned, so fiber disposal unregisters the slot.
|
|
114
|
+
const prompt = ctx.get('tuiPrompt', false) as TuiPromptLike | undefined
|
|
115
|
+
const promptHandle = prompt?.register('activity', undefined)
|
|
116
|
+
|
|
117
|
+
// The `⏵` self-narration contract rides the stable system-prompt sections:
|
|
118
|
+
// injected when the systemPrompt service is composed (agent assemblies
|
|
119
|
+
// always mount it), removed with this fiber.
|
|
120
|
+
if (resolved.narrate) {
|
|
121
|
+
ctx.inject(['systemPrompt'], (promptCtx) => {
|
|
122
|
+
promptCtx.systemPrompt.section({
|
|
123
|
+
name: 'working-activity:narrate',
|
|
124
|
+
order: 60,
|
|
125
|
+
text: NARRATE_INSTRUCTION,
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const trackerFor = (session: Session): ActivityTracker => {
|
|
131
|
+
let tracker = trackers.get(session)
|
|
132
|
+
if (tracker === undefined) {
|
|
133
|
+
tracker = new ActivityTracker(
|
|
134
|
+
{ phrases: resolved.phrases, detailLimit: resolved.detailLimit, showIdle: false },
|
|
135
|
+
Date.now,
|
|
136
|
+
resolved.customActions,
|
|
137
|
+
)
|
|
138
|
+
trackers.set(session, tracker)
|
|
139
|
+
}
|
|
140
|
+
return tracker
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Publish one rendered snapshot: TUI slot update + throttled session event.
|
|
145
|
+
* Callers snapshot the tracker state at event time and hand it here, so a
|
|
146
|
+
* burst of fast events (e.g. a synchronous tool call+result) cannot lose an
|
|
147
|
+
* intermediate phase; the append itself runs inside a microtask because the
|
|
148
|
+
* session's appending guard is still set while session/event callbacks run.
|
|
149
|
+
*/
|
|
150
|
+
const publish = (session: Session, state: ActivityState): void => {
|
|
151
|
+
queueMicrotask(() => {
|
|
152
|
+
const line = state.phase === 'idle' ? undefined : state.line
|
|
153
|
+
promptHandle?.set(line)
|
|
154
|
+
if (!resolved.publish) return
|
|
155
|
+
const nowMs = Date.now()
|
|
156
|
+
const lineChanged = state.line !== lastPublishedLine
|
|
157
|
+
const phaseChanged = state.phase !== lastPublishedPhase
|
|
158
|
+
// Live phases republish on a throttle so elapsed times stay current;
|
|
159
|
+
// settled phases (idle/done) publish only when the line itself changes.
|
|
160
|
+
const liveThrottle = state.phase !== 'idle' && state.phase !== 'done'
|
|
161
|
+
&& nowMs - lastPublishAt >= resolved.publishIntervalMs
|
|
162
|
+
if (!lineChanged && !phaseChanged && !liveThrottle) return
|
|
163
|
+
// Optional fields must be omitted (not undefined): session append rejects
|
|
164
|
+
// data JSON would discard, and `activity/status` is a lossless-JSON event.
|
|
165
|
+
const payload: ActivityStatusEvent = {
|
|
166
|
+
phase: state.phase,
|
|
167
|
+
line: state.line,
|
|
168
|
+
toolCount: state.toolCount,
|
|
169
|
+
turnElapsedMs: state.turnElapsedMs,
|
|
170
|
+
phaseStartedAt: state.phaseStartedAt,
|
|
171
|
+
...(state.label === undefined ? {} : { label: state.label }),
|
|
172
|
+
...(state.detail === undefined ? {} : { detail: state.detail }),
|
|
173
|
+
...(state.phrase === undefined ? {} : { phrase: state.phrase }),
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
session.append('activity/status', payload)
|
|
177
|
+
lastPublishedLine = state.line
|
|
178
|
+
lastPublishedPhase = state.phase
|
|
179
|
+
lastPublishAt = nowMs
|
|
180
|
+
} catch {
|
|
181
|
+
// Session closed or the append guard still held: drop this snapshot;
|
|
182
|
+
// the next tick retries the same line.
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
ctx.on('session/event', (session, event) => {
|
|
188
|
+
const tracker = trackerFor(session)
|
|
189
|
+
tracker.onSessionEvent(event)
|
|
190
|
+
activeSession = session
|
|
191
|
+
publish(session, tracker.render())
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
ctx.on('session/disposed', (session) => {
|
|
195
|
+
trackers.delete(session)
|
|
196
|
+
if (activeSession === session) activeSession = undefined
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
200
|
+
const session = agent.session
|
|
201
|
+
const tracker = trackerFor(session)
|
|
202
|
+
tracker.onAgentStatus(status)
|
|
203
|
+
activeSession = session
|
|
204
|
+
publish(session, tracker.render())
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Continuous tick: elapsed times and the phrase rotation move on their own.
|
|
208
|
+
// A manual timer keeps this plugin free of the @cordisjs/plugin-timer mixin;
|
|
209
|
+
// the effect disposer clears it when this fiber unloads.
|
|
210
|
+
const tickTimer = setInterval(() => {
|
|
211
|
+
if (activeSession === undefined) return
|
|
212
|
+
const tracker = trackers.get(activeSession)
|
|
213
|
+
if (tracker === undefined) return
|
|
214
|
+
publish(activeSession, tracker.render())
|
|
215
|
+
}, resolved.tickMs)
|
|
216
|
+
ctx.effect(() => () => { clearInterval(tickTimer) }, 'working-activity tick timer')
|
|
217
|
+
}
|