@selesai/code 0.7.0 → 0.8.1
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 +16 -0
- package/README.md +4 -2
- package/dist/extensions/agent-browser.test.ts +195 -0
- package/dist/extensions/agent-browser.ts +158 -0
- package/dist/extensions/inline-skills.test.ts +14 -0
- package/dist/extensions/inline-skills.ts +14 -7
- package/dist/extensions/package.json +2 -1
- package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +27 -0
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +156 -0
- package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +16 -0
- package/dist/extensions/pi-powerline-footer/README.md +192 -44
- package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +19 -4
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +80 -25
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +81 -29
- package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -1
- package/dist/extensions/pi-powerline-footer/cd-command.ts +190 -0
- package/dist/extensions/pi-powerline-footer/colors.ts +2 -1
- package/dist/extensions/pi-powerline-footer/context-usage.ts +121 -6
- package/dist/extensions/pi-powerline-footer/currency-rates.ts +142 -0
- package/dist/extensions/pi-powerline-footer/editor-composition.ts +32 -0
- package/dist/extensions/pi-powerline-footer/git-status.ts +108 -6
- package/dist/extensions/pi-powerline-footer/guide.ts +1 -1
- package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
- package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
- package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
- package/dist/extensions/pi-powerline-footer/package.json +10 -6
- package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
- package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
- package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
- package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
- package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
- package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
- package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
- package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
- package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
- package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
- package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
- package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
- package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
- package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +11 -6
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
- package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
- package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
- package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
- package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
- package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
- package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
- package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
- package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
- package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
- package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
- package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
- package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
- package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
- package/dist/extensions/pi-powerline-footer/types.ts +74 -29
- package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
- package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
- package/dist/extensions/ponytail/index.js +1 -31
- package/dist/extensions/ponytail/test/extension.test.js +5 -22
- package/dist/extensions/rtk.test.ts +27 -12
- package/dist/extensions/rtk.ts +41 -35
- package/package.json +1 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { CustomEditor } from "@selesai/code";
|
|
2
|
+
import { CustomEditor, type KeybindingsManager } from "@selesai/code";
|
|
3
3
|
import { isKeyRelease, matchesKey, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import type { KeybindingsManager } from "@selesai/code/dist/core/keybindings.js";
|
|
5
4
|
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
6
5
|
import { matchesConfiguredShortcut } from "../shortcuts.ts";
|
|
7
6
|
import { getOneOffBashCommandContext } from "./completion.ts";
|
|
8
7
|
import type { GhostSuggestion } from "./types.ts";
|
|
9
8
|
|
|
10
9
|
interface EditorBoundaryShortcuts {
|
|
11
|
-
start: string;
|
|
12
|
-
end: string;
|
|
10
|
+
start: string | null;
|
|
11
|
+
end: string | null;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
interface BashModeEditorOptions {
|
|
@@ -89,6 +88,12 @@ function droppedPathTextFromInput(data: string): string | null {
|
|
|
89
88
|
return null;
|
|
90
89
|
}
|
|
91
90
|
|
|
91
|
+
function resetShellHistoryBrowse(state: object): void {
|
|
92
|
+
Reflect.set(state, "shellHistoryIndex", -1);
|
|
93
|
+
Reflect.set(state, "shellHistoryItems", []);
|
|
94
|
+
Reflect.set(state, "shellHistoryDraft", "");
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
export class BashModeEditor extends CustomEditor {
|
|
93
98
|
private readonly keybindingsRef: KeybindingsManager;
|
|
94
99
|
private readonly optionsRef: BashModeEditorOptions;
|
|
@@ -96,6 +101,7 @@ export class BashModeEditor extends CustomEditor {
|
|
|
96
101
|
private shellHistoryIndex = -1;
|
|
97
102
|
private shellHistoryItems: string[] = [];
|
|
98
103
|
private shellHistoryDraft = "";
|
|
104
|
+
private promptHistoryDraft: string | null = null;
|
|
99
105
|
private ghost: GhostSuggestion | null = null;
|
|
100
106
|
private ghostAbort: AbortController | null = null;
|
|
101
107
|
private ghostToken = 0;
|
|
@@ -106,6 +112,11 @@ export class BashModeEditor extends CustomEditor {
|
|
|
106
112
|
this.optionsRef = options;
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
116
|
+
super.setAutocompleteProvider(provider);
|
|
117
|
+
this.wrappedProviderInstalled = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
installAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
110
121
|
this.setAutocompleteProvider(provider);
|
|
111
122
|
this.wrappedProviderInstalled = true;
|
|
@@ -130,13 +141,12 @@ export class BashModeEditor extends CustomEditor {
|
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
dismissBashModeUi(): void {
|
|
133
|
-
this
|
|
134
|
-
this.shellHistoryItems = [];
|
|
135
|
-
this.shellHistoryDraft = "";
|
|
144
|
+
resetShellHistoryBrowse(this);
|
|
136
145
|
this.clearGhostSuggestion();
|
|
137
146
|
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
const cancelAutocomplete = Reflect.get(this, "cancelAutocomplete");
|
|
148
|
+
if (typeof cancelAutocomplete === "function") {
|
|
149
|
+
cancelAutocomplete.call(this);
|
|
140
150
|
}
|
|
141
151
|
this.tui.requestRender();
|
|
142
152
|
}
|
|
@@ -145,9 +155,7 @@ export class BashModeEditor extends CustomEditor {
|
|
|
145
155
|
const droppedPathText = droppedPathTextFromInput(data);
|
|
146
156
|
if (droppedPathText !== null) {
|
|
147
157
|
this.insertTextAtCursor(droppedPathText);
|
|
148
|
-
this
|
|
149
|
-
this.shellHistoryItems = [];
|
|
150
|
-
this.shellHistoryDraft = "";
|
|
158
|
+
resetShellHistoryBrowse(this);
|
|
151
159
|
if (this.isShellCompletionContext()) {
|
|
152
160
|
this.scheduleGhostUpdate();
|
|
153
161
|
} else {
|
|
@@ -167,10 +175,11 @@ export class BashModeEditor extends CustomEditor {
|
|
|
167
175
|
const oneOffBashCommand = !bashMode && this.isOneOffBashCommandContext();
|
|
168
176
|
|
|
169
177
|
if (isCommandUndoShortcut(data)) {
|
|
170
|
-
this
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
const undo = Reflect.get(this, "undo");
|
|
179
|
+
if (typeof undo === "function") {
|
|
180
|
+
undo.call(this);
|
|
181
|
+
}
|
|
182
|
+
resetShellHistoryBrowse(this);
|
|
174
183
|
if (this.isShellCompletionContext()) {
|
|
175
184
|
this.scheduleGhostUpdate();
|
|
176
185
|
} else {
|
|
@@ -223,6 +232,38 @@ export class BashModeEditor extends CustomEditor {
|
|
|
223
232
|
return;
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
if (!bashMode && matchesKey(data, "up") && this.isPromptHistoryRecallPosition()) {
|
|
236
|
+
const navigateHistory = Reflect.get(this, "navigateHistory");
|
|
237
|
+
if (typeof navigateHistory === "function") {
|
|
238
|
+
if (Reflect.get(this, "historyIndex") === -1) {
|
|
239
|
+
this.promptHistoryDraft = this.getText();
|
|
240
|
+
}
|
|
241
|
+
navigateHistory.call(this, -1);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!bashMode && matchesKey(data, "down") && Reflect.get(this, "historyIndex") > -1) {
|
|
247
|
+
const isOnLastVisualLine = Reflect.get(this, "isOnLastVisualLine");
|
|
248
|
+
if (typeof isOnLastVisualLine !== "function" || isOnLastVisualLine.call(this)) {
|
|
249
|
+
const navigateHistory = Reflect.get(this, "navigateHistory");
|
|
250
|
+
if (typeof navigateHistory === "function") {
|
|
251
|
+
navigateHistory.call(this, 1);
|
|
252
|
+
if (Reflect.get(this, "historyIndex") === -1 && this.promptHistoryDraft !== null) {
|
|
253
|
+
const draft = this.promptHistoryDraft;
|
|
254
|
+
this.promptHistoryDraft = null;
|
|
255
|
+
const setTextInternal = Reflect.get(this, "setTextInternal");
|
|
256
|
+
if (typeof setTextInternal === "function") {
|
|
257
|
+
setTextInternal.call(this, draft);
|
|
258
|
+
} else {
|
|
259
|
+
this.setText(draft);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
if (bashMode && this.keybindingsRef.matches(data, "tui.input.submit") && !this.keybindingsRef.matches(data, "tui.input.newLine")) {
|
|
227
268
|
if (this.optionsRef.isShellRunning()) {
|
|
228
269
|
this.optionsRef.onNotify("Shell command already running", "warning");
|
|
@@ -232,9 +273,7 @@ export class BashModeEditor extends CustomEditor {
|
|
|
232
273
|
const command = this.getExpandedText().trim();
|
|
233
274
|
if (!command) return;
|
|
234
275
|
this.clearGhostSuggestion();
|
|
235
|
-
this
|
|
236
|
-
this.shellHistoryItems = [];
|
|
237
|
-
this.shellHistoryDraft = "";
|
|
276
|
+
resetShellHistoryBrowse(this);
|
|
238
277
|
this.optionsRef.onEditorSubmit?.();
|
|
239
278
|
this.optionsRef.onSubmitCommand(command);
|
|
240
279
|
this.setText("");
|
|
@@ -246,9 +285,7 @@ export class BashModeEditor extends CustomEditor {
|
|
|
246
285
|
}
|
|
247
286
|
|
|
248
287
|
if (!this.isShellCompletionContext()) {
|
|
249
|
-
this
|
|
250
|
-
this.shellHistoryItems = [];
|
|
251
|
-
this.shellHistoryDraft = "";
|
|
288
|
+
resetShellHistoryBrowse(this);
|
|
252
289
|
this.clearGhostSuggestion();
|
|
253
290
|
return;
|
|
254
291
|
}
|
|
@@ -267,9 +304,7 @@ export class BashModeEditor extends CustomEditor {
|
|
|
267
304
|
|| this.keybindingsRef.matches(data, "tui.editor.cursorLeft")
|
|
268
305
|
|| this.keybindingsRef.matches(data, "tui.editor.cursorRight")
|
|
269
306
|
) {
|
|
270
|
-
this
|
|
271
|
-
this.shellHistoryItems = [];
|
|
272
|
-
this.shellHistoryDraft = "";
|
|
307
|
+
resetShellHistoryBrowse(this);
|
|
273
308
|
this.scheduleGhostUpdate();
|
|
274
309
|
}
|
|
275
310
|
}
|
|
@@ -345,6 +380,26 @@ export class BashModeEditor extends CustomEditor {
|
|
|
345
380
|
return true;
|
|
346
381
|
}
|
|
347
382
|
|
|
383
|
+
private isPromptHistoryRecallPosition(): boolean {
|
|
384
|
+
if (this.isShowingAutocomplete()) return false;
|
|
385
|
+
|
|
386
|
+
const history = Reflect.get(this, "history");
|
|
387
|
+
if (!Array.isArray(history) || history.length === 0) return false;
|
|
388
|
+
|
|
389
|
+
const lines = this.getLines();
|
|
390
|
+
const cursor = this.getCursor();
|
|
391
|
+
if (lines.length === 1) {
|
|
392
|
+
return cursor.line === 0 && cursor.col === (lines[0]?.length ?? 0);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const isOnFirstVisualLine = Reflect.get(this, "isOnFirstVisualLine");
|
|
396
|
+
if (typeof isOnFirstVisualLine === "function" && !isOnFirstVisualLine.call(this)) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return cursor.line === 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
348
403
|
private navigateShellHistory(direction: -1 | 1): void {
|
|
349
404
|
const prefix = this.shellHistoryDraft || this.getExpandedText();
|
|
350
405
|
if (this.shellHistoryIndex === -1) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { getAgentDir } from "@selesai/code";
|
|
3
|
+
import { getAgentPath, getHomeDir } from "../paths.ts";
|
|
5
4
|
|
|
6
5
|
interface PersistedHistoryEntry {
|
|
7
6
|
command: string;
|
|
@@ -9,12 +8,31 @@ interface PersistedHistoryEntry {
|
|
|
9
8
|
timestamp: number;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// mode and ctimeMs are part of the fingerprint so permission/ACL transitions
|
|
12
|
+
// (e.g. chmod 000) invalidate the cache even when content is unchanged.
|
|
13
|
+
interface FileFingerprint {
|
|
14
|
+
ctimeMs: number;
|
|
15
|
+
mtimeMs: number;
|
|
16
|
+
mode: number;
|
|
17
|
+
size: number;
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
interface CachedHistory<T> extends FileFingerprint {
|
|
21
|
+
entries: T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matchesFingerprint(cached: FileFingerprint, stat: FileFingerprint): boolean {
|
|
25
|
+
return cached.ctimeMs === stat.ctimeMs
|
|
26
|
+
&& cached.mtimeMs === stat.mtimeMs
|
|
27
|
+
&& cached.mode === stat.mode
|
|
28
|
+
&& cached.size === stat.size;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const projectHistoryCache = new Map<string, CachedHistory<PersistedHistoryEntry[]>>();
|
|
32
|
+
const globalHistoryCache = new Map<string, CachedHistory<string[]>>();
|
|
33
|
+
|
|
16
34
|
function getHistoryDir(): string {
|
|
17
|
-
return
|
|
35
|
+
return getAgentPath("powerline-footer", "bash-history");
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
function projectKey(cwd: string): string {
|
|
@@ -44,13 +62,30 @@ function normalizePersistedEntries(value: unknown): PersistedHistoryEntry[] {
|
|
|
44
62
|
|
|
45
63
|
export function readProjectHistory(cwd: string): PersistedHistoryEntry[] {
|
|
46
64
|
const filePath = projectHistoryPath(cwd);
|
|
47
|
-
if (!existsSync(filePath))
|
|
65
|
+
if (!existsSync(filePath)) {
|
|
66
|
+
projectHistoryCache.delete(filePath);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
48
69
|
|
|
49
70
|
try {
|
|
71
|
+
const stat = statSync(filePath);
|
|
72
|
+
const cached = projectHistoryCache.get(filePath);
|
|
73
|
+
if (cached && matchesFingerprint(cached, stat)) {
|
|
74
|
+
return cached.entries;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
51
78
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return [];
|
|
52
|
-
|
|
79
|
+
const entries = normalizePersistedEntries((parsed as { entries?: unknown }).entries)
|
|
53
80
|
.sort((a, b) => b.timestamp - a.timestamp);
|
|
81
|
+
projectHistoryCache.set(filePath, {
|
|
82
|
+
ctimeMs: stat.ctimeMs,
|
|
83
|
+
mtimeMs: stat.mtimeMs,
|
|
84
|
+
mode: stat.mode,
|
|
85
|
+
size: stat.size,
|
|
86
|
+
entries,
|
|
87
|
+
});
|
|
88
|
+
return entries;
|
|
54
89
|
} catch (error) {
|
|
55
90
|
// Project history is a best-effort cache. If it is unreadable or malformed,
|
|
56
91
|
// bash mode should keep working instead of failing command entry entirely.
|
|
@@ -67,12 +102,23 @@ export function appendProjectHistory(cwd: string, command: string, entryCwd: str
|
|
|
67
102
|
const next: PersistedHistoryEntry[] = [
|
|
68
103
|
{ command: normalizedCommand, cwd: entryCwd, timestamp: Date.now() },
|
|
69
104
|
...existing.filter((entry) => entry.command !== normalizedCommand),
|
|
70
|
-
]
|
|
105
|
+
]
|
|
106
|
+
.slice(0, 500)
|
|
107
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
71
108
|
|
|
72
109
|
const filePath = projectHistoryPath(cwd);
|
|
110
|
+
projectHistoryCache.delete(filePath);
|
|
73
111
|
try {
|
|
74
112
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
75
113
|
writeFileSync(filePath, JSON.stringify({ version: 1, entries: next }, null, 2) + "\n");
|
|
114
|
+
const stat = statSync(filePath);
|
|
115
|
+
projectHistoryCache.set(filePath, {
|
|
116
|
+
ctimeMs: stat.ctimeMs,
|
|
117
|
+
mtimeMs: stat.mtimeMs,
|
|
118
|
+
mode: stat.mode,
|
|
119
|
+
size: stat.size,
|
|
120
|
+
entries: next,
|
|
121
|
+
});
|
|
76
122
|
} catch (error) {
|
|
77
123
|
// History persistence should never block a successful shell command from completing.
|
|
78
124
|
console.debug(`[powerline-footer] Failed to persist bash project history to ${filePath}:`, error);
|
|
@@ -105,31 +151,37 @@ function parseFishHistory(raw: string): string[] {
|
|
|
105
151
|
export function readGlobalShellHistory(shellPath: string): string[] {
|
|
106
152
|
const shellName = shellPath.split("/").pop()?.toLowerCase() ?? "";
|
|
107
153
|
const home = getHomeDir();
|
|
154
|
+
const filePath = shellName.includes("fish")
|
|
155
|
+
? join(home, ".local", "share", "fish", "fish_history")
|
|
156
|
+
: process.env.HISTFILE || join(home, shellName.includes("zsh") ? ".zsh_history" : ".bash_history");
|
|
157
|
+
const cacheKey = `${shellName}\0${filePath}`;
|
|
108
158
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return readFileSync(filePath, "utf8")
|
|
114
|
-
.split("\n")
|
|
115
|
-
.map(parseZshHistoryLine)
|
|
116
|
-
.filter((entry): entry is string => Boolean(entry))
|
|
117
|
-
.reverse();
|
|
118
|
-
}
|
|
159
|
+
if (!existsSync(filePath)) {
|
|
160
|
+
globalHistoryCache.delete(cacheKey);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
119
163
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
let stat: FileFingerprint | undefined;
|
|
165
|
+
try {
|
|
166
|
+
stat = statSync(filePath);
|
|
167
|
+
const cached = globalHistoryCache.get(cacheKey);
|
|
168
|
+
if (cached && matchesFingerprint(cached, stat)) {
|
|
169
|
+
return cached.entries;
|
|
124
170
|
}
|
|
125
171
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
const raw = readFileSync(filePath, "utf8");
|
|
173
|
+
const entries = shellName.includes("zsh")
|
|
174
|
+
? raw.split("\n").map(parseZshHistoryLine).filter((entry): entry is string => Boolean(entry)).reverse()
|
|
175
|
+
: shellName.includes("fish")
|
|
176
|
+
? parseFishHistory(raw).reverse()
|
|
177
|
+
: parseBashHistory(raw.split("\n")).reverse();
|
|
178
|
+
globalHistoryCache.set(cacheKey, { ...stat, entries });
|
|
179
|
+
return entries;
|
|
129
180
|
} catch (error) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
181
|
+
if (stat) {
|
|
182
|
+
globalHistoryCache.set(cacheKey, { ...stat, entries: [] });
|
|
183
|
+
console.debug(`[powerline-footer] Failed to read global shell history for ${shellName}:`, error);
|
|
184
|
+
}
|
|
133
185
|
return [];
|
|
134
186
|
}
|
|
135
187
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { SessionManager, type ExtensionAPI, type ExtensionCommandContext } from "@selesai/code";
|
|
5
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
function stripMatchingQuotes(value: string): string {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (trimmed.length >= 2) {
|
|
10
|
+
const first = trimmed[0];
|
|
11
|
+
const last = trimmed[trimmed.length - 1];
|
|
12
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
13
|
+
return trimmed.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unescapePathArg(value: string): string {
|
|
20
|
+
return stripMatchingQuotes(value).replace(/\\(.)/g, "$1");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function expandHomePath(value: string, homeDir = homedir()): string {
|
|
24
|
+
if (value === "~") return homeDir;
|
|
25
|
+
if (value.startsWith("~/")) return join(homeDir, value.slice(2));
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeCompletionPath(value: string): string {
|
|
30
|
+
return value.replace(/([\\\s"'`$&|;<>()[\]{}?!*])/g, "\\$1");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function displayPath(value: string): string {
|
|
34
|
+
return value.replace(/\\/g, "/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveCdTarget(args: string, cwd: string, homeDir = homedir()): { path: string } | { error: string } | null {
|
|
38
|
+
const input = unescapePathArg(args);
|
|
39
|
+
if (!input) return null;
|
|
40
|
+
|
|
41
|
+
const expanded = expandHomePath(input, homeDir);
|
|
42
|
+
const target = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
43
|
+
|
|
44
|
+
if (!existsSync(target)) {
|
|
45
|
+
return { error: `Directory does not exist: ${target}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (!statSync(target).isDirectory()) {
|
|
50
|
+
return { error: `Not a directory: ${target}` };
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return { error: `Cannot access directory: ${message}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { path: target };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pathBase(token: string, cwd: string, homeDir: string): { dir: string; prefix: string; displayPrefix: string } {
|
|
61
|
+
const unescaped = unescapePathArg(token);
|
|
62
|
+
const expanded = expandHomePath(unescaped, homeDir);
|
|
63
|
+
const hasSlash = expanded.includes("/");
|
|
64
|
+
|
|
65
|
+
if (!hasSlash) {
|
|
66
|
+
return { dir: cwd, prefix: expanded, displayPrefix: "" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const baseDir = expanded.endsWith("/") ? expanded.slice(0, -1) : dirname(expanded);
|
|
70
|
+
const dir = isAbsolute(baseDir) ? baseDir : resolve(cwd, baseDir);
|
|
71
|
+
const prefix = expanded.endsWith("/") ? "" : basename(expanded);
|
|
72
|
+
const displayPrefix = unescaped.endsWith("/") ? unescaped : unescaped.slice(0, Math.max(0, unescaped.length - prefix.length));
|
|
73
|
+
return { dir, prefix, displayPrefix };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isDirectoryEntry(dir: string, name: string): boolean {
|
|
77
|
+
try {
|
|
78
|
+
return statSync(join(dir, name)).isDirectory();
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getCdArgumentCompletions(argumentPrefix: string, cwd: string, homeDir = homedir()): AutocompleteItem[] {
|
|
85
|
+
const prefix = argumentPrefix.trimStart();
|
|
86
|
+
const suggestions: AutocompleteItem[] = [];
|
|
87
|
+
|
|
88
|
+
for (const quickPick of ["../", "~/"]) {
|
|
89
|
+
if (!prefix || quickPick.startsWith(prefix)) {
|
|
90
|
+
suggestions.push({ value: quickPick, label: quickPick, description: quickPick === "../" ? "Parent directory" : "Home directory" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { dir, prefix: entryPrefix, displayPrefix } = pathBase(prefix, cwd, homeDir);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
98
|
+
.filter((entry) => entry.name.startsWith(entryPrefix) && (entry.isDirectory() || entry.isSymbolicLink() && isDirectoryEntry(dir, entry.name)))
|
|
99
|
+
.slice(0, 50)
|
|
100
|
+
.map((entry) => {
|
|
101
|
+
const path = displayPath(`${displayPrefix}${entry.name}/`);
|
|
102
|
+
return {
|
|
103
|
+
value: escapeCompletionPath(path),
|
|
104
|
+
label: path,
|
|
105
|
+
} satisfies AutocompleteItem;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
suggestions.push(...entries);
|
|
109
|
+
} catch {
|
|
110
|
+
return suggestions;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
return suggestions.filter((item) => {
|
|
115
|
+
if (seen.has(item.value)) return false;
|
|
116
|
+
seen.add(item.value);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function removeSessionFile(filePath: string | undefined): void {
|
|
122
|
+
if (!filePath) return;
|
|
123
|
+
try {
|
|
124
|
+
unlinkSync(filePath);
|
|
125
|
+
} catch {
|
|
126
|
+
// Best-effort cleanup only; the command still reports the cancellation/error.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function runCdCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
131
|
+
const resolved = resolveCdTarget(args, ctx.cwd);
|
|
132
|
+
if (resolved === null) {
|
|
133
|
+
ctx.ui.notify(ctx.cwd, "info");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if ("error" in resolved) {
|
|
137
|
+
ctx.ui.notify(resolved.error, "error");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (resolved.path === ctx.cwd) {
|
|
141
|
+
ctx.ui.notify(`Already in ${ctx.cwd}`, "info");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
146
|
+
if (!sessionFile) {
|
|
147
|
+
ctx.ui.notify("/cd requires a persisted Pi session", "error");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await ctx.waitForIdle();
|
|
152
|
+
const nextSession = SessionManager.forkFrom(sessionFile, resolved.path);
|
|
153
|
+
const nextSessionFile = nextSession.getSessionFile();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await ctx.switchSession(nextSessionFile!, {
|
|
157
|
+
withSession: async (nextCtx) => {
|
|
158
|
+
try {
|
|
159
|
+
process.chdir(nextCtx.cwd);
|
|
160
|
+
nextCtx.ui.notify(`Changed directory to ${nextCtx.cwd}`, "info");
|
|
161
|
+
nextCtx.ui.setTitle(`pi - ${basename(nextCtx.cwd) || nextCtx.cwd}`);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.debug("[powerline-footer] Failed to update process cwd after /cd:", error);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (result.cancelled) {
|
|
168
|
+
removeSessionFile(nextSessionFile);
|
|
169
|
+
ctx.ui.notify("Directory change cancelled", "warning");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
try {
|
|
175
|
+
ctx.ui.notify(`Failed to change directory: ${message}`, "error");
|
|
176
|
+
} catch {
|
|
177
|
+
console.debug("[powerline-footer] Failed to report /cd error:", error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function registerCdCommand(pi: ExtensionAPI, getCwd: () => string): void {
|
|
183
|
+
pi.registerCommand("cd", {
|
|
184
|
+
description: "Switch the Pi session working directory",
|
|
185
|
+
getArgumentCompletions(argumentPrefix) {
|
|
186
|
+
return getCdArgumentCompletions(argumentPrefix, getCwd());
|
|
187
|
+
},
|
|
188
|
+
handler: runCdCommand,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -32,10 +32,11 @@ const THEME = {
|
|
|
32
32
|
path: "#00afaf", // Teal/cyan
|
|
33
33
|
gitClean: "#5faf5f", // Green
|
|
34
34
|
accent: "#febc38", // Orange
|
|
35
|
+
queue: "#febc38", // Orange
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
// Color name to ANSI code mapping
|
|
38
|
-
type ColorName =
|
|
39
|
+
type ColorName = keyof typeof THEME;
|
|
39
40
|
|
|
40
41
|
function getAnsiCode(color: ColorName): string {
|
|
41
42
|
const value = THEME[color as keyof typeof THEME];
|