@adeildo/pi-look 5.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-look",
3
- "version": "5.4.0",
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.4.0"
41
+ "@adeildo/pi-kit": "5.5.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@earendil-works/pi-ai": "*",
@@ -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
 
@@ -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,10 +1,12 @@
1
1
  import {
2
2
  accountStateOf,
3
3
  createApp,
4
+ currentGoal,
4
5
  defineFeature,
5
6
  drawSkillRefs,
6
7
  type FeatureScope,
7
8
  SKILL_REF,
9
+ type SessionGoal,
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
+ ...goalOf(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 goalOf(scope: FeatureScope): { goal?: SessionGoal } {
327
+ return { goal: currentGoal(scope.events)?.state };
328
+ }
329
+
320
330
  function installEditor(
321
331
  ctx: ExtensionContext,
322
332
  screen: Screen,
@@ -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: "",
@@ -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",
@@ -1,4 +1,4 @@
1
- import type { AccountWindow } from "@adeildo/pi-kit";
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/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 indent = indentOf(this.#screen);
24
- const line = renderSlot(this.#screen, "strip", width - indent * 2);
25
- // Always one line, empty or not: a strip that appears with the first answer moves the editor.
26
- return [line === "" ? "" : `${" ".repeat(indent)}${line}`];
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"