@adeildo/pi-look 5.11.2 → 5.12.1

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-look",
3
- "version": "5.11.2",
3
+ "version": "5.12.1",
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.11.2"
41
+ "@adeildo/pi-kit": "5.12.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@earendil-works/pi-ai": "*",
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { dirname, join } from "node:path";
2
2
 
3
3
  import {
4
- accountStateOf,
4
+ ACCOUNT,
5
+ type AccountState,
5
6
  createApp,
6
- currentPeers,
7
- currentTasks,
7
+ PEERS,
8
+ TASKS,
8
9
  defineFeature,
9
10
  drawSkillRefs,
10
11
  type FeatureScope,
@@ -321,7 +322,7 @@ function createScreen(
321
322
  const snapshot = reader.read(ctx, {
322
323
  branch: live.footerData?.getGitBranch() ?? null,
323
324
  git: live.git,
324
- account: accountStateOf(scope.events, ctx.model?.provider),
325
+ account: accountOf(scope, ctx.model?.provider),
325
326
  request: telemetry.request(),
326
327
  last: telemetry.last(),
327
328
  run: telemetry.run(),
@@ -353,18 +354,22 @@ function createScreen(
353
354
  decorate: (line) => {
354
355
  const theme = live.ctx?.ui.theme;
355
356
  if (theme === undefined) return line;
356
- return drawSkillRefs(line, skillNames(scope.events), (name) =>
357
+ return drawSkillRefs(line, skillNames(scope), (name) =>
357
358
  theme.fg("customMessageLabel", `${SKILL_REF}${name}`),
358
359
  );
359
360
  },
360
- skills: () => skillNames(scope.events).length > 0,
361
+ skills: () => skillNames(scope).length > 0,
361
362
  };
362
363
  }
363
364
 
365
+ function accountOf(scope: FeatureScope, provider: string | undefined): AccountState | undefined {
366
+ return provider === undefined ? undefined : ACCOUNT.current(scope, provider);
367
+ }
368
+
364
369
  function tasksOf(scope: FeatureScope): Pick<LiveInputs, "tasks" | "peers" | "peersUnread"> {
365
- const peers = currentPeers(scope.events);
370
+ const peers = PEERS.current(scope);
366
371
  return {
367
- tasks: currentTasks(scope.events)?.state,
372
+ tasks: TASKS.current(scope)?.state,
368
373
  peers: peers?.peers,
369
374
  peersUnread: peers?.unread,
370
375
  };
@@ -1,6 +1,6 @@
1
- import { isAbsolute, relative, resolve, sep } from "node:path";
1
+ import { sep } from "node:path";
2
2
 
3
- import { formatCost, formatTokens } from "@adeildo/pi-kit";
3
+ import { formatCost, formatTokens, tildify } from "@adeildo/pi-kit";
4
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
5
 
6
6
  export const CELL = {
@@ -57,7 +57,7 @@ export function percent(value: number): string {
57
57
  }
58
58
 
59
59
  export function shortenPath(cwd: string, home: string | undefined, maxWidth: number): string {
60
- const full = home === undefined ? cwd : underHome(cwd, home);
60
+ const full = home === undefined ? cwd : tildify(cwd, home);
61
61
  if (maxWidth <= 0 || visibleWidth(full) <= maxWidth) return full;
62
62
 
63
63
  const parts = full.split(sep);
@@ -104,14 +104,6 @@ export function tabs(text: string): string {
104
104
  return text.replace(/\t/g, " ");
105
105
  }
106
106
 
107
- function underHome(cwd: string, home: string): string {
108
- const rest = relative(resolve(home), resolve(cwd));
109
- const inside =
110
- rest === "" || (rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest));
111
- if (!inside) return cwd;
112
- return rest === "" ? "~" : `~${sep}${rest}`;
113
- }
114
-
115
107
  function trim(fixed: string): string {
116
108
  return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
117
109
  }
@@ -37,6 +37,8 @@ export interface Glyphs {
37
37
  empty: string;
38
38
  levels: readonly string[];
39
39
  ellipsis: string;
40
+ /** Between the agents of the folder and those of each shared room. */
41
+ apart: string;
40
42
  }
41
43
 
42
44
  const COMMON = {
@@ -48,6 +50,7 @@ const COMMON = {
48
50
  conflicted: "!",
49
51
  sent: "↑",
50
52
  received: "↓",
53
+ apart: "│",
51
54
  };
52
55
 
53
56
  export const NERD: Glyphs = {
@@ -114,6 +117,7 @@ export const UNICODE: Glyphs = {
114
117
 
115
118
  export const ASCII: Glyphs = {
116
119
  set: "ascii",
120
+ apart: "|",
117
121
  folder: "",
118
122
  branch: "",
119
123
  host: "@",
@@ -160,7 +160,7 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
160
160
  peers: {
161
161
  priority: 25,
162
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",
163
+ "the other agents in this folder, then those of each shared room after a rule, each by the name it chose and in its colour, then @ how many messages wait for an answer",
164
164
  // The whole folder, then one name, then the bare mark, as the line runs out of room.
165
165
  render: ({ snapshot, glyphs, paint }) => {
166
166
  const peers = snapshot.peers;
@@ -170,7 +170,12 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
170
170
  if (first === undefined) return mentions === "" ? undefined : { text: mentions };
171
171
  const named = (peer: PeerMark) => paint.raw(peer.color, `${glyphs.peers} ${peer.handle}`);
172
172
  const rest = peers.length > 1 ? paint.dim(` +${peers.length - 1}`) : "";
173
- const full = joined([...peers].map(named).join(" "), mentions);
173
+ const rooms = new Map<string, string[]>();
174
+ for (const peer of peers)
175
+ rooms.set(peer.room ?? "", [...(rooms.get(peer.room ?? "") ?? []), named(peer)]);
176
+ const apart = ` ${paint.dim(glyphs.apart)} `;
177
+ const groups = [...rooms.values()].map((group) => group.join(" "));
178
+ const full = joined(groups.join(apart), mentions);
174
179
  const short = joined(named(first) + rest, mentions);
175
180
  const least = joined(paint.raw(first.color, glyphs.peers), mentions);
176
181
  return {
@@ -1,11 +1,10 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { resolve } from "node:path";
4
3
 
5
4
  // While the model writes an edit or a write, pi shows the path, or the first lines of the file,
6
5
  // which stopped changing long ago. These show the end instead: the lines arriving, with a cursor
7
6
  // where the model is writing, and what it has written so far on the bottom rule.
8
- import { GLYPH, isObject } from "@adeildo/pi-kit";
7
+ import { GLYPH, isObject, tildify } from "@adeildo/pi-kit";
9
8
  import { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
10
9
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
11
10
 
@@ -154,9 +153,7 @@ function pathLine(tool: string, path: string | undefined, theme: Theme): string
154
153
  const title = theme.fg("toolTitle", theme.bold(tool));
155
154
  // Pi's own stand-in for a path not written yet.
156
155
  if (path === undefined) return `${title} ${theme.fg("toolOutput", "...")}`;
157
- const home = homedir();
158
- const shown = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
159
- return `${title} ${theme.fg("accent", printable(shown))}`;
156
+ return `${title} ${theme.fg("accent", printable(tildify(path)))}`;
160
157
  }
161
158
 
162
159
  /**
@@ -4,11 +4,9 @@ import {
4
4
  CUT,
5
5
  type CallRuling,
6
6
  type FeatureScope,
7
- FRAMED,
8
- isObject,
9
- RULING_CHANGED,
7
+ CALLS_FRAMED,
8
+ RULING,
10
9
  type RulingTone,
11
- rulingOf,
12
10
  GLYPH,
13
11
  LINE,
14
12
  } from "@adeildo/pi-kit";
@@ -48,12 +46,8 @@ export function registerToolFrames(scope: FeatureScope): void {
48
46
  const written = new Map<string, number>();
49
47
  // The set is asked for on every drawing of every frame, and it changes only with the setting.
50
48
  const set = glyphsOf(() => icons.get(scope));
51
- scope.events.on(FRAMED, (data) => {
52
- if (isObject(data)) data.framed = true;
53
- });
54
- scope.events.on(RULING_CHANGED, (data) => {
55
- if (isObject(data) && typeof data.toolCallId === "string") redraws.get(data.toolCallId)?.();
56
- });
49
+ CALLS_FRAMED.provide(scope, () => true);
50
+ RULING.onChanged(scope, (_ruling, toolCallId) => redraws.get(toolCallId)?.());
57
51
  // Pi completes every call of a message only when the message ends, so a call the model finished
58
52
  // before writing the next one would go on writing. The provider says when each one ends. Pi tells
59
53
  // the extensions first and hands every call its arguments right after, which draws the frame.
@@ -119,7 +113,7 @@ function framed(
119
113
  else clock.writtenAt ??= ended ?? now;
120
114
  if (context.executionStarted) clock.startedAt ??= now;
121
115
  const writing = clock.writtenAt === undefined;
122
- const ruling = rulingOf(scope.events, context.toolCallId);
116
+ const ruling = RULING.current(scope, context.toolCallId);
123
117
 
124
118
  // Pi hands every call of a message its arguments again on each piece the model writes, so a
125
119
  // call that did not change keeps the frame it has, and its renderer is not asked again.
@@ -178,7 +172,7 @@ function framed(
178
172
  renderResult(shown, options, theme, context) {
179
173
  const clock = clockOf(context);
180
174
  // A blocked call prints nothing but the reason the line above already gives.
181
- const blocked = rulingOf(scope.events, context.toolCallId)?.blocked === true;
175
+ const blocked = RULING.current(scope, context.toolCallId)?.blocked === true;
182
176
  // Pi draws the pictures of a result right below the frame, so the frame counts them.
183
177
  const images = shown.content.filter((block) => block.type === "image").length;
184
178
  if (!options.isPartial) clock.endedAt ??= Date.now();