@gavin-hu/my-pi-agent 0.8.1 → 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 +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.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 +306 -68
- 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/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- 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/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- 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 +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -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
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { formatTokens } from "../../../lib/format.ts";
|
|
14
|
-
import { reuseText } from "../../../lib/tui.ts";
|
|
15
|
-
import {
|
|
14
|
+
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
16
|
+
import { SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
16
17
|
import { charLength } from "../text.ts";
|
|
17
18
|
import { oneLine } from "../transcript.ts";
|
|
18
19
|
import { createPageCache, type PageCache } from "./cache.ts";
|
|
@@ -204,9 +205,9 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
204
205
|
renderCall(args, theme, context) {
|
|
205
206
|
const { url, find, startIndex } = args as WebFetchArgs;
|
|
206
207
|
const text = reuseText(context?.lastComponent);
|
|
207
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
208
|
-
if (find?.length) line += theme
|
|
209
|
-
else if (startIndex) line += theme
|
|
208
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(url ?? ""));
|
|
209
|
+
if (find?.length) line += paint(theme, "faint", ` find ${find.map((f) => `"${oneLine(f)}"`).join(", ")}`);
|
|
210
|
+
else if (startIndex) line += paint(theme, "faint", ` (from ${startIndex})`);
|
|
210
211
|
text.setText(line);
|
|
211
212
|
return text;
|
|
212
213
|
},
|
|
@@ -216,21 +217,21 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
216
217
|
const page = result.details as FetchResponse | undefined;
|
|
217
218
|
if (page?.error) {
|
|
218
219
|
const message = oneLine(page.error);
|
|
219
|
-
text.setText(theme
|
|
220
|
+
text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
|
|
220
221
|
return text;
|
|
221
222
|
}
|
|
222
223
|
if (!page || result.isError) {
|
|
223
224
|
const first = result.content[0];
|
|
224
225
|
const message = oneLine(first?.type === "text" ? first.text : "Fetch failed");
|
|
225
|
-
text.setText(theme
|
|
226
|
+
text.setText(paint(theme, "bad", message.startsWith("Error:") ? message : `Error: ${message}`));
|
|
226
227
|
return text;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
const label = oneLine(page.title || page.finalUrl);
|
|
230
|
-
let line = theme
|
|
231
|
-
if (page.matches.length > 0) line += theme
|
|
232
|
-
else line += theme
|
|
233
|
-
if (page.cached) line += theme
|
|
231
|
+
let line = paint(theme, "meta", label);
|
|
232
|
+
if (page.matches.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${page.matches.length} match(es)`);
|
|
233
|
+
else line += paint(theme, "faint", `${SEPARATORS.item}${formatTokens(page.totalChars)} chars`);
|
|
234
|
+
if (page.cached) line += paint(theme, "faint", " (cached)");
|
|
234
235
|
text.setText(line);
|
|
235
236
|
return text;
|
|
236
237
|
},
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { loadSearchConfig } from "../config.ts";
|
|
12
12
|
import { oneLine } from "../transcript.ts";
|
|
13
|
-
import { reuseText } from "../../../lib/tui.ts";
|
|
14
|
-
import {
|
|
13
|
+
import { reuseText } from "../../../lib/tui/chrome.ts";
|
|
14
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
15
|
+
import { expandHint, SEPARATORS } from "../../../lib/tui/glyphs.ts";
|
|
15
16
|
import { formatResults } from "./format.ts";
|
|
16
17
|
import { createThrottle, runSearch, type SearchDeps } from "./search.ts";
|
|
17
18
|
import { resolveRequest, WebSearchOutput, WebSearchParams, type SearchResponse, type WebSearchArgs } from "./schema.ts";
|
|
@@ -68,8 +69,8 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
68
69
|
renderCall(args, theme, context) {
|
|
69
70
|
const { query, maxResults } = args as WebSearchArgs;
|
|
70
71
|
const text = reuseText(context?.lastComponent);
|
|
71
|
-
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
72
|
-
if (maxResults) line += theme
|
|
72
|
+
let line = theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "label", oneLine(query ?? ""));
|
|
73
|
+
if (maxResults) line += paint(theme, "faint", ` (max ${maxResults})`);
|
|
73
74
|
text.setText(line);
|
|
74
75
|
return text;
|
|
75
76
|
},
|
|
@@ -80,11 +81,11 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
80
81
|
if (!details || result.isError) {
|
|
81
82
|
const first = result.content[0];
|
|
82
83
|
const message = first?.type === "text" ? first.text : "Search failed";
|
|
83
|
-
text.setText(theme
|
|
84
|
+
text.setText(paint(theme, "bad", `Error: ${oneLine(message)}`));
|
|
84
85
|
return text;
|
|
85
86
|
}
|
|
86
87
|
if (details.provider === "none") {
|
|
87
|
-
text.setText(theme
|
|
88
|
+
text.setText(paint(theme, "faint", `No results for "${oneLine(details.query)}".`));
|
|
88
89
|
return text;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -92,19 +93,21 @@ export function registerSearchTool(pi: ExtensionAPI, deps: SearchDeps = {}): voi
|
|
|
92
93
|
// A blank line separates the call header from the result body.
|
|
93
94
|
const lines: string[] = [
|
|
94
95
|
"",
|
|
95
|
-
theme
|
|
96
|
+
paint(theme, "faint", `via ${details.provider}${SEPARATORS.item}${count} result${count === 1 ? "" : "s"}`),
|
|
96
97
|
];
|
|
97
|
-
if (details.answer)
|
|
98
|
+
if (details.answer)
|
|
99
|
+
lines.push(`${paint(theme, "faint", "Answer:")} ${paint(theme, "meta", oneLine(details.answer, 160))}`);
|
|
98
100
|
|
|
99
101
|
const shown = options.expanded ? details.results : details.results.slice(0, 5);
|
|
100
|
-
const indent = theme
|
|
102
|
+
const indent = paint(theme, "faint", " ");
|
|
101
103
|
for (const [index, item] of shown.entries()) {
|
|
102
|
-
lines.push(`${theme
|
|
103
|
-
lines.push(`${indent}${theme
|
|
104
|
-
if (options.expanded && item.snippet)
|
|
104
|
+
lines.push(`${paint(theme, "accent", `${index + 1}.`)} ${paint(theme, "meta", oneLine(item.title, 100))}`);
|
|
105
|
+
lines.push(`${indent}${paint(theme, "faint", oneLine(item.url, 100))}`);
|
|
106
|
+
if (options.expanded && item.snippet)
|
|
107
|
+
lines.push(`${indent}${paint(theme, "meta", oneLine(item.snippet, 160))}`);
|
|
105
108
|
}
|
|
106
109
|
const extra = count - shown.length;
|
|
107
|
-
if (extra > 0) lines.push(`${theme
|
|
110
|
+
if (extra > 0) lines.push(`${paint(theme, "faint", `+${extra} more`)} ${expandHint(theme)}`);
|
|
108
111
|
text.setText(lines.join("\n"));
|
|
109
112
|
return text;
|
|
110
113
|
},
|
|
@@ -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
|
|
@@ -252,8 +258,8 @@ status, which is what the old start/stop guidance used to do.
|
|
|
252
258
|
| `outbound.ts` | Upload and send a local file: hash, encrypt, `getUploadUrl`, upload, `sendFileMessage`. |
|
|
253
259
|
| `login.ts` | QR login state machine. |
|
|
254
260
|
| `qrcode.ts` | QR matrix → Unicode half-block terminal lines. |
|
|
255
|
-
| `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`). |
|
|
256
|
-
| `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers. |
|
|
261
|
+
| `login-screen.ts` | The `/wechat login` screen (`ctx.ui.custom`), drawn from the `lib/tui/` kit (`titleRule`, `clip`, `isCloseKey`). |
|
|
262
|
+
| `status-screen.ts` | The `/wechat` control screen (`ctx.ui.custom`) and the pure one-line/body renderers, drawn from the `lib/tui/` kit (`titleRule`, `keyValueRow`, `paint`). |
|
|
257
263
|
| `config.ts` | The `wechat` section of `bytefly.json` loading and normalization. |
|
|
258
264
|
| `credentials.ts` | `credentials.json` (`0600`, multi-account). |
|
|
259
265
|
| `state.ts` | `state.json` (cursor, owner, per-peer tokens). |
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
17
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
17
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
19
|
import { createBridge, type Bridge, type BridgeState } from "./bridge.ts";
|
|
19
20
|
import { createFetchRunner, WechatClient } from "./client.ts";
|
|
20
21
|
import { registerCommands, type CommandDeps } from "./commands.ts";
|
|
@@ -34,9 +35,9 @@ import { registerTools } from "./tools.ts";
|
|
|
34
35
|
export function wechatChip(theme: Theme, state: BridgeState): string | undefined {
|
|
35
36
|
switch (state) {
|
|
36
37
|
case "running":
|
|
37
|
-
return theme
|
|
38
|
+
return paint(theme, "ok", `${GLYPHS.wechat} wechat`);
|
|
38
39
|
case "expired":
|
|
39
|
-
return theme
|
|
40
|
+
return paint(theme, "bad", `${GLYPHS.wechat} wechat expired`);
|
|
40
41
|
case "stopped":
|
|
41
42
|
return undefined;
|
|
42
43
|
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { type Component, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
import { screenHint } from "../../lib/tui/chrome.ts";
|
|
12
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
13
|
+
import { isCloseKey } from "../../lib/tui/cursor.ts";
|
|
14
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
12
15
|
|
|
13
16
|
export interface LoginViewState {
|
|
14
17
|
/** Raw `get_qrcode_status` value, or `wait` before the first poll. */
|
|
@@ -51,7 +54,7 @@ export function loginBody(state: LoginViewState, width: number): string[] {
|
|
|
51
54
|
lines.push(state.url.slice(index, index + target));
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
return lines.map((line) =>
|
|
57
|
+
return lines.map((line) => clip(line, target));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface LoginScreenOptions {
|
|
@@ -69,7 +72,7 @@ export class LoginScreenComponent implements Component {
|
|
|
69
72
|
constructor(private readonly options: LoginScreenOptions) {}
|
|
70
73
|
|
|
71
74
|
handleInput(data: string): void {
|
|
72
|
-
if (
|
|
75
|
+
if (isCloseKey(data)) {
|
|
73
76
|
this.options.onCancel();
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -81,7 +84,7 @@ export class LoginScreenComponent implements Component {
|
|
|
81
84
|
const target = Math.max(1, width);
|
|
82
85
|
const theme = this.options.theme;
|
|
83
86
|
return [
|
|
84
|
-
|
|
87
|
+
titleRule(theme, target, "wechat · login"),
|
|
85
88
|
"",
|
|
86
89
|
...loginBody(this.options.state(), target),
|
|
87
90
|
"",
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
14
|
+
import { screenHint, type ScreenHint } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
16
|
+
import { isCloseKey } from "../../lib/tui/cursor.ts";
|
|
17
|
+
import { keyValueRow } from "../../lib/tui/rows.ts";
|
|
18
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
19
|
+
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
15
20
|
import type { BridgeStatus } from "./bridge.ts";
|
|
16
21
|
import { peerLabel } from "./format.ts";
|
|
17
22
|
import type { BotCredentials } from "./types.ts";
|
|
@@ -72,18 +77,32 @@ export function statusLine(view: StatusView): string {
|
|
|
72
77
|
/** Label column width in the screen body. */
|
|
73
78
|
const LABEL_WIDTH = 11;
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
/** One body row: a `meta` label column and a value in `valueRole`. */
|
|
81
|
+
function row(theme: Theme, width: number, label: string, value: string, valueRole: TuiRole = "label"): string {
|
|
82
|
+
return keyValueRow(theme, width, { label, value, labelWidth: LABEL_WIDTH, labelRole: "meta", valueRole });
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
/** The connection word, uncolored; {@link row} paints it. */
|
|
86
|
+
function connectionText(connection: StatusConnection): string {
|
|
80
87
|
switch (connection) {
|
|
81
88
|
case "open":
|
|
82
|
-
return
|
|
89
|
+
return "open";
|
|
83
90
|
case "expired":
|
|
84
|
-
return
|
|
91
|
+
return "expired";
|
|
85
92
|
case "closed":
|
|
86
|
-
return
|
|
93
|
+
return "closed";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The design role the connection word is drawn in. */
|
|
98
|
+
function connectionRole(connection: StatusConnection): TuiRole {
|
|
99
|
+
switch (connection) {
|
|
100
|
+
case "open":
|
|
101
|
+
return "ok";
|
|
102
|
+
case "expired":
|
|
103
|
+
return "bad";
|
|
104
|
+
case "closed":
|
|
105
|
+
return "meta";
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
@@ -91,28 +110,28 @@ function connectionText(theme: Theme, connection: StatusConnection): string {
|
|
|
91
110
|
export function statusBody(view: StatusView, theme: Theme, width: number): string[] {
|
|
92
111
|
const target = Math.max(1, width);
|
|
93
112
|
const lines = [
|
|
94
|
-
row(theme, "Account", view.account ??
|
|
95
|
-
row(theme, "Connection", connectionText(
|
|
113
|
+
row(theme, target, "Account", view.account ?? "not logged in", view.account ? "label" : "meta"),
|
|
114
|
+
row(theme, target, "Connection", connectionText(view.connection), connectionRole(view.connection)),
|
|
96
115
|
];
|
|
97
116
|
if (view.bridge) {
|
|
98
|
-
if (view.owner && view.owner !== view.account) lines.push(row(theme, "Owner", view.owner));
|
|
99
|
-
lines.push(row(theme, "Peers", String(view.peers)));
|
|
100
|
-
lines.push(row(theme, "Queued", String(view.queued)));
|
|
101
|
-
if (view.refused > 0) lines.push(row(theme, "Refused", String(view.refused)));
|
|
102
|
-
if (view.mediaDropped > 0) lines.push(row(theme, "Media", `${view.mediaDropped} dropped
|
|
103
|
-
if (view.promptPending) lines.push(row(theme,
|
|
117
|
+
if (view.owner && view.owner !== view.account) lines.push(row(theme, target, "Owner", view.owner));
|
|
118
|
+
lines.push(row(theme, target, "Peers", String(view.peers), "meta"));
|
|
119
|
+
lines.push(row(theme, target, "Queued", String(view.queued), "meta"));
|
|
120
|
+
if (view.refused > 0) lines.push(row(theme, target, "Refused", String(view.refused), "meta"));
|
|
121
|
+
if (view.mediaDropped > 0) lines.push(row(theme, target, "Media", `${view.mediaDropped} dropped`, "meta"));
|
|
122
|
+
if (view.promptPending) lines.push(row(theme, target, "Prompt", "waiting for a reply", "accent"));
|
|
104
123
|
}
|
|
105
124
|
if (view.connection === "expired") {
|
|
106
125
|
lines.push("");
|
|
107
|
-
lines.push(` ${theme
|
|
126
|
+
lines.push(clip(` ${paint(theme, "bad", "Session expired — press l to log in again.")}`, target));
|
|
108
127
|
} else if (view.connection === "closed" && view.account) {
|
|
109
128
|
lines.push("");
|
|
110
|
-
lines.push(` ${theme
|
|
129
|
+
lines.push(clip(` ${paint(theme, "meta", "Not connected — press l to log in again.")}`, target));
|
|
111
130
|
} else if (view.connection === "closed") {
|
|
112
131
|
lines.push("");
|
|
113
|
-
lines.push(` ${theme
|
|
132
|
+
lines.push(clip(` ${paint(theme, "meta", "Run /wechat login to connect an account.")}`, target));
|
|
114
133
|
}
|
|
115
|
-
return lines
|
|
134
|
+
return lines;
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
export interface StatusScreenOptions {
|
|
@@ -140,7 +159,7 @@ export class StatusScreenComponent implements Component {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
handleInput(data: string): void {
|
|
143
|
-
if (
|
|
162
|
+
if (isCloseKey(data)) {
|
|
144
163
|
this.options.onClose("close");
|
|
145
164
|
return;
|
|
146
165
|
}
|
|
@@ -166,7 +185,7 @@ export class StatusScreenComponent implements Component {
|
|
|
166
185
|
const target = Math.max(1, width);
|
|
167
186
|
const theme = this.options.theme;
|
|
168
187
|
return [
|
|
169
|
-
|
|
188
|
+
titleRule(theme, target, "wechat · control"),
|
|
170
189
|
"",
|
|
171
190
|
...statusBody(this.options.view(), theme, target),
|
|
172
191
|
"",
|
|
@@ -11,7 +11,8 @@ import type { JsonValue } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Type, type Static } from "typebox";
|
|
13
13
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
14
|
-
import { reuseText, toolDetails } from "../../lib/tui.ts";
|
|
14
|
+
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
15
16
|
import type { Bridge } from "./bridge.ts";
|
|
16
17
|
|
|
17
18
|
export const TOOL_NAME = "send_wechat";
|
|
@@ -85,7 +86,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
85
86
|
const text = reuseText(context.lastComponent);
|
|
86
87
|
const body = (args as { text?: unknown }).text;
|
|
87
88
|
const label = context.argsComplete && typeof body === "string" ? sanitizeLine(body) : "…";
|
|
88
|
-
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + theme
|
|
89
|
+
text.setText(theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) + paint(theme, "meta", label));
|
|
89
90
|
return text;
|
|
90
91
|
},
|
|
91
92
|
|
|
@@ -93,16 +94,16 @@ export function registerTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
|
93
94
|
const text = reuseText(context.lastComponent);
|
|
94
95
|
const details = toolDetails<{ sent?: boolean; error?: string }>(result.details);
|
|
95
96
|
if (details?.sent) {
|
|
96
|
-
text.setText(theme
|
|
97
|
+
text.setText(paint(theme, "ok", "sent to WeChat owner"));
|
|
97
98
|
return text;
|
|
98
99
|
}
|
|
99
100
|
if (details?.error) {
|
|
100
|
-
text.setText(theme
|
|
101
|
+
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
101
102
|
return text;
|
|
102
103
|
}
|
|
103
104
|
// Pi's rejection result carries `{}`; show its message instead.
|
|
104
105
|
const first = result.content[0];
|
|
105
|
-
text.setText(first?.type === "text" ? first.text : theme
|
|
106
|
+
text.setText(first?.type === "text" ? first.text : paint(theme, "bad", "Error: send failed"));
|
|
106
107
|
return text;
|
|
107
108
|
},
|
|
108
109
|
});
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { isInside } from "../../lib/path.ts";
|
|
10
|
-
import { GLYPHS } from "../../lib/
|
|
10
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
11
11
|
import { loadConfig, type WorktreeConfig } from "./config.ts";
|
|
12
12
|
import { WorktreeCancelledError } from "./errors.ts";
|
|
13
13
|
import {
|
|
@@ -24,7 +24,7 @@ import { existsSync } from "node:fs";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
27
|
-
import { GLYPHS } from "../../lib/
|
|
27
|
+
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
28
28
|
import { canonicalize, checkCheckout, repoRoot, unlockWorktree } from "./git.ts";
|
|
29
29
|
import { analyzeBashCommand, guardFileTool } from "./guard.ts";
|
|
30
30
|
import {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { STATUS_KEYS } from "../../lib/
|
|
11
|
+
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
12
|
import { ENV_ROOT } from "../../lib/env.ts";
|
|
13
13
|
import { loadConfig, type WorktreeConfig } from "./config.ts";
|
|
14
14
|
import type { WorktreeState } from "./state.ts";
|
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
|
-
| [`
|
|
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. |
|
|
@@ -32,8 +33,6 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
32
33
|
| [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
|
|
33
34
|
| [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
|
|
34
35
|
| [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
|
|
35
|
-
| [`tui.ts`](./tui.ts) | Screen chrome: `screenHeader`, `screenHint`, `dockListFrame` (the shared docked-list assembly), `viewportRows`, and `FULL_SCREEN_OVERLAY`, plus `reuseText` (per-render `Text` reuse) and `editorTheme` (inline-editor theme). |
|
|
36
|
-
| [`ui.ts`](./ui.ts) | Shared UI vocabulary: `GLYPHS`, `SEPARATORS`, `STATUS_KEYS` (`ctx.ui.setStatus` keys), `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, and the transcript rail `BODY_INDENT` / `GLYPH_GAP`. |
|
|
37
36
|
| [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
|
|
38
37
|
|
|
39
38
|
## Conventions
|
|
@@ -43,7 +42,12 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
43
42
|
- **Inject the host seam.** Modules that need a host capability take a function
|
|
44
43
|
(`RunGit`, `HttpRunner`) rather than the whole `ExtensionAPI`, so the logic is
|
|
45
44
|
testable without a runtime.
|
|
46
|
-
- **Constants that must agree across both sides live in `
|
|
47
|
-
the `setStatus` key are one contract, not two literals. Tool names
|
|
48
|
-
an orchestrating tool and the tool it calls — and parameter names
|
|
49
|
-
extension probes on another — live in `tool-names.ts`.
|
|
45
|
+
- **Constants that must agree across both sides live in `tui/glyphs.ts`** — a
|
|
46
|
+
glyph and the `setStatus` key are one contract, not two literals. Tool names
|
|
47
|
+
shared by an orchestrating tool and the tool it calls — and parameter names
|
|
48
|
+
one extension probes on another — live in `tool-names.ts`.
|
|
49
|
+
- **A color is a role, not a token.** Draw through [`tui/tokens.ts`](./tui/tokens.ts)
|
|
50
|
+
(`paint(theme, role, text)`) so a theme swap re-skins every surface; the
|
|
51
|
+
primitives in `tui/` already do, and a new one should too. The one exception is
|
|
52
|
+
Pi's tool-rendering vocabulary (`toolOutput`, `toolTitle`), which stays direct:
|
|
53
|
+
those tokens style a tool block, and no role maps to them.
|
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.
|
package/lib/notice.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared transcript notice: one custom-message type that any extension in
|
|
3
|
+
* the package can post, plus the model-facing content and the renderer-facing
|
|
4
|
+
* details it carries.
|
|
5
|
+
*
|
|
6
|
+
* Pi renders a custom message only when it was sent with `display: true`, and
|
|
7
|
+
* it resolves a renderer by the exact `customType` — there is no wildcard — so
|
|
8
|
+
* the type and the shape behind it are one contract the producing extension and
|
|
9
|
+
* bytefly's renderer must agree on. Keep both here, never re-typed on either
|
|
10
|
+
* side.
|
|
11
|
+
*
|
|
12
|
+
* `content` is what the model reads; `details` is never sent to the model and
|
|
13
|
+
* is the renderer's whole input, so a producer states the facts once and the
|
|
14
|
+
* renderer decides the look. Value-only: constants and pure functions, no
|
|
15
|
+
* state.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
|
+
|
|
20
|
+
/** The custom-message type every transcript notice is sent under. */
|
|
21
|
+
export const NOTICE_TYPE = "my-pi-agent/notice";
|
|
22
|
+
|
|
23
|
+
/** How a notice, or one of its rows, reads at a glance. */
|
|
24
|
+
export type NoticeSeverity = "info" | "ok" | "warn" | "bad";
|
|
25
|
+
|
|
26
|
+
/** One row under a notice's headline. */
|
|
27
|
+
export interface NoticeItem {
|
|
28
|
+
/** The row's text, e.g. `j1 build`. Sanitized by the renderer. */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Short trailing detail, e.g. `exit 1`. Sanitized by the renderer. */
|
|
31
|
+
detail?: string;
|
|
32
|
+
/** The row's severity; falls back to the notice's own. */
|
|
33
|
+
severity?: NoticeSeverity;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Everything the renderer draws, and nothing the model sees. */
|
|
37
|
+
export interface NoticeDetails {
|
|
38
|
+
/** Who raised it, e.g. `job` or `watch`. */
|
|
39
|
+
source: string;
|
|
40
|
+
/** One-line headline. */
|
|
41
|
+
title: string;
|
|
42
|
+
/** The headline's severity; a row may override it. */
|
|
43
|
+
severity?: NoticeSeverity;
|
|
44
|
+
/** Rows under the headline, in order. */
|
|
45
|
+
items?: readonly NoticeItem[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NoticeInput {
|
|
49
|
+
/** Model-facing text; the notice is also shown when non-empty. */
|
|
50
|
+
content: string;
|
|
51
|
+
/** Renderer-facing structure. */
|
|
52
|
+
details: NoticeDetails;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The message shape both send paths accept (`pi.sendMessage`, `before_agent_start`). */
|
|
56
|
+
export interface NoticeMessage {
|
|
57
|
+
customType: string;
|
|
58
|
+
content: string;
|
|
59
|
+
display: true;
|
|
60
|
+
details: NoticeDetails;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A conversation message narrowed to a notice. */
|
|
64
|
+
export type NoticeEntry = AgentMessage & { customType: string; details?: NoticeDetails };
|
|
65
|
+
|
|
66
|
+
/** Build a visible notice message: shared type, `display: true`, the given content and details. */
|
|
67
|
+
export function noticeMessage(input: NoticeInput): NoticeMessage {
|
|
68
|
+
return { customType: NOTICE_TYPE, content: input.content, display: true, details: input.details };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether a conversation message is a notice. */
|
|
72
|
+
export function isNotice(message: AgentMessage): message is NoticeEntry {
|
|
73
|
+
return message.role === "custom" && (message as { customType?: string }).customType === NOTICE_TYPE;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Keep only the newest notice of `source` in a transcript, so stale injected
|
|
78
|
+
* context does not accumulate. Notices of other sources and every non-notice
|
|
79
|
+
* are left alone. Returns `undefined` when nothing is stale, so a `context`
|
|
80
|
+
* handler can leave the messages untouched.
|
|
81
|
+
*/
|
|
82
|
+
export function pruneStaleNotices(messages: readonly AgentMessage[], source: string): AgentMessage[] | undefined {
|
|
83
|
+
const ofSource = messages.filter((message) => isNotice(message) && message.details?.source === source);
|
|
84
|
+
if (ofSource.length <= 1) return undefined;
|
|
85
|
+
const keep = ofSource[ofSource.length - 1];
|
|
86
|
+
return messages.filter((message) => !(isNotice(message) && message.details?.source === source && message !== keep));
|
|
87
|
+
}
|
package/lib/requirements.ts
CHANGED
|
@@ -166,7 +166,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
166
166
|
optional: [{ kind: "module", name: "unpdf", enables: "PDF text extraction in `web_fetch`", hint: pdfGuidance() }],
|
|
167
167
|
},
|
|
168
168
|
"file-browse": {
|
|
169
|
-
optional: [gitCheck("the branch chip")],
|
|
169
|
+
optional: [gitCheck("the branch chip and the diff view")],
|
|
170
170
|
notes: ["The default-browser opener (`open` / `xdg-open` / `cmd`) is best-effort; the URL is printed either way."],
|
|
171
171
|
},
|
|
172
172
|
document: {},
|
|
@@ -194,7 +194,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
194
194
|
bytefly: {
|
|
195
195
|
configFiles: [SHARED_CONFIG_FILE],
|
|
196
196
|
notes: [
|
|
197
|
-
"TUI only for the header, footer, and turn rule; `/bytefly`
|
|
197
|
+
"TUI only for the header, footer, and turn rule; `/bytefly` prints a listing and `/bytefly doctor` the report in RPC, JSON, and print modes.",
|
|
198
198
|
"The panel writes extension enablement through Pi's own settings; project scope requires a trusted project.",
|
|
199
199
|
"`/bytefly`'s list screen and the proactive missing-prerequisite warning need an interactive UI.",
|
|
200
200
|
],
|