@groeponline/pi-wishcraft 0.19.2 → 0.21.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -1
  3. package/ROADMAP.md +37 -33
  4. package/banner.png +0 -0
  5. package/bash-mode/editor-ghost.ts +63 -0
  6. package/bash-mode/editor-history.ts +81 -0
  7. package/bash-mode/editor-input.ts +35 -0
  8. package/bash-mode/editor.ts +16 -134
  9. package/bash-mode/shell-session.ts +12 -15
  10. package/bash-mode/types.ts +2 -0
  11. package/docs/bash-mode.md +46 -0
  12. package/docs/commands.md +121 -0
  13. package/docs/configuration.md +289 -0
  14. package/docs/index.md +18 -0
  15. package/docs/segments.md +168 -0
  16. package/docs/skill-manager.md +7 -0
  17. package/docs/stash-and-shortcuts.md +64 -0
  18. package/docs/working-vibes.md +71 -0
  19. package/package.json +3 -1
  20. package/queue/store.ts +72 -3
  21. package/src/config/custom-items.ts +45 -0
  22. package/src/config/parse.ts +19 -3
  23. package/src/config/powerline-config.ts +5 -0
  24. package/src/config/primitives.ts +13 -1
  25. package/src/config/segment-options.ts +29 -24
  26. package/src/config/settings-patch.ts +23 -2
  27. package/src/config/types.ts +29 -17
  28. package/src/extension/commands/bash-mode-actions.ts +1 -0
  29. package/src/extension/commands/commands.ts +30 -20
  30. package/src/extension/commands/powerline-completions.ts +8 -0
  31. package/src/extension/commands/powerline-doctor.ts +247 -0
  32. package/src/extension/commands/powerline-export.ts +94 -0
  33. package/src/extension/commands/queue-commands.ts +29 -1
  34. package/src/extension/commands/vibe-command.ts +49 -1
  35. package/src/extension/core/constants.ts +1 -0
  36. package/src/extension/core/segment-context.ts +19 -2
  37. package/src/extension/core/state.ts +4 -1
  38. package/src/extension/core/status-export.ts +68 -0
  39. package/src/extension/core/types.ts +2 -0
  40. package/src/extension/session/activate.ts +1 -2
  41. package/src/extension/session/cost-alert.ts +36 -0
  42. package/src/extension/session/session-lifecycle.ts +41 -0
  43. package/src/extension/settings/settings-io.ts +23 -17
  44. package/src/extension/shortcuts/shortcuts-config.ts +5 -0
  45. package/src/extension/ui/layout.ts +1 -1
  46. package/src/extension/ui/menu-items.ts +379 -0
  47. package/src/extension/ui/menu-views.ts +352 -149
  48. package/src/extension/ui/powerline-menu-view.ts +83 -0
  49. package/src/extension/ui/powerline-menu.ts +54 -0
  50. package/src/extension/welcome/welcome-integration.ts +5 -9
  51. package/src/segments/registry.ts +100 -39
  52. package/src/segments/system.ts +198 -39
  53. package/src/theme/theme.ts +11 -2
  54. package/src/usage/ledger.ts +6 -0
  55. package/src/welcome/banner.ts +2 -1
  56. package/src/welcome/index.ts +3 -1
  57. package/src/welcome/overlay.ts +2 -0
  58. package/src/welcome/renderer.ts +52 -10
  59. package/src/welcome/types.ts +1 -2
  60. package/src/welcome/whats-new.ts +156 -0
  61. package/src/welcome/widgets/index.ts +3 -0
  62. package/src/welcome/widgets/whats-new-widget.ts +19 -0
  63. package/src/working-vibes/generate.ts +144 -58
  64. package/src/working-vibes/index.ts +10 -2
  65. package/src/working-vibes/storage.ts +5 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.21.0] - 2026-08-20
6
+
7
+ ### Added
8
+ - `/powerline doctor` and `/powerline export`.
9
+ - `/queue archive [hours]` with a single clock snapshot so sent items cannot vanish from both inbox and archive.
10
+ - Focused guides under `docs/` (commands, configuration, segments, bash mode, vibes, shortcuts).
11
+ - What's New welcome widget, cost-alert threshold, ChefGroep status export, and optional fleet SSH open-port probes.
12
+
13
+ ## [0.20.0] - 2026-08-20
14
+
15
+ ### Added
16
+ - `alt+p` menu is three overlay entries (Navigate, Configure, Status) with a Status drill-down for ports, TPS, and toggle.
17
+
18
+ ### Fixed
19
+ - `theme.json` in the working directory overrides icons, and the theme cache busts when that file changes.
20
+
21
+ ### Changed
22
+ - npm pack includes `banner.png` so the published tarball matches `pi.image`.
23
+
5
24
  ## [0.19.2] - 2026-08-20
6
25
 
7
26
  ### Added
package/README.md CHANGED
@@ -66,6 +66,8 @@ curl -fsSL https://raw.githubusercontent.com/GroepOnline/pi-wishcraft/main/scrip
66
66
 
67
67
  Restart or `/reload` pi to activate.
68
68
 
69
+ Extended guides (segments, bash mode, commands, configuration) live in [`docs/`](docs/index.md).
70
+
69
71
  ## Usage
70
72
 
71
73
  Activates automatically. Toggle with `/powerline`, switch presets with `/powerline <name>`, and move the primary row with `/powerline placement above|below|toggle`. Tab after `/powerline` completes preset names and `placement`; tab again after `placement` completes `above`, `below`, or `toggle`.
@@ -247,7 +249,7 @@ Interactivity (Pi core renders the footer as static text, so live click is not p
247
249
 
248
250
  - `/tps [value]`: show or set `POWERLINE_TPS`
249
251
  - `/open-ports`: list listening ports and pick one
250
- - `alt+p`: **powerline menu**: navigate the live segments (`↑`/`↓` + `enter`), configure (preset / TPS / UDP / labels), or open the full ports list
252
+ - `alt+p`: **powerline menu**: three overlays (Navigate, Configure, Status). Status drills down to ports, TPS, and toggle.
251
253
  - `alt+i`: **powerline info**: full open-ports list
252
254
 
253
255
  Both `alt+p` and `alt+i` are rebindable (see Keybinds below); changes apply after `/reload`.
package/ROADMAP.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Roadmap — pi-wishcraft
2
2
 
3
- Herschreven 2026-08-18. Vierde pass 2026-08-20: conflict-restant uit
4
- `19d6cc0` weg. 0.19-code + catalogus staan op main (#12, #11). Overlay-
5
- submenus en de rest van de repairs blijven 0.20. CHE-40 is follow-up,
6
- geen 0.19-blocker. `v0.19.0` en `v0.19.1` staan op npm.
3
+ Herschreven 2026-08-18. Vijfde pass 2026-08-20: 0.19.0–0.19.2 staan
4
+ op npm. CHE-40 (`/powerline` tab) is Done via #18. Overlay-submenus
5
+ (CHE-42 / #19) starten 0.20. ROADMAP was achter op de code: hooks,
6
+ repairs-subset en skills-manager v2 UI zitten al op `main`.
7
7
 
8
8
  Pi core is de engine. Wishcraft is de cockpit. Elke feature dient één van
9
9
  drie doelen: **grip** (skills, tokens, config), **prestatie** (repairs,
@@ -76,7 +76,7 @@ Niet de fork. Niet de SaaS-agent.
76
76
  gebruikerscontent.
77
77
  - Geen skills-markt als identiteit vóór 1.0. Eerst discovery die klopt
78
78
  en een manager die zoekt.
79
- - Geen fleet-SSH `open_ports` totdat iemand het concreet vraagt.
79
+ - Geen fleet-SSH `open_ports` zonder expliciete opt-in (`segmentOptions.openPorts.host`; sanitized, geen shell-injectie).
80
80
  - Geen versie-reset naar 1.0.0. We blijven op 0.19 → 0.20 → 1.0 wanneer
81
81
  de cockpit stabiel is.
82
82
 
@@ -84,16 +84,15 @@ Niet de fork. Niet de SaaS-agent.
84
84
 
85
85
  ## Mijlpalen
86
86
 
87
- - **0.19.0 — "Correctheid"** (deze campagne). Bugs en hygiëne eerst.
88
- Kleine config-afmakers. Geen hooks, geen repairs, geen manager-v2-UI,
89
- geen overlay-submenus. Done = npm 0.19.0 live, `/skills` filtert,
90
- `$test` expandeert geen debris, `npm test` + `npm run typecheck` +
91
- `madge --circular` groen op de tag. Als de config-PR uitloopt:
92
- tag **0.18.1** met alleen PR A (bugs). Correctheid wacht niet op
93
- labels.
94
- - **0.20.0 "Harness"**. Hooks + tool-input repairs + skills-manager
95
- v2-UI + token-overlays. Done = drie README-hookvoorbeelden werken,
96
- repair-teller zichtbaar, `/skills` zoekt op naam+beschrijving+pad.
87
+ - **0.19.0–0.19.2 — "Correctheid"** (geland). Bugs, hygiëne, catalogus,
88
+ auto-release, `/powerline` tab. Hooks, repairs-subset en skills
89
+ manager v2 UI gingen mee in #12, eerder dan deze sectie beloofde.
90
+ Done = npm 0.19.2 live, `/skills` filtert, `$test` expandeert geen
91
+ debris, verify-trio groen op de tag.
92
+ - **0.20.0 "Harness"**. Overlay-chrome + CHE-42 drill-down, Configure
93
+ uit `ctx.ui.select`, token-overlays, rest-repairs, README-hooks die
94
+ waar zijn. Done = drie README-hookvoorbeelden werken, repair-teller
95
+ zichtbaar, `alt+p` overlay-boom, `/tps` deelt de ring met het segment.
97
96
  - **1.0 — "Cockpit"**. Skills-doctor/install, declaratieve policy,
98
97
  preset-editor, idee-review, stabiele ChefGroep-statuskeys,
99
98
  documentatie die waar is. Done = README dekt alles wat we shipten,
@@ -173,7 +172,13 @@ GRO-1061 (runner-queue) is ops, geen product-slice.
173
172
 
174
173
  ## 0.20.0 — Harness
175
174
 
176
- Vier stacked PRs. Pas starten als 0.19 (of 0.18.1) op npm staat.
175
+ Vier stacked PRs. 0.19.2 staat op npm. Hooks, repairs-subset en
176
+ skills-manager v2 UI zijn al op `main` (#12). Op `feat/complete-wishcraft-backlog`
177
+ (reopen #13) landen nu de unieke 0.20-expansies bovenop CHE-42 (#19):
178
+ `/powerline doctor` + `export`, queue-archive + retention (P1 single-clock),
179
+ `customItems.auto`, fleet-SSH `openPorts.host`, what's-new welcome widget,
180
+ cost-alert, bash editor-history split, `docs/` guides, custom-preset builder in
181
+ Configure. Nog open: token-overlays, rest-repairs, README-hookvoorbeelden.
177
182
 
178
183
  Overlay-chrome kit, één keer, daarna hergebruiken: box + ronde hoeken,
179
184
  accent-kop, dim metadata, rechts uitgelijnde counts, `→` detail /
@@ -216,15 +221,13 @@ zijn eigen schema's.
216
221
  Done: pure `repairToolInput(tool, input)` + table-driven tests voor
217
222
  de zes gevallen + de parse-vóór-wrap invariant.
218
223
 
219
- ### PR G — overlay-submenus (CHE-42)
224
+ ### PR G — overlay-submenus (CHE-42) — ✅ #19 + expansion (#13 reopen)
220
225
 
221
226
  Het `alt+p`-menu krijgt gestapelde `SelectList`-overlays (pijltjes +
222
227
  descriptions) in plaats van platte `ctx.ui.select`. Max drie
223
- top-level ingangen. Pure functie `buildPowerlineMenuItems`
224
- unit-testen. Sluit CHE-42. CHE-40 (`/powerline <tab>` subcommands)
225
- blijft open — 0.18 shipped alleen bash token-step, niet deze
226
- hiërarchie. Landt in 0.19 PR B of een kleine follow-up. CHE-41
227
- wordt de per-segment detail view in 1.0, geen tweede `alt+i`-pad.
228
+ top-level ingangen. #19 landt de top-level boom (Navigate / Configure /
229
+ Status). Configure gebruikt nu `showSelectOverlay` (CHE-42 rest). CHE-40
230
+ is Done (#18). CHE-41 is 1.0 per-segment detail, geen tweede `alt+i`-pad.
228
231
 
229
232
  ### PR H — skills manager v2 UI + token-overlays
230
233
 
@@ -301,11 +304,11 @@ Pas na 0.20. Geen parallelle 1.0-tak.
301
304
 
302
305
  | Ticket | Actie |
303
306
  |---|---|
304
- | GRO-1060 fork cleanup | Deels gedaan (tags). Rest = PR A debris + concept-png. CHANGELOG niet inkorten. Versie niet resetten. |
307
+ | GRO-1060 fork cleanup | Done. Release-pad bewezen. CHANGELOG niet inkorten. Versie niet resetten. `banner.png` blijft. |
305
308
  | GRO-1061 CI queue | Ops, niet deze roadmap. |
306
- | CHE-40 `/powerline` tab | In Progress. `/powerline` tab completes presets and placement above, below, or toggle. |
307
- | CHE-41 alt+i info | Wordt 1.0 detail-view. Ticket hernoemen of GRO-child. |
308
- | CHE-42 drill-down | = 0.20 PR G. Hernoemen naar wishcraft / GRO. |
309
+ | CHE-40 `/powerline` tab | Done (#18 / 0.19.2). |
310
+ | CHE-41 per-segment detail | 1.0. Ticket hernoemd; geen tweede `alt+i`-pad. |
311
+ | CHE-42 drill-down | In Progress. #19 = drie top-level overlays + Status. Configure-overlay volgt. |
309
312
 
310
313
  Oude `pi-powerline-footer`-projecttickets niet laten staan alsof
311
314
  die package nog leeft.
@@ -333,8 +336,8 @@ die package nog leeft.
333
336
  - `npm deprecate` van de oude naam faalt tot de scope-owner het
334
337
  token verruimt. Gebruikers die `pi-powerline-footer` installeren
335
338
  blijven op 0.17.2.
336
- - Hooks spawnen processen. Default timeout 30s; kill-switch moet
337
- in 0.20 vanaf dag één bestaan.
339
+ - Hooks spawnen processen. Default timeout 30s; kill-switch
340
+ `wishcraft.hooksEnabled` bestaat al op `main`.
338
341
  - Repairs op core-tools raken we niet aan. Als DeepSeek-achtige
339
342
  modellen daar alsnog op stuklopen, is dat een gesprek met pi
340
343
  core, geen stille override.
@@ -369,8 +372,9 @@ die package nog leeft.
369
372
  Verify-trio groen. (#12)
370
373
  - [x] Gallery-contract gemerged (`chore/pi-dev-gallery`): keywords,
371
374
  `publishConfig.access`, `pi.image`, `npm run verify:package`. (#11)
372
- - [x] PR C: merge to `main` tagged `v0.19.0` then `v0.19.1` and published
373
- (`npm view` = `0.19.1`). Same-job bump publish via #16.
374
- - [ ] README skills-sectie waar; deze ROADMAP in sync.
375
- - [ ] CHE-40: `/powerline` subcommand-tab landt (deze branch) of blijft een
376
- open GRO-child. CHE-41/42 hernoemen.
375
+ - [x] PR C: merge to `main` tagged `v0.19.0` then `v0.19.1` then
376
+ `v0.19.2` and published (`npm view` = `0.19.2`). Same-job bump
377
+ publish via #16. CHE-40 tab-complete via #18.
378
+ - [x] README skills-sectie waar; deze ROADMAP in sync met 0.19.2.
379
+ - [x] CHE-40: `/powerline` subcommand-tab geland (#18). CHE-41/42
380
+ hernoemd naar wishcraft.
package/banner.png ADDED
Binary file
@@ -38,3 +38,66 @@ export function overlayGhostSuggestion(
38
38
  next[contentLine] = `${text}${cursorBlock}${ghostText}${padding}`;
39
39
  return next;
40
40
  }
41
+
42
+ /**
43
+ * Insert the full ghost suggestion when the cursor sits at the end of a
44
+ * single-line buffer that is a strict prefix of the ghost. Returns true when
45
+ * the suggestion was accepted.
46
+ */
47
+ export function acceptGhostSuggestion(editor: any): boolean {
48
+ const ghost = Reflect.get(editor, "ghost") as GhostSuggestion | null;
49
+ if (!ghost) return false;
50
+
51
+ const text = editor.getExpandedText();
52
+ if (text.includes("\n")) return false;
53
+
54
+ const cursor = editor.getCursor();
55
+ if (cursor.line !== 0 || cursor.col !== text.length) return false;
56
+
57
+ if (!ghost.value.startsWith(text) || ghost.value === text) return false;
58
+ editor.setText(ghost.value);
59
+ editor.clearGhostSuggestion();
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Advance the buffer by exactly one token/segment toward the active ghost
65
+ * suggestion (the next whitespace-delimited chunk). Repeated Tabs step through
66
+ * the rest of the suggestion one token at a time instead of inserting the whole
67
+ * line at once. The ghost stays live after a partial step so the next Tab
68
+ * continues from where it left off, and it is only cleared once the full
69
+ * suggestion has been inserted.
70
+ */
71
+ export function completeGhostSuggestionOneToken(editor: any): boolean {
72
+ const ghost = Reflect.get(editor, "ghost") as GhostSuggestion | null;
73
+ if (!ghost) return false;
74
+
75
+ const text = editor.getExpandedText();
76
+ if (text.includes("\n")) return false;
77
+
78
+ const cursor = editor.getCursor();
79
+ if (cursor.line !== 0 || cursor.col !== text.length) return false;
80
+
81
+ const value = ghost.value;
82
+ if (!value.startsWith(text) || value === text) return false;
83
+
84
+ // Next chunk = leading whitespace (when the current token is complete) plus
85
+ // the next whitespace-delimited token from the projected ghost value.
86
+ const rest = value.slice(text.length);
87
+ const nextChunk = rest.match(/^\s*\S*/)?.[0];
88
+ if (!nextChunk) return false;
89
+
90
+ const next = text + nextChunk;
91
+ editor.setText(next);
92
+
93
+ if (value === next) {
94
+ // The full ghost suggestion is now in the buffer; nothing is left to step.
95
+ editor.clearGhostSuggestion();
96
+ } else if (value.startsWith(next)) {
97
+ // Re-resolve against the updated buffer rather than reusing a stale ghost.
98
+ editor.scheduleGhostUpdate();
99
+ } else {
100
+ editor.clearGhostSuggestion();
101
+ }
102
+ return true;
103
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * History navigation for `BashModeEditor`.
3
+ *
4
+ * These helpers operate on the editor instance through its public methods plus
5
+ * `Reflect`-based access to fields that are private on the editor class. This
6
+ * mirrors the existing `resetShellHistoryBrowse` convention in `editor-input.ts`
7
+ * and keeps the main editor class under the module-size ceiling.
8
+ */
9
+
10
+ /** Whether pressing ↑ in non-bash mode should recall the previous prompt. */
11
+ export function isPromptHistoryRecallPosition(editor: any): boolean {
12
+ if (editor.isShowingAutocomplete()) return false;
13
+
14
+ const history = Reflect.get(editor, "history");
15
+ if (!Array.isArray(history) || history.length === 0) return false;
16
+
17
+ const lines = editor.getLines();
18
+ const cursor = editor.getCursor();
19
+ if (lines.length === 1) {
20
+ return cursor.line === 0 && cursor.col === (lines[0]?.length ?? 0);
21
+ }
22
+
23
+ const isOnFirstVisualLine = Reflect.get(editor, "isOnFirstVisualLine");
24
+ if (
25
+ typeof isOnFirstVisualLine === "function" &&
26
+ !isOnFirstVisualLine.call(editor)
27
+ ) {
28
+ return false;
29
+ }
30
+
31
+ return cursor.line === 0;
32
+ }
33
+
34
+ /**
35
+ * Step through the shell history list in bash mode. `direction` is -1 (older,
36
+ * i.e. cursor-up) or +1 (newer, i.e. cursor-down). The first step snapshots the
37
+ * current draft and loads matching history entries; returning past the newest
38
+ * entry restores that draft.
39
+ */
40
+ export function navigateShellHistory(
41
+ editor: any,
42
+ direction: -1 | 1,
43
+ ): void {
44
+ const optionsRef = Reflect.get(editor, "optionsRef");
45
+ let index = Reflect.get(editor, "shellHistoryIndex") as number;
46
+ let items = Reflect.get(editor, "shellHistoryItems") as string[];
47
+ let draft = Reflect.get(editor, "shellHistoryDraft") as string;
48
+
49
+ const prefix = draft || editor.getExpandedText();
50
+ if (index === -1) {
51
+ draft = prefix;
52
+ items = optionsRef.getHistoryEntries(prefix);
53
+ }
54
+
55
+ if (items.length === 0) {
56
+ Reflect.set(editor, "shellHistoryIndex", index);
57
+ Reflect.set(editor, "shellHistoryItems", items);
58
+ Reflect.set(editor, "shellHistoryDraft", draft);
59
+ optionsRef.onNotify("No shell history matches", "info");
60
+ return;
61
+ }
62
+
63
+ if (direction < 0) {
64
+ index = Math.min(items.length - 1, index + 1);
65
+ } else {
66
+ index -= 1;
67
+ }
68
+
69
+ if (index < 0) {
70
+ index = -1;
71
+ editor.setText(draft);
72
+ editor.scheduleGhostUpdate();
73
+ } else {
74
+ editor.setText(items[index] ?? draft);
75
+ editor.clearGhostSuggestion();
76
+ }
77
+
78
+ Reflect.set(editor, "shellHistoryIndex", index);
79
+ Reflect.set(editor, "shellHistoryItems", items);
80
+ Reflect.set(editor, "shellHistoryDraft", draft);
81
+ }
@@ -78,3 +78,38 @@ export function resetShellHistoryBrowse(state: object): void {
78
78
  Reflect.set(state, "shellHistoryItems", []);
79
79
  Reflect.set(state, "shellHistoryDraft", "");
80
80
  }
81
+
82
+ /**
83
+ * Move the editor cursor to the first or last visual line/column. Reads and
84
+ * writes the editor's internal cursor state through `Reflect` so the helper can
85
+ * live outside the editor class without widening its private surface.
86
+ */
87
+ export function moveCursorToEditorBoundary(
88
+ editor: any,
89
+ position: "start" | "end",
90
+ ): void {
91
+ const state = Reflect.get(editor, "state");
92
+ const lines =
93
+ state && typeof state === "object" ? Reflect.get(state, "lines") : null;
94
+ if (!Array.isArray(lines)) {
95
+ throw new Error("Editor cursor state is unavailable");
96
+ }
97
+
98
+ if (position === "start") {
99
+ Reflect.set(state, "cursorLine", 0);
100
+ Reflect.set(state, "cursorCol", 0);
101
+ } else {
102
+ const lastLine = Math.max(0, lines.length - 1);
103
+ Reflect.set(state, "cursorLine", lastLine);
104
+ Reflect.set(
105
+ state,
106
+ "cursorCol",
107
+ typeof lines[lastLine] === "string" ? lines[lastLine].length : 0,
108
+ );
109
+ }
110
+
111
+ Reflect.set(editor, "lastAction", null);
112
+ Reflect.set(editor, "preferredVisualCol", null);
113
+ Reflect.set(editor, "snappedFromCursorCol", null);
114
+ editor.tui.requestRender();
115
+ }
@@ -14,9 +14,18 @@ import {
14
14
  droppedPathTextFromInput,
15
15
  isCommandUndoShortcut,
16
16
  isPrintableInput,
17
+ moveCursorToEditorBoundary as moveCursorToEditorBoundaryImpl,
17
18
  resetShellHistoryBrowse,
18
19
  } from "./editor-input.ts";
19
- import { overlayGhostSuggestion } from "./editor-ghost.ts";
20
+ import {
21
+ acceptGhostSuggestion as acceptGhostSuggestionImpl,
22
+ completeGhostSuggestionOneToken as completeGhostSuggestionOneTokenImpl,
23
+ overlayGhostSuggestion,
24
+ } from "./editor-ghost.ts";
25
+ import {
26
+ isPromptHistoryRecallPosition as isPromptHistoryRecallPositionImpl,
27
+ navigateShellHistory as navigateShellHistoryImpl,
28
+ } from "./editor-history.ts";
20
29
  import type {
21
30
  BashModeEditorOptions,
22
31
  GhostSuggestion,
@@ -301,155 +310,28 @@ export class BashModeEditor extends CustomEditor {
301
310
  return (
302
311
  this.optionsRef.isBashModeActive() || this.isOneOffBashCommandContext()
303
312
  );
304
- }
305
-
306
- private isOneOffBashCommandContext(): boolean {
313
+ } private isOneOffBashCommandContext(): boolean {
307
314
  return getOneOffBashCommandContext(this.getExpandedText()) !== null;
308
315
  }
309
316
 
310
317
  private moveCursorToEditorBoundary(position: "start" | "end"): void {
311
- const state = Reflect.get(this, "state");
312
- const lines =
313
- state && typeof state === "object" ? Reflect.get(state, "lines") : null;
314
- if (!Array.isArray(lines)) {
315
- throw new Error("Editor cursor state is unavailable");
316
- }
317
-
318
- if (position === "start") {
319
- Reflect.set(state, "cursorLine", 0);
320
- Reflect.set(state, "cursorCol", 0);
321
- } else {
322
- const lastLine = Math.max(0, lines.length - 1);
323
- Reflect.set(state, "cursorLine", lastLine);
324
- Reflect.set(
325
- state,
326
- "cursorCol",
327
- typeof lines[lastLine] === "string" ? lines[lastLine].length : 0,
328
- );
329
- }
330
-
331
- Reflect.set(this, "lastAction", null);
332
- Reflect.set(this, "preferredVisualCol", null);
333
- Reflect.set(this, "snappedFromCursorCol", null);
334
- this.tui.requestRender();
318
+ moveCursorToEditorBoundaryImpl(this, position);
335
319
  }
336
320
 
337
321
  private acceptGhostSuggestion(): boolean {
338
- if (!this.ghost) return false;
339
- const text = this.getExpandedText();
340
- if (text.includes("\n")) return false;
341
-
342
- const cursor = this.getCursor();
343
- if (cursor.line !== 0 || cursor.col !== text.length) return false;
344
-
345
- if (!this.ghost.value.startsWith(text) || this.ghost.value === text)
346
- return false;
347
- this.setText(this.ghost.value);
348
- this.clearGhostSuggestion();
349
- return true;
322
+ return acceptGhostSuggestionImpl(this);
350
323
  }
351
324
 
352
- /**
353
- * Advance the buffer by exactly one token/segment toward the active ghost
354
- * suggestion (the next whitespace-delimited chunk). Repeated Tabs step
355
- * through the rest of the suggestion one token at a time instead of
356
- * inserting the whole line at once. The ghost stays live after a partial
357
- * step so the next Tab continues from where it left off, and it is only
358
- * cleared once the full suggestion has been inserted.
359
- */
360
325
  private completeGhostSuggestionOneToken(): boolean {
361
- if (!this.ghost) return false;
362
- const text = this.getExpandedText();
363
- if (text.includes("\n")) return false;
364
-
365
- const cursor = this.getCursor();
366
- if (cursor.line !== 0 || cursor.col !== text.length) return false;
367
-
368
- const value = this.ghost.value;
369
- if (!value.startsWith(text) || value === text) return false;
370
-
371
- // Next chunk = leading whitespace (when the current token is complete)
372
- // plus the next whitespace-delimited token from the projected ghost value.
373
- const rest = value.slice(text.length);
374
- const nextChunk = rest.match(/^\s*\S*/)?.[0];
375
- if (!nextChunk) return false;
376
-
377
- const next = text + nextChunk;
378
- this.setText(next);
379
-
380
- if (value === next) {
381
- // The full ghost suggestion is now in the buffer; nothing is left to
382
- // step through.
383
- this.clearGhostSuggestion();
384
- } else if (value.startsWith(next)) {
385
- // Re-resolve against the updated buffer rather than reusing a stale ghost.
386
- this.scheduleGhostUpdate();
387
- } else {
388
- this.clearGhostSuggestion();
389
- }
390
- return true;
326
+ return completeGhostSuggestionOneTokenImpl(this);
391
327
  }
392
328
 
393
329
  private isPromptHistoryRecallPosition(): boolean {
394
- if (this.isShowingAutocomplete()) return false;
395
-
396
- const history = Reflect.get(this, "history");
397
- if (!Array.isArray(history) || history.length === 0) return false;
398
-
399
- const lines = this.getLines();
400
- const cursor = this.getCursor();
401
- if (lines.length === 1) {
402
- return cursor.line === 0 && cursor.col === (lines[0]?.length ?? 0);
403
- }
404
-
405
- const isOnFirstVisualLine = Reflect.get(this, "isOnFirstVisualLine");
406
- if (
407
- typeof isOnFirstVisualLine === "function" &&
408
- !isOnFirstVisualLine.call(this)
409
- ) {
410
- return false;
411
- }
412
-
413
- return cursor.line === 0;
330
+ return isPromptHistoryRecallPositionImpl(this);
414
331
  }
415
332
 
416
333
  private navigateShellHistory(direction: -1 | 1): void {
417
- const prefix = this.shellHistoryDraft || this.getExpandedText();
418
- if (this.shellHistoryIndex === -1) {
419
- this.shellHistoryDraft = prefix;
420
- this.shellHistoryItems = this.optionsRef.getHistoryEntries(prefix);
421
- }
422
-
423
- if (this.shellHistoryItems.length === 0) {
424
- this.optionsRef.onNotify("No shell history matches", "info");
425
- return;
426
- }
427
-
428
- if (direction < 0) {
429
- this.shellHistoryIndex = Math.min(
430
- this.shellHistoryItems.length - 1,
431
- this.shellHistoryIndex + 1,
432
- );
433
- this.setText(
434
- this.shellHistoryItems[this.shellHistoryIndex] ??
435
- this.shellHistoryDraft,
436
- );
437
- this.clearGhostSuggestion();
438
- return;
439
- }
440
-
441
- this.shellHistoryIndex -= 1;
442
- if (this.shellHistoryIndex < 0) {
443
- this.shellHistoryIndex = -1;
444
- this.setText(this.shellHistoryDraft);
445
- this.scheduleGhostUpdate();
446
- return;
447
- }
448
-
449
- this.setText(
450
- this.shellHistoryItems[this.shellHistoryIndex] ?? this.shellHistoryDraft,
451
- );
452
- this.clearGhostSuggestion();
334
+ navigateShellHistoryImpl(this, direction);
453
335
  }
454
336
 
455
337
  private scheduleGhostUpdate(): void {
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { basename, join } from "node:path";
4
4
  import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
@@ -87,6 +87,7 @@ print -r -- "${READY_SENTINEL}:$PWD"
87
87
 
88
88
  export class ManagedShellSession {
89
89
  private readonly shellPath: string;
90
+ private readonly initScript: string | null;
90
91
  private readonly transcript: BashTranscriptStore;
91
92
  private readonly onStateChange: () => void;
92
93
  private readonly onCommandSuccess: (command: string, cwd: string) => void;
@@ -107,8 +108,10 @@ export class ManagedShellSession {
107
108
  transcript: BashTranscriptStore,
108
109
  onStateChange: () => void,
109
110
  onCommandSuccess: (command: string, cwd: string) => void,
111
+ initScript: string | null = null,
110
112
  ) {
111
113
  this.shellPath = shellPath;
114
+ this.initScript = initScript;
112
115
  this.transcript = transcript;
113
116
  this.onStateChange = onStateChange;
114
117
  this.onCommandSuccess = onCommandSuccess;
@@ -174,7 +177,12 @@ export class ManagedShellSession {
174
177
  this.onStateChange();
175
178
  });
176
179
 
177
- this.sendRaw(getShellInitScript(this.state.shellName) + "\n");
180
+ const projectInit = this.initScript
181
+ ? this.initScript.endsWith("\n")
182
+ ? this.initScript
183
+ : `${this.initScript}\n`
184
+ : "";
185
+ this.sendRaw(projectInit + getShellInitScript(this.state.shellName) + "\n");
178
186
  return this.readyPromise;
179
187
  }
180
188
 
@@ -214,11 +222,6 @@ export class ManagedShellSession {
214
222
  this.readyPromise = null;
215
223
  this.readyResolve = null;
216
224
  this.readyReject = null;
217
- try {
218
- rmSync(this.tempDir, { recursive: true, force: true });
219
- } catch {
220
- // tempdir opruimen is best-effort
221
- }
222
225
  if (!this.process) return;
223
226
  try {
224
227
  process.kill(-this.process.pid!, "SIGKILL");
@@ -257,10 +260,7 @@ export class ManagedShellSession {
257
260
  }
258
261
 
259
262
  if (line.startsWith(`${COMMAND_START_SENTINEL}:`)) {
260
- // cwd kan zelf ':' bevatten: alles na het tweede veld is cwd
261
- const parts = line.split(":");
262
- const id = parts[1];
263
- const cwd = parts.slice(2).join(":");
263
+ const [, id, cwd] = line.split(":");
264
264
  if (cwd) this.state.cwd = cwd;
265
265
  this.currentCommandId = id ?? this.currentCommandId;
266
266
  this.onStateChange();
@@ -268,10 +268,7 @@ export class ManagedShellSession {
268
268
  }
269
269
 
270
270
  if (line.startsWith(`${COMMAND_DONE_SENTINEL}:`)) {
271
- const parts = line.split(":");
272
- const id = parts[1];
273
- const exitCodeText = parts[2];
274
- const cwd = parts.slice(3).join(":");
271
+ const [, id, exitCodeText, cwd] = line.split(":");
275
272
  const exitCode = Number.parseInt(exitCodeText ?? "1", 10);
276
273
  this.state.running = false;
277
274
  this.state.lastExitCode = Number.isFinite(exitCode) ? exitCode : 1;
@@ -6,6 +6,8 @@ export interface BashModeSettings {
6
6
  toggleShortcut: string | null;
7
7
  transcriptMaxLines: number;
8
8
  transcriptMaxBytes: number;
9
+ /** Shell commands sourced once when the managed shell starts (project-scoped init). */
10
+ initScript: string | null;
9
11
  }
10
12
 
11
13
  export interface BashCommandRecord {