@gavin-hu/my-pi-agent 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bytefly's live working timer.
|
|
3
|
+
*
|
|
4
|
+
* Pi's working row shows a static `Working`, so a long run reads the same as a
|
|
5
|
+
* short one. This surface advances the message once a second while the row is
|
|
6
|
+
* up, and hands it back to Pi when the row comes down.
|
|
7
|
+
*
|
|
8
|
+
* The timer is tied to the row's visibility. The host shows the working
|
|
9
|
+
* indicator in its own `turn_start` handler and clears it at `agent_end`
|
|
10
|
+
* (`interactive-mode.js`), so the tick starts at `turn_start` and stops at
|
|
11
|
+
* `agent_end` — the two moments the row appears and disappears. Turns inside
|
|
12
|
+
* one run do not restart it: the host leaves the same indicator up across
|
|
13
|
+
* turns, and `start` is a no-op while it is counting.
|
|
14
|
+
*
|
|
15
|
+
* Compaction and auto-retry also replace the row, but both run outside this
|
|
16
|
+
* window (`auto_retry_start` follows `agent_end`; the before-settle compaction
|
|
17
|
+
* runs after it), and neither is an extension event, so there is nothing to
|
|
18
|
+
* hook and nothing to undo.
|
|
19
|
+
*
|
|
20
|
+
* The handle is global (`setWorkingMessage` drives the single working row), so
|
|
21
|
+
* the tick is cancelled and the message reset on stop and, belt and braces, on
|
|
22
|
+
* `session_shutdown` — the factory itself only registers.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { WORKING_CONFIG } from "./config.ts";
|
|
27
|
+
import { workingCounter } from "./format.ts";
|
|
28
|
+
|
|
29
|
+
/** Host seams, injected by tests so the tick is deterministic. */
|
|
30
|
+
export interface WorkingDeps {
|
|
31
|
+
/** The clock the count is read from. */
|
|
32
|
+
now?: () => number;
|
|
33
|
+
/** Start the repeating tick; returns a cancel handle. */
|
|
34
|
+
schedule?: (tick: () => void, intervalMs: number) => () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A repeating timer that never holds the process open on its own. */
|
|
38
|
+
function defaultSchedule(tick: () => void, intervalMs: number): () => void {
|
|
39
|
+
const timer = setInterval(tick, intervalMs);
|
|
40
|
+
(timer as { unref?: () => void }).unref?.();
|
|
41
|
+
return () => clearInterval(timer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Register the timer; it runs only in interactive (`tui`) sessions. */
|
|
45
|
+
export function registerWorkingTimer(pi: ExtensionAPI, deps: WorkingDeps = {}): void {
|
|
46
|
+
const now = deps.now ?? (() => Date.now());
|
|
47
|
+
const schedule = deps.schedule ?? defaultSchedule;
|
|
48
|
+
let cancel: (() => void) | undefined;
|
|
49
|
+
let startedAt = 0;
|
|
50
|
+
// A boolean, not `startedAt === 0`: an injected clock is allowed to start at 0.
|
|
51
|
+
let active = false;
|
|
52
|
+
|
|
53
|
+
/** Stop the tick and hand the working message back to Pi. Idempotent. */
|
|
54
|
+
const stop = (ctx: ExtensionContext): void => {
|
|
55
|
+
cancel?.();
|
|
56
|
+
cancel = undefined;
|
|
57
|
+
if (!active) return;
|
|
58
|
+
active = false;
|
|
59
|
+
ctx.ui.setWorkingMessage(undefined);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Start the count for the current turn; a no-op while already counting. */
|
|
63
|
+
const start = (ctx: ExtensionContext): void => {
|
|
64
|
+
if (active) return;
|
|
65
|
+
startedAt = now();
|
|
66
|
+
active = true;
|
|
67
|
+
const tick = (): void => {
|
|
68
|
+
ctx.ui.setWorkingMessage(workingCounter(now() - startedAt));
|
|
69
|
+
};
|
|
70
|
+
tick();
|
|
71
|
+
cancel = schedule(tick, WORKING_CONFIG.intervalMs);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// The host shows the working indicator on its own `turn_start` handler, so
|
|
75
|
+
// this is the first frame the row is up.
|
|
76
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
77
|
+
if (ctx.mode !== "tui") return;
|
|
78
|
+
start(ctx);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The host clears the working indicator at `agent_end`.
|
|
82
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
83
|
+
if (ctx.mode !== "tui") return;
|
|
84
|
+
stop(ctx);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("session_shutdown", (_event, ctx) => stop(ctx));
|
|
88
|
+
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
the effective working directory with the same symlink-aware guard the built-in
|
|
9
9
|
path tools use.
|
|
10
10
|
|
|
11
|
+
## Tool rendering
|
|
12
|
+
|
|
13
|
+
How the `read_doc` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
14
|
+
|
|
15
|
+
<img src="../../assets/tools/read_doc.png" alt="The read_doc tool's result: the call line and an indented body with the file path, format, size, and the extracted text preview" width="100%">
|
|
16
|
+
|
|
11
17
|
## Quickstart
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -76,19 +82,20 @@ interactive transcript (and HTML exports).
|
|
|
76
82
|
|
|
77
83
|
The call line carries the path (`~`-shortened, OSC-8 linked when the terminal
|
|
78
84
|
supports it) and any paging options. The result line drops the path — it is
|
|
79
|
-
already on the call line — and leads with the format and
|
|
85
|
+
already on the call line — and leads with the format and the character range,
|
|
86
|
+
grouped so a `k` suffix never sits on one bound and not the other:
|
|
80
87
|
|
|
81
88
|
```
|
|
82
89
|
read_doc ~/docs/spec.pdf (from 40k, max 80k)
|
|
83
|
-
PDF ·
|
|
90
|
+
PDF · chars 40,001–120,000 of 250,000 · next at 120,000 · ctrl+o
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
Expanded adds a blank line before the preview body, matching the built-in
|
|
87
94
|
header/output gap:
|
|
88
95
|
|
|
89
96
|
```
|
|
90
|
-
read_doc ~/docs/spec.pdf
|
|
91
|
-
PDF ·
|
|
97
|
+
read_doc ~/docs/spec.pdf (from 40k, max 80k)
|
|
98
|
+
PDF · chars 40,001–120,000 of 250,000 · next at 120,000
|
|
92
99
|
|
|
93
100
|
Chapter 1
|
|
94
101
|
This specification defines ...
|
|
@@ -218,7 +225,7 @@ config defaults, and error listing all derive from it.
|
|
|
218
225
|
| Tool | `read_doc`, `exposure: "direct"`, `defaultActive: true`, default (`parallel`) execution, `annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }`, TypeBox `parameters` and `outputSchema`, and `renderCall`/`renderResult` (reuse `context.lastComponent`; read `context.cwd` and `context.isError`). |
|
|
219
226
|
| State | None persisted. Per-result state lives in the tool-result `details` / `structuredContent`; the renderer reads only that. |
|
|
220
227
|
| Lifecycle | No `session_start` / `session_shutdown` hooks: each read imports a parser, runs it, and holds no long-lived resources. |
|
|
221
|
-
| Host seams | `resolveEffectiveCwd` (`lib/env.ts`), `isInsideReal` / `realPathOfNearest` (`lib/path.ts`), `loadConfigSection` (`lib/config.ts`), `stripControlChars` / `sanitize` / `formatTokens` (`lib/format.ts`), `SEPARATORS` (`lib/
|
|
228
|
+
| 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`). |
|
|
222
229
|
| Config | The `document` section of the shared `bytefly.json` via `loadConfigSection` (`~/.pi/agent`, `<cwd>/.pi`). |
|
|
223
230
|
|
|
224
231
|
## Design notes
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the clean slice; the text adds a header and any truncation note.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { formatCount } from "../../lib/format.ts";
|
|
10
10
|
import type { FormatId } from "./formats.ts";
|
|
11
11
|
|
|
12
12
|
export interface DocPageInput {
|
|
@@ -72,9 +72,9 @@ export interface DocSummaryInput {
|
|
|
72
72
|
export interface DocSummary {
|
|
73
73
|
/** Uppercase format id, e.g. `PDF`. */
|
|
74
74
|
format: string;
|
|
75
|
-
/** Range detail, e.g. `1–
|
|
75
|
+
/** Range detail, e.g. `chars 1–40,000 of 42,478`. */
|
|
76
76
|
detail: string;
|
|
77
|
-
/** `
|
|
77
|
+
/** `next at N` when truncated, `complete` for a non-first final page. */
|
|
78
78
|
note?: string;
|
|
79
79
|
/** Whether more text remains; the renderer colours the note with this. */
|
|
80
80
|
truncated: boolean;
|
|
@@ -83,9 +83,9 @@ export interface DocSummary {
|
|
|
83
83
|
/**
|
|
84
84
|
* Describe a `read_doc` result for the transcript, without theme colours.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* A complete first read shows the total; a paged read shows the returned range
|
|
87
|
+
* and where to continue, so a truncated result is never mistaken for the whole
|
|
88
|
+
* document.
|
|
89
89
|
*/
|
|
90
90
|
export function summarizeDoc(input: DocSummaryInput): DocSummary {
|
|
91
91
|
const format = input.format.toUpperCase();
|
|
@@ -93,18 +93,18 @@ export function summarizeDoc(input: DocSummaryInput): DocSummary {
|
|
|
93
93
|
if (input.startIndex >= input.chars) {
|
|
94
94
|
return {
|
|
95
95
|
format,
|
|
96
|
-
detail: `no text at ${
|
|
96
|
+
detail: `no text at ${formatCount(input.startIndex)} of ${formatCount(input.chars)} chars`,
|
|
97
97
|
truncated: false,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
if (input.startIndex <= 0 && !input.truncated) {
|
|
101
|
-
return { format, detail: `${
|
|
101
|
+
return { format, detail: `${formatCount(input.chars)} chars`, truncated: false };
|
|
102
102
|
}
|
|
103
|
-
const range =
|
|
103
|
+
const range = `chars ${formatCount(input.startIndex + 1)}–${formatCount(input.nextIndex)} of ${formatCount(input.chars)}`;
|
|
104
104
|
return {
|
|
105
105
|
format,
|
|
106
106
|
detail: range,
|
|
107
|
-
note: input.truncated ? `
|
|
107
|
+
note: input.truncated ? `next at ${formatCount(input.nextIndex)}` : "complete",
|
|
108
108
|
truncated: input.truncated,
|
|
109
109
|
};
|
|
110
110
|
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Transcript rendering for `read_doc`.
|
|
3
3
|
*
|
|
4
4
|
* Theme-aware formatting only: the tool definition in `tool.ts` wires these to
|
|
5
|
-
* `renderCall` / `renderResult`. The call line carries the path (
|
|
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
|
-
* leads with the format and
|
|
8
|
-
* The expanded view appends a sanitized, line-capped preview of the
|
|
9
|
-
* text. Every field is sanitized before a theme colour is applied.
|
|
7
|
+
* leads with the format and the exact character range, so the path is never
|
|
8
|
+
* repeated. The expanded view appends a sanitized, line-capped preview of the
|
|
9
|
+
* extracted text. Every field is sanitized before a theme colour is applied.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -14,19 +14,20 @@ import { pathToFileURL } from "node:url";
|
|
|
14
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { getCapabilities, hyperlink } from "@earendil-works/pi-tui";
|
|
16
16
|
import { formatTokens, sanitizeLine, shortenPath, stripControlChars } from "../../lib/format.ts";
|
|
17
|
-
import { expandKey, SEPARATORS } from "../../lib/
|
|
17
|
+
import { expandKey, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
18
19
|
import { summarizeDoc } from "./paging.ts";
|
|
19
20
|
import { TOOL_NAME, type DocArgs, type DocResult } from "./schema.ts";
|
|
20
21
|
|
|
21
22
|
/** Lines of extracted text shown in the expanded preview. */
|
|
22
23
|
export const PREVIEW_LINES = 20;
|
|
23
24
|
|
|
24
|
-
/** Render the argument path:
|
|
25
|
+
/** Render the argument path: body colour, `~`-shortened, OSC-8 linked when supported. */
|
|
25
26
|
export function renderDocPath(rawPath: string | undefined, theme: Theme, cwd: string): string {
|
|
26
|
-
if (typeof rawPath !== "string") return theme
|
|
27
|
+
if (typeof rawPath !== "string") return paint(theme, "bad", "[invalid arg]");
|
|
27
28
|
const value = sanitizeLine(rawPath);
|
|
28
29
|
if (!value) return theme.fg("toolOutput", "...");
|
|
29
|
-
const styled = theme
|
|
30
|
+
const styled = paint(theme, "label", shortenPath(value));
|
|
30
31
|
if (!getCapabilities().hyperlinks) return styled;
|
|
31
32
|
const abs = isAbsolute(value) ? value : resolve(cwd, value);
|
|
32
33
|
return hyperlink(styled, pathToFileURL(abs).href);
|
|
@@ -63,7 +64,7 @@ export function formatDocCall(args: DocArgs, theme: Theme, context: { cwd: strin
|
|
|
63
64
|
const options: string[] = [];
|
|
64
65
|
if (typeof startIndex === "number" && startIndex > 0) options.push(`from ${formatTokens(startIndex)}`);
|
|
65
66
|
if (typeof maxChars === "number") options.push(`max ${formatTokens(maxChars)}`);
|
|
66
|
-
if (options.length > 0) text += theme
|
|
67
|
+
if (options.length > 0) text += paint(theme, "faint", ` (${options.join(", ")})`);
|
|
67
68
|
return text;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -72,7 +73,7 @@ export function formatDocCall(args: DocArgs, theme: Theme, context: { cwd: strin
|
|
|
72
73
|
* expanded, a sanitized preview of the extracted text.
|
|
73
74
|
*/
|
|
74
75
|
export function formatDocResult(result: DocRenderResult, options: DocRenderOptions, theme: Theme): string {
|
|
75
|
-
if (options.isPartial) return theme
|
|
76
|
+
if (options.isPartial) return paint(theme, "warn", "Reading...");
|
|
76
77
|
|
|
77
78
|
const isError = result.isError ?? false;
|
|
78
79
|
const details = result.details as DocResult | undefined;
|
|
@@ -81,23 +82,23 @@ export function formatDocResult(result: DocRenderResult, options: DocRenderOptio
|
|
|
81
82
|
const first = result.content[0];
|
|
82
83
|
const message = first?.type === "text" && first.text ? first.text : "Read failed";
|
|
83
84
|
const safe = sanitizeLine(message.startsWith("Error:") ? message : `Error: ${message}`);
|
|
84
|
-
return theme
|
|
85
|
+
return paint(theme, "bad", safe);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
const summary = summarizeDoc(details);
|
|
88
|
-
const sep = theme
|
|
89
|
-
let text = theme
|
|
90
|
-
if (summary.note) text += theme
|
|
89
|
+
const sep = paint(theme, "faint", SEPARATORS.item);
|
|
90
|
+
let text = paint(theme, "meta", summary.format) + sep + theme.fg("toolOutput", summary.detail);
|
|
91
|
+
if (summary.note) text += paint(theme, summary.truncated ? "accent" : "faint", SEPARATORS.item + summary.note);
|
|
91
92
|
|
|
92
93
|
if (details.text.length > 0) {
|
|
93
94
|
if (options.expanded) {
|
|
94
95
|
const { body, moreLines } = previewText(details.text, PREVIEW_LINES);
|
|
95
96
|
// Blank line before the body, matching the built-ins' header/output gap.
|
|
96
97
|
text += `\n\n${theme.fg("toolOutput", body)}`;
|
|
97
|
-
if (moreLines > 0) text += `\n${theme
|
|
98
|
+
if (moreLines > 0) text += `\n${paint(theme, "meta", `... (${moreLines} more lines)`)}`;
|
|
98
99
|
} else {
|
|
99
100
|
const hint = expandKey(theme);
|
|
100
|
-
if (hint) text += theme
|
|
101
|
+
if (hint) text += paint(theme, "meta", SEPARATORS.item) + hint;
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -15,7 +15,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
15
15
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
16
16
|
import { sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
17
17
|
import { isInsideReal, realPathOfNearest } from "../../lib/path.ts";
|
|
18
|
-
import { reuseText } from "../../lib/tui.ts";
|
|
18
|
+
import { reuseText } from "../../lib/tui/chrome.ts";
|
|
19
19
|
import { isFormatEnabled, loadConfig } from "./config.ts";
|
|
20
20
|
import type { DocExtractor } from "./extract/file.ts";
|
|
21
21
|
import { detectFormat, supportedExtensions, unsupportedHint, type FormatId } from "./formats.ts";
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
directory, opens the default browser at it, and prints the URL. The page is a
|
|
5
5
|
two-pane file browser: a collapsible path tree on the left, directory listings
|
|
6
6
|
and file views on the right, with image thumbnails, rendered Markdown,
|
|
7
|
-
highlighted code, per-language icons,
|
|
7
|
+
highlighted code, per-language icons, a fuzzy quick-open palette, and a
|
|
8
|
+
read-only git diff view. It is
|
|
8
9
|
bound to `127.0.0.1`
|
|
9
10
|
and ships no dependencies — `node:http` plus one small vanilla script.
|
|
10
11
|
|
|
@@ -22,7 +23,9 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
22
23
|
- **Two-pane UI.** A server-rendered tree, directory listings, and file pages,
|
|
23
24
|
progressively enhanced by one first-party script (filter, lazy tree
|
|
24
25
|
expansion, thumbnail fallback). Below 860px the tree collapses into a
|
|
25
|
-
`Files` disclosure above the listing.
|
|
26
|
+
`Files` disclosure above the listing. A hairline progress bar tracks whichever
|
|
27
|
+
column is scrolling (the card body for a file or diff, the main column
|
|
28
|
+
otherwise), and the served folder names the root page.
|
|
26
29
|
- **Rich previews.** Text, rendered Markdown, images (thumbnails), and
|
|
27
30
|
binary/over-size download cards, with per-language icons.
|
|
28
31
|
- **First-party syntax highlighting.** Code files render with keywords,
|
|
@@ -33,8 +36,11 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
33
36
|
Python (`py`, `pyw`, `pyi`), Java, YAML (`yaml`, `yml`), and SQL. Other text
|
|
34
37
|
types stay plain.
|
|
35
38
|
- **Rendered Markdown.** `.md`/`.markdown` files render as headings, lists,
|
|
36
|
-
tables, code, inline and reference links, and images, with
|
|
37
|
-
**raw** view (`?raw=1`).
|
|
39
|
+
tables, code, inline and reference links, bare-URL autolinks, and images, with
|
|
40
|
+
a one-click **raw** view (`?raw=1`). The body follows GitHub's Markdown content
|
|
41
|
+
column: one centred measure of at most 1012px carrying headings, prose,
|
|
42
|
+
tables, code, and images, with wide tables scrolling inside it. Tables size
|
|
43
|
+
their columns to the content and scroll
|
|
38
44
|
when wide, and named and numeric character references are decoded. Fenced code
|
|
39
45
|
blocks are syntax-highlighted when the fence names a supported language.
|
|
40
46
|
Headings get stable anchor ids, so in-document `#anchor` links and a table of
|
|
@@ -51,9 +57,24 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
51
57
|
- **Quick open.** `/` (or `Ctrl/Cmd+K`, or the sidebar button) opens a fuzzy
|
|
52
58
|
path finder over a capped index of the tree, with keyboard navigation and a
|
|
53
59
|
highlighted match; Enter opens the file.
|
|
60
|
+
- **Keyboard.** `/` or `Ctrl/Cmd+K` opens quick open, `u` toggles line wrapping,
|
|
61
|
+
`r` switches rendered and raw Markdown, `n`/`p` step between a diff's hunks,
|
|
62
|
+
and `?` opens a shortcut sheet. A diff's hunk headers pin to the top of the
|
|
63
|
+
code viewport while its body scrolls.
|
|
54
64
|
- **Read-only and loopback-scoped.** No editing, no writing, no LAN exposure.
|
|
55
65
|
- **Git context.** A read-only header chip shows the checked-out branch (or
|
|
56
|
-
detached head) and
|
|
66
|
+
detached head) and reflows on narrow screens. Uncommitted changes are stated
|
|
67
|
+
once, by the changes chip below; turn the diff view off (`gitDiff: false`) and
|
|
68
|
+
the header reports no changes at all.
|
|
69
|
+
- **Git diff view.** A `● N changes +A −D` chip links to `/diff`, a change
|
|
70
|
+
overview with `git diff --stat` and a per-file status table, and `/diff/<rel>`
|
|
71
|
+
renders one file's unified diff, whose head carries its own `+A −D` and hunk
|
|
72
|
+
count. Changed lines are syntax-highlighted with the
|
|
73
|
+
file's language, and dirty files and their directories carry a marker in the
|
|
74
|
+
listing and the tree. The scope is switchable between `head` / `staged` /
|
|
75
|
+
`worktree`; untracked files show as all-add, an unborn `HEAD` falls back to the
|
|
76
|
+
index, and a non-repository is a plain empty state. Read-only, capped, and
|
|
77
|
+
rendered with the shared diff colors.
|
|
57
78
|
- **No dependencies.** `node:http` plus the inline sprite, CSS, and client
|
|
58
79
|
script.
|
|
59
80
|
|
|
@@ -93,6 +114,8 @@ reflects only this session's server, with no cross-session sharing.
|
|
|
93
114
|
| `/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
|
|
94
115
|
| `/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
|
|
95
116
|
| `/raw/<rel>` | raw bytes, streamed; downloads and image sources |
|
|
117
|
+
| `/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
|
|
118
|
+
| `/diff/<rel>[?mode=…]` | one file's unified diff |
|
|
96
119
|
| `/api/tree?path=<rel>` | JSON children for lazy tree expansion |
|
|
97
120
|
| `/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
|
|
98
121
|
| `/app.js` | the first-party client script |
|
|
@@ -115,7 +138,10 @@ document and the project document (project wins):
|
|
|
115
138
|
"maxDirEntries": 2000, // listed entries before truncation
|
|
116
139
|
"thumbnails": true,
|
|
117
140
|
"markdown": true, // render Markdown files as formatted HTML
|
|
118
|
-
"maxIndexEntries": 20000
|
|
141
|
+
"maxIndexEntries": 20000, // files collected for quick open before truncation
|
|
142
|
+
"gitDiff": true, // offer the read-only diff view
|
|
143
|
+
"diffContext": 3, // unified diff context lines
|
|
144
|
+
"maxDiffLines": 4000 // diff lines rendered before truncation
|
|
119
145
|
}
|
|
120
146
|
```
|
|
121
147
|
|
|
@@ -142,8 +168,11 @@ document and the project document (project wins):
|
|
|
142
168
|
attributes; only the fixed `tok-*` class names reach the page.
|
|
143
169
|
- **Caps.** `maxFileBytes` (inline rendering), `maxTextLines` (rendered lines),
|
|
144
170
|
`maxThumbBytes` (thumbnails), and `maxDirEntries` (listings) bound every read.
|
|
145
|
-
- **Git reads.** The header chip
|
|
146
|
-
|
|
171
|
+
- **Git reads.** The header chip and the diff view run read-only git
|
|
172
|
+
(`rev-parse`, `status --porcelain=v1 -z`, `diff --stat`, `diff -U<n>`) with a
|
|
173
|
+
timeout and a short cache; any failure simply omits the chip or the diff. A
|
|
174
|
+
per-file diff sets `GIT_LITERAL_PATHSPECS=1`, so a filename containing glob
|
|
175
|
+
characters is matched literally.
|
|
147
176
|
- **Read-only.** Nothing is ever written.
|
|
148
177
|
|
|
149
178
|
## Limitations
|
|
@@ -237,6 +266,16 @@ document and the project document (project wins):
|
|
|
237
266
|
`maxIndexEntries`, and is cached per server for 10 s by `createFileIndex`. The
|
|
238
267
|
palette is a `dialog`/`combobox`/`listbox` with `aria-activedescendant` and
|
|
239
268
|
`Esc` / arrow / `Enter` handling.
|
|
269
|
+
- **Diff view.** `git.ts` extends the read-only provider set with a TTL-cached
|
|
270
|
+
`git status --porcelain=v1 -z` and a per-mode `git diff`; `diff.ts` parses the
|
|
271
|
+
unified diff and the porcelain records into plain view models, so `router.ts`
|
|
272
|
+
and `html.ts` stay pure. `head` compares against `HEAD` (falling back to
|
|
273
|
+
`--cached` before the first commit), `staged` against the index, and
|
|
274
|
+
`worktree` against the index from the other side. An untracked file renders as
|
|
275
|
+
all-add through `readFileView`, so binary and over-size handling is shared
|
|
276
|
+
with the file view. `git diff --shortstat` supplies the chip totals and
|
|
277
|
+
`changeDirs` derives the directory markers; the changed lines are highlighted
|
|
278
|
+
per hunk by rebuilding the old and new sides for `lib/highlight`.
|
|
240
279
|
- **Streaming responses.** The server streams `filePath` responses and
|
|
241
280
|
suppresses bodies for `HEAD`, so large `/raw` files are not buffered.
|
|
242
281
|
- **Root confinement over feature breadth.** The root is deliberately confined
|
|
@@ -254,6 +293,7 @@ document and the project document (project wins):
|
|
|
254
293
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
|
|
255
294
|
| [`../../lib/web/index.ts`](../../lib/web/index.ts) | The shared Bytefly web brand system: tokens, base, components, layout, runtime, and markup |
|
|
256
295
|
| `files.ts` | Classification (incl. Markdown), listings, text reads, sidebar tree |
|
|
296
|
+
| `diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
|
|
257
297
|
| `fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
|
|
258
298
|
| `markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted) |
|
|
259
299
|
| `git.ts` | Read-only branch/dirty provider for the header chip |
|
|
@@ -275,8 +315,12 @@ highlighting, safety, and path resolution),
|
|
|
275
315
|
the icon mapping and sprite, HTML escaping/encoding/rendering, the client script
|
|
276
316
|
and its serving, the platform browser commands with an injected spawn, the router
|
|
277
317
|
(routes, `/api/tree`, `/api/files`, `?raw=1`, status codes, `Host`, `HEAD`, `/raw`
|
|
278
|
-
headers)
|
|
318
|
+
headers, and the `/diff` view routes and mode validation)
|
|
279
319
|
without a socket, a real `127.0.0.1:0` server, the session-port lifecycle (fixed
|
|
280
320
|
on first start, reused, and re-rolled after a conflict), the git status provider
|
|
281
|
-
(branch, dirty, detached, non-repo, failure, and TTL cache),
|
|
321
|
+
(branch, dirty, detached, non-repo, failure, and TTL cache), the git diff parser
|
|
322
|
+
and provider (line kinds, line numbers, status and rename parsing, mode
|
|
323
|
+
filtering, untracked all-add, unborn-HEAD fallback, literal pathspec, and TTL
|
|
324
|
+
cache), the diff renderers (highlighted changed lines, marker sets, and chip
|
|
325
|
+
totals), and the command
|
|
282
326
|
lifecycle with injected seams.
|
|
@@ -525,7 +525,96 @@ export const CLIENT_JS = `(function () {
|
|
|
525
525
|
if (result) window.location.assign("/view/" + encodeSegments(result.path));
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
// Keyboard: a shortcut sheet, the per-page actions the server rendered, and
|
|
529
|
+
// hunk-to-hunk navigation through a diff.
|
|
530
|
+
var keysDialog = document.getElementById("keys-dialog");
|
|
531
|
+
var keysButton = document.getElementById("keys-open");
|
|
532
|
+
var hunkAnchors = Array.prototype.slice.call(document.querySelectorAll("[data-hunk]"));
|
|
533
|
+
var progressFill = document.getElementById("progress-fill");
|
|
534
|
+
|
|
535
|
+
function toggleKeys(force) {
|
|
536
|
+
if (!keysDialog) return;
|
|
537
|
+
var open = force !== undefined ? force : keysDialog.hidden;
|
|
538
|
+
keysDialog.hidden = !open;
|
|
539
|
+
if (open) {
|
|
540
|
+
var close = document.getElementById("keys-close");
|
|
541
|
+
if (close) close.focus();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function toggleRaw() {
|
|
546
|
+
var next = document.querySelector(".bf-card__actions .bf-segmented a.bf-segmented__item:not([aria-current])");
|
|
547
|
+
if (next) window.location.assign(next.getAttribute("href") || next.href);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// The hunk whose header is pinned (or last passed), then one step either way.
|
|
551
|
+
function moveHunk(delta) {
|
|
552
|
+
if (hunkAnchors.length === 0) return;
|
|
553
|
+
var scroller = primaryScroller();
|
|
554
|
+
// The line a pinned hunk header sticks to: the code viewport's top inset
|
|
555
|
+
// by its padding. With no scrolling pane (narrow screens) the page is the
|
|
556
|
+
// viewport, so the anchor is the window top.
|
|
557
|
+
var lineTop = 8;
|
|
558
|
+
if (scroller) lineTop = scroller.getBoundingClientRect().top + (parseFloat(getComputedStyle(scroller).paddingTop) || 0);
|
|
559
|
+
var current = -1;
|
|
560
|
+
for (var index = 0; index < hunkAnchors.length; index++) {
|
|
561
|
+
if (hunkAnchors[index].getBoundingClientRect().top <= lineTop + 2) current = index;
|
|
562
|
+
}
|
|
563
|
+
var next = Math.max(0, Math.min(hunkAnchors.length - 1, current + delta));
|
|
564
|
+
if (scroller) scroller.scrollTop += hunkAnchors[next].getBoundingClientRect().top - lineTop;
|
|
565
|
+
else hunkAnchors[next].scrollIntoView({ block: "start" });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// The page's primary scroller: the card body for a file or diff, the main
|
|
569
|
+
// column otherwise.
|
|
570
|
+
function primaryScroller() {
|
|
571
|
+
var candidates = [
|
|
572
|
+
document.getElementById("main"),
|
|
573
|
+
document.querySelector(".bf-md"),
|
|
574
|
+
document.querySelector("pre.bf-code")
|
|
575
|
+
];
|
|
576
|
+
var best = null;
|
|
577
|
+
var bestRange = 0;
|
|
578
|
+
candidates.forEach(function (node) {
|
|
579
|
+
if (!node) return;
|
|
580
|
+
var range = node.scrollHeight - node.clientHeight;
|
|
581
|
+
if (range > bestRange) { bestRange = range; best = node; }
|
|
582
|
+
});
|
|
583
|
+
return bestRange > 0 ? best : null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function updateProgress() {
|
|
587
|
+
if (!progressFill) return;
|
|
588
|
+
var scroller = primaryScroller();
|
|
589
|
+
var doc = document.scrollingElement || document.documentElement;
|
|
590
|
+
var top = doc ? doc.scrollTop : 0;
|
|
591
|
+
var height = doc ? doc.scrollHeight : 0;
|
|
592
|
+
var view = doc ? doc.clientHeight : 0;
|
|
593
|
+
if (scroller) { top = scroller.scrollTop; height = scroller.scrollHeight; view = scroller.clientHeight; }
|
|
594
|
+
var max = height - view;
|
|
595
|
+
var percent = max > 0 ? Math.min(100, Math.max(0, (top / max) * 100)) : 0;
|
|
596
|
+
progressFill.style.width = percent + "%";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (keysButton) keysButton.addEventListener("click", function () { toggleKeys(); });
|
|
600
|
+
if (keysDialog) {
|
|
601
|
+
keysDialog.addEventListener("click", function (event) {
|
|
602
|
+
if (event.target === keysDialog) toggleKeys(false);
|
|
603
|
+
});
|
|
604
|
+
var keysClose = document.getElementById("keys-close");
|
|
605
|
+
if (keysClose) keysClose.addEventListener("click", function () { toggleKeys(false); });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
window.addEventListener("scroll", updateProgress, true);
|
|
609
|
+
window.addEventListener("resize", updateProgress);
|
|
610
|
+
updateProgress();
|
|
611
|
+
|
|
528
612
|
document.addEventListener("keydown", function (event) {
|
|
613
|
+
if (event.key === "Escape" && keysDialog && !keysDialog.hidden) {
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
toggleKeys(false);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
529
618
|
if (paletteOpen()) {
|
|
530
619
|
if (event.key === "Escape") {
|
|
531
620
|
event.preventDefault();
|
|
@@ -533,15 +622,34 @@ export const CLIENT_JS = `(function () {
|
|
|
533
622
|
}
|
|
534
623
|
return;
|
|
535
624
|
}
|
|
625
|
+
if (keysDialog && !keysDialog.hidden) return;
|
|
536
626
|
var target = event.target;
|
|
537
627
|
var editable = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable);
|
|
538
628
|
if (editable) return;
|
|
539
|
-
if (event.
|
|
629
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
|
540
630
|
event.preventDefault();
|
|
541
631
|
openPalette();
|
|
542
|
-
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
635
|
+
if (event.key === "/") {
|
|
543
636
|
event.preventDefault();
|
|
544
637
|
openPalette();
|
|
638
|
+
} else if (event.key === "?") {
|
|
639
|
+
event.preventDefault();
|
|
640
|
+
toggleKeys();
|
|
641
|
+
} else if (event.key === "u" && wrapButton) {
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
wrapButton.click();
|
|
644
|
+
} else if (event.key === "r" && document.querySelector(".bf-card__actions .bf-segmented")) {
|
|
645
|
+
event.preventDefault();
|
|
646
|
+
toggleRaw();
|
|
647
|
+
} else if (event.key === "n" && hunkAnchors.length > 0) {
|
|
648
|
+
event.preventDefault();
|
|
649
|
+
moveHunk(1);
|
|
650
|
+
} else if (event.key === "p" && hunkAnchors.length > 0) {
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
moveHunk(-1);
|
|
545
653
|
}
|
|
546
654
|
});
|
|
547
655
|
|
|
@@ -28,6 +28,12 @@ export interface ServeConfig {
|
|
|
28
28
|
markdown: boolean;
|
|
29
29
|
/** Files collected for the quick-open index before it is truncated. */
|
|
30
30
|
maxIndexEntries: number;
|
|
31
|
+
/** Offer the read-only git diff view. */
|
|
32
|
+
gitDiff: boolean;
|
|
33
|
+
/** Unified diff context lines. */
|
|
34
|
+
diffContext: number;
|
|
35
|
+
/** Diff lines rendered before the view is truncated. */
|
|
36
|
+
maxDiffLines: number;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export const DEFAULT_CONFIG: ServeConfig = {
|
|
@@ -40,6 +46,9 @@ export const DEFAULT_CONFIG: ServeConfig = {
|
|
|
40
46
|
thumbnails: true,
|
|
41
47
|
markdown: true,
|
|
42
48
|
maxIndexEntries: 20000,
|
|
49
|
+
gitDiff: true,
|
|
50
|
+
diffContext: 3,
|
|
51
|
+
maxDiffLines: 4000,
|
|
43
52
|
};
|
|
44
53
|
|
|
45
54
|
/** Base of the per-session port range. */
|
|
@@ -73,6 +82,9 @@ export function normalizeConfig(
|
|
|
73
82
|
thumbnails: bool(raw.thumbnails, base.thumbnails),
|
|
74
83
|
markdown: bool(raw.markdown, base.markdown),
|
|
75
84
|
maxIndexEntries: clampInteger(raw.maxIndexEntries, base.maxIndexEntries, 1, 1_000_000),
|
|
85
|
+
gitDiff: bool(raw.gitDiff, base.gitDiff),
|
|
86
|
+
diffContext: clampInteger(raw.diffContext, base.diffContext, 0, 100),
|
|
87
|
+
maxDiffLines: clampInteger(raw.maxDiffLines, base.maxDiffLines, 1, 1_000_000),
|
|
76
88
|
};
|
|
77
89
|
}
|
|
78
90
|
|