@gavin-hu/my-pi-agent 0.15.1 → 0.17.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 +22 -13
- package/README.zh-CN.md +19 -11
- package/extensions/bytefly/README.md +255 -200
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +52 -22
- package/extensions/bytefly/panel/join.ts +36 -15
- package/extensions/bytefly/panel/settings.ts +72 -392
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -58
- package/extensions/bytefly/panel/tui/frame.ts +11 -3
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -333
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +22 -10
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +172 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +84 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +332 -0
- package/extensions/context/tui.ts +284 -0
- package/extensions/context/types.ts +124 -0
- package/extensions/file-browse/README.md +67 -19
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +9 -7
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/server.ts +13 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +76 -57
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +10 -8
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +16 -25
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/index.ts +20 -45
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +58 -103
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +10 -20
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +16 -17
- package/extensions/plan/tools/exit.ts +60 -50
- package/extensions/plan/tools/steps.ts +13 -26
- package/extensions/plan/tools/write.ts +30 -38
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/plan/web.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/background.ts +1 -7
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +15 -20
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +38 -77
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +7 -18
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +15 -11
- package/lib/config.ts +12 -0
- package/lib/escape.ts +15 -2
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/pdf.ts +2 -2
- package/lib/report.ts +75 -2
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +19 -11
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +10 -4
- package/skills/changelog/SKILL.md +148 -0
- package/skills/next/SKILL.md +184 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
package/lib/timer.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer seams: one shot, repeating, and a sleep that can be aborted.
|
|
3
|
+
*
|
|
4
|
+
* Extensions need the same three things, and each has the same two properties a
|
|
5
|
+
* test cares about: it is cancellable, and its production default never holds
|
|
6
|
+
* the process open. The WeChat bridge arms a one-shot per acknowledgement,
|
|
7
|
+
* typing keep-alive, and prompt timeout; plan's review page arms one for the
|
|
8
|
+
* closed-tab grace; `job` and `watch` repaint on a repeating one; the browse
|
|
9
|
+
* live channel needs both; `job`, `watch`, `check`, `subagent`, and `wechat`
|
|
10
|
+
* sleep between polls. Keeping the defaults here means one definition of "arm
|
|
11
|
+
* it, and let it be cancelled", and one place where the `unref` lives.
|
|
12
|
+
*
|
|
13
|
+
* Value-only: types and constant functions, no module state.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Arm a one-shot timer; the returned function cancels it. */
|
|
17
|
+
export type ArmTimer = (ms: number, fn: () => void) => () => void;
|
|
18
|
+
|
|
19
|
+
/** The production timer: `setTimeout`, detached from the event loop. */
|
|
20
|
+
export const defaultTimer: ArmTimer = (ms, fn) => {
|
|
21
|
+
const timer = setTimeout(fn, ms);
|
|
22
|
+
(timer as { unref?: () => void }).unref?.();
|
|
23
|
+
return () => clearTimeout(timer);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Arm a repeating timer; the returned function stops it. */
|
|
27
|
+
export type RepeatTimer = (ms: number, tick: () => void) => () => void;
|
|
28
|
+
|
|
29
|
+
/** The production ticker: `setInterval`, detached from the event loop. */
|
|
30
|
+
export const defaultInterval: RepeatTimer = (ms, tick) => {
|
|
31
|
+
const timer = setInterval(tick, ms);
|
|
32
|
+
(timer as { unref?: () => void }).unref?.();
|
|
33
|
+
return () => clearInterval(timer);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve after `ms`, or reject when `signal` aborts first.
|
|
38
|
+
*
|
|
39
|
+
* The one rejection is `Error("aborted")` whatever the signal carries, so a
|
|
40
|
+
* caller that names its own abort error maps one case rather than inspecting a
|
|
41
|
+
* reason. The timer is cleared on both paths and never `unref`'d: a sleep exists
|
|
42
|
+
* to keep the caller waiting, so nothing else is pending to keep the process
|
|
43
|
+
* alive, and an unref'd one could be dropped mid-wait.
|
|
44
|
+
*/
|
|
45
|
+
export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
reject(new Error("aborted"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
signal?.removeEventListener("abort", onAbort);
|
|
53
|
+
resolve();
|
|
54
|
+
}, ms);
|
|
55
|
+
const onAbort = (): void => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
reject(new Error("aborted"));
|
|
58
|
+
};
|
|
59
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
60
|
+
});
|
|
61
|
+
}
|
package/lib/tool-result.ts
CHANGED
|
@@ -1,17 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared shape of a
|
|
2
|
+
* The shared shape of a tool result.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every extension tool returns the same envelope: one text block the model
|
|
5
|
+
* reads, the extension's own `details` for the renderer and the session branch,
|
|
6
|
+
* and — where the tool declares an `outputSchema` — the JSON mirror Pi calls
|
|
7
|
+
* `structuredContent`. A rejected call is that envelope marked `isError`, still
|
|
8
|
+
* carrying the previous state so the model can retry.
|
|
9
|
+
*
|
|
10
|
+
* `textResult` and `failedResult` build the two, so the flag and the absent
|
|
11
|
+
* `structuredContent` are decided once. What stays with the extension is the
|
|
12
|
+
* text and the mirror: `structuredContent` drops `undefined` field by field and
|
|
13
|
+
* projects nested objects, so it is written per `details` type rather than
|
|
14
|
+
* derived. `toolError` / `errorResult` keep the shorthand the
|
|
15
|
+
* action-and-message tools (`job` and `watch`) share.
|
|
7
16
|
*/
|
|
8
17
|
|
|
18
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
19
|
+
|
|
9
20
|
/** One text block in a tool result. */
|
|
10
21
|
export interface ToolErrorContent {
|
|
11
22
|
type: "text";
|
|
12
23
|
text: string;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
/** A tool result: what the model reads, the extension's details, and its JSON mirror. */
|
|
27
|
+
export interface ToolResult<Details> {
|
|
28
|
+
content: ToolErrorContent[];
|
|
29
|
+
details: Details;
|
|
30
|
+
structuredContent?: Record<string, JsonValue>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A rejected call: the result, marked so the host reports it as an error. */
|
|
34
|
+
export interface ToolFailure<Details> extends ToolResult<Details> {
|
|
35
|
+
isError: true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The content and details both results share. */
|
|
39
|
+
function envelope<Details>(text: string, details: Details): ToolResult<Details> {
|
|
40
|
+
return { content: [{ type: "text", text }], details };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A successful call. */
|
|
44
|
+
export function textResult<Details>(
|
|
45
|
+
text: string,
|
|
46
|
+
details: Details,
|
|
47
|
+
structuredContent?: Record<string, JsonValue>,
|
|
48
|
+
): ToolResult<Details> {
|
|
49
|
+
const result = envelope(text, details);
|
|
50
|
+
return structuredContent === undefined ? result : { ...result, structuredContent };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A rejected call, keeping the state the caller had before it. */
|
|
54
|
+
export function failedResult<Details>(
|
|
55
|
+
text: string,
|
|
56
|
+
details: Details,
|
|
57
|
+
structuredContent?: Record<string, JsonValue>,
|
|
58
|
+
): ToolFailure<Details> {
|
|
59
|
+
const result = envelope(text, details);
|
|
60
|
+
return structuredContent === undefined
|
|
61
|
+
? { ...result, isError: true }
|
|
62
|
+
: { ...result, structuredContent, isError: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
/** A rejected tool call: the message as content, the extension's details, and the error flag. */
|
|
16
66
|
export function toolError<Details extends { action: string; error: string }>(
|
|
17
67
|
message: string,
|
package/lib/tui/README.md
CHANGED
|
@@ -35,17 +35,24 @@ Reach for a `lib/tui/` primitive before composing your own: `plainRule` /
|
|
|
35
35
|
`fitCells` for two columns, `LinePane` for a scrollable log, `ListCursor` /
|
|
36
36
|
`IdCursor` for selection, `byRunningFirst` for a live list's order,
|
|
37
37
|
`DockListScreen` for a list that opens a pane, `TailResult` for a tool result
|
|
38
|
-
that lists rows or tails output,
|
|
38
|
+
that lists rows or tails output, `screenFooter` for a screen's own last three
|
|
39
|
+
rows, and `ConfirmState` before a destructive action.
|
|
39
40
|
|
|
40
41
|
## Docked list screens
|
|
41
42
|
|
|
42
43
|
Every list screen is `dockListFrame`: the header rule, an optional summary and
|
|
43
|
-
its blank line, the body, an optional range row, an optional detail block,
|
|
44
|
-
the
|
|
45
|
-
it — including the blank line above the detail block, which
|
|
46
|
-
from reading as one more row. A screen that sizes its body
|
|
47
|
-
counts the block with `detailBlockRows`, or the hint row is
|
|
48
|
-
bottom of a short terminal.
|
|
44
|
+
its blank line, the body, an optional range row, an optional detail block, the
|
|
45
|
+
key-hint row, and the closing rule. The frame owns that spacing, so a screen
|
|
46
|
+
never hand-rolls it — including the blank line above the detail block, which
|
|
47
|
+
keeps a footnote from reading as one more row. A screen that sizes its body
|
|
48
|
+
through `fitRows` counts the block with `detailBlockRows`, or the hint row is
|
|
49
|
+
pushed off the bottom of a short terminal.
|
|
50
|
+
|
|
51
|
+
The closing rule is not decoration: Pi's built-in selectors draw one under the
|
|
52
|
+
list and over the footer, and the editor a docked screen replaces draws its own,
|
|
53
|
+
so a screen that ends on a blank removes the dock's bottom edge. A screen that
|
|
54
|
+
lays out its own rows closes its block with `screenFooter(theme, width, hint)`;
|
|
55
|
+
a screen whose row above the hint varies places `plainRule` itself.
|
|
49
56
|
|
|
50
57
|
A list that **opens a pane over one row** — `/jobs`, `/watch`, `/agents`,
|
|
51
58
|
`/check` — subclasses [`dock-screen.ts`](./dock-screen.ts) instead of
|
|
@@ -92,7 +99,8 @@ Resolve colors through a [`tokens.ts`](./tokens.ts) role
|
|
|
92
99
|
(`paint(theme, role, text)`), never a literal ANSI sequence, hex value, or Pi
|
|
93
100
|
token named inline; a theme swap must re-skin every surface. Use the shared
|
|
94
101
|
`selectionMarker`, `GLYPHS`, `SEPARATORS`, and `STATUS_KEYS` instead of retyping
|
|
95
|
-
their literals,
|
|
102
|
+
their literals, `screenHint` / Pi's `keyHint` for the hint row, and
|
|
103
|
+
`screenFooter` for the hint row with its blank and closing rule.
|
|
96
104
|
|
|
97
105
|
A list accents the focused row's label (`labelRole: selected ? "accent" :
|
|
98
106
|
"label"`); the `❯` marker alone is not enough to tell which row is live. The
|
|
@@ -151,6 +159,6 @@ and the first call throws at runtime.
|
|
|
151
159
|
|
|
152
160
|
## Tests
|
|
153
161
|
|
|
154
|
-
A
|
|
155
|
-
ordering, and glyphs — never literal column padding. An extension's
|
|
156
|
-
|
|
162
|
+
A capability suite groups the modules it covers and asserts fields, width
|
|
163
|
+
bounds, ordering, and glyphs — never literal column padding. An extension's
|
|
164
|
+
screen coverage lives in its declared capability suite (for example `ui.test.ts`).
|
package/lib/tui/chrome.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* the transcript visible.
|
|
9
9
|
*
|
|
10
10
|
* Screens are full-width, terminal-height-aware components. These helpers build
|
|
11
|
-
* the top rule, clamp a screen's body to the terminal, and
|
|
12
|
-
* footer, so each screen keeps its own layout but not its
|
|
13
|
-
* arithmetic or hint vocabulary.
|
|
11
|
+
* the top rule and the closing rule, clamp a screen's body to the terminal, and
|
|
12
|
+
* render the key-hint footer, so each screen keeps its own layout but not its
|
|
13
|
+
* own copy of the arithmetic or hint vocabulary.
|
|
14
14
|
*
|
|
15
15
|
* No runtime dependencies beyond `@earendil-works/pi-tui`: the theme is passed
|
|
16
16
|
* in, and the viewport options are explicit.
|
|
@@ -20,6 +20,7 @@ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
20
20
|
import { Text, truncateToWidth, visibleWidth, type EditorTheme, type OverlayOptions } from "@earendil-works/pi-tui";
|
|
21
21
|
import { SEPARATORS } from "./glyphs.ts";
|
|
22
22
|
import { BODY_INDENT } from "./indent.ts";
|
|
23
|
+
import { plainRule } from "./rules.ts";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* `ctx.ui.custom` options that give a screen the whole terminal.
|
|
@@ -88,12 +89,29 @@ export function screenHint(theme: Theme, width: number, hints: ScreenHint[], ind
|
|
|
88
89
|
return truncateToWidth(`${lead}${kept.map(render).join(separator)}`, target);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bottom of a screen: a blank row, its key-hint row, and the closing rule.
|
|
94
|
+
*
|
|
95
|
+
* {@link dockListFrame} ends a docked list screen with this, and a screen that
|
|
96
|
+
* lays out its own rows — `/context`, the WeChat login and control screens —
|
|
97
|
+
* closes its block with it too, so no screen spells the three rows out alone. A
|
|
98
|
+
* screen whose row above the hint varies (a scroll gauge or a blank) places its
|
|
99
|
+
* own rule with `plainRule` instead.
|
|
100
|
+
*/
|
|
101
|
+
export function screenFooter(theme: Theme, width: number, hint: string): string[] {
|
|
102
|
+
return ["", hint, plainRule(theme, width)];
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* The sections of a docked list screen, assembled by {@link dockListFrame}.
|
|
93
107
|
* `body`, `range`, and `detail` lines are built (and truncated) by the caller;
|
|
94
108
|
* the frame only places them.
|
|
95
109
|
*/
|
|
96
110
|
export interface DockListFrame {
|
|
111
|
+
/** The active theme, used for the closing rule. */
|
|
112
|
+
theme: Theme;
|
|
113
|
+
/** The screen width in columns, used for the closing rule. */
|
|
114
|
+
width: number;
|
|
97
115
|
/** The pre-rendered header line, usually `titleRule(theme, width, title)`. */
|
|
98
116
|
header: string;
|
|
99
117
|
/** A summary line under the header; a blank line follows it. */
|
|
@@ -132,11 +150,14 @@ export function detailBlockRows(detail: readonly string[] | undefined, blank = t
|
|
|
132
150
|
/**
|
|
133
151
|
* Assemble a docked list screen: the caller's header rule, an optional summary
|
|
134
152
|
* and its blank line, the body, an optional range row, optional detail rows after
|
|
135
|
-
* their own blank line,
|
|
153
|
+
* their own blank line, the key-hint row, and the closing rule.
|
|
136
154
|
*
|
|
137
155
|
* This is the convention every docked list screen shares — the bytefly panel,
|
|
138
156
|
* `/jobs`, `/watch`, `/todos`, `/memory`, and `/rewind`. The header line is built
|
|
139
|
-
* by the caller (usually `titleRule`), so the frame never picks
|
|
157
|
+
* by the caller (usually `titleRule`), so the frame never picks the *opening*
|
|
158
|
+
* rule style; the closing rule is always the muted fill, matching the closing
|
|
159
|
+
* rule Pi's built-in selectors draw and the bottom border of the editor slot the
|
|
160
|
+
* screen replaces.
|
|
140
161
|
*/
|
|
141
162
|
export function dockListFrame(frame: DockListFrame): string[] {
|
|
142
163
|
const lines: string[] = [frame.header];
|
|
@@ -147,7 +168,7 @@ export function dockListFrame(frame: DockListFrame): string[] {
|
|
|
147
168
|
if (frame.detailBlank !== false) lines.push("");
|
|
148
169
|
lines.push(...frame.detail);
|
|
149
170
|
}
|
|
150
|
-
lines.push(
|
|
171
|
+
lines.push(...screenFooter(frame.theme, frame.width, frame.hint));
|
|
151
172
|
return lines;
|
|
152
173
|
}
|
|
153
174
|
|
package/lib/tui/columns.ts
CHANGED
|
@@ -15,14 +15,17 @@ import { sanitizeLine, sanitizeUntrustedLine } from "../format.ts";
|
|
|
15
15
|
/** The columns a label gets before it stops reading as a label. */
|
|
16
16
|
export const LABEL_WIDTH = 48;
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A usable drawing width in columns: terminal arithmetic starts from one column,
|
|
20
|
+
* never zero, so a narrow or unknown width never produces an empty frame.
|
|
21
|
+
*/
|
|
22
|
+
export function usableWidth(width: number): number {
|
|
20
23
|
return Math.max(1, Math.floor(width));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/** Clip a possibly-themed string to `width` columns, marking a cut with `ellipsis`. */
|
|
24
27
|
export function clip(text: string, width: number, ellipsis = "…"): string {
|
|
25
|
-
return truncateToWidth(text,
|
|
28
|
+
return truncateToWidth(text, usableWidth(width), ellipsis);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -34,7 +37,7 @@ export function clip(text: string, width: number, ellipsis = "…"): string {
|
|
|
34
37
|
* A themed string keeps its colours on both the head and the tail.
|
|
35
38
|
*/
|
|
36
39
|
export function clipMiddle(text: string, width: number, ellipsis = "…"): string {
|
|
37
|
-
const w =
|
|
40
|
+
const w = usableWidth(width);
|
|
38
41
|
if (visibleWidth(text) <= w) return text;
|
|
39
42
|
const ellipsisWidth = visibleWidth(ellipsis);
|
|
40
43
|
const budget = w - ellipsisWidth;
|
|
@@ -69,7 +72,7 @@ export function clipLabel(text: string): string {
|
|
|
69
72
|
|
|
70
73
|
/** Pad `text` on the right to exactly `width` columns. Never truncates. */
|
|
71
74
|
export function padTo(text: string, width: number): string {
|
|
72
|
-
const missing =
|
|
75
|
+
const missing = usableWidth(width) - visibleWidth(text);
|
|
73
76
|
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -83,7 +86,7 @@ export function padTo(text: string, width: number): string {
|
|
|
83
86
|
* plain left-aligned line with no trailing spaces.
|
|
84
87
|
*/
|
|
85
88
|
export function splitRow(left: string, right: string, width: number, minGap = 1): string {
|
|
86
|
-
const w =
|
|
89
|
+
const w = usableWidth(width);
|
|
87
90
|
if (!right) return clip(left, w);
|
|
88
91
|
const gap = Math.max(0, Math.floor(minGap));
|
|
89
92
|
const rightCell = clip(right, w);
|
|
@@ -93,13 +96,35 @@ export function splitRow(left: string, right: string, width: number, minGap = 1)
|
|
|
93
96
|
return padTo(clip(left, leftBudget), w - rightWidth) + rightCell;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Left cell, then a gap, then the right cell, with the result ending after the
|
|
101
|
+
* right cell instead of filling the width.
|
|
102
|
+
*
|
|
103
|
+
* The sibling of {@link splitRow}: a label and its value read as one pair on a
|
|
104
|
+
* wide terminal instead of sitting at opposite edges, while a tight line still
|
|
105
|
+
* keeps the value — the left cell is clipped to make room, and the gap shrinks
|
|
106
|
+
* in whole columns before the right cell loses anything.
|
|
107
|
+
*/
|
|
108
|
+
export function inlineRow(left: string, right: string, width: number, gap = 2): string {
|
|
109
|
+
const w = usableWidth(width);
|
|
110
|
+
if (!right) return clip(left, w);
|
|
111
|
+
const space = Math.max(0, Math.floor(gap));
|
|
112
|
+
const rightCell = clip(right, w);
|
|
113
|
+
const rightWidth = visibleWidth(rightCell);
|
|
114
|
+
const leftBudget = w - rightWidth - space;
|
|
115
|
+
if (leftBudget <= 0) return rightCell;
|
|
116
|
+
const leftCell = clip(left, leftBudget);
|
|
117
|
+
const separator = " ".repeat(Math.max(0, Math.min(space, w - visibleWidth(leftCell) - rightWidth)));
|
|
118
|
+
return `${leftCell}${separator}${rightCell}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
/**
|
|
97
122
|
* Join as many whole cells as fit, dropping trailing cells rather than
|
|
98
123
|
* clipping one in half. The first cell is always kept, so an over-wide line
|
|
99
124
|
* degrades to a clipped first cell instead of an empty one.
|
|
100
125
|
*/
|
|
101
126
|
export function fitCells(cells: readonly string[], width: number, separator = " "): string {
|
|
102
|
-
const w =
|
|
127
|
+
const w = usableWidth(width);
|
|
103
128
|
const kept: string[] = [];
|
|
104
129
|
for (const cell of cells) {
|
|
105
130
|
const candidate = [...kept, cell].join(separator);
|
|
@@ -118,7 +143,7 @@ export function fitCells(cells: readonly string[], width: number, separator = "
|
|
|
118
143
|
* {@link fitCells} it may return several lines, and always returns one.
|
|
119
144
|
*/
|
|
120
145
|
export function flowCells(cells: readonly string[], width: number): string[] {
|
|
121
|
-
const w =
|
|
146
|
+
const w = usableWidth(width);
|
|
122
147
|
const lines: string[] = [];
|
|
123
148
|
let current = "";
|
|
124
149
|
let currentWidth = 0;
|
package/lib/tui/dock-screen.ts
CHANGED
|
@@ -33,10 +33,10 @@ import { paint } from "./tokens.ts";
|
|
|
33
33
|
/** Rows the body shows when the terminal height is unknown. */
|
|
34
34
|
const SCREEN_DEFAULT_ROWS = 14;
|
|
35
35
|
|
|
36
|
-
/** Fixed rows around the list body: header, summary, blank, blank, hint,
|
|
36
|
+
/** Fixed rows around the list body: header, summary, blank, blank, hint, closing rule. */
|
|
37
37
|
const LIST_CHROME_ROWS = 6;
|
|
38
38
|
|
|
39
|
-
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint,
|
|
39
|
+
/** Fixed rows around a detail body: header, subtitle, blank, blank, hint, closing rule. */
|
|
40
40
|
const DETAIL_CHROME_ROWS = 6;
|
|
41
41
|
|
|
42
42
|
/** What a detail pane shows: its title, lines, and the one-line subtitle. */
|
|
@@ -353,6 +353,8 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
return dockListFrame({
|
|
356
|
+
theme: this.theme,
|
|
357
|
+
width: w,
|
|
356
358
|
header: titleRule(this.theme, w, this.options.title),
|
|
357
359
|
summary: this.summary(rows, w),
|
|
358
360
|
body,
|
|
@@ -389,6 +391,8 @@ export abstract class DockListScreen<T> implements Component {
|
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
return dockListFrame({
|
|
394
|
+
theme: this.theme,
|
|
395
|
+
width: w,
|
|
392
396
|
header: titleRule(this.theme, w, this.pane.title(this.options.detailTitle)),
|
|
393
397
|
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
394
398
|
body,
|
package/lib/tui/gauge.ts
CHANGED
|
@@ -26,16 +26,20 @@ export const DEFAULT_GAUGE_THRESHOLDS: GaugeThresholds = { warn: 70, danger: 90
|
|
|
26
26
|
/**
|
|
27
27
|
* Filled and empty block counts for a percentage across `blocks` cells.
|
|
28
28
|
*
|
|
29
|
-
* The fill
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* The fill rounds to the nearest cell, so a bar sits no more than half a cell
|
|
30
|
+
* away from the number printed beside it, and the last cell is kept for 100% so
|
|
31
|
+
* a bar reads full only at a full value. Any non-zero usage still forces one
|
|
32
|
+
* block, so a small percentage stays visible. A `null` percent (unknown) fills
|
|
33
|
+
* nothing, leaving the caller to decide whether to draw the empty track; a known
|
|
34
|
+
* `0` draws it.
|
|
33
35
|
*/
|
|
34
36
|
export function computeGauge(percent: number | null, blocks: number): { filled: number; empty: number } {
|
|
35
37
|
if (blocks <= 0) return { filled: 0, empty: 0 };
|
|
36
38
|
if (percent === null) return { filled: 0, empty: blocks };
|
|
37
39
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
38
|
-
const
|
|
40
|
+
const nearest = Math.round((clamped / 100) * blocks);
|
|
41
|
+
const capped = clamped < 100 ? Math.min(blocks - 1, nearest) : nearest;
|
|
42
|
+
const filled = Math.max(clamped > 0 ? 1 : 0, capped);
|
|
39
43
|
return { filled, empty: blocks - filled };
|
|
40
44
|
}
|
|
41
45
|
|
package/lib/tui/rows.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { clip, padTo, splitRow } from "./columns.ts";
|
|
16
|
+
import { clip, inlineRow, padTo, splitRow } from "./columns.ts";
|
|
17
17
|
import { selectionMarker } from "./cursor.ts";
|
|
18
18
|
import { GLYPHS } from "./glyphs.ts";
|
|
19
19
|
import { BODY_INDENT } from "./indent.ts";
|
|
@@ -41,21 +41,29 @@ export interface ListRow {
|
|
|
41
41
|
/** The row's text. */
|
|
42
42
|
label: string;
|
|
43
43
|
labelRole?: TuiRole;
|
|
44
|
-
/**
|
|
44
|
+
/** Meta that closes the row: right-aligned, or beside the label when `align` is `inline`. */
|
|
45
45
|
trailing?: string;
|
|
46
46
|
trailingRole?: TuiRole;
|
|
47
|
+
/**
|
|
48
|
+
* Where the trailing cell sits. `end` (the default) pushes it to the right
|
|
49
|
+
* edge, which suits a column of counts. `inline` keeps it beside the label
|
|
50
|
+
* with a two-column gap, so a label and its value read as one pair on a wide
|
|
51
|
+
* terminal instead of sitting at opposite ends of the line.
|
|
52
|
+
*/
|
|
53
|
+
align?: "end" | "inline";
|
|
47
54
|
/** Extra columns before the marker, for a nested row. */
|
|
48
55
|
indent?: number;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* One list row: an optional indent, the selection marker, an optional glyph,
|
|
53
|
-
* the label, and
|
|
60
|
+
* the label, and the trailing meta.
|
|
54
61
|
*
|
|
55
62
|
* The marker column is reserved by default, so selected and unselected rows
|
|
56
63
|
* align; `marker: false` drops it for a non-interactive row. The trailing meta
|
|
57
|
-
* is the anchor when the terminal is narrow (see `splitRow`),
|
|
58
|
-
* end of the row survives
|
|
64
|
+
* is the anchor when the terminal is narrow (see `splitRow` and `inlineRow`),
|
|
65
|
+
* so a value at the end of the row survives either way; `align` decides whether
|
|
66
|
+
* it sits at the right edge or right after the label.
|
|
59
67
|
*/
|
|
60
68
|
export function listRow(theme: Theme, width: number, row: ListRow): string {
|
|
61
69
|
const indent = " ".repeat(Math.max(0, Math.floor(row.indent ?? 0)));
|
|
@@ -64,7 +72,8 @@ export function listRow(theme: Theme, width: number, row: ListRow): string {
|
|
|
64
72
|
const glyph = row.glyph ? `${paint(theme, row.glyphRole ?? "info", row.glyph)} ` : "";
|
|
65
73
|
const label = paint(theme, row.labelRole ?? "label", row.label);
|
|
66
74
|
const trailing = row.trailing ? paint(theme, row.trailingRole ?? "meta", row.trailing) : "";
|
|
67
|
-
|
|
75
|
+
const head = `${indent}${marker}${leading}${glyph}${label}`;
|
|
76
|
+
return row.align === "inline" ? inlineRow(head, trailing, width) : splitRow(head, trailing, width);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/** A status row (a job, a watcher): the marker, a status glyph, a label, and right-aligned meta. */
|
package/lib/tui/rules.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
+
import { usableWidth } from "./columns.ts";
|
|
18
19
|
import { paint, type TuiRole } from "./tokens.ts";
|
|
19
20
|
|
|
20
21
|
/** The glyph and colors a rule is drawn with. */
|
|
@@ -39,14 +40,9 @@ export interface CenteredRuleStyle extends RuleStyle {
|
|
|
39
40
|
*/
|
|
40
41
|
export const RULE_GLYPH = "─";
|
|
41
42
|
|
|
42
|
-
/** A usable origin: terminal arithmetic starts from one column, never zero. */
|
|
43
|
-
function widthOf(width: number): number {
|
|
44
|
-
return Math.max(1, Math.floor(width));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
43
|
/** A full-width divider in one color. */
|
|
48
44
|
export function plainRule(theme: Theme, width: number, style: RuleStyle = {}): string {
|
|
49
|
-
const w =
|
|
45
|
+
const w = usableWidth(width);
|
|
50
46
|
return paint(theme, style.fillRole ?? "rule", (style.glyph ?? RULE_GLYPH).repeat(w));
|
|
51
47
|
}
|
|
52
48
|
|
|
@@ -64,7 +60,7 @@ function labelRule(
|
|
|
64
60
|
fillRole: TuiRole,
|
|
65
61
|
style: RuleStyle,
|
|
66
62
|
): string {
|
|
67
|
-
const w =
|
|
63
|
+
const w = usableWidth(width);
|
|
68
64
|
const text = label.trim();
|
|
69
65
|
if (!text) return plainRule(theme, w, style);
|
|
70
66
|
const labelWidth = visibleWidth(text);
|
|
@@ -75,7 +71,7 @@ function labelRule(
|
|
|
75
71
|
|
|
76
72
|
/** A centered rule: fill, label, and the rest of the fill. */
|
|
77
73
|
export function centeredRule(theme: Theme, width: number, label: string, style: CenteredRuleStyle = {}): string {
|
|
78
|
-
const w =
|
|
74
|
+
const w = usableWidth(width);
|
|
79
75
|
const glyph = style.glyph ?? RULE_GLYPH;
|
|
80
76
|
const fillRole = style.fillRole ?? "ruleStrong";
|
|
81
77
|
if (!label.trim()) return paint(theme, fillRole, glyph.repeat(w));
|
package/lib/tui/tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared tool-transcript kit: a call title, a
|
|
3
|
-
*
|
|
2
|
+
* The shared tool-transcript kit: a call title, the call line a renderer draws
|
|
3
|
+
* with it, the failure line a result shows before its body, the expand hint, and
|
|
4
|
+
* the running timer.
|
|
4
5
|
*
|
|
5
6
|
* Every extension's `renderCall` / `renderResult` used to repeat the same four
|
|
6
7
|
* idioms — a bold `toolTitle`, a raw `result.content[0]`, a hand-built
|
|
@@ -34,6 +35,58 @@ export function toolCallTitle(theme: Theme, name: string): string {
|
|
|
34
35
|
return theme.fg("toolTitle", theme.bold(name));
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* One call line: the bold tool name, then the subject in the meta role.
|
|
40
|
+
*
|
|
41
|
+
* The subject is flattened to one line here, because a call line is a label: a
|
|
42
|
+
* stored note, a task, or a path carrying a newline cannot wrap it into the body
|
|
43
|
+
* column, where it would read as a result. Pass the clip the subject deserves —
|
|
44
|
+
* `middle` for a path or a URL whose tail is the identity, `end` for prose.
|
|
45
|
+
*
|
|
46
|
+
* A subject drawn in another role (an argument value in `label`, a suffix in
|
|
47
|
+
* `faint`) stays hand-built at its call site; this is the plain headline.
|
|
48
|
+
*/
|
|
49
|
+
export function callLine(
|
|
50
|
+
theme: Theme,
|
|
51
|
+
last: unknown,
|
|
52
|
+
name: string,
|
|
53
|
+
subject: string,
|
|
54
|
+
clip: CallClip = "end",
|
|
55
|
+
): ToolCallText {
|
|
56
|
+
const text = reuseCallText(last, "", clip);
|
|
57
|
+
text.setText(toolCallTitle(theme, `${name} `) + paint(theme, "meta", sanitizeLine(subject)));
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A result resolved to its details, or to the one line a failure shows.
|
|
63
|
+
*/
|
|
64
|
+
export type DetailsOrLine<Details> = { line: string } | { details: Details };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a result to its details, or to the line to show instead.
|
|
68
|
+
*
|
|
69
|
+
* The failure is one branch whether the call carried no details at all (Pi's
|
|
70
|
+
* `{ details: {}, isError: true }` envelope) or the extension reported its own
|
|
71
|
+
* `details.error`, and it goes through `errorLine`: a multi-line message keeps
|
|
72
|
+
* its lines and `Error:` is prefixed exactly once, which a hand-built
|
|
73
|
+
* `paint(theme, "bad", \`Error: ${sanitizeLine(...)}\`)` does not.
|
|
74
|
+
*/
|
|
75
|
+
export function detailsOrLine<Details extends object>(
|
|
76
|
+
theme: Theme,
|
|
77
|
+
result: ToolMessageResult,
|
|
78
|
+
expanded: boolean,
|
|
79
|
+
details: Details | undefined,
|
|
80
|
+
): DetailsOrLine<Details> {
|
|
81
|
+
if (!details) return { line: rejectionLine(theme, result, expanded) };
|
|
82
|
+
// The one property this reads is the extension's own report field; a details
|
|
83
|
+
// block without it (a plan summary, a check report) simply has no failure.
|
|
84
|
+
if ("error" in details && typeof details.error === "string" && details.error !== "") {
|
|
85
|
+
return { line: errorLine(theme, details.error) };
|
|
86
|
+
}
|
|
87
|
+
return { details };
|
|
88
|
+
}
|
|
89
|
+
|
|
37
90
|
/** How a call line gives up columns when the terminal is narrow. */
|
|
38
91
|
export type CallClip = "end" | "middle";
|
|
39
92
|
|
package/lib/usage.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage counters this package accounts in.
|
|
3
|
+
*
|
|
4
|
+
* A session, a delegated child run, and a priced record are all measured in the
|
|
5
|
+
* same five numbers, so the list lives here rather than in each caller:
|
|
6
|
+
* bytefly's footer accumulates them for the session and for its `↳` roll-up, and
|
|
7
|
+
* `subagent` accumulates them from a child's JSON events.
|
|
8
|
+
*
|
|
9
|
+
* `turns` and the context size are deliberately not counters — one is counted
|
|
10
|
+
* per message, the other is a window occupancy rather than something to sum — so
|
|
11
|
+
* each owner tracks those beside these.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The counters, in the order a usage line prints them. */
|
|
15
|
+
export const USAGE_COUNTERS = ["input", "output", "cacheRead", "cacheWrite", "cost"] as const;
|
|
16
|
+
|
|
17
|
+
/** One counter in a usage accumulator. */
|
|
18
|
+
export type UsageCounter = (typeof USAGE_COUNTERS)[number];
|
|
19
|
+
|
|
20
|
+
/** A usage accumulator: one running total per counter. */
|
|
21
|
+
export type UsageCounts = Record<UsageCounter, number>;
|
|
22
|
+
|
|
23
|
+
/** A zeroed accumulator, ready to sum into. */
|
|
24
|
+
export function zeroUsage(): UsageCounts {
|
|
25
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
26
|
+
}
|