@pellux/goodvibes-tui 0.27.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 +23 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- 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/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.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 +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- 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 +31 -12
- package/src/panels/memory-panel.ts +5 -4
- 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 +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- 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 +40 -17
- 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 +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- 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 +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.28.0] — 2026-06-30
|
|
8
|
+
|
|
9
|
+
Panel navigation & UI/UX overhaul, plus the accumulated post-0.27.0 UX fixes.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Unified panel navigation**: a single consolidated tab bar with a focus-accent border; jump directly to any panel with `Alt+1`–`Alt+9`, click tabs, and toggle the active pane with `Ctrl+G`.
|
|
13
|
+
- **In-panel filtering**: press `/` to filter the list in panels that support it.
|
|
14
|
+
- **Mouse-wheel scrolling** on every panel (via `BasePanel.handleScroll`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Width-aware formatting** across panels: shared toolkit primitives (`fitDisplay`, table/tree/badge/meter helpers) replace hand-rolled `.slice()`/`.padEnd()`, so rows stay aligned across emoji, CJK, and other wide characters.
|
|
18
|
+
- **Scroll-back**: you can scroll up during and after a turn; the view only auto-follows the tail when parked at the bottom.
|
|
19
|
+
- **Auto-compaction is now owned entirely by the SDK** (`@pellux/goodvibes-sdk` 0.35.0 `handlePostTurnContextMaintenance`). The redundant TUI-side trigger was removed to prevent double-compaction; manual `/compact` now builds a richer handoff context (running agents, WRFC chains, active plan, session lineage).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Timer/async panels repaint while the main thread is idle instead of looking frozen (repaints gated on panel-active state).
|
|
23
|
+
- Input/keybinding papercuts: reverse-search accept, Home/End, and the command palette.
|
|
24
|
+
- Crash/signal safety net: the terminal is always restored on uncaught exceptions and termination signals.
|
|
25
|
+
- Consolidated number/duration/context-usage formatting and routed the context window through one corrected `getContextWindowForModel` source across every surface.
|
|
26
|
+
|
|
27
|
+
### Internal
|
|
28
|
+
- Split `polish.ts` into leaf `polish-core.ts` + `polish-tables.ts` and extracted `wrfc-panel-format.ts` to satisfy the 800-line architecture gate; no public API changes (all symbols re-exported).
|
|
29
|
+
|
|
7
30
|
## [0.27.0] — 2026-06-30
|
|
8
31
|
|
|
9
32
|
Full deep-review audit of the TUI (33 findings fixed, each reviewed to a score of 10) and adoption of `@pellux/goodvibes-sdk` 0.35.0.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context-window usage computation.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the tokens/contextWindow ratio so all panels, auto-compact,
|
|
5
|
+
* and session-maintenance read from one formula rather than six diverging
|
|
6
|
+
* hand-rolled copies.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: compaction-preview.ts intentionally displays usage > 100%
|
|
9
|
+
* ("exceeds window") — callers that need the unclamped value must use
|
|
10
|
+
* rawRatio, not clampedRatio / pct.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ContextUsage {
|
|
14
|
+
/**
|
|
15
|
+
* Raw (unclamped) ratio: max(0, tokens) / contextWindow.
|
|
16
|
+
* May exceed 1.0 when token count surpasses the context window.
|
|
17
|
+
* 0 when contextWindow <= 0.
|
|
18
|
+
*/
|
|
19
|
+
rawRatio: number;
|
|
20
|
+
/**
|
|
21
|
+
* Ratio clamped to [0, 1]. Use for progress-bar fill and color bands.
|
|
22
|
+
*/
|
|
23
|
+
clampedRatio: number;
|
|
24
|
+
/**
|
|
25
|
+
* Integer percentage clamped to [0, 100].
|
|
26
|
+
* Equivalent to Math.min(100, Math.round(rawRatio * 100)).
|
|
27
|
+
* Use for numeric displays and threshold comparisons.
|
|
28
|
+
*/
|
|
29
|
+
pct: number;
|
|
30
|
+
/**
|
|
31
|
+
* Remaining tokens: max(0, contextWindow - tokens).
|
|
32
|
+
* 0 when contextWindow <= 0.
|
|
33
|
+
*/
|
|
34
|
+
remaining: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute context-window usage metrics from raw token counts.
|
|
39
|
+
*
|
|
40
|
+
* @param tokens Current input-token count (negative values are treated as 0).
|
|
41
|
+
* @param contextWindow Model context window size (0 or negative → all fields return 0).
|
|
42
|
+
*/
|
|
43
|
+
export function computeContextUsage(tokens: number, contextWindow: number): ContextUsage {
|
|
44
|
+
if (contextWindow <= 0) {
|
|
45
|
+
return { rawRatio: 0, clampedRatio: 0, pct: 0, remaining: 0 };
|
|
46
|
+
}
|
|
47
|
+
const safeTokens = Math.max(0, tokens);
|
|
48
|
+
const rawRatio = safeTokens / contextWindow;
|
|
49
|
+
const clampedRatio = Math.min(1, rawRatio);
|
|
50
|
+
const pct = Math.min(100, Math.round(rawRatio * 100));
|
|
51
|
+
const remaining = Math.max(0, contextWindow - tokens);
|
|
52
|
+
return { rawRatio, clampedRatio, pct, remaining };
|
|
53
|
+
}
|
package/src/core/conversation.ts
CHANGED
|
@@ -110,7 +110,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
110
110
|
* Message index at the time of the last clearDisplay() call.
|
|
111
111
|
* rebuildHistory() renders only messages at or after this index, so the
|
|
112
112
|
* display stays blank for messages added before the clear while LLM history
|
|
113
|
-
* is fully preserved.
|
|
113
|
+
* is fully preserved. Persists until resetAll(). rebuildHistory() never resets this value.
|
|
114
114
|
*/
|
|
115
115
|
private _displayFromMessageIndex = 0;
|
|
116
116
|
|
|
@@ -404,8 +404,6 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
404
404
|
|
|
405
405
|
// When _displayFromMessageIndex > 0, clearDisplay() was called. Only render
|
|
406
406
|
// messages added after the clear — the pre-clear history stays off-screen.
|
|
407
|
-
// On a full rebuild (e.g. width change), reset the display-start to 0 so the
|
|
408
|
-
// user can scroll back to the full history if needed.
|
|
409
407
|
const displayStart = this._displayFromMessageIndex;
|
|
410
408
|
const visibleSnapshot = displayStart > 0 ? renderSnapshot.slice(displayStart) : renderSnapshot;
|
|
411
409
|
|
|
@@ -662,9 +660,9 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
662
660
|
// messageKindRegistry is NOT cleared here. The underlying messages array is
|
|
663
661
|
// preserved by clearDisplay(); kind entries for pre-clear messages are harmless
|
|
664
662
|
// because those messages are hidden by _displayFromMessageIndex and never rendered.
|
|
665
|
-
// Clearing the registry would cause kind loss for pre-clear messages
|
|
666
|
-
//
|
|
667
|
-
//
|
|
663
|
+
// Clearing the registry would cause kind loss for pre-clear messages; if
|
|
664
|
+
// resetAll() later restores displayStart to 0, those messages re-render
|
|
665
|
+
// and their kind entries must still be present to suppress navigation.
|
|
668
666
|
// Advance _displayFromMessageIndex to exclude all current messages from display.
|
|
669
667
|
// rebuildHistory() will only render messages added AFTER this point.
|
|
670
668
|
this._displayFromMessageIndex = this.getMessageSnapshot().length;
|
|
@@ -115,6 +115,8 @@ export function replayJournalIntoConversation(
|
|
|
115
115
|
conversation.fromJSON({
|
|
116
116
|
...preserved,
|
|
117
117
|
messages: replayedMessages as never[],
|
|
118
|
+
title: conversation.title,
|
|
119
|
+
titleSource: conversation.getTitleSource(),
|
|
118
120
|
});
|
|
119
121
|
conversation.rebuildHistory();
|
|
120
122
|
|
|
@@ -19,6 +19,7 @@ interface TurnOrchestrator {
|
|
|
19
19
|
/** Minimal provider registry surface required by turn-event wiring. */
|
|
20
20
|
interface TurnProviderRegistry {
|
|
21
21
|
getCurrentModel(): { readonly contextWindow: number };
|
|
22
|
+
getContextWindowForModel(model: { readonly contextWindow: number }): number;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** Minimal config manager surface required by turn-event wiring. */
|
|
@@ -109,8 +109,9 @@ async function handleCompare(args: string[], context: CommandContext): Promise<v
|
|
|
109
109
|
// ── /eval gate ────────────────────────────────────────────────────────────────
|
|
110
110
|
|
|
111
111
|
async function handleGate(args: string[], context: CommandContext): Promise<void> {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
112
|
+
const positionals = args.filter(a => !a.startsWith('--'));
|
|
113
|
+
const suiteName = positionals[0];
|
|
114
|
+
const baselineFile = positionals[1] ?? '.goodvibes/eval/baseline.json';
|
|
114
115
|
const saveFlag = args.includes('--save-baseline');
|
|
115
116
|
const projectRoot = requireShellPaths(context).workingDirectory;
|
|
116
117
|
|
|
@@ -141,7 +142,7 @@ async function handleGate(args: string[], context: CommandContext): Promise<void
|
|
|
141
142
|
context.print(formatGateResult(gate));
|
|
142
143
|
|
|
143
144
|
if (saveFlag || !baseline) {
|
|
144
|
-
const label =
|
|
145
|
+
const label = positionals[0] ?? 'latest';
|
|
145
146
|
const newBaseline = captureBaseline(label, [fresh]);
|
|
146
147
|
try {
|
|
147
148
|
await writeBaseline(baselineFile, newBaseline, projectRoot);
|
|
@@ -13,6 +13,8 @@ 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 { renderPanelWorkspaceBar } from '../renderer/panel-workspace-bar.ts';
|
|
17
|
+
import type { TabHitRegion } from '../renderer/tab-strip.ts';
|
|
16
18
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
17
19
|
import type { KillRing } from './kill-ring.ts';
|
|
18
20
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -84,6 +86,23 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
84
86
|
state.cyclePanelTab('prev');
|
|
85
87
|
return { handled: true, panelFocused };
|
|
86
88
|
}
|
|
89
|
+
if (kb.matches('panel-focus-toggle', token)) {
|
|
90
|
+
// Switch keyboard focus between the top and bottom panes (no-op when
|
|
91
|
+
// there is no visible, non-empty bottom pane).
|
|
92
|
+
state.panelManager.togglePaneFocus();
|
|
93
|
+
state.requestRender();
|
|
94
|
+
return { handled: true, panelFocused };
|
|
95
|
+
}
|
|
96
|
+
// Alt+1..9 — jump directly to the Nth workspace tab (across both panes).
|
|
97
|
+
// The tokenizer maps the Alt modifier onto `meta`, so an Alt-held digit
|
|
98
|
+
// arrives as { logicalName: '1'..'9', meta: true }. Gating on the modifier
|
|
99
|
+
// keeps plain digits reaching the focused panel.
|
|
100
|
+
if (token.meta && !token.ctrl && /^[1-9]$/.test(token.logicalName ?? '')) {
|
|
101
|
+
const index = Number(token.logicalName) - 1;
|
|
102
|
+
state.panelManager.activateWorkspaceIndex(index);
|
|
103
|
+
state.requestRender();
|
|
104
|
+
return { handled: true, panelFocused };
|
|
105
|
+
}
|
|
87
106
|
if (kb.matches('panel-close-all', token)) {
|
|
88
107
|
const pm = state.panelManager;
|
|
89
108
|
for (const p of pm.getAllOpen()) pm.close(p.id);
|
|
@@ -577,18 +596,48 @@ function getPanelUnderMouse(
|
|
|
577
596
|
return getActivePanelInPane(panelManager, 'top');
|
|
578
597
|
}
|
|
579
598
|
|
|
599
|
+
// Single consolidated workspace bar (row 0) + h-separator; the rest splits
|
|
600
|
+
// between the two panes' content.
|
|
580
601
|
const panelAreaRows = Math.max(0, layout.height - 1);
|
|
581
|
-
const contentRows = Math.max(0, panelAreaRows -
|
|
602
|
+
const contentRows = Math.max(0, panelAreaRows - 1);
|
|
582
603
|
const topContentRows = contentRows <= 1
|
|
583
604
|
? contentRows
|
|
584
605
|
: Math.max(1, Math.floor(contentRows * clampRatio(layout.verticalSplitRatio)));
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return panelRow <= topLastRow
|
|
606
|
+
// panelRow 0 = workspace bar; rows 1..topContentRows = top pane; rest = bottom.
|
|
607
|
+
return panelRow <= topContentRows
|
|
588
608
|
? getActivePanelInPane(panelManager, 'top')
|
|
589
609
|
: getActivePanelInPane(panelManager, 'bottom');
|
|
590
610
|
}
|
|
591
611
|
|
|
612
|
+
/**
|
|
613
|
+
* If the mouse is over the consolidated workspace tab bar (the first panel
|
|
614
|
+
* row), return the index of the tab under the cursor, else null. Recomputes the
|
|
615
|
+
* tab hit regions by rendering the bar with a layout callback — cheap and keeps
|
|
616
|
+
* the click geometry in lockstep with what was drawn.
|
|
617
|
+
*/
|
|
618
|
+
function workspaceTabAtMouse(
|
|
619
|
+
panelManager: PanelManager,
|
|
620
|
+
layout: PanelMouseLayout | null,
|
|
621
|
+
row: number,
|
|
622
|
+
col: number,
|
|
623
|
+
): number | null {
|
|
624
|
+
if (
|
|
625
|
+
layout === null
|
|
626
|
+
|| !panelManager.isVisible()
|
|
627
|
+
|| panelManager.getAllOpen().length === 0
|
|
628
|
+
|| row !== layout.y // workspace bar is the first panel row
|
|
629
|
+
|| col < layout.x
|
|
630
|
+
|| col >= layout.x + layout.width
|
|
631
|
+
) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
let regions: readonly TabHitRegion[] = [];
|
|
635
|
+
renderPanelWorkspaceBar(panelManager.getWorkspaceTabs(), layout.width, true, (r) => { regions = r; });
|
|
636
|
+
const relCol = col - layout.x;
|
|
637
|
+
const hit = regions.find((rg) => relCol >= rg.startCol && relCol < rg.endCol);
|
|
638
|
+
return hit ? hit.index : null;
|
|
639
|
+
}
|
|
640
|
+
|
|
592
641
|
function scrollPanelUnderMouse(
|
|
593
642
|
state: MouseRouteState,
|
|
594
643
|
token: Extract<InputToken, { type: 'mouse' }>,
|
|
@@ -634,6 +683,14 @@ export function handleMouseToken(state: MouseRouteState, token: InputToken): {
|
|
|
634
683
|
return { handled: true, mouseDownRow, mouseDownCol };
|
|
635
684
|
}
|
|
636
685
|
if (token.button === 0 && token.action === 'press') {
|
|
686
|
+
// Click on a workspace tab activates it (checked before starting a text
|
|
687
|
+
// selection so a tab click doesn't begin a drag-select).
|
|
688
|
+
const tabIndex = workspaceTabAtMouse(state.panelManager, state.panelMouseLayout, token.row, token.col);
|
|
689
|
+
if (tabIndex !== null) {
|
|
690
|
+
state.panelManager.activateWorkspaceIndex(tabIndex);
|
|
691
|
+
state.requestRender();
|
|
692
|
+
return { handled: true, mouseDownRow, mouseDownCol };
|
|
693
|
+
}
|
|
637
694
|
mouseDownRow = token.row;
|
|
638
695
|
mouseDownCol = token.col;
|
|
639
696
|
state.selection.startSelection(token.col, viewportRow, state.scrollTop, state.viewportHeight, state.lineCount);
|
|
@@ -296,6 +296,19 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
296
296
|
context.cursorPos = shortcutState.cursorPos;
|
|
297
297
|
context.commandMode = shortcutState.commandMode;
|
|
298
298
|
context.panelFocused = shortcutState.panelFocused;
|
|
299
|
+
if (context.commandMode) {
|
|
300
|
+
if (!context.prompt.startsWith('/')) {
|
|
301
|
+
context.commandMode = false;
|
|
302
|
+
context.autocomplete?.reset();
|
|
303
|
+
} else {
|
|
304
|
+
const q = context.prompt.slice(1);
|
|
305
|
+
if (q.indexOf(' ') === -1) {
|
|
306
|
+
context.autocomplete?.update(q);
|
|
307
|
+
} else {
|
|
308
|
+
context.autocomplete?.reset();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
299
312
|
continue;
|
|
300
313
|
}
|
|
301
314
|
}
|
|
@@ -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);
|
|
@@ -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;
|
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';
|
|
@@ -77,6 +77,8 @@ const COLOR = {
|
|
|
77
77
|
selected: '#ffee88',
|
|
78
78
|
dimmed: '#555566',
|
|
79
79
|
expandHint: '#445566',
|
|
80
|
+
stalled: '#f59e0b', // amber — stalled-agent warning
|
|
81
|
+
cursorBg: '#1a2233', // timeline cursor row background
|
|
80
82
|
} as const;
|
|
81
83
|
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
@@ -256,9 +258,11 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
256
258
|
width,
|
|
257
259
|
agents.length === 0 ? ' No agents running' : ' No agent selected',
|
|
258
260
|
agents.length === 0
|
|
259
|
-
? 'Spawn an agent to inspect its live and historical timeline.'
|
|
261
|
+
? 'Spawn an agent to inspect its live and historical timeline, tool calls, and output.'
|
|
260
262
|
: 'Press Tab to cycle through available agents and open one in the inspector.',
|
|
261
|
-
|
|
263
|
+
agents.length === 0
|
|
264
|
+
? [{ command: '/spawn <task>', summary: 'launch an agent, then Tab here to inspect its timeline' }]
|
|
265
|
+
: [{ command: 'Tab', summary: 'select the first available agent' }],
|
|
262
266
|
DEFAULT_PANEL_PALETTE,
|
|
263
267
|
),
|
|
264
268
|
},
|
|
@@ -274,7 +278,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
274
278
|
const now = Date.now();
|
|
275
279
|
const isStalled = this._isAgentStalled(rec, now);
|
|
276
280
|
if (isStalled) {
|
|
277
|
-
summaryLines.push(buildPanelLine(width, [[' STALLED',
|
|
281
|
+
summaryLines.push(buildPanelLine(width, [[' STALLED', COLOR.stalled], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
278
282
|
}
|
|
279
283
|
const allRows = this._getCachedRows();
|
|
280
284
|
if (allRows.length === 0) {
|
|
@@ -288,8 +292,12 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
288
292
|
lines: buildEmptyState(
|
|
289
293
|
width,
|
|
290
294
|
' No messages yet',
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
rec.status === 'running'
|
|
296
|
+
? 'The selected agent is running but has not emitted visible timeline entries yet — live output streams in as it works.'
|
|
297
|
+
: 'The selected agent has not emitted any visible timeline entries.',
|
|
298
|
+
rec.status === 'running'
|
|
299
|
+
? [{ command: 'c', summary: 'cancel this running agent if it appears stalled' }]
|
|
300
|
+
: [{ command: 'Tab', summary: 'cycle to another agent with timeline activity' }],
|
|
293
301
|
DEFAULT_PANEL_PALETTE,
|
|
294
302
|
),
|
|
295
303
|
},
|
|
@@ -397,9 +405,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
397
405
|
const elapsed = (rec.completedAt ?? now) - rec.startedAt;
|
|
398
406
|
const taskPreview = rec.task.split('\n')[0] ?? '';
|
|
399
407
|
const maxTask = Math.max(0, width - 40);
|
|
400
|
-
const taskDisplay = taskPreview
|
|
401
|
-
? taskPreview.slice(0, maxTask - 1) + '\u2026'
|
|
402
|
-
: taskPreview;
|
|
408
|
+
const taskDisplay = truncateDisplay(taskPreview, maxTask);
|
|
403
409
|
return buildPanelLine(width, [
|
|
404
410
|
[' Status ', DEFAULT_PANEL_PALETTE.label],
|
|
405
411
|
[rec.status.toUpperCase(), rec.status === 'running' ? DEFAULT_PANEL_PALETTE.good : rec.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
@@ -425,7 +431,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
425
431
|
row: DisplayRow,
|
|
426
432
|
isCursor: boolean,
|
|
427
433
|
): Line {
|
|
428
|
-
const bg = isCursor ?
|
|
434
|
+
const bg = isCursor ? COLOR.cursorBg : '';
|
|
429
435
|
const ts = shortTime(row.timestamp);
|
|
430
436
|
const { fg, prefix } = agentKindStyle(row.kind, COLOR);
|
|
431
437
|
const hint = row.hasDetail ? (row.expanded ? ' ▾' : ' ▸') : '';
|
|
@@ -527,9 +533,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
527
533
|
kind,
|
|
528
534
|
timestamp: msg.timestamp,
|
|
529
535
|
label: msg.from,
|
|
530
|
-
content: msg.content
|
|
531
|
-
? msg.content.slice(0, 197) + '\u2026'
|
|
532
|
-
: msg.content,
|
|
536
|
+
content: truncateDisplay(msg.content, 200),
|
|
533
537
|
expanded: false,
|
|
534
538
|
} satisfies TimelineEntry);
|
|
535
539
|
}
|
|
@@ -669,7 +673,8 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
669
673
|
if (!this.selectedAgentId) return;
|
|
670
674
|
const rec = this.deps.agentManager.getStatus(this.selectedAgentId);
|
|
671
675
|
if (!rec || AGENT_TERMINAL_STATUSES.has(rec.status)) return;
|
|
672
|
-
const
|
|
676
|
+
const firstLine = rec.task.split('\n')[0];
|
|
677
|
+
const label = firstLine ? truncateDisplay(firstLine, 40) : rec.id.slice(-8);
|
|
673
678
|
this.confirmCancel = { subject: rec.id, label };
|
|
674
679
|
this.markDirty();
|
|
675
680
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatDuration } from '../utils/format-duration.ts';
|
|
2
|
+
|
|
1
3
|
export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
|
|
2
4
|
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
@@ -61,9 +63,7 @@ export function agentStatusColor(status: string, colors: Record<string, string>)
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export function formatAgentDuration(ms: number): string {
|
|
64
|
-
|
|
65
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
66
|
-
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
66
|
+
return formatDuration(ms);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function formatAgentTime(ts: number): string {
|