@monotykamary/pi-loop 0.1.12 → 0.1.14
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 +1 -1
- package/src/index.ts +24 -3
- package/src/ui/renderer.ts +6 -4
- package/tests/width-regression.test.ts +41 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -85,6 +85,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
85
85
|
const widgetState = createInitialState();
|
|
86
86
|
let currentCtx: ExtensionContext | undefined;
|
|
87
87
|
let idleSteers = 0; // consecutive agent_end steers; reset on done/stop/new loop
|
|
88
|
+
let userInputEpoch = 0;
|
|
89
|
+
|
|
90
|
+
pi.on('input', (event) => {
|
|
91
|
+
if (event.source === 'interactive' || event.source === 'rpc') {
|
|
92
|
+
userInputEpoch++;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
pi.on('before_agent_start', () => {
|
|
96
|
+
userInputEpoch++;
|
|
97
|
+
});
|
|
88
98
|
|
|
89
99
|
// ---- Session lifecycle: restore state ----
|
|
90
100
|
|
|
@@ -212,6 +222,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
212
222
|
pi.on('agent_settled', async (_event, ctx) => {
|
|
213
223
|
currentCtx = ctx;
|
|
214
224
|
if (!state.isActive()) return;
|
|
225
|
+
const inputEpochAtStart = userInputEpoch;
|
|
215
226
|
|
|
216
227
|
state.incrementTurnCount();
|
|
217
228
|
const s = state.getState()!;
|
|
@@ -261,7 +272,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
272
|
const decision = await analyze(
|
|
262
273
|
ctx,
|
|
263
274
|
s,
|
|
264
|
-
true /*
|
|
275
|
+
true /* fully settled checkpoint */,
|
|
265
276
|
ineffectivePattern,
|
|
266
277
|
undefined,
|
|
267
278
|
(accumulated) => {
|
|
@@ -275,6 +286,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
286
|
}
|
|
276
287
|
);
|
|
277
288
|
|
|
289
|
+
// A real user prompt supersedes a decision computed from the preceding
|
|
290
|
+
// settled snapshot. Do not race that prompt or steer from stale context.
|
|
291
|
+
if (userInputEpoch !== inputEpochAtStart) {
|
|
292
|
+
updateUI(ctx, widgetState, state.getState(), {
|
|
293
|
+
type: 'watching',
|
|
294
|
+
reframeTier: state.getReframeTier(),
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
278
299
|
if (decision.action === 'steer' && decision.message) {
|
|
279
300
|
idleSteers++;
|
|
280
301
|
state.addIntervention({
|
|
@@ -289,7 +310,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
289
310
|
message: decision.message,
|
|
290
311
|
reframeTier: state.getReframeTier(),
|
|
291
312
|
});
|
|
292
|
-
pi.sendUserMessage(decision.message);
|
|
313
|
+
pi.sendUserMessage(decision.message, { deliverAs: 'followUp' });
|
|
293
314
|
} else if (decision.action === 'done') {
|
|
294
315
|
idleSteers = 0;
|
|
295
316
|
state.resetReframeTier();
|
|
@@ -317,7 +338,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
317
338
|
message: steerMessage,
|
|
318
339
|
reframeTier: state.getReframeTier(),
|
|
319
340
|
});
|
|
320
|
-
pi.sendUserMessage(steerMessage);
|
|
341
|
+
pi.sendUserMessage(steerMessage, { deliverAs: 'followUp' });
|
|
321
342
|
} else {
|
|
322
343
|
updateUI(ctx, widgetState, state.getState(), {
|
|
323
344
|
type: 'watching',
|
package/src/ui/renderer.ts
CHANGED
|
@@ -175,6 +175,8 @@ function renderWithState(
|
|
|
175
175
|
return {
|
|
176
176
|
render: (width: number) => {
|
|
177
177
|
const paddedWidth = Math.max(0, width - 1);
|
|
178
|
+
const fitLines = (lines: string[]) =>
|
|
179
|
+
lines.map((line) => truncateToWidth(line, paddedWidth, ''));
|
|
178
180
|
widgetState.lastRenderedWidth = paddedWidth;
|
|
179
181
|
const suffix = suffixParts.length > 0 ? sep + suffixParts.join(sep) : '';
|
|
180
182
|
|
|
@@ -206,7 +208,7 @@ function renderWithState(
|
|
|
206
208
|
const visibleLines = widgetState.lastThinkingLines
|
|
207
209
|
.slice(0, visibleCount)
|
|
208
210
|
.map((ln) => theme.fg('dim', ln));
|
|
209
|
-
return [l1, ...visibleLines];
|
|
211
|
+
return fitLines([l1, ...visibleLines]);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
// For non-analyzing states (steering/done/waiting), preserve existing line breaks.
|
|
@@ -214,12 +216,12 @@ function renderWithState(
|
|
|
214
216
|
const isAnalyzing = action.type === 'analyzing';
|
|
215
217
|
if (!isAnalyzing && widgetState.lastThinkingLines.length > 0) {
|
|
216
218
|
const visibleLines = widgetState.lastThinkingLines.map((ln) => theme.fg('dim', ln));
|
|
217
|
-
return [l1, ...visibleLines];
|
|
219
|
+
return fitLines([l1, ...visibleLines]);
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
// No thinking to display
|
|
221
223
|
if (!rawThinking) {
|
|
222
|
-
return [l1];
|
|
224
|
+
return fitLines([l1]);
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
const thinkingIndent = stripAnsi(thinkingPrefix).length;
|
|
@@ -249,7 +251,7 @@ function renderWithState(
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
widgetState.lastThinkingLines = plainLines;
|
|
252
|
-
return [l1, ...thinkingLines];
|
|
254
|
+
return fitLines([l1, ...thinkingLines]);
|
|
253
255
|
},
|
|
254
256
|
invalidate: () => {},
|
|
255
257
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { updateUI } from '../src/ui/status-widget.js';
|
|
3
|
+
|
|
4
|
+
vi.mock('@earendil-works/pi-tui', () => ({
|
|
5
|
+
truncateToWidth: (text: string, width: number) => text.slice(0, Math.max(0, width)),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const theme = { fg: (_color: string, text: string) => text };
|
|
9
|
+
|
|
10
|
+
describe('loop widget width', () => {
|
|
11
|
+
it('bounds long words and cached lines after a terminal shrink', () => {
|
|
12
|
+
const setWidget = vi.fn();
|
|
13
|
+
const ctx = {
|
|
14
|
+
ui: { setWidget, setStatus: vi.fn(), notify: vi.fn(), select: vi.fn() },
|
|
15
|
+
sessionManager: { getBranch: vi.fn(() => []) },
|
|
16
|
+
model: { provider: 'test', id: 'model' },
|
|
17
|
+
} as any;
|
|
18
|
+
const state = {
|
|
19
|
+
active: true,
|
|
20
|
+
outcome: 'goal',
|
|
21
|
+
interventions: [],
|
|
22
|
+
turnCount: 1,
|
|
23
|
+
justSteered: false,
|
|
24
|
+
lastAnalyzedTurn: 0,
|
|
25
|
+
snapshotBuffer: [],
|
|
26
|
+
reframeTier: 0,
|
|
27
|
+
lastSteerTurn: -1,
|
|
28
|
+
} as any;
|
|
29
|
+
|
|
30
|
+
updateUI(ctx, state, { type: 'analyzing', turn: 1, thinking: 'x'.repeat(200) });
|
|
31
|
+
let factory = setWidget.mock.calls.at(-1)[1];
|
|
32
|
+
factory(null, theme).render(100);
|
|
33
|
+
|
|
34
|
+
updateUI(ctx, state, { type: 'done' });
|
|
35
|
+
factory = setWidget.mock.calls.at(-1)[1];
|
|
36
|
+
const width = 20;
|
|
37
|
+
const lines = factory(null, theme).render(width);
|
|
38
|
+
|
|
39
|
+
expect(lines.every((line: string) => line.length <= width - 1)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|