@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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createPadStore, type PadSource } from "../pad.ts";
|
|
15
16
|
import { SeparatorComponent } from "./component.ts";
|
|
16
17
|
import { CONFIG } from "./config.ts";
|
|
17
18
|
import type { TurnSeparatorData } from "./types.ts";
|
|
@@ -26,11 +27,11 @@ function countSeparators(entries: readonly { type: string; customType?: string }
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/** Register the separator renderer and append one on every settled turn. */
|
|
29
|
-
export function registerTurnSeparator(pi: ExtensionAPI): void {
|
|
30
|
+
export function registerTurnSeparator(pi: ExtensionAPI, pad: PadSource = createPadStore()): void {
|
|
30
31
|
pi.registerEntryRenderer<TurnSeparatorData>(CONFIG.customType, (entry, _options, theme) => {
|
|
31
32
|
const turn = entry.data?.turn;
|
|
32
33
|
if (typeof turn !== "number" || !Number.isFinite(turn)) return undefined;
|
|
33
|
-
return new SeparatorComponent(turn, theme);
|
|
34
|
+
return new SeparatorComponent(turn, theme, pad.resolve());
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
pi.on("agent_settled", (_event, ctx) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuning for bytefly's live working timer.
|
|
3
|
+
*
|
|
4
|
+
* While Pi's working row is up, the counter is written into the working message
|
|
5
|
+
* Pi renders beside its spinner. Edit this file to change the word or how often
|
|
6
|
+
* the count advances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const WORKING_CONFIG = {
|
|
10
|
+
/** Word before the count. */
|
|
11
|
+
working: "Working",
|
|
12
|
+
/** Between the word and the count. */
|
|
13
|
+
ellipsis: "…",
|
|
14
|
+
/** How often the count advances, in milliseconds. */
|
|
15
|
+
intervalMs: 1000,
|
|
16
|
+
} as const satisfies {
|
|
17
|
+
working: string;
|
|
18
|
+
ellipsis: string;
|
|
19
|
+
intervalMs: number;
|
|
20
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text for the live working timer.
|
|
3
|
+
*
|
|
4
|
+
* Whole seconds, because the counter advances on a one-second tick: `0s` at the
|
|
5
|
+
* start, `42s`, `1m05s`, `1h02m`. Negative and non-finite input reads as `0s`,
|
|
6
|
+
* so a clock that steps backwards cannot print a negative count.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WORKING_CONFIG } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
/** Compact elapsed time in whole seconds: `0s`, `42s`, `1m05s`, `1h02m`. */
|
|
12
|
+
export function formatElapsed(ms: number): string {
|
|
13
|
+
const total = Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1000) : 0;
|
|
14
|
+
const seconds = total % 60;
|
|
15
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
16
|
+
const hours = Math.floor(total / 3600);
|
|
17
|
+
if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
|
|
18
|
+
if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
19
|
+
return `${seconds}s`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The working-row message for an elapsed time, e.g. `Working… 4s`. */
|
|
23
|
+
export function workingCounter(ms: number): string {
|
|
24
|
+
return `${WORKING_CONFIG.working}${WORKING_CONFIG.ellipsis} ${formatElapsed(ms)}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bytefly's live working timer.
|
|
3
|
+
*
|
|
4
|
+
* Pi's working row shows a static `Working`, so a long run reads the same as a
|
|
5
|
+
* short one. This surface advances the message once a second while the row is
|
|
6
|
+
* up, and hands it back to Pi when the row comes down.
|
|
7
|
+
*
|
|
8
|
+
* The timer is tied to the row's visibility. The host shows the working
|
|
9
|
+
* indicator in its own `turn_start` handler and clears it at `agent_end`
|
|
10
|
+
* (`interactive-mode.js`), so the tick starts at `turn_start` and stops at
|
|
11
|
+
* `agent_end` — the two moments the row appears and disappears. Turns inside
|
|
12
|
+
* one run do not restart it: the host leaves the same indicator up across
|
|
13
|
+
* turns, and `start` is a no-op while it is counting.
|
|
14
|
+
*
|
|
15
|
+
* Compaction and auto-retry also replace the row, but both run outside this
|
|
16
|
+
* window (`auto_retry_start` follows `agent_end`; the before-settle compaction
|
|
17
|
+
* runs after it), and neither is an extension event, so there is nothing to
|
|
18
|
+
* hook and nothing to undo.
|
|
19
|
+
*
|
|
20
|
+
* The handle is global (`setWorkingMessage` drives the single working row), so
|
|
21
|
+
* the tick is cancelled and the message reset on stop and, belt and braces, on
|
|
22
|
+
* `session_shutdown` — the factory itself only registers.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { WORKING_CONFIG } from "./config.ts";
|
|
27
|
+
import { workingCounter } from "./format.ts";
|
|
28
|
+
|
|
29
|
+
/** Host seams, injected by tests so the tick is deterministic. */
|
|
30
|
+
export interface WorkingDeps {
|
|
31
|
+
/** The clock the count is read from. */
|
|
32
|
+
now?: () => number;
|
|
33
|
+
/** Start the repeating tick; returns a cancel handle. */
|
|
34
|
+
schedule?: (tick: () => void, intervalMs: number) => () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A repeating timer that never holds the process open on its own. */
|
|
38
|
+
function defaultSchedule(tick: () => void, intervalMs: number): () => void {
|
|
39
|
+
const timer = setInterval(tick, intervalMs);
|
|
40
|
+
(timer as { unref?: () => void }).unref?.();
|
|
41
|
+
return () => clearInterval(timer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Register the timer; it runs only in interactive (`tui`) sessions. */
|
|
45
|
+
export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}): void {
|
|
46
|
+
const now = deps.now ?? (() => Date.now());
|
|
47
|
+
const schedule = deps.schedule ?? defaultSchedule;
|
|
48
|
+
let cancel: (() => void) | undefined;
|
|
49
|
+
let startedAt = 0;
|
|
50
|
+
// A boolean, not `startedAt === 0`: an injected clock is allowed to start at 0.
|
|
51
|
+
let active = false;
|
|
52
|
+
|
|
53
|
+
/** Stop the tick and hand the working message back to Pi. Idempotent. */
|
|
54
|
+
const stop = (ctx: ExtensionContext): void => {
|
|
55
|
+
cancel?.();
|
|
56
|
+
cancel = undefined;
|
|
57
|
+
if (!active) return;
|
|
58
|
+
active = false;
|
|
59
|
+
ctx.ui.setWorkingMessage(undefined);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Start the count for the current turn; a no-op while already counting. */
|
|
63
|
+
const start = (ctx: ExtensionContext): void => {
|
|
64
|
+
if (active) return;
|
|
65
|
+
startedAt = now();
|
|
66
|
+
active = true;
|
|
67
|
+
const tick = (): void => {
|
|
68
|
+
ctx.ui.setWorkingMessage(workingCounter(now() - startedAt));
|
|
69
|
+
};
|
|
70
|
+
tick();
|
|
71
|
+
cancel = schedule(tick, WORKING_CONFIG.intervalMs);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// The host shows the working indicator on its own `turn_start` handler, so
|
|
75
|
+
// this is the first frame the row is up.
|
|
76
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
77
|
+
if (ctx.mode !== "tui") return;
|
|
78
|
+
start(ctx);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The host clears the working indicator at `agent_end`.
|
|
82
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
83
|
+
if (ctx.mode !== "tui") return;
|
|
84
|
+
stop(ctx);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("session_shutdown", (_event, ctx) => stop(ctx));
|
|
88
|
+
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
the effective working directory with the same symlink-aware guard the built-in
|
|
9
9
|
path tools use.
|
|
10
10
|
|
|
11
|
+
## Tool rendering
|
|
12
|
+
|
|
13
|
+
How the `read_doc` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
14
|
+
|
|
15
|
+
<img src="../../assets/tools/read_doc.png" alt="The read_doc tool's result: the call line and an indented body with the file path, format, size, and the extracted text preview" width="100%">
|
|
16
|
+
|
|
11
17
|
## Quickstart
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -76,19 +82,20 @@ interactive transcript (and HTML exports).
|
|
|
76
82
|
|
|
77
83
|
The call line carries the path (`~`-shortened, OSC-8 linked when the terminal
|
|
78
84
|
supports it) and any paging options. The result line drops the path — it is
|
|
79
|
-
already on the call line — and leads with the format and
|
|
85
|
+
already on the call line — and leads with the format and the character range,
|
|
86
|
+
grouped so a `k` suffix never sits on one bound and not the other:
|
|
80
87
|
|
|
81
88
|
```
|
|
82
89
|
read_doc ~/docs/spec.pdf (from 40k, max 80k)
|
|
83
|
-
PDF ·
|
|
90
|
+
PDF · chars 40,001–120,000 of 250,000 · next at 120,000 · ctrl+o
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
Expanded adds a blank line before the preview body, matching the built-in
|
|
87
94
|
header/output gap:
|
|
88
95
|
|
|
89
96
|
```
|
|
90
|
-
read_doc ~/docs/spec.pdf
|
|
91
|
-
PDF ·
|
|
97
|
+
read_doc ~/docs/spec.pdf (from 40k, max 80k)
|
|
98
|
+
PDF · chars 40,001–120,000 of 250,000 · next at 120,000
|
|
92
99
|
|
|
93
100
|
Chapter 1
|
|
94
101
|
This specification defines ...
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the clean slice; the text adds a header and any truncation note.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { formatCount } from "../../lib/format.ts";
|
|
10
10
|
import type { FormatId } from "./formats.ts";
|
|
11
11
|
|
|
12
12
|
export interface DocPageInput {
|
|
@@ -72,9 +72,9 @@ export interface DocSummaryInput {
|
|
|
72
72
|
export interface DocSummary {
|
|
73
73
|
/** Uppercase format id, e.g. `PDF`. */
|
|
74
74
|
format: string;
|
|
75
|
-
/** Range detail, e.g. `1–
|
|
75
|
+
/** Range detail, e.g. `chars 1–40,000 of 42,478`. */
|
|
76
76
|
detail: string;
|
|
77
|
-
/** `
|
|
77
|
+
/** `next at N` when truncated, `complete` for a non-first final page. */
|
|
78
78
|
note?: string;
|
|
79
79
|
/** Whether more text remains; the renderer colours the note with this. */
|
|
80
80
|
truncated: boolean;
|
|
@@ -83,9 +83,9 @@ export interface DocSummary {
|
|
|
83
83
|
/**
|
|
84
84
|
* Describe a `read_doc` result for the transcript, without theme colours.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* A complete first read shows the total; a paged read shows the returned range
|
|
87
|
+
* and where to continue, so a truncated result is never mistaken for the whole
|
|
88
|
+
* document.
|
|
89
89
|
*/
|
|
90
90
|
export function summarizeDoc(input: DocSummaryInput): DocSummary {
|
|
91
91
|
const format = input.format.toUpperCase();
|
|
@@ -93,18 +93,18 @@ export function summarizeDoc(input: DocSummaryInput): DocSummary {
|
|
|
93
93
|
if (input.startIndex >= input.chars) {
|
|
94
94
|
return {
|
|
95
95
|
format,
|
|
96
|
-
detail: `no text at ${
|
|
96
|
+
detail: `no text at ${formatCount(input.startIndex)} of ${formatCount(input.chars)} chars`,
|
|
97
97
|
truncated: false,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
if (input.startIndex <= 0 && !input.truncated) {
|
|
101
|
-
return { format, detail: `${
|
|
101
|
+
return { format, detail: `${formatCount(input.chars)} chars`, truncated: false };
|
|
102
102
|
}
|
|
103
|
-
const range =
|
|
103
|
+
const range = `chars ${formatCount(input.startIndex + 1)}–${formatCount(input.nextIndex)} of ${formatCount(input.chars)}`;
|
|
104
104
|
return {
|
|
105
105
|
format,
|
|
106
106
|
detail: range,
|
|
107
|
-
note: input.truncated ? `
|
|
107
|
+
note: input.truncated ? `next at ${formatCount(input.nextIndex)}` : "complete",
|
|
108
108
|
truncated: input.truncated,
|
|
109
109
|
};
|
|
110
110
|
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Transcript rendering for `read_doc`.
|
|
3
3
|
*
|
|
4
4
|
* Theme-aware formatting only: the tool definition in `tool.ts` wires these to
|
|
5
|
-
* `renderCall` / `renderResult`. The call line carries the path (
|
|
5
|
+
* `renderCall` / `renderResult`. The call line carries the path (body colour,
|
|
6
6
|
* `~`-shortened, OSC-8 linked when the terminal supports it); the result line
|
|
7
|
-
* leads with the format and
|
|
8
|
-
* The expanded view appends a sanitized, line-capped preview of the
|
|
9
|
-
* text. Every field is sanitized before a theme colour is applied.
|
|
7
|
+
* leads with the format and the exact character range, so the path is never
|
|
8
|
+
* repeated. The expanded view appends a sanitized, line-capped preview of the
|
|
9
|
+
* extracted text. Every field is sanitized before a theme colour is applied.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -22,12 +22,12 @@ import { TOOL_NAME, type DocArgs, type DocResult } from "./schema.ts";
|
|
|
22
22
|
/** Lines of extracted text shown in the expanded preview. */
|
|
23
23
|
export const PREVIEW_LINES = 20;
|
|
24
24
|
|
|
25
|
-
/** Render the argument path:
|
|
25
|
+
/** Render the argument path: body colour, `~`-shortened, OSC-8 linked when supported. */
|
|
26
26
|
export function renderDocPath(rawPath: string | undefined, theme: Theme, cwd: string): string {
|
|
27
27
|
if (typeof rawPath !== "string") return paint(theme, "bad", "[invalid arg]");
|
|
28
28
|
const value = sanitizeLine(rawPath);
|
|
29
29
|
if (!value) return theme.fg("toolOutput", "...");
|
|
30
|
-
const styled = paint(theme, "
|
|
30
|
+
const styled = paint(theme, "label", shortenPath(value));
|
|
31
31
|
if (!getCapabilities().hyperlinks) return styled;
|
|
32
32
|
const abs = isAbsolute(value) ? value : resolve(cwd, value);
|
|
33
33
|
return hyperlink(styled, pathToFileURL(abs).href);
|
|
@@ -64,7 +64,7 @@ export function formatDocCall(args: DocArgs, theme: Theme, context: { cwd: strin
|
|
|
64
64
|
const options: string[] = [];
|
|
65
65
|
if (typeof startIndex === "number" && startIndex > 0) options.push(`from ${formatTokens(startIndex)}`);
|
|
66
66
|
if (typeof maxChars === "number") options.push(`max ${formatTokens(maxChars)}`);
|
|
67
|
-
if (options.length > 0) text += theme
|
|
67
|
+
if (options.length > 0) text += paint(theme, "faint", ` (${options.join(", ")})`);
|
|
68
68
|
return text;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -63,7 +63,9 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
63
63
|
code viewport while its body scrolls.
|
|
64
64
|
- **Read-only and loopback-scoped.** No editing, no writing, no LAN exposure.
|
|
65
65
|
- **Git context.** A read-only header chip shows the checked-out branch (or
|
|
66
|
-
detached head) and
|
|
66
|
+
detached head) and reflows on narrow screens. Uncommitted changes are stated
|
|
67
|
+
once, by the changes chip below; turn the diff view off (`gitDiff: false`) and
|
|
68
|
+
the header reports no changes at all.
|
|
67
69
|
- **Git diff view.** A `● N changes +A −D` chip links to `/diff`, a change
|
|
68
70
|
overview with `git diff --stat` and a per-file status table, and `/diff/<rel>`
|
|
69
71
|
renders one file's unified diff, whose head carries its own `+A −D` and hunk
|
|
@@ -550,12 +550,19 @@ export const CLIENT_JS = `(function () {
|
|
|
550
550
|
// The hunk whose header is pinned (or last passed), then one step either way.
|
|
551
551
|
function moveHunk(delta) {
|
|
552
552
|
if (hunkAnchors.length === 0) return;
|
|
553
|
+
var scroller = primaryScroller();
|
|
554
|
+
// The line a pinned hunk header sticks to: the code viewport's top inset
|
|
555
|
+
// by its padding. With no scrolling pane (narrow screens) the page is the
|
|
556
|
+
// viewport, so the anchor is the window top.
|
|
557
|
+
var lineTop = 8;
|
|
558
|
+
if (scroller) lineTop = scroller.getBoundingClientRect().top + (parseFloat(getComputedStyle(scroller).paddingTop) || 0);
|
|
553
559
|
var current = -1;
|
|
554
560
|
for (var index = 0; index < hunkAnchors.length; index++) {
|
|
555
|
-
if (hunkAnchors[index].getBoundingClientRect().top <=
|
|
561
|
+
if (hunkAnchors[index].getBoundingClientRect().top <= lineTop + 2) current = index;
|
|
556
562
|
}
|
|
557
563
|
var next = Math.max(0, Math.min(hunkAnchors.length - 1, current + delta));
|
|
558
|
-
hunkAnchors[next].
|
|
564
|
+
if (scroller) scroller.scrollTop += hunkAnchors[next].getBoundingClientRect().top - lineTop;
|
|
565
|
+
else hunkAnchors[next].scrollIntoView({ block: "start" });
|
|
559
566
|
}
|
|
560
567
|
|
|
561
568
|
// The page's primary scroller: the card body for a file or diff, the main
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-only git context for the browse pages.
|
|
3
3
|
*
|
|
4
|
-
* The header shows the checked-out branch (or the detached head)
|
|
5
|
-
*
|
|
4
|
+
* The header shows the checked-out branch (or the detached head); uncommitted
|
|
5
|
+
* changes are the changes chip's concern. Queries go through the shared
|
|
6
|
+
* `lib/git` seams so the read-only
|
|
6
7
|
* plumbing lives in one place. State (the TTL cache) lives in the factory
|
|
7
8
|
* closure, never in `lib/`.
|
|
8
9
|
*/
|
|
@@ -25,8 +26,6 @@ export interface GitStatus {
|
|
|
25
26
|
branch?: string;
|
|
26
27
|
/** Short object id, when HEAD is detached. */
|
|
27
28
|
head?: string;
|
|
28
|
-
/** True when `git status --porcelain` reports any change. */
|
|
29
|
-
dirty: boolean;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
export type GitStatusProvider = () => Promise<GitStatus | undefined>;
|
|
@@ -60,9 +59,7 @@ export function createGitStatus(
|
|
|
60
59
|
const branch = await currentBranch(run, root);
|
|
61
60
|
const head = branch ? undefined : (await revParse(run, root, "HEAD"))?.slice(0, 7);
|
|
62
61
|
if (!branch && !head) return undefined;
|
|
63
|
-
|
|
64
|
-
if (status.code !== 0) return undefined;
|
|
65
|
-
return { branch, head, dirty: status.stdout.trim().length > 0 };
|
|
62
|
+
return { branch, head };
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
return async () => {
|
|
@@ -41,15 +41,14 @@ function iconMarkup(spec: IconSpec, hidden = false): string {
|
|
|
41
41
|
return `<svg class="browse-cat ${spec.colorClass}"${hidden ? " hidden" : ""} aria-hidden="true" title="${escapeHtml(spec.label)}"><use href="${iconHref(spec.icon)}"/></svg>`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** The header's git chip: branch
|
|
44
|
+
/** The header's git chip: the branch, or the detached head; changes are the changes chip's job. */
|
|
45
45
|
function renderGit(git: GitStatus | undefined): string {
|
|
46
46
|
if (!git) return "";
|
|
47
47
|
const name = git.branch ?? git.head;
|
|
48
48
|
if (!name) return "";
|
|
49
49
|
const glyph = git.branch ? GLYPHS.branch : GLYPHS.detached;
|
|
50
|
-
const aria =
|
|
51
|
-
|
|
52
|
-
return `<span class="bf-chip browse-git${git.dirty ? " browse-git--dirty" : ""}" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span>${escapeHtml(name)}${dot}</span>`;
|
|
50
|
+
const aria = git.branch ? `git branch ${git.branch}` : `git detached at ${git.head ?? ""}`;
|
|
51
|
+
return `<span class="bf-chip browse-git" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span>${escapeHtml(name)}</span>`;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
/** The changed-path index for markers, plus the totals shown in the chip. */
|
|
@@ -98,8 +97,6 @@ ${WEB_COMPONENTS_CSS}
|
|
|
98
97
|
.browse-side{padding:var(--space-3)}
|
|
99
98
|
.browse-root{flex:1 1 auto;direction:rtl;text-align:left}
|
|
100
99
|
.browse-git{font-family:var(--font-mono)}
|
|
101
|
-
.browse-git--dirty .browse-git__glyph{color:var(--warn)}
|
|
102
|
-
.browse-dirty{color:var(--warn);font-size:9px;line-height:1}
|
|
103
100
|
.browse-quick-open{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);width:100%;margin-bottom:var(--space-2);padding:6px var(--space-2);border:1px solid var(--border);border-radius:var(--radius-md);background:var(--canvas);color:var(--muted);font:var(--fs-sm) var(--font-ui);cursor:pointer}
|
|
104
101
|
.browse-quick-open:hover{border-color:var(--accent);color:var(--accent)}
|
|
105
102
|
.browse-quick-open kbd{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--dim)}
|
|
@@ -149,7 +146,10 @@ body.palette-open{overflow:hidden}
|
|
|
149
146
|
.bf-diff-line--hunk{color:var(--accent);background:var(--canvas)}
|
|
150
147
|
.bf-diff-line--meta{color:var(--muted)}
|
|
151
148
|
.bf-diff-line--note{color:var(--muted);font-style:italic}
|
|
152
|
-
/* A hunk header pins to the top of the diff viewport while its body scrolls.
|
|
149
|
+
/* A hunk header pins to the top of the diff viewport while its body scrolls.
|
|
150
|
+
Each hunk is a section so the pinned header is bounded by its own body and
|
|
151
|
+
cannot stack on the next one. */
|
|
152
|
+
.bf-diff-hunk{display:block}
|
|
153
153
|
.bf-diff .bf-diff-line--hunk{position:sticky;top:0;z-index:1}
|
|
154
154
|
/* Reading progress, pinned to the sticky header's bottom edge. */
|
|
155
155
|
.browse-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden}
|
|
@@ -575,17 +575,27 @@ function highlightDiffText(lines: DiffLine[], language?: HighlightLanguage): str
|
|
|
575
575
|
/** Render unified-diff lines with a two-column gutter and per-kind styling. */
|
|
576
576
|
export function renderDiffLines(lines: DiffLine[], language?: HighlightLanguage): string {
|
|
577
577
|
const html = highlightDiffText(lines, language);
|
|
578
|
+
// Every hunk is a section so its sticky header is bounded by its own body;
|
|
579
|
+
// the section also gives the keyboard a stable, non-sticky scroll target.
|
|
580
|
+
const out: string[] = [];
|
|
578
581
|
let hunks = 0;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
.
|
|
582
|
+
let open = false;
|
|
583
|
+
lines.forEach((line, index) => {
|
|
584
|
+
if (line.kind === "hunk") {
|
|
585
|
+
if (open) out.push("</span>");
|
|
586
|
+
hunks += 1;
|
|
587
|
+
out.push(`<span class="bf-diff-hunk" id="hunk-${hunks}" data-hunk="${hunks}">`);
|
|
588
|
+
open = true;
|
|
589
|
+
}
|
|
590
|
+
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : line.kind === "ctx" ? " " : "";
|
|
591
|
+
const gutter = `<span class="browse-diff-no" aria-hidden="true">${line.oldNo ?? ""}</span><span class="browse-diff-no" aria-hidden="true">${line.newNo ?? ""}</span>`;
|
|
592
|
+
const marker = `<span class="browse-diff-sign" aria-hidden="true">${sign}</span>`;
|
|
593
|
+
out.push(
|
|
594
|
+
`<span class="bf-code__line bf-diff-line bf-diff-line--${line.kind}">${gutter}${marker}${html[index]}</span>`,
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
if (open) out.push("</span>");
|
|
598
|
+
return out.join("");
|
|
589
599
|
}
|
|
590
600
|
|
|
591
601
|
/** The scope label for a mode. */
|
|
@@ -5,6 +5,12 @@ handler, or reveals it in the file manager. `/open` does it interactively and
|
|
|
5
5
|
the `open_path` tool does it from a script, both resolving the path against the
|
|
6
6
|
effective working directory.
|
|
7
7
|
|
|
8
|
+
## Tool rendering
|
|
9
|
+
|
|
10
|
+
How the `open_path` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
11
|
+
|
|
12
|
+
<img src="../../assets/tools/open_path.png" alt="The open_path tool's call and result, drawn by the generic fallback" width="100%">
|
|
13
|
+
|
|
8
14
|
## Quickstart
|
|
9
15
|
|
|
10
16
|
```bash
|
|
@@ -9,6 +9,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent packa
|
|
|
9
9
|
pi install ./ # install the package
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Tool rendering
|
|
13
|
+
|
|
14
|
+
How the `goal` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
15
|
+
|
|
16
|
+
<img src="../../assets/tools/goal.png" alt="The goal tool's result: the call line and an indented body with the Goal header and the objective body" width="100%">
|
|
17
|
+
|
|
12
18
|
## What it does
|
|
13
19
|
|
|
14
20
|
- Registers one model-callable tool, `goal`, that records the session
|
|
@@ -110,14 +116,13 @@ The tool works in every mode. The persistent widget requires interactive
|
|
|
110
116
|
`/goal <prompt>` runs the loop: each model call uses a cancellable `tui` spinner
|
|
111
117
|
in interactive mode and runs directly in RPC; the questions and the confirm step
|
|
112
118
|
are host dialogs, forwarded by RPC. In print and JSON mode the loop is skipped
|
|
113
|
-
and the typed text is sent. The widget is always a single line; the transcript
|
|
114
|
-
|
|
119
|
+
and the typed text is sent. The widget is always a single line; the transcript
|
|
120
|
+
result is the status header alone, directly under the call line that names the
|
|
121
|
+
objective:
|
|
115
122
|
|
|
116
123
|
```
|
|
117
124
|
goal → set: Refactor the parser to support streaming input
|
|
118
|
-
|
|
119
|
-
Goal · active
|
|
120
|
-
⊚ Refactor the parser to support streaming input and ship it with tests
|
|
125
|
+
Goal · active
|
|
121
126
|
```
|
|
122
127
|
|
|
123
128
|
The renderer reuses the slot's component (`context.lastComponent`) rather than
|
|
@@ -205,9 +210,10 @@ depends on config.
|
|
|
205
210
|
hide all rails and restore them on close.
|
|
206
211
|
- **One-line widget, achieved hidden by default.** The rail follows the
|
|
207
212
|
`todo` grammar (`Goal · active · <objective>`): accent label leads, status
|
|
208
|
-
follows, no prefixed glyph, clipped to the terminal. The transcript
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
follows, no prefixed glyph, clipped to the terminal. The transcript result is
|
|
214
|
+
the status header alone (`Goal · active`), because the call line above it
|
|
215
|
+
already carries the objective; it does not restate it. `achieved: "show"`
|
|
216
|
+
restores the dimmed widget line; config only affects presentation.
|
|
211
217
|
- **Sanitize at the boundary.** `normalizeGoal` and `parseClarification` both route
|
|
212
218
|
model text through `lib/format.ts`'s `sanitizeLine`, so an embedded newline
|
|
213
219
|
cannot corrupt the widget and raw escapes cannot restyle the terminal.
|
|
@@ -223,9 +229,9 @@ depends on config.
|
|
|
223
229
|
| `clarify-parse.ts` | Parse the `{ understood, goal, questions }` reply (and the plain-text fallback), normalize questions, and expose the limits (pure). |
|
|
224
230
|
| `clarify.ts` | The two model calls, their cancellable loaders, the in-command questionnaire, and the confirm/edit dialog (host). |
|
|
225
231
|
| `state.ts` | Branch reconstruction (pure). |
|
|
226
|
-
| `format.ts` | Model-facing and terminal text plus the
|
|
232
|
+
| `format.ts` | Model-facing and terminal text plus the header and rail vocabulary (pure). |
|
|
227
233
|
| `config.ts` | Widget config load/validation (the `goal` section of `bytefly.json`). |
|
|
228
|
-
| `tui.ts` | The persistent goal widget and the transcript
|
|
234
|
+
| `tui.ts` | The persistent goal widget and the transcript status header. |
|
|
229
235
|
| `runtime.ts` | Session-scoped state and widget synchronization. |
|
|
230
236
|
| `tools.ts` | `goal` tool registration and rendering. |
|
|
231
237
|
| `commands.ts` | `/goal`. |
|
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model-facing and transcript text for the goal (pure).
|
|
3
3
|
*
|
|
4
|
-
* The transcript result is
|
|
5
|
-
* line
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
* (`Goal · active · <objective>`) matching the `todo` and `jobs` widgets. The
|
|
9
|
-
* symbols and labels live here; the rail indent constants are shared with
|
|
10
|
-
* `todo` in `lib/tui/glyphs.ts`.
|
|
4
|
+
* The transcript result is the status header (`Goal · active`) under the call
|
|
5
|
+
* line, which already names the objective; the persistent widget is a plain
|
|
6
|
+
* label-first line (`Goal · active · <objective>`) matching the `todo` and
|
|
7
|
+
* `jobs` widgets. The status labels live here.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
14
11
|
import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
-
import {
|
|
12
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
13
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
14
|
import type { Goal, GoalStatus } from "./types.ts";
|
|
18
15
|
|
|
19
|
-
/** Active-goal glyph. */
|
|
20
|
-
export const ACTIVE_SYMBOL = GLYPHS.target;
|
|
21
|
-
/** Achieved-goal glyph. */
|
|
22
|
-
export const ACHIEVED_SYMBOL = GLYPHS.check;
|
|
23
|
-
|
|
24
16
|
/** Longest objective preview shown in the transcript call line. */
|
|
25
17
|
const CALL_PREVIEW_WIDTH = 48;
|
|
26
18
|
|
|
27
|
-
/** Themed status glyph, shared by the widget and the transcript renderer. */
|
|
28
|
-
export function goalGlyph(goal: Goal, theme: Theme): string {
|
|
29
|
-
return goal.status === "achieved" ? paint(theme, "ok", ACHIEVED_SYMBOL) : paint(theme, "accent", ACTIVE_SYMBOL);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
19
|
/** Human label for a status, e.g. `active`. */
|
|
33
20
|
export function goalStatusLabel(status: GoalStatus): string {
|
|
34
21
|
return status === "achieved" ? "achieved" : "active";
|
|
@@ -40,7 +27,7 @@ export function goalStatusLabel(status: GoalStatus): string {
|
|
|
40
27
|
*/
|
|
41
28
|
export function goalHeader(goal: Goal, theme: Theme): string {
|
|
42
29
|
const label = paint(theme, "accent", "Goal");
|
|
43
|
-
const separator = paint(theme, "faint",
|
|
30
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
44
31
|
const word = goalStatusLabel(goal.status);
|
|
45
32
|
const status = goal.status === "achieved" ? paint(theme, "ok", word) : paint(theme, "faint", word);
|
|
46
33
|
return `${label}${separator}${status}`;
|
|
@@ -53,7 +40,7 @@ export function goalHeader(goal: Goal, theme: Theme): string {
|
|
|
53
40
|
* Callers clip it to the available width.
|
|
54
41
|
*/
|
|
55
42
|
export function goalLine(goal: Goal, theme: Theme): string {
|
|
56
|
-
const separator = paint(theme, "faint",
|
|
43
|
+
const separator = paint(theme, "faint", SEPARATORS.item);
|
|
57
44
|
const word = goalStatusLabel(goal.status);
|
|
58
45
|
const status = goal.status === "achieved" ? paint(theme, "ok", word) : paint(theme, "faint", word);
|
|
59
46
|
return `${paint(theme, "accent", "Goal")}${separator}${status}${separator}${goalObjective(goal, theme)}`;
|
package/extensions/goal/tools.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
11
|
-
import type { ExtensionAPI
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
13
|
import { GOAL_TOOL } from "../../lib/tool-names.ts";
|
|
14
14
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
15
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
16
16
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
17
|
-
import { formatCallText, formatGoalText
|
|
17
|
+
import { formatCallText, formatGoalText } from "./format.ts";
|
|
18
18
|
import type { GoalRuntime } from "./runtime.ts";
|
|
19
19
|
import { GoalParams, GoalResult, normalizeGoal, type GoalArgs } from "./schema.ts";
|
|
20
20
|
import { GoalResult as GoalResultView } from "./tui.ts";
|
|
@@ -22,9 +22,6 @@ import type { GoalAction, GoalDetails } from "./types.ts";
|
|
|
22
22
|
|
|
23
23
|
export const TOOL_NAME = GOAL_TOOL;
|
|
24
24
|
|
|
25
|
-
/** Longest objective shown in an unexpanded transcript result. */
|
|
26
|
-
const RESULT_PREVIEW_WIDTH = 72;
|
|
27
|
-
|
|
28
25
|
/** Best-effort action of a rejected call, for the error result's details. */
|
|
29
26
|
function attemptedAction(args: Partial<GoalArgs>): GoalAction {
|
|
30
27
|
if (args.status === "achieved") return "achieve";
|
|
@@ -32,14 +29,6 @@ function attemptedAction(args: Partial<GoalArgs>): GoalAction {
|
|
|
32
29
|
return "set";
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
/** Themed body for the transcript result: the collapsed preview or the full objective. */
|
|
36
|
-
function resultBody(goal: NonNullable<GoalDetails["goal"]>, expanded: boolean, theme: Theme): string {
|
|
37
|
-
const text = expanded ? goal.objective : previewObjective(goal.objective, RESULT_PREVIEW_WIDTH);
|
|
38
|
-
// Route both states through `goalObjective`, so an achieved goal is dimmed
|
|
39
|
-
// whether it is collapsed or expanded.
|
|
40
|
-
return goalObjective({ objective: text, status: goal.status }, theme);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
32
|
/**
|
|
44
33
|
* JSON-safe mirror of the goal for `structuredContent`.
|
|
45
34
|
*
|
|
@@ -108,7 +97,7 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
108
97
|
return text;
|
|
109
98
|
},
|
|
110
99
|
|
|
111
|
-
renderResult(result,
|
|
100
|
+
renderResult(result, _options, theme, context) {
|
|
112
101
|
const text = reuseText(context.lastComponent);
|
|
113
102
|
const details = toolDetails<GoalDetails>(result.details);
|
|
114
103
|
if (!details) {
|
|
@@ -125,12 +114,11 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
125
114
|
text.setText(paint(theme, "faint", `${GLYPHS.stopped} `) + paint(theme, "meta", "Cleared the goal"));
|
|
126
115
|
return text;
|
|
127
116
|
}
|
|
128
|
-
const body = resultBody(details.goal, expanded, theme);
|
|
129
117
|
const view =
|
|
130
118
|
context.lastComponent instanceof GoalResultView
|
|
131
119
|
? context.lastComponent
|
|
132
|
-
: new GoalResultView(details.goal,
|
|
133
|
-
view.update(details.goal,
|
|
120
|
+
: new GoalResultView(details.goal, theme);
|
|
121
|
+
view.update(details.goal, theme);
|
|
134
122
|
return view;
|
|
135
123
|
},
|
|
136
124
|
});
|