pi-archimedes 2.11.0 → 2.11.2

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": "pi-archimedes",
3
- "version": "2.11.0",
3
+ "version": "2.11.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/danielcherubini/pi-archimedes.git"
@@ -16,18 +16,18 @@
16
16
  ],
17
17
  "main": "./src/index.ts",
18
18
  "dependencies": {
19
- "@pi-archimedes/core": "2.11.0",
20
- "@pi-archimedes/footer": "2.11.0",
21
- "@pi-archimedes/diff": "2.11.0",
22
- "@pi-archimedes/ask": "2.11.0",
23
- "@pi-archimedes/image-paste": "2.11.0",
24
- "@pi-archimedes/subagent": "2.11.0",
25
- "@pi-archimedes/todo": "2.11.0",
26
- "@pi-archimedes/notify": "2.11.0",
27
- "@pi-archimedes/session-name": "2.11.0",
28
- "@pi-archimedes/sudo": "2.11.0",
29
- "@pi-archimedes/ui": "2.11.0",
30
- "@pi-archimedes/web": "2.11.0"
19
+ "@pi-archimedes/core": "2.11.2",
20
+ "@pi-archimedes/footer": "2.11.2",
21
+ "@pi-archimedes/diff": "2.11.2",
22
+ "@pi-archimedes/ask": "2.11.2",
23
+ "@pi-archimedes/image-paste": "2.11.2",
24
+ "@pi-archimedes/subagent": "2.11.2",
25
+ "@pi-archimedes/todo": "2.11.2",
26
+ "@pi-archimedes/notify": "2.11.2",
27
+ "@pi-archimedes/session-name": "2.11.2",
28
+ "@pi-archimedes/sudo": "2.11.2",
29
+ "@pi-archimedes/ui": "2.11.2",
30
+ "@pi-archimedes/web": "2.11.2"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": ">=1.2.0",
@@ -48,6 +48,31 @@ const theme = {
48
48
 
49
49
  const plainTheme: OverlayTheme = { fg: (token: string, text: string) => String(theme.fg(token, text)) };
50
50
 
51
+ /**
52
+ * The free-text "model" row's seeded value, and the wide value the audit types
53
+ * on top of it (37 drawn columns in total — more than the narrowest frame hands
54
+ * the field, which is what makes the prompt capture a crowded row).
55
+ */
56
+ const SEEDED_VALUE = "anthropic/claude-opus-4.5";
57
+ const TYPED_VALUE = "検証モデル名";
58
+ /**
59
+ * The head of the seeded value — its first path segment, derived so a fixture
60
+ * that renames the value cannot leave the crowding check below pointing at a
61
+ * string nothing draws any more. A horizontally scrolled field drops these
62
+ * columns first, which is what the check reads them for.
63
+ *
64
+ * The `/` is asserted rather than assumed: `slice(0, -1)` — what `indexOf` returns
65
+ * when it finds nothing — would silently cut only the last character, so the head
66
+ * would still be found on screen and the crowding tripwire below would weaken
67
+ * instead of failing. A leading `/` is refused for the same reason: it makes the
68
+ * head empty, and an empty string is found in every frame.
69
+ */
70
+ const SEPARATOR = SEEDED_VALUE.indexOf("/");
71
+ if (SEPARATOR <= 0) {
72
+ throw new Error("`SEEDED_VALUE` needs a non-empty first path segment — the head below is it");
73
+ }
74
+ const SEEDED_HEAD = SEEDED_VALUE.slice(0, SEPARATOR);
75
+
51
76
  const ITEMS: SettingItem[] = [
52
77
  {
53
78
  id: "wide",
@@ -65,7 +90,7 @@ const ITEMS: SettingItem[] = [
65
90
  {
66
91
  id: "model",
67
92
  label: "モデル スコープ モデル",
68
- currentValue: "anthropic/claude-opus-4.5",
93
+ currentValue: SEEDED_VALUE,
69
94
  description: `${"説明文".repeat(60)}`,
70
95
  },
71
96
  ];
@@ -113,6 +138,44 @@ function drawnIncludes(raw: string[], needle: string): boolean {
113
138
  return raw.some((l) => stripTerminalSequences(l).includes(needle));
114
139
  }
115
140
 
141
+ /**
142
+ * What `drawnRow()` found. `line` is present ONLY in the unambiguous case — one
143
+ * row drew the needle — so a caller that wants it has to handle the case where
144
+ * zero or several did, and `matches`/`rows` say which. Under
145
+ * `exactOptionalPropertyTypes` `line?: string` means "absent", never
146
+ * "explicitly undefined", so the two failures cannot be conflated by a caller
147
+ * that reads the line without asking how many rows matched.
148
+ */
149
+ interface DrawnRow {
150
+ /** How many rows drew the needle; 1 is the only unambiguous answer. */
151
+ readonly matches: number;
152
+ /** Every matched row, sequences and border glyphs stripped, for the message. */
153
+ readonly rows: readonly string[];
154
+ /** The untouched line of the matched row — only when exactly one matched. */
155
+ readonly line?: string;
156
+ }
157
+
158
+ /**
159
+ * The row that draws `needle`, plus the evidence to report when there is not
160
+ * exactly one such row. Strips terminal sequences and the border glyphs the way
161
+ * `searchRow()` does to *locate* the row, then hands back the untouched line so
162
+ * a caller can still look inside it for the zero-width `CURSOR_MARKER` — which
163
+ * the strip would remove.
164
+ *
165
+ * The match count is part of the result, not just the line: "the needle is not
166
+ * drawn" and "the needle is drawn on four rows" are different failures with the
167
+ * same return value, and reporting the first when the second happened sends the
168
+ * reader to the wrong code.
169
+ */
170
+ function drawnRow(raw: string[], needle: string): DrawnRow {
171
+ const hits = raw
172
+ .map((line) => ({ line, bare: stripTerminalSequences(line).replace(/[│┌└┐┘─]/g, "").trim() }))
173
+ .filter((hit) => hit.bare.includes(needle));
174
+ const rows = hits.map((h) => h.bare);
175
+ const only = hits.length === 1 ? hits[0] : undefined;
176
+ return only ? { line: only.line, matches: 1, rows } : { matches: hits.length, rows };
177
+ }
178
+
116
179
  function makeManager() {
117
180
  return createSettingsManager({
118
181
  items: ITEMS.map((i) => ({ ...i })),
@@ -141,7 +204,11 @@ function makeOnboarding() {
141
204
  }
142
205
 
143
206
  describe("overlay surfaces frame-width audit", () => {
207
+ // `NARROWEST_FRAME` is derived rather than written twice: the prompt row's
208
+ // crowding check below has to fire on the frame where crowding is guaranteed,
209
+ // and a list that grows by a narrower entry must move that target with it.
144
210
  const FRAME_WIDTHS = [40, 60, 84, 100];
211
+ const NARROWEST_FRAME = Math.min(...FRAME_WIDTHS);
145
212
 
146
213
  it("keeps every produced line on the frame, on every surface and mode", () => {
147
214
  const violations: Violation[] = [];
@@ -206,9 +273,55 @@ describe("overlay surfaces frame-width audit", () => {
206
273
  prompting.handleInput("\x1b[B");
207
274
  prompting.handleInput("\x1b[B");
208
275
  prompting.handleInput("\r");
209
- for (const ch of "検証モデル名") prompting.handleInput(ch);
276
+ for (const ch of TYPED_VALUE) prompting.handleInput(ch);
210
277
  capture("settings:prompt", prompting.render(frameWidth), (raw) => {
211
- if (!drawnIncludes(raw, "検証")) return "prompt field does not show the typed wide value";
278
+ // #100: the cursor marker survives a full prompt row. `CURSOR_MARKER` is
279
+ // zero width, so on a row whose value already fills the frame it is the
280
+ // one thing that costs nothing to keep and everything to lose — the old
281
+ // `.length`-based padding counted it as drawn columns and the truncation
282
+ // that followed cut it off the tail, leaving the user a value, no cursor
283
+ // and no clue why. The search rows above already carry the sibling
284
+ // assertion; this is the prompt row's.
285
+ //
286
+ // It is bound to the row that draws the value, not to the frame: while a
287
+ // field is open the search row is not drawn, but a whole-frame `.some()`
288
+ // would still be satisfied by a marker that migrated onto any other row,
289
+ // which is exactly the regression shape this guards. `drawnRow()` refuses
290
+ // an ambiguous match for the same reason.
291
+ //
292
+ // The locator is the whole typed value, not a prefix of it: a prefix
293
+ // would still match a row whose tail had been cut, which is the very
294
+ // regression the marker check below is watching for. It also subsumes the
295
+ // `drawnIncludes` this guard used to lead with, and it is deliberately the
296
+ // full value rather than the shorter `検証` — the `wide` fixture's
297
+ // description carries `検証` three times over, so the moment `listBody()`
298
+ // stops returning early behind an open field (`settings-manager.ts:526`)
299
+ // the short needle would match several rows and the guard would lose its
300
+ // anchor. Matching the full value keeps it anchored on the field's row.
301
+ const valueRow = drawnRow(raw, TYPED_VALUE);
302
+ if (valueRow.line === undefined) {
303
+ const sample = valueRow.rows.map((r) => JSON.stringify(r)).join(" | ");
304
+ return `the typed value is not on exactly one row (${valueRow.matches} matched): ${sample || "(nothing drew it)"}`;
305
+ }
306
+ const row = valueRow.line;
307
+ if (!row.includes(CURSOR_MARKER)) return "cursor marker missing from the prompt value row";
308
+ // Tripwire against a decorative guard: crowding is what the assertion
309
+ // above is *for*, and crowding has a chrome-independent signature. The
310
+ // value is 37 columns; on the narrowest frame audited pi-tui's `Input` is
311
+ // handed 34, so it scrolls the row and the HEAD of the value is not drawn
312
+ // at all while the typed tail is. That is what "over-full" means to the
313
+ // component doing the drawing. Unlike the measured column count this
314
+ // replaces, it names no border, no padding and no fake-cursor cell, so a
315
+ // chrome change cannot outlive it, and it cannot go green on a row that
316
+ // stopped being crowded. The head is looked for across the whole frame on
317
+ // purpose: absent everywhere is the claim, so a field that wrapped the
318
+ // value onto a second row instead of scrolling it is reported rather than
319
+ // mistaken for a row that merely moved. And it is keyed to the *narrowest*
320
+ // audited frame, not to slot 0 of the list: scrolling only gets harder as
321
+ // the frame shrinks, so a width added to the audit can only tighten this.
322
+ if (frameWidth === NARROWEST_FRAME && drawnIncludes(raw, SEEDED_HEAD)) {
323
+ return `prompt row is not crowded on the narrowest frame: the head of the value (${SEEDED_HEAD}) is still drawn`;
324
+ }
212
325
  return null;
213
326
  });
214
327