@gavin-hu/my-pi-agent 0.9.0 → 0.11.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 +113 -35
- package/extensions/job/commands.ts +1 -0
- package/extensions/job/config.ts +4 -0
- package/extensions/job/format.ts +19 -7
- package/extensions/job/index.ts +25 -20
- package/extensions/job/process.ts +24 -2
- package/extensions/job/registry.ts +3 -0
- package/extensions/job/runtime.ts +253 -18
- package/extensions/job/schema.ts +34 -0
- package/extensions/job/store.ts +13 -1
- package/extensions/job/tools.ts +55 -15
- package/extensions/job/tui.ts +41 -15
- package/extensions/job/types.ts +9 -1
- 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
|
@@ -8,12 +8,20 @@ about to end after mutating work without a todo update, the extension injects
|
|
|
8
8
|
one short reminder and forces a continuation, so the list does not silently
|
|
9
9
|
drift behind the work.
|
|
10
10
|
|
|
11
|
+
<img src="../../assets/todo.png" alt="The /todos screen: a Todos title rule, a 3/7 completed summary, and the list with check, spinner, and circle glyphs" width="100%">
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
pi --extension ./extensions/todo # load just this extension
|
|
13
15
|
pi -e . # load the whole @gavin-hu/my-pi-agent package
|
|
14
16
|
pi install ./ # install the package
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
## Tool rendering
|
|
20
|
+
|
|
21
|
+
How the `todo` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
22
|
+
|
|
23
|
+
<img src="../../assets/tools/todo.png" alt="The todo tool's result: the call line and an indented body with the progress line and the task rows below it, each led by a check, spinner, or circle glyph" width="100%">
|
|
24
|
+
|
|
17
25
|
## What it does
|
|
18
26
|
|
|
19
27
|
- Registers one model-callable tool, `todo`, that **replaces** the task list.
|
|
@@ -49,22 +57,20 @@ Todos · 1/3 · Writing the tests
|
|
|
49
57
|
`/todos` is the expanded view and keeps the model's list order (use it to see
|
|
50
58
|
the plan as written).
|
|
51
59
|
|
|
52
|
-
In the transcript, the call line
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
submitted order:
|
|
60
|
+
In the transcript, the call line carries progress only (`todo → 1/4`); the
|
|
61
|
+
result keeps the `✓` / `◒` / `◌` rows, directly under the call line. The item
|
|
62
|
+
text lives on the rows, so the call line never repeats it. Collapsed, it leads
|
|
63
|
+
with active work; expanded, it keeps the submitted order:
|
|
57
64
|
|
|
58
65
|
```
|
|
59
|
-
todo → 1/4
|
|
60
|
-
|
|
66
|
+
todo → 1/4
|
|
61
67
|
◒ Writing tests
|
|
62
68
|
◌ Ship it
|
|
63
69
|
◌ Write docs
|
|
64
70
|
✓ Write schema
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
Progress lives on the call line, so the
|
|
73
|
+
Progress lives on the call line, so the rows have no footer; the persistent
|
|
68
74
|
widget and `/todos` still show the count. A capped collapsed rail ends with
|
|
69
75
|
`… N more` and the `app.tools.expand` hint.
|
|
70
76
|
|
|
@@ -204,10 +210,11 @@ depends on config.
|
|
|
204
210
|
widget is a single line (`currentTodo` picks the `in_progress` item, else the
|
|
205
211
|
first pending); `/todos` keeps the submitted order, and the transcript result
|
|
206
212
|
orders by status via `compareByActivity` so active work is never hidden. The
|
|
207
|
-
transcript result draws the
|
|
208
|
-
|
|
209
|
-
long items with
|
|
210
|
-
the
|
|
213
|
+
transcript result draws the glyph rows at the block's body indent (the frame
|
|
214
|
+
supplies `BODY_INDENT`; `GLYPH_GAP` comes from
|
|
215
|
+
[`lib/tui/glyphs.ts`](../../lib/tui/glyphs.ts)), wrapping long items with
|
|
216
|
+
continuation rows aligned under the text. Progress lives on the call line, so
|
|
217
|
+
the rows carry no footer.
|
|
211
218
|
- **The transcript reuses its components.** `renderCall` reuses the slot's
|
|
212
219
|
`Text`, and `renderResult` reuses the `TodoResult` view via
|
|
213
220
|
`context.lastComponent`, updating each in place instead of allocating on every
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
|
+
import { expandHint, GLYPH_GAP, GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
8
8
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
9
9
|
import { completedCount, currentTodo } from "./state.ts";
|
|
10
10
|
import type { Todo, TodoStatus } from "./types.ts";
|
|
@@ -17,9 +17,6 @@ export function compareByActivity(a: Todo, b: Todo): number {
|
|
|
17
17
|
return STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** Longest first-item preview shown in the transcript call line. */
|
|
21
|
-
const CALL_PREVIEW_WIDTH = 40;
|
|
22
|
-
|
|
23
20
|
/** Plain status glyph per status; `todoGlyph` adds the theme color. */
|
|
24
21
|
const STATUS_GLYPH: Record<TodoStatus, string> = {
|
|
25
22
|
pending: GLYPHS.pending,
|
|
@@ -107,22 +104,16 @@ export function formatTodoText(todos: Todo[]): string {
|
|
|
107
104
|
* One-line body for the transcript call renderer.
|
|
108
105
|
*
|
|
109
106
|
* The renderer owns the styled `todo ` title, so this returns only the body.
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* the
|
|
107
|
+
* Progress is the summary; the result rail lists the items (active work first),
|
|
108
|
+
* so the call line does not repeat the current item. `todos` is `undefined`
|
|
109
|
+
* while the call's arguments are still streaming, which is distinct from an
|
|
110
|
+
* empty list (a real clear). `argsComplete` disambiguates the tail end of the
|
|
111
|
+
* stream.
|
|
113
112
|
*/
|
|
114
113
|
export function formatCallText(todos: Todo[] | undefined, argsComplete = true): string {
|
|
115
114
|
if (todos === undefined) return argsComplete ? "→ clear list" : "→ …";
|
|
116
115
|
if (todos.length === 0) return "→ clear list";
|
|
117
|
-
|
|
118
|
-
const current = currentTodo(todos);
|
|
119
|
-
const label = current ? (current.activeForm ?? current.content).trim() : "completed";
|
|
120
|
-
if (!label) return argsComplete ? `→ ${progressCount(todos)} · …` : "→ …";
|
|
121
|
-
// `strict` drops a wide grapheme that would cross the boundary, so the slice
|
|
122
|
-
// plus ellipsis never exceeds `CALL_PREVIEW_WIDTH` columns.
|
|
123
|
-
const preview =
|
|
124
|
-
visibleWidth(label) > CALL_PREVIEW_WIDTH ? `${sliceByColumn(label, 0, CALL_PREVIEW_WIDTH - 1, true)}…` : label;
|
|
125
|
-
return `→ ${progressCount(todos)} · ${preview}`;
|
|
116
|
+
return `→ ${progressCount(todos)}`;
|
|
126
117
|
}
|
|
127
118
|
|
|
128
119
|
/** Rows and footer for the transcript result rail. */
|
|
@@ -134,18 +125,19 @@ export interface TodoRailInput {
|
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
/**
|
|
137
|
-
* Transcript result rail: each row is
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
128
|
+
* Transcript result rail: each row is led by its status glyph, and its label
|
|
129
|
+
* wrapped with continuation rows aligned under the text. A trailing `… N more`
|
|
130
|
+
* follows when rows were capped. Progress lives on the call line, so the rail
|
|
131
|
+
* carries no footer. Every line is clipped to `width`.
|
|
132
|
+
*
|
|
133
|
+
* The rail draws at its own left edge; the tool-block frame supplies
|
|
134
|
+
* `BODY_INDENT`, so the rail must not add it again.
|
|
142
135
|
*/
|
|
143
136
|
export function todoRailLines(input: TodoRailInput, theme: Theme, width: number): string[] {
|
|
144
137
|
const w = Math.max(1, width);
|
|
145
138
|
// All three status glyphs are single-column, so the text column is fixed.
|
|
146
|
-
const textColumn =
|
|
139
|
+
const textColumn = 1 + GLYPH_GAP;
|
|
147
140
|
const inner = Math.max(1, w - textColumn);
|
|
148
|
-
const pad = " ".repeat(BODY_INDENT);
|
|
149
141
|
const gap = " ".repeat(GLYPH_GAP);
|
|
150
142
|
const continuation = " ".repeat(textColumn);
|
|
151
143
|
const lines: string[] = [];
|
|
@@ -153,7 +145,7 @@ export function todoRailLines(input: TodoRailInput, theme: Theme, width: number)
|
|
|
153
145
|
const glyph = todoGlyph(todo, theme);
|
|
154
146
|
const wrapped = wrapTextWithAnsi(todoLabel(todo, theme), inner);
|
|
155
147
|
wrapped.forEach((line, index) => {
|
|
156
|
-
lines.push(index === 0 ? `${
|
|
148
|
+
lines.push(index === 0 ? `${glyph}${gap}${line}` : `${continuation}${line}`);
|
|
157
149
|
});
|
|
158
150
|
}
|
|
159
151
|
if (input.more && input.more > 0) {
|
package/extensions/todo/tools.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { JsonValue } from "@earendil-works/pi-ai";
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
16
|
import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
17
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
17
18
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
18
19
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
19
20
|
import { compareByActivity, formatCallText, formatTodoText, type TodoRailInput } from "./format.ts";
|
|
@@ -118,7 +119,7 @@ export function registerTools(pi: ExtensionAPI, runtime: TodoRuntime): void {
|
|
|
118
119
|
return text;
|
|
119
120
|
}
|
|
120
121
|
if (details.todos.length === 0) {
|
|
121
|
-
text.setText(paint(theme, "ok",
|
|
122
|
+
text.setText(paint(theme, "ok", `${GLYPHS.check} `) + paint(theme, "meta", "Cleared the todo list"));
|
|
122
123
|
return text;
|
|
123
124
|
}
|
|
124
125
|
|
package/extensions/todo/tui.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { clampScroll, fitRows, formatRange, isCloseKey, navigateScroll, wheelDel
|
|
|
12
12
|
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
13
13
|
import { clip } from "../../lib/tui/columns.ts";
|
|
14
14
|
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
15
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
15
16
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
16
17
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
18
|
import {
|
|
@@ -50,10 +51,10 @@ function todoRow(todo: Todo, theme: Theme, width: number): string {
|
|
|
50
51
|
* `jobs` widgets. Callers clip it to the available width.
|
|
51
52
|
*/
|
|
52
53
|
function todoLine(todos: Todo[], theme: Theme): string {
|
|
53
|
-
const head = `${paint(theme, "accent", "Todos")}
|
|
54
|
+
const head = `${paint(theme, "accent", "Todos")}${paint(theme, "faint", SEPARATORS.item)}${paint(theme, "faint", progressCount(todos))}`;
|
|
54
55
|
const current = currentTodo(todos);
|
|
55
56
|
if (!current) return `${head} ${paint(theme, "faint", "completed")}`;
|
|
56
|
-
return `${head}
|
|
57
|
+
return `${head}${paint(theme, "faint", SEPARATORS.item)}${todoLabel(current, theme)}`;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/** Transcript result block: the indented glyph rail, wrapping at the render width. */
|
|
@@ -11,6 +11,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pack
|
|
|
11
11
|
pi install ./ # install the package
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Tool rendering
|
|
15
|
+
|
|
16
|
+
How the `watch` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
17
|
+
|
|
18
|
+
<img src="../../assets/tools/watch.png" alt="The watch tool's result: the call line and an indented body with a running file watcher and an exited command watcher" width="100%">
|
|
19
|
+
|
|
14
20
|
## What it does
|
|
15
21
|
|
|
16
22
|
- Registers one model-callable tool, `watch`, with actions `add`, `list`,
|
|
@@ -85,9 +91,11 @@ and the command's `pid`/`exitCode`/`signal` when applicable.
|
|
|
85
91
|
|
|
86
92
|
### Transcript
|
|
87
93
|
|
|
88
|
-
`add` and `stop` render one outcome line; `list` renders
|
|
89
|
-
capped at eight rows
|
|
90
|
-
|
|
94
|
+
`add` and `stop` render one outcome line; `list` renders the `/watch` rows
|
|
95
|
+
capped at eight rows, minus the selection-marker column (`listRow`'s
|
|
96
|
+
`marker: false`) that only the interactive screen needs; `matches` and `logs`
|
|
97
|
+
render the tail of the sanitized lines, with the hidden count noted above and
|
|
98
|
+
`(ctrl+o to expand)`.
|
|
91
99
|
|
|
92
100
|
### Command: /watch
|
|
93
101
|
|
|
@@ -172,8 +180,8 @@ other.
|
|
|
172
180
|
| Annotations | `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`. |
|
|
173
181
|
| `outputSchema` | None; the result carries `details` only (no `structuredContent` mirror). |
|
|
174
182
|
| State storage | In-memory runtime closure; no registry, no `getBranch()` rebuild. |
|
|
175
|
-
| Injected context | A
|
|
176
|
-
| Lifecycle | `session_start` loads config and re-arms `onMatch`; `session_tree` repaints; `agent_start`/`agent_settled` track busy; `before_agent_start`
|
|
183
|
+
| Injected context | A shared transcript notice (`my-pi-agent/notice`), rendered by bytefly; pending matches are read from the runtime, never from the transcript. |
|
|
184
|
+
| Lifecycle | `session_start` loads config and re-arms `onMatch`; `session_tree` repaints; `agent_start`/`agent_settled` track busy; `before_agent_start` posts pending matches as a notice; `context` prunes stale notices of its own source; `session_shutdown` kills commands and clears chips. |
|
|
177
185
|
|
|
178
186
|
`watch` is gated by `PI_DISABLED_EXTENSIONS` like every extension.
|
|
179
187
|
|
|
@@ -193,7 +201,7 @@ turn — so a match is never dropped when the wake window is busy.
|
|
|
193
201
|
|
|
194
202
|
| Module | Responsibility |
|
|
195
203
|
|---|---|
|
|
196
|
-
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle and
|
|
204
|
+
| `index.ts` | Extension factory: registers the tool and command, wires the lifecycle and the shared notices. |
|
|
197
205
|
| `tools.ts` | The `watch` tool definition, execution, and transcript rendering. |
|
|
198
206
|
| `schema.ts` | TypeBox parameters and pure `normalizeCall` validation. |
|
|
199
207
|
| `types.ts` | `WatchAction`/`WatchStatus`, `WatchRecord`, `WatchDetails`, `PendingMatch`. |
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
15
|
-
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
15
|
+
import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
16
16
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
17
17
|
import type { PendingMatch, WatchRecord, WatchStatus } from "./types.ts";
|
|
18
18
|
|
|
@@ -195,7 +195,7 @@ export function formatCallText(action: string, args: Record<string, unknown>, ar
|
|
|
195
195
|
|
|
196
196
|
/** One-line outcome for a watcher, themed for the transcript result. */
|
|
197
197
|
export function formatWatchOutcomeLine(watch: WatchRecord, theme: Theme, now = Date.now()): string {
|
|
198
|
-
const sep = paint(theme, "faint",
|
|
198
|
+
const sep = paint(theme, "faint", SEPARATORS.item);
|
|
199
199
|
const age = Math.max(0, (watch.endedAt ?? now) - watch.startedAt);
|
|
200
200
|
const outcome =
|
|
201
201
|
watch.status === "error"
|
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers the `watch` tool and `/watch` command. Watchers read a file or a
|
|
5
5
|
* command's merged output, match each new line against a regular expression,
|
|
6
|
-
* and alert the user; matching lines are reported to the model
|
|
7
|
-
* turn, or wake a turn immediately when the
|
|
8
|
-
* are session-only and stopped on shutdown.
|
|
6
|
+
* and alert the user; matching lines are reported to the model — and shown as a
|
|
7
|
+
* bytefly notice — at the next turn, or wake a turn immediately when the
|
|
8
|
+
* watcher opted into `wake`. Watchers are session-only and stopped on shutdown.
|
|
9
9
|
*
|
|
10
10
|
* Load with: pi --extension ./extensions/watch
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
14
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
|
+
import { sanitizeLine } from "../../lib/format.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 { formatPendingMatch } from "./format.ts";
|
|
19
|
+
import { clipLabel, formatPendingMatch } from "./format.ts";
|
|
19
20
|
import { createWatchRuntime, type WatchRuntime } from "./runtime.ts";
|
|
20
21
|
import { registerTools } from "./tools.ts";
|
|
21
22
|
import type { PendingMatch } from "./types.ts";
|
|
@@ -23,13 +24,10 @@ import type { PendingMatch } from "./types.ts";
|
|
|
23
24
|
/** Marker embedded in the injected match context. */
|
|
24
25
|
const WATCH_CONTEXT_MARKER = "[WATCH MATCHES]";
|
|
25
26
|
|
|
26
|
-
/** Custom-entry type used for the injected match message. */
|
|
27
|
-
export const WATCH_CONTEXT_TYPE = "watch-context";
|
|
28
|
-
|
|
29
27
|
/** At most this many match lines are injected in one message. */
|
|
30
28
|
const MAX_CONTEXT_MATCHES = 20;
|
|
31
29
|
|
|
32
|
-
/** Build the
|
|
30
|
+
/** Build the model-facing text for the pending-match notice. */
|
|
33
31
|
export function buildWatchContext(matches: PendingMatch[]): string {
|
|
34
32
|
const shown = matches.slice(0, MAX_CONTEXT_MATCHES);
|
|
35
33
|
const lines = shown.map(formatPendingMatch);
|
|
@@ -38,8 +36,21 @@ export function buildWatchContext(matches: PendingMatch[]): string {
|
|
|
38
36
|
return `${WATCH_CONTEXT_MARKER}\n${matches.length} watched line${matches.length === 1 ? "" : "s"} matched:\n${lines.join("\n")}${suffix}\nRead more with \`watch matches <id>\`.`;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/** The shared notice for pending matches: model text plus renderer details. */
|
|
40
|
+
function watchNotice(matches: PendingMatch[]): NoticeMessage {
|
|
41
|
+
const count = matches.length;
|
|
42
|
+
return noticeMessage({
|
|
43
|
+
content: buildWatchContext(matches),
|
|
44
|
+
details: {
|
|
45
|
+
source: "watch",
|
|
46
|
+
title: `${count} watched line${count === 1 ? "" : "s"} matched`,
|
|
47
|
+
items: matches.slice(0, MAX_CONTEXT_MATCHES).map((match) => ({
|
|
48
|
+
label: `${match.watcherId} ${clipLabel(match.label)}`,
|
|
49
|
+
detail: sanitizeLine(match.line),
|
|
50
|
+
severity: "warn",
|
|
51
|
+
})),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export interface WatchDeps {
|
|
@@ -64,10 +75,7 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
64
75
|
const pending = runtime.pendingMatches();
|
|
65
76
|
if (pending.length === 0) return;
|
|
66
77
|
try {
|
|
67
|
-
pi.sendMessage(
|
|
68
|
-
{ customType: WATCH_CONTEXT_TYPE, content: buildWatchContext(pending), display: false },
|
|
69
|
-
{ triggerTurn: true },
|
|
70
|
-
);
|
|
78
|
+
pi.sendMessage(watchNotice(pending), { triggerTurn: true });
|
|
71
79
|
runtime.markReported(pending);
|
|
72
80
|
waking = true;
|
|
73
81
|
} catch {
|
|
@@ -103,17 +111,14 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
103
111
|
if (pending.length === 0) return undefined;
|
|
104
112
|
runtime.markReported(pending);
|
|
105
113
|
runtime.setStatus(ctx);
|
|
106
|
-
return {
|
|
107
|
-
message: { customType: WATCH_CONTEXT_TYPE, content: buildWatchContext(pending), display: false },
|
|
108
|
-
};
|
|
114
|
+
return { message: watchNotice(pending) };
|
|
109
115
|
});
|
|
110
116
|
|
|
111
|
-
// Keep stale match
|
|
117
|
+
// Keep stale match notices out of later turns. Only this extension's notices
|
|
118
|
+
// are pruned; another source's are left for its own handler.
|
|
112
119
|
pi.on("context", (event) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const last = contexts[contexts.length - 1];
|
|
116
|
-
return { messages: event.messages.filter((message) => !isWatchContext(message) || message === last) };
|
|
120
|
+
const pruned = pruneStaleNotices(event.messages, "watch");
|
|
121
|
+
return pruned ? { messages: pruned } : undefined;
|
|
117
122
|
});
|
|
118
123
|
|
|
119
124
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } 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
|
compareWatches,
|
|
@@ -193,8 +194,9 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
193
194
|
const text = reuseText(context.lastComponent);
|
|
194
195
|
text.setText(
|
|
195
196
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
paint(
|
|
198
|
+
theme,
|
|
199
|
+
"meta",
|
|
198
200
|
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
199
201
|
),
|
|
200
202
|
);
|
|
@@ -269,7 +271,7 @@ export function registerTools(pi: ExtensionAPI, runtime: WatchRuntime): void {
|
|
|
269
271
|
const badges: string[] = [];
|
|
270
272
|
if (details.watch.wake) badges.push("wake");
|
|
271
273
|
if (details.watch.fromStart) badges.push("from start");
|
|
272
|
-
if (badges.length > 0) line += paint(theme, "faint",
|
|
274
|
+
if (badges.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${badges.join(SEPARATORS.item)}`);
|
|
273
275
|
}
|
|
274
276
|
text.setText(line);
|
|
275
277
|
return text;
|
package/extensions/watch/tui.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { clip, fitCells } from "../../lib/tui/columns.ts";
|
|
|
18
18
|
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
19
19
|
import { expandHint, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
20
20
|
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
21
|
-
import { emptyState,
|
|
21
|
+
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
22
22
|
import { titleRule } from "../../lib/tui/rules.ts";
|
|
23
23
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
24
24
|
import { compareWatches, formatWatchDetail, shortLabel, statusGlyphChar, statusRole } from "./format.ts";
|
|
@@ -37,14 +37,18 @@ const LINES_CHROME_ROWS = 6;
|
|
|
37
37
|
const watchId = (watch: WatchRecord): string => watch.id;
|
|
38
38
|
|
|
39
39
|
/** One watcher row: marker, status glyph, id, label, and a right-aligned count. */
|
|
40
|
-
function watchRow(
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
function watchRow(
|
|
41
|
+
watch: WatchRecord,
|
|
42
|
+
theme: Theme,
|
|
43
|
+
width: number,
|
|
44
|
+
options: Pick<StatusRow, "selected" | "marker"> = {},
|
|
45
|
+
): string {
|
|
46
|
+
return statusRow(theme, width, {
|
|
47
|
+
...options,
|
|
43
48
|
glyph: statusGlyphChar(watch.status),
|
|
44
49
|
glyphRole: statusRole(watch.status),
|
|
45
50
|
label: `${watch.id} ${shortLabel(watch)}`,
|
|
46
51
|
trailing: `${watch.matchCount}`,
|
|
47
|
-
trailingRole: "faint",
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -80,7 +84,7 @@ export class WatchResult implements Component {
|
|
|
80
84
|
// A leading blank line separates the result from the call header.
|
|
81
85
|
const lines: string[] = [""];
|
|
82
86
|
if (this.input.kind === "list") {
|
|
83
|
-
for (const watch of this.input.watches) lines.push(watchRow(watch, this.theme, w));
|
|
87
|
+
for (const watch of this.input.watches) lines.push(watchRow(watch, this.theme, w, { marker: false }));
|
|
84
88
|
if (this.input.more > 0) {
|
|
85
89
|
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
86
90
|
lines.push(clip(` ${note}`, w));
|
|
@@ -311,7 +315,8 @@ export class WatchListComponent implements Component {
|
|
|
311
315
|
body.push(...emptyState(this.theme, w, { label: "No watchers." }));
|
|
312
316
|
} else {
|
|
313
317
|
const end = Math.min(watches.length, this.cursor.scroll + this.cursor.visible);
|
|
314
|
-
for (let i = this.cursor.scroll; i < end; i++)
|
|
318
|
+
for (let i = this.cursor.scroll; i < end; i++)
|
|
319
|
+
body.push(watchRow(watches[i], this.theme, w, { selected: i === index }));
|
|
315
320
|
if (this.cursor.scroll > 0 || end < watches.length) {
|
|
316
321
|
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: watches.length });
|
|
317
322
|
}
|
|
@@ -12,6 +12,14 @@ pi -e . # load the whole @gavin-hu/my-pi-agent
|
|
|
12
12
|
pi install ./ # or install the package
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Tool rendering
|
|
16
|
+
|
|
17
|
+
How the `web_search` and `web_fetch` 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/web_search.png" alt="The web_search tool's call and result: the call line and an indented body with the query and the ranked results" width="100%">
|
|
20
|
+
|
|
21
|
+
<img src="../../assets/tools/web_fetch.png" alt="The web_fetch tool's call and result: the call line and an indented body with the URL, title, and fetched text preview" width="100%">
|
|
22
|
+
|
|
15
23
|
## What it does
|
|
16
24
|
|
|
17
25
|
- Registers `web_search`, general web search through a pluggable provider:
|
|
@@ -46,17 +54,18 @@ web_fetch({ url: "https://pi.dev/", find: ["documentation", "extensions"] })
|
|
|
46
54
|
|
|
47
55
|
### Transcript
|
|
48
56
|
|
|
49
|
-
The call line names the query or URL
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
result
|
|
57
|
+
The call line names the query or URL and states the requested limit
|
|
58
|
+
(`(max 8)`), so it never reads as the number of results returned. The result
|
|
59
|
+
separates the header from its body and sanitizes untrusted web text (answers,
|
|
60
|
+
titles, fetched titles and errors) to one line before it reaches the terminal.
|
|
61
|
+
A search result labels its answer and lists one URL per result; expanding
|
|
62
|
+
reveals snippets and the full result list.
|
|
54
63
|
|
|
55
64
|
```
|
|
56
|
-
web_search "pi coding agent" (8
|
|
65
|
+
web_search "pi coding agent" (max 8)
|
|
57
66
|
|
|
58
67
|
via searxng · 8 results
|
|
59
|
-
|
|
68
|
+
Answer: Pi is a coding agent harness …
|
|
60
69
|
1. Pi — a coding agent harness
|
|
61
70
|
https://pi.dev/
|
|
62
71
|
2. GitHub - earendil-works/pi
|
|
@@ -205,7 +205,7 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
205
205
|
renderCall(args, theme, context) {
|
|
206
206
|
const { url, find, startIndex } = args as WebFetchArgs;
|
|
207
207
|
const text = reuseText(context?.lastComponent);
|
|
208
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "
|
|
208
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(url ?? ""));
|
|
209
209
|
if (find?.length) line += paint(theme, "faint", ` find ${find.map((f) => `"${oneLine(f)}"`).join(", ")}`);
|
|
210
210
|
else if (startIndex) line += paint(theme, "faint", ` (from ${startIndex})`);
|
|
211
211
|
text.setText(line);
|
|
@@ -69,8 +69,8 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
69
69
|
renderCall(args, theme, context) {
|
|
70
70
|
const { query, maxResults } = args as WebSearchArgs;
|
|
71
71
|
const text = reuseText(context?.lastComponent);
|
|
72
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "
|
|
73
|
-
if (maxResults) line += paint(theme, "faint", ` (${maxResults}
|
|
72
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(query ?? ""));
|
|
73
|
+
if (maxResults) line += paint(theme, "faint", ` (max ${maxResults})`);
|
|
74
74
|
text.setText(line);
|
|
75
75
|
return text;
|
|
76
76
|
},
|
|
@@ -96,7 +96,7 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
96
96
|
paint(theme, "faint", `via ${details.provider}${SEPARATORS.item}${count} result${count === 1 ? "" : "s"}`),
|
|
97
97
|
];
|
|
98
98
|
if (details.answer)
|
|
99
|
-
lines.push(`${paint(theme, "faint", "
|
|
99
|
+
lines.push(`${paint(theme, "faint", "Answer:")} ${paint(theme, "meta", oneLine(details.answer, 160))}`);
|
|
100
100
|
|
|
101
101
|
const shown = options.expanded ? details.results : details.results.slice(0, 5);
|
|
102
102
|
const indent = paint(theme, "faint", " ");
|
|
@@ -11,6 +11,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pack
|
|
|
11
11
|
pi install ./ # install the package
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Tool rendering
|
|
15
|
+
|
|
16
|
+
How the `send_wechat` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
17
|
+
|
|
18
|
+
<img src="../../assets/tools/send_wechat.png" alt="The send_wechat tool's call and result: the call line and an indented body with the message text and the sent confirmation" width="100%">
|
|
19
|
+
|
|
14
20
|
## What it does
|
|
15
21
|
|
|
16
22
|
- **Inbound** — a long-poll loop receives WeChat text, images, and files and
|
|
@@ -14,6 +14,12 @@ pi install ./ # install the package
|
|
|
14
14
|
pi -e . --worktree feature-auth # load the package and enter a worktree at startup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Tool rendering
|
|
18
|
+
|
|
19
|
+
How the `list_worktrees` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
20
|
+
|
|
21
|
+
<img src="../../assets/tools/list_worktrees.png" alt="The list_worktrees tool's call and result, drawn by the generic fallback" width="100%">
|
|
22
|
+
|
|
17
23
|
## What it does
|
|
18
24
|
|
|
19
25
|
- `enter_worktree` creates `<repo>/.pi/worktrees/<name>` on branch
|
package/lib/README.md
CHANGED
|
@@ -20,11 +20,12 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
20
20
|
| [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
|
|
21
21
|
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
|
|
22
22
|
| [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` and `JobFinishedEvent`, published by `job` and consumed by bytefly's notify capability. |
|
|
23
|
+
| [`notice.ts`](./notice.ts) | The shared transcript notice: `NOTICE_TYPE`, the `NoticeDetails` / `NoticeItem` shape, `noticeMessage` (the `display: true` message both send paths accept), `isNotice`, and `pruneStaleNotices` — posted by `job` and `watch`, rendered by bytefly. |
|
|
23
24
|
| [`format.ts`](./format.ts) | Pure text/number formatting shared by renderers: `formatTokens`, `stripControlChars`, `sanitize`, `sanitizeLine` — the boundary for untrusted, single-line text — and `shortenPath`. |
|
|
24
25
|
| [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote adapter installs on `ctx.ui` under a string key, the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file/text preface) plus the shared “Other” label. |
|
|
25
26
|
| [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
|
|
26
27
|
| [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
|
|
27
|
-
| [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `
|
|
28
|
+
| [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job` and `watch` share), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
|
|
28
29
|
| [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, and doc guards share. |
|
|
29
30
|
| [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
|
|
30
31
|
| [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
|
package/lib/format.ts
CHANGED
|
@@ -17,6 +17,23 @@ export function formatTokens(count: number): string {
|
|
|
17
17
|
return `${Math.round(count / 1000000)}M`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* An integer grouped with thousands separators (`999`, `8,000`, `42,478`).
|
|
22
|
+
* Hand-rolled rather than `toLocaleString`, so a count is identical under any
|
|
23
|
+
* locale and ICU build.
|
|
24
|
+
*/
|
|
25
|
+
export function formatCount(count: number): string {
|
|
26
|
+
if (!Number.isFinite(count)) return "0";
|
|
27
|
+
const sign = count < 0 ? "-" : "";
|
|
28
|
+
const digits = Math.abs(Math.trunc(count)).toString();
|
|
29
|
+
let out = "";
|
|
30
|
+
for (let i = 0; i < digits.length; i++) {
|
|
31
|
+
if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
|
|
32
|
+
out += digits[i];
|
|
33
|
+
}
|
|
34
|
+
return sign + out;
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
/**
|
|
21
38
|
* Replace terminal control characters (including ESC and the C1 block) with
|
|
22
39
|
* spaces, keeping `\n` and `\t` for callers that render multi-line text.
|