@gavin-hu/my-pi-agent 0.15.1 → 0.16.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 +10 -7
- package/README.zh-CN.md +8 -5
- package/extensions/bytefly/README.md +98 -116
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/format.ts +3 -8
- package/extensions/bytefly/panel/join.ts +1 -6
- package/extensions/bytefly/panel/settings.ts +74 -377
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
- package/extensions/bytefly/panel/tui/frame.ts +2 -2
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui.ts +4 -6
- package/extensions/bytefly/picker/resources.ts +7 -3
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/context/README.md +162 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +82 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +319 -0
- package/extensions/context/tui.ts +268 -0
- package/extensions/context/types.ts +122 -0
- package/extensions/file-browse/README.md +38 -18
- package/extensions/file-browse/config.ts +8 -0
- package/extensions/file-browse/html.ts +2 -2
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +13 -13
- package/extensions/file-browse/live.ts +44 -10
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/server.ts +13 -2
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +72 -56
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +3 -3
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +10 -14
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +7 -26
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +6 -14
- package/extensions/plan/tools/enter.ts +6 -7
- package/extensions/plan/tools/exit.ts +6 -7
- package/extensions/plan/tools/steps.ts +6 -7
- package/extensions/plan/tools/write.ts +8 -9
- package/extensions/plan/web.ts +21 -12
- package/extensions/question/tools.ts +5 -4
- package/extensions/subagent/background.ts +1 -7
- package/extensions/todo/tools.ts +10 -14
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +7 -18
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +4 -7
- package/lib/README.md +9 -6
- package/lib/escape.ts +15 -2
- package/lib/pdf.ts +2 -2
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tui/README.md +3 -3
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/package.json +8 -4
- package/skills/changelog/SKILL.md +136 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transcript rendering for `read_doc
|
|
2
|
+
* Transcript rendering for the `read_doc` tool.
|
|
3
3
|
*
|
|
4
|
-
* Theme-aware formatting only: the tool
|
|
4
|
+
* Theme-aware formatting only: the tool definitions in `tool.ts` wire these to
|
|
5
5
|
* `renderCall` / `renderResult`. The call line carries the path (body colour,
|
|
6
6
|
* `~`-shortened, OSC-8 linked when the terminal supports it); the result line
|
|
7
7
|
* leads with the format and the exact character range, so the path is never
|
|
@@ -17,14 +17,14 @@ import { formatTokens, sanitizeLine, shortenPath, stripControlChars } from "../.
|
|
|
17
17
|
import { expandKey, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
18
18
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
19
19
|
import { rejectionLine, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { summarizeRead } from "./paging.ts";
|
|
21
|
+
import type { ReadArgs, ReadResult } from "./schema.ts";
|
|
22
22
|
|
|
23
23
|
/** Lines of extracted text shown in the expanded preview. */
|
|
24
24
|
export const PREVIEW_LINES = 20;
|
|
25
25
|
|
|
26
26
|
/** Render the argument path: body colour, `~`-shortened, OSC-8 linked when supported. */
|
|
27
|
-
export function
|
|
27
|
+
export function renderReadPath(rawPath: string | undefined, theme: Theme, cwd: string, argsComplete = true): string {
|
|
28
28
|
if (rawPath === undefined && !argsComplete) return theme.fg("toolOutput", "...");
|
|
29
29
|
if (typeof rawPath !== "string") return paint(theme, "bad", "[invalid arg]");
|
|
30
30
|
const value = sanitizeLine(rawPath);
|
|
@@ -47,14 +47,14 @@ export function previewText(text: string, maxLines: number): { body: string; mor
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** A result shape broad enough for both a test double and the host. */
|
|
50
|
-
export interface
|
|
50
|
+
export interface ReadRenderResult {
|
|
51
51
|
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
52
52
|
details?: unknown;
|
|
53
53
|
isError?: boolean;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Options the host passes to `renderResult`. */
|
|
57
|
-
export interface
|
|
57
|
+
export interface ReadRenderOptions {
|
|
58
58
|
expanded: boolean;
|
|
59
59
|
isPartial: boolean;
|
|
60
60
|
/** Pre-painted elapsed suffix shown while reading. */
|
|
@@ -62,10 +62,14 @@ export interface DocRenderOptions {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** The call line: `read_doc <path> (from N, max M)`. */
|
|
65
|
-
export function
|
|
65
|
+
export function formatReadCall(
|
|
66
|
+
tool: string,
|
|
67
|
+
args: ReadArgs,
|
|
68
|
+
theme: Theme,
|
|
69
|
+
context: { cwd: string; argsComplete?: boolean },
|
|
70
|
+
): string {
|
|
66
71
|
const { path, startIndex, maxChars } = args;
|
|
67
|
-
let text =
|
|
68
|
-
toolCallTitle(theme, `${TOOL_NAME} `) + renderDocPath(path, theme, context.cwd, context.argsComplete ?? true);
|
|
72
|
+
let text = toolCallTitle(theme, `${tool} `) + renderReadPath(path, theme, context.cwd, context.argsComplete ?? true);
|
|
69
73
|
const options: string[] = [];
|
|
70
74
|
if (typeof startIndex === "number" && startIndex > 0) options.push(`from ${formatTokens(startIndex)}`);
|
|
71
75
|
if (typeof maxChars === "number") options.push(`max ${formatTokens(maxChars)}`);
|
|
@@ -77,17 +81,17 @@ export function formatDocCall(args: DocArgs, theme: Theme, context: { cwd: strin
|
|
|
77
81
|
* The result: a format + range summary, an expand hint when collapsed, and, when
|
|
78
82
|
* expanded, a sanitized preview of the extracted text.
|
|
79
83
|
*/
|
|
80
|
-
export function
|
|
84
|
+
export function formatReadResult(result: ReadRenderResult, options: ReadRenderOptions, theme: Theme): string {
|
|
81
85
|
if (options.isPartial) return paint(theme, "warn", "Reading...") + (options.progress ?? "");
|
|
82
86
|
|
|
83
87
|
const isError = result.isError ?? false;
|
|
84
|
-
const details = result.details as
|
|
88
|
+
const details = result.details as ReadResult | undefined;
|
|
85
89
|
|
|
86
90
|
if (!details || isError) {
|
|
87
91
|
return rejectionLine(theme, { content: result.content, isError }, options.expanded);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
const summary =
|
|
94
|
+
const summary = summarizeRead(details);
|
|
91
95
|
const sep = paint(theme, "faint", SEPARATORS.item);
|
|
92
96
|
let text = paint(theme, "meta", summary.format) + sep + theme.fg("toolOutput", summary.detail);
|
|
93
97
|
if (summary.note) text += paint(theme, summary.truncated ? "accent" : "faint", SEPARATORS.item + summary.note);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parameter schema, output schema, and result
|
|
2
|
+
* Parameter schema, output schema, and result types for the `read_doc` tool.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type, type Static } from "typebox";
|
|
@@ -12,16 +12,16 @@ export const MIN_CHARS = 200;
|
|
|
12
12
|
export const MAX_CHARS = 100_000;
|
|
13
13
|
export const MAX_PATH_LENGTH = 4096;
|
|
14
14
|
|
|
15
|
-
export const
|
|
15
|
+
export const ReadParams = Type.Object({
|
|
16
16
|
path: Type.String({
|
|
17
17
|
minLength: 1,
|
|
18
18
|
maxLength: MAX_PATH_LENGTH,
|
|
19
|
-
description: "Path to a supported
|
|
19
|
+
description: "Path to a supported file, relative to the working directory or absolute inside it.",
|
|
20
20
|
}),
|
|
21
21
|
startIndex: Type.Optional(
|
|
22
22
|
Type.Integer({
|
|
23
23
|
minimum: 0,
|
|
24
|
-
description: "Code-point offset to start from. Use this to read a truncated
|
|
24
|
+
description: "Code-point offset to start from. Use this to read a truncated file in chunks.",
|
|
25
25
|
}),
|
|
26
26
|
),
|
|
27
27
|
maxChars: Type.Optional(
|
|
@@ -33,9 +33,9 @@ export const DocParams = Type.Object({
|
|
|
33
33
|
),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
export type
|
|
36
|
+
export type ReadArgs = Static<typeof ReadParams>;
|
|
37
37
|
|
|
38
|
-
export const
|
|
38
|
+
export const ReadOutput = Type.Object({
|
|
39
39
|
path: Type.String(),
|
|
40
40
|
format: Type.String(),
|
|
41
41
|
bytes: Type.Number(),
|
|
@@ -47,4 +47,4 @@ export const DocOutput = Type.Object({
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
/** The runtime result, with `format` narrowed to the registry id. */
|
|
50
|
-
export type
|
|
50
|
+
export type ReadResult = Omit<Static<typeof ReadOutput>, "format"> & { format: FormatId };
|
|
@@ -19,16 +19,14 @@ import { reuseText } from "../../lib/tui/chrome.ts";
|
|
|
19
19
|
import { elapsedSuffix, reuseCallText, startToolProgress, syncElapsedTimer } from "../../lib/tui/tool.ts";
|
|
20
20
|
import { isFormatEnabled, loadConfig } from "./config.ts";
|
|
21
21
|
import { withDeadline } from "./deadline.ts";
|
|
22
|
-
import type {
|
|
22
|
+
import type { FormatExtractor } from "./extract/file.ts";
|
|
23
23
|
import { detectFormat, supportedExtensions, unsupportedHint, type FormatId } from "./formats.ts";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
export { TOOL_NAME } from "./schema.ts";
|
|
24
|
+
import { formatPage } from "./paging.ts";
|
|
25
|
+
import { formatReadCall, formatReadResult } from "./render.ts";
|
|
26
|
+
import { ReadOutput, ReadParams, MAX_CHARS, MIN_CHARS, TOOL_NAME, type ReadArgs, type ReadResult } from "./schema.ts";
|
|
29
27
|
|
|
30
28
|
/** Resolve a request path under `root`, refusing anything that escapes it. */
|
|
31
|
-
function
|
|
29
|
+
function resolveReadPath(root: string, rawPath: string): { display: string; abs: string } {
|
|
32
30
|
// Resolve on the raw path; sanitize only the displayed/error strings, so a
|
|
33
31
|
// pathological control character cannot hide a path or forge a transcript line.
|
|
34
32
|
const trimmed = rawPath.trim();
|
|
@@ -44,7 +42,7 @@ function resolveDocument(root: string, rawPath: string): { display: string; abs:
|
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
/** Stat the file and raise a model-readable error for a missing/unreadable target. */
|
|
47
|
-
async function statFile(abs: string, display: string, maxFileBytes: number): Promise<number> {
|
|
45
|
+
async function statFile(abs: string, display: string, maxFileBytes: number, tool: string): Promise<number> {
|
|
48
46
|
let info: Awaited<ReturnType<typeof stat>>;
|
|
49
47
|
try {
|
|
50
48
|
info = await stat(abs);
|
|
@@ -56,54 +54,57 @@ async function statFile(abs: string, display: string, maxFileBytes: number): Pro
|
|
|
56
54
|
}
|
|
57
55
|
if (!info.isFile()) throw new Error(`Not a file: ${display}`);
|
|
58
56
|
if (info.size > maxFileBytes) {
|
|
59
|
-
throw new Error(`File is ${info.size} bytes, larger than the ${maxFileBytes}-byte limit for
|
|
57
|
+
throw new Error(`File is ${info.size} bytes, larger than the ${maxFileBytes}-byte limit for ${tool}.`);
|
|
60
58
|
}
|
|
61
59
|
return info.size;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
/** Per-format extractor overrides, for tests; the format table supplies the rest. */
|
|
65
|
-
export interface
|
|
66
|
-
extractors?: Partial<Record<FormatId,
|
|
67
|
-
/** Test seam: replace the configured `
|
|
63
|
+
export interface ReadToolDeps {
|
|
64
|
+
extractors?: Partial<Record<FormatId, FormatExtractor>>;
|
|
65
|
+
/** Test seam: replace the configured `file-read.extractTimeoutMs`. */
|
|
68
66
|
timeoutMs?: number;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
export function
|
|
69
|
+
export function registerReadTool(pi: ExtensionAPI, deps: ReadToolDeps = {}): void {
|
|
72
70
|
pi.registerTool({
|
|
73
71
|
name: TOOL_NAME,
|
|
74
72
|
label: "Read document",
|
|
75
73
|
description:
|
|
76
74
|
"Read a local PDF, DOCX, DOC, ODT, RTF, or XLSX file and return its extracted text, pageable with " +
|
|
77
75
|
"startIndex/maxChars. Extraction runs in process, so no external tools are needed; a read that outlasts " +
|
|
78
|
-
"
|
|
76
|
+
"file-read.extractTimeoutMs fails with the limit named.",
|
|
79
77
|
promptSnippet: "Read a PDF, DOCX, DOC, ODT, RTF, or XLSX as text, paged with startIndex/maxChars.",
|
|
80
78
|
promptGuidelines: [
|
|
81
|
-
"Use read_doc for .pdf
|
|
82
|
-
"
|
|
79
|
+
"Use read_doc for .pdf, .docx, .doc, .odt, .rtf, and .xlsx; the built-in read tool is for text and images.",
|
|
80
|
+
"Only text survives: tables, images, and formatting are dropped, and an .xlsx read returns one [Sheet] heading per worksheet followed by tab-separated rows of the workbook's displayed values.",
|
|
81
|
+
"A scanned or image-only PDF has no text layer, so it returns little or nothing; there is no OCR.",
|
|
83
82
|
],
|
|
84
|
-
parameters:
|
|
85
|
-
outputSchema:
|
|
83
|
+
parameters: ReadParams,
|
|
84
|
+
outputSchema: ReadOutput,
|
|
86
85
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
87
86
|
|
|
88
87
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
89
|
-
const args = params as
|
|
88
|
+
const args = params as ReadArgs;
|
|
90
89
|
const config = loadConfig(ctx.cwd);
|
|
91
90
|
const root = resolveEffectiveCwd(ctx.cwd);
|
|
92
|
-
const { display, abs } =
|
|
91
|
+
const { display, abs } = resolveReadPath(root, args.path);
|
|
93
92
|
|
|
94
93
|
const format = detectFormat(display);
|
|
95
94
|
if (!format) {
|
|
96
95
|
const hint = unsupportedHint(display);
|
|
97
96
|
if (hint) throw new Error(hint);
|
|
98
|
-
throw new Error(
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Unsupported file format for ${TOOL_NAME}. Supported extensions: ${supportedExtensions().join(", ")}.`,
|
|
99
|
+
);
|
|
99
100
|
}
|
|
100
101
|
if (!isFormatEnabled(config, format.id)) {
|
|
101
102
|
throw new Error(
|
|
102
|
-
`${format.label} extraction is disabled in the bytefly.json
|
|
103
|
+
`${format.label} extraction is disabled in the bytefly.json file-read section (file-read.formats.${format.id} = false).`,
|
|
103
104
|
);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
const size = await statFile(abs, display, config.maxFileBytes);
|
|
107
|
+
const size = await statFile(abs, display, config.maxFileBytes, TOOL_NAME);
|
|
107
108
|
const extractor = deps.extractors?.[format.id] ?? format.extract;
|
|
108
109
|
const timeoutMs = deps.timeoutMs ?? config.extractTimeoutMs;
|
|
109
110
|
startToolProgress(onUpdate);
|
|
@@ -111,7 +112,7 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
111
112
|
timeoutMs,
|
|
112
113
|
signal,
|
|
113
114
|
timeoutMessage:
|
|
114
|
-
`Extraction of ${display} did not finish within ${timeoutMs} ms (
|
|
115
|
+
`Extraction of ${display} did not finish within ${timeoutMs} ms (file-read.extractTimeoutMs). ` +
|
|
115
116
|
`The ${format.label} parser was abandoned; raise the limit or read a smaller file.`,
|
|
116
117
|
});
|
|
117
118
|
|
|
@@ -119,7 +120,8 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
119
120
|
const requested = typeof args.maxChars === "number" ? args.maxChars : config.maxChars;
|
|
120
121
|
const maxChars = Math.min(config.maxChars, MAX_CHARS, Math.max(MIN_CHARS, Math.round(requested)));
|
|
121
122
|
|
|
122
|
-
const formatted =
|
|
123
|
+
const formatted = formatPage({
|
|
124
|
+
tool: TOOL_NAME,
|
|
123
125
|
path: display,
|
|
124
126
|
format: format.id,
|
|
125
127
|
bytes: size,
|
|
@@ -128,7 +130,7 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
128
130
|
maxChars,
|
|
129
131
|
});
|
|
130
132
|
|
|
131
|
-
const result:
|
|
133
|
+
const result: ReadResult = {
|
|
132
134
|
path: display,
|
|
133
135
|
format: format.id,
|
|
134
136
|
bytes: size,
|
|
@@ -149,7 +151,9 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
149
151
|
renderCall(args, theme, context) {
|
|
150
152
|
const cwd = context?.cwd ?? process.cwd();
|
|
151
153
|
const text = reuseCallText(context?.lastComponent, "", "middle");
|
|
152
|
-
text.setText(
|
|
154
|
+
text.setText(
|
|
155
|
+
formatReadCall(TOOL_NAME, args as ReadArgs, theme, { cwd, argsComplete: context?.argsComplete ?? true }),
|
|
156
|
+
);
|
|
153
157
|
return text;
|
|
154
158
|
},
|
|
155
159
|
|
|
@@ -159,7 +163,11 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
159
163
|
const isError = context?.isError ?? result.isError ?? false;
|
|
160
164
|
const text = reuseText(context?.lastComponent);
|
|
161
165
|
text.setText(
|
|
162
|
-
|
|
166
|
+
formatReadResult(
|
|
167
|
+
{ content: result.content, details: result.details, isError },
|
|
168
|
+
{ ...options, progress },
|
|
169
|
+
theme,
|
|
170
|
+
),
|
|
163
171
|
);
|
|
164
172
|
return text;
|
|
165
173
|
},
|
package/extensions/goal/tools.ts
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
12
|
import { GOAL_TOOL } from "../../lib/tool-names.ts";
|
|
13
|
+
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
14
14
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
-
import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
|
|
16
15
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
17
16
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
18
17
|
import { formatCallText, formatGoalText } from "./format.ts";
|
|
@@ -118,25 +117,22 @@ export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
|
|
118
117
|
},
|
|
119
118
|
|
|
120
119
|
renderCall(args, theme, context) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
return callLine(
|
|
121
|
+
theme,
|
|
122
|
+
context.lastComponent,
|
|
123
|
+
TOOL_NAME,
|
|
124
|
+
formatCallText(args.objective, context.argsComplete, args.status),
|
|
125
125
|
);
|
|
126
|
-
return text;
|
|
127
126
|
},
|
|
128
127
|
|
|
129
128
|
renderResult(result, options, theme, context) {
|
|
130
129
|
const text = reuseText(context.lastComponent);
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
text.setText(
|
|
134
|
-
return text;
|
|
135
|
-
}
|
|
136
|
-
if (details.error) {
|
|
137
|
-
text.setText(paint(theme, "bad", `Error: ${sanitizeLine(details.error)}`));
|
|
130
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<GoalDetails>(result.details));
|
|
131
|
+
if ("line" in outcome) {
|
|
132
|
+
text.setText(outcome.line);
|
|
138
133
|
return text;
|
|
139
134
|
}
|
|
135
|
+
const details = outcome.details;
|
|
140
136
|
if (!details.goal) {
|
|
141
137
|
// A neutral marker, deliberately not the achieved check.
|
|
142
138
|
text.setText(paint(theme, "faint", `${GLYPHS.stopped} `) + paint(theme, "meta", "Cleared the goal"));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
9
|
+
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
10
10
|
import { formatJobList } from "./format.ts";
|
|
11
11
|
import type { JobsRuntime } from "./runtime.ts";
|
|
12
12
|
import { JobListComponent } from "./tui.ts";
|
|
@@ -23,53 +23,39 @@ export function registerCommands(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
theme,
|
|
39
|
-
{
|
|
40
|
-
sessionId: () => runtime.sessionId,
|
|
41
|
-
logs: (id) => runtime.logs(id, 200),
|
|
42
|
-
kill: (id) => {
|
|
43
|
-
runtime.kill(id);
|
|
44
|
-
},
|
|
45
|
-
clear: () => {
|
|
46
|
-
const result = runtime.clear(undefined, false);
|
|
47
|
-
if (result.cleared > 0) {
|
|
48
|
-
ctx.ui.notify(`Cleared ${result.cleared} job${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
49
|
-
}
|
|
50
|
-
},
|
|
26
|
+
await withPolledScreen(
|
|
27
|
+
pi,
|
|
28
|
+
ctx,
|
|
29
|
+
(screen) =>
|
|
30
|
+
new JobListComponent(
|
|
31
|
+
() => runtime.list(),
|
|
32
|
+
screen.theme,
|
|
33
|
+
{
|
|
34
|
+
sessionId: () => runtime.sessionId,
|
|
35
|
+
logs: (id) => runtime.logs(id, 200),
|
|
36
|
+
kill: (id) => {
|
|
37
|
+
runtime.kill(id);
|
|
51
38
|
},
|
|
52
|
-
() => {
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
clear: () => {
|
|
40
|
+
const result = runtime.clear(undefined, false);
|
|
41
|
+
if (result.cleared > 0) {
|
|
42
|
+
ctx.ui.notify(`Cleared ${result.cleared} job${result.cleared === 1 ? "" : "s"}.`, "info");
|
|
43
|
+
}
|
|
55
44
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} finally {
|
|
71
|
-
stopTimer();
|
|
72
|
-
}
|
|
45
|
+
},
|
|
46
|
+
screen.close,
|
|
47
|
+
screen.requestRender,
|
|
48
|
+
screen.viewportRows,
|
|
49
|
+
),
|
|
50
|
+
{
|
|
51
|
+
pollMs: LOG_POLL_MS,
|
|
52
|
+
// Only the open log pane is polled; the runtime's own clock repaints the list.
|
|
53
|
+
poll: (component) => {
|
|
54
|
+
const id = component.currentLogId();
|
|
55
|
+
if (id) component.refreshLogs(id);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
);
|
|
73
59
|
},
|
|
74
60
|
});
|
|
75
61
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { loadConfig, type JobsConfig } from "./config.ts";
|
|
35
35
|
import { createLogWriter, type LogHandle } from "./log-writer.ts";
|
|
36
36
|
import { sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
37
|
+
import { defaultInterval } from "../../lib/timer.ts";
|
|
37
38
|
import { clipLabel } from "../../lib/tui/columns.ts";
|
|
38
39
|
import { compareJobs, formatLogs } from "./format.ts";
|
|
39
40
|
import { readLogTail } from "./logs.ts";
|
|
@@ -193,7 +194,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
193
194
|
let config: JobsConfig = options.config ?? loadConfig(process.cwd());
|
|
194
195
|
let sessionId = "session";
|
|
195
196
|
let disposed = false;
|
|
196
|
-
let clock:
|
|
197
|
+
let clock: (() => void) | undefined;
|
|
197
198
|
|
|
198
199
|
/** Jobs this session owns (a dead session's adopted records included). */
|
|
199
200
|
const ownJobs = (): Job[] => [...jobs.values()].filter((job) => job.sessionId === sessionId);
|
|
@@ -247,10 +248,8 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
247
248
|
isSessionAlive(readSessionMarker(store.directory(), owner), now(), config.sessionTtlMs, liveness);
|
|
248
249
|
|
|
249
250
|
const stopClock = (): void => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
clock = undefined;
|
|
253
|
-
}
|
|
251
|
+
clock?.();
|
|
252
|
+
clock = undefined;
|
|
254
253
|
};
|
|
255
254
|
|
|
256
255
|
const ensureClock = (): void => {
|
|
@@ -260,7 +259,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
260
259
|
return;
|
|
261
260
|
}
|
|
262
261
|
if (clock) return;
|
|
263
|
-
clock =
|
|
262
|
+
clock = defaultInterval(config.repaintMs, () => {
|
|
264
263
|
touchMarker();
|
|
265
264
|
pollExternal();
|
|
266
265
|
// Re-assert the chip every tick. It is otherwise only set on a job
|
|
@@ -268,8 +267,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
268
267
|
// another UI path) would hide a still-running job's chip until the next
|
|
269
268
|
// transition. The clock is the only periodic hook while jobs run.
|
|
270
269
|
ui.setStatus();
|
|
271
|
-
}
|
|
272
|
-
clock.unref?.();
|
|
270
|
+
});
|
|
273
271
|
};
|
|
274
272
|
|
|
275
273
|
const paint = (): void => {
|
package/extensions/job/tools.ts
CHANGED
|
@@ -9,17 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { sanitizeLine } from "../../lib/format.ts";
|
|
13
12
|
import { errorResult } from "../../lib/tool-result.ts";
|
|
14
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
|
-
import {
|
|
16
|
-
elapsedSuffix,
|
|
17
|
-
errorLine,
|
|
18
|
-
rejectionLine,
|
|
19
|
-
reuseCallText,
|
|
20
|
-
syncElapsedTimer,
|
|
21
|
-
toolCallTitle,
|
|
22
|
-
} from "../../lib/tui/tool.ts";
|
|
14
|
+
import { callLine, detailsOrLine, elapsedSuffix, rejectionLine, syncElapsedTimer } from "../../lib/tui/tool.ts";
|
|
23
15
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
24
16
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
25
17
|
import {
|
|
@@ -229,30 +221,19 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
229
221
|
|
|
230
222
|
renderCall(args, theme, context) {
|
|
231
223
|
const action = typeof args.action === "string" ? args.action : "job";
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
toolCallTitle(theme, `${TOOL_NAME} `) +
|
|
235
|
-
paint(
|
|
236
|
-
theme,
|
|
237
|
-
"meta",
|
|
238
|
-
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
239
|
-
),
|
|
240
|
-
);
|
|
241
|
-
return text;
|
|
224
|
+
const subject = formatCallText(action, args as Record<string, unknown>, context.argsComplete);
|
|
225
|
+
return callLine(theme, context.lastComponent, TOOL_NAME, subject, "middle");
|
|
242
226
|
},
|
|
243
227
|
|
|
244
228
|
renderResult(result, options, theme, context) {
|
|
245
229
|
const text = reuseText(context.lastComponent);
|
|
246
230
|
const startedAt = syncElapsedTimer(options.isPartial, context);
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
text.setText(
|
|
250
|
-
return text;
|
|
251
|
-
}
|
|
252
|
-
if (details.error) {
|
|
253
|
-
text.setText(errorLine(theme, details.error));
|
|
231
|
+
const outcome = detailsOrLine(theme, result, options.expanded, toolDetails<JobDetails>(result.details));
|
|
232
|
+
if ("line" in outcome) {
|
|
233
|
+
text.setText(outcome.line);
|
|
254
234
|
return text;
|
|
255
235
|
}
|
|
236
|
+
const details = outcome.details;
|
|
256
237
|
|
|
257
238
|
if (options.isPartial) {
|
|
258
239
|
const id = details.job?.id;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
14
14
|
import { messageOf } from "./errors.ts";
|
|
15
15
|
import { formatHealth, memoryHealth } from "./doctor.ts";
|
|
16
16
|
import { formatNotice, scopeLabel } from "./format.ts";
|
|
@@ -55,49 +55,35 @@ async function openPicker(pi: ExtensionAPI, runtime: MemoryRuntime, ctx: Extensi
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Pick up an external edit while the screen is open, repainting only on a change.
|
|
90
|
-
timer = setInterval(() => {
|
|
91
|
-
if (runtime.refresh().changed.length > 0) tui.requestRender();
|
|
92
|
-
}, PICKER_POLL_MS);
|
|
93
|
-
timer.unref?.();
|
|
94
|
-
|
|
95
|
-
return component;
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
} finally {
|
|
99
|
-
stopTimer();
|
|
100
|
-
}
|
|
58
|
+
await withPolledScreen(
|
|
59
|
+
pi,
|
|
60
|
+
ctx,
|
|
61
|
+
(screen) =>
|
|
62
|
+
new MemoryPickerComponent(
|
|
63
|
+
screen.tui,
|
|
64
|
+
screen.theme,
|
|
65
|
+
{
|
|
66
|
+
entries: () => runtime.entries(),
|
|
67
|
+
trusted: () => runtime.trusted(),
|
|
68
|
+
refresh: () => runtime.refresh().changed.length > 0,
|
|
69
|
+
add: (scope, text) => runtime.add(scope, text),
|
|
70
|
+
update: (scope, from, to) => runtime.update(scope, from, to),
|
|
71
|
+
forget: (scope, entry) => runtime.forget(scope, entry),
|
|
72
|
+
merge: (scope, first, second, merged) => runtime.merge(scope, first, second, merged),
|
|
73
|
+
move: (from, to, text) => runtime.move(from, to, text),
|
|
74
|
+
},
|
|
75
|
+
screen.close,
|
|
76
|
+
screen.requestRender,
|
|
77
|
+
screen.viewportRows,
|
|
78
|
+
),
|
|
79
|
+
{
|
|
80
|
+
pollMs: PICKER_POLL_MS,
|
|
81
|
+
// Pick up an external edit while the screen is open, repainting only on a change.
|
|
82
|
+
poll: (_component, handles) => {
|
|
83
|
+
if (runtime.refresh().changed.length > 0) handles.requestRender();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
101
87
|
}
|
|
102
88
|
|
|
103
89
|
export function registerCommands(pi: ExtensionAPI, runtime: MemoryRuntime): void {
|