@huseynsnmz/pi-suite 0.1.0 → 0.2.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/CHANGELOG.md +14 -0
- package/README.md +2 -1
- package/package.json +6 -7
- package/packages/board/package.json +0 -2
- package/packages/board/src/core/rows.mjs +21 -5
- package/packages/board/src/ui/dashboard.ts +36 -20
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/README.md +1 -1
- package/packages/frame/package.json +1 -4
- package/packages/frame/src/data/github.ts +1 -1
- package/packages/frame/src/data/mode.ts +28 -2
- package/packages/frame/src/data/snapshot.ts +1 -1
- package/packages/frame/src/data/start-notes.ts +1 -1
- package/packages/frame/src/data/totals.ts +2 -2
- package/packages/frame/src/data/visits.ts +1 -1
- package/packages/frame/src/index.ts +22 -2
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +55 -2
- package/packages/frame/src/render/fit.ts +1 -1
- package/packages/frame/src/render/format.ts +1 -1
- package/packages/frame/src/render/segments.ts +1 -1
- package/packages/frame/src/render/streaming.ts +1 -1
- package/packages/frame/src/render/tools.ts +1 -1
- package/packages/frame/src/settings.ts +1 -1
- package/packages/frame/src/ui/editor.ts +1 -1
- package/packages/frame/src/ui/effort.ts +1 -1
- package/packages/frame/src/ui/header.ts +1 -1
- package/packages/frame/src/ui/outputs.ts +1 -1
- package/packages/frame/src/ui/pi-entries.ts +28 -6
- package/packages/frame/src/ui/previews.ts +1 -1
- package/packages/frame/src/ui/queue.ts +1 -1
- package/packages/frame/src/ui/todos.ts +1 -1
- package/packages/frame/src/ui/tool-frame.ts +1 -1
- package/packages/frame/src/view/state.ts +1 -1
- package/packages/frame/src/view/transcript.ts +1 -1
- package/packages/frame/src/view/view.ts +1 -1
- package/packages/web/package.json +1 -3
- package/packages/web/src/extract.ts +2 -2
- package/packages/web/src/markdown.ts +122 -0
- package/src/index.ts +1 -1
- package/src/settings.ts +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Every character the harness draws with, by name. Nerd Font marks live in look's icon sets.
|
|
2
|
+
|
|
3
|
+
/** Marks in running text. */
|
|
4
|
+
export const GLYPH = {
|
|
5
|
+
/** π, pi's own entries and the harness's name. */
|
|
6
|
+
pi: "\u03c0",
|
|
7
|
+
/** ✓ done, picked, passed. */
|
|
8
|
+
check: "\u2713",
|
|
9
|
+
/** ✗ failed. */
|
|
10
|
+
cross: "\u2717",
|
|
11
|
+
/** × close, remove. */
|
|
12
|
+
times: "\u00d7",
|
|
13
|
+
/** ↑ */
|
|
14
|
+
up: "\u2191",
|
|
15
|
+
/** ↓ */
|
|
16
|
+
down: "\u2193",
|
|
17
|
+
/** ← */
|
|
18
|
+
left: "\u2190",
|
|
19
|
+
/** → to whom, from what to what. */
|
|
20
|
+
right: "\u2192",
|
|
21
|
+
/** ▰ ▱ how well an agent fits a request, in the peers tool's bids. */
|
|
22
|
+
barFull: "\u25b0",
|
|
23
|
+
barEmpty: "\u25b1",
|
|
24
|
+
/** ↵ enter. */
|
|
25
|
+
enter: "\u21b5",
|
|
26
|
+
/** ↻ again. */
|
|
27
|
+
again: "\u21bb",
|
|
28
|
+
/** ❯ the selected row. */
|
|
29
|
+
pointer: "\u276f",
|
|
30
|
+
/** › a note on an option, a continuation. */
|
|
31
|
+
chevron: "\u203a",
|
|
32
|
+
/** ▸ a small pointer. */
|
|
33
|
+
triangle: "\u25b8",
|
|
34
|
+
/** ◆ an agent, a task in progress. */
|
|
35
|
+
diamond: "\u25c6",
|
|
36
|
+
/** ◇ an empty diamond: a task not started, a quiet mark. */
|
|
37
|
+
diamondEmpty: "\u25c7",
|
|
38
|
+
/** ◈ the judge. */
|
|
39
|
+
judge: "\u25c8",
|
|
40
|
+
/** ● on. */
|
|
41
|
+
dot: "\u25cf",
|
|
42
|
+
/** ○ off, not answered. */
|
|
43
|
+
ring: "\u25cb",
|
|
44
|
+
/** ◐ half: hearing a room without taking part in it. */
|
|
45
|
+
half: "\u25d0",
|
|
46
|
+
/** ◌ hidden, as tool output pruned from the context. */
|
|
47
|
+
ringDotted: "\u25cc",
|
|
48
|
+
/** ✎ words you typed. */
|
|
49
|
+
pencil: "\u270e",
|
|
50
|
+
/** ⚑ flagged. */
|
|
51
|
+
flag: "\u2691",
|
|
52
|
+
/** ⏵ running. */
|
|
53
|
+
play: "\u23f5",
|
|
54
|
+
/** ⌕ search. */
|
|
55
|
+
search: "\u2315",
|
|
56
|
+
/** ▍ where words arrive. */
|
|
57
|
+
cursor: "\u258d",
|
|
58
|
+
/** ▌ the bar on a selected row. */
|
|
59
|
+
bar: "\u258c",
|
|
60
|
+
/** ⋮ lines hidden above. */
|
|
61
|
+
ellipsisUp: "\u22ee",
|
|
62
|
+
/** ⋯ a gap between parts, as between two edits. */
|
|
63
|
+
ellipsisAcross: "\u22ef",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
/** Frames of the marks that move while something lasts, in order. */
|
|
67
|
+
export const SPIN = {
|
|
68
|
+
/** ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ a dot going round in one cell: someone writing. */
|
|
69
|
+
braille: [
|
|
70
|
+
"\u280b",
|
|
71
|
+
"\u2819",
|
|
72
|
+
"\u2839",
|
|
73
|
+
"\u2838",
|
|
74
|
+
"\u283c",
|
|
75
|
+
"\u2834",
|
|
76
|
+
"\u2826",
|
|
77
|
+
"\u2827",
|
|
78
|
+
"\u2807",
|
|
79
|
+
"\u280f",
|
|
80
|
+
],
|
|
81
|
+
/** ◴ ◷ ◶ ◵ a quarter of a circle going round: a call running. */
|
|
82
|
+
quadrants: ["\u25f4", "\u25f7", "\u25f6", "\u25f5"],
|
|
83
|
+
/** ⧖ ⧗ an hourglass and the same one flowing: a call waiting on you. */
|
|
84
|
+
hourglass: ["\u29d6", "\u29d7"],
|
|
85
|
+
/** | / - \ the same, in ASCII. */
|
|
86
|
+
ascii: ["|", "/", "-", "\\"],
|
|
87
|
+
/** ✎ ✏ ✐ ✏ a pencil that tilts as it writes, in one cell: the model writing a call. */
|
|
88
|
+
pencil: ["\u270e", "\u270f", "\u2710", "\u270f"],
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
/** Three dots rather than `…`, which is one column in every font. */
|
|
92
|
+
export const CUT = "...";
|
|
93
|
+
|
|
94
|
+
/** The lines frames, rules and scroll bars are drawn with. */
|
|
95
|
+
export const LINE = {
|
|
96
|
+
/** ─ */
|
|
97
|
+
h: "\u2500",
|
|
98
|
+
/** │ */
|
|
99
|
+
v: "\u2502",
|
|
100
|
+
/** ┃ the thumb of a scroll bar, and the sides of what is chosen. */
|
|
101
|
+
vHeavy: "\u2503",
|
|
102
|
+
/** ━ */
|
|
103
|
+
hHeavy: "\u2501",
|
|
104
|
+
/** ┏ */
|
|
105
|
+
topLeftHeavy: "\u250f",
|
|
106
|
+
/** ┓ */
|
|
107
|
+
topRightHeavy: "\u2513",
|
|
108
|
+
/** ┗ */
|
|
109
|
+
bottomLeftHeavy: "\u2517",
|
|
110
|
+
/** ┛ */
|
|
111
|
+
bottomRightHeavy: "\u251b",
|
|
112
|
+
/** ┄ a frame around what has not happened yet. */
|
|
113
|
+
hDashed: "\u2504",
|
|
114
|
+
/** ┆ */
|
|
115
|
+
vDashed: "\u2506",
|
|
116
|
+
/** ╭ */
|
|
117
|
+
topLeft: "\u256d",
|
|
118
|
+
/** ╮ */
|
|
119
|
+
topRight: "\u256e",
|
|
120
|
+
/** ╰ */
|
|
121
|
+
bottomLeft: "\u2570",
|
|
122
|
+
/** ╯ */
|
|
123
|
+
bottomRight: "\u256f",
|
|
124
|
+
/** ├ a rule that meets the left side. */
|
|
125
|
+
teeLeft: "\u251c",
|
|
126
|
+
/** ┤ a rule that meets the right side. */
|
|
127
|
+
teeRight: "\u2524",
|
|
128
|
+
/** ┬ a column that meets the top edge. */
|
|
129
|
+
teeTop: "\u252c",
|
|
130
|
+
/** ┴ a column that meets the bottom edge. */
|
|
131
|
+
teeBottom: "\u2534",
|
|
132
|
+
} as const;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ThumbRange {
|
|
2
|
+
/** From the top. */
|
|
3
|
+
start: number;
|
|
4
|
+
/** At least one. */
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Undefined when everything fits. */
|
|
9
|
+
export function thumbRange(total: number, height: number, scroll: number): ThumbRange | undefined {
|
|
10
|
+
if (total <= height) return undefined;
|
|
11
|
+
const size = Math.max(1, Math.round((height * height) / total));
|
|
12
|
+
const start = Math.round((scroll / (total - height)) * (height - size));
|
|
13
|
+
return { start, size };
|
|
14
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// An edit's diff inside the frame, without pi's colored box behind it: the line numbers in a gutter,
|
|
2
2
|
// the sign and the text in the diff colors, and only the changed words on a tinted background.
|
|
3
|
-
import { GLYPH, isObject } from "
|
|
3
|
+
import { GLYPH, isObject } from "../kit/index.ts";
|
|
4
4
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { type Component, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
6
|
-
import { diffWords } from "diff";
|
|
7
6
|
|
|
8
7
|
import { printable } from "./streaming.ts";
|
|
9
8
|
|
|
@@ -102,3 +101,57 @@ function changedWords(
|
|
|
102
101
|
}
|
|
103
102
|
return texts;
|
|
104
103
|
}
|
|
104
|
+
|
|
105
|
+
const TOKENS = /\s+|[\p{L}\p{N}_]+|./gsu;
|
|
106
|
+
/** A pair of very long lines costs this much to compare, and a minified file isn't worth it. */
|
|
107
|
+
const MOST_CELLS = 4_000_000;
|
|
108
|
+
|
|
109
|
+
export interface WordChange {
|
|
110
|
+
value: string;
|
|
111
|
+
added?: boolean;
|
|
112
|
+
removed?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** What changed between two lines, word by word: the longest run of words they share stays. */
|
|
116
|
+
export function diffWords(before: string, after: string): WordChange[] {
|
|
117
|
+
const a = before.match(TOKENS) ?? [];
|
|
118
|
+
const b = after.match(TOKENS) ?? [];
|
|
119
|
+
if ((a.length + 1) * (b.length + 1) > MOST_CELLS) {
|
|
120
|
+
return [
|
|
121
|
+
...(before === "" ? [] : [{ value: before, removed: true }]),
|
|
122
|
+
...(after === "" ? [] : [{ value: after, added: true }]),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
const width = b.length + 1;
|
|
126
|
+
const shared = new Uint32Array((a.length + 1) * width);
|
|
127
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
128
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
129
|
+
shared[i * width + j] =
|
|
130
|
+
a[i] === b[j]
|
|
131
|
+
? (shared[(i + 1) * width + j + 1] ?? 0) + 1
|
|
132
|
+
: Math.max(shared[(i + 1) * width + j] ?? 0, shared[i * width + j + 1] ?? 0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const parts: WordChange[] = [];
|
|
136
|
+
const push = (value: string, change: { added?: boolean; removed?: boolean }) => {
|
|
137
|
+
const last = parts.at(-1);
|
|
138
|
+
if (last !== undefined && last.added === change.added && last.removed === change.removed) last.value += value;
|
|
139
|
+
else parts.push({ value, ...change });
|
|
140
|
+
};
|
|
141
|
+
let i = 0;
|
|
142
|
+
let j = 0;
|
|
143
|
+
while (i < a.length || j < b.length) {
|
|
144
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
145
|
+
push(a[i] as string, {});
|
|
146
|
+
i++;
|
|
147
|
+
j++;
|
|
148
|
+
} else if (j >= b.length || (i < a.length && (shared[(i + 1) * width + j] ?? 0) >= (shared[i * width + j + 1] ?? 0))) {
|
|
149
|
+
push(a[i] as string, { removed: true });
|
|
150
|
+
i++;
|
|
151
|
+
} else {
|
|
152
|
+
push(b[j] as string, { added: true });
|
|
153
|
+
j++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return parts;
|
|
157
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sep } from "node:path";
|
|
2
2
|
|
|
3
|
-
import { formatCost, formatTokens, tildify } from "
|
|
3
|
+
import { formatCost, formatTokens, tildify } from "../kit/index.ts";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
6
6
|
export const CELL = {
|
|
@@ -2,7 +2,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
// While the model writes an edit or a write, these show the lines arriving, with a cursor.
|
|
5
|
-
import { GLYPH, isObject, tildify } from "
|
|
5
|
+
import { GLYPH, isObject, tildify } from "../kit/index.ts";
|
|
6
6
|
import { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
import { tabs } from "./format.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { CUT, GLYPH } from "
|
|
3
|
+
import { CUT, GLYPH } from "../kit/index.ts";
|
|
4
4
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Alt+O: one tool call's whole output in a scrollable frame, newest first; the arrows move between calls.
|
|
2
|
-
import { callTarget, CUT, frame, frameInner, FRAME_Y, GLYPH, type KeyHint } from "
|
|
2
|
+
import { callTarget, CUT, frame, frameInner, FRAME_Y, GLYPH, type KeyHint } from "../kit/index.ts";
|
|
3
3
|
import type { SessionEntry, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
type Component,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pi's own transcript entries in the harness frame instead of a background. Pi has no hook for
|
|
2
2
|
// drawing them, so this wraps each component's render.
|
|
3
|
-
import { CHANGES_CUT, CHANGES_X, entryFrame, type EntryFrame, GLYPH } from "
|
|
3
|
+
import { CHANGES_CUT, CHANGES_X, entryFrame, type EntryFrame, GLYPH } from "../kit/index.ts";
|
|
4
4
|
import {
|
|
5
5
|
BashExecutionComponent,
|
|
6
6
|
BranchSummaryMessageComponent,
|
|
@@ -12,12 +12,20 @@ import {
|
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Container, Spacer, stripTerminalSequences, Text } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
|
+
import { type Mode, modeColor } from "../data/mode.ts";
|
|
15
16
|
import type { IconSet } from "../render/glyphs.ts";
|
|
16
17
|
import { isBlank } from "./tool-frame.ts";
|
|
17
18
|
|
|
18
19
|
/** Undefined leaves pi's entries as they are. `from` names the client a prompt came from. */
|
|
19
20
|
export type EntryPaint = () =>
|
|
20
|
-
| {
|
|
21
|
+
| {
|
|
22
|
+
theme: Theme;
|
|
23
|
+
set: IconSet;
|
|
24
|
+
you: boolean;
|
|
25
|
+
from?: (text: string) => string | undefined;
|
|
26
|
+
/** The mode a message went out in. A live message has no id yet, so it goes by its text. */
|
|
27
|
+
sentIn?: (id: string | undefined, text: string) => Mode | undefined;
|
|
28
|
+
}
|
|
21
29
|
| undefined;
|
|
22
30
|
|
|
23
31
|
const YOU: Readonly<Record<IconSet, string>> = {
|
|
@@ -56,7 +64,7 @@ export function framePiEntries(source: EntryPaint): void {
|
|
|
56
64
|
if (installed) return;
|
|
57
65
|
installed = true;
|
|
58
66
|
wrap(UserMessageComponent, (self, look) =>
|
|
59
|
-
look.you ? youFrame(look, fromOf(self, look)) : undefined,
|
|
67
|
+
look.you ? youFrame(look, fromOf(self, look), sentInOf(self, look)) : undefined,
|
|
60
68
|
);
|
|
61
69
|
wrap(CompactionSummaryMessageComponent, (_self, look) => pi(look, "compacted"));
|
|
62
70
|
wrap(BranchSummaryMessageComponent, (_self, look) => pi(look, "branch summary"));
|
|
@@ -167,10 +175,24 @@ function fromOf(self: object, look: Paint): string | undefined {
|
|
|
167
175
|
return sentFrom.get(self);
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
const sentModes = new WeakMap<object, Mode | undefined>();
|
|
179
|
+
|
|
180
|
+
function sentInOf(self: object, look: Paint): Mode | undefined {
|
|
181
|
+
if (sentModes.has(self)) return sentModes.get(self);
|
|
182
|
+
const { messageId, text } = self as { messageId?: string; text?: string };
|
|
183
|
+
const mode = look.sentIn?.(messageId, text ?? "");
|
|
184
|
+
// A later message with the same text may go out in another mode, so a found mode sticks to this one.
|
|
185
|
+
if (mode !== undefined || messageId !== undefined) sentModes.set(self, mode);
|
|
186
|
+
return mode;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Plan is the same color as a plain message, so the name says it too. */
|
|
190
|
+
function youFrame(look: Paint, from: string | undefined, mode: Mode | undefined): Framing {
|
|
191
|
+
const color = (mode && modeColor(mode)) ?? "accent";
|
|
192
|
+
const named = mode === undefined || mode === "normal" ? "" : ` ${look.theme.fg(color, mode)}`;
|
|
193
|
+
const you = `${look.theme.fg(color, YOU[look.set])} ${look.theme.fg("muted", "you")}${named}`;
|
|
172
194
|
const top = from === undefined ? you : `${you} ${look.theme.fg("dim", `from ${from}`)}`;
|
|
173
|
-
return { top, color
|
|
195
|
+
return { top, color };
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
function pi(look: Paint, what: string): Framing {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// What each look setting changes, drawn under the list in Alt+S.
|
|
2
|
-
import { entryFrame, GLYPH, type Json } from "
|
|
2
|
+
import { entryFrame, GLYPH, type Json } from "../kit/index.ts";
|
|
3
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Messages sent while the agent works, in a dashed frame above the editor. Pi has no hook for
|
|
2
2
|
// drawing them, so this wraps its method.
|
|
3
|
-
import { CUT, drawnByWidth, CHANGES_X, entryFrame } from "
|
|
3
|
+
import { CUT, drawnByWidth, CHANGES_X, entryFrame } from "../kit/index.ts";
|
|
4
4
|
import { InteractiveMode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The agent's checklist: the `todo` tool, a panel above the editor and `/todos`.
|
|
2
|
-
import { CUT, entryFrame, type FeatureScope } from "
|
|
2
|
+
import { CUT, entryFrame, type FeatureScope } from "../kit/index.ts";
|
|
3
3
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { type Component, Text, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { Type } from "typebox";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A viewed session's folded state and scroll position, kept apart from drawing so it tests without a terminal.
|
|
2
|
-
import type { SessionSummary, SourceItem } from "
|
|
2
|
+
import type { SessionSummary, SourceItem } from "../kit/index.ts";
|
|
3
3
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export class ViewState {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Another session's entries, built as pi's interactive mode builds a loaded session.
|
|
2
|
-
import { messageRendererFor, resolveToolRenderers } from "
|
|
2
|
+
import { messageRendererFor, resolveToolRenderers } from "../kit/index.ts";
|
|
3
3
|
import {
|
|
4
4
|
AssistantMessageComponent,
|
|
5
5
|
BashExecutionComponent,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Another session over the whole screen. A read-only source has a line in place of the editor.
|
|
2
2
|
import { keyLabel } from "../render/keys.ts";
|
|
3
|
-
import { entryFrame, type SessionSummary, type SessionSource } from "
|
|
3
|
+
import { entryFrame, type SessionSummary, type SessionSource } from "../kit/index.ts";
|
|
4
4
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import {
|
|
6
6
|
type Component,
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "0.6.0",
|
|
44
|
-
"linkedom": "0.18.12"
|
|
45
|
-
"turndown": "7.2.2"
|
|
44
|
+
"linkedom": "0.18.12"
|
|
46
45
|
},
|
|
47
46
|
"peerDependencies": {
|
|
48
47
|
"@earendil-works/pi-ai": "*",
|
|
@@ -53,7 +52,6 @@
|
|
|
53
52
|
"@earendil-works/pi-ai": "1.5.0",
|
|
54
53
|
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
55
54
|
"@types/node": "26.6.6",
|
|
56
|
-
"@types/turndown": "5.0.6",
|
|
57
55
|
"typebox": "1.4.3",
|
|
58
56
|
"typescript": "7.0.2"
|
|
59
57
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Turns a response into plain Markdown, leaving out what a person wouldn't see: that's where attacks on a reader hide. */
|
|
2
2
|
import { Readability } from "@mozilla/readability";
|
|
3
3
|
import { parseHTML } from "linkedom";
|
|
4
|
-
import
|
|
4
|
+
import { markdownOf } from "./markdown.ts";
|
|
5
5
|
import { clean } from "./sanitize.ts";
|
|
6
6
|
|
|
7
7
|
const NEVER_SHOWN = "script,style,noscript,template,iframe,object,embed,svg,canvas,form,input,textarea,select,button,img,picture,video,audio,meta,link,head";
|
|
@@ -49,7 +49,7 @@ function htmlToMarkdown(html: string, base: string): string {
|
|
|
49
49
|
for (const node of document.querySelectorAll(`body ${NEVER_SHOWN.split(",").join(",body ")},${HIDDEN}`)) node.remove();
|
|
50
50
|
const article = new Readability(document as unknown as Document).parse();
|
|
51
51
|
const content = article?.content ?? document.querySelector("body")?.innerHTML ?? "";
|
|
52
|
-
const markdown =
|
|
52
|
+
const markdown = markdownOf(content);
|
|
53
53
|
return title ? `# ${title}\n\n${markdown}` : markdown;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Page HTML to Markdown. The page is hostile, so this only ever emits text, and escapes what would read as Markdown. */
|
|
2
|
+
import { parseHTML } from "linkedom";
|
|
3
|
+
|
|
4
|
+
interface DomNode {
|
|
5
|
+
nodeType: number;
|
|
6
|
+
tagName?: string;
|
|
7
|
+
textContent: string | null;
|
|
8
|
+
childNodes: Iterable<DomNode>;
|
|
9
|
+
getAttribute?(name: string): string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TEXT = 3;
|
|
13
|
+
const ELEMENT = 1;
|
|
14
|
+
const NEVER = new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link", "iframe", "object", "embed", "svg", "canvas", "form", "input", "textarea", "select", "button", "img", "picture", "video", "audio"]);
|
|
15
|
+
const BLOCK = new Set(["p", "div", "section", "article", "main", "aside", "header", "footer", "nav", "figure", "figcaption", "address", "details", "summary", "dl", "dt", "dd", "center"]);
|
|
16
|
+
|
|
17
|
+
function escaped(text: string): string {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/\\/g, "\\\\")
|
|
20
|
+
.replace(/[*`[\]_]/g, "\\$&")
|
|
21
|
+
.replace(/^(\s*)([-+=>]|#{1,6}(?=\s)|~~~)/, "$1\\$2")
|
|
22
|
+
.replace(/^(\s*\d+)\./, "$1\\.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Text with backticks in it needs a longer run of them around it. */
|
|
26
|
+
function fenceFor(text: string, shortest: number): string {
|
|
27
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((run) => run[0].length));
|
|
28
|
+
return "`".repeat(Math.max(shortest, longest + 1));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inside(node: DomNode): string {
|
|
32
|
+
return [...node.childNodes].map(render).join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function list(node: DomNode, ordered: boolean): string {
|
|
36
|
+
let number = 0;
|
|
37
|
+
const items = [...node.childNodes]
|
|
38
|
+
.filter((child) => child.tagName?.toLowerCase() === "li")
|
|
39
|
+
.map((item) => {
|
|
40
|
+
number++;
|
|
41
|
+
const marker = ordered ? `${number}. ` : "- ";
|
|
42
|
+
const body = inside(item).trim().replace(/\n{3,}/g, "\n\n");
|
|
43
|
+
return marker + body.replace(/\n/g, `\n${" ".repeat(marker.length)}`);
|
|
44
|
+
});
|
|
45
|
+
return `\n\n${items.join("\n")}\n\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function render(node: DomNode): string {
|
|
49
|
+
if (node.nodeType === TEXT) return escaped((node.textContent ?? "").replace(/\s+/g, " "));
|
|
50
|
+
// Comments are never shown, so they don't count as the page's text.
|
|
51
|
+
if (node.nodeType !== ELEMENT) return "";
|
|
52
|
+
const tag = (node.tagName ?? "").toLowerCase();
|
|
53
|
+
if (NEVER.has(tag)) return "";
|
|
54
|
+
const heading = /^h([1-6])$/.exec(tag)?.[1];
|
|
55
|
+
if (heading !== undefined) {
|
|
56
|
+
const text = inside(node).trim();
|
|
57
|
+
return text === "" ? "" : `\n\n${"#".repeat(Number(heading))} ${text.replace(/\s*\n\s*/g, " ")}\n\n`;
|
|
58
|
+
}
|
|
59
|
+
if (BLOCK.has(tag)) return `\n\n${inside(node)}\n\n`;
|
|
60
|
+
switch (tag) {
|
|
61
|
+
case "br":
|
|
62
|
+
return "\n";
|
|
63
|
+
case "hr":
|
|
64
|
+
return "\n\n---\n\n";
|
|
65
|
+
case "ul":
|
|
66
|
+
case "ol":
|
|
67
|
+
return list(node, tag === "ol");
|
|
68
|
+
case "li":
|
|
69
|
+
return `\n- ${inside(node).trim()}\n`;
|
|
70
|
+
case "blockquote": {
|
|
71
|
+
const text = inside(node).trim().replace(/\n{3,}/g, "\n\n");
|
|
72
|
+
return text === "" ? "" : `\n\n${text.replace(/^/gm, "> ")}\n\n`;
|
|
73
|
+
}
|
|
74
|
+
case "pre": {
|
|
75
|
+
const text = (node.textContent ?? "").replace(/\n$/, "");
|
|
76
|
+
const fence = fenceFor(text, 3);
|
|
77
|
+
const code = [...node.childNodes].find((child) => child.tagName?.toLowerCase() === "code");
|
|
78
|
+
const language = /language-([\w+-]+)/.exec(code?.getAttribute?.("class") ?? "")?.[1] ?? "";
|
|
79
|
+
return `\n\n${fence}${language}\n${text}\n${fence}\n\n`;
|
|
80
|
+
}
|
|
81
|
+
case "code": {
|
|
82
|
+
const text = (node.textContent ?? "").replace(/\s+/g, " ");
|
|
83
|
+
if (text.trim() === "") return "";
|
|
84
|
+
const fence = fenceFor(text, 1);
|
|
85
|
+
const pad = text.startsWith("`") || text.endsWith("`") ? " " : "";
|
|
86
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
87
|
+
}
|
|
88
|
+
case "strong":
|
|
89
|
+
case "b":
|
|
90
|
+
case "em":
|
|
91
|
+
case "i": {
|
|
92
|
+
const text = inside(node);
|
|
93
|
+
const mark = tag === "strong" || tag === "b" ? "**" : "_";
|
|
94
|
+
return text.trim() === "" ? text : `${mark}${text.trim()}${mark}`;
|
|
95
|
+
}
|
|
96
|
+
case "a": {
|
|
97
|
+
const text = inside(node).trim().replace(/\s*\n\s*/g, " ");
|
|
98
|
+
const href = node.getAttribute?.("href");
|
|
99
|
+
if (text === "" || !href) return text;
|
|
100
|
+
return `[${text}](${href.replace(/[()\s]/g, encodeURIComponent)})`;
|
|
101
|
+
}
|
|
102
|
+
case "table":
|
|
103
|
+
return `\n\n${[...node.childNodes].map(render).join("").trim()}\n\n`;
|
|
104
|
+
case "thead":
|
|
105
|
+
case "tbody":
|
|
106
|
+
case "tfoot":
|
|
107
|
+
return [...node.childNodes].filter((child) => child.nodeType === ELEMENT).map(render).join("");
|
|
108
|
+
case "tr":
|
|
109
|
+
return `${[...node.childNodes].map((cell) => render(cell).trim()).filter(Boolean).join(" | ")}\n`;
|
|
110
|
+
default:
|
|
111
|
+
return inside(node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function markdownOf(html: string): string {
|
|
116
|
+
const { document } = parseHTML(`<!doctype html><html><body>${html}</body></html>`);
|
|
117
|
+
return render(document.body as unknown as DomNode)
|
|
118
|
+
.replace(/[ \t]*\n[ \t]*(?=\n)/g, "\n")
|
|
119
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
120
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// One package,
|
|
1
|
+
// One package, seven parts. A part that's off isn't even imported, so it adds no tools, prompts or start-up time.
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
package/src/settings.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const PARTS = {
|
|
|
9
9
|
memory: "Memory the agent keeps between sessions",
|
|
10
10
|
frame: "The start card, framed editor, tool boxes, footer and todo list",
|
|
11
11
|
board: "The board of agent sessions",
|
|
12
|
+
commands: "/clear and /security-review",
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
15
|
export type Part = keyof typeof PARTS;
|