@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/job/tui.ts
CHANGED
|
@@ -6,31 +6,28 @@
|
|
|
6
6
|
* computed from `Date.now()` at render, so the command can refresh the screen
|
|
7
7
|
* without rebuilding the component.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The frame, the navigation keys, the y/N confirmation, and the log pane come
|
|
10
|
+
* from the shared `DockListScreen`, so this module owns only the job vocabulary:
|
|
11
|
+
* the row, the focus block, the counts, the hints, and what the verbs do.
|
|
12
12
|
*
|
|
13
13
|
* Log text arrives already sanitized by the runtime, so no untrusted escape
|
|
14
14
|
* sequence reaches the terminal through here.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
import { formatDuration } from "../../lib/format.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { emptyState, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
26
|
-
import { titleRule } from "../../lib/tui/rules.ts";
|
|
18
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
19
|
+
import { formatDuration, sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
20
|
+
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
21
|
+
import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
|
|
22
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
23
|
+
import { statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
24
|
+
import { TailResult, type TailResultInput } from "../../lib/tui/tool.ts";
|
|
27
25
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
28
26
|
import {
|
|
29
27
|
compareJobs,
|
|
30
28
|
elapsedMs,
|
|
31
29
|
formatJobDetail,
|
|
32
30
|
jobCounts,
|
|
33
|
-
sanitizeLogLine,
|
|
34
31
|
shortLabel,
|
|
35
32
|
statusGlyphChar,
|
|
36
33
|
statusRole,
|
|
@@ -38,18 +35,6 @@ import {
|
|
|
38
35
|
} from "./format.ts";
|
|
39
36
|
import type { JobRecord } from "./types.ts";
|
|
40
37
|
|
|
41
|
-
/** Rows the `/jobs` screen body shows when the terminal height is unknown. */
|
|
42
|
-
const SCREEN_DEFAULT_ROWS = 14;
|
|
43
|
-
|
|
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
38
|
/** One job row: selection marker, status glyph, id, label, and a right-aligned
|
|
54
39
|
* elapsed value, tagged `peer` when another session owns it. `statusRow` owns
|
|
55
40
|
* the clipping and the alignment. */
|
|
@@ -65,6 +50,7 @@ function jobRow(
|
|
|
65
50
|
glyph: statusGlyphChar(job.status),
|
|
66
51
|
glyphRole: statusRole(job.status),
|
|
67
52
|
label: `${job.id} ${shortLabel(job)}`,
|
|
53
|
+
labelRole: options.selected ? "accent" : "label",
|
|
68
54
|
// The tag rides the right-aligned cell, which survives a narrow terminal.
|
|
69
55
|
trailing: options.foreign ? `${elapsed}${SEPARATORS.item}peer` : elapsed,
|
|
70
56
|
});
|
|
@@ -75,51 +61,44 @@ export type JobResultInput =
|
|
|
75
61
|
| { kind: "list"; jobs: JobRecord[]; more: number; sessionId?: string }
|
|
76
62
|
| { kind: "logs"; lines: string[]; earlier: number };
|
|
77
63
|
|
|
64
|
+
/** Map the job vocabulary onto the shared transcript tail block. */
|
|
65
|
+
function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
66
|
+
if (input.kind === "logs") {
|
|
67
|
+
return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: "lines" };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: "list",
|
|
71
|
+
rows: input.jobs,
|
|
72
|
+
more: input.more,
|
|
73
|
+
rowLine: (job, width, theme) => {
|
|
74
|
+
const foreign = input.sessionId !== undefined && job.sessionId !== input.sessionId;
|
|
75
|
+
return jobRow(job, theme, width, { marker: false, foreign });
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
78
80
|
/**
|
|
79
|
-
* Transcript result block for the `job` tool:
|
|
80
|
-
* `/jobs`-style row rail
|
|
81
|
-
* themed log tail. Reused across renders via `setInput`.
|
|
81
|
+
* Transcript result block for the `job` tool: the shared tail block over the
|
|
82
|
+
* `/jobs`-style row rail or the themed log tail. Reused across renders.
|
|
82
83
|
*/
|
|
83
84
|
export class JobResult implements Component {
|
|
84
|
-
private
|
|
85
|
-
private theme: Theme;
|
|
85
|
+
private readonly view: TailResult<JobRecord>;
|
|
86
86
|
|
|
87
87
|
constructor(input: JobResultInput, theme: Theme) {
|
|
88
|
-
this.
|
|
89
|
-
this.theme = theme;
|
|
88
|
+
this.view = new TailResult(jobTail(input), theme);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/** Update in place so the transcript can reuse this component across renders. */
|
|
93
92
|
setInput(input: JobResultInput, theme: Theme): void {
|
|
94
|
-
this.input
|
|
95
|
-
this.theme = theme;
|
|
93
|
+
this.view.setInput(jobTail(input), theme);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
invalidate(): void {
|
|
96
|
+
invalidate(): void {
|
|
97
|
+
this.view.invalidate();
|
|
98
|
+
}
|
|
99
99
|
|
|
100
100
|
render(width: number): string[] {
|
|
101
|
-
|
|
102
|
-
// A leading blank line separates the result from the call header.
|
|
103
|
-
const lines: string[] = [""];
|
|
104
|
-
if (this.input.kind === "list") {
|
|
105
|
-
for (const job of this.input.jobs) {
|
|
106
|
-
const foreign = this.input.sessionId !== undefined && job.sessionId !== this.input.sessionId;
|
|
107
|
-
lines.push(jobRow(job, this.theme, w, { marker: false, foreign }));
|
|
108
|
-
}
|
|
109
|
-
if (this.input.more > 0) {
|
|
110
|
-
const note = `${paint(this.theme, "faint", `… ${this.input.more} more`)} ${expandHint(this.theme)}`;
|
|
111
|
-
lines.push(clip(` ${note}`, w));
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
if (this.input.earlier > 0) {
|
|
115
|
-
const note = `${paint(this.theme, "faint", `… ${this.input.earlier} earlier lines`)} ${expandHint(this.theme)}`;
|
|
116
|
-
lines.push(clip(note, w));
|
|
117
|
-
}
|
|
118
|
-
for (const line of this.input.lines) {
|
|
119
|
-
lines.push(this.theme.fg("toolOutput", clip(line, w)));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return lines;
|
|
101
|
+
return this.view.render(width);
|
|
123
102
|
}
|
|
124
103
|
}
|
|
125
104
|
|
|
@@ -132,55 +111,44 @@ export interface JobListCallbacks {
|
|
|
132
111
|
clear(): void;
|
|
133
112
|
}
|
|
134
113
|
|
|
135
|
-
/** A destructive action waiting for y/N confirmation. */
|
|
136
|
-
type PendingConfirm =
|
|
137
|
-
| { kind: "kill"; id: string; label: string }
|
|
138
|
-
| { kind: "clear"; count: number; unreported: number };
|
|
139
|
-
|
|
140
114
|
/** Dismissible `/jobs` screen: a selectable list with a focus pane and log view. */
|
|
141
|
-
export class JobListComponent
|
|
142
|
-
private mode: "list" | "logs" = "list";
|
|
143
|
-
/** Focus anchored to a job id; the list is re-sorted from live state each render. */
|
|
144
|
-
private readonly cursor = new IdCursor();
|
|
145
|
-
/** The open log tail. */
|
|
146
|
-
private readonly pane = new LinePane();
|
|
147
|
-
/** Pending destructive action awaiting y/N. */
|
|
148
|
-
private confirm: PendingConfirm | undefined;
|
|
149
|
-
/** Transient feedback shown in place of the hint, e.g. a no-op action. */
|
|
150
|
-
private notice: string | undefined;
|
|
151
|
-
|
|
115
|
+
export class JobListComponent extends DockListScreen<JobRecord> {
|
|
152
116
|
constructor(
|
|
153
117
|
private readonly jobs: () => JobRecord[],
|
|
154
|
-
|
|
118
|
+
theme: Theme,
|
|
155
119
|
private readonly callbacks: JobListCallbacks,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) {
|
|
160
|
-
|
|
161
|
-
|
|
120
|
+
onClose: () => void,
|
|
121
|
+
requestRender: () => void,
|
|
122
|
+
viewportRowsSource?: ViewportRowsSource,
|
|
123
|
+
) {
|
|
124
|
+
super({
|
|
125
|
+
theme,
|
|
126
|
+
title: "Jobs",
|
|
127
|
+
detailTitle: "Job Logs",
|
|
128
|
+
emptyList: "No background jobs.",
|
|
129
|
+
emptyDetail: "No output yet.",
|
|
130
|
+
requestRender,
|
|
131
|
+
onClose,
|
|
132
|
+
viewportRowsSource,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
162
135
|
|
|
163
136
|
/** Replace the log pane contents (called by the command's poll loop). */
|
|
164
137
|
setLogs(title: string, lines: string[], meta: { more?: boolean; logPath?: string } = {}): void {
|
|
165
138
|
// The poll fires every 500ms; `LinePane.update` skips the repaint when
|
|
166
139
|
// nothing changed so an idle log pane does not redraw (and fight the
|
|
167
140
|
// user's scroll) constantly.
|
|
168
|
-
|
|
141
|
+
this.setDetail({ title, lines, more: meta.more, subtitle: meta.logPath });
|
|
169
142
|
}
|
|
170
143
|
|
|
171
144
|
/** The job whose log pane is open, or undefined in list mode. */
|
|
172
145
|
currentLogId(): string | undefined {
|
|
173
|
-
return this.
|
|
146
|
+
return this.currentDetailId();
|
|
174
147
|
}
|
|
175
148
|
|
|
176
149
|
/** Re-read the open log pane from the runtime. */
|
|
177
150
|
refreshLogs(id: string): void {
|
|
178
|
-
|
|
179
|
-
if (result && this.pane.update({ lines: result.lines, more: result.more })) this.requestRender();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private get ordered(): JobRecord[] {
|
|
183
|
-
return [...this.jobs()].sort(compareJobs);
|
|
151
|
+
this.refreshDetail(id);
|
|
184
152
|
}
|
|
185
153
|
|
|
186
154
|
/** Whether another session owns this job, so the screen leaves it alone. */
|
|
@@ -188,246 +156,88 @@ export class JobListComponent implements Component {
|
|
|
188
156
|
return job.sessionId !== this.callbacks.sessionId();
|
|
189
157
|
}
|
|
190
158
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const jobs = this.ordered;
|
|
194
|
-
if (jobs.length === 0) return;
|
|
195
|
-
this.cursor.select(jobs, jobId, next);
|
|
196
|
-
this.requestRender();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private openLogs(): void {
|
|
200
|
-
const job = this.ordered[this.cursor.indexOf(this.ordered, jobId)];
|
|
201
|
-
if (!job) return;
|
|
202
|
-
this.mode = "logs";
|
|
203
|
-
const result = this.callbacks.logs(job.id);
|
|
204
|
-
this.pane.open(job.id, {
|
|
205
|
-
title: `${job.id} ${shortLabel(job)}`,
|
|
206
|
-
lines: result?.lines ?? [],
|
|
207
|
-
more: result?.more ?? false,
|
|
208
|
-
subtitle: job.logPath ? sanitizeLogLine(job.logPath) : undefined,
|
|
209
|
-
});
|
|
210
|
-
this.requestRender();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
handleInput(data: string): void {
|
|
214
|
-
if (this.confirm) {
|
|
215
|
-
this.handleConfirmInput(data);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (this.mode === "list") this.handleListInput(data);
|
|
219
|
-
else this.handleLogInput(data);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Wheel scrolling moves the selection, or scrolls the log pane in log mode. */
|
|
223
|
-
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
224
|
-
const delta = wheelDelta(event);
|
|
225
|
-
if (delta === undefined) return undefined;
|
|
226
|
-
if (this.confirm) return { handled: true };
|
|
227
|
-
if (this.mode === "list") {
|
|
228
|
-
this.select(this.cursor.indexOf(this.ordered, jobId) + delta);
|
|
229
|
-
} else {
|
|
230
|
-
this.pane.setScroll(this.pane.scroll + delta);
|
|
231
|
-
this.requestRender();
|
|
232
|
-
}
|
|
233
|
-
return { handled: true };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private handleListInput(data: string): void {
|
|
237
|
-
if (isCloseKey(data) || data === "q") {
|
|
238
|
-
this.onClose();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
// Any other key dismisses a prior notice, so it does not linger.
|
|
242
|
-
if (this.notice !== undefined) {
|
|
243
|
-
this.notice = undefined;
|
|
244
|
-
this.requestRender();
|
|
245
|
-
}
|
|
246
|
-
const ordered = this.ordered;
|
|
247
|
-
const index = this.cursor.indexOf(ordered, jobId);
|
|
248
|
-
switch (navIntent(data)) {
|
|
249
|
-
case "up":
|
|
250
|
-
this.select(index - 1);
|
|
251
|
-
return;
|
|
252
|
-
case "down":
|
|
253
|
-
this.select(index + 1);
|
|
254
|
-
return;
|
|
255
|
-
case "pageUp":
|
|
256
|
-
this.select(index - this.cursor.visible);
|
|
257
|
-
return;
|
|
258
|
-
case "pageDown":
|
|
259
|
-
this.select(index + this.cursor.visible);
|
|
260
|
-
return;
|
|
261
|
-
case "home":
|
|
262
|
-
this.select(0);
|
|
263
|
-
return;
|
|
264
|
-
case "end":
|
|
265
|
-
this.select(ordered.length - 1);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (matchesKey(data, Key.enter) || data === "l") this.openLogs();
|
|
269
|
-
else if (data === "d" || data === "K") {
|
|
270
|
-
const job = ordered[index];
|
|
271
|
-
if (!job) return;
|
|
272
|
-
if (this.isForeign(job)) {
|
|
273
|
-
// Another session's job is shown here, not managed from here.
|
|
274
|
-
this.notice = `${job.id} is another session's job; not killed.`;
|
|
275
|
-
} else if (job.status === "running") {
|
|
276
|
-
this.confirm = { kind: "kill", id: job.id, label: shortLabel(job) };
|
|
277
|
-
} else {
|
|
278
|
-
// A finished job has nothing to kill; say so instead of a silent no-op.
|
|
279
|
-
this.notice = `${job.id} already ${job.status}; nothing to kill.`;
|
|
280
|
-
}
|
|
281
|
-
this.requestRender();
|
|
282
|
-
} else if (data === "x") {
|
|
283
|
-
// Only this session's finished jobs are ours to clear, so the prompt and the
|
|
284
|
-
// result agree.
|
|
285
|
-
const finished = ordered.filter((job) => !this.isForeign(job) && job.status !== "running");
|
|
286
|
-
if (finished.length > 0) {
|
|
287
|
-
this.confirm = {
|
|
288
|
-
kind: "clear",
|
|
289
|
-
count: finished.length,
|
|
290
|
-
unreported: finished.filter((job) => !job.seen).length,
|
|
291
|
-
};
|
|
292
|
-
} else {
|
|
293
|
-
this.notice = "No finished jobs to clear.";
|
|
294
|
-
}
|
|
295
|
-
this.requestRender();
|
|
296
|
-
}
|
|
159
|
+
protected rows(): readonly JobRecord[] {
|
|
160
|
+
return [...this.jobs()].sort(compareJobs);
|
|
297
161
|
}
|
|
298
162
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const pending = this.confirm;
|
|
302
|
-
if (!pending) return;
|
|
303
|
-
if (data === "y" || data === "Y" || matchesKey(data, Key.enter)) {
|
|
304
|
-
this.confirm = undefined;
|
|
305
|
-
if (pending.kind === "kill") this.callbacks.kill(pending.id);
|
|
306
|
-
else {
|
|
307
|
-
this.callbacks.clear();
|
|
308
|
-
this.cursor.clamp(this.ordered, jobId);
|
|
309
|
-
}
|
|
310
|
-
this.requestRender();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (data === "n" || data === "N" || isCloseKey(data) || data === "q") {
|
|
314
|
-
this.confirm = undefined;
|
|
315
|
-
this.requestRender();
|
|
316
|
-
}
|
|
163
|
+
protected idOf(job: JobRecord): string {
|
|
164
|
+
return job.id;
|
|
317
165
|
}
|
|
318
166
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const pending = this.confirm;
|
|
322
|
-
if (!pending) return "";
|
|
323
|
-
if (pending.kind === "kill") return `Kill ${pending.id} (${pending.label})? y/N`;
|
|
324
|
-
const note =
|
|
325
|
-
pending.unreported > 0
|
|
326
|
-
? ` ${pending.unreported} unreported result${pending.unreported === 1 ? "" : "s"} will be discarded.`
|
|
327
|
-
: "";
|
|
328
|
-
return `Clear ${pending.count} finished job${pending.count === 1 ? "" : "s"}?${note} y/N`;
|
|
167
|
+
protected rowLine(job: JobRecord, width: number, selected: boolean): string {
|
|
168
|
+
return jobRow(job, this.theme, width, { selected, foreign: this.isForeign(job) });
|
|
329
169
|
}
|
|
330
170
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
this.mode = "list";
|
|
334
|
-
this.pane.close();
|
|
335
|
-
this.requestRender();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (this.pane.handleKey(data)) this.requestRender();
|
|
171
|
+
protected detailBlock(job: JobRecord): string[] {
|
|
172
|
+
return formatJobDetail(job, Date.now(), this.callbacks.sessionId());
|
|
339
173
|
}
|
|
340
174
|
|
|
341
|
-
|
|
175
|
+
protected summaryCells(rows: readonly JobRecord[]): string[] {
|
|
176
|
+
const counts: JobCounts = jobCounts([...rows]);
|
|
342
177
|
const cells = [paint(this.theme, "meta", `${counts.total} job${counts.total === 1 ? "" : "s"}`)];
|
|
343
178
|
if (counts.running > 0) cells.push(paint(this.theme, "meta", `${counts.running} running`));
|
|
344
179
|
if (counts.failed > 0) cells.push(paint(this.theme, "bad", `${counts.failed} failed`));
|
|
180
|
+
const foreign = rows.filter((job) => this.isForeign(job)).length;
|
|
345
181
|
if (foreign > 0) cells.push(paint(this.theme, "faint", `${foreign} peer`));
|
|
346
|
-
|
|
347
|
-
return clip(` ${fitCells(cells, Math.max(1, width - 2), separator)}`, width);
|
|
182
|
+
return cells;
|
|
348
183
|
}
|
|
349
184
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
185
|
+
protected listHints(): ScreenHint[] {
|
|
186
|
+
return [
|
|
187
|
+
"Esc close",
|
|
188
|
+
"↑/↓ or j/k select",
|
|
189
|
+
{ text: "Enter or l logs", color: "accent" },
|
|
190
|
+
{ text: "d or K kill", color: "warning" },
|
|
191
|
+
{ text: "x clear finished", color: "warning" },
|
|
192
|
+
"PgUp/PgDn",
|
|
193
|
+
];
|
|
353
194
|
}
|
|
354
195
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const index = this.cursor.indexOf(jobs, jobId);
|
|
359
|
-
const focused = jobs[index];
|
|
360
|
-
const detail = focused ? formatJobDetail(focused, Date.now(), this.callbacks.sessionId()) : [];
|
|
361
|
-
this.cursor.visible = fitRows(
|
|
362
|
-
this.viewportRowsSource,
|
|
363
|
-
jobs.length,
|
|
364
|
-
LIST_CHROME_ROWS + detail.length,
|
|
365
|
-
SCREEN_DEFAULT_ROWS,
|
|
366
|
-
);
|
|
367
|
-
this.cursor.clamp(jobs, jobId);
|
|
368
|
-
this.cursor.ensureVisible(this.cursor.indexOf(jobs, jobId));
|
|
369
|
-
|
|
370
|
-
const body: string[] = [];
|
|
371
|
-
let range: string | undefined;
|
|
372
|
-
if (jobs.length === 0) {
|
|
373
|
-
body.push(...emptyState(this.theme, w, { label: "No background jobs." }));
|
|
374
|
-
} else {
|
|
375
|
-
const end = Math.min(jobs.length, this.cursor.scroll + this.cursor.visible);
|
|
376
|
-
for (let i = this.cursor.scroll; i < end; i++) {
|
|
377
|
-
const row = jobs[i];
|
|
378
|
-
body.push(jobRow(row, this.theme, w, { selected: i === index, foreign: this.isForeign(row) }));
|
|
379
|
-
}
|
|
380
|
-
if (this.cursor.scroll > 0 || end < jobs.length) {
|
|
381
|
-
range = formatRange(this.theme, w, { start: this.cursor.scroll, end, total: jobs.length });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const hint = this.confirm
|
|
386
|
-
? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
|
|
387
|
-
: this.notice
|
|
388
|
-
? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
|
|
389
|
-
: screenHint(this.theme, w, [
|
|
390
|
-
"Esc close",
|
|
391
|
-
"↑/↓ or j/k select",
|
|
392
|
-
"Enter or l logs",
|
|
393
|
-
"d or K kill",
|
|
394
|
-
"x clear finished",
|
|
395
|
-
"PgUp/PgDn",
|
|
396
|
-
]);
|
|
196
|
+
protected detailHints(): ScreenHint[] {
|
|
197
|
+
return ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"];
|
|
198
|
+
}
|
|
397
199
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
200
|
+
protected detailFor(job: JobRecord): DetailContents {
|
|
201
|
+
const result = this.callbacks.logs(job.id);
|
|
202
|
+
return {
|
|
203
|
+
title: `${job.id} ${shortLabel(job)}`,
|
|
204
|
+
lines: result?.lines ?? [],
|
|
205
|
+
more: result?.more ?? false,
|
|
206
|
+
subtitle: job.logPath ? sanitizeUntrustedLine(job.logPath) : undefined,
|
|
207
|
+
};
|
|
407
208
|
}
|
|
408
209
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
210
|
+
protected detailById(id: string): { lines: string[]; more?: boolean } | undefined {
|
|
211
|
+
return this.callbacks.logs(id);
|
|
212
|
+
}
|
|
412
213
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
214
|
+
protected handleVerb(key: string, job: JobRecord | undefined): void {
|
|
215
|
+
if (key === "d" || key === "K") {
|
|
216
|
+
if (!job) return;
|
|
217
|
+
if (this.isForeign(job)) {
|
|
218
|
+
// Another session's job is shown here, not managed from here.
|
|
219
|
+
this.notify(`${job.id} is another session's job; not killed.`);
|
|
220
|
+
} else if (job.status === "running") {
|
|
221
|
+
this.confirm(`Kill ${job.id} (${shortLabel(job)})? y/N`, () => this.callbacks.kill(job.id));
|
|
222
|
+
} else {
|
|
223
|
+
// A finished job has nothing to kill; say so instead of a silent no-op.
|
|
224
|
+
this.notify(`${job.id} already ${job.status}; nothing to kill.`);
|
|
422
225
|
}
|
|
226
|
+
return;
|
|
423
227
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
228
|
+
if (key !== "x") return;
|
|
229
|
+
// Only this session's finished jobs are ours to clear, so the prompt and the
|
|
230
|
+
// result agree.
|
|
231
|
+
const finished = this.rows().filter((row) => !this.isForeign(row) && row.status !== "running");
|
|
232
|
+
if (finished.length === 0) {
|
|
233
|
+
this.notify("No finished jobs to clear.");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const unreported = finished.filter((row) => !row.seen).length;
|
|
237
|
+
const note =
|
|
238
|
+
unreported > 0 ? ` ${unreported} unreported result${unreported === 1 ? "" : "s"} will be discarded.` : "";
|
|
239
|
+
this.confirm(`Clear ${finished.length} finished job${finished.length === 1 ? "" : "s"}?${note} y/N`, () =>
|
|
240
|
+
this.callbacks.clear(),
|
|
241
|
+
);
|
|
432
242
|
}
|
|
433
243
|
}
|
package/extensions/job/ui.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Presentation for the jobs extension: the footer status chips
|
|
2
|
+
* Presentation for the jobs extension: the footer status chips and the failure
|
|
3
|
+
* alert.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* keys, glyphs, and counts — is job-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 job-specific.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
10
|
import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
12
11
|
import { createStatusController, type StatusChip, type StatusController } from "../../lib/tui/status-controller.ts";
|
|
13
12
|
import type { JobsConfig } from "./config.ts";
|
|
@@ -26,8 +25,6 @@ export interface UiOptions {
|
|
|
26
25
|
|
|
27
26
|
export function createUiController(options: UiOptions): UiController {
|
|
28
27
|
const { getJobs, getConfig } = options;
|
|
29
|
-
// The notification also needs the context, so it is mirrored here.
|
|
30
|
-
let ctx: ExtensionContext | undefined;
|
|
31
28
|
|
|
32
29
|
const controller = createStatusController({
|
|
33
30
|
keys: [STATUS_KEYS.jobs, STATUS_KEYS.jobsFailure],
|
|
@@ -43,32 +40,9 @@ export function createUiController(options: UiOptions): UiController {
|
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
const notifyFailure = (job: JobRecord): void => {
|
|
46
|
-
const target = ctx;
|
|
47
|
-
if (!target) return;
|
|
48
43
|
const exit = job.exitCode !== null ? ` (exit ${job.exitCode})` : "";
|
|
49
|
-
|
|
50
|
-
target.ui.notify(`Job ${job.id} failed: ${shortLabel(job)}${exit}`, "error");
|
|
51
|
-
} catch {
|
|
52
|
-
// UI may be unavailable in non-interactive modes.
|
|
53
|
-
}
|
|
44
|
+
controller.notify(`Job ${job.id} failed: ${shortLabel(job)}${exit}`, "error");
|
|
54
45
|
};
|
|
55
46
|
|
|
56
|
-
return {
|
|
57
|
-
attach: (next) => {
|
|
58
|
-
ctx = next;
|
|
59
|
-
controller.attach(next);
|
|
60
|
-
},
|
|
61
|
-
detach: () => {
|
|
62
|
-
ctx = undefined;
|
|
63
|
-
controller.detach();
|
|
64
|
-
},
|
|
65
|
-
paint: () => {
|
|
66
|
-
controller.paint();
|
|
67
|
-
},
|
|
68
|
-
setStatus: (next) => {
|
|
69
|
-
if (next) ctx = next;
|
|
70
|
-
controller.setStatus(next);
|
|
71
|
-
},
|
|
72
|
-
notifyFailure,
|
|
73
|
-
};
|
|
47
|
+
return { ...controller, notifyFailure };
|
|
74
48
|
}
|
|
@@ -254,7 +254,7 @@ when consolidating, or merge the pair in the picker afterward.
|
|
|
254
254
|
| `doctor.ts` | The health report (pure): counts, file bytes, the injection plan's omissions, and near-duplicate pairs. |
|
|
255
255
|
| `runtime.ts` | Session view of both stores, stamp-based refresh, trust gating, mutations. |
|
|
256
256
|
| `format.ts` | Pure call/result/notice text and the bounded injection body. |
|
|
257
|
-
| `errors.ts` | `messageOf`, the shared error-text extraction for the tool and command. |
|
|
257
|
+
| `errors.ts` | Re-exports `messageOf`, the shared error-text extraction (defined in `lib/errors.ts`) for the tool and command. |
|
|
258
258
|
| `tui.ts` | The `/memory` picker component (note list, inline editor, merge mark, scope move, scope filter), drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
259
259
|
| `tools.ts` | `registerTool("memory", …)`. |
|
|
260
260
|
| `commands.ts` | `registerCommand("memory", …)`. |
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error-text extraction shared by the memory tool and the `/memory` command
|
|
3
|
-
*
|
|
2
|
+
* Error-text extraction shared by the memory tool and the `/memory` command.
|
|
3
|
+
*
|
|
4
|
+
* The single definition lives in `lib/errors.ts`; this module keeps the
|
|
5
|
+
* extension-local import path used by the tool, command, and picker.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
export function messageOf(error: unknown): string {
|
|
8
|
-
return error instanceof Error ? error.message : String(error);
|
|
9
|
-
}
|
|
8
|
+
export { messageOf } from "../../lib/errors.ts";
|