@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadSkillByTrigger } from '@pellux/goodvibes-sdk/platform/tools';
|
|
2
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
2
3
|
import type { CommandContext, CommandRegistry } from './command-registry.ts';
|
|
3
4
|
import type { AutocompleteEngine } from './autocomplete.ts';
|
|
4
5
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
@@ -92,25 +93,34 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
|
|
|
92
93
|
const commandPromise = state.commandRegistry.get(name)
|
|
93
94
|
? state.commandRegistry.execute(name, args, ctx)
|
|
94
95
|
: (ctx.executeCommand?.(name, args) ?? Promise.resolve(false));
|
|
95
|
-
commandPromise
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
const shellPaths = state.commandContext?.workspace.shellPaths;
|
|
100
|
-
const skillContent = shellPaths
|
|
101
|
-
? loadSkillByTrigger('/' + name, {
|
|
102
|
-
workingDirectory: shellPaths.workingDirectory,
|
|
103
|
-
homeDirectory: shellPaths.homeDirectory,
|
|
104
|
-
})
|
|
105
|
-
: null;
|
|
106
|
-
if (skillContent) {
|
|
107
|
-
state.commandContext?.submitInput?.(skillContent);
|
|
108
|
-
} else {
|
|
109
|
-
state.conversationManager?.log(`Unknown command: /${name}. Type /help for available commands.`, { fg: '#ef4444' });
|
|
96
|
+
commandPromise
|
|
97
|
+
.then((handled) => {
|
|
98
|
+
if (handled) {
|
|
110
99
|
state.requestRender();
|
|
100
|
+
} else {
|
|
101
|
+
const shellPaths = state.commandContext?.workspace.shellPaths;
|
|
102
|
+
const skillContent = shellPaths
|
|
103
|
+
? loadSkillByTrigger('/' + name, {
|
|
104
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
105
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
106
|
+
})
|
|
107
|
+
: null;
|
|
108
|
+
if (skillContent) {
|
|
109
|
+
state.commandContext?.submitInput?.(skillContent);
|
|
110
|
+
} else {
|
|
111
|
+
state.conversationManager?.log(`Unknown command: /${name}. Type /help for available commands.`, { fg: '#ef4444' });
|
|
112
|
+
state.requestRender();
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
115
|
+
})
|
|
116
|
+
.catch((err: unknown) => {
|
|
117
|
+
// Defense in depth for EVERY command, not just the ones that
|
|
118
|
+
// already guard their own internals: a handler that throws or
|
|
119
|
+
// awaits a rejected promise must never become a silent unhandled
|
|
120
|
+
// rejection — it renders the same way an unknown command does.
|
|
121
|
+
state.conversationManager?.log(`Command /${name} failed: ${summarizeError(err)}`, { fg: '#ef4444' });
|
|
122
|
+
state.requestRender();
|
|
123
|
+
});
|
|
114
124
|
} else {
|
|
115
125
|
closeCommandMode();
|
|
116
126
|
}
|
|
@@ -133,9 +143,16 @@ function withPanelFocusSync(context: CommandContext, state: CommandModeRouteStat
|
|
|
133
143
|
}
|
|
134
144
|
: undefined,
|
|
135
145
|
showPanel: context.showPanel
|
|
136
|
-
? (panelId, pane) => {
|
|
137
|
-
context.showPanel?.(panelId, pane);
|
|
138
|
-
|
|
146
|
+
? (panelId, pane, target, opts) => {
|
|
147
|
+
context.showPanel?.(panelId, pane, target, opts);
|
|
148
|
+
// UX-C: showPanel no longer transfers focus by default — the command
|
|
149
|
+
// path leaves the composer focused (ui-openers.ts's showPanel only
|
|
150
|
+
// calls focusPanels() when opts.focus is set). Only flip the local
|
|
151
|
+
// panelFocused mirror when the caller explicitly asked for focus;
|
|
152
|
+
// otherwise leave it exactly as it was (the composer, since you can
|
|
153
|
+
// only type a command from there in the first place). `target` is the
|
|
154
|
+
// DEBT-5 deep-link jump target, forwarded verbatim.
|
|
155
|
+
if (opts?.focus) state.panelFocused = true;
|
|
139
156
|
}
|
|
140
157
|
: undefined,
|
|
141
158
|
focusPanels: context.focusPanels
|
|
@@ -421,7 +421,7 @@ export function handleCtrlC(
|
|
|
421
421
|
saveUndoState: () => void,
|
|
422
422
|
setPrompt: (value: string) => void,
|
|
423
423
|
setCursorPos: (value: number) => void,
|
|
424
|
-
cancelGeneration: (() => void) | undefined,
|
|
424
|
+
cancelGeneration: (() => boolean | void) | undefined,
|
|
425
425
|
exitApp: () => void,
|
|
426
426
|
requestRender: () => void,
|
|
427
427
|
lastCtrlCTime: number,
|
|
@@ -436,7 +436,22 @@ export function handleCtrlC(
|
|
|
436
436
|
setCursorPos(0);
|
|
437
437
|
return;
|
|
438
438
|
}
|
|
439
|
-
|
|
439
|
+
// Stopping in-flight speech / aborting the turn must never prevent the quit
|
|
440
|
+
// double-press window from advancing. A throw from the async TTS-stop path
|
|
441
|
+
// (e.g. killing the audio subprocess) would otherwise swallow this press and
|
|
442
|
+
// strand the user unable to quit while TTS is speaking. Decouple the two: the
|
|
443
|
+
// press below is recorded no matter how speech-stop fares. (UX-B item 6b.)
|
|
444
|
+
let stoppedSpeech = false;
|
|
445
|
+
try {
|
|
446
|
+
stoppedSpeech = cancelGeneration?.() === true;
|
|
447
|
+
} catch {
|
|
448
|
+
// Non-fatal to the exit chord; the quit-window bookkeeping still runs.
|
|
449
|
+
}
|
|
450
|
+
// A press that silenced LIVE speech is consumed by that job (batch replay
|
|
451
|
+
// D5) — symmetric with the prompt-clearing press above. The quit chord
|
|
452
|
+
// ("Ctrl+C x2") starts from a quiet state; turn-aborts still count toward
|
|
453
|
+
// the double-press.
|
|
454
|
+
if (stoppedSpeech) return;
|
|
440
455
|
const now = Date.now();
|
|
441
456
|
// Clear any hide-timer pending from a prior press before deciding this
|
|
442
457
|
// one's outcome, whichever branch below runs. Without this, a hide-timer
|
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
} from './handler-prompt-buffer.ts';
|
|
14
14
|
import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, registerPaste } from './handler-content-actions.ts';
|
|
15
15
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
16
|
-
import {
|
|
17
|
-
import type {
|
|
16
|
+
import { getPanelUnderMouse, workspaceTabAtMouse } from './panel-mouse-geometry.ts';
|
|
17
|
+
import type { PanelMouseLayout } from './panel-mouse-geometry.ts';
|
|
18
|
+
export type { PanelMouseLayout };
|
|
18
19
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
19
20
|
import type { KillRing } from './kill-ring.ts';
|
|
20
21
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
22
|
+
import { trackPanelPasteFloodGuard, type PanelBurstGuardState } from './panel-paste-flood-guard.ts';
|
|
21
23
|
|
|
22
24
|
export type PanelFocusRouteState = {
|
|
23
25
|
panelManager: PanelManager;
|
|
@@ -31,14 +33,29 @@ export type PanelFocusRouteState = {
|
|
|
31
33
|
cyclePanelTab: (direction: 'next' | 'prev') => void;
|
|
32
34
|
onPanelInputConsumed?: (activePanel: import('../panels/types.ts').Panel | null, key: string) => void;
|
|
33
35
|
/**
|
|
34
|
-
* True when
|
|
35
|
-
* one
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* True when THIS token is a paste: a single 'text' token whose value holds
|
|
37
|
+
* more than one character. The SDK tokenizer emits a bracketed paste
|
|
38
|
+
* (\x1b[?2004h, enabled in main.ts init) as exactly one such token; discrete
|
|
39
|
+
* keystrokes — even several batched into one feed() by render-tick latency —
|
|
40
|
+
* arrive as separate 1-char tokens. See the text-token branch below.
|
|
41
|
+
*/
|
|
42
|
+
isPasteToken: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* One-shot hint emitted when a paste is dropped because the focused panel has
|
|
45
|
+
* no text capture (Invariant A: focus must not silently flip to the composer).
|
|
46
|
+
*/
|
|
47
|
+
onPasteDropped?: (panelName: string) => void;
|
|
48
|
+
/** Wall-clock time (ms) for this token; `burstGuard` is DEBT-5 item 5's flood-guard state, mutated in place across tokens (see panel-paste-flood-guard.ts). */
|
|
49
|
+
now: number;
|
|
50
|
+
burstGuard: PanelBurstGuardState;
|
|
51
|
+
/**
|
|
52
|
+
* True while a turn is actively streaming; gates Escape's cancel-first
|
|
53
|
+
* precedence (I6.1) ahead of the panel's own two-stage escape contract
|
|
39
54
|
* below.
|
|
40
55
|
*/
|
|
41
|
-
|
|
56
|
+
isTurnActive: () => boolean;
|
|
57
|
+
/** Cancel the active turn. Wired to commandContext.cancelGeneration. */
|
|
58
|
+
cancelGeneration: () => void;
|
|
42
59
|
};
|
|
43
60
|
|
|
44
61
|
export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputToken): {
|
|
@@ -75,6 +92,18 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
75
92
|
// (e.g. dismiss a confirm dialog or clear search). Only unfocus if the
|
|
76
93
|
// panel returns false (unconsumed) or there is no active panel.
|
|
77
94
|
if (token.logicalName === 'escape') {
|
|
95
|
+
// I6.1/replay R5: while streaming, a focused panel must not swallow the
|
|
96
|
+
// only way to cancel — Escape's first job is always cancel-turn; the
|
|
97
|
+
// panel's own two-stage consume-or-unfocus contract only runs once no
|
|
98
|
+
// turn is active, so a *second* Escape falls through to it normally
|
|
99
|
+
// (panelFocused stays unchanged here). Calls cancelGeneration()
|
|
100
|
+
// directly rather than the shared handleEscape() (handler-modal-stack.ts),
|
|
101
|
+
// whose fallthrough order doesn't guarantee reaching it first.
|
|
102
|
+
if (state.isTurnActive()) {
|
|
103
|
+
state.cancelGeneration();
|
|
104
|
+
state.requestRender();
|
|
105
|
+
return { handled: true, panelFocused };
|
|
106
|
+
}
|
|
78
107
|
const activePanel = state.panelManager.getActive();
|
|
79
108
|
const panelConsumedEscape = activePanel?.handleInput?.('escape') ?? false;
|
|
80
109
|
if (panelConsumedEscape) {
|
|
@@ -88,13 +117,10 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
88
117
|
}
|
|
89
118
|
const kb = state.keybindingsManager;
|
|
90
119
|
// NOTE: panel-tab-next/prev, panel-close, and panel-close-all are handled
|
|
91
|
-
// globally in handleGlobalShortcutToken
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// and close/close-all semantics now live in exactly one place. Only
|
|
96
|
-
// panel-focus-toggle stays here, because it is meaningful only while the
|
|
97
|
-
// panel workspace already owns focus (it swaps between the two panes).
|
|
120
|
+
// globally in handleGlobalShortcutToken (runs earlier in the feed loop) —
|
|
121
|
+
// their old, drifted copies here were unreachable and are gone. Only
|
|
122
|
+
// panel-focus-toggle stays, since it's meaningful only once the panel
|
|
123
|
+
// workspace already owns focus (it swaps between the two panes).
|
|
98
124
|
if (kb.matches('panel-focus-toggle', token)) {
|
|
99
125
|
// Switch keyboard focus between the top and bottom panes (no-op when
|
|
100
126
|
// there is no visible, non-empty bottom pane).
|
|
@@ -122,19 +148,32 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
122
148
|
|
|
123
149
|
if (token.type === 'text' && token.value) {
|
|
124
150
|
const activePanel = state.panelManager.getActive();
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// hotkeys leaves the user with no echo and no error, looking exactly
|
|
128
|
-
// like dead keystrokes. Unless the active panel has its own text-capture
|
|
129
|
-
// buffer open (a `/`-search or draft-answer field that deliberately
|
|
130
|
-
// wants every character, burst or not — see isCapturingTextBurst), unfocus
|
|
131
|
-
// the panel and let the token fall through to the prompt route instead,
|
|
132
|
-
// same as it would from an unfocused panel.
|
|
133
|
-
if (state.isPrintableBurst && !activePanel?.isCapturingTextBurst?.()) {
|
|
151
|
+
// UX-C: '/' is an explicit transfer verb back to the composer (a capturing panel, isCapturingTextBurst, keeps '/' for itself).
|
|
152
|
+
if (token.value === '/' && !activePanel?.isCapturingTextBurst?.()) {
|
|
134
153
|
panelFocused = false;
|
|
135
|
-
state.requestRender();
|
|
136
154
|
return { handled: false, panelFocused };
|
|
137
155
|
}
|
|
156
|
+
// Invariant A/B (W6.2): a paste (isPasteToken, one multi-char token) into
|
|
157
|
+
// a capturing panel forwards verbatim below; elsewhere it is DROPPED with
|
|
158
|
+
// a one-shot hint (never exploded into hotkeys, never a silent focus
|
|
159
|
+
// flip — focus moves only on an explicit verb). Discrete keystrokes fall
|
|
160
|
+
// through to the per-char dispatch below.
|
|
161
|
+
if (state.isPasteToken && !activePanel?.isCapturingTextBurst?.()) {
|
|
162
|
+
state.onPasteDropped?.(activePanel?.name ?? 'the panel');
|
|
163
|
+
state.requestRender();
|
|
164
|
+
return { handled: true, panelFocused };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// DEBT-5 item 5 — paste flood guard (rate-based; see panel-paste-flood-guard.ts).
|
|
168
|
+
if (!state.isPasteToken && !activePanel?.isCapturingTextBurst?.()) {
|
|
169
|
+
const guard = trackPanelPasteFloodGuard(state.burstGuard, state.now);
|
|
170
|
+
if (!guard.dispatch) {
|
|
171
|
+
if (guard.showHintNow) state.onPasteDropped?.(activePanel?.name ?? 'the panel');
|
|
172
|
+
state.requestRender();
|
|
173
|
+
return { handled: true, panelFocused };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
if (activePanel?.handleInput) {
|
|
139
178
|
for (const ch of token.value) {
|
|
140
179
|
activePanel.handleInput(ch);
|
|
@@ -150,7 +189,8 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
150
189
|
export type IndicatorFocusRouteState = {
|
|
151
190
|
indicatorFocused: boolean;
|
|
152
191
|
modalOpened: (name: string) => void;
|
|
153
|
-
|
|
192
|
+
/** W2.2: the footer process indicator's [Enter] opens the Fleet panel (F2 also opens the Fleet panel; the retired process modal was removed in W6.2). */
|
|
193
|
+
openFleetPanel: () => void;
|
|
154
194
|
requestRender: () => void;
|
|
155
195
|
};
|
|
156
196
|
|
|
@@ -171,8 +211,8 @@ export function handleIndicatorFocusToken(state: IndicatorFocusRouteState, token
|
|
|
171
211
|
}
|
|
172
212
|
if (token.logicalName === 'enter') {
|
|
173
213
|
indicatorFocused = false;
|
|
174
|
-
|
|
175
|
-
state.
|
|
214
|
+
// W2.2: opens the Fleet panel (see openFleetPanel's own doc, line ~352).
|
|
215
|
+
state.openFleetPanel();
|
|
176
216
|
state.requestRender();
|
|
177
217
|
return { handled: true, indicatorFocused };
|
|
178
218
|
}
|
|
@@ -297,7 +337,8 @@ export type KeyRouteState = {
|
|
|
297
337
|
commandRegistry?: CommandRegistry | null;
|
|
298
338
|
autocomplete: AutocompleteEngine | null;
|
|
299
339
|
blockActionsMenu: { open: (block: BlockMeta) => void };
|
|
300
|
-
|
|
340
|
+
/** W6.2 e: F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
|
|
341
|
+
openFleetPanel: () => void;
|
|
301
342
|
modalOpened: (name: string) => void;
|
|
302
343
|
saveUndoState: () => void;
|
|
303
344
|
/** Break the undo coalescing group (call on cursor moves). */
|
|
@@ -568,10 +609,12 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
568
609
|
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
569
610
|
}
|
|
570
611
|
|
|
612
|
+
// UX-C: F2 is now handled globally with toggle semantics (handler-shortcuts.ts)
|
|
613
|
+
// BEFORE it ever reaches this route; this branch is dead in the real
|
|
614
|
+
// pipeline but stays for panel-entry-points-reachable.test.ts's direct test.
|
|
571
615
|
if (token.logicalName === 'f2') {
|
|
572
616
|
indicatorFocused = false;
|
|
573
|
-
state.
|
|
574
|
-
state.processModal.open();
|
|
617
|
+
state.openFleetPanel();
|
|
575
618
|
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
576
619
|
}
|
|
577
620
|
|
|
@@ -594,89 +637,6 @@ export type MouseRouteState = {
|
|
|
594
637
|
handleCopy: () => void;
|
|
595
638
|
};
|
|
596
639
|
|
|
597
|
-
export type PanelMouseLayout = {
|
|
598
|
-
x: number;
|
|
599
|
-
y: number;
|
|
600
|
-
width: number;
|
|
601
|
-
height: number;
|
|
602
|
-
hasBottomPane: boolean;
|
|
603
|
-
verticalSplitRatio: number;
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
function clampRatio(value: number): number {
|
|
607
|
-
return Math.max(0.2, Math.min(0.8, value));
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function getActivePanelInPane(panelManager: PanelManager, pane: 'top' | 'bottom') {
|
|
611
|
-
const target = pane === 'top' ? panelManager.getTopPane() : panelManager.getBottomPane();
|
|
612
|
-
return target.panels[target.activeIndex] ?? null;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function getPanelUnderMouse(
|
|
616
|
-
panelManager: PanelManager,
|
|
617
|
-
layout: PanelMouseLayout | null,
|
|
618
|
-
row: number,
|
|
619
|
-
col: number,
|
|
620
|
-
) {
|
|
621
|
-
if (
|
|
622
|
-
layout === null
|
|
623
|
-
|| !panelManager.isVisible()
|
|
624
|
-
|| panelManager.getAllOpen().length === 0
|
|
625
|
-
|| col < layout.x
|
|
626
|
-
|| col >= layout.x + layout.width
|
|
627
|
-
|| row < layout.y
|
|
628
|
-
|| row >= layout.y + layout.height
|
|
629
|
-
) {
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const panelRow = row - layout.y;
|
|
634
|
-
if (!layout.hasBottomPane) {
|
|
635
|
-
return getActivePanelInPane(panelManager, 'top');
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Single consolidated workspace bar (row 0) + h-separator; the rest splits
|
|
639
|
-
// between the two panes' content.
|
|
640
|
-
const panelAreaRows = Math.max(0, layout.height - 1);
|
|
641
|
-
const contentRows = Math.max(0, panelAreaRows - 1);
|
|
642
|
-
const topContentRows = contentRows <= 1
|
|
643
|
-
? contentRows
|
|
644
|
-
: Math.max(1, Math.floor(contentRows * clampRatio(layout.verticalSplitRatio)));
|
|
645
|
-
// panelRow 0 = workspace bar; rows 1..topContentRows = top pane; rest = bottom.
|
|
646
|
-
return panelRow <= topContentRows
|
|
647
|
-
? getActivePanelInPane(panelManager, 'top')
|
|
648
|
-
: getActivePanelInPane(panelManager, 'bottom');
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* If the mouse is over the consolidated workspace tab bar (the first panel
|
|
653
|
-
* row), return the index of the tab under the cursor, else null. Recomputes the
|
|
654
|
-
* tab hit regions by rendering the bar with a layout callback — cheap and keeps
|
|
655
|
-
* the click geometry in lockstep with what was drawn.
|
|
656
|
-
*/
|
|
657
|
-
function workspaceTabAtMouse(
|
|
658
|
-
panelManager: PanelManager,
|
|
659
|
-
layout: PanelMouseLayout | null,
|
|
660
|
-
row: number,
|
|
661
|
-
col: number,
|
|
662
|
-
): number | null {
|
|
663
|
-
if (
|
|
664
|
-
layout === null
|
|
665
|
-
|| !panelManager.isVisible()
|
|
666
|
-
|| panelManager.getAllOpen().length === 0
|
|
667
|
-
|| row !== layout.y // workspace bar is the first panel row
|
|
668
|
-
|| col < layout.x
|
|
669
|
-
|| col >= layout.x + layout.width
|
|
670
|
-
) {
|
|
671
|
-
return null;
|
|
672
|
-
}
|
|
673
|
-
let regions: readonly TabHitRegion[] = [];
|
|
674
|
-
renderPanelWorkspaceBar(panelManager.getWorkspaceTabs(), layout.width, true, (r) => { regions = r; });
|
|
675
|
-
const relCol = col - layout.x;
|
|
676
|
-
const hit = regions.find((rg) => relCol >= rg.startCol && relCol < rg.endCol);
|
|
677
|
-
return hit ? hit.index : null;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
640
|
function scrollPanelUnderMouse(
|
|
681
641
|
state: MouseRouteState,
|
|
682
642
|
token: Extract<InputToken, { type: 'mouse' }>,
|
|
@@ -9,15 +9,13 @@ import type { SelectionResult } from './selection-modal.ts';
|
|
|
9
9
|
import { SearchManager } from './search.ts';
|
|
10
10
|
import type { InputHistory, HistorySearch } from './input-history.ts';
|
|
11
11
|
import type { BlockMeta, ConversationManager } from '../core/conversation';
|
|
12
|
-
import { ProcessModal } from '../renderer/process-modal.ts';
|
|
13
|
-
import { LiveTailModal } from '../renderer/live-tail-modal.ts';
|
|
14
12
|
import { BlockActionsMenu } from '../renderer/block-actions.ts';
|
|
15
|
-
import { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
|
|
16
13
|
import { ContextInspectorModal } from '../renderer/context-inspector.ts';
|
|
17
14
|
import { BookmarkModal } from './bookmark-modal.ts';
|
|
18
15
|
import { SettingsModal } from './settings-modal.ts';
|
|
19
16
|
import type { McpWorkspace } from './mcp-workspace.ts';
|
|
20
17
|
import { SessionPickerModal } from './session-picker-modal.ts';
|
|
18
|
+
import type { ConfigModal } from './config-modal.ts';
|
|
21
19
|
import { ProfilePickerModal } from './profile-picker-modal.ts';
|
|
22
20
|
import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
|
|
23
21
|
import type { OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
|
|
@@ -34,6 +32,7 @@ import {
|
|
|
34
32
|
handlePromptTextToken,
|
|
35
33
|
type PanelMouseLayout,
|
|
36
34
|
} from './handler-feed-routes.ts';
|
|
35
|
+
import type { PanelBurstGuardState } from './panel-paste-flood-guard.ts';
|
|
37
36
|
import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
|
|
38
37
|
import { handleModalTokenRoutes } from './handler-modal-token-routes.ts';
|
|
39
38
|
import { handleCommandModeToken } from './handler-command-route.ts';
|
|
@@ -45,6 +44,7 @@ import type { PanelManager } from '../panels/panel-manager.ts';
|
|
|
45
44
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
46
45
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
47
46
|
import type { KillRing } from './kill-ring.ts';
|
|
47
|
+
import type { FocusTracker } from '../core/focus-tracker.ts';
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* InputFeedContext — The single long-lived context object passed to feedInputTokens
|
|
@@ -72,8 +72,8 @@ import type { KillRing } from './kill-ring.ts';
|
|
|
72
72
|
* - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
|
|
73
73
|
* - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
|
|
74
74
|
* `profilePickerModal` — modal objects constructed once in InputHandler constructor
|
|
75
|
-
* - `filePicker`, `modelPicker`, `onboardingWizard`,
|
|
76
|
-
* `
|
|
75
|
+
* - `filePicker`, `modelPicker`, `onboardingWizard`,
|
|
76
|
+
* `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
|
|
77
77
|
* service objects constructed once
|
|
78
78
|
* - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
|
|
79
79
|
* - `modalStack` — reference to the handler's shared array (mutated in place)
|
|
@@ -111,6 +111,7 @@ export interface InputFeedContext {
|
|
|
111
111
|
readonly mcpWorkspace: McpWorkspace;
|
|
112
112
|
readonly sessionPickerModal: SessionPickerModal;
|
|
113
113
|
readonly profilePickerModal: ProfilePickerModal;
|
|
114
|
+
readonly configModal: ConfigModal;
|
|
114
115
|
readonly historySearch: HistorySearch;
|
|
115
116
|
commandRegistry: CommandRegistry | null;
|
|
116
117
|
commandContext: CommandContext | undefined;
|
|
@@ -118,9 +119,6 @@ export interface InputFeedContext {
|
|
|
118
119
|
readonly filePicker: FilePickerModal;
|
|
119
120
|
readonly modelPicker: ModelPickerModal;
|
|
120
121
|
readonly onboardingWizard: OnboardingWizardController;
|
|
121
|
-
readonly processModal: ProcessModal;
|
|
122
|
-
readonly liveTailModal: LiveTailModal;
|
|
123
|
-
readonly agentDetailModal: AgentDetailModal;
|
|
124
122
|
readonly contextInspectorModal: ContextInspectorModal;
|
|
125
123
|
readonly blockActionsMenu: BlockActionsMenu;
|
|
126
124
|
readonly searchManager: SearchManager;
|
|
@@ -131,6 +129,10 @@ export interface InputFeedContext {
|
|
|
131
129
|
inputHistory: InputHistory | null;
|
|
132
130
|
conversationManager: ConversationManager | null;
|
|
133
131
|
readonly killRing: KillRing;
|
|
132
|
+
/** Terminal focus tracker (W2.3) — updated here from 'focus' tokens, read by the unfocused-alert notifiers in core/. */
|
|
133
|
+
readonly focusTracker: FocusTracker;
|
|
134
|
+
/** DEBT-5 item 5 — the paste-flood guard's persistent state, mutated in place across tokens by handlePanelFocusToken (see panel-paste-flood-guard.ts). Never reallocated. */
|
|
135
|
+
readonly panelBurstGuard: PanelBurstGuardState;
|
|
134
136
|
readonly getHistory: () => InfiniteBuffer;
|
|
135
137
|
readonly getViewportHeight: () => number;
|
|
136
138
|
readonly getScrollTop: () => number;
|
|
@@ -180,17 +182,39 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
180
182
|
const lineCount = history.getLineCount();
|
|
181
183
|
const keybindings = context.keybindingsManager;
|
|
182
184
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
// Shared opener for the Fleet panel: makes it visible AND transfers keyboard
|
|
186
|
+
// focus to it. panelManager.open() only makes the panel active — focus is a
|
|
187
|
+
// separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
|
|
188
|
+
// focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
|
|
189
|
+
// the footer indicator's [Enter] and by F2 (W6.2 e: F2 and the footer
|
|
190
|
+
// indicator both subsume the retired process modal). Mirrors the Ctrl+P
|
|
191
|
+
// panel-picker launcher (ui-openers.ts openPanelPicker).
|
|
192
|
+
const openFleetPanel = (): void => {
|
|
193
|
+
context.panelManager.open('fleet');
|
|
194
|
+
context.panelManager.focusPanels();
|
|
195
|
+
context.panelFocused = true;
|
|
196
|
+
};
|
|
192
197
|
|
|
198
|
+
// Paste-ness is a per-TOKEN property, computed at the handlePanelFocusToken
|
|
199
|
+
// call below — never a per-feed character sum. The SDK tokenizer emits a
|
|
200
|
+
// bracketed paste (\x1b[?2004h, enabled in main.ts terminal init) as ONE
|
|
201
|
+
// 'text' token holding the whole payload, while discrete keystrokes — even
|
|
202
|
+
// several batched into one feed() by render-tick latency — arrive as
|
|
203
|
+
// separate 1-char 'text' tokens. The old per-feed sum misread two quick nav
|
|
204
|
+
// keystrokes (e.g. j then k in one drain) as a "burst" and yanked focus.
|
|
205
|
+
// DEBT-5 item 5's flood guard reuses that same per-token model but adds
|
|
206
|
+
// real timing: one `now` per feed() call (not per token) is intentional —
|
|
207
|
+
// a genuine flood delivers many tokens in one drain, and they should all
|
|
208
|
+
// measure as arriving "at once", not spread across meaningless sub-ms noise.
|
|
209
|
+
const now = Date.now();
|
|
193
210
|
for (const token of tokens) {
|
|
211
|
+
// Focus-reporting tokens (CSI ?1004h, W2.3) never reach the composer or any
|
|
212
|
+
// modal route — consumed here, first, unconditionally. No render needed.
|
|
213
|
+
if (token.type === 'focus') {
|
|
214
|
+
context.focusTracker.setFocused(token.action === 'in');
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
194
218
|
if (token.type === 'key' && context.keybindingsManager.matches('clear-cancel', token)) {
|
|
195
219
|
context.handleCtrlC();
|
|
196
220
|
continue;
|
|
@@ -210,6 +234,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
210
234
|
mcpWorkspace: context.mcpWorkspace,
|
|
211
235
|
sessionPickerModal: context.sessionPickerModal,
|
|
212
236
|
profilePickerModal: context.profilePickerModal,
|
|
237
|
+
configModal: context.configModal,
|
|
213
238
|
onboardingWizard: context.onboardingWizard,
|
|
214
239
|
helpOverlayActive: context.helpOverlayActive,
|
|
215
240
|
helpScrollOffset: context.helpScrollOffset,
|
|
@@ -224,9 +249,6 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
224
249
|
getViewportHeight: context.getViewportHeight,
|
|
225
250
|
requestRender: context.requestRender,
|
|
226
251
|
handleEscape: context.handleEscape,
|
|
227
|
-
liveTailModal: context.liveTailModal,
|
|
228
|
-
processModal: context.processModal,
|
|
229
|
-
agentDetailModal: context.agentDetailModal,
|
|
230
252
|
contextInspectorModal: context.contextInspectorModal,
|
|
231
253
|
modalOpened: context.modalOpened,
|
|
232
254
|
filePicker: context.filePicker,
|
|
@@ -265,6 +287,12 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
265
287
|
// focusTarget and self-heals it, and context.panelFocused was seeded from
|
|
266
288
|
// it at feed entry — so no manual "unfocus if panels vanished" patch is
|
|
267
289
|
// needed here anymore.
|
|
290
|
+
// Snapshot these four BEFORE dispatch so the write-back below can tell
|
|
291
|
+
// a pipeline-driven change (see comment there) from a stale copy.
|
|
292
|
+
const promptBefore = context.prompt;
|
|
293
|
+
const cursorPosBefore = context.cursorPos;
|
|
294
|
+
const commandModeBefore = context.commandMode;
|
|
295
|
+
const panelFocusedBefore = context.panelFocused;
|
|
268
296
|
const shortcutState = {
|
|
269
297
|
panelFocused: context.panelFocused,
|
|
270
298
|
prompt: context.prompt,
|
|
@@ -298,10 +326,22 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
298
326
|
killRing: context.killRing,
|
|
299
327
|
};
|
|
300
328
|
if (handleGlobalShortcutToken(shortcutState, token, viewportHeight)) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
context
|
|
304
|
-
context.
|
|
329
|
+
// Some branches (handleEscape, handleCtrlC, handleUndo, handleRedo,
|
|
330
|
+
// handlePaste) mutate handler state directly and immediately sync it
|
|
331
|
+
// into this same `context` via syncFeedContextMutableFields — e.g.
|
|
332
|
+
// Escape closing the slash-command palette clears context.prompt and
|
|
333
|
+
// context.commandMode on the live handler mid-call. `shortcutState`
|
|
334
|
+
// above is a snapshot taken BEFORE that call, so applying it
|
|
335
|
+
// unconditionally would stomp the live update right back to its
|
|
336
|
+
// stale pre-action value (undoing the very close it just performed,
|
|
337
|
+
// and even re-arming the palette below). This is the same class of
|
|
338
|
+
// bug as the overlay-flag write-back guard in handler.ts's feed().
|
|
339
|
+
// Only apply the snapshot for a field the dispatched action did NOT
|
|
340
|
+
// already update out from under us.
|
|
341
|
+
context.prompt = context.prompt === promptBefore ? shortcutState.prompt : context.prompt;
|
|
342
|
+
context.cursorPos = context.cursorPos === cursorPosBefore ? shortcutState.cursorPos : context.cursorPos;
|
|
343
|
+
context.commandMode = context.commandMode === commandModeBefore ? shortcutState.commandMode : context.commandMode;
|
|
344
|
+
context.panelFocused = context.panelFocused === panelFocusedBefore ? shortcutState.panelFocused : context.panelFocused;
|
|
305
345
|
if (context.commandMode) {
|
|
306
346
|
if (!context.prompt.startsWith('/')) {
|
|
307
347
|
context.commandMode = false;
|
|
@@ -330,7 +370,19 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
330
370
|
panelManager: context.panelManager,
|
|
331
371
|
keybindingsManager: context.keybindingsManager,
|
|
332
372
|
onPanelInputConsumed: context.onPanelInputConsumed,
|
|
333
|
-
|
|
373
|
+
now,
|
|
374
|
+
burstGuard: context.panelBurstGuard,
|
|
375
|
+
// Per-token paste classification (Invariant B): a paste is a single
|
|
376
|
+
// 'text' token whose value holds more than one character.
|
|
377
|
+
isPasteToken: token.type === 'text' && token.value.length > 1,
|
|
378
|
+
// One-shot honesty hint when a paste is dropped into a non-capturing
|
|
379
|
+
// focused panel (Invariant A: focus never silently flips to the composer).
|
|
380
|
+
onPasteDropped: (panelName: string) =>
|
|
381
|
+
context.commandContext?.print(
|
|
382
|
+
`paste ignored — focus is on ${panelName}; Tab returns to composer`,
|
|
383
|
+
),
|
|
384
|
+
isTurnActive: () => context.commandContext?.isGenerating?.() ?? false,
|
|
385
|
+
cancelGeneration: () => context.commandContext?.cancelGeneration?.(),
|
|
334
386
|
}, token);
|
|
335
387
|
context.panelFocused = panelRoute.panelFocused;
|
|
336
388
|
if (panelRoute.handled) {
|
|
@@ -340,7 +392,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
340
392
|
const indicatorRoute = handleIndicatorFocusToken({
|
|
341
393
|
indicatorFocused: context.indicatorFocused,
|
|
342
394
|
modalOpened: context.modalOpened,
|
|
343
|
-
|
|
395
|
+
openFleetPanel,
|
|
344
396
|
requestRender: context.requestRender,
|
|
345
397
|
}, token);
|
|
346
398
|
context.indicatorFocused = indicatorRoute.indicatorFocused;
|
|
@@ -422,7 +474,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
422
474
|
commandRegistry: context.commandRegistry,
|
|
423
475
|
autocomplete: context.autocomplete,
|
|
424
476
|
blockActionsMenu: { open: (block: BlockMeta) => context.blockActionsMenu.open(block) },
|
|
425
|
-
|
|
477
|
+
openFleetPanel,
|
|
426
478
|
modalOpened: context.modalOpened,
|
|
427
479
|
saveUndoState: context.saveUndoState,
|
|
428
480
|
breakUndoCoalesce: context.breakUndoCoalesce,
|
|
@@ -225,14 +225,12 @@ export function handleEscapeForHandler(handler: InputHandler): void {
|
|
|
225
225
|
helpOverlayActive: handler.helpOverlayActive,
|
|
226
226
|
shortcutsOverlayActive: handler.shortcutsOverlayActive,
|
|
227
227
|
bookmarkModal: handler.bookmarkModal,
|
|
228
|
-
agentDetailModal: handler.agentDetailModal,
|
|
229
|
-
liveTailModal: handler.liveTailModal,
|
|
230
228
|
settingsModal: handler.settingsModal,
|
|
231
229
|
mcpWorkspace: handler.mcpWorkspace,
|
|
232
230
|
sessionPickerModal: handler.sessionPickerModal,
|
|
233
231
|
profilePickerModal: handler.profilePickerModal,
|
|
232
|
+
configModal: handler.configModal,
|
|
234
233
|
contextInspectorModal: handler.contextInspectorModal,
|
|
235
|
-
processModal: handler.processModal,
|
|
236
234
|
modelPicker: handler.modelPicker,
|
|
237
235
|
filePicker: handler.filePicker,
|
|
238
236
|
blockActionsMenu: handler.blockActionsMenu,
|