@gavin-hu/my-pi-agent 0.8.0 → 0.9.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 +9 -8
- package/README.zh-CN.md +8 -8
- package/assets/file-browse.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +49 -36
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +6 -5
- 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 +33 -16
- package/extensions/bytefly/header/config.ts +28 -18
- package/extensions/bytefly/index.ts +1 -1
- 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/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +13 -40
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/document/README.md +1 -1
- package/extensions/document/render.ts +11 -10
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +62 -14
- package/extensions/file-browse/client.ts +103 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +176 -1
- package/extensions/file-browse/html.ts +412 -23
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +98 -5
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/goal/format.ts +11 -10
- package/extensions/goal/tools.ts +6 -5
- package/extensions/goal/tui.ts +1 -1
- package/extensions/job/README.md +2 -2
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +1 -1
- package/extensions/job/tools.ts +11 -10
- package/extensions/job/tui.ts +141 -220
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +64 -34
- 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 +23 -22
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +335 -30
- package/extensions/plan/web-page.ts +110 -30
- package/extensions/plan/web-router.ts +6 -4
- package/extensions/plan/web.ts +41 -10
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +12 -11
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +1 -1
- package/extensions/rewind/format.ts +29 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/render.ts +62 -47
- package/extensions/todo/README.md +2 -2
- package/extensions/todo/format.ts +31 -10
- package/extensions/todo/tools.ts +5 -4
- package/extensions/todo/tui.ts +45 -26
- package/extensions/watch/README.md +2 -2
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +1 -1
- package/extensions/watch/tools.ts +12 -7
- package/extensions/watch/tui.ts +135 -194
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +2 -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/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +10 -7
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +9 -30
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +56 -3
- package/lib/tui/gauge.ts +78 -0
- package/lib/tui/index.ts +25 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +109 -0
- package/lib/tui/rules.ts +88 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +58 -0
- package/package.json +2 -2
- /package/lib/{ui.ts → tui/glyphs.ts} +0 -0
|
@@ -5,33 +5,29 @@
|
|
|
5
5
|
* The component is derived: it rebuilds the merged model from the picker
|
|
6
6
|
* runtime and the current reports on every render, keeps the focused row's key
|
|
7
7
|
* across rebuilds, and requests a repaint only after an input changes state.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The screen is drawn from the design system — a label-first `titleRule`, one
|
|
9
|
+
* `listRow` grammar, one `checkbox` — and its scroll math from
|
|
10
|
+
* `lib/tui/cursor.ts`. A row without a report renders no glyph and no summary,
|
|
11
|
+
* never a false `ok`.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
Key,
|
|
15
|
-
matchesKey,
|
|
16
|
-
truncateToWidth,
|
|
17
|
-
visibleWidth,
|
|
18
|
-
type Component,
|
|
19
|
-
type TuiMouseEvent,
|
|
20
|
-
type TuiMouseEventResult,
|
|
21
|
-
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
22
16
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
selectionMarker,
|
|
30
|
-
wheelDelta,
|
|
31
|
-
} from "../../../lib/list-cursor.ts";
|
|
17
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../../lib/tui/chrome.ts";
|
|
18
|
+
import { clip, fitCells } from "../../../lib/tui/columns.ts";
|
|
19
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navIntent, wheelDelta } from "../../../lib/tui/cursor.ts";
|
|
20
|
+
import { checkbox, emptyState, listRow } from "../../../lib/tui/rows.ts";
|
|
21
|
+
import { titleRule } from "../../../lib/tui/rules.ts";
|
|
22
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
32
23
|
import type { ExtensionReport } from "../../../lib/requirements.ts";
|
|
33
|
-
import {
|
|
34
|
-
|
|
24
|
+
import {
|
|
25
|
+
detailLines as reportDetailLines,
|
|
26
|
+
reportSeverity,
|
|
27
|
+
reportSummary,
|
|
28
|
+
severityGlyphChar,
|
|
29
|
+
severityRole,
|
|
30
|
+
} from "../doctor/format.ts";
|
|
35
31
|
import type { ExtensionsPickerRuntime } from "../picker/runtime.ts";
|
|
36
32
|
import { summarySegments } from "./format.ts";
|
|
37
33
|
import { buildPanelModel } from "./join.ts";
|
|
@@ -283,50 +279,37 @@ export class PanelComponent implements Component {
|
|
|
283
279
|
return { handled: true };
|
|
284
280
|
}
|
|
285
281
|
|
|
282
|
+
/** The headline: whole segments only, so a narrow screen drops a count. */
|
|
286
283
|
private summary(width: number, model: PanelModel): string {
|
|
287
284
|
const segments = summarySegments(model, this.options.runtime.scope(), {
|
|
288
285
|
filter: this.options.filter,
|
|
289
286
|
compact: width < 72,
|
|
290
287
|
});
|
|
291
|
-
const separator = this.theme
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const styled = this.theme.fg(segment.color, segment.text);
|
|
295
|
-
if (kept.length > 0 && visibleWidth(` ${[...kept, styled].join(separator)}`) > width) break;
|
|
296
|
-
kept.push(styled);
|
|
297
|
-
}
|
|
298
|
-
return truncateToWidth(` ${kept.join(separator)}`, width, "…");
|
|
288
|
+
const separator = paint(this.theme, "faint", " · ");
|
|
289
|
+
const styled = segments.map((segment) => paint(this.theme, segment.color, segment.text));
|
|
290
|
+
return clip(` ${fitCells(styled, Math.max(1, width - 2), separator)}`, width);
|
|
299
291
|
}
|
|
300
292
|
|
|
301
293
|
private groupLine(entry: Extract<PanelEntry, { kind: "group" }>, width: number): string {
|
|
302
|
-
const label = this.theme
|
|
303
|
-
const count = this.theme
|
|
304
|
-
return
|
|
294
|
+
const label = paint(this.theme, "meta", sanitizeLine(entry.label));
|
|
295
|
+
const count = paint(this.theme, "faint", `(${entry.count})`);
|
|
296
|
+
return clip(` ${label} ${count}`, width);
|
|
305
297
|
}
|
|
306
298
|
|
|
307
299
|
private rowLine(row: PanelRow, selected: boolean, width: number): string {
|
|
308
|
-
const checkbox = row.item
|
|
309
|
-
? row.item.enabled
|
|
310
|
-
? this.theme.fg("success", "[x]")
|
|
311
|
-
: this.theme.fg("dim", "[ ]")
|
|
312
|
-
: this.theme.fg("dim", " ");
|
|
313
300
|
const severity = row.report ? reportSeverity(row.report) : undefined;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
: row.item
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const name = truncateToWidth(styled, available, "…");
|
|
327
|
-
const pad = " ".repeat(Math.max(0, available - visibleWidth(name)));
|
|
328
|
-
const tone = severity === "missing" ? "error" : severity === "degraded" ? "warning" : "dim";
|
|
329
|
-
return truncateToWidth(`${head}${name}${pad} ${this.theme.fg(tone, summary)}`, width, "…");
|
|
301
|
+
return listRow(this.theme, width, {
|
|
302
|
+
selected,
|
|
303
|
+
// A reported-only row has no enable pattern, but still reserves the box
|
|
304
|
+
// column so the list stays aligned.
|
|
305
|
+
leading: row.item ? checkbox(this.theme, row.item.enabled) : " ",
|
|
306
|
+
glyph: severity ? severityGlyphChar(severity) : " ",
|
|
307
|
+
glyphRole: severity ? severityRole(severity) : "faint",
|
|
308
|
+
label: sanitizeLine(row.label),
|
|
309
|
+
labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
|
|
310
|
+
trailing: row.report ? reportSummary(row.report) : undefined,
|
|
311
|
+
trailingRole: severity === "missing" ? "bad" : severity === "degraded" ? "warn" : "faint",
|
|
312
|
+
});
|
|
330
313
|
}
|
|
331
314
|
|
|
332
315
|
/** The focused row's label, findings, and the pending-changes note. */
|
|
@@ -334,19 +317,14 @@ export class PanelComponent implements Component {
|
|
|
334
317
|
const lines: string[] = [];
|
|
335
318
|
if (row?.report) {
|
|
336
319
|
const tail = reportSummary(row.report);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
` ${this.theme.fg("muted", sanitizeLine(row.label))} ${this.theme.fg("dim", "—")} ${this.theme.fg(row.report.enabled ? "muted" : "warning", tail)}`,
|
|
340
|
-
width,
|
|
341
|
-
"…",
|
|
342
|
-
),
|
|
343
|
-
);
|
|
320
|
+
const head = `${paint(this.theme, "meta", sanitizeLine(row.label))} ${paint(this.theme, "faint", "—")} ${paint(this.theme, row.report.enabled ? "meta" : "warn", tail)}`;
|
|
321
|
+
lines.push(clip(` ${head}`, width));
|
|
344
322
|
if (budget > 0) lines.push(...reportDetailLines(row.report, this.theme, width, budget, this.showDetail));
|
|
345
323
|
} else if (row?.item) {
|
|
346
|
-
lines.push(
|
|
324
|
+
lines.push(clip(` ${paint(this.theme, "meta", sanitizeLine(row.item.path))}`, width));
|
|
347
325
|
}
|
|
348
326
|
if (this.options.runtime.isDirty()) {
|
|
349
|
-
lines.push(
|
|
327
|
+
lines.push(clip(` ${paint(this.theme, "warn", "Saved · reload to apply")}`, width));
|
|
350
328
|
}
|
|
351
329
|
return lines;
|
|
352
330
|
}
|
|
@@ -356,14 +334,13 @@ export class PanelComponent implements Component {
|
|
|
356
334
|
const model = this.model();
|
|
357
335
|
const entries = entriesOf(model);
|
|
358
336
|
const summary = this.summary(w, model);
|
|
337
|
+
const header = titleRule(this.theme, w, "bytefly");
|
|
359
338
|
|
|
360
339
|
if (entries.length === 0) {
|
|
361
340
|
return dockListFrame({
|
|
362
|
-
|
|
363
|
-
width: w,
|
|
364
|
-
title: "bytefly",
|
|
341
|
+
header,
|
|
365
342
|
summary,
|
|
366
|
-
body:
|
|
343
|
+
body: emptyState(this.theme, w, { label: "No extensions found." }),
|
|
367
344
|
hint: screenHint(this.theme, w, ["Esc close"]),
|
|
368
345
|
});
|
|
369
346
|
}
|
|
@@ -394,9 +371,7 @@ export class PanelComponent implements Component {
|
|
|
394
371
|
}
|
|
395
372
|
|
|
396
373
|
return dockListFrame({
|
|
397
|
-
|
|
398
|
-
width: w,
|
|
399
|
-
title: "bytefly",
|
|
374
|
+
header,
|
|
400
375
|
summary,
|
|
401
376
|
body,
|
|
402
377
|
range: this.cursor.clipped(entries.length)
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Tuning for bytefly's turn separator.
|
|
3
3
|
*
|
|
4
4
|
* Edit this file to change how the line between completed turns looks. The
|
|
5
|
-
*
|
|
6
|
-
* theme automatically.
|
|
5
|
+
* glyph and the colors are plain design-system roles, so the line follows the
|
|
6
|
+
* active theme automatically.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { GLYPHS } from "../../../lib/
|
|
9
|
+
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
10
|
+
import type { TuiRole } from "../../../lib/tui/tokens.ts";
|
|
10
11
|
|
|
11
12
|
export const CONFIG = {
|
|
12
13
|
/** Custom entry type. Changing it orphans separators recorded by an older build. */
|
|
@@ -17,8 +18,15 @@ export const CONFIG = {
|
|
|
17
18
|
labelPrefix: "turn",
|
|
18
19
|
/** Spaces on each side of the label. */
|
|
19
20
|
pad: 1,
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
} as const
|
|
21
|
+
/** Role for the dashes. */
|
|
22
|
+
dashRole: "ruleStrong",
|
|
23
|
+
/** Role for the label. */
|
|
24
|
+
labelRole: "meta",
|
|
25
|
+
} as const satisfies {
|
|
26
|
+
customType: string;
|
|
27
|
+
glyph: string;
|
|
28
|
+
labelPrefix: string;
|
|
29
|
+
pad: number;
|
|
30
|
+
dashRole: TuiRole;
|
|
31
|
+
labelRole: TuiRole;
|
|
32
|
+
};
|
|
@@ -1,50 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting for bytefly's turn separator line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The centered `╌╌╌ turn N ╌╌╌` layout comes from the design system's
|
|
5
|
+
* `centeredRule`, so this module only names the turn; the shared rule owns the
|
|
6
|
+
* centering, the degradation, and the colors.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import { centeredRule } from "../../../lib/tui/rules.ts";
|
|
11
11
|
import { CONFIG } from "./config.ts";
|
|
12
|
-
import type { SeparatorParts } from "./types.ts";
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
export function separatorLabel(turn: number): string {
|
|
16
|
-
const pad = " ".repeat(CONFIG.pad);
|
|
17
|
-
return `${pad}${CONFIG.labelPrefix} ${turn}${pad}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Split `width` columns into centered dashes and a label.
|
|
22
|
-
*
|
|
23
|
-
* When the label does not fit, the line degrades to a plain dashed rule so it
|
|
24
|
-
* never overflows the transcript.
|
|
25
|
-
*/
|
|
26
|
-
export function separatorParts(turn: number, width: number): SeparatorParts {
|
|
27
|
-
const target = Math.max(1, Math.floor(width));
|
|
28
|
-
const label = separatorLabel(turn);
|
|
29
|
-
const labelWidth = visibleWidth(label);
|
|
30
|
-
|
|
31
|
-
if (labelWidth >= target) {
|
|
32
|
-
return { left: CONFIG.glyph.repeat(target), label: "", right: "" };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const remaining = target - labelWidth;
|
|
36
|
-
const left = Math.floor(remaining / 2);
|
|
37
|
-
const right = remaining - left;
|
|
38
|
-
return {
|
|
39
|
-
left: CONFIG.glyph.repeat(left),
|
|
40
|
-
label,
|
|
41
|
-
right: CONFIG.glyph.repeat(right),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** One rendered line: colored dashes around a colored label. */
|
|
13
|
+
/** One rendered line: the centered turn rule, colored by role. */
|
|
46
14
|
export function separatorLine(turn: number, width: number, theme: Theme): string[] {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
15
|
+
return [
|
|
16
|
+
centeredRule(theme, width, `${CONFIG.labelPrefix} ${turn}`, {
|
|
17
|
+
glyph: CONFIG.glyph,
|
|
18
|
+
pad: CONFIG.pad,
|
|
19
|
+
fillRole: CONFIG.dashRole,
|
|
20
|
+
labelRole: CONFIG.labelRole,
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
50
23
|
}
|
|
@@ -10,13 +10,3 @@ export interface TurnSeparatorData {
|
|
|
10
10
|
/** 1-based index of the completed user turn this separator follows. */
|
|
11
11
|
turn: number;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
/** A separator line split into its dashes and label. */
|
|
15
|
-
export interface SeparatorParts {
|
|
16
|
-
/** Dashes before the label. */
|
|
17
|
-
left: string;
|
|
18
|
-
/** The centred `turn N` label. */
|
|
19
|
-
label: string;
|
|
20
|
-
/** Dashes after the label. */
|
|
21
|
-
right: string;
|
|
22
|
-
}
|
|
@@ -218,7 +218,7 @@ config defaults, and error listing all derive from it.
|
|
|
218
218
|
| 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
219
|
| State | None persisted. Per-result state lives in the tool-result `details` / `structuredContent`; the renderer reads only that. |
|
|
220
220
|
| 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/
|
|
221
|
+
| 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
222
|
| Config | The `document` section of the shared `bytefly.json` via `loadConfigSection` (`~/.pi/agent`, `<cwd>/.pi`). |
|
|
223
223
|
|
|
224
224
|
## Design notes
|
|
@@ -14,7 +14,8 @@ 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
|
|
|
@@ -23,10 +24,10 @@ export const PREVIEW_LINES = 20;
|
|
|
23
24
|
|
|
24
25
|
/** Render the argument path: accent, `~`-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, "accent", 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);
|
|
@@ -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,12 +36,17 @@ 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
|
-
blocks are syntax-highlighted when the fence names a supported language.
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
blocks are syntax-highlighted when the fence names a supported language.
|
|
46
|
+
Headings get stable anchor ids, so in-document `#anchor` links and a table of
|
|
47
|
+
contents work, and a document with four or more headings gets an **On this
|
|
48
|
+
page** outline. Raw HTML in the source is escaped, never executed, and
|
|
49
|
+
external images are shown as links rather than fetched.
|
|
42
50
|
- **Sortable listings.** The Name / Size / Modified headers sort the current
|
|
43
51
|
directory, directories first, and the choice is remembered per browser.
|
|
44
52
|
- **Copy and wrap.** Copy a file's root-relative path or its contents, and
|
|
@@ -49,9 +57,22 @@ pi -e . # load the whole @gavin-hu/my-pi-agen
|
|
|
49
57
|
- **Quick open.** `/` (or `Ctrl/Cmd+K`, or the sidebar button) opens a fuzzy
|
|
50
58
|
path finder over a capped index of the tree, with keyboard navigation and a
|
|
51
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.
|
|
52
64
|
- **Read-only and loopback-scoped.** No editing, no writing, no LAN exposure.
|
|
53
65
|
- **Git context.** A read-only header chip shows the checked-out branch (or
|
|
54
66
|
detached head) and a dirty marker, and reflows on narrow screens.
|
|
67
|
+
- **Git diff view.** A `● N changes +A −D` chip links to `/diff`, a change
|
|
68
|
+
overview with `git diff --stat` and a per-file status table, and `/diff/<rel>`
|
|
69
|
+
renders one file's unified diff, whose head carries its own `+A −D` and hunk
|
|
70
|
+
count. Changed lines are syntax-highlighted with the
|
|
71
|
+
file's language, and dirty files and their directories carry a marker in the
|
|
72
|
+
listing and the tree. The scope is switchable between `head` / `staged` /
|
|
73
|
+
`worktree`; untracked files show as all-add, an unborn `HEAD` falls back to the
|
|
74
|
+
index, and a non-repository is a plain empty state. Read-only, capped, and
|
|
75
|
+
rendered with the shared diff colors.
|
|
55
76
|
- **No dependencies.** `node:http` plus the inline sprite, CSS, and client
|
|
56
77
|
script.
|
|
57
78
|
|
|
@@ -91,6 +112,8 @@ reflects only this session's server, with no cross-session sharing.
|
|
|
91
112
|
| `/view/<rel>` | tree + file page (text, Markdown, image, binary, or over-size) |
|
|
92
113
|
| `/view/<rel>?raw=1` | raw Markdown/text source instead of the rendered view |
|
|
93
114
|
| `/raw/<rel>` | raw bytes, streamed; downloads and image sources |
|
|
115
|
+
| `/diff[?mode=head\|staged\|worktree]` | changed-file overview with `--stat` |
|
|
116
|
+
| `/diff/<rel>[?mode=…]` | one file's unified diff |
|
|
94
117
|
| `/api/tree?path=<rel>` | JSON children for lazy tree expansion |
|
|
95
118
|
| `/api/files?q=<query>` | JSON fuzzy path matches for the quick-open palette |
|
|
96
119
|
| `/app.js` | the first-party client script |
|
|
@@ -113,7 +136,10 @@ document and the project document (project wins):
|
|
|
113
136
|
"maxDirEntries": 2000, // listed entries before truncation
|
|
114
137
|
"thumbnails": true,
|
|
115
138
|
"markdown": true, // render Markdown files as formatted HTML
|
|
116
|
-
"maxIndexEntries": 20000
|
|
139
|
+
"maxIndexEntries": 20000, // files collected for quick open before truncation
|
|
140
|
+
"gitDiff": true, // offer the read-only diff view
|
|
141
|
+
"diffContext": 3, // unified diff context lines
|
|
142
|
+
"maxDiffLines": 4000 // diff lines rendered before truncation
|
|
117
143
|
}
|
|
118
144
|
```
|
|
119
145
|
|
|
@@ -140,8 +166,11 @@ document and the project document (project wins):
|
|
|
140
166
|
attributes; only the fixed `tok-*` class names reach the page.
|
|
141
167
|
- **Caps.** `maxFileBytes` (inline rendering), `maxTextLines` (rendered lines),
|
|
142
168
|
`maxThumbBytes` (thumbnails), and `maxDirEntries` (listings) bound every read.
|
|
143
|
-
- **Git reads.** The header chip
|
|
144
|
-
|
|
169
|
+
- **Git reads.** The header chip and the diff view run read-only git
|
|
170
|
+
(`rev-parse`, `status --porcelain=v1 -z`, `diff --stat`, `diff -U<n>`) with a
|
|
171
|
+
timeout and a short cache; any failure simply omits the chip or the diff. A
|
|
172
|
+
per-file diff sets `GIT_LITERAL_PATHSPECS=1`, so a filename containing glob
|
|
173
|
+
characters is matched literally.
|
|
145
174
|
- **Read-only.** Nothing is ever written.
|
|
146
175
|
|
|
147
176
|
## Limitations
|
|
@@ -209,8 +238,12 @@ document and the project document (project wins):
|
|
|
209
238
|
the full `[text][id]`, collapsed `[text][]`, and shortcut `[text]` forms, and
|
|
210
239
|
numeric and common named character references are resolved. The Rendered/Raw
|
|
211
240
|
switch is a server-side `?raw=1`
|
|
212
|
-
link, so it works without JavaScript.
|
|
213
|
-
the
|
|
241
|
+
link, so it works without JavaScript. Headings are demoted one level so the
|
|
242
|
+
card title stays the page's only `h1`, and each gets a slug `id` (unique in
|
|
243
|
+
the document); `renderMarkdownDocument` returns the heading list and `html.ts`
|
|
244
|
+
renders an **On this page** disclosure for four or more headings. Set
|
|
245
|
+
`markdown: false` to fall back to the plain-text view. A fenced code block
|
|
246
|
+
whose info string names a supported
|
|
214
247
|
language is highlighted through `lib/highlight.ts` (either an extension or a
|
|
215
248
|
long-form alias, `fenceLanguage`); an absent or unknown language stays plain.
|
|
216
249
|
- **Syntax highlighting.** `lib/highlight.ts` is a first-party, escape-first
|
|
@@ -231,6 +264,16 @@ document and the project document (project wins):
|
|
|
231
264
|
`maxIndexEntries`, and is cached per server for 10 s by `createFileIndex`. The
|
|
232
265
|
palette is a `dialog`/`combobox`/`listbox` with `aria-activedescendant` and
|
|
233
266
|
`Esc` / arrow / `Enter` handling.
|
|
267
|
+
- **Diff view.** `git.ts` extends the read-only provider set with a TTL-cached
|
|
268
|
+
`git status --porcelain=v1 -z` and a per-mode `git diff`; `diff.ts` parses the
|
|
269
|
+
unified diff and the porcelain records into plain view models, so `router.ts`
|
|
270
|
+
and `html.ts` stay pure. `head` compares against `HEAD` (falling back to
|
|
271
|
+
`--cached` before the first commit), `staged` against the index, and
|
|
272
|
+
`worktree` against the index from the other side. An untracked file renders as
|
|
273
|
+
all-add through `readFileView`, so binary and over-size handling is shared
|
|
274
|
+
with the file view. `git diff --shortstat` supplies the chip totals and
|
|
275
|
+
`changeDirs` derives the directory markers; the changed lines are highlighted
|
|
276
|
+
per hunk by rebuilding the old and new sides for `lib/highlight`.
|
|
234
277
|
- **Streaming responses.** The server streams `filePath` responses and
|
|
235
278
|
suppresses bodies for `HEAD`, so large `/raw` files are not buffered.
|
|
236
279
|
- **Root confinement over feature breadth.** The root is deliberately confined
|
|
@@ -248,6 +291,7 @@ document and the project document (project wins):
|
|
|
248
291
|
| [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer for the code view (shared with `plan`) |
|
|
249
292
|
| [`../../lib/web/index.ts`](../../lib/web/index.ts) | The shared Bytefly web brand system: tokens, base, components, layout, runtime, and markup |
|
|
250
293
|
| `files.ts` | Classification (incl. Markdown), listings, text reads, sidebar tree |
|
|
294
|
+
| `diff.ts` | Pure unified-diff and porcelain-status parsing for the diff view |
|
|
251
295
|
| `fuzzy.ts` | Fuzzy path matching and ranking for the quick-open palette |
|
|
252
296
|
| `markdown.ts` | First-party escape-first Markdown-to-HTML renderer (fenced code highlighted) |
|
|
253
297
|
| `git.ts` | Read-only branch/dirty provider for the header chip |
|
|
@@ -269,8 +313,12 @@ highlighting, safety, and path resolution),
|
|
|
269
313
|
the icon mapping and sprite, HTML escaping/encoding/rendering, the client script
|
|
270
314
|
and its serving, the platform browser commands with an injected spawn, the router
|
|
271
315
|
(routes, `/api/tree`, `/api/files`, `?raw=1`, status codes, `Host`, `HEAD`, `/raw`
|
|
272
|
-
headers)
|
|
316
|
+
headers, and the `/diff` view routes and mode validation)
|
|
273
317
|
without a socket, a real `127.0.0.1:0` server, the session-port lifecycle (fixed
|
|
274
318
|
on first start, reused, and re-rolled after a conflict), the git status provider
|
|
275
|
-
(branch, dirty, detached, non-repo, failure, and TTL cache),
|
|
319
|
+
(branch, dirty, detached, non-repo, failure, and TTL cache), the git diff parser
|
|
320
|
+
and provider (line kinds, line numbers, status and rename parsing, mode
|
|
321
|
+
filtering, untracked all-add, unborn-HEAD fallback, literal pathspec, and TTL
|
|
322
|
+
cache), the diff renderers (highlighted changed lines, marker sets, and chip
|
|
323
|
+
totals), and the command
|
|
276
324
|
lifecycle with injected seams.
|