@pellux/goodvibes-tui 1.7.0 → 1.9.1
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 +11 -0
- package/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/config/credential-availability.ts +122 -0
- package/src/config/index.ts +15 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +46 -24
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +55 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
|
@@ -4,23 +4,17 @@
|
|
|
4
4
|
// W2.2 — FleetPanel: the live unified observability tree. Renders the
|
|
5
5
|
// registered process fleet (agents incl. WRFC roles, WRFC chains/subtasks,
|
|
6
6
|
// workflow-tool FSMs, watchers, background processes) as a depth-first tree
|
|
7
|
-
// with a selected-row detail region.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// tab. See fleet-tabs.ts for the tab-state model and fleet-transcript.ts for
|
|
13
|
-
// tab content rendering — this file owns input dispatch and layout only.
|
|
14
|
-
//
|
|
15
|
-
// Coexists with (does not replace) the process modal, ops, wrfc, and
|
|
16
|
-
// inspector entry points this wave — only the footer process indicator's
|
|
17
|
-
// [Enter] is repointed to open this panel (see handler-feed-routes.ts).
|
|
18
|
-
// Removal of the older entry points is Wave 6.
|
|
7
|
+
// with a selected-row detail region. Session tabs (W3.1 Part C): Enter on an
|
|
8
|
+
// attachable node (agent/wrfc-chain) opens a tab with that process's
|
|
9
|
+
// transcript/member summary — see fleet-tabs.ts + fleet-transcript.ts. This
|
|
10
|
+
// file owns input dispatch and layout; fleet-deep-link.ts owns the
|
|
11
|
+
// cross-panel jump target (DEBT-5 item 4).
|
|
19
12
|
// ---------------------------------------------------------------------------
|
|
20
13
|
|
|
21
14
|
import { readFile } from 'node:fs/promises';
|
|
22
15
|
import type { Line } from '../types/grid.ts';
|
|
23
16
|
import type { ProcessCostState, ProcessUsage, SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
17
|
+
import { formatWorkItemIsolationDetailFromRaw } from './fleet-panel-worktree-detail.ts';
|
|
24
18
|
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
25
19
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
26
20
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
@@ -52,62 +46,15 @@ import {
|
|
|
52
46
|
type FleetTabsState,
|
|
53
47
|
type SteerBadge,
|
|
54
48
|
} from './fleet-tabs.ts';
|
|
55
|
-
import { reconcileSteerBadges as reconcileSteerBadgesPure, steerBadgeGlyph, steerBadgeTone } from './fleet-steer.ts';
|
|
56
|
-
import { FleetStopTracker, fleetStateDisplay, toggleFleetPause, buildFleetTreeHints } from './fleet-stop.ts';
|
|
49
|
+
import { liveSteerableLabels, reconcileSteerBadges as reconcileSteerBadgesPure, renderSteerBadgeLine, steerBadgeGlyph, steerBadgeTone, steerRefusalMessage } from './fleet-steer.ts';
|
|
50
|
+
import { FleetStopTracker, fleetStateDisplay, toggleFleetPause, buildFleetTreeHints, countDescendantStats } from './fleet-stop.ts';
|
|
51
|
+
import { resolveFleetDeepLinkIndex } from './fleet-deep-link.ts';
|
|
57
52
|
import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from './fleet-transcript.ts';
|
|
58
53
|
import { renderFleetTabStrip } from '../renderer/fleet-tab-strip.ts';
|
|
54
|
+
import { planColumns, toneColor, formatFleetTokens, formatFleetCost } from './fleet-panel-format.ts';
|
|
59
55
|
|
|
60
56
|
const C = DEFAULT_PANEL_PALETTE;
|
|
61
57
|
|
|
62
|
-
// Column widths for the tree row layout. `label` absorbs whatever width is
|
|
63
|
-
// left over after the fixed columns + gaps; on hostile (narrow) widths the
|
|
64
|
-
// trailing columns simply clip (buildAlignedRow/buildSelectablePanelLine stop
|
|
65
|
-
// writing once a cell would overflow the row) rather than throwing or
|
|
66
|
-
// wrapping — the tree stays readable, just denser.
|
|
67
|
-
const KIND_W = 8;
|
|
68
|
-
const ELAPSED_W = 7;
|
|
69
|
-
const TOKENS_W = 7;
|
|
70
|
-
const COST_W = 8;
|
|
71
|
-
const ACTIVITY_W = 20;
|
|
72
|
-
const GAP = 1;
|
|
73
|
-
const FIXED_W = 1 /* glyph */ + KIND_W + ELAPSED_W + TOKENS_W + COST_W + ACTIVITY_W + GAP * 6;
|
|
74
|
-
|
|
75
|
-
function planColumns(width: number): ColumnSpec[] {
|
|
76
|
-
const labelWidth = Math.max(10, width - FIXED_W);
|
|
77
|
-
return [
|
|
78
|
-
{ width: 1 },
|
|
79
|
-
{ width: KIND_W },
|
|
80
|
-
{ width: labelWidth },
|
|
81
|
-
{ width: ELAPSED_W, align: 'right' },
|
|
82
|
-
{ width: TOKENS_W, align: 'right' },
|
|
83
|
-
{ width: COST_W, align: 'right' },
|
|
84
|
-
{ width: ACTIVITY_W },
|
|
85
|
-
];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
|
|
89
|
-
switch (tone) {
|
|
90
|
-
case 'active': return palette.info ?? DEFAULT_PANEL_PALETTE.info;
|
|
91
|
-
case 'success': return palette.good ?? DEFAULT_PANEL_PALETTE.good;
|
|
92
|
-
case 'failure': return palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
93
|
-
case 'warn': return palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
|
|
94
|
-
case 'muted': return palette.dim;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function formatFleetTokens(usage: ProcessUsage | undefined): string {
|
|
99
|
-
const total = fleetUsageTokens(usage);
|
|
100
|
-
if (total === null) return 'n/a';
|
|
101
|
-
return total >= 1000 ? `${(total / 1000).toFixed(1)}k` : String(total);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Honest cost display: never a fabricated $0.00 — 'unpriced' when costState says so. */
|
|
105
|
-
function formatFleetCost(costUsd: number | null | undefined, costState: ProcessCostState): string {
|
|
106
|
-
if (!hasFleetCost(costUsd, costState)) return 'unpriced';
|
|
107
|
-
const formatted = formatAgentCost(costUsd as number);
|
|
108
|
-
return costState === 'estimated' ? `~${formatted}` : formatted;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
58
|
export interface FleetActionCallbacks {
|
|
112
59
|
/** Graceful interruption (AgentManager.cancel / trigger-schedule disable, via the registry). */
|
|
113
60
|
readonly interrupt: (id: string) => boolean;
|
|
@@ -286,15 +233,11 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
286
233
|
}
|
|
287
234
|
|
|
288
235
|
/**
|
|
289
|
-
* W3.3 (cross-restart honesty)
|
|
290
|
-
* bridge from a prior TUI
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* is a real, permanent limitation (not a bug to fix in this item — see the
|
|
295
|
-
* W3.3 brief's design point 5), so it is documented plainly here rather
|
|
296
|
-
* than silently leaving the empty tree looking like "nothing has ever run"
|
|
297
|
-
* or, worse, implying a restart would bring prior processes back.
|
|
236
|
+
* W3.3 (cross-restart honesty): a restart always starts from an empty
|
|
237
|
+
* `AgentManager` Map (no bridge from a prior TUI/daemon registry), so a
|
|
238
|
+
* completed process from a previous session can never reappear here — a
|
|
239
|
+
* real, permanent limitation (W3.3 brief point 5), documented rather than
|
|
240
|
+
* silently implying "nothing has ever run" or that a restart brings it back.
|
|
298
241
|
*/
|
|
299
242
|
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
300
243
|
return [
|
|
@@ -361,6 +304,18 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
361
304
|
return this.getItems().find((row) => row.node.id === nodeId)?.node ?? null;
|
|
362
305
|
}
|
|
363
306
|
|
|
307
|
+
/** DEBT-5 item 4 (see fleet-deep-link.ts). */
|
|
308
|
+
public receiveDeepLink(target: { readonly id: string; readonly kind?: string }): void {
|
|
309
|
+
const idx = resolveFleetDeepLinkIndex(this.getItems(), target);
|
|
310
|
+
if (idx < 0) { this.setError('node no longer present.'); return; }
|
|
311
|
+
// A successful reveal must be VISIBLE: with a session tab active, render()
|
|
312
|
+
// routes to the tab view and the selection change would be silently
|
|
313
|
+
// swallowed (batch refutation finding 1) — return to the tree first
|
|
314
|
+
// (tabs kept; only the active view switches).
|
|
315
|
+
this.tabsState = { tabs: this.tabsState.tabs, activeTabIndex: 0 };
|
|
316
|
+
this.clearError(); this.selectedIndex = idx; this.selectedNodeId = target.id; this.markDirty();
|
|
317
|
+
}
|
|
318
|
+
|
|
364
319
|
/**
|
|
365
320
|
* Wave-3 (W3.2, risk #2 — "dropped inference"): called on every read-model
|
|
366
321
|
* snapshot update and on the panel's 1s tick. See fleet-steer.ts's
|
|
@@ -373,31 +328,6 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
373
328
|
if (changed) this.markDirty();
|
|
374
329
|
}
|
|
375
330
|
|
|
376
|
-
/** Count of descendants of `nodeId` (in the current snapshot) that report `capabilities.killable` — the Wave-3 K-confirm child count (Part C7). */
|
|
377
|
-
private countKillableDescendants(nodeId: string): number {
|
|
378
|
-
const rows = this.getItems();
|
|
379
|
-
const byParent = new Map<string, string[]>();
|
|
380
|
-
for (const row of rows) {
|
|
381
|
-
if (!row.node.parentId) continue;
|
|
382
|
-
const siblings = byParent.get(row.node.parentId) ?? [];
|
|
383
|
-
siblings.push(row.node.id);
|
|
384
|
-
byParent.set(row.node.parentId, siblings);
|
|
385
|
-
}
|
|
386
|
-
const byId = new Map(rows.map((row) => [row.node.id, row] as const));
|
|
387
|
-
let count = 0;
|
|
388
|
-
const stack = [...(byParent.get(nodeId) ?? [])];
|
|
389
|
-
const seen = new Set<string>();
|
|
390
|
-
while (stack.length > 0) {
|
|
391
|
-
const id = stack.pop()!;
|
|
392
|
-
if (seen.has(id)) continue;
|
|
393
|
-
seen.add(id);
|
|
394
|
-
const row = byId.get(id);
|
|
395
|
-
if (row?.node.capabilities.killable) count++;
|
|
396
|
-
for (const child of byParent.get(id) ?? []) stack.push(child);
|
|
397
|
-
}
|
|
398
|
-
return count;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
331
|
/** Kick off (once) the async ledger-fallback load for a tab whose conversation snapshot is unavailable. */
|
|
402
332
|
private ensureLedgerLoaded(tab: FleetTab): void {
|
|
403
333
|
if (tab.ledgerLoadStarted) return;
|
|
@@ -479,8 +409,8 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
479
409
|
}
|
|
480
410
|
const node = selected.node;
|
|
481
411
|
const shortId = node.id.length > 8 ? node.id.slice(-8) : node.id;
|
|
482
|
-
const
|
|
483
|
-
const suffix =
|
|
412
|
+
const stats = countDescendantStats(this.getItems(), node.id);
|
|
413
|
+
const suffix = stats.total > 0 ? ` (+${stats.total} descendant${stats.total === 1 ? '' : 's'}, ${stats.active} active)` : '';
|
|
484
414
|
this.confirmOverlay.arm({
|
|
485
415
|
id: node.id,
|
|
486
416
|
label: `${node.kind} ${shortId}${suffix}`,
|
|
@@ -527,6 +457,28 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
527
457
|
return true;
|
|
528
458
|
}
|
|
529
459
|
|
|
460
|
+
if (key === 's') {
|
|
461
|
+
// Batch replay D4: 's' from the TREE was silently dead — steering
|
|
462
|
+
// required an undiscoverable Enter-attach first. Attach-and-steer in
|
|
463
|
+
// one press for a steerable node; honest refusal otherwise.
|
|
464
|
+
if (!selected) return false;
|
|
465
|
+
const node = selected.node;
|
|
466
|
+
// Closure replay note: a node that FINISHED in the hint→keypress race
|
|
467
|
+
// window must say so — "does not support steer" is misleading for an
|
|
468
|
+
// agent whose only problem is being done.
|
|
469
|
+
if (isTerminalProcessState(node.state)) {
|
|
470
|
+
this.setError(`${node.kind} already finished — nothing to steer.`);
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
if (!node.capabilities.steerable || !isAttachableFleetKind(node.kind)) {
|
|
474
|
+
this.setError(`${node.kind} does not support steer.`);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
this.tabsState = attachFleetTab(this.tabsState, node);
|
|
478
|
+
this.markDirty();
|
|
479
|
+
return this.tryOpenSteerDraft(activeFleetTab(this.tabsState)!);
|
|
480
|
+
}
|
|
481
|
+
|
|
530
482
|
const consumed = super.handleInput(key);
|
|
531
483
|
if (consumed) this.captureSelectionAnchor();
|
|
532
484
|
return consumed;
|
|
@@ -539,8 +491,12 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
539
491
|
*/
|
|
540
492
|
private tryOpenSteerDraft(tab: FleetTab): boolean {
|
|
541
493
|
const node = this.findLiveNode(tab.nodeId);
|
|
542
|
-
if (!node || isTerminalProcessState(node.state)
|
|
543
|
-
this.setError(`${node?.kind ?? tab.kind}
|
|
494
|
+
if (!node || isTerminalProcessState(node.state)) {
|
|
495
|
+
this.setError(`${node?.kind ?? tab.kind} already finished — nothing to steer.`);
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (!node.capabilities.steerable) {
|
|
499
|
+
this.setError(`${node.kind} does not support steering.`);
|
|
544
500
|
return true;
|
|
545
501
|
}
|
|
546
502
|
tab.steerDraft = '';
|
|
@@ -557,8 +513,15 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
557
513
|
}
|
|
558
514
|
if (isPanelSearchCommit(key)) {
|
|
559
515
|
const text = (tab.steerDraft ?? '').trim();
|
|
560
|
-
|
|
561
|
-
|
|
516
|
+
if (text.length === 0) {
|
|
517
|
+
tab.steerDraft = null;
|
|
518
|
+
this.markDirty();
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
// Clear the draft ONLY on a confirmed queue. A refusal keeps the composer
|
|
522
|
+
// open with the text intact (submitSteer explains why + suggests live targets)
|
|
523
|
+
// so the operator never loses what they typed to a just-idle agent.
|
|
524
|
+
if (this.submitSteer(tab, text)) tab.steerDraft = null;
|
|
562
525
|
this.markDirty();
|
|
563
526
|
return true;
|
|
564
527
|
}
|
|
@@ -580,16 +543,25 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
580
543
|
return true; // absorb every other key while composing
|
|
581
544
|
}
|
|
582
545
|
|
|
583
|
-
/**
|
|
584
|
-
|
|
546
|
+
/**
|
|
547
|
+
* Submit a composed steer message. Returns whether it was queued so the caller
|
|
548
|
+
* clears the draft only on a confirmed send (WO UX-A item 4): a refusal (target
|
|
549
|
+
* went idle while composing) PRESERVES the typed text and suggests which agents
|
|
550
|
+
* ARE steerable, instead of silently discarding the draft. On a successful queue
|
|
551
|
+
* the ⧗ badge line is the immediate honest acknowledgment.
|
|
552
|
+
*/
|
|
553
|
+
private submitSteer(tab: FleetTab, text: string): boolean {
|
|
585
554
|
const result = this.actions.steer(tab.nodeId, text);
|
|
586
555
|
if (result.queued) {
|
|
587
556
|
// queuedAt drives reconcileSteerBadges' TTL-expiry fallback (fleet-steer.ts).
|
|
588
557
|
tab.steerBadge = { messageId: result.messageId, status: 'queued', queuedAt: Date.now() };
|
|
589
|
-
|
|
590
|
-
|
|
558
|
+
this.markDirty();
|
|
559
|
+
return true;
|
|
591
560
|
}
|
|
561
|
+
const siblings = liveSteerableLabels(this.readModel.getSnapshot().rows.map((row) => row.node), tab.nodeId);
|
|
562
|
+
this.setError(steerRefusalMessage(result.reason, siblings));
|
|
592
563
|
this.markDirty();
|
|
564
|
+
return false;
|
|
593
565
|
}
|
|
594
566
|
|
|
595
567
|
protected renderItem(row: FleetTreeRow, _index: number, _selected: boolean, width: number): Line {
|
|
@@ -662,12 +634,10 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
662
634
|
[' activity ', C.label],
|
|
663
635
|
[truncateDisplay(activityText, Math.max(0, width - 11)), C.dim],
|
|
664
636
|
]);
|
|
665
|
-
//
|
|
666
|
-
const line4 = buildPanelLine(width, [
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
]);
|
|
670
|
-
return [line1, line2, line3, line4];
|
|
637
|
+
// Approval history attaches here once session tabs land.
|
|
638
|
+
const line4 = buildPanelLine(width, [[' approvals ', C.label], ['—', C.dim]]);
|
|
639
|
+
const isolationDetail = node.kind === 'work-item' ? formatWorkItemIsolationDetailFromRaw(node.raw) : null;
|
|
640
|
+
return [line1, line2, line3, line4, ...(isolationDetail ? [buildPanelLine(width, [[' isolation ', C.label], [isolationDetail, C.dim]])] : [])];
|
|
671
641
|
}
|
|
672
642
|
|
|
673
643
|
/** Renders the active session tab's content (transcript / chain summary / ledger fallback) in place of the tree. */
|
|
@@ -706,8 +676,12 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
706
676
|
palette,
|
|
707
677
|
{ active: true, bg: palette.inputBg, valueColor: palette.info },
|
|
708
678
|
));
|
|
709
|
-
} else if (tab.steerBadge) {
|
|
710
|
-
|
|
679
|
+
} else if (tab.steerBadge && !this.stopTracker.isStopping(tab.nodeId)) {
|
|
680
|
+
// A queued-steer promise ("delivers on its next turn") must not render
|
|
681
|
+
// while a stop is in flight for this node — there may be no next turn
|
|
682
|
+
// (refutation finding 4). The badge reconciles to dropped/consumed once
|
|
683
|
+
// the node's terminal state lands.
|
|
684
|
+
composerLines.push(renderSteerBadgeLine(tab.steerBadge, width, palette, liveNode?.label));
|
|
711
685
|
}
|
|
712
686
|
|
|
713
687
|
const footerLines = [
|
|
@@ -749,22 +723,6 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
749
723
|
return buildPanelWorkspace(width, height, { title: `Fleet — ${tab.label}`, sections, footerLines, palette });
|
|
750
724
|
}
|
|
751
725
|
|
|
752
|
-
/** One-line honest status for the active tab's steer badge (queued/consumed/dropped) — see fleet-tabs.ts's SteerBadgeStatus doc. */
|
|
753
|
-
private renderSteerBadgeLine(badge: SteerBadge, width: number, palette: PanelPalette): Line {
|
|
754
|
-
const glyph = steerBadgeGlyph(badge.status);
|
|
755
|
-
const tone = steerBadgeTone(badge.status, palette);
|
|
756
|
-
const label = badge.status === 'queued'
|
|
757
|
-
? 'steer queued — awaiting the agent\'s next turn'
|
|
758
|
-
: badge.status === 'consumed'
|
|
759
|
-
? 'steer consumed'
|
|
760
|
-
: `steer dropped — ${badge.note ?? 'the target ended before delivery'}`;
|
|
761
|
-
return buildPanelLine(width, [
|
|
762
|
-
[' ', palette.dim],
|
|
763
|
-
[glyph, tone],
|
|
764
|
-
[` ${label}`, palette.dim],
|
|
765
|
-
]);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
726
|
private renderTreeView(width: number, height: number): Line[] {
|
|
769
727
|
const selected = this.getSelectedItem();
|
|
770
728
|
const footer: Line[] = [];
|
|
@@ -71,6 +71,15 @@ export interface FleetSnapshot {
|
|
|
71
71
|
|
|
72
72
|
export type FleetStateTone = 'active' | 'success' | 'failure' | 'warn' | 'muted';
|
|
73
73
|
|
|
74
|
+
// W6-P1 ruling: this is a DIFFERENT table from the SDK presentation
|
|
75
|
+
// contract's STATE_GLYPHS (@pellux/goodvibes-sdk/platform/presentation) — that
|
|
76
|
+
// one is a 4-state semantic alias (good/warn/bad/info) shared by the TUI and
|
|
77
|
+
// agent renderers; this one is a 12-state ProcessNode taxonomy specific to the
|
|
78
|
+
// Fleet tree panel (thinking/executing-tool/awaiting-approval/streaming/
|
|
79
|
+
// stalled/retrying/done/failed/killed/interrupted/idle/queued/paused), and no
|
|
80
|
+
// other surface (checked: the agent has no fleet-panel renderer) consumes it
|
|
81
|
+
// today. Per the presentation-parity brief, a table only moves to the SDK
|
|
82
|
+
// contract when 2+ surfaces need it — this one stays TUI-renderer-local.
|
|
74
83
|
const STATE_GLYPHS: Record<ProcessState, string> = {
|
|
75
84
|
thinking: '◔',
|
|
76
85
|
'executing-tool': '●',
|
|
@@ -374,6 +383,40 @@ export function buildFleetSnapshot(nodes: readonly ProcessNode[], capturedAt: nu
|
|
|
374
383
|
return { rows, totalCost, totalTokens, runningCount, capturedAt };
|
|
375
384
|
}
|
|
376
385
|
|
|
386
|
+
/**
|
|
387
|
+
* The fleet's honest leaf cost total — the SAME figure as
|
|
388
|
+
* FleetSnapshot.totalCost, but computed in one pass WITHOUT building the display
|
|
389
|
+
* rows, for cheap per-frame use (the always-visible footer). Excludes rollup
|
|
390
|
+
* aggregates (chain/subtask/workstream/phase) AND the WRFC owner (its usage is a
|
|
391
|
+
* mixed-model rollup of its children), so it is a true leaf-sum with no
|
|
392
|
+
* double-count. Null when nothing priced. Add this to the main session cost for
|
|
393
|
+
* the true "you + fleet" total.
|
|
394
|
+
*/
|
|
395
|
+
export function fleetLeafCostTotal(nodes: readonly ProcessNode[]): number | null {
|
|
396
|
+
let total: number | null = null;
|
|
397
|
+
for (const node of nodes) {
|
|
398
|
+
if (isFleetAggregateRollupNode(node)) continue;
|
|
399
|
+
if (!hasFleetCost(node.costUsd, node.costState)) continue;
|
|
400
|
+
total = (total ?? 0) + (node.costUsd as number);
|
|
401
|
+
}
|
|
402
|
+
return total;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The fleet cost figure for the always-visible footer: fleetLeafCostTotal over a
|
|
407
|
+
* fresh registry query, but ONLY while a fleet is live — the idle single-session
|
|
408
|
+
* path must never pay the aggregate-on-read query() scan. Query failures degrade to
|
|
409
|
+
* null (honest "no fleet cost"), never a throw into the render loop.
|
|
410
|
+
*/
|
|
411
|
+
export function footerFleetCost(queryNodes: () => readonly ProcessNode[], hasLiveFleet: boolean): number | null {
|
|
412
|
+
if (!hasLiveFleet) return null;
|
|
413
|
+
try {
|
|
414
|
+
return fleetLeafCostTotal(queryNodes());
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
377
420
|
// ---------------------------------------------------------------------------
|
|
378
421
|
// Read-model — two-factory shape (live / static), mirrors cockpit-read-model.ts
|
|
379
422
|
// ---------------------------------------------------------------------------
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import { STEER_TTL_MS, type ProcessNode } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
13
13
|
import { isTerminalProcessState } from './fleet-read-model.ts';
|
|
14
|
-
import type { FleetTab, SteerBadgeStatus } from './fleet-tabs.ts';
|
|
15
|
-
import { DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
|
|
14
|
+
import type { FleetTab, SteerBadge, SteerBadgeStatus } from './fleet-tabs.ts';
|
|
15
|
+
import { buildPanelLine, DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
|
|
16
|
+
import type { Line } from '../types/grid.ts';
|
|
16
17
|
|
|
17
18
|
/** Linger before an auto-resolved (consumed/dropped) steer badge is cleared from the tab. */
|
|
18
19
|
export const STEER_BADGE_LINGER_MS = 4_000;
|
|
@@ -33,6 +34,38 @@ export function steerBadgeTone(status: SteerBadgeStatus, palette: PanelPalette):
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** Labels of the currently live, steerable agent nodes other than `excludeNodeId` — offered when a steer target has gone inactive (WO UX-A item 4). */
|
|
38
|
+
export function liveSteerableLabels(nodes: readonly ProcessNode[], excludeNodeId: string): string[] {
|
|
39
|
+
return nodes
|
|
40
|
+
.filter((node) => node.id !== excludeNodeId && node.kind === 'agent' && node.capabilities.steerable && !isTerminalProcessState(node.state))
|
|
41
|
+
.map((node) => node.label);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Refusal message for a steer that could not be queued: states why + preserves the draft + suggests live targets. */
|
|
45
|
+
export function steerRefusalMessage(reason: string, siblingLabels: readonly string[]): string {
|
|
46
|
+
const suggestion = siblingLabels.length > 0
|
|
47
|
+
? ` Draft kept — steerable now: ${siblingLabels.slice(0, 3).join(', ')}${siblingLabels.length > 3 ? '…' : ''}.`
|
|
48
|
+
: ' Draft kept — no other agents are currently steerable.';
|
|
49
|
+
return `${reason}.${suggestion}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One-line honest status for a tab's steer badge; the queued line names the target and points at the ⧗ delivery badge (WO UX-A item 4). */
|
|
53
|
+
export function renderSteerBadgeLine(badge: SteerBadge, width: number, palette: PanelPalette, targetLabel?: string): Line {
|
|
54
|
+
const glyph = steerBadgeGlyph(badge.status);
|
|
55
|
+
const tone = steerBadgeTone(badge.status, palette);
|
|
56
|
+
const forTarget = targetLabel ? ` for ${targetLabel}` : '';
|
|
57
|
+
const label = badge.status === 'queued'
|
|
58
|
+
? `steer queued${forTarget} — delivers on its next turn (watch the ${glyph} badge)`
|
|
59
|
+
: badge.status === 'consumed'
|
|
60
|
+
? 'steer consumed'
|
|
61
|
+
: `steer dropped — ${badge.note ?? 'the target ended before delivery'}`;
|
|
62
|
+
return buildPanelLine(width, [
|
|
63
|
+
[' ', palette.dim],
|
|
64
|
+
[glyph, tone],
|
|
65
|
+
[` ${label}`, palette.dim],
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
/**
|
|
37
70
|
* Wave-3 (W3.2, risk #2 — "dropped inference"): the SDK emits no
|
|
38
71
|
* cancelled/expired signal for a queued steer, so a badge left `queued`
|
package/src/panels/fleet-stop.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
import type { ProcessNode, ProcessState } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
11
|
-
import { fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone } from './fleet-read-model.ts';
|
|
11
|
+
import { fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone, type FleetTreeRow } from './fleet-read-model.ts';
|
|
12
12
|
|
|
13
13
|
/** How long 'stopping…' lingers after a stop keypress before the true state is shown regardless (never masks a stuck kill). */
|
|
14
14
|
export const STOP_SETTLE_MS = 1500;
|
|
@@ -115,6 +115,7 @@ export function buildFleetTreeHints(
|
|
|
115
115
|
{ keys: 'j/k', label: 'navigate' },
|
|
116
116
|
{ keys: 'Enter', label: 'attach' },
|
|
117
117
|
];
|
|
118
|
+
if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); // D4: discoverable from the tree (attach-and-steer)
|
|
118
119
|
if (live && selected.capabilities.interruptible) hints.push({ keys: 'i', label: 'interrupt' });
|
|
119
120
|
if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
|
|
120
121
|
if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
|
|
@@ -123,3 +124,30 @@ export function buildFleetTreeHints(
|
|
|
123
124
|
if (hasTabs) hints.push({ keys: '[ ]', label: 'tabs' });
|
|
124
125
|
return hints;
|
|
125
126
|
}
|
|
127
|
+
|
|
128
|
+
/** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" (Wave-3 C7), undercounting a mixed subtree. */
|
|
129
|
+
export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
|
|
130
|
+
const byParent = new Map<string, string[]>();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (!row.node.parentId) continue;
|
|
133
|
+
const siblings = byParent.get(row.node.parentId) ?? [];
|
|
134
|
+
siblings.push(row.node.id);
|
|
135
|
+
byParent.set(row.node.parentId, siblings);
|
|
136
|
+
}
|
|
137
|
+
const byId = new Map(rows.map((row) => [row.node.id, row] as const));
|
|
138
|
+
let total = 0, active = 0;
|
|
139
|
+
const stack = [...(byParent.get(nodeId) ?? [])];
|
|
140
|
+
const seen = new Set<string>();
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const id = stack.pop()!;
|
|
143
|
+
if (seen.has(id)) continue;
|
|
144
|
+
seen.add(id);
|
|
145
|
+
const row = byId.get(id);
|
|
146
|
+
if (row && !isTerminalProcessState(row.node.state)) {
|
|
147
|
+
total++;
|
|
148
|
+
if (row.node.capabilities.killable) active++;
|
|
149
|
+
}
|
|
150
|
+
for (const child of byParent.get(id) ?? []) stack.push(child);
|
|
151
|
+
}
|
|
152
|
+
return { total, active };
|
|
153
|
+
}
|
|
@@ -111,7 +111,22 @@ class KeybindingsModalSurface implements ConfigModalSurface {
|
|
|
111
111
|
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
112
112
|
if (id === 'refresh') { ctx.setStatus('Docs & shortcuts are read live.'); ctx.requestRender(); return; }
|
|
113
113
|
if (id !== 'activate') return;
|
|
114
|
-
if (ctx.tabId === 'tools') {
|
|
114
|
+
if (ctx.tabId === 'tools') {
|
|
115
|
+
// DEBT-5 item 4: pass the tool name through as a deep-link target.
|
|
116
|
+
// Honest caveat (documented, not a bug to silently paper over): fleet's
|
|
117
|
+
// ProcessKind set has no 'tool' node — a docs Tools row names a static
|
|
118
|
+
// tool DEFINITION, not a live process, so FleetPanel.receiveDeepLink
|
|
119
|
+
// will not currently find a match and shows its honest "node no longer
|
|
120
|
+
// present" line. The plumbing is still worth wiring now (matches the
|
|
121
|
+
// work-plan agent/wrfc jumps' shape) — it starts resolving for free the
|
|
122
|
+
// day fleet grows a per-tool-call node (the retired DocsPanel's own
|
|
123
|
+
// comment already anticipated this: "no filter-by-tool equivalent
|
|
124
|
+
// there yet").
|
|
125
|
+
const toolName = ctx.row?.id.startsWith('tool:') ? ctx.row.id.slice('tool:'.length) : null;
|
|
126
|
+
void ctx.executeCommand?.('panel', toolName ? ['open', 'fleet', '--target', `${toolName}:tool`] : ['open', 'fleet']);
|
|
127
|
+
ctx.setStatus('Opened the tool inspector (fleet).');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
115
130
|
if (ctx.tabId === 'models') {
|
|
116
131
|
const key = ctx.row?.id.startsWith('model:') ? ctx.row.id.slice('model:'.length) : null;
|
|
117
132
|
if (!key) return;
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { activeUiTones, registerThemeRefresh } from '../../renderer/theme.ts';
|
|
2
2
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Semantic tone tokens for the W6.1 group-B config modals.
|
|
5
5
|
//
|
|
6
|
-
// Derived entirely from the chrome token set
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// Derived entirely from the chrome token set so the builders carry NO raw hex
|
|
7
|
+
// color literals (architecture: no-raw-hex-literal-growth). Rebuilt IN PLACE
|
|
8
|
+
// on theme flips via registerThemeRefresh — the group-B surfaces read
|
|
9
|
+
// MODAL_TONES by reference at render time, so a dark-pinned module const here
|
|
10
|
+
// left them rendering dark accents on light chrome (batch refutation
|
|
11
|
+
// finding 2). Mirrors the polish-core/overlay-box/modal-factory pattern.
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
function buildModalTones() {
|
|
15
|
+
const T = activeUiTones();
|
|
16
|
+
return {
|
|
17
|
+
/** Informational / accent (blue). */
|
|
18
|
+
info: T.state.info,
|
|
19
|
+
/** Positive / healthy (green). */
|
|
20
|
+
good: T.state.good,
|
|
21
|
+
/** Caution (amber). */
|
|
22
|
+
warn: T.state.warn,
|
|
23
|
+
/** Error / danger (red). */
|
|
24
|
+
bad: T.state.bad,
|
|
25
|
+
/** Reasoning / secondary accent (purple). */
|
|
26
|
+
reasoning: T.state.reasoning,
|
|
27
|
+
/** Muted secondary text. */
|
|
28
|
+
muted: T.fg.muted,
|
|
29
|
+
/** Dim tertiary text. */
|
|
30
|
+
dim: T.fg.dim,
|
|
31
|
+
/** Primary text. */
|
|
32
|
+
primary: T.fg.primary,
|
|
33
|
+
/** QR module foreground — high-contrast, mode-appropriate. */
|
|
34
|
+
qrDark: T.fg.inverse,
|
|
35
|
+
/** QR field background — high-contrast, mode-appropriate. */
|
|
36
|
+
qrLight: T.fg.primary,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
14
39
|
|
|
15
|
-
export const MODAL_TONES =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/** Positive / healthy (green). */
|
|
19
|
-
good: T.state.good,
|
|
20
|
-
/** Caution (amber). */
|
|
21
|
-
warn: T.state.warn,
|
|
22
|
-
/** Error / danger (red). */
|
|
23
|
-
bad: T.state.bad,
|
|
24
|
-
/** Reasoning / secondary accent (purple). */
|
|
25
|
-
reasoning: T.state.reasoning,
|
|
26
|
-
/** Muted secondary text. */
|
|
27
|
-
muted: T.fg.muted,
|
|
28
|
-
/** Dim tertiary text. */
|
|
29
|
-
dim: T.fg.dim,
|
|
30
|
-
/** Primary (near-white) text. */
|
|
31
|
-
primary: T.fg.primary,
|
|
32
|
-
/** QR module foreground — high-contrast dark, not pure black. */
|
|
33
|
-
qrDark: T.fg.inverse,
|
|
34
|
-
/** QR field background — high-contrast light, not pure white. */
|
|
35
|
-
qrLight: T.fg.primary,
|
|
36
|
-
} as const;
|
|
40
|
+
export const MODAL_TONES: Readonly<ReturnType<typeof buildModalTones>> = buildModalTones();
|
|
41
|
+
|
|
42
|
+
registerThemeRefresh(() => Object.assign(MODAL_TONES as Record<string, string>, buildModalTones()));
|
|
@@ -18,9 +18,11 @@ import { encodeConnectionPayload, generateQrMatrix, renderQrToString } from '@pe
|
|
|
18
18
|
// buildView, cached) so a missing daemon home degrades honestly rather than
|
|
19
19
|
// throwing at modal-registration time.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// DEBT-3: the regenerate action now dispatches the real `/qrcode regenerate`
|
|
22
|
+
// verb (which rotates the shared operator-token store) and, once it resolves,
|
|
23
|
+
// RE-PULLS the lazy connection-info thunk so the modal shows the rotated token +
|
|
24
|
+
// QR in place without being re-opened. `confirm: true` keeps the two-press
|
|
25
|
+
// guard so a stray keystroke never rotates a live companion's token.
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
|
|
26
28
|
export interface PairingModalConnectionInfo {
|
|
@@ -58,7 +60,7 @@ class PairingModalSurface implements ConfigModalSurface {
|
|
|
58
60
|
readonly actions = [
|
|
59
61
|
{ key: 'v', id: 'toggleReveal', label: 'reveal token' },
|
|
60
62
|
{ key: 'c', id: 'copyToken', label: 'copy token', enabledFor: () => Boolean(this.deps.copyToClipboard) },
|
|
61
|
-
{ key: 'r', id: 'regenerate', label: 'regenerate token' },
|
|
63
|
+
{ key: 'r', id: 'regenerate', label: 'regenerate token', confirm: true },
|
|
62
64
|
];
|
|
63
65
|
|
|
64
66
|
onOpen(requestRender: () => void): void { this.requestRender = requestRender; }
|
|
@@ -117,8 +119,25 @@ class PairingModalSurface implements ConfigModalSurface {
|
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
121
|
if (id === 'regenerate') {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const dispatched = ctx.executeCommand?.('qrcode', ['regenerate']);
|
|
123
|
+
if (!dispatched) {
|
|
124
|
+
ctx.setStatus('Token rotation is unavailable in this runtime.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
ctx.setStatus('Rotating pairing token…');
|
|
128
|
+
void dispatched
|
|
129
|
+
.then((ok) => {
|
|
130
|
+
if (ok === false) { ctx.setStatus('Token rotation did not complete.'); return; }
|
|
131
|
+
// Re-pull the lazy connection-info thunk: /qrcode regenerate rewrote
|
|
132
|
+
// the shared token store, so a fresh getConnectionInfo() reflects the
|
|
133
|
+
// rotated token + QR. ensureInfo caches on first build — clear it.
|
|
134
|
+
this.info = undefined;
|
|
135
|
+
this.revealed = false;
|
|
136
|
+
this.ensureInfo();
|
|
137
|
+
this.requestRender();
|
|
138
|
+
ctx.setStatus('Pairing token rotated — new token and QR loaded.');
|
|
139
|
+
})
|
|
140
|
+
.catch(() => { ctx.setStatus('Token rotation failed.'); });
|
|
122
141
|
}
|
|
123
142
|
}
|
|
124
143
|
}
|