@pellux/goodvibes-tui 0.27.0 → 0.29.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -9,29 +9,23 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
9
9
|
import { BasePanel } from './base-panel.ts';
|
|
10
10
|
import {
|
|
11
11
|
buildEmptyState,
|
|
12
|
+
buildKeyboardHints,
|
|
12
13
|
buildPanelLine,
|
|
13
14
|
buildSearchInputLine,
|
|
14
|
-
|
|
15
|
+
buildTreeRow,
|
|
15
16
|
buildPanelWorkspace,
|
|
16
17
|
resolveScrollablePanelSection,
|
|
17
18
|
DEFAULT_PANEL_PALETTE,
|
|
19
|
+
extendPalette,
|
|
18
20
|
} from './polish.ts';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
getPanelSearchFocusTransition,
|
|
22
|
-
isPanelSearchBackspace,
|
|
23
|
-
isPanelSearchCancel,
|
|
24
|
-
isPanelSearchCommit,
|
|
25
|
-
isPanelSearchPrintable,
|
|
26
|
-
} from './search-focus.ts';
|
|
21
|
+
import { isPanelSearchBackspace, isPanelSearchPrintable } from './search-focus.ts';
|
|
27
22
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
|
+
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
28
24
|
|
|
29
25
|
// ---------------------------------------------------------------------------
|
|
30
26
|
// Constants
|
|
31
27
|
// ---------------------------------------------------------------------------
|
|
32
28
|
|
|
33
|
-
const MAX_DEPTH = 5;
|
|
34
|
-
|
|
35
29
|
/** Directories / files to skip (gitignore-style). */
|
|
36
30
|
const SKIP_NAMES = new Set([
|
|
37
31
|
'node_modules', '.git', '.svn', '.hg',
|
|
@@ -84,15 +78,12 @@ interface TreeNode {
|
|
|
84
78
|
// ---------------------------------------------------------------------------
|
|
85
79
|
// Colour palette
|
|
86
80
|
// ---------------------------------------------------------------------------
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const CLR_SEARCH_FG = '#ff79c6';
|
|
94
|
-
const CLR_TOGGLE = '244'; // ▶/▼ toggle arrows
|
|
95
|
-
const CLR_ICON = '244'; // file type icon
|
|
81
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
82
|
+
dirColor: '#00ffff', // cyan — directories
|
|
83
|
+
fileColor: '#e0e0e0', // near-white — files
|
|
84
|
+
sizeColor: '244', // dim grey — sizes
|
|
85
|
+
cursorBg: '#1a2a3a', // cursor background
|
|
86
|
+
});
|
|
96
87
|
|
|
97
88
|
// ---------------------------------------------------------------------------
|
|
98
89
|
// Helpers
|
|
@@ -123,6 +114,12 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
123
114
|
private cacheValid: boolean = false;
|
|
124
115
|
private readyPromise: Promise<void> | null = null;
|
|
125
116
|
|
|
117
|
+
// --- git status decorations ---
|
|
118
|
+
// Reuse one GitService instance across refreshes (I2 pattern, git-panel.ts).
|
|
119
|
+
private readonly git: GitService;
|
|
120
|
+
/** absolute file path -> single-char status code (git-panel.ts:146-151 classification). */
|
|
121
|
+
private gitStatus: Map<string, { code: string; tone: 'good' | 'warn' | 'bad' | 'dim' }> = new Map();
|
|
122
|
+
|
|
126
123
|
// --- navigation ---
|
|
127
124
|
private cursor: number = 0;
|
|
128
125
|
private scrollTop: number = 0;
|
|
@@ -135,6 +132,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
135
132
|
super('explorer', 'Explorer', 'E', 'development');
|
|
136
133
|
this.workingDirectory = workingDirectory;
|
|
137
134
|
this.rootPath = rootPath ?? workingDirectory;
|
|
135
|
+
this.git = new GitService(this.workingDirectory);
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -172,14 +170,54 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
172
170
|
|
|
173
171
|
// ── Input ──────────────────────────────────────────────────────────────────
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
/**
|
|
174
|
+
* WO-153: converged modal '/' filter (mirrors ScrollableListPanel's
|
|
175
|
+
* _handleFilterKey). Returns `true`/`false` when consumed/ignored in
|
|
176
|
+
* filter context, or `null` to fall through to navigation/action keys.
|
|
177
|
+
*/
|
|
178
|
+
private _handleFilterKey(key: string): boolean | null {
|
|
179
|
+
if (this.searchMode) {
|
|
180
|
+
if (key === 'escape') {
|
|
181
|
+
this.searchMode = false;
|
|
182
|
+
this.searchQuery = '';
|
|
183
|
+
this._rebuildFlat();
|
|
184
|
+
this.markDirty();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (key === 'return' || key === 'enter') {
|
|
188
|
+
this.searchMode = false; // commit; keep the query applied
|
|
189
|
+
this.markDirty();
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (isPanelSearchBackspace(key)) {
|
|
193
|
+
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
194
|
+
this._rebuildFlat();
|
|
195
|
+
this.markDirty();
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
// Arrow/paging keys navigate the filtered tree — fall through.
|
|
199
|
+
if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown' || key === 'home' || key === 'end') {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// Any printable character (including single-letter action keys like r/d) extends the query.
|
|
203
|
+
if (isPanelSearchPrintable(key)) {
|
|
204
|
+
this.searchQuery += key;
|
|
205
|
+
this._rebuildFlat();
|
|
206
|
+
this.markDirty();
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (key === '/') {
|
|
180
212
|
this._enterSearch();
|
|
181
213
|
return true;
|
|
182
214
|
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleInput(key: string): boolean {
|
|
219
|
+
const filterResult = this._handleFilterKey(key);
|
|
220
|
+
if (filterResult !== null) return filterResult;
|
|
183
221
|
|
|
184
222
|
switch (key) {
|
|
185
223
|
case 'up': case 'k': this._moveCursor(-1); return true;
|
|
@@ -191,8 +229,8 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
191
229
|
case 'return': case 'enter': this._activateNode(); return true;
|
|
192
230
|
case 'right': this._expandNode(); return true;
|
|
193
231
|
case 'left': this._collapseNode(); return true;
|
|
194
|
-
case '/': this._enterSearch(); return true;
|
|
195
232
|
case 'r': this.refresh(); return true;
|
|
233
|
+
case 'd': return this.getFocusedFilePath() !== null;
|
|
196
234
|
default: return false;
|
|
197
235
|
}
|
|
198
236
|
}
|
|
@@ -209,11 +247,23 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
209
247
|
|
|
210
248
|
render(width: number, height: number): Line[] {
|
|
211
249
|
this.needsRender = false;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
|
|
251
|
+
// Context-aware hints: only surface keys that act in the current mode.
|
|
252
|
+
const hintsLine = this.searchMode
|
|
253
|
+
? buildKeyboardHints(width, [
|
|
254
|
+
{ keys: 'type', label: 'filter' },
|
|
255
|
+
{ keys: '↑/↓', label: 'move' },
|
|
256
|
+
{ keys: 'Enter', label: 'keep results' },
|
|
257
|
+
{ keys: 'Esc', label: 'clear' },
|
|
258
|
+
], C)
|
|
259
|
+
: buildKeyboardHints(width, [
|
|
260
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
261
|
+
{ keys: 'Enter/→', label: 'expand' },
|
|
262
|
+
{ keys: '←', label: 'collapse' },
|
|
263
|
+
{ keys: '/', label: 'filter' },
|
|
264
|
+
{ keys: 'r', label: 'refresh' },
|
|
265
|
+
{ keys: 'd', label: 'diff' },
|
|
266
|
+
], C);
|
|
217
267
|
|
|
218
268
|
if (this.flat.length === 0) {
|
|
219
269
|
return buildPanelWorkspace(width, height, {
|
|
@@ -221,23 +271,12 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
221
271
|
intro: 'Browse the project tree, expand directories, and search for paths.',
|
|
222
272
|
sections: [
|
|
223
273
|
{
|
|
224
|
-
lines: buildEmptyState(
|
|
225
|
-
width,
|
|
226
|
-
' No files found',
|
|
227
|
-
this.searchQuery
|
|
228
|
-
? 'No files or directories match the current search.'
|
|
229
|
-
: 'This root did not produce any visible files after the explorer filters were applied.',
|
|
230
|
-
[],
|
|
231
|
-
DEFAULT_PANEL_PALETTE,
|
|
232
|
-
),
|
|
274
|
+
lines: buildEmptyState(width, ' No files found', this.searchQuery ? 'No files or directories match the current search.' : 'This root did not produce any visible files after the explorer filters were applied.', this.searchQuery ? [{ command: 'Esc', summary: 'clear the filter to show the full tree again' }] : [{ command: 'r', summary: 'rescan this root from disk' }], C),
|
|
233
275
|
},
|
|
234
276
|
],
|
|
235
277
|
footerLines: [
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
239
|
-
}),
|
|
240
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter/Right', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim], [' Left', DEFAULT_PANEL_PALETTE.info], [' collapse', DEFAULT_PANEL_PALETTE.dim], [' /', DEFAULT_PANEL_PALETTE.info], [' search', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
278
|
+
this._buildFilterLine(width),
|
|
279
|
+
hintsLine,
|
|
241
280
|
],
|
|
242
281
|
palette: DEFAULT_PANEL_PALETTE,
|
|
243
282
|
});
|
|
@@ -247,7 +286,9 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
247
286
|
title: 'Summary',
|
|
248
287
|
lines: [
|
|
249
288
|
buildPanelLine(width, [
|
|
250
|
-
['
|
|
289
|
+
[' Root ', DEFAULT_PANEL_PALETTE.label],
|
|
290
|
+
[relative(this.workingDirectory, this.rootPath) || '.', DEFAULT_PANEL_PALETTE.value],
|
|
291
|
+
[' Visible ', DEFAULT_PANEL_PALETTE.label],
|
|
251
292
|
[String(this.flat.length), DEFAULT_PANEL_PALETTE.value],
|
|
252
293
|
[' Search ', DEFAULT_PANEL_PALETTE.label],
|
|
253
294
|
[this.searchQuery || 'none', this.searchQuery ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
@@ -275,11 +316,8 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
275
316
|
const treeSection = resolveScrollablePanelSection(width, height, {
|
|
276
317
|
intro: 'Browse the project tree, expand directories, and search for paths.',
|
|
277
318
|
footerLines: [
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
281
|
-
}),
|
|
282
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter/Right', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim], [' Left', DEFAULT_PANEL_PALETTE.info], [' collapse', DEFAULT_PANEL_PALETTE.dim], [' /', DEFAULT_PANEL_PALETTE.info], [' search', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
319
|
+
this._buildFilterLine(width),
|
|
320
|
+
hintsLine,
|
|
283
321
|
],
|
|
284
322
|
palette: DEFAULT_PANEL_PALETTE,
|
|
285
323
|
beforeSections: [summarySection],
|
|
@@ -287,24 +325,15 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
287
325
|
title: 'Tree',
|
|
288
326
|
scrollableLines: this.flat.map((node, absoluteIdx) => {
|
|
289
327
|
const isCursor = absoluteIdx === this.cursor;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
node.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
];
|
|
300
|
-
if (!node.isDir && node.size > 0) {
|
|
301
|
-
const sizeStr = ` ${formatSize(node.size)}`;
|
|
302
|
-
const contentWidth = getDisplayWidth(indent) + 2 + getDisplayWidth(node.name);
|
|
303
|
-
const gap = Math.max(1, width - contentWidth - getDisplayWidth(sizeStr));
|
|
304
|
-
segments.push({ text: ' '.repeat(gap), fg: baseFg });
|
|
305
|
-
segments.push({ text: sizeStr, fg: CLR_SIZE, dim: true });
|
|
306
|
-
}
|
|
307
|
-
return buildSelectablePanelLine(width, segments, { selected: isCursor, selectedBg: baseBg, fillFg: baseFg });
|
|
328
|
+
return buildTreeRow(width, {
|
|
329
|
+
depth: node.depth,
|
|
330
|
+
label: node.name,
|
|
331
|
+
icon: node.isDir ? undefined : fileIcon(node.name),
|
|
332
|
+
expandable: node.isDir,
|
|
333
|
+
expanded: node.expanded,
|
|
334
|
+
labelColor: node.isDir ? C.dirColor : C.fileColor,
|
|
335
|
+
metadata: this._buildRowMetadata(node),
|
|
336
|
+
}, C, { selected: isCursor, selectedBg: C.cursorBg });
|
|
308
337
|
}),
|
|
309
338
|
selectedIndex: this.cursor,
|
|
310
339
|
scrollOffset: this.scrollTop,
|
|
@@ -322,11 +351,8 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
322
351
|
selectedSection,
|
|
323
352
|
],
|
|
324
353
|
footerLines: [
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
328
|
-
}),
|
|
329
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter/Right', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim], [' Left', DEFAULT_PANEL_PALETTE.info], [' collapse', DEFAULT_PANEL_PALETTE.dim], [' /', DEFAULT_PANEL_PALETTE.info], [' search', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
354
|
+
this._buildFilterLine(width),
|
|
355
|
+
hintsLine,
|
|
330
356
|
],
|
|
331
357
|
palette: DEFAULT_PANEL_PALETTE,
|
|
332
358
|
});
|
|
@@ -334,14 +360,35 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
334
360
|
|
|
335
361
|
// ── Private: tree building ─────────────────────────────────────────────────
|
|
336
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Build (or rebuild) the tree root and load ONLY its immediate children.
|
|
365
|
+
* Deeper directories stay unloaded (`loaded: false`, empty `children`)
|
|
366
|
+
* until the user actually expands them via _expandNode()/_activateNode() \u2014
|
|
367
|
+
* this replaces the old eager depth-5 recursive scan, which walked (and
|
|
368
|
+
* silently truncated at a hardcoded depth cutoff) the entire visible tree
|
|
369
|
+
* up front and blocked opening the panel on large repos.
|
|
370
|
+
*/
|
|
337
371
|
private _buildTreeAsync(): Promise<void> {
|
|
338
372
|
const p = (async () => {
|
|
339
373
|
try {
|
|
340
374
|
await this.withLoading('Scanning directory\u2026', async () => {
|
|
341
|
-
|
|
375
|
+
const root: TreeNode = {
|
|
376
|
+
path: this.rootPath,
|
|
377
|
+
name: basename(this.rootPath) || this.rootPath,
|
|
378
|
+
isDir: true,
|
|
379
|
+
depth: 0,
|
|
380
|
+
size: 0,
|
|
381
|
+
expanded: true,
|
|
382
|
+
children: [],
|
|
383
|
+
loaded: false,
|
|
384
|
+
};
|
|
385
|
+
await this._loadChildren(root);
|
|
386
|
+
this.root = root;
|
|
342
387
|
this._rebuildFlat();
|
|
343
388
|
this.cacheValid = true;
|
|
344
389
|
});
|
|
390
|
+
// Best-effort \u2014 a missing/failed git status just means no decorations.
|
|
391
|
+
await this._loadGitStatus();
|
|
345
392
|
} catch (err) {
|
|
346
393
|
this.setError(summarizeError(err));
|
|
347
394
|
}
|
|
@@ -356,36 +403,27 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
356
403
|
return this.readyPromise ?? Promise.resolve();
|
|
357
404
|
}
|
|
358
405
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
size: 0,
|
|
367
|
-
expanded: depth === 0,
|
|
368
|
-
children: [],
|
|
369
|
-
loaded: false,
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
if (depth >= MAX_DEPTH) return node;
|
|
406
|
+
/**
|
|
407
|
+
* Read one directory's immediate entries and populate `node.children` with
|
|
408
|
+
* fully-resolved file nodes and unloaded directory stubs. Idempotent and
|
|
409
|
+
* safe to call on an already-loaded node (no-ops).
|
|
410
|
+
*/
|
|
411
|
+
private async _loadChildren(node: TreeNode): Promise<void> {
|
|
412
|
+
if (node.loaded || !node.isDir) return;
|
|
373
413
|
|
|
374
414
|
let entries: string[];
|
|
375
415
|
try {
|
|
376
|
-
entries = await fsPromises.readdir(
|
|
416
|
+
entries = await fsPromises.readdir(node.path);
|
|
377
417
|
} catch {
|
|
378
|
-
|
|
418
|
+
node.loaded = true;
|
|
419
|
+
return;
|
|
379
420
|
}
|
|
380
421
|
|
|
381
|
-
node.loaded = true;
|
|
382
|
-
|
|
383
|
-
// Sort: dirs first, then files, alphabetically within each group
|
|
384
422
|
const filtered = entries.filter(e => !shouldSkip(e));
|
|
385
423
|
const statResults = await Promise.all(
|
|
386
424
|
filtered.map(async (e) => {
|
|
387
425
|
try {
|
|
388
|
-
const s = await fsPromises.stat(join(
|
|
426
|
+
const s = await fsPromises.stat(join(node.path, e));
|
|
389
427
|
return { name: e, isDir: s.isDirectory(), size: s.size, stat: s };
|
|
390
428
|
} catch {
|
|
391
429
|
return { name: e, isDir: false, size: 0, stat: null };
|
|
@@ -398,26 +436,81 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
398
436
|
return a.name.localeCompare(b.name);
|
|
399
437
|
});
|
|
400
438
|
|
|
439
|
+
const children: TreeNode[] = [];
|
|
401
440
|
for (const entry of sorted) {
|
|
402
441
|
if (entry.stat === null) continue;
|
|
403
|
-
const fullPath = join(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
442
|
+
const fullPath = join(node.path, entry.name);
|
|
443
|
+
children.push(entry.isDir
|
|
444
|
+
? {
|
|
445
|
+
path: fullPath,
|
|
446
|
+
name: entry.name,
|
|
447
|
+
isDir: true,
|
|
448
|
+
depth: node.depth + 1,
|
|
449
|
+
size: 0,
|
|
450
|
+
expanded: false,
|
|
451
|
+
children: [],
|
|
452
|
+
loaded: false,
|
|
453
|
+
}
|
|
454
|
+
: {
|
|
455
|
+
path: fullPath,
|
|
456
|
+
name: entry.name,
|
|
457
|
+
isDir: false,
|
|
458
|
+
depth: node.depth + 1,
|
|
459
|
+
size: entry.size,
|
|
460
|
+
expanded: false,
|
|
461
|
+
children: [],
|
|
462
|
+
loaded: true,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
node.children = children;
|
|
466
|
+
node.loaded = true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Fetch git status for the working directory and index it by absolute
|
|
471
|
+
* path for O(1) lookup from the tree renderer. Classification mirrors
|
|
472
|
+
* git-panel's staged/unstaged split from the raw porcelain index/
|
|
473
|
+
* working_dir columns (git-panel.ts:146-151), collapsed to a single
|
|
474
|
+
* decoration badge per file: working-tree changes take priority over
|
|
475
|
+
* staged-only changes, since that's the state most relevant while
|
|
476
|
+
* browsing the tree.
|
|
477
|
+
*/
|
|
478
|
+
private async _loadGitStatus(): Promise<void> {
|
|
479
|
+
try {
|
|
480
|
+
const status = await this.git.status();
|
|
481
|
+
const next = new Map<string, { code: string; tone: 'good' | 'warn' | 'bad' | 'dim' }>();
|
|
482
|
+
for (const f of status.files) {
|
|
483
|
+
const fullPath = join(this.workingDirectory, f.path);
|
|
484
|
+
if (f.index === '?' && f.working_dir === '?') {
|
|
485
|
+
next.set(fullPath, { code: '?', tone: 'dim' }); // untracked
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const code = f.working_dir !== ' ' ? f.working_dir : f.index;
|
|
489
|
+
if (!code || code === ' ') continue;
|
|
490
|
+
const tone = code === 'D' ? 'bad' : code === 'A' ? 'good' : 'warn';
|
|
491
|
+
next.set(fullPath, { code, tone });
|
|
417
492
|
}
|
|
493
|
+
this.gitStatus = next;
|
|
494
|
+
} catch {
|
|
495
|
+
// Not a git repo (or git unavailable) \u2014 decorations are optional.
|
|
496
|
+
this.gitStatus = new Map();
|
|
418
497
|
}
|
|
498
|
+
}
|
|
419
499
|
|
|
420
|
-
|
|
500
|
+
/** Right-aligned row metadata: git status badge (files only) + file size. */
|
|
501
|
+
private _buildRowMetadata(node: TreeNode): Array<{ text: string; fg: string }> | undefined {
|
|
502
|
+
if (node.isDir) return undefined;
|
|
503
|
+
const meta: Array<{ text: string; fg: string }> = [];
|
|
504
|
+
const status = this.gitStatus.get(node.path);
|
|
505
|
+
if (status) {
|
|
506
|
+
const fg = status.tone === 'good' ? DEFAULT_PANEL_PALETTE.good
|
|
507
|
+
: status.tone === 'bad' ? DEFAULT_PANEL_PALETTE.bad
|
|
508
|
+
: status.tone === 'warn' ? DEFAULT_PANEL_PALETTE.warn
|
|
509
|
+
: DEFAULT_PANEL_PALETTE.dim;
|
|
510
|
+
meta.push({ text: status.code, fg });
|
|
511
|
+
}
|
|
512
|
+
if (node.size > 0) meta.push({ text: formatSize(node.size), fg: C.sizeColor });
|
|
513
|
+
return meta.length > 0 ? meta : undefined;
|
|
421
514
|
}
|
|
422
515
|
|
|
423
516
|
/**
|
|
@@ -474,15 +567,6 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
474
567
|
this.markDirty();
|
|
475
568
|
}
|
|
476
569
|
|
|
477
|
-
private _clampScroll(viewHeight: number): void {
|
|
478
|
-
if (this.cursor < this.scrollTop) {
|
|
479
|
-
this.scrollTop = this.cursor;
|
|
480
|
-
} else if (this.cursor >= this.scrollTop + viewHeight) {
|
|
481
|
-
this.scrollTop = this.cursor - viewHeight + 1;
|
|
482
|
-
}
|
|
483
|
-
this.scrollTop = Math.max(0, this.scrollTop);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
570
|
private _activateNode(): void {
|
|
487
571
|
const node = this.flat[this.cursor];
|
|
488
572
|
if (!node) return;
|
|
@@ -490,6 +574,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
490
574
|
node.expanded = !node.expanded;
|
|
491
575
|
this._rebuildFlat();
|
|
492
576
|
this.markDirty();
|
|
577
|
+
if (node.expanded) this._loadNodeChildrenAndRefresh(node);
|
|
493
578
|
}
|
|
494
579
|
// For files: callers can read getFocusedNode() after the input returns true
|
|
495
580
|
}
|
|
@@ -500,6 +585,20 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
500
585
|
node.expanded = true;
|
|
501
586
|
this._rebuildFlat();
|
|
502
587
|
this.markDirty();
|
|
588
|
+
this._loadNodeChildrenAndRefresh(node);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Lazily populate a directory's children (if not already loaded) and
|
|
593
|
+
* refresh the flattened view once they arrive, without blocking the
|
|
594
|
+
* expand keystroke itself.
|
|
595
|
+
*/
|
|
596
|
+
private _loadNodeChildrenAndRefresh(node: TreeNode): void {
|
|
597
|
+
if (node.loaded) return;
|
|
598
|
+
void this._loadChildren(node).then(() => {
|
|
599
|
+
this._rebuildFlat();
|
|
600
|
+
this.markDirty();
|
|
601
|
+
});
|
|
503
602
|
}
|
|
504
603
|
|
|
505
604
|
private _collapseNode(): void {
|
|
@@ -536,50 +635,30 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
536
635
|
// ── Private: search ───────────────────────────────────────────────────────
|
|
537
636
|
|
|
538
637
|
private _enterSearch(): void {
|
|
638
|
+
// Re-entering search (e.g. pressing '/' again) must EDIT the existing
|
|
639
|
+
// query, not silently clear it — the bug this fixes unconditionally
|
|
640
|
+
// reset searchQuery to '' on every entry, discarding whatever filter
|
|
641
|
+
// was already active.
|
|
539
642
|
this.searchMode = true;
|
|
540
|
-
this.searchQuery = '';
|
|
541
643
|
this._rebuildFlat();
|
|
542
644
|
this.markDirty();
|
|
543
645
|
}
|
|
544
646
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
if (isPanelSearchCommit(key)) {
|
|
562
|
-
// Confirm search, stay in results, exit search-input mode
|
|
563
|
-
this.searchMode = false;
|
|
564
|
-
this.markDirty();
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
if (isPanelSearchBackspace(key)) {
|
|
568
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
569
|
-
this._rebuildFlat();
|
|
570
|
-
this.markDirty();
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
// Printable single characters
|
|
574
|
-
if (isPanelSearchPrintable(key)) {
|
|
575
|
-
this.searchQuery += key;
|
|
576
|
-
this._rebuildFlat();
|
|
577
|
-
this.markDirty();
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
// Navigation still works during search
|
|
581
|
-
if (key === 'up' || key === 'k') { this._moveCursor(-1); return true; }
|
|
582
|
-
if (key === 'down' || key === 'j') { this._moveCursor(1); return true; }
|
|
583
|
-
return false;
|
|
647
|
+
/**
|
|
648
|
+
* The filter input line — pinned rendering contract shared with
|
|
649
|
+
* ScrollableListPanel.buildFilterLine: 'Filter: ' unfocused / '[Filter] '
|
|
650
|
+
* focused, literal trailing '_' cursor while active (active:false is
|
|
651
|
+
* passed to buildSearchInputLine to suppress its block-glyph cursor
|
|
652
|
+
* substitution).
|
|
653
|
+
*/
|
|
654
|
+
private _buildFilterLine(width: number): Line {
|
|
655
|
+
const label = this.searchMode ? '[Filter] ' : 'Filter: ';
|
|
656
|
+
const value = this.searchMode ? `${this.searchQuery}_` : this.searchQuery;
|
|
657
|
+
return buildSearchInputLine(width, label, value, DEFAULT_PANEL_PALETTE, {
|
|
658
|
+
active: false,
|
|
659
|
+
bg: this.searchMode ? DEFAULT_PANEL_PALETTE.inputBg : DEFAULT_PANEL_PALETTE.sectionBg,
|
|
660
|
+
emptyLabel: '(/ to filter)',
|
|
661
|
+
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : (this.searchQuery ? DEFAULT_PANEL_PALETTE.value : DEFAULT_PANEL_PALETTE.dim),
|
|
662
|
+
});
|
|
584
663
|
}
|
|
585
664
|
}
|