@groeponline/pi-wishcraft 0.20.0 → 0.22.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 +17 -0
- package/README.md +83 -0
- package/ROADMAP.md +18 -20
- package/bash-mode/editor-ghost.ts +63 -0
- package/bash-mode/editor-history.ts +81 -0
- package/bash-mode/editor-input.ts +35 -0
- package/bash-mode/editor.ts +16 -134
- package/bash-mode/shell-session.ts +12 -15
- package/bash-mode/types.ts +2 -0
- package/docs/bash-mode.md +46 -0
- package/docs/commands.md +122 -0
- package/docs/configuration.md +307 -0
- package/docs/index.md +18 -0
- package/docs/segments.md +168 -0
- package/docs/skill-manager.md +7 -0
- package/docs/stash-and-shortcuts.md +64 -0
- package/docs/working-vibes.md +71 -0
- package/package.json +2 -1
- package/queue/store.ts +72 -3
- package/src/config/custom-items.ts +45 -0
- package/src/config/parse.ts +19 -3
- package/src/config/powerline-config.ts +5 -0
- package/src/config/primitives.ts +13 -1
- package/src/config/segment-options.ts +29 -24
- package/src/config/settings-patch.ts +23 -2
- package/src/config/types.ts +32 -17
- package/src/extension/commands/bash-mode-actions.ts +1 -0
- package/src/extension/commands/commands.ts +36 -18
- package/src/extension/commands/powerline-completions.ts +8 -0
- package/src/extension/commands/powerline-doctor.ts +247 -0
- package/src/extension/commands/powerline-export.ts +94 -0
- package/src/extension/commands/queue-commands.ts +29 -1
- package/src/extension/commands/vibe-command.ts +49 -1
- package/src/extension/core/constants.ts +1 -0
- package/src/extension/core/segment-context.ts +38 -2
- package/src/extension/core/state.ts +5 -1
- package/src/extension/core/status-export.ts +68 -0
- package/src/extension/core/types.ts +4 -0
- package/src/extension/hooks/repairs.ts +89 -16
- package/src/extension/session/activate.ts +1 -2
- package/src/extension/session/cost-alert.ts +36 -0
- package/src/extension/session/session-lifecycle.ts +82 -1
- package/src/extension/settings/settings-io.ts +23 -17
- package/src/extension/settings/wishcraft-config.ts +2 -1
- package/src/extension/shortcuts/shortcuts-config.ts +5 -0
- package/src/extension/ui/layout.ts +1 -1
- package/src/extension/ui/menu-items.ts +379 -0
- package/src/extension/ui/menu-views.ts +377 -191
- package/src/extension/ui/overlay-chrome.ts +163 -0
- package/src/extension/ui/powerline-menu-view.ts +2 -4
- package/src/extension/ui/token-overlays.ts +82 -0
- package/src/extension/welcome/welcome-integration.ts +5 -9
- package/src/segments/registry.ts +100 -39
- package/src/segments/system.ts +204 -64
- package/src/segments/usage.ts +10 -4
- package/src/usage/ledger.ts +6 -0
- package/src/usage/token-budget.ts +57 -0
- package/src/usage/tps-ring.ts +151 -0
- package/src/usage/usage-store.ts +351 -0
- package/src/welcome/banner.ts +2 -1
- package/src/welcome/index.ts +3 -1
- package/src/welcome/overlay.ts +2 -0
- package/src/welcome/renderer.ts +52 -10
- package/src/welcome/types.ts +1 -2
- package/src/welcome/whats-new.ts +156 -0
- package/src/welcome/widgets/index.ts +3 -0
- package/src/welcome/widgets/whats-new-widget.ts +19 -0
- package/src/working-vibes/generate.ts +144 -58
- package/src/working-vibes/index.ts +10 -2
- package/src/working-vibes/storage.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.22.0] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `/tps` overlay reads the same 1s/5s ring as the segment; `/usage` shows session / today / week from `wishcraft-usage.json`.
|
|
9
|
+
- Rest of the tool-input repairs on custom tools: JSON-string arrays, `{}` placeholders, bare-string wrap, and path aliases.
|
|
10
|
+
- README hook examples (bash-guard, write-audit, SessionStart git-status) plus `wishcraft.hooksEnabled` kill-switch docs.
|
|
11
|
+
- Overlay type-to-filter uses substring on label, value, and description.
|
|
12
|
+
- `wishcraft.tokenBudget.daily` colours the cost segment at 80%/100% and warns on welcome; it never blocks.
|
|
13
|
+
|
|
14
|
+
## [0.21.0] - 2026-08-20
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `/powerline doctor` and `/powerline export`.
|
|
18
|
+
- `/queue archive [hours]` with a single clock snapshot so sent items cannot vanish from both inbox and archive.
|
|
19
|
+
- Focused guides under `docs/` (commands, configuration, segments, bash mode, vibes, shortcuts).
|
|
20
|
+
- What's New welcome widget, cost-alert threshold, ChefGroep status export, and optional fleet SSH open-port probes.
|
|
21
|
+
|
|
5
22
|
## [0.20.0] - 2026-08-20
|
|
6
23
|
|
|
7
24
|
### 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`.
|
|
@@ -246,6 +248,7 @@ Define your own preset in settings; it merges over built-ins and is selectable v
|
|
|
246
248
|
Interactivity (Pi core renders the footer as static text, so live click is not possible; actions live in commands and a navigable overlay):
|
|
247
249
|
|
|
248
250
|
- `/tps [value]`: show or set `POWERLINE_TPS`
|
|
251
|
+
- `/usage`: today / week / session token overlay (same ledger as the cost segment)
|
|
249
252
|
- `/open-ports`: list listening ports and pick one
|
|
250
253
|
- `alt+p`: **powerline menu**: three overlays (Navigate, Configure, Status). Status drills down to ports, TPS, and toggle.
|
|
251
254
|
- `alt+i`: **powerline info**: full open-ports list
|
|
@@ -472,6 +475,86 @@ Browse and insert your installed skills (`SKILL.md` files and `*.md`/`*.txt` pro
|
|
|
472
475
|
|
|
473
476
|
The manager reuses the same skill discovery as inline `/command`/`$skill` triggers, so anything you can inline you can also browse and insert manually.
|
|
474
477
|
|
|
478
|
+
## Hooks (harness)
|
|
479
|
+
|
|
480
|
+
Wishcraft can run Command Code-style hooks on Pi's native events (`tool_call`, `tool_result`, `session_start`, `turn_end`). Each hook is a command that reads JSON on stdin. Set `wishcraft.hooksEnabled` to `false` to kill-switch every hook without deleting the config. Hook *definitions* are read from the global agent settings file only (project `.pi/settings.json` cannot install new commands).
|
|
481
|
+
|
|
482
|
+
Put the scripts somewhere executable (example: `~/.pi/agent/hooks/`) and point settings at them:
|
|
483
|
+
|
|
484
|
+
```json
|
|
485
|
+
{
|
|
486
|
+
"wishcraft": {
|
|
487
|
+
"hooksEnabled": true,
|
|
488
|
+
"hooks": {
|
|
489
|
+
"preToolUse": [
|
|
490
|
+
{
|
|
491
|
+
"matcher": "bash",
|
|
492
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/bash-guard.sh", "timeout": 5 }]
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
"postToolUse": [
|
|
496
|
+
{
|
|
497
|
+
"matcher": "write",
|
|
498
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/write-audit.sh", "timeout": 5 }]
|
|
499
|
+
}
|
|
500
|
+
],
|
|
501
|
+
"sessionStart": [
|
|
502
|
+
{
|
|
503
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/session-git-status.sh", "timeout": 10 }]
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**1. bash-guard** — deny `rm -rf /` (and close variants) before bash runs. Exit 2 is deny; the first stderr line or `permissionDecisionReason` is what the model sees.
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
#!/usr/bin/env bash
|
|
515
|
+
# ~/.pi/agent/hooks/bash-guard.sh
|
|
516
|
+
payload=$(cat)
|
|
517
|
+
cmd=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
|
|
518
|
+
if printf '%s' "$cmd" | grep -Eq '(^|[[:space:]])rm[[:space:]]+(-[a-zA-Z]*[[:space:]]+)*-r[a-zA-Z]*f|-fr[a-zA-Z]*|[[:space:]]/[[:space:]]*$'; then
|
|
519
|
+
printf '%s\n' '{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"blocked destructive rm"}}'
|
|
520
|
+
echo "blocked destructive rm" >&2
|
|
521
|
+
exit 2
|
|
522
|
+
fi
|
|
523
|
+
exit 0
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**2. write-audit** — append-only log of write tool calls (does not block).
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
#!/usr/bin/env bash
|
|
530
|
+
# ~/.pi/agent/hooks/write-audit.sh
|
|
531
|
+
mkdir -p "$HOME/.pi/agent/logs"
|
|
532
|
+
printf '%s\n' "$(date -Is) $1" >> "$HOME/.pi/agent/logs/write-audit.jsonl"
|
|
533
|
+
cat >> "$HOME/.pi/agent/logs/write-audit.jsonl"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The hook receives the JSON payload on stdin; the snippet above stores the raw event. Trim or jq-filter as you like.
|
|
537
|
+
|
|
538
|
+
**3. SessionStart git-status** — inject `git status --short` as extra context at session start.
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
#!/usr/bin/env bash
|
|
542
|
+
# ~/.pi/agent/hooks/session-git-status.sh
|
|
543
|
+
status=$(git status --short 2>/dev/null | head -n 40)
|
|
544
|
+
CTX="$status" python3 - <<'PY'
|
|
545
|
+
import json, os
|
|
546
|
+
print(json.dumps({
|
|
547
|
+
"hookSpecificOutput": {
|
|
548
|
+
"additionalContext": "git status:\n" + os.environ.get("CTX", "")
|
|
549
|
+
}
|
|
550
|
+
}))
|
|
551
|
+
PY
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Tool-input repairs (custom/extension tools only) run before hooks: null-for-optional, JSON-string arrays, `{}` → `[]` on array keys, bare-string wrap, path aliases, and markdown auto-link unwrap. `/repairs` shows the counters. Core tools (`bash`, `read`, `edit`, `write`, `grep`, `find`, `ls`) are never rewritten.
|
|
555
|
+
|
|
556
|
+
`wishcraft.tokenBudget.daily` (token count) paints the cost segment warning/red at 80%/100% and notifies on welcome. It never blocks a turn. `/usage` shows session / today / week from `~/.pi/agent/wishcraft-usage.json`. `/tps` with no args opens the live ring overlay (same sampler as the segment).
|
|
557
|
+
|
|
475
558
|
## Working Vibes
|
|
476
559
|
|
|
477
560
|
Transform boring "Working..." messages into themed phrases that match your style:
|
package/ROADMAP.md
CHANGED
|
@@ -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`
|
|
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
|
|
|
@@ -89,10 +89,10 @@ Niet de fork. Niet de SaaS-agent.
|
|
|
89
89
|
manager v2 UI gingen mee in #12, eerder dan deze sectie beloofde.
|
|
90
90
|
Done = npm 0.19.2 live, `/skills` filtert, `$test` expandeert geen
|
|
91
91
|
debris, verify-trio groen op de tag.
|
|
92
|
-
- **0.20.0 — "Harness"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
- **0.20.0–0.22 — "Harness"** (0.20.0 + 0.21.0 op npm; leftovers in GRO-1414).
|
|
93
|
+
Overlay-chrome + CHE-42 drill-down, Configure als SelectList, token-overlays,
|
|
94
|
+
rest-repairs, README-hooks. Done = drie README-hookvoorbeelden, repair-teller,
|
|
95
|
+
`alt+p` overlay-boom, `/tps` deelt de ring met het segment.
|
|
96
96
|
- **1.0 — "Cockpit"**. Skills-doctor/install, declaratieve policy,
|
|
97
97
|
preset-editor, idee-review, stabiele ChefGroep-statuskeys,
|
|
98
98
|
documentatie die waar is. Done = README dekt alles wat we shipten,
|
|
@@ -172,10 +172,9 @@ GRO-1061 (runner-queue) is ops, geen product-slice.
|
|
|
172
172
|
|
|
173
173
|
## 0.20.0 — Harness
|
|
174
174
|
|
|
175
|
-
Vier stacked PRs. 0.19.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
README-voorbeelden.
|
|
175
|
+
Vier stacked PRs. 0.20.0 (#19) en 0.21.0 (#13) staan op npm. CHE-42
|
|
176
|
+
Configure-overlay, doctor/export, queue-archive en `docs/` zijn geland.
|
|
177
|
+
GRO-1414 sluit README-hooks, rest-repairs, `/tps`+`/usage`, substring-filter.
|
|
179
178
|
|
|
180
179
|
Overlay-chrome kit, één keer, daarna hergebruiken: box + ronde hoeken,
|
|
181
180
|
accent-kop, dim metadata, rechts uitgelijnde counts, `→` detail /
|
|
@@ -183,7 +182,7 @@ accent-kop, dim metadata, rechts uitgelijnde counts, `→` detail /
|
|
|
183
182
|
skills v2; tweede = `/usage`; derde = queue/idea. Pure render-
|
|
184
183
|
functies, geen `ctx.ui`-mock.
|
|
185
184
|
|
|
186
|
-
### PR E — hooks — ✅ geland in `feat/wishcraft-0.19
|
|
185
|
+
### PR E — hooks — ✅ geland in `feat/wishcraft-0.19`; README-voorbeelden in GRO-1414
|
|
187
186
|
|
|
188
187
|
Settings: `wishcraft.hooks` met events
|
|
189
188
|
`preToolUse | postToolUse | sessionStart | turnEnd`. Per hook
|
|
@@ -198,7 +197,7 @@ Done: `parseHookOutput` unit-testen. README met drie werkende
|
|
|
198
197
|
voorbeelden: bash-guard (`rm -rf /` blokkeren), write-audit
|
|
199
198
|
(append-only log), SessionStart git-status injectie.
|
|
200
199
|
|
|
201
|
-
### PR F — tool-input repairs — ✅
|
|
200
|
+
### PR F — tool-input repairs — ✅ schema-loze subset in 0.19; rest (JSON-array, `{}`, bare-wrap, path aliases) in GRO-1414. Core tools blijven met rust.
|
|
202
201
|
|
|
203
202
|
`tool_call`-handler repareert bekende malformaties vóór executie
|
|
204
203
|
(mutable input). Volgorde vast: json-parse vóór bare-wrap.
|
|
@@ -218,15 +217,13 @@ zijn eigen schema's.
|
|
|
218
217
|
Done: pure `repairToolInput(tool, input)` + table-driven tests voor
|
|
219
218
|
de zes gevallen + de parse-vóór-wrap invariant.
|
|
220
219
|
|
|
221
|
-
### PR G — overlay-submenus (CHE-42) —
|
|
220
|
+
### PR G — overlay-submenus (CHE-42) — ✅ #19 + expansion (#13 reopen)
|
|
222
221
|
|
|
223
222
|
Het `alt+p`-menu krijgt gestapelde `SelectList`-overlays (pijltjes +
|
|
224
223
|
descriptions) in plaats van platte `ctx.ui.select`. Max drie
|
|
225
|
-
top-level ingangen.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
0.20-snede. CHE-40 is Done (#18). CHE-41 is 1.0 per-segment detail,
|
|
229
|
-
geen tweede `alt+i`-pad.
|
|
224
|
+
top-level ingangen. #19 landt de top-level boom (Navigate / Configure /
|
|
225
|
+
Status). Configure gebruikt nu `showSelectOverlay` (CHE-42 rest). CHE-40
|
|
226
|
+
is Done (#18). CHE-41 is 1.0 per-segment detail, geen tweede `alt+i`-pad.
|
|
230
227
|
|
|
231
228
|
### PR H — skills manager v2 UI + token-overlays
|
|
232
229
|
|
|
@@ -307,7 +304,8 @@ Pas na 0.20. Geen parallelle 1.0-tak.
|
|
|
307
304
|
| GRO-1061 CI queue | Ops, niet deze roadmap. |
|
|
308
305
|
| CHE-40 `/powerline` tab | Done (#18 / 0.19.2). |
|
|
309
306
|
| CHE-41 per-segment detail | 1.0. Ticket hernoemd; geen tweede `alt+i`-pad. |
|
|
310
|
-
| CHE-42 drill-down |
|
|
307
|
+
| CHE-42 drill-down | Done (#19 + Configure in #13). |
|
|
308
|
+
| GRO-1414 0.20 leftovers | In Progress. Hooks docs, rest-repairs, `/tps` `/usage`, substring filter. |
|
|
311
309
|
|
|
312
310
|
Oude `pi-powerline-footer`-projecttickets niet laten staan alsof
|
|
313
311
|
die package nog leeft.
|
|
@@ -330,8 +328,8 @@ die package nog leeft.
|
|
|
330
328
|
|
|
331
329
|
## Residual risks
|
|
332
330
|
|
|
333
|
-
- `SelectList.setFilter` matcht alleen prefix op `value`.
|
|
334
|
-
|
|
331
|
+
- `SelectList.setFilter` matcht alleen prefix op `value`. Overlay-chrome
|
|
332
|
+
filtert zelf op substring (GRO-1414). Skills-manager had dat al.
|
|
335
333
|
- `npm deprecate` van de oude naam faalt tot de scope-owner het
|
|
336
334
|
token verruimt. Gebruikers die `pi-powerline-footer` installeren
|
|
337
335
|
blijven op 0.17.2.
|
|
@@ -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
|
+
}
|
package/bash-mode/editor.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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;
|
package/bash-mode/types.ts
CHANGED
|
@@ -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 {
|