@pellux/goodvibes-agent 1.4.4 → 1.5.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 +14 -0
- package/README.md +7 -7
- package/dist/package/main.js +7424 -12241
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +11 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +111 -89
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
|
@@ -8,10 +8,24 @@
|
|
|
8
8
|
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
9
|
import type { ModelPickerModal } from '../input/model-picker.ts';
|
|
10
10
|
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
11
|
+
import { isLocalFitRecommendation, isProviderSignInRow, LOCAL_REC_PROVIDER } from '../input/model-picker-local-fit.ts';
|
|
12
|
+
import { estimateModelBytes, fitAssessment, fitVerdictLabel, paramCountFromModel, readHardwareProfileSync, REPRESENTATIVE_7B_PARAMS } from '../core/hardware-profile.ts';
|
|
11
13
|
import type { Line } from '../types/grid.ts';
|
|
12
|
-
import {
|
|
14
|
+
import { createStyledCell } from '../types/grid.ts';
|
|
13
15
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
14
16
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
17
|
+
import {
|
|
18
|
+
clamp,
|
|
19
|
+
clipDisplay,
|
|
20
|
+
contentLine,
|
|
21
|
+
fillRange,
|
|
22
|
+
makeLine,
|
|
23
|
+
padDisplay,
|
|
24
|
+
stableWindow,
|
|
25
|
+
writeText,
|
|
26
|
+
borderLine as primsBorderLine,
|
|
27
|
+
drawVerticalRule,
|
|
28
|
+
} from './fullscreen-primitives.ts';
|
|
15
29
|
|
|
16
30
|
const PALETTE = {
|
|
17
31
|
border: '#64748b',
|
|
@@ -58,98 +72,30 @@ const MODEL_WORKSPACE_CONTEXT_CAP_INPUT_HELP = 'Type digits to set a cap. Enter
|
|
|
58
72
|
const MODEL_WORKSPACE_FOOTER_SEARCH_ACTIVE = 'Typing filters search; Esc clears search';
|
|
59
73
|
const MODEL_WORKSPACE_FOOTER_SEARCH_INACTIVE = '/ search';
|
|
60
74
|
const MODEL_WORKSPACE_FOOTER_CONTROLS = 'Up/Down navigate • Left/Right pane • Enter select • <search> • Tab price • C caps • A available • B benchmark • G group • Esc close';
|
|
75
|
+
const MODEL_WORKSPACE_LOCAL_ONLY_HEADER = 'No provider signed in — these run on your machine';
|
|
76
|
+
const MODEL_WORKSPACE_LOCAL_ONLY_SIGN_IN = 'Sign in instead: select "Sign in to a provider" in the list below to connect a cloud or local provider.';
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fg: cell.fg,
|
|
71
|
-
bg,
|
|
72
|
-
bold: cell.bold,
|
|
73
|
-
dim: cell.dim,
|
|
74
|
-
underline: cell.underline,
|
|
75
|
-
italic: cell.italic,
|
|
76
|
-
strikethrough: cell.strikethrough,
|
|
77
|
-
link: cell.link,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function makeLine(width: number, bg = ''): Line {
|
|
83
|
-
const line = createEmptyLine(width);
|
|
84
|
-
if (bg) fillRange(line, 0, width - 1, bg);
|
|
85
|
-
return line;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function writeText(line: Line, startX: number, maxWidth: number, text: string, style: Partial<Omit<Line[number], 'char'>> = {}): void {
|
|
89
|
-
let x = startX;
|
|
90
|
-
let used = 0;
|
|
91
|
-
for (const ch of text) {
|
|
92
|
-
const width = getDisplayWidth(ch);
|
|
93
|
-
if (width <= 0) continue;
|
|
94
|
-
if (used + width > maxWidth || x >= line.length) break;
|
|
95
|
-
line[x] = createStyledCell(ch, style);
|
|
96
|
-
if (width > 1 && x + 1 < line.length) {
|
|
97
|
-
line[x + 1] = createStyledCell(' ', style);
|
|
98
|
-
}
|
|
99
|
-
x += width;
|
|
100
|
-
used += width;
|
|
101
|
-
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns true when every model in the picker's list is a synthetic local
|
|
80
|
+
* fit recommendation (no real credentials configured).
|
|
81
|
+
*/
|
|
82
|
+
function isLocalOnlyList(picker: ModelPickerModal): boolean {
|
|
83
|
+
if (picker.mode !== 'model') return false;
|
|
84
|
+
const models = picker.models;
|
|
85
|
+
return models.length > 0 && models.every((m) => m.provider === LOCAL_REC_PROVIDER || isLocalFitRecommendation(m));
|
|
102
86
|
}
|
|
103
87
|
|
|
88
|
+
// Local wrappers that forward PALETTE.border as the fg/borderFg default, keeping
|
|
89
|
+
// the same visual output as the deleted local copies.
|
|
104
90
|
function borderLine(width: number, left: string, fill: string, right: string): Line {
|
|
105
|
-
|
|
106
|
-
if (width <= 0) return line;
|
|
107
|
-
line[0] = createStyledCell(left, { fg: PALETTE.border });
|
|
108
|
-
for (let x = 1; x < width - 1; x += 1) {
|
|
109
|
-
line[x] = createStyledCell(fill, { fg: PALETTE.border });
|
|
110
|
-
}
|
|
111
|
-
if (width > 1) line[width - 1] = createStyledCell(right, { fg: PALETTE.border });
|
|
112
|
-
return line;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function contentLine(width: number, bg: string): Line {
|
|
116
|
-
const line = makeLine(width, bg);
|
|
117
|
-
if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
118
|
-
if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
119
|
-
return line;
|
|
91
|
+
return primsBorderLine(width, left, fill, right, PALETTE.border);
|
|
120
92
|
}
|
|
121
93
|
|
|
94
|
+
// drawVertical intentionally keeps the original guard (x<=0) which differs from
|
|
95
|
+
// drawVerticalRule in fullscreen-primitives (x<0). Callers rely on the <=0 skip.
|
|
122
96
|
function drawVertical(line: Line, x: number, bg = ''): void {
|
|
123
97
|
if (x <= 0 || x >= line.length - 1) return;
|
|
124
|
-
line
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function clipDisplay(text: string, width: number): string {
|
|
128
|
-
if (width <= 0) return '';
|
|
129
|
-
let used = 0;
|
|
130
|
-
let output = '';
|
|
131
|
-
for (const ch of text) {
|
|
132
|
-
const chWidth = getDisplayWidth(ch);
|
|
133
|
-
if (chWidth <= 0) continue;
|
|
134
|
-
if (used + chWidth > width) break;
|
|
135
|
-
output += ch;
|
|
136
|
-
used += chWidth;
|
|
137
|
-
}
|
|
138
|
-
return output;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function padDisplay(text: string, width: number): string {
|
|
142
|
-
const clipped = clipDisplay(text, width);
|
|
143
|
-
return clipped + ' '.repeat(Math.max(0, width - getDisplayWidth(clipped)));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function stableWindow(total: number, selected: number, visible: number): { start: number; end: number } {
|
|
147
|
-
if (total <= 0 || visible <= 0) return { start: 0, end: 0 };
|
|
148
|
-
const clamped = clamp(selected, 0, total - 1);
|
|
149
|
-
const half = Math.floor(visible / 2);
|
|
150
|
-
const maxStart = Math.max(0, total - visible);
|
|
151
|
-
const start = clamp(clamped - half, 0, maxStart);
|
|
152
|
-
return { start, end: Math.min(total, start + visible) };
|
|
98
|
+
drawVerticalRule(line, x, PALETTE.border, bg);
|
|
153
99
|
}
|
|
154
100
|
|
|
155
101
|
function formatContext(value: number | undefined): string {
|
|
@@ -241,6 +187,8 @@ export function renderModelWorkspacePackageText(): string {
|
|
|
241
187
|
'Detected context: <context>',
|
|
242
188
|
'Override: <cap>',
|
|
243
189
|
MODEL_WORKSPACE_CONTEXT_CAP_INPUT_HELP,
|
|
190
|
+
MODEL_WORKSPACE_LOCAL_ONLY_HEADER,
|
|
191
|
+
MODEL_WORKSPACE_LOCAL_ONLY_SIGN_IN,
|
|
244
192
|
'Focus targets',
|
|
245
193
|
'Focus list',
|
|
246
194
|
modelWorkspaceFooterControls(MODEL_WORKSPACE_FOOTER_SEARCH_ACTIVE),
|
|
@@ -248,12 +196,66 @@ export function renderModelWorkspacePackageText(): string {
|
|
|
248
196
|
].join('\n');
|
|
249
197
|
}
|
|
250
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Returns a plain-language hardware fit line for a local model, or an empty
|
|
201
|
+
* string when the model is not local or when the verdict is unknown.
|
|
202
|
+
* Uses the process-level cached hardware probe — never probes per render.
|
|
203
|
+
*/
|
|
204
|
+
/**
|
|
205
|
+
* Format a param count (e.g. 70e9) as a human-readable size token (e.g. '70B', '1.5B').
|
|
206
|
+
* Used to build the sizeDescriptor passed to fitVerdictLabel.
|
|
207
|
+
*/
|
|
208
|
+
function formatParamDescriptor(params: number): string {
|
|
209
|
+
const b = params / 1e9;
|
|
210
|
+
// Avoid floating-point noise: round to 1 decimal, strip trailing .0
|
|
211
|
+
const rounded = Math.round(b * 10) / 10;
|
|
212
|
+
return rounded === Math.floor(rounded) ? `${Math.floor(rounded)}B` : `${rounded}B`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function modelHardwareFitLine(model: ModelDefinition): string {
|
|
216
|
+
const provider = (model.provider ?? '').toLowerCase();
|
|
217
|
+
// Anchor on known local-provider identities only; cloud providers that happen
|
|
218
|
+
// to serve Llama/local-named models (Groq 'llama-3.1-70b', Together
|
|
219
|
+
// 'meta-llama/...') are NOT local and must not be annotated.
|
|
220
|
+
const isLocal =
|
|
221
|
+
provider === 'ollama' ||
|
|
222
|
+
provider === 'vllm' ||
|
|
223
|
+
provider === 'llama-cpp' ||
|
|
224
|
+
provider === 'llama.cpp' ||
|
|
225
|
+
provider === 'lm-studio' ||
|
|
226
|
+
provider === 'lmstudio' ||
|
|
227
|
+
provider === 'localai' ||
|
|
228
|
+
provider === 'tgi' ||
|
|
229
|
+
provider === 'local-openai' ||
|
|
230
|
+
provider === 'openai-compatible-local';
|
|
231
|
+
if (!isLocal) return '';
|
|
232
|
+
// Use the actual param count from the model id/displayName when parseable;
|
|
233
|
+
// fall back to the shared 7B representative constant otherwise.
|
|
234
|
+
const parsedParams = paramCountFromModel(model);
|
|
235
|
+
const params = parsedParams ?? REPRESENTATIVE_7B_PARAMS;
|
|
236
|
+
const sizeBytes = estimateModelBytes(params);
|
|
237
|
+
// Build a human-readable size descriptor only when we could parse the real size.
|
|
238
|
+
const sizeDescriptor = parsedParams !== null
|
|
239
|
+
? formatParamDescriptor(parsedParams)
|
|
240
|
+
: undefined;
|
|
241
|
+
const label = fitVerdictLabel(fitAssessment(sizeBytes, readHardwareProfileSync()), sizeDescriptor);
|
|
242
|
+
return label ? `Hardware: ${label}` : '';
|
|
243
|
+
}
|
|
244
|
+
|
|
251
245
|
function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
252
246
|
const target = picker.getSelectedTargetInfo();
|
|
253
247
|
const targetLabel = target?.label ?? targetLabelFor(picker.target);
|
|
254
248
|
const targetState = target ? (target.enabled ? 'enabled' : 'disabled') : 'active';
|
|
255
249
|
const selected = selectedModel(picker);
|
|
256
250
|
const lines: string[] = [];
|
|
251
|
+
|
|
252
|
+
// Local-only header: shown when the list contains only synthetic fit recommendations.
|
|
253
|
+
// Never show when any real provider is configured.
|
|
254
|
+
if (isLocalOnlyList(picker)) {
|
|
255
|
+
lines.push(MODEL_WORKSPACE_LOCAL_ONLY_HEADER);
|
|
256
|
+
lines.push(MODEL_WORKSPACE_LOCAL_ONLY_SIGN_IN);
|
|
257
|
+
}
|
|
258
|
+
|
|
257
259
|
lines.push(`Target: ${targetLabel} (${targetState})`);
|
|
258
260
|
if (target) lines.push(`Current: ${targetSummary(target)}`);
|
|
259
261
|
if (picker.mode === 'provider') {
|
|
@@ -269,7 +271,20 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
|
269
271
|
caps.multimodal ? 'vision' : '',
|
|
270
272
|
caps.toolCalling ? 'tools' : '',
|
|
271
273
|
].filter(Boolean).join(', ') || 'standard';
|
|
272
|
-
|
|
274
|
+
// Suppress the key:id line for synthetic local recommendations — they have
|
|
275
|
+
// no real registry key and displaying the synthetic id would confuse users.
|
|
276
|
+
if (isProviderSignInRow(selected)) {
|
|
277
|
+
lines.push('Sign in to a provider to connect a cloud or local model.');
|
|
278
|
+
if (selected.description) lines.push(selected.description);
|
|
279
|
+
} else if (!isLocalFitRecommendation(selected)) {
|
|
280
|
+
lines.push(`Selected: ${modelKey(selected)} | ${selected.displayName} | context ${formatContext(selected.contextWindow)} | ${capText}`);
|
|
281
|
+
} else {
|
|
282
|
+
lines.push(`Selected: ${selected.displayName}`);
|
|
283
|
+
// Show the fit hint from the description (e.g. "not yet installed (fits in GPU memory)")
|
|
284
|
+
if (selected.description) lines.push(selected.description);
|
|
285
|
+
}
|
|
286
|
+
const fitLine = modelHardwareFitLine(selected);
|
|
287
|
+
if (fitLine) lines.push(fitLine);
|
|
273
288
|
}
|
|
274
289
|
} else if (picker.mode === 'effort') {
|
|
275
290
|
lines.push(MODEL_WORKSPACE_REASONING_EFFORT_DETAIL);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
3
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
4
|
-
import { getSurfaceContentRows, getTrackedVisibleWindow, getVisibleWindow, type VisibleWindow } from '
|
|
5
|
-
import { GLYPHS, UI_TONES } from '
|
|
6
|
-
import { type StatusState, STATE_GLYPHS } from '
|
|
4
|
+
import { getSurfaceContentRows, getTrackedVisibleWindow, getVisibleWindow, type VisibleWindow } from './surface-layout.ts';
|
|
5
|
+
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
6
|
+
import { type StatusState, STATE_GLYPHS } from './status-glyphs.ts';
|
|
7
7
|
|
|
8
8
|
export interface PanelPalette {
|
|
9
9
|
readonly label: string;
|
|
@@ -36,17 +36,16 @@ export interface ShellFooterBuildResult {
|
|
|
36
36
|
readonly height: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
const
|
|
39
|
+
// Footer chrome: box top + box bottom + status line + hints line.
|
|
40
|
+
const FOOTER_BASE_ROWS = 4;
|
|
41
41
|
const PROCESS_INDICATOR_ROWS = 1;
|
|
42
42
|
|
|
43
43
|
export function estimateShellFooterHeight(
|
|
44
44
|
promptLineCount: number,
|
|
45
|
-
|
|
45
|
+
_contextWindow?: number,
|
|
46
46
|
): number {
|
|
47
47
|
const safePromptLines = Math.max(1, promptLineCount);
|
|
48
|
-
|
|
49
|
-
return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS;
|
|
48
|
+
return FOOTER_BASE_ROWS + safePromptLines + PROCESS_INDICATOR_ROWS;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
export function buildShellFooter(
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
14
|
import type { Cell } from '../types/grid.ts';
|
|
15
|
-
import { DEFAULT_PANEL_PALETTE } from '
|
|
15
|
+
import { DEFAULT_PANEL_PALETTE } from './polish.ts';
|
|
16
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
16
17
|
import { type StatusState, STATE_GLYPHS } from './status-glyphs.ts';
|
|
17
18
|
|
|
18
19
|
// Re-export for downstream consumers that import from this module.
|
|
@@ -54,14 +55,33 @@ export function buildStatusToken(
|
|
|
54
55
|
const suffix = opts?.count !== undefined ? ` (${opts.count})` : '';
|
|
55
56
|
const text = `${glyph} ${label}${suffix}`;
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
const cells: Cell[] = [];
|
|
59
|
+
for (const char of text) {
|
|
60
|
+
const displayW = getDisplayWidth(char);
|
|
61
|
+
cells.push({
|
|
62
|
+
char,
|
|
63
|
+
fg: color,
|
|
64
|
+
bg: '',
|
|
65
|
+
bold: false,
|
|
66
|
+
dim: false,
|
|
67
|
+
underline: false,
|
|
68
|
+
italic: false,
|
|
69
|
+
strikethrough: false,
|
|
70
|
+
});
|
|
71
|
+
if (displayW > 1) {
|
|
72
|
+
// Continuation cell for wide characters (CJK, emoji) to occupy the
|
|
73
|
+
// second terminal column without introducing a broken surrogate half.
|
|
74
|
+
cells.push({
|
|
75
|
+
char: '',
|
|
76
|
+
fg: color,
|
|
77
|
+
bg: '',
|
|
78
|
+
bold: false,
|
|
79
|
+
dim: false,
|
|
80
|
+
underline: false,
|
|
81
|
+
italic: false,
|
|
82
|
+
strikethrough: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return cells;
|
|
67
87
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
|
-
import { buildStyledPanelLine, type StyledPanelSegment } from '
|
|
3
|
+
import { buildStyledPanelLine, type StyledPanelSegment } from './polish.ts';
|
|
4
4
|
|
|
5
5
|
export interface TabStripItem {
|
|
6
6
|
readonly label: string;
|
|
@@ -3,9 +3,10 @@ import { LAYOUT, TOOL_STATUS } from './layout.ts';
|
|
|
3
3
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
4
|
import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
|
|
5
5
|
import { stripDangerousAnsi } from './ansi-sanitize.ts';
|
|
6
|
+
import { friendlyToolLabel } from './tool-labels.ts';
|
|
6
7
|
|
|
7
8
|
const TOOL_NAME_MIN_WIDTH = 8;
|
|
8
|
-
const TOOL_NAME_MAX_WIDTH =
|
|
9
|
+
const TOOL_NAME_MAX_WIDTH = 30;
|
|
9
10
|
|
|
10
11
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
12
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -137,11 +138,11 @@ function extractKeyArg(toolCall: ToolCall): string {
|
|
|
137
138
|
if (url) return url;
|
|
138
139
|
}
|
|
139
140
|
// Agent
|
|
140
|
-
if (typeof args.task === 'string') return args.task.slice(0, 40);
|
|
141
|
+
if (typeof args.task === 'string') return [...args.task].slice(0, 40).join('');
|
|
141
142
|
if (typeof args.mode === 'string') return args.mode;
|
|
142
143
|
// Fallback: first string value
|
|
143
144
|
for (const val of Object.values(args)) {
|
|
144
|
-
if (typeof val === 'string' && val.length > 0) return val.slice(0, 40);
|
|
145
|
+
if (typeof val === 'string' && val.length > 0) return [...val].slice(0, 40).join('');
|
|
145
146
|
}
|
|
146
147
|
return '';
|
|
147
148
|
}
|
|
@@ -200,13 +201,11 @@ export function renderToolCallBlock(
|
|
|
200
201
|
}
|
|
201
202
|
col += 2; // icon + space
|
|
202
203
|
|
|
203
|
-
//
|
|
204
|
-
const rawName = toolCall.name
|
|
205
|
-
? toolCall.name.split('__').pop()!
|
|
206
|
-
: toolCall.name;
|
|
204
|
+
// Human phrase for the tool ("Searching the web") instead of the raw name.
|
|
205
|
+
const rawName = friendlyToolLabel(toolCall.name);
|
|
207
206
|
const keyArg = stripDangerousAnsi(extractKeyArg(toolCall));
|
|
208
207
|
const suffixText = status === 'error' && errorMsg
|
|
209
|
-
? `- ${stripDangerousAnsi(errorMsg).slice(0, 40)}`
|
|
208
|
+
? `- ${[...stripDangerousAnsi(errorMsg)].slice(0, 40).join('')}`
|
|
210
209
|
: status === 'done' && resultSummary
|
|
211
210
|
? `(${stripDangerousAnsi(resultSummary)})`
|
|
212
211
|
: '';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friendly tool labels — turn internal tool names into the phrase a person
|
|
3
|
+
* would say. Shown on tool-call lines in the conversation; the raw tool name
|
|
4
|
+
* is still discoverable in the Agent workspace tool catalog.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const TOOL_LABELS: Record<string, string> = {
|
|
8
|
+
// SDK file/shell/web tools
|
|
9
|
+
read: 'Reading files',
|
|
10
|
+
write: 'Writing a file',
|
|
11
|
+
edit: 'Editing a file',
|
|
12
|
+
exec: 'Running a command',
|
|
13
|
+
fetch: 'Fetching a web page',
|
|
14
|
+
web_search: 'Searching the web',
|
|
15
|
+
find: 'Searching files',
|
|
16
|
+
analyze: 'Analyzing code',
|
|
17
|
+
inspect: 'Inspecting files',
|
|
18
|
+
|
|
19
|
+
// Agent first-class tools
|
|
20
|
+
personal_ops: 'Checking your inbox & calendar',
|
|
21
|
+
research: 'Researching',
|
|
22
|
+
agent_research_runs: 'Researching',
|
|
23
|
+
agent_research_sources: 'Reviewing sources',
|
|
24
|
+
agent_research_report: 'Writing a research report',
|
|
25
|
+
schedule: 'Checking schedules',
|
|
26
|
+
agent_schedule_edit: 'Updating a schedule',
|
|
27
|
+
agent_autonomy_schedule: 'Scheduling background work',
|
|
28
|
+
agent_reminder_schedule: 'Setting a reminder',
|
|
29
|
+
autonomy: 'Checking background work',
|
|
30
|
+
channels: 'Checking message channels',
|
|
31
|
+
agent_channel_send: 'Sending a message',
|
|
32
|
+
notify: 'Sending a notification',
|
|
33
|
+
memory: 'Recalling memory',
|
|
34
|
+
agent_knowledge: 'Searching knowledge',
|
|
35
|
+
agent_knowledge_ingest: 'Saving to knowledge',
|
|
36
|
+
agent_learning_consolidation: 'Consolidating what I learned',
|
|
37
|
+
agent_local_registry: 'Checking notes & routines',
|
|
38
|
+
models: 'Checking models',
|
|
39
|
+
agent_model_compare: 'Comparing models',
|
|
40
|
+
settings: 'Adjusting settings',
|
|
41
|
+
agent_settings_import: 'Importing settings',
|
|
42
|
+
setup: 'Checking setup',
|
|
43
|
+
security: 'Reviewing safety',
|
|
44
|
+
sessions: 'Looking at past conversations',
|
|
45
|
+
computer: 'Using the browser',
|
|
46
|
+
device: 'Checking this device',
|
|
47
|
+
host: 'Checking the assistant service',
|
|
48
|
+
execution: 'Managing background work',
|
|
49
|
+
terminal_process: 'Watching a process',
|
|
50
|
+
support: 'Preparing a support bundle',
|
|
51
|
+
vibe: 'Reading your VIBE profile',
|
|
52
|
+
workspace: 'Driving the workspace',
|
|
53
|
+
agent_harness: 'Checking app internals',
|
|
54
|
+
route: 'Choosing the best route',
|
|
55
|
+
delegation: 'Handing work off',
|
|
56
|
+
audit: 'Reviewing evidence',
|
|
57
|
+
context: 'Reading project context',
|
|
58
|
+
agent_documents: 'Working on a document',
|
|
59
|
+
agent_artifacts: 'Managing saved files',
|
|
60
|
+
agent_media_generate: 'Generating media',
|
|
61
|
+
agent_operator_method: 'Calling the assistant service',
|
|
62
|
+
agent_operator_briefing: 'Preparing a briefing',
|
|
63
|
+
agent_operator_action: 'Running an approved action',
|
|
64
|
+
agent_work_plan: 'Organizing work',
|
|
65
|
+
agent_review_packet_presets: 'Preparing a review packet',
|
|
66
|
+
agent_review_packet_share: 'Sharing a review packet',
|
|
67
|
+
propose_skill_drafts: 'Proposing skill drafts',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Exported catalog of all registered tool IDs and their human phrases.
|
|
72
|
+
* Tests import this to enumerate the full set of tool IDs for coverage checks.
|
|
73
|
+
*/
|
|
74
|
+
export const TOOL_LABELS_CATALOG: Readonly<Record<string, string>> = TOOL_LABELS;
|
|
75
|
+
|
|
76
|
+
function humanize(name: string): string {
|
|
77
|
+
const short = name.includes('__') ? name.split('__').pop()! : name;
|
|
78
|
+
return (short
|
|
79
|
+
.replace(/^agent[_-]/, '')
|
|
80
|
+
.replace(/[_-]+/g, ' ')
|
|
81
|
+
.trim())
|
|
82
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return the human phrase for a tool call, falling back to a humanized
|
|
87
|
+
* version of the raw name for unknown tools (including MCP tools).
|
|
88
|
+
*/
|
|
89
|
+
export function friendlyToolLabel(toolName: string): string {
|
|
90
|
+
const short = toolName.includes('__') ? toolName.split('__').pop()! : toolName;
|
|
91
|
+
return TOOL_LABELS[short] ?? TOOL_LABELS[toolName] ?? humanize(toolName);
|
|
92
|
+
}
|
|
@@ -215,113 +215,65 @@ export class UIFactory {
|
|
|
215
215
|
const bottomLine = createBaseLine();
|
|
216
216
|
for (let x = 0; x < boxWidth; x++) bottomLine[boxStartX + x] = { char: GLYPHS.surface.bottom, fg: BORDER_COLOR, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
217
217
|
lines.push(bottomLine);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: '#38bdf8', bold: true });
|
|
221
|
-
if (composerPendingRisk && composerPendingRisk !== 'none') {
|
|
222
|
-
const riskColor = composerPendingRisk === 'approval-wait'
|
|
223
|
-
? '#f59e0b'
|
|
224
|
-
: composerPendingRisk === 'shell'
|
|
225
|
-
? '#ef4444'
|
|
226
|
-
: composerPendingRisk === 'remote'
|
|
227
|
-
? '#a78bfa'
|
|
228
|
-
: '#f59e0b';
|
|
229
|
-
composerTokens.push({ text: ` risk:${composerPendingRisk} `, fg: riskColor, bold: true });
|
|
230
|
-
}
|
|
231
|
-
if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
|
|
232
|
-
if (composerFlags && composerFlags.length > 0) composerTokens.push({ text: ` flags:${composerFlags.join(',')} `, fg: '244', dim: true });
|
|
233
|
-
if (composerTokens.length > 0) {
|
|
234
|
-
const postureLine = createBaseLine();
|
|
235
|
-
let px = 2;
|
|
236
|
-
for (const token of composerTokens) {
|
|
237
|
-
for (const ch of token.text) {
|
|
238
|
-
if (px >= width) break;
|
|
239
|
-
postureLine[px] = {
|
|
240
|
-
char: ch,
|
|
241
|
-
fg: token.fg,
|
|
242
|
-
bg: '',
|
|
243
|
-
bold: token.bold ?? false,
|
|
244
|
-
dim: token.dim ?? false,
|
|
245
|
-
underline: false,
|
|
246
|
-
italic: false,
|
|
247
|
-
strikethrough: false,
|
|
248
|
-
};
|
|
249
|
-
px += getDisplayWidth(ch);
|
|
250
|
-
}
|
|
251
|
-
if (px >= width) break;
|
|
252
|
-
}
|
|
253
|
-
lines.push(postureLine);
|
|
254
|
-
lines.push(createBaseLine());
|
|
255
|
-
}
|
|
218
|
+
|
|
219
|
+
// ── Status line: model · context meter · tokens, with alerts on the right ──
|
|
256
220
|
const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
|
|
257
|
-
|
|
258
|
-
const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number };
|
|
221
|
+
const u = usage as { input?: number; output?: number; up?: number; down?: number };
|
|
259
222
|
const inp = u.input ?? u.up ?? 0;
|
|
260
223
|
const out = u.output ?? u.down ?? 0;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const tokenLine = ` Token Usage [ Input: ${fmtNum(inp)}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)} ]`;
|
|
266
|
-
const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
|
|
267
|
-
const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
|
|
268
|
-
lines.push(this.stringToLine(statsLine, width, { fg: isRecentlyCopied ? '81' : '244', bold: isRecentlyCopied }));
|
|
269
|
-
// Context usage progress bar
|
|
224
|
+
const statusTokens: Array<{ text: string; fg: string; bold?: boolean }> = [];
|
|
225
|
+
if (model) {
|
|
226
|
+
statusTokens.push({ text: provider ? `${model} · ${provider}` : model, fg: '245' });
|
|
227
|
+
}
|
|
270
228
|
if (contextWindow && contextWindow > 0) {
|
|
271
229
|
const ctxTokens = lastInputTokens ?? 0;
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
230
|
+
const pct = Math.min(100, Math.round((ctxTokens / contextWindow) * 100));
|
|
231
|
+
const filled = Math.round((pct / 100) * 6);
|
|
232
|
+
const meter = GLYPHS.meter.filled.repeat(filled) + GLYPHS.meter.empty.repeat(6 - filled);
|
|
233
|
+
const meterFg = pct >= 85 ? '#ef4444' : pct >= 65 ? '#f59e0b' : '240';
|
|
234
|
+
statusTokens.push({ text: `context ${meter} ${pct}%`, fg: meterFg, bold: pct >= 85 });
|
|
235
|
+
}
|
|
236
|
+
if (inp > 0 || out > 0) {
|
|
237
|
+
statusTokens.push({ text: `↑${fmtNum(inp)} ↓${fmtNum(out)}`, fg: '240' });
|
|
238
|
+
}
|
|
239
|
+
if (composerPendingRisk === 'approval-wait') {
|
|
240
|
+
statusTokens.push({ text: 'waiting for your approval', fg: '#f59e0b', bold: true });
|
|
278
241
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
242
|
+
const rightNotice = isRecentlyCopied
|
|
243
|
+
? { text: `copied ${GLYPHS.status.success} `, fg: '81', bold: true }
|
|
244
|
+
: dangerMode
|
|
245
|
+
? { text: '⚠ auto-approve is on ', fg: '#ef4444', bold: true }
|
|
246
|
+
: null;
|
|
247
|
+
const statusLine = createBaseLine();
|
|
248
|
+
let sx = 3;
|
|
249
|
+
const writeStatusText = (text: string, fg: string, bold = false) => {
|
|
250
|
+
for (const ch of text) {
|
|
251
|
+
if (sx >= width - 1) break;
|
|
252
|
+
statusLine[sx] = { char: ch, fg, bg: '', bold, dim: false, underline: false, italic: false, strikethrough: false };
|
|
253
|
+
sx += getDisplayWidth(ch);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
statusTokens.forEach((token, index) => {
|
|
257
|
+
if (index > 0) writeStatusText(` ${GLYPHS.navigation.pipeSeparator} `, '238');
|
|
258
|
+
writeStatusText(token.text, token.fg, token.bold ?? false);
|
|
259
|
+
});
|
|
260
|
+
if (rightNotice) {
|
|
261
|
+
let col = Math.max(sx + 2, width - getDisplayWidth(rightNotice.text));
|
|
262
|
+
for (const ch of rightNotice.text) {
|
|
263
|
+
if (col >= width) break;
|
|
264
|
+
statusLine[col] = { char: ch, fg: rightNotice.fg, bg: '', bold: rightNotice.bold ?? false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
265
|
+
col += getDisplayWidth(ch);
|
|
289
266
|
}
|
|
290
|
-
if (toolCount) ctxParts.push(`${toolCount} tools`);
|
|
291
|
-
if (hitlMode) ctxParts.push(`hitl:${hitlMode}`);
|
|
292
|
-
if (composerMode) ctxParts.push(`mode:${composerMode}`);
|
|
293
|
-
if (composerStatus && composerStatus !== 'idle') ctxParts.push(`status:${composerStatus}`);
|
|
294
|
-
if (composerFlags && composerFlags.length > 0) ctxParts.push(composerFlags.join(','));
|
|
295
|
-
const ctxLine = ' ' + ctxParts.join(` ${GLYPHS.navigation.pipeSeparator} `);
|
|
296
|
-
lines.push(createBaseLine());
|
|
297
|
-
lines.push(this.stringToLine(truncateDisplay(ctxLine, width), width, { fg: '240', dim: true }));
|
|
298
|
-
lines.push(createBaseLine());
|
|
299
267
|
}
|
|
268
|
+
lines.push(statusLine);
|
|
269
|
+
|
|
270
|
+
// ── Hints line ──
|
|
300
271
|
if (showExitNotice) {
|
|
301
|
-
|
|
302
|
-
lines.push(this.stringToLine(fitDisplay(notice, width), width, { fg: '196', bold: true }));
|
|
272
|
+
lines.push(this.stringToLine(fitDisplay(' Press Ctrl+C again to exit ', width), width, { fg: '196', bold: true }));
|
|
303
273
|
} else {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const helpW = getDisplayWidth(help);
|
|
307
|
-
const dangerW = getDisplayWidth(dangerWarn);
|
|
308
|
-
const spacerW = Math.max(0, width - helpW - dangerW);
|
|
309
|
-
const combinedLine = help + ' '.repeat(spacerW) + dangerWarn;
|
|
310
|
-
const line = this.stringToLine(truncateDisplay(combinedLine, width), width, { fg: '240', dim: true });
|
|
311
|
-
// Overlay the danger warning in red bold
|
|
312
|
-
if (dangerMode && dangerW > 0) {
|
|
313
|
-
let col = helpW + spacerW;
|
|
314
|
-
for (const ch of dangerWarn) {
|
|
315
|
-
if (col >= width) break;
|
|
316
|
-
const cw = getDisplayWidth(ch);
|
|
317
|
-
line[col] = { char: ch, fg: '#ef4444', bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
|
|
318
|
-
if (cw === 2 && col + 1 < width) line[col + 1] = { ...line[col], char: '' };
|
|
319
|
-
col += cw;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
lines.push(line);
|
|
274
|
+
const hints = ` /help commands ${GLYPHS.navigation.pipeSeparator} Ctrl+P settings ${GLYPHS.navigation.pipeSeparator} Ctrl+O activity `;
|
|
275
|
+
lines.push(this.stringToLine(truncateDisplay(hints, width), width, { fg: '240', dim: true }));
|
|
323
276
|
}
|
|
324
|
-
lines.push(createBaseLine());
|
|
325
277
|
return lines;
|
|
326
278
|
}
|
|
327
279
|
|