@adeildo/pi-look 5.3.0 → 5.5.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/package.json +2 -2
- package/src/data/snapshot.ts +6 -1
- package/src/data/totals.ts +10 -0
- package/src/index.ts +27 -1
- package/src/render/glyphs.ts +8 -0
- package/src/render/paint.ts +4 -0
- package/src/render/segments.ts +33 -1
- package/src/settings.ts +7 -0
- package/src/ui/editor.ts +16 -1
- package/src/ui/footer.ts +19 -4
- package/src/ui/screen.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeildo/pi-look",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "Pi's start screen, a framed editor and the footer, redrawn around the data that matters and painted only with the theme's colours.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@adeildo/pi-kit": "5.
|
|
41
|
+
"@adeildo/pi-kit": "5.5.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
package/src/data/snapshot.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
2
|
|
|
3
|
-
import type { AccountState } from "@adeildo/pi-kit";
|
|
3
|
+
import type { AccountState, SessionGoal } from "@adeildo/pi-kit";
|
|
4
4
|
import { VERSION, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
import type { GitState } from "./git.ts";
|
|
@@ -49,6 +49,8 @@ export interface Snapshot {
|
|
|
49
49
|
run: RunView | undefined;
|
|
50
50
|
averages: Averages;
|
|
51
51
|
statuses: ReadonlyMap<string, string>;
|
|
52
|
+
/** What the session is after, when a goal feature keeps it. */
|
|
53
|
+
goal: SessionGoal | undefined;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function emptySnapshot(): Snapshot {
|
|
@@ -74,6 +76,7 @@ export function emptySnapshot(): Snapshot {
|
|
|
74
76
|
run: undefined,
|
|
75
77
|
averages: { decode: undefined, prefill: undefined },
|
|
76
78
|
statuses: new Map(),
|
|
79
|
+
goal: undefined,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -85,6 +88,7 @@ export interface LiveInputs {
|
|
|
85
88
|
last: RequestView | undefined;
|
|
86
89
|
run: RunView | undefined;
|
|
87
90
|
statuses: ReadonlyMap<string, string>;
|
|
91
|
+
goal?: SessionGoal | undefined;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
interface Derived {
|
|
@@ -134,6 +138,7 @@ export class SnapshotReader {
|
|
|
134
138
|
.map(([key, text]): [string, string] => [key, sanitize(text)])
|
|
135
139
|
.filter(([, text]) => text !== ""),
|
|
136
140
|
),
|
|
141
|
+
goal: live.goal,
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
|
package/src/data/totals.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { goalUsageOf } from "@adeildo/pi-kit";
|
|
1
2
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
|
|
3
4
|
export interface Totals {
|
|
@@ -61,6 +62,7 @@ export function totalsOf(entries: readonly SessionEntry[]): Totals {
|
|
|
61
62
|
add(totals, entry.message.usage);
|
|
62
63
|
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage)
|
|
63
64
|
add(totals, entry.usage);
|
|
65
|
+
else if (entry.type === "custom") addGoal(totals, goalUsageOf(entry));
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
return totals;
|
|
@@ -126,6 +128,14 @@ function isRecord(
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// The goal package's model calls are not pi's, so pi does not count them. They count here.
|
|
132
|
+
function addGoal(totals: Totals, usage: ReturnType<typeof goalUsageOf>): void {
|
|
133
|
+
if (usage === undefined) return;
|
|
134
|
+
totals.input += usage.input;
|
|
135
|
+
totals.output += usage.output;
|
|
136
|
+
totals.cost += usage.cost;
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
function add(totals: Totals, usage: UsageLike): void {
|
|
130
140
|
totals.input += usage.input;
|
|
131
141
|
totals.output += usage.output;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
accountStateOf,
|
|
3
|
+
createApp,
|
|
4
|
+
currentGoal,
|
|
5
|
+
defineFeature,
|
|
6
|
+
drawSkillRefs,
|
|
7
|
+
type FeatureScope,
|
|
8
|
+
SKILL_REF,
|
|
9
|
+
type SessionGoal,
|
|
10
|
+
skillNames,
|
|
11
|
+
} from "@adeildo/pi-kit";
|
|
2
12
|
import type {
|
|
3
13
|
ExtensionAPI,
|
|
4
14
|
ExtensionContext,
|
|
@@ -32,6 +42,7 @@ import {
|
|
|
32
42
|
SECTIONS,
|
|
33
43
|
SEPARATORS,
|
|
34
44
|
strip,
|
|
45
|
+
stripRight,
|
|
35
46
|
topLeft,
|
|
36
47
|
topRight,
|
|
37
48
|
} from "./settings.ts";
|
|
@@ -210,6 +221,7 @@ export const look = defineFeature({
|
|
|
210
221
|
|
|
211
222
|
const SLOT_TITLES: Record<SlotName, string> = {
|
|
212
223
|
strip: "Above the editor, this answer",
|
|
224
|
+
stripRight: "Above the editor, on the right",
|
|
213
225
|
topLeft: "Frame, top left",
|
|
214
226
|
topRight: "Frame, top right",
|
|
215
227
|
bottomLeft: "Frame, bottom left",
|
|
@@ -258,6 +270,7 @@ function createScreen(
|
|
|
258
270
|
): Screen {
|
|
259
271
|
const slots: Record<SlotName, typeof strip> = {
|
|
260
272
|
strip,
|
|
273
|
+
stripRight,
|
|
261
274
|
topLeft,
|
|
262
275
|
topRight,
|
|
263
276
|
bottomLeft,
|
|
@@ -279,6 +292,7 @@ function createScreen(
|
|
|
279
292
|
last: telemetry.last(),
|
|
280
293
|
run: telemetry.run(),
|
|
281
294
|
statuses: live.footerData?.getExtensionStatuses() ?? new Map(),
|
|
295
|
+
...goalOf(scope),
|
|
282
296
|
});
|
|
283
297
|
live.frame = snapshot;
|
|
284
298
|
queueMicrotask(() => {
|
|
@@ -298,9 +312,21 @@ function createScreen(
|
|
|
298
312
|
slot: (name) => slots[name].get(scope),
|
|
299
313
|
frameStyle: () => frame.get(scope),
|
|
300
314
|
cursor: () => cursor.get(scope),
|
|
315
|
+
decorate: (line) => {
|
|
316
|
+
const theme = live.ctx?.ui.theme;
|
|
317
|
+
if (theme === undefined) return line;
|
|
318
|
+
return drawSkillRefs(line, skillNames(scope.events), (name) =>
|
|
319
|
+
theme.fg("customMessageLabel", `${SKILL_REF}${name}`),
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
skills: () => skillNames(scope.events).length > 0,
|
|
301
323
|
};
|
|
302
324
|
}
|
|
303
325
|
|
|
326
|
+
function goalOf(scope: FeatureScope): { goal?: SessionGoal } {
|
|
327
|
+
return { goal: currentGoal(scope.events)?.state };
|
|
328
|
+
}
|
|
329
|
+
|
|
304
330
|
function installEditor(
|
|
305
331
|
ctx: ExtensionContext,
|
|
306
332
|
screen: Screen,
|
package/src/render/glyphs.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface Glyphs {
|
|
|
7
7
|
branch: string;
|
|
8
8
|
host: string;
|
|
9
9
|
session: string;
|
|
10
|
+
goal: string;
|
|
11
|
+
later: string;
|
|
10
12
|
version: string;
|
|
11
13
|
model: string;
|
|
12
14
|
effort: string;
|
|
@@ -53,6 +55,8 @@ export const NERD: Glyphs = {
|
|
|
53
55
|
branch: "\u{e0a0}", // nf-pl-branch
|
|
54
56
|
host: "\u{f108}", // nf-fa-desktop
|
|
55
57
|
session: "\u{f02b}", // nf-fa-tag
|
|
58
|
+
goal: "\u{f04fe}", // nf-md-target
|
|
59
|
+
later: "\u{f051f}", // nf-md-timer_sand
|
|
56
60
|
version: "π",
|
|
57
61
|
model: "\u{f06a9}", // nf-md-robot
|
|
58
62
|
effort: "\u{f09d1}", // nf-md-brain
|
|
@@ -80,6 +84,8 @@ export const UNICODE: Glyphs = {
|
|
|
80
84
|
branch: "⎇",
|
|
81
85
|
host: "⌂",
|
|
82
86
|
session: "#",
|
|
87
|
+
goal: "▸",
|
|
88
|
+
later: "⧗",
|
|
83
89
|
version: "π",
|
|
84
90
|
model: "◆",
|
|
85
91
|
effort: "✦",
|
|
@@ -106,6 +112,8 @@ export const ASCII: Glyphs = {
|
|
|
106
112
|
branch: "",
|
|
107
113
|
host: "@",
|
|
108
114
|
session: "#",
|
|
115
|
+
goal: ">",
|
|
116
|
+
later: "",
|
|
109
117
|
version: "v",
|
|
110
118
|
model: "",
|
|
111
119
|
effort: "",
|
package/src/render/paint.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type Role =
|
|
|
11
11
|
| "conflicted"
|
|
12
12
|
| "host"
|
|
13
13
|
| "session"
|
|
14
|
+
| "goal"
|
|
15
|
+
| "later"
|
|
14
16
|
| "version"
|
|
15
17
|
| "provider"
|
|
16
18
|
| "model"
|
|
@@ -36,6 +38,8 @@ export const ROLE_TOKENS: Record<Role, ThemeColor> = {
|
|
|
36
38
|
conflicted: "error",
|
|
37
39
|
host: "syntaxType",
|
|
38
40
|
session: "mdHeading",
|
|
41
|
+
goal: "accent",
|
|
42
|
+
later: "warning",
|
|
39
43
|
version: "muted",
|
|
40
44
|
provider: "syntaxKeyword",
|
|
41
45
|
model: "text",
|
package/src/render/segments.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AccountWindow, itemsWith, nowOf } from "@adeildo/pi-kit";
|
|
2
2
|
|
|
3
3
|
import type { Snapshot } from "../data/snapshot.ts";
|
|
4
4
|
import { promptOf, tokensOf, type Totals } from "../data/totals.ts";
|
|
@@ -26,6 +26,7 @@ export const SEGMENT_IDS = [
|
|
|
26
26
|
"branch",
|
|
27
27
|
"host",
|
|
28
28
|
"session",
|
|
29
|
+
"goal",
|
|
29
30
|
"version",
|
|
30
31
|
"clock",
|
|
31
32
|
"provider",
|
|
@@ -64,6 +65,7 @@ export function isSegmentId(value: string): value is SegmentId {
|
|
|
64
65
|
/** Status keys a builtin segment already draws, so the statuses segment skips them. */
|
|
65
66
|
const CLAIMED_BY_SEGMENT: Partial<Record<BuiltinSegment, string>> = {
|
|
66
67
|
model: "pi-providers:account",
|
|
68
|
+
goal: "pi-goal",
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
|
|
@@ -152,6 +154,30 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
|
|
|
152
154
|
? undefined
|
|
153
155
|
: { text: `${mark(glyphs.host, paint, "host")}${paint.role("host", snapshot.host)}` },
|
|
154
156
|
},
|
|
157
|
+
goal: {
|
|
158
|
+
priority: 30,
|
|
159
|
+
describe:
|
|
160
|
+
"the step the session is on, then ✓ how many are done and ⧗ how many wait for later, from the goal package",
|
|
161
|
+
// No cut of its own: the line cuts it only where the room really ends, and the counts go first.
|
|
162
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
163
|
+
const goal = snapshot.goal;
|
|
164
|
+
if (goal === undefined) return undefined;
|
|
165
|
+
// While the agent works, the step reads in its running form.
|
|
166
|
+
const current = nowOf(goal);
|
|
167
|
+
const running = snapshot.run?.running === true ? current?.activeForm : undefined;
|
|
168
|
+
const step = running ?? current?.text ?? goal.goal;
|
|
169
|
+
const counts = [
|
|
170
|
+
tally(itemsWith(goal, "done").length, glyphs.done, "done", paint),
|
|
171
|
+
tally(itemsWith(goal, "later").length, glyphs.later, "later", paint),
|
|
172
|
+
].filter((part) => part !== "");
|
|
173
|
+
if (step === undefined && counts.length === 0) return undefined;
|
|
174
|
+
// Between steps the line stays, so what was done and what waits do not vanish with them.
|
|
175
|
+
const words = step === undefined ? paint.dim("idle") : paint.text(step);
|
|
176
|
+
const head = `${mark(glyphs.goal, paint, "goal")}${words}`;
|
|
177
|
+
if (counts.length === 0) return { text: head };
|
|
178
|
+
return { text: `${head} ${counts.join(" ")}`, compact: head };
|
|
179
|
+
},
|
|
180
|
+
},
|
|
155
181
|
session: {
|
|
156
182
|
priority: 45,
|
|
157
183
|
describe: "the session name, once it has one",
|
|
@@ -488,6 +514,12 @@ function mark(glyph: string, paint: Paint, role: Role): string {
|
|
|
488
514
|
return glyph === "" ? "" : `${paint.role(role, glyph)} `;
|
|
489
515
|
}
|
|
490
516
|
|
|
517
|
+
/** `✓2` in the color of what it counts, like the branch's marks. ASCII icons have no glyph, so the word goes. */
|
|
518
|
+
function tally(count: number, glyph: string, role: "done" | "later", paint: Paint): string {
|
|
519
|
+
if (count === 0) return "";
|
|
520
|
+
return paint.role(role, glyph === "" ? `${count} ${role}` : `${glyph} ${count}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
491
523
|
function counted(value: number, glyph: string, role: Role, paint: Paint): string {
|
|
492
524
|
return value > 0 ? paint.role(role, `${glyph}${value}`) : "";
|
|
493
525
|
}
|
package/src/settings.ts
CHANGED
|
@@ -85,6 +85,12 @@ export const strip = slot(
|
|
|
85
85
|
"The line above the editor: how long the agent has worked, and the last call that finished.",
|
|
86
86
|
["elapsed", "last"],
|
|
87
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
|
+
["goal"],
|
|
93
|
+
);
|
|
88
94
|
export const topLeft = slot("frame.topLeft", "Frame, top left", "After the working spinner.", [
|
|
89
95
|
"branch",
|
|
90
96
|
"session",
|
|
@@ -219,6 +225,7 @@ export const peek = setting({
|
|
|
219
225
|
|
|
220
226
|
export const LOOK_SETTINGS = [
|
|
221
227
|
strip,
|
|
228
|
+
stripRight,
|
|
222
229
|
topLeft,
|
|
223
230
|
topRight,
|
|
224
231
|
bottomLeft,
|
package/src/ui/editor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { skillQueryAt } from "@adeildo/pi-kit";
|
|
1
2
|
import { CustomEditor, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
type EditorTheme,
|
|
@@ -59,6 +60,14 @@ export class LookEditor extends CustomEditor {
|
|
|
59
60
|
this.tui.requestRender();
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** Pi opens its palette on `/` only at the start of a message. Further in, `/` lists the skills. */
|
|
64
|
+
override handleInput(data: string): void {
|
|
65
|
+
super.handleInput(data);
|
|
66
|
+
if (data !== "/" || this.isShowingAutocomplete() || !this.#screen.skills()) return;
|
|
67
|
+
const { line, col } = this.getCursor();
|
|
68
|
+
if (skillQueryAt(this.getLines(), line, col) !== undefined) openAutocomplete(this);
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
override handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
63
72
|
if (this.#inset === 0) return super.handleMouse(event);
|
|
64
73
|
return super.handleMouse({
|
|
@@ -146,7 +155,7 @@ export class LookEditor extends CustomEditor {
|
|
|
146
155
|
);
|
|
147
156
|
inside = false;
|
|
148
157
|
} else if (inside) {
|
|
149
|
-
const text = cursor === "block" ? line : withoutSoftwareCursor(line);
|
|
158
|
+
const text = this.#screen.decorate(cursor === "block" ? line : withoutSoftwareCursor(line));
|
|
150
159
|
result.push(`${left}${fill(text, inner)}${right}`);
|
|
151
160
|
} else {
|
|
152
161
|
result.push(`${" ".repeat(this.#inset)}${line}`);
|
|
@@ -165,6 +174,12 @@ export class LookEditor extends CustomEditor {
|
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
// The editor keeps the method that opens its list private. Nothing opens if a pi release renames it.
|
|
178
|
+
function openAutocomplete(editor: CustomEditor): void {
|
|
179
|
+
const open: unknown = Reflect.get(editor, "tryTriggerAutocomplete");
|
|
180
|
+
if (typeof open === "function") open.call(editor);
|
|
181
|
+
}
|
|
182
|
+
|
|
168
183
|
/** With the terminal's cursor showing, pi's inverted cell would be a second cursor. */
|
|
169
184
|
function withoutSoftwareCursor(line: string): string {
|
|
170
185
|
return line.replace(INVERTED_CELL, "$1");
|
package/src/ui/footer.ts
CHANGED
|
@@ -6,6 +6,12 @@ import type { Paint } from "../render/paint.ts";
|
|
|
6
6
|
import { type Screen, type SlotName, slotPieces } from "./screen.ts";
|
|
7
7
|
|
|
8
8
|
const GAP = 3;
|
|
9
|
+
/**
|
|
10
|
+
* Room kept for the strip when its right side sits beside it, about what a run shows: the time, the
|
|
11
|
+
* calls, the last call and the speed. The strip changes width every second, so whether the right
|
|
12
|
+
* side fits is measured against this, not against what the strip shows now.
|
|
13
|
+
*/
|
|
14
|
+
const STRIP_ROOM = 56;
|
|
9
15
|
|
|
10
16
|
export function indentOf(screen: Screen): number {
|
|
11
17
|
const style = screen.frameStyle();
|
|
@@ -20,10 +26,19 @@ export class StripComponent implements Component {
|
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
render(width: number): string[] {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const screen = this.#screen;
|
|
30
|
+
const indent = indentOf(screen);
|
|
31
|
+
const room = width - indent * 2;
|
|
32
|
+
const pad = (line: string) => (line === "" ? "" : `${" ".repeat(indent)}${line}`);
|
|
33
|
+
const right = slotPieces(screen, "stripRight", screen.snapshot(), screen.paint());
|
|
34
|
+
// Always one line for the strip, empty or not: one that appears with the first answer moves
|
|
35
|
+
// the editor.
|
|
36
|
+
if (right.length === 0 || room <= 0) return [pad(renderSlot(screen, "strip", room))];
|
|
37
|
+
|
|
38
|
+
const full = fitLine(right, Number.MAX_SAFE_INTEGER, fitOptions(screen, screen.paint()));
|
|
39
|
+
if (visibleWidth(full) + GAP + STRIP_ROOM <= room)
|
|
40
|
+
return [pad(spread(screen, "strip", "stripRight", room))];
|
|
41
|
+
return [pad(renderSlot(screen, "strip", room)), pad(renderSlot(screen, "stripRight", room))];
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
invalidate(): void {}
|
package/src/ui/screen.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { CursorStyle } from "../settings.ts";
|
|
|
8
8
|
|
|
9
9
|
export type SlotName =
|
|
10
10
|
| "strip"
|
|
11
|
+
| "stripRight"
|
|
11
12
|
| "topLeft"
|
|
12
13
|
| "topRight"
|
|
13
14
|
| "bottomLeft"
|
|
@@ -24,6 +25,10 @@ export interface Screen {
|
|
|
24
25
|
slot(name: SlotName): readonly SegmentId[];
|
|
25
26
|
frameStyle(): FrameStyle | "off";
|
|
26
27
|
cursor(): CursorStyle;
|
|
28
|
+
/** A line of what you type, with the skills it names colored. */
|
|
29
|
+
decorate(line: string): string;
|
|
30
|
+
/** True when a skills feature expands `/skill:` references. */
|
|
31
|
+
skills(): boolean;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export function slotPieces(
|