klyro 0.1.45 → 0.1.47
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/dist/cli/repl.js +115 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +312 -121
- package/dist/tui/app.test.js +67 -0
- package/dist/tui/measure.d.ts +85 -0
- package/dist/tui/measure.js +186 -0
- package/dist/tui/mouse.d.ts +36 -0
- package/dist/tui/mouse.js +94 -0
- package/dist/tui/scroll-flow.test.d.ts +1 -0
- package/dist/tui/scroll-flow.test.js +94 -0
- package/dist/tui/scroll-model.d.ts +67 -0
- package/dist/tui/scroll-model.js +99 -0
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -19,6 +19,7 @@ import { TuiApprovalBridge } from '../tui/approval.js';
|
|
|
19
19
|
import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
20
20
|
import { parse } from './slash/parser.js';
|
|
21
21
|
import { resolveProvider, providerHelp } from '../providers.js';
|
|
22
|
+
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE } from '../tui/mouse.js';
|
|
22
23
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
23
24
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
24
25
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
@@ -85,7 +86,25 @@ export async function startRepl(opts = {}) {
|
|
|
85
86
|
const pendingQueue = [];
|
|
86
87
|
let isMounted = false;
|
|
87
88
|
let directHooks;
|
|
89
|
+
// Plain-text mirror for exit replay (scroll.md §1.2: session survives in
|
|
90
|
+
// native scrollback after the alt screen is torn down). Cap 300 lines.
|
|
91
|
+
const exitMirror = [];
|
|
92
|
+
function mirrorLine(item) {
|
|
93
|
+
let line = null;
|
|
94
|
+
if (item.kind === 'text')
|
|
95
|
+
line = `${item.role === 'user' ? '> ' : ''}${item.text}`;
|
|
96
|
+
else if (item.kind === 'error')
|
|
97
|
+
line = `[error] ${item.message}`;
|
|
98
|
+
else if (item.kind === 'file_changed')
|
|
99
|
+
line = `[${item.op}] ${item.path}`;
|
|
100
|
+
if (line === null)
|
|
101
|
+
return;
|
|
102
|
+
exitMirror.push(line.slice(0, 2000));
|
|
103
|
+
if (exitMirror.length > 300)
|
|
104
|
+
exitMirror.splice(0, exitMirror.length - 300);
|
|
105
|
+
}
|
|
88
106
|
function queuedAppend(item) {
|
|
107
|
+
mirrorLine(item);
|
|
89
108
|
if (isMounted && directHooks)
|
|
90
109
|
directHooks.append(item);
|
|
91
110
|
else
|
|
@@ -121,6 +140,7 @@ export async function startRepl(opts = {}) {
|
|
|
121
140
|
try {
|
|
122
141
|
process.stdout.write('\x1b[?1049h\x1b[?25l'); // alt screen + hide cursor
|
|
123
142
|
process.stdout.write('\x1b[H\x1b[2J'); // home + clear
|
|
143
|
+
process.stdout.write(MOUSE_ENABLE); // wheel events (SGR), see tui/mouse.ts
|
|
124
144
|
}
|
|
125
145
|
catch { /* ignore */ }
|
|
126
146
|
};
|
|
@@ -128,10 +148,45 @@ export async function startRepl(opts = {}) {
|
|
|
128
148
|
if (!isAltScreen)
|
|
129
149
|
return;
|
|
130
150
|
try {
|
|
151
|
+
process.stdout.write(MOUSE_DISABLE);
|
|
131
152
|
process.stdout.write('\x1b[?25h\x1b[?1049l'); // show cursor + leave alt
|
|
132
153
|
}
|
|
133
154
|
catch { /* ignore */ }
|
|
134
155
|
};
|
|
156
|
+
// OpenCode-style wheel scrolling (§8.5, S8): Ink owns stdin and cannot see
|
|
157
|
+
// mouse events, so tap stdin.emit — wheel deltas drive the App's scroll
|
|
158
|
+
// hooks, everything else passes through to Ink untouched.
|
|
159
|
+
const mouseFilter = new MouseFilter();
|
|
160
|
+
const origStdinEmit = process.stdin.emit.bind(process.stdin);
|
|
161
|
+
let mouseTapInstalled = false;
|
|
162
|
+
function installMouseTap() {
|
|
163
|
+
if (!isAltScreen || mouseTapInstalled)
|
|
164
|
+
return;
|
|
165
|
+
mouseTapInstalled = true;
|
|
166
|
+
process.stdin.emit = (...a) => {
|
|
167
|
+
if (a[0] === 'data' && Buffer.isBuffer(a[1])) {
|
|
168
|
+
const split = mouseFilter.push(a[1]);
|
|
169
|
+
for (const w of split.wheels) {
|
|
170
|
+
try {
|
|
171
|
+
if (isMounted && directHooks)
|
|
172
|
+
directHooks.scrollLines(w);
|
|
173
|
+
}
|
|
174
|
+
catch { /* ignore */ }
|
|
175
|
+
}
|
|
176
|
+
if (split.kept.length === 0)
|
|
177
|
+
return false;
|
|
178
|
+
a[1] = split.kept;
|
|
179
|
+
}
|
|
180
|
+
return origStdinEmit(...a);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function removeMouseTap() {
|
|
184
|
+
if (!mouseTapInstalled)
|
|
185
|
+
return;
|
|
186
|
+
mouseTapInstalled = false;
|
|
187
|
+
mouseFilter.reset();
|
|
188
|
+
process.stdin.emit = origStdinEmit;
|
|
189
|
+
}
|
|
135
190
|
// Declare app before handler to avoid TDZ; handler added after render
|
|
136
191
|
let app;
|
|
137
192
|
let sigintHandler;
|
|
@@ -140,6 +195,50 @@ export async function startRepl(opts = {}) {
|
|
|
140
195
|
let tuiSessionId;
|
|
141
196
|
if (isAltScreen)
|
|
142
197
|
enterAlt();
|
|
198
|
+
// I7 (scroll.md §8.6, S8): while the TUI owns stdout, route console.*
|
|
199
|
+
// to a ring buffer + ~/.klyro/debug.log so stray tool/provider logs
|
|
200
|
+
// can't corrupt the frame. Restored on exit.
|
|
201
|
+
const consoleRing = [];
|
|
202
|
+
const origConsoleFns = {
|
|
203
|
+
log: console.log,
|
|
204
|
+
info: console.info,
|
|
205
|
+
warn: console.warn,
|
|
206
|
+
error: console.error,
|
|
207
|
+
debug: console.debug,
|
|
208
|
+
};
|
|
209
|
+
function patchConsole() {
|
|
210
|
+
if (!isAltScreen)
|
|
211
|
+
return;
|
|
212
|
+
const sink = (...args) => {
|
|
213
|
+
const line = `[${new Date().toISOString()}] ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}`;
|
|
214
|
+
consoleRing.push(line);
|
|
215
|
+
if (consoleRing.length > 200)
|
|
216
|
+
consoleRing.splice(0, consoleRing.length - 200);
|
|
217
|
+
try {
|
|
218
|
+
const fs = require('node:fs');
|
|
219
|
+
const path = require('node:path');
|
|
220
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? cwd;
|
|
221
|
+
const dir = path.join(home, '.klyro');
|
|
222
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
223
|
+
fs.appendFileSync(path.join(dir, 'debug.log'), line + '\n');
|
|
224
|
+
}
|
|
225
|
+
catch { /* ignore */ }
|
|
226
|
+
};
|
|
227
|
+
console.log = sink;
|
|
228
|
+
console.info = sink;
|
|
229
|
+
console.warn = sink;
|
|
230
|
+
console.error = sink;
|
|
231
|
+
console.debug = sink;
|
|
232
|
+
}
|
|
233
|
+
function restoreConsole() {
|
|
234
|
+
console.log = origConsoleFns.log;
|
|
235
|
+
console.info = origConsoleFns.info;
|
|
236
|
+
console.warn = origConsoleFns.warn;
|
|
237
|
+
console.error = origConsoleFns.error;
|
|
238
|
+
console.debug = origConsoleFns.debug;
|
|
239
|
+
}
|
|
240
|
+
patchConsole();
|
|
241
|
+
installMouseTap();
|
|
143
242
|
const EFFORT_STEPS = { low: 10, medium: 30, high: 50, max: 100 };
|
|
144
243
|
// P1 session/permission state (commands.md Priority 1)
|
|
145
244
|
let sessionLabel = '';
|
|
@@ -278,6 +377,7 @@ export async function startRepl(opts = {}) {
|
|
|
278
377
|
leaveAlt();
|
|
279
378
|
};
|
|
280
379
|
process.once('SIGINT', sigintHandler);
|
|
380
|
+
process.once('SIGTERM', sigintHandler);
|
|
281
381
|
async function runWithBridge(text) {
|
|
282
382
|
if (!model) {
|
|
283
383
|
queuedAppend({ id: `err-${Date.now()}`, kind: 'error', message: 'no model configured' });
|
|
@@ -2092,9 +2192,23 @@ export async function startRepl(opts = {}) {
|
|
|
2092
2192
|
// ac.aborted indicates SIGINT; return 130 (128+SIGINT) like shells do.
|
|
2093
2193
|
return new Promise((resolve) => {
|
|
2094
2194
|
const onExit = () => {
|
|
2095
|
-
if (sigintHandler)
|
|
2195
|
+
if (sigintHandler) {
|
|
2096
2196
|
process.removeListener('SIGINT', sigintHandler);
|
|
2197
|
+
process.removeListener('SIGTERM', sigintHandler);
|
|
2198
|
+
}
|
|
2199
|
+
restoreConsole();
|
|
2200
|
+
removeMouseTap();
|
|
2097
2201
|
leaveAlt();
|
|
2202
|
+
// §1.2 exit behavior: replay a plain-text transcript into the main
|
|
2203
|
+
// buffer so the session survives in native scrollback.
|
|
2204
|
+
if (exitMirror.length > 0) {
|
|
2205
|
+
try {
|
|
2206
|
+
process.stdout.write('\n--- klyro session transcript ---\n');
|
|
2207
|
+
for (const line of exitMirror.slice(-100))
|
|
2208
|
+
process.stdout.write(line + '\n');
|
|
2209
|
+
}
|
|
2210
|
+
catch { /* ignore */ }
|
|
2211
|
+
}
|
|
2098
2212
|
resolve(ac.signal.aborted ? 130 : 0);
|
|
2099
2213
|
};
|
|
2100
2214
|
if (!app) {
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface AppProps {
|
|
|
22
22
|
updateStatus: (s: Partial<StatusSnapshot>) => void;
|
|
23
23
|
updatePlan: (p: PlanStep[]) => void;
|
|
24
24
|
clearTranscript: () => void;
|
|
25
|
+
scrollLines: (delta: number) => void;
|
|
26
|
+
scrollToBottom: () => void;
|
|
25
27
|
}) => void;
|
|
26
28
|
version?: string;
|
|
27
29
|
isFullscreen?: boolean;
|