@pellux/goodvibes-tui 0.29.0 → 1.1.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 +69 -98
- package/README.md +16 -7
- 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/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +90 -4
- package/src/core/stream-event-wiring.ts +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/export/cost-utils.ts +90 -8
- package/src/input/command-registry.ts +16 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/session-content.ts +12 -0
- package/src/input/commands/session-workflow.ts +3 -0
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/handler-content-actions.ts +30 -11
- package/src/input/handler-feed-routes.ts +63 -1
- package/src/input/handler-feed.ts +12 -0
- package/src/input/handler-interactions.ts +2 -0
- package/src/input/handler-types.ts +1 -0
- package/src/input/handler.ts +4 -0
- package/src/input/input-history.ts +17 -4
- package/src/main.ts +27 -23
- package/src/panels/agent-inspector-shared.ts +33 -10
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/development.ts +1 -1
- package/src/panels/cockpit-read-model.ts +16 -11
- package/src/panels/cost-tracker-panel.ts +28 -5
- package/src/panels/debug-panel.ts +11 -5
- package/src/panels/diff-panel.ts +122 -22
- package/src/panels/docs-panel.ts +9 -0
- package/src/panels/file-explorer-panel.ts +9 -0
- package/src/panels/git-panel.ts +11 -11
- package/src/panels/knowledge-graph-panel.ts +9 -0
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/panel-list-panel.ts +9 -0
- package/src/panels/project-planning-panel.ts +10 -0
- package/src/panels/provider-health-tracker.ts +5 -1
- package/src/panels/provider-health-views.ts +8 -1
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/session-browser-panel.ts +9 -0
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +13 -0
- package/src/panels/work-plan-panel.ts +10 -0
- package/src/renderer/agent-detail-modal.ts +42 -12
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/fullscreen-primitives.ts +1 -1
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/model-picker-overlay.ts +8 -499
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +7 -9
- package/src/renderer/process-indicator.ts +13 -25
- package/src/renderer/process-modal.ts +17 -2
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/shell-surface.ts +29 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +98 -14
- package/src/renderer/ui-primitives.ts +18 -1
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +5 -0
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +12 -1
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/utils/splash-lines.ts +10 -2
- package/src/version.ts +1 -1
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -100
- package/src/renderer/status-token.ts +0 -67
|
@@ -114,6 +114,7 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
114
114
|
sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
115
115
|
wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
116
116
|
componentHealthMonitor: import('@/runtime/index.ts').ComponentHealthMonitor;
|
|
117
|
+
hydrateSessionUsage?: () => void;
|
|
117
118
|
};
|
|
118
119
|
|
|
119
120
|
export function createBootstrapCommandContext(
|
|
@@ -167,6 +168,7 @@ export function createBootstrapCommandContext(
|
|
|
167
168
|
sessionLineageTracker,
|
|
168
169
|
wrfcController,
|
|
169
170
|
changeTracker,
|
|
171
|
+
hydrateSessionUsage,
|
|
170
172
|
planManager,
|
|
171
173
|
adaptivePlanner,
|
|
172
174
|
sessionOrchestration,
|
|
@@ -223,6 +225,7 @@ export function createBootstrapCommandContext(
|
|
|
223
225
|
sessionLineageTracker,
|
|
224
226
|
wrfcController,
|
|
225
227
|
changeTracker,
|
|
228
|
+
hydrateSessionUsage,
|
|
226
229
|
});
|
|
227
230
|
const provider = createBootstrapCommandProviderSection({
|
|
228
231
|
providerRegistry,
|
|
@@ -109,6 +109,7 @@ export interface BootstrapCommandSectionOptions {
|
|
|
109
109
|
readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
|
|
110
110
|
readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
|
|
111
111
|
readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
|
|
112
|
+
readonly hydrateSessionUsage?: () => void;
|
|
112
113
|
readonly agentManager?: ShellAgentManagerService;
|
|
113
114
|
readonly modeManager?: ShellModeManagerService;
|
|
114
115
|
readonly automationManager?: ShellAutomationManagerRuntimeService;
|
|
@@ -300,7 +301,7 @@ export function createBootstrapCommandActions(
|
|
|
300
301
|
export function createBootstrapCommandSessionSection(
|
|
301
302
|
options: Pick<
|
|
302
303
|
BootstrapCommandSectionOptions,
|
|
303
|
-
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker'
|
|
304
|
+
'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage'
|
|
304
305
|
>,
|
|
305
306
|
): BootstrapCommandSessionSection {
|
|
306
307
|
return {
|
|
@@ -311,6 +312,7 @@ export function createBootstrapCommandSessionSection(
|
|
|
311
312
|
sessionLineageTracker: options.sessionLineageTracker,
|
|
312
313
|
wrfcController: options.wrfcController,
|
|
313
314
|
changeTracker: options.changeTracker,
|
|
315
|
+
hydrateSessionUsage: options.hydrateSessionUsage,
|
|
314
316
|
};
|
|
315
317
|
}
|
|
316
318
|
|
|
@@ -28,6 +28,7 @@ import { loadBootstrapSystemPrompt, syncConfiguredServices } from '@/runtime/ind
|
|
|
28
28
|
import { registerBootstrapHookBridge } from '@/runtime/index.ts';
|
|
29
29
|
import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
|
|
30
30
|
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
31
|
+
import { setPricingSource } from '../export/cost-utils.ts';
|
|
31
32
|
import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
|
|
32
33
|
import { join } from 'node:path';
|
|
33
34
|
import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
|
|
@@ -255,6 +256,10 @@ export async function initializeBootstrapCore(
|
|
|
255
256
|
providerRegistry.initModelLimits();
|
|
256
257
|
services.benchmarkStore.initBenchmarks();
|
|
257
258
|
providerRegistry.initCatalog();
|
|
259
|
+
// Wire cost-utils to the live catalog so cost displays distinguish real
|
|
260
|
+
// pricing from unpriced (WO-315) instead of silently reading zero for any
|
|
261
|
+
// model the small static fallback table doesn't cover.
|
|
262
|
+
setPricingSource(() => providerRegistry.getRawCatalogModels());
|
|
258
263
|
services.keybindingsManager.loadFromDisk();
|
|
259
264
|
domainDispatch.syncControlPlaneState({
|
|
260
265
|
enabled: Boolean(configManager.get('controlPlane.enabled')),
|
|
@@ -29,6 +29,8 @@ export interface ResumeSessionOptions {
|
|
|
29
29
|
readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
|
|
30
30
|
readonly providerRegistry: Pick<ProviderRegistry, 'get' | 'getCurrentModel' | 'getForModel' | 'require'>;
|
|
31
31
|
readonly homeDirectory: string;
|
|
32
|
+
/** See CommandSessionServices.hydrateSessionUsage (command-registry.ts). */
|
|
33
|
+
readonly hydrateSessionUsage?: () => void;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function createResumeSessionHandler(options: ResumeSessionOptions): (sessionId: string) => void {
|
|
@@ -65,6 +67,9 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
|
|
|
65
67
|
});
|
|
66
68
|
},
|
|
67
69
|
});
|
|
70
|
+
// Hydrate the footer's token counters from the resumed history now that
|
|
71
|
+
// fromJSON()/journal replay are both applied — before requestRender() below.
|
|
72
|
+
options.hydrateSessionUsage?.();
|
|
68
73
|
options.onSessionIdChanged?.(sessionId);
|
|
69
74
|
if (meta?.model) options.runtime.model = meta.model;
|
|
70
75
|
if (meta?.provider) options.runtime.provider = meta.provider;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import type
|
|
2
|
+
import { sumConversationUsage, type ConversationManager } from '../core/conversation';
|
|
3
3
|
import type { Orchestrator } from '../core/orchestrator';
|
|
4
4
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
5
|
import type { RuntimeEventBus } from '@/runtime/index.ts';
|
|
@@ -93,6 +93,15 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
93
93
|
} = options;
|
|
94
94
|
|
|
95
95
|
const systemMessagesPanel = new SystemMessagesPanel(configManager, services.componentHealthMonitor);
|
|
96
|
+
// W0.9: after any resume seam replays historical messages into `conversation`,
|
|
97
|
+
// the freshly-constructed `orchestrator` still has its zeroed default usage
|
|
98
|
+
// (SDK gap — Orchestrator.usage is never persisted/reseeded). Recompute it
|
|
99
|
+
// from the replayed history so the footer doesn't show Input: 0 post-resume.
|
|
100
|
+
const hydrateSessionUsage = (): void => {
|
|
101
|
+
const { usage, lastInputTokens } = sumConversationUsage(conversation.getMessageSnapshot());
|
|
102
|
+
orchestrator.usage = usage;
|
|
103
|
+
orchestrator.lastInputTokens = lastInputTokens;
|
|
104
|
+
};
|
|
96
105
|
const resumeSession = createResumeSessionHandler({
|
|
97
106
|
runtimeBus,
|
|
98
107
|
runtime,
|
|
@@ -107,6 +116,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
107
116
|
configManager,
|
|
108
117
|
providerRegistry: services.providerRegistry,
|
|
109
118
|
homeDirectory: services.homeDirectory,
|
|
119
|
+
hydrateSessionUsage,
|
|
110
120
|
});
|
|
111
121
|
|
|
112
122
|
const foundationClients = createRuntimeFoundationClients({
|
|
@@ -282,6 +292,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
282
292
|
},
|
|
283
293
|
completeModelSelectionSideEffect,
|
|
284
294
|
componentHealthMonitor: services.componentHealthMonitor,
|
|
295
|
+
hydrateSessionUsage,
|
|
285
296
|
});
|
|
286
297
|
commandContextRef = commandContext;
|
|
287
298
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-scheduler.ts — same-tick render coalescing for the TUI shell (WO-208).
|
|
3
|
+
*
|
|
4
|
+
* main.ts fans out its own direct render() calls across turn/stream/input wiring
|
|
5
|
+
* (~14 direct invocation contexts). Each one previously ran a full synchronous
|
|
6
|
+
* Compositor.composite() the instant it was called. When several fire within a
|
|
7
|
+
* single event-loop tick — a streaming burst is the canonical case — only the
|
|
8
|
+
* LAST frame is ever visible; the earlier composites are immediately overwritten.
|
|
9
|
+
* This scheduler collapses every schedule() call made within one tick into a
|
|
10
|
+
* single composite flushed on the microtask queue, so the tick produces exactly
|
|
11
|
+
* one frame. That frame is byte-identical to what the last synchronous render()
|
|
12
|
+
* would have produced, because the coalesced flush runs after all of the tick's
|
|
13
|
+
* synchronous state mutations, reading the same final state the last direct
|
|
14
|
+
* render() would have read.
|
|
15
|
+
*
|
|
16
|
+
* This is deliberately SEPARATE from bootstrap-core's requestRender(), which is a
|
|
17
|
+
* cross-tick, 16ms-throttled (~60fps) coalescer for the panel/input/runtime
|
|
18
|
+
* fan-out — that one caps repaint RATE across ticks. This one collapses the
|
|
19
|
+
* WITHIN-tick burst with no added latency: a microtask flushes at the tail of the
|
|
20
|
+
* current tick, before the event loop yields to I/O, so the frame still lands in
|
|
21
|
+
* the same turn it was requested in.
|
|
22
|
+
*
|
|
23
|
+
* flushNow() preserves a synchronous immediate path for callers that genuinely
|
|
24
|
+
* need the frame composited before they return. Terminal resize is the known
|
|
25
|
+
* case: the resize handler resets the compositor diff and must repaint against
|
|
26
|
+
* the new dimensions synchronously rather than waiting for the microtask.
|
|
27
|
+
* flushNow() also clears any pending coalesced flush, so a resize (or any
|
|
28
|
+
* immediate paint) followed by an already-queued microtask never double-composites.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface RenderScheduler {
|
|
32
|
+
/**
|
|
33
|
+
* Coalesced request: schedule a composite for the current tick. N calls within
|
|
34
|
+
* one tick collapse into exactly one composite, flushed on the microtask queue.
|
|
35
|
+
*/
|
|
36
|
+
readonly schedule: () => void;
|
|
37
|
+
/**
|
|
38
|
+
* Immediate request: run the composite synchronously right now, and cancel any
|
|
39
|
+
* pending coalesced flush so the tick still composites only once. Use only where
|
|
40
|
+
* a caller genuinely requires synchronous output (terminal resize).
|
|
41
|
+
*/
|
|
42
|
+
readonly flushNow: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a render scheduler around `renderNow` (the synchronous composite closure).
|
|
47
|
+
*
|
|
48
|
+
* `scheduleFlush` is the deferral primitive; it defaults to `queueMicrotask` so
|
|
49
|
+
* bursts coalesce within the current tick. Tests and benchmarks inject a manual
|
|
50
|
+
* queue to flush deterministically.
|
|
51
|
+
*/
|
|
52
|
+
export function createRenderScheduler(
|
|
53
|
+
renderNow: () => void,
|
|
54
|
+
scheduleFlush: (flush: () => void) => void = queueMicrotask,
|
|
55
|
+
): RenderScheduler {
|
|
56
|
+
let scheduled = false;
|
|
57
|
+
|
|
58
|
+
const flush = (): void => {
|
|
59
|
+
// A synchronous flushNow() (or a superseding immediate paint) before this
|
|
60
|
+
// microtask ran clears `scheduled`, turning this into a no-op so a single
|
|
61
|
+
// tick never composites twice.
|
|
62
|
+
if (!scheduled) return;
|
|
63
|
+
scheduled = false;
|
|
64
|
+
renderNow();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const schedule = (): void => {
|
|
68
|
+
if (scheduled) return;
|
|
69
|
+
scheduled = true;
|
|
70
|
+
scheduleFlush(flush);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const flushNow = (): void => {
|
|
74
|
+
// Satisfy any pending coalesced flush, then composite synchronously.
|
|
75
|
+
scheduled = false;
|
|
76
|
+
renderNow();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return { schedule, flushNow };
|
|
80
|
+
}
|
|
@@ -20,7 +20,8 @@ const SEPARATOR = '━'.repeat(ART_W);
|
|
|
20
20
|
*/
|
|
21
21
|
const TAGLINE = '[ good vibes ・ A I ・ いい雰囲気 ]';
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const versionLine = (version: string) =>
|
|
24
|
+
` ✦ v${version} █ terminal AI assistant █ 自動コード ✦`;
|
|
24
25
|
|
|
25
26
|
/** Fixed hint line — the three primary shell entry points. */
|
|
26
27
|
const HINT_LINE = 'Ctrl+P panels / ? help / F2 processes';
|
|
@@ -35,6 +36,13 @@ export interface SplashOptions {
|
|
|
35
36
|
* When present, the splash advertises a resume affordance.
|
|
36
37
|
*/
|
|
37
38
|
lastSessionId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Version string rendered on the splash's version line. Defaults to the
|
|
41
|
+
* build VERSION; injectable so the golden-frame tests can pin a fixture —
|
|
42
|
+
* the version's display width shifts the line's centering, so goldens tied
|
|
43
|
+
* to the live VERSION break on every release bump (v1.0.0 release failure).
|
|
44
|
+
*/
|
|
45
|
+
version?: string;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
/** Collapse a $HOME-prefixed working directory to a leading `~`. */
|
|
@@ -54,7 +62,7 @@ export function getSplashLines(columns: number, opts: SplashOptions = {}): strin
|
|
|
54
62
|
...ART_LINES.map((line) => center(line, columns)),
|
|
55
63
|
center(SEPARATOR, columns),
|
|
56
64
|
center(TAGLINE, columns),
|
|
57
|
-
center(
|
|
65
|
+
center(versionLine(opts.version ?? VERSION), columns),
|
|
58
66
|
'',
|
|
59
67
|
];
|
|
60
68
|
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '
|
|
9
|
+
let _version = '1.1.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { type Line, createStyledCell } from '../types/grid.ts';
|
|
2
|
-
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
-
|
|
5
|
-
/** Color by file extension category. */
|
|
6
|
-
function getFileColor(name: string): string {
|
|
7
|
-
if (name.endsWith('/')) return '#00ffff'; // directory
|
|
8
|
-
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
|
9
|
-
if (['ts', 'tsx', 'js', 'jsx', 'mjs'].includes(ext)) return '#dcdcaa';
|
|
10
|
-
if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '#ce9178';
|
|
11
|
-
if (['md', 'txt', 'rst'].includes(ext)) return '252';
|
|
12
|
-
if (['sh', 'bash', 'zsh'].includes(ext)) return '#22c55e';
|
|
13
|
-
if (['css', 'scss', 'less'].includes(ext)) return '#569cd6';
|
|
14
|
-
if (['html', 'htm', 'xml', 'svg'].includes(ext)) return '#f97316';
|
|
15
|
-
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'].includes(ext)) return '#a855f7';
|
|
16
|
-
if (['lock', 'env', 'gitignore'].includes(name)) return '238';
|
|
17
|
-
return '252';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FileTreeEntry {
|
|
21
|
-
name: string;
|
|
22
|
-
isDir: boolean;
|
|
23
|
-
size?: number;
|
|
24
|
-
depth: number;
|
|
25
|
-
isLast: boolean;
|
|
26
|
-
isLastAtDepth: boolean[]; // tracks if each ancestor was last child
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* renderFileTree - Render a directory listing as a tree with colored file types.
|
|
31
|
-
* Returns Line[] for the cell-based pipeline.
|
|
32
|
-
*/
|
|
33
|
-
export function renderFileTree(
|
|
34
|
-
entries: FileTreeEntry[],
|
|
35
|
-
width: number,
|
|
36
|
-
title?: string
|
|
37
|
-
): Line[] {
|
|
38
|
-
const lines: Line[] = [];
|
|
39
|
-
|
|
40
|
-
// Optional header
|
|
41
|
-
if (title) {
|
|
42
|
-
lines.push(UIFactory.stringToLine(` [dir] ${title}`, width, { fg: '#00ffff', bold: true }));
|
|
43
|
-
lines.push(UIFactory.stringToLine(' ' + '-'.repeat(width - 2), width, { fg: '240' }));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
const { name, isDir, size, depth, isLast, isLastAtDepth } = entry;
|
|
48
|
-
|
|
49
|
-
// Build tree prefix
|
|
50
|
-
let prefix = '';
|
|
51
|
-
for (let d = 0; d < depth; d++) {
|
|
52
|
-
if (d < depth - 1) {
|
|
53
|
-
prefix += isLastAtDepth[d] ? ' ' : '| ';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (depth > 0) {
|
|
57
|
-
prefix += isLast ? '`-- ' : '|-- ';
|
|
58
|
-
} else {
|
|
59
|
-
prefix += '';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const displayName = isDir ? name + '/' : name;
|
|
63
|
-
const fg = getFileColor(isDir ? displayName : name);
|
|
64
|
-
|
|
65
|
-
// Size info (dimmed)
|
|
66
|
-
let sizeStr = '';
|
|
67
|
-
if (size !== undefined && !isDir) {
|
|
68
|
-
if (size < 1024) sizeStr = ` ${size}B`;
|
|
69
|
-
else if (size < 1024 * 1024) sizeStr = ` ${(size / 1024).toFixed(1)}K`;
|
|
70
|
-
else sizeStr = ` ${(size / (1024 * 1024)).toFixed(1)}M`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const fullText = prefix + displayName;
|
|
74
|
-
const sizeW = getDisplayWidth(sizeStr);
|
|
75
|
-
const nameW = getDisplayWidth(fullText);
|
|
76
|
-
const maxNameW = width - sizeW - 1;
|
|
77
|
-
|
|
78
|
-
const truncated = nameW > maxNameW ? fullText.slice(0, Math.max(0, maxNameW - 3)) + '...' : fullText;
|
|
79
|
-
|
|
80
|
-
// Compose: name + padding + size
|
|
81
|
-
const paddingW = Math.max(0, width - getDisplayWidth(truncated) - sizeW);
|
|
82
|
-
const fullLine = truncated + ' '.repeat(paddingW);
|
|
83
|
-
|
|
84
|
-
// Build the line with per-character styles
|
|
85
|
-
const line = UIFactory.stringToLine(fullLine, width, { fg });
|
|
86
|
-
|
|
87
|
-
// Dim the size info
|
|
88
|
-
if (sizeStr && sizeW > 0) {
|
|
89
|
-
const sizeStartX = width - sizeW;
|
|
90
|
-
let cx = sizeStartX;
|
|
91
|
-
for (const ch of sizeStr) {
|
|
92
|
-
if (cx >= width) break;
|
|
93
|
-
const cw = getDisplayWidth(ch);
|
|
94
|
-
line[cx] = createStyledCell(ch, { fg: '240', dim: true });
|
|
95
|
-
if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
|
|
96
|
-
cx += cw;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
lines.push(line);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return lines;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* parseListDirOutput - Convert a simple list directory output string to FileTreeEntry[].
|
|
108
|
-
* Expects one path per line, relative paths with '/' for directories.
|
|
109
|
-
*/
|
|
110
|
-
export function parseListDirOutput(output: string, rootDir: string): FileTreeEntry[] {
|
|
111
|
-
const rawLines = output.trim().split('\n').filter(Boolean);
|
|
112
|
-
const entries: FileTreeEntry[] = [];
|
|
113
|
-
|
|
114
|
-
for (const line of rawLines) {
|
|
115
|
-
const trimmed = line.trim();
|
|
116
|
-
if (!trimmed) continue;
|
|
117
|
-
const isDir = trimmed.endsWith('/');
|
|
118
|
-
const name = isDir ? trimmed.slice(0, -1).split('/').pop() + '/' : trimmed.split('/').pop() ?? trimmed;
|
|
119
|
-
const depth = (trimmed.match(/\//g) ?? []).length - (isDir ? 1 : 0);
|
|
120
|
-
|
|
121
|
-
entries.push({
|
|
122
|
-
name: name || trimmed,
|
|
123
|
-
isDir,
|
|
124
|
-
depth: Math.max(0, depth),
|
|
125
|
-
isLast: false,
|
|
126
|
-
isLastAtDepth: [],
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Mark isLast for each entry (last sibling at same depth under same parent)
|
|
131
|
-
for (let i = 0; i < entries.length; i++) {
|
|
132
|
-
const nextSameOrLower = entries.slice(i + 1).findIndex(
|
|
133
|
-
e => e.depth <= entries[i].depth
|
|
134
|
-
);
|
|
135
|
-
entries[i].isLast = nextSameOrLower === 0 || nextSameOrLower === -1;
|
|
136
|
-
|
|
137
|
-
// Build isLastAtDepth[d] = true if the ancestor at depth d was the last child
|
|
138
|
-
// of its parent. This governs whether to draw '| ' or ' ' vertical bars.
|
|
139
|
-
const isLastAtDepth: boolean[] = new Array(entries[i].depth).fill(false);
|
|
140
|
-
for (let d = 0; d < entries[i].depth; d++) {
|
|
141
|
-
// Walk backwards to find the most recent ancestor at depth d
|
|
142
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
143
|
-
if (entries[j].depth === d) {
|
|
144
|
-
isLastAtDepth[d] = entries[j].isLast;
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
entries[i].isLastAtDepth = isLastAtDepth;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return entries;
|
|
153
|
-
}
|
package/src/renderer/progress.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
-
import { abbreviateCount } from '../utils/format-number.ts';
|
|
5
|
-
import { SPINNER_FRAMES } from './ui-primitives.ts';
|
|
6
|
-
|
|
7
|
-
// Rich spinner frames (used by progress indicators) — single source in ui-primitives.ts.
|
|
8
|
-
export { SPINNER_FRAMES };
|
|
9
|
-
// Braille thinking spinner frames (used by the orchestrator thinking animation) — same frame set.
|
|
10
|
-
export const THINKING_SPINNER_FRAMES = SPINNER_FRAMES;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* renderSpinner - Render a spinner with label as a single Line.
|
|
14
|
-
*/
|
|
15
|
-
export function renderSpinner(
|
|
16
|
-
frame: string,
|
|
17
|
-
label: string,
|
|
18
|
-
width: number,
|
|
19
|
-
fg: string = '135'
|
|
20
|
-
): Line {
|
|
21
|
-
const text = ` ${frame} ${label}`;
|
|
22
|
-
return UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg, bold: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* renderToolProgress - Render tool execution progress.
|
|
27
|
-
* E.g. "[2/5] Editing src/config.ts..."
|
|
28
|
-
*/
|
|
29
|
-
export function renderToolProgress(
|
|
30
|
-
current: number,
|
|
31
|
-
total: number,
|
|
32
|
-
label: string,
|
|
33
|
-
width: number
|
|
34
|
-
): Line[] {
|
|
35
|
-
const counter = `[${current}/${total}]`;
|
|
36
|
-
const text = ` ${counter} ${label}`;
|
|
37
|
-
return [
|
|
38
|
-
UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg: '#ffcc00', bold: true }),
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* renderTokenBar - Render a token usage bar for the footer.
|
|
44
|
-
* Shows used/max tokens as a visual bar + numbers.
|
|
45
|
-
*/
|
|
46
|
-
export function renderTokenBar(
|
|
47
|
-
used: number,
|
|
48
|
-
max: number,
|
|
49
|
-
width: number,
|
|
50
|
-
model: string,
|
|
51
|
-
toolCount: number
|
|
52
|
-
): Line[] {
|
|
53
|
-
const lines: Line[] = [];
|
|
54
|
-
|
|
55
|
-
// Stats row
|
|
56
|
-
const usedK = abbreviateCount(used, { noM: true });
|
|
57
|
-
const maxK = abbreviateCount(max, { noM: true });
|
|
58
|
-
const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
|
|
59
|
-
const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
|
|
60
|
-
const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
|
|
61
|
-
|
|
62
|
-
// Progress bar
|
|
63
|
-
const barLabel = ' ctx ';
|
|
64
|
-
const barLabelW = getDisplayWidth(barLabel);
|
|
65
|
-
const barW = Math.max(10, Math.floor(width * 0.3));
|
|
66
|
-
const filled = Math.round((pct / 100) * barW);
|
|
67
|
-
const empty = barW - filled;
|
|
68
|
-
|
|
69
|
-
// Color based on usage
|
|
70
|
-
const barFg = pct > 85 ? '#ef4444' : pct > 60 ? '#ffcc00' : '#22c55e';
|
|
71
|
-
|
|
72
|
-
const bar = '#'.repeat(filled) + '-'.repeat(empty);
|
|
73
|
-
const barText = barLabel + bar;
|
|
74
|
-
|
|
75
|
-
const statsW = getDisplayWidth(statsText);
|
|
76
|
-
const barTextW = getDisplayWidth(barText);
|
|
77
|
-
const spacingW = Math.max(1, width - statsW - barTextW - 2);
|
|
78
|
-
|
|
79
|
-
// Build with mixed colors
|
|
80
|
-
const line = UIFactory.stringToLine(statsText + ' '.repeat(spacingW), width, { fg: '244', dim: true });
|
|
81
|
-
|
|
82
|
-
// Overlay the bar with color
|
|
83
|
-
let barStartX = getDisplayWidth(statsText) + spacingW;
|
|
84
|
-
for (const ch of barLabel) {
|
|
85
|
-
if (barStartX >= width) break;
|
|
86
|
-
const cw = getDisplayWidth(ch);
|
|
87
|
-
line[barStartX] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
88
|
-
if (cw === 2 && barStartX + 1 < width) line[barStartX + 1] = { ...line[barStartX], char: '' };
|
|
89
|
-
barStartX += cw;
|
|
90
|
-
}
|
|
91
|
-
for (let i = 0; i < filled && barStartX + i < width; i++) {
|
|
92
|
-
line[barStartX + i] = { char: '#', fg: barFg, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
93
|
-
}
|
|
94
|
-
for (let i = 0; i < empty && barStartX + filled + i < width; i++) {
|
|
95
|
-
line[barStartX + filled + i] = { char: '-', fg: '238', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
lines.push(line);
|
|
99
|
-
return lines;
|
|
100
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// buildStatusToken — always glyph + color, never color-only.
|
|
3
|
-
//
|
|
4
|
-
// Maps a semantic state to a Unicode glyph AND a palette color so that
|
|
5
|
-
// colorblind users and screen readers can distinguish states without color.
|
|
6
|
-
//
|
|
7
|
-
// Glyphs:
|
|
8
|
-
// good ✓ (CHECK MARK)
|
|
9
|
-
// warn ⚠ (WARNING SIGN)
|
|
10
|
-
// bad ✕ (MULTIPLICATION X)
|
|
11
|
-
// info ○ (WHITE CIRCLE)
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
import type { Cell } from '../types/grid.ts';
|
|
15
|
-
import { DEFAULT_PANEL_PALETTE } from '../panels/polish.ts';
|
|
16
|
-
import { type StatusState, STATE_GLYPHS } from './status-glyphs.ts';
|
|
17
|
-
|
|
18
|
-
// Re-export for downstream consumers that import from this module.
|
|
19
|
-
export type { StatusState } from './status-glyphs.ts';
|
|
20
|
-
export { STATE_GLYPHS } from './status-glyphs.ts';
|
|
21
|
-
|
|
22
|
-
const STATE_COLORS: Record<StatusState, string> = {
|
|
23
|
-
good: DEFAULT_PANEL_PALETTE.good,
|
|
24
|
-
warn: DEFAULT_PANEL_PALETTE.warn,
|
|
25
|
-
bad: DEFAULT_PANEL_PALETTE.bad,
|
|
26
|
-
info: DEFAULT_PANEL_PALETTE.info,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface StatusTokenOpts {
|
|
30
|
-
/** Append a numeric count after the label, e.g. "label (3)". */
|
|
31
|
-
count?: number;
|
|
32
|
-
/** Override the default glyph for this state. */
|
|
33
|
-
glyph?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build a small sequence of styled cells: [glyph, space, label, optional count]
|
|
38
|
-
*
|
|
39
|
-
* Always prepends the glyph so colorblind users can parse the state.
|
|
40
|
-
* The color is applied to both glyph and label as a redundant cue.
|
|
41
|
-
*
|
|
42
|
-
* @param state Semantic state — controls default glyph and color.
|
|
43
|
-
* @param label Human-readable label string.
|
|
44
|
-
* @param opts Optional count suffix or glyph override.
|
|
45
|
-
* @returns Array of Cell objects ready to embed in a Line.
|
|
46
|
-
*/
|
|
47
|
-
export function buildStatusToken(
|
|
48
|
-
state: StatusState,
|
|
49
|
-
label: string,
|
|
50
|
-
opts?: StatusTokenOpts,
|
|
51
|
-
): Cell[] {
|
|
52
|
-
const glyph = opts?.glyph ?? STATE_GLYPHS[state];
|
|
53
|
-
const color = STATE_COLORS[state];
|
|
54
|
-
const suffix = opts?.count !== undefined ? ` (${opts.count})` : '';
|
|
55
|
-
const text = `${glyph} ${label}${suffix}`;
|
|
56
|
-
|
|
57
|
-
return text.split('').map((char): Cell => ({
|
|
58
|
-
char,
|
|
59
|
-
fg: color,
|
|
60
|
-
bg: '',
|
|
61
|
-
bold: false,
|
|
62
|
-
dim: false,
|
|
63
|
-
underline: false,
|
|
64
|
-
italic: false,
|
|
65
|
-
strikethrough: false,
|
|
66
|
-
}));
|
|
67
|
-
}
|