@mjasnikovs/pi-task 0.7.0 β 0.7.2
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 +37 -39
- package/assets/pipeline.svg +90 -0
- package/assets/task-auto.svg +65 -0
- package/dist/remote/bridge.d.ts +1 -10
- package/dist/remote/bridge.js +3 -19
- package/dist/remote/events.d.ts +3 -2
- package/dist/remote/events.js +25 -50
- package/dist/remote/history.d.ts +18 -5
- package/dist/remote/history.js +11 -4
- package/dist/remote/protocol.d.ts +8 -5
- package/dist/remote/register.js +7 -11
- package/dist/remote/server.d.ts +1 -2
- package/dist/remote/server.js +6 -13
- package/dist/remote/session-state.d.ts +54 -0
- package/dist/remote/session-state.js +179 -0
- package/dist/remote/ui.js +154 -37
- package/dist/task/auto-io.d.ts +7 -0
- package/dist/task/auto-io.js +24 -13
- package/dist/task/auto-orchestrator.d.ts +6 -1
- package/dist/task/auto-orchestrator.js +15 -3
- package/dist/task/orchestrator.d.ts +6 -1
- package/dist/task/orchestrator.js +9 -2
- package/dist/task/widget.js +7 -7
- package/dist/workers/html-clean.js +77 -9
- package/dist/workers/pi-worker-docs.js +14 -11
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +8 -3
- package/dist/workers/pi-worker.js +9 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<img src="./assets/pipeline.svg" alt="pi-task pipeline: a /task request runs through refine, research, grill, compose and critique, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable." width="820"/>
|
|
4
|
+
|
|
3
5
|
# π§© pi-task
|
|
4
6
|
|
|
5
7
|
**Deterministic spec-orchestration for local models β with bundled web, docs, fetch, and worker sub-agent tools.**
|
|
@@ -7,7 +9,7 @@
|
|
|
7
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
8
10
|
[](./LICENSE)
|
|
9
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
10
|
-
[](#development)
|
|
11
13
|
[](./tsconfig.json)
|
|
12
14
|
|
|
13
15
|
</div>
|
|
@@ -16,24 +18,7 @@
|
|
|
16
18
|
|
|
17
19
|
## What it does
|
|
18
20
|
|
|
19
|
-
Local models drift. Ask one to plan a non-trivial change and it skips context, hallucinates APIs, and forgets what you actually asked. `pi-task` fixes this by **not trusting a single prompt** β it drives your request through a fixed, persisted pipeline of small, verifiable steps, then hands the main session a clean spec to execute.
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
/task add rate-limiting to the public API
|
|
23
|
-
β
|
|
24
|
-
βΌ
|
|
25
|
-
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
|
|
26
|
-
β refine ββββΆβ research ββββΆβ grill ββββΆβ compose ββββΆβ critique β
|
|
27
|
-
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
|
|
28
|
-
sharpen the parallel clarifying assemble triage +
|
|
29
|
-
raw prompt sub-agents: questions the spec rewrite if
|
|
30
|
-
files Β· APIs Β· (auto- or the draft
|
|
31
|
-
context Β· you answer) isn't clean
|
|
32
|
-
tooling
|
|
33
|
-
β
|
|
34
|
-
βΌ
|
|
35
|
-
final spec βββΆ main pi session (you keep working in the same chat)
|
|
36
|
-
```
|
|
21
|
+
Local models drift. Ask one to plan a non-trivial change and it skips context, hallucinates APIs, and forgets what you actually asked. `pi-task` fixes this by **not trusting a single prompt** β it drives your request through a fixed, persisted pipeline of small, verifiable steps (shown above), then hands the main session a clean spec to execute.
|
|
37
22
|
|
|
38
23
|
Every phase boundary is written to `.pi-tasks/TASK_NNNN.md`, so a task survives a crash, a restart, or a `/task-cancel` β pick it back up with `/task-resume`.
|
|
39
24
|
|
|
@@ -51,7 +36,7 @@ Every phase boundary is written to `.pi-tasks/TASK_NNNN.md`, so a task survives
|
|
|
51
36
|
pi install npm:@mjasnikovs/pi-task
|
|
52
37
|
```
|
|
53
38
|
|
|
54
|
-
> Requires [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (the Earendil coding agent) β₯ 0.
|
|
39
|
+
> Requires [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (the Earendil coding agent) β₯ 0.78.
|
|
55
40
|
|
|
56
41
|
## Slash commands
|
|
57
42
|
|
|
@@ -82,22 +67,11 @@ The finished spec is delivered to your main `pi` conversation via `sendUserMessa
|
|
|
82
67
|
|
|
83
68
|
A real feature is usually several tasks, not one. `/task-auto` is a thin planner on top of the single-task pipeline:
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
β clarify ββββΆβ decomposeββββΆβ TASK_AUTO_β¦ β resumable list of task titles
|
|
91
|
-
β gray β β β titles β β .md (titles) β
|
|
92
|
-
β areas β ββββββββββββ ββββββββ¬ββββββββ
|
|
93
|
-
ββββββββββββ β
|
|
94
|
-
ββββββββββββββΌββββββββββββββ
|
|
95
|
-
β for each unchecked title β
|
|
96
|
-
β β full /task pipeline β (spec + implement)
|
|
97
|
-
β β wait until it finishes β
|
|
98
|
-
β β check the box, next β
|
|
99
|
-
ββββββββββββββββββββββββββββββ
|
|
100
|
-
```
|
|
70
|
+
<div align="center">
|
|
71
|
+
|
|
72
|
+
<img src="./assets/task-auto.svg" alt="/task-auto plans a feature: it clarifies the gray areas, decomposes the answers into an ordered list of task titles written to TASK_AUTO_NNNN.md, then runs each unchecked title through the full /task pipeline one at a time, ticking the box before moving on." width="820"/>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
101
75
|
|
|
102
76
|
- **It only produces titles.** All the depth β refine, research, grill, compose, critique β is `/task`'s job, run fresh per title. `/task-auto` never researches or specs anything itself.
|
|
103
77
|
- **Clarify first.** It asks the few clarifying questions whose answers change how the feature splits, then decomposes the answers into an ordered list of task titles written to `.pi-tasks/TASK_AUTO_NNNN.md`.
|
|
@@ -114,6 +88,26 @@ The remote server is **always on** β it starts automatically with each session
|
|
|
114
88
|
|
|
115
89
|
Prompts use a **first-answer-wins race**: the same question shows in the local TUI *and* every connected browser, and whoever answers first wins β the other surfaces dismiss. With nobody connected, `/task` behaves exactly as before; the remote path is purely additive.
|
|
116
90
|
|
|
91
|
+
### Push notifications
|
|
92
|
+
|
|
93
|
+
Tap the bell (β― β β) in the remote header to get pushed a notification β even with the app backgrounded or the phone locked β when:
|
|
94
|
+
|
|
95
|
+
- a **grill / clarify question** needs answering (*"pi needs your input"*),
|
|
96
|
+
- a **task finishes** (*"Task finished"*), or
|
|
97
|
+
- the agent hits an **error** (*"Agent error"*).
|
|
98
|
+
|
|
99
|
+
Delivery is **server β push service β device** over the [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) standard (service worker + VAPID), so it reaches a suspended device. It works on desktop browsers and on iOS home-screen PWAs.
|
|
100
|
+
|
|
101
|
+
**iOS setup** (these are Apple's requirements, not ours):
|
|
102
|
+
|
|
103
|
+
1. Open the **HTTPS** Tailscale URL (`/remote` lists it). iOS only allows push from a secure context β the plain `http://` LAN URL won't work.
|
|
104
|
+
2. **Share β Add to Home Screen**, then open the app from that icon. iOS only permits notifications for installed PWAs.
|
|
105
|
+
3. Launch the app, **tap the bell**, and **Allow** when prompted.
|
|
106
|
+
|
|
107
|
+
The subscription is kept in memory, so after restarting `pi` just reload the app (it re-subscribes automatically) or tap the bell again. Notifications are suppressed while the app is focused in the foreground β the in-page UI already shows the prompt there.
|
|
108
|
+
|
|
109
|
+
VAPID keys are generated once and persisted to `${XDG_DATA_HOME:-~/.local/share}/pi-task/vapid.json` (deleting them invalidates existing subscriptions). The JWT contact (`sub`) defaults to the project URL; override it with `PI_REMOTE_PUSH_SUBJECT` (e.g. your own `mailto:you@domain.com`). To debug delivery, set `PI_REMOTE_PUSH_DEBUG=1` and tail `/tmp/pi-task-push.log` β it records each push and the **push-service HTTP status** (`201` delivered, `403`/`400` token/key problem, `410` stale subscription).
|
|
110
|
+
|
|
117
111
|
`/remote stop` shuts the server down for the rest of the session (it comes back on the next session start). There is **no authentication** β it's a personal LAN/Tailscale tool. Don't expose the port to untrusted networks.
|
|
118
112
|
|
|
119
113
|
## Bundled tools
|
|
@@ -129,9 +123,9 @@ Runs a Brave Search query and returns a compact markdown list (title Β· URL Β· s
|
|
|
129
123
|
> **Requires** `BRAVE_SEARCH_API_KEY` (also accepted as `BRAVE_API_KEY`). Grab a free key at [api.search.brave.com/app/keys](https://api.search.brave.com/app/keys).
|
|
130
124
|
|
|
131
125
|
### `pi-worker-fetch`
|
|
132
|
-
Fetches a URL, cleans
|
|
126
|
+
Fetches a URL, cleans HTML to markdown ([Readability](https://github.com/mozilla/readability) + [Turndown](https://github.com/mixmark-io/turndown)), then hands it to an isolated child that extracts **only** the content answering your `query`. The parent never sees the raw page.
|
|
133
127
|
|
|
134
|
-
-
|
|
128
|
+
- HTML is cleaned; text formats (plain text, markdown, JSON, XML/feeds, `llms.txt`, β¦) pass through verbatim. Binary responses β PDFs, images, octet-streams β return a clear error.
|
|
135
129
|
- Bodies over 2 MB are rejected.
|
|
136
130
|
- The extraction child runs with `--no-tools` to mitigate visible-text prompt injection.
|
|
137
131
|
|
|
@@ -148,6 +142,10 @@ Resolves an installed npm package, indexes its `.d.ts` files and README into a l
|
|
|
148
142
|
| --- | --- | --- |
|
|
149
143
|
| `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required for web search. |
|
|
150
144
|
| `XDG_CACHE_HOME` | `pi-worker-docs` | Overrides the docs cache location (defaults to `~/.cache`). |
|
|
145
|
+
| `XDG_DATA_HOME` | remote push | Where the VAPID keypair is stored (defaults to `~/.local/share`). |
|
|
146
|
+
| `PI_REMOTE_PUSH_SUBJECT` | remote push | VAPID JWT `sub` contact. Defaults to the project URL; set your own `mailto:you@domain.com` or `https://β¦`. |
|
|
147
|
+
| `PI_REMOTE_PUSH_DEBUG` | remote push | When set (e.g. `1`), logs push delivery and push-service HTTP status. Off by default. |
|
|
148
|
+
| `PI_REMOTE_PUSH_LOG` | remote push | Path for the debug log (defaults to `/tmp/pi-task-push.log`). |
|
|
151
149
|
|
|
152
150
|
Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your `.gitignore` if you don't want them checked in.
|
|
153
151
|
|
|
@@ -155,7 +153,7 @@ Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your
|
|
|
155
153
|
|
|
156
154
|
```sh
|
|
157
155
|
bun install
|
|
158
|
-
bun test src/ #
|
|
156
|
+
bun test src/ # 559 tests across 46 files
|
|
159
157
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
160
158
|
bun run build # tsc β dist/
|
|
161
159
|
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 396" width="900" height="396" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" role="img" aria-label="pi-task pipeline: a /task request runs through refine, research, grill, compose and critique phases, then the final spec is delivered to your main pi session in the same chat. Every phase boundary is persisted to .pi-tasks/TASK_NNNN.md, so the task is crash-safe and resumable.">
|
|
2
|
+
<defs>
|
|
3
|
+
<marker id="ah" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
4
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#6e7681"/>
|
|
5
|
+
</marker>
|
|
6
|
+
<style>
|
|
7
|
+
.title { font-size:17px; font-weight:600; fill:#e6edf3; }
|
|
8
|
+
.cap { font-size:11.5px; fill:#8b949e; }
|
|
9
|
+
.num { font-size:11px; font-weight:700; }
|
|
10
|
+
.code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
11
|
+
.foot { font-size:11.5px; fill:#768390; font-style:italic; }
|
|
12
|
+
</style>
|
|
13
|
+
</defs>
|
|
14
|
+
|
|
15
|
+
<rect x="6" y="6" width="888" height="384" rx="16" fill="#0d1117" stroke="#30363d" stroke-width="1.5"/>
|
|
16
|
+
|
|
17
|
+
<!-- input -->
|
|
18
|
+
<rect x="220" y="32" width="460" height="38" rx="19" fill="#161b22" stroke="#7c3aed" stroke-width="1.5"/>
|
|
19
|
+
<text x="450" y="57" text-anchor="middle" class="code" font-size="14"><tspan fill="#a371f7" font-weight="700">/task</tspan><tspan fill="#c9d1d9" dx="7">add rate-limiting to the public API</tspan></text>
|
|
20
|
+
<line x1="450" y1="70" x2="450" y2="106" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
21
|
+
|
|
22
|
+
<!-- phase row -->
|
|
23
|
+
<!-- refine -->
|
|
24
|
+
<g>
|
|
25
|
+
<rect x="32" y="118" width="150" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
26
|
+
<rect x="32" y="118" width="5" height="72" rx="2.5" fill="#58a6ff"/>
|
|
27
|
+
<text x="50" y="138" class="num" fill="#58a6ff">1</text>
|
|
28
|
+
<text x="107" y="161" text-anchor="middle" class="title">refine</text>
|
|
29
|
+
</g>
|
|
30
|
+
<text x="107" y="210" text-anchor="middle" class="cap">sharpen the</text>
|
|
31
|
+
<text x="107" y="225" text-anchor="middle" class="cap">raw prompt</text>
|
|
32
|
+
|
|
33
|
+
<!-- research -->
|
|
34
|
+
<g>
|
|
35
|
+
<rect x="203.5" y="118" width="150" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
36
|
+
<rect x="203.5" y="118" width="5" height="72" rx="2.5" fill="#39c5cf"/>
|
|
37
|
+
<text x="221.5" y="138" class="num" fill="#39c5cf">2</text>
|
|
38
|
+
<text x="278.5" y="161" text-anchor="middle" class="title">research</text>
|
|
39
|
+
</g>
|
|
40
|
+
<text x="278.5" y="210" text-anchor="middle" class="cap">parallel sub-agents:</text>
|
|
41
|
+
<text x="278.5" y="225" text-anchor="middle" class="cap">files Β· APIs Β·</text>
|
|
42
|
+
<text x="278.5" y="240" text-anchor="middle" class="cap">context Β· tooling</text>
|
|
43
|
+
|
|
44
|
+
<!-- grill -->
|
|
45
|
+
<g>
|
|
46
|
+
<rect x="375" y="118" width="150" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
47
|
+
<rect x="375" y="118" width="5" height="72" rx="2.5" fill="#d29922"/>
|
|
48
|
+
<text x="393" y="138" class="num" fill="#d29922">3</text>
|
|
49
|
+
<text x="450" y="161" text-anchor="middle" class="title">grill</text>
|
|
50
|
+
</g>
|
|
51
|
+
<text x="450" y="210" text-anchor="middle" class="cap">clarifying Q&A</text>
|
|
52
|
+
<text x="450" y="225" text-anchor="middle" class="cap">(auto, or you answer)</text>
|
|
53
|
+
|
|
54
|
+
<!-- compose -->
|
|
55
|
+
<g>
|
|
56
|
+
<rect x="546.5" y="118" width="150" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
57
|
+
<rect x="546.5" y="118" width="5" height="72" rx="2.5" fill="#3fb950"/>
|
|
58
|
+
<text x="564.5" y="138" class="num" fill="#3fb950">4</text>
|
|
59
|
+
<text x="621.5" y="161" text-anchor="middle" class="title">compose</text>
|
|
60
|
+
</g>
|
|
61
|
+
<text x="621.5" y="210" text-anchor="middle" class="cap">assemble</text>
|
|
62
|
+
<text x="621.5" y="225" text-anchor="middle" class="cap">the spec</text>
|
|
63
|
+
|
|
64
|
+
<!-- critique -->
|
|
65
|
+
<g>
|
|
66
|
+
<rect x="718" y="118" width="150" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
67
|
+
<rect x="718" y="118" width="5" height="72" rx="2.5" fill="#a371f7"/>
|
|
68
|
+
<text x="736" y="138" class="num" fill="#a371f7">5</text>
|
|
69
|
+
<text x="793" y="161" text-anchor="middle" class="title">critique</text>
|
|
70
|
+
</g>
|
|
71
|
+
<text x="793" y="210" text-anchor="middle" class="cap">triage, rewrite</text>
|
|
72
|
+
<text x="793" y="225" text-anchor="middle" class="cap">if not clean</text>
|
|
73
|
+
|
|
74
|
+
<!-- inter-phase arrows -->
|
|
75
|
+
<line x1="182" y1="154" x2="201.5" y2="154" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
76
|
+
<line x1="353.5" y1="154" x2="373" y2="154" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
77
|
+
<line x1="525" y1="154" x2="544.5" y2="154" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
78
|
+
<line x1="696.5" y1="154" x2="716" y2="154" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
79
|
+
|
|
80
|
+
<!-- to result -->
|
|
81
|
+
<line x1="450" y1="252" x2="450" y2="292" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah)"/>
|
|
82
|
+
|
|
83
|
+
<!-- result -->
|
|
84
|
+
<rect x="120" y="296" width="660" height="48" rx="12" fill="#0f261a" stroke="#2ea043" stroke-width="1.5"/>
|
|
85
|
+
<text x="450" y="318" text-anchor="middle" font-size="14" font-weight="600" fill="#e6edf3">final spec β delivered to your main <tspan class="code" fill="#7ee787">pi</tspan> session</text>
|
|
86
|
+
<text x="450" y="334" text-anchor="middle" class="cap">same chat β no handoff, no copy-paste</text>
|
|
87
|
+
|
|
88
|
+
<!-- footnote -->
|
|
89
|
+
<text x="450" y="372" text-anchor="middle" class="foot">every phase boundary is persisted to <tspan class="code" font-style="normal">.pi-tasks/TASK_NNNN.md</tspan> β crash-safe & resumable</text>
|
|
90
|
+
</svg>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 332" width="900" height="332" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" role="img" aria-label="/task-auto plans a feature: it clarifies the gray areas, decomposes the answers into an ordered list of task titles written to TASK_AUTO_NNNN.md, then runs each unchecked title through the full /task pipeline one at a time, ticking the box before moving on.">
|
|
2
|
+
<defs>
|
|
3
|
+
<marker id="ah2" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
4
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#6e7681"/>
|
|
5
|
+
</marker>
|
|
6
|
+
<style>
|
|
7
|
+
.title { font-size:17px; font-weight:600; fill:#e6edf3; }
|
|
8
|
+
.titlec { font-size:15px; font-weight:600; fill:#e6edf3; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
9
|
+
.cap { font-size:11.5px; fill:#8b949e; }
|
|
10
|
+
.num { font-size:11px; font-weight:700; }
|
|
11
|
+
.code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
12
|
+
</style>
|
|
13
|
+
</defs>
|
|
14
|
+
|
|
15
|
+
<rect x="6" y="6" width="888" height="320" rx="16" fill="#0d1117" stroke="#30363d" stroke-width="1.5"/>
|
|
16
|
+
|
|
17
|
+
<!-- input -->
|
|
18
|
+
<rect x="240" y="30" width="420" height="38" rx="19" fill="#161b22" stroke="#7c3aed" stroke-width="1.5"/>
|
|
19
|
+
<text x="450" y="55" text-anchor="middle" class="code" font-size="14"><tspan fill="#a371f7" font-weight="700">/task-auto</tspan><tspan fill="#c9d1d9" dx="7">add multi-tenant billing</tspan></text>
|
|
20
|
+
<line x1="450" y1="68" x2="450" y2="100" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah2)"/>
|
|
21
|
+
|
|
22
|
+
<!-- clarify -->
|
|
23
|
+
<g>
|
|
24
|
+
<rect x="32" y="110" width="200" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
25
|
+
<rect x="32" y="110" width="5" height="72" rx="2.5" fill="#d29922"/>
|
|
26
|
+
<text x="50" y="130" class="num" fill="#d29922">1</text>
|
|
27
|
+
<text x="132" y="153" text-anchor="middle" class="title">clarify gray areas</text>
|
|
28
|
+
</g>
|
|
29
|
+
<text x="132" y="202" text-anchor="middle" class="cap">ask only what</text>
|
|
30
|
+
<text x="132" y="217" text-anchor="middle" class="cap">changes the split</text>
|
|
31
|
+
|
|
32
|
+
<!-- decompose -->
|
|
33
|
+
<g>
|
|
34
|
+
<rect x="350" y="110" width="200" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
35
|
+
<rect x="350" y="110" width="5" height="72" rx="2.5" fill="#58a6ff"/>
|
|
36
|
+
<text x="368" y="130" class="num" fill="#58a6ff">2</text>
|
|
37
|
+
<text x="450" y="153" text-anchor="middle" class="title">decompose β titles</text>
|
|
38
|
+
</g>
|
|
39
|
+
<text x="450" y="202" text-anchor="middle" class="cap">ordered list of</text>
|
|
40
|
+
<text x="450" y="217" text-anchor="middle" class="cap">task titles only</text>
|
|
41
|
+
|
|
42
|
+
<!-- file -->
|
|
43
|
+
<g>
|
|
44
|
+
<rect x="668" y="110" width="200" height="72" rx="10" fill="#161b22" stroke="#30363d"/>
|
|
45
|
+
<rect x="668" y="110" width="5" height="72" rx="2.5" fill="#3fb950"/>
|
|
46
|
+
<text x="686" y="130" class="num" fill="#3fb950">3</text>
|
|
47
|
+
<text x="768" y="154" text-anchor="middle" class="titlec">TASK_AUTO_NNNN.md</text>
|
|
48
|
+
</g>
|
|
49
|
+
<text x="768" y="202" text-anchor="middle" class="cap">resumable checklist</text>
|
|
50
|
+
<text x="768" y="217" text-anchor="middle" class="cap">of titles</text>
|
|
51
|
+
|
|
52
|
+
<!-- arrows between -->
|
|
53
|
+
<line x1="232" y1="146" x2="348" y2="146" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah2)"/>
|
|
54
|
+
<line x1="550" y1="146" x2="666" y2="146" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah2)"/>
|
|
55
|
+
|
|
56
|
+
<!-- to loop -->
|
|
57
|
+
<line x1="768" y1="227" x2="768" y2="248" stroke="#6e7681" stroke-width="1.5"/>
|
|
58
|
+
<path d="M768,248 H450 V248" fill="none" stroke="#6e7681" stroke-width="1.5"/>
|
|
59
|
+
<line x1="450" y1="244" x2="450" y2="252" stroke="#6e7681" stroke-width="1.5" marker-end="url(#ah2)"/>
|
|
60
|
+
|
|
61
|
+
<!-- loop -->
|
|
62
|
+
<rect x="120" y="256" width="660" height="54" rx="12" fill="#0d1117" stroke="#a371f7" stroke-width="1.5"/>
|
|
63
|
+
<text x="450" y="278" text-anchor="middle" font-size="14" font-weight="600" fill="#e6edf3">β» for each unchecked title</text>
|
|
64
|
+
<text x="450" y="296" text-anchor="middle" class="cap">run the full <tspan class="code" fill="#a371f7">/task</tspan> pipeline β implement β wait until done β tick the box β next</text>
|
|
65
|
+
</svg>
|
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ServerMessage } from './protocol.js';
|
|
3
3
|
export interface BridgeState {
|
|
4
4
|
/** promptId β resolver that settles the remote side of an ask() race. */
|
|
5
5
|
pending: Map<string, (value: string | undefined) => void>;
|
|
6
|
-
/** The prompt currently awaiting an answer (replayed to late joiners), or null. */
|
|
7
|
-
activePrompt: PromptMessage | null;
|
|
8
|
-
/** Last lines pushed per widget key (replayed to late joiners). */
|
|
9
|
-
activeWidgets: Map<string, string[]>;
|
|
10
|
-
/** Most recent context-window usage (replayed to seed late joiners' bar), or null. */
|
|
11
|
-
lastContextUsage: ContextUsage | null;
|
|
12
6
|
nextId: number;
|
|
13
7
|
/** Command name β handler, populated as pi-task registers its commands. */
|
|
14
8
|
commands: Map<string, (args: string, ctx: ExtensionCommandContext) => unknown>;
|
|
@@ -43,9 +37,6 @@ export declare class SessionUI {
|
|
|
43
37
|
/** Race the local input against a remote answer; first to settle wins. */
|
|
44
38
|
ask(spec: AskSpec): Promise<string | undefined>;
|
|
45
39
|
}
|
|
46
|
-
/** Mirror a status widget to browsers and remember it for late joiners.
|
|
47
|
-
* `lines === undefined` clears the widget (broadcast as `lines: null`). */
|
|
48
|
-
export declare function publishWidget(key: string, lines: string[] | undefined): void;
|
|
49
40
|
export declare function publishNotify(message: string, level: 'info' | 'warning' | 'error'): void;
|
|
50
41
|
export declare function publishViewer(title: string, text: string): void;
|
|
51
42
|
/**
|
package/dist/remote/bridge.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { broadcast as wsBroadcast } from './broadcast.js';
|
|
2
2
|
import { pushNotify } from './push.js';
|
|
3
|
+
import { setPrompt, clearPrompt } from './session-state.js';
|
|
3
4
|
const g = globalThis;
|
|
4
5
|
export function getBridge() {
|
|
5
6
|
if (!g.__piBridge) {
|
|
6
7
|
g.__piBridge = {
|
|
7
8
|
pending: new Map(),
|
|
8
|
-
activePrompt: null,
|
|
9
|
-
activeWidgets: new Map(),
|
|
10
|
-
lastContextUsage: null,
|
|
11
9
|
nextId: 0,
|
|
12
10
|
commands: new Map(),
|
|
13
11
|
currentCtx: null,
|
|
@@ -56,8 +54,7 @@ export class SessionUI {
|
|
|
56
54
|
recommended: spec.recommended,
|
|
57
55
|
allowSkip: spec.allowSkip
|
|
58
56
|
};
|
|
59
|
-
|
|
60
|
-
b.broadcast(prompt);
|
|
57
|
+
setPrompt(prompt);
|
|
61
58
|
// Reaches a backgrounded/suspended phone, which the in-page UI can't.
|
|
62
59
|
void pushNotify('pi needs your input', spec.question, 'pi-prompt').catch(() => { });
|
|
63
60
|
// Local: resolves to a value/undefined, or undefined on abort. Swallow
|
|
@@ -78,23 +75,10 @@ export class SessionUI {
|
|
|
78
75
|
}
|
|
79
76
|
finally {
|
|
80
77
|
b.pending.delete(id);
|
|
81
|
-
|
|
82
|
-
b.broadcast({ type: 'prompt_resolved', id });
|
|
78
|
+
clearPrompt(id);
|
|
83
79
|
}
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
|
-
/** Mirror a status widget to browsers and remember it for late joiners.
|
|
87
|
-
* `lines === undefined` clears the widget (broadcast as `lines: null`). */
|
|
88
|
-
export function publishWidget(key, lines) {
|
|
89
|
-
const b = getBridge();
|
|
90
|
-
if (lines === undefined) {
|
|
91
|
-
b.activeWidgets.delete(key);
|
|
92
|
-
b.broadcast({ type: 'widget', key, lines: null });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
b.activeWidgets.set(key, lines);
|
|
96
|
-
b.broadcast({ type: 'widget', key, lines });
|
|
97
|
-
}
|
|
98
82
|
export function publishNotify(message, level) {
|
|
99
83
|
getBridge().broadcast({ type: 'notify', message, level });
|
|
100
84
|
}
|
package/dist/remote/events.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** Mirror pi agent events into the authoritative SessionState. Each handler
|
|
3
|
+
* drives a mutator, which updates the snapshot AND broadcasts the live delta. */
|
|
4
|
+
export declare function setupEvents(pi: ExtensionAPI): void;
|
package/dist/remote/events.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { setAgentIdle } from './state.js';
|
|
2
|
-
import { getBridge } from './bridge.js';
|
|
3
2
|
import { pushNotify } from './push.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { publishNotify } from './bridge.js';
|
|
4
|
+
import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
|
|
5
|
+
/** Mirror pi agent events into the authoritative SessionState. Each handler
|
|
6
|
+
* drives a mutator, which updates the snapshot AND broadcasts the live delta. */
|
|
7
|
+
export function setupEvents(pi) {
|
|
8
8
|
pi.on('agent_start', (_event, _ctx) => {
|
|
9
|
-
currentText = '';
|
|
10
|
-
currentTools.length = 0;
|
|
11
|
-
pendingArgs.clear();
|
|
12
9
|
setAgentIdle(false);
|
|
13
|
-
|
|
10
|
+
agentStart();
|
|
14
11
|
});
|
|
15
12
|
pi.on('message_update', (event, _ctx) => {
|
|
16
13
|
const ae = event.assistantMessageEvent;
|
|
17
14
|
if (ae.type === 'text_delta' && 'delta' in ae && typeof ae.delta === 'string') {
|
|
18
|
-
|
|
19
|
-
broadcastFn({ type: 'text_delta', delta: ae.delta });
|
|
15
|
+
appendText(ae.delta);
|
|
20
16
|
}
|
|
21
17
|
else if (ae.type === 'error') {
|
|
22
18
|
const errorMessage = 'error' in ae && ae.error && typeof ae.error.errorMessage === 'string' ?
|
|
@@ -25,63 +21,42 @@ export function setupEvents(pi, history, broadcastFn) {
|
|
|
25
21
|
// Skip silent user aborts (no message); only surface genuine failures.
|
|
26
22
|
if (errorMessage || ae.reason === 'error') {
|
|
27
23
|
const message = errorMessage || 'Request failed';
|
|
28
|
-
|
|
29
|
-
broadcastFn({ type: 'agent_error', message });
|
|
24
|
+
addError(message);
|
|
30
25
|
void pushNotify('Agent error', message, 'pi-error').catch(() => { });
|
|
31
26
|
}
|
|
32
27
|
}
|
|
33
28
|
});
|
|
34
29
|
pi.on('message_end', (_event, _ctx) => {
|
|
35
|
-
|
|
30
|
+
textEnd();
|
|
36
31
|
});
|
|
37
32
|
pi.on('tool_execution_start', (event, _ctx) => {
|
|
38
|
-
|
|
39
|
-
broadcastFn({
|
|
40
|
-
type: 'tool_start',
|
|
41
|
-
toolCallId: event.toolCallId,
|
|
42
|
-
toolName: event.toolName,
|
|
43
|
-
args: event.args
|
|
44
|
-
});
|
|
33
|
+
startTool(event.toolCallId, event.toolName, event.args);
|
|
45
34
|
});
|
|
46
35
|
pi.on('tool_execution_update', (event, _ctx) => {
|
|
47
|
-
|
|
48
|
-
type: 'tool_update',
|
|
49
|
-
toolCallId: event.toolCallId,
|
|
50
|
-
partialResult: event.partialResult
|
|
51
|
-
});
|
|
36
|
+
updateTool(event.toolCallId, event.partialResult);
|
|
52
37
|
});
|
|
53
38
|
pi.on('tool_execution_end', (event, _ctx) => {
|
|
54
|
-
|
|
55
|
-
pendingArgs.delete(event.toolCallId);
|
|
56
|
-
currentTools.push({
|
|
57
|
-
toolName: event.toolName,
|
|
58
|
-
args,
|
|
59
|
-
result: event.result,
|
|
60
|
-
isError: event.isError
|
|
61
|
-
});
|
|
62
|
-
broadcastFn({
|
|
63
|
-
type: 'tool_end',
|
|
64
|
-
toolCallId: event.toolCallId,
|
|
65
|
-
toolName: event.toolName,
|
|
66
|
-
result: event.result,
|
|
67
|
-
isError: event.isError
|
|
68
|
-
});
|
|
39
|
+
endTool(event.toolCallId, event.toolName, event.result, event.isError);
|
|
69
40
|
});
|
|
70
41
|
pi.on('agent_end', (_event, ctx) => {
|
|
71
|
-
const contextUsage = ctx.getContextUsage();
|
|
72
42
|
setAgentIdle(true);
|
|
73
|
-
|
|
74
|
-
getBridge().lastContextUsage = contextUsage;
|
|
75
|
-
history.addAssistantTurn(currentText, [...currentTools]);
|
|
76
|
-
broadcastFn({ type: 'agent_end', contextUsage });
|
|
43
|
+
agentEnd(ctx.getContextUsage());
|
|
77
44
|
void pushNotify('Task finished', '', 'pi-end').catch(() => { });
|
|
78
|
-
currentText = '';
|
|
79
|
-
currentTools.length = 0;
|
|
80
45
|
});
|
|
81
46
|
pi.on('input', (event, _ctx) => {
|
|
82
47
|
if (event.source === 'interactive' && typeof event.text === 'string') {
|
|
83
|
-
|
|
84
|
-
broadcastFn({ type: 'user_message', text: event.text });
|
|
48
|
+
addUserTurn(event.text);
|
|
85
49
|
}
|
|
86
50
|
});
|
|
51
|
+
// Context-window compaction (incl. the auto-compaction triggered by a context
|
|
52
|
+
// overflow) is invisible to a remote viewer otherwise β mirror it as a toast so
|
|
53
|
+
// they see the same "compactingβ¦" status the terminal shows.
|
|
54
|
+
pi.on('session_before_compact', (_event, _ctx) => {
|
|
55
|
+
publishNotify('Context full β compactingβ¦', 'warning');
|
|
56
|
+
});
|
|
57
|
+
pi.on('session_compact', (_event, _ctx) => {
|
|
58
|
+
// Persistent inline note so it's still visible after a reconnect, not just a
|
|
59
|
+
// transient toast.
|
|
60
|
+
addSystemNote('Context compacted');
|
|
61
|
+
});
|
|
87
62
|
}
|
package/dist/remote/history.d.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface TextPart {
|
|
2
|
+
kind: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolPart {
|
|
6
|
+
kind: 'tool';
|
|
7
|
+
toolCallId: string;
|
|
2
8
|
toolName: string;
|
|
3
9
|
args: unknown;
|
|
4
10
|
result: unknown;
|
|
5
11
|
isError: boolean;
|
|
12
|
+
/** false while the tool is still running (result not in yet). */
|
|
13
|
+
done: boolean;
|
|
6
14
|
}
|
|
15
|
+
export type Part = TextPart | ToolPart;
|
|
7
16
|
export interface Turn {
|
|
8
|
-
role: 'user' | 'assistant';
|
|
9
|
-
text
|
|
10
|
-
|
|
17
|
+
role: 'user' | 'assistant' | 'system';
|
|
18
|
+
/** User text, error text, or a system note. Assistant content lives in `parts`. */
|
|
19
|
+
text?: string;
|
|
20
|
+
/** Ordered assistant content (text + tools). */
|
|
21
|
+
parts?: Part[];
|
|
11
22
|
error?: boolean;
|
|
12
23
|
}
|
|
13
24
|
export declare class HistoryBuffer {
|
|
@@ -15,8 +26,10 @@ export declare class HistoryBuffer {
|
|
|
15
26
|
private readonly limit;
|
|
16
27
|
constructor(limit?: number);
|
|
17
28
|
addUserMessage(text: string): void;
|
|
18
|
-
addAssistantTurn(
|
|
29
|
+
addAssistantTurn(parts: Part[]): void;
|
|
19
30
|
addError(text: string): void;
|
|
31
|
+
/** A persistent, inline system note (e.g. "Context compacted"). */
|
|
32
|
+
addSystemNote(text: string): void;
|
|
20
33
|
getEntries(): Turn[];
|
|
21
34
|
private _push;
|
|
22
35
|
}
|
package/dist/remote/history.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// An assistant turn is an ORDERED list of parts β text segments and tool calls
|
|
2
|
+
// interleaved exactly as they happened β so the remote transcript reproduces the
|
|
3
|
+
// terminal's layout instead of collapsing a whole run into one text blob.
|
|
1
4
|
export class HistoryBuffer {
|
|
2
5
|
entries = [];
|
|
3
6
|
limit;
|
|
@@ -5,13 +8,17 @@ export class HistoryBuffer {
|
|
|
5
8
|
this.limit = limit;
|
|
6
9
|
}
|
|
7
10
|
addUserMessage(text) {
|
|
8
|
-
this._push({ role: 'user', text
|
|
11
|
+
this._push({ role: 'user', text });
|
|
9
12
|
}
|
|
10
|
-
addAssistantTurn(
|
|
11
|
-
this._push({ role: 'assistant',
|
|
13
|
+
addAssistantTurn(parts) {
|
|
14
|
+
this._push({ role: 'assistant', parts });
|
|
12
15
|
}
|
|
13
16
|
addError(text) {
|
|
14
|
-
this._push({ role: 'assistant', text,
|
|
17
|
+
this._push({ role: 'assistant', text, error: true });
|
|
18
|
+
}
|
|
19
|
+
/** A persistent, inline system note (e.g. "Context compacted"). */
|
|
20
|
+
addSystemNote(text) {
|
|
21
|
+
this._push({ role: 'system', text });
|
|
15
22
|
}
|
|
16
23
|
getEntries() {
|
|
17
24
|
return [...this.entries];
|
|
@@ -9,9 +9,9 @@ export interface PromptResolvedMessage {
|
|
|
9
9
|
type: 'prompt_resolved';
|
|
10
10
|
id: string;
|
|
11
11
|
}
|
|
12
|
+
/** The single task-widget slot. `lines: null` clears it. */
|
|
12
13
|
export interface WidgetMessage {
|
|
13
14
|
type: 'widget';
|
|
14
|
-
key: string;
|
|
15
15
|
lines: string[] | null;
|
|
16
16
|
}
|
|
17
17
|
export interface NotifyMessage {
|
|
@@ -40,10 +40,13 @@ export interface ContextMessage {
|
|
|
40
40
|
export interface ResetMessage {
|
|
41
41
|
type: 'reset';
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/** The full authoritative state sent to a (re)connecting client. Defined in
|
|
44
|
+
* session-state.ts (its serializer); re-exported here as part of the wire type. */
|
|
45
|
+
export type { SnapshotMessage } from './session-state.js';
|
|
46
|
+
/** Server β browser messages. The live text_delta / tool_* / agent_* /
|
|
47
|
+
* client_count / user_message deltas are emitted by the SessionState mutators
|
|
48
|
+
* and not all enumerated here; the snapshot below carries the full state. */
|
|
49
|
+
export type ServerMessage = PromptMessage | PromptResolvedMessage | WidgetMessage | NotifyMessage | ViewerMessage | ContextMessage | ResetMessage | import('./session-state.js').SnapshotMessage;
|
|
47
50
|
/** Browser β server messages. */
|
|
48
51
|
export interface ClientChatMessage {
|
|
49
52
|
type: 'message';
|