@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -18,21 +18,14 @@ import {
|
|
|
18
18
|
DEFAULT_PANEL_PALETTE,
|
|
19
19
|
extendPalette,
|
|
20
20
|
} from './polish.ts';
|
|
21
|
-
import {
|
|
22
|
-
getPanelSearchFocusTransition,
|
|
23
|
-
isPanelSearchBackspace,
|
|
24
|
-
isPanelSearchCancel,
|
|
25
|
-
isPanelSearchCommit,
|
|
26
|
-
isPanelSearchPrintable,
|
|
27
|
-
} from './search-focus.ts';
|
|
21
|
+
import { isPanelSearchBackspace, isPanelSearchPrintable } from './search-focus.ts';
|
|
28
22
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
|
+
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
29
24
|
|
|
30
25
|
// ---------------------------------------------------------------------------
|
|
31
26
|
// Constants
|
|
32
27
|
// ---------------------------------------------------------------------------
|
|
33
28
|
|
|
34
|
-
const MAX_DEPTH = 5;
|
|
35
|
-
|
|
36
29
|
/** Directories / files to skip (gitignore-style). */
|
|
37
30
|
const SKIP_NAMES = new Set([
|
|
38
31
|
'node_modules', '.git', '.svn', '.hg',
|
|
@@ -121,6 +114,12 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
121
114
|
private cacheValid: boolean = false;
|
|
122
115
|
private readyPromise: Promise<void> | null = null;
|
|
123
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
|
+
|
|
124
123
|
// --- navigation ---
|
|
125
124
|
private cursor: number = 0;
|
|
126
125
|
private scrollTop: number = 0;
|
|
@@ -133,6 +132,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
133
132
|
super('explorer', 'Explorer', 'E', 'development');
|
|
134
133
|
this.workingDirectory = workingDirectory;
|
|
135
134
|
this.rootPath = rootPath ?? workingDirectory;
|
|
135
|
+
this.git = new GitService(this.workingDirectory);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -170,14 +170,54 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
170
170
|
|
|
171
171
|
// ── Input ──────────────────────────────────────────────────────────────────
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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 === '/') {
|
|
178
212
|
this._enterSearch();
|
|
179
213
|
return true;
|
|
180
214
|
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleInput(key: string): boolean {
|
|
219
|
+
const filterResult = this._handleFilterKey(key);
|
|
220
|
+
if (filterResult !== null) return filterResult;
|
|
181
221
|
|
|
182
222
|
switch (key) {
|
|
183
223
|
case 'up': case 'k': this._moveCursor(-1); return true;
|
|
@@ -189,8 +229,8 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
189
229
|
case 'return': case 'enter': this._activateNode(); return true;
|
|
190
230
|
case 'right': this._expandNode(); return true;
|
|
191
231
|
case 'left': this._collapseNode(); return true;
|
|
192
|
-
case '/': this._enterSearch(); return true;
|
|
193
232
|
case 'r': this.refresh(); return true;
|
|
233
|
+
case 'd': return this.getFocusedFilePath() !== null;
|
|
194
234
|
default: return false;
|
|
195
235
|
}
|
|
196
236
|
}
|
|
@@ -207,11 +247,6 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
207
247
|
|
|
208
248
|
render(width: number, height: number): Line[] {
|
|
209
249
|
this.needsRender = false;
|
|
210
|
-
const searchLine = this.searchMode
|
|
211
|
-
? `/ ${this.searchQuery}_`
|
|
212
|
-
: this.searchQuery
|
|
213
|
-
? `Filter: ${this.searchQuery} (/ or up at top to edit)`
|
|
214
|
-
: `Root: ${relative(this.workingDirectory, this.rootPath) || '.'} (/ or up at top to search)`;
|
|
215
250
|
|
|
216
251
|
// Context-aware hints: only surface keys that act in the current mode.
|
|
217
252
|
const hintsLine = this.searchMode
|
|
@@ -225,8 +260,9 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
225
260
|
{ keys: '↑/↓', label: 'navigate' },
|
|
226
261
|
{ keys: 'Enter/→', label: 'expand' },
|
|
227
262
|
{ keys: '←', label: 'collapse' },
|
|
228
|
-
{ keys: '/', label: '
|
|
263
|
+
{ keys: '/', label: 'filter' },
|
|
229
264
|
{ keys: 'r', label: 'refresh' },
|
|
265
|
+
{ keys: 'd', label: 'diff' },
|
|
230
266
|
], C);
|
|
231
267
|
|
|
232
268
|
if (this.flat.length === 0) {
|
|
@@ -239,10 +275,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
239
275
|
},
|
|
240
276
|
],
|
|
241
277
|
footerLines: [
|
|
242
|
-
|
|
243
|
-
active: this.searchMode,
|
|
244
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
245
|
-
}),
|
|
278
|
+
this._buildFilterLine(width),
|
|
246
279
|
hintsLine,
|
|
247
280
|
],
|
|
248
281
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -253,7 +286,9 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
253
286
|
title: 'Summary',
|
|
254
287
|
lines: [
|
|
255
288
|
buildPanelLine(width, [
|
|
256
|
-
['
|
|
289
|
+
[' Root ', DEFAULT_PANEL_PALETTE.label],
|
|
290
|
+
[relative(this.workingDirectory, this.rootPath) || '.', DEFAULT_PANEL_PALETTE.value],
|
|
291
|
+
[' Visible ', DEFAULT_PANEL_PALETTE.label],
|
|
257
292
|
[String(this.flat.length), DEFAULT_PANEL_PALETTE.value],
|
|
258
293
|
[' Search ', DEFAULT_PANEL_PALETTE.label],
|
|
259
294
|
[this.searchQuery || 'none', this.searchQuery ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
@@ -281,10 +316,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
281
316
|
const treeSection = resolveScrollablePanelSection(width, height, {
|
|
282
317
|
intro: 'Browse the project tree, expand directories, and search for paths.',
|
|
283
318
|
footerLines: [
|
|
284
|
-
|
|
285
|
-
active: this.searchMode,
|
|
286
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
287
|
-
}),
|
|
319
|
+
this._buildFilterLine(width),
|
|
288
320
|
hintsLine,
|
|
289
321
|
],
|
|
290
322
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -300,9 +332,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
300
332
|
expandable: node.isDir,
|
|
301
333
|
expanded: node.expanded,
|
|
302
334
|
labelColor: node.isDir ? C.dirColor : C.fileColor,
|
|
303
|
-
metadata: (
|
|
304
|
-
? [{ text: formatSize(node.size), fg: C.sizeColor }]
|
|
305
|
-
: undefined,
|
|
335
|
+
metadata: this._buildRowMetadata(node),
|
|
306
336
|
}, C, { selected: isCursor, selectedBg: C.cursorBg });
|
|
307
337
|
}),
|
|
308
338
|
selectedIndex: this.cursor,
|
|
@@ -321,10 +351,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
321
351
|
selectedSection,
|
|
322
352
|
],
|
|
323
353
|
footerLines: [
|
|
324
|
-
|
|
325
|
-
active: this.searchMode,
|
|
326
|
-
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
327
|
-
}),
|
|
354
|
+
this._buildFilterLine(width),
|
|
328
355
|
hintsLine,
|
|
329
356
|
],
|
|
330
357
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -333,14 +360,35 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
333
360
|
|
|
334
361
|
// ── Private: tree building ─────────────────────────────────────────────────
|
|
335
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
|
+
*/
|
|
336
371
|
private _buildTreeAsync(): Promise<void> {
|
|
337
372
|
const p = (async () => {
|
|
338
373
|
try {
|
|
339
374
|
await this.withLoading('Scanning directory\u2026', async () => {
|
|
340
|
-
|
|
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;
|
|
341
387
|
this._rebuildFlat();
|
|
342
388
|
this.cacheValid = true;
|
|
343
389
|
});
|
|
390
|
+
// Best-effort \u2014 a missing/failed git status just means no decorations.
|
|
391
|
+
await this._loadGitStatus();
|
|
344
392
|
} catch (err) {
|
|
345
393
|
this.setError(summarizeError(err));
|
|
346
394
|
}
|
|
@@ -355,36 +403,27 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
355
403
|
return this.readyPromise ?? Promise.resolve();
|
|
356
404
|
}
|
|
357
405
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
size: 0,
|
|
366
|
-
expanded: depth === 0,
|
|
367
|
-
children: [],
|
|
368
|
-
loaded: false,
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
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;
|
|
372
413
|
|
|
373
414
|
let entries: string[];
|
|
374
415
|
try {
|
|
375
|
-
entries = await fsPromises.readdir(
|
|
416
|
+
entries = await fsPromises.readdir(node.path);
|
|
376
417
|
} catch {
|
|
377
|
-
|
|
418
|
+
node.loaded = true;
|
|
419
|
+
return;
|
|
378
420
|
}
|
|
379
421
|
|
|
380
|
-
node.loaded = true;
|
|
381
|
-
|
|
382
|
-
// Sort: dirs first, then files, alphabetically within each group
|
|
383
422
|
const filtered = entries.filter(e => !shouldSkip(e));
|
|
384
423
|
const statResults = await Promise.all(
|
|
385
424
|
filtered.map(async (e) => {
|
|
386
425
|
try {
|
|
387
|
-
const s = await fsPromises.stat(join(
|
|
426
|
+
const s = await fsPromises.stat(join(node.path, e));
|
|
388
427
|
return { name: e, isDir: s.isDirectory(), size: s.size, stat: s };
|
|
389
428
|
} catch {
|
|
390
429
|
return { name: e, isDir: false, size: 0, stat: null };
|
|
@@ -397,26 +436,81 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
397
436
|
return a.name.localeCompare(b.name);
|
|
398
437
|
});
|
|
399
438
|
|
|
439
|
+
const children: TreeNode[] = [];
|
|
400
440
|
for (const entry of sorted) {
|
|
401
441
|
if (entry.stat === null) continue;
|
|
402
|
-
const fullPath = join(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 });
|
|
416
492
|
}
|
|
493
|
+
this.gitStatus = next;
|
|
494
|
+
} catch {
|
|
495
|
+
// Not a git repo (or git unavailable) \u2014 decorations are optional.
|
|
496
|
+
this.gitStatus = new Map();
|
|
417
497
|
}
|
|
498
|
+
}
|
|
418
499
|
|
|
419
|
-
|
|
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;
|
|
420
514
|
}
|
|
421
515
|
|
|
422
516
|
/**
|
|
@@ -473,15 +567,6 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
473
567
|
this.markDirty();
|
|
474
568
|
}
|
|
475
569
|
|
|
476
|
-
private _clampScroll(viewHeight: number): void {
|
|
477
|
-
if (this.cursor < this.scrollTop) {
|
|
478
|
-
this.scrollTop = this.cursor;
|
|
479
|
-
} else if (this.cursor >= this.scrollTop + viewHeight) {
|
|
480
|
-
this.scrollTop = this.cursor - viewHeight + 1;
|
|
481
|
-
}
|
|
482
|
-
this.scrollTop = Math.max(0, this.scrollTop);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
570
|
private _activateNode(): void {
|
|
486
571
|
const node = this.flat[this.cursor];
|
|
487
572
|
if (!node) return;
|
|
@@ -489,6 +574,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
489
574
|
node.expanded = !node.expanded;
|
|
490
575
|
this._rebuildFlat();
|
|
491
576
|
this.markDirty();
|
|
577
|
+
if (node.expanded) this._loadNodeChildrenAndRefresh(node);
|
|
492
578
|
}
|
|
493
579
|
// For files: callers can read getFocusedNode() after the input returns true
|
|
494
580
|
}
|
|
@@ -499,6 +585,20 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
499
585
|
node.expanded = true;
|
|
500
586
|
this._rebuildFlat();
|
|
501
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
|
+
});
|
|
502
602
|
}
|
|
503
603
|
|
|
504
604
|
private _collapseNode(): void {
|
|
@@ -535,50 +635,30 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
535
635
|
// ── Private: search ───────────────────────────────────────────────────────
|
|
536
636
|
|
|
537
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.
|
|
538
642
|
this.searchMode = true;
|
|
539
|
-
this.searchQuery = '';
|
|
540
643
|
this._rebuildFlat();
|
|
541
644
|
this.markDirty();
|
|
542
645
|
}
|
|
543
646
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
if (isPanelSearchCommit(key)) {
|
|
561
|
-
// Confirm search, stay in results, exit search-input mode
|
|
562
|
-
this.searchMode = false;
|
|
563
|
-
this.markDirty();
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
if (isPanelSearchBackspace(key)) {
|
|
567
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
568
|
-
this._rebuildFlat();
|
|
569
|
-
this.markDirty();
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
// Printable single characters
|
|
573
|
-
if (isPanelSearchPrintable(key)) {
|
|
574
|
-
this.searchQuery += key;
|
|
575
|
-
this._rebuildFlat();
|
|
576
|
-
this.markDirty();
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
// Navigation still works during search
|
|
580
|
-
if (key === 'up' || key === 'k') { this._moveCursor(-1); return true; }
|
|
581
|
-
if (key === 'down' || key === 'j') { this._moveCursor(1); return true; }
|
|
582
|
-
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
|
+
});
|
|
583
663
|
}
|
|
584
664
|
}
|