@gavin-hu/my-pi-agent 0.9.0 → 0.10.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 +28 -2
- package/README.zh-CN.md +27 -2
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +261 -36
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/tools.ts +5 -2
- package/extensions/bytefly/header/component.ts +21 -10
- package/extensions/bytefly/header/config.ts +0 -3
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/format.ts +17 -10
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +11 -4
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +7 -7
- package/extensions/file-browse/README.md +3 -1
- package/extensions/file-browse/client.ts +9 -2
- package/extensions/file-browse/git.ts +4 -7
- package/extensions/file-browse/html.ts +27 -17
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +7 -20
- package/extensions/goal/tools.ts +5 -17
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +21 -12
- package/extensions/job/format.ts +2 -2
- package/extensions/job/index.ts +25 -20
- package/extensions/job/tools.ts +5 -3
- package/extensions/job/tui.ts +13 -8
- package/extensions/memory/README.md +6 -0
- package/extensions/memory/format.ts +5 -3
- package/extensions/plan/README.md +32 -12
- package/extensions/plan/tools.ts +20 -5
- package/extensions/plan/web-client.ts +58 -17
- package/extensions/plan/web-page.ts +34 -9
- package/extensions/plan/web-router.ts +40 -9
- package/extensions/plan/web.ts +22 -2
- package/extensions/question/README.md +13 -1
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +11 -6
- package/extensions/rewind/README.md +2 -0
- package/extensions/rewind/format.ts +6 -1
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +56 -29
- package/extensions/todo/README.md +19 -12
- package/extensions/todo/format.ts +17 -25
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/tui.ts +3 -2
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/format.ts +2 -2
- package/extensions/watch/index.ts +28 -23
- package/extensions/watch/tools.ts +5 -3
- package/extensions/watch/tui.ts +12 -7
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +1 -1
- package/extensions/web-access/search/tool.ts +3 -3
- package/extensions/wechat/README.md +6 -0
- package/extensions/worktree/README.md +6 -0
- package/lib/README.md +2 -1
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/tui/chrome.ts +8 -4
- package/lib/tui/cursor.ts +2 -1
- package/lib/tui/glyphs.ts +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +3 -1
- package/lib/tui/rows.ts +39 -5
- package/lib/tui/rules.ts +9 -3
- package/lib/tui/tokens.ts +14 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
package/extensions/goal/tui.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal rendering for the goal.
|
|
3
3
|
*
|
|
4
|
-
* Two surfaces share one vocabulary.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* aligned under the text; it matches the todo transcript result's indent + glyph
|
|
10
|
-
* grammar so the two read as a pair.
|
|
11
|
-
* `GoalResult` wraps `goalRailLines` uncapped for the transcript.
|
|
4
|
+
* Two surfaces share one vocabulary. The persistent above-editor widget is a
|
|
5
|
+
* single label-first rail line (`Goal · active · <objective>`, dimmed once
|
|
6
|
+
* achieved), matching the `todo` and `jobs` widgets. The transcript result is
|
|
7
|
+
* the status header alone (`Goal · active`): the call line already names the
|
|
8
|
+
* objective, so the result only states the new status.
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { truncateToWidth,
|
|
16
|
-
import {
|
|
17
|
-
import { goalGlyph, goalHeader, goalLine } from "./format.ts";
|
|
12
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
13
|
+
import { goalHeader, goalLine } from "./format.ts";
|
|
18
14
|
import type { AchievedStyle, Goal } from "./types.ts";
|
|
19
15
|
|
|
20
16
|
/** Widget key used with `ctx.ui.setWidget()`. */
|
|
@@ -25,27 +21,6 @@ export interface GoalWidgetOptions {
|
|
|
25
21
|
achieved?: AchievedStyle;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
/**
|
|
29
|
-
* Rows for the transcript goal rail: the themed header plus `body` wrapped and
|
|
30
|
-
* led by the status glyph. Continuation rows are padded to the text column.
|
|
31
|
-
*/
|
|
32
|
-
export function goalRailLines(goal: Goal, body: string, theme: Theme, width: number): string[] {
|
|
33
|
-
const w = Math.max(1, width);
|
|
34
|
-
const glyph = goalGlyph(goal, theme);
|
|
35
|
-
const glyphWidth = visibleWidth(glyph);
|
|
36
|
-
const textColumn = BODY_INDENT + glyphWidth + GLYPH_GAP;
|
|
37
|
-
const inner = Math.max(1, w - textColumn);
|
|
38
|
-
|
|
39
|
-
const wrapped = wrapTextWithAnsi(body, inner);
|
|
40
|
-
const pad = " ".repeat(BODY_INDENT);
|
|
41
|
-
const continuation = " ".repeat(textColumn);
|
|
42
|
-
const lines = [goalHeader(goal, theme)];
|
|
43
|
-
wrapped.forEach((line, index) => {
|
|
44
|
-
lines.push(index === 0 ? `${pad}${glyph}${" ".repeat(GLYPH_GAP)}${line}` : `${continuation}${line}`);
|
|
45
|
-
});
|
|
46
|
-
return lines.map((line) => truncateToWidth(line, w, "…"));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
24
|
/** Rows for the goal widget: a single rail line, or nothing when hidden. */
|
|
50
25
|
export function goalWidgetLines(goal: Goal, theme: Theme, width: number, options: GoalWidgetOptions = {}): string[] {
|
|
51
26
|
const w = Math.max(1, width);
|
|
@@ -68,30 +43,27 @@ export class GoalWidget implements Component {
|
|
|
68
43
|
}
|
|
69
44
|
}
|
|
70
45
|
|
|
71
|
-
/** Transcript result block: the
|
|
46
|
+
/** Transcript result block: the status header, separated from the call line. */
|
|
72
47
|
export class GoalResult implements Component {
|
|
73
48
|
private goal: Goal;
|
|
74
|
-
private body: string;
|
|
75
49
|
private theme: Theme;
|
|
76
50
|
|
|
77
|
-
constructor(goal: Goal,
|
|
51
|
+
constructor(goal: Goal, theme: Theme) {
|
|
78
52
|
this.goal = goal;
|
|
79
|
-
this.body = body;
|
|
80
53
|
this.theme = theme;
|
|
81
54
|
}
|
|
82
55
|
|
|
83
56
|
/** Update in place so the transcript can reuse this component across renders. */
|
|
84
|
-
update(goal: Goal,
|
|
57
|
+
update(goal: Goal, theme: Theme): void {
|
|
85
58
|
this.goal = goal;
|
|
86
|
-
this.body = body;
|
|
87
59
|
this.theme = theme;
|
|
88
60
|
}
|
|
89
61
|
|
|
90
62
|
invalidate(): void {}
|
|
91
63
|
|
|
92
64
|
render(width: number): string[] {
|
|
93
|
-
// The leading blank line separates the
|
|
65
|
+
// The leading blank line separates the header from the call line, matching
|
|
94
66
|
// the built-ins' header/output gap.
|
|
95
|
-
return ["",
|
|
67
|
+
return ["", truncateToWidth(goalHeader(this.goal, this.theme), Math.max(1, width), "…")];
|
|
96
68
|
}
|
|
97
69
|
}
|
package/extensions/job/README.md
CHANGED
|
@@ -5,12 +5,20 @@ the background and manage them without blocking the turn. A job starts
|
|
|
5
5
|
immediately and keeps running across tool calls; poll it, tail its output, wait
|
|
6
6
|
for it, or kill it.
|
|
7
7
|
|
|
8
|
+
<img src="../../assets/job.png" alt="The /jobs screen: a summary line, the job rows with status glyphs and elapsed times, and the focused job's command, cwd, and last output line" width="100%">
|
|
9
|
+
|
|
8
10
|
```
|
|
9
11
|
pi --extension ./extensions/job # load just this extension
|
|
10
12
|
pi -e . # load the whole @gavin-hu/my-pi-agent package
|
|
11
13
|
pi install ./ # install the package
|
|
12
14
|
```
|
|
13
15
|
|
|
16
|
+
## Tool rendering
|
|
17
|
+
|
|
18
|
+
How the `job` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
19
|
+
|
|
20
|
+
<img src="../../assets/tools/job.png" alt="The job tool's result: the call line and an indented body with a finished and a failed job row, each with its status glyph and elapsed time" width="100%">
|
|
21
|
+
|
|
14
22
|
## What it does
|
|
15
23
|
|
|
16
24
|
- Registers one model-callable tool, `job`, with actions `start`, `list`,
|
|
@@ -81,7 +89,7 @@ JobDetails {
|
|
|
81
89
|
### Transcript
|
|
82
90
|
|
|
83
91
|
The call line names the action and its target. The result is themed and reuses
|
|
84
|
-
the slot's component: `list` draws the same
|
|
92
|
+
the slot's component: `list` draws the same rows as `/jobs`, `logs` draws a
|
|
85
93
|
blank line and the sanitized tail, and a single-job action draws one outcome
|
|
86
94
|
line. A `kill` that sent a signal appends it (`· SIGTERM sent`) instead of
|
|
87
95
|
showing the job's pre-kill running state, a `start` badges `detached` and
|
|
@@ -89,7 +97,6 @@ showing the job's pre-kill running state, a `start` badges `detached` and
|
|
|
89
97
|
|
|
90
98
|
```
|
|
91
99
|
job list
|
|
92
|
-
|
|
93
100
|
▸ j1 tests 3.2s
|
|
94
101
|
✗ j2 build 0.4s
|
|
95
102
|
```
|
|
@@ -199,8 +206,8 @@ rewritten.
|
|
|
199
206
|
| Execution mode | `sequential` — actions share the in-memory job table. |
|
|
200
207
|
| Annotations | `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true` — plan mode blocks it. |
|
|
201
208
|
| `outputSchema` / `structuredContent` | None. Results return `JobDetails` in tool-result `details` only. |
|
|
202
|
-
| State storage | One durable registry file per session, `~/.pi/agent/jobs/<projectKey>/registry-<sessionHash>.json` (keyed by the session cwd, so entering a worktree does not move it), merged from every session file on load; a dead session's records are adopted and a legacy shared `registry.json` is migrated once. Ids are reserved with an atomic `O_EXCL` `<id>.lock` before `start`, and a POSIX job's exit code is written to a per-job `.status` file. Tool results carry `JobDetails`; unreported completions arrive as
|
|
203
|
-
| Lifecycle | `session_start` loads and reconciles the registry and arms `onFinish` and `onComplete`; `session_shutdown` kills session-owned jobs and clears both status chips. `session_tree` repaints; `agent_start`/`agent_settled` track busy; `context` prunes stale
|
|
209
|
+
| State storage | One durable registry file per session, `~/.pi/agent/jobs/<projectKey>/registry-<sessionHash>.json` (keyed by the session cwd, so entering a worktree does not move it), merged from every session file on load; a dead session's records are adopted and a legacy shared `registry.json` is migrated once. Ids are reserved with an atomic `O_EXCL` `<id>.lock` before `start`, and a POSIX job's exit code is written to a per-job `.status` file. Tool results carry `JobDetails`; unreported completions arrive as a shared transcript notice (`my-pi-agent/notice`) at `before_agent_start`. |
|
|
210
|
+
| Lifecycle | `session_start` loads and reconciles the registry and arms `onFinish` and `onComplete`; `session_shutdown` kills session-owned jobs and clears both status chips. `session_tree` repaints; `agent_start`/`agent_settled` track busy; `context` prunes stale notices of its own source. |
|
|
204
211
|
| Event bus | Each job that reaches a terminal state is published on `my-pi-agent/job-finished` (`lib/events.ts`) with its id, label, status, exit code, and signal, from the one `settle` path every transition goes through. bytefly's notify capability subscribes there instead of importing this extension. |
|
|
205
212
|
|
|
206
213
|
## Design notes
|
|
@@ -248,11 +255,11 @@ rewritten.
|
|
|
248
255
|
settles it (recovering an exit code from the status file, else `unknown`)
|
|
249
256
|
when the pid disappears.
|
|
250
257
|
- **Notify by default, wake opt-in.** A failed job raises one `ctx.ui.notify`
|
|
251
|
-
(a clean exit is left to the list and chips). Finished jobs are also
|
|
252
|
-
as a
|
|
253
|
-
`wake: true` job (or `wakeOnFinish`) additionally
|
|
254
|
-
agent is idle; auto-wake is not the default because
|
|
255
|
-
tokens.
|
|
258
|
+
(a clean exit is left to the list and chips). Finished jobs are also posted
|
|
259
|
+
as a shared transcript notice — model-facing text plus the notice block — at the
|
|
260
|
+
next `before_agent_start`. A `wake: true` job (or `wakeOnFinish`) additionally
|
|
261
|
+
triggers one turn when the agent is idle; auto-wake is not the default because
|
|
262
|
+
it easily loops and burns tokens.
|
|
256
263
|
- **Width-1 glyphs and cached latest line.** The running chip is `▸ N` and the
|
|
257
264
|
failure chip `✗ N`, each a two-token badge under its own status key, so the
|
|
258
265
|
status bar compacts each independently (`▸N` / `✗N`) and an unreported
|
|
@@ -264,8 +271,10 @@ rewritten.
|
|
|
264
271
|
spawn/liveness/kill/clock functions, so the whole suite runs without
|
|
265
272
|
launching a process.
|
|
266
273
|
- **The transcript reuses the `/jobs` grammar.** `job list` renders the same
|
|
267
|
-
`jobRow`
|
|
268
|
-
|
|
274
|
+
`jobRow` rows as the screen (running first, capped, `… N more`), minus the
|
|
275
|
+
selection-marker column (`listRow`'s `marker: false`), which only the
|
|
276
|
+
interactive screen needs; `job logs` draws a themed tail with a `… N earlier
|
|
277
|
+
lines` note, and single-job actions
|
|
269
278
|
draw one `formatJobOutcomeLine` (plus a signal note on `kill` and
|
|
270
279
|
`detached`/`wake` badges on `start`). A capped collapsed result ends with the
|
|
271
280
|
`app.tools.expand` hint, and an expanded list shows every job. The renderers
|
|
@@ -292,7 +301,7 @@ rewritten.
|
|
|
292
301
|
| `ui.ts` | Footer status chips, over the shared `lib/tui/status-controller.ts`. |
|
|
293
302
|
| `waiters.ts` | `wait` resolver bookkeeping. |
|
|
294
303
|
| `runtime.ts` | Composition: job table, handles, process events, repaint clock. |
|
|
295
|
-
| `tui.ts` | The `/jobs` screen and the transcript `JobResult`
|
|
304
|
+
| `tui.ts` | The `/jobs` screen and the transcript `JobResult` rows, drawn from the `lib/tui/` kit (`titleRule`, `listRow`, `emptyState`, `IdCursor`, `LinePane`). |
|
|
296
305
|
| `tools.ts` | The `job` tool and its transcript rendering. |
|
|
297
306
|
| `commands.ts` | The `/jobs` command. |
|
|
298
307
|
| `types.ts` | Shared types. |
|
package/extensions/job/format.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
|
-
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
17
|
+
import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
18
18
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
19
19
|
import type { JobRecord, JobStatus } from "./types.ts";
|
|
20
20
|
|
|
@@ -251,7 +251,7 @@ export function jobOutcome(job: JobRecord): string {
|
|
|
251
251
|
|
|
252
252
|
/** One-line outcome for a single job, themed for the transcript result. */
|
|
253
253
|
export function formatJobOutcomeLine(job: JobRecord, theme: Theme, now = Date.now()): string {
|
|
254
|
-
const sep = paint(theme, "faint",
|
|
254
|
+
const sep = paint(theme, "faint", SEPARATORS.item);
|
|
255
255
|
const outcome =
|
|
256
256
|
job.status === "failed" ? paint(theme, "bad", jobOutcome(job)) : paint(theme, "faint", jobOutcome(job));
|
|
257
257
|
return (
|
package/extensions/job/index.ts
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
* Registers the `job` tool and `/jobs` command. Jobs run as detached shell
|
|
5
5
|
* processes with their output streamed to log files; the status chip shows
|
|
6
6
|
* running and unreported-failure counts, and finished jobs are reported to the
|
|
7
|
-
* model at the next turn
|
|
7
|
+
* model — and shown as a bytefly notice — at the next turn, or by triggering one
|
|
8
|
+
* turn for a `wake` job.
|
|
8
9
|
*
|
|
9
10
|
* Load with: pi --extension ./extensions/job
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
15
|
import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
|
|
16
|
+
import { noticeMessage, type NoticeMessage, pruneStaleNotices } from "../../lib/notice.ts";
|
|
16
17
|
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
17
18
|
import { registerCommands } from "./commands.ts";
|
|
18
|
-
import { formatCompletion } from "./format.ts";
|
|
19
|
+
import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
|
|
19
20
|
import { createJobsRuntime, type JobsRuntime } from "./runtime.ts";
|
|
20
21
|
import { registerTools } from "./tools.ts";
|
|
21
22
|
import type { Job, JobRecord } from "./types.ts";
|
|
@@ -23,16 +24,26 @@ import type { Job, JobRecord } from "./types.ts";
|
|
|
23
24
|
/** Marker embedded in the injected completion context. */
|
|
24
25
|
const JOB_CONTEXT_MARKER = "[BACKGROUND JOBS]";
|
|
25
26
|
|
|
26
|
-
/** Custom-entry type used for the injected completion message. */
|
|
27
|
-
export const JOB_CONTEXT_TYPE = "job-context";
|
|
28
|
-
|
|
29
27
|
function buildContext(jobs: JobRecord[]): string {
|
|
30
28
|
const lines = jobs.map((job) => formatCompletion(job));
|
|
31
29
|
return `${JOB_CONTEXT_MARKER}\n${jobs.length} background job${jobs.length === 1 ? "" : "s"} finished:\n${lines.join("\n")}\nRun \`job logs <id>\` for output.`;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/** The shared notice for pending completions: model text plus renderer details. */
|
|
33
|
+
function jobNotice(jobs: JobRecord[]): NoticeMessage {
|
|
34
|
+
const count = jobs.length;
|
|
35
|
+
return noticeMessage({
|
|
36
|
+
content: buildContext(jobs),
|
|
37
|
+
details: {
|
|
38
|
+
source: "job",
|
|
39
|
+
title: `${count} background job${count === 1 ? "" : "s"} finished`,
|
|
40
|
+
items: jobs.map((job) => ({
|
|
41
|
+
label: `${job.id} ${shortLabel(job)}`,
|
|
42
|
+
detail: jobOutcome(job),
|
|
43
|
+
severity: job.status === "failed" ? "bad" : job.status === "exited" ? "ok" : "warn",
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/** Trim a finished record to the fields the event bus carries. */
|
|
@@ -67,10 +78,7 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
|
67
78
|
const pending = runtime.pendingCompletions();
|
|
68
79
|
if (pending.length === 0) return;
|
|
69
80
|
try {
|
|
70
|
-
pi.sendMessage(
|
|
71
|
-
{ customType: JOB_CONTEXT_TYPE, content: buildContext(pending), display: false },
|
|
72
|
-
{ triggerTurn: true },
|
|
73
|
-
);
|
|
81
|
+
pi.sendMessage(jobNotice(pending), { triggerTurn: true });
|
|
74
82
|
// Mark only once the send succeeded; a throw leaves them for the next turn.
|
|
75
83
|
runtime.markReported(pending.map((job) => job.id));
|
|
76
84
|
} catch {
|
|
@@ -113,17 +121,14 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
|
113
121
|
if (pending.length === 0) return undefined;
|
|
114
122
|
runtime.markReported(pending.map((job) => job.id));
|
|
115
123
|
runtime.setStatus(ctx);
|
|
116
|
-
return {
|
|
117
|
-
message: { customType: JOB_CONTEXT_TYPE, content: buildContext(pending), display: false },
|
|
118
|
-
};
|
|
124
|
+
return { message: jobNotice(pending) };
|
|
119
125
|
});
|
|
120
126
|
|
|
121
|
-
// Keep stale completion
|
|
127
|
+
// Keep stale completion notices out of later turns. Only this extension's
|
|
128
|
+
// notices are pruned; another source's are left for its own handler.
|
|
122
129
|
pi.on("context", (event) => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const last = contexts[contexts.length - 1];
|
|
126
|
-
return { messages: event.messages.filter((message) => !isJobContext(message) || message === last) };
|
|
130
|
+
const pruned = pruneStaleNotices(event.messages, "job");
|
|
131
|
+
return pruned ? { messages: pruned } : undefined;
|
|
127
132
|
});
|
|
128
133
|
|
|
129
134
|
pi.on("session_shutdown", async (_event, ctx) => {
|
package/extensions/job/tools.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
12
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
13
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
13
14
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
14
15
|
import {
|
|
15
16
|
compareJobs,
|
|
@@ -209,8 +210,9 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
209
210
|
const text = reuseText(context.lastComponent);
|
|
210
211
|
text.setText(
|
|
211
212
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
paint(
|
|
214
|
+
theme,
|
|
215
|
+
"meta",
|
|
214
216
|
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
215
217
|
),
|
|
216
218
|
);
|
|
@@ -282,7 +284,7 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
282
284
|
const badges: string[] = [];
|
|
283
285
|
if (details.job.detached) badges.push("detached");
|
|
284
286
|
if (details.job.wake) badges.push("wake");
|
|
285
|
-
if (badges.length > 0) line += paint(theme, "faint",
|
|
287
|
+
if (badges.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${badges.join(SEPARATORS.item)}`);
|
|
286
288
|
}
|
|
287
289
|
text.setText(line);
|
|
288
290
|
return text;
|
package/extensions/job/tui.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { clip, fitCells } from "../../lib/tui/columns.ts";
|
|
|
21
21
|
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
22
22
|
import { expandHint, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
23
23
|
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
24
|
-
import { emptyState,
|
|
24
|
+
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
25
25
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
26
26
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
27
|
import {
|
|
@@ -51,15 +51,19 @@ const LOG_CHROME_ROWS = 6;
|
|
|
51
51
|
const jobId = (job: JobRecord): string => job.id;
|
|
52
52
|
|
|
53
53
|
/** One job row: selection marker, status glyph, id, label, and a right-aligned
|
|
54
|
-
* elapsed value. `
|
|
55
|
-
function jobRow(
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
* elapsed value. `statusRow` owns the clipping and the alignment. */
|
|
55
|
+
function jobRow(
|
|
56
|
+
job: JobRecord,
|
|
57
|
+
theme: Theme,
|
|
58
|
+
width: number,
|
|
59
|
+
options: Pick<StatusRow, "selected" | "marker"> = {},
|
|
60
|
+
): string {
|
|
61
|
+
return statusRow(theme, width, {
|
|
62
|
+
...options,
|
|
58
63
|
glyph: statusGlyphChar(job.status),
|
|
59
64
|
glyphRole: statusRole(job.status),
|
|
60
65
|
label: `${job.id} ${shortLabel(job)}`,
|
|
61
66
|
trailing: formatDuration(elapsedMs(job, Date.now())),
|
|
62
|
-
trailingRole: "faint",
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -95,7 +99,7 @@ export class JobResult implements Component {
|
|
|
95
99
|
// A leading blank line separates the result from the call header.
|
|
96
100
|
const lines: string[] = [""];
|
|
97
101
|
if (this.input.kind === "list") {
|
|
98
|
-
for (const job of this.input.jobs) lines.push(jobRow(job, this.theme, w));
|
|
102
|
+
for (const job of this.input.jobs) lines.push(jobRow(job, this.theme, w, { marker: false }));
|
|
99
103
|
if (this.input.more > 0) {
|
|
100
104
|
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
101
105
|
lines.push(clip(` ${note}`, w));
|
|
@@ -350,7 +354,8 @@ export class JobListComponent implements Component {
|
|
|
350
354
|
body.push(...emptyState(this.theme, w, { label: "No background jobs." }));
|
|
351
355
|
} else {
|
|
352
356
|
const end = Math.min(jobs.length, this.cursor.scroll + this.cursor.visible);
|
|
353
|
-
for (let i = this.cursor.scroll; i < end; i++)
|
|
357
|
+
for (let i = this.cursor.scroll; i < end; i++)
|
|
358
|
+
body.push(jobRow(jobs[i], this.theme, w, { selected: i === index }));
|
|
354
359
|
if (this.cursor.scroll > 0 || end < jobs.length) {
|
|
355
360
|
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: jobs.length });
|
|
356
361
|
}
|
|
@@ -13,6 +13,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pac
|
|
|
13
13
|
pi install ./ # install the package
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Tool rendering
|
|
17
|
+
|
|
18
|
+
How the `memory` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
19
|
+
|
|
20
|
+
<img src="../../assets/tools/memory.png" alt="The memory tool's result: the call line and an indented body confirming the write and the new project and global counts" width="100%">
|
|
21
|
+
|
|
16
22
|
## What it does
|
|
17
23
|
|
|
18
24
|
- Registers one model-callable tool, `memory`, with four actions: `add` a note,
|
|
@@ -78,7 +78,9 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
|
|
|
78
78
|
].join("\n");
|
|
79
79
|
case "add":
|
|
80
80
|
if (details.changed) {
|
|
81
|
-
|
|
81
|
+
// The call line already names the note, so the result confirms the write
|
|
82
|
+
// and the new counts rather than repeating it.
|
|
83
|
+
return `Added to ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
|
|
82
84
|
}
|
|
83
85
|
if (details.existing !== undefined && details.existing !== details.entry) {
|
|
84
86
|
return `Already in ${scopeLabel(details.scope)} memory (near-duplicate): ${details.existing}`;
|
|
@@ -86,10 +88,10 @@ export function formatResultText(details: MemoryDetails, expanded = false): stri
|
|
|
86
88
|
return `Already in ${scopeLabel(details.scope)} memory: ${details.existing ?? details.entry ?? ""}`;
|
|
87
89
|
case "update":
|
|
88
90
|
return details.changed
|
|
89
|
-
? `Updated ${scopeLabel(details.scope)} memory
|
|
91
|
+
? `Updated ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`
|
|
90
92
|
: `Already set in ${scopeLabel(details.scope)} memory: ${details.entry ?? ""}`;
|
|
91
93
|
case "forget":
|
|
92
|
-
return `Removed from ${scopeLabel(details.scope)} memory
|
|
94
|
+
return `Removed from ${scopeLabel(details.scope)} memory\nNow ${countLine(entries)}.`;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -12,6 +12,14 @@ pi -e . # load the whole @gavin-hu/my-pi-agent packag
|
|
|
12
12
|
pi --plan # start in plan mode
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Tool rendering
|
|
16
|
+
|
|
17
|
+
How the `write_plan` and `exit_plan_mode` calls and their results look in the transcript — the call line as a headline and the body indented under it:
|
|
18
|
+
|
|
19
|
+
<img src="../../assets/tools/write_plan.png" alt="The write_plan tool's call and result: the call line and an indented body with the saved plan path and byte count" width="100%">
|
|
20
|
+
|
|
21
|
+
<img src="../../assets/tools/exit_plan_mode.png" alt="The exit_plan_mode tool's call and result: the call line and an indented body with the approval line and the approved plan preview" width="100%">
|
|
22
|
+
|
|
15
23
|
## What it does
|
|
16
24
|
|
|
17
25
|
- **Read-only gating, with one exception.** Tools that are not read-only are
|
|
@@ -142,7 +150,9 @@ the user.
|
|
|
142
150
|
|
|
143
151
|
Browser review is a local-turn feature: the server binds `127.0.0.1`, so a remote
|
|
144
152
|
(WeChat) turn cannot reach it and `/plan review` there refuses with a notice
|
|
145
|
-
instead of opening a server nobody can use.
|
|
153
|
+
instead of opening a server nobody can use. The browser's **Approve & run** is
|
|
154
|
+
offered only for the review `exit_plan_mode` opens, since only that flow owns
|
|
155
|
+
leaving plan mode; `/plan review` stays comment-only.
|
|
146
156
|
|
|
147
157
|
### Transcript
|
|
148
158
|
|
|
@@ -254,15 +264,15 @@ agent file that overrides it.
|
|
|
254
264
|
|
|
255
265
|
## Browser review
|
|
256
266
|
|
|
257
|
-
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title and Save
|
|
267
|
+
<img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail and an inline review thread" width="100%">
|
|
258
268
|
|
|
259
269
|
Press `w` on the review screen — or run `/plan review [path]` — to open the plan
|
|
260
270
|
in the default browser on `127.0.0.1`. The page is a reading-first review
|
|
261
271
|
surface: a sticky header carrying the plan's H1 (falling back to the file-name
|
|
262
272
|
slug), a live status chip (connection, comment count, and unsaved changes), the
|
|
263
|
-
theme toggle, and
|
|
264
|
-
side panel shows the plan's step progress and comment count above a
|
|
265
|
-
outline**, and the document is GitHub's Markdown content column — one centred
|
|
273
|
+
theme toggle, and the review actions, over a hairline reading-progress bar. A
|
|
274
|
+
sticky side panel shows the plan's step progress and comment count above a
|
|
275
|
+
**section outline**, and the document is GitHub's Markdown content column — one centred
|
|
266
276
|
measure of at most 1012px with a comment rail in its left margin. Every block
|
|
267
277
|
reveals a **+ Comment** action on hover; select
|
|
268
278
|
text first to quote it. **Save feedback** writes the comments into the plan
|
|
@@ -284,12 +294,22 @@ file and returns them to the model.
|
|
|
284
294
|
labelled and indented as a thread, so it reads in place and the plan keeps one
|
|
285
295
|
measure at every width. There is no separate column to reflow.
|
|
286
296
|
- **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
|
|
287
|
-
on the current block, `
|
|
288
|
-
|
|
297
|
+
on the current block, `a` approves (the approval flow only), `s` saves, `Esc`
|
|
298
|
+
closes the composer or the sheet and then the review, and `?` opens a shortcut
|
|
299
|
+
sheet; the header's `?` button opens the same sheet.
|
|
300
|
+
- **Decision in place.** A review opened for approval (`w` on the review screen)
|
|
301
|
+
shows **Approve & run** beside Save feedback: it persists the current comments,
|
|
302
|
+
ends plan mode, restores write access, seeds `todo`, and hands control back to
|
|
303
|
+
the model. **Save feedback** keeps plan mode and returns the comments as a
|
|
304
|
+
refinement; **Back to Pi** closes the review with no decision. Each action
|
|
305
|
+
settles the page on a terminal state — `Approved`, `Saved`, or `Closed` — so
|
|
306
|
+
the tab never falls to Offline. `/plan review` is comment-only: it renders no
|
|
307
|
+
approve action and the server refuses one.
|
|
289
308
|
- **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
|
|
290
|
-
code, GFM tables, `---` rules, and bare-URL autolinks render;
|
|
291
|
-
|
|
292
|
-
|
|
309
|
+
code, GFM tables, `---` rules, and bare-URL autolinks render; a list item's
|
|
310
|
+
continuation lines hang under the item's text, inline code is a soft fill
|
|
311
|
+
rather than a bordered chip, tables honour the alignment row and scroll
|
|
312
|
+
horizontally when narrow.
|
|
293
313
|
- **Highlighted code.** Fenced code is tokenized by the shared
|
|
294
314
|
`lib/highlight.ts`; the client copies only text and `tok-*` spans through
|
|
295
315
|
`DOMParser`, so a code line is never parsed as markup.
|
|
@@ -439,10 +459,10 @@ file and returns them to the model.
|
|
|
439
459
|
| `tui.ts` | `PlanViewComponent` (sectioned Markdown body, section jump, scroll gauge, approve/refine/browser/keep). |
|
|
440
460
|
| `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
|
|
441
461
|
| `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
|
|
442
|
-
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/cancel/abort session. |
|
|
462
|
+
| `web.ts` | `createPlanWebReview`: the loopback server, the section outline and code highlighting, and the save/approve/cancel/abort session. |
|
|
443
463
|
| `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
|
|
444
464
|
| `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
|
|
445
|
-
| `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/
|
|
465
|
+
| `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
|
|
446
466
|
| `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
|
|
447
467
|
| [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
|
|
448
468
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
|
package/extensions/plan/tools.ts
CHANGED
|
@@ -26,12 +26,12 @@ import { FULL_SCREEN_OVERLAY, reuseText, toolDetails } from "../../lib/tui/chrom
|
|
|
26
26
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
27
27
|
import { expandHint } from "../../lib/tui/glyphs.ts";
|
|
28
28
|
import { READ_ONLY_SUMMARY } from "./policy.ts";
|
|
29
|
-
import { feedbackSummary } from "./feedback.ts";
|
|
29
|
+
import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
|
|
30
30
|
import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
|
|
31
31
|
import { PLAN_OUTLINE, extractPlanSteps, type PlanStep, type StepUpdate } from "./steps.ts";
|
|
32
32
|
import { PlanViewComponent, sanitizePlanText, type PlanViewAction } from "./tui.ts";
|
|
33
33
|
import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
|
|
34
|
-
import { createPlanWebReview, type PlanWebReview } from "./web.ts";
|
|
34
|
+
import { createPlanWebReview, type PlanReviewResult, type PlanReviewSession, type PlanWebReview } from "./web.ts";
|
|
35
35
|
|
|
36
36
|
const ExitPlanModeParams = Type.Object({
|
|
37
37
|
plan_path: Type.String({
|
|
@@ -394,6 +394,8 @@ export function registerTools(
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
let outcome = await reviewPlan(ctx, file);
|
|
397
|
+
// Comments carried by a browser approval, reported alongside the approval.
|
|
398
|
+
let browserComments: PlanAnnotation[] = [];
|
|
397
399
|
// Review, refine, and browser review all loop back here until the user
|
|
398
400
|
// approves, keeps planning, or a refinement is handed to the model. The
|
|
399
401
|
// dialog-based refine editor carries no text from the review screen, so an
|
|
@@ -401,15 +403,25 @@ export function registerTools(
|
|
|
401
403
|
// as "not approved".
|
|
402
404
|
for (;;) {
|
|
403
405
|
if (outcome.action === "browser") {
|
|
404
|
-
const session = await web
|
|
406
|
+
const session: PlanReviewSession | undefined = await web
|
|
407
|
+
.start(ctx.cwd, file, { canApprove: true })
|
|
408
|
+
.catch(() => undefined);
|
|
405
409
|
if (!session) {
|
|
406
410
|
ctx.ui.notify("Plan review: could not start the browser review server.", "error");
|
|
407
411
|
outcome = await reviewPlan(ctx, file);
|
|
408
412
|
continue;
|
|
409
413
|
}
|
|
410
414
|
ctx.ui.notify(`Plan review: opened ${session.url}`, "info");
|
|
411
|
-
const result = await session.done;
|
|
415
|
+
const result: PlanReviewResult = await session.done;
|
|
412
416
|
await session.close();
|
|
417
|
+
if (result.status === "approved") {
|
|
418
|
+
// Approve from the browser is the TUI's approve: keep the comments it
|
|
419
|
+
// carried and fall through to the shared approval path below.
|
|
420
|
+
file = result.plan;
|
|
421
|
+
browserComments = result.annotations;
|
|
422
|
+
outcome = { action: "approve" };
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
413
425
|
if (result.status === "saved") {
|
|
414
426
|
// The comments are already in the file; hand them back as a refinement so
|
|
415
427
|
// the model revises the same plan and calls exit_plan_mode again.
|
|
@@ -491,11 +503,14 @@ export function registerTools(
|
|
|
491
503
|
: steps.length > 0
|
|
492
504
|
? ` Could not record the steps in the todo list${error ? `: ${error}` : ""}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
|
|
493
505
|
: " If the todo tool is available, record the steps with it before you start.";
|
|
506
|
+
const browserNote = browserComments.length
|
|
507
|
+
? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(browserComments)}`
|
|
508
|
+
: "";
|
|
494
509
|
return {
|
|
495
510
|
content: [
|
|
496
511
|
{
|
|
497
512
|
type: "text",
|
|
498
|
-
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${tail}${listing}`,
|
|
513
|
+
text: `Plan approved. Plan mode is off and write access is restored. The plan file ${file.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${listing}`,
|
|
499
514
|
},
|
|
500
515
|
],
|
|
501
516
|
details: {
|