@adeildo/pi-look 5.4.0 → 5.6.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 +1 -1
- package/assets/preview.png +0 -0
- package/package.json +2 -2
- package/src/data/snapshot.ts +6 -1
- package/src/data/totals.ts +10 -0
- package/src/index.ts +10 -0
- package/src/render/fit.ts +6 -0
- package/src/render/glyphs.ts +12 -0
- package/src/render/paint.ts +6 -0
- package/src/render/segments.ts +44 -1
- package/src/settings.ts +7 -0
- package/src/ui/footer.ts +19 -4
- package/src/ui/screen.ts +1 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<!-- docs:look/preview -->
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<img src="https://raw.githubusercontent.com/felipeadeildo/pi-harness/main/packages/look/assets/preview.png" alt="Pi with the look: the start card, then the strip with the stopwatch and the last call, the framed editor with the branch, the model and the context, and below it the cost, the tokens, the cache and the average speeds." width="860">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/felipeadeildo/pi-harness/main/packages/look/assets/preview.png" alt="Pi with the look: the start card, then the strip with the stopwatch and the last call, the task under way with how many wait, are queued and are done, the framed editor with the branch, the model and the context, and below it the cost, the tokens, the cache and the average speeds." width="860">
|
|
19
19
|
<br>
|
|
20
20
|
<em>The start card, then a prompt halfway through its answer.</em>
|
|
21
21
|
</p>
|
package/assets/preview.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeildo/pi-look",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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.6.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, SessionTasks } 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 doing, when a tasks feature keeps it. */
|
|
53
|
+
tasks: SessionTasks | 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
|
+
tasks: 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
|
+
tasks?: SessionTasks | 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
|
+
tasks: live.tasks,
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
|
package/src/data/totals.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tasksUsageOf } 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") addTasks(totals, tasksUsageOf(entry));
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
return totals;
|
|
@@ -126,6 +128,14 @@ function isRecord(
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// The tasks package's model calls are not pi's, so pi does not count them. They count here.
|
|
132
|
+
function addTasks(totals: Totals, usage: ReturnType<typeof tasksUsageOf>): 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,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accountStateOf,
|
|
3
3
|
createApp,
|
|
4
|
+
currentTasks,
|
|
4
5
|
defineFeature,
|
|
5
6
|
drawSkillRefs,
|
|
6
7
|
type FeatureScope,
|
|
7
8
|
SKILL_REF,
|
|
9
|
+
type SessionTasks,
|
|
8
10
|
skillNames,
|
|
9
11
|
} from "@adeildo/pi-kit";
|
|
10
12
|
import type {
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
SECTIONS,
|
|
41
43
|
SEPARATORS,
|
|
42
44
|
strip,
|
|
45
|
+
stripRight,
|
|
43
46
|
topLeft,
|
|
44
47
|
topRight,
|
|
45
48
|
} from "./settings.ts";
|
|
@@ -218,6 +221,7 @@ export const look = defineFeature({
|
|
|
218
221
|
|
|
219
222
|
const SLOT_TITLES: Record<SlotName, string> = {
|
|
220
223
|
strip: "Above the editor, this answer",
|
|
224
|
+
stripRight: "Above the editor, on the right",
|
|
221
225
|
topLeft: "Frame, top left",
|
|
222
226
|
topRight: "Frame, top right",
|
|
223
227
|
bottomLeft: "Frame, bottom left",
|
|
@@ -266,6 +270,7 @@ function createScreen(
|
|
|
266
270
|
): Screen {
|
|
267
271
|
const slots: Record<SlotName, typeof strip> = {
|
|
268
272
|
strip,
|
|
273
|
+
stripRight,
|
|
269
274
|
topLeft,
|
|
270
275
|
topRight,
|
|
271
276
|
bottomLeft,
|
|
@@ -287,6 +292,7 @@ function createScreen(
|
|
|
287
292
|
last: telemetry.last(),
|
|
288
293
|
run: telemetry.run(),
|
|
289
294
|
statuses: live.footerData?.getExtensionStatuses() ?? new Map(),
|
|
295
|
+
...tasksOf(scope),
|
|
290
296
|
});
|
|
291
297
|
live.frame = snapshot;
|
|
292
298
|
queueMicrotask(() => {
|
|
@@ -317,6 +323,10 @@ function createScreen(
|
|
|
317
323
|
};
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
function tasksOf(scope: FeatureScope): { tasks?: SessionTasks } {
|
|
327
|
+
return { tasks: currentTasks(scope.events)?.state };
|
|
328
|
+
}
|
|
329
|
+
|
|
320
330
|
function installEditor(
|
|
321
331
|
ctx: ExtensionContext,
|
|
322
332
|
screen: Screen,
|
package/src/render/fit.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface Piece {
|
|
|
4
4
|
text: string;
|
|
5
5
|
compact?: string | undefined;
|
|
6
6
|
priority: number;
|
|
7
|
+
/** How the piece fits in less room than its compact form, when it is alone on the line. */
|
|
8
|
+
shrink?: (width: number) => string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface FitOptions {
|
|
@@ -45,6 +47,10 @@ export function fitRegions(
|
|
|
45
47
|
const joined = join(regions.length, slots, options.separator);
|
|
46
48
|
if (measure() <= width) return joined;
|
|
47
49
|
|
|
50
|
+
const kept = slots.filter((slot) => !slot.dropped);
|
|
51
|
+
const alone = regions.length === 1 ? kept[0]?.piece.shrink : undefined;
|
|
52
|
+
if (kept.length === 1 && alone !== undefined) return [alone(width)];
|
|
53
|
+
|
|
48
54
|
const overhead = options.regionOverhead ?? 0;
|
|
49
55
|
return joined.map((text) =>
|
|
50
56
|
text === ""
|
package/src/render/glyphs.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface Glyphs {
|
|
|
7
7
|
branch: string;
|
|
8
8
|
host: string;
|
|
9
9
|
session: string;
|
|
10
|
+
tasks: string;
|
|
11
|
+
waiting: string;
|
|
12
|
+
queued: string;
|
|
10
13
|
version: string;
|
|
11
14
|
model: string;
|
|
12
15
|
effort: string;
|
|
@@ -53,6 +56,9 @@ export const NERD: Glyphs = {
|
|
|
53
56
|
branch: "\u{e0a0}", // nf-pl-branch
|
|
54
57
|
host: "\u{f108}", // nf-fa-desktop
|
|
55
58
|
session: "\u{f02b}", // nf-fa-tag
|
|
59
|
+
tasks: "\u{f04fe}", // nf-md-target
|
|
60
|
+
waiting: "\u{f024}", // nf-fa-flag
|
|
61
|
+
queued: "\u{f051f}", // nf-md-timer_sand
|
|
56
62
|
version: "π",
|
|
57
63
|
model: "\u{f06a9}", // nf-md-robot
|
|
58
64
|
effort: "\u{f09d1}", // nf-md-brain
|
|
@@ -80,6 +86,9 @@ export const UNICODE: Glyphs = {
|
|
|
80
86
|
branch: "⎇",
|
|
81
87
|
host: "⌂",
|
|
82
88
|
session: "#",
|
|
89
|
+
tasks: "▸",
|
|
90
|
+
waiting: "⚑",
|
|
91
|
+
queued: "⧗",
|
|
83
92
|
version: "π",
|
|
84
93
|
model: "◆",
|
|
85
94
|
effort: "✦",
|
|
@@ -106,6 +115,9 @@ export const ASCII: Glyphs = {
|
|
|
106
115
|
branch: "",
|
|
107
116
|
host: "@",
|
|
108
117
|
session: "#",
|
|
118
|
+
tasks: ">",
|
|
119
|
+
waiting: "",
|
|
120
|
+
queued: "",
|
|
109
121
|
version: "v",
|
|
110
122
|
model: "",
|
|
111
123
|
effort: "",
|
package/src/render/paint.ts
CHANGED
|
@@ -11,6 +11,9 @@ export type Role =
|
|
|
11
11
|
| "conflicted"
|
|
12
12
|
| "host"
|
|
13
13
|
| "session"
|
|
14
|
+
| "tasks"
|
|
15
|
+
| "waiting"
|
|
16
|
+
| "queued"
|
|
14
17
|
| "version"
|
|
15
18
|
| "provider"
|
|
16
19
|
| "model"
|
|
@@ -36,6 +39,9 @@ export const ROLE_TOKENS: Record<Role, ThemeColor> = {
|
|
|
36
39
|
conflicted: "error",
|
|
37
40
|
host: "syntaxType",
|
|
38
41
|
session: "mdHeading",
|
|
42
|
+
tasks: "accent",
|
|
43
|
+
waiting: "warning",
|
|
44
|
+
queued: "muted",
|
|
39
45
|
version: "muted",
|
|
40
46
|
provider: "syntaxKeyword",
|
|
41
47
|
model: "text",
|
package/src/render/segments.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AccountWindow, tasksWith } from "@adeildo/pi-kit";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
import type { Snapshot } from "../data/snapshot.ts";
|
|
4
5
|
import { promptOf, tokensOf, type Totals } from "../data/totals.ts";
|
|
@@ -26,6 +27,7 @@ export const SEGMENT_IDS = [
|
|
|
26
27
|
"branch",
|
|
27
28
|
"host",
|
|
28
29
|
"session",
|
|
30
|
+
"tasks",
|
|
29
31
|
"version",
|
|
30
32
|
"clock",
|
|
31
33
|
"provider",
|
|
@@ -64,6 +66,7 @@ export function isSegmentId(value: string): value is SegmentId {
|
|
|
64
66
|
/** Status keys a builtin segment already draws, so the statuses segment skips them. */
|
|
65
67
|
const CLAIMED_BY_SEGMENT: Partial<Record<BuiltinSegment, string>> = {
|
|
66
68
|
model: "pi-providers:account",
|
|
69
|
+
tasks: "pi-tasks",
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
|
|
@@ -152,6 +155,35 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
|
|
|
152
155
|
? undefined
|
|
153
156
|
: { text: `${mark(glyphs.host, paint, "host")}${paint.role("host", snapshot.host)}` },
|
|
154
157
|
},
|
|
158
|
+
tasks: {
|
|
159
|
+
priority: 30,
|
|
160
|
+
describe:
|
|
161
|
+
"what the agent is doing, then ⚑ how many tasks wait on you or on something outside, ⧗ how many are queued and ✓ how many are done, from the tasks package",
|
|
162
|
+
// The counts outlive the task's words: a short line cuts the words.
|
|
163
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
164
|
+
const state = snapshot.tasks;
|
|
165
|
+
if (state === undefined) return undefined;
|
|
166
|
+
const doing = tasksWith(state, "doing");
|
|
167
|
+
const step = doing[0]?.text ?? state.title;
|
|
168
|
+
const more = doing.length > 1 ? paint.dim(` +${doing.length - 1}`) : "";
|
|
169
|
+
const counts = [
|
|
170
|
+
tally(tasksWith(state, "waiting").length, glyphs.waiting, "waiting", paint),
|
|
171
|
+
tally(tasksWith(state, "queued").length, glyphs.queued, "queued", paint),
|
|
172
|
+
tally(tasksWith(state, "done").length, glyphs.done, "done", paint),
|
|
173
|
+
].filter((part) => part !== "");
|
|
174
|
+
if (step === undefined && counts.length === 0) return undefined;
|
|
175
|
+
// Between tasks the line stays, so what was done and what waits do not vanish with them.
|
|
176
|
+
const icon = mark(glyphs.tasks, paint, "tasks");
|
|
177
|
+
const words = (room: number) =>
|
|
178
|
+
step === undefined ? paint.dim("idle") : paint.text(fitWidth(step, room, glyphs.ellipsis));
|
|
179
|
+
const tail = `${more}${counts.length === 0 ? "" : ` ${counts.join(" ")}`}`;
|
|
180
|
+
const fixed = visibleWidth(icon) + visibleWidth(tail);
|
|
181
|
+
return {
|
|
182
|
+
text: `${icon}${words(Number.MAX_SAFE_INTEGER)}${tail}`,
|
|
183
|
+
shrink: (width) => `${icon}${words(Math.max(1, width - fixed))}${tail}`,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
},
|
|
155
187
|
session: {
|
|
156
188
|
priority: 45,
|
|
157
189
|
describe: "the session name, once it has one",
|
|
@@ -488,6 +520,17 @@ function mark(glyph: string, paint: Paint, role: Role): string {
|
|
|
488
520
|
return glyph === "" ? "" : `${paint.role(role, glyph)} `;
|
|
489
521
|
}
|
|
490
522
|
|
|
523
|
+
/** `✓2` in the color of what it counts, like the branch's marks. ASCII icons have no glyph, so the word goes. */
|
|
524
|
+
function tally(
|
|
525
|
+
size: number,
|
|
526
|
+
glyph: string,
|
|
527
|
+
role: "done" | "queued" | "waiting",
|
|
528
|
+
paint: Paint,
|
|
529
|
+
): string {
|
|
530
|
+
if (size === 0) return "";
|
|
531
|
+
return paint.role(role, glyph === "" ? `${size} ${role}` : `${glyph} ${size}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
491
534
|
function counted(value: number, glyph: string, role: Role, paint: Paint): string {
|
|
492
535
|
return value > 0 ? paint.role(role, `${glyph}${value}`) : "";
|
|
493
536
|
}
|
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
|
+
["tasks"],
|
|
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/footer.ts
CHANGED
|
@@ -6,6 +6,8 @@ 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
|
+
/** Room reserved for the strip, so its changing width never moves the right side between lines. */
|
|
10
|
+
const STRIP_ROOM = 56;
|
|
9
11
|
|
|
10
12
|
export function indentOf(screen: Screen): number {
|
|
11
13
|
const style = screen.frameStyle();
|
|
@@ -20,10 +22,23 @@ export class StripComponent implements Component {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
render(width: number): string[] {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const screen = this.#screen;
|
|
26
|
+
const indent = indentOf(screen);
|
|
27
|
+
const room = width - indent * 2;
|
|
28
|
+
const pad = (line: string) => (line === "" ? "" : `${" ".repeat(indent)}${line}`);
|
|
29
|
+
const right = slotPieces(screen, "stripRight", screen.snapshot(), screen.paint());
|
|
30
|
+
// Always one line, so the editor does not jump when the first answer arrives.
|
|
31
|
+
if (right.length === 0 || room <= 0) return [pad(renderSlot(screen, "strip", room))];
|
|
32
|
+
|
|
33
|
+
const full = fitLine(right, Number.MAX_SAFE_INTEGER, fitOptions(screen, screen.paint()));
|
|
34
|
+
const beside = room - visibleWidth(full) - GAP;
|
|
35
|
+
// Beside the strip, the right side stays whole and the strip gives way.
|
|
36
|
+
if (beside >= STRIP_ROOM) {
|
|
37
|
+
const left = renderSlot(screen, "strip", beside);
|
|
38
|
+
const gap = " ".repeat(Math.max(GAP, room - visibleWidth(left) - visibleWidth(full)));
|
|
39
|
+
return [pad(`${left}${gap}${full}`)];
|
|
40
|
+
}
|
|
41
|
+
return [pad(renderSlot(screen, "strip", room)), pad(renderSlot(screen, "stripRight", room))];
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
invalidate(): void {}
|