@adeildo/pi-look 5.5.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 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>
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-look",
3
- "version": "5.5.0",
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.5.0"
41
+ "@adeildo/pi-kit": "5.6.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, SessionGoal } 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,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 after, when a goal feature keeps it. */
53
- goal: SessionGoal | undefined;
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
- goal: undefined,
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
- goal?: SessionGoal | undefined;
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
- goal: live.goal,
141
+ tasks: live.tasks,
142
142
  };
143
143
  }
144
144
 
@@ -1,4 +1,4 @@
1
- import { goalUsageOf } from "@adeildo/pi-kit";
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") addGoal(totals, goalUsageOf(entry));
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 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 {
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
- currentGoal,
4
+ currentTasks,
5
5
  defineFeature,
6
6
  drawSkillRefs,
7
7
  type FeatureScope,
8
8
  SKILL_REF,
9
- type SessionGoal,
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
- ...goalOf(scope),
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 goalOf(scope: FeatureScope): { goal?: SessionGoal } {
327
- return { goal: currentGoal(scope.events)?.state };
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 === ""
@@ -7,8 +7,9 @@ export interface Glyphs {
7
7
  branch: string;
8
8
  host: string;
9
9
  session: string;
10
- goal: string;
11
- later: string;
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
- goal: "\u{f04fe}", // nf-md-target
59
- later: "\u{f051f}", // nf-md-timer_sand
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
- goal: "▸",
88
- later: "⧗",
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
- goal: ">",
116
- later: "",
118
+ tasks: ">",
119
+ waiting: "",
120
+ queued: "",
117
121
  version: "v",
118
122
  model: "",
119
123
  effort: "",
@@ -11,8 +11,9 @@ export type Role =
11
11
  | "conflicted"
12
12
  | "host"
13
13
  | "session"
14
- | "goal"
15
- | "later"
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
- goal: "accent",
42
- later: "warning",
42
+ tasks: "accent",
43
+ waiting: "warning",
44
+ queued: "muted",
43
45
  version: "muted",
44
46
  provider: "syntaxKeyword",
45
47
  model: "text",
@@ -1,4 +1,5 @@
1
- import { type AccountWindow, itemsWith, nowOf } from "@adeildo/pi-kit";
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
- "goal",
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
- goal: "pi-goal",
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
- goal: {
158
+ tasks: {
158
159
  priority: 30,
159
160
  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.
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 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;
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(itemsWith(goal, "done").length, glyphs.done, "done", paint),
171
- tally(itemsWith(goal, "later").length, glyphs.later, "later", paint),
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 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 };
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(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}`);
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
- ["goal"],
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 for the strip, empty or not: one that appears with the first answer moves
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
- if (visibleWidth(full) + GAP + STRIP_ROOM <= room)
40
- return [pad(spread(screen, "strip", "stripRight", room))];
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