@adeildo/pi-look 5.7.2 → 5.9.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
@@ -72,3 +72,12 @@ The look never names a colour. Every piece names a theme token, so it follows wh
72
72
  The framed editor, and the idea of writing into its borders, come from [pi-open-tui](https://github.com/OldSuns/pi-open-tui), which this replaces. The segments with priorities come from [oh-my-pi](https://github.com/can1357/oh-my-pi).
73
73
 
74
74
  Part of [pi-harness](https://github.com/felipeadeildo/pi-harness), which brings every piece in one install.
75
+
76
+ ## License
77
+
78
+ MIT
79
+
80
+ <p align="center">
81
+ <a href="https://adeildo.dev"><img src="https://adeildo.dev/assets/images/cat.svg" width="42" height="42" alt="adeildo.dev"></a><br>
82
+ Brought to you by <a href="https://adeildo.dev">adeildo.dev</a>
83
+ </p>
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-look",
3
- "version": "5.7.2",
3
+ "version": "5.9.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.7.2"
41
+ "@adeildo/pi-kit": "5.9.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, SessionTasks } from "@adeildo/pi-kit";
3
+ import type { AccountState, PeerMark, 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";
@@ -51,6 +51,10 @@ export interface Snapshot {
51
51
  statuses: ReadonlyMap<string, string>;
52
52
  /** What the session is doing, when a tasks feature keeps it. */
53
53
  tasks: SessionTasks | undefined;
54
+ /** The other sessions in this folder, when a peers feature keeps them. */
55
+ peers: PeerMark[];
56
+ /** Messages a peer sent this session that its agent has not read yet. */
57
+ peersUnread: number;
54
58
  }
55
59
 
56
60
  export function emptySnapshot(): Snapshot {
@@ -77,6 +81,8 @@ export function emptySnapshot(): Snapshot {
77
81
  averages: { decode: undefined, prefill: undefined },
78
82
  statuses: new Map(),
79
83
  tasks: undefined,
84
+ peers: [],
85
+ peersUnread: 0,
80
86
  };
81
87
  }
82
88
 
@@ -89,6 +95,8 @@ export interface LiveInputs {
89
95
  run: RunView | undefined;
90
96
  statuses: ReadonlyMap<string, string>;
91
97
  tasks?: SessionTasks | undefined;
98
+ peers?: PeerMark[] | undefined;
99
+ peersUnread?: number | undefined;
92
100
  }
93
101
 
94
102
  interface Derived {
@@ -139,6 +147,8 @@ export class SnapshotReader {
139
147
  .filter(([, text]) => text !== ""),
140
148
  ),
141
149
  tasks: live.tasks,
150
+ peers: live.peers ?? [],
151
+ peersUnread: live.peersUnread ?? 0,
142
152
  };
143
153
  }
144
154
 
package/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  accountStateOf,
3
3
  createApp,
4
+ currentPeers,
4
5
  currentTasks,
5
6
  defineFeature,
6
7
  drawSkillRefs,
7
8
  type FeatureScope,
8
9
  SKILL_REF,
9
- type SessionTasks,
10
10
  skillNames,
11
11
  } from "@adeildo/pi-kit";
12
12
  import type {
@@ -17,7 +17,7 @@ import type {
17
17
  import type { TUI } from "@earendil-works/pi-tui";
18
18
 
19
19
  import { type GitState, readGit } from "./data/git.ts";
20
- import { emptySnapshot, type Snapshot, SnapshotReader } from "./data/snapshot.ts";
20
+ import { emptySnapshot, type LiveInputs, type Snapshot, SnapshotReader } from "./data/snapshot.ts";
21
21
  import { Telemetry } from "./data/telemetry.ts";
22
22
  import type { DeltaKind } from "./data/telemetry.ts";
23
23
  import { REQUEST_ENTRY, type RequestRecord } from "./data/totals.ts";
@@ -323,8 +323,13 @@ function createScreen(
323
323
  };
324
324
  }
325
325
 
326
- function tasksOf(scope: FeatureScope): { tasks?: SessionTasks } {
327
- return { tasks: currentTasks(scope.events)?.state };
326
+ function tasksOf(scope: FeatureScope): Pick<LiveInputs, "tasks" | "peers" | "peersUnread"> {
327
+ const peers = currentPeers(scope.events);
328
+ return {
329
+ tasks: currentTasks(scope.events)?.state,
330
+ peers: peers?.peers,
331
+ peersUnread: peers?.unread,
332
+ };
328
333
  }
329
334
 
330
335
  function installEditor(
package/src/render/fit.ts CHANGED
@@ -55,7 +55,7 @@ export function fitRegions(
55
55
  return joined.map((text) =>
56
56
  text === ""
57
57
  ? ""
58
- : truncateToWidth(text, Math.max(0, width - overhead), options.ellipsis ?? "…"),
58
+ : truncateToWidth(text, Math.max(0, width - overhead), options.ellipsis ?? "..."),
59
59
  );
60
60
  }
61
61
 
@@ -72,11 +72,11 @@ export function shortenPath(cwd: string, home: string | undefined, maxWidth: num
72
72
  let kept = "";
73
73
  for (let index = parts.length - 1; index > 0; index--) {
74
74
  const candidate = `${sep}${parts[index] ?? ""}${kept}`;
75
- if (visibleWidth(`…${candidate}`) > maxWidth) break;
75
+ if (visibleWidth(`...${candidate}`) > maxWidth) break;
76
76
  kept = candidate;
77
77
  }
78
78
  if (kept === "") return truncateTail(parts.at(-1) ?? full, maxWidth);
79
- return `…${kept}`;
79
+ return `...${kept}`;
80
80
  }
81
81
 
82
82
  export function basename(cwd: string): string {
@@ -92,7 +92,7 @@ export function truncateTail(text: string, maxWidth: number): string {
92
92
  if (maxWidth <= 0) return "";
93
93
  if (visibleWidth(text) <= maxWidth) return text;
94
94
  const chars = [...text];
95
- let width = 1;
95
+ let width = 3;
96
96
  let start = chars.length;
97
97
  while (start > 0) {
98
98
  const next = visibleWidth(chars[start - 1] ?? "");
@@ -100,10 +100,10 @@ export function truncateTail(text: string, maxWidth: number): string {
100
100
  width += next;
101
101
  start--;
102
102
  }
103
- return `…${chars.slice(start).join("")}`;
103
+ return `...${chars.slice(start).join("")}`;
104
104
  }
105
105
 
106
- export function fitWidth(text: string, width: number, ellipsis = "…"): string {
106
+ export function fitWidth(text: string, width: number, ellipsis = "..."): string {
107
107
  return truncateToWidth(text, Math.max(0, width), ellipsis);
108
108
  }
109
109
 
@@ -8,6 +8,7 @@ export interface Glyphs {
8
8
  host: string;
9
9
  session: string;
10
10
  tasks: string;
11
+ peers: string;
11
12
  waiting: string;
12
13
  queued: string;
13
14
  version: string;
@@ -57,6 +58,7 @@ export const NERD: Glyphs = {
57
58
  host: "\u{f108}", // nf-fa-desktop
58
59
  session: "\u{f02b}", // nf-fa-tag
59
60
  tasks: "\u{f04fe}", // nf-md-target
61
+ peers: "\u{f0c0}", // nf-fa-users
60
62
  waiting: "\u{f024}", // nf-fa-flag
61
63
  queued: "\u{f051f}", // nf-md-timer_sand
62
64
  version: "π",
@@ -76,7 +78,7 @@ export const NERD: Glyphs = {
76
78
  fill: "█",
77
79
  empty: "░",
78
80
  levels: ["▂", "▃", "▄", "▅", "▆", "▇"],
79
- ellipsis: "…",
81
+ ellipsis: "...",
80
82
  };
81
83
 
82
84
  export const UNICODE: Glyphs = {
@@ -87,6 +89,7 @@ export const UNICODE: Glyphs = {
87
89
  host: "⌂",
88
90
  session: "#",
89
91
  tasks: "▸",
92
+ peers: "◆",
90
93
  waiting: "⚑",
91
94
  queued: "⧗",
92
95
  version: "π",
@@ -106,7 +109,7 @@ export const UNICODE: Glyphs = {
106
109
  fill: "█",
107
110
  empty: "░",
108
111
  levels: ["▂", "▃", "▄", "▅", "▆", "▇"],
109
- ellipsis: "…",
112
+ ellipsis: "...",
110
113
  };
111
114
 
112
115
  export const ASCII: Glyphs = {
@@ -116,6 +119,7 @@ export const ASCII: Glyphs = {
116
119
  host: "@",
117
120
  session: "#",
118
121
  tasks: ">",
122
+ peers: "@",
119
123
  waiting: "",
120
124
  queued: "",
121
125
  version: "v",
@@ -80,6 +80,8 @@ export interface Paint {
80
80
  stress(percent: number, text: string): string;
81
81
  money(amount: number, text: string): string;
82
82
  frame(text: string): string;
83
+ /** A theme colour a package named in a contract, like a peer's own colour. */
84
+ raw(color: string, text: string): string;
83
85
  }
84
86
 
85
87
  export function themePaint(theme: Theme, frame?: (text: string) => string): Paint {
@@ -94,6 +96,11 @@ export function themePaint(theme: Theme, frame?: (text: string) => string): Pain
94
96
  stress: (percent, text) => theme.fg(stressToken(percent), text),
95
97
  money: (amount, text) => theme.fg(moneyToken(amount), text),
96
98
  frame: frame ?? ((text) => theme.fg("border", text)),
99
+ raw: (color, text) =>
100
+ theme.fg(
101
+ theme.colors[color as ThemeColor] === undefined ? "muted" : (color as ThemeColor),
102
+ text,
103
+ ),
97
104
  };
98
105
  }
99
106
 
@@ -120,6 +127,7 @@ export const PLAIN: Paint = {
120
127
  stress: (_percent, text) => text,
121
128
  money: (_amount, text) => text,
122
129
  frame: identity,
130
+ raw: (_color, text) => text,
123
131
  };
124
132
 
125
133
  function identity(text: string): string {
@@ -1,4 +1,4 @@
1
- import { type AccountWindow, tasksWith } from "@adeildo/pi-kit";
1
+ import { type AccountWindow, type PeerMark, tasksWith } from "@adeildo/pi-kit";
2
2
  import { visibleWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  import type { Snapshot } from "../data/snapshot.ts";
@@ -28,6 +28,7 @@ export const SEGMENT_IDS = [
28
28
  "host",
29
29
  "session",
30
30
  "tasks",
31
+ "peers",
31
32
  "version",
32
33
  "clock",
33
34
  "provider",
@@ -67,6 +68,7 @@ export function isSegmentId(value: string): value is SegmentId {
67
68
  const CLAIMED_BY_SEGMENT: Partial<Record<BuiltinSegment, string>> = {
68
69
  model: "pi-providers:account",
69
70
  tasks: "pi-tasks",
71
+ peers: "pi-peers",
70
72
  };
71
73
 
72
74
  export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
@@ -155,6 +157,29 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
155
157
  ? undefined
156
158
  : { text: `${mark(glyphs.host, paint, "host")}${paint.role("host", snapshot.host)}` },
157
159
  },
160
+ peers: {
161
+ priority: 25,
162
+ describe:
163
+ "the other agents in this folder, each by the name it chose and in its colour, then @ how many messages wait for an answer",
164
+ // The whole folder, then one name, then the bare mark, as the line runs out of room.
165
+ render: ({ snapshot, glyphs, paint }) => {
166
+ const peers = snapshot.peers;
167
+ const mentions =
168
+ snapshot.peersUnread > 0 ? paint.role("waiting", `@${snapshot.peersUnread}`) : "";
169
+ const first = peers[0];
170
+ if (first === undefined) return mentions === "" ? undefined : { text: mentions };
171
+ const named = (peer: PeerMark) => paint.raw(peer.color, `${glyphs.peers} ${peer.handle}`);
172
+ const rest = peers.length > 1 ? paint.dim(` +${peers.length - 1}`) : "";
173
+ const full = joined([...peers].map(named).join(" "), mentions);
174
+ const short = joined(named(first) + rest, mentions);
175
+ const least = joined(paint.raw(first.color, glyphs.peers), mentions);
176
+ return {
177
+ text: full,
178
+ shrink: (width) =>
179
+ visibleWidth(full) <= width ? full : visibleWidth(short) <= width ? short : least,
180
+ };
181
+ },
182
+ },
158
183
  tasks: {
159
184
  priority: 30,
160
185
  describe:
@@ -516,6 +541,11 @@ function quotaWindow(paint: Paint, window: AccountWindow): string {
516
541
  return `${paint.dim(window.name)} ${used}${reset}`;
517
542
  }
518
543
 
544
+ /** The parts that have something to say, with the gap the footer uses between things. */
545
+ function joined(...parts: string[]): string {
546
+ return parts.filter((part) => part !== "").join(" ");
547
+ }
548
+
519
549
  function mark(glyph: string, paint: Paint, role: Role): string {
520
550
  return glyph === "" ? "" : `${paint.role(role, glyph)} `;
521
551
  }
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
- ["tasks"],
92
+ ["tasks", "peers"],
93
93
  );
94
94
  export const topLeft = slot("frame.topLeft", "Frame, top left", "After the working spinner.", [
95
95
  "branch",
package/src/ui/header.ts CHANGED
@@ -110,7 +110,7 @@ export class HeaderComponent implements Component {
110
110
  const hints = HINTS()
111
111
  .map((hint) => `${paint.role("brand", hint.key)} ${paint.dim(hint.does)}`)
112
112
  .join(separator);
113
- return [first, hints, ""].map((line) => truncateToWidth(` ${line}`, width, "…"));
113
+ return [first, hints, ""].map((line) => truncateToWidth(` ${line}`, width, "..."));
114
114
  }
115
115
 
116
116
  #title(paint: Paint): string {
@@ -192,7 +192,7 @@ export function halfBlocks(pixels: readonly string[]): string[] {
192
192
  }
193
193
 
194
194
  function pad(text: string, width: number): string {
195
- const clipped = truncateToWidth(text, Math.max(0, width), "…");
195
+ const clipped = truncateToWidth(text, Math.max(0, width), "...");
196
196
  return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
197
197
  }
198
198
 
@@ -358,7 +358,7 @@ function inside(lines: readonly string[], width: number, theme: Theme): string[]
358
358
  const inner = room(width);
359
359
  return trim(lines).map((line) => {
360
360
  // Measuring is cheap, and cutting a line that already fits is not.
361
- const text = visibleWidth(line) > inner ? truncateToWidth(line, inner, "\u2026", true) : line;
361
+ const text = visibleWidth(line) > inner ? truncateToWidth(line, inner, "...", true) : line;
362
362
  const pad = " ".repeat(Math.max(0, inner - visibleWidth(text)));
363
363
  return `${paint("\u2502")} ${text}${pad} ${paint("\u2502")}`;
364
364
  });