@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -5,6 +5,7 @@ import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy,
|
|
|
5
5
|
import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
|
|
6
6
|
import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
|
|
7
7
|
import type { InputHandlerLike as InputHandler } from './handler-types.ts';
|
|
8
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
9
|
|
|
9
10
|
export function openOnboardingWizardForHandler(
|
|
10
11
|
handler: InputHandler,
|
|
@@ -49,7 +50,7 @@ export async function hydrateOnboardingWizardFromRuntimeForHandler(handler: Inpu
|
|
|
49
50
|
handler.requestRender();
|
|
50
51
|
} catch (error) {
|
|
51
52
|
if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
|
|
52
|
-
const message =
|
|
53
|
+
const message = summarizeError(error);
|
|
53
54
|
handler.onboardingWizard.failRuntimeHydration(message);
|
|
54
55
|
handler.commandContext?.print?.(`Onboarding runtime snapshot failed: ${message}`);
|
|
55
56
|
handler.requestRender();
|
|
@@ -3,6 +3,7 @@ import type { SelectionResult, SelectionAction } from './selection-modal.ts';
|
|
|
3
3
|
import type { CommandContext } from './command-registry.ts';
|
|
4
4
|
import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
|
|
5
5
|
import { isTextBackspace } from './delete-key-policy.ts';
|
|
6
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
|
|
7
8
|
type SelectionRouteState = {
|
|
8
9
|
selectionModal: {
|
|
@@ -285,7 +286,7 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
|
|
|
285
286
|
return;
|
|
286
287
|
}
|
|
287
288
|
void openTtsVoicePicker(state.commandContext).catch((error: unknown) => {
|
|
288
|
-
state.commandContext?.print(`Unable to list TTS voices: ${
|
|
289
|
+
state.commandContext?.print(`Unable to list TTS voices: ${summarizeError(error)}`);
|
|
289
290
|
state.requestRender();
|
|
290
291
|
});
|
|
291
292
|
return;
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getCloudflareSetupSource,
|
|
23
23
|
shouldShowCloudflareStep,
|
|
24
24
|
} from './onboarding/onboarding-wizard-cloudflare.ts';
|
|
25
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
25
26
|
|
|
26
27
|
type CloudflareOnboardingAction = Extract<OnboardingWizardAction,
|
|
27
28
|
| 'cloudflare-token-requirements'
|
|
@@ -44,7 +45,7 @@ function normalizeCloudflareActionError(error: unknown): string {
|
|
|
44
45
|
if (error instanceof CloudflareDaemonRouteError) {
|
|
45
46
|
return `${error.message} (HTTP ${error.status}, ${error.code})`;
|
|
46
47
|
}
|
|
47
|
-
return
|
|
48
|
+
return summarizeError(error);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function setCloudflareWizardStatusForHandler(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
|
-
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
|
|
3
|
+
import { summarizeError, openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
4
|
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
5
5
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
6
6
|
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
@@ -258,7 +258,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
258
258
|
deleteWizardProgress(handler.uiServices.environment.shellPaths);
|
|
259
259
|
} catch (markerError) {
|
|
260
260
|
handler.commandContext?.print?.(
|
|
261
|
-
`Onboarding check marker could not be written: ${
|
|
261
|
+
`Onboarding check marker could not be written: ${summarizeError(markerError)}`,
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
264
|
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
@@ -275,7 +275,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
275
275
|
severity: 'error',
|
|
276
276
|
title: 'Apply failed',
|
|
277
277
|
summary: 'The wizard could not persist these settings. No service restart was attempted.',
|
|
278
|
-
messages: [
|
|
278
|
+
messages: [summarizeError(error)],
|
|
279
279
|
});
|
|
280
280
|
return;
|
|
281
281
|
} finally {
|
|
@@ -346,7 +346,7 @@ export async function refreshOnboardingHydrationForHandler(handler: InputHandler
|
|
|
346
346
|
handler.requestRender();
|
|
347
347
|
} catch (error) {
|
|
348
348
|
if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
|
|
349
|
-
const message =
|
|
349
|
+
const message = summarizeError(error);
|
|
350
350
|
handler.onboardingWizard.failRuntimeHydration(message);
|
|
351
351
|
handler.requestRender();
|
|
352
352
|
}
|
|
@@ -405,7 +405,7 @@ export async function handleOpenAiSubscriptionStartForHandler(handler: InputHand
|
|
|
405
405
|
listener?.close();
|
|
406
406
|
handler.commandContext?.print?.([
|
|
407
407
|
'OpenAI subscription sign-in could not start.',
|
|
408
|
-
` ${
|
|
408
|
+
` ${summarizeError(error)}`,
|
|
409
409
|
].join('\n'));
|
|
410
410
|
handler.requestRender();
|
|
411
411
|
} finally {
|
|
@@ -449,7 +449,7 @@ export async function completeOpenAiSubscriptionFromListenerForHandler(
|
|
|
449
449
|
} catch (error) {
|
|
450
450
|
handler.commandContext?.print?.([
|
|
451
451
|
'OpenAI subscription listener could not complete automatically.',
|
|
452
|
-
` listener: ${
|
|
452
|
+
` listener: ${summarizeError(error)}`,
|
|
453
453
|
'Paste the callback code or URL into the OpenAI callback field to finish in onboarding.',
|
|
454
454
|
].join('\n'));
|
|
455
455
|
handler.requestRender();
|
|
@@ -501,7 +501,7 @@ export async function handleOpenAiSubscriptionFinishForHandler(handler: InputHan
|
|
|
501
501
|
} catch (error) {
|
|
502
502
|
handler.commandContext?.print?.([
|
|
503
503
|
'OpenAI subscription sign-in could not finish.',
|
|
504
|
-
` ${
|
|
504
|
+
` ${summarizeError(error)}`,
|
|
505
505
|
].join('\n'));
|
|
506
506
|
handler.requestRender();
|
|
507
507
|
} finally {
|
|
@@ -663,7 +663,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
663
663
|
return [{
|
|
664
664
|
id: 'runtime:activation-restart',
|
|
665
665
|
status: 'fail',
|
|
666
|
-
message: `Background services could not restart: ${
|
|
666
|
+
message: `Background services could not restart: ${summarizeError(error)}`,
|
|
667
667
|
target: 'service',
|
|
668
668
|
}];
|
|
669
669
|
}
|
|
@@ -98,13 +98,22 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
98
98
|
if (capModel) {
|
|
99
99
|
const rawInput = state.modelPicker.contextCapQuery.trim();
|
|
100
100
|
const parsedCap = rawInput.length > 0 ? parseInt(rawInput, 10) : null;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (parsedCap !== null && (parsedCap <= 0 || parsedCap > 10_000_000)) {
|
|
102
|
+
// Non-empty but out of valid range — surface notice; keep picker open for correction.
|
|
103
|
+
state.modelPicker.contextCapError = 'Context cap must be 1–10,000,000';
|
|
104
|
+
} else {
|
|
105
|
+
state.modelPicker.contextCapError = null;
|
|
106
|
+
const validCap = parsedCap !== null && parsedCap > 0 && parsedCap <= 10_000_000 ? parsedCap : null;
|
|
107
|
+
const effort = state.commandContext?.session.runtime.reasoningEffort ?? 'medium';
|
|
108
|
+
const handled = state.onModelPickerCommit?.() ?? false;
|
|
109
|
+
if (!handled) state.commandContext?.completeModelSelection?.({ model: capModel, effort, contextCap: validCap, target: state.modelPicker.target });
|
|
110
|
+
state.modelPicker.close();
|
|
111
|
+
if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
state.modelPicker.close();
|
|
115
|
+
if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
|
|
105
116
|
}
|
|
106
|
-
state.modelPicker.close();
|
|
107
|
-
if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
|
|
108
117
|
}
|
|
109
118
|
} else if (token.logicalName === 'up') {
|
|
110
119
|
if (state.modelPicker.focusPane === 'targets') {
|
|
@@ -158,14 +167,10 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
158
167
|
if (state.modelPicker.mode === 'contextCap') {
|
|
159
168
|
if (token.value.length === 1) state.modelPicker.appendContextCapChar(token.value);
|
|
160
169
|
} else if ((state.modelPicker.mode === 'model' || state.modelPicker.mode === 'provider') && state.modelPicker.searchFocused) {
|
|
170
|
+
// When search is focused every printable char — including space — goes to the query.
|
|
171
|
+
// The space=context-cap shortcut remains active only in the non-search branch below.
|
|
161
172
|
const ch = token.value;
|
|
162
|
-
if (ch === ' '
|
|
163
|
-
const selected = state.modelPicker.getSelected();
|
|
164
|
-
if (selected && state.modelPicker.isLocalModel(selected)) state.modelPicker.enterContextCapMode(selected);
|
|
165
|
-
else if (ch.length === 1 && ch >= ' ') state.modelPicker.appendChar(ch);
|
|
166
|
-
} else if (ch.length === 1 && ch >= ' ') {
|
|
167
|
-
state.modelPicker.appendChar(ch);
|
|
168
|
-
}
|
|
173
|
+
if (ch.length === 1 && ch >= ' ') state.modelPicker.appendChar(ch);
|
|
169
174
|
} else if (token.value === ' ' && state.modelPicker.mode === 'model') {
|
|
170
175
|
const selected = state.modelPicker.getSelected();
|
|
171
176
|
if (selected && state.modelPicker.isLocalModel(selected)) state.modelPicker.enterContextCapMode(selected);
|
|
@@ -345,7 +345,7 @@ export function handleHistorySearchToken(state: HistorySearchRouteState, token:
|
|
|
345
345
|
if (token.logicalName === 'escape' || (token.ctrl && token.logicalName === 'g')) {
|
|
346
346
|
state.prompt = state.historySearch.cancel();
|
|
347
347
|
state.cursorPos = state.prompt.length;
|
|
348
|
-
} else if (token.logicalName === 'return') {
|
|
348
|
+
} else if (token.logicalName === 'enter' || token.logicalName === 'return') {
|
|
349
349
|
state.prompt = state.historySearch.accept();
|
|
350
350
|
state.cursorPos = state.prompt.length;
|
|
351
351
|
} else if (token.logicalName === 'backspace') {
|
|
@@ -354,6 +354,10 @@ export function handleHistorySearchToken(state: HistorySearchRouteState, token:
|
|
|
354
354
|
state.historySearch.stepOlder();
|
|
355
355
|
} else if (token.ctrl && token.logicalName === 's') {
|
|
356
356
|
state.historySearch.stepNewer();
|
|
357
|
+
} else if (token.logicalName === 'up') {
|
|
358
|
+
state.historySearch.stepOlder();
|
|
359
|
+
} else if (token.logicalName === 'down') {
|
|
360
|
+
state.historySearch.stepNewer();
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
|
package/src/input/keybindings.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type KeyAction =
|
|
|
38
38
|
| 'panel-close-all'
|
|
39
39
|
| 'panel-tab-next'
|
|
40
40
|
| 'panel-tab-prev'
|
|
41
|
+
| 'panel-focus-toggle'
|
|
41
42
|
| 'history-search'
|
|
42
43
|
| 'search'
|
|
43
44
|
| 'block-copy'
|
|
@@ -69,6 +70,7 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
|
|
|
69
70
|
'panel-close-all': 'Close all open panels',
|
|
70
71
|
'panel-tab-next': 'Next workspace panel tab',
|
|
71
72
|
'panel-tab-prev': 'Previous workspace panel tab',
|
|
73
|
+
'panel-focus-toggle': 'Switch keyboard focus between top and bottom pane',
|
|
72
74
|
'history-search': 'Reverse input history search',
|
|
73
75
|
'search': 'Toggle conversation search',
|
|
74
76
|
'block-copy': 'Copy nearest block to clipboard',
|
|
@@ -101,6 +103,9 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
|
|
|
101
103
|
'panel-close-all': [{ key: 'x', ctrl: true, shift: true }],
|
|
102
104
|
'panel-tab-next': [{ key: ']', ctrl: true }],
|
|
103
105
|
'panel-tab-prev': [{ key: '[', ctrl: true }],
|
|
106
|
+
// Ctrl+G: toggle keyboard focus between the top and bottom panes. Ctrl+G is
|
|
107
|
+
// otherwise unbound in the default table.
|
|
108
|
+
'panel-focus-toggle': [{ key: 'g', ctrl: true }],
|
|
104
109
|
'history-search': [{ key: 'r', ctrl: true }],
|
|
105
110
|
'search': [{ key: 'f', ctrl: true }],
|
|
106
111
|
'block-copy': [{ key: 'y', ctrl: true }],
|
|
@@ -92,6 +92,8 @@ export class ModelPickerModal {
|
|
|
92
92
|
public contextCapPendingModel: ModelDefinition | null = null;
|
|
93
93
|
/** Current input string in contextCap mode. */
|
|
94
94
|
public contextCapQuery = '';
|
|
95
|
+
/** Validation error from the last context cap commit (e.g. out-of-range value); null when no error. */
|
|
96
|
+
public contextCapError: string | null = null;
|
|
95
97
|
|
|
96
98
|
// ── Search / filter ─────────────────────────────────────────────────────────────────────────────
|
|
97
99
|
/** Current search query string (empty = no filter). */
|
|
@@ -215,6 +217,7 @@ export class ModelPickerModal {
|
|
|
215
217
|
this.previousMode = 'model';
|
|
216
218
|
this.contextCapPendingModel = model;
|
|
217
219
|
this.contextCapQuery = '';
|
|
220
|
+
this.contextCapError = null;
|
|
218
221
|
this.mode = 'contextCap';
|
|
219
222
|
}
|
|
220
223
|
|
|
@@ -245,6 +248,7 @@ export class ModelPickerModal {
|
|
|
245
248
|
this.query = '';
|
|
246
249
|
this.categoryFilter = 'all';
|
|
247
250
|
this.capabilityFilter = 'none';
|
|
251
|
+
this.availableOnly = true;
|
|
248
252
|
const filtered = this.getFilteredModels();
|
|
249
253
|
const idx = filtered.findIndex(m => m.id === currentModelId);
|
|
250
254
|
this.selectedIndex = idx >= 0 ? idx : 0;
|
|
@@ -310,6 +314,7 @@ export class ModelPickerModal {
|
|
|
310
314
|
this.pendingModel = null;
|
|
311
315
|
this.contextCapPendingModel = null;
|
|
312
316
|
this.contextCapQuery = '';
|
|
317
|
+
this.contextCapError = null;
|
|
313
318
|
this.searchFocused = false;
|
|
314
319
|
this.selectedIndex = 0;
|
|
315
320
|
this.scrollOffset = 0;
|
|
@@ -41,6 +41,16 @@ export function handlePanelIntegrationAction(
|
|
|
41
41
|
): boolean {
|
|
42
42
|
if (!activePanel) return false;
|
|
43
43
|
|
|
44
|
+
// Prefer the panel's own integration hook when it provides one. Panels migrated
|
|
45
|
+
// onto the formal hook opt out of the instanceof routing below.
|
|
46
|
+
if (activePanel.handlePanelIntegrationAction) {
|
|
47
|
+
const consumed = activePanel.handlePanelIntegrationAction(key, {
|
|
48
|
+
panelManager,
|
|
49
|
+
executeCommand: commandContext?.executeCommand,
|
|
50
|
+
});
|
|
51
|
+
if (consumed) return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
if ((key === 'enter' || key === 'return' || key === 'right') && activePanel instanceof FileExplorerPanel) {
|
|
45
55
|
const filePath = activePanel.getFocusedFilePath();
|
|
46
56
|
if (!filePath) return false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
3
|
import type { SelectionItem } from './selection-modal.ts';
|
|
4
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
5
|
|
|
5
6
|
function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
|
|
6
7
|
const registry = ctx.platform.voiceProviderRegistry;
|
|
@@ -94,7 +95,7 @@ export async function openTtsVoicePicker(ctx: CommandContext, providerArg?: stri
|
|
|
94
95
|
});
|
|
95
96
|
return true;
|
|
96
97
|
} catch (error) {
|
|
97
|
-
ctx.print(`Unable to list TTS voices: ${
|
|
98
|
+
ctx.print(`Unable to list TTS voices: ${summarizeError(error)}`);
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
100
101
|
}
|
package/src/main.ts
CHANGED
|
@@ -29,7 +29,6 @@ import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
|
|
|
29
29
|
import { buildPanelCompositeData } from './renderer/panel-composite.ts';
|
|
30
30
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
31
31
|
import { registerBuiltinPanels } from './panels/builtin-panels.ts';
|
|
32
|
-
import { renderPanelTabBar } from './renderer/panel-tab-bar.ts';
|
|
33
32
|
import { bootstrapRuntime } from './runtime/bootstrap.ts';
|
|
34
33
|
import type { BootstrapContext } from './runtime/bootstrap.ts';
|
|
35
34
|
import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
|
|
@@ -52,6 +51,7 @@ import { renderToolCallBlock } from './renderer/tool-call.ts';
|
|
|
52
51
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
53
52
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
54
53
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
54
|
+
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
55
55
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
56
56
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
57
57
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
@@ -165,6 +165,9 @@ async function main() {
|
|
|
165
165
|
|
|
166
166
|
let scrollTop = 0;
|
|
167
167
|
let scrollLocked = true;
|
|
168
|
+
// Cached from the overlay-aware clamp the renderer computes (conversation-layout) each
|
|
169
|
+
// frame so scroll() clamps against exactly what is displayed, not a footer estimate.
|
|
170
|
+
let lastMaxScroll: number | null = null;
|
|
168
171
|
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
169
172
|
const streamMetrics: StreamMetrics = {
|
|
170
173
|
startTime: 0,
|
|
@@ -187,18 +190,23 @@ async function main() {
|
|
|
187
190
|
if (input.onboardingWizard.active) return stdout.rows || 24;
|
|
188
191
|
const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
|
|
189
192
|
const currentModel = providerRegistry.getCurrentModel();
|
|
190
|
-
|
|
193
|
+
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
194
|
+
return (stdout.rows || 24) - 2 - estimateShellFooterHeight(promptLines, contextWindow);
|
|
191
195
|
};
|
|
192
196
|
|
|
193
197
|
const scroll = (delta: number) => {
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
// Prefer the last clamp the renderer computed (overlay- and real-footer-aware).
|
|
199
|
+
// The footer estimate is only a fallback for the pre-first-render frame.
|
|
200
|
+
const maxScroll = lastMaxScroll ?? Math.max(0, conversation.history.getLineCount() - getViewportHeight());
|
|
196
201
|
scrollTop = Math.max(0, Math.min(scrollTop + delta, maxScroll));
|
|
197
202
|
// Re-lock if user scrolled to bottom, otherwise unlock
|
|
198
203
|
scrollLocked = scrollTop >= maxScroll;
|
|
199
204
|
};
|
|
200
205
|
|
|
201
206
|
const scrollToEnd = (vHeight: number) => {
|
|
207
|
+
// Respect a manual scroll-up by the user: only auto-follow the tail when parked at the
|
|
208
|
+
// bottom (scrollLocked). submitInput re-locks on new input so turns resume following.
|
|
209
|
+
if (!scrollLocked) return;
|
|
202
210
|
scrollTop = Math.max(0, conversation.history.getLineCount() - vHeight);
|
|
203
211
|
};
|
|
204
212
|
|
|
@@ -207,59 +215,31 @@ async function main() {
|
|
|
207
215
|
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
208
216
|
let recoveryPending = false;
|
|
209
217
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
render();
|
|
236
|
-
};
|
|
237
|
-
const resizeHandler = (): void => {
|
|
238
|
-
input.setContentWidth(getPromptContentWidth());
|
|
239
|
-
compositor.resetDiff();
|
|
240
|
-
render();
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const exitApp = (): void => {
|
|
244
|
-
stopSpokenOutputForExit?.();
|
|
245
|
-
unsubs.forEach(fn => fn());
|
|
246
|
-
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
|
|
247
|
-
ctx.shutdown({ ...snapshot, ...buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints()) }).catch((err) => {
|
|
248
|
-
logger.debug('ctx.shutdown error during exitApp (non-fatal)', { error: summarizeError(err) });
|
|
249
|
-
});
|
|
250
|
-
if (recoveryInterval !== null) { clearInterval(recoveryInterval); recoveryInterval = null; }
|
|
251
|
-
deleteRecoveryFile({ homeDirectory });
|
|
252
|
-
stdin.removeAllListeners('data');
|
|
253
|
-
stdout.removeListener('resize', resizeHandler);
|
|
254
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
255
|
-
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
256
|
-
const exitScreen = cli.flags.noAltScreen ? CLEAR_SCREEN : CLEAR_SCREEN + ALT_SCREEN_EXIT;
|
|
257
|
-
allowTerminalWrite(() => stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + CURSOR_SHOW + exitScreen));
|
|
258
|
-
terminalOutputGuard.dispose();
|
|
259
|
-
stdin.setRawMode(false);
|
|
260
|
-
process.exit(0);
|
|
261
|
-
};
|
|
262
|
-
|
|
218
|
+
const lifecycle = installProcessLifecycle({
|
|
219
|
+
stdin,
|
|
220
|
+
stdout,
|
|
221
|
+
ctx,
|
|
222
|
+
noAltScreen: cli.flags.noAltScreen,
|
|
223
|
+
ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
|
|
224
|
+
getInput: () => input,
|
|
225
|
+
render: () => render(),
|
|
226
|
+
getPromptContentWidth,
|
|
227
|
+
getTerminalOutputGuard: () => terminalOutputGuard,
|
|
228
|
+
buildSessionContinuityHints,
|
|
229
|
+
unsubs,
|
|
230
|
+
getRecoveryInterval: () => recoveryInterval,
|
|
231
|
+
setRecoveryInterval: (value) => { recoveryInterval = value; },
|
|
232
|
+
getStopSpokenOutputForExit: () => stopSpokenOutputForExit,
|
|
233
|
+
});
|
|
234
|
+
const {
|
|
235
|
+
exitApp,
|
|
236
|
+
resizeHandler,
|
|
237
|
+
sigintHandler,
|
|
238
|
+
unhandledRejectionHandler,
|
|
239
|
+
uncaughtExceptionHandler,
|
|
240
|
+
terminationSignalHandler,
|
|
241
|
+
exitListener,
|
|
242
|
+
} = lifecycle;
|
|
263
243
|
commandContext.exit = exitApp;
|
|
264
244
|
|
|
265
245
|
const spokenTurns = wireSpokenTurnRuntime({
|
|
@@ -456,6 +436,9 @@ async function main() {
|
|
|
456
436
|
|
|
457
437
|
// Cache the current model for consistent values across the entire render frame
|
|
458
438
|
const currentModel = providerRegistry.getCurrentModel();
|
|
439
|
+
// Resolve the effective context window (provider_api / configured_cap overrides) once,
|
|
440
|
+
// so the footer meter, footer-height, and context inspector agree with the Tokens panel.
|
|
441
|
+
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
459
442
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
460
443
|
const agentSnapshot = uiServices.readModels.agents.getSnapshot();
|
|
461
444
|
|
|
@@ -481,7 +464,7 @@ async function main() {
|
|
|
481
464
|
const maintenanceStatus = evaluateSessionMaintenance({
|
|
482
465
|
configManager,
|
|
483
466
|
currentTokens: orchestrator.lastInputTokens,
|
|
484
|
-
contextWindow
|
|
467
|
+
contextWindow,
|
|
485
468
|
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
486
469
|
});
|
|
487
470
|
const contextStatusHint = buildContextStatusHint({
|
|
@@ -505,7 +488,7 @@ async function main() {
|
|
|
505
488
|
toolCount: toolRegistry.list().length,
|
|
506
489
|
workingDir,
|
|
507
490
|
provider: runtime.provider,
|
|
508
|
-
contextWindow
|
|
491
|
+
contextWindow,
|
|
509
492
|
contextStatusHint,
|
|
510
493
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
511
494
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
@@ -586,6 +569,7 @@ async function main() {
|
|
|
586
569
|
overlayRows,
|
|
587
570
|
});
|
|
588
571
|
scrollTop = conversationViewport.nextScrollTop;
|
|
572
|
+
lastMaxScroll = conversationViewport.maxScroll;
|
|
589
573
|
let viewport = conversationViewport.viewport;
|
|
590
574
|
|
|
591
575
|
if (orchestrator.isThinking) {
|
|
@@ -628,7 +612,7 @@ async function main() {
|
|
|
628
612
|
keybindingsManager: ctx.services.keybindingsManager,
|
|
629
613
|
conversationWidth,
|
|
630
614
|
viewportHeight: vHeight,
|
|
631
|
-
contextWindow
|
|
615
|
+
contextWindow,
|
|
632
616
|
});
|
|
633
617
|
|
|
634
618
|
// Panel composite data
|
|
@@ -732,6 +716,14 @@ async function main() {
|
|
|
732
716
|
}
|
|
733
717
|
});
|
|
734
718
|
|
|
719
|
+
// Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a
|
|
720
|
+
// throw during terminal setup or the initial render still restores the terminal; the
|
|
721
|
+
// 'exit' listener is the final safety net for any process.exit path.
|
|
722
|
+
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
723
|
+
process.on('SIGTERM', terminationSignalHandler);
|
|
724
|
+
process.on('SIGHUP', terminationSignalHandler);
|
|
725
|
+
process.on('exit', exitListener);
|
|
726
|
+
|
|
735
727
|
// --- Terminal setup ---
|
|
736
728
|
stdin.setRawMode(true);
|
|
737
729
|
stdin.resume();
|