@groeponline/pi-wishcraft 1.4.1 → 1.6.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 (64) hide show
  1. package/AGENTS.md +28 -0
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +22 -18
  4. package/ROADMAP.md +3 -3
  5. package/STRATEGY.md +54 -0
  6. package/bash-mode/editor-input.ts +3 -1
  7. package/bash-mode/editor.ts +44 -7
  8. package/bash-mode/forward.ts +64 -0
  9. package/bash-mode/pty-session.ts +356 -0
  10. package/bash-mode/ptyshell-managed.ts +145 -0
  11. package/bash-mode/session-factory.ts +64 -0
  12. package/bash-mode/transcript-v2.ts +77 -0
  13. package/bash-mode/transcript.ts +18 -0
  14. package/bash-mode/types.ts +27 -0
  15. package/docs/bash-mode.md +4 -0
  16. package/docs/configuration.md +45 -1
  17. package/docs/index.md +4 -1
  18. package/package.json +1 -1
  19. package/src/config/effective.ts +122 -0
  20. package/src/config/settings-registry.ts +2 -0
  21. package/src/config/structural-presets.ts +1 -1
  22. package/src/extension/commands/bash-mode-actions.ts +11 -10
  23. package/src/extension/commands/commands.ts +8 -0
  24. package/src/extension/core/state.ts +0 -1
  25. package/src/extension/core/types.ts +2 -2
  26. package/src/extension/session/session-lifecycle.ts +21 -0
  27. package/src/extension/skills/skill-templates.ts +12 -3
  28. package/src/extension/ui/custom-editor.ts +4 -0
  29. package/src/extension/ui/status-line-renderers.ts +6 -7
  30. package/src/extension/welcome/welcome-integration.ts +23 -1
  31. package/src/motion/frames.ts +19 -9
  32. package/src/render/layout.ts +95 -0
  33. package/src/render/motion-rail.ts +79 -0
  34. package/src/render/paint.ts +40 -0
  35. package/src/render/v2-adapter.ts +68 -0
  36. package/src/render/v2-entry.ts +132 -0
  37. package/src/segments/pipeline.ts +164 -0
  38. package/src/segments/types.ts +36 -0
  39. package/src/segments/usage.ts +42 -3
  40. package/src/studio/actions.ts +105 -0
  41. package/src/studio/advice-pane.ts +73 -0
  42. package/src/studio/advise/context.ts +89 -0
  43. package/src/studio/advise/engine.ts +117 -0
  44. package/src/studio/advise/prompts.ts +68 -0
  45. package/src/studio/component.ts +105 -0
  46. package/src/studio/deepwiki/cache.ts +74 -0
  47. package/src/studio/deepwiki/client.ts +73 -0
  48. package/src/studio/deepwiki/extract.ts +66 -0
  49. package/src/studio/inspect.ts +38 -0
  50. package/src/studio/list.ts +48 -0
  51. package/src/studio/open.ts +44 -0
  52. package/src/studio/state.ts +100 -0
  53. package/src/studio/types.ts +30 -0
  54. package/src/welcome/banner.ts +6 -0
  55. package/src/welcome/index.ts +7 -0
  56. package/src/welcome/overlay.ts +6 -0
  57. package/src/welcome/renderer.ts +6 -39
  58. package/src/welcome/types.ts +6 -0
  59. package/src/welcome/welcome-art.ts +148 -0
  60. package/src/working-vibes/frames.ts +25 -0
  61. package/src/working-vibes/index.ts +2 -0
  62. package/src/working-vibes/storage.ts +11 -0
  63. package/bash-mode/shell-session.ts +0 -294
  64. package/src/signal/render.ts +0 -221
package/AGENTS.md CHANGED
@@ -72,3 +72,31 @@ npm test # node --experimental-strip-types --test tests/**/*.test.
72
72
  ```
73
73
 
74
74
  Run both before proposing any non-trivial change.
75
+
76
+ Compound Engineering overlay: `.compound-engineering/` (tracked `config.yaml`, gitignored `config.local.yaml`). Artifact root `.compound-engineering/artifacts/`. Portable skills `~/.agents/skills/ce-*`; native Cursor plugin is fallback only when this overlay is absent.
77
+
78
+ ## Cursor Cloud specific instructions
79
+
80
+ The Cloud Agent environment is provisioned so the extension can be tested both as
81
+ a library and inside real `pi`, with Docker for containerized/parallel runs.
82
+
83
+ - **Node:** the platform `node` on `PATH` is v22.14 (fine for `npm ci`,
84
+ `typecheck`, and the test suite, which use `--experimental-strip-types`).
85
+ Node 24 is available via `nvm` and is what CI uses.
86
+ - **pi CLI:** installed and pinned to the wishcraft peer range
87
+ (`@earendil-works/pi-coding-agent` `>=0.81.0 <0.85.0`). pi requires Node
88
+ `>=22.19`, so the `pi` launcher is wrapped to run under the `nvm` Node 24.
89
+ `~/.pi/agent/settings.json` loads this checkout (`packages: ["/workspace"]`,
90
+ `preset: chef`) so `pi` renders the wishcraft welcome/powerline on startup.
91
+ Running a model needs a provider key (`/login`); the extension loads without
92
+ one.
93
+ - **Docker:** `dockerd` runs with the `fuse-overlayfs` storage driver (required
94
+ in the nested VM). `scripts/cloud-agent-start.sh` starts it per boot;
95
+ `scripts/cloud-agent-install.sh` is the idempotent bootstrap (`npm ci` plus
96
+ self-healing pi setup).
97
+ - **Containerized / parallel testing:** `scripts/docker-test.sh [-n N]` runs the
98
+ full check suite (`typecheck` + `test` + `circular`) in `node:24` containers,
99
+ each with an isolated copy of the tree, so `N` runs are safe in parallel.
100
+ - **Publishing / pi.dev:** merges to `main` auto-publish to npm via the org
101
+ `NPM_TOKEN` (`.github/workflows/release.yml`), and pi.dev mirrors npm. Gate
102
+ the catalog contract with `npm run verify:package`.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.6.0] - 2026-08-28
6
+
7
+ ### Added (v2 Platform — wishcraft-v2-platform plan)
8
+ - Powerline v2: the status line now renders through a single path (`renderStatusLineV2` -> `computeLaneLayout` -> `paintLayout`) with the motion rail as a first-class layout segment (reading order left -> rail -> right preserved; under width pressure the right lane yields first, the rail later). The legacy v1 three-lane renderer (`src/signal/render.ts`) is deleted; rail semantics live in `src/render/motion-rail.ts`. Deliberate visible delta: the configured separator now joins every primary segment. Golden pinned post-cutover in `tests/signal-golden.test.ts` (pre-cutover baseline preserved in git history at 84b2e2a).
9
+ - Bash v2: PTY session core (`bash-mode/pty-session.ts`, SGR-safe ANSI filter, sentinel-based command boundary) plus the long-lived `PtyManagedShellSession` cutover: `session-factory.ts` routes `auto`/`v2` through the PTY-backed session and the editor forwards printable input to the running command's stdin while it runs (interactive programs work; Ctrl-C stays an interrupt). When `script(1)` is missing, commands degrade per-run to plain pipes with a one-time warning. The legacy pipe-based `ManagedShellSession` is deleted.
10
+ - Skill Studio modules: shell + state machine + `/studio` command, list/inspect/actions (create from template, overwrite confirm, doctor), DeepWiki client with 7-day disk cache (`src/studio/deepwiki/`), AI advice engine with pi-ai streaming and char-capped context (`src/studio/advise/`), and an advice pane that streams + inserts into the session. Operator exposure remains deferred and fail-closed until the Studio panes are connected.
11
+ - Tests: 704 passing across new and existing suites; full `typecheck`, `madge --circular`, and `npm test` green.
12
+
13
+ ### Deferred
14
+ - Editor live-tail and v1 session cleanup.
15
+ - PTY echo/privacy.
16
+ - Paste while running.
17
+ - Managed-suite pipe-mode safeguards.
18
+ - Studio operator exposure until its panes are connected.
19
+ - Deck left-rail navigation bug (arrow-down skipping straight to the skills list).
20
+
21
+ ## [1.5.1] - 2026-08-27
22
+
23
+ ### Added
24
+ - Versioned autoresearch harness (`.auto/measure.sh`, `checks.sh`, `config.json`, `prompt.md`): one-run `METRIC` output for test/typecheck/circular plus signal-render and registry micro-benchmarks. Future autoresearch sessions run against this contract.
25
+
26
+ ## [1.5.0] - 2026-08-27
27
+
5
28
  ## [1.4.1] - 2026-08-27
6
29
 
7
30
  ## [1.4.0] - 2026-08-27
package/README.md CHANGED
@@ -12,12 +12,17 @@
12
12
  # pi-wishcraft
13
13
 
14
14
  Cockpit and harness for the [pi](https://github.com/badlogic/pi-mono) coding agent: a live status bar, overlay menus, skills, an idea inbox, sticky bash, hooks, and tool-input repairs. Stock pi stays the engine. This package is the operator layer.
15
-
16
15
  Kongming lanterns started as battlefield signals and later carried wishes. Wishcraft is that split in a coding session: telemetry on the bar, thoughts you can park without interrupting the run.
17
16
 
18
17
  Install `@groeponline/pi-wishcraft`. It is listed on the [Pi package catalog](https://pi.dev/packages/@groeponline/pi-wishcraft). Grew out of [`nicobailon/pi-powerline-footer`](https://github.com/nicobailon/pi-powerline-footer). Maintained by [GroepOnline](https://github.com/GroepOnline).
19
18
 
20
- Guides live in [`docs/`](docs/index.md). This page is the contract: what ships, how to install it, and what can fail.
19
+ Guides live in [`docs/`](docs/index.md). This is the Pi extension contract: what ships, how to install it, and what can fail.
20
+
21
+ ## Quick start tutorial
22
+
23
+ Install, restart Pi (or run `/reload`), then press `alt+p` for the Deck. Try `/signal menu`, `/skills`, `/ideas`, and `ctrl+shift+b`. Full setup: [docs/configuration.md](docs/configuration.md).
24
+
25
+ ## v2 Platform (in this release)
21
26
 
22
27
  ## Install
23
28
 
@@ -27,13 +32,13 @@ Pi package manager (usual path):
27
32
  pi install npm:@groeponline/pi-wishcraft
28
33
  ```
29
34
 
30
- Ephemeral VMs / CI:
35
+ For a local checkout or CI environment, run the repository bootstrap script with Bash:
31
36
 
32
37
  ```bash
33
- curl -fsSL https://raw.githubusercontent.com/GroepOnline/pi-wishcraft/main/scripts/install.sh | bash
38
+ bash ./scripts/install.sh
34
39
  ```
35
40
 
36
- Then restart pi or `/reload`. Pi host packages are declared as `peerDependencies: "*"`, matching the Pi package contract.
41
+ Then restart Pi or `/reload`. Pi host packages are declared as `peerDependencies: "*"`, matching the Pi package contract.
37
42
 
38
43
  ## What you get
39
44
 
@@ -48,6 +53,8 @@ Then restart pi or `/reload`. Pi host packages are declared as `peerDependencies
48
53
  | Hooks + repairs | Command hooks on pi events. Custom-tool input repairs before execution. Kill-switch: `wishcraft.hooksEnabled`. |
49
54
  | Read hints | Appends a one-line continuation hint after a partial `read`, so the model knows the next offset. Opt-out: `wishcraft.readHints: false`. |
50
55
  | Policy | In-process deny/inject rules in global settings. No spawn. Kill-switch: `wishcraft.policyEnabled`. |
56
+ | Working indicator | Four deterministic styles (`dots`, `pulse`, `bar`, `ascii`) with accessibility-aware static fallbacks. |
57
+ | Welcome art | `lantern`, `balloon`, and `normal` opening artwork, each with a different silhouette and narrow-terminal fallback. |
51
58
 
52
59
  Pi owns the footer chrome, feed scrolling, and input. Wishcraft supplies widgets, overlays, and the bash/stash/editor integrations. The bar is not clickable; actions are commands and overlays.
53
60
 
@@ -126,7 +133,9 @@ At 80% the cost segment warns; at 100% it goes red and welcome notifies. `/usage
126
133
 
127
134
  ## Hooks
128
135
 
129
- Hooks are commands that read JSON on stdin. Definitions come from the **global** agent settings file only. Project `.pi/settings.json` cannot install new hook commands. `wishcraft.hooksEnabled: false` disables every hook without deleting the config.
136
+ Hooks are commands that read JSON on stdin. See [docs/configuration.md](docs/configuration.md) for examples.
137
+
138
+ Definitions come from the **global** agent settings file only. Project `.pi/settings.json` cannot install new hook commands. `wishcraft.hooksEnabled: false` disables every hook without deleting the config.
130
139
 
131
140
  ```json
132
141
  {
@@ -147,7 +156,7 @@ Hooks are commands that read JSON on stdin. Definitions come from the **global**
147
156
  }
148
157
  ```
149
158
 
150
- **bash-guard** (exit 2 = deny):
159
+ **Example hook** (exit 2 = deny):
151
160
 
152
161
  ```bash
153
162
  #!/usr/bin/env bash
@@ -161,7 +170,7 @@ fi
161
170
  exit 0
162
171
  ```
163
172
 
164
- **write-audit** (append-only, never blocks):
173
+ **Another example** (append-only, never blocks):
165
174
 
166
175
  ```bash
167
176
  #!/usr/bin/env bash
@@ -169,7 +178,7 @@ mkdir -p "$HOME/.pi/agent/logs"
169
178
  cat >> "$HOME/.pi/agent/logs/write-audit.jsonl"
170
179
  ```
171
180
 
172
- **SessionStart git-status** (extra context, never blocks):
181
+ **SessionStart example** (extra context, never blocks):
173
182
 
174
183
  ```bash
175
184
  #!/usr/bin/env bash
@@ -188,7 +197,8 @@ Repairs run on custom/extension tools only, before hooks: drop null optionals, p
188
197
 
189
198
  ## Policy
190
199
 
191
- Declarative deny/inject rules in the **global** agent settings file. No shell commands — pure in-process regex. Evaluated before command hooks. `wishcraft.policyEnabled: false` disables policy without deleting rules.
200
+ Declarative deny/inject rules live in the global settings file; see [docs/configuration.md](docs/configuration.md).
201
+ No shell commands — pure in-process regex. Evaluated before command hooks. `wishcraft.policyEnabled: false` disables policy without deleting rules.
192
202
 
193
203
  ```json
194
204
  {
@@ -211,24 +221,18 @@ Declarative deny/inject rules in the **global** agent settings file. No shell co
211
221
  }
212
222
  ```
213
223
 
214
- **deny** — regex on tool input (`bash` uses `command`; other tools use JSON-serialized input). First match wins; the tool call is blocked with `reason`.
215
-
216
- **inject** — regex on file path after a matching tool completes; context is appended to the tool result (same shape as postToolUse hook `additionalContext`).
217
224
 
218
225
  ## Limits
219
226
 
220
227
  - No mouse on the live footer. Pi core owns that surface.
221
228
  - No second `alt+i` product. Ports stay on `alt+i`; other detail is `→` in the navigator.
222
229
  - ChefGroep status keys (`powerline.preset`, `powerline.tps`, `powerline.ports`) exist for other extensions. They are not the public pitch.
223
- - The legacy `@groeponline/pi-powerline-footer` package is deprecated on npm in favor of `@groeponline/pi-wishcraft`; the GitHub fork relationship is retained for upstream history and attribution.
230
+ - The legacy `@groeponline/pi-powerline-footer` package is deprecated in favor of `@groeponline/pi-wishcraft`.
224
231
  - Tags are not rewritten. 0.19.x through current stay on the timeline.
225
232
 
226
233
  ## vNext Direction
227
234
 
228
- Wishcraft is evolving into Pi's animated operator layer intent, skills, ideas, guardrails, and session state made visible and controllable without turning Pi into an IDE.
229
-
230
- - **[vNext Stacked PR Release Plan](docs/design/vnext-release-plan.md)**: Specifications for PR0 through PR8.
231
- - **[Design Corpus](docs/index.md#design-system--vnext-specifications)**: Deck, Signal, Motion Gallery, tokens, and the 10 structural presets. `npm run preview` renders the Deck frames.
235
+ Wishcraft is Pi's animated operator layer. See the [release plan](docs/design/vnext-release-plan.md) and [design corpus](docs/index.md#design-system--vnext-specifications).
232
236
 
233
237
  ## Docs
234
238
 
package/ROADMAP.md CHANGED
@@ -14,11 +14,11 @@ Baseline: **v1.3.1** (`e79737f`, tag `v1.3.1` 2026-08-25, npm `1.3.1`) — `orig
14
14
 
15
15
  ## 1.4 — Extension Contract (current, split)
16
16
 
17
- **1.4a — Settings registry** — *shipped* `0bfa5a2` `feat(config): typed settings registry` on `origin/main`. `src/config/settings-registry.ts` (28 defs, `SETTING_DEFAULTS`, `get/validate` O(1) maps, `SETTING_GROUPS`). `src/extension/settings/wishcraft-config-items.ts` now renders from registry (124 lines removed). Tests `tests/settings-registry.test.ts` + `wishcraft-config.test.ts` green (557). Still TODO: `contributeSettings()` Pi-contribution + `defaults/migrations` + docgen from registry.
17
+ **1.4a — Settings registry** — *partly shipped* `0bfa5a2` `feat(config): typed settings registry` on `origin/main`. `src/config/settings-registry.ts` (28 defs, `SETTING_DEFAULTS`, `get/validate` O(1) maps, `SETTING_GROUPS`). `src/extension/settings/wishcraft-config-items.ts` now renders from registry (124 lines removed). Tests `tests/settings-registry.test.ts` + `wishcraft-config.test.ts` green. Still TODO: `contributeSettings()` Pi-contribution + `defaults/migrations` + docgen from registry.
18
18
 
19
- **1.4b — Contribution API** — *pending* `62d6156` `feat(contract): typed contribution API` on `origin/feat/contribution-api` (not yet on `origin/main`). `src/extension/contrib/{types,registry}.ts` ships `registerDeckRoute`/`registerSignalSource` (validate `id /^[a-z0-9][a-z0-9-_]*$/`, dedup, `false` never throw). Deck wired via `src/extension/ui/deck/routes.ts` `getAllDeckRouteDefs()`/`isDeckRoute()`. Still TODO: `registerMotion()`, `registerAppearanceContribution()`, `registerRecipeOrAction()` as separate slices (one PR per contract).
19
+ **1.4b — Contribution API** — *shipped on main* (1.4 line; `src/extension/contrib/{types,registry,appearance}.ts`, tests `contrib-registry.test.ts` + `appearance-contrib.test.ts`). `registerDeckRoute`/`registerSignalSource`/`registerAppearanceContribution` (validate `id /^[a-z0-9][a-z0-9-_]*$/`, dedup, `false` never throw). Deck wired via `getAllDeckRouteDefs()`/`isDeckRoute()` (calls `getContributedDeckRoutes()`); Signal render calls `getContributedSignalSources()` fault-isolated (render.ts:196-203; a throwing/empty source never takes down Signal). Still TODO: `registerMotion()`, `registerRecipeOrAction()` as separate slices (one PR per contract).
20
20
 
21
- **1.4c — Signal wiring** — *next* after 1.4b merge. `src/signal/render.ts` must call `getContributedSignalSources()` with per-source fault isolation (`try/catch` → `null`), versioned capability scope. Done when both Deck and Signal contributions render without taking down the bar (`tests/contrib-registry.test.ts` + `signal.test.ts` + fault test).
21
+ **1.4c — Signal wiring** — *shipped* (with 1.4b): both Deck and Signal contributions render fault-isolated; covered by `contrib-registry.test.ts` + `appearance-contrib.test.ts`.
22
22
 
23
23
  **1.4d — Preset composer** — Decouple `PresetDef` `tokens/chrome/signal/motion/deck/welcome/glyphs` from `src/config/structural-presets.ts` to allow `contributedAppearance` overrides per preset with validation. Done when `tests/structural-presets.test.ts` + `tokens.test.ts` cover contributed palette/motion.
24
24
 
package/STRATEGY.md ADDED
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: pi-wishcraft
3
+ last_updated: 2026-08-28
4
+ ---
5
+
6
+ # pi-wishcraft Strategy
7
+
8
+ ## Target problem
9
+
10
+ pi-wishcraft today is a bundle of loosely-coupled segments (motion, ports, bash, vibes, skills) — each works on its own, none hangs together. There is no "this is how pi should feel" layer. The crux: wishcraft is not yet a default skin, it is a grab-bag of features.
11
+
12
+ ## Our approach
13
+
14
+ pi-wishcraft is a customizable, feel-good default skin for pi — vibrant, warm, and tunable down to the last spinner. Where a tool must be finished to ship, a skin evolves, so we ship fast and iterate; perfection is the wrong goal. The bet: **vibe + customization depth** — the difference between wishcraft and any other pi extension is not feature count, it is that the skin lives.
15
+
16
+ ## Who it's for
17
+
18
+ **Primary:** someone installing pi on day 1, typing `/wishcraft`, and choosing their first preset within five minutes — without reading the manual. They are hiring pi-wishcraft to make pi feel like their terminal, with essentials (bash, /cd, skills, health) already working on day one.
19
+
20
+ ## Key metrics
21
+
22
+ - **Essentials-reliability** — bash-mode session breaks + ports/health crashes per release, measured via GitHub issues and npm feedback. Regresses the moment the foundation rots.
23
+ - **First-touch customization** — % of fresh installs where a settings-file write happens within 7 days (opt-in local usage file). Stand-in for "felt at home" until real telemetry is in place.
24
+ - **Customization depth** — average number of active theme / segment / preset tweaks per user after 30 days (settings file). Indicates "made it theirs."
25
+
26
+ ## Tracks
27
+
28
+ ### How we work
29
+
30
+ Every track runs the same loop: **observe → root-cause → simplify → ship**. Half-finished work is removed, not shipped. Presets are data, not code, so the community can contribute via PR.
31
+
32
+ ### Skin & feel
33
+
34
+ The v2 cutover landing this week: powerline, welcome, bash, ports, skills, motion as one coherent skin — not a pile of segments. The first preset must work out of the box.
35
+
36
+ _Loop in this track:_ segment crashes and motion stalls are bugs, not features; we trace and remove them before adding more.
37
+
38
+ ### Customization depth
39
+
40
+ Every detail tunable — colour, font, animation, spinner, preset. Deck and (soon) Studio are the two entry points. Presets and segments live as data so users and the community can extend without forking.
41
+
42
+ _Loop in this track:_ presets that no one uses get simplified or removed; a knob nobody touches is dead weight.
43
+
44
+ ### Skill Studio & modes
45
+
46
+ Studio (in progress, U5+) as the in-process workshop for skills, plus extension modes like creator, PTC, minimal, code, verbose. Joins up with deepseek, codex, claude, and oh-my-pi conventions — wishcraft does not ship its own harness.
47
+
48
+ _Loop in this track:_ advice that users ignore is a signal; modes that feel "cool" but unused get cut.
49
+
50
+ ## Not working on
51
+
52
+ - A standalone agents / harness runtime — wishcraft plugs into existing harnesses, never replaces one.
53
+ - Tool-product features that don't make the skin more beautiful or more tunable (analytics dashboards, productivity trackers).
54
+ - Mobile / web / cloud variants — pi is terminal-native; that stays the case.
@@ -11,7 +11,9 @@ export const DEFAULT_EDITOR_BOUNDARY_SHORTCUTS: EditorBoundaryShortcuts = {
11
11
  };
12
12
 
13
13
  export function isPrintableInput(data: string): boolean {
14
- return data.length === 1 && data.charCodeAt(0) >= 32;
14
+ const points = [...data];
15
+ if (points.length !== 1) return false;
16
+ return (points[0]?.codePointAt(0) ?? 0) >= 32;
15
17
  }
16
18
 
17
19
  export function isCommandUndoShortcut(data: string): boolean {
@@ -42,6 +42,7 @@ export class BashModeEditor extends CustomEditor {
42
42
  private ghost: GhostSuggestion | null = null;
43
43
  private ghostAbort: AbortController | null = null;
44
44
  private ghostToken = 0;
45
+ private ghostSchedule: ReturnType<typeof setTimeout> | null = null;
45
46
 
46
47
  constructor(
47
48
  tui: any,
@@ -76,9 +77,26 @@ export class BashModeEditor extends CustomEditor {
76
77
  this.scheduleGhostUpdate();
77
78
  }
78
79
 
80
+ /** Resolve a ghost immediately for callers that need a settled refresh. */
81
+ async refreshGhostSuggestionNow(): Promise<void> {
82
+ const text = this.getExpandedText();
83
+ const currentToken = ++this.ghostToken;
84
+ this.ghostAbort?.abort();
85
+ this.ghostSchedule = null;
86
+
87
+ const controller = new AbortController();
88
+ this.ghostAbort = controller;
89
+ const ghost = await this.optionsRef.resolveGhostSuggestion(text, controller.signal);
90
+ if (controller.signal.aborted || currentToken !== this.ghostToken) return;
91
+ this.ghost = ghost;
92
+ this.tui.requestRender();
93
+ }
94
+
79
95
  clearGhostSuggestion(): void {
80
96
  this.ghostAbort?.abort();
81
97
  this.ghostAbort = null;
98
+ if (this.ghostSchedule) clearTimeout(this.ghostSchedule);
99
+ this.ghostSchedule = null;
82
100
  this.ghost = null;
83
101
  }
84
102
 
@@ -145,6 +163,24 @@ export class BashModeEditor extends CustomEditor {
145
163
  return;
146
164
  }
147
165
 
166
+ // v2 forward-mode: while a command runs, printable input goes to the
167
+ // PTY stdin (AE1). Interrupt stays a keybinding above; key-releases
168
+ // and multi-key sequences stay in the editor. Enter (\r) and EOF
169
+ // (\x04) must also forward — line-oriented stdin programs (read,
170
+ // sudo, git rebase -i) cannot proceed without a line terminator.
171
+ // Opt-in via forwardWhileRunning so v1 run-blocked behavior unchanged.
172
+ if (
173
+ bashMode &&
174
+ this.optionsRef.isShellRunning() &&
175
+ (this.optionsRef.forwardWhileRunning?.() ?? false) &&
176
+ this.optionsRef.onForwardInput != null &&
177
+ !isKeyRelease(data) &&
178
+ (isPrintableInput(data) || data === "\r" || data === "\n" || data === "\x04")
179
+ ) {
180
+ this.optionsRef.onForwardInput(data);
181
+ return;
182
+ }
183
+
148
184
  if (
149
185
  bashMode &&
150
186
  this.keybindingsRef.matches(data, "tui.editor.cursorUp")
@@ -338,23 +374,24 @@ export class BashModeEditor extends CustomEditor {
338
374
  const text = this.getExpandedText();
339
375
  const currentToken = ++this.ghostToken;
340
376
  this.ghostAbort?.abort();
377
+ if (this.ghostSchedule) clearTimeout(this.ghostSchedule);
378
+ this.ghostSchedule = null;
341
379
 
342
380
  const controller = new AbortController();
343
381
  this.ghostAbort = controller;
382
+ // Start immediately so editor actions have deterministic refresh semantics.
383
+ // The abort signal and token still discard stale results when typing races
384
+ // with an in-flight history/filesystem lookup.
344
385
  this.optionsRef
345
386
  .resolveGhostSuggestion(text, controller.signal)
346
387
  .then((ghost) => {
347
- if (controller.signal.aborted || currentToken !== this.ghostToken)
348
- return;
388
+ if (controller.signal.aborted || currentToken !== this.ghostToken) return;
349
389
  this.ghost = ghost;
350
390
  this.tui.requestRender();
351
391
  })
352
392
  .catch((error) => {
353
- if (error instanceof Error && error.message === "aborted") return;
354
- console.debug(
355
- "[wishcraft] Failed to resolve bash ghost suggestion:",
356
- error,
357
- );
393
+ if (controller.signal.aborted || (error instanceof Error && error.message === "aborted")) return;
394
+ console.debug("[wishcraft] Failed to resolve bash ghost suggestion:", error);
358
395
  });
359
396
  }
360
397
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Forward state for bash v2 (U4). Pure reducer: given a ForwardState and
3
+ * raw input data, decide what the PTY session should do (stdin, interrupt,
4
+ * nothing) and what the new state should be. The BashForwardEditor drives
5
+ * this; it has no TUI imports so the unit is fully testable.
6
+ */
7
+
8
+ import { matchesKey } from "@earendil-works/pi-tui";
9
+
10
+ export interface ForwardState {
11
+ running: boolean;
12
+ lastExitCode: number | null;
13
+ cwd: string;
14
+ }
15
+
16
+ export type PtyAction = "stdin" | "interrupt";
17
+
18
+ export interface ForwardDecision {
19
+ state: ForwardState;
20
+ ptyAction?: PtyAction;
21
+ }
22
+
23
+ const DONE_SENTINEL = "__PI_DONE__";
24
+
25
+ export function createForwardState(): ForwardState {
26
+ return { running: false, lastExitCode: null, cwd: "" };
27
+ }
28
+
29
+ function parseSentinel(data: string): { exitCode: number; cwd: string } | null {
30
+ if (!data.startsWith(`${DONE_SENTINEL}:`)) return null;
31
+ const rest = data.slice(DONE_SENTINEL.length + 1);
32
+ const colon = rest.indexOf(":");
33
+ if (colon === -1) return null;
34
+ const exitCode = Number.parseInt(rest.slice(0, colon), 10);
35
+ const cwd = rest.slice(colon + 1);
36
+ if (!Number.isFinite(exitCode)) return null;
37
+ return { exitCode, cwd };
38
+ }
39
+
40
+ export function handleForwardInput(
41
+ state: ForwardState,
42
+ data: string,
43
+ ): ForwardDecision {
44
+ if (data.length === 0) return { state };
45
+
46
+ const sentinel = parseSentinel(data);
47
+ if (sentinel) {
48
+ return {
49
+ state: {
50
+ running: false,
51
+ lastExitCode: sentinel.exitCode,
52
+ cwd: sentinel.cwd,
53
+ },
54
+ };
55
+ }
56
+
57
+ if (!state.running) return { state };
58
+
59
+ if (matchesKey(data, "ctrl+c") || data === "\x03") {
60
+ return { state, ptyAction: "interrupt" };
61
+ }
62
+
63
+ return { state, ptyAction: "stdin" };
64
+ }