pi-archimedes 2.11.0 → 2.11.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.
- package/package.json +13 -13
- package/src/overlay-width-audit.test.ts +116 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-archimedes",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
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.
|
|
20
|
-
"@pi-archimedes/footer": "2.11.
|
|
21
|
-
"@pi-archimedes/diff": "2.11.
|
|
22
|
-
"@pi-archimedes/ask": "2.11.
|
|
23
|
-
"@pi-archimedes/image-paste": "2.11.
|
|
24
|
-
"@pi-archimedes/subagent": "2.11.
|
|
25
|
-
"@pi-archimedes/todo": "2.11.
|
|
26
|
-
"@pi-archimedes/notify": "2.11.
|
|
27
|
-
"@pi-archimedes/session-name": "2.11.
|
|
28
|
-
"@pi-archimedes/sudo": "2.11.
|
|
29
|
-
"@pi-archimedes/ui": "2.11.
|
|
30
|
-
"@pi-archimedes/web": "2.11.
|
|
19
|
+
"@pi-archimedes/core": "2.11.1",
|
|
20
|
+
"@pi-archimedes/footer": "2.11.1",
|
|
21
|
+
"@pi-archimedes/diff": "2.11.1",
|
|
22
|
+
"@pi-archimedes/ask": "2.11.1",
|
|
23
|
+
"@pi-archimedes/image-paste": "2.11.1",
|
|
24
|
+
"@pi-archimedes/subagent": "2.11.1",
|
|
25
|
+
"@pi-archimedes/todo": "2.11.1",
|
|
26
|
+
"@pi-archimedes/notify": "2.11.1",
|
|
27
|
+
"@pi-archimedes/session-name": "2.11.1",
|
|
28
|
+
"@pi-archimedes/sudo": "2.11.1",
|
|
29
|
+
"@pi-archimedes/ui": "2.11.1",
|
|
30
|
+
"@pi-archimedes/web": "2.11.1"
|
|
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:
|
|
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
|
|
276
|
+
for (const ch of TYPED_VALUE) prompting.handleInput(ch);
|
|
210
277
|
capture("settings:prompt", prompting.render(frameWidth), (raw) => {
|
|
211
|
-
|
|
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
|
|