@adeildo/pi-look 5.9.2 → 5.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 +22 -8
- package/assets/preview.png +0 -0
- package/package.json +2 -2
- package/src/data/git.ts +12 -9
- package/src/data/github.ts +141 -0
- package/src/data/runner.ts +25 -0
- package/src/data/snapshot.ts +6 -3
- package/src/data/start-notes.ts +108 -0
- package/src/data/telemetry.ts +6 -1
- package/src/data/totals.ts +12 -11
- package/src/data/visits.ts +38 -0
- package/src/index.ts +147 -71
- package/src/render/fit.ts +2 -1
- package/src/render/format.ts +9 -12
- package/src/render/glyphs.ts +14 -0
- package/src/render/segments.ts +30 -31
- package/src/render/streaming.ts +217 -0
- package/src/render/tools.ts +76 -12
- package/src/settings.ts +61 -210
- package/src/ui/editor.ts +8 -2
- package/src/ui/footer.ts +64 -5
- package/src/ui/header.ts +175 -71
- package/src/ui/outputs.ts +173 -0
- package/src/ui/pi-entries.ts +185 -0
- package/src/ui/previews.ts +76 -0
- package/src/ui/queue.ts +75 -0
- package/src/ui/screen.ts +8 -0
- package/src/ui/tool-frame.ts +118 -39
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// While the model writes an edit or a write, pi shows the path, or the first lines of the file,
|
|
6
|
+
// which stopped changing long ago. These show the end instead: the lines arriving, with a cursor
|
|
7
|
+
// where the model is writing, and what it has written so far on the bottom rule.
|
|
8
|
+
import { GLYPH, isObject } from "@adeildo/pi-kit";
|
|
9
|
+
import { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
import { tabs } from "./format.ts";
|
|
12
|
+
|
|
13
|
+
/** What the frame shows inside, and the counts it adds to the bottom rule. */
|
|
14
|
+
export interface Stream {
|
|
15
|
+
lines: string[];
|
|
16
|
+
progress: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StreamContext {
|
|
20
|
+
cwd: string;
|
|
21
|
+
expanded: boolean;
|
|
22
|
+
/** Shared by every render of one call, which keeps what was read from disk. */
|
|
23
|
+
state: { streamTarget?: Target };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Streamer = (args: unknown, theme: Theme, context: StreamContext) => Stream | undefined;
|
|
27
|
+
|
|
28
|
+
/** How many of the last lines a write shows while it streams. */
|
|
29
|
+
const WRITE_TAIL = 8;
|
|
30
|
+
/** How many lines of the diff an edit shows while it streams. */
|
|
31
|
+
const EDIT_TAIL = 10;
|
|
32
|
+
const CURSOR = GLYPH.cursor;
|
|
33
|
+
/** Larger files are not read to count their lines. */
|
|
34
|
+
const COUNT_LIMIT = 4 * 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
export const STREAMERS: Readonly<Record<string, Streamer>> = {
|
|
37
|
+
write: writeStream,
|
|
38
|
+
edit: editStream,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** What a write is about to replace. */
|
|
42
|
+
interface Target {
|
|
43
|
+
path: string;
|
|
44
|
+
lines?: number;
|
|
45
|
+
exists: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeStream(
|
|
49
|
+
args: unknown,
|
|
50
|
+
theme: Theme,
|
|
51
|
+
context: StreamContext,
|
|
52
|
+
): Stream | undefined {
|
|
53
|
+
const path = pathOf(args);
|
|
54
|
+
const content = field(args, "content");
|
|
55
|
+
if (path === undefined || content === undefined) return undefined;
|
|
56
|
+
|
|
57
|
+
const target = targetOf(path, context);
|
|
58
|
+
const header = `${pathLine("write", path, theme)} ${theme.fg("muted", replaces(target))}`;
|
|
59
|
+
const lines = content.split("\n");
|
|
60
|
+
const shown = context.expanded ? lines : lines.slice(-WRITE_TAIL);
|
|
61
|
+
const first = lines.length - shown.length;
|
|
62
|
+
const gutter = String(lines.length).length;
|
|
63
|
+
const lang = getLanguageFromPath(path);
|
|
64
|
+
const painted = lang
|
|
65
|
+
? highlightCode(tabs(shown.join("\n")), lang)
|
|
66
|
+
: shown.map((line) => theme.fg("toolOutput", tabs(line)));
|
|
67
|
+
const body = shown.map(
|
|
68
|
+
(_, index) =>
|
|
69
|
+
`${theme.fg("dim", String(first + index + 1).padStart(gutter))} ${painted[index] ?? ""}`,
|
|
70
|
+
);
|
|
71
|
+
return {
|
|
72
|
+
lines: [header, ...above(first, gutter, theme), ...withCursor(body, theme)],
|
|
73
|
+
progress: [`${lines.length} line${lines.length === 1 ? "" : "s"}`, size(content)],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function editStream(
|
|
78
|
+
args: unknown,
|
|
79
|
+
theme: Theme,
|
|
80
|
+
context: StreamContext,
|
|
81
|
+
): Stream | undefined {
|
|
82
|
+
const path = pathOf(args);
|
|
83
|
+
const edits = editsOf(args);
|
|
84
|
+
if (path === undefined || edits.length === 0) return undefined;
|
|
85
|
+
|
|
86
|
+
let removed = 0;
|
|
87
|
+
let added = 0;
|
|
88
|
+
const diff: string[] = [];
|
|
89
|
+
for (const [index, edit] of edits.entries()) {
|
|
90
|
+
if (index > 0) diff.push(theme.fg("dim", GLYPH.ellipsisAcross));
|
|
91
|
+
// Only the last edit can still be arriving, so only the others have a known end.
|
|
92
|
+
const lines = editLines(edit, index < edits.length - 1);
|
|
93
|
+
for (const line of lines.before) diff.push(theme.fg("toolDiffContext", ` ${tabs(line)}`));
|
|
94
|
+
for (const line of lines.out) diff.push(theme.fg("toolDiffRemoved", `-${tabs(line)}`));
|
|
95
|
+
for (const line of lines.in) diff.push(theme.fg("toolDiffAdded", `+${tabs(line)}`));
|
|
96
|
+
for (const line of lines.after) diff.push(theme.fg("toolDiffContext", ` ${tabs(line)}`));
|
|
97
|
+
removed += lines.out.length;
|
|
98
|
+
added += lines.in.length;
|
|
99
|
+
}
|
|
100
|
+
const shown = context.expanded ? diff : diff.slice(-EDIT_TAIL);
|
|
101
|
+
const hidden = diff.length - shown.length;
|
|
102
|
+
return {
|
|
103
|
+
lines: [pathLine("edit", path, theme), ...above(hidden, 0, theme), ...withCursor(shown, theme)],
|
|
104
|
+
progress: [...(edits.length > 1 ? [`edit ${edits.length}`] : []), `-${removed} +${added}`],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The lines an edit takes out and puts in. The model repeats the lines around the change in both
|
|
110
|
+
* texts, so the lines both start with, and the lines both end with once the edit is complete, are
|
|
111
|
+
* context. A line the new text has not repeated yet counts as taken out until it does.
|
|
112
|
+
*/
|
|
113
|
+
export function editLines(
|
|
114
|
+
edit: PartialEdit,
|
|
115
|
+
complete: boolean,
|
|
116
|
+
): { before: string[]; out: string[]; in: string[]; after: string[] } {
|
|
117
|
+
const old = edit.oldText?.split("\n") ?? [];
|
|
118
|
+
const next = edit.newText?.split("\n") ?? [];
|
|
119
|
+
// The last line of a text still arriving may be cut short.
|
|
120
|
+
const settled = complete ? next.length : next.length - 1;
|
|
121
|
+
let start = 0;
|
|
122
|
+
while (start < Math.min(old.length, settled) && old[start] === next[start]) start++;
|
|
123
|
+
let end = 0;
|
|
124
|
+
if (complete)
|
|
125
|
+
while (
|
|
126
|
+
end < Math.min(old.length, next.length) - start &&
|
|
127
|
+
old[old.length - 1 - end] === next[next.length - 1 - end]
|
|
128
|
+
)
|
|
129
|
+
end++;
|
|
130
|
+
return {
|
|
131
|
+
before: old.slice(0, start),
|
|
132
|
+
out: old.slice(start, old.length - end),
|
|
133
|
+
in: next.slice(start, next.length - end),
|
|
134
|
+
after: old.slice(old.length - end),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The first line pi's own renderer draws, so nothing moves when it takes over. */
|
|
139
|
+
function pathLine(tool: string, path: string, theme: Theme): string {
|
|
140
|
+
const home = homedir();
|
|
141
|
+
const shown = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
142
|
+
return `${theme.fg("toolTitle", theme.bold(tool))} ${theme.fg("accent", shown)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function above(hidden: number, gutter: number, theme: Theme): string[] {
|
|
146
|
+
if (hidden <= 0) return [];
|
|
147
|
+
const lines = `${hidden} line${hidden === 1 ? "" : "s"} above`;
|
|
148
|
+
return [
|
|
149
|
+
`${" ".repeat(Math.max(0, gutter - 1))}${theme.fg("dim", GLYPH.ellipsisUp)} ${theme.fg("muted", lines)}`,
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function withCursor(lines: readonly string[], theme: Theme): string[] {
|
|
154
|
+
if (lines.length === 0) return [];
|
|
155
|
+
return [...lines.slice(0, -1), `${lines.at(-1)}${theme.fg("accent", CURSOR)}`];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function replaces(target: Target): string {
|
|
159
|
+
if (!target.exists) return "new file";
|
|
160
|
+
if (target.lines === undefined) return "replaces a file";
|
|
161
|
+
return `replaces ${target.lines} line${target.lines === 1 ? "" : "s"}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Reads the file once per call, when the path is complete. */
|
|
165
|
+
function targetOf(path: string, context: StreamContext): Target {
|
|
166
|
+
const known = context.state.streamTarget;
|
|
167
|
+
if (known?.path === path) return known;
|
|
168
|
+
const full = resolve(context.cwd, path);
|
|
169
|
+
let target: Target = { path, exists: false };
|
|
170
|
+
try {
|
|
171
|
+
const info = statSync(full);
|
|
172
|
+
target = { path, exists: true };
|
|
173
|
+
if (info.isFile() && info.size <= COUNT_LIMIT) {
|
|
174
|
+
const text = readFileSync(full, "utf8");
|
|
175
|
+
target.lines = text === "" ? 0 : text.replace(/\n$/, "").split("\n").length;
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// A path that is not there is a new file; one that cannot be read is still a file.
|
|
179
|
+
target = { path, exists: (error as NodeJS.ErrnoException).code !== "ENOENT" };
|
|
180
|
+
}
|
|
181
|
+
context.state.streamTarget = target;
|
|
182
|
+
return target;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function size(text: string): string {
|
|
186
|
+
const bytes = Buffer.byteLength(text);
|
|
187
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
188
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
189
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pathOf(args: unknown): string | undefined {
|
|
193
|
+
const path = field(args, "path") ?? field(args, "file_path");
|
|
194
|
+
return path === "" ? undefined : path;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function field(args: unknown, key: string): string | undefined {
|
|
198
|
+
const value = isObject(args) ? args[key] : undefined;
|
|
199
|
+
return typeof value === "string" ? value : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface PartialEdit {
|
|
203
|
+
oldText?: string;
|
|
204
|
+
newText?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The edits written so far. The last one may have its old text and nothing else yet. */
|
|
208
|
+
function editsOf(args: unknown): PartialEdit[] {
|
|
209
|
+
if (!isObject(args)) return [];
|
|
210
|
+
const { edits } = args;
|
|
211
|
+
const list = Array.isArray(edits) ? edits : [args];
|
|
212
|
+
return list.flatMap((edit) => {
|
|
213
|
+
const oldText = field(edit, "oldText");
|
|
214
|
+
const newText = field(edit, "newText");
|
|
215
|
+
return oldText === undefined && newText === undefined ? [] : [{ oldText, newText }];
|
|
216
|
+
});
|
|
217
|
+
}
|
package/src/render/tools.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
// The mark and the name a tool call is drawn with. The mark follows the icon set in the look's
|
|
2
2
|
// settings, like every other glyph on the screen.
|
|
3
|
+
import { GLYPH, SPIN } from "@adeildo/pi-kit";
|
|
4
|
+
|
|
3
5
|
import type { IconSet } from "./glyphs.ts";
|
|
4
6
|
|
|
5
|
-
type ToolKind =
|
|
7
|
+
type ToolKind =
|
|
8
|
+
| "shell"
|
|
9
|
+
| "read"
|
|
10
|
+
| "search"
|
|
11
|
+
| "edit"
|
|
12
|
+
| "web"
|
|
13
|
+
| "agent"
|
|
14
|
+
| "ask"
|
|
15
|
+
| "peers"
|
|
16
|
+
| "memory"
|
|
17
|
+
| "mcp"
|
|
18
|
+
| "other";
|
|
6
19
|
|
|
7
20
|
const ICONS: Record<IconSet, Record<ToolKind, string>> = {
|
|
8
21
|
nerd: {
|
|
@@ -12,26 +25,35 @@ const ICONS: Record<IconSet, Record<ToolKind, string>> = {
|
|
|
12
25
|
edit: "\u{f044}", // nf-fa-pencil
|
|
13
26
|
web: "\u{f0ac}", // nf-fa-globe
|
|
14
27
|
agent: "\u{f06a9}", // nf-md-robot
|
|
28
|
+
ask: "\u{f059}", // nf-fa-circle_question
|
|
29
|
+
peers: "\u{f0c0}", // nf-fa-users
|
|
30
|
+
memory: "\u{f09d1}", // nf-md-brain
|
|
15
31
|
mcp: "\u{f1e6}", // nf-fa-plug
|
|
16
32
|
other: "\u{f013}", // nf-fa-gear
|
|
17
33
|
},
|
|
18
34
|
unicode: {
|
|
19
|
-
shell:
|
|
35
|
+
shell: GLYPH.pointer,
|
|
20
36
|
read: "\u25a4",
|
|
21
|
-
search:
|
|
22
|
-
edit:
|
|
37
|
+
search: GLYPH.search,
|
|
38
|
+
edit: GLYPH.pencil,
|
|
23
39
|
web: "\u25cd",
|
|
24
|
-
agent:
|
|
40
|
+
agent: GLYPH.diamond,
|
|
41
|
+
ask: "?",
|
|
42
|
+
peers: "\u2042",
|
|
43
|
+
memory: GLYPH.diamondEmpty,
|
|
25
44
|
mcp: "\u226b",
|
|
26
|
-
other:
|
|
45
|
+
other: GLYPH.ring,
|
|
27
46
|
},
|
|
28
47
|
ascii: {
|
|
29
48
|
shell: "$",
|
|
30
49
|
read: "=",
|
|
31
|
-
search: "
|
|
50
|
+
search: "/",
|
|
32
51
|
edit: "+",
|
|
33
52
|
web: "@",
|
|
34
53
|
agent: "*",
|
|
54
|
+
ask: "?",
|
|
55
|
+
peers: "&",
|
|
56
|
+
memory: "#",
|
|
35
57
|
mcp: ">",
|
|
36
58
|
other: "-",
|
|
37
59
|
},
|
|
@@ -48,17 +70,48 @@ const MARKS: Record<IconSet, Record<CallPhase, string>> = {
|
|
|
48
70
|
failed: "\u{f00d}", // nf-fa-times
|
|
49
71
|
},
|
|
50
72
|
unicode: {
|
|
51
|
-
writing:
|
|
73
|
+
writing: GLYPH.pencil,
|
|
52
74
|
waiting: "\u29d6",
|
|
53
75
|
running: "\u25f7",
|
|
54
|
-
done:
|
|
55
|
-
failed:
|
|
76
|
+
done: GLYPH.check,
|
|
77
|
+
failed: GLYPH.cross,
|
|
56
78
|
},
|
|
57
79
|
ascii: { writing: "~", waiting: ":", running: "...", done: "ok", failed: "!" },
|
|
58
80
|
};
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
/** A mark that moves while its phase lasts: frames, and how long each stays. */
|
|
83
|
+
interface Motion {
|
|
84
|
+
frames: readonly string[];
|
|
85
|
+
stepMs: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A clock outline for every hour, from twelve round to eleven. */
|
|
89
|
+
const CLOCK = [0xf1456, ...Array.from({ length: 11 }, (_, hour) => 0xf144b + hour)].map((code) =>
|
|
90
|
+
String.fromCodePoint(code),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const MOTIONS: Record<IconSet, Partial<Record<CallPhase, Motion>>> = {
|
|
94
|
+
nerd: {
|
|
95
|
+
// nf-md-pencil_outline and nf-md-pencil: the pencil fills with ink and empties again.
|
|
96
|
+
writing: { frames: ["\u{f0cb6}", "\u{f03eb}"], stepMs: 350 },
|
|
97
|
+
waiting: { frames: ["\u{f251}", "\u{f252}", "\u{f253}"], stepMs: 400 }, // nf-fa-hourglass_start..end
|
|
98
|
+
running: { frames: CLOCK, stepMs: 110 }, // nf-md-clock_time_*_outline
|
|
99
|
+
},
|
|
100
|
+
unicode: {
|
|
101
|
+
writing: { frames: SPIN.pencil, stepMs: 180 },
|
|
102
|
+
waiting: { frames: SPIN.hourglass, stepMs: 500 },
|
|
103
|
+
running: { frames: SPIN.quadrants, stepMs: 150 },
|
|
104
|
+
},
|
|
105
|
+
ascii: {
|
|
106
|
+
running: { frames: SPIN.ascii, stepMs: 150 },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The mark of a phase. A phase that lasts moves, so a call that is still going looks alive. */
|
|
111
|
+
export function stateMark(phase: CallPhase, set: IconSet, now: number = Date.now()): string {
|
|
112
|
+
const motion = MOTIONS[set][phase];
|
|
113
|
+
if (motion === undefined) return MARKS[set][phase];
|
|
114
|
+
return motion.frames[Math.floor(now / motion.stepMs) % motion.frames.length] ?? MARKS[set][phase];
|
|
62
115
|
}
|
|
63
116
|
|
|
64
117
|
export function toolIcon(toolName: string, set: IconSet): string {
|
|
@@ -73,6 +126,7 @@ export function toolLabel(toolName: string): string {
|
|
|
73
126
|
|
|
74
127
|
function toolKind(toolName: string): ToolKind {
|
|
75
128
|
if (toolName.startsWith("mcp__")) return "mcp";
|
|
129
|
+
if (toolName.startsWith("memory_")) return "memory";
|
|
76
130
|
switch (toolName) {
|
|
77
131
|
case "bash":
|
|
78
132
|
case "powershell":
|
|
@@ -94,11 +148,21 @@ function toolKind(toolName: string): ToolKind {
|
|
|
94
148
|
case "fetch":
|
|
95
149
|
case "webfetch":
|
|
96
150
|
case "web_fetch":
|
|
151
|
+
case "fetch_content":
|
|
152
|
+
case "get_search_content":
|
|
153
|
+
case "source_check":
|
|
97
154
|
return "web";
|
|
98
155
|
case "task":
|
|
99
156
|
case "agent":
|
|
100
157
|
case "subagent":
|
|
158
|
+
case "bg_wait":
|
|
101
159
|
return "agent";
|
|
160
|
+
case "ask_questions":
|
|
161
|
+
return "ask";
|
|
162
|
+
case "peers":
|
|
163
|
+
return "peers";
|
|
164
|
+
case "scratchpad":
|
|
165
|
+
return "memory";
|
|
102
166
|
default:
|
|
103
167
|
return "other";
|
|
104
168
|
}
|
package/src/settings.ts
CHANGED
|
@@ -1,244 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
boolean,
|
|
3
|
-
type Control,
|
|
4
|
-
type Decoder,
|
|
5
|
-
integer,
|
|
6
|
-
literal,
|
|
7
|
-
pass,
|
|
8
|
-
problem,
|
|
9
|
-
setting,
|
|
10
|
-
} from "@adeildo/pi-kit";
|
|
1
|
+
import { boolean, literal, setting } from "@adeildo/pi-kit";
|
|
11
2
|
|
|
12
3
|
import type { FrameStyle } from "./render/frame.ts";
|
|
13
4
|
import type { IconMode } from "./render/glyphs.ts";
|
|
14
|
-
import {
|
|
5
|
+
import type { SegmentId } from "./render/segments.ts";
|
|
6
|
+
import { framePreview, iconsPreview, userFramePreview } from "./ui/previews.ts";
|
|
7
|
+
import type { SlotName } from "./ui/screen.ts";
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
// The look has opinions: where each piece of information sits, the separators, the cursor and the
|
|
10
|
+
// start screen are fixed. Only what depends on your terminal or your taste in lines is a setting:
|
|
11
|
+
// the icon set, the frame's line style, and whether your messages are framed. See DESIGN.md.
|
|
17
12
|
|
|
18
|
-
const
|
|
19
|
-
[PERMISSION_STATUS]: "permission mode",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const SECTIONS = ["Editor", "Slots", "Footer", "Start screen", "Working line"];
|
|
13
|
+
export const SECTIONS = ["Appearance"];
|
|
23
14
|
|
|
24
|
-
export type HeaderStyle = "card" | "compact"
|
|
25
|
-
export type SeparatorStyle = "dot" | "bar" | "slash" | "space";
|
|
15
|
+
export type HeaderStyle = "card" | "compact";
|
|
26
16
|
export type CursorStyle = "block" | "bar" | "underline";
|
|
27
17
|
|
|
28
|
-
export const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
export const PERMISSION_MODE: SegmentId = "status:pi-ask-permission:mode";
|
|
19
|
+
|
|
20
|
+
/** The line above the editor: how long the agent has worked, and the last call that finished. */
|
|
21
|
+
export const strip: readonly SegmentId[] = ["elapsed"];
|
|
22
|
+
/** The other side of that line, or a line of its own under it when it does not fit. */
|
|
23
|
+
export const stripRight: readonly SegmentId[] = ["tasks", "peers"];
|
|
24
|
+
/** After the working spinner on the editor's top edge. */
|
|
25
|
+
export const topLeft: readonly SegmentId[] = ["branch", "session"];
|
|
26
|
+
/** Where you are. */
|
|
27
|
+
export const topRight: readonly SegmentId[] = ["path", "host"];
|
|
28
|
+
/** How what you type is handled, and who does the work. */
|
|
29
|
+
export const bottomLeft: readonly SegmentId[] = [PERMISSION_MODE, "model", "effort"];
|
|
30
|
+
/** How full the context is. */
|
|
31
|
+
export const bottomRight: readonly SegmentId[] = ["context"];
|
|
32
|
+
/** The session: cost, tokens, cache, speeds, and what other packages report. */
|
|
33
|
+
export const below: readonly SegmentId[] = ["cost", "tokens", "cache", "average", "statuses"];
|
|
34
|
+
/** The plan's quota: what each window spent, and when it resets. */
|
|
35
|
+
export const belowRight: readonly SegmentId[] = ["quota"];
|
|
36
|
+
|
|
37
|
+
/** Where each piece of the screen comes from. The docs fixture draws with the same map. */
|
|
38
|
+
export const SLOTS: Record<SlotName, readonly SegmentId[]> = {
|
|
39
|
+
strip,
|
|
40
|
+
stripRight,
|
|
41
|
+
topLeft,
|
|
42
|
+
topRight,
|
|
43
|
+
bottomLeft,
|
|
44
|
+
bottomRight,
|
|
45
|
+
below,
|
|
46
|
+
belowRight,
|
|
33
47
|
};
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const value: SegmentId[] = [];
|
|
42
|
-
const unknown: string[] = [];
|
|
43
|
-
for (const entry of input) {
|
|
44
|
-
if (typeof entry === "string" && isSegmentId(entry)) value.push(entry);
|
|
45
|
-
else unknown.push(String(entry));
|
|
46
|
-
}
|
|
47
|
-
const problems =
|
|
48
|
-
unknown.length === 0
|
|
49
|
-
? []
|
|
50
|
-
: [problem(path, `ignored unknown segments: ${unknown.join(", ")}`)];
|
|
51
|
-
return pass(value, problems);
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Any other package's status can have a slot of its own, so the ones already placed are offered too.
|
|
57
|
-
function slotControl(fallback: readonly SegmentId[]): Control {
|
|
58
|
-
const statuses = [PERMISSION_STATUS, ...fallback.filter((id) => id.startsWith("status:"))];
|
|
59
|
-
return {
|
|
60
|
-
type: "list",
|
|
61
|
-
options: [
|
|
62
|
-
...SEGMENT_IDS.map((id) => ({ value: id, description: SEGMENTS[id].describe })),
|
|
63
|
-
...[...new Set(statuses)].map((id) => ({
|
|
64
|
-
value: id,
|
|
65
|
-
label: STATUS_LABELS[id] ?? id.slice("status:".length),
|
|
66
|
-
description: `what ${id.slice("status:".length)} reports`,
|
|
67
|
-
})),
|
|
68
|
-
],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function slot(id: string, label: string, description: string, fallback: readonly SegmentId[]) {
|
|
73
|
-
return setting<SegmentId[]>({
|
|
74
|
-
id: `look.${id}`,
|
|
75
|
-
default: [...fallback],
|
|
76
|
-
decoder: segments(fallback),
|
|
77
|
-
project: true,
|
|
78
|
-
ui: { section: "Slots", label, description, control: slotControl(fallback) },
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const strip = slot(
|
|
83
|
-
"strip",
|
|
84
|
-
"Answer strip",
|
|
85
|
-
"The line above the editor: how long the agent has worked, and the last call that finished.",
|
|
86
|
-
["elapsed", "last"],
|
|
87
|
-
);
|
|
88
|
-
export const stripRight = slot(
|
|
89
|
-
"stripRight",
|
|
90
|
-
"Answer strip, on the right",
|
|
91
|
-
"The other side of that line. When it does not fit beside the strip, it gets a line of its own under it.",
|
|
92
|
-
["tasks", "peers"],
|
|
93
|
-
);
|
|
94
|
-
export const topLeft = slot("frame.topLeft", "Frame, top left", "After the working spinner.", [
|
|
95
|
-
"branch",
|
|
96
|
-
"session",
|
|
97
|
-
]);
|
|
98
|
-
export const topRight = slot("frame.topRight", "Frame, top right", "Where you are.", [
|
|
99
|
-
"path",
|
|
100
|
-
"host",
|
|
101
|
-
]);
|
|
102
|
-
export const PERMISSION_MODE: SegmentId = PERMISSION_STATUS;
|
|
103
|
-
|
|
104
|
-
export const bottomLeft = slot(
|
|
105
|
-
"frame.bottomLeft",
|
|
106
|
-
"Frame, bottom left",
|
|
107
|
-
"How what you type is handled, and who does the work.",
|
|
108
|
-
[PERMISSION_MODE, "model", "effort"],
|
|
109
|
-
);
|
|
110
|
-
export const bottomRight = slot("frame.bottomRight", "Frame, bottom right", "How full it is.", [
|
|
111
|
-
"context",
|
|
112
|
-
]);
|
|
113
|
-
export const below = slot(
|
|
114
|
-
"below",
|
|
115
|
-
"Below the editor",
|
|
116
|
-
"The session: cost, tokens, cache, average speeds, and what other packages report, last because it changes the most.",
|
|
117
|
-
["cost", "tokens", "cache", "average", "statuses"],
|
|
118
|
-
);
|
|
119
|
-
export const belowRight = slot(
|
|
120
|
-
"belowRight",
|
|
121
|
-
"Below the editor, on the right",
|
|
122
|
-
"The plan's quota: what each window spent, and when it resets.",
|
|
123
|
-
["quota"],
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
export const frame = setting<FrameStyle | "off">({
|
|
127
|
-
id: "look.frame.style",
|
|
128
|
-
default: "rounded",
|
|
129
|
-
decoder: literal("rounded", "square", "heavy", "line", "off"),
|
|
130
|
-
project: true,
|
|
131
|
-
ui: {
|
|
132
|
-
section: "Editor",
|
|
133
|
-
label: "Frame",
|
|
134
|
-
description:
|
|
135
|
-
"The box around the editor. `line` keeps pi's two rules, `off` leaves the editor alone and moves the frame slots to the footer.",
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
export const cursor = setting<CursorStyle>({
|
|
140
|
-
id: "look.frame.cursor",
|
|
141
|
-
default: "bar",
|
|
142
|
-
decoder: literal("block", "bar", "underline"),
|
|
143
|
-
ui: {
|
|
144
|
-
section: "Editor",
|
|
145
|
-
label: "Cursor",
|
|
146
|
-
description: "Bar and underline use the terminal's own cursor, so it blinks the way it does.",
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
export const header = setting<HeaderStyle>({
|
|
151
|
-
id: "look.header",
|
|
152
|
-
default: "card",
|
|
153
|
-
decoder: literal("card", "compact", "off"),
|
|
154
|
-
project: true,
|
|
155
|
-
ui: {
|
|
156
|
-
section: "Start screen",
|
|
157
|
-
label: "Start screen",
|
|
158
|
-
description: "The card pi opens with. Off keeps pi's own.",
|
|
159
|
-
},
|
|
160
|
-
});
|
|
49
|
+
export const SEPARATOR = " ";
|
|
50
|
+
export const cursor: CursorStyle = "bar";
|
|
51
|
+
export const header: HeaderStyle = "card";
|
|
52
|
+
export const pathLength = 40;
|
|
53
|
+
export const gaugeCells = 8;
|
|
54
|
+
export const labels = true;
|
|
161
55
|
|
|
162
56
|
export const icons = setting<IconMode>({
|
|
163
57
|
id: "look.icons",
|
|
164
58
|
default: "auto",
|
|
165
59
|
decoder: literal("auto", "nerd", "unicode", "ascii"),
|
|
166
60
|
ui: {
|
|
167
|
-
section: "
|
|
61
|
+
section: "Appearance",
|
|
168
62
|
label: "Icons",
|
|
169
63
|
description: "Auto uses Nerd Font glyphs locally, plain Unicode over SSH.",
|
|
64
|
+
preview: iconsPreview,
|
|
170
65
|
},
|
|
171
66
|
});
|
|
172
67
|
|
|
173
|
-
export const
|
|
174
|
-
id: "look.
|
|
175
|
-
default: "
|
|
176
|
-
decoder: literal("
|
|
177
|
-
project: true,
|
|
178
|
-
ui: { section: "Footer", label: "Separator", description: "Between the pieces of a slot." },
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
export const pathLength = setting({
|
|
182
|
-
id: "look.pathLength",
|
|
183
|
-
default: 40,
|
|
184
|
-
decoder: integer(0, 500),
|
|
185
|
-
project: true,
|
|
186
|
-
ui: {
|
|
187
|
-
section: "Footer",
|
|
188
|
-
label: "Path length",
|
|
189
|
-
description: "Longest the folder may be before it is shortened from the left. 0 keeps it all.",
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
export const gaugeCells = setting({
|
|
194
|
-
id: "look.gaugeCells",
|
|
195
|
-
default: 8,
|
|
196
|
-
decoder: integer(0, 40),
|
|
197
|
-
project: true,
|
|
198
|
-
ui: { section: "Footer", label: "Gauge", description: "Cells in the context gauge. 0 hides it." },
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
export const labels = setting({
|
|
202
|
-
id: "look.labels",
|
|
203
|
-
default: true,
|
|
204
|
-
decoder: boolean,
|
|
68
|
+
export const frame = setting<FrameStyle | "off">({
|
|
69
|
+
id: "look.frame.style",
|
|
70
|
+
default: "rounded",
|
|
71
|
+
decoder: literal("rounded", "square", "heavy", "line", "off"),
|
|
205
72
|
project: true,
|
|
206
73
|
ui: {
|
|
207
|
-
section: "
|
|
208
|
-
label: "
|
|
74
|
+
section: "Appearance",
|
|
75
|
+
label: "Editor frame",
|
|
209
76
|
description:
|
|
210
|
-
"
|
|
77
|
+
"The lines of the box around the editor. `line` keeps pi's two rules, `off` leaves the editor alone and moves what sits on its edges to the footer.",
|
|
78
|
+
preview: framePreview,
|
|
211
79
|
},
|
|
212
80
|
});
|
|
213
81
|
|
|
214
|
-
export const
|
|
215
|
-
id: "look.
|
|
82
|
+
export const userFrame = setting({
|
|
83
|
+
id: "look.userFrame",
|
|
216
84
|
default: true,
|
|
217
85
|
decoder: boolean,
|
|
218
86
|
ui: {
|
|
219
|
-
section: "
|
|
220
|
-
label: "
|
|
87
|
+
section: "Appearance",
|
|
88
|
+
label: "Your messages",
|
|
221
89
|
description:
|
|
222
|
-
"
|
|
90
|
+
"Your messages in a frame like the tool calls', named you, with no blank row above and below the text. Off keeps pi's block.",
|
|
91
|
+
preview: userFramePreview,
|
|
223
92
|
},
|
|
224
93
|
});
|
|
225
94
|
|
|
226
|
-
export const LOOK_SETTINGS = [
|
|
227
|
-
strip,
|
|
228
|
-
stripRight,
|
|
229
|
-
topLeft,
|
|
230
|
-
topRight,
|
|
231
|
-
bottomLeft,
|
|
232
|
-
bottomRight,
|
|
233
|
-
below,
|
|
234
|
-
belowRight,
|
|
235
|
-
frame,
|
|
236
|
-
cursor,
|
|
237
|
-
header,
|
|
238
|
-
icons,
|
|
239
|
-
separator,
|
|
240
|
-
pathLength,
|
|
241
|
-
gaugeCells,
|
|
242
|
-
labels,
|
|
243
|
-
peek,
|
|
244
|
-
];
|
|
95
|
+
export const LOOK_SETTINGS = [icons, frame, userFrame];
|