@pellux/goodvibes-tui 1.0.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -127
- package/README.md +32 -14
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +58 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +126 -8
- package/src/input/command-registry.ts +54 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +12 -0
- package/src/input/commands/session-workflow.ts +19 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-content-actions.ts +22 -8
- package/src/input/handler-feed-routes.ts +107 -7
- package/src/input/handler-feed.ts +49 -13
- package/src/input/handler-interactions.ts +3 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +3 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +10 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +41 -44
- package/src/panels/agent-inspector-shared.ts +35 -11
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +16 -62
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +63 -14
- package/src/panels/diff-panel.ts +123 -60
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/git-panel.ts +9 -9
- package/src/panels/index.ts +7 -31
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +26 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/turn-injection.ts +114 -0
- package/src/renderer/ui-factory.ts +91 -7
- package/src/runtime/bootstrap-command-context.ts +24 -1
- package/src/runtime/bootstrap-command-parts.ts +36 -7
- package/src/runtime/bootstrap-core.ts +19 -4
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +41 -17
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -233
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -609
- package/src/panels/docs-panel.ts +0 -375
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -664
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -506
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -557
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -721
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -306
- package/src/panels/provider-health-views.ts +0 -560
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -487
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -530
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -465
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -656
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
20
20
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
21
|
+
import { formatQualityScoreLine } from './compaction-quality.ts';
|
|
21
22
|
import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
22
23
|
import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
|
|
24
|
+
import type { CompactionQualityScore } from './compaction-quality.ts';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Default compaction totalCeiling from context-compaction DEFAULT_COMPACTION_CONFIG.
|
|
@@ -44,6 +46,12 @@ export interface CompactionAfterOptions {
|
|
|
44
46
|
readonly event: CompactionEvent;
|
|
45
47
|
/** Number of session memories that survived compaction. */
|
|
46
48
|
readonly pinnedMemoryCount: number;
|
|
49
|
+
/**
|
|
50
|
+
* Out-of-band quality-score grade for this run (W5.4/B28). Omitted or null
|
|
51
|
+
* when no score was computed (e.g. the small-window compaction path, which
|
|
52
|
+
* has no CompactionEvent to key a score by) — no line is rendered then.
|
|
53
|
+
*/
|
|
54
|
+
readonly qualityScore?: CompactionQualityScore | null | undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -82,7 +90,7 @@ export function buildCompactionPreview(opts: CompactionPreviewOptions): string {
|
|
|
82
90
|
* after token counts. The trigger field controls wording.
|
|
83
91
|
*/
|
|
84
92
|
export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string {
|
|
85
|
-
const { event, pinnedMemoryCount } = opts;
|
|
93
|
+
const { event, pinnedMemoryCount, qualityScore } = opts;
|
|
86
94
|
const {
|
|
87
95
|
messagesBeforeCompaction,
|
|
88
96
|
messagesAfterCompaction,
|
|
@@ -102,11 +110,14 @@ export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string
|
|
|
102
110
|
|
|
103
111
|
const triggerStr = trigger === 'auto' ? 'Auto-compact complete' : 'Compact complete';
|
|
104
112
|
|
|
113
|
+
const qualityStr = qualityScore ? `\n ${formatQualityScoreLine(qualityScore)}` : '';
|
|
114
|
+
|
|
105
115
|
return (
|
|
106
116
|
`[Context] ${triggerStr}: ${messagesBeforeCompaction} → ${messagesAfterCompaction} messages,` +
|
|
107
117
|
` ~${fmtN(tokensBeforeEstimate)} → ~${fmtN(tokensAfterEstimate)} tokens` +
|
|
108
118
|
` (saved ~${fmtN(savings)}, ${savingsPct}%).` +
|
|
109
|
-
(pinStr ? ` ${pinStr.trim()}` : '')
|
|
119
|
+
(pinStr ? ` ${pinStr.trim()}` : '') +
|
|
120
|
+
qualityStr
|
|
110
121
|
);
|
|
111
122
|
}
|
|
112
123
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction quality-score bridging (Wave-5 W5.4 / B28).
|
|
3
|
+
*
|
|
4
|
+
* Reality check (wo803): the SDK's `computeQualityScore()`
|
|
5
|
+
* (platform/runtime/compaction/quality-score.ts) is only ever called from a
|
|
6
|
+
* SEPARATE pipeline, `CompactionManager` (platform/runtime/compaction/manager.ts).
|
|
7
|
+
* The TUI's actual `/compact` path never instantiates `CompactionManager` — it
|
|
8
|
+
* goes through `platform/core/context-compaction.ts` via
|
|
9
|
+
* `ConversationManager.compact()` (see `compactConversation()` in
|
|
10
|
+
* runtime-services.ts), which has no scoring of its own. Adopting the whole
|
|
11
|
+
* `CompactionManager` pipeline just to get the score would be a far bigger
|
|
12
|
+
* change than this item calls for.
|
|
13
|
+
*
|
|
14
|
+
* Instead, this module calls `computeQualityScore()` directly and out-of-band,
|
|
15
|
+
* fed by the before/after message sets and token estimates
|
|
16
|
+
* `compactConversation()` already holds. That's enough to close the gap:
|
|
17
|
+
* quality scoring reaches `/compact` and `/compact-history` without adopting
|
|
18
|
+
* `CompactionManager`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty note: the `strategy` value passed to `computeQualityScore()` is a
|
|
21
|
+
* BORROWED rubric label describing what `compactConversation()`'s structured
|
|
22
|
+
* multi-section extraction actually does (collapse the whole conversation
|
|
23
|
+
* into one handoff message) — it is NOT a real `CompactionStrategy`
|
|
24
|
+
* escalation result, and no strategy switch is triggered from it. Every
|
|
25
|
+
* rendering of the score says so explicitly (see `formatQualityScoreLine`).
|
|
26
|
+
*
|
|
27
|
+
* Storage: scores are kept in a TUI-local, timestamp-keyed store, never added
|
|
28
|
+
* to the SDK's public `CompactionEvent` type (that would force an
|
|
29
|
+
* api-extractor regen for a value the SDK's own compaction log never
|
|
30
|
+
* populates). Keyed by `CompactionEvent.timestamp` rather than array index,
|
|
31
|
+
* because `compactSmallWindow`'s null-event path (small-context-window
|
|
32
|
+
* models — see runtime-services.ts) can create index gaps.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { computeQualityScore, describeScore } from '@/runtime/index.ts';
|
|
36
|
+
import type { CompactionQualityScore, StrategyInput, StrategyOutput } from '@/runtime/index.ts';
|
|
37
|
+
import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
|
|
38
|
+
|
|
39
|
+
export type { CompactionQualityScore } from '@/runtime/index.ts';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bound the TUI-local score store the same way the SDK bounds its own
|
|
43
|
+
* compaction event log (context-compaction.ts caps at 50 and shifts the
|
|
44
|
+
* oldest entry) — a long-running session should not accumulate this forever.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_TRACKED_SCORES = 50;
|
|
47
|
+
|
|
48
|
+
const scoresByTimestamp = new Map<number, CompactionQualityScore>();
|
|
49
|
+
|
|
50
|
+
/** Record a computed quality score, keyed by the CompactionEvent's timestamp. */
|
|
51
|
+
export function recordCompactionQualityScore(timestamp: number, score: CompactionQualityScore): void {
|
|
52
|
+
scoresByTimestamp.set(timestamp, score);
|
|
53
|
+
if (scoresByTimestamp.size > MAX_TRACKED_SCORES) {
|
|
54
|
+
const oldestKey = scoresByTimestamp.keys().next().value;
|
|
55
|
+
if (oldestKey !== undefined) scoresByTimestamp.delete(oldestKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Look up a previously-recorded quality score for a CompactionEvent, by timestamp. */
|
|
60
|
+
export function getCompactionQualityScore(timestamp: number): CompactionQualityScore | undefined {
|
|
61
|
+
return scoresByTimestamp.get(timestamp);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ScoreCompactionRunInput {
|
|
65
|
+
readonly sessionId: string;
|
|
66
|
+
readonly contextWindow: number;
|
|
67
|
+
readonly messagesBefore: readonly ProviderMessage[];
|
|
68
|
+
readonly messagesAfter: readonly ProviderMessage[];
|
|
69
|
+
readonly tokensBefore: number;
|
|
70
|
+
readonly tokensAfter: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compute a CompactionQualityScore for one compaction run, out-of-band from
|
|
75
|
+
* the SDK's own (unused, in this TUI) CompactionManager pipeline.
|
|
76
|
+
*/
|
|
77
|
+
export function scoreCompactionRun(input: ScoreCompactionRunInput): CompactionQualityScore {
|
|
78
|
+
const strategyInput: StrategyInput = {
|
|
79
|
+
sessionId: input.sessionId,
|
|
80
|
+
messages: input.messagesBefore,
|
|
81
|
+
tokensBefore: input.tokensBefore,
|
|
82
|
+
contextWindow: input.contextWindow,
|
|
83
|
+
// Borrowed rubric label, not a real strategy escalation — see module doc.
|
|
84
|
+
strategy: 'collapse',
|
|
85
|
+
};
|
|
86
|
+
const strategyOutput: StrategyOutput = {
|
|
87
|
+
messages: [...input.messagesAfter],
|
|
88
|
+
tokensAfter: input.tokensAfter,
|
|
89
|
+
summary: '',
|
|
90
|
+
strategy: 'collapse',
|
|
91
|
+
durationMs: 0,
|
|
92
|
+
warnings: [],
|
|
93
|
+
};
|
|
94
|
+
return computeQualityScore(strategyInput, strategyOutput);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One-line grade summary for the /compact after-notice and /compact-history entries. */
|
|
98
|
+
export function formatQualityScoreLine(score: CompactionQualityScore): string {
|
|
99
|
+
return `Quality: ${describeScore(score)} (rubric applied out-of-band; no strategy escalation ran)`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
|
|
3
|
+
import { resolveUiTones } from './theme.ts';
|
|
4
|
+
import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
|
|
5
|
+
|
|
6
|
+
const TONES = resolveUiTones('dark');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* renderConfigModal — the single render path for every ConfigModalSurface.
|
|
10
|
+
* Reads the host's frozen-structure-plus-live-values render model and maps it
|
|
11
|
+
* onto ModalFactory.createModal (the same renderer settings-modal uses), so the
|
|
12
|
+
* chrome, tab strip, list, and footer are one dialect across all migrated
|
|
13
|
+
* surfaces. Pure: takes the modal's render model, returns Line[].
|
|
14
|
+
*/
|
|
15
|
+
export function renderConfigModal(modal: ConfigModal, width: number, height: number): Line[] {
|
|
16
|
+
// Size the visible row window to the available height so the list scrolls
|
|
17
|
+
// rather than overflowing (posture header + separator + chrome ≈ 8 rows).
|
|
18
|
+
const visible = Math.max(3, Math.min(16, height - 8));
|
|
19
|
+
modal.setViewportRows(visible);
|
|
20
|
+
const model = modal.getRenderModel();
|
|
21
|
+
return renderConfigModalModel(model, width);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
|
|
25
|
+
function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
|
|
26
|
+
const sections: ModalSection[] = [];
|
|
27
|
+
|
|
28
|
+
if (model.degraded) {
|
|
29
|
+
sections.push({ type: 'text', content: `⚠ ${model.degraded}`, style: { fg: TONES.state.warn, bold: true } });
|
|
30
|
+
sections.push({ type: 'separator' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const line of model.header) {
|
|
34
|
+
sections.push({ type: 'text', content: line, style: { fg: TONES.fg.secondary } });
|
|
35
|
+
}
|
|
36
|
+
if (model.header.length > 0) sections.push({ type: 'separator' });
|
|
37
|
+
|
|
38
|
+
if (model.rows.length === 0) {
|
|
39
|
+
sections.push({ type: 'text', content: model.emptyText ?? 'Nothing to show.', style: { fg: TONES.fg.muted, dim: true } });
|
|
40
|
+
} else {
|
|
41
|
+
sections.push({
|
|
42
|
+
type: 'list',
|
|
43
|
+
items: model.rows.map((row) => ({
|
|
44
|
+
label: row.label,
|
|
45
|
+
selected: row.selected,
|
|
46
|
+
style: row.stale
|
|
47
|
+
? { fg: TONES.fg.muted, dim: true }
|
|
48
|
+
: row.style,
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tabs: ModalTab[] | undefined = model.tabs.length > 1
|
|
54
|
+
? model.tabs.map((t) => ({ label: t.label, active: t.active }))
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
// Keep the content region a stable height so live-value ticks never change
|
|
58
|
+
// the box size: frozen header + separator + the fixed visible-row window.
|
|
59
|
+
const targetContentRows =
|
|
60
|
+
(model.degraded ? 2 : 0) +
|
|
61
|
+
model.header.length +
|
|
62
|
+
(model.header.length > 0 ? 1 : 0) +
|
|
63
|
+
model.scroll.visible;
|
|
64
|
+
|
|
65
|
+
const helpers = model.status
|
|
66
|
+
? [{ content: model.status, accent: true }]
|
|
67
|
+
: undefined;
|
|
68
|
+
|
|
69
|
+
return ModalFactory.createModal(
|
|
70
|
+
{
|
|
71
|
+
title: model.title,
|
|
72
|
+
width: 76,
|
|
73
|
+
tabs,
|
|
74
|
+
sections,
|
|
75
|
+
targetContentRows,
|
|
76
|
+
helpers,
|
|
77
|
+
hints: [...model.hints],
|
|
78
|
+
},
|
|
79
|
+
width,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -8,11 +8,9 @@ import { renderModelWorkspace } from './model-workspace.ts';
|
|
|
8
8
|
import { renderSelectionModalOverlay } from './selection-modal-overlay.ts';
|
|
9
9
|
import { renderSearchOverlay } from './search-overlay.ts';
|
|
10
10
|
import { renderHistorySearchOverlay } from './history-search-overlay.ts';
|
|
11
|
-
import { renderProcessModal } from './process-modal.ts';
|
|
12
|
-
import { renderAgentDetailModal } from './agent-detail-modal.ts';
|
|
13
|
-
import { renderLiveTailModal } from './live-tail-modal.ts';
|
|
14
11
|
import { renderContextInspector } from './context-inspector.ts';
|
|
15
12
|
import { renderSettingsModal } from './settings-modal.ts';
|
|
13
|
+
import { renderConfigModal } from './config-modal.ts';
|
|
16
14
|
import { renderMcpWorkspace } from './mcp-workspace.ts';
|
|
17
15
|
import { renderSessionPickerModal } from './session-picker-modal.ts';
|
|
18
16
|
import { renderProfilePickerModal } from './profile-picker-modal.ts';
|
|
@@ -78,20 +76,9 @@ export function applyConversationOverlays(
|
|
|
78
76
|
next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!fullscreenClaimed && input.agentDetailModal.active) {
|
|
87
|
-
const lines = renderAgentDetailModal(input.agentDetailModal, conversationWidth, viewportHeight);
|
|
88
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!fullscreenClaimed && input.liveTailModal.active) {
|
|
92
|
-
const lines = renderLiveTailModal(input.liveTailModal, conversationWidth, viewportHeight);
|
|
93
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
94
|
-
}
|
|
79
|
+
// W6.1 retirement: the process, agent-detail, and live-tail modal overlays
|
|
80
|
+
// were removed with those modals — F2 now opens the Fleet panel, which
|
|
81
|
+
// subsumes the live process tree.
|
|
95
82
|
|
|
96
83
|
if (!fullscreenClaimed && input.contextInspectorModal.active) {
|
|
97
84
|
const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
|
|
@@ -104,6 +91,12 @@ export function applyConversationOverlays(
|
|
|
104
91
|
fullscreenClaimed = true;
|
|
105
92
|
}
|
|
106
93
|
|
|
94
|
+
if (!fullscreenClaimed && input.configModal.active) {
|
|
95
|
+
const lines = renderConfigModal(input.configModal, conversationWidth, viewportHeight);
|
|
96
|
+
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
97
|
+
fullscreenClaimed = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
107
100
|
if (input.mcpWorkspace.active) {
|
|
108
101
|
const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
|
|
109
102
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-tab-strip.ts
|
|
3
|
+
//
|
|
4
|
+
// Wave-3 (W3.1 Part C3) — renders FleetPanel's session-tab strip by reusing
|
|
5
|
+
// renderTabStrip exactly as panel-workspace-bar.ts does for the workspace
|
|
6
|
+
// tab bar. Deliberately a SEPARATE, visually distinct strip from the
|
|
7
|
+
// workspace bar: workspace tabs switch PANELS, fleet session tabs switch
|
|
8
|
+
// ATTACHED PROCESSES within one panel — conflating their styling would blur
|
|
9
|
+
// that distinction for the operator.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
import type { Line } from '../types/grid.ts';
|
|
13
|
+
import type { FleetTabsState } from '../panels/fleet-tabs.ts';
|
|
14
|
+
import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
|
|
15
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
16
|
+
|
|
17
|
+
const ACTIVE_FG = UI_TONES.fg.primary;
|
|
18
|
+
const ACTIVE_BG = UI_TONES.bg.selected;
|
|
19
|
+
const INACTIVE_FG = UI_TONES.fg.muted;
|
|
20
|
+
const SEPARATOR_FG = UI_TONES.fg.dim;
|
|
21
|
+
const LABEL_FG = UI_TONES.fg.secondary;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render the fleet session-tab strip, or `null` when there are no attached
|
|
25
|
+
* tabs — the panel omits the strip entirely in that case (root-tab-only),
|
|
26
|
+
* which is what keeps the pre-Wave-3 fleet-panel goldens byte-identical.
|
|
27
|
+
*/
|
|
28
|
+
export function renderFleetTabStrip(
|
|
29
|
+
state: FleetTabsState,
|
|
30
|
+
width: number,
|
|
31
|
+
onLayout?: (regions: readonly TabHitRegion[]) => void,
|
|
32
|
+
): Line | null {
|
|
33
|
+
if (state.tabs.length === 0) return null;
|
|
34
|
+
return renderTabStrip({
|
|
35
|
+
width,
|
|
36
|
+
onLayout,
|
|
37
|
+
tabs: [
|
|
38
|
+
{ label: 'Tree', active: state.activeTabIndex === 0 },
|
|
39
|
+
...state.tabs.map((tab, index) => ({
|
|
40
|
+
label: tab.label,
|
|
41
|
+
active: state.activeTabIndex === index + 1,
|
|
42
|
+
})),
|
|
43
|
+
],
|
|
44
|
+
prefixLabel: ' SESSIONS ',
|
|
45
|
+
style: {
|
|
46
|
+
activeFg: ACTIVE_FG,
|
|
47
|
+
activeBg: ACTIVE_BG,
|
|
48
|
+
activeBold: true,
|
|
49
|
+
inactiveFg: INACTIVE_FG,
|
|
50
|
+
separatorFg: SEPARATOR_FG,
|
|
51
|
+
labelFg: LABEL_FG,
|
|
52
|
+
overflowFg: SEPARATOR_FG,
|
|
53
|
+
trailingFg: INACTIVE_FG,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -25,9 +25,17 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const TIP_PANELS = 'Ctrl+P panels';
|
|
28
|
-
|
|
28
|
+
// W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
|
|
29
|
+
// tip names 'fleet', not 'processes'.
|
|
30
|
+
const TIP_PROCESSES = 'F2 fleet';
|
|
29
31
|
const TIP_HELP = '? help';
|
|
30
|
-
|
|
32
|
+
// W6.2 f: an empty-composer Ctrl+C does NOT quit on the first press — it arms a
|
|
33
|
+
// ~1s "press again to exit" confirm, and only a SECOND Ctrl+C within that window
|
|
34
|
+
// exits (an intentional accidental-exit guard). The old 'Ctrl+C quit' tip
|
|
35
|
+
// implied a single press quits, so two presses seconds apart appeared to do
|
|
36
|
+
// nothing. 'Ctrl+C x2 quit' matches the real double-press contract (and the
|
|
37
|
+
// help overlay's "Ctrl+C x2 Exit" row).
|
|
38
|
+
const TIP_QUIT = 'Ctrl+C x2 quit';
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Build the footer discoverability tip. Segments join with the shared middle
|
|
@@ -24,6 +24,7 @@ export class GitStatusProvider {
|
|
|
24
24
|
private lastFetch = 0;
|
|
25
25
|
private readonly ttlMs = 2000;
|
|
26
26
|
private fetching = false;
|
|
27
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
27
28
|
|
|
28
29
|
constructor(private readonly workingDirectory: string) {}
|
|
29
30
|
|
|
@@ -53,6 +54,45 @@ export class GitStatusProvider {
|
|
|
53
54
|
return this.cache;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Start a lightweight live-repo-state poll. Mirrors DiffPanel's principle of
|
|
59
|
+
* never trusting a cached "is this a repo" flag (see diff-panel.ts
|
|
60
|
+
* showGitDiff/showFileDiffs/showStagedDiff, all gated on a fresh
|
|
61
|
+
* GitService.isGitRepo() check) but amortizes the cost for the header: the
|
|
62
|
+
* cheap synchronous isGitRepo() spawn runs every tick; the heavier async
|
|
63
|
+
* status()+branch() fetch (this.refresh()) only runs when that boolean
|
|
64
|
+
* actually flips — e.g. an external `git init`, or `.git` removed.
|
|
65
|
+
*/
|
|
66
|
+
startPolling(intervalMs: number, onChange: (info: GitHeaderInfo) => void): void {
|
|
67
|
+
if (this.pollTimer !== null) return;
|
|
68
|
+
const checkIsRepo = (): boolean | null => {
|
|
69
|
+
try {
|
|
70
|
+
return GitService.isGitRepo(this.workingDirectory);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.debug('GitStatusProvider: isGitRepo poll check failed', { error: summarizeError(err) });
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
let lastKnownIsRepo = checkIsRepo();
|
|
77
|
+
this.pollTimer = setInterval(() => {
|
|
78
|
+
const isRepoNow = checkIsRepo();
|
|
79
|
+
if (isRepoNow !== null && isRepoNow !== lastKnownIsRepo) {
|
|
80
|
+
lastKnownIsRepo = isRepoNow;
|
|
81
|
+
this.refresh().then(onChange).catch((err) => {
|
|
82
|
+
logger.debug('GitStatusProvider: poll-triggered refresh failed', { error: summarizeError(err) });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}, intervalMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stop the live-repo-state poll started by startPolling(). No-op if not polling. */
|
|
89
|
+
stopPolling(): void {
|
|
90
|
+
if (this.pollTimer !== null) {
|
|
91
|
+
clearInterval(this.pollTimer);
|
|
92
|
+
this.pollTimer = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
private async _fetch(): Promise<void> {
|
|
57
97
|
if (this.fetching) return;
|
|
58
98
|
this.fetching = true;
|
|
@@ -289,7 +289,7 @@ export function renderShortcutsOverlay(
|
|
|
289
289
|
row(kb('block-copy'), 'Copy block to clipboard'),
|
|
290
290
|
row(kb('block-save'), 'Save block to file'),
|
|
291
291
|
row(kb('copy-selection'), 'Copy selection'),
|
|
292
|
-
row('F2', '
|
|
292
|
+
row('F2', 'Open the Fleet panel'),
|
|
293
293
|
row('?', 'Help overlay'),
|
|
294
294
|
row(`${kb('clear-cancel')} x2`, 'Exit'),
|
|
295
295
|
'',
|
|
@@ -303,7 +303,7 @@ export function renderShortcutsOverlay(
|
|
|
303
303
|
row(kb('panel-focus-toggle'), 'Swap focus between top / bottom pane'),
|
|
304
304
|
row(kb('panel-close'), 'Close active panel'),
|
|
305
305
|
row(kb('panel-close-all'), 'Close all panels'),
|
|
306
|
-
row(kb('panel-ops'), 'Open the
|
|
306
|
+
row(kb('panel-ops'), 'Open and focus the Fleet panel'),
|
|
307
307
|
'',
|
|
308
308
|
' In-Panel Controls',
|
|
309
309
|
' ' + '\u2500'.repeat(40),
|
|
@@ -124,6 +124,10 @@ function modelKey(model: ModelDefinition): string {
|
|
|
124
124
|
|
|
125
125
|
function targetSummary(info: ModelPickerTargetInfo): string {
|
|
126
126
|
if (!info.enabled) return 'disabled';
|
|
127
|
+
// 'embeddings' has no model concept — configuredNote carries the honest
|
|
128
|
+
// provider id + dimensions + configured-state summary instead of a
|
|
129
|
+
// computed provider:model route (which would print a phantom "model:").
|
|
130
|
+
if (info.configuredNote) return info.configuredNote;
|
|
127
131
|
const route = formatRoute(info.provider, info.model);
|
|
128
132
|
if (info.inherited) return `inherits ${route}`;
|
|
129
133
|
return route;
|
|
@@ -145,6 +149,7 @@ function targetLabelFor(target: string): string {
|
|
|
145
149
|
if (target === 'helper') return 'Helper Model';
|
|
146
150
|
if (target === 'tool') return 'Tool LLM';
|
|
147
151
|
if (target === 'tts') return 'TTS LLM';
|
|
152
|
+
if (target === 'embeddings') return 'Embeddings';
|
|
148
153
|
return 'Main Chat';
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -174,6 +179,13 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
|
174
179
|
}
|
|
175
180
|
} else if (picker.mode === 'effort') {
|
|
176
181
|
lines.push(`Reasoning effort applies to the main chat model. Select the default effort for this model.`);
|
|
182
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
183
|
+
lines.push('Embedding provider selection: choose the provider memory search and the code index use to embed content.');
|
|
184
|
+
const selectedEmbedding = picker.embeddingProviders[picker.selectedIndex];
|
|
185
|
+
if (selectedEmbedding) {
|
|
186
|
+
const status = selectedEmbedding.configured ? 'configured' : 'unconfigured';
|
|
187
|
+
lines.push(`Selected: ${selectedEmbedding.id} | ${selectedEmbedding.dimensions} dimensions | ${status}${selectedEmbedding.detail ? ` | ${selectedEmbedding.detail}` : ''}`);
|
|
188
|
+
}
|
|
177
189
|
} else {
|
|
178
190
|
lines.push(`Context cap overrides the detected local-model context window for this selection.`);
|
|
179
191
|
}
|
|
@@ -288,6 +300,28 @@ function renderContextCapRows(picker: ModelPickerModal, lines: Line[], rows: num
|
|
|
288
300
|
}
|
|
289
301
|
}
|
|
290
302
|
|
|
303
|
+
function renderEmbeddingProviderRows(picker: ModelPickerModal, lines: Line[], rows: number, startX: number, width: number): void {
|
|
304
|
+
const providers = picker.embeddingProviders;
|
|
305
|
+
const { start, end } = stableWindow(providers.length, picker.selectedIndex, rows);
|
|
306
|
+
for (let visibleRow = 0; visibleRow < rows; visibleRow += 1) {
|
|
307
|
+
const absolute = start + visibleRow;
|
|
308
|
+
const provider = providers[absolute];
|
|
309
|
+
const line = lines[visibleRow]!;
|
|
310
|
+
if (!provider) continue;
|
|
311
|
+
const selected = absolute === picker.selectedIndex;
|
|
312
|
+
const bg = selected ? PALETTE.selectedBg : PALETTE.bodyBg;
|
|
313
|
+
fillRange(line, startX, startX + width - 1, bg);
|
|
314
|
+
const marker = selected ? (picker.focusPane === 'items' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
315
|
+
const status = provider.configured ? 'configured' : 'unconfigured';
|
|
316
|
+
writeText(line, startX + 1, width, padDisplay(marker, 2), { fg: PALETTE.text, bg, bold: selected });
|
|
317
|
+
writeText(line, startX + 3, Math.max(0, width - 30), padDisplay(provider.label, Math.max(0, width - 30)), { fg: selected ? PALETTE.text : PALETTE.muted, bg, bold: selected });
|
|
318
|
+
writeText(line, startX + Math.max(4, width - 25), 12, padDisplay(`${provider.dimensions}d`, 12), { fg: PALETTE.dim, bg });
|
|
319
|
+
writeText(line, startX + Math.max(4, width - 13), 12, padDisplay(status, 12), { fg: provider.configured ? PALETTE.good : PALETTE.warn, bg });
|
|
320
|
+
}
|
|
321
|
+
if (start > 0 && lines[0]) writeText(lines[0], startX + 1, width - 2, `${GLYPHS.navigation.moreAbove} ${start} more provider(s) above`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
322
|
+
if (end < providers.length && lines[rows - 1]) writeText(lines[rows - 1]!, startX + 1, width - 2, `${GLYPHS.navigation.moreBelow} ${providers.length - end} more provider(s) below`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
323
|
+
}
|
|
324
|
+
|
|
291
325
|
function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number, width: number): void {
|
|
292
326
|
fillRange(line, startX, startX + width - 1, PALETTE.footerBg);
|
|
293
327
|
const style = { fg: PALETTE.muted, bg: PALETTE.footerBg, bold: true };
|
|
@@ -303,6 +337,10 @@ function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number,
|
|
|
303
337
|
writeText(line, startX + 1, width - 2, 'Context cap input', style);
|
|
304
338
|
return;
|
|
305
339
|
}
|
|
340
|
+
if (picker.mode === 'embeddingProvider') {
|
|
341
|
+
writeText(line, startX + 1, width - 2, 'Embedding provider Dimensions Status', style);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
306
344
|
const providerW = clamp(Math.floor(width * 0.14), 10, 18);
|
|
307
345
|
const ctxW = 8;
|
|
308
346
|
const tierW = 8;
|
|
@@ -337,7 +375,7 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
|
|
|
337
375
|
const header = contentLine(safeWidth, PALETTE.footerBg);
|
|
338
376
|
writeText(header, 2, targetW - 2, 'Targets', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
339
377
|
drawVertical(header, targetW, PALETTE.footerBg);
|
|
340
|
-
const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : 'Context cap';
|
|
378
|
+
const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : picker.mode === 'embeddingProvider' ? 'Embedding providers' : 'Context cap';
|
|
341
379
|
writeText(header, contentX + 1, contentW - 2, `${modeLabel} • ${picker.getItemCount()} item(s)`, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
342
380
|
lines.push(header);
|
|
343
381
|
|
|
@@ -380,6 +418,8 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
|
|
|
380
418
|
renderModelRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
381
419
|
} else if (picker.mode === 'contextCap') {
|
|
382
420
|
renderContextCapRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
421
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
422
|
+
renderEmbeddingProviderRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
383
423
|
} else {
|
|
384
424
|
renderEffortRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
385
425
|
}
|
|
@@ -432,6 +472,8 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
|
|
|
432
472
|
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
433
473
|
} else if (picker.mode === 'contextCap') {
|
|
434
474
|
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
|
|
475
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
476
|
+
base.push(objectId(picker.embeddingProviders), picker.embeddingProviders.length);
|
|
435
477
|
}
|
|
436
478
|
|
|
437
479
|
return base.join('\u001e');
|
|
@@ -467,6 +509,7 @@ function keyForTargets(values: readonly ModelPickerTargetInfo[]): string {
|
|
|
467
509
|
entry.model,
|
|
468
510
|
entry.enabled ? 1 : 0,
|
|
469
511
|
entry.inherited ? 1 : 0,
|
|
512
|
+
entry.configuredNote ?? '',
|
|
470
513
|
].join('\u001d'))
|
|
471
514
|
.join('\u001f');
|
|
472
515
|
}
|
|
@@ -21,12 +21,18 @@ export function renderPanelWorkspaceBar(
|
|
|
21
21
|
return renderTabStrip({
|
|
22
22
|
width,
|
|
23
23
|
onLayout,
|
|
24
|
-
tabs: tabs.map((tab) => ({
|
|
24
|
+
tabs: tabs.map((tab, i) => ({
|
|
25
25
|
// tab.active = selected in its own pane (drives highlighted background).
|
|
26
26
|
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
27
27
|
// A tab can be active-but-not-focused (selected in the unfocused pane) or
|
|
28
28
|
// active-and-focused (selected in the focused pane).
|
|
29
|
-
|
|
29
|
+
//
|
|
30
|
+
// A14: the pane marker uses ▲/▼ (top/bottom pane). The old '^'/'v'
|
|
31
|
+
// prefixes read as a Ctrl caret (^) implying a non-existent Ctrl+<tab>
|
|
32
|
+
// hotkey. The REAL per-tab jump key is Alt+N (panel-tab-1..9), rendered
|
|
33
|
+
// here as ⌥N for the first nine tabs so the shown affordance matches the
|
|
34
|
+
// binding (tabs past nine have no Alt+N chord, so no index is shown).
|
|
35
|
+
label: `${tab.pane === 'bottom' ? '▼' : '▲'}${i < 9 ? ` ⌥${i + 1}` : ''} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
|
|
30
36
|
active: tab.active,
|
|
31
37
|
})),
|
|
32
38
|
prefixLabel: ' PANELS ',
|
|
@@ -8,6 +8,13 @@ export interface ShellFooterBuildOptions {
|
|
|
8
8
|
readonly promptLineCount: number;
|
|
9
9
|
readonly promptCursorPos?: number;
|
|
10
10
|
readonly promptFocused?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True when the panel workspace owns keyboard focus. Only consulted as a
|
|
13
|
+
* fallback when `promptFocused` is not supplied — see buildShellFooter's
|
|
14
|
+
* default expression. Callers that already compute `promptFocused`
|
|
15
|
+
* themselves (main.ts does) do not need to also pass this.
|
|
16
|
+
*/
|
|
17
|
+
readonly panelFocused?: boolean;
|
|
11
18
|
readonly usage: { up: number; down: number };
|
|
12
19
|
readonly showExitNotice: boolean;
|
|
13
20
|
readonly lastCopyTime: number;
|
|
@@ -107,7 +114,7 @@ export function buildShellFooter(
|
|
|
107
114
|
options.lastInputTokens,
|
|
108
115
|
options.commandArgsHint,
|
|
109
116
|
options.hitlMode,
|
|
110
|
-
options.promptFocused ?? !options.indicatorFocused,
|
|
117
|
+
options.promptFocused ?? (!options.indicatorFocused && !options.panelFocused),
|
|
111
118
|
options.composerMode,
|
|
112
119
|
options.composerStatus,
|
|
113
120
|
options.composerFlags,
|