@gavin-hu/my-pi-agent 0.14.0 → 0.15.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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
package/extensions/watch/tui.ts
CHANGED
|
@@ -6,36 +6,23 @@
|
|
|
6
6
|
* tracks the selection by id so a live re-sort cannot move the cursor, through
|
|
7
7
|
* the shared `IdCursor` and `LinePane`.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
9
|
+
* The frame, the navigation keys, the y/N confirmation, and the matches pane come
|
|
10
|
+
* from the shared `DockListScreen`, so this module owns only the watch
|
|
11
|
+
* vocabulary: the row, the focus block, the counts, the hints, and what the
|
|
12
|
+
* verbs do. Matched lines arrive already sanitized by the runtime, so no
|
|
13
|
+
* untrusted escape sequence reaches the terminal here.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
21
|
-
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
22
|
-
import { titleRule } from "../../lib/tui/rules.ts";
|
|
17
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
18
|
+
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
19
|
+
import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
|
|
20
|
+
import { statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
21
|
+
import { TailResult, type TailResultInput } from "../../lib/tui/tool.ts";
|
|
23
22
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
24
23
|
import { compareWatches, formatWatchDetail, shortLabel, statusGlyphChar, statusRole } from "./format.ts";
|
|
25
24
|
import type { WatchRecord } from "./types.ts";
|
|
26
25
|
|
|
27
|
-
/** Rows the `/watch` screen body shows when the terminal height is unknown. */
|
|
28
|
-
const SCREEN_DEFAULT_ROWS = 14;
|
|
29
|
-
|
|
30
|
-
/** Fixed rows around the list body: header, summary, blank, blank, hint, blank. */
|
|
31
|
-
const LIST_CHROME_ROWS = 7;
|
|
32
|
-
|
|
33
|
-
/** Fixed rows around the matches body: header, subtitle, blank, blank, hint, blank. */
|
|
34
|
-
const LINES_CHROME_ROWS = 6;
|
|
35
|
-
|
|
36
|
-
/** A watcher's stable identity, for the id-anchored cursor. */
|
|
37
|
-
const watchId = (watch: WatchRecord): string => watch.id;
|
|
38
|
-
|
|
39
26
|
/** One watcher row: marker, status glyph, id, label, and a right-aligned count. */
|
|
40
27
|
function watchRow(
|
|
41
28
|
watch: WatchRecord,
|
|
@@ -48,6 +35,7 @@ function watchRow(
|
|
|
48
35
|
glyph: statusGlyphChar(watch.status),
|
|
49
36
|
glyphRole: statusRole(watch.status),
|
|
50
37
|
label: `${watch.id} ${shortLabel(watch)}`,
|
|
38
|
+
labelRole: options.selected ? "accent" : "label",
|
|
51
39
|
trailing: `${watch.matchCount}`,
|
|
52
40
|
});
|
|
53
41
|
}
|
|
@@ -57,48 +45,41 @@ export type WatchResultInput =
|
|
|
57
45
|
| { kind: "list"; watches: WatchRecord[]; more: number }
|
|
58
46
|
| { kind: "lines"; label: string; lines: string[]; earlier: number };
|
|
59
47
|
|
|
48
|
+
/** Map the watch vocabulary onto the shared transcript tail block. */
|
|
49
|
+
function watchTail(input: WatchResultInput): TailResultInput<WatchRecord> {
|
|
50
|
+
if (input.kind === "lines") {
|
|
51
|
+
return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: input.label };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
kind: "list",
|
|
55
|
+
rows: input.watches,
|
|
56
|
+
more: input.more,
|
|
57
|
+
rowLine: (watch, width, theme) => watchRow(watch, theme, width, { marker: false }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
60
61
|
/**
|
|
61
|
-
* Transcript result block for the `watch` tool:
|
|
62
|
-
* `/watch`-style row rail
|
|
63
|
-
* themed matched-line tail. Reused across renders via `setInput`.
|
|
62
|
+
* Transcript result block for the `watch` tool: the shared tail block over the
|
|
63
|
+
* `/watch`-style row rail or the themed matched-line tail. Reused across renders.
|
|
64
64
|
*/
|
|
65
65
|
export class WatchResult implements Component {
|
|
66
|
-
private
|
|
67
|
-
private theme: Theme;
|
|
66
|
+
private readonly view: TailResult<WatchRecord>;
|
|
68
67
|
|
|
69
68
|
constructor(input: WatchResultInput, theme: Theme) {
|
|
70
|
-
this.
|
|
71
|
-
this.theme = theme;
|
|
69
|
+
this.view = new TailResult(watchTail(input), theme);
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
/** Update in place so the transcript can reuse this component across renders. */
|
|
75
73
|
setInput(input: WatchResultInput, theme: Theme): void {
|
|
76
|
-
this.input
|
|
77
|
-
this.theme = theme;
|
|
74
|
+
this.view.setInput(watchTail(input), theme);
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
invalidate(): void {
|
|
77
|
+
invalidate(): void {
|
|
78
|
+
this.view.invalidate();
|
|
79
|
+
}
|
|
81
80
|
|
|
82
81
|
render(width: number): string[] {
|
|
83
|
-
|
|
84
|
-
// A leading blank line separates the result from the call header.
|
|
85
|
-
const lines: string[] = [""];
|
|
86
|
-
if (this.input.kind === "list") {
|
|
87
|
-
for (const watch of this.input.watches) lines.push(watchRow(watch, this.theme, w, { marker: false }));
|
|
88
|
-
if (this.input.more > 0) {
|
|
89
|
-
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
90
|
-
lines.push(clip(` ${note}`, w));
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
if (this.input.earlier > 0) {
|
|
94
|
-
const note = `${paint(this.theme, "faint", `… ${this.input.earlier} earlier ${this.input.label}`)} ${expandHint(this.theme)}`;
|
|
95
|
-
lines.push(clip(note, w));
|
|
96
|
-
}
|
|
97
|
-
for (const line of this.input.lines) {
|
|
98
|
-
lines.push(this.theme.fg("toolOutput", clip(line, w)));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return lines;
|
|
82
|
+
return this.view.render(width);
|
|
102
83
|
}
|
|
103
84
|
}
|
|
104
85
|
|
|
@@ -109,265 +90,114 @@ export interface WatchListCallbacks {
|
|
|
109
90
|
clear(): void;
|
|
110
91
|
}
|
|
111
92
|
|
|
112
|
-
/** A destructive action waiting for y/N confirmation. */
|
|
113
|
-
type PendingConfirm = { kind: "stop"; id: string; label: string } | { kind: "clear"; count: number };
|
|
114
|
-
|
|
115
93
|
/** Dismissible `/watch` screen: a selectable list with a focus pane and a matches view. */
|
|
116
|
-
export class WatchListComponent
|
|
117
|
-
private mode: "list" | "lines" = "list";
|
|
118
|
-
/** Focus anchored to a watcher id; the list is re-sorted from live state each render. */
|
|
119
|
-
private readonly cursor = new IdCursor();
|
|
120
|
-
/** The open matches tail. */
|
|
121
|
-
private readonly pane = new LinePane();
|
|
122
|
-
private confirm: PendingConfirm | undefined;
|
|
123
|
-
private notice: string | undefined;
|
|
124
|
-
|
|
94
|
+
export class WatchListComponent extends DockListScreen<WatchRecord> {
|
|
125
95
|
constructor(
|
|
126
96
|
private readonly watches: () => WatchRecord[],
|
|
127
|
-
|
|
97
|
+
theme: Theme,
|
|
128
98
|
private readonly callbacks: WatchListCallbacks,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
) {
|
|
133
|
-
|
|
134
|
-
|
|
99
|
+
onClose: () => void,
|
|
100
|
+
requestRender: () => void,
|
|
101
|
+
viewportRowsSource?: ViewportRowsSource,
|
|
102
|
+
) {
|
|
103
|
+
super({
|
|
104
|
+
theme,
|
|
105
|
+
title: "Watch",
|
|
106
|
+
detailTitle: "Watch Matches",
|
|
107
|
+
emptyList: "No watchers.",
|
|
108
|
+
emptyDetail: "No matches yet.",
|
|
109
|
+
requestRender,
|
|
110
|
+
onClose,
|
|
111
|
+
viewportRowsSource,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
135
114
|
|
|
136
115
|
/** Replace the matches pane contents (called by the command's poll loop). */
|
|
137
116
|
setLines(title: string, lines: string[], meta: { more?: boolean } = {}): void {
|
|
138
|
-
|
|
117
|
+
this.setDetail({ title, lines, more: meta.more });
|
|
139
118
|
}
|
|
140
119
|
|
|
141
120
|
/** The watcher whose matches pane is open, or undefined in list mode. */
|
|
142
121
|
currentLineId(): string | undefined {
|
|
143
|
-
return this.
|
|
122
|
+
return this.currentDetailId();
|
|
144
123
|
}
|
|
145
124
|
|
|
146
125
|
/** Re-read the open matches pane from the runtime. */
|
|
147
126
|
refreshLines(id: string): void {
|
|
148
|
-
|
|
149
|
-
if (result && this.pane.update({ lines: result.lines, more: result.more })) this.requestRender();
|
|
127
|
+
this.refreshDetail(id);
|
|
150
128
|
}
|
|
151
129
|
|
|
152
|
-
|
|
130
|
+
protected rows(): readonly WatchRecord[] {
|
|
153
131
|
return [...this.watches()].sort(compareWatches);
|
|
154
132
|
}
|
|
155
133
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const ordered = this.ordered;
|
|
159
|
-
if (ordered.length === 0) return;
|
|
160
|
-
this.cursor.select(ordered, watchId, next);
|
|
161
|
-
this.requestRender();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private openLines(): void {
|
|
165
|
-
const watch = this.ordered[this.cursor.indexOf(this.ordered, watchId)];
|
|
166
|
-
if (!watch) return;
|
|
167
|
-
this.mode = "lines";
|
|
168
|
-
const result = this.callbacks.lines(watch.id);
|
|
169
|
-
this.pane.open(watch.id, {
|
|
170
|
-
title: `${watch.id} ${shortLabel(watch)}`,
|
|
171
|
-
lines: result?.lines ?? [],
|
|
172
|
-
more: result?.more ?? false,
|
|
173
|
-
});
|
|
174
|
-
this.requestRender();
|
|
134
|
+
protected idOf(watch: WatchRecord): string {
|
|
135
|
+
return watch.id;
|
|
175
136
|
}
|
|
176
137
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.handleConfirmInput(data);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (this.mode === "list") this.handleListInput(data);
|
|
183
|
-
else this.handleLinesInput(data);
|
|
138
|
+
protected rowLine(watch: WatchRecord, width: number, selected: boolean): string {
|
|
139
|
+
return watchRow(watch, this.theme, width, { selected });
|
|
184
140
|
}
|
|
185
141
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (delta === undefined) return undefined;
|
|
189
|
-
if (this.confirm) return { handled: true };
|
|
190
|
-
if (this.mode === "list") {
|
|
191
|
-
this.select(this.cursor.indexOf(this.ordered, watchId) + delta);
|
|
192
|
-
} else {
|
|
193
|
-
this.pane.setScroll(this.pane.scroll + delta);
|
|
194
|
-
this.requestRender();
|
|
195
|
-
}
|
|
196
|
-
return { handled: true };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private handleListInput(data: string): void {
|
|
200
|
-
if (isCloseKey(data) || data === "q") {
|
|
201
|
-
this.onClose();
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (this.notice !== undefined) {
|
|
205
|
-
this.notice = undefined;
|
|
206
|
-
this.requestRender();
|
|
207
|
-
}
|
|
208
|
-
const ordered = this.ordered;
|
|
209
|
-
const index = this.cursor.indexOf(ordered, watchId);
|
|
210
|
-
switch (navIntent(data)) {
|
|
211
|
-
case "up":
|
|
212
|
-
this.select(index - 1);
|
|
213
|
-
return;
|
|
214
|
-
case "down":
|
|
215
|
-
this.select(index + 1);
|
|
216
|
-
return;
|
|
217
|
-
case "pageUp":
|
|
218
|
-
this.select(index - this.cursor.visible);
|
|
219
|
-
return;
|
|
220
|
-
case "pageDown":
|
|
221
|
-
this.select(index + this.cursor.visible);
|
|
222
|
-
return;
|
|
223
|
-
case "home":
|
|
224
|
-
this.select(0);
|
|
225
|
-
return;
|
|
226
|
-
case "end":
|
|
227
|
-
this.select(ordered.length - 1);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (matchesKey(data, Key.enter) || data === "l") {
|
|
231
|
-
this.openLines();
|
|
232
|
-
} else if (data === "x" || data === "K") {
|
|
233
|
-
const watch = ordered[index];
|
|
234
|
-
if (!watch) return;
|
|
235
|
-
if (watch.status === "running") {
|
|
236
|
-
this.confirm = { kind: "stop", id: watch.id, label: shortLabel(watch) };
|
|
237
|
-
} else {
|
|
238
|
-
this.notice = `${watch.id} is ${watch.status}; nothing to stop.`;
|
|
239
|
-
}
|
|
240
|
-
this.requestRender();
|
|
241
|
-
} else if (data === "d") {
|
|
242
|
-
const stopped = ordered.filter((watch) => watch.status !== "running");
|
|
243
|
-
if (stopped.length > 0) this.confirm = { kind: "clear", count: stopped.length };
|
|
244
|
-
else this.notice = "No stopped watchers to clear.";
|
|
245
|
-
this.requestRender();
|
|
246
|
-
}
|
|
142
|
+
protected detailBlock(watch: WatchRecord): string[] {
|
|
143
|
+
return formatWatchDetail(watch, Date.now());
|
|
247
144
|
}
|
|
248
145
|
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.cursor.clamp(this.ordered, watchId);
|
|
257
|
-
this.requestRender();
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (data === "n" || data === "N" || isCloseKey(data) || data === "q") {
|
|
261
|
-
this.confirm = undefined;
|
|
262
|
-
this.requestRender();
|
|
263
|
-
}
|
|
146
|
+
protected summaryCells(rows: readonly WatchRecord[]): string[] {
|
|
147
|
+
const cells = [paint(this.theme, "meta", `${rows.length} watcher${rows.length === 1 ? "" : "s"}`)];
|
|
148
|
+
const running = rows.filter((watch) => watch.status === "running").length;
|
|
149
|
+
const matched = rows.reduce((sum, watch) => sum + watch.matchCount, 0);
|
|
150
|
+
if (running > 0) cells.push(paint(this.theme, "meta", `${running} running`));
|
|
151
|
+
if (matched > 0) cells.push(paint(this.theme, "accent", `${matched} matches`));
|
|
152
|
+
return cells;
|
|
264
153
|
}
|
|
265
154
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
155
|
+
protected listHints(): ScreenHint[] {
|
|
156
|
+
return [
|
|
157
|
+
"Esc close",
|
|
158
|
+
"↑/↓ or j/k select",
|
|
159
|
+
{ text: "Enter or l matches", color: "accent" },
|
|
160
|
+
{ text: "x or K stop", color: "warning" },
|
|
161
|
+
{ text: "d clear stopped", color: "warning" },
|
|
162
|
+
"PgUp/PgDn",
|
|
163
|
+
];
|
|
271
164
|
}
|
|
272
165
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
this.mode = "list";
|
|
276
|
-
this.pane.close();
|
|
277
|
-
this.requestRender();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (this.pane.handleKey(data)) this.requestRender();
|
|
166
|
+
protected detailHints(): ScreenHint[] {
|
|
167
|
+
return ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"];
|
|
281
168
|
}
|
|
282
169
|
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return clip(` ${fitCells(cells, Math.max(1, width - 2), separator)}`, width);
|
|
170
|
+
protected detailFor(watch: WatchRecord): DetailContents {
|
|
171
|
+
const result = this.callbacks.lines(watch.id);
|
|
172
|
+
return {
|
|
173
|
+
title: `${watch.id} ${shortLabel(watch)}`,
|
|
174
|
+
lines: result?.lines ?? [],
|
|
175
|
+
more: result?.more ?? false,
|
|
176
|
+
};
|
|
291
177
|
}
|
|
292
178
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return this.mode === "lines" ? this.renderLines(w) : this.renderList(w);
|
|
179
|
+
protected detailById(id: string): { lines: string[]; more?: boolean } | undefined {
|
|
180
|
+
return this.callbacks.lines(id);
|
|
296
181
|
}
|
|
297
182
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
this.viewportRowsSource,
|
|
306
|
-
watches.length,
|
|
307
|
-
LIST_CHROME_ROWS + detail.length,
|
|
308
|
-
SCREEN_DEFAULT_ROWS,
|
|
309
|
-
);
|
|
310
|
-
this.cursor.clamp(watches, watchId);
|
|
311
|
-
|
|
312
|
-
const body: string[] = [];
|
|
313
|
-
let range: string | undefined;
|
|
314
|
-
if (watches.length === 0) {
|
|
315
|
-
body.push(...emptyState(this.theme, w, { label: "No watchers." }));
|
|
316
|
-
} else {
|
|
317
|
-
const end = Math.min(watches.length, this.cursor.scroll + this.cursor.visible);
|
|
318
|
-
for (let i = this.cursor.scroll; i < end; i++)
|
|
319
|
-
body.push(watchRow(watches[i], this.theme, w, { selected: i === index }));
|
|
320
|
-
if (this.cursor.scroll > 0 || end < watches.length) {
|
|
321
|
-
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: watches.length });
|
|
183
|
+
protected handleVerb(key: string, watch: WatchRecord | undefined): void {
|
|
184
|
+
if (key === "x" || key === "K") {
|
|
185
|
+
if (!watch) return;
|
|
186
|
+
if (watch.status === "running") {
|
|
187
|
+
this.confirm(`Stop ${watch.id} (${shortLabel(watch)})? y/N`, () => this.callbacks.stop(watch.id));
|
|
188
|
+
} else {
|
|
189
|
+
this.notify(`${watch.id} is ${watch.status}; nothing to stop.`);
|
|
322
190
|
}
|
|
191
|
+
return;
|
|
323
192
|
}
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
: screenHint(this.theme, w, [
|
|
330
|
-
"Esc close",
|
|
331
|
-
"↑/↓ or j/k select",
|
|
332
|
-
"Enter or l matches",
|
|
333
|
-
"x or K stop",
|
|
334
|
-
"d clear stopped",
|
|
335
|
-
"PgUp/PgDn",
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
return dockListFrame({
|
|
339
|
-
header: titleRule(this.theme, w, "Watch"),
|
|
340
|
-
summary: this.summary(watches, w),
|
|
341
|
-
body,
|
|
342
|
-
range,
|
|
343
|
-
// The detail pane is separated from the list by a blank line.
|
|
344
|
-
detail: ["", ...detail.map((line) => clip(` ${paint(this.theme, "meta", line)}`, w))],
|
|
345
|
-
hint,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private renderLines(w: number): string[] {
|
|
350
|
-
const visible = fitRows(this.viewportRowsSource, this.pane.count, LINES_CHROME_ROWS, SCREEN_DEFAULT_ROWS);
|
|
351
|
-
this.pane.fit(visible);
|
|
352
|
-
|
|
353
|
-
const body: string[] = [];
|
|
354
|
-
let range: string | undefined;
|
|
355
|
-
if (this.pane.count === 0) {
|
|
356
|
-
body.push(...emptyState(this.theme, w, { label: "No matches yet." }));
|
|
357
|
-
} else {
|
|
358
|
-
const { start, end } = this.pane.window();
|
|
359
|
-
for (let i = start; i < end; i++) body.push(clip(` ${this.pane.lines[i]}`, w));
|
|
360
|
-
if (start > 0 || end < this.pane.count) {
|
|
361
|
-
range = formatRange(this.theme, w, { start, end, total: this.pane.count, label: "line" });
|
|
362
|
-
}
|
|
193
|
+
if (key !== "d") return;
|
|
194
|
+
const stopped = this.rows().filter((row) => row.status !== "running");
|
|
195
|
+
if (stopped.length === 0) {
|
|
196
|
+
this.notify("No stopped watchers to clear.");
|
|
197
|
+
return;
|
|
363
198
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
368
|
-
body,
|
|
369
|
-
range,
|
|
370
|
-
hint: screenHint(this.theme, w, ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"]),
|
|
371
|
-
});
|
|
199
|
+
this.confirm(`Clear ${stopped.length} stopped watcher${stopped.length === 1 ? "" : "s"}? y/N`, () =>
|
|
200
|
+
this.callbacks.clear(),
|
|
201
|
+
);
|
|
372
202
|
}
|
|
373
203
|
}
|
package/extensions/watch/ui.ts
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
* Presentation for the watch extension: the footer status chips and the match
|
|
3
3
|
* alert.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* keys, glyphs, and counts — is watch-specific.
|
|
5
|
+
* The chips and the context they paint through come from the shared
|
|
6
|
+
* `lib/tui/status-controller.ts`, so only the vocabulary — the keys, glyphs,
|
|
7
|
+
* counts, and message text — is watch-specific.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
10
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
13
11
|
import { createStatusController, type StatusChip, type StatusController } from "../../lib/tui/status-controller.ts";
|
|
14
12
|
import type { WatchConfig } from "./config.ts";
|
|
@@ -28,8 +26,6 @@ export interface UiOptions {
|
|
|
28
26
|
|
|
29
27
|
export function createUiController(options: UiOptions): UiController {
|
|
30
28
|
const { getWatches, getUnseenMatches, getConfig } = options;
|
|
31
|
-
// The match alert also needs the context, so it is mirrored here.
|
|
32
|
-
let ctx: ExtensionContext | undefined;
|
|
33
29
|
|
|
34
30
|
const controller = createStatusController({
|
|
35
31
|
keys: [STATUS_KEYS.watch, STATUS_KEYS.watchMatches],
|
|
@@ -45,31 +41,8 @@ export function createUiController(options: UiOptions): UiController {
|
|
|
45
41
|
});
|
|
46
42
|
|
|
47
43
|
const notifyMatch = (watch: WatchRecord, line: string): void => {
|
|
48
|
-
|
|
49
|
-
if (!target) return;
|
|
50
|
-
try {
|
|
51
|
-
target.ui.notify(`watch ${watch.id} (${shortLabel(watch)}) matched: ${line}`, "warning");
|
|
52
|
-
} catch {
|
|
53
|
-
// UI may be unavailable in non-interactive modes.
|
|
54
|
-
}
|
|
44
|
+
controller.notify(`watch ${watch.id} (${shortLabel(watch)}) matched: ${line}`, "warning");
|
|
55
45
|
};
|
|
56
46
|
|
|
57
|
-
return {
|
|
58
|
-
attach: (next) => {
|
|
59
|
-
ctx = next;
|
|
60
|
-
controller.attach(next);
|
|
61
|
-
},
|
|
62
|
-
detach: () => {
|
|
63
|
-
ctx = undefined;
|
|
64
|
-
controller.detach();
|
|
65
|
-
},
|
|
66
|
-
paint: () => {
|
|
67
|
-
controller.paint();
|
|
68
|
-
},
|
|
69
|
-
setStatus: (next) => {
|
|
70
|
-
if (next) ctx = next;
|
|
71
|
-
controller.setStatus(next);
|
|
72
|
-
},
|
|
73
|
-
notifyMatch,
|
|
74
|
-
};
|
|
47
|
+
return { ...controller, notifyMatch };
|
|
75
48
|
}
|
|
@@ -371,7 +371,7 @@ the content type and size.
|
|
|
371
371
|
| `fetch/page.ts` | Fetch + redirect-following + content-type routing |
|
|
372
372
|
| `fetch/ssrf.ts` | URL/host validation and private-address blocking |
|
|
373
373
|
| `fetch/extract.ts` | HTML → title + readable text |
|
|
374
|
-
| `fetch/
|
|
374
|
+
| `fetch/page.ts` | Fetch + redirect-following + content-type routing; PDF extraction is injected via `FetchDeps` from the shared `lib/pdf.ts` |
|
|
375
375
|
| `fetch/find.ts` | Passage search (exact/insensitive/fuzzy) |
|
|
376
376
|
| `fetch/cache.ts` | Per-registration LRU/TTL page cache (`createPageCache`) |
|
|
377
377
|
| `fetch/format.ts`, `fetch/types.ts` | Result text and data types |
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { WebFetchConfig } from "./config.ts";
|
|
13
13
|
import { extractReadable } from "./extract.ts";
|
|
14
|
-
import { extractPdfText, type PdfExtractor } from "
|
|
14
|
+
import { extractPdfText, type PdfExtractor } from "../../../lib/pdf.ts";
|
|
15
15
|
import { createFetchRunner, type HttpRunner, type HttpResponse } from "../http.ts";
|
|
16
16
|
import { decodeHtml } from "../charset.ts";
|
|
17
17
|
import { assertAllowedUrl } from "./ssrf.ts";
|
|
@@ -149,7 +149,11 @@ function pageResult(loaded: LoadedPage, request: FetchRequest): { response: Fetc
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/** Build the response entry and text for a failed URL. */
|
|
152
|
-
function
|
|
152
|
+
function fetchErrorResult(
|
|
153
|
+
url: string,
|
|
154
|
+
request: FetchRequest,
|
|
155
|
+
message: string,
|
|
156
|
+
): { response: FetchResponse; text: string } {
|
|
153
157
|
return {
|
|
154
158
|
response: {
|
|
155
159
|
url,
|
|
@@ -209,7 +213,7 @@ export function registerFetchTool(pi: ExtensionAPI, deps: FetchToolDeps = {}): v
|
|
|
209
213
|
({ response: page, text } = pageResult(loaded, request));
|
|
210
214
|
} catch (error) {
|
|
211
215
|
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
-
({ response: page, text } =
|
|
216
|
+
({ response: page, text } = fetchErrorResult(request.url, request, message));
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
return {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* object created per extension instance, so there is no module-level state.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { messageOf } from "../../../lib/errors.ts";
|
|
10
11
|
import { createFetchRunner, type HttpRunner } from "../http.ts";
|
|
11
12
|
import { resolveSearchApiKey, type WebSearchConfig } from "./config.ts";
|
|
12
13
|
import { resolveProvider } from "./registry.ts";
|
|
@@ -75,10 +76,6 @@ export function createThrottle(now: () => number = Date.now, sleep: ThrottleSlee
|
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function describe(error: unknown): string {
|
|
79
|
-
return error instanceof Error ? error.message : String(error);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
79
|
/** A model-readable reason a forced provider cannot run. */
|
|
83
80
|
function notConfigured(id: SearchProviderId): string {
|
|
84
81
|
if (id === "searxng") {
|
|
@@ -119,7 +116,7 @@ export async function runSearch(
|
|
|
119
116
|
});
|
|
120
117
|
} catch (error) {
|
|
121
118
|
if (error instanceof WebSearchError) throw error;
|
|
122
|
-
throw new WebSearchError(`Search failed: ${
|
|
119
|
+
throw new WebSearchError(`Search failed: ${messageOf(error)}`);
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
if (!result.answer && result.results.length === 0) {
|
|
@@ -516,7 +516,7 @@ status, which is what the old start/stop guidance used to do.
|
|
|
516
516
|
| `lock.ts` | Poller lockfile. |
|
|
517
517
|
| `format.ts` | Sanitize, chunk, and label helpers (text only). |
|
|
518
518
|
| `notices.ts` | The peer-facing notice catalog: one line per kind, in the peer's language. |
|
|
519
|
-
| `errors.ts` | `messageOf`, the one error-to-line helper every notice and tool error uses. |
|
|
519
|
+
| `errors.ts` | Re-exports `messageOf` from `lib/errors.ts`, the one error-to-line helper every notice and tool error uses. |
|
|
520
520
|
| `timer.ts` | The timer seam: the cancellable one-shot `ArmTimer`, `defaultTimer`, and the abortable `defaultSleep`. |
|
|
521
521
|
| `interaction.ts` | The WeChat `InteractionChannel`: pending-prompt state machine and reply routing. |
|
|
522
522
|
| `prompt.ts` | Pure prompt formatting and reply parsing. |
|
|
@@ -421,6 +421,29 @@ export class WechatClient {
|
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
/** A response body's rejection: the code it reported and the server's own description. */
|
|
425
|
+
export interface Rejection {
|
|
426
|
+
/** The non-zero `ret`, or the `errcode` when `ret` is zero or absent. */
|
|
427
|
+
code: number;
|
|
428
|
+
/** The server's `errmsg`, when it sent one. */
|
|
429
|
+
detail?: string;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The body-level rejection a response carries, or `undefined` when it was
|
|
434
|
+
* accepted.
|
|
435
|
+
*
|
|
436
|
+
* A body may carry `ret: 0` plus a real `errcode`, so the non-zero value wins:
|
|
437
|
+
* reporting `ret 0` would hide the actual reason. The server's `errmsg` is the
|
|
438
|
+
* only way to tell a bad token from a rate limit, so it travels with the code.
|
|
439
|
+
*/
|
|
440
|
+
export function rejectionOf(response: { ret?: number; errcode?: number; errmsg?: string }): Rejection | undefined {
|
|
441
|
+
const ret = response.ret ?? 0;
|
|
442
|
+
const errcode = response.errcode ?? 0;
|
|
443
|
+
if (ret === 0 && errcode === 0) return undefined;
|
|
444
|
+
return { code: ret !== 0 ? ret : errcode, detail: response.errmsg };
|
|
445
|
+
}
|
|
446
|
+
|
|
424
447
|
/** True when a response reports an expired bot token. */
|
|
425
448
|
export function isSessionExpired(response: { ret?: number; errcode?: number }): boolean {
|
|
426
449
|
return response.ret === SESSION_EXPIRED || response.errcode === SESSION_EXPIRED;
|