@adeildo/pi-look 5.5.0 → 5.7.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 -6
- package/src/data/totals.ts +4 -4
- package/src/index.ts +5 -5
- package/src/render/fit.ts +6 -0
- package/src/render/glyphs.ts +12 -8
- package/src/render/paint.ts +6 -4
- package/src/render/segments.ts +33 -22
- package/src/settings.ts +1 -1
- package/src/ui/footer.ts +9 -9
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.7.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.7.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,
|
|
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,8 +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
|
|
53
|
-
|
|
52
|
+
/** What the session is doing, when a tasks feature keeps it. */
|
|
53
|
+
tasks: SessionTasks | undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function emptySnapshot(): Snapshot {
|
|
@@ -76,7 +76,7 @@ export function emptySnapshot(): Snapshot {
|
|
|
76
76
|
run: undefined,
|
|
77
77
|
averages: { decode: undefined, prefill: undefined },
|
|
78
78
|
statuses: new Map(),
|
|
79
|
-
|
|
79
|
+
tasks: undefined,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -88,7 +88,7 @@ export interface LiveInputs {
|
|
|
88
88
|
last: RequestView | undefined;
|
|
89
89
|
run: RunView | undefined;
|
|
90
90
|
statuses: ReadonlyMap<string, string>;
|
|
91
|
-
|
|
91
|
+
tasks?: SessionTasks | undefined;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
interface Derived {
|
|
@@ -138,7 +138,7 @@ export class SnapshotReader {
|
|
|
138
138
|
.map(([key, text]): [string, string] => [key, sanitize(text)])
|
|
139
139
|
.filter(([, text]) => text !== ""),
|
|
140
140
|
),
|
|
141
|
-
|
|
141
|
+
tasks: live.tasks,
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
|
package/src/data/totals.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tasksUsageOf } from "@adeildo/pi-kit";
|
|
2
2
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
export interface Totals {
|
|
@@ -62,7 +62,7 @@ export function totalsOf(entries: readonly SessionEntry[]): Totals {
|
|
|
62
62
|
add(totals, entry.message.usage);
|
|
63
63
|
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage)
|
|
64
64
|
add(totals, entry.usage);
|
|
65
|
-
else if (entry.type === "custom")
|
|
65
|
+
else if (entry.type === "custom") addTasks(totals, tasksUsageOf(entry));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return totals;
|
|
@@ -128,8 +128,8 @@ function isRecord(
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// The
|
|
132
|
-
function
|
|
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
133
|
if (usage === undefined) return;
|
|
134
134
|
totals.input += usage.input;
|
|
135
135
|
totals.output += usage.output;
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accountStateOf,
|
|
3
3
|
createApp,
|
|
4
|
-
|
|
4
|
+
currentTasks,
|
|
5
5
|
defineFeature,
|
|
6
6
|
drawSkillRefs,
|
|
7
7
|
type FeatureScope,
|
|
8
8
|
SKILL_REF,
|
|
9
|
-
type
|
|
9
|
+
type SessionTasks,
|
|
10
10
|
skillNames,
|
|
11
11
|
} from "@adeildo/pi-kit";
|
|
12
12
|
import type {
|
|
@@ -292,7 +292,7 @@ function createScreen(
|
|
|
292
292
|
last: telemetry.last(),
|
|
293
293
|
run: telemetry.run(),
|
|
294
294
|
statuses: live.footerData?.getExtensionStatuses() ?? new Map(),
|
|
295
|
-
...
|
|
295
|
+
...tasksOf(scope),
|
|
296
296
|
});
|
|
297
297
|
live.frame = snapshot;
|
|
298
298
|
queueMicrotask(() => {
|
|
@@ -323,8 +323,8 @@ function createScreen(
|
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
function
|
|
327
|
-
return {
|
|
326
|
+
function tasksOf(scope: FeatureScope): { tasks?: SessionTasks } {
|
|
327
|
+
return { tasks: currentTasks(scope.events)?.state };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
function installEditor(
|
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,8 +7,9 @@ export interface Glyphs {
|
|
|
7
7
|
branch: string;
|
|
8
8
|
host: string;
|
|
9
9
|
session: string;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
tasks: string;
|
|
11
|
+
waiting: string;
|
|
12
|
+
queued: string;
|
|
12
13
|
version: string;
|
|
13
14
|
model: string;
|
|
14
15
|
effort: string;
|
|
@@ -55,8 +56,9 @@ export const NERD: Glyphs = {
|
|
|
55
56
|
branch: "\u{e0a0}", // nf-pl-branch
|
|
56
57
|
host: "\u{f108}", // nf-fa-desktop
|
|
57
58
|
session: "\u{f02b}", // nf-fa-tag
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
tasks: "\u{f04fe}", // nf-md-target
|
|
60
|
+
waiting: "\u{f024}", // nf-fa-flag
|
|
61
|
+
queued: "\u{f051f}", // nf-md-timer_sand
|
|
60
62
|
version: "π",
|
|
61
63
|
model: "\u{f06a9}", // nf-md-robot
|
|
62
64
|
effort: "\u{f09d1}", // nf-md-brain
|
|
@@ -84,8 +86,9 @@ export const UNICODE: Glyphs = {
|
|
|
84
86
|
branch: "⎇",
|
|
85
87
|
host: "⌂",
|
|
86
88
|
session: "#",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
tasks: "▸",
|
|
90
|
+
waiting: "⚑",
|
|
91
|
+
queued: "⧗",
|
|
89
92
|
version: "π",
|
|
90
93
|
model: "◆",
|
|
91
94
|
effort: "✦",
|
|
@@ -112,8 +115,9 @@ export const ASCII: Glyphs = {
|
|
|
112
115
|
branch: "",
|
|
113
116
|
host: "@",
|
|
114
117
|
session: "#",
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
tasks: ">",
|
|
119
|
+
waiting: "",
|
|
120
|
+
queued: "",
|
|
117
121
|
version: "v",
|
|
118
122
|
model: "",
|
|
119
123
|
effort: "",
|
package/src/render/paint.ts
CHANGED
|
@@ -11,8 +11,9 @@ export type Role =
|
|
|
11
11
|
| "conflicted"
|
|
12
12
|
| "host"
|
|
13
13
|
| "session"
|
|
14
|
-
| "
|
|
15
|
-
| "
|
|
14
|
+
| "tasks"
|
|
15
|
+
| "waiting"
|
|
16
|
+
| "queued"
|
|
16
17
|
| "version"
|
|
17
18
|
| "provider"
|
|
18
19
|
| "model"
|
|
@@ -38,8 +39,9 @@ export const ROLE_TOKENS: Record<Role, ThemeColor> = {
|
|
|
38
39
|
conflicted: "error",
|
|
39
40
|
host: "syntaxType",
|
|
40
41
|
session: "mdHeading",
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
tasks: "accent",
|
|
43
|
+
waiting: "warning",
|
|
44
|
+
queued: "muted",
|
|
43
45
|
version: "muted",
|
|
44
46
|
provider: "syntaxKeyword",
|
|
45
47
|
model: "text",
|
package/src/render/segments.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type AccountWindow,
|
|
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,7 +27,7 @@ export const SEGMENT_IDS = [
|
|
|
26
27
|
"branch",
|
|
27
28
|
"host",
|
|
28
29
|
"session",
|
|
29
|
-
"
|
|
30
|
+
"tasks",
|
|
30
31
|
"version",
|
|
31
32
|
"clock",
|
|
32
33
|
"provider",
|
|
@@ -65,7 +66,7 @@ export function isSegmentId(value: string): value is SegmentId {
|
|
|
65
66
|
/** Status keys a builtin segment already draws, so the statuses segment skips them. */
|
|
66
67
|
const CLAIMED_BY_SEGMENT: Partial<Record<BuiltinSegment, string>> = {
|
|
67
68
|
model: "pi-providers:account",
|
|
68
|
-
|
|
69
|
+
tasks: "pi-tasks",
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
|
|
@@ -154,28 +155,33 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
|
|
|
154
155
|
? undefined
|
|
155
156
|
: { text: `${mark(glyphs.host, paint, "host")}${paint.role("host", snapshot.host)}` },
|
|
156
157
|
},
|
|
157
|
-
|
|
158
|
+
tasks: {
|
|
158
159
|
priority: 30,
|
|
159
160
|
describe:
|
|
160
|
-
"
|
|
161
|
-
//
|
|
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.
|
|
162
163
|
render: ({ snapshot, glyphs, paint }) => {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const step = running ?? current?.text ?? goal.goal;
|
|
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
169
|
const counts = [
|
|
170
|
-
tally(
|
|
171
|
-
tally(
|
|
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),
|
|
172
173
|
].filter((part) => part !== "");
|
|
173
174
|
if (step === undefined && counts.length === 0) return undefined;
|
|
174
|
-
// Between
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
};
|
|
179
185
|
},
|
|
180
186
|
},
|
|
181
187
|
session: {
|
|
@@ -515,9 +521,14 @@ function mark(glyph: string, paint: Paint, role: Role): string {
|
|
|
515
521
|
}
|
|
516
522
|
|
|
517
523
|
/** `✓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(
|
|
519
|
-
|
|
520
|
-
|
|
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}`);
|
|
521
532
|
}
|
|
522
533
|
|
|
523
534
|
function counted(value: number, glyph: string, role: Role, paint: Paint): string {
|
package/src/settings.ts
CHANGED
|
@@ -89,7 +89,7 @@ export const stripRight = slot(
|
|
|
89
89
|
"stripRight",
|
|
90
90
|
"Answer strip, on the right",
|
|
91
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
|
-
["
|
|
92
|
+
["tasks"],
|
|
93
93
|
);
|
|
94
94
|
export const topLeft = slot("frame.topLeft", "Frame, top left", "After the working spinner.", [
|
|
95
95
|
"branch",
|
package/src/ui/footer.ts
CHANGED
|
@@ -6,11 +6,7 @@ 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
|
-
*/
|
|
9
|
+
/** Room reserved for the strip, so its changing width never moves the right side between lines. */
|
|
14
10
|
const STRIP_ROOM = 56;
|
|
15
11
|
|
|
16
12
|
export function indentOf(screen: Screen): number {
|
|
@@ -31,13 +27,17 @@ export class StripComponent implements Component {
|
|
|
31
27
|
const room = width - indent * 2;
|
|
32
28
|
const pad = (line: string) => (line === "" ? "" : `${" ".repeat(indent)}${line}`);
|
|
33
29
|
const right = slotPieces(screen, "stripRight", screen.snapshot(), screen.paint());
|
|
34
|
-
// Always one line
|
|
35
|
-
// the editor.
|
|
30
|
+
// Always one line, so the editor does not jump when the first answer arrives.
|
|
36
31
|
if (right.length === 0 || room <= 0) return [pad(renderSlot(screen, "strip", room))];
|
|
37
32
|
|
|
38
33
|
const full = fitLine(right, Number.MAX_SAFE_INTEGER, fitOptions(screen, screen.paint()));
|
|
39
|
-
|
|
40
|
-
|
|
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
41
|
return [pad(renderSlot(screen, "strip", room)), pad(renderSlot(screen, "stripRight", room))];
|
|
42
42
|
}
|
|
43
43
|
|