@aemonge-dev/pi-stardeck 0.0.0-stage → 0.5.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.
- package/LICENSE +21 -0
- package/README.md +239 -2
- package/demo/pi-stardeck.tape +59 -0
- package/docs/architecture.md +103 -0
- package/docs/assets/pi-stardeck.gif +0 -0
- package/docs/development.md +128 -0
- package/licenses/README.md +17 -0
- package/licenses/pi-fancy-footer-LICENSE +21 -0
- package/licenses/pi-vim-LICENSE +21 -0
- package/package.json +81 -4
- package/packages/pi-fancy-footer-full-palette/LICENSE +21 -0
- package/packages/pi-fancy-footer-full-palette/LOCAL-CHANGES.md +41 -0
- package/packages/pi-fancy-footer-full-palette/README.md +468 -0
- package/packages/pi-fancy-footer-full-palette/package.json +52 -0
- package/packages/pi-fancy-footer-full-palette/src/api.ts +204 -0
- package/packages/pi-fancy-footer-full-palette/src/config-editor.ts +634 -0
- package/packages/pi-fancy-footer-full-palette/src/config.ts +852 -0
- package/packages/pi-fancy-footer-full-palette/src/data-widgets.ts +259 -0
- package/packages/pi-fancy-footer-full-palette/src/git.ts +130 -0
- package/packages/pi-fancy-footer-full-palette/src/index.ts +479 -0
- package/packages/pi-fancy-footer-full-palette/src/layout.ts +309 -0
- package/packages/pi-fancy-footer-full-palette/src/provider-status.ts +1466 -0
- package/packages/pi-fancy-footer-full-palette/src/render.ts +1061 -0
- package/packages/pi-fancy-footer-full-palette/src/shared.ts +903 -0
- package/packages/pi-stardeck/README.md +219 -0
- package/packages/pi-stardeck/index.ts +691 -0
- package/packages/pi-stardeck/package.json +32 -0
- package/packages/pi-stardeck/src/activity-age.ts +184 -0
- package/packages/pi-stardeck/src/capabilities.ts +105 -0
- package/packages/pi-stardeck/src/deck.ts +807 -0
- package/packages/pi-stardeck/src/gauge.ts +1323 -0
- package/packages/pi-stardeck/src/git-refresh.ts +5 -0
- package/packages/pi-stardeck/src/process-resources.ts +428 -0
- package/packages/pi-stardeck/src/title-state.ts +195 -0
- package/packages/pi-stardeck/src/title.ts +443 -0
- package/packages/pi-vim-top-border/LICENSE +21 -0
- package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
- package/packages/pi-vim-top-border/README.md +577 -0
- package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
- package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
- package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
- package/packages/pi-vim-top-border/index.ts +4195 -0
- package/packages/pi-vim-top-border/input-keys.ts +49 -0
- package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
- package/packages/pi-vim-top-border/mode-colors.ts +97 -0
- package/packages/pi-vim-top-border/mode-label.ts +62 -0
- package/packages/pi-vim-top-border/motions.ts +297 -0
- package/packages/pi-vim-top-border/package.json +47 -0
- package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
- package/packages/pi-vim-top-border/prompt-status.ts +131 -0
- package/packages/pi-vim-top-border/settings.ts +287 -0
- package/packages/pi-vim-top-border/text-objects.ts +389 -0
- package/packages/pi-vim-top-border/types.ts +51 -0
- package/packages/pi-vim-top-border/visual.ts +65 -0
- package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
- package/packages/status/README.md +398 -0
- package/packages/status/index.ts +1717 -0
- package/packages/status/package.json +33 -0
- package/packages/status/src/active-work.ts +235 -0
- package/packages/status/src/config.ts +218 -0
- package/packages/status/src/diagnostics.ts +238 -0
- package/packages/status/src/goal-block.ts +377 -0
- package/packages/status/src/goal.ts +170 -0
- package/packages/status/src/ledger.ts +101 -0
- package/packages/status/src/openspec.ts +394 -0
- package/packages/status/src/orchestration.ts +237 -0
- package/packages/status/src/publisher.ts +1294 -0
- package/packages/status/src/reducer.ts +190 -0
- package/packages/status/src/runtime-runs.ts +267 -0
- package/packages/status/src/state.ts +178 -0
- package/packages/status/src/title-activity.ts +224 -0
- package/packages/status/src/title-state.ts +218 -0
- package/packages/status/src/turn-cost.ts +63 -0
- package/packages/status/src/types.ts +228 -0
- package/src/deck-surface.ts +46 -0
- package/src/health.ts +228 -0
- package/src/index.ts +28 -0
- package/src/pi-status.ts +334 -0
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
import type { SessionSubject, SessionWorkFocus } from './active-work.ts';
|
|
2
|
+
import type { GoalHeaderState } from './goal.ts';
|
|
3
|
+
import type { ActiveRunSpanView, ActiveRuntimeRuns } from './runtime-runs.ts';
|
|
4
|
+
import type { TitleState } from './title-state.ts';
|
|
5
|
+
import type { TurnCostSnapshot } from './turn-cost.ts';
|
|
6
|
+
import type {
|
|
7
|
+
DiagnosticsState,
|
|
8
|
+
FancyFooterMessage,
|
|
9
|
+
FancyFooterWidget,
|
|
10
|
+
OpenSpecFeedback,
|
|
11
|
+
OpenSpecOverview,
|
|
12
|
+
OpenSpecState,
|
|
13
|
+
OrchestrationState,
|
|
14
|
+
PublishOperation,
|
|
15
|
+
SourceMeta,
|
|
16
|
+
StatusSnapshot,
|
|
17
|
+
WidgetColor,
|
|
18
|
+
} from './types.ts';
|
|
19
|
+
|
|
20
|
+
export const FANCY_FOOTER_PROTOCOL_VERSION = 1 as const;
|
|
21
|
+
export const FANCY_FOOTER_WIDGET_CHANNEL = 'pi-fancy-footer:widget';
|
|
22
|
+
export const FANCY_FOOTER_READY_CHANNEL = 'pi-fancy-footer:ready';
|
|
23
|
+
export const GALACTICA_HEADER_CHANNEL = 'galactica-status:header';
|
|
24
|
+
|
|
25
|
+
const OPEN_SPEC_WIDGET_ID = 'galactica.openspec';
|
|
26
|
+
const OPEN_SPEC_SCOPE_SEPARATOR_WIDGET_ID = 'galactica.openspec-scope-separator';
|
|
27
|
+
const OPEN_SPEC_PROGRESS_WIDGET_ID = 'galactica.openspec-progress';
|
|
28
|
+
const SESSION_WORK_WIDGET_ID = 'galactica.work';
|
|
29
|
+
const GOAL_WIDGET_ID = 'galactica.goal';
|
|
30
|
+
const DIAGNOSTICS_WIDGET_ID = 'galactica.diagnostics';
|
|
31
|
+
const ORCHESTRATION_WIDGET_ID = 'galactica.orchestration';
|
|
32
|
+
|
|
33
|
+
function normalizeInline(value: string): string {
|
|
34
|
+
return value.replace(/\s+/gu, ' ').trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shorten(value: string, maximum: number): string {
|
|
38
|
+
const points = Array.from(normalizeInline(value));
|
|
39
|
+
if (points.length <= maximum) return points.join('');
|
|
40
|
+
return `${points.slice(0, Math.max(1, maximum - 1)).join('')}…`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeChangeTitle(title: string): string {
|
|
44
|
+
return normalizeInline(title).replace(
|
|
45
|
+
/^(?:implement|create|build|add|shape|configure|support|manage|reconcile)\s+(?:an?\s+|the\s+)?/iu,
|
|
46
|
+
'',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compactChangeTitle(title: string, maximum: number): string {
|
|
51
|
+
return shorten(normalizeChangeTitle(title), maximum);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function focusedTaskText(
|
|
55
|
+
task: { id: string; title: string },
|
|
56
|
+
compact: boolean,
|
|
57
|
+
): string {
|
|
58
|
+
const title = normalizeInline(task.title);
|
|
59
|
+
const step = title.match(/^(?:Step|Task)\s+(\d+(?:\.\d+)*)(?:[.:])?\s+(.+)$/iu);
|
|
60
|
+
const id = step?.[1] ?? task.id;
|
|
61
|
+
const body = step?.[2] ?? title;
|
|
62
|
+
return `${id} ${compact ? shorten(body, 14) : body}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatDuration(ms: number | undefined): string | undefined {
|
|
66
|
+
if (ms === undefined || ms < 0) return undefined;
|
|
67
|
+
if (ms < 60_000) return `${Math.max(1, Math.round(ms / 1_000))}s`;
|
|
68
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
69
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ageText(updatedAt: number | undefined, now: number): string | undefined {
|
|
73
|
+
if (!updatedAt) return undefined;
|
|
74
|
+
return formatDuration(Math.max(0, now - updatedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isStale(meta: SourceMeta, now: number): boolean {
|
|
78
|
+
return (
|
|
79
|
+
meta.stale ||
|
|
80
|
+
(meta.updatedAt !== undefined &&
|
|
81
|
+
meta.staleAfterMs !== undefined &&
|
|
82
|
+
now - meta.updatedAt > meta.staleAfterMs)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openSpecColor(state: OpenSpecState): WidgetColor {
|
|
87
|
+
if (state.meta.stale) return 'warning';
|
|
88
|
+
if (['blocked', 'error', 'failed'].includes(state.phase)) return 'error';
|
|
89
|
+
if (state.phase === 'complete') return 'success';
|
|
90
|
+
return 'accent';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatSessionWork(focus: SessionWorkFocus): string {
|
|
94
|
+
if (focus.state === 'clear') return 'Work · clear';
|
|
95
|
+
return focus.state === 'validation'
|
|
96
|
+
? `Validate · ${focus.intent}`
|
|
97
|
+
: `Work · ${focus.intent}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatOpenSpecOverview(overview: OpenSpecOverview): string {
|
|
101
|
+
return (
|
|
102
|
+
`${overview.completedPlans}/${overview.totalPlans}` +
|
|
103
|
+
` › ${overview.completedTasks}/${overview.totalTasks}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatOpenSpec(
|
|
108
|
+
state: OpenSpecState,
|
|
109
|
+
focusedTaskId: string | null,
|
|
110
|
+
compact = false,
|
|
111
|
+
): string {
|
|
112
|
+
const progress = `${state.completedTasks}/${state.totalTasks}`;
|
|
113
|
+
const title = compact
|
|
114
|
+
? compactChangeTitle(state.title, 26)
|
|
115
|
+
: normalizeChangeTitle(state.title);
|
|
116
|
+
const stale = state.meta.stale
|
|
117
|
+
? ` stale ${ageText(state.meta.updatedAt, Date.now()) ?? ''}`.trimEnd()
|
|
118
|
+
: '';
|
|
119
|
+
|
|
120
|
+
if (state.phase === 'complete') return `${progress} ${title}${stale}`;
|
|
121
|
+
|
|
122
|
+
const activeTask = focusedTaskId
|
|
123
|
+
? (state.allTasks ?? state.pendingTasks).find((task) => task.id === focusedTaskId)
|
|
124
|
+
: undefined;
|
|
125
|
+
if (activeTask) {
|
|
126
|
+
return `${progress} ▶ ${focusedTaskText(activeTask, compact)}${stale}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `${progress} ${title}${stale}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function diagnosticsColor(state: DiagnosticsState, now: number): WidgetColor {
|
|
133
|
+
if (
|
|
134
|
+
state.errors + state.blockers > 0 ||
|
|
135
|
+
state.tests === 'fail' ||
|
|
136
|
+
state.typecheck === 'fail'
|
|
137
|
+
) {
|
|
138
|
+
return 'error';
|
|
139
|
+
}
|
|
140
|
+
if (isStale(state.meta, now) || state.warnings > 0) return 'warning';
|
|
141
|
+
if (state.tests === 'pass' || state.typecheck === 'pass') return 'success';
|
|
142
|
+
return 'text';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkText(name: string, state: DiagnosticsState['tests']): string | undefined {
|
|
146
|
+
if (state === 'pass') return `✓ ${name}`;
|
|
147
|
+
if (state === 'fail') return `✗ ${name}`;
|
|
148
|
+
if (state === 'running') return `◷ ${name}`;
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatDiagnostics(
|
|
153
|
+
state: DiagnosticsState,
|
|
154
|
+
now = Date.now(),
|
|
155
|
+
compact = false,
|
|
156
|
+
): string {
|
|
157
|
+
const parts: string[] = [];
|
|
158
|
+
const errors = state.errors + state.blockers;
|
|
159
|
+
if (errors > 0) parts.push(`${errors}E`);
|
|
160
|
+
if (state.warnings > 0 && !compact) parts.push(`${state.warnings}W`);
|
|
161
|
+
if (errors === 0 && state.warnings === 0) parts.push('clean');
|
|
162
|
+
|
|
163
|
+
const tests = checkText('tests', state.tests);
|
|
164
|
+
const types = checkText('types', state.typecheck);
|
|
165
|
+
if (tests) parts.push(tests);
|
|
166
|
+
if (types && !compact) parts.push(types);
|
|
167
|
+
if (isStale(state.meta, now)) {
|
|
168
|
+
parts.push(`stale ${ageText(state.meta.updatedAt, now) ?? ''}`.trimEnd());
|
|
169
|
+
}
|
|
170
|
+
return parts.join(' ');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function orchestrationColor(state: OrchestrationState, now: number): WidgetColor {
|
|
174
|
+
if (isStale(state.meta, now)) return 'warning';
|
|
175
|
+
if (state.state === 'blocked') return 'error';
|
|
176
|
+
if (state.state === 'complete') return 'success';
|
|
177
|
+
return 'accent';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function formatOrchestration(
|
|
181
|
+
state: OrchestrationState,
|
|
182
|
+
now = Date.now(),
|
|
183
|
+
compact = false,
|
|
184
|
+
): string {
|
|
185
|
+
if (state.state === 'blocked') {
|
|
186
|
+
return `blocked${state.currentTask ? `: ${shorten(state.currentTask, compact ? 16 : 30)}` : ''}`;
|
|
187
|
+
}
|
|
188
|
+
if (state.state === 'complete') return 'complete';
|
|
189
|
+
|
|
190
|
+
const elapsed =
|
|
191
|
+
state.elapsedMs ??
|
|
192
|
+
(state.startedAt ? Math.max(0, now - state.startedAt) : undefined);
|
|
193
|
+
const parts: string[] = [];
|
|
194
|
+
if (state.activeWorkers > 0) parts.push(String(state.activeWorkers));
|
|
195
|
+
if (state.phase) parts.push(shorten(state.phase, compact ? 14 : 24));
|
|
196
|
+
else if (state.currentTask) parts.push(shorten(state.currentTask, compact ? 14 : 24));
|
|
197
|
+
if (!compact) {
|
|
198
|
+
const duration = formatDuration(elapsed);
|
|
199
|
+
if (duration) parts.push(duration);
|
|
200
|
+
}
|
|
201
|
+
if (isStale(state.meta, now)) parts.push('stale');
|
|
202
|
+
return parts.join(' ') || state.state;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function buildWidgetSnapshots(
|
|
206
|
+
snapshot: StatusSnapshot,
|
|
207
|
+
options: {
|
|
208
|
+
compact?: boolean;
|
|
209
|
+
now?: number;
|
|
210
|
+
focusedTaskId?: string;
|
|
211
|
+
openSpecFeedback?: OpenSpecFeedback;
|
|
212
|
+
openSpecOverview?: OpenSpecOverview;
|
|
213
|
+
openSpecProjectDetected?: boolean;
|
|
214
|
+
workFocus?: SessionWorkFocus;
|
|
215
|
+
goal?: GoalHeaderState | null;
|
|
216
|
+
} = {},
|
|
217
|
+
): FancyFooterWidget[] {
|
|
218
|
+
const compact = options.compact ?? false;
|
|
219
|
+
const now = options.now ?? Date.now();
|
|
220
|
+
const widgets: FancyFooterWidget[] = [];
|
|
221
|
+
|
|
222
|
+
const hasActiveFallback =
|
|
223
|
+
!options.openSpecOverview &&
|
|
224
|
+
(Boolean(options.goal) ||
|
|
225
|
+
Boolean(options.workFocus && options.workFocus.state !== 'clear'));
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
options.openSpecOverview ||
|
|
229
|
+
(options.openSpecProjectDetected === false && !hasActiveFallback)
|
|
230
|
+
) {
|
|
231
|
+
widgets.push({
|
|
232
|
+
id: OPEN_SPEC_SCOPE_SEPARATOR_WIDGET_ID,
|
|
233
|
+
label: 'OpenSpec scope separator',
|
|
234
|
+
description: 'Separates the working directory from OpenSpec state',
|
|
235
|
+
content: { type: 'text', text: '⟩' },
|
|
236
|
+
icon: false,
|
|
237
|
+
style: { textColor: 'thinkingHigh' },
|
|
238
|
+
layout: {
|
|
239
|
+
enabled: false,
|
|
240
|
+
row: 0,
|
|
241
|
+
position: 0,
|
|
242
|
+
align: 'right',
|
|
243
|
+
fill: 'none',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (options.openSpecOverview && options.focusedTaskId) {
|
|
249
|
+
const overview = options.openSpecOverview;
|
|
250
|
+
const complete =
|
|
251
|
+
overview.totalPlans > 0 &&
|
|
252
|
+
overview.completedPlans >= overview.totalPlans &&
|
|
253
|
+
(overview.totalTasks === 0 || overview.completedTasks >= overview.totalTasks);
|
|
254
|
+
const color: WidgetColor =
|
|
255
|
+
overview.totalPlans === 0 ? 'dim' : complete ? 'success' : 'accent';
|
|
256
|
+
widgets.push({
|
|
257
|
+
id: OPEN_SPEC_PROGRESS_WIDGET_ID,
|
|
258
|
+
label: 'OpenSpec progress',
|
|
259
|
+
description: 'Aggregate completion across OpenSpec Plans and their Tasks',
|
|
260
|
+
content: { type: 'text', text: formatOpenSpecOverview(overview) },
|
|
261
|
+
icon: {
|
|
262
|
+
glyphs: { nerd: '', emoji: '📋', unicode: '▤', ascii: 'P' },
|
|
263
|
+
color,
|
|
264
|
+
},
|
|
265
|
+
style: { textColor: color },
|
|
266
|
+
layout: { row: 0, position: 0, align: 'right', fill: 'none', minWidth: 8 },
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// Frozen contract: without an OpenSpec focus there is no progress surface —
|
|
270
|
+
// no aggregate overview, no 'no OpenSpec' narration.
|
|
271
|
+
|
|
272
|
+
if (!snapshot.openspec && !options.openSpecOverview && options.goal) {
|
|
273
|
+
widgets.push({
|
|
274
|
+
id: GOAL_WIDGET_ID,
|
|
275
|
+
label: 'Goal',
|
|
276
|
+
description: 'Active Goal scope; its detailed objective remains in the header',
|
|
277
|
+
content: { type: 'text', text: 'Goal' },
|
|
278
|
+
icon: {
|
|
279
|
+
glyphs: { nerd: '', emoji: '🎯', unicode: '◎', ascii: 'G' },
|
|
280
|
+
color: goalHeaderColor(options.goal),
|
|
281
|
+
},
|
|
282
|
+
style: { textColor: goalHeaderColor(options.goal) },
|
|
283
|
+
layout: {
|
|
284
|
+
row: 0,
|
|
285
|
+
position: 0,
|
|
286
|
+
align: 'right',
|
|
287
|
+
fill: 'none',
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
} else if (
|
|
291
|
+
!snapshot.openspec &&
|
|
292
|
+
!options.openSpecOverview &&
|
|
293
|
+
options.workFocus &&
|
|
294
|
+
options.workFocus.state !== 'clear'
|
|
295
|
+
) {
|
|
296
|
+
const color: WidgetColor =
|
|
297
|
+
options.workFocus.state === 'validation' ? 'warning' : 'accent';
|
|
298
|
+
const glyph =
|
|
299
|
+
options.workFocus.state === 'validation'
|
|
300
|
+
? { nerd: '◇⠀', emoji: '◇⠀', unicode: '◇⠀', ascii: '? ' }
|
|
301
|
+
: { nerd: '▶⠀', emoji: '▶⠀', unicode: '▶⠀', ascii: '> ' };
|
|
302
|
+
widgets.push({
|
|
303
|
+
id: SESSION_WORK_WIDGET_ID,
|
|
304
|
+
label: options.workFocus.state === 'validation' ? 'Validate' : 'Focus',
|
|
305
|
+
description:
|
|
306
|
+
options.workFocus.state === 'validation'
|
|
307
|
+
? 'Session focus awaiting Human validation; its title remains in the header'
|
|
308
|
+
: 'Active Session Work focus; its title remains in the header',
|
|
309
|
+
content: {
|
|
310
|
+
type: 'text',
|
|
311
|
+
text: options.workFocus.state === 'validation' ? 'Validate' : 'Focus',
|
|
312
|
+
},
|
|
313
|
+
icon: { glyphs: glyph, color },
|
|
314
|
+
style: { textColor: color },
|
|
315
|
+
layout: {
|
|
316
|
+
row: 0,
|
|
317
|
+
position: 0,
|
|
318
|
+
align: 'right',
|
|
319
|
+
fill: 'none',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
!snapshot.openspec &&
|
|
326
|
+
!options.workFocus &&
|
|
327
|
+
!options.goal &&
|
|
328
|
+
options.openSpecFeedback
|
|
329
|
+
) {
|
|
330
|
+
const unavailable = options.openSpecFeedback === 'unavailable';
|
|
331
|
+
const color: WidgetColor = unavailable ? 'warning' : 'dim';
|
|
332
|
+
const stateText =
|
|
333
|
+
options.openSpecFeedback === 'no-focus' ? 'no focus' : options.openSpecFeedback;
|
|
334
|
+
widgets.push({
|
|
335
|
+
id: OPEN_SPEC_WIDGET_ID,
|
|
336
|
+
label: 'OpenSpec',
|
|
337
|
+
description: unavailable
|
|
338
|
+
? 'OpenSpec project detected, but its status is unavailable'
|
|
339
|
+
: options.openSpecFeedback === 'no-focus'
|
|
340
|
+
? 'OpenSpec has actionable work, but this session has no task focus'
|
|
341
|
+
: 'OpenSpec has no actionable task work',
|
|
342
|
+
content: { type: 'text', text: `OpenSpec · ${stateText}` },
|
|
343
|
+
icon: {
|
|
344
|
+
glyphs: {
|
|
345
|
+
nerd: '○⠀',
|
|
346
|
+
emoji: '○⠀',
|
|
347
|
+
unicode: '○⠀',
|
|
348
|
+
ascii: 'o ',
|
|
349
|
+
},
|
|
350
|
+
color,
|
|
351
|
+
},
|
|
352
|
+
style: { textColor: color },
|
|
353
|
+
layout: {
|
|
354
|
+
row: 0,
|
|
355
|
+
position: 8,
|
|
356
|
+
align: 'left',
|
|
357
|
+
fill: 'none',
|
|
358
|
+
minWidth: 10,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (snapshot.openspec) {
|
|
364
|
+
const color = openSpecColor(snapshot.openspec);
|
|
365
|
+
const checkbox =
|
|
366
|
+
snapshot.openspec.phase === 'complete'
|
|
367
|
+
? '[x]'
|
|
368
|
+
: ['blocked', 'error', 'failed'].includes(snapshot.openspec.phase)
|
|
369
|
+
? '[!]'
|
|
370
|
+
: '[-]';
|
|
371
|
+
widgets.push({
|
|
372
|
+
id: OPEN_SPEC_WIDGET_ID,
|
|
373
|
+
label: 'OpenSpec',
|
|
374
|
+
description: 'Focused OpenSpec change, task progress, and pending work',
|
|
375
|
+
content: {
|
|
376
|
+
type: 'text',
|
|
377
|
+
text: formatOpenSpec(snapshot.openspec, options.focusedTaskId ?? null, compact),
|
|
378
|
+
},
|
|
379
|
+
icon: {
|
|
380
|
+
glyphs: {
|
|
381
|
+
nerd: `${checkbox}⠀`,
|
|
382
|
+
emoji: `${checkbox}⠀`,
|
|
383
|
+
unicode: `${checkbox}⠀`,
|
|
384
|
+
ascii: `${checkbox}⠀`,
|
|
385
|
+
},
|
|
386
|
+
color,
|
|
387
|
+
},
|
|
388
|
+
style: { textColor: color },
|
|
389
|
+
layout: {
|
|
390
|
+
row: 0,
|
|
391
|
+
position: 8,
|
|
392
|
+
align: 'left',
|
|
393
|
+
fill: 'none',
|
|
394
|
+
minWidth: 10,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (snapshot.orchestration) {
|
|
400
|
+
const color = orchestrationColor(snapshot.orchestration, now);
|
|
401
|
+
widgets.push({
|
|
402
|
+
id: ORCHESTRATION_WIDGET_ID,
|
|
403
|
+
label: 'Orchestration',
|
|
404
|
+
description: 'Local workflow phase, workers, task, and elapsed time',
|
|
405
|
+
content: {
|
|
406
|
+
type: 'text',
|
|
407
|
+
text: formatOrchestration(snapshot.orchestration, now, compact),
|
|
408
|
+
},
|
|
409
|
+
icon: {
|
|
410
|
+
glyphs:
|
|
411
|
+
snapshot.orchestration.state === 'blocked'
|
|
412
|
+
? { nerd: '', emoji: '⚠️', unicode: '!', ascii: '!' }
|
|
413
|
+
: { nerd: '', emoji: '👥', unicode: '◎', ascii: 'A' },
|
|
414
|
+
color,
|
|
415
|
+
},
|
|
416
|
+
style: { textColor: color },
|
|
417
|
+
layout: { row: 0, position: 9, align: 'left', fill: 'none', minWidth: 6 },
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (snapshot.diagnostics) {
|
|
422
|
+
const color = diagnosticsColor(snapshot.diagnostics, now);
|
|
423
|
+
widgets.push({
|
|
424
|
+
id: DIAGNOSTICS_WIDGET_ID,
|
|
425
|
+
label: 'Diagnostics',
|
|
426
|
+
description: 'Aggregate blockers, warnings, tests, types, and freshness',
|
|
427
|
+
content: {
|
|
428
|
+
type: 'text',
|
|
429
|
+
text: formatDiagnostics(snapshot.diagnostics, now, compact),
|
|
430
|
+
},
|
|
431
|
+
icon: {
|
|
432
|
+
glyphs: { nerd: '', emoji: '🧪', unicode: '◆', ascii: 'D' },
|
|
433
|
+
color,
|
|
434
|
+
},
|
|
435
|
+
style: { textColor: color },
|
|
436
|
+
layout: {
|
|
437
|
+
row: 1,
|
|
438
|
+
position: 0,
|
|
439
|
+
align: 'right',
|
|
440
|
+
fill: 'none',
|
|
441
|
+
minWidth: 7,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return widgets;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const HEADER_ACTIVITY_KINDS = [
|
|
450
|
+
'understanding',
|
|
451
|
+
'inspection',
|
|
452
|
+
'synthesis',
|
|
453
|
+
'planning',
|
|
454
|
+
'skill-proposal',
|
|
455
|
+
'mutation',
|
|
456
|
+
'implementation',
|
|
457
|
+
'change-review',
|
|
458
|
+
'regression',
|
|
459
|
+
'verification',
|
|
460
|
+
'result-review',
|
|
461
|
+
'recovery',
|
|
462
|
+
'awaiting-validation',
|
|
463
|
+
'awaiting-input',
|
|
464
|
+
'operation-aborted',
|
|
465
|
+
] as const;
|
|
466
|
+
|
|
467
|
+
export type HeaderActivity = {
|
|
468
|
+
kind: (typeof HEADER_ACTIVITY_KINDS)[number];
|
|
469
|
+
current?: number;
|
|
470
|
+
total?: number;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
export type HeaderActivitySegment = {
|
|
474
|
+
id: string;
|
|
475
|
+
label: string;
|
|
476
|
+
compact?: string;
|
|
477
|
+
current?: number;
|
|
478
|
+
total?: number;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export const HEADER_LIFECYCLES = [
|
|
482
|
+
'understanding',
|
|
483
|
+
'working',
|
|
484
|
+
'waiting',
|
|
485
|
+
'assuring',
|
|
486
|
+
'learning',
|
|
487
|
+
'answering',
|
|
488
|
+
'blocked',
|
|
489
|
+
'listening',
|
|
490
|
+
'aborted',
|
|
491
|
+
] as const;
|
|
492
|
+
|
|
493
|
+
export type HeaderLifecycle = (typeof HEADER_LIFECYCLES)[number];
|
|
494
|
+
|
|
495
|
+
export const HEADER_SELECTION_SOURCES = [
|
|
496
|
+
'openspec',
|
|
497
|
+
'goal',
|
|
498
|
+
'session-work',
|
|
499
|
+
'subject',
|
|
500
|
+
] as const;
|
|
501
|
+
|
|
502
|
+
export type HeaderSelectionGoalDetail = {
|
|
503
|
+
status: string;
|
|
504
|
+
elapsedSeconds?: number;
|
|
505
|
+
tasksDone?: number;
|
|
506
|
+
tasksActive?: number;
|
|
507
|
+
tasksTotal?: number;
|
|
508
|
+
tokensUsed?: number;
|
|
509
|
+
tokenBudget?: number;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export type HeaderSelection = {
|
|
513
|
+
source: (typeof HEADER_SELECTION_SOURCES)[number];
|
|
514
|
+
titles: string[];
|
|
515
|
+
color: WidgetColor;
|
|
516
|
+
goal?: HeaderSelectionGoalDetail;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const HEADER_SUGGESTIONS = [
|
|
520
|
+
'requesting-validation',
|
|
521
|
+
'awaiting-continuation',
|
|
522
|
+
'requesting-redirection',
|
|
523
|
+
'awaiting-resume',
|
|
524
|
+
'requesting-input',
|
|
525
|
+
] as const;
|
|
526
|
+
|
|
527
|
+
export type HeaderSuggestion = (typeof HEADER_SUGGESTIONS)[number];
|
|
528
|
+
|
|
529
|
+
export type HeaderStatusEvent = {
|
|
530
|
+
protocol: 1;
|
|
531
|
+
activity: HeaderActivity | null;
|
|
532
|
+
selection: HeaderSelection | null;
|
|
533
|
+
suggestion: HeaderSuggestion | null;
|
|
534
|
+
work: {
|
|
535
|
+
lifecycle: string;
|
|
536
|
+
titles: string[];
|
|
537
|
+
color: WidgetColor;
|
|
538
|
+
activity?: HeaderActivity;
|
|
539
|
+
activityPath?: HeaderActivitySegment[];
|
|
540
|
+
} | null;
|
|
541
|
+
diagnostics: {
|
|
542
|
+
text: string;
|
|
543
|
+
color: WidgetColor;
|
|
544
|
+
state: 'pass' | 'fail' | 'running';
|
|
545
|
+
/** Aggregate findings count for the deck's notice slot (Task 3.1). */
|
|
546
|
+
count?: number;
|
|
547
|
+
} | null;
|
|
548
|
+
backgroundActivity: boolean;
|
|
549
|
+
approvalRequired: boolean;
|
|
550
|
+
blocked: boolean;
|
|
551
|
+
openGoals?: number;
|
|
552
|
+
activeRunSpans?: ActiveRunSpanView[];
|
|
553
|
+
/** Versioned deck-title-state contract for the deck's upper half. */
|
|
554
|
+
title?: TitleState;
|
|
555
|
+
/** Per-turn cost meter published beside the title state. */
|
|
556
|
+
turn?: TurnCostSnapshot;
|
|
557
|
+
counters: {
|
|
558
|
+
agents: { active: number; total: number };
|
|
559
|
+
activeRuns?: ActiveRuntimeRuns;
|
|
560
|
+
tasks?: { completed: number; total: number };
|
|
561
|
+
steps?: { completed: number; total: number };
|
|
562
|
+
files?: { completed: number; total: number };
|
|
563
|
+
};
|
|
564
|
+
progress: { icon: string; completed: number; total: number; color: WidgetColor }[];
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
function narrativeTaskTitle(title: string): string {
|
|
568
|
+
return normalizeInline(title).replace(
|
|
569
|
+
/^(?:Step|Task)\s+\d+(?:\.\d+)*(?:[.:])?\s+/iu,
|
|
570
|
+
'',
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function selectedOpenSpecTitles(
|
|
575
|
+
state: OpenSpecState,
|
|
576
|
+
focusedTaskId: string | undefined,
|
|
577
|
+
): string[] {
|
|
578
|
+
const activeTask = focusedTaskId
|
|
579
|
+
? (state.allTasks ?? state.pendingTasks).find((task) => task.id === focusedTaskId)
|
|
580
|
+
: undefined;
|
|
581
|
+
return activeTask
|
|
582
|
+
? [normalizeInline(state.title), narrativeTaskTitle(activeTask.title)]
|
|
583
|
+
: [normalizeInline(state.changeId), normalizeInline(focusedTaskId ?? 'no focus')];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function headerSelection(options: {
|
|
587
|
+
openSpec?: OpenSpecState | null;
|
|
588
|
+
focusedTaskId?: string;
|
|
589
|
+
goal?: GoalHeaderState | null;
|
|
590
|
+
workFocus?: SessionWorkFocus;
|
|
591
|
+
subject?: SessionSubject;
|
|
592
|
+
}): HeaderSelection | null {
|
|
593
|
+
if (options.openSpec) {
|
|
594
|
+
return {
|
|
595
|
+
source: 'openspec',
|
|
596
|
+
titles: selectedOpenSpecTitles(options.openSpec, options.focusedTaskId),
|
|
597
|
+
color: openSpecColor(options.openSpec),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (options.goal) {
|
|
601
|
+
const goal = options.goal;
|
|
602
|
+
const detail = {
|
|
603
|
+
status: goal.status,
|
|
604
|
+
...(goal.elapsedSeconds !== undefined
|
|
605
|
+
? { elapsedSeconds: goal.elapsedSeconds }
|
|
606
|
+
: {}),
|
|
607
|
+
...(goal.tasksDone !== undefined ? { tasksDone: goal.tasksDone } : {}),
|
|
608
|
+
...(goal.tasksActive !== undefined ? { tasksActive: goal.tasksActive } : {}),
|
|
609
|
+
...(goal.tasksTotal !== undefined ? { tasksTotal: goal.tasksTotal } : {}),
|
|
610
|
+
...(goal.tokensUsed !== undefined ? { tokensUsed: goal.tokensUsed } : {}),
|
|
611
|
+
...(goal.tokenBudget !== undefined ? { tokenBudget: goal.tokenBudget } : {}),
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
source: 'goal',
|
|
615
|
+
titles: [goal.objective ? normalizeInline(goal.objective) : 'Goal'],
|
|
616
|
+
color: goalHeaderColor(goal),
|
|
617
|
+
goal: detail,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (options.workFocus && options.workFocus.state !== 'clear') {
|
|
621
|
+
return {
|
|
622
|
+
source: 'session-work',
|
|
623
|
+
titles: [normalizeInline(options.workFocus.intent)],
|
|
624
|
+
color: options.workFocus.state === 'validation' ? 'warning' : 'accent',
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
if (options.subject && options.subject.state === 'set') {
|
|
628
|
+
return {
|
|
629
|
+
source: 'subject',
|
|
630
|
+
titles: [normalizeInline(options.subject.title)],
|
|
631
|
+
color: 'accent',
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function headerSuggestion(options: {
|
|
638
|
+
lifecycle: HeaderLifecycle;
|
|
639
|
+
activity?: HeaderActivity;
|
|
640
|
+
selection: HeaderSelection | null;
|
|
641
|
+
approvalRequired: boolean;
|
|
642
|
+
blocked: boolean;
|
|
643
|
+
}): HeaderSuggestion | null {
|
|
644
|
+
if (options.blocked) return 'requesting-redirection';
|
|
645
|
+
if (options.approvalRequired || options.activity?.kind === 'awaiting-validation') {
|
|
646
|
+
return 'requesting-validation';
|
|
647
|
+
}
|
|
648
|
+
if (options.activity?.kind === 'awaiting-input') return 'requesting-input';
|
|
649
|
+
if (options.lifecycle === 'aborted') return 'awaiting-resume';
|
|
650
|
+
if (['waiting', 'listening'].includes(options.lifecycle) && options.selection) {
|
|
651
|
+
return 'awaiting-continuation';
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function classifyHeaderActivity(sourceText: string): HeaderActivity | undefined {
|
|
657
|
+
const source = normalizeInline(sourceText).toLowerCase().replaceAll('-', ' ');
|
|
658
|
+
if (!source) return undefined;
|
|
659
|
+
if (/\bskill\s+proposal\b/u.test(source)) return { kind: 'skill-proposal' };
|
|
660
|
+
if (/\b(?:regression|test|testing|test engineer|check|checking)\b/u.test(source)) {
|
|
661
|
+
return { kind: 'regression' };
|
|
662
|
+
}
|
|
663
|
+
if (
|
|
664
|
+
/\b(?:verify|verifier|verification|review|reviewer|audit|critic)\b/u.test(source)
|
|
665
|
+
) {
|
|
666
|
+
return { kind: 'verification' };
|
|
667
|
+
}
|
|
668
|
+
if (/\b(?:recover|recovery|repair|fix)\b/u.test(source)) {
|
|
669
|
+
return { kind: 'recovery' };
|
|
670
|
+
}
|
|
671
|
+
if (
|
|
672
|
+
/\b(?:plan|planner|planning|proposal|design|scout|analyst|enrichment|spike)\b/u.test(
|
|
673
|
+
source,
|
|
674
|
+
)
|
|
675
|
+
) {
|
|
676
|
+
return { kind: 'planning' };
|
|
677
|
+
}
|
|
678
|
+
if (
|
|
679
|
+
/\b(?:implement|implementation|execute|executor|build|builder|edit|write|deliver|happy path)\b/u.test(
|
|
680
|
+
source,
|
|
681
|
+
)
|
|
682
|
+
) {
|
|
683
|
+
return { kind: 'implementation' };
|
|
684
|
+
}
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function orchestrationHeaderActivity(
|
|
689
|
+
state: OrchestrationState | null | undefined,
|
|
690
|
+
): HeaderActivity | undefined {
|
|
691
|
+
if (!state || state.state !== 'running') return undefined;
|
|
692
|
+
return classifyHeaderActivity(`${state.phase ?? ''} ${state.currentTask ?? ''}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function headerLifecycleForActivity(
|
|
696
|
+
activity: HeaderActivity | undefined,
|
|
697
|
+
): HeaderLifecycle | undefined {
|
|
698
|
+
if (!activity) return undefined;
|
|
699
|
+
if (
|
|
700
|
+
['understanding', 'inspection', 'synthesis', 'planning', 'skill-proposal'].includes(
|
|
701
|
+
activity.kind,
|
|
702
|
+
)
|
|
703
|
+
) {
|
|
704
|
+
return 'understanding';
|
|
705
|
+
}
|
|
706
|
+
if (['regression', 'verification', 'result-review'].includes(activity.kind)) {
|
|
707
|
+
return 'assuring';
|
|
708
|
+
}
|
|
709
|
+
if (['awaiting-validation', 'awaiting-input'].includes(activity.kind)) {
|
|
710
|
+
return 'waiting';
|
|
711
|
+
}
|
|
712
|
+
if (activity.kind === 'operation-aborted') return 'aborted';
|
|
713
|
+
return 'working';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function goalHeaderLifecycle(
|
|
717
|
+
goal: GoalHeaderState,
|
|
718
|
+
activeFallback: HeaderLifecycle = 'working',
|
|
719
|
+
): HeaderLifecycle {
|
|
720
|
+
if (goal.status === 'blocked') return 'blocked';
|
|
721
|
+
if (goal.status === 'active' && !goal.waiting) return activeFallback;
|
|
722
|
+
return 'waiting';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function goalHeaderActivityPath(
|
|
726
|
+
goal: GoalHeaderState,
|
|
727
|
+
automaticTurnLimit?: number,
|
|
728
|
+
): HeaderActivitySegment[] {
|
|
729
|
+
if (goal.status === 'complete') {
|
|
730
|
+
return [
|
|
731
|
+
{
|
|
732
|
+
id: 'awaiting-human-validation',
|
|
733
|
+
label: 'awaiting Human validation',
|
|
734
|
+
compact: 'validation',
|
|
735
|
+
},
|
|
736
|
+
];
|
|
737
|
+
}
|
|
738
|
+
if (goal.status === 'blocked') {
|
|
739
|
+
return [{ id: 'goal-blocked', label: 'Goal blocked', compact: 'blocked' }];
|
|
740
|
+
}
|
|
741
|
+
if (goal.status === 'paused') {
|
|
742
|
+
return [{ id: 'goal-paused', label: 'Goal paused', compact: 'paused' }];
|
|
743
|
+
}
|
|
744
|
+
if (goal.status === 'usage_limited') {
|
|
745
|
+
return [
|
|
746
|
+
{
|
|
747
|
+
id: 'goal-usage-limited',
|
|
748
|
+
label: 'Goal usage limited',
|
|
749
|
+
compact: 'usage limit',
|
|
750
|
+
},
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
if (goal.status === 'budget_limited') {
|
|
754
|
+
return [
|
|
755
|
+
{
|
|
756
|
+
id: 'goal-budget-limited',
|
|
757
|
+
label: 'Goal budget limited',
|
|
758
|
+
compact: 'budget limit',
|
|
759
|
+
},
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
if (goal.status === 'queued') {
|
|
763
|
+
return [{ id: 'goal-queued', label: 'Goal queued', compact: 'queued' }];
|
|
764
|
+
}
|
|
765
|
+
if (goal.waiting) {
|
|
766
|
+
return [
|
|
767
|
+
{
|
|
768
|
+
id: 'goal-waiting',
|
|
769
|
+
label: 'waiting for external event',
|
|
770
|
+
compact: 'waiting',
|
|
771
|
+
},
|
|
772
|
+
];
|
|
773
|
+
}
|
|
774
|
+
return [
|
|
775
|
+
{
|
|
776
|
+
id: 'goal-work',
|
|
777
|
+
label: 'completing Goal',
|
|
778
|
+
compact: 'Goal',
|
|
779
|
+
...(automaticTurnLimit === undefined
|
|
780
|
+
? {}
|
|
781
|
+
: {
|
|
782
|
+
current: Math.min(goal.automaticModelTurns, automaticTurnLimit),
|
|
783
|
+
total: automaticTurnLimit,
|
|
784
|
+
}),
|
|
785
|
+
},
|
|
786
|
+
];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function goalHeaderColor(goal: GoalHeaderState): WidgetColor {
|
|
790
|
+
if (goal.status === 'blocked') return 'error';
|
|
791
|
+
if (goal.status === 'active' && !goal.waiting) return 'accent';
|
|
792
|
+
if (goal.status === 'complete') return 'warning';
|
|
793
|
+
return 'dim';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function orchestrationHeaderLifecycle(
|
|
797
|
+
state: OrchestrationState | null | undefined,
|
|
798
|
+
): HeaderLifecycle | undefined {
|
|
799
|
+
if (!state) return undefined;
|
|
800
|
+
if (state.state === 'blocked') return 'blocked';
|
|
801
|
+
if (state.state !== 'running') return undefined;
|
|
802
|
+
const source = normalizeInline(
|
|
803
|
+
`${state.phase ?? ''} ${state.currentTask ?? ''}`,
|
|
804
|
+
).toLowerCase();
|
|
805
|
+
if (/\b(?:understand|scout|plan|analyst|enrichment|spike)\b/u.test(source)) {
|
|
806
|
+
return 'understanding';
|
|
807
|
+
}
|
|
808
|
+
if (/\b(?:assure|verify|test|review|critic|gate)\b/u.test(source)) {
|
|
809
|
+
return 'assuring';
|
|
810
|
+
}
|
|
811
|
+
if (/\b(?:learn|memory|evidence|retain)\b/u.test(source)) return 'learning';
|
|
812
|
+
if (/\b(?:report(?:er|ing)?|relay|writer|answer)\b/u.test(source)) {
|
|
813
|
+
return 'answering';
|
|
814
|
+
}
|
|
815
|
+
return 'working';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export function taskflowCallHeaderActivity(input: unknown): HeaderActivity | undefined {
|
|
819
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined;
|
|
820
|
+
const candidate = input as Record<string, unknown>;
|
|
821
|
+
if (candidate.action !== 'run' || typeof candidate.name !== 'string') {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
return classifyHeaderActivity(candidate.name);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function taskflowInitialHeaderPhase(input: unknown): string | undefined {
|
|
828
|
+
const candidate = record(input);
|
|
829
|
+
return candidate?.action === 'run' && candidate.name === 'ramona-loop'
|
|
830
|
+
? 'understanding'
|
|
831
|
+
: undefined;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
835
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
836
|
+
? (value as Record<string, unknown>)
|
|
837
|
+
: undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function boundedTaskflowPhase(value: unknown): string | undefined {
|
|
841
|
+
if (typeof value !== 'string') return undefined;
|
|
842
|
+
const normalized = normalizeInline(value);
|
|
843
|
+
return normalized ? shorten(normalized, 64) : undefined;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function taskflowUpdateHeaderPhase(partialResult: unknown): string | undefined {
|
|
847
|
+
const details = record(record(partialResult)?.details);
|
|
848
|
+
if (details?.action !== 'run') return undefined;
|
|
849
|
+
const state = record(details.state);
|
|
850
|
+
const phaseStates = record(state?.phases);
|
|
851
|
+
if (!state || !phaseStates) return undefined;
|
|
852
|
+
|
|
853
|
+
const promoted = new Set<string>();
|
|
854
|
+
for (const phaseState of Object.values(phaseStates)) {
|
|
855
|
+
const promotedPhases = record(record(phaseState)?.promotedPhases);
|
|
856
|
+
if (!promotedPhases) continue;
|
|
857
|
+
for (const id of Object.keys(promotedPhases)) promoted.add(id);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const definitions = record(state.def)?.phases;
|
|
861
|
+
const orderedIds = Array.isArray(definitions)
|
|
862
|
+
? definitions.flatMap((definition) => {
|
|
863
|
+
const id = boundedTaskflowPhase(record(definition)?.id);
|
|
864
|
+
return id ? [id] : [];
|
|
865
|
+
})
|
|
866
|
+
: Object.keys(phaseStates).flatMap((id) => {
|
|
867
|
+
const bounded = boundedTaskflowPhase(id);
|
|
868
|
+
return bounded ? [bounded] : [];
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
return orderedIds.find((id) => {
|
|
872
|
+
if (promoted.has(id)) return false;
|
|
873
|
+
return record(phaseStates[id])?.status === 'running';
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function taskflowPhaseHeaderLifecycle(phase: string): HeaderLifecycle {
|
|
878
|
+
const normalized = normalizeInline(phase).toLowerCase();
|
|
879
|
+
if (['ask-for-clarification', 'request-human-decision'].includes(normalized)) {
|
|
880
|
+
return 'waiting';
|
|
881
|
+
}
|
|
882
|
+
if (normalized === 'explain-boundary') return 'blocked';
|
|
883
|
+
const source = normalized.replaceAll('-', ' ');
|
|
884
|
+
if (
|
|
885
|
+
/\b(?:understand|gather|investigat|scout|plan|analyst|enrichment|spike)\w*\b/u.test(
|
|
886
|
+
source,
|
|
887
|
+
)
|
|
888
|
+
) {
|
|
889
|
+
return 'understanding';
|
|
890
|
+
}
|
|
891
|
+
if (/\b(?:assur|verify|test|review|critic|gate)\w*\b/u.test(source)) {
|
|
892
|
+
return 'assuring';
|
|
893
|
+
}
|
|
894
|
+
if (/\b(?:learn|memory|evidence|retain)\w*\b/u.test(source)) return 'learning';
|
|
895
|
+
if (/\b(?:report|relay|writer|answer)\w*\b/u.test(source)) return 'answering';
|
|
896
|
+
return 'working';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const GLOBAL_LOOP_ACTIVITY_PATHS: Readonly<
|
|
900
|
+
Record<string, readonly HeaderActivitySegment[]>
|
|
901
|
+
> = {
|
|
902
|
+
understanding: [
|
|
903
|
+
{
|
|
904
|
+
id: 'interpreting-intent',
|
|
905
|
+
label: 'interpreting intent',
|
|
906
|
+
compact: 'interpreting',
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
'gather-context': [
|
|
910
|
+
{ id: 'gathering-context', label: 'gathering context', compact: 'gathering' },
|
|
911
|
+
],
|
|
912
|
+
'finalize-understanding': [
|
|
913
|
+
{
|
|
914
|
+
id: 'finalizing-understanding',
|
|
915
|
+
label: 'finalizing current request',
|
|
916
|
+
compact: 'finalizing',
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
'answer-now': [
|
|
920
|
+
{ id: 'preparing-answer', label: 'preparing answer', compact: 'answering' },
|
|
921
|
+
],
|
|
922
|
+
'ask-for-clarification': [
|
|
923
|
+
{
|
|
924
|
+
id: 'awaiting-clarification',
|
|
925
|
+
label: 'awaiting clarification',
|
|
926
|
+
compact: 'clarification',
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
'explain-boundary': [
|
|
930
|
+
{ id: 'explaining-boundary', label: 'explaining boundary', compact: 'boundary' },
|
|
931
|
+
],
|
|
932
|
+
working: [
|
|
933
|
+
{ id: 'completing-request', label: 'completing request', compact: 'working' },
|
|
934
|
+
],
|
|
935
|
+
'request-human-decision': [
|
|
936
|
+
{ id: 'awaiting-decision', label: 'awaiting your decision', compact: 'decision' },
|
|
937
|
+
],
|
|
938
|
+
assuring: [
|
|
939
|
+
{ id: 'checking-quality', label: 'checking quality', compact: 'checking' },
|
|
940
|
+
],
|
|
941
|
+
'review-result': [
|
|
942
|
+
{ id: 'reviewing-result', label: 'reviewing result', compact: 'reviewing' },
|
|
943
|
+
],
|
|
944
|
+
learning: [
|
|
945
|
+
{ id: 'capturing-learning', label: 'capturing learning', compact: 'learning' },
|
|
946
|
+
],
|
|
947
|
+
answering: [
|
|
948
|
+
{ id: 'preparing-answer', label: 'preparing answer', compact: 'answering' },
|
|
949
|
+
],
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
export function taskflowPhaseHeaderActivityPath(
|
|
953
|
+
phase: string,
|
|
954
|
+
): HeaderActivitySegment[] | undefined {
|
|
955
|
+
const normalized = normalizeInline(phase).toLowerCase();
|
|
956
|
+
const path = GLOBAL_LOOP_ACTIVITY_PATHS[normalized];
|
|
957
|
+
return path?.map((segment) => ({ ...segment }));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function directCommandText(input: unknown): string {
|
|
961
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return '';
|
|
962
|
+
const command = (input as Record<string, unknown>).command;
|
|
963
|
+
return typeof command === 'string' ? normalizeInline(command).toLowerCase() : '';
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function directToolCompletionHeaderActivity(
|
|
967
|
+
activity: HeaderActivity | undefined,
|
|
968
|
+
isError = false,
|
|
969
|
+
): HeaderActivity | undefined {
|
|
970
|
+
if (!activity) return undefined;
|
|
971
|
+
if (isError) return { kind: 'recovery' };
|
|
972
|
+
if (
|
|
973
|
+
['understanding', 'inspection', 'planning', 'skill-proposal'].includes(
|
|
974
|
+
activity.kind,
|
|
975
|
+
)
|
|
976
|
+
) {
|
|
977
|
+
return { kind: 'synthesis' };
|
|
978
|
+
}
|
|
979
|
+
if (['mutation', 'implementation', 'change-review'].includes(activity.kind)) {
|
|
980
|
+
return { kind: 'change-review' };
|
|
981
|
+
}
|
|
982
|
+
if (['regression', 'verification', 'result-review'].includes(activity.kind)) {
|
|
983
|
+
return { kind: 'result-review' };
|
|
984
|
+
}
|
|
985
|
+
return activity;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function directToolHeaderActivity(
|
|
989
|
+
toolName: string,
|
|
990
|
+
input: unknown,
|
|
991
|
+
): HeaderActivity | undefined {
|
|
992
|
+
const tool = normalizeInline(toolName).toLowerCase();
|
|
993
|
+
if (['read', 'grep', 'find', 'ls', 'skill'].includes(tool)) {
|
|
994
|
+
return { kind: 'inspection' };
|
|
995
|
+
}
|
|
996
|
+
if (['edit', 'write'].includes(tool)) return { kind: 'mutation' };
|
|
997
|
+
if (tool !== 'bash') return undefined;
|
|
998
|
+
|
|
999
|
+
const command = directCommandText(input);
|
|
1000
|
+
if (!command) return undefined;
|
|
1001
|
+
if (
|
|
1002
|
+
/\b(?:validate|verification|markdownlint|prettier\s+--check|typecheck|lint|node\s+--test|npm\s+(?:run\s+)?test|pytest|cargo\s+test)\b/u.test(
|
|
1003
|
+
command,
|
|
1004
|
+
)
|
|
1005
|
+
) {
|
|
1006
|
+
return { kind: 'verification' };
|
|
1007
|
+
}
|
|
1008
|
+
if (/\bopenspec\s+(?:new|init|update|archive)\b/u.test(command)) {
|
|
1009
|
+
return { kind: 'mutation' };
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
/\bopenspec\s+(?:status|instructions|show|list|schemas|schema|context)\b/u.test(
|
|
1013
|
+
command,
|
|
1014
|
+
) ||
|
|
1015
|
+
/(?:^|[;&|]\s*)(?:pwd|find|rg|grep|ls|stat|command\s+-v)\b/u.test(command)
|
|
1016
|
+
) {
|
|
1017
|
+
return { kind: 'inspection' };
|
|
1018
|
+
}
|
|
1019
|
+
return undefined;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function buildHeaderStatusEvent(
|
|
1023
|
+
widgets: readonly FancyFooterWidget[],
|
|
1024
|
+
options: {
|
|
1025
|
+
openSpec?: OpenSpecState | null;
|
|
1026
|
+
focusedTaskId?: string;
|
|
1027
|
+
workFocus?: SessionWorkFocus;
|
|
1028
|
+
subject?: SessionSubject;
|
|
1029
|
+
orchestration?: OrchestrationState | null;
|
|
1030
|
+
lifecycle?: HeaderLifecycle;
|
|
1031
|
+
activity?: HeaderActivity;
|
|
1032
|
+
activityPath?: HeaderActivitySegment[];
|
|
1033
|
+
taskflowPhase?: string;
|
|
1034
|
+
fallbackTitles?: string[];
|
|
1035
|
+
goal?: GoalHeaderState | null;
|
|
1036
|
+
openGoals?: number;
|
|
1037
|
+
goalAutomaticTurnLimit?: number;
|
|
1038
|
+
activeRuns?: ActiveRuntimeRuns;
|
|
1039
|
+
activeRunSpans?: ActiveRunSpanView[];
|
|
1040
|
+
idleMs?: number;
|
|
1041
|
+
title?: TitleState | null;
|
|
1042
|
+
turn?: TurnCostSnapshot | null;
|
|
1043
|
+
/** Aggregate diagnostics state; supplies the findings count. */
|
|
1044
|
+
diagnostics?: DiagnosticsState | null;
|
|
1045
|
+
} = {},
|
|
1046
|
+
): HeaderStatusEvent {
|
|
1047
|
+
const diagnosticsWidget = widgets.find(
|
|
1048
|
+
(widget) => widget.id === DIAGNOSTICS_WIDGET_ID,
|
|
1049
|
+
);
|
|
1050
|
+
const diagnosticsState = diagnosticsWidget
|
|
1051
|
+
? diagnosticsWidget.style?.textColor === 'error'
|
|
1052
|
+
? 'fail'
|
|
1053
|
+
: diagnosticsWidget.content.text.includes('◷')
|
|
1054
|
+
? 'running'
|
|
1055
|
+
: diagnosticsWidget.style?.textColor === 'success'
|
|
1056
|
+
? 'pass'
|
|
1057
|
+
: null
|
|
1058
|
+
: null;
|
|
1059
|
+
|
|
1060
|
+
const orchestration = options.orchestration;
|
|
1061
|
+
const blocked =
|
|
1062
|
+
orchestration?.state === 'blocked' ||
|
|
1063
|
+
options.goal?.status === 'blocked' ||
|
|
1064
|
+
Boolean(
|
|
1065
|
+
options.openSpec &&
|
|
1066
|
+
['blocked', 'error', 'failed'].includes(options.openSpec.phase),
|
|
1067
|
+
);
|
|
1068
|
+
const activity =
|
|
1069
|
+
options.activity ?? orchestrationHeaderActivity(options.orchestration);
|
|
1070
|
+
const orchestrationLifecycle = orchestrationHeaderLifecycle(options.orchestration);
|
|
1071
|
+
const taskflowPhase = boundedTaskflowPhase(options.taskflowPhase);
|
|
1072
|
+
const activityPath =
|
|
1073
|
+
options.activityPath ??
|
|
1074
|
+
(taskflowPhase ? taskflowPhaseHeaderActivityPath(taskflowPhase) : undefined) ??
|
|
1075
|
+
(options.goal
|
|
1076
|
+
? goalHeaderActivityPath(options.goal, options.goalAutomaticTurnLimit)
|
|
1077
|
+
: undefined);
|
|
1078
|
+
const ordinaryLifecycle =
|
|
1079
|
+
orchestrationLifecycle ??
|
|
1080
|
+
options.lifecycle ??
|
|
1081
|
+
headerLifecycleForActivity(activity) ??
|
|
1082
|
+
(options.goal?.status === 'active' && !options.goal.waiting
|
|
1083
|
+
? 'working'
|
|
1084
|
+
: options.workFocus?.state === 'validation'
|
|
1085
|
+
? 'waiting'
|
|
1086
|
+
: 'listening');
|
|
1087
|
+
const lifecycle: HeaderLifecycle = blocked
|
|
1088
|
+
? 'blocked'
|
|
1089
|
+
: options.lifecycle === 'aborted'
|
|
1090
|
+
? 'aborted'
|
|
1091
|
+
: ((taskflowPhase ? taskflowPhaseHeaderLifecycle(taskflowPhase) : undefined) ??
|
|
1092
|
+
(options.goal
|
|
1093
|
+
? goalHeaderLifecycle(options.goal, ordinaryLifecycle)
|
|
1094
|
+
: undefined) ??
|
|
1095
|
+
ordinaryLifecycle);
|
|
1096
|
+
const projectedActivity =
|
|
1097
|
+
activityPath !== undefined ? { activityPath } : activity ? { activity } : {};
|
|
1098
|
+
const selection = headerSelection(options);
|
|
1099
|
+
const approvalRequired =
|
|
1100
|
+
options.workFocus?.state === 'validation' || options.goal?.status === 'complete';
|
|
1101
|
+
const suggestion = headerSuggestion({
|
|
1102
|
+
lifecycle,
|
|
1103
|
+
activity,
|
|
1104
|
+
selection,
|
|
1105
|
+
approvalRequired,
|
|
1106
|
+
blocked,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
let work: HeaderStatusEvent['work'] = null;
|
|
1110
|
+
if (options.openSpec) {
|
|
1111
|
+
const state = options.openSpec;
|
|
1112
|
+
work = {
|
|
1113
|
+
lifecycle,
|
|
1114
|
+
titles: selectedOpenSpecTitles(state, options.focusedTaskId),
|
|
1115
|
+
color: openSpecColor(state),
|
|
1116
|
+
...projectedActivity,
|
|
1117
|
+
};
|
|
1118
|
+
} else if (options.goal) {
|
|
1119
|
+
work = {
|
|
1120
|
+
lifecycle,
|
|
1121
|
+
titles: ['Goal'],
|
|
1122
|
+
color: goalHeaderColor(options.goal),
|
|
1123
|
+
...projectedActivity,
|
|
1124
|
+
};
|
|
1125
|
+
} else if (options.workFocus && options.workFocus.state !== 'clear') {
|
|
1126
|
+
work = {
|
|
1127
|
+
lifecycle,
|
|
1128
|
+
titles: [normalizeInline(options.workFocus.intent)],
|
|
1129
|
+
color: options.workFocus.state === 'validation' ? 'warning' : 'accent',
|
|
1130
|
+
...projectedActivity,
|
|
1131
|
+
};
|
|
1132
|
+
} else if (options.orchestration) {
|
|
1133
|
+
work = {
|
|
1134
|
+
lifecycle,
|
|
1135
|
+
titles: ['Taskflow run'],
|
|
1136
|
+
color: orchestrationColor(options.orchestration, Date.now()),
|
|
1137
|
+
...projectedActivity,
|
|
1138
|
+
};
|
|
1139
|
+
} else {
|
|
1140
|
+
work = {
|
|
1141
|
+
lifecycle,
|
|
1142
|
+
titles:
|
|
1143
|
+
taskflowPhase && activityPath !== undefined
|
|
1144
|
+
? []
|
|
1145
|
+
: taskflowPhase
|
|
1146
|
+
? ['Taskflow run']
|
|
1147
|
+
: [], // frozen contract: no selection narrates nothing
|
|
1148
|
+
color: 'accent',
|
|
1149
|
+
...projectedActivity,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const progress = options.openSpec?.totalTasks
|
|
1153
|
+
? [
|
|
1154
|
+
{
|
|
1155
|
+
icon: '',
|
|
1156
|
+
completed: options.openSpec.completedTasks,
|
|
1157
|
+
total: options.openSpec.totalTasks,
|
|
1158
|
+
color: openSpecColor(options.openSpec),
|
|
1159
|
+
},
|
|
1160
|
+
]
|
|
1161
|
+
: [];
|
|
1162
|
+
|
|
1163
|
+
return {
|
|
1164
|
+
protocol: 1,
|
|
1165
|
+
...(options.openGoals ? { openGoals: options.openGoals } : {}),
|
|
1166
|
+
...(options.activeRunSpans ? { activeRunSpans: options.activeRunSpans } : {}),
|
|
1167
|
+
...(options.idleMs ? { idleMs: options.idleMs } : {}),
|
|
1168
|
+
...(options.title ? { title: options.title } : {}),
|
|
1169
|
+
...(options.turn ? { turn: options.turn } : {}),
|
|
1170
|
+
activity: activity ?? null,
|
|
1171
|
+
selection,
|
|
1172
|
+
suggestion,
|
|
1173
|
+
work,
|
|
1174
|
+
diagnostics:
|
|
1175
|
+
diagnosticsWidget && diagnosticsState
|
|
1176
|
+
? {
|
|
1177
|
+
text: diagnosticsWidget.content.text,
|
|
1178
|
+
color: diagnosticsWidget.style?.textColor ?? 'text',
|
|
1179
|
+
state: diagnosticsState,
|
|
1180
|
+
...(options.diagnostics
|
|
1181
|
+
? {
|
|
1182
|
+
count:
|
|
1183
|
+
options.diagnostics.errors +
|
|
1184
|
+
options.diagnostics.blockers +
|
|
1185
|
+
options.diagnostics.warnings,
|
|
1186
|
+
}
|
|
1187
|
+
: {}),
|
|
1188
|
+
}
|
|
1189
|
+
: null,
|
|
1190
|
+
backgroundActivity: Boolean(
|
|
1191
|
+
orchestration?.state === 'running' ||
|
|
1192
|
+
(orchestration?.activeWorkers ?? 0) > 0 ||
|
|
1193
|
+
(options.goal?.status === 'active' && !options.goal.waiting),
|
|
1194
|
+
),
|
|
1195
|
+
approvalRequired,
|
|
1196
|
+
blocked,
|
|
1197
|
+
counters: {
|
|
1198
|
+
agents: {
|
|
1199
|
+
active: orchestration?.activeWorkers ?? 0,
|
|
1200
|
+
total: orchestration
|
|
1201
|
+
? Math.max(orchestration.activeWorkers, orchestration.totalWorkers ?? 0)
|
|
1202
|
+
: 0,
|
|
1203
|
+
},
|
|
1204
|
+
...(options.activeRuns ? { activeRuns: options.activeRuns } : {}),
|
|
1205
|
+
...(options.openSpec?.hierarchy
|
|
1206
|
+
? { tasks: options.openSpec.hierarchy.tasks }
|
|
1207
|
+
: {}),
|
|
1208
|
+
...(options.openSpec?.hierarchy &&
|
|
1209
|
+
options.focusedTaskId &&
|
|
1210
|
+
options.openSpec.hierarchy.stepsByTask[options.focusedTaskId]
|
|
1211
|
+
? {
|
|
1212
|
+
steps: options.openSpec.hierarchy.stepsByTask[options.focusedTaskId],
|
|
1213
|
+
}
|
|
1214
|
+
: {}),
|
|
1215
|
+
...(orchestration?.completedFiles === undefined ||
|
|
1216
|
+
orchestration.totalFiles === undefined
|
|
1217
|
+
? {}
|
|
1218
|
+
: {
|
|
1219
|
+
files: {
|
|
1220
|
+
completed: orchestration.completedFiles,
|
|
1221
|
+
total: orchestration.totalFiles,
|
|
1222
|
+
},
|
|
1223
|
+
}),
|
|
1224
|
+
},
|
|
1225
|
+
progress,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function signature(widget: FancyFooterWidget): string {
|
|
1230
|
+
return JSON.stringify(widget);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export class FancyFooterPublisher {
|
|
1234
|
+
private published = new Map<string, FancyFooterWidget>();
|
|
1235
|
+
private readonly emit: (message: FancyFooterMessage) => void;
|
|
1236
|
+
|
|
1237
|
+
constructor(emit: (message: FancyFooterMessage) => void) {
|
|
1238
|
+
this.emit = emit;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
sync(widgets: readonly FancyFooterWidget[]): PublishOperation[] {
|
|
1242
|
+
const next = new Map(widgets.map((widget) => [widget.id, widget]));
|
|
1243
|
+
const operations: PublishOperation[] = [];
|
|
1244
|
+
|
|
1245
|
+
for (const [id] of this.published) {
|
|
1246
|
+
if (next.has(id)) continue;
|
|
1247
|
+
operations.push({ type: 'remove', id });
|
|
1248
|
+
this.emit({
|
|
1249
|
+
protocol: FANCY_FOOTER_PROTOCOL_VERSION,
|
|
1250
|
+
type: 'remove',
|
|
1251
|
+
id,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
for (const widget of widgets) {
|
|
1256
|
+
const previous = this.published.get(widget.id);
|
|
1257
|
+
if (previous && signature(previous) === signature(widget)) continue;
|
|
1258
|
+
operations.push({ type: 'upsert', widget });
|
|
1259
|
+
this.emit({
|
|
1260
|
+
protocol: FANCY_FOOTER_PROTOCOL_VERSION,
|
|
1261
|
+
type: 'upsert',
|
|
1262
|
+
widget,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
this.published = next;
|
|
1267
|
+
return operations;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
republish(): void {
|
|
1271
|
+
for (const widget of this.published.values()) {
|
|
1272
|
+
this.emit({
|
|
1273
|
+
protocol: FANCY_FOOTER_PROTOCOL_VERSION,
|
|
1274
|
+
type: 'upsert',
|
|
1275
|
+
widget,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
removeAll(): void {
|
|
1281
|
+
for (const id of this.published.keys()) {
|
|
1282
|
+
this.emit({
|
|
1283
|
+
protocol: FANCY_FOOTER_PROTOCOL_VERSION,
|
|
1284
|
+
type: 'remove',
|
|
1285
|
+
id,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
this.published.clear();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
snapshots(): FancyFooterWidget[] {
|
|
1292
|
+
return [...this.published.values()];
|
|
1293
|
+
}
|
|
1294
|
+
}
|