gentle-pi 3.3.0 → 3.5.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/README.md +88 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/bin/gentle-shell.mjs +198 -0
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-agents-activity.md +95 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +99 -6
- package/extensions/ask-user-choice.ts +70 -22
- package/extensions/ask-user-question.ts +338 -0
- package/extensions/gentle-agents.ts +41 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-rpc-publisher.ts +342 -0
- package/lib/agents-runner.ts +7 -2
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/gentle-shell-launcher.ts +482 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/rpc-host.ts +36 -0
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +5 -1
- package/runtime/gentle-shell-launcher.mjs +483 -0
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +14 -7
- package/scripts/install-tui-mode-setting.mjs +78 -1
- package/scripts/verify-package-files.mjs +6 -3
- package/tests/agents-rpc-publisher.test.ts +407 -0
- package/tests/agents-runner.test.ts +10 -0
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-choice.test.ts +129 -0
- package/tests/ask-user-question.test.ts +661 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +161 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell-bin.test.ts +188 -0
- package/tests/gentle-shell-launcher.test.ts +718 -0
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/install-tui-mode-guard.test.ts +99 -0
- package/tests/install-tui-mode-setting.test.ts +39 -1
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/rpc-host.test.ts +77 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Gentle Agents activity schema (`gentle-agents.activity/v1`)
|
|
2
|
+
|
|
3
|
+
An interactive RPC host — a client that runs `pi --mode rpc` itself, such as the Gentle Shell desktop app — receives live Gentle Agents subagent state as one bounded JSON document per coalescing window, so it can render a per-chat Helpers view without polling `subagent_status`.
|
|
4
|
+
|
|
5
|
+
Source map: [publisher](../lib/agents-rpc-publisher.ts), [wiring](../extensions/gentle-agents.ts), [store](../lib/agents-protocol.ts).
|
|
6
|
+
|
|
7
|
+
## Turning it on
|
|
8
|
+
|
|
9
|
+
Set `GENTLE_SHELL_INTERACTIVE_HOST=1` on the `pi --mode rpc` process the host spawns directly. `lib/rpc-host.ts`'s `isInteractiveRpcHost(mode, env)` gates the feature on that exact value; any other value, or its absence, keeps RPC headless — the existing subagent-child behavior is byte-identical. `lib/agents-runner.ts` strips the variable from every subagent child's environment, so a subagent spawned by an interactive host never inherits it and stays headless itself.
|
|
10
|
+
|
|
11
|
+
## Transport
|
|
12
|
+
|
|
13
|
+
Pi's `setWidget` is the only fire-and-forget RPC push structured enough to carry this: in RPC mode it accepts a `string[]` (sent as `extension_ui_request`) and silently ignores a component-factory function (the shape the TUI card above the editor uses). The publisher and the TUI card therefore share one widget key without colliding on the wire — a plain RPC host or a TUI session only ever sees the factory call, which its own transport ignores or renders locally.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"type": "extension_ui_request",
|
|
18
|
+
"method": "setWidget",
|
|
19
|
+
"widgetKey": "gentle-agents",
|
|
20
|
+
"widgetLines": ["{\"schema\":\"gentle-agents.activity/v1\", ...}"]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`widgetLines` is always exactly one line: one JSON document, `JSON.stringify`'d, never pretty-printed. Parse it as `gentle-agents.activity/v1`.
|
|
25
|
+
|
|
26
|
+
## Payload shape
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"schema": "gentle-agents.activity/v1",
|
|
31
|
+
"summary": { "running": 1, "queued": 0, "waiting": 0, "finished": 2 },
|
|
32
|
+
"tasks": [
|
|
33
|
+
{
|
|
34
|
+
"summary": {
|
|
35
|
+
"id": "t_abc123",
|
|
36
|
+
"agent": "explore",
|
|
37
|
+
"label": "Map the auth module",
|
|
38
|
+
"prompt": "Explore how authentication works…",
|
|
39
|
+
"status": "running",
|
|
40
|
+
"createdAt": 1732000000000,
|
|
41
|
+
"startedAt": 1732000000100,
|
|
42
|
+
"endedAt": null,
|
|
43
|
+
"lastStep": "reading lib/auth.ts",
|
|
44
|
+
"lastActivityAt": 1732000005000,
|
|
45
|
+
"turns": 2,
|
|
46
|
+
"toolCalls": 3,
|
|
47
|
+
"error": null
|
|
48
|
+
},
|
|
49
|
+
"thread": {
|
|
50
|
+
"version": 7,
|
|
51
|
+
"dropped": 0,
|
|
52
|
+
"items": [
|
|
53
|
+
{ "kind": "text", "text": "Looking at the auth flow first." },
|
|
54
|
+
{ "kind": "tool", "name": "read", "args": "{\"path\":\"lib/auth.ts\"}", "running": false, "isError": false, "output": "…file contents…" }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`summary` is `TaskSummary` from `lib/agents-protocol.ts`, unchanged. Each task's `summary` is a field whitelist of its `TaskRecord`: `id`, `agent`, `label`, `prompt`, `status`, `createdAt`, `startedAt`, `endedAt`, `lastStep`, `lastActivityAt`, `turns`, `toolCalls`, `error`. Every other `TaskRecord` field — `cwd`, `parentSessionId`, `mode`, `model`, `thinking`, `sessionPath`, `result`, `tokens`, `cost` — is deliberately left out, the same discipline `lib/orchestrator-presence.ts`'s `projectActivity` already applies to same-profile peer discovery.
|
|
63
|
+
|
|
64
|
+
`thread.items` is a `ThreadItem[]` whitelist too: text/thinking/note items keep `{ kind, text }` (`text` bounded, see below); tool items carry `{ kind: "tool", name, args, running, isError, output }`, where `args` is the tool's argument object `JSON.stringify`'d (never the raw object). `thread.dropped` is the store's own ring-buffer drop counter (unrelated to the per-push item cap below); `thread.version` increments on every thread mutation.
|
|
65
|
+
|
|
66
|
+
Tasks are ordered `running`, `waiting`, `queued`, then finished tasks by `endedAt` descending (most recently finished first).
|
|
67
|
+
|
|
68
|
+
## Bounds
|
|
69
|
+
|
|
70
|
+
Every bound below fails closed: a value that cannot fit is truncated or dropped, and `lib/agents-rpc-publisher.ts`'s `encodeActivityLines` never throws.
|
|
71
|
+
|
|
72
|
+
| Field | Bound |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `summary.prompt` | 200 characters, trailing `…` |
|
|
75
|
+
| `summary.error`, `summary.label`, `summary.lastStep` | 500 characters, trailing `…` |
|
|
76
|
+
| tool `args` (stringified) | 500 characters, trailing `…` |
|
|
77
|
+
| tool `output` | 500 characters, trailing `…` |
|
|
78
|
+
| text/thinking/note item `text` | 2000 characters, trailing `…` |
|
|
79
|
+
| `thread.items` per task | last 40, most recent last |
|
|
80
|
+
| whole payload | 256 KiB |
|
|
81
|
+
|
|
82
|
+
Truncation always keeps the field's prefix and marks the cut with a trailing `…` (never a separate `truncated` flag) — the same convention `projectRpcActivity`'s other bounded fields already use.
|
|
83
|
+
|
|
84
|
+
When the whole-payload bound is still exceeded after the field- and item-level truncations above, `encodeActivityLines` shrinks the payload in this order:
|
|
85
|
+
|
|
86
|
+
1. Halve every task's kept `thread.items` (repeatedly, down to one item each).
|
|
87
|
+
2. Empty finished tasks' threads entirely.
|
|
88
|
+
3. Drop whole finished tasks — oldest-finished first, by `endedAt`.
|
|
89
|
+
4. Last resort: once only active (running/waiting/queued) tasks remain, each already down to one thread item, empty every remaining task's thread too — a summary-only payload.
|
|
90
|
+
|
|
91
|
+
An active task's `summary` (running, waiting or queued) is never dropped; only its `thread.items` shrink. Finished tasks can be dropped whole by step 3, oldest first.
|
|
92
|
+
|
|
93
|
+
## Coalescing
|
|
94
|
+
|
|
95
|
+
`createRpcActivityPublisher` subscribes to `TaskStore#subscribeSummary` (task added, removed, or changed status) and to `TaskStore#subscribe(id)` for every known task, including ones added after `start()`. Changes inside a 150 ms window collapse into exactly one `setWidget("gentle-agents", [line])` call; `stop()` tears down every subscription and publishes one final frame.
|
package/docs/gentle-shell.md
CHANGED
|
@@ -15,7 +15,7 @@ The [v2.6.0 release](https://github.com/Gentleman-Programming/gentle-pi/releases
|
|
|
15
15
|
- The Agents List and Details views preserve the orchestrator/session hierarchy and completion, abort, and lost-exit history. Parent-child queries and notifications have an explicit handoff path, while model, effort, and usage stay observable per task.
|
|
16
16
|
- Named `/gentle:profiles` atomically route the orchestrator separately from packaged and review roles; see the [technical reference](readme-reference.md#agent-model-profiles) for the profile model.
|
|
17
17
|
|
|
18
|
-
The source checkout currently prepares `gentle-pi` `3.
|
|
18
|
+
The source checkout currently prepares `gentle-pi` `3.4.0` with a package-local Gentle AI `v3.5.0` pin; this is not a claim that `3.4.0` is published.
|
|
19
19
|
|
|
20
20
|
## Shell interactions and runtime behavior
|
|
21
21
|
|
|
@@ -118,6 +118,20 @@ The panel rows a provider reports its windows with:
|
|
|
118
118
|
- For Codex, usage comes from the same account usage endpoint the Codex CLI reads, using the OAuth token pi already holds. It is fetched at session start, at most every 5 minutes after a turn, and on `r` in the panel. Rate-limit headers on SSE responses are picked up too.
|
|
119
119
|
- For Claude Pro/Max, usage arrives in the rate-limit headers of every response, so the 5h and weekly windows appear after the first turn.
|
|
120
120
|
- For NaN Cloud, usage comes from the quota endpoint the official dashboard reads, with the same API key pi already holds. Each metered model reports one allowance for the billing period, and that window carries no label: the model id names it in the bar and the reset text says what it is in the panel. A model that also reports a rolling window shows that one labeled next to it (`4h`), which today's payload does not send; percentages are tokens used over the allowance, exactly as the dashboard draws them, and the allowance is the full-period cap (`fullCap`) whenever the model reports a positive one, because `cap` alone is the prorated allowance of the period in progress. It is fetched under the same 5-minute rule as Codex, counted per provider so a switch fetches the provider it switched to, refuses redirects so the bearer cannot be replayed to another origin, and keeps no cached copy. The endpoint sits outside NaN's published OpenAPI, so the parser reads it defensively: a model that reports no allowance is skipped, as the dashboard skips it, while a metered model whose usage cannot be read fails the whole read, so a partial payload never replaces a complete snapshot with a cheaper-looking one. A session that already has a snapshot keeps the last valid one through a malformed payload or a failed fetch, and the pending note appears only while there is nothing to draw.
|
|
121
|
+
- Extensions can register a usage source for their own provider: gentle-shell has no built-in knowledge of it, but treats it exactly like Codex or NaN once registered. Emit `gentle-pi:usage-source/v1` on `pi.events` with `{ schema: "gentle-pi.usage-source/v1", provider, pendingNote?, fetch(apiKey, fetchFn, now) }`, where `fetch` resolves a `ProviderUsage` the same shape the built-in providers produce, or `undefined` when there is nothing to show yet. A malformed payload, a `fetch` that isn't a function, a provider id outside the safe id pattern, or a `fetch` call that throws or rejects is ignored rather than crashing the shell. Re-registering the same provider replaces its source, so emitting again at every `session_start` is safe and keeps load order irrelevant. Once registered, the provider shows `pendingNote` (or the same "no usage yet · r to fetch" default the built-ins use) until its first fetch, and a registration that arrives after the session already started, for the provider currently active, triggers one immediate refresh instead of waiting for the next turn or the 5-minute window. Example, using a neutral provider id:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
pi.events.emit("gentle-pi:usage-source/v1", {
|
|
125
|
+
schema: "gentle-pi.usage-source/v1",
|
|
126
|
+
provider: "acme-cloud",
|
|
127
|
+
fetch: async (apiKey, fetchFn, now) => {
|
|
128
|
+
if (!apiKey) return undefined;
|
|
129
|
+
const response = await fetchFn("https://acme.example/usage", { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
130
|
+
if (!response.ok) return undefined;
|
|
131
|
+
return { provider: "acme-cloud", plan: "Acme · 42 credits", limits: [], fetchedAt: now };
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
121
135
|
- The bar names the subscription it shows (`codex`, `claude`, a NaN model) and always follows the active model. A provider with per-model allowances draws the session model's own meter, falling back to its family and then to the account total, never to whichever model the payload happens to list first — and that holds for a payload that reports a single metered model too, because one allowance is still per-model data rather than a reason to echo the first entry. The panel puts the active provider first, marked with the petal, and says why it has no data when it does not: API-key providers have no subscription windows, Claude reports after the first response, Codex and NaN wait for a fetch. A provider without per-model allowances keeps its single aggregate line in the sidebar, unchanged.
|
|
122
136
|
- A provider with per-model allowances is ordered by family on both surfaces: a family stays together, the family that consumes most comes first, and the models inside it follow the same rule, most used first. There are no `total` rows anywhere — an aggregate nobody can act on only costs space — so the account and family totals survive only as the bar's fallback name when the session model holds no allowance of its own (`nan total`). An allowance row leaves the window label empty and prints `name meter percent`, while a labeled sub-window (`4h`) keeps its column, and the reset a window reports rides that same line after a `·`; a window without one ends at its percentage, never on a dangling separator. The sidebar's Usage group prints those same rows in that same order, so the breakdown does not require opening the panel, and stops at the percentage: the reset dates stay in the panel. A row whose windows all round to `0%` is dropped from that group — an allowance nobody has touched yet tells the reader nothing the missing row does not — and the same rule retires the aggregate line of a provider without raw allowances once every window it shows sits at `0%`; the bar and the panel keep printing it, so a zeroed subscription is still verifiable there.
|
|
123
137
|
- Only the plan name and the windows are kept; account details in the payload are discarded.
|
|
@@ -136,6 +150,16 @@ Gentle notices are drawn as cards: the same rounded frame as the prompt. An info
|
|
|
136
150
|
- An active dev-binary override shows above the editor at startup, in amber, naming the binary and its digest, and leaves with the first prompt; an invalid override shows in red with the reason.
|
|
137
151
|
- Subagents draw their own card; see Gentle Agents below.
|
|
138
152
|
|
|
153
|
+
### Native interactive tools
|
|
154
|
+
|
|
155
|
+
Gentle Shell ships its own interactive tools instead of depending on third-party extensions; the built-ins replace `npm:pi-subagents-j0k3r` and `npm:@juicesharp/rpiv-todo` (see Gentle Agents and Gentle Todo below for the removal steps).
|
|
156
|
+
|
|
157
|
+
- **`ask_user_question`** — one to four structured questions in a single questionnaire, each with two to four options, multi-select, per-option descriptions and previews — rendered as real TUI dialogs, usable in the live session.
|
|
158
|
+
- **`ask_user_choice`** — one exactly representable single-select question, with an opt-in free-text response.
|
|
159
|
+
- **`todo`** — plan tracking with the Gentle Todo card (see Gentle Todo below).
|
|
160
|
+
- **`gentle_review` / capture tools** — the native review surface for receipt-driven development.
|
|
161
|
+
- **Optional companions** (separately installed, never bundled): `gentle-engram` for persistent memory, `pi-web-access` for web access when a task needs it and your policy allows it, `pi-lens` for additional inspection surfaces, `pi-intercom` for cross-session communication where your Pi setup supports it, and `@juicesharp/rpiv-ask-user-question` for interactive choice support where a separately installed extension fits your setup. These are companions, not hidden prerequisites or a claim that every Pi installation has every capability; persistent memory is **not** bundled with `gentle-pi`.
|
|
162
|
+
|
|
139
163
|
### Gentle Agents
|
|
140
164
|
|
|
141
165
|
The current package requires Pi 0.85.1 or newer (development tests pin 0.85.1). Use the latest Pi release; gentle-pi does not update your installed Pi automatically. Children, including any `GENTLE_PI_AGENTS_PI` override, must emit `agent_settled`: `agent_end` records a run's output but is not completion because retries or queued continuations may follow.
|
|
@@ -156,7 +180,7 @@ The card is above the editor in every mode, including fullscreen — it is not o
|
|
|
156
180
|
Every subagent is its own `pi --mode rpc` child process, so the terminal never runs subagent work: the host reads JSON lines, applies each one as a small delta to a bounded per-task thread, and notifies only the listeners of that task. A task-mode child's question (`ctx.ui.select`, `confirm`, `input`, `editor`) reaches you as an ordinary pi dialog; a background child's question is dismissed. Subagents have no automatic total execution timeout: a long-running child remains live while it continues emitting RPC events. A silent child still times out through the configurable `stall_timeout_ms` watchdog (default four minutes). An announced tool call that is still running is live work, not silence, so it is bounded by `tool_stall_timeout_ms` instead (default 30 minutes, never below `stall_timeout_ms`). Closing pi stops the children that are still running.
|
|
157
181
|
|
|
158
182
|
- `subagent_list_agents`, `subagent_run` (`agent`, `task`, `label?`, `context?`, `workspace_root?`, `mode?` task or background), `subagent_status`, `subagent_result`, `subagent_list_tasks`, `subagent_reply` (one current-session reply to a live child query), `subagent_cancel`, `subagent_send_message` (steer a running child), `subagent_continue` (resume a finished task in its own session).
|
|
159
|
-
- `orchestrator_session_id`, `orchestrator_list`, and `orchestrator_send_message` provide local-profile session notifications. List results advertise IDs only and reachability remains unknown. Sending selects the sole advertised peer or asks the user to choose; a successful ACK means the peer accepted the notification for delivery, not that it read or completed work. This is notification-and-ACK transport only: it has no cross-session queries, offline queue, retries, broadcasts, or read/completion guarantees. On Unix, presence records remain in the profile's private transport directory while socket endpoints use a private, profile-hashed directory below the canonical system temporary directory, keeping endpoint length independent of the profile path and at most 100 encoded bytes. The shared system temporary parent is only validated (current-user-owned without group/other write, or root/current-user-owned, world-writable, and sticky); it is never claimed, permissioned, or cleaned up by gentle-pi. On POSIX, the transport uses private Unix-domain sockets; on Windows, it uses private named pipes scoped by the current account SID. Notification and ACK limits remain bounded across platforms.
|
|
183
|
+
- `orchestrator_session_id`, `orchestrator_list`, and `orchestrator_send_message` provide local-profile session notifications. List results advertise IDs only and reachability remains unknown. Sending selects the sole advertised peer or asks the user to choose; a successful ACK means the peer accepted the notification for delivery, not that it read or completed work. This is notification-and-ACK transport only: it has no cross-session queries, offline queue, retries, broadcasts, or read/completion guarantees. On Unix, presence records remain in the profile's private transport directory while socket endpoints use a private, profile-hashed directory below the canonical system temporary directory, keeping endpoint length independent of the profile path and at most 100 encoded bytes. The shared system temporary parent is only validated (current-user-owned without group/other write, or root/current-user-owned, world-writable, and sticky); it is never claimed, permissioned, or cleaned up by gentle-pi. On POSIX, the transport uses private Unix-domain sockets; on Windows, it uses private named pipes scoped by the current account SID, served by a package-local PowerShell helper (`runtime/windows-session-transport.ps1`): the transport selects that fixed helper, and availability and delivery depend on the helper's bounded startup and pipe checks. Notification and ACK limits remain bounded across platforms.
|
|
160
184
|
- `subagent_run.workspace_root` selects an existing worktree in the session's Git clone. Validation happens before queueing; the child runs at that canonical root. Successful OS spawn registers the root in the originating parent session, including delayed queued launches, even without an active shell listener. Failed spawns do not register. `subagent_continue` retains the previous task's cwd; status and task details expose it.
|
|
161
185
|
- Background work requires a live interactive/RPC parent. Both `subagent_run` and `subagent_continue` reject background mode in `pi -p` before creating or spawning a task: the parent exits before it can receive a later result. Use task mode for bounded print-mode work.
|
|
162
186
|
- A background task's result comes back to the model as a `gentle-agents.result` message, drawn as a rose card, and starts a new turn when the agent is idle; the model never polls.
|
package/docs/readme-reference.md
CHANGED
|
@@ -106,7 +106,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
|
|
|
106
106
|
| **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
|
|
107
107
|
| **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
|
|
108
108
|
| **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
|
|
109
|
-
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.
|
|
109
|
+
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.5.0 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
|
|
110
110
|
| **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
|
|
111
111
|
|
|
112
112
|
## Native pointer regions
|
|
@@ -143,7 +143,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
|
|
|
143
143
|
|
|
144
144
|
### Source checkout
|
|
145
145
|
|
|
146
|
-
This checkout prepares `gentle-pi` `3.
|
|
146
|
+
This checkout prepares `gentle-pi` `3.4.0`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.5.0`, distinct from the published `v2.6.0` pairing.
|
|
147
147
|
|
|
148
148
|
The native SDD status consumer accepts both the pinned producer's legacy
|
|
149
149
|
`apply`/`verify`/`remediate`/`archive` instruction record and the classical
|
|
@@ -154,7 +154,7 @@ Unknown or incomplete instruction records still fail closed.
|
|
|
154
154
|
The Pi runtime now uses native status exclusively for SDD and retires standalone
|
|
155
155
|
sync. The full chain follows completed apply to archive, where applicable delta
|
|
156
156
|
specs are composed; verification remains explicitly invokable. With the current
|
|
157
|
-
3.
|
|
157
|
+
3.5.0 pin, native still requires verification and its emitted evidence requirements;
|
|
158
158
|
a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
|
|
159
159
|
those exact instructions without overriding readiness or inventing legacy evidence.
|
|
160
160
|
Classical direct-archive behavior is compatibility-tested with an identified
|
|
@@ -188,7 +188,7 @@ pi install npm:gentle-pi@2.6.0
|
|
|
188
188
|
|
|
189
189
|
RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
|
|
190
190
|
|
|
191
|
-
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.
|
|
191
|
+
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.5.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.5.0` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
|
|
192
192
|
|
|
193
193
|
Recommended companion packages:
|
|
194
194
|
|
|
@@ -225,6 +225,73 @@ An orphan branch with commits and no parent has no branch point to name as `base
|
|
|
225
225
|
- Create an empty root commit to open the branch: `git commit --allow-empty -m "chore: open the feature branch"`. The next commit can then use that root commit as its `baseRef`.
|
|
226
226
|
- Omit `baseRef` while the branch is still unborn (no commits yet); the review uses Git's empty tree as the base automatically.
|
|
227
227
|
|
|
228
|
+
## gentle-shell launcher
|
|
229
|
+
|
|
230
|
+
`gentle-shell` (installed by `npm i -g gentle-pi`, exposed as the package's `bin`) opens pi with the Gentle Shell package loaded, without installing it into your pi agent or touching its `settings.json`. It is a thin `bin/gentle-shell.mjs` wrapper around the pure, unit-tested `lib/gentle-shell-launcher.ts` (built to `runtime/gentle-shell-launcher.mjs`); the wrapper owns process, filesystem, and child-process wiring only.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
gentle-shell [options] [-- pi-args...]
|
|
234
|
+
gentle-shell home [link|isolated|<path>]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Flags
|
|
238
|
+
|
|
239
|
+
| Flag | Effect |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `--link` | Home is `PI_CODING_AGENT_DIR` or `~/.pi/agent`. Reuses your existing pi sign-ins, models, and chats; never writes to its `settings.json`. |
|
|
242
|
+
| `--isolated` | Home is `GENTLE_SHELL_HOME` or `~/.gentle-shell/agent`. No credential seeding. Default when nothing else is configured. |
|
|
243
|
+
| `--home <path>` | Home is the given directory. |
|
|
244
|
+
| `--help`, `-h` | Print usage (flags, commands, env vars) and exit 0. |
|
|
245
|
+
| `--version` | Print `gentle-shell <version>`, `pi <version>`, and `home <mode> <dir>`, then exit 0. |
|
|
246
|
+
| `--` | Everything after is forwarded to pi verbatim, even text that looks like a `gentle-shell` flag. |
|
|
247
|
+
|
|
248
|
+
`--link`, `--isolated`, and `--home` are mutually exclusive; combining two is a usage error, as is `--home` or `--home=` with an empty value. Effective-home precedence: an explicit flag wins, then the persisted `home` subcommand choice, then the `--isolated` default. Every argument gentle-shell does not recognize — `--mode rpc`, `-p "..."`, etc. — is forwarded to pi unchanged.
|
|
249
|
+
|
|
250
|
+
### `home` subcommand and `~/.gentle-shell/config.json`
|
|
251
|
+
|
|
252
|
+
`gentle-shell home` alone prints the effective mode and directory (`<mode> <dir>`) without persisting anything. `gentle-shell home link`, `gentle-shell home isolated`, or `gentle-shell home <path>` persists that choice to `~/.gentle-shell/config.json` as `{"home": "link" | "isolated" | "<path>"}`, so a later plain `gentle-shell` picks it up; a flag on a given invocation still overrides the persisted config without rewriting it.
|
|
253
|
+
|
|
254
|
+
### Managing packages
|
|
255
|
+
|
|
256
|
+
`gentle-shell install npm:<pkg>`, `gentle-shell remove ...`, `gentle-shell list`, `gentle-shell update ...`, `gentle-shell config`, and `gentle-shell auth ...` run pi's own commands against the resolved home — the `--isolated` home by default, or your own pi home with `--link`. A launcher flag before the subcommand (`--link`, `--isolated`, `--home <path>`) still selects which home the subcommand runs against. Running `gentle-shell install npm:gentle-pi` inside the isolated home is unnecessary: the launcher already loads the Gentle Shell package itself (see "Loading the package" below).
|
|
257
|
+
|
|
258
|
+
### pi runtime resolution
|
|
259
|
+
|
|
260
|
+
1. `GENTLE_SHELL_PI` — path to a pi executable, when set to a non-empty value.
|
|
261
|
+
2. The bundled `@earendil-works/pi-coding-agent` resolved next to gentle-pi (`dist/bundle/cli.js`, run with the current `node`), when installed as its optional peer dependency.
|
|
262
|
+
3. `pi` on `PATH`.
|
|
263
|
+
|
|
264
|
+
If none resolve, `gentle-shell` exits 1 naming all three options. Once a runtime is found, its `pi --version` must be at least `0.85.1` (the pinned peer minimum): an older version exits 1 naming the found and required versions, and unparsable `--version` output exits 1 naming the required minimum.
|
|
265
|
+
|
|
266
|
+
### Environment variables
|
|
267
|
+
|
|
268
|
+
| Variable | Effect |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| `GENTLE_SHELL_PI` | Overrides pi runtime resolution (see above). |
|
|
271
|
+
| `GENTLE_SHELL_HOME` | Overrides the isolated home directory (default `~/.gentle-shell/agent`). |
|
|
272
|
+
| `PI_CODING_AGENT_DIR` | Read to resolve the `--link` home; also set on the pi child process to the effective home. |
|
|
273
|
+
| `GENTLE_PI_AGENT_HOME` | Set on the pi child process to the effective home; gentle-pi's own home resolution reads it back. |
|
|
274
|
+
|
|
275
|
+
### Loading the package
|
|
276
|
+
|
|
277
|
+
Unless the target home's `settings.json` already lists `npm:gentle-pi` in its `packages` array (checked only for `--link`), every invocation injects `-e <package root> --theme <root>/themes --skill <root>/skills --prompt-template <root>/prompts` ahead of the forwarded arguments, so the Gentle Shell extensions, themes, skills, and prompt templates load without a separate `pi install`. Isolated and `--home <path>` homes never declare the package, so they always get the injection — except when the forwarded arguments start with one of pi's own subcommands (`install`, `remove`, `uninstall`, `update`, `list`, `config`, `auth`): pi dispatches those on `argv[0]` before it parses any flags, so the injection is skipped entirely and pi sees the bare subcommand, e.g. `gentle-shell install npm:x` runs exactly `pi install npm:x`.
|
|
278
|
+
|
|
279
|
+
### First run in an isolated or custom home
|
|
280
|
+
|
|
281
|
+
The first time `gentle-shell` resolves to an isolated or `--home <path>` home that does not already exist, it creates the directory, writes `"tuiMode": "fullscreen"` into its `settings.json`, and prints one hint to stderr pointing at `--link`. A `--link` home is never bootstrapped this way — it is assumed to already exist as your pi agent home. Later runs against the same home skip both the write and the hint.
|
|
282
|
+
|
|
283
|
+
### Windows shims
|
|
284
|
+
|
|
285
|
+
On win32, when the resolved pi command ends in `.cmd` or `.bat` — the shape an npm-installed `pi` or a `GENTLE_SHELL_PI` override commonly takes — `gentle-shell` runs it through `cmd.exe` as one quoted command line instead of spawning it directly, because current Node releases refuse to spawn a batch file without `shell: true`. This applies to both the version probe and the real launch.
|
|
286
|
+
|
|
287
|
+
### Postinstall fullscreen guard
|
|
288
|
+
|
|
289
|
+
gentle-pi's postinstall only writes the global `tuiMode: fullscreen` setting when the running package directory is a pi-managed install: under an `npm/node_modules` segment, or the exact `git/github.com/Gentleman-Programming` Git layout. `npm i -g gentle-pi`, a development checkout, and other layouts are recognized and skipped, logging `gentle-pi skipped enabling fullscreen in global Pi settings: <dir> is not a pi-managed install (npm install -g, a git checkout, and npx all land here).`
|
|
290
|
+
|
|
291
|
+
### Interactive RPC hosts
|
|
292
|
+
|
|
293
|
+
Setting `GENTLE_SHELL_INTERACTIVE_HOST=1` on a `pi --mode rpc` process turns on two things a plain headless RPC host does not get: dialogs for `ask_user_question` and `ask_user_choice` (one `ctx.ui.select` prompt per question, looped for multiSelect), and Gentle Agents' helper activity pushed live through `setWidget`. A subagent child spawned by such a host never inherits the variable, so nested children stay headless regardless of their parent. See the [activity payload reference](gentle-agents-activity.md) for the exact schema, field bounds, and shrink order.
|
|
294
|
+
|
|
228
295
|
## Quick start
|
|
229
296
|
|
|
230
297
|
```text
|
|
@@ -237,6 +304,7 @@ An orphan branch with commits and no parent has no branch point to name as `base
|
|
|
237
304
|
/gentle:persona Switch between gentleman and neutral persona modes.
|
|
238
305
|
/gentle:background-subagents Show or set the managed background-subagents policy, with its deciding source.
|
|
239
306
|
/gentle:review-mode Show or set the receipt-driven development mode (status|enable|disable).
|
|
307
|
+
/gentle:animations Show or set global animations: quality, performance, or potato.
|
|
240
308
|
/gentle:banner Configure startup rose, text logo, and color preset.
|
|
241
309
|
```
|
|
242
310
|
|
|
@@ -362,13 +430,13 @@ flowchart TD
|
|
|
362
430
|
|
|
363
431
|
VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
|
|
364
432
|
|
|
365
|
-
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.
|
|
433
|
+
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.5.0 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
|
|
366
434
|
|
|
367
435
|
Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
|
|
368
436
|
|
|
369
437
|
Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
|
|
370
438
|
|
|
371
|
-
Once the source checkout's pinned gentle-ai runtime (currently v3.
|
|
439
|
+
Once the source checkout's pinned gentle-ai runtime (currently v3.5.0) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
|
|
372
440
|
|
|
373
441
|
### FINALIZE wrapper input
|
|
374
442
|
|
|
@@ -824,6 +892,7 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen
|
|
|
824
892
|
| `/gentle:persona` | Switches global persona mode, with project override support. |
|
|
825
893
|
| `/gentle:background-subagents` | Shows or sets the managed background-subagents policy (`status\|enable\|disable`), naming the source that decided it. |
|
|
826
894
|
| `/gentle:double-esc-cancel` | Shows or sets the double-esc-cancel preference (`status\|enable\|disable`); no argument toggles it. |
|
|
895
|
+
| `/gentle:animations` | Shows or sets global animations (`status\|quality\|performance\|potato`); no argument reports status. |
|
|
827
896
|
| `/gentle:telemetry` | Shows or changes the local Gentle AI telemetry trigger (`status\|enable\|disable\|preview`). |
|
|
828
897
|
| `/gentle:review-mode` | Shows or sets the receipt-driven development mode (`status\|enable\|disable`); user-initiated only, Pi automation never toggles it. |
|
|
829
898
|
| `/gentle:banner` | Configures startup banner rose, text logo, and color preset. |
|
|
@@ -840,6 +909,16 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen
|
|
|
840
909
|
|
|
841
910
|
Startup installs and refreshes only delegation and review assets. SDD assets are installed/refreshed on demand; status and doctor report never-installed SDD assets as informational, while missing or stale assets from an existing installation identify their owner-specific repair command. User and project overrides are reported separately from package drift. Package refresh preserves overrides; explicit saved model settings may still update existing SDD or custom-agent routing at startup.
|
|
842
911
|
|
|
912
|
+
### Native cache warming (Pi 0.86.1+)
|
|
913
|
+
|
|
914
|
+
To allow warming while the parent waits for background results, explicitly set `"cacheWarming": "idle"` in Pi's `settings.json` (user scope: `~/.pi/agent/settings.json`, or project scope: `.pi/settings.json`). Gentle Shell never changes this setting. Native `"streaming"` mode stops when the agent settles: no idle decision is offered for this hook to override. `"off"` remains an opt-out.
|
|
915
|
+
|
|
916
|
+
Pi owns provider cache-lifetime eligibility, safe replay, scheduling, and the fixed 30-minute idle / one-hour streaming horizons. Unknown provider lifetimes do not get inferred. Real provider requests replace Pi's schedule; Gentle Shell adds no timer or maintenance message. Refresh usage stays outside model context. Warming is best-effort, not a guarantee of a future cache hit.
|
|
917
|
+
|
|
918
|
+
Ordinary idle decisions retain Pi's 15% continuation assumption. When the active parent owns queued or running background tasks, Gentle Shell treats continuation probability as 1, but still requires estimated cache-miss savings minus refresh cost to be at least $0.05. Restored, foreign-session, foreground, waiting-for-input, and finished tasks do not strengthen that decision. This only overrides candidates Pi actually offers; it never starts, inspects, polls, steers, or duplicates children.
|
|
919
|
+
|
|
920
|
+
Completion remains push-driven through `gentle-agents.result`. Retain the task ID, end the parent turn when independent work is done, and never sleep or periodically poll status/results to maintain cache or detect completion. Status inspection is for a concrete orchestration decision, not a heartbeat.
|
|
921
|
+
|
|
843
922
|
### Background subagents policy
|
|
844
923
|
|
|
845
924
|
Background delegation requires a live interactive/RPC parent and is rejected in `pi -p`, even when the policy is on. Use task mode for bounded print-mode work.
|
|
@@ -899,6 +978,20 @@ Three sources can decide the policy, and the first hit wins:
|
|
|
899
978
|
|
|
900
979
|
The file uses the strict shape `{"schema":"gentle-pi.double-esc-cancel/v1","policy":"on"}`. A file that is present but malformed fails closed to `off` instead of falling through to the environment variable, and the command reports that case as a warning instead of an ordinary `off`. The extension resolves the policy once at startup and updates it in memory when the command runs; the prompt never re-reads the file on every keypress.
|
|
901
980
|
|
|
981
|
+
### Animation modes
|
|
982
|
+
|
|
983
|
+
Use `/gentle:animations performance` to reduce redraw frequency, or `/gentle:animations potato` to stop Gentle-owned periodic animation. Find **Animation mode** under the command palette's **Configuration** group. `/gentle:animations` and `/gentle:animations status` report the effective mode and deciding source without writing; `/gentle:animations quality` restores the default.
|
|
984
|
+
|
|
985
|
+
| Mode | Working prompt | Startup banner |
|
|
986
|
+
|------|----------------|----------------|
|
|
987
|
+
| `quality` (default) | Existing frames every 80ms | Existing animation every 25ms |
|
|
988
|
+
| `performance` | One animation pulse every 1000ms | One paint every 250ms, advancing 10 logical ticks to retain approximately the original duration |
|
|
989
|
+
| `potato` | Static idle/working/queued state, no animation interval | Completed static artwork immediately, no animation interval |
|
|
990
|
+
|
|
991
|
+
The selection is global: `<configHome>/animations.json`, where `configHome` honors `GENTLE_PI_CONFIG_HOME` and defaults to `~/.pi/gentle-ai`. The strict file shape is `{"schema":"gentle-pi.animations/v1","policy":"quality"}`. There is no project or environment mode override. Missing files use `quality`; malformed or unreadable files also fall back to `quality`, with an attributable warning in status, and are not silently rewritten.
|
|
992
|
+
|
|
993
|
+
A successful command applies to the live prompt immediately, including while working. Starting and settling still request immediate renders. Pi owns enqueue repaint scheduling; Gentle shows the current queued state on the next host render without requiring an animation tick. A running startup banner retains its creation-time policy; the new selection applies at the next banner creation. Operational polling, refresh/debounce timers, Pi core animations, and install-time `tuiMode` are unchanged.
|
|
994
|
+
|
|
902
995
|
Startup banner settings remain global in `banner.json` under `GENTLE_PI_CONFIG_HOME` (default `~/.pi/gentle-ai`). Existing `showRose` and `showTextLogo` opt-outs independently control the main startup artwork; both default to enabled. Changes apply on the next session or `/reload`. Color presets are `pink` (default), `cyan`, `yellow`, and `green`. The static sidebar heading is independent of these preferences and follows the active theme.
|
|
903
996
|
|
|
904
997
|
Startup flag:
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Container, Input, isKeyRelease, matchesKey, Text, type KeybindingsManager, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
4
|
import { type Static, Type } from "typebox";
|
|
5
5
|
import { NativeChoiceList } from "../lib/native-choice-list.ts";
|
|
6
6
|
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
7
|
+
import { isInteractiveMode, isInteractiveRpcHost } from "../lib/rpc-host.ts";
|
|
7
8
|
|
|
8
9
|
const CHOICE_TOOL_NAME = "ask_user_choice";
|
|
9
10
|
const ASK_USER_CHOICE_BLOCKED_EVENT = "gentle-pi:ask-user-choice:blocked";
|
|
@@ -147,11 +148,11 @@ class ChoiceModeView extends Container {
|
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
function reconcileToolAvailability(pi: ExtensionAPI,
|
|
151
|
+
function reconcileToolAvailability(pi: ExtensionAPI, interactive: boolean): void {
|
|
151
152
|
const active = pi.getActiveTools();
|
|
152
153
|
const isActive = active.includes(CHOICE_TOOL_NAME);
|
|
153
|
-
if (
|
|
154
|
-
const next =
|
|
154
|
+
if (interactive === isActive) return;
|
|
155
|
+
const next = interactive
|
|
155
156
|
? [...new Set([...active, CHOICE_TOOL_NAME])]
|
|
156
157
|
: active.filter((name) => name !== CHOICE_TOOL_NAME);
|
|
157
158
|
pi.setActiveTools(next);
|
|
@@ -161,6 +162,57 @@ function resultDetails(params: ChoiceParams): ChoiceDetails {
|
|
|
161
162
|
return { question: params.question, options: params.options };
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
interface ChoiceToolResult {
|
|
166
|
+
content: Array<{ type: "text"; text: string }>;
|
|
167
|
+
details: ChoiceDetails;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Same result shapes for both the TUI and the RPC-dialog fallback. */
|
|
171
|
+
function choiceToolResult(params: ChoiceParams, selection: ChoiceResult | undefined): ChoiceToolResult {
|
|
172
|
+
if (selection === undefined) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: "User cancelled the choice" }],
|
|
175
|
+
details: { ...resultDetails(params), cancelled: true },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if ("customResponse" in selection) {
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: `User responded: ${selection.customResponse}` }],
|
|
181
|
+
details: { ...resultDetails(params), customResponse: selection.customResponse },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: `User selected: ${selection.index}. ${selection.label} (value: ${selection.value})` }],
|
|
186
|
+
details: { ...resultDetails(params), selection },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Label for the opt-in free-text entry appended to the RPC-dialog select options. */
|
|
191
|
+
const OTHER_OPTION_LABEL = "Other…";
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Interactive-RPC-host fallback: one `ctx.ui.select` over the option labels
|
|
195
|
+
* (plus "Other…" when `allowCustomResponse`), then `ctx.ui.input` for the
|
|
196
|
+
* free-text response. Keeps the exact TUI result shapes; a cancel at either
|
|
197
|
+
* step cancels the choice, matching the TUI Escape key.
|
|
198
|
+
*/
|
|
199
|
+
async function askThroughDialogs(
|
|
200
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
201
|
+
params: ChoiceParams,
|
|
202
|
+
): Promise<ChoiceResult | undefined> {
|
|
203
|
+
const labels = params.options.map((choiceOption) => choiceOption.label);
|
|
204
|
+
const dialogOptions = params.allowCustomResponse === true ? [...labels, OTHER_OPTION_LABEL] : labels;
|
|
205
|
+
const picked = await ctx.ui.select(params.question, dialogOptions);
|
|
206
|
+
if (picked === undefined) return undefined;
|
|
207
|
+
if (params.allowCustomResponse === true && picked === OTHER_OPTION_LABEL) {
|
|
208
|
+
const customResponse = await ctx.ui.input(params.question, "Type your response");
|
|
209
|
+
return customResponse === undefined ? undefined : { customResponse };
|
|
210
|
+
}
|
|
211
|
+
const index = params.options.findIndex((choiceOption) => choiceOption.label === picked);
|
|
212
|
+
const option = params.options[index];
|
|
213
|
+
return option === undefined ? undefined : { value: option.value, label: option.label, index: index + 1 };
|
|
214
|
+
}
|
|
215
|
+
|
|
164
216
|
export default function askUserChoice(pi: ExtensionAPI): void {
|
|
165
217
|
pi.registerTool({
|
|
166
218
|
name: CHOICE_TOOL_NAME,
|
|
@@ -175,7 +227,18 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
175
227
|
executionMode: "sequential",
|
|
176
228
|
async execute(_toolCallId, params: ChoiceParams, _signal, _onUpdate, ctx) {
|
|
177
229
|
if (ctx.mode !== "tui") {
|
|
178
|
-
|
|
230
|
+
if (!isInteractiveRpcHost(ctx.mode, process.env)) {
|
|
231
|
+
throw new Error("ask_user_choice is unavailable outside the interactive TUI");
|
|
232
|
+
}
|
|
233
|
+
let rpcSelection: ChoiceResult | undefined;
|
|
234
|
+
try {
|
|
235
|
+
pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: true });
|
|
236
|
+
rpcSelection = await askThroughDialogs(ctx, params);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
|
|
240
|
+
}
|
|
241
|
+
return choiceToolResult(params, rpcSelection);
|
|
179
242
|
}
|
|
180
243
|
|
|
181
244
|
const items = params.options.map((option, index) => ({
|
|
@@ -239,22 +302,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
239
302
|
pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
|
|
240
303
|
}
|
|
241
304
|
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
content: [{ type: "text", text: "User cancelled the choice" }],
|
|
245
|
-
details: { ...resultDetails(params), cancelled: true },
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
if ("customResponse" in selection) {
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: "text", text: `User responded: ${selection.customResponse}` }],
|
|
251
|
-
details: { ...resultDetails(params), customResponse: selection.customResponse },
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
content: [{ type: "text", text: `User selected: ${selection.index}. ${selection.label} (value: ${selection.value})` }],
|
|
256
|
-
details: { ...resultDetails(params), selection },
|
|
257
|
-
};
|
|
305
|
+
return choiceToolResult(params, selection);
|
|
258
306
|
},
|
|
259
307
|
renderCall(args, theme) {
|
|
260
308
|
const options = Array.isArray(args.options) ? args.options : [];
|
|
@@ -280,6 +328,6 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
280
328
|
});
|
|
281
329
|
|
|
282
330
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
283
|
-
reconcileToolAvailability(pi, ctx.mode
|
|
331
|
+
reconcileToolAvailability(pi, isInteractiveMode(ctx.mode, process.env));
|
|
284
332
|
});
|
|
285
333
|
}
|