@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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// panel-paste-flood-guard.ts — DEBT-5 item 5.
|
|
3
|
+
//
|
|
4
|
+
// A terminal WITHOUT bracketed paste delivers a pasted block as a burst of
|
|
5
|
+
// discrete 1-char 'text' tokens (isPasteToken stays false for every one of
|
|
6
|
+
// them, since that flag only fires for a single token whose value.length > 1
|
|
7
|
+
// — see handler-feed-routes.ts's own Invariant B doc). Before this guard,
|
|
8
|
+
// each such character became a real panel hotkey on a focused non-capturing
|
|
9
|
+
// panel (K arms kill, etc.) once it reached handlePanelFocusToken's per-char
|
|
10
|
+
// dispatch loop.
|
|
11
|
+
//
|
|
12
|
+
// This is a RATE guard: more than PANEL_PASTE_FLOOD_THRESHOLD qualifying
|
|
13
|
+
// tokens within the trailing PANEL_PASTE_FLOOD_WINDOW_MS, evaluated with a
|
|
14
|
+
// real sliding window (old timestamps age out of `timestamps` every call).
|
|
15
|
+
// It is deliberately NOT the retired per-feed char-SUM burst heuristic this
|
|
16
|
+
// same input layer already tore out once (see panel-focus-burst.test.ts's
|
|
17
|
+
// header doc): that old guard summed one feed()'s character count with no
|
|
18
|
+
// timing signal at all, so two ordinary nav keystrokes landing in a single
|
|
19
|
+
// feed() — a real, common case, not an edge case — were misread as a burst
|
|
20
|
+
// and focus was silently flipped to the composer. This guard:
|
|
21
|
+
// - never touches focus (panelFocused is untouched by trackPanelPasteFloodGuard
|
|
22
|
+
// and by its caller);
|
|
23
|
+
// - is keyed on WALL-CLOCK TIMING, not a per-feed token count, so it
|
|
24
|
+
// doesn't care how many tokens land in one feed() call, only how fast
|
|
25
|
+
// they arrive relative to each other;
|
|
26
|
+
// - is sticky once tripped (only a quiet gap — no qualifying token for a
|
|
27
|
+
// full window — clears it) so it doesn't flap dispatch on/off as the
|
|
28
|
+
// count oscillates near the threshold mid-flood.
|
|
29
|
+
//
|
|
30
|
+
// ~8 keys/120ms is far beyond sustained human typing (a fast typist peaks
|
|
31
|
+
// well under that inter-key rate over any real span) but is exactly the
|
|
32
|
+
// shape an unbracketed paste replay takes. It also keeps the pre-existing
|
|
33
|
+
// Invariant B regression tests (2 keys in one feed(), no timing control at
|
|
34
|
+
// all) far under threshold while catching a real flood within its first ~9
|
|
35
|
+
// characters.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export const PANEL_PASTE_FLOOD_WINDOW_MS = 120;
|
|
39
|
+
export const PANEL_PASTE_FLOOD_THRESHOLD = 8;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Guard state — a single persistent instance lives on the caller's
|
|
43
|
+
* long-lived context (handler-feed.ts's InputFeedContext, mirroring how that
|
|
44
|
+
* object already owns `nextPasteId`/`mouseDownRow`/etc.) and is MUTATED IN
|
|
45
|
+
* PLACE by trackPanelPasteFloodGuard below, never replaced — so callers never
|
|
46
|
+
* need to thread a return value back into their own state (unlike
|
|
47
|
+
* `panelFocused`, which handlePanelFocusToken returns because it is NOT a
|
|
48
|
+
* mutable outparam).
|
|
49
|
+
*/
|
|
50
|
+
export interface PanelBurstGuardState {
|
|
51
|
+
timestamps: readonly number[];
|
|
52
|
+
suspended: boolean;
|
|
53
|
+
hintShown: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PanelBurstGuardResult {
|
|
57
|
+
/** False while suspended — the caller must drop this token, not dispatch it. */
|
|
58
|
+
readonly dispatch: boolean;
|
|
59
|
+
/** True exactly once per burst: the call that just tripped suspension. */
|
|
60
|
+
readonly showHintNow: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Advance `guard` (mutated in place) by one qualifying token at time `now` (ms). */
|
|
64
|
+
export function trackPanelPasteFloodGuard(guard: PanelBurstGuardState, now: number): PanelBurstGuardResult {
|
|
65
|
+
const lastAt = guard.timestamps.length > 0 ? guard.timestamps[guard.timestamps.length - 1]! : -Infinity;
|
|
66
|
+
const isQuietGap = now - lastAt > PANEL_PASTE_FLOOD_WINDOW_MS;
|
|
67
|
+
if (isQuietGap && guard.suspended) {
|
|
68
|
+
// A silence at least as long as the window means whatever burst was
|
|
69
|
+
// happening has ended — un-suspend so a LATER burst gets its own fresh
|
|
70
|
+
// count and its own one-shot hint.
|
|
71
|
+
guard.suspended = false;
|
|
72
|
+
guard.hintShown = false;
|
|
73
|
+
}
|
|
74
|
+
guard.timestamps = isQuietGap
|
|
75
|
+
? [now]
|
|
76
|
+
: [...guard.timestamps.filter((t) => t > now - PANEL_PASTE_FLOOD_WINDOW_MS), now];
|
|
77
|
+
if (guard.timestamps.length > PANEL_PASTE_FLOOD_THRESHOLD) {
|
|
78
|
+
guard.suspended = true;
|
|
79
|
+
}
|
|
80
|
+
let showHintNow = false;
|
|
81
|
+
if (guard.suspended && !guard.hintShown) {
|
|
82
|
+
guard.hintShown = true;
|
|
83
|
+
showHintNow = true;
|
|
84
|
+
}
|
|
85
|
+
return { dispatch: !guard.suspended, showHintNow };
|
|
86
|
+
}
|
|
@@ -36,6 +36,14 @@ export class SelectionModal {
|
|
|
36
36
|
public selectedIndex = 0;
|
|
37
37
|
public allowSearch = true;
|
|
38
38
|
public customActions: Map<string, SelectionAction> = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* UX-C vocab unification: overrides the default per-primaryAction Enter
|
|
41
|
+
* verb ("Select"/"Toggle"/"Edit"/"Delete") shown in the footer hint. Used by
|
|
42
|
+
* callers whose items are really commands (e.g. /help) so the hint reads
|
|
43
|
+
* "[Enter] Run", matching the slash-command palette's own verb, instead of
|
|
44
|
+
* the generic "Select".
|
|
45
|
+
*/
|
|
46
|
+
public primaryVerbLabel: string | undefined = undefined;
|
|
39
47
|
|
|
40
48
|
/** Open the modal with items and title */
|
|
41
49
|
open(
|
|
@@ -45,6 +53,7 @@ export class SelectionModal {
|
|
|
45
53
|
preSelectId?: string;
|
|
46
54
|
allowSearch?: boolean;
|
|
47
55
|
customActions?: Map<string, SelectionAction>;
|
|
56
|
+
primaryVerbLabel?: string;
|
|
48
57
|
}
|
|
49
58
|
): void {
|
|
50
59
|
this.title = title;
|
|
@@ -53,6 +62,7 @@ export class SelectionModal {
|
|
|
53
62
|
this.allowSearch = opts?.allowSearch ?? true;
|
|
54
63
|
this.searchFocused = false;
|
|
55
64
|
this.customActions = opts?.customActions ?? new Map();
|
|
65
|
+
this.primaryVerbLabel = opts?.primaryVerbLabel;
|
|
56
66
|
this.active = true;
|
|
57
67
|
this.filterItems();
|
|
58
68
|
|
|
@@ -74,6 +84,7 @@ export class SelectionModal {
|
|
|
74
84
|
this.filteredItems = [];
|
|
75
85
|
this.selectedIndex = 0;
|
|
76
86
|
this.customActions = new Map();
|
|
87
|
+
this.primaryVerbLabel = undefined;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
moveUp(): void {
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SessionPickerModal — state management for the /sessions picker modal.
|
|
3
3
|
*
|
|
4
|
-
* Lists
|
|
5
|
-
*
|
|
4
|
+
* Lists LOCAL saved transcript files from SessionManager.list() (load/delete
|
|
5
|
+
* operate on these — a saved JSONL session is a different thing from a live
|
|
6
|
+
* control-plane session, see below), tracks selected index, and handles
|
|
7
|
+
* load/delete actions.
|
|
8
|
+
*
|
|
9
|
+
* W3-T2 (union-sessions surface): additionally surfaces the cross-surface
|
|
10
|
+
* session union from `sessionBroker` (a SessionReadFacade — normally
|
|
11
|
+
* `uiServices.sessions.sessionBroker`, the SessionUnionCache) so a user can
|
|
12
|
+
* SEE what sessions are live/closed across every surface sharing this
|
|
13
|
+
* daemon (TUI, webui, companion, automation), not just this process's own
|
|
14
|
+
* saved files. This is deliberately READ-ONLY: a SharedSessionRecord has no
|
|
15
|
+
* on-disk transcript this process can load()/delete() the way a local
|
|
16
|
+
* SessionInfo does, so cross-surface rows are visible + badged, not
|
|
17
|
+
* selectable for load/delete. `sessionBroker` is optional so every existing
|
|
18
|
+
* caller/test that only cares about local saved-session management keeps
|
|
19
|
+
* working unchanged.
|
|
6
20
|
*/
|
|
7
21
|
|
|
8
22
|
import { unlinkSync } from 'node:fs';
|
|
9
23
|
import type { SessionInfo, SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
24
|
+
import type { SharedSessionRecord } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
25
|
+
import type { CrossSurfaceView, SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
10
26
|
import type { ConversationManager } from '../core/conversation';
|
|
11
27
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
12
28
|
|
|
@@ -14,9 +30,22 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
14
30
|
// SessionPickerModal
|
|
15
31
|
// ---------------------------------------------------------------------------
|
|
16
32
|
|
|
33
|
+
/** Honest default posture when no `sessionBroker` was wired: no cross-surface claim. */
|
|
34
|
+
const DORMANT_CROSS_SURFACE_VIEW: CrossSurfaceView = {
|
|
35
|
+
mode: 'local',
|
|
36
|
+
online: false,
|
|
37
|
+
stale: false,
|
|
38
|
+
lastSyncAt: null,
|
|
39
|
+
offlineNote: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
17
42
|
export class SessionPickerModal {
|
|
18
43
|
public active = false;
|
|
19
44
|
public sessions: SessionInfo[] = [];
|
|
45
|
+
/** Cross-surface session union (view-only) — see class doc. Empty until open(). */
|
|
46
|
+
public crossSurfaceSessions: readonly SharedSessionRecord[] = [];
|
|
47
|
+
/** Honest posture for crossSurfaceSessions: mode/online/stale/offlineNote. */
|
|
48
|
+
public crossSurfaceView: CrossSurfaceView = DORMANT_CROSS_SURFACE_VIEW;
|
|
20
49
|
public selectedIndex = 0;
|
|
21
50
|
public scrollOffset = 0;
|
|
22
51
|
public visibleRows = 8;
|
|
@@ -25,13 +54,24 @@ export class SessionPickerModal {
|
|
|
25
54
|
/** Last status message to show in the modal (e.g. error or success). */
|
|
26
55
|
public statusMessage = '';
|
|
27
56
|
|
|
28
|
-
public constructor(
|
|
57
|
+
public constructor(
|
|
58
|
+
private readonly sessionManager: SessionManager,
|
|
59
|
+
private readonly sessionBroker?: SessionReadFacade,
|
|
60
|
+
) {}
|
|
29
61
|
|
|
30
62
|
/**
|
|
31
|
-
* Open the modal, loading sessions from SessionManager
|
|
63
|
+
* Open the modal, loading local saved sessions from SessionManager and (when
|
|
64
|
+
* a sessionBroker was wired) the cross-surface session union, honestly.
|
|
32
65
|
*/
|
|
33
66
|
open(): void {
|
|
34
67
|
this.sessions = this.sessionManager.list();
|
|
68
|
+
if (this.sessionBroker) {
|
|
69
|
+
this.crossSurfaceSessions = this.sessionBroker.listSessions();
|
|
70
|
+
this.crossSurfaceView = this.sessionBroker.crossSurfaceView;
|
|
71
|
+
} else {
|
|
72
|
+
this.crossSurfaceSessions = [];
|
|
73
|
+
this.crossSurfaceView = DORMANT_CROSS_SURFACE_VIEW;
|
|
74
|
+
}
|
|
35
75
|
this.selectedIndex = 0;
|
|
36
76
|
this.scrollOffset = 0;
|
|
37
77
|
this.statusMessage = '';
|
|
@@ -15,6 +15,13 @@ import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
|
|
|
15
15
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
16
16
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
17
17
|
import { CODE_INDEX_ENABLED_CONFIG_KEY } from '../runtime/code-index-services.ts';
|
|
18
|
+
import {
|
|
19
|
+
THEME_MODE_CONFIG_KEY,
|
|
20
|
+
THEME_MODE_VALUES,
|
|
21
|
+
THEME_MODE_DEFAULT,
|
|
22
|
+
THEME_MODE_DESCRIPTION,
|
|
23
|
+
coerceThemeModeSetting,
|
|
24
|
+
} from '../renderer/theme-mode-config.ts';
|
|
18
25
|
import {
|
|
19
26
|
SETTINGS_CATEGORIES,
|
|
20
27
|
type FlagEntry,
|
|
@@ -94,6 +101,14 @@ export function buildSettingGroups(
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
// DEBT-2: inject the synthetic display.themeMode enum (auto|dark|light). TUI-local
|
|
105
|
+
// key stored under the existing `display` section (see theme-mode-config.ts for why
|
|
106
|
+
// not `appearance`), same rationale as the other synthetic settings below.
|
|
107
|
+
const displayEntries = groups.get('display');
|
|
108
|
+
if (displayEntries && !displayEntries.some((e) => e.setting.key === (THEME_MODE_CONFIG_KEY as ConfigKey))) {
|
|
109
|
+
displayEntries.push(buildThemeModeSyntheticEntry(configManager));
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
const uiEntries = groups.get('ui');
|
|
98
113
|
if (uiEntries) {
|
|
99
114
|
const uiPriority: Record<string, number> = {
|
|
@@ -199,6 +214,34 @@ export function buildTtsSpeedSyntheticEntry(configManager: Pick<ConfigManager, '
|
|
|
199
214
|
};
|
|
200
215
|
}
|
|
201
216
|
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// display.themeMode synthetic setting (DEBT-2 light theme)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The synthetic ConfigSetting descriptor for display.themeMode. TUI-local (not
|
|
223
|
+
* in the SDK ConfigKey union); the key is cast to ConfigKey as the other
|
|
224
|
+
* synthetic settings do. Stored under the existing `display` section so
|
|
225
|
+
* ConfigManager.setDynamic/get round-trip it with no SDK change.
|
|
226
|
+
*/
|
|
227
|
+
export const THEME_MODE_SYNTHETIC_SETTING: ConfigSetting = {
|
|
228
|
+
key: THEME_MODE_CONFIG_KEY as ConfigKey,
|
|
229
|
+
type: 'enum',
|
|
230
|
+
default: THEME_MODE_DEFAULT,
|
|
231
|
+
enumValues: [...THEME_MODE_VALUES],
|
|
232
|
+
description: THEME_MODE_DESCRIPTION,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** Build the synthetic SettingEntry for display.themeMode. */
|
|
236
|
+
export function buildThemeModeSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
237
|
+
const currentValue = coerceThemeModeSetting(configManager.get(THEME_MODE_CONFIG_KEY as ConfigKey));
|
|
238
|
+
return {
|
|
239
|
+
setting: THEME_MODE_SYNTHETIC_SETTING,
|
|
240
|
+
currentValue,
|
|
241
|
+
isDefault: currentValue === THEME_MODE_DEFAULT,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
202
245
|
// ---------------------------------------------------------------------------
|
|
203
246
|
// behavior.notifyAfterSeconds synthetic setting
|
|
204
247
|
// ---------------------------------------------------------------------------
|
|
@@ -401,6 +444,8 @@ export function refreshEntryValues(
|
|
|
401
444
|
// construction time so isDefault stays accurate.
|
|
402
445
|
if (entry.setting.key === ('tts.speed' as ConfigKey)) {
|
|
403
446
|
entry.currentValue = normalizeTtsSpeedValue(raw);
|
|
447
|
+
} else if (entry.setting.key === (THEME_MODE_CONFIG_KEY as ConfigKey)) {
|
|
448
|
+
entry.currentValue = coerceThemeModeSetting(raw);
|
|
404
449
|
} else {
|
|
405
450
|
entry.currentValue = raw;
|
|
406
451
|
}
|
|
@@ -422,8 +467,12 @@ export function updateEntryForKey(
|
|
|
422
467
|
const entry = entries.find((candidate) => candidate.setting.key === key);
|
|
423
468
|
if (entry) {
|
|
424
469
|
const raw = configManager.get(key);
|
|
425
|
-
// Synthetic
|
|
426
|
-
entry.currentValue = key === ('tts.speed' as ConfigKey)
|
|
470
|
+
// Synthetic entries: normalize using the same fallback logic as construction.
|
|
471
|
+
entry.currentValue = key === ('tts.speed' as ConfigKey)
|
|
472
|
+
? normalizeTtsSpeedValue(raw)
|
|
473
|
+
: key === (THEME_MODE_CONFIG_KEY as ConfigKey)
|
|
474
|
+
? coerceThemeModeSetting(raw)
|
|
475
|
+
: raw;
|
|
427
476
|
entry.isDefault = deepEqual(entry.currentValue, entry.setting.default);
|
|
428
477
|
}
|
|
429
478
|
}
|
|
@@ -11,11 +11,13 @@ export type SettingsCategory =
|
|
|
11
11
|
| 'storage'
|
|
12
12
|
| 'permissions'
|
|
13
13
|
| 'orchestration'
|
|
14
|
+
| 'planner'
|
|
14
15
|
| 'wrfc'
|
|
15
16
|
| 'tools'
|
|
16
17
|
| 'helper'
|
|
17
18
|
| 'tts'
|
|
18
19
|
| 'service'
|
|
20
|
+
| 'daemon'
|
|
19
21
|
| 'controlPlane'
|
|
20
22
|
| 'httpListener'
|
|
21
23
|
| 'web'
|
|
@@ -42,9 +44,9 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
42
44
|
}> = [
|
|
43
45
|
{ label: 'Interface', categories: ['display', 'ui', 'behavior', 'permissions'] },
|
|
44
46
|
{ label: 'AI Routing', categories: ['provider', 'subscriptions', 'helper', 'tools', 'tts'] },
|
|
45
|
-
{ label: 'Service & Network', categories: ['service', 'network', 'controlPlane', 'httpListener', 'web'] },
|
|
47
|
+
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web'] },
|
|
46
48
|
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'mcp', 'cloudflare'] },
|
|
47
|
-
{ label: 'Automation', categories: ['batch', 'automation', 'watchers', 'orchestration', 'wrfc'] },
|
|
49
|
+
{ label: 'Automation', categories: ['batch', 'automation', 'watchers', 'orchestration', 'planner', 'wrfc'] },
|
|
48
50
|
{ label: 'Runtime & Data', categories: ['storage', 'sandbox', 'runtime', 'cache', 'telemetry'] },
|
|
49
51
|
{ label: 'Advanced', categories: ['flags', 'release', 'danger'] },
|
|
50
52
|
];
|
package/src/main.ts
CHANGED
|
@@ -34,15 +34,9 @@ import { bootstrapRuntime } from './runtime/bootstrap.ts';
|
|
|
34
34
|
import type { BootstrapContext } from './runtime/bootstrap.ts';
|
|
35
35
|
import { buildSharedOrchestratorCoreServices } from './runtime/orchestrator-core-services.ts';
|
|
36
36
|
import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
|
|
37
|
-
import {
|
|
38
|
-
checkRecoveryFile,
|
|
39
|
-
deleteRecoveryFile,
|
|
40
|
-
loadRecoveryConversation,
|
|
41
|
-
readLastSessionPointer,
|
|
42
|
-
writeRecoveryFile,
|
|
43
|
-
} from '@/runtime/index.ts';
|
|
37
|
+
import { readLastSessionPointer, writeRecoveryFile } from '@/runtime/index.ts';
|
|
44
38
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
45
|
-
import { createPersistRecoverySnapshot, createReopenRecoveryPanels, handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
|
|
39
|
+
import { createPersistRecoverySnapshot, createRecoveryFileOps, createReopenRecoveryPanels, handleErrorAffordanceKey, resolveStartupRecoveryInfo } from './shell/recovery-input-helpers.ts';
|
|
46
40
|
import { wireShellUiOpeners } from './shell/ui-openers.ts';
|
|
47
41
|
import { deriveComposerState } from './core/composer-state.ts';
|
|
48
42
|
import { buildPersistedSessionContext, formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
|
|
@@ -58,6 +52,7 @@ import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
|
58
52
|
import { createRenderScheduler } from './runtime/render-scheduler.ts';
|
|
59
53
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
60
54
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
55
|
+
import { footerFleetCost } from './panels/fleet-read-model.ts';
|
|
61
56
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
62
57
|
import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
63
58
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
@@ -67,12 +62,8 @@ import { createCancelGeneration } from './core/turn-cancellation.ts';
|
|
|
67
62
|
import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
|
|
68
63
|
import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const CURSOR_HIDE = '\x1b[?25l'; const CURSOR_SHOW = '\x1b[?25h'; const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
|
|
73
|
-
const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u'; const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
|
|
74
|
-
const PASTE_ENABLE = '\x1b[?2004h'; const PASTE_DISABLE = '\x1b[?2004l';
|
|
75
|
-
const FOCUS_ENABLE = '\x1b[?1004h'; const FOCUS_DISABLE = '\x1b[?1004l';
|
|
65
|
+
import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
|
|
66
|
+
import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
|
|
76
67
|
|
|
77
68
|
async function main() {
|
|
78
69
|
const stdout = process.stdout;
|
|
@@ -213,6 +204,8 @@ async function main() {
|
|
|
213
204
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
214
205
|
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
215
206
|
let recoveryPending = false;
|
|
207
|
+
// Which file the current recovery prompt should load/delete from (see recovery-input-helpers.ts).
|
|
208
|
+
let recoverySource: 'live' | 'preserved' = 'live';
|
|
216
209
|
|
|
217
210
|
const lifecycle = installProcessLifecycle({
|
|
218
211
|
stdin,
|
|
@@ -392,7 +385,7 @@ async function main() {
|
|
|
392
385
|
},
|
|
393
386
|
sessions: {
|
|
394
387
|
sessionManager: ctx.services.sessionManager,
|
|
395
|
-
sessionBroker:
|
|
388
|
+
sessionBroker: uiServices.sessions.sessionBroker,
|
|
396
389
|
sessionOrchestration: ctx.services.sessionOrchestration,
|
|
397
390
|
sessionMemoryStore: ctx.services.sessionMemoryStore,
|
|
398
391
|
},
|
|
@@ -477,7 +470,7 @@ async function main() {
|
|
|
477
470
|
.slice(0, promptInfo.visibleCursorLine)
|
|
478
471
|
.reduce((sum: number, line: string) => sum + line.length + 1, 0) + promptInfo.visibleCursorCol
|
|
479
472
|
: undefined,
|
|
480
|
-
usage: { up: orchestrator.usage.input, down: orchestrator.usage.output },
|
|
473
|
+
usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
|
|
481
474
|
showExitNotice: input.showExitNotice,
|
|
482
475
|
lastCopyTime: input.lastCopyTime,
|
|
483
476
|
model: runtime.model,
|
|
@@ -504,8 +497,8 @@ async function main() {
|
|
|
504
497
|
lastInputTokens: orchestrator.lastInputTokens,
|
|
505
498
|
commandArgsHint,
|
|
506
499
|
hitlMode: modeManager.getHITLMode(),
|
|
507
|
-
|
|
508
|
-
runningProcessCount,
|
|
500
|
+
// S3d: cross-surface spine posture segment (adopted-daemon mode only).
|
|
501
|
+
sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
|
|
509
502
|
// Composer must not read as focused while the panel/process indicator owns keyboard focus.
|
|
510
503
|
promptFocused: !input.panelFocused && !input.indicatorFocused,
|
|
511
504
|
indicatorFocused: input.indicatorFocused,
|
|
@@ -552,7 +545,7 @@ async function main() {
|
|
|
552
545
|
// Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
|
|
553
546
|
let overlayRows = 0;
|
|
554
547
|
if (orchestrator.isThinking) overlayRows += 2; // spinner + blank
|
|
555
|
-
if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState);
|
|
548
|
+
if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded);
|
|
556
549
|
overlayRows += orchestrator.messageQueue.length * 3; // queued messages
|
|
557
550
|
// File picker and model picker overlay rows computed from actual rendered line count below
|
|
558
551
|
// Selection modal overlay rows are computed from actual rendered line count below
|
|
@@ -591,6 +584,7 @@ async function main() {
|
|
|
591
584
|
turnElapsedMs,
|
|
592
585
|
streamMetrics.ttftMs,
|
|
593
586
|
stallInfo,
|
|
587
|
+
pendingPermission !== null,
|
|
594
588
|
);
|
|
595
589
|
viewport.push(...thinking);
|
|
596
590
|
// Live tool timer: render the currently executing tool row with ticking elapsed.
|
|
@@ -601,7 +595,7 @@ async function main() {
|
|
|
601
595
|
}
|
|
602
596
|
|
|
603
597
|
if (pendingPermission) {
|
|
604
|
-
viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState));
|
|
598
|
+
viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded));
|
|
605
599
|
}
|
|
606
600
|
|
|
607
601
|
orchestrator.messageQueue.forEach(msg => {
|
|
@@ -648,8 +642,8 @@ async function main() {
|
|
|
648
642
|
});
|
|
649
643
|
};
|
|
650
644
|
const renderScheduler = createRenderScheduler(renderNow); // WO-208 same-tick coalescer
|
|
651
|
-
const render = (): void => renderScheduler.schedule();
|
|
652
|
-
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr,
|
|
645
|
+
const render = (): void => renderScheduler.schedule(); // captured direct writes → activity log + quiet /debug counter, not repeated transcript lines (UX-B 1a)
|
|
646
|
+
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
|
|
653
647
|
|
|
654
648
|
setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
|
|
655
649
|
setPanelFrameRequester(render); // live panels repaint when idle (Wave-6 replay: fleet sat stale until keypress)
|
|
@@ -711,6 +705,7 @@ async function main() {
|
|
|
711
705
|
events: uiServices.events, orchestrator, providerRegistry,
|
|
712
706
|
systemMessageRouter, render, metrics: streamMetrics,
|
|
713
707
|
providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn,
|
|
708
|
+
isApprovalPending: () => pendingPermission !== null,
|
|
714
709
|
});
|
|
715
710
|
unsubs.push(...streamResult.unsubs);
|
|
716
711
|
// Activate one-key retry affordance when a user-visible error surfaces.
|
|
@@ -734,15 +729,18 @@ async function main() {
|
|
|
734
729
|
stdin.resume();
|
|
735
730
|
stdin.setEncoding('utf8');
|
|
736
731
|
allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE + FOCUS_ENABLE));
|
|
732
|
+
// DEBT-2: forced dark/light applies before first paint; auto (TTY only) fires the
|
|
733
|
+
// OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
|
|
734
|
+
const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
|
|
737
735
|
|
|
738
736
|
applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, render });
|
|
739
737
|
|
|
740
|
-
stdin.on('data', (
|
|
738
|
+
stdin.on('data', (raw: string) => {
|
|
739
|
+
const data = themeProbe.filterInput(raw); if (data.length === 0) return;
|
|
741
740
|
const blocking = handleBlockingShellInput({
|
|
742
741
|
data, pendingPermission, recoveryPending, conversation, systemMessageRouter, render,
|
|
743
742
|
abortTurn: () => orchestrator.abort(),
|
|
744
|
-
|
|
745
|
-
deleteRecoveryFile: () => deleteRecoveryFile({ homeDirectory }),
|
|
743
|
+
...createRecoveryFileOps(() => recoverySource, { homeDirectory }),
|
|
746
744
|
homeDirectory, sessionId: runtime.sessionId,
|
|
747
745
|
persistSnapshot: createPersistRecoverySnapshot({ sessionManager: ctx.services.sessionManager, runtime, conversation }),
|
|
748
746
|
reopenPanels: createReopenRecoveryPanels({ panelManager, render }),
|
|
@@ -769,9 +767,10 @@ async function main() {
|
|
|
769
767
|
conversation.rebuildHistory();
|
|
770
768
|
render();
|
|
771
769
|
|
|
772
|
-
// --- Crash recovery check ---
|
|
773
|
-
const recoveryInfo =
|
|
770
|
+
// --- Crash recovery check (also checks the preserve-on-dismiss sibling; see recovery-input-helpers.ts) ---
|
|
771
|
+
const recoveryInfo = resolveStartupRecoveryInfo({ workingDirectory: workingDir, homeDirectory });
|
|
774
772
|
if (recoveryInfo) {
|
|
773
|
+
recoverySource = recoveryInfo.source;
|
|
775
774
|
systemMessageRouter.high(`[Recovery] Found unsaved session from ${new Date(recoveryInfo.timestamp).toLocaleString()}. Title: "${recoveryInfo.title}". Press Ctrl+R to restore, Esc to discard, or start typing to ignore it.`);
|
|
776
775
|
for (const line of formatReturnContextForDisplay(recoveryInfo.returnContext)) {
|
|
777
776
|
systemMessageRouter.low(`[Recovery] ${line}`);
|
|
@@ -782,6 +781,8 @@ async function main() {
|
|
|
782
781
|
|
|
783
782
|
// --- Auto-save to recovery file every 60s ---
|
|
784
783
|
recoveryInterval = setInterval(() => {
|
|
784
|
+
// Skip while an earlier recovery prompt is unresolved (avoids racing preserve-on-dismiss).
|
|
785
|
+
if (recoveryPending) return;
|
|
785
786
|
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot> };
|
|
786
787
|
const persisted = buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints());
|
|
787
788
|
writeRecoveryFile(
|
|
@@ -4,7 +4,8 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
|
4
4
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
5
|
import type { Orchestrator } from '../../core/orchestrator';
|
|
6
6
|
import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
7
|
-
import type { ApprovalBroker
|
|
7
|
+
import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
8
|
+
import type { SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
8
9
|
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
9
10
|
import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
10
11
|
import type { UiRuntimeServices } from '../../runtime/ui-services.ts';
|
|
@@ -84,8 +85,13 @@ export interface BuiltinPanelDeps {
|
|
|
84
85
|
policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
|
|
85
86
|
/** Approval broker for control-plane/operator panels. */
|
|
86
87
|
approvalBroker?: ApprovalBroker;
|
|
87
|
-
/**
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* S3d: cross-surface session READ facade for control-plane/operator panels.
|
|
90
|
+
* Sync listSessions()/getSession() shape preserved; in adopted-daemon mode it
|
|
91
|
+
* serves the daemon-hosted union (with an honest offline note when the wire is
|
|
92
|
+
* down) instead of only this process's local broker. See session-union-cache.ts.
|
|
93
|
+
*/
|
|
94
|
+
sessionBroker?: SessionReadFacade;
|
|
89
95
|
/** Automation manager for schedule/operator panels. */
|
|
90
96
|
automationManager?: AutomationManager;
|
|
91
97
|
/** Recent control-plane events provider for control-plane/operator panels. */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-deep-link.ts — DEBT-5 item 4.
|
|
3
|
+
//
|
|
4
|
+
// PanelManager.open('fleet', pane, target) hands FleetPanel a
|
|
5
|
+
// PanelDeepLinkTarget (a process id + optional ProcessKind) so a cross-panel
|
|
6
|
+
// jump (the work-plan modal's 'i'/'w' agent/WRFC-chain jumps, the docs
|
|
7
|
+
// modal's tools-tab Enter) can select + reveal a SPECIFIC node instead of
|
|
8
|
+
// just opening Fleet generically. Extracted from fleet-panel.ts (already at
|
|
9
|
+
// the 800-line architecture cap) so the panel stays a thin caller.
|
|
10
|
+
//
|
|
11
|
+
// `kind`, when given, must also match: ProcessNode ids are unique per the
|
|
12
|
+
// SDK's own contract, so this is a cheap extra honesty check, not the
|
|
13
|
+
// primary lookup key. Reveal is "free" once selectedIndex is set —
|
|
14
|
+
// ScrollableListPanel's render-time scroll-window resolution keeps the
|
|
15
|
+
// selected row visible, so FleetPanel.receiveDeepLink does not need its own
|
|
16
|
+
// scroll math.
|
|
17
|
+
//
|
|
18
|
+
// A target that isn't found (pruned, killed, never existed, or a domain —
|
|
19
|
+
// like the docs Tools tab's static tool definitions — that has no
|
|
20
|
+
// corresponding fleet node at all) resolves to -1; the caller degrades
|
|
21
|
+
// honestly (FleetPanel.setError('node no longer present.')) rather than
|
|
22
|
+
// silently landing on an unrelated row.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
import type { PanelDeepLinkTarget } from './types.ts';
|
|
26
|
+
import type { FleetTreeRow } from './fleet-read-model.ts';
|
|
27
|
+
|
|
28
|
+
/** The row index matching `target`'s id (+ kind, if given), or -1. Pure — no I/O, no mutation. */
|
|
29
|
+
export function resolveFleetDeepLinkIndex(rows: readonly FleetTreeRow[], target: PanelDeepLinkTarget): number {
|
|
30
|
+
return rows.findIndex((row) => row.node.id === target.id && (!target.kind || row.node.kind === target.kind));
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-panel-format.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure column-layout + cell-formatting helpers for FleetPanel's tree rows.
|
|
5
|
+
// Extracted from fleet-panel.ts (S3b hygiene) to hold that file under the
|
|
6
|
+
// 800-line architecture cap; these are stateless functions the panel's
|
|
7
|
+
// renderItem/renderDetail call, with no `this` dependency.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import type { ProcessCostState, ProcessUsage } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
11
|
+
import { formatAgentCost } from './agent-inspector-shared.ts';
|
|
12
|
+
import { DEFAULT_PANEL_PALETTE, type ColumnSpec, type PanelPalette } from './polish.ts';
|
|
13
|
+
import { fleetUsageTokens, hasFleetCost, type FleetStateTone } from './fleet-read-model.ts';
|
|
14
|
+
|
|
15
|
+
// Column widths for the tree row layout. `label` absorbs whatever width is
|
|
16
|
+
// left over after the fixed columns + gaps; on hostile (narrow) widths the
|
|
17
|
+
// trailing columns simply clip (buildAlignedRow/buildSelectablePanelLine stop
|
|
18
|
+
// writing once a cell would overflow the row) rather than throwing or
|
|
19
|
+
// wrapping — the tree stays readable, just denser.
|
|
20
|
+
const KIND_W = 8;
|
|
21
|
+
const ELAPSED_W = 7;
|
|
22
|
+
const TOKENS_W = 7;
|
|
23
|
+
const COST_W = 8;
|
|
24
|
+
const ACTIVITY_W = 20;
|
|
25
|
+
const GAP = 1;
|
|
26
|
+
const FIXED_W = 1 /* glyph */ + KIND_W + ELAPSED_W + TOKENS_W + COST_W + ACTIVITY_W + GAP * 6;
|
|
27
|
+
|
|
28
|
+
export function planColumns(width: number): ColumnSpec[] {
|
|
29
|
+
const labelWidth = Math.max(10, width - FIXED_W);
|
|
30
|
+
return [
|
|
31
|
+
{ width: 1 },
|
|
32
|
+
{ width: KIND_W },
|
|
33
|
+
{ width: labelWidth },
|
|
34
|
+
{ width: ELAPSED_W, align: 'right' },
|
|
35
|
+
{ width: TOKENS_W, align: 'right' },
|
|
36
|
+
{ width: COST_W, align: 'right' },
|
|
37
|
+
{ width: ACTIVITY_W },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
|
|
42
|
+
switch (tone) {
|
|
43
|
+
case 'active': return palette.info ?? DEFAULT_PANEL_PALETTE.info;
|
|
44
|
+
case 'success': return palette.good ?? DEFAULT_PANEL_PALETTE.good;
|
|
45
|
+
case 'failure': return palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
46
|
+
case 'warn': return palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
|
|
47
|
+
case 'muted': return palette.dim;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatFleetTokens(usage: ProcessUsage | undefined): string {
|
|
52
|
+
const total = fleetUsageTokens(usage);
|
|
53
|
+
if (total === null) return 'n/a';
|
|
54
|
+
return total >= 1000 ? `${(total / 1000).toFixed(1)}k` : String(total);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Honest cost display: never a fabricated $0.00 — 'unpriced' when costState says so. */
|
|
58
|
+
export function formatFleetCost(costUsd: number | null | undefined, costState: ProcessCostState): string {
|
|
59
|
+
if (!hasFleetCost(costUsd, costState)) return 'unpriced';
|
|
60
|
+
const formatted = formatAgentCost(costUsd as number);
|
|
61
|
+
return costState === 'estimated' ? `~${formatted}` : formatted;
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-panel-worktree-detail.ts
|
|
3
|
+
//
|
|
4
|
+
// NET-NEW helper (not moved code — it was authored here because fleet-panel.ts
|
|
5
|
+
// sits at the architecture check's 800-line cap, scripts/check-architecture.ts):
|
|
6
|
+
// a single pure helper for the Fleet panel's per-work-item detail view, so a
|
|
7
|
+
// new panel feature doesn't have to fight that cap inline.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import type { WorkItem } from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-item worktree-isolation detail text for the Fleet panel's expanded
|
|
14
|
+
* node detail (fleet-panel.ts renderDetail), or null for a shared-isolation
|
|
15
|
+
* item (or any node that isn't a work item). Inferred from the item's OWN
|
|
16
|
+
* fields (worktreePath/worktreeBranch/mergeState) rather than a
|
|
17
|
+
* workstream-level flag — adaptWorkItem's ProcessNode.raw carries
|
|
18
|
+
* `{ item, workstreamId }`, not the full Workstream, and these fields are
|
|
19
|
+
* set ONLY when the owning workstream's isolation is 'worktree'
|
|
20
|
+
* (engine.ts/worktree-isolation.ts, SDK), so they are a reliable
|
|
21
|
+
* self-contained signal with no fleet-adapter change needed.
|
|
22
|
+
*/
|
|
23
|
+
export function formatWorkItemIsolationDetail(item: WorkItem): string | null {
|
|
24
|
+
const isolated = item.worktreePath !== undefined || item.worktreeBranch !== undefined || item.mergeState !== undefined;
|
|
25
|
+
if (!isolated) return null;
|
|
26
|
+
switch (item.mergeState) {
|
|
27
|
+
case 'merged':
|
|
28
|
+
// Papercut sweep item 3: worktree-isolation items whose integration had
|
|
29
|
+
// no changes to merge (mergeState 'merged', mergeHash undefined — a
|
|
30
|
+
// documented cosmetic case, not an error) used to read as a bare/awkward
|
|
31
|
+
// "merged (nothing to merge)". "merged (no changes)" says the same thing
|
|
32
|
+
// in the same shape as the other merge-state phrases below.
|
|
33
|
+
return item.mergeHash ? `worktree — merged ${item.mergeHash.slice(0, 12)}` : 'worktree — merged (no changes)';
|
|
34
|
+
case 'conflict':
|
|
35
|
+
return 'worktree — merge-conflict (kept for inspection)';
|
|
36
|
+
case 'pending':
|
|
37
|
+
return 'worktree — merge pending';
|
|
38
|
+
case 'n-a':
|
|
39
|
+
default:
|
|
40
|
+
return item.worktreeKept ? 'worktree — kept' : 'worktree — not yet integrated';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `node.raw` for a 'work-item' ProcessNode is `{ item, workstreamId }` (unknown to the panel's own types) — this narrows without fleet-panel.ts needing its own WorkItem import at the call site. Non-work-item nodes (raw is undefined/some other shape) fall through to null. */
|
|
45
|
+
export function formatWorkItemIsolationDetailFromRaw(raw: unknown): string | null {
|
|
46
|
+
const item = (raw as { item?: WorkItem } | undefined)?.item;
|
|
47
|
+
return item ? formatWorkItemIsolationDetail(item) : null;
|
|
48
|
+
}
|