@xp266/dshtui 0.1.5 → 0.1.7
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/lib/dialog.mjs +1 -1
- package/lib/index.mjs +23 -106
- package/lib/{surface-B7w3R_kh.mjs → surface-DSDGGCSN.mjs} +150 -137
- package/package.json +1 -1
- package/src/chat/session-list.ts +29 -139
- package/src/terminal/cursor-shape.ts +4 -1
- package/src/terminal/mouse.ts +19 -2
- package/src/ui/app.tsx +6 -0
package/lib/dialog.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./prod-react-Dzof4qJx.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { L as panelAnchorRow, c as asTextItem, i as Dialog, l as registerWidget, n as panelLegend, o as rowHeight, r as CloseGuardContext, s as wrapStatusLines, t as PanelSurface } from "./surface-DSDGGCSN.mjs";
|
|
3
3
|
export { CloseGuardContext, Dialog, PanelSurface, asTextItem, panelAnchorRow, panelLegend, registerWidget, rowHeight, wrapStatusLines };
|
package/lib/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./prod-react-Dzof4qJx.mjs";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as colToCharIndex, A as writeCursorShape, At as colorLevel, B as hintBlockTop, C as useCaret, Ct as permissionModeInfo, D as SelectableText, Dt as subscribePalettes, E as markKeymapConsumed, Et as setThemeMode, F as inputContentContains, Ft as installCrashHandlers, G as segmentsKey, H as inputStatusRow, I as messageBackgroundWidth, It as warn, J as sliceByColumns, K as wrapSegments, L as panelAnchorRow, M as isMouseResidue, Mt as env, N as writeMouseDisable, Nt as configureLogs, O as SelectionContext, Ot as themeMode, P as hintSpan, Pt as error, Q as charWidth, R as panelContains, S as caretNonceText, St as paletteColor, T as isKeyConsumed, Tt as replacePalettes, U as glyphs, V as inputFrameTop, W as mergeRuns, X as selectedRange, Y as clampFocusRow, Z as toScreenSelection, _ as editDeleteWordRight, _t as registerFieldKind, a as DIALOG_MAX_HEIGHT, at as truncate, b as caretNonceBold, bt as specialFieldFactory, ct as charactersChipLabel, d as editCursorLeft, dt as fieldStyleOf, et as errorText, f as editCursorRight, ft as hasFieldChar, g as editDeleteWordLeft, gt as linesChipLabel, h as editDelete, ht as isFieldChar, i as Dialog, it as textWidth, j as createMouseController, jt as setColorLevel, k as Region, kt as keyedRegistry, l as registerWidget, lt as fieldExpandOf, m as editCursorWordRight, mt as imageChipLabel, n as panelLegend, nt as locToPoint, ot as wrapLines, p as editCursorWordLeft, pt as hasFieldSlots, q as chromeSelectionText, r as CloseGuardContext, rt as segmentGraphemes, st as allocateField, t as PanelSurface, tt as lineBreaks, u as editBackspace, ut as fieldSlotOf, v as editInsert, vt as releaseFields, w as beginKeyEvent, wt as registerPalette, x as caretNonceColor, xt as COLORS, y as CapText, yt as releaseUnreferenced, z as scrollbarColumn } from "./surface-DSDGGCSN.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
@@ -7197,9 +7197,9 @@ function sessionTime(session) {
|
|
|
7197
7197
|
* with the client-side visibility rules: live sessions plus persisted cold
|
|
7198
7198
|
* sessions with a cwd, blank and subagent-origin rows hidden, newest-first,
|
|
7199
7199
|
* without any workspace filtering (the picker labels ungrouped sessions).
|
|
7200
|
-
* Cold rows
|
|
7201
|
-
*
|
|
7202
|
-
*
|
|
7200
|
+
* Cold rows take their titles from the host's batched session-title fold
|
|
7201
|
+
* (`sessionQuery.readTitleSnapshots`), which reads each log once per
|
|
7202
|
+
* release of the query corpus and never loads full sessions into memory.
|
|
7203
7203
|
*/
|
|
7204
7204
|
async function computeSessionList(ctx) {
|
|
7205
7205
|
try {
|
|
@@ -7224,8 +7224,7 @@ async function computeSessionListInner(ctx) {
|
|
|
7224
7224
|
const title = titleService?.get?.(session)?.title ?? firstUserText(session.snapshotEvents());
|
|
7225
7225
|
summaries.push(toSummary(id, title, session.header.cwd, session.header.createdAt, lastPromptAt(session.snapshotEvents()), workspacePaths));
|
|
7226
7226
|
}
|
|
7227
|
-
|
|
7228
|
-
await mergeColdSummaries(ctx, persistence, await listColdHeaders(ctx, persistence, attached), archived, workspacePaths, summaries);
|
|
7227
|
+
await mergeColdSummaries(ctx, await listColdHeaders(ctx, ctx.get("sessionPersistence"), attached), archived, workspacePaths, summaries);
|
|
7229
7228
|
await attachModifiedTimes(ctx, summaries);
|
|
7230
7229
|
return summaries.sort((a, b) => sessionTime(b) - sessionTime(a));
|
|
7231
7230
|
}
|
|
@@ -7286,102 +7285,26 @@ function lastPromptAt(events) {
|
|
|
7286
7285
|
for (const event of events) if (event.type === "user/message" && event.data.source.kind === "user") last = event.time;
|
|
7287
7286
|
return last;
|
|
7288
7287
|
}
|
|
7289
|
-
|
|
7290
|
-
const
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
function listMetadataHint(values) {
|
|
7303
|
-
const hint = values.sessionListMetadata;
|
|
7304
|
-
if (typeof hint?.blank !== "boolean") return void 0;
|
|
7305
|
-
return {
|
|
7306
|
-
blank: hint.blank,
|
|
7307
|
-
lastPromptAt: typeof hint.lastPromptAt === "number" ? hint.lastPromptAt : null
|
|
7308
|
-
};
|
|
7309
|
-
}
|
|
7310
|
-
/**
|
|
7311
|
-
* Cold-session metadata without unconditional full-log reads: projection
|
|
7312
|
-
* hints first (zero I/O), then a direct read only when the artifact is small
|
|
7313
|
-
* enough to be a blank-session log. A hint-less oversized artifact is
|
|
7314
|
-
* conservatively visible; its title comes from the projection cache's cold
|
|
7315
|
-
* ladder, which reads the log once and writes the row back durably.
|
|
7316
|
-
*/
|
|
7317
|
-
async function resolveColdMeta(cache, persistence, header) {
|
|
7318
|
-
let titleHint;
|
|
7319
|
-
let metadata;
|
|
7320
|
-
try {
|
|
7321
|
-
const values = cache?.cachedSnapshot?.(header)?.values ?? {};
|
|
7322
|
-
if (typeof values.title === "string" && values.title !== "") titleHint = values.title;
|
|
7323
|
-
metadata = listMetadataHint(values);
|
|
7324
|
-
} catch {}
|
|
7325
|
-
if (metadata?.blank === false) return {
|
|
7326
|
-
title: await coldTitle(cache, header, titleHint),
|
|
7327
|
-
blank: false,
|
|
7328
|
-
promptAt: metadata.lastPromptAt ?? 0
|
|
7329
|
-
};
|
|
7330
|
-
const probed = await probeSmallArtifact(persistence, header);
|
|
7331
|
-
if (probed !== void 0) return {
|
|
7332
|
-
title: probed.title ?? titleHint,
|
|
7333
|
-
blank: probed.blank,
|
|
7334
|
-
promptAt: probed.promptAt
|
|
7335
|
-
};
|
|
7336
|
-
return {
|
|
7337
|
-
title: await coldTitle(cache, header, titleHint),
|
|
7338
|
-
blank: false,
|
|
7339
|
-
promptAt: metadata?.lastPromptAt ?? 0
|
|
7340
|
-
};
|
|
7341
|
-
}
|
|
7342
|
-
async function coldTitle(cache, header, titleHint) {
|
|
7343
|
-
if (titleHint !== void 0 || cache?.coldSnapshot === void 0) return titleHint;
|
|
7344
|
-
try {
|
|
7345
|
-
const title = (await cache.coldSnapshot(String(header.id)))?.values?.title;
|
|
7346
|
-
return typeof title === "string" && title !== "" ? title : void 0;
|
|
7347
|
-
} catch {
|
|
7348
|
-
return;
|
|
7349
|
-
}
|
|
7350
|
-
}
|
|
7351
|
-
async function probeSmallArtifact(persistence, header) {
|
|
7352
|
-
if (persistence.locate === void 0 || persistence.readFrom === void 0) return void 0;
|
|
7353
|
-
const location = persistence.locate(header);
|
|
7354
|
-
if (location === void 0) return void 0;
|
|
7355
|
-
try {
|
|
7356
|
-
if ((await stat(location.path)).size > COLD_PROBE_MAX_BYTES) return void 0;
|
|
7357
|
-
const { events } = await persistence.readFrom(String(header.id), 0);
|
|
7358
|
-
return {
|
|
7359
|
-
blank: isBlankSession(events),
|
|
7360
|
-
title: titleFromEvents(events) ?? firstUserText(events),
|
|
7361
|
-
promptAt: lastPromptAt(events)
|
|
7362
|
-
};
|
|
7363
|
-
} catch {
|
|
7364
|
-
return;
|
|
7288
|
+
async function mergeColdSummaries(ctx, cold, archived, workspacePaths, summaries) {
|
|
7289
|
+
const query = ctx.get("sessionQuery");
|
|
7290
|
+
if (query?.readTitleSnapshots === void 0) return;
|
|
7291
|
+
const observed = await query.readTitleSnapshots(cold.map((header) => String(header.id))).catch(() => []);
|
|
7292
|
+
const titleById = /* @__PURE__ */ new Map();
|
|
7293
|
+
for (const entry of observed) {
|
|
7294
|
+
if (entry.status !== "fulfilled") continue;
|
|
7295
|
+
const snapshot = entry.value?.title;
|
|
7296
|
+
if (snapshot?.title === void 0 || snapshot.title === "") continue;
|
|
7297
|
+
titleById.set(String(entry.sessionId), {
|
|
7298
|
+
title: snapshot.title,
|
|
7299
|
+
promptAt: snapshot.updatedAt ?? 0
|
|
7300
|
+
});
|
|
7365
7301
|
}
|
|
7366
|
-
}
|
|
7367
|
-
async function mergeColdSummaries(ctx, persistence, cold, archived, workspacePaths, summaries) {
|
|
7368
|
-
if (persistence === void 0) return;
|
|
7369
|
-
const cache = ctx.get("sessionProjectionCache");
|
|
7370
|
-
const pending = [];
|
|
7371
7302
|
for (const header of cold) {
|
|
7372
7303
|
if (archived.has(String(header.id))) continue;
|
|
7373
|
-
const
|
|
7374
|
-
if (
|
|
7375
|
-
|
|
7376
|
-
continue;
|
|
7377
|
-
}
|
|
7378
|
-
pending.push(header);
|
|
7304
|
+
const meta = titleById.get(String(header.id));
|
|
7305
|
+
if (meta === void 0) continue;
|
|
7306
|
+
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths));
|
|
7379
7307
|
}
|
|
7380
|
-
for (let index = 0; index < pending.length; index += PROBE_BATCH_SIZE) await Promise.all(pending.slice(index, index + PROBE_BATCH_SIZE).map(async (header) => {
|
|
7381
|
-
const meta = await resolveColdMeta(cache, persistence, header);
|
|
7382
|
-
cacheSessionMeta(String(header.id), meta);
|
|
7383
|
-
if (!meta.blank) summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths));
|
|
7384
|
-
}));
|
|
7385
7308
|
}
|
|
7386
7309
|
function fallbackName(id, directory) {
|
|
7387
7310
|
if (directory !== void 0 && directory !== "") {
|
|
@@ -7390,14 +7313,6 @@ function fallbackName(id, directory) {
|
|
|
7390
7313
|
}
|
|
7391
7314
|
return id.match(/[^/]+$/)?.[0] ?? id;
|
|
7392
7315
|
}
|
|
7393
|
-
function titleFromEvents(events) {
|
|
7394
|
-
let title;
|
|
7395
|
-
for (const event of events) if (event.type === "session/title") {
|
|
7396
|
-
const data = event.data;
|
|
7397
|
-
if (data?.title !== void 0 && data.title !== "") title = data.title;
|
|
7398
|
-
}
|
|
7399
|
-
return title;
|
|
7400
|
-
}
|
|
7401
7316
|
function firstUserText(events) {
|
|
7402
7317
|
for (const event of events) {
|
|
7403
7318
|
if (event.type !== "user/message") continue;
|
|
@@ -9227,6 +9142,8 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9227
9142
|
}
|
|
9228
9143
|
if (!exiting.current) {
|
|
9229
9144
|
exiting.current = true;
|
|
9145
|
+
writeCursorShape("reset");
|
|
9146
|
+
writeMouseDisable();
|
|
9230
9147
|
process.kill(process.pid, "SIGINT");
|
|
9231
9148
|
setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref();
|
|
9232
9149
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
|
|
@@ -1298,6 +1298,154 @@ function scrollbarColumn(columns) {
|
|
|
1298
1298
|
function messageBackgroundWidth(width) {
|
|
1299
1299
|
return width - 2 - 3 - 1;
|
|
1300
1300
|
}
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/terminal/mouse.ts
|
|
1303
|
+
const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
|
|
1304
|
+
const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
|
|
1305
|
+
/**
|
|
1306
|
+
* Terminal-mode sequences must reach the device even when the process exits
|
|
1307
|
+
* right after: on Windows, TTY writes through process.stdout are
|
|
1308
|
+
* asynchronous and a trailing restore sequence is silently dropped, leaving
|
|
1309
|
+
* the terminal in mouse-tracking mode. fd 1 writes are synchronous.
|
|
1310
|
+
*/
|
|
1311
|
+
function writeStdoutRaw(text) {
|
|
1312
|
+
try {
|
|
1313
|
+
writeSync(1, text);
|
|
1314
|
+
} catch {
|
|
1315
|
+
process.stdout.write(text);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function writeMouseEnable() {
|
|
1319
|
+
writeStdoutRaw(MOUSE_ENABLE_SEQUENCES);
|
|
1320
|
+
}
|
|
1321
|
+
function writeMouseDisable() {
|
|
1322
|
+
writeStdoutRaw(MOUSE_DISABLE_SEQUENCES);
|
|
1323
|
+
}
|
|
1324
|
+
const MAX_BUFFER = 64;
|
|
1325
|
+
const SGR_PREFIX = "\x1B[<";
|
|
1326
|
+
function partialPrefixLength(buffer) {
|
|
1327
|
+
for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
|
|
1328
|
+
return 0;
|
|
1329
|
+
}
|
|
1330
|
+
function createMouseParser(onEvent) {
|
|
1331
|
+
let buffer = "";
|
|
1332
|
+
const pressedButtons = /* @__PURE__ */ new Set();
|
|
1333
|
+
function parseSequenceAt(str, offset) {
|
|
1334
|
+
if (!str.startsWith("\x1B[<", offset)) return null;
|
|
1335
|
+
let index = offset + 3;
|
|
1336
|
+
const values = [];
|
|
1337
|
+
let current = 0;
|
|
1338
|
+
let hasDigit = false;
|
|
1339
|
+
let terminator = "";
|
|
1340
|
+
for (; index < str.length; index++) {
|
|
1341
|
+
const ch = str[index];
|
|
1342
|
+
if (ch >= "0" && ch <= "9") {
|
|
1343
|
+
current = current * 10 + (ch.charCodeAt(0) - 48);
|
|
1344
|
+
hasDigit = true;
|
|
1345
|
+
} else if (ch === ";" && hasDigit) {
|
|
1346
|
+
values.push(current);
|
|
1347
|
+
current = 0;
|
|
1348
|
+
hasDigit = false;
|
|
1349
|
+
} else if (ch === "M" || ch === "m") {
|
|
1350
|
+
if (!hasDigit) return null;
|
|
1351
|
+
values.push(current);
|
|
1352
|
+
terminator = ch;
|
|
1353
|
+
break;
|
|
1354
|
+
} else return null;
|
|
1355
|
+
}
|
|
1356
|
+
if (terminator === "") return null;
|
|
1357
|
+
if (values.length !== 3) return null;
|
|
1358
|
+
return {
|
|
1359
|
+
event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
|
|
1360
|
+
consumed: index - offset + 1
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
return { feed(chunk) {
|
|
1364
|
+
buffer += chunk;
|
|
1365
|
+
while (true) {
|
|
1366
|
+
const start = buffer.indexOf("\x1B[<");
|
|
1367
|
+
if (start === -1) {
|
|
1368
|
+
const keep = partialPrefixLength(buffer);
|
|
1369
|
+
buffer = keep === 0 ? "" : buffer.slice(-keep);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (start > 0) {
|
|
1373
|
+
buffer = buffer.slice(start);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (buffer.length > MAX_BUFFER) {
|
|
1377
|
+
buffer = "";
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const parsed = parseSequenceAt(buffer, 0);
|
|
1381
|
+
if (parsed === null) return;
|
|
1382
|
+
const { event, consumed } = parsed;
|
|
1383
|
+
buffer = buffer.slice(consumed);
|
|
1384
|
+
onEvent(event);
|
|
1385
|
+
}
|
|
1386
|
+
} };
|
|
1387
|
+
}
|
|
1388
|
+
function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
|
|
1389
|
+
const button = rawButtonCode & 3;
|
|
1390
|
+
const isScroll = (rawButtonCode & 64) !== 0;
|
|
1391
|
+
const isMotion = (rawButtonCode & 32) !== 0;
|
|
1392
|
+
const modifiers = {
|
|
1393
|
+
shift: (rawButtonCode & 4) !== 0,
|
|
1394
|
+
alt: (rawButtonCode & 8) !== 0,
|
|
1395
|
+
ctrl: (rawButtonCode & 16) !== 0
|
|
1396
|
+
};
|
|
1397
|
+
let type;
|
|
1398
|
+
let scrollDirection;
|
|
1399
|
+
if (isScroll) {
|
|
1400
|
+
type = "scroll";
|
|
1401
|
+
scrollDirection = button === 0 ? "up" : "down";
|
|
1402
|
+
} else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
|
|
1403
|
+
else if (pressRelease === "M") {
|
|
1404
|
+
type = "down";
|
|
1405
|
+
if (button !== 3) pressed.add(button);
|
|
1406
|
+
} else {
|
|
1407
|
+
type = "up";
|
|
1408
|
+
if (button === 3) pressed.clear();
|
|
1409
|
+
else pressed.delete(button);
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
type,
|
|
1413
|
+
button: button === 3 ? 0 : button,
|
|
1414
|
+
x: wireX - 1,
|
|
1415
|
+
y: wireY - 1,
|
|
1416
|
+
modifiers,
|
|
1417
|
+
...scrollDirection === void 0 ? {} : { scrollDirection }
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function isMouseResidue(input) {
|
|
1421
|
+
return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
|
|
1422
|
+
}
|
|
1423
|
+
function createMouseController(onEvent) {
|
|
1424
|
+
const parser = createMouseParser(onEvent);
|
|
1425
|
+
const onData = (chunk) => parser.feed(chunk.toString("latin1"));
|
|
1426
|
+
return {
|
|
1427
|
+
enable() {
|
|
1428
|
+
writeMouseEnable();
|
|
1429
|
+
process.stdin.on("data", onData);
|
|
1430
|
+
},
|
|
1431
|
+
disable() {
|
|
1432
|
+
writeMouseDisable();
|
|
1433
|
+
process.stdin.off("data", onData);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
//#endregion
|
|
1438
|
+
//#region src/terminal/cursor-shape.ts
|
|
1439
|
+
const SEQUENCES = {
|
|
1440
|
+
beam: "\x1B[1 q",
|
|
1441
|
+
block: "\x1B[2 q",
|
|
1442
|
+
reset: "\x1B[0 q",
|
|
1443
|
+
hide: "\x1B[?25l",
|
|
1444
|
+
show: "\x1B[?25h"
|
|
1445
|
+
};
|
|
1446
|
+
function writeCursorShape(shape) {
|
|
1447
|
+
writeStdoutRaw(SEQUENCES[shape]);
|
|
1448
|
+
}
|
|
1301
1449
|
const RegionContext = createContext({
|
|
1302
1450
|
x: 0,
|
|
1303
1451
|
y: 0
|
|
@@ -1435,129 +1583,6 @@ function renderSegments(segments, start, end, baseColor) {
|
|
|
1435
1583
|
return out;
|
|
1436
1584
|
}
|
|
1437
1585
|
//#endregion
|
|
1438
|
-
//#region src/terminal/mouse.ts
|
|
1439
|
-
const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
|
|
1440
|
-
const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
|
|
1441
|
-
function writeMouseEnable() {
|
|
1442
|
-
process.stdout.write(MOUSE_ENABLE_SEQUENCES);
|
|
1443
|
-
}
|
|
1444
|
-
function writeMouseDisable() {
|
|
1445
|
-
process.stdout.write(MOUSE_DISABLE_SEQUENCES);
|
|
1446
|
-
}
|
|
1447
|
-
const MAX_BUFFER = 64;
|
|
1448
|
-
const SGR_PREFIX = "\x1B[<";
|
|
1449
|
-
function partialPrefixLength(buffer) {
|
|
1450
|
-
for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
|
|
1451
|
-
return 0;
|
|
1452
|
-
}
|
|
1453
|
-
function createMouseParser(onEvent) {
|
|
1454
|
-
let buffer = "";
|
|
1455
|
-
const pressedButtons = /* @__PURE__ */ new Set();
|
|
1456
|
-
function parseSequenceAt(str, offset) {
|
|
1457
|
-
if (!str.startsWith("\x1B[<", offset)) return null;
|
|
1458
|
-
let index = offset + 3;
|
|
1459
|
-
const values = [];
|
|
1460
|
-
let current = 0;
|
|
1461
|
-
let hasDigit = false;
|
|
1462
|
-
let terminator = "";
|
|
1463
|
-
for (; index < str.length; index++) {
|
|
1464
|
-
const ch = str[index];
|
|
1465
|
-
if (ch >= "0" && ch <= "9") {
|
|
1466
|
-
current = current * 10 + (ch.charCodeAt(0) - 48);
|
|
1467
|
-
hasDigit = true;
|
|
1468
|
-
} else if (ch === ";" && hasDigit) {
|
|
1469
|
-
values.push(current);
|
|
1470
|
-
current = 0;
|
|
1471
|
-
hasDigit = false;
|
|
1472
|
-
} else if (ch === "M" || ch === "m") {
|
|
1473
|
-
if (!hasDigit) return null;
|
|
1474
|
-
values.push(current);
|
|
1475
|
-
terminator = ch;
|
|
1476
|
-
break;
|
|
1477
|
-
} else return null;
|
|
1478
|
-
}
|
|
1479
|
-
if (terminator === "") return null;
|
|
1480
|
-
if (values.length !== 3) return null;
|
|
1481
|
-
return {
|
|
1482
|
-
event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
|
|
1483
|
-
consumed: index - offset + 1
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
return { feed(chunk) {
|
|
1487
|
-
buffer += chunk;
|
|
1488
|
-
while (true) {
|
|
1489
|
-
const start = buffer.indexOf("\x1B[<");
|
|
1490
|
-
if (start === -1) {
|
|
1491
|
-
const keep = partialPrefixLength(buffer);
|
|
1492
|
-
buffer = keep === 0 ? "" : buffer.slice(-keep);
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
if (start > 0) {
|
|
1496
|
-
buffer = buffer.slice(start);
|
|
1497
|
-
continue;
|
|
1498
|
-
}
|
|
1499
|
-
if (buffer.length > MAX_BUFFER) {
|
|
1500
|
-
buffer = "";
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
const parsed = parseSequenceAt(buffer, 0);
|
|
1504
|
-
if (parsed === null) return;
|
|
1505
|
-
const { event, consumed } = parsed;
|
|
1506
|
-
buffer = buffer.slice(consumed);
|
|
1507
|
-
onEvent(event);
|
|
1508
|
-
}
|
|
1509
|
-
} };
|
|
1510
|
-
}
|
|
1511
|
-
function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
|
|
1512
|
-
const button = rawButtonCode & 3;
|
|
1513
|
-
const isScroll = (rawButtonCode & 64) !== 0;
|
|
1514
|
-
const isMotion = (rawButtonCode & 32) !== 0;
|
|
1515
|
-
const modifiers = {
|
|
1516
|
-
shift: (rawButtonCode & 4) !== 0,
|
|
1517
|
-
alt: (rawButtonCode & 8) !== 0,
|
|
1518
|
-
ctrl: (rawButtonCode & 16) !== 0
|
|
1519
|
-
};
|
|
1520
|
-
let type;
|
|
1521
|
-
let scrollDirection;
|
|
1522
|
-
if (isScroll) {
|
|
1523
|
-
type = "scroll";
|
|
1524
|
-
scrollDirection = button === 0 ? "up" : "down";
|
|
1525
|
-
} else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
|
|
1526
|
-
else if (pressRelease === "M") {
|
|
1527
|
-
type = "down";
|
|
1528
|
-
if (button !== 3) pressed.add(button);
|
|
1529
|
-
} else {
|
|
1530
|
-
type = "up";
|
|
1531
|
-
if (button === 3) pressed.clear();
|
|
1532
|
-
else pressed.delete(button);
|
|
1533
|
-
}
|
|
1534
|
-
return {
|
|
1535
|
-
type,
|
|
1536
|
-
button: button === 3 ? 0 : button,
|
|
1537
|
-
x: wireX - 1,
|
|
1538
|
-
y: wireY - 1,
|
|
1539
|
-
modifiers,
|
|
1540
|
-
...scrollDirection === void 0 ? {} : { scrollDirection }
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
function isMouseResidue(input) {
|
|
1544
|
-
return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
|
|
1545
|
-
}
|
|
1546
|
-
function createMouseController(onEvent) {
|
|
1547
|
-
const parser = createMouseParser(onEvent);
|
|
1548
|
-
const onData = (chunk) => parser.feed(chunk.toString("latin1"));
|
|
1549
|
-
return {
|
|
1550
|
-
enable() {
|
|
1551
|
-
writeMouseEnable();
|
|
1552
|
-
process.stdin.on("data", onData);
|
|
1553
|
-
},
|
|
1554
|
-
disable() {
|
|
1555
|
-
writeMouseDisable();
|
|
1556
|
-
process.stdin.off("data", onData);
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
//#endregion
|
|
1561
1586
|
//#region src/ui/key-arbiter.ts
|
|
1562
1587
|
/**
|
|
1563
1588
|
* Key-event suppression for ink's broadcast-based useInput.
|
|
@@ -1626,18 +1651,6 @@ function caretNonceBold(cursor) {
|
|
|
1626
1651
|
return Math.floor(cursor / 480) % 2 === 0;
|
|
1627
1652
|
}
|
|
1628
1653
|
//#endregion
|
|
1629
|
-
//#region src/terminal/cursor-shape.ts
|
|
1630
|
-
const SEQUENCES = {
|
|
1631
|
-
beam: "\x1B[1 q",
|
|
1632
|
-
block: "\x1B[2 q",
|
|
1633
|
-
reset: "\x1B[0 q",
|
|
1634
|
-
hide: "\x1B[?25l",
|
|
1635
|
-
show: "\x1B[?25h"
|
|
1636
|
-
};
|
|
1637
|
-
function writeCursorShape(shape) {
|
|
1638
|
-
process.stdout.write(SEQUENCES[shape]);
|
|
1639
|
-
}
|
|
1640
|
-
//#endregion
|
|
1641
1654
|
//#region src/ui/chrome/caps.tsx
|
|
1642
1655
|
function CapText({ background, top, width }) {
|
|
1643
1656
|
if (glyphs.halfBlockCaps) return /* @__PURE__ */ jsx(Text, {
|
|
@@ -3123,4 +3136,4 @@ function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }
|
|
|
3123
3136
|
}) });
|
|
3124
3137
|
}
|
|
3125
3138
|
//#endregion
|
|
3126
|
-
export {
|
|
3139
|
+
export { colToCharIndex as $, writeCursorShape as A, colorLevel as At, hintBlockTop as B, useCaret as C, permissionModeInfo as Ct, SelectableText as D, subscribePalettes as Dt, markKeymapConsumed as E, setThemeMode as Et, inputContentContains as F, installCrashHandlers as Ft, segmentsKey as G, inputStatusRow as H, messageBackgroundWidth as I, warn as It, sliceByColumns as J, wrapSegments as K, panelAnchorRow as L, isMouseResidue as M, env as Mt, writeMouseDisable as N, configureLogs as Nt, SelectionContext as O, themeMode as Ot, hintSpan as P, error as Pt, charWidth as Q, panelContains as R, caretNonceText as S, paletteColor as St, isKeyConsumed as T, replacePalettes as Tt, glyphs as U, inputFrameTop as V, mergeRuns as W, selectedRange as X, clampFocusRow as Y, toScreenSelection as Z, editDeleteWordRight as _, registerFieldKind as _t, DIALOG_MAX_HEIGHT as a, truncate as at, caretNonceBold as b, specialFieldFactory as bt, asTextItem as c, charactersChipLabel as ct, editCursorLeft as d, fieldStyleOf as dt, errorText as et, editCursorRight as f, hasFieldChar as ft, editDeleteWordLeft as g, linesChipLabel as gt, editDelete as h, isFieldChar as ht, Dialog as i, textWidth as it, createMouseController as j, setColorLevel as jt, Region as k, keyedRegistry as kt, registerWidget as l, fieldExpandOf as lt, editCursorWordRight as m, imageChipLabel as mt, panelLegend as n, locToPoint as nt, rowHeight as o, wrapLines as ot, editCursorWordLeft as p, hasFieldSlots as pt, chromeSelectionText as q, CloseGuardContext as r, segmentGraphemes as rt, wrapStatusLines as s, allocateField as st, PanelSurface as t, lineBreaks as tt, editBackspace as u, fieldSlotOf as ut, editInsert as v, releaseFields as vt, beginKeyEvent as w, registerPalette as wt, caretNonceColor as x, COLORS as xt, CapText as y, releaseUnreferenced as yt, scrollbarColumn as z };
|
package/package.json
CHANGED
package/src/chat/session-list.ts
CHANGED
|
@@ -23,9 +23,9 @@ export function sessionTime(session: SessionSummary): number {
|
|
|
23
23
|
* with the client-side visibility rules: live sessions plus persisted cold
|
|
24
24
|
* sessions with a cwd, blank and subagent-origin rows hidden, newest-first,
|
|
25
25
|
* without any workspace filtering (the picker labels ungrouped sessions).
|
|
26
|
-
* Cold rows
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Cold rows take their titles from the host's batched session-title fold
|
|
27
|
+
* (`sessionQuery.readTitleSnapshots`), which reads each log once per
|
|
28
|
+
* release of the query corpus and never loads full sessions into memory.
|
|
29
29
|
*/
|
|
30
30
|
export async function computeSessionList(ctx: Context): Promise<SessionSummary[]> {
|
|
31
31
|
try {
|
|
@@ -55,7 +55,7 @@ async function computeSessionListInner(ctx: Context): Promise<SessionSummary[]>
|
|
|
55
55
|
}
|
|
56
56
|
const persistence = ctx.get('sessionPersistence') as PersistenceLike | undefined
|
|
57
57
|
const cold = await listColdHeaders(ctx, persistence, attached)
|
|
58
|
-
await mergeColdSummaries(ctx,
|
|
58
|
+
await mergeColdSummaries(ctx, cold, archived, workspacePaths, summaries)
|
|
59
59
|
await attachModifiedTimes(ctx, summaries)
|
|
60
60
|
return summaries.sort((a, b) => sessionTime(b) - sessionTime(a))
|
|
61
61
|
}
|
|
@@ -67,13 +67,18 @@ interface ColdHeader {
|
|
|
67
67
|
origin?: string
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
interface
|
|
71
|
-
|
|
70
|
+
interface PersistenceLike {
|
|
71
|
+
list(signal?: AbortSignal): Promise<ColdHeader[]>
|
|
72
|
+
locate?(header: { cwd?: string; id: string }): { path: string } | undefined
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
interface
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
interface SessionQueryLike {
|
|
76
|
+
listSessions?(signal?: AbortSignal): Promise<Array<{ header: ColdHeader; live: boolean; persisted: boolean }>>
|
|
77
|
+
readTitleSnapshots?(sessionIds: readonly string[]): Promise<Array<{
|
|
78
|
+
sessionId: string
|
|
79
|
+
status: 'fulfilled' | 'rejected'
|
|
80
|
+
value?: { title?: { title?: string; updatedAt?: number } }
|
|
81
|
+
}>>
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
@@ -159,134 +164,30 @@ function lastPromptAt(events: readonly SessionEvent[]): number {
|
|
|
159
164
|
return last
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
interface PersistenceLike {
|
|
163
|
-
list(signal?: AbortSignal): Promise<ColdHeader[]>
|
|
164
|
-
locate?(header: { cwd?: string; id: string }): { path: string } | undefined
|
|
165
|
-
readFrom?(id: string, offset: number, signal?: AbortSignal): Promise<{ events: SessionEvent[] }>
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
interface SessionMetaCacheEntry {
|
|
169
|
-
title: string | undefined
|
|
170
|
-
blank: boolean | undefined
|
|
171
|
-
promptAt: number
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const SESSION_META_CACHE_MAX = 512
|
|
175
|
-
const sessionMetaCache = new Map<string, SessionMetaCacheEntry>()
|
|
176
|
-
|
|
177
|
-
function cacheSessionMeta(id: string, entry: SessionMetaCacheEntry): void {
|
|
178
|
-
while (sessionMetaCache.size >= SESSION_META_CACHE_MAX) {
|
|
179
|
-
const oldest = sessionMetaCache.keys().next()
|
|
180
|
-
if (oldest.done) break
|
|
181
|
-
sessionMetaCache.delete(oldest.value)
|
|
182
|
-
}
|
|
183
|
-
sessionMetaCache.set(id, entry)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const PROBE_BATCH_SIZE = 16
|
|
187
|
-
|
|
188
|
-
/** Mirrors the host's cold blank-probe gate: only small artifacts are read. */
|
|
189
|
-
const COLD_PROBE_MAX_BYTES = 1024
|
|
190
|
-
|
|
191
|
-
interface ColdMeta {
|
|
192
|
-
title: string | undefined
|
|
193
|
-
blank: boolean
|
|
194
|
-
promptAt: number
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function listMetadataHint(values: Record<string, unknown>): { blank: boolean; lastPromptAt: number | null } | undefined {
|
|
198
|
-
const hint = values.sessionListMetadata as { blank?: unknown; lastPromptAt?: unknown } | undefined
|
|
199
|
-
if (typeof hint?.blank !== 'boolean') return undefined
|
|
200
|
-
return {
|
|
201
|
-
blank: hint.blank,
|
|
202
|
-
lastPromptAt: typeof hint.lastPromptAt === 'number' ? hint.lastPromptAt : null,
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Cold-session metadata without unconditional full-log reads: projection
|
|
208
|
-
* hints first (zero I/O), then a direct read only when the artifact is small
|
|
209
|
-
* enough to be a blank-session log. A hint-less oversized artifact is
|
|
210
|
-
* conservatively visible; its title comes from the projection cache's cold
|
|
211
|
-
* ladder, which reads the log once and writes the row back durably.
|
|
212
|
-
*/
|
|
213
|
-
async function resolveColdMeta(cache: ProjectionCacheLike | undefined, persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta> {
|
|
214
|
-
let titleHint: string | undefined
|
|
215
|
-
let metadata: { blank: boolean; lastPromptAt: number | null } | undefined
|
|
216
|
-
try {
|
|
217
|
-
const values = cache?.cachedSnapshot?.(header)?.values ?? {}
|
|
218
|
-
if (typeof values.title === 'string' && values.title !== '') titleHint = values.title
|
|
219
|
-
metadata = listMetadataHint(values)
|
|
220
|
-
} catch {
|
|
221
|
-
// A broken projection cache must not block the session picker; fall back to probing.
|
|
222
|
-
}
|
|
223
|
-
if (metadata?.blank === false) {
|
|
224
|
-
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata.lastPromptAt ?? 0 }
|
|
225
|
-
}
|
|
226
|
-
const probed = await probeSmallArtifact(persistence, header)
|
|
227
|
-
if (probed !== undefined) {
|
|
228
|
-
return { title: probed.title ?? titleHint, blank: probed.blank, promptAt: probed.promptAt }
|
|
229
|
-
}
|
|
230
|
-
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata?.lastPromptAt ?? 0 }
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function coldTitle(cache: ProjectionCacheLike | undefined, header: ColdHeader, titleHint: string | undefined): Promise<string | undefined> {
|
|
234
|
-
if (titleHint !== undefined || cache?.coldSnapshot === undefined) return titleHint
|
|
235
|
-
try {
|
|
236
|
-
const snapshot = await cache.coldSnapshot(String(header.id))
|
|
237
|
-
const title = snapshot?.values?.title
|
|
238
|
-
return typeof title === 'string' && title !== '' ? title : undefined
|
|
239
|
-
} catch {
|
|
240
|
-
// A failed cold-snapshot read leaves the session without a title.
|
|
241
|
-
return undefined
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function probeSmallArtifact(persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta | undefined> {
|
|
246
|
-
if (persistence.locate === undefined || persistence.readFrom === undefined) return undefined
|
|
247
|
-
const location = persistence.locate(header)
|
|
248
|
-
if (location === undefined) return undefined
|
|
249
|
-
try {
|
|
250
|
-
const info = await stat(location.path)
|
|
251
|
-
if (info.size > COLD_PROBE_MAX_BYTES) return undefined
|
|
252
|
-
const { events } = await persistence.readFrom(String(header.id), 0)
|
|
253
|
-
return { blank: isBlankSession(events), title: titleFromEvents(events) ?? firstUserText(events), promptAt: lastPromptAt(events) }
|
|
254
|
-
} catch {
|
|
255
|
-
// An unreadable artifact is treated as a blank session (skipped in the list).
|
|
256
|
-
return undefined
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
167
|
async function mergeColdSummaries(
|
|
261
168
|
ctx: Context,
|
|
262
|
-
persistence: PersistenceLike | undefined,
|
|
263
169
|
cold: ColdHeader[],
|
|
264
170
|
archived: ReadonlySet<string>,
|
|
265
171
|
workspacePaths: ReadonlySet<string>,
|
|
266
172
|
summaries: SessionSummary[],
|
|
267
173
|
): Promise<void> {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
174
|
+
const query = ctx.get('sessionQuery') as SessionQueryLike | undefined
|
|
175
|
+
if (query?.readTitleSnapshots === undefined) return
|
|
176
|
+
const observed = await query.readTitleSnapshots(cold.map(header => String(header.id))).catch(() => [])
|
|
177
|
+
const titleById = new Map<string, { title: string; promptAt: number }>()
|
|
178
|
+
for (const entry of observed) {
|
|
179
|
+
if (entry.status !== 'fulfilled') continue
|
|
180
|
+
const snapshot = entry.value?.title
|
|
181
|
+
if (snapshot?.title === undefined || snapshot.title === '') continue
|
|
182
|
+
titleById.set(String(entry.sessionId), { title: snapshot.title, promptAt: snapshot.updatedAt ?? 0 })
|
|
183
|
+
}
|
|
271
184
|
for (const header of cold) {
|
|
272
185
|
if (archived.has(String(header.id))) continue
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
continue
|
|
279
|
-
}
|
|
280
|
-
pending.push(header)
|
|
281
|
-
}
|
|
282
|
-
for (let index = 0; index < pending.length; index += PROBE_BATCH_SIZE) {
|
|
283
|
-
await Promise.all(pending.slice(index, index + PROBE_BATCH_SIZE).map(async header => {
|
|
284
|
-
const meta = await resolveColdMeta(cache, persistence, header)
|
|
285
|
-
cacheSessionMeta(String(header.id), meta)
|
|
286
|
-
if (!meta.blank) {
|
|
287
|
-
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths))
|
|
288
|
-
}
|
|
289
|
-
}))
|
|
186
|
+
// No title snapshot means the session never ran a turn: the host's
|
|
187
|
+
// deterministic fallback titles every session with a user message.
|
|
188
|
+
const meta = titleById.get(String(header.id))
|
|
189
|
+
if (meta === undefined) continue
|
|
190
|
+
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths))
|
|
290
191
|
}
|
|
291
192
|
}
|
|
292
193
|
|
|
@@ -299,17 +200,6 @@ function fallbackName(id: string, directory?: string): string {
|
|
|
299
200
|
return match?.[0] ?? id
|
|
300
201
|
}
|
|
301
202
|
|
|
302
|
-
function titleFromEvents(events: readonly SessionEvent[]): string | undefined {
|
|
303
|
-
let title: string | undefined
|
|
304
|
-
for (const event of events) {
|
|
305
|
-
if ((event as { type?: string }).type === 'session/title') {
|
|
306
|
-
const data = (event as { data?: { title?: string } }).data
|
|
307
|
-
if (data?.title !== undefined && data.title !== '') title = data.title
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return title
|
|
311
|
-
}
|
|
312
|
-
|
|
313
203
|
function firstUserText(events: readonly SessionEvent[]): string | undefined {
|
|
314
204
|
for (const event of events) {
|
|
315
205
|
if (event.type !== 'user/message') continue
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
import { writeStdoutRaw } from './mouse.ts'
|
|
3
|
+
|
|
1
4
|
const SEQUENCES = {
|
|
2
5
|
beam: '\x1b[1 q',
|
|
3
6
|
block: '\x1b[2 q',
|
|
@@ -9,5 +12,5 @@ const SEQUENCES = {
|
|
|
9
12
|
export type CursorShape = keyof typeof SEQUENCES
|
|
10
13
|
|
|
11
14
|
export function writeCursorShape(shape: CursorShape): void {
|
|
12
|
-
|
|
15
|
+
writeStdoutRaw(SEQUENCES[shape])
|
|
13
16
|
}
|
package/src/terminal/mouse.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeSync } from 'node:fs'
|
|
2
|
+
|
|
1
3
|
export type MouseEventType = 'down' | 'up' | 'drag' | 'move' | 'scroll'
|
|
2
4
|
|
|
3
5
|
export interface MouseEventData {
|
|
@@ -12,12 +14,27 @@ export interface MouseEventData {
|
|
|
12
14
|
const MOUSE_ENABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000h\x1b[?1002h\x1b[?1006h'
|
|
13
15
|
const MOUSE_DISABLE_SEQUENCES = '\x1b[?1003l\x1b[?1000l\x1b[?1002l\x1b[?1006l'
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Terminal-mode sequences must reach the device even when the process exits
|
|
19
|
+
* right after: on Windows, TTY writes through process.stdout are
|
|
20
|
+
* asynchronous and a trailing restore sequence is silently dropped, leaving
|
|
21
|
+
* the terminal in mouse-tracking mode. fd 1 writes are synchronous.
|
|
22
|
+
*/
|
|
23
|
+
export function writeStdoutRaw(text: string): void {
|
|
24
|
+
try {
|
|
25
|
+
writeSync(1, text)
|
|
26
|
+
} catch {
|
|
27
|
+
// A full stdout buffer (EAGAIN) can only be retried asynchronously.
|
|
28
|
+
process.stdout.write(text)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export function writeMouseEnable(): void {
|
|
16
|
-
|
|
33
|
+
writeStdoutRaw(MOUSE_ENABLE_SEQUENCES)
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
export function writeMouseDisable(): void {
|
|
20
|
-
|
|
37
|
+
writeStdoutRaw(MOUSE_DISABLE_SEQUENCES)
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
interface ParsedSequence {
|
package/src/ui/app.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import type { ChatBridge } from '../chat/bridge.ts'
|
|
|
9
9
|
import type { AgentActivity } from '../chat/store.ts'
|
|
10
10
|
import { rowIndexFor, selectionText } from './message/layout.ts'
|
|
11
11
|
import { glyphs } from '../terminal/glyphs.ts'
|
|
12
|
+
import { writeMouseDisable } from '../terminal/mouse.ts'
|
|
13
|
+
import { writeCursorShape } from '../terminal/cursor-shape.ts'
|
|
12
14
|
import { chromeSelectionText } from './selection-registry.ts'
|
|
13
15
|
import type { ScreenCapture } from '../terminal/screen.ts'
|
|
14
16
|
import type { LineSelection } from '../model/selection.ts'
|
|
@@ -332,6 +334,10 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
332
334
|
}
|
|
333
335
|
if (!exiting.current) {
|
|
334
336
|
exiting.current = true
|
|
337
|
+
// Restore terminal modes synchronously before signaling: on Windows
|
|
338
|
+
// the queued async writes of the dispose path can be lost at exit.
|
|
339
|
+
writeCursorShape('reset')
|
|
340
|
+
writeMouseDisable()
|
|
335
341
|
process.kill(process.pid, 'SIGINT')
|
|
336
342
|
setTimeout(() => process.exit(0), FORCE_EXIT_DELAY_MS).unref()
|
|
337
343
|
}
|