@pellux/goodvibes-tui 1.1.0 → 1.9.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 +85 -21
- package/README.md +28 -18
- 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/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/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -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 +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- 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/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- 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 +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- 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 +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- 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 +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- 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 +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- 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 +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -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-integration-actions.ts +9 -170
- 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 +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- 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 +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- 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 +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -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 +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -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 +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- 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/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- 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 +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- 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 +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -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 +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- 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 +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- 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 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- 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 -515
- 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 -566
- 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 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- 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 -496
- 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 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
100
100
|
"@ast-grep/napi": "^0.42.0",
|
|
101
101
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
102
|
-
"@pellux/goodvibes-sdk": "0.
|
|
102
|
+
"@pellux/goodvibes-sdk": "1.0.0",
|
|
103
103
|
"bash-language-server": "^5.6.0",
|
|
104
104
|
"fuse.js": "^7.1.0",
|
|
105
105
|
"graphql": "^16.13.2",
|
|
@@ -61,7 +61,16 @@ export class SpokenTurnController {
|
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Returns whether speech was actually ACTIVE when stopped. The notice only
|
|
66
|
+
* prints in that case — stop() on an idle controller used to notify anyway,
|
|
67
|
+
* spamming "[TTS] Spoken output stopped." on every Ctrl+C (batch replay
|
|
68
|
+
* D5/N-noise); callers use the return to decide whether the press "did a
|
|
69
|
+
* job" (see handleCtrlC's consume-on-speech-stop).
|
|
70
|
+
*/
|
|
71
|
+
stop(message?: string): boolean {
|
|
72
|
+
const wasActive = this.pendingPrompt !== null || this.activeTurnId !== null
|
|
73
|
+
|| this.chunker !== null || this.abortControllers.size > 0;
|
|
65
74
|
this.pendingPrompt = null;
|
|
66
75
|
this.activeTurnId = null;
|
|
67
76
|
this.chunker?.reset();
|
|
@@ -72,7 +81,8 @@ export class SpokenTurnController {
|
|
|
72
81
|
this.player.stop();
|
|
73
82
|
this.playbackChain = Promise.resolve();
|
|
74
83
|
this.errorReportedForTurn = false;
|
|
75
|
-
if (message) this.notify?.(`[TTS] ${message}`);
|
|
84
|
+
if (message && wasActive) this.notify?.(`[TTS] ${message}`);
|
|
85
|
+
return wasActive;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
handleTurnEvent(event: TurnEvent): void {
|
|
@@ -8,7 +8,8 @@ import { SpokenTurnController } from './spoken-turn-controller.ts';
|
|
|
8
8
|
export interface SpokenTurnRuntime {
|
|
9
9
|
readonly unsubs: readonly (() => void)[];
|
|
10
10
|
submitNextTurn(prompt: string): boolean;
|
|
11
|
-
|
|
11
|
+
/** Returns whether speech was actually active (see controller.stop). */
|
|
12
|
+
stop(message?: string): boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface WireSpokenTurnRuntimeOptions {
|
package/src/cli/help.ts
CHANGED
|
@@ -283,10 +283,17 @@ export function renderGoodVibesCommandHelp(topic: string, binary = 'goodvibes'):
|
|
|
283
283
|
|
|
284
284
|
export function renderGoodVibesDaemonHelp(binary = 'goodvibes-daemon'): string {
|
|
285
285
|
return [
|
|
286
|
-
`Usage: ${binary} [OPTIONS]`,
|
|
286
|
+
`Usage: ${binary} [COMMAND] [OPTIONS]`,
|
|
287
287
|
'',
|
|
288
288
|
'Starts the headless GoodVibes daemon/API host.',
|
|
289
289
|
'',
|
|
290
|
+
'Commands:',
|
|
291
|
+
' install-service Install + enable the daemon as a systemd user service (survives reboots)',
|
|
292
|
+
' uninstall-service Disable + remove the daemon systemd user service',
|
|
293
|
+
' service-status Show whether the daemon service is installed / enabled / active',
|
|
294
|
+
' migrate-service Guided takeover of a legacy goodvibes-daemon.service unit; prints a plan',
|
|
295
|
+
' unless run with -y/--yes (never auto-migrates)',
|
|
296
|
+
'',
|
|
290
297
|
'Options:',
|
|
291
298
|
' --daemon-home <dir> Override daemon home',
|
|
292
299
|
' --working-dir <dir> Override working directory',
|
|
@@ -5,6 +5,7 @@ import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:pa
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { PlatformServiceManager } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
7
7
|
import type { ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
8
|
+
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
8
9
|
import type { ConfigManager } from '../config/index.ts';
|
|
9
10
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
10
11
|
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
@@ -392,7 +393,7 @@ export async function buildCliServicePosture(
|
|
|
392
393
|
enabled: runtime.configManager.get('service.enabled') === true,
|
|
393
394
|
autostart: runtime.configManager.get('service.autostart') === true,
|
|
394
395
|
restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
|
|
395
|
-
daemonEnabled: runtime.configManager
|
|
396
|
+
daemonEnabled: resolveDaemonEnabled(runtime.configManager),
|
|
396
397
|
};
|
|
397
398
|
const serverBackedEnabled = config.daemonEnabled || endpoints.some((endpoint) => endpoint.enabled);
|
|
398
399
|
const issues: string[] = [];
|
package/src/cli/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
+
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
3
|
import type { OnboardingCheckMarkersState } from '../runtime/onboarding/index.ts';
|
|
3
4
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
4
5
|
import { isNetworkFacing } from './network-posture.ts';
|
|
@@ -98,7 +99,7 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
98
99
|
const serviceEnabled = config.get('service.enabled') === true;
|
|
99
100
|
const serviceAutostart = config.get('service.autostart') === true;
|
|
100
101
|
const restartOnFailure = config.get('service.restartOnFailure') === true;
|
|
101
|
-
const daemonEnabled = config
|
|
102
|
+
const daemonEnabled = resolveDaemonEnabled(config);
|
|
102
103
|
const listenerEnabled = config.get('danger.httpListener') === true;
|
|
103
104
|
const webEnabled = config.get('web.enabled') === true;
|
|
104
105
|
const controlPlaneEnabled = config.get('controlPlane.enabled') === true;
|
|
@@ -47,7 +47,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
47
47
|
runtime.configManager.setDynamic('web.enabled', enabled);
|
|
48
48
|
if (enabled) {
|
|
49
49
|
enableFeatureFlags(runtime.configManager, getServerSurfaceFeatureFlags({ serverBacked: true, web: true }));
|
|
50
|
-
runtime.configManager.setDynamic('
|
|
50
|
+
runtime.configManager.setDynamic('daemon.enabled', true);
|
|
51
51
|
runtime.configManager.setDynamic('controlPlane.enabled', true);
|
|
52
52
|
const webError = applyTargetEndpointFlagsOrDefault(runtime, 'web');
|
|
53
53
|
if (webError) return { output: webError, exitCode: 2 };
|
|
@@ -70,7 +70,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
70
70
|
}
|
|
71
71
|
else if (target === 'control-plane' || target === 'controlPlane') {
|
|
72
72
|
runtime.configManager.setDynamic('controlPlane.enabled', enabled);
|
|
73
|
-
runtime.configManager.setDynamic('
|
|
73
|
+
runtime.configManager.setDynamic('daemon.enabled', enabled);
|
|
74
74
|
if (enabled) {
|
|
75
75
|
const controlPlaneError = applyTargetEndpointFlagsOrDefault(runtime, 'controlPlane');
|
|
76
76
|
if (controlPlaneError) return { output: controlPlaneError, exitCode: 2 };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alert-gating — shared config + focus gating logic for the unfocused-user
|
|
3
|
+
* alert classes (budget-breach, long-task, agent/chain failure,
|
|
4
|
+
* awaiting-approval). Each alert class has its own on/off config key
|
|
5
|
+
* (default true); a single master key, `behavior.notifyOnlyWhenUnfocused`
|
|
6
|
+
* (default true), decides whether any of them additionally require the
|
|
7
|
+
* terminal to be unfocused-or-unknown before firing.
|
|
8
|
+
*
|
|
9
|
+
* Extracted so every alert-class module (long-task-notifier.ts,
|
|
10
|
+
* budget-breach-notifier.ts, approval-alert.ts, and the agent/chain-failure
|
|
11
|
+
* wiring in turn-event-wiring.ts) shares one implementation of "should this
|
|
12
|
+
* fire right now" instead of re-deriving the same two config reads and the
|
|
13
|
+
* same focus check.
|
|
14
|
+
*/
|
|
15
|
+
import type { FocusTracker } from './focus-tracker.ts';
|
|
16
|
+
|
|
17
|
+
/** Minimal config-read surface every gating check needs. */
|
|
18
|
+
export type ConfigGet = (key: string) => unknown;
|
|
19
|
+
|
|
20
|
+
/** Read a TUI-local synthetic boolean setting, defaulting when absent/invalid. */
|
|
21
|
+
export function readBooleanConfig(configGet: ConfigGet, key: string, defaultValue: boolean): boolean {
|
|
22
|
+
const raw = configGet(key);
|
|
23
|
+
if (typeof raw === 'boolean') return raw;
|
|
24
|
+
if (raw === undefined || raw === null) return defaultValue;
|
|
25
|
+
// Tolerate string/number forms the same way the rest of the config layer does
|
|
26
|
+
// (settings-modal writes real booleans, but env/file edits may not).
|
|
27
|
+
if (raw === 'true' || raw === 1) return true;
|
|
28
|
+
if (raw === 'false' || raw === 0) return false;
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `behavior.notifyOnlyWhenUnfocused` — master gate, default on. */
|
|
33
|
+
export function readNotifyOnlyWhenUnfocused(configGet: ConfigGet): boolean {
|
|
34
|
+
return readBooleanConfig(configGet, 'behavior.notifyOnlyWhenUnfocused', true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decide whether a given alert class should fire right now.
|
|
39
|
+
*
|
|
40
|
+
* `classConfigKey` is the per-class on/off switch (e.g.
|
|
41
|
+
* 'behavior.notifyOnBudgetBreach'); default true when absent. When that key
|
|
42
|
+
* is off, the class never fires regardless of focus. When it's on, the
|
|
43
|
+
* result additionally depends on the master `notifyOnlyWhenUnfocused` gate:
|
|
44
|
+
* if that master gate is off, the class fires unconditionally (today's
|
|
45
|
+
* pre-focus-tracking behavior); if on (the default), it only fires when the
|
|
46
|
+
* terminal is unfocused or focus state was never observed (see
|
|
47
|
+
* FocusTracker.shouldAlertWhenUnfocused).
|
|
48
|
+
*/
|
|
49
|
+
export function shouldFireAlert(
|
|
50
|
+
focusTracker: Pick<FocusTracker, 'shouldAlertWhenUnfocused'>,
|
|
51
|
+
configGet: ConfigGet,
|
|
52
|
+
classConfigKey: string,
|
|
53
|
+
): boolean {
|
|
54
|
+
if (!readBooleanConfig(configGet, classConfigKey, true)) return false;
|
|
55
|
+
if (!readNotifyOnlyWhenUnfocused(configGet)) return true;
|
|
56
|
+
return focusTracker.shouldAlertWhenUnfocused();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* notifyCompletion (SDK platform/utils) only rings the bell above 5s of
|
|
61
|
+
* "duration" and only pops a desktop notification above 30s — a heuristic
|
|
62
|
+
* tuned for long-running-turn notifications. The alert classes in this
|
|
63
|
+
* directory are point-in-time events with no natural duration, so they pass
|
|
64
|
+
* this constant as the `durationMs` argument to force both the bell and the
|
|
65
|
+
* desktop popup unconditionally.
|
|
66
|
+
*/
|
|
67
|
+
export const FORCE_NOTIFY_DURATION_MS = 30_001;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* approval-alert — fires an unfocused-user alert the moment a tool call
|
|
3
|
+
* becomes a real, user-blocking permission prompt.
|
|
4
|
+
*
|
|
5
|
+
* Anchor: `PERMISSION_REQUESTED` (SDK events/permissions.ts) is the wrong
|
|
6
|
+
* signal — it fires for every policy-evaluation step, including ones
|
|
7
|
+
* auto-resolved by allow-lists/yolo-mode in milliseconds. The only place a
|
|
8
|
+
* request becomes an actual user-facing prompt is where main.ts assigns
|
|
9
|
+
* `permissionPromptRef.requestPermission`. There is no `permissions` feed on
|
|
10
|
+
* UiRuntimeEvents to subscribe to instead, so this wraps the handler at that
|
|
11
|
+
* assignment site.
|
|
12
|
+
*
|
|
13
|
+
* wrapRequestPermissionWithAlert fires the alert synchronously (before the
|
|
14
|
+
* wrapped promise settles) so it reflects "a prompt just appeared", not
|
|
15
|
+
* "a prompt was just resolved".
|
|
16
|
+
*
|
|
17
|
+
* PRIVACY: message content is tool name + permission category only — no
|
|
18
|
+
* args, no file contents, no command strings (mirrors long-task-notifier's
|
|
19
|
+
* privacy guarantee).
|
|
20
|
+
*/
|
|
21
|
+
import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
|
|
22
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
|
+
import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
24
|
+
import type { PermissionRequestHandler, PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
25
|
+
import type { FocusTracker } from './focus-tracker.ts';
|
|
26
|
+
import { shouldFireAlert, FORCE_NOTIFY_DURATION_MS, type ConfigGet } from './alert-gating.ts';
|
|
27
|
+
|
|
28
|
+
export interface ApprovalAlertDeps {
|
|
29
|
+
readonly focusTracker: Pick<FocusTracker, 'shouldAlertWhenUnfocused'>;
|
|
30
|
+
readonly configGet: ConfigGet;
|
|
31
|
+
readonly webhookNotifier?: WebhookNotifier | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a PermissionRequestHandler so every call also fires an unfocused-user
|
|
36
|
+
* alert (subject to the behavior.notifyOnApprovalPending / notifyOnlyWhenUnfocused
|
|
37
|
+
* gates). The wrapped handler's behavior (what it resolves to) is completely
|
|
38
|
+
* unchanged — this only adds a side effect at call time.
|
|
39
|
+
*/
|
|
40
|
+
export function wrapRequestPermissionWithAlert(
|
|
41
|
+
original: PermissionRequestHandler,
|
|
42
|
+
deps: ApprovalAlertDeps,
|
|
43
|
+
): PermissionRequestHandler {
|
|
44
|
+
return (request: PermissionPromptRequest) => {
|
|
45
|
+
fireApprovalAlert(request, deps);
|
|
46
|
+
return original(request);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fireApprovalAlert(request: PermissionPromptRequest, deps: ApprovalAlertDeps): void {
|
|
51
|
+
if (!shouldFireAlert(deps.focusTracker, deps.configGet, 'behavior.notifyOnApprovalPending')) return;
|
|
52
|
+
|
|
53
|
+
const title = 'GoodVibes — approval needed';
|
|
54
|
+
// PRIVACY: tool name + category only. Never request.args.
|
|
55
|
+
const message = `${request.tool} (${request.category}) is waiting for approval`;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
notifyCompletion(title, message, FORCE_NOTIFY_DURATION_MS);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logger.debug('approval-alert: desktop notify error', { error: String(err) });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const webhookNotifier = deps.webhookNotifier;
|
|
64
|
+
if (webhookNotifier) {
|
|
65
|
+
const urls = webhookNotifier.getUrls();
|
|
66
|
+
if (urls.length > 0) {
|
|
67
|
+
webhookNotifier.send(message).catch((err: unknown) => {
|
|
68
|
+
logger.debug('approval-alert: webhook send error', { error: String(err) });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* budget-breach-notifier — fires an unfocused-user alert the moment session
|
|
3
|
+
* cost crosses the configured budget threshold (behavior.budgetAlertUsd).
|
|
4
|
+
*
|
|
5
|
+
* One-shot semantics: fires exactly once on the false→true edge (crossing
|
|
6
|
+
* into breach), never again while still over budget on every subsequent
|
|
7
|
+
* turn. The "already notified" latch resets when the session drops back
|
|
8
|
+
* under budget (so a later re-breach can fire again) or when the threshold
|
|
9
|
+
* itself changes (raising, lowering, or clearing the budget re-arms it
|
|
10
|
+
* against the new value).
|
|
11
|
+
*
|
|
12
|
+
* Delivery mirrors long-task-notifier.ts: desktop notification
|
|
13
|
+
* (notifyCompletion) + outbound webhook (WebhookNotifier), both gated by
|
|
14
|
+
* the shared focus/config gating in alert-gating.ts. Skips evaluation
|
|
15
|
+
* entirely when the session model is unpriced — a $0 placeholder cost must
|
|
16
|
+
* never be reported as a real breach (mirrors CostTrackerPanel's "unpriced"
|
|
17
|
+
* display convention).
|
|
18
|
+
*
|
|
19
|
+
* PRIVACY: message text is built from cost/threshold numbers and the
|
|
20
|
+
* session id prefix only — no conversation content.
|
|
21
|
+
*/
|
|
22
|
+
import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
24
|
+
import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
25
|
+
import { calcSessionCost, computeBudgetBreach, isModelPriced } from '../export/cost-utils.ts';
|
|
26
|
+
import type { FocusTracker } from './focus-tracker.ts';
|
|
27
|
+
import { shouldFireAlert, FORCE_NOTIFY_DURATION_MS, type ConfigGet } from './alert-gating.ts';
|
|
28
|
+
|
|
29
|
+
export interface BudgetBreachUsageSnapshot {
|
|
30
|
+
readonly input: number;
|
|
31
|
+
readonly output: number;
|
|
32
|
+
readonly cacheRead: number;
|
|
33
|
+
readonly cacheWrite: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BudgetBreachNotifierDeps {
|
|
37
|
+
readonly focusTracker: Pick<FocusTracker, 'shouldAlertWhenUnfocused'>;
|
|
38
|
+
readonly configGet: ConfigGet;
|
|
39
|
+
readonly webhookNotifier?: WebhookNotifier | null;
|
|
40
|
+
readonly sessionId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Stateful edge-trigger checker — construct once per session, call on every TURN_COMPLETED. */
|
|
44
|
+
export interface BudgetBreachNotifier {
|
|
45
|
+
/**
|
|
46
|
+
* Evaluate current usage against the configured threshold and fire an
|
|
47
|
+
* alert on the false→true breach edge. Returns true when an alert was
|
|
48
|
+
* fired this call (for tests / observability), false otherwise.
|
|
49
|
+
*/
|
|
50
|
+
check(usage: BudgetBreachUsageSnapshot, sessionModel: string, budgetThresholdUsd: number): boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createBudgetBreachNotifier(deps: BudgetBreachNotifierDeps): BudgetBreachNotifier {
|
|
54
|
+
let lastThreshold: number | null = null;
|
|
55
|
+
let notified = false;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
check(usage, sessionModel, budgetThresholdUsd) {
|
|
59
|
+
if (budgetThresholdUsd !== lastThreshold) {
|
|
60
|
+
// Threshold raised, lowered, or cleared since the last check — re-arm
|
|
61
|
+
// the latch so a breach against the new threshold can fire again.
|
|
62
|
+
lastThreshold = budgetThresholdUsd;
|
|
63
|
+
notified = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (budgetThresholdUsd <= 0) return false; // disabled
|
|
67
|
+
if (!isModelPriced(sessionModel)) return false; // cost would be a placeholder, not real
|
|
68
|
+
|
|
69
|
+
const sessionCost = calcSessionCost(usage.input, usage.output, usage.cacheRead, usage.cacheWrite, sessionModel);
|
|
70
|
+
const breached = computeBudgetBreach(sessionCost, budgetThresholdUsd);
|
|
71
|
+
|
|
72
|
+
if (!breached) {
|
|
73
|
+
notified = false; // dropped back under budget — can re-fire on the next crossing
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (notified) return false; // already alerted for this crossing
|
|
77
|
+
|
|
78
|
+
notified = true;
|
|
79
|
+
fireBudgetBreachAlert(deps, sessionCost, budgetThresholdUsd);
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fireBudgetBreachAlert(deps: BudgetBreachNotifierDeps, sessionCost: number, budgetThresholdUsd: number): void {
|
|
86
|
+
if (!shouldFireAlert(deps.focusTracker, deps.configGet, 'behavior.notifyOnBudgetBreach')) return;
|
|
87
|
+
|
|
88
|
+
const title = 'GoodVibes — budget breach';
|
|
89
|
+
const message = `session cost $${sessionCost.toFixed(2)} exceeded budget $${budgetThresholdUsd.toFixed(2)} · session ${deps.sessionId.slice(0, 8)}`;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
notifyCompletion(title, message, FORCE_NOTIFY_DURATION_MS);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.debug('budget-breach-notifier: desktop notify error', { error: String(err) });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const webhookNotifier = deps.webhookNotifier;
|
|
98
|
+
if (webhookNotifier) {
|
|
99
|
+
const urls = webhookNotifier.getUrls();
|
|
100
|
+
if (urls.length > 0) {
|
|
101
|
+
webhookNotifier.send(message).catch((err: unknown) => {
|
|
102
|
+
logger.debug('budget-breach-notifier: webhook send error', { error: String(err) });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -20,8 +20,13 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
|
|
|
20
20
|
const flags: string[] = [];
|
|
21
21
|
let pendingRisk: ComposerState['pendingRisk'] = 'none';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// An approval wait is the dominant composer state and owns the single honest
|
|
24
|
+
// status tag: `risk:approval-wait`. It is therefore NOT also duplicated as a
|
|
25
|
+
// `approval` flag, and (below) it suppresses the competing `state:` turn tag —
|
|
26
|
+
// the turn is blocked on the user, not really streaming. Precedence:
|
|
27
|
+
// approval-wait > live turn state. (UX-B item 4.)
|
|
28
|
+
const approvalWait = input.pendingApproval === true;
|
|
29
|
+
if (approvalWait) {
|
|
25
30
|
pendingRisk = 'approval-wait';
|
|
26
31
|
}
|
|
27
32
|
if (intent.kind === 'shell') {
|
|
@@ -35,7 +40,7 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
|
|
|
35
40
|
}
|
|
36
41
|
if (input.hasAttachments) flags.push('attachments');
|
|
37
42
|
|
|
38
|
-
const
|
|
43
|
+
const turnStatus = (() => {
|
|
39
44
|
switch (input.turnState) {
|
|
40
45
|
case 'preflight': return 'preflight';
|
|
41
46
|
case 'streaming': return 'streaming';
|
|
@@ -47,6 +52,9 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
|
|
|
47
52
|
default: return 'idle';
|
|
48
53
|
}
|
|
49
54
|
})();
|
|
55
|
+
// 'idle' is suppressed by the footer renderer, so an approval wait shows only
|
|
56
|
+
// `risk:approval-wait` — one honest tag, not three spellings of the wait.
|
|
57
|
+
const statusLabel = approvalWait ? 'idle' : turnStatus;
|
|
50
58
|
|
|
51
59
|
return {
|
|
52
60
|
intent,
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
renderConversationSystemMessage,
|
|
50
50
|
renderConversationToolMessage,
|
|
51
51
|
renderConversationUserMessage,
|
|
52
|
+
collectCompletedToolCallIds,
|
|
52
53
|
} from './conversation-rendering.ts';
|
|
53
54
|
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
54
55
|
// SystemMessageKind imported from runtime directly to avoid a cycle, mirroring
|
|
@@ -94,6 +95,18 @@ interface KeyMeta {
|
|
|
94
95
|
readonly showReasoningSummary: boolean;
|
|
95
96
|
readonly blockBase: number;
|
|
96
97
|
readonly kind: SystemMessageKind | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* True when this (assistant) message has a tool call with no result yet — it
|
|
100
|
+
* renders as pending rather than done. Keyed so the entry invalidates and
|
|
101
|
+
* re-renders ✓ once the tool result arrives. (UX-B item 2c.)
|
|
102
|
+
*/
|
|
103
|
+
readonly hasPendingTool: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether an assistant message has any tool call still awaiting a result. */
|
|
107
|
+
function hasPendingToolCall(m: Message, completed: ReadonlySet<string>): boolean {
|
|
108
|
+
if (m.role !== 'assistant' || !m.toolCalls) return false;
|
|
109
|
+
return m.toolCalls.some((tc) => tc.id === undefined || !completed.has(tc.id));
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
interface CacheEntry {
|
|
@@ -272,6 +285,11 @@ export class MessageLineCache {
|
|
|
272
285
|
showReasoningSummary: context.configManager?.get('display.showReasoningSummary') ?? false,
|
|
273
286
|
};
|
|
274
287
|
|
|
288
|
+
// Tool calls with no matching tool-result message are still pending; the
|
|
289
|
+
// render context and the cache key both depend on this. (UX-B item 2c.)
|
|
290
|
+
const completedToolCallIds = collectCompletedToolCallIds(messages);
|
|
291
|
+
const renderContext: ConversationRenderContext = { ...context, completedToolCallIds };
|
|
292
|
+
|
|
275
293
|
const touched = new Set<number>();
|
|
276
294
|
|
|
277
295
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -283,17 +301,18 @@ export class MessageLineCache {
|
|
|
283
301
|
|
|
284
302
|
const uncacheable = absoluteIdx === streamingPlaceholderAbsIdx;
|
|
285
303
|
const kind = context.messageKindRegistry.get(absoluteIdx);
|
|
304
|
+
const hasPendingTool = hasPendingToolCall(message, completedToolCallIds);
|
|
286
305
|
|
|
287
306
|
if (!uncacheable) {
|
|
288
307
|
const existing = this.entries.get(absoluteIdx);
|
|
289
|
-
if (existing && this.isValid(existing, message, width, cfg, blockBase, kind, context.collapseState)) {
|
|
308
|
+
if (existing && this.isValid(existing, message, width, cfg, blockBase, kind, hasPendingTool, context.collapseState)) {
|
|
290
309
|
this.applyEntry(context, existing, base);
|
|
291
310
|
touched.add(absoluteIdx);
|
|
292
311
|
continue;
|
|
293
312
|
}
|
|
294
313
|
}
|
|
295
314
|
|
|
296
|
-
const entry = this.renderScratch(
|
|
315
|
+
const entry = this.renderScratch(renderContext, message, width, absoluteIdx, cfg, blockBase, kind, hasPendingTool);
|
|
297
316
|
this.applyEntry(context, entry, base);
|
|
298
317
|
if (!uncacheable) {
|
|
299
318
|
this.entries.set(absoluteIdx, entry);
|
|
@@ -319,6 +338,7 @@ export class MessageLineCache {
|
|
|
319
338
|
cfg: RenderConfig,
|
|
320
339
|
blockBase: number,
|
|
321
340
|
kind: SystemMessageKind | undefined,
|
|
341
|
+
hasPendingTool: boolean,
|
|
322
342
|
collapseState: Map<string, boolean>,
|
|
323
343
|
): boolean {
|
|
324
344
|
const k = entry.keyMeta;
|
|
@@ -330,7 +350,8 @@ export class MessageLineCache {
|
|
|
330
350
|
k.showThinking !== cfg.showThinking ||
|
|
331
351
|
k.showReasoningSummary !== cfg.showReasoningSummary ||
|
|
332
352
|
k.blockBase !== blockBase ||
|
|
333
|
-
k.kind !== kind
|
|
353
|
+
k.kind !== kind ||
|
|
354
|
+
k.hasPendingTool !== hasPendingTool
|
|
334
355
|
) {
|
|
335
356
|
return false;
|
|
336
357
|
}
|
|
@@ -355,6 +376,7 @@ export class MessageLineCache {
|
|
|
355
376
|
cfg: RenderConfig,
|
|
356
377
|
blockBase: number,
|
|
357
378
|
kind: SystemMessageKind | undefined,
|
|
379
|
+
hasPendingTool: boolean,
|
|
358
380
|
): CacheEntry {
|
|
359
381
|
const scratchLines: Line[] = [];
|
|
360
382
|
const scratchHistory = {
|
|
@@ -378,6 +400,7 @@ export class MessageLineCache {
|
|
|
378
400
|
messageKindRegistry: context.messageKindRegistry,
|
|
379
401
|
configManager: context.configManager,
|
|
380
402
|
splashOptions: context.splashOptions,
|
|
403
|
+
completedToolCallIds: context.completedToolCallIds,
|
|
381
404
|
};
|
|
382
405
|
|
|
383
406
|
renderOne(scratchCtx, message, width, absoluteIdx, cfg);
|
|
@@ -397,6 +420,7 @@ export class MessageLineCache {
|
|
|
397
420
|
showReasoningSummary: cfg.showReasoningSummary,
|
|
398
421
|
blockBase,
|
|
399
422
|
kind,
|
|
423
|
+
hasPendingTool,
|
|
400
424
|
},
|
|
401
425
|
contentSig: contentSigOf(message),
|
|
402
426
|
lines: scratchLines,
|