@dungle-scrubs/tallow 0.8.27 → 0.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 +42 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -9
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +20 -9
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +20 -9
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_icons/__tests__/icons.test.ts +0 -1
- package/extensions/_icons/index.ts +0 -2
- package/extensions/_shared/pid-registry.ts +5 -5
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +9 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +62 -1
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +42 -0
- package/extensions/render-stabilizer/extension.json +5 -0
- package/extensions/render-stabilizer/index.ts +66 -0
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +66 -6
- package/extensions/subagent-tool/__tests__/model-router-explicit-resolution.test.ts +79 -5
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +4 -4
- package/extensions/subagent-tool/index.ts +4 -2
- package/extensions/subagent-tool/process.ts +26 -8
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +56 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +205 -5
- package/package.json +9 -9
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/schemas/settings.schema.json +0 -5
- package/skills/tallow-expert/SKILL.md +6 -4
- package/extensions/plan-mode-tool/__tests__/e2e.mjs +0 -350
- package/extensions/plan-mode-tool/__tests__/index.test.ts +0 -213
- package/extensions/plan-mode-tool/__tests__/utils.test.ts +0 -381
- package/extensions/plan-mode-tool/extension.json +0 -22
- package/extensions/plan-mode-tool/index.ts +0 -583
- package/extensions/plan-mode-tool/utils.ts +0 -257
|
@@ -819,7 +819,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
|
|
819
819
|
if (modifier === 0) {
|
|
820
820
|
return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
|
|
821
821
|
}
|
|
822
|
-
return
|
|
822
|
+
return (
|
|
823
|
+
matchesKittySequence(data, CODEPOINTS.tab, modifier) ||
|
|
824
|
+
matchesModifyOtherKeys(data, CODEPOINTS.tab, modifier)
|
|
825
|
+
);
|
|
823
826
|
|
|
824
827
|
case "enter":
|
|
825
828
|
case "return":
|
|
@@ -873,7 +876,8 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
|
|
873
876
|
}
|
|
874
877
|
return (
|
|
875
878
|
matchesKittySequence(data, CODEPOINTS.enter, modifier) ||
|
|
876
|
-
matchesKittySequence(data, CODEPOINTS.kpEnter, modifier)
|
|
879
|
+
matchesKittySequence(data, CODEPOINTS.kpEnter, modifier) ||
|
|
880
|
+
matchesModifyOtherKeys(data, CODEPOINTS.enter, modifier)
|
|
877
881
|
);
|
|
878
882
|
|
|
879
883
|
case "backspace":
|
|
@@ -1108,21 +1112,33 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
|
|
1108
1112
|
if (ctrl && !shift && !alt) {
|
|
1109
1113
|
// Legacy: ctrl+key sends the control character
|
|
1110
1114
|
if (rawCtrl && data === rawCtrl) return true;
|
|
1111
|
-
return
|
|
1115
|
+
return (
|
|
1116
|
+
matchesKittySequence(data, codepoint, MODIFIERS.ctrl) ||
|
|
1117
|
+
matchesModifyOtherKeys(data, codepoint, MODIFIERS.ctrl)
|
|
1118
|
+
);
|
|
1112
1119
|
}
|
|
1113
1120
|
|
|
1114
1121
|
if (ctrl && shift && !alt) {
|
|
1115
|
-
return
|
|
1122
|
+
return (
|
|
1123
|
+
matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl) ||
|
|
1124
|
+
matchesModifyOtherKeys(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl)
|
|
1125
|
+
);
|
|
1116
1126
|
}
|
|
1117
1127
|
|
|
1118
1128
|
if (shift && !ctrl && !alt) {
|
|
1119
1129
|
// Legacy: shift+letter produces uppercase
|
|
1120
1130
|
if (data === key.toUpperCase()) return true;
|
|
1121
|
-
return
|
|
1131
|
+
return (
|
|
1132
|
+
matchesKittySequence(data, codepoint, MODIFIERS.shift) ||
|
|
1133
|
+
matchesModifyOtherKeys(data, codepoint, MODIFIERS.shift)
|
|
1134
|
+
);
|
|
1122
1135
|
}
|
|
1123
1136
|
|
|
1124
1137
|
if (modifier !== 0) {
|
|
1125
|
-
return
|
|
1138
|
+
return (
|
|
1139
|
+
matchesKittySequence(data, codepoint, modifier) ||
|
|
1140
|
+
matchesModifyOtherKeys(data, codepoint, modifier)
|
|
1141
|
+
);
|
|
1126
1142
|
}
|
|
1127
1143
|
|
|
1128
1144
|
// Check both raw char and Kitty sequence (needed for release events)
|
|
@@ -1251,3 +1267,65 @@ export function parseKey(data: string): string | undefined {
|
|
|
1251
1267
|
|
|
1252
1268
|
return undefined;
|
|
1253
1269
|
}
|
|
1270
|
+
|
|
1271
|
+
// =============================================================================
|
|
1272
|
+
// Mouse Event Parsing
|
|
1273
|
+
// =============================================================================
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Parsed mouse event from SGR extended format.
|
|
1277
|
+
* SGR format: \x1b[<button;column;row[Mm]
|
|
1278
|
+
* M = press, m = release
|
|
1279
|
+
*/
|
|
1280
|
+
export interface MouseEvent {
|
|
1281
|
+
/** Event type */
|
|
1282
|
+
type: "scroll-up" | "scroll-down" | "press" | "release" | "drag";
|
|
1283
|
+
/** 0=left, 1=middle, 2=right */
|
|
1284
|
+
button: number;
|
|
1285
|
+
/** 1-indexed column */
|
|
1286
|
+
x: number;
|
|
1287
|
+
/** 1-indexed row */
|
|
1288
|
+
y: number;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** SGR extended mouse format: \x1b[<button;x;y[Mm] */
|
|
1292
|
+
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Parse an SGR mouse event from raw terminal input.
|
|
1296
|
+
*
|
|
1297
|
+
* @param data - Raw terminal input
|
|
1298
|
+
* @returns Parsed mouse event, or null if not a mouse sequence
|
|
1299
|
+
*/
|
|
1300
|
+
export function parseMouseEvent(data: string): MouseEvent | null {
|
|
1301
|
+
const match = data.match(SGR_MOUSE_RE);
|
|
1302
|
+
if (!match) return null;
|
|
1303
|
+
|
|
1304
|
+
const code = parseInt(match[1]!, 10);
|
|
1305
|
+
const x = parseInt(match[2]!, 10);
|
|
1306
|
+
const y = parseInt(match[3]!, 10);
|
|
1307
|
+
const isRelease = match[4] === "m";
|
|
1308
|
+
|
|
1309
|
+
// Scroll wheel: codes 64 (up) and 65 (down)
|
|
1310
|
+
if (code === 64) return { type: "scroll-up", button: 0, x, y };
|
|
1311
|
+
if (code === 65) return { type: "scroll-down", button: 0, x, y };
|
|
1312
|
+
|
|
1313
|
+
// Button number is in the low 2 bits
|
|
1314
|
+
const button = code & 0x03;
|
|
1315
|
+
|
|
1316
|
+
// Bit 5 (32) = motion/drag
|
|
1317
|
+
if (code & 32) return { type: "drag", button, x, y };
|
|
1318
|
+
|
|
1319
|
+
return { type: isRelease ? "release" : "press", button, x, y };
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Fast check: is this data an SGR mouse event?
|
|
1324
|
+
* Avoids regex for non-mouse input.
|
|
1325
|
+
*
|
|
1326
|
+
* @param data - Raw terminal input
|
|
1327
|
+
* @returns true if the input is an SGR mouse sequence
|
|
1328
|
+
*/
|
|
1329
|
+
export function isMouseEvent(data: string): boolean {
|
|
1330
|
+
return data.length >= 9 && data.startsWith("\x1b[<");
|
|
1331
|
+
}
|
|
@@ -30,6 +30,9 @@ export interface Terminal {
|
|
|
30
30
|
// Whether Kitty keyboard protocol is active
|
|
31
31
|
get kittyProtocolActive(): boolean;
|
|
32
32
|
|
|
33
|
+
// Whether running inside tmux (using modifyOtherKeys instead of Kitty protocol)
|
|
34
|
+
get isTmux(): boolean;
|
|
35
|
+
|
|
33
36
|
// Cursor positioning (relative to current position)
|
|
34
37
|
moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
|
|
35
38
|
|
|
@@ -42,6 +45,10 @@ export interface Terminal {
|
|
|
42
45
|
clearFromCursor(): void; // Clear from cursor to end of screen
|
|
43
46
|
clearScreen(): void; // Clear entire screen and move cursor to (0,0)
|
|
44
47
|
|
|
48
|
+
// Mouse reporting
|
|
49
|
+
enableMouse(): void; // Enable SGR mouse tracking (scroll, click)
|
|
50
|
+
disableMouse(): void; // Disable mouse tracking
|
|
51
|
+
|
|
45
52
|
// Screen buffer mode
|
|
46
53
|
enterAlternateScreen(): void; // Switch to alternate screen buffer (no scrollback)
|
|
47
54
|
leaveAlternateScreen(): void; // Restore normal screen buffer and scrollback
|
|
@@ -67,11 +74,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
67
74
|
private stdinDataHandler?: (data: string) => void;
|
|
68
75
|
private writeLogPath = process.env.PI_TUI_WRITE_LOG || "";
|
|
69
76
|
private alternateScreenActive = false;
|
|
77
|
+
private mouseActive = false;
|
|
70
78
|
|
|
71
79
|
get kittyProtocolActive(): boolean {
|
|
72
80
|
return this._kittyProtocolActive;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
get isTmux(): boolean {
|
|
84
|
+
return !!process.env.TMUX;
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
start(onInput: (data: string) => void, onResize: () => void): void {
|
|
76
88
|
this.inputHandler = onInput;
|
|
77
89
|
this.resizeHandler = onResize;
|
|
@@ -97,10 +109,24 @@ export class ProcessTerminal implements Terminal {
|
|
|
97
109
|
process.kill(process.pid, "SIGWINCH");
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
112
|
+
// Mouse tracking (enableMouse/disableMouse) is available but NOT
|
|
113
|
+
// enabled anywhere yet. The TUI has no scroll viewport — content
|
|
114
|
+
// off-screen is only reachable via terminal scrollback (or tmux
|
|
115
|
+
// copy-mode). Enabling mouse tracking steals scroll events without
|
|
116
|
+
// providing scroll handling, making things strictly worse. Enable
|
|
117
|
+
// only after implementing a scroll viewport in the TUI.
|
|
118
|
+
|
|
119
|
+
// Enable keyboard protocol for modified key detection.
|
|
120
|
+
// tmux doesn't support the Kitty keyboard protocol but does support xterm's
|
|
121
|
+
// modifyOtherKeys. Detect tmux and use the appropriate protocol.
|
|
122
|
+
if (process.env.TMUX) {
|
|
123
|
+
this.setupTmuxInput();
|
|
124
|
+
} else {
|
|
125
|
+
// Query and enable Kitty keyboard protocol
|
|
126
|
+
// The query handler intercepts input temporarily, then installs the user's handler
|
|
127
|
+
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
128
|
+
this.queryAndEnableKittyProtocol();
|
|
129
|
+
}
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
/**
|
|
@@ -154,6 +180,22 @@ export class ProcessTerminal implements Terminal {
|
|
|
154
180
|
};
|
|
155
181
|
}
|
|
156
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Set up stdin handling for tmux without Kitty keyboard protocol.
|
|
185
|
+
*
|
|
186
|
+
* tmux doesn't support the Kitty keyboard protocol. Escape and Ctrl+C
|
|
187
|
+
* arrive as raw bytes (\x1b and \x03) which the key matching handles natively.
|
|
188
|
+
*
|
|
189
|
+
* For Shift+Enter, tmux needs `extended-keys on` and `extended-keys-format csi-u`
|
|
190
|
+
* in tmux.conf. With that config, we request modifyOtherKeys mode 1 so tmux
|
|
191
|
+
* encodes modified keys (Shift+Enter → CSI 13;2 u) while leaving standard
|
|
192
|
+
* keys (Escape, Ctrl+C, regular typing) as raw bytes.
|
|
193
|
+
*/
|
|
194
|
+
private setupTmuxInput(): void {
|
|
195
|
+
this.setupStdinBuffer();
|
|
196
|
+
process.stdin.on("data", this.stdinDataHandler!);
|
|
197
|
+
}
|
|
198
|
+
|
|
157
199
|
/**
|
|
158
200
|
* Query terminal for Kitty keyboard protocol support and enable if available.
|
|
159
201
|
*
|
|
@@ -209,6 +251,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
209
251
|
this.leaveAlternateScreen();
|
|
210
252
|
}
|
|
211
253
|
|
|
254
|
+
// Disable mouse tracking
|
|
255
|
+
this.disableMouse();
|
|
256
|
+
|
|
212
257
|
// Disable bracketed paste mode
|
|
213
258
|
process.stdout.write("\x1b[?2004l");
|
|
214
259
|
|
|
@@ -297,6 +342,26 @@ export class ProcessTerminal implements Terminal {
|
|
|
297
342
|
process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1)
|
|
298
343
|
}
|
|
299
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Enable SGR mouse tracking.
|
|
347
|
+
* Mode 1000 = button press/release (includes scroll wheel).
|
|
348
|
+
* Mode 1006 = SGR extended format (avoids 223-column limit).
|
|
349
|
+
*/
|
|
350
|
+
enableMouse(): void {
|
|
351
|
+
if (this.mouseActive) return;
|
|
352
|
+
process.stdout.write("\x1b[?1000h\x1b[?1006h");
|
|
353
|
+
this.mouseActive = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Disable mouse tracking and restore default terminal mouse handling.
|
|
358
|
+
*/
|
|
359
|
+
disableMouse(): void {
|
|
360
|
+
if (!this.mouseActive) return;
|
|
361
|
+
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
362
|
+
this.mouseActive = false;
|
|
363
|
+
}
|
|
364
|
+
|
|
300
365
|
/**
|
|
301
366
|
* Switch to alternate screen buffer and clear it.
|
|
302
367
|
* @returns void
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isKeyRelease,
|
|
10
|
+
isMouseEvent,
|
|
11
|
+
type MouseEvent,
|
|
12
|
+
matchesKey,
|
|
13
|
+
parseMouseEvent,
|
|
14
|
+
} from "./keys.js";
|
|
9
15
|
import type { Terminal } from "./terminal.js";
|
|
10
16
|
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
17
|
import {
|
|
@@ -209,6 +215,13 @@ export class TUI extends Container {
|
|
|
209
215
|
|
|
210
216
|
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
211
217
|
public onDebug?: () => void;
|
|
218
|
+
/**
|
|
219
|
+
* Callback for mouse events. Called when a mouse event is received.
|
|
220
|
+
* Scroll events are the primary use case (scroll-up, scroll-down).
|
|
221
|
+
* Return value is ignored — mouse events are always consumed and never
|
|
222
|
+
* forwarded to focused components.
|
|
223
|
+
*/
|
|
224
|
+
public onMouse?: (event: MouseEvent) => void;
|
|
212
225
|
private renderRequested = false;
|
|
213
226
|
private pendingRenderHandle?: ReturnType<typeof setTimeout>;
|
|
214
227
|
private cursorRow = 0; // Logical cursor row (end of rendered content)
|
|
@@ -220,6 +233,7 @@ export class TUI extends Container {
|
|
|
220
233
|
private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
221
234
|
private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
222
235
|
private fullRedrawCount = 0;
|
|
236
|
+
private rollingShrinkPeak = 0; // Recent peak line count for gradual shrink detection
|
|
223
237
|
private stopped = false;
|
|
224
238
|
private pendingScrollbackClear = false; // Clear scrollback on next full render (session breaks)
|
|
225
239
|
|
|
@@ -269,6 +283,19 @@ export class TUI extends Container {
|
|
|
269
283
|
this.clearOnShrink = enabled;
|
|
270
284
|
}
|
|
271
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Reset the startup grace period timer, suppressing screen-clearing full
|
|
288
|
+
* redraws for another {@link STARTUP_GRACE_MS} milliseconds.
|
|
289
|
+
*
|
|
290
|
+
* Call this at the start of a session switch so the chatContainer.clear()
|
|
291
|
+
* → renderInitialMessages() transition doesn't cause visible flicker.
|
|
292
|
+
*
|
|
293
|
+
* @returns {void}
|
|
294
|
+
*/
|
|
295
|
+
resetRenderGrace(): void {
|
|
296
|
+
this.startedAtMs = Date.now();
|
|
297
|
+
}
|
|
298
|
+
|
|
272
299
|
/**
|
|
273
300
|
* Request that the next full render clears the terminal scrollback buffer.
|
|
274
301
|
*
|
|
@@ -389,8 +416,25 @@ export class TUI extends Container {
|
|
|
389
416
|
for (const overlay of this.overlayStack) overlay.component.invalidate?.();
|
|
390
417
|
}
|
|
391
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Timestamp when `start()` was called.
|
|
421
|
+
* Used by startup grace period to suppress screen-clearing full redraws.
|
|
422
|
+
*/
|
|
423
|
+
private startedAtMs = 0;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Duration (ms) after `start()` during which shrink-triggered full redraws
|
|
427
|
+
* use a gentler line-by-line overwrite instead of screen clear.
|
|
428
|
+
*
|
|
429
|
+
* This prevents the visual flicker that occurs when session resume causes
|
|
430
|
+
* rapid content height changes (extension hooks, widget adds/removes) before
|
|
431
|
+
* the full message history is rendered.
|
|
432
|
+
*/
|
|
433
|
+
private static readonly STARTUP_GRACE_MS = 3000;
|
|
434
|
+
|
|
392
435
|
start(): void {
|
|
393
436
|
this.stopped = false;
|
|
437
|
+
this.startedAtMs = Date.now();
|
|
394
438
|
this.terminal.start(
|
|
395
439
|
(data) => this.handleInput(data),
|
|
396
440
|
() => this.requestRender()
|
|
@@ -405,6 +449,12 @@ export class TUI extends Container {
|
|
|
405
449
|
if (!getCapabilities().images) {
|
|
406
450
|
return;
|
|
407
451
|
}
|
|
452
|
+
// Skip cell size query inside tmux — tmux doesn't forward CSI 16 t responses,
|
|
453
|
+
// so cellSizeQueryPending would stay true and parseCellSizeResponse would eat
|
|
454
|
+
// bare \x1b (Escape key) as a "partial response", breaking Escape handling.
|
|
455
|
+
if (process.env.TMUX) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
408
458
|
// Query terminal for cell size in pixels: CSI 16 t
|
|
409
459
|
// Response format: CSI 6 ; height ; width t
|
|
410
460
|
this.cellSizeQueryPending = true;
|
|
@@ -434,6 +484,45 @@ export class TUI extends Container {
|
|
|
434
484
|
this.terminal.stop();
|
|
435
485
|
}
|
|
436
486
|
|
|
487
|
+
/** When >0, scheduled renders are deferred until the batch completes. */
|
|
488
|
+
private renderBatchDepth = 0;
|
|
489
|
+
|
|
490
|
+
/** Whether a render was requested while batching was active. */
|
|
491
|
+
private renderDeferredDuringBatch = false;
|
|
492
|
+
|
|
493
|
+
/** Whether a forced render was requested while batching was active. */
|
|
494
|
+
private renderForceDeferredDuringBatch = false;
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Begin a render batch — all `requestRender()` calls are coalesced and
|
|
498
|
+
* deferred until the matching `endRenderBatch()`. Nestable.
|
|
499
|
+
*
|
|
500
|
+
* Use to prevent intermediate renders (and the screen clears they cause)
|
|
501
|
+
* during multi-step UI mutations such as session resume.
|
|
502
|
+
*
|
|
503
|
+
* @returns {void}
|
|
504
|
+
*/
|
|
505
|
+
beginRenderBatch(): void {
|
|
506
|
+
this.renderBatchDepth++;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* End a render batch. When the outermost batch ends, a single render is
|
|
511
|
+
* scheduled if any were deferred.
|
|
512
|
+
*
|
|
513
|
+
* @returns {void}
|
|
514
|
+
*/
|
|
515
|
+
endRenderBatch(): void {
|
|
516
|
+
if (this.renderBatchDepth <= 0) return;
|
|
517
|
+
this.renderBatchDepth--;
|
|
518
|
+
if (this.renderBatchDepth === 0 && this.renderDeferredDuringBatch) {
|
|
519
|
+
const wasForce = this.renderForceDeferredDuringBatch;
|
|
520
|
+
this.renderDeferredDuringBatch = false;
|
|
521
|
+
this.renderForceDeferredDuringBatch = false;
|
|
522
|
+
this.requestRender(wasForce);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
437
526
|
requestRender(force = false): void {
|
|
438
527
|
if (force) {
|
|
439
528
|
this.previousLines = [];
|
|
@@ -442,6 +531,12 @@ export class TUI extends Container {
|
|
|
442
531
|
this.hardwareCursorRow = 0;
|
|
443
532
|
this.maxLinesRendered = 0;
|
|
444
533
|
this.previousViewportTop = 0;
|
|
534
|
+
this.rollingShrinkPeak = 0;
|
|
535
|
+
}
|
|
536
|
+
if (this.renderBatchDepth > 0) {
|
|
537
|
+
this.renderDeferredDuringBatch = true;
|
|
538
|
+
if (force) this.renderForceDeferredDuringBatch = true;
|
|
539
|
+
return;
|
|
445
540
|
}
|
|
446
541
|
if (this.renderRequested) return;
|
|
447
542
|
this.scheduleRender();
|
|
@@ -511,6 +606,16 @@ export class TUI extends Container {
|
|
|
511
606
|
data = filtered;
|
|
512
607
|
}
|
|
513
608
|
|
|
609
|
+
// Mouse events — intercept before any key handling.
|
|
610
|
+
// Always consumed: mouse sequences must never reach components as text.
|
|
611
|
+
if (isMouseEvent(data)) {
|
|
612
|
+
const event = parseMouseEvent(data);
|
|
613
|
+
if (event && this.onMouse) {
|
|
614
|
+
this.onMouse(event);
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
514
619
|
// Global debug key handler (Shift+Ctrl+D)
|
|
515
620
|
if (matchesKey(data, "shift+ctrl+d") && this.onDebug) {
|
|
516
621
|
this.onDebug();
|
|
@@ -556,6 +661,19 @@ export class TUI extends Container {
|
|
|
556
661
|
if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {
|
|
557
662
|
return;
|
|
558
663
|
}
|
|
664
|
+
if (process.env.TALLOW_KEY_DEBUG && (data === "\x1b" || data === "\x03")) {
|
|
665
|
+
const escMatch = matchesKey(data, "escape");
|
|
666
|
+
const ctrlcMatch = matchesKey(data, "ctrl+c");
|
|
667
|
+
const comp = this.focusedComponent as unknown as {
|
|
668
|
+
onEscape?: () => void;
|
|
669
|
+
actionHandlers?: Map<string, unknown>;
|
|
670
|
+
};
|
|
671
|
+
const hasOnEscape = typeof comp.onEscape === "function";
|
|
672
|
+
const actionCount = comp.actionHandlers instanceof Map ? comp.actionHandlers.size : -1;
|
|
673
|
+
process.stderr.write(
|
|
674
|
+
`[key] ${data === "\x1b" ? "ESC" : "C-C"} matchEsc=${escMatch} matchCC=${ctrlcMatch} hasOnEscape=${hasOnEscape} actions=${actionCount}\n`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
559
677
|
this.focusedComponent.handleInput(data);
|
|
560
678
|
this.requestRender();
|
|
561
679
|
}
|
|
@@ -960,6 +1078,11 @@ export class TUI extends Container {
|
|
|
960
1078
|
// Width changed - need full re-render (line wrapping changes)
|
|
961
1079
|
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
|
|
962
1080
|
|
|
1081
|
+
// Whether we are within the startup grace period where screen-clearing
|
|
1082
|
+
// full redraws are softened to prevent flicker during session resume.
|
|
1083
|
+
const inStartupGrace =
|
|
1084
|
+
this.startedAtMs > 0 && Date.now() - this.startedAtMs < TUI.STARTUP_GRACE_MS;
|
|
1085
|
+
|
|
963
1086
|
// Helper to clear viewport (and optionally scrollback) and render all new lines
|
|
964
1087
|
const fullRender = (clear: boolean): void => {
|
|
965
1088
|
this.fullRedrawCount += 1;
|
|
@@ -985,6 +1108,45 @@ export class TUI extends Container {
|
|
|
985
1108
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
986
1109
|
}
|
|
987
1110
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
1111
|
+
this.rollingShrinkPeak = newLines.length;
|
|
1112
|
+
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1113
|
+
this.previousLines = newLines;
|
|
1114
|
+
this.previousWidth = width;
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Gentle full redraw: home cursor + overwrite each line + clear below.
|
|
1119
|
+
*
|
|
1120
|
+
* Used during the startup grace period instead of fullRender(true) for
|
|
1121
|
+
* shrink-triggered redraws. Avoids the visible blank frame caused by
|
|
1122
|
+
* `\x1b[2J` (clear screen), which makes messages appear to flash in and
|
|
1123
|
+
* out when session resume triggers rapid content height changes.
|
|
1124
|
+
*
|
|
1125
|
+
* Unlike fullRender(true), this never clears the screen — it writes each
|
|
1126
|
+
* line with a preceding `\x1b[2K` (clear line) so stale content is
|
|
1127
|
+
* overwritten without a blank frame. Lines below the new content are
|
|
1128
|
+
* individually erased.
|
|
1129
|
+
*/
|
|
1130
|
+
const gentleFullRender = (): void => {
|
|
1131
|
+
this.fullRedrawCount += 1;
|
|
1132
|
+
let buffer = "\x1b[?2026h\x1b[H"; // Begin synchronized output + home cursor
|
|
1133
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
1134
|
+
buffer += "\x1b[2K"; // Clear current line
|
|
1135
|
+
buffer += newLines[i];
|
|
1136
|
+
if (i < newLines.length - 1) buffer += "\r\n";
|
|
1137
|
+
}
|
|
1138
|
+
// Erase lines that were previously rendered but are no longer needed
|
|
1139
|
+
const staleLines = Math.max(0, this.maxLinesRendered - newLines.length);
|
|
1140
|
+
for (let i = 0; i < staleLines; i++) {
|
|
1141
|
+
buffer += "\r\n\x1b[2K";
|
|
1142
|
+
}
|
|
1143
|
+
buffer += "\x1b[?2026l"; // End synchronized output
|
|
1144
|
+
this.terminal.write(buffer);
|
|
1145
|
+
this.cursorRow = Math.max(0, newLines.length + staleLines - 1);
|
|
1146
|
+
this.hardwareCursorRow = this.cursorRow;
|
|
1147
|
+
this.maxLinesRendered = newLines.length;
|
|
1148
|
+
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
1149
|
+
this.rollingShrinkPeak = newLines.length;
|
|
988
1150
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
989
1151
|
this.previousLines = newLines;
|
|
990
1152
|
this.previousWidth = width;
|
|
@@ -1021,7 +1183,11 @@ export class TUI extends Container {
|
|
|
1021
1183
|
this.overlayStack.length === 0
|
|
1022
1184
|
) {
|
|
1023
1185
|
logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
|
|
1024
|
-
|
|
1186
|
+
if (inStartupGrace) {
|
|
1187
|
+
gentleFullRender();
|
|
1188
|
+
} else {
|
|
1189
|
+
fullRender(true);
|
|
1190
|
+
}
|
|
1025
1191
|
return;
|
|
1026
1192
|
}
|
|
1027
1193
|
|
|
@@ -1032,7 +1198,30 @@ export class TUI extends Container {
|
|
|
1032
1198
|
const shrinkDelta = this.previousLines.length - newLines.length;
|
|
1033
1199
|
if (shrinkDelta > 5 && this.overlayStack.length === 0) {
|
|
1034
1200
|
logRedraw(`large shrink (${shrinkDelta} lines)`);
|
|
1035
|
-
|
|
1201
|
+
if (inStartupGrace) {
|
|
1202
|
+
gentleFullRender();
|
|
1203
|
+
} else {
|
|
1204
|
+
fullRender(true);
|
|
1205
|
+
}
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Rolling shrink detection: catches gradual shrinks where each individual
|
|
1210
|
+
// frame-to-frame delta is ≤5 lines (below the large-shrink threshold) but the
|
|
1211
|
+
// accumulated shrink from a recent peak exceeds it. This happens when
|
|
1212
|
+
// pollStates.clear() collapses tool-result anchors across multiple render
|
|
1213
|
+
// cycles while animations (loader, widget spinners) keep triggering renders.
|
|
1214
|
+
if (newLines.length >= this.rollingShrinkPeak) {
|
|
1215
|
+
this.rollingShrinkPeak = newLines.length;
|
|
1216
|
+
} else if (this.overlayStack.length === 0 && this.rollingShrinkPeak - newLines.length > 5) {
|
|
1217
|
+
logRedraw(
|
|
1218
|
+
`rolling shrink (peak=${this.rollingShrinkPeak}, now=${newLines.length}, delta=${this.rollingShrinkPeak - newLines.length})`
|
|
1219
|
+
);
|
|
1220
|
+
if (inStartupGrace) {
|
|
1221
|
+
gentleFullRender();
|
|
1222
|
+
} else {
|
|
1223
|
+
fullRender(true);
|
|
1224
|
+
}
|
|
1036
1225
|
return;
|
|
1037
1226
|
}
|
|
1038
1227
|
|
|
@@ -1107,7 +1296,11 @@ export class TUI extends Container {
|
|
|
1107
1296
|
const extraLines = this.previousLines.length - newLines.length;
|
|
1108
1297
|
if (extraLines > height) {
|
|
1109
1298
|
logRedraw(`extraLines > height (${extraLines} > ${height})`);
|
|
1110
|
-
|
|
1299
|
+
if (inStartupGrace) {
|
|
1300
|
+
gentleFullRender();
|
|
1301
|
+
} else {
|
|
1302
|
+
fullRender(true);
|
|
1303
|
+
}
|
|
1111
1304
|
return;
|
|
1112
1305
|
}
|
|
1113
1306
|
if (extraLines > 0) {
|
|
@@ -1135,7 +1328,11 @@ export class TUI extends Container {
|
|
|
1135
1328
|
// If first changed line is above the current viewport basis, partial redraw is unsafe.
|
|
1136
1329
|
if (firstChanged < prevViewportTop) {
|
|
1137
1330
|
logRedraw(`firstChanged < viewportTop (${firstChanged} < ${prevViewportTop})`);
|
|
1138
|
-
|
|
1331
|
+
if (inStartupGrace) {
|
|
1332
|
+
gentleFullRender();
|
|
1333
|
+
} else {
|
|
1334
|
+
fullRender(true);
|
|
1335
|
+
}
|
|
1139
1336
|
return;
|
|
1140
1337
|
}
|
|
1141
1338
|
|
|
@@ -1306,6 +1503,9 @@ export class TUI extends Container {
|
|
|
1306
1503
|
this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
|
|
1307
1504
|
}
|
|
1308
1505
|
this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
|
|
1506
|
+
// Update rolling peak for gradual shrink detection (partial path only —
|
|
1507
|
+
// fullRender paths reset it inside the fullRender closure).
|
|
1508
|
+
this.rollingShrinkPeak = Math.max(this.rollingShrinkPeak, newLines.length);
|
|
1309
1509
|
|
|
1310
1510
|
// Position hardware cursor for IME
|
|
1311
1511
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dungle-scrubs/tallow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "An opinionated coding agent. Built on pi.",
|
|
5
5
|
"piConfig": {
|
|
6
6
|
"name": "tallow",
|
|
@@ -74,25 +74,25 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@clack/prompts": "^1.1.0",
|
|
77
|
-
"@dungle-scrubs/synapse": "0.1.
|
|
78
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
79
|
-
"@mariozechner/pi-tui": "^0.
|
|
77
|
+
"@dungle-scrubs/synapse": "0.1.8",
|
|
78
|
+
"@mariozechner/pi-coding-agent": "^0.64.0",
|
|
79
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
80
80
|
"@opentelemetry/api": "^1.9.0",
|
|
81
81
|
"@sinclair/typebox": "0.34.48",
|
|
82
82
|
"ai": "^6.0.116",
|
|
83
83
|
"commander": "^14.0.3",
|
|
84
84
|
"unpdf": "^1.4.0",
|
|
85
|
-
"vscode-jsonrpc": "8.2.1"
|
|
85
|
+
"vscode-jsonrpc": "8.2.1",
|
|
86
|
+
"vscode-languageserver-protocol": "3.17.5"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@biomejs/biome": "2.4.2",
|
|
89
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
90
|
-
"@mariozechner/pi-ai": "^0.
|
|
90
|
+
"@mariozechner/pi-agent-core": "^0.64.0",
|
|
91
|
+
"@mariozechner/pi-ai": "^0.64.0",
|
|
91
92
|
"@types/node": "25.2.3",
|
|
92
93
|
"husky": "^9.1.7",
|
|
93
94
|
"lint-staged": "^16.4.0",
|
|
94
|
-
"typescript": "^5.9.3"
|
|
95
|
-
"vscode-languageserver-protocol": "3.17.5"
|
|
95
|
+
"typescript": "^5.9.3"
|
|
96
96
|
},
|
|
97
97
|
"engines": {
|
|
98
98
|
"node": ">=22"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveRuntimeModuleUrl } from "./resolve-module.js";
|
|
2
|
+
|
|
3
|
+
const mod = (await import(
|
|
4
|
+
resolveRuntimeModuleUrl("model-metadata-overrides.js")
|
|
5
|
+
)) as typeof import("../src/model-metadata-overrides.js");
|
|
6
|
+
|
|
7
|
+
export const applyKnownModelMetadataOverrides = mod.applyKnownModelMetadataOverrides;
|
|
@@ -429,11 +429,6 @@
|
|
|
429
429
|
"default": ["◐", "◓", "◑", "◒"],
|
|
430
430
|
"minItems": 1
|
|
431
431
|
},
|
|
432
|
-
"plan_mode": {
|
|
433
|
-
"type": "string",
|
|
434
|
-
"description": "Plan mode indicator (default: '⏸').",
|
|
435
|
-
"default": "⏸"
|
|
436
|
-
},
|
|
437
432
|
"task_list": {
|
|
438
433
|
"type": "string",
|
|
439
434
|
"description": "Task list indicator (default: '📋').",
|