@pellux/goodvibes-tui 0.20.3 → 0.22.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/CHANGELOG.md +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +31 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +118 -13
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +27 -1
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
package/src/panels/wrfc-panel.ts
CHANGED
|
@@ -16,6 +16,23 @@ import {
|
|
|
16
16
|
buildEmptyState,
|
|
17
17
|
} from './polish.ts';
|
|
18
18
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
19
|
+
import {
|
|
20
|
+
type ConfirmState,
|
|
21
|
+
handleConfirmInput,
|
|
22
|
+
} from './confirm-state.ts';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Chains in non-terminal state with no event for this long are shown as STALLED. */
|
|
29
|
+
const STALL_THRESHOLD_MS = 5 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
/** Terminal states — chains in these states cannot be resumed or cancelled. */
|
|
32
|
+
const TERMINAL_STATES: readonly WrfcState[] = ['passed', 'failed'];
|
|
33
|
+
|
|
34
|
+
/** States from which resume is permitted (pending = chain is awaiting retry). */
|
|
35
|
+
const RESUMABLE_STATES: readonly WrfcState[] = ['pending'];
|
|
19
36
|
|
|
20
37
|
// ---------------------------------------------------------------------------
|
|
21
38
|
// Colour palette
|
|
@@ -53,6 +70,7 @@ const C = {
|
|
|
53
70
|
constraintSat: '#22c55e', // green — satisfied
|
|
54
71
|
constraintUnsat:'#ef4444', // red — unsatisfied
|
|
55
72
|
constraintUnv: '#4b5563', // grey — unverified (no finding yet)
|
|
73
|
+
stalled: '#f59e0b', // amber — stalled / no recent event
|
|
56
74
|
} as const;
|
|
57
75
|
|
|
58
76
|
// ---------------------------------------------------------------------------
|
|
@@ -157,7 +175,9 @@ export function constraintStatusMarker(
|
|
|
157
175
|
// Panel
|
|
158
176
|
// ---------------------------------------------------------------------------
|
|
159
177
|
export interface WrfcPanelDeps {
|
|
160
|
-
readonly controller: Pick<WrfcController, 'listChains'>;
|
|
178
|
+
readonly controller: Pick<WrfcController, 'listChains' | 'resumeChain'>;
|
|
179
|
+
/** Cancel the active agent for a chain by its ownerAgentId. Returns true if cancelled. */
|
|
180
|
+
readonly cancelChain: (agentId: string) => boolean;
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
export class WrfcPanel extends BasePanel {
|
|
@@ -166,6 +186,12 @@ export class WrfcPanel extends BasePanel {
|
|
|
166
186
|
private scrollOffset = 0;
|
|
167
187
|
private expandedChainIds = new Set<string>();
|
|
168
188
|
private unsubscribers: Array<() => void> = [];
|
|
189
|
+
/** Last event timestamp per chain id, for stall detection. */
|
|
190
|
+
private lastEventAt = new Map<string, number>();
|
|
191
|
+
/** Pending cancel confirmation: subject is the chain id to cancel. */
|
|
192
|
+
private confirmCancel: ConfirmState<string> | null = null;
|
|
193
|
+
/** Controller error seen after initialization (distinct from pre-init silence). */
|
|
194
|
+
private controllerError: string | null = null;
|
|
169
195
|
|
|
170
196
|
constructor(
|
|
171
197
|
private readonly workflowEvents: UiEventFeed<WorkflowEvent>,
|
|
@@ -193,11 +219,35 @@ export class WrfcPanel extends BasePanel {
|
|
|
193
219
|
// Input
|
|
194
220
|
// -------------------------------------------------------------------------
|
|
195
221
|
handleInput(key: string): boolean {
|
|
222
|
+
// Confirm-cancel flow takes priority over all other keys.
|
|
223
|
+
// Enter and y both confirm; n, escape both cancel; any other key is absorbed.
|
|
224
|
+
if (this.confirmCancel) {
|
|
225
|
+
const confirmResult = handleConfirmInput(this.confirmCancel, key);
|
|
226
|
+
if (confirmResult === 'confirmed') {
|
|
227
|
+
const chain = this.chains.find(c => c.id === this.confirmCancel!.subject);
|
|
228
|
+
if (chain && !TERMINAL_STATES.includes(chain.state)) {
|
|
229
|
+
this.deps.cancelChain(chain.ownerAgentId);
|
|
230
|
+
}
|
|
231
|
+
this.confirmCancel = null;
|
|
232
|
+
this.markDirty();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (confirmResult === 'cancelled') {
|
|
236
|
+
this.confirmCancel = null;
|
|
237
|
+
this.markDirty();
|
|
238
|
+
}
|
|
239
|
+
// absorbed: confirm stays pending, key swallowed
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Normal key dispatch.
|
|
196
244
|
switch (key) {
|
|
197
245
|
case 'up': this.moveSelection(-1); return true;
|
|
198
246
|
case 'down': this.moveSelection(1); return true;
|
|
199
247
|
case 'return':
|
|
200
248
|
case 'enter': this.toggleExpanded(); return true;
|
|
249
|
+
case 'c': this.beginCancelConfirm(); return true;
|
|
250
|
+
case 'r': this.doResume(); return true;
|
|
201
251
|
default: return false;
|
|
202
252
|
}
|
|
203
253
|
}
|
|
@@ -207,25 +257,37 @@ export class WrfcPanel extends BasePanel {
|
|
|
207
257
|
// -------------------------------------------------------------------------
|
|
208
258
|
render(width: number, height: number): Line[] {
|
|
209
259
|
return this.trackedRender(() => {
|
|
210
|
-
const
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const activeCount = this.chains.filter(c => !TERMINAL_STATES.includes(c.state)).length;
|
|
211
262
|
const passedCount = this.chains.filter(c => c.state === 'passed').length;
|
|
212
263
|
const failedCount = this.chains.filter(c => c.state === 'failed').length;
|
|
213
264
|
|
|
214
265
|
if (this.chains.length === 0) {
|
|
266
|
+
const emptySections: PanelWorkspaceSection[] = [
|
|
267
|
+
{
|
|
268
|
+
lines: buildEmptyState(
|
|
269
|
+
width,
|
|
270
|
+
' No WRFC chains yet',
|
|
271
|
+
'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
|
|
272
|
+
[],
|
|
273
|
+
DEFAULT_PANEL_PALETTE,
|
|
274
|
+
),
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
if (this.controllerError) {
|
|
278
|
+
emptySections.push({
|
|
279
|
+
lines: [
|
|
280
|
+
buildPanelLine(width, [
|
|
281
|
+
[' controller: ', DEFAULT_PANEL_PALETTE.dim],
|
|
282
|
+
[truncate(this.controllerError, width - 16), DEFAULT_PANEL_PALETTE.warn],
|
|
283
|
+
]),
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
215
287
|
return buildPanelWorkspace(width, height, {
|
|
216
288
|
title: ' WRFC Chain Monitor',
|
|
217
289
|
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
218
|
-
sections:
|
|
219
|
-
{
|
|
220
|
-
lines: buildEmptyState(
|
|
221
|
-
width,
|
|
222
|
-
' No WRFC chains yet',
|
|
223
|
-
'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
|
|
224
|
-
[],
|
|
225
|
-
DEFAULT_PANEL_PALETTE,
|
|
226
|
-
),
|
|
227
|
-
},
|
|
228
|
-
],
|
|
290
|
+
sections: emptySections,
|
|
229
291
|
palette: DEFAULT_PANEL_PALETTE,
|
|
230
292
|
});
|
|
231
293
|
}
|
|
@@ -238,9 +300,10 @@ export class WrfcPanel extends BasePanel {
|
|
|
238
300
|
const isExpanded = this.expandedChainIds.has(chain.id);
|
|
239
301
|
const rowBg = isSelected ? C.selected : '';
|
|
240
302
|
const rowFg = isSelected ? C.selectedFg : '';
|
|
303
|
+
const stalled = this.isStalled(chain, now);
|
|
241
304
|
|
|
242
305
|
if (isSelected) selectedLineIndex = chainLines.length;
|
|
243
|
-
chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg));
|
|
306
|
+
chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled));
|
|
244
307
|
|
|
245
308
|
if (isExpanded) {
|
|
246
309
|
chainLines.push(...this.renderChainDetail(chain, width, 12));
|
|
@@ -298,11 +361,39 @@ export class WrfcPanel extends BasePanel {
|
|
|
298
361
|
title: 'Selected',
|
|
299
362
|
lines: selectedLines,
|
|
300
363
|
};
|
|
364
|
+
// Confirm-cancel overlay section.
|
|
365
|
+
const confirmSection: PanelWorkspaceSection | null = this.confirmCancel ? {
|
|
366
|
+
title: 'Confirm Cancel',
|
|
367
|
+
lines: [
|
|
368
|
+
buildPanelLine(width, [
|
|
369
|
+
[' Cancel chain "', DEFAULT_PANEL_PALETTE.warn],
|
|
370
|
+
[truncate(this.confirmCancel.label, Math.max(8, width - 32)), DEFAULT_PANEL_PALETTE.value],
|
|
371
|
+
['"?', DEFAULT_PANEL_PALETTE.warn],
|
|
372
|
+
]),
|
|
373
|
+
buildPanelLine(width, [
|
|
374
|
+
[' y', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
375
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
376
|
+
[' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
377
|
+
]),
|
|
378
|
+
],
|
|
379
|
+
} : null;
|
|
380
|
+
|
|
381
|
+
// Footer: show resume-disabled reason for the selected chain.
|
|
382
|
+
const selectedForFooter = this.chains[this.selectedIndex];
|
|
383
|
+
const resumeReason = selectedForFooter ? this.resumeDisabledReason(selectedForFooter) : null;
|
|
384
|
+
const footerLines: Line[] = [
|
|
385
|
+
buildPanelLine(width, [
|
|
386
|
+
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
387
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
388
|
+
[' c', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
389
|
+
[' r', resumeReason ? DEFAULT_PANEL_PALETTE.dim : DEFAULT_PANEL_PALETTE.info],
|
|
390
|
+
[resumeReason ? ` resume (${resumeReason})` : ' resume', DEFAULT_PANEL_PALETTE.dim],
|
|
391
|
+
]),
|
|
392
|
+
];
|
|
393
|
+
|
|
301
394
|
const chainsSection = resolveScrollablePanelSection(width, height, {
|
|
302
395
|
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
303
|
-
footerLines
|
|
304
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
305
|
-
],
|
|
396
|
+
footerLines,
|
|
306
397
|
palette: DEFAULT_PANEL_PALETTE,
|
|
307
398
|
beforeSections: [summarySection],
|
|
308
399
|
section: {
|
|
@@ -312,23 +403,41 @@ export class WrfcPanel extends BasePanel {
|
|
|
312
403
|
scrollOffset: this.scrollOffset,
|
|
313
404
|
minRows: 8,
|
|
314
405
|
},
|
|
315
|
-
afterSections:
|
|
406
|
+
afterSections: confirmSection
|
|
407
|
+
? [selectedSection, confirmSection]
|
|
408
|
+
: [selectedSection],
|
|
316
409
|
});
|
|
317
410
|
this.scrollOffset = chainsSection.scrollOffset;
|
|
318
|
-
const sections: PanelWorkspaceSection[] = [
|
|
411
|
+
const sections: PanelWorkspaceSection[] = [
|
|
412
|
+
summarySection,
|
|
413
|
+
chainsSection.section,
|
|
414
|
+
selectedSection,
|
|
415
|
+
...(confirmSection ? [confirmSection] : []),
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
// Controller error section (post-init only).
|
|
419
|
+
if (this.controllerError) {
|
|
420
|
+
sections.push({
|
|
421
|
+
lines: [
|
|
422
|
+
buildPanelLine(width, [
|
|
423
|
+
[' controller: ', DEFAULT_PANEL_PALETTE.dim],
|
|
424
|
+
[truncate(this.controllerError, width - 16), DEFAULT_PANEL_PALETTE.warn],
|
|
425
|
+
]),
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
319
429
|
|
|
320
430
|
return buildPanelWorkspace(width, height, {
|
|
321
431
|
title: ' WRFC Chain Monitor',
|
|
322
432
|
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
323
433
|
sections,
|
|
324
|
-
footerLines
|
|
325
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
326
|
-
],
|
|
434
|
+
footerLines,
|
|
327
435
|
palette: DEFAULT_PANEL_PALETTE,
|
|
328
436
|
});
|
|
329
437
|
});
|
|
330
438
|
}
|
|
331
439
|
|
|
440
|
+
|
|
332
441
|
// -------------------------------------------------------------------------
|
|
333
442
|
// Rendering helpers
|
|
334
443
|
// -------------------------------------------------------------------------
|
|
@@ -339,9 +448,12 @@ export class WrfcPanel extends BasePanel {
|
|
|
339
448
|
isExpanded: boolean,
|
|
340
449
|
bg: string,
|
|
341
450
|
fg: string,
|
|
451
|
+
stalled = false,
|
|
342
452
|
): Line[] {
|
|
343
|
-
const stateCol = stateColor(chain.state);
|
|
344
|
-
const stateTag =
|
|
453
|
+
const stateCol = stalled ? C.stalled : stateColor(chain.state);
|
|
454
|
+
const stateTag = stalled
|
|
455
|
+
? ` STALLED`
|
|
456
|
+
: ` ${stateLabel(chain.state).padEnd(6)}`;
|
|
345
457
|
const arrow = isExpanded ? '▾' : '▸';
|
|
346
458
|
const chainIdShort = chain.id.slice(-6);
|
|
347
459
|
const prefix = ` ${arrow} [${chainIdShort}] `;
|
|
@@ -350,6 +462,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
350
462
|
const latestScore = chain.reviewScores.length > 0
|
|
351
463
|
? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
|
|
352
464
|
: '';
|
|
465
|
+
const stalledBadge = stalled ? ' [STALLED]' : '';
|
|
353
466
|
// Constraint badge: c:sat/total — only when constraints exist
|
|
354
467
|
let constraintBadge = '';
|
|
355
468
|
if (chain.constraints.length > 0) {
|
|
@@ -358,7 +471,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
358
471
|
const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
|
|
359
472
|
constraintBadge = ` c:${satisfied}/${total}`;
|
|
360
473
|
}
|
|
361
|
-
const rightInfo = `${latestScore}${fixes}${cycles}${constraintBadge} `;
|
|
474
|
+
const rightInfo = `${stalledBadge}${latestScore}${fixes}${cycles}${constraintBadge} `;
|
|
362
475
|
|
|
363
476
|
// Compute how much space the task text can use, then check if rightInfo fits.
|
|
364
477
|
// If the terminal is narrow and rightInfo would overflow, omit it entirely
|
|
@@ -572,28 +685,49 @@ export class WrfcPanel extends BasePanel {
|
|
|
572
685
|
// Event subscriptions
|
|
573
686
|
// -------------------------------------------------------------------------
|
|
574
687
|
private subscribeToEvents(): void {
|
|
575
|
-
const
|
|
688
|
+
const refreshWithTimestamp = (chainId?: string) => {
|
|
689
|
+
if (chainId) this.lastEventAt.set(chainId, Date.now());
|
|
690
|
+
this.syncFromController();
|
|
691
|
+
this.markDirty();
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Each workflow event carries the chainId in its payload — record it for
|
|
695
|
+
// stall tracking. The handler signature is (event) => void; we extract
|
|
696
|
+
// chainId where present using a narrow type guard so we never guess.
|
|
697
|
+
const timestamped = (e: WorkflowEvent) => {
|
|
698
|
+
const chainId = 'chainId' in e && typeof e.chainId === 'string' ? e.chainId : undefined;
|
|
699
|
+
refreshWithTimestamp(chainId);
|
|
700
|
+
};
|
|
576
701
|
|
|
577
702
|
this.unsubscribers.push(
|
|
578
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_CREATED',
|
|
579
|
-
this.workflowEvents.on('WORKFLOW_STATE_CHANGED',
|
|
580
|
-
this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED',
|
|
581
|
-
this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED',
|
|
582
|
-
this.workflowEvents.on('WORKFLOW_GATE_RESULT',
|
|
583
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_PASSED',
|
|
584
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_FAILED',
|
|
585
|
-
this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED',
|
|
586
|
-
this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED',
|
|
587
|
-
this.workflowEvents.on('WORKFLOW_CONSTRAINTS_ENUMERATED',
|
|
703
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_CREATED', timestamped),
|
|
704
|
+
this.workflowEvents.on('WORKFLOW_STATE_CHANGED', timestamped),
|
|
705
|
+
this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED', timestamped),
|
|
706
|
+
this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED', timestamped),
|
|
707
|
+
this.workflowEvents.on('WORKFLOW_GATE_RESULT', timestamped),
|
|
708
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_PASSED', timestamped),
|
|
709
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', timestamped),
|
|
710
|
+
this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', timestamped),
|
|
711
|
+
this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', timestamped),
|
|
712
|
+
this.workflowEvents.on('WORKFLOW_CONSTRAINTS_ENUMERATED', timestamped),
|
|
588
713
|
);
|
|
589
714
|
}
|
|
590
715
|
|
|
591
716
|
private syncFromController(): void {
|
|
717
|
+
// Distinguish two failure modes:
|
|
718
|
+
// 1. Controller not yet initialized — chains is empty Map, listChains
|
|
719
|
+
// returns [] with no error. This is the normal pre-ready path.
|
|
720
|
+
// 2. Controller throws after initialization — an actual error we surface.
|
|
721
|
+
// We detect (2) by checking whether we previously had chains: if chains
|
|
722
|
+
// were present and listChains now throws, that is a post-init error.
|
|
723
|
+
const hadChains = this.chains.length > 0;
|
|
592
724
|
try {
|
|
593
|
-
// Sort: active first (by createdAt desc), then completed
|
|
594
725
|
const all = this.deps.controller.listChains();
|
|
595
|
-
|
|
596
|
-
|
|
726
|
+
this.controllerError = null;
|
|
727
|
+
|
|
728
|
+
// Sort: active first (by createdAt desc), then completed
|
|
729
|
+
const active = all.filter(c => !TERMINAL_STATES.includes(c.state));
|
|
730
|
+
const done = all.filter(c => TERMINAL_STATES.includes(c.state));
|
|
597
731
|
active.sort((a, b) => b.createdAt - a.createdAt);
|
|
598
732
|
done.sort( (a, b) => (b.completedAt ?? 0) - (a.completedAt ?? 0));
|
|
599
733
|
this.chains = [...active, ...done];
|
|
@@ -602,8 +736,57 @@ export class WrfcPanel extends BasePanel {
|
|
|
602
736
|
if (this.chains.length > 0) {
|
|
603
737
|
this.selectedIndex = Math.min(this.selectedIndex, this.chains.length - 1);
|
|
604
738
|
}
|
|
605
|
-
} catch {
|
|
606
|
-
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (hadChains) {
|
|
741
|
+
// Post-init error: the controller was working and now throws. Surface
|
|
742
|
+
// a faint diagnostic rather than silently preserving stale state.
|
|
743
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
744
|
+
this.controllerError = msg;
|
|
745
|
+
console.debug('[WrfcPanel] controller.listChains() error post-init:', msg);
|
|
746
|
+
}
|
|
747
|
+
// Pre-init: controller not ready yet, leave chain list empty (no error).
|
|
607
748
|
}
|
|
608
749
|
}
|
|
750
|
+
|
|
751
|
+
// -------------------------------------------------------------------------
|
|
752
|
+
// Cancel / resume actions
|
|
753
|
+
// -------------------------------------------------------------------------
|
|
754
|
+
|
|
755
|
+
/** Initiate cancel-confirm flow for the selected chain (noop if terminal). */
|
|
756
|
+
private beginCancelConfirm(): void {
|
|
757
|
+
const chain = this.chains[this.selectedIndex];
|
|
758
|
+
if (!chain || TERMINAL_STATES.includes(chain.state)) return;
|
|
759
|
+
this.confirmCancel = {
|
|
760
|
+
subject: chain.id,
|
|
761
|
+
label: truncate(chain.task, 40),
|
|
762
|
+
};
|
|
763
|
+
this.markDirty();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Resume the selected chain via the controller.
|
|
768
|
+
* Only permitted when the chain state is in RESUMABLE_STATES.
|
|
769
|
+
* Emits a visible noop reason when the chain is not resumable.
|
|
770
|
+
*/
|
|
771
|
+
private doResume(): void {
|
|
772
|
+
const chain = this.chains[this.selectedIndex];
|
|
773
|
+
if (!chain) return;
|
|
774
|
+
if (!RESUMABLE_STATES.includes(chain.state)) return;
|
|
775
|
+
this.deps.controller.resumeChain(chain.id);
|
|
776
|
+
this.markDirty();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Returns a human-readable reason why resume is disabled for a chain, or null if it is allowed. */
|
|
780
|
+
private resumeDisabledReason(chain: WrfcChain): string | null {
|
|
781
|
+
if (TERMINAL_STATES.includes(chain.state)) return 'chain is complete';
|
|
782
|
+
if (!RESUMABLE_STATES.includes(chain.state)) return `active (${stateLabel(chain.state)})`;
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Returns whether a chain is considered stalled (non-terminal, no recent event). */
|
|
787
|
+
private isStalled(chain: WrfcChain, now: number): boolean {
|
|
788
|
+
if (TERMINAL_STATES.includes(chain.state)) return false;
|
|
789
|
+
const last = this.lastEventAt.get(chain.id) ?? chain.createdAt;
|
|
790
|
+
return (now - last) >= STALL_THRESHOLD_MS;
|
|
791
|
+
}
|
|
609
792
|
}
|
|
@@ -8,19 +8,25 @@ import { formatDuration } from './modal-utils.ts';
|
|
|
8
8
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
9
|
import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
10
10
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
|
+
import { handleConfirmInput, type ConfirmState } from '../panels/confirm-state.ts';
|
|
12
|
+
import { AGENT_TERMINAL_STATUSES as MODAL_TERMINAL_STATUSES, AGENT_STALL_THRESHOLD_MS as MODAL_STALL_THRESHOLD_MS } from '../panels/agent-inspector-shared.ts';
|
|
11
13
|
|
|
12
14
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
13
15
|
|
|
14
|
-
const TOKENS_PER_TOOL_CALL = 400;
|
|
15
16
|
const MAX_LOG_ENTRIES = 10;
|
|
16
17
|
const AGENT_ID_DISPLAY_LENGTH = 16;
|
|
17
18
|
|
|
19
|
+
// MODAL_TERMINAL_STATUSES and MODAL_STALL_THRESHOLD_MS are re-exported aliases
|
|
20
|
+
// from agent-inspector-shared.ts (imported above alongside ConfirmState).
|
|
21
|
+
|
|
18
22
|
export interface AgentDetailModalDeps {
|
|
19
|
-
readonly agentManager: Pick<AgentManager, 'getStatus'>;
|
|
23
|
+
readonly agentManager: Pick<AgentManager, 'getStatus' | 'list'>;
|
|
20
24
|
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
21
25
|
readonly sessionLogPathResolver: (agentId: string) => string;
|
|
22
26
|
/** Optional — when supplied, constraint data from the agent's WRFC chain is shown (SDK 0.23.0). */
|
|
23
27
|
readonly wrfcController?: Pick<WrfcController, 'getChain'>;
|
|
28
|
+
/** Cancel the agent by id using the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
29
|
+
readonly cancelAgent: (agentId: string) => boolean;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
// ─── AgentDetailModal ─────────────────────────────────────────────────────────
|
|
@@ -40,6 +46,9 @@ export class AgentDetailModal {
|
|
|
40
46
|
public logEntries: Record<string, unknown>[] = [];
|
|
41
47
|
public logTotal = 0;
|
|
42
48
|
|
|
49
|
+
/** Pending cancel confirmation. Subject is the agent id to cancel. */
|
|
50
|
+
public confirmCancel: ConfirmState<string> | null = null;
|
|
51
|
+
|
|
43
52
|
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
44
53
|
private onRefresh: (() => void) | null = null;
|
|
45
54
|
|
|
@@ -68,12 +77,88 @@ export class AgentDetailModal {
|
|
|
68
77
|
this.agentId = null;
|
|
69
78
|
this.logEntries = [];
|
|
70
79
|
this.logTotal = 0;
|
|
80
|
+
this.confirmCancel = null;
|
|
71
81
|
if (this.refreshTimer) {
|
|
72
82
|
clearInterval(this.refreshTimer);
|
|
73
83
|
this.refreshTimer = null;
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Handle a key press while the modal is active.
|
|
89
|
+
* Must be called BEFORE the Esc handler closes the modal.
|
|
90
|
+
*
|
|
91
|
+
* Routes:
|
|
92
|
+
* - 'c' initiates cancel confirm (if agent is non-terminal)
|
|
93
|
+
* - confirm keys (Enter/y/n/Esc) are forwarded to handleConfirmInput
|
|
94
|
+
*
|
|
95
|
+
* Returns true when the key was consumed (caller should NOT propagate).
|
|
96
|
+
*/
|
|
97
|
+
handleKey(key: string): boolean {
|
|
98
|
+
if (!this.active) return false;
|
|
99
|
+
|
|
100
|
+
if (this.confirmCancel) {
|
|
101
|
+
const result = handleConfirmInput(this.confirmCancel, key);
|
|
102
|
+
if (result === 'confirmed') {
|
|
103
|
+
if (this.agentId) {
|
|
104
|
+
const rec = this.deps.agentManager.getStatus(this.agentId);
|
|
105
|
+
if (rec && !MODAL_TERMINAL_STATUSES.has(rec.status)) {
|
|
106
|
+
this.deps.cancelAgent(rec.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.confirmCancel = null;
|
|
110
|
+
this.onRefresh?.();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (result === 'cancelled') {
|
|
114
|
+
this.confirmCancel = null;
|
|
115
|
+
this.onRefresh?.();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// absorbed — key swallowed while confirm is pending
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (key === 'c') {
|
|
123
|
+
if (this.agentId) {
|
|
124
|
+
const rec = this.deps.agentManager.getStatus(this.agentId);
|
|
125
|
+
if (rec && !MODAL_TERMINAL_STATUSES.has(rec.status)) {
|
|
126
|
+
const label = rec.task.split('\n')[0]?.slice(0, 40) ?? rec.id.slice(-8);
|
|
127
|
+
this.confirmCancel = { subject: rec.id, label };
|
|
128
|
+
this.onRefresh?.();
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Non-cancellable — absorb key silently
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns whether the current agent is considered stalled.
|
|
141
|
+
* Non-terminal agent with elapsed time exceeding MODAL_STALL_THRESHOLD_MS.
|
|
142
|
+
*/
|
|
143
|
+
isCurrentAgentStalled(): boolean {
|
|
144
|
+
if (!this.agentId) return false;
|
|
145
|
+
const rec = this.deps.agentManager.getStatus(this.agentId);
|
|
146
|
+
if (!rec || MODAL_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
147
|
+
return (Date.now() - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Count of all stalled agents across the agentManager list.
|
|
152
|
+
* Non-terminal agents with elapsed time >= MODAL_STALL_THRESHOLD_MS.
|
|
153
|
+
*/
|
|
154
|
+
getStalledAgentCount(): number {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
return this.deps.agentManager.list().filter(rec => {
|
|
157
|
+
if (MODAL_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
158
|
+
return (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
|
|
159
|
+
}).length;
|
|
160
|
+
}
|
|
161
|
+
|
|
77
162
|
async loadLog(): Promise<void> {
|
|
78
163
|
if (!this.agentId) { this.logEntries = []; this.logTotal = 0; return; }
|
|
79
164
|
try {
|
|
@@ -100,13 +185,6 @@ export class AgentDetailModal {
|
|
|
100
185
|
}
|
|
101
186
|
}
|
|
102
187
|
|
|
103
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
/** Rough token estimate: toolCallCount * avg tokens per tool exchange. */
|
|
106
|
-
function estimateTokens(toolCallCount: number): number {
|
|
107
|
-
return toolCallCount * TOKENS_PER_TOOL_CALL;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
188
|
// ─── renderAgentDetailModal ───────────────────────────────────────────────────
|
|
111
189
|
|
|
112
190
|
/**
|
|
@@ -150,7 +228,6 @@ export function renderAgentDetailModal(
|
|
|
150
228
|
|
|
151
229
|
const now = Date.now();
|
|
152
230
|
const elapsedMs = (rec.completedAt ?? now) - rec.startedAt;
|
|
153
|
-
const tokenEst = estimateTokens(rec.toolCallCount);
|
|
154
231
|
|
|
155
232
|
// ── Build sections ────────────────────────────────────────────────────────
|
|
156
233
|
|
|
@@ -170,13 +247,24 @@ export function renderAgentDetailModal(
|
|
|
170
247
|
const modelStr = rec.model ? `${rec.provider ?? ''}/${rec.model}` : (rec.provider ?? '(default)');
|
|
171
248
|
sections.push({ type: 'text', content: `Template : ${rec.template}` });
|
|
172
249
|
sections.push({ type: 'text', content: `Model : ${modelStr}` });
|
|
173
|
-
|
|
250
|
+
const isStalled = !MODAL_TERMINAL_STATUSES.has(rec.status) && (now - rec.startedAt) >= MODAL_STALL_THRESHOLD_MS;
|
|
251
|
+
sections.push({ type: 'text', content: `Status : ${rec.status}${isStalled ? ' [STALLED — 5+ min no activity]' : ''}` });
|
|
174
252
|
sections.push({ type: 'text', content: `Duration : ${formatDuration(elapsedMs)}` });
|
|
175
253
|
sections.push({ type: 'separator' });
|
|
176
254
|
|
|
177
255
|
// Metrics
|
|
178
256
|
sections.push({ type: 'text', content: `Tool calls : ${rec.toolCallCount}` });
|
|
179
|
-
|
|
257
|
+
if (rec.usage) {
|
|
258
|
+
const totalIn = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
259
|
+
sections.push({ type: 'text', content: `Tokens in : ${totalIn.toLocaleString()}` });
|
|
260
|
+
sections.push({ type: 'text', content: `Tokens out : ${rec.usage.outputTokens.toLocaleString()}` });
|
|
261
|
+
} else {
|
|
262
|
+
sections.push({
|
|
263
|
+
type: 'text',
|
|
264
|
+
content: 'Tokens : n/a (agent running)',
|
|
265
|
+
style: { dim: true },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
180
268
|
|
|
181
269
|
// SDK 0.23.0: systemPromptAddendum indicator — confirms WRFC constraint addendum was injected
|
|
182
270
|
if (rec.systemPromptAddendum) {
|
|
@@ -320,12 +408,29 @@ export function renderAgentDetailModal(
|
|
|
320
408
|
}
|
|
321
409
|
}
|
|
322
410
|
|
|
411
|
+
// Cancel confirm overlay (when pending)
|
|
412
|
+
const cancellable = !MODAL_TERMINAL_STATUSES.has(rec.status);
|
|
413
|
+
if (modal.confirmCancel) {
|
|
414
|
+
sections.push({ type: 'separator' });
|
|
415
|
+
sections.push({
|
|
416
|
+
type: 'text',
|
|
417
|
+
content: `Cancel agent "${modal.confirmCancel.label}"?`,
|
|
418
|
+
style: { fg: '#f59e0b' },
|
|
419
|
+
});
|
|
420
|
+
sections.push({
|
|
421
|
+
type: 'text',
|
|
422
|
+
content: 'y / Enter confirm n / Esc cancel',
|
|
423
|
+
style: { dim: true },
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const cancelHint = cancellable ? '[c] Cancel ' : '';
|
|
323
428
|
return ModalFactory.createModal({
|
|
324
429
|
title: `Agent: ${rec.id.slice(0, AGENT_ID_DISPLAY_LENGTH)}`,
|
|
325
430
|
width: metrics.boxWidth,
|
|
326
431
|
margin: metrics.margin,
|
|
327
432
|
targetContentRows,
|
|
328
433
|
sections,
|
|
329
|
-
hints: ['[Esc] Close'],
|
|
434
|
+
hints: [cancelHint + '[Esc] Close'],
|
|
330
435
|
}, width);
|
|
331
436
|
}
|
|
@@ -268,6 +268,14 @@ function tokenizePlain(line: string): SyntaxToken[] {
|
|
|
268
268
|
|
|
269
269
|
// ─── Main Renderer ───────────────────────────────────────────────────────────
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Module-level SyntaxHighlighter singleton.
|
|
273
|
+
* Shared across all renderCodeBlock calls so the parse cache and pending-dedup
|
|
274
|
+
* set persist between renders. Creating a new instance per call was throwing away
|
|
275
|
+
* cached results and defeating the FIFO-200 cache.
|
|
276
|
+
*/
|
|
277
|
+
const _sharedHighlighter = new SyntaxHighlighter();
|
|
278
|
+
|
|
271
279
|
/**
|
|
272
280
|
* renderCodeBlock - Render lines of code with syntax highlighting and line numbers.
|
|
273
281
|
* Returns Line[] for the cell-based pipeline.
|
|
@@ -276,7 +284,7 @@ export function renderCodeBlock(
|
|
|
276
284
|
codeLines: string[],
|
|
277
285
|
lang: string,
|
|
278
286
|
width: number,
|
|
279
|
-
opts: { showLineNumbers?: boolean } = {},
|
|
287
|
+
opts: { showLineNumbers?: boolean; isStreaming?: boolean } = {},
|
|
280
288
|
): Line[] {
|
|
281
289
|
const lines: Line[] = [];
|
|
282
290
|
const language = detectLanguage(lang);
|
|
@@ -291,7 +299,7 @@ export function renderCodeBlock(
|
|
|
291
299
|
// Try tree-sitter highlight cache first (populated asynchronously).
|
|
292
300
|
// Falls back to regex tokenizer when parser not yet ready or language unsupported.
|
|
293
301
|
const fullCode = codeLines.join('\n');
|
|
294
|
-
const hlLines = lang ?
|
|
302
|
+
const hlLines = lang ? _sharedHighlighter.highlight(fullCode, lang, opts.isStreaming ?? false) : null;
|
|
295
303
|
|
|
296
304
|
// Regex tokenizer fallback (used when tree-sitter not ready)
|
|
297
305
|
const regexTokenize = (line: string): SyntaxToken[] => {
|
|
@@ -4,6 +4,10 @@ import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
|
|
|
4
4
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
5
|
import type { SearchManager } from '../input/search.ts';
|
|
6
6
|
import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
|
|
7
|
+
import { probeTermCaps, type TermColorCaps } from './term-caps.ts';
|
|
8
|
+
import { DARK_THEME } from './theme.ts';
|
|
9
|
+
|
|
10
|
+
const T = DARK_THEME;
|
|
7
11
|
|
|
8
12
|
export interface SelectionInfo {
|
|
9
13
|
isCellSelected: (col: number, absoluteRow: number) => boolean;
|
|
@@ -58,9 +62,19 @@ export class Compositor {
|
|
|
58
62
|
/** Double-buffer reuse: back is written, front is the last-rendered reference. */
|
|
59
63
|
private frontBuffer: TerminalBuffer | null = null;
|
|
60
64
|
private backBuffer: TerminalBuffer | null = null;
|
|
61
|
-
private
|
|
65
|
+
private readonly caps: TermColorCaps;
|
|
66
|
+
private diffEngine: DiffEngine;
|
|
67
|
+
|
|
68
|
+
constructor(private stdout: NodeJS.WriteStream) {
|
|
69
|
+
// Probe terminal capabilities once at construction time.
|
|
70
|
+
this.caps = probeTermCaps(stdout);
|
|
71
|
+
this.diffEngine = new DiffEngine(this.caps);
|
|
72
|
+
}
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
/** Exposed for unit tests — returns the detected color capability. */
|
|
75
|
+
public get termCapsForTest(): TermColorCaps {
|
|
76
|
+
return this.caps;
|
|
77
|
+
}
|
|
64
78
|
|
|
65
79
|
/** Exposed for unit tests — returns the last composited buffer. */
|
|
66
80
|
public get lastBufferForTest(): TerminalBuffer | null {
|
|
@@ -241,9 +255,9 @@ export class Compositor {
|
|
|
241
255
|
const isCurrent = search.manager.isCurrentMatch(absoluteRow, match.col);
|
|
242
256
|
for (let x = match.col; x < match.col + match.length && x < leftWidth; x++) {
|
|
243
257
|
if (isCurrent) {
|
|
244
|
-
newBuffer.setCell(x, screenY, { bg:
|
|
258
|
+
newBuffer.setCell(x, screenY, { bg: T.searchCurrentBg, fg: T.searchCurrentFg, bold: true, dim: false });
|
|
245
259
|
} else {
|
|
246
|
-
newBuffer.setCell(x, screenY, { bg:
|
|
260
|
+
newBuffer.setCell(x, screenY, { bg: T.searchMatchBg, fg: T.searchMatchFg, bold: false, dim: false });
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
}
|