@pellux/goodvibes-tui 1.0.0 → 1.7.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 +130 -127
- package/README.md +32 -14
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +58 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +126 -8
- package/src/input/command-registry.ts +54 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +12 -0
- package/src/input/commands/session-workflow.ts +19 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-content-actions.ts +22 -8
- package/src/input/handler-feed-routes.ts +107 -7
- package/src/input/handler-feed.ts +49 -13
- package/src/input/handler-interactions.ts +3 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +3 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +10 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +41 -44
- package/src/panels/agent-inspector-shared.ts +35 -11
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +16 -62
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +63 -14
- package/src/panels/diff-panel.ts +123 -60
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/git-panel.ts +9 -9
- package/src/panels/index.ts +7 -31
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +26 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/turn-injection.ts +114 -0
- package/src/renderer/ui-factory.ts +91 -7
- package/src/runtime/bootstrap-command-context.ts +24 -1
- package/src/runtime/bootstrap-command-parts.ts +36 -7
- package/src/runtime/bootstrap-core.ts +19 -4
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +41 -17
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -233
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -609
- package/src/panels/docs-panel.ts +0 -375
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -664
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -506
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -557
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -721
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -306
- package/src/panels/provider-health-views.ts +0 -560
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -487
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -530
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -465
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -656
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn knowledge injection record rendering (Wave-5 W5.2, wo803).
|
|
3
|
+
*
|
|
4
|
+
* The SDK's passive per-turn retrieval engine (wo801, W5.1 —
|
|
5
|
+
* packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
|
|
6
|
+
* `TurnInjectionRecord` per turn on `AgentRecord.turnInjections` (a bounded
|
|
7
|
+
* ring, default 20 entries) and appends the same record to the agent's
|
|
8
|
+
* session transcript as `{type:'knowledge_injection', turn, ...record}`.
|
|
9
|
+
*
|
|
10
|
+
* `TurnInjectionRecord` itself is not re-exported from the SDK's
|
|
11
|
+
* `platform/agents` barrel (only `AgentRecord.turnInjections` carries its
|
|
12
|
+
* structural shape through the already-public `platform/tools` barrel), so
|
|
13
|
+
* the entry type here is DERIVED from `AgentRecord` rather than imported by
|
|
14
|
+
* name — this needs no SDK export change.
|
|
15
|
+
*
|
|
16
|
+
* Reality check (updated wo805): as of Wave-5 wo805 the TUI's main interactive
|
|
17
|
+
* session DOES route through this engine. The SDK `Orchestrator` runs per-turn
|
|
18
|
+
* passive injection on the evolving primary conversation and records each turn
|
|
19
|
+
* on its own bounded ring, exposed via `Orchestrator.getTurnInjections()` — the
|
|
20
|
+
* main-session counterpart to `AgentRecord.turnInjections` (there is no
|
|
21
|
+
* AgentRecord for the primary conversation). `buildMainSessionTurnInjectionsText`
|
|
22
|
+
* renders that ring for `/recall injections` with no agent id. Spawned agents
|
|
23
|
+
* (Task-tool runs, automation-triggered runs, session continuations) still
|
|
24
|
+
* record onto their own `AgentRecord.turnInjections`, rendered per-agent by
|
|
25
|
+
* `buildTurnInjectionsText` when an explicit agent id is given.
|
|
26
|
+
*/
|
|
27
|
+
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
28
|
+
|
|
29
|
+
/** One TurnInjectionRecord entry, derived from AgentRecord (see module doc). */
|
|
30
|
+
export type TurnInjectionEntry = NonNullable<AgentRecord['turnInjections']>[number];
|
|
31
|
+
|
|
32
|
+
function fmtN(n: number): string {
|
|
33
|
+
return n.toLocaleString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Render a single TurnInjectionRecord as one readable line. */
|
|
37
|
+
export function formatTurnInjectionEntry(entry: TurnInjectionEntry): string {
|
|
38
|
+
const backendTag = entry.embeddingBackend === 'fallback-lexical' ? ' [lexical fallback]' : '';
|
|
39
|
+
if (entry.injectedIds.length === 0) {
|
|
40
|
+
// Honest empty state: the engine ran this turn but injected nothing —
|
|
41
|
+
// distinct from "the engine never ran" (see buildTurnInjectionsText).
|
|
42
|
+
const reasonText = entry.reason === 'no records cleared relevance floor'
|
|
43
|
+
? 'nothing injected this turn — nothing cleared the relevance floor'
|
|
44
|
+
: `nothing injected this turn — ${entry.reason ?? 'unknown reason'}`;
|
|
45
|
+
return ` turn ${entry.turn}: ${reasonText}${backendTag} (considered ${entry.candidatesConsidered}, floor ${entry.relevanceFloor})`;
|
|
46
|
+
}
|
|
47
|
+
const droppedStr = entry.droppedForBudget.length > 0
|
|
48
|
+
? `, dropped for budget: ${entry.droppedForBudget.join(', ')}`
|
|
49
|
+
: '';
|
|
50
|
+
// The retrieval query is part of the record's honesty contract — without it
|
|
51
|
+
// an injected line can't be traced back to WHY those ids were retrieved
|
|
52
|
+
// (Wave-5 replay flagged the omission). Truncated to keep the line scannable.
|
|
53
|
+
const queryStr = entry.query ? ` for ${JSON.stringify(truncateQuery(entry.query))}` : '';
|
|
54
|
+
return (
|
|
55
|
+
` turn ${entry.turn}: injected ${entry.injectedIds.join(', ')}${queryStr} ` +
|
|
56
|
+
`(~${fmtN(entry.tokenCost)}/${fmtN(entry.budgetTokens)} tok, floor ${entry.relevanceFloor})${droppedStr}${backendTag}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function truncateQuery(query: string): string {
|
|
61
|
+
const flat = query.replace(/\s+/g, ' ').trim();
|
|
62
|
+
return flat.length > 48 ? `${flat.slice(0, 47)}…` : flat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render the full per-turn injection history for one agent, most-recent-turn
|
|
67
|
+
* first, with an honest empty state when nothing has been recorded yet.
|
|
68
|
+
*
|
|
69
|
+
* An empty `entries` array is deliberately ambiguous about WHY it's empty
|
|
70
|
+
* (flag disabled, no turn with new input has run yet, or every turn's token
|
|
71
|
+
* budget had no headroom) — the SDK does not distinguish these cases in the
|
|
72
|
+
* ring itself, so this renders all three possibilities rather than guessing.
|
|
73
|
+
*/
|
|
74
|
+
export function buildTurnInjectionsText(agentId: string, entries: readonly TurnInjectionEntry[]): string {
|
|
75
|
+
if (entries.length === 0) {
|
|
76
|
+
return (
|
|
77
|
+
`[recall] No per-turn injection records for agent ${agentId} yet. This means one of: ` +
|
|
78
|
+
'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
|
|
79
|
+
'token budget had no headroom on every turn so far — there is no record either way.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const lines = [`[recall] Per-turn knowledge injections for agent ${agentId} (${entries.length}, most recent first):`];
|
|
83
|
+
for (const entry of [...entries].reverse()) {
|
|
84
|
+
lines.push(formatTurnInjectionEntry(entry));
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render the MAIN interactive session's per-turn injection ring
|
|
91
|
+
* (`Orchestrator.getTurnInjections()`, wo805), most-recent-turn first, with the
|
|
92
|
+
* same honest empty state as the per-agent path. The main-session counterpart
|
|
93
|
+
* to {@link buildTurnInjectionsText}: no agent id, main-session-appropriate
|
|
94
|
+
* wording, but the identical per-entry rendering (`formatTurnInjectionEntry`).
|
|
95
|
+
*
|
|
96
|
+
* As with the agent path, an empty `entries` array is deliberately ambiguous
|
|
97
|
+
* about WHY it's empty (flag disabled, no turn with new input has run yet, or
|
|
98
|
+
* every turn's token budget had no headroom) — the ring does not distinguish
|
|
99
|
+
* these, so this renders all three possibilities rather than guessing.
|
|
100
|
+
*/
|
|
101
|
+
export function buildMainSessionTurnInjectionsText(entries: readonly TurnInjectionEntry[]): string {
|
|
102
|
+
if (entries.length === 0) {
|
|
103
|
+
return (
|
|
104
|
+
'[recall] No per-turn injection records for the main session yet. This means one of: ' +
|
|
105
|
+
'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
|
|
106
|
+
'token budget had no headroom on every turn so far — there is no record either way.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const lines = [`[recall] Per-turn knowledge injections for the main session (${entries.length}, most recent first):`];
|
|
110
|
+
for (const entry of [...entries].reverse()) {
|
|
111
|
+
lines.push(formatTurnInjectionEntry(entry));
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
@@ -8,13 +8,36 @@ import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
|
8
8
|
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
9
9
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
10
10
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
11
|
-
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
11
|
+
import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
|
|
12
12
|
import { buildFooterTip, isAgentActive } from './footer-tips.ts';
|
|
13
|
+
import type { StreamMetrics } from '../core/stream-event-wiring.ts';
|
|
13
14
|
|
|
14
15
|
/** Number of frames before the animated gradient completes one full cycle. */
|
|
15
16
|
const GRADIENT_CYCLE_FRAMES = 50;
|
|
16
17
|
/** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
|
|
17
18
|
const PHRASE_ROTATION_FRAMES = 375;
|
|
19
|
+
/**
|
|
20
|
+
* Ms since the last STREAM_DELTA before the whimsical phrase rotation freezes
|
|
21
|
+
* and createThinkingFragment shows an honest "stalled Ns" / "reconnecting"
|
|
22
|
+
* label instead. Deliberately much shorter than the 30s stream-stall-watchdog
|
|
23
|
+
* hint threshold (stream-stall-watchdog.ts) — that threshold gates a
|
|
24
|
+
* low-priority system message about a likely-dead connection; this one gates
|
|
25
|
+
* a cosmetic label so the UI stops claiming "Vibing..." within a couple of
|
|
26
|
+
* seconds of real silence, well before the stall is confirmed as a problem.
|
|
27
|
+
*/
|
|
28
|
+
const THINKING_STALL_FREEZE_MS = 2_500;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stall/reconnect state for the live thinking indicator, computed by the
|
|
32
|
+
* caller every render frame from streamMetrics (see stream-event-wiring.ts).
|
|
33
|
+
* `reconnect` is populated only once the SDK's STREAM_RETRY event fires
|
|
34
|
+
* (structurally consumed — absent from SDK 0.35.0's TurnEvent union today).
|
|
35
|
+
*/
|
|
36
|
+
export interface ThinkingStallInfo {
|
|
37
|
+
/** Ms since the last STREAM_DELTA (or STREAM_START if none yet this turn). */
|
|
38
|
+
readonly msSinceLastDelta: number;
|
|
39
|
+
readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
/** Build the git segment string and its display width. Single source of truth for header layout. */
|
|
20
43
|
function buildGitSegment(gitInfo: GitHeaderInfo): { text: string; width: number } {
|
|
@@ -313,7 +336,15 @@ export class UIFactory {
|
|
|
313
336
|
const cw = u.cacheWrite ?? 0;
|
|
314
337
|
const total = inp + out + cr + cw;
|
|
315
338
|
const tokenSep = ` ${GLYPHS.navigation.pipeSeparator} `;
|
|
316
|
-
|
|
339
|
+
// 'n/a' (not 'unpriced') to stay compact in the single-line footer and
|
|
340
|
+
// match the existing "no priceable data" convention used elsewhere
|
|
341
|
+
// (cockpit-panel formatCost, agent-inspector-shared) — the footer has no
|
|
342
|
+
// room for a longer marker before truncation kicks in.
|
|
343
|
+
const costSegment = model
|
|
344
|
+
? isModelPriced(model)
|
|
345
|
+
? `${tokenSep}~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
|
|
346
|
+
: `${tokenSep}~n/a`
|
|
347
|
+
: '';
|
|
317
348
|
const tokenLine = ` Token Usage [ Input: ${fmtNum(inp)}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
|
|
318
349
|
const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
|
|
319
350
|
const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
|
|
@@ -345,7 +376,11 @@ export class UIFactory {
|
|
|
345
376
|
ctxParts.push(model + (provider ? ` (${provider})` : ''));
|
|
346
377
|
}
|
|
347
378
|
if (toolCount) ctxParts.push(`${toolCount} tools`);
|
|
348
|
-
|
|
379
|
+
// Labeled "notify" (not "hitl") — /mode (aliased /hitl) governs UX
|
|
380
|
+
// notification verbosity (quiet/balanced/operator), not tool
|
|
381
|
+
// auto-approval, so it must not share vocabulary with the DANGER MODE
|
|
382
|
+
// risk banner rendered a few lines below.
|
|
383
|
+
if (hitlMode) ctxParts.push(`notify:${hitlMode}`);
|
|
349
384
|
const ctxLine = ' ' + ctxParts.join(` ${GLYPHS.navigation.pipeSeparator} `);
|
|
350
385
|
lines.push(this.stringToLine(truncateDisplay(ctxLine, width), width, { fg: '240', dim: true }));
|
|
351
386
|
}
|
|
@@ -406,10 +441,59 @@ export class UIFactory {
|
|
|
406
441
|
private static readonly THINK_GRADIENT_START = UI_TONES.accent.gradientStart;
|
|
407
442
|
private static readonly THINK_GRADIENT_END = UI_TONES.accent.gradientEnd;
|
|
408
443
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Per-frame stall info from stream metrics — computed from lastDeltaAtMs every render (not
|
|
446
|
+
* from any event) so it degrades gracefully with zero new SDK events. Undefined until the
|
|
447
|
+
* first delta clock exists this turn.
|
|
448
|
+
*/
|
|
449
|
+
public static computeStallInfo(lastDeltaAtMs: number | undefined, reconnectAttempt: number | undefined, reconnectMaxAttempts: number | undefined, nowMs: number): ThinkingStallInfo | undefined {
|
|
450
|
+
if (lastDeltaAtMs === undefined) return undefined;
|
|
451
|
+
const reconnect = reconnectAttempt !== undefined && reconnectMaxAttempts !== undefined
|
|
452
|
+
? { attempt: reconnectAttempt, maxAttempts: reconnectMaxAttempts }
|
|
453
|
+
: undefined;
|
|
454
|
+
return { msSinceLastDelta: nowMs - lastDeltaAtMs, reconnect };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Render-frame stall-info decision used at the main render loop's call
|
|
459
|
+
* site: suppress stall detection entirely while a tool is actively
|
|
460
|
+
* executing. lastDeltaAtMs only tracks STREAM_START/STREAM_DELTA and is
|
|
461
|
+
* never advanced during tool execution (the model isn't producing tokens
|
|
462
|
+
* then), so without this gate any tool call longer than
|
|
463
|
+
* THINKING_STALL_FREEZE_MS would make the thinking fragment print
|
|
464
|
+
* "Stalled Ns..." directly above the ticking "executing (Ns)" tool row — a
|
|
465
|
+
* false positive during ordinary tool execution (see stream-event-wiring.ts
|
|
466
|
+
* TOOL_EXECUTING/TOOL_SUCCEEDED/TOOL_FAILED/TOOL_CANCELLED handlers).
|
|
467
|
+
* Genuine no-delta silence while waiting on the provider — including the
|
|
468
|
+
* pre-first-token case, where lastDeltaAtMs is seeded at STREAM_START —
|
|
469
|
+
* still stall-detects normally here, since no tool is active then; that is
|
|
470
|
+
* the honest stall case this indicator exists for.
|
|
471
|
+
*/
|
|
472
|
+
public static computeRenderStallInfo(
|
|
473
|
+
metrics: Pick<StreamMetrics, 'activeToolName' | 'lastDeltaAtMs' | 'reconnectAttempt' | 'reconnectMaxAttempts'>,
|
|
474
|
+
nowMs: number,
|
|
475
|
+
): ThinkingStallInfo | undefined {
|
|
476
|
+
return metrics.activeToolName === undefined
|
|
477
|
+
? this.computeStallInfo(metrics.lastDeltaAtMs, metrics.reconnectAttempt, metrics.reconnectMaxAttempts, nowMs)
|
|
478
|
+
: undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number, stallInfo?: ThinkingStallInfo): Line[] {
|
|
482
|
+
// Freeze the whimsical phrase rotation once real silence has gone on
|
|
483
|
+
// long enough to be misleading (THINKING_STALL_FREEZE_MS), and show an
|
|
484
|
+
// honest label instead: the SDK's reconnect attempt/maxAttempts once
|
|
485
|
+
// STREAM_RETRY is available, else a plain elapsed-silence readout.
|
|
486
|
+
const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
|
|
487
|
+
let phrase: string;
|
|
488
|
+
if (stallInfo?.reconnect) {
|
|
489
|
+
phrase = `Reconnecting (attempt ${stallInfo.reconnect.attempt}/${stallInfo.reconnect.maxAttempts})...`;
|
|
490
|
+
} else if (isStalled) {
|
|
491
|
+
phrase = `Stalled ${Math.floor(stallInfo.msSinceLastDelta / 1000)}s...`;
|
|
492
|
+
} else {
|
|
493
|
+
// Rotate phrase every ~30 seconds (frame ticks at 80ms, so ~375 frames)
|
|
494
|
+
const phraseIndex = Math.floor(frame / PHRASE_ROTATION_FRAMES) % this.THINKING_PHRASES.length;
|
|
495
|
+
phrase = this.THINKING_PHRASES[phraseIndex];
|
|
496
|
+
}
|
|
413
497
|
const speedSuffix = (tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
|
|
414
498
|
const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
|
|
415
499
|
const ttftSuffix = (ttftMs !== undefined && ttftMs > 0) ? ` ttft:${ttftMs}ms` : '';
|
|
@@ -16,6 +16,7 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
|
16
16
|
import type { ForensicsRegistry } from '@/runtime/index.ts';
|
|
17
17
|
import type { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
18
18
|
import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
19
|
+
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
19
20
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
20
21
|
import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
21
22
|
import type { IntegrationHelperService } from '@/runtime/index.ts';
|
|
@@ -61,6 +62,10 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
61
62
|
mcpRegistry: McpRegistry;
|
|
62
63
|
voiceProviderRegistry?: VoiceProviderRegistry;
|
|
63
64
|
voiceService?: VoiceService;
|
|
65
|
+
/** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
|
|
66
|
+
webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
|
|
67
|
+
mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
|
|
68
|
+
artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
|
|
64
69
|
forensicsRegistry: ForensicsRegistry;
|
|
65
70
|
policyRuntimeState: PolicyRuntimeState;
|
|
66
71
|
readModels: UiReadModels;
|
|
@@ -68,6 +73,7 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
68
73
|
remoteRuntime?: RemoteCommandService;
|
|
69
74
|
planRuntime?: PlanRuntimeService;
|
|
70
75
|
fileUndoManager: FileUndoManager;
|
|
76
|
+
workspaceCheckpointManager?: WorkspaceCheckpointManager;
|
|
71
77
|
memoryRegistry?: MemoryRegistry;
|
|
72
78
|
integrationHelpers?: IntegrationHelperService;
|
|
73
79
|
automationManager?: ShellAutomationManagerRuntimeService;
|
|
@@ -114,6 +120,10 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
114
120
|
sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
115
121
|
wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
116
122
|
componentHealthMonitor: import('@/runtime/index.ts').ComponentHealthMonitor;
|
|
123
|
+
hydrateSessionUsage?: () => void;
|
|
124
|
+
workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
|
|
125
|
+
codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
|
|
126
|
+
getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
|
|
117
127
|
};
|
|
118
128
|
|
|
119
129
|
export function createBootstrapCommandContext(
|
|
@@ -131,6 +141,9 @@ export function createBootstrapCommandContext(
|
|
|
131
141
|
mcpRegistry,
|
|
132
142
|
voiceProviderRegistry,
|
|
133
143
|
voiceService,
|
|
144
|
+
webSearchService,
|
|
145
|
+
mediaProviders,
|
|
146
|
+
artifactStore,
|
|
134
147
|
forensicsRegistry,
|
|
135
148
|
policyRuntimeState,
|
|
136
149
|
readModels,
|
|
@@ -138,6 +151,7 @@ export function createBootstrapCommandContext(
|
|
|
138
151
|
remoteRuntime,
|
|
139
152
|
planRuntime,
|
|
140
153
|
fileUndoManager,
|
|
154
|
+
workspaceCheckpointManager,
|
|
141
155
|
memoryRegistry,
|
|
142
156
|
integrationHelpers,
|
|
143
157
|
automationManager,
|
|
@@ -167,6 +181,10 @@ export function createBootstrapCommandContext(
|
|
|
167
181
|
sessionLineageTracker,
|
|
168
182
|
wrfcController,
|
|
169
183
|
changeTracker,
|
|
184
|
+
hydrateSessionUsage,
|
|
185
|
+
workstreamEngine,
|
|
186
|
+
codeIndexStore,
|
|
187
|
+
getMainSessionTurnInjections,
|
|
170
188
|
planManager,
|
|
171
189
|
adaptivePlanner,
|
|
172
190
|
sessionOrchestration,
|
|
@@ -223,6 +241,10 @@ export function createBootstrapCommandContext(
|
|
|
223
241
|
sessionLineageTracker,
|
|
224
242
|
wrfcController,
|
|
225
243
|
changeTracker,
|
|
244
|
+
hydrateSessionUsage,
|
|
245
|
+
workstreamEngine,
|
|
246
|
+
codeIndexStore,
|
|
247
|
+
getMainSessionTurnInjections,
|
|
226
248
|
});
|
|
227
249
|
const provider = createBootstrapCommandProviderSection({
|
|
228
250
|
providerRegistry,
|
|
@@ -233,6 +255,7 @@ export function createBootstrapCommandContext(
|
|
|
233
255
|
const workspace = createBootstrapCommandWorkspaceSection({
|
|
234
256
|
keybindingsManager,
|
|
235
257
|
fileUndoManager,
|
|
258
|
+
workspaceCheckpointManager,
|
|
236
259
|
panelManager,
|
|
237
260
|
profileManager,
|
|
238
261
|
bookmarkManager,
|
|
@@ -240,7 +263,7 @@ export function createBootstrapCommandContext(
|
|
|
240
263
|
projectPlanningProjectId,
|
|
241
264
|
workPlanStore,
|
|
242
265
|
}, shellServices);
|
|
243
|
-
const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
|
|
266
|
+
const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
|
|
244
267
|
const extensions = createBootstrapCommandExtensionsSection({
|
|
245
268
|
toolRegistry,
|
|
246
269
|
mcpRegistry,
|
|
@@ -16,7 +16,8 @@ import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/pe
|
|
|
16
16
|
import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
17
17
|
import type { ForensicsRegistry } from '@/runtime/index.ts';
|
|
18
18
|
import type { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
19
|
-
import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
19
|
+
import type { CodeIndexStore, FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
20
|
+
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
20
21
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
21
22
|
import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
22
23
|
import type { IntegrationHelperService } from '@/runtime/index.ts';
|
|
@@ -79,11 +80,16 @@ export interface BootstrapCommandSectionOptions {
|
|
|
79
80
|
readonly mcpRegistry: McpRegistry;
|
|
80
81
|
readonly voiceProviderRegistry?: VoiceProviderRegistry;
|
|
81
82
|
readonly voiceService?: VoiceService;
|
|
83
|
+
/** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
|
|
84
|
+
readonly webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
|
|
85
|
+
readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
|
|
86
|
+
readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
|
|
82
87
|
readonly forensicsRegistry: ForensicsRegistry;
|
|
83
88
|
readonly policyRuntimeState: PolicyRuntimeState;
|
|
84
89
|
readonly readModels: UiReadModels;
|
|
85
90
|
readonly shellPaths: ShellPathService;
|
|
86
91
|
readonly fileUndoManager: FileUndoManager;
|
|
92
|
+
readonly workspaceCheckpointManager?: WorkspaceCheckpointManager;
|
|
87
93
|
readonly memoryRegistry?: MemoryRegistry;
|
|
88
94
|
readonly integrationHelpers?: IntegrationHelperService;
|
|
89
95
|
readonly knowledgeService?: KnowledgeService;
|
|
@@ -109,6 +115,10 @@ export interface BootstrapCommandSectionOptions {
|
|
|
109
115
|
readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
110
116
|
readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
111
117
|
readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
|
|
118
|
+
readonly hydrateSessionUsage?: () => void;
|
|
119
|
+
readonly workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
|
|
120
|
+
readonly codeIndexStore?: CodeIndexStore;
|
|
121
|
+
readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
|
|
112
122
|
readonly agentManager?: ShellAgentManagerService;
|
|
113
123
|
readonly modeManager?: ShellModeManagerService;
|
|
114
124
|
readonly automationManager?: ShellAutomationManagerRuntimeService;
|
|
@@ -181,8 +191,15 @@ export function createBootstrapCommandActions(
|
|
|
181
191
|
} = options;
|
|
182
192
|
|
|
183
193
|
const showPanel = (panelId: string, pane?: 'top' | 'bottom') => {
|
|
194
|
+
// W6.1 (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
|
|
195
|
+
// panelManager.open() fires the injected openModal callback and returns a
|
|
196
|
+
// no-op sentinel — so skip panelManager.show() (which would reveal an empty
|
|
197
|
+
// panel workspace behind the modal) when this id redirects. Keeps every
|
|
198
|
+
// showPanel-based front-door (openHooksPanel/openSecurityPanel/… and the
|
|
199
|
+
// migrated command runtimes) opening the modal cleanly.
|
|
200
|
+
const redirected = panelManager.getModalRedirect(panelId) !== undefined;
|
|
184
201
|
panelManager.open(panelId, pane);
|
|
185
|
-
panelManager.show();
|
|
202
|
+
if (!redirected) panelManager.show();
|
|
186
203
|
requestRender();
|
|
187
204
|
};
|
|
188
205
|
|
|
@@ -278,11 +295,15 @@ export function createBootstrapCommandActions(
|
|
|
278
295
|
openMemoryPanel: () => {
|
|
279
296
|
showPanel('memory');
|
|
280
297
|
},
|
|
298
|
+
// W6.1: remote/subscription migrated to config-modal surfaces. open() hits
|
|
299
|
+
// the modal redirect and invokes the openModal callback — do NOT go through
|
|
300
|
+
// showPanel here, which would additionally reveal + focus an (empty) panel
|
|
301
|
+
// workspace behind the fullscreen modal.
|
|
281
302
|
openRemotePanel: () => {
|
|
282
|
-
|
|
303
|
+
panelManager.open('remote');
|
|
283
304
|
},
|
|
284
305
|
openSubscriptionPanel: () => {
|
|
285
|
-
|
|
306
|
+
panelManager.open('subscription');
|
|
286
307
|
},
|
|
287
308
|
openLocalAuthMaskedEntry: (kind, username) => {
|
|
288
309
|
showPanel('local-auth');
|
|
@@ -300,7 +321,7 @@ export function createBootstrapCommandActions(
|
|
|
300
321
|
export function createBootstrapCommandSessionSection(
|
|
301
322
|
options: Pick<
|
|
302
323
|
BootstrapCommandSectionOptions,
|
|
303
|
-
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker'
|
|
324
|
+
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'getMainSessionTurnInjections'
|
|
304
325
|
>,
|
|
305
326
|
): BootstrapCommandSessionSection {
|
|
306
327
|
return {
|
|
@@ -311,6 +332,10 @@ export function createBootstrapCommandSessionSection(
|
|
|
311
332
|
sessionLineageTracker: options.sessionLineageTracker,
|
|
312
333
|
wrfcController: options.wrfcController,
|
|
313
334
|
changeTracker: options.changeTracker,
|
|
335
|
+
hydrateSessionUsage: options.hydrateSessionUsage,
|
|
336
|
+
workstreamEngine: options.workstreamEngine,
|
|
337
|
+
codeIndexStore: options.codeIndexStore,
|
|
338
|
+
getMainSessionTurnInjections: options.getMainSessionTurnInjections,
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
341
|
|
|
@@ -331,7 +356,7 @@ export function createBootstrapCommandProviderSection(
|
|
|
331
356
|
export function createBootstrapCommandWorkspaceSection(
|
|
332
357
|
options: Pick<
|
|
333
358
|
BootstrapCommandSectionOptions,
|
|
334
|
-
'keybindingsManager' | 'fileUndoManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
|
|
359
|
+
'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
|
|
335
360
|
| 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
|
|
336
361
|
>,
|
|
337
362
|
shellServices: BootstrapCommandShellServices,
|
|
@@ -339,6 +364,7 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
339
364
|
return {
|
|
340
365
|
keybindingsManager: options.keybindingsManager,
|
|
341
366
|
fileUndoManager: options.fileUndoManager,
|
|
367
|
+
workspaceCheckpointManager: options.workspaceCheckpointManager,
|
|
342
368
|
panelManager: options.panelManager,
|
|
343
369
|
profileManager: options.profileManager,
|
|
344
370
|
bookmarkManager: options.bookmarkManager,
|
|
@@ -352,7 +378,7 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
352
378
|
export function createBootstrapCommandPlatformSection(
|
|
353
379
|
options: Pick<
|
|
354
380
|
BootstrapCommandSectionOptions,
|
|
355
|
-
'configManager' | 'voiceProviderRegistry' | 'voiceService'
|
|
381
|
+
'configManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
|
|
356
382
|
>,
|
|
357
383
|
shellServices: BootstrapCommandShellServices,
|
|
358
384
|
): BootstrapCommandPlatformSection {
|
|
@@ -361,6 +387,9 @@ export function createBootstrapCommandPlatformSection(
|
|
|
361
387
|
configManager: options.configManager,
|
|
362
388
|
voiceProviderRegistry: options.voiceProviderRegistry,
|
|
363
389
|
voiceService: options.voiceService,
|
|
390
|
+
webSearchService: options.webSearchService,
|
|
391
|
+
mediaProviders: options.mediaProviders,
|
|
392
|
+
artifactStore: options.artifactStore,
|
|
364
393
|
...shellServices.platform,
|
|
365
394
|
};
|
|
366
395
|
}
|
|
@@ -7,7 +7,6 @@ import { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
|
7
7
|
import { registerAllTools } from '@pellux/goodvibes-sdk/platform/tools';
|
|
8
8
|
import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
9
9
|
import { Notifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
10
|
-
import { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
11
10
|
import { Compositor } from '../renderer/compositor.ts';
|
|
12
11
|
import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
13
12
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
@@ -28,11 +27,12 @@ import { loadBootstrapSystemPrompt, syncConfiguredServices } from '@/runtime/ind
|
|
|
28
27
|
import { registerBootstrapHookBridge } from '@/runtime/index.ts';
|
|
29
28
|
import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
|
|
30
29
|
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
30
|
+
import { setPricingSource } from '../export/cost-utils.ts';
|
|
31
31
|
import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
|
|
34
34
|
import { createWrfcPersistence, type WrfcPersistence } from './wrfc-persistence.ts';
|
|
35
|
-
import type { SystemMessagePriority } from '../
|
|
35
|
+
import type { SystemMessagePriority } from '../core/system-message-router.ts';
|
|
36
36
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
// Pre-router buffer
|
|
@@ -255,6 +255,10 @@ export async function initializeBootstrapCore(
|
|
|
255
255
|
providerRegistry.initModelLimits();
|
|
256
256
|
services.benchmarkStore.initBenchmarks();
|
|
257
257
|
providerRegistry.initCatalog();
|
|
258
|
+
// Wire cost-utils to the live catalog so cost displays distinguish real
|
|
259
|
+
// pricing from unpriced (WO-315) instead of silently reading zero for any
|
|
260
|
+
// model the small static fallback table doesn't cover.
|
|
261
|
+
setPricingSource(() => providerRegistry.getRawCatalogModels());
|
|
258
262
|
services.keybindingsManager.loadFromDisk();
|
|
259
263
|
domainDispatch.syncControlPlaneState({
|
|
260
264
|
enabled: Boolean(configManager.get('controlPlane.enabled')),
|
|
@@ -632,10 +636,20 @@ export async function initializeBootstrapCore(
|
|
|
632
636
|
|
|
633
637
|
providerRegistry.startWatching(runtimeBus);
|
|
634
638
|
|
|
639
|
+
// W2.3: attach the SAME WebhookNotifier instance that `/notify add|remove|clear`
|
|
640
|
+
// (notify-runtime.ts) keeps live via ctx.services.webhookNotifier, rather than
|
|
641
|
+
// constructing a second, boot-time-only instance here. Before this fix, a
|
|
642
|
+
// webhook URL added mid-session reached long-task notifications (which read
|
|
643
|
+
// ctx.services.webhookNotifier directly) but never reached this instance's
|
|
644
|
+
// AGENT_FAILED/WORKFLOW_CHAIN_FAILED/WORKFLOW_CHAIN_PASSED runtime-bus
|
|
645
|
+
// listeners until restart — and if the session started with zero URLs
|
|
646
|
+
// configured, attachToRuntimeBus was never even called, so those listeners
|
|
647
|
+
// never existed at all for the rest of the session. Always attaching
|
|
648
|
+
// (regardless of initial URL count) and seeding the shared instance fixes
|
|
649
|
+
// both: `send()` is already a safe no-op with zero URLs configured.
|
|
635
650
|
const webhookUrls = (configManager.getCategory('notifications') as { webhookUrls?: string[] }).webhookUrls ?? [];
|
|
636
651
|
if (webhookUrls.length > 0) {
|
|
637
|
-
|
|
638
|
-
webhookNotifier.attachToRuntimeBus(runtimeBus);
|
|
652
|
+
services.webhookNotifier.setUrls(webhookUrls);
|
|
639
653
|
domainDispatch.syncIntegration({
|
|
640
654
|
id: 'webhooks',
|
|
641
655
|
displayName: 'Webhooks',
|
|
@@ -647,6 +661,7 @@ export async function initializeBootstrapCore(
|
|
|
647
661
|
meta: { urlCount: webhookUrls.length },
|
|
648
662
|
}, 'bootstrap.webhooks');
|
|
649
663
|
}
|
|
664
|
+
services.webhookNotifier.attachToRuntimeBus(runtimeBus);
|
|
650
665
|
|
|
651
666
|
const notifier = await Notifier.fromConfig(services.serviceRegistry);
|
|
652
667
|
const queueStatuses = notifier.getQueueStatus();
|
|
@@ -29,6 +29,8 @@ export interface ResumeSessionOptions {
|
|
|
29
29
|
readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
|
|
30
30
|
readonly providerRegistry: Pick<ProviderRegistry, 'get' | 'getCurrentModel' | 'getForModel' | 'require'>;
|
|
31
31
|
readonly homeDirectory: string;
|
|
32
|
+
/** See CommandSessionServices.hydrateSessionUsage (command-registry.ts). */
|
|
33
|
+
readonly hydrateSessionUsage?: () => void;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function createResumeSessionHandler(options: ResumeSessionOptions): (sessionId: string) => void {
|
|
@@ -65,6 +67,9 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
|
|
|
65
67
|
});
|
|
66
68
|
},
|
|
67
69
|
});
|
|
70
|
+
// Hydrate the footer's token counters from the resumed history now that
|
|
71
|
+
// fromJSON()/journal replay are both applied — before requestRender() below.
|
|
72
|
+
options.hydrateSessionUsage?.();
|
|
68
73
|
options.onSessionIdChanged?.(sessionId);
|
|
69
74
|
if (meta?.model) options.runtime.model = meta.model;
|
|
70
75
|
if (meta?.provider) options.runtime.provider = meta.provider;
|