@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
package/extensions/job/tui.ts
CHANGED
|
@@ -6,34 +6,24 @@
|
|
|
6
6
|
* computed from `Date.now()` at render, so the command can refresh the screen
|
|
7
7
|
* without rebuilding the component.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The screen is drawn from the design system: a label-first `titleRule`, one
|
|
10
|
+
* `listRow` grammar, `emptyState`, and the shared `IdCursor` (a live job list is
|
|
11
|
+
* re-sorted under the focus) and `LinePane` (the log tail).
|
|
12
|
+
*
|
|
13
|
+
* Log text arrives already sanitized by the runtime, so no untrusted escape
|
|
14
|
+
* sequence reaches the terminal through here.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "
|
|
24
|
-
import {
|
|
25
|
-
clampScroll,
|
|
26
|
-
fitRows,
|
|
27
|
-
formatRange,
|
|
28
|
-
isCloseKey,
|
|
29
|
-
keepVisible,
|
|
30
|
-
navIntent,
|
|
31
|
-
navigateScroll,
|
|
32
|
-
selectionMarker,
|
|
33
|
-
wheelDelta,
|
|
34
|
-
} from "../../lib/list-cursor.ts";
|
|
35
|
-
import { screenHeader, screenHint, type ViewportRowsSource } from "../../lib/tui.ts";
|
|
36
|
-
import { expandHint } from "../../lib/ui.ts";
|
|
18
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
19
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
20
|
+
import { clip, fitCells } from "../../lib/tui/columns.ts";
|
|
21
|
+
import { fitRows, formatRange, IdCursor, isCloseKey, navIntent, wheelDelta } from "../../lib/tui/cursor.ts";
|
|
22
|
+
import { expandHint, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
23
|
+
import { LinePane } from "../../lib/tui/line-pane.ts";
|
|
24
|
+
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
25
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
26
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
37
27
|
import {
|
|
38
28
|
compareJobs,
|
|
39
29
|
elapsedMs,
|
|
@@ -42,7 +32,8 @@ import {
|
|
|
42
32
|
jobCounts,
|
|
43
33
|
sanitizeLogLine,
|
|
44
34
|
shortLabel,
|
|
45
|
-
|
|
35
|
+
statusGlyphChar,
|
|
36
|
+
statusRole,
|
|
46
37
|
type JobCounts,
|
|
47
38
|
} from "./format.ts";
|
|
48
39
|
import type { JobRecord } from "./types.ts";
|
|
@@ -50,18 +41,30 @@ import type { JobRecord } from "./types.ts";
|
|
|
50
41
|
/** Rows the `/jobs` screen body shows when the terminal height is unknown. */
|
|
51
42
|
const SCREEN_DEFAULT_ROWS = 14;
|
|
52
43
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
/** Fixed rows around the list body: header, summary, blank, blank, hint, blank. */
|
|
45
|
+
const LIST_CHROME_ROWS = 7;
|
|
46
|
+
|
|
47
|
+
/** Fixed rows around the log body: header, subtitle, blank, blank, hint, blank. */
|
|
48
|
+
const LOG_CHROME_ROWS = 6;
|
|
49
|
+
|
|
50
|
+
/** A job's stable identity, for the id-anchored cursor. */
|
|
51
|
+
const jobId = (job: JobRecord): string => job.id;
|
|
52
|
+
|
|
53
|
+
/** One job row: selection marker, status glyph, id, label, and a right-aligned
|
|
54
|
+
* elapsed value. `statusRow` owns the clipping and the alignment. */
|
|
55
|
+
function jobRow(
|
|
56
|
+
job: JobRecord,
|
|
57
|
+
theme: Theme,
|
|
58
|
+
width: number,
|
|
59
|
+
options: Pick<StatusRow, "selected" | "marker"> = {},
|
|
60
|
+
): string {
|
|
61
|
+
return statusRow(theme, width, {
|
|
62
|
+
...options,
|
|
63
|
+
glyph: statusGlyphChar(job.status),
|
|
64
|
+
glyphRole: statusRole(job.status),
|
|
65
|
+
label: `${job.id} ${shortLabel(job)}`,
|
|
66
|
+
trailing: formatDuration(elapsedMs(job, Date.now())),
|
|
67
|
+
});
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/** Input for the transcript result: a themed job list or a log tail. */
|
|
@@ -96,24 +99,18 @@ export class JobResult implements Component {
|
|
|
96
99
|
// A leading blank line separates the result from the call header.
|
|
97
100
|
const lines: string[] = [""];
|
|
98
101
|
if (this.input.kind === "list") {
|
|
99
|
-
for (const job of this.input.jobs) lines.push(jobRow(job, this.theme, w));
|
|
102
|
+
for (const job of this.input.jobs) lines.push(jobRow(job, this.theme, w, { marker: false }));
|
|
100
103
|
if (this.input.more > 0) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
+
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
105
|
+
lines.push(clip(` ${note}`, w));
|
|
104
106
|
}
|
|
105
107
|
} else {
|
|
106
108
|
if (this.input.earlier > 0) {
|
|
107
|
-
lines.
|
|
108
|
-
|
|
109
|
-
`${this.theme.fg("dim", `… ${this.input.earlier} earlier lines`)} ${expandHint(this.theme)}`,
|
|
110
|
-
w,
|
|
111
|
-
"…",
|
|
112
|
-
),
|
|
113
|
-
);
|
|
109
|
+
const note = `${paint(this.theme, "faint", `… ${this.input.earlier} earlier lines`)} ${expandHint(this.theme)}`;
|
|
110
|
+
lines.push(clip(note, w));
|
|
114
111
|
}
|
|
115
112
|
for (const line of this.input.lines) {
|
|
116
|
-
lines.push(this.theme.fg("toolOutput",
|
|
113
|
+
lines.push(this.theme.fg("toolOutput", clip(line, w)));
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
return lines;
|
|
@@ -135,24 +132,14 @@ type PendingConfirm =
|
|
|
135
132
|
/** Dismissible `/jobs` screen: a selectable list with a focus pane and log view. */
|
|
136
133
|
export class JobListComponent implements Component {
|
|
137
134
|
private mode: "list" | "logs" = "list";
|
|
138
|
-
/**
|
|
139
|
-
private
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
private logJobId: string | undefined;
|
|
143
|
-
private logScroll = 0;
|
|
144
|
-
private logLines: string[] = [];
|
|
145
|
-
private logTitle = "";
|
|
146
|
-
/** Absolute log-file path of the open job, and whether earlier lines exist. */
|
|
147
|
-
private logPath: string | undefined;
|
|
148
|
-
private logMore = false;
|
|
149
|
-
private follow = true;
|
|
135
|
+
/** Focus anchored to a job id; the list is re-sorted from live state each render. */
|
|
136
|
+
private readonly cursor = new IdCursor();
|
|
137
|
+
/** The open log tail. */
|
|
138
|
+
private readonly pane = new LinePane();
|
|
150
139
|
/** Pending destructive action awaiting y/N. */
|
|
151
140
|
private confirm: PendingConfirm | undefined;
|
|
152
141
|
/** Transient feedback shown in place of the hint, e.g. a no-op action. */
|
|
153
142
|
private notice: string | undefined;
|
|
154
|
-
/** Rows shown at once; recomputed from the terminal height on every render. */
|
|
155
|
-
private visible = SCREEN_DEFAULT_ROWS;
|
|
156
143
|
|
|
157
144
|
constructor(
|
|
158
145
|
private readonly jobs: () => JobRecord[],
|
|
@@ -167,94 +154,47 @@ export class JobListComponent implements Component {
|
|
|
167
154
|
|
|
168
155
|
/** Replace the log pane contents (called by the command's poll loop). */
|
|
169
156
|
setLogs(title: string, lines: string[], meta: { more?: boolean; logPath?: string } = {}): void {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.logTitle = title;
|
|
175
|
-
this.logLines = lines;
|
|
176
|
-
if (meta.more !== undefined) this.logMore = meta.more;
|
|
177
|
-
if (meta.logPath !== undefined) this.logPath = meta.logPath;
|
|
178
|
-
if (this.follow) this.logScroll = this.maxLogScroll;
|
|
179
|
-
else this.logScroll = Math.min(this.logScroll, this.maxLogScroll);
|
|
180
|
-
// The poll fires every 500ms; skip the repaint when nothing changed so an
|
|
181
|
-
// idle log pane does not redraw (and fight the user's scroll) constantly.
|
|
182
|
-
if (titleChanged || linesChanged || moreChanged || pathChanged) this.requestRender();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private sameLines(next: string[]): boolean {
|
|
186
|
-
if (next.length !== this.logLines.length) return false;
|
|
187
|
-
for (let i = 0; i < next.length; i++) {
|
|
188
|
-
if (next[i] !== this.logLines[i]) return false;
|
|
189
|
-
}
|
|
190
|
-
return true;
|
|
157
|
+
// The poll fires every 500ms; `LinePane.update` skips the repaint when
|
|
158
|
+
// nothing changed so an idle log pane does not redraw (and fight the
|
|
159
|
+
// user's scroll) constantly.
|
|
160
|
+
if (this.pane.update({ title, lines, more: meta.more, subtitle: meta.logPath })) this.requestRender();
|
|
191
161
|
}
|
|
192
162
|
|
|
193
163
|
/** The job whose log pane is open, or undefined in list mode. */
|
|
194
164
|
currentLogId(): string | undefined {
|
|
195
|
-
return this.mode === "logs" ? this.
|
|
165
|
+
return this.mode === "logs" && this.pane.id ? this.pane.id : undefined;
|
|
196
166
|
}
|
|
197
167
|
|
|
198
168
|
/** Re-read the open log pane from the runtime. */
|
|
199
169
|
refreshLogs(id: string): void {
|
|
200
170
|
const result = this.callbacks.logs(id);
|
|
201
|
-
if (result
|
|
171
|
+
if (result && this.pane.update({ lines: result.lines, more: result.more })) this.requestRender();
|
|
202
172
|
}
|
|
203
173
|
|
|
204
174
|
private get ordered(): JobRecord[] {
|
|
205
175
|
return [...this.jobs()].sort(compareJobs);
|
|
206
176
|
}
|
|
207
177
|
|
|
208
|
-
/**
|
|
209
|
-
private
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Scroll the list so `index` is visible. */
|
|
216
|
-
private ensureVisible(index: number): void {
|
|
217
|
-
this.listScroll = Math.max(0, keepVisible(this.listScroll, index, this.visible));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private get maxLogScroll(): number {
|
|
221
|
-
return Math.max(0, this.logLines.length - this.visible);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private setSelection(next: number): void {
|
|
225
|
-
const ordered = this.ordered;
|
|
226
|
-
if (ordered.length === 0) return;
|
|
227
|
-
const index = Math.min(Math.max(0, next), ordered.length - 1);
|
|
228
|
-
this.selectedId = ordered[index].id;
|
|
229
|
-
this.ensureVisible(index);
|
|
178
|
+
/** Focus job `next` (clamped) and keep it visible. */
|
|
179
|
+
private select(next: number): void {
|
|
180
|
+
const jobs = this.ordered;
|
|
181
|
+
if (jobs.length === 0) return;
|
|
182
|
+
this.cursor.select(jobs, jobId, next);
|
|
230
183
|
this.requestRender();
|
|
231
184
|
}
|
|
232
185
|
|
|
233
|
-
private clampSelection(): void {
|
|
234
|
-
const ordered = this.ordered;
|
|
235
|
-
if (ordered.length === 0) {
|
|
236
|
-
this.selectedId = undefined;
|
|
237
|
-
this.listScroll = 0;
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
this.selectedId = ordered[this.indexOfSelected(ordered)].id;
|
|
241
|
-
this.listScroll = clampScroll(this.listScroll, ordered.length, this.visible);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
186
|
private openLogs(): void {
|
|
245
|
-
const job = this.ordered[this.
|
|
187
|
+
const job = this.ordered[this.cursor.indexOf(this.ordered, jobId)];
|
|
246
188
|
if (!job) return;
|
|
247
189
|
this.mode = "logs";
|
|
248
|
-
this.logJobId = job.id;
|
|
249
|
-
this.logScroll = 0;
|
|
250
|
-
this.follow = true;
|
|
251
|
-
this.logLines = [];
|
|
252
|
-
this.logTitle = `${job.id} ${shortLabel(job)}`;
|
|
253
|
-
this.logPath = job.logPath;
|
|
254
|
-
this.logMore = false;
|
|
255
190
|
const result = this.callbacks.logs(job.id);
|
|
256
|
-
|
|
257
|
-
|
|
191
|
+
this.pane.open(job.id, {
|
|
192
|
+
title: `${job.id} ${shortLabel(job)}`,
|
|
193
|
+
lines: result?.lines ?? [],
|
|
194
|
+
more: result?.more ?? false,
|
|
195
|
+
subtitle: job.logPath ? sanitizeLogLine(job.logPath) : undefined,
|
|
196
|
+
});
|
|
197
|
+
this.requestRender();
|
|
258
198
|
}
|
|
259
199
|
|
|
260
200
|
handleInput(data: string): void {
|
|
@@ -272,9 +212,10 @@ export class JobListComponent implements Component {
|
|
|
272
212
|
if (delta === undefined) return undefined;
|
|
273
213
|
if (this.confirm) return { handled: true };
|
|
274
214
|
if (this.mode === "list") {
|
|
275
|
-
this.
|
|
215
|
+
this.select(this.cursor.indexOf(this.ordered, jobId) + delta);
|
|
276
216
|
} else {
|
|
277
|
-
this.
|
|
217
|
+
this.pane.setScroll(this.pane.scroll + delta);
|
|
218
|
+
this.requestRender();
|
|
278
219
|
}
|
|
279
220
|
return { handled: true };
|
|
280
221
|
}
|
|
@@ -290,25 +231,25 @@ export class JobListComponent implements Component {
|
|
|
290
231
|
this.requestRender();
|
|
291
232
|
}
|
|
292
233
|
const ordered = this.ordered;
|
|
293
|
-
const index = this.
|
|
234
|
+
const index = this.cursor.indexOf(ordered, jobId);
|
|
294
235
|
switch (navIntent(data)) {
|
|
295
236
|
case "up":
|
|
296
|
-
this.
|
|
237
|
+
this.select(index - 1);
|
|
297
238
|
return;
|
|
298
239
|
case "down":
|
|
299
|
-
this.
|
|
240
|
+
this.select(index + 1);
|
|
300
241
|
return;
|
|
301
242
|
case "pageUp":
|
|
302
|
-
this.
|
|
243
|
+
this.select(index - this.cursor.visible);
|
|
303
244
|
return;
|
|
304
245
|
case "pageDown":
|
|
305
|
-
this.
|
|
246
|
+
this.select(index + this.cursor.visible);
|
|
306
247
|
return;
|
|
307
248
|
case "home":
|
|
308
|
-
this.
|
|
249
|
+
this.select(0);
|
|
309
250
|
return;
|
|
310
251
|
case "end":
|
|
311
|
-
this.
|
|
252
|
+
this.select(ordered.length - 1);
|
|
312
253
|
return;
|
|
313
254
|
}
|
|
314
255
|
if (matchesKey(data, Key.enter) || data === "l") this.openLogs();
|
|
@@ -346,7 +287,7 @@ export class JobListComponent implements Component {
|
|
|
346
287
|
if (pending.kind === "kill") this.callbacks.kill(pending.id);
|
|
347
288
|
else {
|
|
348
289
|
this.callbacks.clear();
|
|
349
|
-
this.
|
|
290
|
+
this.cursor.clamp(this.ordered, jobId);
|
|
350
291
|
}
|
|
351
292
|
this.requestRender();
|
|
352
293
|
return;
|
|
@@ -369,35 +310,22 @@ export class JobListComponent implements Component {
|
|
|
369
310
|
return `Clear ${pending.count} finished job${pending.count === 1 ? "" : "s"}?${note} y/N`;
|
|
370
311
|
}
|
|
371
312
|
|
|
372
|
-
private setLogScroll(next: number): void {
|
|
373
|
-
this.logScroll = clampScroll(next, this.logLines.length, this.visible);
|
|
374
|
-
// Follow the tail only while the view is parked at the bottom.
|
|
375
|
-
this.follow = this.logScroll >= this.maxLogScroll;
|
|
376
|
-
this.requestRender();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
313
|
private handleLogInput(data: string): void {
|
|
380
314
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.backspace) || data === "q") {
|
|
381
315
|
this.mode = "list";
|
|
382
|
-
this.
|
|
316
|
+
this.pane.close();
|
|
383
317
|
this.requestRender();
|
|
384
318
|
return;
|
|
385
319
|
}
|
|
386
|
-
|
|
387
|
-
if (next !== undefined) {
|
|
388
|
-
this.setLogScroll(next);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (data === "g") this.setLogScroll(0);
|
|
392
|
-
else if (data === "G") this.setLogScroll(this.maxLogScroll);
|
|
320
|
+
if (this.pane.handleKey(data)) this.requestRender();
|
|
393
321
|
}
|
|
394
322
|
|
|
395
323
|
private summary(counts: JobCounts, width: number): string {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
if (counts.
|
|
399
|
-
|
|
400
|
-
return
|
|
324
|
+
const cells = [paint(this.theme, "meta", `${counts.total} job${counts.total === 1 ? "" : "s"}`)];
|
|
325
|
+
if (counts.running > 0) cells.push(paint(this.theme, "meta", `${counts.running} running`));
|
|
326
|
+
if (counts.failed > 0) cells.push(paint(this.theme, "bad", `${counts.failed} failed`));
|
|
327
|
+
const separator = paint(this.theme, "meta", SEPARATORS.item);
|
|
328
|
+
return clip(` ${fitCells(cells, Math.max(1, width - 2), separator)}`, width);
|
|
401
329
|
}
|
|
402
330
|
|
|
403
331
|
render(width: number): string[] {
|
|
@@ -407,80 +335,78 @@ export class JobListComponent implements Component {
|
|
|
407
335
|
|
|
408
336
|
private renderList(w: number): string[] {
|
|
409
337
|
const jobs = this.ordered;
|
|
410
|
-
this.
|
|
411
|
-
const index = this.
|
|
338
|
+
this.cursor.clamp(jobs, jobId);
|
|
339
|
+
const index = this.cursor.indexOf(jobs, jobId);
|
|
412
340
|
const focused = jobs[index];
|
|
413
341
|
const detail = focused ? formatJobDetail(focused, Date.now()) : [];
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
342
|
+
this.cursor.visible = fitRows(
|
|
343
|
+
this.viewportRowsSource,
|
|
344
|
+
jobs.length,
|
|
345
|
+
LIST_CHROME_ROWS + detail.length,
|
|
346
|
+
SCREEN_DEFAULT_ROWS,
|
|
347
|
+
);
|
|
348
|
+
this.cursor.clamp(jobs, jobId);
|
|
349
|
+
this.cursor.ensureVisible(this.cursor.indexOf(jobs, jobId));
|
|
419
350
|
|
|
420
|
-
const
|
|
351
|
+
const body: string[] = [];
|
|
352
|
+
let range: string | undefined;
|
|
421
353
|
if (jobs.length === 0) {
|
|
422
|
-
|
|
354
|
+
body.push(...emptyState(this.theme, w, { label: "No background jobs." }));
|
|
423
355
|
} else {
|
|
424
|
-
const end = Math.min(jobs.length, this.
|
|
425
|
-
for (let i = this.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
lines.push(formatRange(this.theme, w, { start: this.listScroll, end, total: jobs.length }));
|
|
356
|
+
const end = Math.min(jobs.length, this.cursor.scroll + this.cursor.visible);
|
|
357
|
+
for (let i = this.cursor.scroll; i < end; i++)
|
|
358
|
+
body.push(jobRow(jobs[i], this.theme, w, { selected: i === index }));
|
|
359
|
+
if (this.cursor.scroll > 0 || end < jobs.length) {
|
|
360
|
+
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: jobs.length });
|
|
430
361
|
}
|
|
431
362
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
363
|
+
|
|
364
|
+
const hint = this.confirm
|
|
365
|
+
? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
|
|
366
|
+
: this.notice
|
|
367
|
+
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
368
|
+
: screenHint(this.theme, w, [
|
|
369
|
+
"Esc close",
|
|
370
|
+
"↑/↓ or j/k select",
|
|
371
|
+
"Enter or l logs",
|
|
372
|
+
"d or K kill",
|
|
373
|
+
"x clear finished",
|
|
374
|
+
"PgUp/PgDn",
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
return dockListFrame({
|
|
378
|
+
header: titleRule(this.theme, w, "Jobs"),
|
|
379
|
+
summary: this.summary(jobCounts(jobs), w),
|
|
380
|
+
body,
|
|
381
|
+
range,
|
|
382
|
+
// The detail pane is separated from the list by a blank line.
|
|
383
|
+
detail: ["", ...detail.map((line) => clip(` ${paint(this.theme, "meta", line)}`, w))],
|
|
384
|
+
hint,
|
|
385
|
+
});
|
|
451
386
|
}
|
|
452
387
|
|
|
453
388
|
private renderLogs(w: number): string[] {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const parts: string[] = [];
|
|
462
|
-
if (this.logPath) parts.push(sanitizeLogLine(this.logPath));
|
|
463
|
-
if (this.logMore && this.logLines.length > 0) parts.push(`showing last ${this.logLines.length} lines`);
|
|
464
|
-
// Say whether the pane follows the tail or the user has scrolled away from it.
|
|
465
|
-
if (this.logLines.length > 0) parts.push(this.follow ? "following" : "paused");
|
|
466
|
-
const pathLine = truncateToWidth(` ${this.theme.fg("dim", parts.join(" · "))}`, w);
|
|
467
|
-
const lines: string[] = [screenHeader(this.theme, w, label), pathLine, ""];
|
|
468
|
-
if (this.logLines.length === 0) {
|
|
469
|
-
lines.push(truncateToWidth(` ${this.theme.fg("dim", "No output yet.")}`, w));
|
|
389
|
+
const visible = fitRows(this.viewportRowsSource, this.pane.count, LOG_CHROME_ROWS, SCREEN_DEFAULT_ROWS);
|
|
390
|
+
this.pane.fit(visible);
|
|
391
|
+
|
|
392
|
+
const body: string[] = [];
|
|
393
|
+
let range: string | undefined;
|
|
394
|
+
if (this.pane.count === 0) {
|
|
395
|
+
body.push(...emptyState(this.theme, w, { label: "No output yet." }));
|
|
470
396
|
} else {
|
|
471
|
-
const end =
|
|
472
|
-
for (let i =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (this.logScroll > 0 || end < this.logLines.length) {
|
|
476
|
-
lines.push(
|
|
477
|
-
formatRange(this.theme, w, { start: this.logScroll, end, total: this.logLines.length, label: "line" }),
|
|
478
|
-
);
|
|
397
|
+
const { start, end } = this.pane.window();
|
|
398
|
+
for (let i = start; i < end; i++) body.push(clip(` ${this.pane.lines[i]}`, w));
|
|
399
|
+
if (start > 0 || end < this.pane.count) {
|
|
400
|
+
range = formatRange(this.theme, w, { start, end, total: this.pane.count, label: "line" });
|
|
479
401
|
}
|
|
480
402
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
403
|
+
|
|
404
|
+
return dockListFrame({
|
|
405
|
+
header: titleRule(this.theme, w, this.pane.title("Job Logs")),
|
|
406
|
+
summary: clip(` ${paint(this.theme, "faint", this.pane.subtitleLine())}`, w),
|
|
407
|
+
body,
|
|
408
|
+
range,
|
|
409
|
+
hint: screenHint(this.theme, w, ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"]),
|
|
410
|
+
});
|
|
485
411
|
}
|
|
486
412
|
}
|
package/extensions/job/ui.ts
CHANGED
|
@@ -2,27 +2,19 @@
|
|
|
2
2
|
* Presentation for the jobs extension: the footer status chips.
|
|
3
3
|
*
|
|
4
4
|
* Kept separate from the runtime so the runtime owns lifecycle (the job table,
|
|
5
|
-
* registry, clock) and this owns "how jobs are shown".
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* current mode.
|
|
5
|
+
* registry, clock) and this owns "how jobs are shown". The chips themselves come
|
|
6
|
+
* from the shared `lib/tui/status-controller.ts`, so only the vocabulary — the
|
|
7
|
+
* keys, glyphs, and counts — is job-specific.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { GLYPHS, STATUS_KEYS } from "../../lib/
|
|
11
|
+
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
|
+
import { createStatusController, type StatusChip, type StatusController } from "../../lib/tui/status-controller.ts";
|
|
13
13
|
import type { JobsConfig } from "./config.ts";
|
|
14
14
|
import { pendingFailures, shortLabel } from "./format.ts";
|
|
15
15
|
import type { JobRecord } from "./types.ts";
|
|
16
16
|
|
|
17
|
-
export interface UiController {
|
|
18
|
-
/** Remember the live context; later calls use it when no context is passed. */
|
|
19
|
-
attach(ctx: ExtensionContext): void;
|
|
20
|
-
/** Forget the context, for example on shutdown or reload. */
|
|
21
|
-
detach(): void;
|
|
22
|
-
/** Repaint the chips using the attached context. */
|
|
23
|
-
paint(): void;
|
|
24
|
-
/** Publish the footer chips (and attach `ctx` when given). */
|
|
25
|
-
setStatus(ctx?: ExtensionContext): void;
|
|
17
|
+
export interface UiController extends StatusController {
|
|
26
18
|
/** Notify the user that a job failed, when a UI is attached. */
|
|
27
19
|
notifyFailure(job: JobRecord): void;
|
|
28
20
|
}
|
|
@@ -34,43 +26,21 @@ export interface UiOptions {
|
|
|
34
26
|
|
|
35
27
|
export function createUiController(options: UiOptions): UiController {
|
|
36
28
|
const { getJobs, getConfig } = options;
|
|
29
|
+
// The notification also needs the context, so it is mirrored here.
|
|
37
30
|
let ctx: ExtensionContext | undefined;
|
|
38
31
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return text;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const setStatus = (next?: ExtensionContext): void => {
|
|
48
|
-
if (next) ctx = next;
|
|
49
|
-
const target = ctx;
|
|
50
|
-
if (!target) return;
|
|
51
|
-
try {
|
|
52
|
-
if (!getConfig().showStatus) {
|
|
53
|
-
target.ui.setStatus(STATUS_KEYS.jobs, undefined);
|
|
54
|
-
target.ui.setStatus(STATUS_KEYS.jobsFailure, undefined);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
32
|
+
const controller = createStatusController({
|
|
33
|
+
keys: [STATUS_KEYS.jobs, STATUS_KEYS.jobsFailure],
|
|
34
|
+
enabled: () => getConfig().showStatus,
|
|
35
|
+
chips: (): StatusChip[] => {
|
|
57
36
|
const jobs = [...getJobs()];
|
|
58
37
|
const running = jobs.filter((job) => job.status === "running").length;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
target.ui.setStatus(
|
|
67
|
-
STATUS_KEYS.jobsFailure,
|
|
68
|
-
unseenFailures > 0 ? theme(target, "error", `${GLYPHS.jobsFailure} ${unseenFailures}`) : undefined,
|
|
69
|
-
);
|
|
70
|
-
} catch {
|
|
71
|
-
// UI may be unavailable in non-interactive modes.
|
|
72
|
-
}
|
|
73
|
-
};
|
|
38
|
+
return [
|
|
39
|
+
{ key: STATUS_KEYS.jobs, glyph: GLYPHS.jobsRunning, count: running, role: "accent" },
|
|
40
|
+
{ key: STATUS_KEYS.jobsFailure, glyph: GLYPHS.jobsFailure, count: pendingFailures(jobs).length, role: "bad" },
|
|
41
|
+
];
|
|
42
|
+
},
|
|
43
|
+
});
|
|
74
44
|
|
|
75
45
|
const notifyFailure = (job: JobRecord): void => {
|
|
76
46
|
const target = ctx;
|
|
@@ -86,14 +56,19 @@ export function createUiController(options: UiOptions): UiController {
|
|
|
86
56
|
return {
|
|
87
57
|
attach: (next) => {
|
|
88
58
|
ctx = next;
|
|
59
|
+
controller.attach(next);
|
|
89
60
|
},
|
|
90
61
|
detach: () => {
|
|
91
62
|
ctx = undefined;
|
|
63
|
+
controller.detach();
|
|
92
64
|
},
|
|
93
65
|
paint: () => {
|
|
94
|
-
|
|
66
|
+
controller.paint();
|
|
67
|
+
},
|
|
68
|
+
setStatus: (next) => {
|
|
69
|
+
if (next) ctx = next;
|
|
70
|
+
controller.setStatus(next);
|
|
95
71
|
},
|
|
96
|
-
setStatus,
|
|
97
72
|
notifyFailure,
|
|
98
73
|
};
|
|
99
74
|
}
|
|
@@ -13,6 +13,12 @@ pi -e . # load the whole @gavin-hu/my-pi-agent pac
|
|
|
13
13
|
pi install ./ # install the package
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Tool rendering
|
|
17
|
+
|
|
18
|
+
How the `memory` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
19
|
+
|
|
20
|
+
<img src="../../assets/tools/memory.png" alt="The memory tool's result: the call line and an indented body confirming the write and the new project and global counts" width="100%">
|
|
21
|
+
|
|
16
22
|
## What it does
|
|
17
23
|
|
|
18
24
|
- Registers one model-callable tool, `memory`, with four actions: `add` a note,
|
|
@@ -205,7 +211,7 @@ when consolidating.
|
|
|
205
211
|
| `runtime.ts` | Session view of both stores, stamp-based refresh, trust gating, mutations. |
|
|
206
212
|
| `format.ts` | Pure call/result/notice text and the bounded injection body. |
|
|
207
213
|
| `errors.ts` | `messageOf`, the shared error-text extraction for the tool and command. |
|
|
208
|
-
| `tui.ts` | The `/memory` picker component (note list, inline editor, scope filter). |
|
|
214
|
+
| `tui.ts` | The `/memory` picker component (note list, inline editor, scope filter), drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
209
215
|
| `tools.ts` | `registerTool("memory", …)`. |
|
|
210
216
|
| `commands.ts` | `registerCommand("memory", …)`. |
|
|
211
217
|
|