@groeponline/pi-wishcraft 0.17.3

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.
Files changed (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,416 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import type { AutocompleteItem, AutocompleteProvider, AutocompleteSuggestions } from "@earendil-works/pi-tui";
5
+ import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory } from "./history.ts";
6
+ import type { ExtendedCompletionItem, GhostSuggestion } from "./types.ts";
7
+
8
+ interface TokenContext {
9
+ line: string;
10
+ cursorCol: number;
11
+ beforeCursor: string;
12
+ afterCursor: string;
13
+ token: string;
14
+ tokenStart: number;
15
+ tokenEnd: number;
16
+ tokenIndex: number;
17
+ previousTokens: string[];
18
+ }
19
+
20
+ export interface OneOffBashCommandContext {
21
+ prefix: string;
22
+ command: string;
23
+ offset: number;
24
+ }
25
+
26
+ const GIT_SUBCOMMANDS = [
27
+ "add", "bisect", "branch", "checkout", "cherry-pick", "clean", "clone", "commit", "diff", "fetch",
28
+ "grep", "init", "log", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert", "rm",
29
+ "show", "stash", "status", "switch", "tag", "worktree",
30
+ ];
31
+
32
+ function tokenizeBeforeCursor(text: string): string[] {
33
+ const tokens: string[] = [];
34
+ let current = "";
35
+ let quote: "'" | '"' | null = null;
36
+ let escaped = false;
37
+
38
+ for (const char of text) {
39
+ if (escaped) {
40
+ current += char;
41
+ escaped = false;
42
+ continue;
43
+ }
44
+
45
+ if (char === "\\") {
46
+ escaped = true;
47
+ current += char;
48
+ continue;
49
+ }
50
+
51
+ if (quote) {
52
+ current += char;
53
+ if (char === quote) quote = null;
54
+ continue;
55
+ }
56
+
57
+ if (char === "'" || char === '"') {
58
+ quote = char;
59
+ current += char;
60
+ continue;
61
+ }
62
+
63
+ if (/\s/.test(char)) {
64
+ if (current) {
65
+ tokens.push(current);
66
+ current = "";
67
+ }
68
+ continue;
69
+ }
70
+
71
+ current += char;
72
+ }
73
+
74
+ if (current) tokens.push(current);
75
+ return tokens;
76
+ }
77
+
78
+ function getTokenContext(line: string, cursorCol: number): TokenContext {
79
+ const beforeCursor = line.slice(0, cursorCol);
80
+ const afterCursor = line.slice(cursorCol);
81
+ const tokens = tokenizeBeforeCursor(beforeCursor);
82
+
83
+ let tokenStart = 0;
84
+ for (let i = beforeCursor.length - 1; i >= 0; i -= 1) {
85
+ const char = beforeCursor[i];
86
+ if (char && /\s/.test(char)) {
87
+ tokenStart = i + 1;
88
+ break;
89
+ }
90
+ }
91
+
92
+ let tokenEnd = cursorCol;
93
+ for (let i = cursorCol; i < line.length; i += 1) {
94
+ const char = line[i];
95
+ if (char && /\s/.test(char)) break;
96
+ tokenEnd = i + 1;
97
+ }
98
+
99
+ return {
100
+ line,
101
+ cursorCol,
102
+ beforeCursor,
103
+ afterCursor,
104
+ token: line.slice(tokenStart, tokenEnd),
105
+ tokenStart,
106
+ tokenEnd,
107
+ tokenIndex: Math.max(0, tokens.length - 1),
108
+ previousTokens: tokens,
109
+ };
110
+ }
111
+
112
+ export function getOneOffBashCommandContext(line: string): OneOffBashCommandContext | null {
113
+ if (line.startsWith("!!")) {
114
+ return {
115
+ prefix: "!!",
116
+ command: line.slice(2),
117
+ offset: 2,
118
+ };
119
+ }
120
+
121
+ if (line.startsWith("!")) {
122
+ return {
123
+ prefix: "!",
124
+ command: line.slice(1),
125
+ offset: 1,
126
+ };
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ export function supportsShouldTriggerFileCompletion(
133
+ provider: AutocompleteProvider,
134
+ ): provider is AutocompleteProvider & {
135
+ shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
136
+ } {
137
+ return "shouldTriggerFileCompletion" in provider && typeof provider.shouldTriggerFileCompletion === "function";
138
+ }
139
+
140
+ export function isExtendedCompletionItem(item: AutocompleteItem): item is ExtendedCompletionItem {
141
+ return "replacement" in item
142
+ && typeof item.replacement === "string"
143
+ && "startCol" in item
144
+ && typeof item.startCol === "number"
145
+ && "endCol" in item
146
+ && typeof item.endCol === "number";
147
+ }
148
+
149
+ function uniqueByReplacement(items: ExtendedCompletionItem[]): ExtendedCompletionItem[] {
150
+ const best = new Map<string, ExtendedCompletionItem>();
151
+ for (const item of items) {
152
+ const key = `${item.startCol}:${item.endCol}:${item.replacement}`;
153
+ const existing = best.get(key);
154
+ if (!existing || item.score > existing.score) {
155
+ best.set(key, item);
156
+ }
157
+ }
158
+ return [...best.values()].sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
159
+ }
160
+
161
+ function pathBase(token: string, cwd: string): { dir: string; prefix: string; displayPrefix: string } {
162
+ const expanded = token.startsWith("~/")
163
+ ? join(process.env.HOME || "", token.slice(2))
164
+ : token;
165
+
166
+ const hasSlash = expanded.includes("/");
167
+ if (!hasSlash) {
168
+ return { dir: cwd, prefix: expanded, displayPrefix: "" };
169
+ }
170
+
171
+ const baseDir = expanded.endsWith("/") ? expanded.slice(0, -1) : dirname(expanded);
172
+ const resolvedDir = isAbsolute(baseDir) ? baseDir : resolve(cwd, baseDir);
173
+ const prefix = expanded.endsWith("/") ? "" : basename(expanded);
174
+ const displayPrefix = token.endsWith("/") ? token : token.slice(0, Math.max(0, token.length - prefix.length));
175
+ return { dir: resolvedDir, prefix, displayPrefix };
176
+ }
177
+
178
+ function escapeShellPath(value: string): string {
179
+ return value.replace(/([\\\s"'`$&|;<>()[\]{}?!*])/g, "\\$1");
180
+ }
181
+
182
+ function getPathSuggestions(token: string, cwd: string): ExtendedCompletionItem[] {
183
+ if (!token) return [];
184
+ const { dir, prefix, displayPrefix } = pathBase(token, cwd);
185
+
186
+ try {
187
+ const entries = readdirSync(dir, { withFileTypes: true })
188
+ .filter((entry) => prefix.length === 0 || entry.name.startsWith(prefix))
189
+ .slice(0, 100);
190
+
191
+ return entries.map((entry) => {
192
+ const suffix = entry.isDirectory() ? "/" : "";
193
+ const label = `${displayPrefix}${entry.name}${suffix}`;
194
+ const replacement = `${displayPrefix}${escapeShellPath(entry.name)}${suffix}`;
195
+ return {
196
+ value: replacement,
197
+ label,
198
+ replacement,
199
+ startCol: 0,
200
+ endCol: 0,
201
+ source: "path",
202
+ score: 40 + (entry.isDirectory() ? 4 : 0),
203
+ } satisfies ExtendedCompletionItem;
204
+ });
205
+ } catch {
206
+ // Missing or unreadable directories should only remove this completion source.
207
+ return [];
208
+ }
209
+ }
210
+
211
+ function runGit(args: string[], cwd: string): string[] {
212
+ try {
213
+ const result = spawnSync("git", args, {
214
+ cwd,
215
+ encoding: "utf8",
216
+ stdio: ["ignore", "pipe", "ignore"],
217
+ });
218
+ if (result.status !== 0 || !result.stdout) return [];
219
+ return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
220
+ } catch {
221
+ // Git-aware completions are optional and should not break the main completion flow.
222
+ return [];
223
+ }
224
+ }
225
+
226
+ function getGitSuggestions(ctx: TokenContext, cwd: string): ExtendedCompletionItem[] {
227
+ const tokens = ctx.previousTokens;
228
+ if (tokens[0] !== "git") return [];
229
+
230
+ if (ctx.tokenIndex <= 1) {
231
+ return GIT_SUBCOMMANDS
232
+ .filter((command) => command.startsWith(ctx.token))
233
+ .map((command) => ({
234
+ value: command,
235
+ label: command,
236
+ replacement: command,
237
+ startCol: 0,
238
+ endCol: 0,
239
+ source: "git",
240
+ score: 52,
241
+ }));
242
+ }
243
+
244
+ const subcommand = tokens[1] ?? "";
245
+ if (!["checkout", "switch", "merge", "rebase", "branch", "show", "diff"].includes(subcommand)) {
246
+ return [];
247
+ }
248
+
249
+ const refs = [
250
+ ...runGit(["branch", "--format=%(refname:short)"], cwd),
251
+ ...runGit(["tag", "--list"], cwd),
252
+ ];
253
+
254
+ return [...new Set(refs)]
255
+ .filter((ref) => ref.startsWith(ctx.token))
256
+ .slice(0, 100)
257
+ .map((ref) => ({
258
+ value: ref,
259
+ label: ref,
260
+ replacement: ref,
261
+ startCol: 0,
262
+ endCol: 0,
263
+ source: "git",
264
+ score: 50,
265
+ }));
266
+ }
267
+
268
+ function canUseHistorySuggestion(ctx: TokenContext): boolean {
269
+ return ctx.cursorCol === ctx.line.length && ctx.line.trim().length > 0;
270
+ }
271
+
272
+ function withRange(items: ExtendedCompletionItem[], startCol: number, endCol: number): ExtendedCompletionItem[] {
273
+ return items.map((item) => ({ ...item, startCol, endCol }));
274
+ }
275
+
276
+ function applyCompletionToLine(line: string, item: ExtendedCompletionItem): string {
277
+ return line.slice(0, item.startCol) + item.replacement + line.slice(item.endCol);
278
+ }
279
+
280
+ function commandHead(value: string): string {
281
+ return tokenizeBeforeCursor(value.trim())[0] ?? "";
282
+ }
283
+
284
+ function findNewestHistoryMatchForHead(entries: string[], prefix: string, head: string): string | null {
285
+ for (const rawEntry of entries) {
286
+ const entry = rawEntry.trim();
287
+ if (!entry || !entry.startsWith(prefix)) continue;
288
+ if (commandHead(entry) !== head) continue;
289
+ return entry;
290
+ }
291
+ return null;
292
+ }
293
+
294
+ function getCuratedCommandFallback(prefix: string): GhostSuggestion | null {
295
+ const trimmed = prefix.trim();
296
+ if (!trimmed) return null;
297
+
298
+ if ("cd".startsWith(trimmed)) {
299
+ return { value: "cd ..", source: "path" };
300
+ }
301
+
302
+ if ("git".startsWith(trimmed)) {
303
+ return { value: "git status", source: "git" };
304
+ }
305
+
306
+ return null;
307
+ }
308
+
309
+ function isLikelyGitCommandHead(prefix: string): boolean {
310
+ const trimmed = prefix.trim();
311
+ return trimmed.length >= 2 && "git".startsWith(trimmed);
312
+ }
313
+
314
+ function boostValidatedItemsFromGlobalHistory(
315
+ line: string,
316
+ items: ExtendedCompletionItem[],
317
+ globalHistoryMatches: string[],
318
+ ): ExtendedCompletionItem[] {
319
+ if (items.length === 0 || globalHistoryMatches.length === 0) {
320
+ return items;
321
+ }
322
+
323
+ const boosts = new Map<string, number>();
324
+ for (const [index, value] of globalHistoryMatches.entries()) {
325
+ boosts.set(value, Math.max(1, 4 - index));
326
+ }
327
+
328
+ return items.map((item) => {
329
+ const boost = boosts.get(applyCompletionToLine(line, item));
330
+ return boost ? { ...item, score: item.score + boost } : item;
331
+ });
332
+ }
333
+
334
+ export class BashCompletionEngine {
335
+ async getGhostSuggestion(line: string, cwd: string, shellPath: string, signal: AbortSignal): Promise<GhostSuggestion | null> {
336
+ const projectHistoryEntries = readProjectHistory(cwd);
337
+
338
+ if (line.trim().length === 0) {
339
+ const prefix = line;
340
+ const projectHistory = matchHistoryEntries(
341
+ projectHistoryEntries.map((entry) => entry.command),
342
+ line,
343
+ 1,
344
+ );
345
+ if (projectHistory.length > 0) {
346
+ return { value: `${prefix}${projectHistory[0]!}`, source: "project-history" };
347
+ }
348
+
349
+ return null;
350
+ }
351
+
352
+ const ctx = getTokenContext(line, line.length);
353
+ if (!canUseHistorySuggestion(ctx)) return null;
354
+
355
+ const projectHistory = matchHistoryEntries(
356
+ projectHistoryEntries.map((entry) => entry.command),
357
+ line,
358
+ 10,
359
+ );
360
+ if (projectHistory.length > 0) {
361
+ return { value: projectHistory[0]!, source: "project-history" };
362
+ }
363
+
364
+ if (ctx.tokenIndex === 0) {
365
+ return this.getCommandPositionGhostSuggestion(line, readGlobalShellHistory(shellPath));
366
+ }
367
+
368
+ const deterministic = this.getDeterministicInlineSuggestions(ctx, cwd);
369
+ const globalHistory = matchHistoryEntries(readGlobalShellHistory(shellPath), line, 5);
370
+ const ranked = boostValidatedItemsFromGlobalHistory(
371
+ line,
372
+ uniqueByReplacement(deterministic),
373
+ globalHistory,
374
+ );
375
+
376
+ for (const item of ranked) {
377
+ const value = this.buildInlineSuggestionValue(line, item);
378
+ if (value) {
379
+ return { value, source: item.source };
380
+ }
381
+ }
382
+
383
+ return null;
384
+ }
385
+
386
+ private getCommandPositionGhostSuggestion(
387
+ line: string,
388
+ globalHistoryEntries: string[],
389
+ ): GhostSuggestion | null {
390
+ if (isLikelyGitCommandHead(line)) {
391
+ const globalMatch = findNewestHistoryMatchForHead(globalHistoryEntries, line, "git");
392
+ if (globalMatch) {
393
+ return { value: globalMatch, source: "global-history" };
394
+ }
395
+ }
396
+
397
+ return getCuratedCommandFallback(line);
398
+ }
399
+
400
+ private getDeterministicInlineSuggestions(ctx: TokenContext, cwd: string): ExtendedCompletionItem[] {
401
+ const items: ExtendedCompletionItem[] = [];
402
+ items.push(...withRange(getGitSuggestions(ctx, cwd), ctx.tokenStart, ctx.tokenEnd));
403
+ items.push(...withRange(getPathSuggestions(ctx.token, cwd), ctx.tokenStart, ctx.tokenEnd));
404
+
405
+ return uniqueByReplacement(items);
406
+ }
407
+
408
+ private buildInlineSuggestionValue(line: string, item: ExtendedCompletionItem): string | null {
409
+ const value = applyCompletionToLine(line, item);
410
+ if (!value.startsWith(line) || value === line) {
411
+ return null;
412
+ }
413
+ return value;
414
+ }
415
+
416
+ }
@@ -0,0 +1,40 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+
3
+ import type { GhostSuggestion } from "./types.ts";
4
+
5
+ /**
6
+ * Overlay the ghost suggestion suffix on the editor's prompt line. Returns a
7
+ * copy of the rendered lines with the ghost applied, or null when the ghost
8
+ * cannot be shown (multi-line input, cursor not at end, already-typed text
9
+ * mismatch, or a terminal too narrow). Callers fall back to the original
10
+ * lines when this returns null.
11
+ */
12
+ export function overlayGhostSuggestion(
13
+ lines: string[],
14
+ width: number,
15
+ text: string,
16
+ ghost: GhostSuggestion,
17
+ cursor: { line: number; col: number },
18
+ ): string[] | null {
19
+ if (text.includes("\n")) return null;
20
+ if (cursor.line !== 0 || cursor.col !== text.length) return null;
21
+ if (!ghost.value.startsWith(text) || ghost.value === text) return null;
22
+ if (lines.length < 3) return null;
23
+
24
+ const suffix = ghost.value.slice(text.length);
25
+ const contentLine = 1;
26
+ const cursorBlock = "\x1b[7m \x1b[0m";
27
+ const availableWidth = Math.max(0, width - visibleWidth(text) - 1);
28
+ if (availableWidth === 0) return null;
29
+
30
+ const shownSuffix = truncateToWidth(suffix, availableWidth, "", true);
31
+ if (!shownSuffix) return null;
32
+
33
+ const padding = " ".repeat(
34
+ Math.max(0, width - visibleWidth(text) - 1 - visibleWidth(shownSuffix)),
35
+ );
36
+ const ghostText = `\x1b[38;5;244m${shownSuffix}\x1b[0m`;
37
+ const next = [...lines];
38
+ next[contentLine] = `${text}${cursorBlock}${ghostText}${padding}`;
39
+ return next;
40
+ }
@@ -0,0 +1,80 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ export interface EditorBoundaryShortcuts {
4
+ start: string | null;
5
+ end: string | null;
6
+ }
7
+
8
+ export const DEFAULT_EDITOR_BOUNDARY_SHORTCUTS: EditorBoundaryShortcuts = {
9
+ start: "super+shift+up",
10
+ end: "super+shift+down",
11
+ };
12
+
13
+ export function isPrintableInput(data: string): boolean {
14
+ return data.length === 1 && data.charCodeAt(0) >= 32;
15
+ }
16
+
17
+ export function isCommandUndoShortcut(data: string): boolean {
18
+ return (
19
+ data === "\x1b[122;9u" ||
20
+ data === "\x1b[122;9:1u" ||
21
+ data === "\x1b[122;9:2u" ||
22
+ data === "\x1b[27;9;122~"
23
+ );
24
+ }
25
+
26
+ export function bracketedPasteContent(data: string): string | null {
27
+ const startMarker = "\x1b[200~";
28
+ const endMarker = "\x1b[201~";
29
+ const start = data.indexOf(startMarker);
30
+ if (start !== 0) return null;
31
+
32
+ const end = data.indexOf(endMarker, startMarker.length);
33
+ if (end === -1 || end + endMarker.length !== data.length) return null;
34
+
35
+ return data.slice(startMarker.length, end);
36
+ }
37
+
38
+ export function decodeFileUriList(text: string): string | null {
39
+ const entries = text
40
+ .split(/\r?\n|\s+/)
41
+ .map((entry) => entry.trim())
42
+ .filter((entry) => entry.length > 0 && !entry.startsWith("#"));
43
+
44
+ if (
45
+ entries.length === 0 ||
46
+ entries.some((entry) => !entry.startsWith("file://"))
47
+ ) {
48
+ return null;
49
+ }
50
+
51
+ try {
52
+ return entries.map((entry) => fileURLToPath(entry)).join(" ");
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function droppedPathTextFromInput(data: string): string | null {
59
+ const pasteContent = bracketedPasteContent(data);
60
+ const text = pasteContent ?? data;
61
+ const uriList = decodeFileUriList(text);
62
+ if (uriList) return uriList;
63
+
64
+ const trimmed = text.replace(/^[\r\n]+|[\r\n]+$/g, "");
65
+ if (trimmed.length <= 1 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(trimmed)) {
66
+ return null;
67
+ }
68
+
69
+ if (/^(?:\/|~\/|\.\.?\/)/.test(trimmed) && !/[\r\n]/.test(trimmed)) {
70
+ return trimmed;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ export function resetShellHistoryBrowse(state: object): void {
77
+ Reflect.set(state, "shellHistoryIndex", -1);
78
+ Reflect.set(state, "shellHistoryItems", []);
79
+ Reflect.set(state, "shellHistoryDraft", "");
80
+ }