@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- 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-cancelled.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-long-subject.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-running.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-rejected.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
|
@@ -44,7 +44,7 @@ them along. Then ask the model to read a document, or call the tool directly:
|
|
|
44
44
|
|
|
45
45
|
## Tool
|
|
46
46
|
|
|
47
|
-
`read_doc` —
|
|
47
|
+
`read_doc` — direct exposure, active by default, read-only, closed-world
|
|
48
48
|
(no network).
|
|
49
49
|
|
|
50
50
|
| Parameter | Type | Meaning |
|
|
@@ -124,11 +124,12 @@ merged over the defaults and clamped:
|
|
|
124
124
|
|---|---|---|---|
|
|
125
125
|
| `maxFileBytes` | `20971520` (20 MiB) | 1 KiB – 512 MiB | Reject larger files. |
|
|
126
126
|
| `maxChars` | `40000` | 200 – 100000 | Default slice size. |
|
|
127
|
+
| `extractTimeoutMs` | `30000` (30 s) | 1 s – 5 min | Per-call extraction deadline; a parser that outlasts it is abandoned. |
|
|
127
128
|
| `formats` | `{ "pdf": true, "docx": true, "doc": true, "odt": true, "rtf": true, "xlsx": true }` | — | Per-format on/off, merged over each registry entry's default; unknown keys are ignored. |
|
|
128
129
|
|
|
129
130
|
```jsonc
|
|
130
131
|
// bytefly.json → "document"
|
|
131
|
-
{ "maxChars": 80000, "formats": { "docx": false } }
|
|
132
|
+
{ "maxChars": 80000, "extractTimeoutMs": 60000, "formats": { "docx": false } }
|
|
132
133
|
```
|
|
133
134
|
|
|
134
135
|
`doc.json` is not read, and neither is the old per-extension `document.json`;
|
|
@@ -142,8 +143,9 @@ run `bun run config:migrate` to move a `document.json` into the shared file.
|
|
|
142
143
|
`resolveEffectiveCwd`; absolute paths are allowed only inside it. Containment
|
|
143
144
|
is symlink-aware (`isInsideReal`), and the real, resolved path is what the
|
|
144
145
|
parser receives.
|
|
145
|
-
- **Size-capped.** Files larger than `maxFileBytes` are rejected
|
|
146
|
-
parser runs
|
|
146
|
+
- **Size-capped and time-bounded.** Files larger than `maxFileBytes` are rejected
|
|
147
|
+
before any parser runs, and a parser that outlasts `extractTimeoutMs` is
|
|
148
|
+
abandoned so the turn is not held open by one document.
|
|
147
149
|
- **No subprocesses.** Extraction never spawns a process, so it grants no
|
|
148
150
|
privileges beyond the current process.
|
|
149
151
|
- **Untrusted text.** Extracted document text is treated as untrusted: it is
|
|
@@ -171,6 +173,10 @@ Each extractor builds a plain-text document from the format's bytes:
|
|
|
171
173
|
The tool is pageable across the whole extracted text, so a word-processing
|
|
172
174
|
document or workbook is read in the same `startIndex` / `maxChars` way as a PDF.
|
|
173
175
|
|
|
176
|
+
Each call is bounded by `extractTimeoutMs` (30 s by default). A parser that
|
|
177
|
+
outlasts the deadline is abandoned — the read fails with the limit named, and a
|
|
178
|
+
late result is discarded — so a pathological document cannot park the turn.
|
|
179
|
+
|
|
174
180
|
## Supported formats
|
|
175
181
|
|
|
176
182
|
| Format | Extensions | Extractor | Enabled by default |
|
|
@@ -203,13 +209,17 @@ config defaults, and error listing all derive from it.
|
|
|
203
209
|
|
|
204
210
|
- Parsing is best-effort and format-specific: output fidelity varies by library,
|
|
205
211
|
and a malformed file fails with that library's message.
|
|
206
|
-
- Legacy `.doc` relies on `word-extractor
|
|
207
|
-
|
|
212
|
+
- Legacy `.doc` relies on `word-extractor`, which now has a committed OLE fixture
|
|
213
|
+
(`test/helpers/fixtures/files/legacy.doc`, two paragraphs generated once by a
|
|
214
|
+
real word processor). Real Word 6/95 documents vary far more than one fixture
|
|
215
|
+
can show, so the format stays best-effort.
|
|
208
216
|
- No OCR: scanned or image-only PDFs return little or no text.
|
|
209
217
|
- Formatting, tables, and images are not preserved (XLSX keeps the row/column
|
|
210
218
|
shape but drops cell styling and formulas; DOCX/ODT keep table cells as tabs).
|
|
211
|
-
-
|
|
212
|
-
|
|
219
|
+
- The deadline stops waiting and discards the result; it cannot preempt a parser
|
|
220
|
+
that never yields to the event loop, because JavaScript cannot interrupt a
|
|
221
|
+
synchronous parse. A worker thread could; the package does not use one.
|
|
222
|
+
- `maxChars` limits the returned slice, not peak extraction memory.
|
|
213
223
|
- Only one file per call; legacy `.xls` gets a conversion hint.
|
|
214
224
|
|
|
215
225
|
## Non-goals
|
|
@@ -226,7 +236,7 @@ config defaults, and error listing all derive from it.
|
|
|
226
236
|
| Aspect | Contract |
|
|
227
237
|
|---|---|
|
|
228
238
|
| Registration | `document(pi)` in `index.ts` calls `registerDocTool(pi)`, which registers `read_doc`. The factory only registers; no parser loads until a read needs it. |
|
|
229
|
-
| Tool | `read_doc`,
|
|
239
|
+
| Tool | `read_doc`, direct exposure and active on registration (the defaults), default (`parallel`) execution, `annotations: { readOnlyHint: true, openWorldHint: false }`, TypeBox `parameters` and `outputSchema`, and `renderCall`/`renderResult` (reuse `context.lastComponent`; read `context.cwd` and `context.isError`). |
|
|
230
240
|
| State | None persisted. Per-result state lives in the tool-result `details` / `structuredContent`; the renderer reads only that. |
|
|
231
241
|
| Lifecycle | No `session_start` / `session_shutdown` hooks: each read imports a parser, runs it, and holds no long-lived resources. |
|
|
232
242
|
| Host seams | `resolveEffectiveCwd` (`lib/env.ts`), `isInsideReal` / `realPathOfNearest` (`lib/path.ts`), `loadConfigSection` (`lib/config.ts`), `stripControlChars` / `sanitize` / `formatTokens` (`lib/format.ts`), `SEPARATORS` (`lib/tui/glyphs.ts`). |
|
|
@@ -285,8 +295,8 @@ bun test extensions/document
|
|
|
285
295
|
```
|
|
286
296
|
|
|
287
297
|
Extractor tests run against real minimal files (`test/helpers/fixtures/documents.ts`
|
|
288
|
-
builds a genuine PDF, DOCX, and ODT; XLSX is written with `exceljs
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`test/helpers/fixtures/document.ts`
|
|
292
|
-
symlink.
|
|
298
|
+
builds a genuine PDF, DOCX, and ODT; XLSX is written with `exceljs`; legacy `.doc`
|
|
299
|
+
reads the committed `files/legacy.doc`), so the libraries are exercised, not
|
|
300
|
+
stubbed. `tool.test.ts` injects fake extractors for behavioural cases and reads a
|
|
301
|
+
real PDF and a real `.doc` end to end; `test/helpers/fixtures/document.ts`
|
|
302
|
+
provides the scratch root and an escape symlink.
|
|
@@ -15,6 +15,8 @@ export interface DocConfig {
|
|
|
15
15
|
maxFileBytes: number;
|
|
16
16
|
/** Default character budget for the returned slice. */
|
|
17
17
|
maxChars: number;
|
|
18
|
+
/** Per-call extraction deadline; a parser that outlasts it is abandoned. */
|
|
19
|
+
extractTimeoutMs: number;
|
|
18
20
|
/** Per-format on/off, merged over each registry entry's `defaultEnabled`. */
|
|
19
21
|
formats: Record<FormatId, boolean>;
|
|
20
22
|
}
|
|
@@ -22,6 +24,7 @@ export interface DocConfig {
|
|
|
22
24
|
export const DEFAULT_CONFIG: DocConfig = {
|
|
23
25
|
maxFileBytes: 20 * 1024 * 1024,
|
|
24
26
|
maxChars: 40_000,
|
|
27
|
+
extractTimeoutMs: 30_000,
|
|
25
28
|
formats: Object.fromEntries(FORMATS.map((format) => [format.id, format.defaultEnabled])) as Record<FormatId, boolean>,
|
|
26
29
|
};
|
|
27
30
|
|
|
@@ -43,6 +46,7 @@ export function normalizeConfig(raw: Record<string, unknown> | undefined, base:
|
|
|
43
46
|
return {
|
|
44
47
|
maxFileBytes: clampInteger(raw.maxFileBytes, base.maxFileBytes, 1_024, 512 * 1024 * 1024),
|
|
45
48
|
maxChars: clampInteger(raw.maxChars, base.maxChars, 200, 100_000),
|
|
49
|
+
extractTimeoutMs: clampInteger(raw.extractTimeoutMs, base.extractTimeoutMs, 1_000, 300_000),
|
|
46
50
|
formats: normalizeFormats(raw.formats, base.formats),
|
|
47
51
|
};
|
|
48
52
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-call deadline for one in-process extraction.
|
|
3
|
+
*
|
|
4
|
+
* A parser is a library call, not a subprocess: JavaScript cannot preempt a
|
|
5
|
+
* synchronous parse, so the deadline stops *waiting* for the work and discards
|
|
6
|
+
* its result rather than killing it. What it guarantees is the turn: a
|
|
7
|
+
* pathological file fails with the limit named instead of parking the session,
|
|
8
|
+
* and a late settle from the abandoned promise never surfaces.
|
|
9
|
+
*
|
|
10
|
+
* The timer is a plain `setTimeout` cleared in `finally` (unlike
|
|
11
|
+
* `AbortSignal.timeout`, whose timer cannot be cancelled and would stay pending
|
|
12
|
+
* after every fast read). The deadline is deliberately not passed into `work`:
|
|
13
|
+
* the caller's abort signal is the only abort an extractor sees, so a timeout
|
|
14
|
+
* can never be reported as a turn abort or the reverse.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** How the deadline reports, and when to give up. */
|
|
18
|
+
export interface DeadlineOptions {
|
|
19
|
+
/** Milliseconds to wait before abandoning the work. */
|
|
20
|
+
timeoutMs: number;
|
|
21
|
+
/** The error message a timeout throws; the caller names its own limit. */
|
|
22
|
+
timeoutMessage: string;
|
|
23
|
+
/** The turn's abort signal; an abort wins over the deadline. */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run `work` with a deadline. Resolves with its value, rethrows its error
|
|
29
|
+
* unchanged, or rejects with `timeoutMessage` (or the caller's abort reason).
|
|
30
|
+
*/
|
|
31
|
+
export async function withDeadline<T>(work: () => Promise<T>, options: DeadlineOptions): Promise<T> {
|
|
32
|
+
const { timeoutMs, timeoutMessage, signal } = options;
|
|
33
|
+
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
const expiry = new Promise<never>((_resolve, reject) => {
|
|
36
|
+
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
37
|
+
// A pending timer must not keep a short-lived process alive.
|
|
38
|
+
(timer as { unref?: () => void }).unref?.();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let onAbort: (() => void) | undefined;
|
|
42
|
+
const aborted = signal
|
|
43
|
+
? new Promise<never>((_resolve, reject) => {
|
|
44
|
+
onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
45
|
+
if (signal.aborted) onAbort();
|
|
46
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
47
|
+
})
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
const pending = work();
|
|
51
|
+
// One observed promise: if the deadline or the abort wins, a late resolve or
|
|
52
|
+
// rejection from the abandoned work has nobody left to reach.
|
|
53
|
+
pending.catch(() => {});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race(aborted ? [pending, expiry, aborted] : [pending, expiry]);
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -18,6 +18,7 @@ import { isInsideReal, realPathOfNearest } from "../../lib/path.ts";
|
|
|
18
18
|
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
|
+
import { withDeadline } from "./deadline.ts";
|
|
21
22
|
import type { DocExtractor } from "./extract/file.ts";
|
|
22
23
|
import { detectFormat, supportedExtensions, unsupportedHint, type FormatId } from "./formats.ts";
|
|
23
24
|
import { formatDoc } from "./paging.ts";
|
|
@@ -63,6 +64,8 @@ async function statFile(abs: string, display: string, maxFileBytes: number): Pro
|
|
|
63
64
|
/** Per-format extractor overrides, for tests; the format table supplies the rest. */
|
|
64
65
|
export interface DocToolDeps {
|
|
65
66
|
extractors?: Partial<Record<FormatId, DocExtractor>>;
|
|
67
|
+
/** Test seam: replace the configured `document.extractTimeoutMs`. */
|
|
68
|
+
timeoutMs?: number;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void {
|
|
@@ -71,7 +74,8 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
71
74
|
label: "Read document",
|
|
72
75
|
description:
|
|
73
76
|
"Read a local PDF, DOCX, DOC, ODT, RTF, or XLSX file and return its extracted text, pageable with " +
|
|
74
|
-
"startIndex/maxChars. Extraction runs in process, so no external tools are needed
|
|
77
|
+
"startIndex/maxChars. Extraction runs in process, so no external tools are needed; a read that outlasts " +
|
|
78
|
+
"document.extractTimeoutMs fails with the limit named.",
|
|
75
79
|
promptSnippet: "Read a PDF, DOCX, DOC, ODT, RTF, or XLSX as text, paged with startIndex/maxChars.",
|
|
76
80
|
promptGuidelines: [
|
|
77
81
|
"Use read_doc for .pdf/.docx/.doc/.odt/.rtf/.xlsx; the built-in read tool is for text and images.",
|
|
@@ -79,9 +83,7 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
79
83
|
],
|
|
80
84
|
parameters: DocParams,
|
|
81
85
|
outputSchema: DocOutput,
|
|
82
|
-
|
|
83
|
-
defaultActive: true,
|
|
84
|
-
annotations: { readOnlyHint: true, openWorldHint: false, destructiveHint: false },
|
|
86
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
85
87
|
|
|
86
88
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
87
89
|
const args = params as DocArgs;
|
|
@@ -103,8 +105,15 @@ export function registerDocTool(pi: ExtensionAPI, deps: DocToolDeps = {}): void
|
|
|
103
105
|
|
|
104
106
|
const size = await statFile(abs, display, config.maxFileBytes);
|
|
105
107
|
const extractor = deps.extractors?.[format.id] ?? format.extract;
|
|
108
|
+
const timeoutMs = deps.timeoutMs ?? config.extractTimeoutMs;
|
|
106
109
|
startToolProgress(onUpdate);
|
|
107
|
-
const text = await extractor({ path: abs, signal })
|
|
110
|
+
const text = await withDeadline(() => extractor({ path: abs, signal }), {
|
|
111
|
+
timeoutMs,
|
|
112
|
+
signal,
|
|
113
|
+
timeoutMessage:
|
|
114
|
+
`Extraction of ${display} did not finish within ${timeoutMs} ms (document.extractTimeoutMs). ` +
|
|
115
|
+
`The ${format.label} parser was abandoned; raise the limit or read a smaller file.`,
|
|
116
|
+
});
|
|
108
117
|
|
|
109
118
|
const startIndex = typeof args.startIndex === "number" ? Math.max(0, Math.round(args.startIndex)) : 0;
|
|
110
119
|
const requested = typeof args.maxChars === "number" ? args.maxChars : config.maxChars;
|
|
@@ -22,8 +22,9 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
22
22
|
opens the browser (unless `autoOpen: false`).
|
|
23
23
|
- **Two-pane UI.** A server-rendered tree, directory listings, and file pages,
|
|
24
24
|
progressively enhanced by one first-party script (filter, lazy tree
|
|
25
|
-
expansion, thumbnail fallback).
|
|
26
|
-
|
|
25
|
+
expansion, thumbnail fallback). The tree scrolls in its own pane with quick
|
|
26
|
+
open and the filter pinned above it; below 860px it collapses into a `Files`
|
|
27
|
+
disclosure above the listing. A hairline progress bar tracks whichever
|
|
27
28
|
column is scrolling (the card body for a file or diff, the main column
|
|
28
29
|
otherwise), and the served folder names the root page.
|
|
29
30
|
- **Rich previews.** Text, rendered Markdown, images (thumbnails), and
|
|
@@ -222,11 +223,14 @@ document and the project document (project wins):
|
|
|
222
223
|
non-matching rows while keeping ancestors, disclosure triangles lazily
|
|
223
224
|
`fetch("/api/tree")`, broken thumbnails fall back to the category icon, the
|
|
224
225
|
listing headers sort in place, the wrap toggle is remembered, and the copy
|
|
225
|
-
buttons use the clipboard.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
buttons use the clipboard. On wide screens the tree is its own scroll pane
|
|
227
|
+
under the pinned filter and quick-open trigger; the sidebar is a `<details>`
|
|
228
|
+
whose content wrapper has no definite height, so the panel fills it
|
|
229
|
+
absolutely and the tree flexes inside. Without JS the page still browses and
|
|
230
|
+
renders Markdown, and the JS-only controls (filter, disclosure, actions, quick
|
|
231
|
+
open) are hidden; the sort buttons lose their pointer so they do not look
|
|
232
|
+
live. The palette is the shared Bytefly web theme (`lib/web/tokens.ts`),
|
|
233
|
+
derived from the `bytefly-dark`/`bytefly-light` terminal themes; it follows
|
|
230
234
|
`prefers-color-scheme` until an explicit choice sets `data-theme` on the root:
|
|
231
235
|
`/theme.js` applies the saved choice in the head before first paint, and the
|
|
232
236
|
header toggle flips and stores it (`pi-web-theme` in `localStorage`, the key
|
|
@@ -273,7 +277,7 @@ document and the project document (project wins):
|
|
|
273
277
|
`--cached` before the first commit), `staged` against the index, and
|
|
274
278
|
`worktree` against the index from the other side. An untracked file renders as
|
|
275
279
|
all-add through `readFileView`, so binary and over-size handling is shared
|
|
276
|
-
with the file view. `git diff --
|
|
280
|
+
with the file view. `git diff --stat` supplies the chip totals and
|
|
277
281
|
`changeDirs` derives the directory markers; the changed lines are highlighted
|
|
278
282
|
per hunk by rebuilding the old and new sides for `lib/highlight`.
|
|
279
283
|
- **Streaming responses.** The server streams `filePath` responses and
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and `bun build --no-bundle` needs no separate asset.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
14
|
+
import { SIDEBAR_COLLAPSE_MAX, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
15
15
|
|
|
16
16
|
export const CLIENT_JS = `(function () {
|
|
17
17
|
"use strict";
|
|
@@ -183,7 +183,7 @@ export const CLIENT_JS = `(function () {
|
|
|
183
183
|
|
|
184
184
|
var panel = document.querySelector("details.bf-app__sidebar");
|
|
185
185
|
if (panel && window.matchMedia) {
|
|
186
|
-
var mq = window.matchMedia("(max-width:
|
|
186
|
+
var mq = window.matchMedia("(max-width: ${SIDEBAR_COLLAPSE_MAX}px)");
|
|
187
187
|
var syncSide = function () { panel.open = !mq.matches; };
|
|
188
188
|
syncSide();
|
|
189
189
|
if (mq.addEventListener) mq.addEventListener("change", syncSide);
|
|
@@ -289,11 +289,11 @@ export const CLIENT_JS = `(function () {
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
function flash(button, label) {
|
|
292
|
+
function flash(button, label, message) {
|
|
293
293
|
var original = button.textContent;
|
|
294
294
|
button.textContent = label;
|
|
295
295
|
var status = document.getElementById("copy-status");
|
|
296
|
-
if (status) status.textContent =
|
|
296
|
+
if (status) status.textContent = message;
|
|
297
297
|
window.setTimeout(function () { button.textContent = original; }, 1200);
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -310,8 +310,8 @@ export const CLIENT_JS = `(function () {
|
|
|
310
310
|
}
|
|
311
311
|
source
|
|
312
312
|
.then(function (text) { return copyText(text); })
|
|
313
|
-
.then(function () { flash(button, "copied"); })
|
|
314
|
-
.catch(function () { flash(button, "failed"); });
|
|
313
|
+
.then(function () { flash(button, "copied", "Copied to clipboard"); })
|
|
314
|
+
.catch(function () { flash(button, "failed", "Copy failed"); });
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
317
|
|
|
@@ -483,7 +483,6 @@ export const CLIENT_JS = `(function () {
|
|
|
483
483
|
option.id = "palette-option-" + index;
|
|
484
484
|
option.setAttribute("role", "option");
|
|
485
485
|
option.setAttribute("data-index", String(index));
|
|
486
|
-
option.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
|
487
486
|
appendHighlighted(option, result.path, result.positions);
|
|
488
487
|
paletteList.appendChild(option);
|
|
489
488
|
});
|
|
@@ -46,7 +46,6 @@ const TEXT_EXTS = new Set([
|
|
|
46
46
|
".ini",
|
|
47
47
|
".cfg",
|
|
48
48
|
".conf",
|
|
49
|
-
".env",
|
|
50
49
|
".xml",
|
|
51
50
|
".csv",
|
|
52
51
|
".tsv",
|
|
@@ -89,9 +88,6 @@ const TEXT_EXTS = new Set([
|
|
|
89
88
|
".vue",
|
|
90
89
|
".svelte",
|
|
91
90
|
".astro",
|
|
92
|
-
".gitignore",
|
|
93
|
-
".gitattributes",
|
|
94
|
-
".editorconfig",
|
|
95
91
|
]);
|
|
96
92
|
const TEXT_NAMES = new Set([
|
|
97
93
|
"license",
|
|
@@ -137,8 +137,8 @@ interface RepoState {
|
|
|
137
137
|
const INSERTIONS = /(\d+) insertions?\(\+\)/;
|
|
138
138
|
const DELETIONS = /(\d+) deletions?\(-\)/;
|
|
139
139
|
|
|
140
|
-
/** A count from a
|
|
141
|
-
function
|
|
140
|
+
/** A count from a diffstat summary line, or `0` when the line does not carry it. */
|
|
141
|
+
function summaryCount(text: string, pattern: RegExp): number {
|
|
142
142
|
const match = pattern.exec(text);
|
|
143
143
|
return match ? Number(match[1]) : 0;
|
|
144
144
|
}
|
|
@@ -218,17 +218,16 @@ export function createGitDiff(
|
|
|
218
218
|
const current = await state();
|
|
219
219
|
if (!current) return undefined;
|
|
220
220
|
const args = diffArgs(effectiveMode(mode, current.hasCommits));
|
|
221
|
-
const [stat,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const summary = shortstat.code === 0 ? shortstat.stdout : "";
|
|
221
|
+
const stat = await run(["diff", "--no-color", "--stat", ...args, "--"], { cwd: root });
|
|
222
|
+
const output = stat.code === 0 ? stat.stdout : "";
|
|
223
|
+
const trimmed = output.trimEnd();
|
|
224
|
+
const summary = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
|
|
226
225
|
return {
|
|
227
226
|
mode,
|
|
228
227
|
files: current.files.filter((file) => inMode(file, mode)),
|
|
229
|
-
stat:
|
|
230
|
-
insertions:
|
|
231
|
-
deletions:
|
|
228
|
+
stat: output.trim(),
|
|
229
|
+
insertions: summaryCount(summary, INSERTIONS),
|
|
230
|
+
deletions: summaryCount(summary, DELETIONS),
|
|
232
231
|
};
|
|
233
232
|
},
|
|
234
233
|
async file(rel, mode) {
|
|
@@ -12,6 +12,7 @@ import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
|
12
12
|
import {
|
|
13
13
|
brandLockup,
|
|
14
14
|
docHead,
|
|
15
|
+
SIDEBAR_COLLAPSE_MAX,
|
|
15
16
|
WEB_BASE_CSS,
|
|
16
17
|
WEB_COMPONENTS_CSS,
|
|
17
18
|
WEB_LAYOUT_CSS,
|
|
@@ -61,7 +62,7 @@ export interface ChangeSummary {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/** Whether a path carries a change marker: a changed file, or a directory above one. */
|
|
64
|
-
|
|
65
|
+
function isChanged(summary: ChangeSummary | undefined, rel: string, isDir: boolean): boolean {
|
|
65
66
|
if (!summary || !rel) return false;
|
|
66
67
|
return isDir ? summary.dirs.has(rel) : summary.files.has(rel);
|
|
67
68
|
}
|
|
@@ -169,6 +170,16 @@ body.palette-open{overflow:hidden}
|
|
|
169
170
|
.browse-keys dt{white-space:nowrap}
|
|
170
171
|
.browse-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
|
|
171
172
|
.browse-kbd{display:inline-block;min-width:20px;padding:1px 6px;border:1px solid var(--border);border-bottom-width:2px;border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:var(--fs-xs) var(--font-mono);text-align:center}
|
|
173
|
+
/* Wide screens: pin quick open and the filter, and scroll the tree on its own
|
|
174
|
+
box. The sidebar is a details element, whose content sits in an anonymous
|
|
175
|
+
wrapper with no definite height, so the panel fills the sidebar by absolute
|
|
176
|
+
positioning; below the sidebar breakpoint the disclosure keeps normal page
|
|
177
|
+
scroll. */
|
|
178
|
+
@media not (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
179
|
+
.browse-sidebar{position:relative;overflow:hidden}
|
|
180
|
+
.browse-side{position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden}
|
|
181
|
+
.browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
|
|
182
|
+
}
|
|
172
183
|
@media (max-width:820px){
|
|
173
184
|
.browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
|
|
174
185
|
}
|
|
@@ -238,7 +249,7 @@ ${SPRITE}
|
|
|
238
249
|
<button type="button" class="bf-btn bf-btn--icon bf-theme-toggle" id="theme-toggle" aria-label="Toggle light and dark theme" title="Toggle light and dark theme"><span class="theme-glyph" aria-hidden="true">☾</span></button>
|
|
239
250
|
</div><div class="browse-progress" aria-hidden="true"><span class="browse-progress__fill" id="progress-fill"></span></div></header>
|
|
240
251
|
<div class="bf-app__body">
|
|
241
|
-
<details class="bf-app__sidebar" open>
|
|
252
|
+
<details class="bf-app__sidebar browse-sidebar" open>
|
|
242
253
|
<summary class="bf-panel__summary">Files</summary>
|
|
243
254
|
<aside class="bf-panel browse-side">
|
|
244
255
|
<button type="button" class="browse-quick-open" id="quick-open" aria-haspopup="dialog"><span>quick open</span><kbd>/</kbd></button>
|
|
@@ -252,7 +263,7 @@ ${SPRITE}
|
|
|
252
263
|
${layout.main}
|
|
253
264
|
</main>
|
|
254
265
|
</div>
|
|
255
|
-
<footer class="bf-app__footer">served from ${escapeHtml(layout.rootLabel)} · read-only · ${layout.footerNote}</footer>
|
|
266
|
+
<footer class="bf-app__footer">served from ${escapeHtml(layout.rootLabel)} · read-only · ${escapeHtml(layout.footerNote)}</footer>
|
|
256
267
|
${layout.keys && layout.keys.length > 0 ? renderKeysSheet(layout.keys) : ""}
|
|
257
268
|
<script src="/app.js" defer></script>
|
|
258
269
|
</body>
|
|
@@ -283,7 +294,7 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
|
|
|
283
294
|
const expanded = node.children.length > 0;
|
|
284
295
|
const isDir = node.isDir;
|
|
285
296
|
const spec = isDir ? FOLDER_ICON : languageOf(node.name);
|
|
286
|
-
const href =
|
|
297
|
+
const href = hrefFor(node);
|
|
287
298
|
const current = node.rel === currentRel ? ` aria-current="page"` : "";
|
|
288
299
|
const twisty = isDir
|
|
289
300
|
? `<button type="button" class="bf-tree__tw bf-tree__tw--toggle" aria-expanded="${expanded}" aria-label="${escapeHtml(node.name)}">${expanded ? "▾" : "▸"}</button>`
|
|
@@ -495,7 +506,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
495
506
|
mainClass: rendered ? "browse-page browse-measure" : "browse-page",
|
|
496
507
|
keys,
|
|
497
508
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
498
|
-
footerNote:
|
|
509
|
+
footerNote: page.rel,
|
|
499
510
|
git: page.git,
|
|
500
511
|
summary: page.summary,
|
|
501
512
|
});
|
|
@@ -715,7 +726,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
715
726
|
mainClass: "browse-page",
|
|
716
727
|
keys,
|
|
717
728
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
718
|
-
footerNote: `diff · ${
|
|
729
|
+
footerNote: `diff · ${modeScope(page.mode)}`,
|
|
719
730
|
git: page.git,
|
|
720
731
|
summary: page.summary,
|
|
721
732
|
});
|
|
@@ -726,7 +737,6 @@ export interface ErrorPage {
|
|
|
726
737
|
status: number;
|
|
727
738
|
message: string;
|
|
728
739
|
tree: TreeNode;
|
|
729
|
-
rel: string;
|
|
730
740
|
git?: GitStatus;
|
|
731
741
|
}
|
|
732
742
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* tested without a socket.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { Stats } from "node:fs";
|
|
10
11
|
import { stat } from "node:fs/promises";
|
|
11
12
|
import { THEME_JS } from "../../lib/web/index.ts";
|
|
12
13
|
import { CLIENT_JS } from "./client.ts";
|
|
@@ -87,8 +88,8 @@ function jsonResponse(status: number, value: unknown): ServeResponse {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async function treeFor(ctx: ServeContext,
|
|
91
|
-
return buildTree(ctx.root,
|
|
91
|
+
async function treeFor(ctx: ServeContext, currentRel: string): Promise<TreeNode> {
|
|
92
|
+
return buildTree(ctx.root, currentRel, ctx.config.maxDirEntries);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Resolve the header's git context, never failing the request. */
|
|
@@ -153,18 +154,30 @@ async function errorResponse(
|
|
|
153
154
|
status,
|
|
154
155
|
message,
|
|
155
156
|
tree,
|
|
156
|
-
rel: "",
|
|
157
157
|
git: await gitFor(ctx),
|
|
158
158
|
});
|
|
159
159
|
return { status, headers: { ...HTML_SECURITY, ...extra }, body };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
/** Resolve a request path once and render the directory or file page it names. */
|
|
163
|
+
async function renderPath(ctx: ServeContext, rawRel: string, raw: boolean): Promise<ServeResponse> {
|
|
163
164
|
const abs = await resolveRequestPath(ctx.root, rawRel);
|
|
164
165
|
const rel = relativePath(ctx.root, abs);
|
|
165
|
-
const info = await stat(abs).catch(() => undefined);
|
|
166
|
-
if (
|
|
166
|
+
const info: Stats | undefined = await stat(abs).catch(() => undefined);
|
|
167
|
+
if (info?.isDirectory()) return directoryResponse(ctx, rel);
|
|
168
|
+
if (!info) throw new HttpError(404, "That path does not exist.");
|
|
169
|
+
return fileResponse(ctx, rel, raw);
|
|
170
|
+
}
|
|
167
171
|
|
|
172
|
+
async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse> {
|
|
173
|
+
return renderPath(ctx, rawRel, false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function view(ctx: ServeContext, rawRel: string, raw: boolean): Promise<ServeResponse> {
|
|
177
|
+
return renderPath(ctx, rawRel, raw);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeResponse> {
|
|
168
181
|
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
169
182
|
const tree = await treeFor(ctx, rel);
|
|
170
183
|
const git = await gitFor(ctx);
|
|
@@ -184,13 +197,7 @@ async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse>
|
|
|
184
197
|
);
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
async function
|
|
188
|
-
const abs = await resolveRequestPath(ctx.root, rawRel);
|
|
189
|
-
const rel = relativePath(ctx.root, abs);
|
|
190
|
-
const info = await stat(abs).catch(() => undefined);
|
|
191
|
-
if (info?.isDirectory()) return browse(ctx, rawRel);
|
|
192
|
-
if (!info) throw new HttpError(404, "That path does not exist.");
|
|
193
|
-
|
|
200
|
+
async function fileResponse(ctx: ServeContext, rel: string, raw: boolean): Promise<ServeResponse> {
|
|
194
201
|
const file = await readFileView(ctx.root, rel, ctx.config.maxFileBytes, ctx.config.maxTextLines, ctx.config.markdown);
|
|
195
202
|
const tree = await treeFor(ctx, rel);
|
|
196
203
|
const git = await gitFor(ctx);
|
|
@@ -375,7 +382,9 @@ export async function handleRequest(
|
|
|
375
382
|
if (pathname.startsWith("/raw/")) return await raw(ctx, pathname.slice("/raw/".length));
|
|
376
383
|
return errorResponse(ctx, 404, "That page does not exist.");
|
|
377
384
|
} catch (error) {
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
386
|
+
const message = error instanceof HttpError ? error.message : "Something went wrong reading that path.";
|
|
387
|
+
if (pathname.startsWith("/api/")) return jsonResponse(status, { error: message });
|
|
388
|
+
return errorResponse(ctx, status, message);
|
|
380
389
|
}
|
|
381
390
|
}
|