@lzdi/pty-remote-cli 0.1.3
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/bin/pty-remote-cli.js +24 -0
- package/cli.conf +51 -0
- package/codex_template.jsonl +1 -0
- package/package.json +45 -0
- package/scripts/ensure-node-pty-helper.js +24 -0
- package/src/attachments/manager.ts +196 -0
- package/src/cli/cli-config.ts +58 -0
- package/src/cli/client.ts +674 -0
- package/src/cli/jsonl.ts +483 -0
- package/src/cli/pty-manager.ts +1509 -0
- package/src/cli/pty.ts +162 -0
- package/src/cli-main.ts +18 -0
- package/src/project-history.ts +175 -0
- package/src/providers/claude-history.ts +124 -0
- package/src/providers/claude.ts +66 -0
- package/src/providers/codex-history.ts +390 -0
- package/src/providers/codex-jsonl.ts +604 -0
- package/src/providers/codex-manager.ts +1662 -0
- package/src/providers/codex-pty.ts +144 -0
- package/src/providers/codex-resume-session.ts +253 -0
- package/src/providers/codex.ts +67 -0
- package/src/providers/provider-runtime.ts +58 -0
- package/src/providers/slash-commands.ts +115 -0
- package/src/terminal/frame-state.ts +457 -0
- package/src/threads-cli.ts +164 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import xtermHeadless from '@xterm/headless';
|
|
2
|
+
import type { IBufferCell, IBufferLine } from '@xterm/headless';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cloneTerminalFrameSnapshot,
|
|
6
|
+
encodeTerminalFrameStyle,
|
|
7
|
+
type TerminalFrameLine,
|
|
8
|
+
type TerminalFramePatch,
|
|
9
|
+
type TerminalFrameSnapshot,
|
|
10
|
+
type TerminalFrameStyle
|
|
11
|
+
} from '@lzdi/pty-remote-protocol/terminal-frame.ts';
|
|
12
|
+
|
|
13
|
+
const STYLE_FLAG_BOLD = 1 << 0;
|
|
14
|
+
const STYLE_FLAG_ITALIC = 1 << 1;
|
|
15
|
+
const STYLE_FLAG_DIM = 1 << 2;
|
|
16
|
+
const STYLE_FLAG_UNDERLINE = 1 << 3;
|
|
17
|
+
const STYLE_FLAG_BLINK = 1 << 4;
|
|
18
|
+
const STYLE_FLAG_INVERSE = 1 << 5;
|
|
19
|
+
const STYLE_FLAG_INVISIBLE = 1 << 6;
|
|
20
|
+
const STYLE_FLAG_STRIKETHROUGH = 1 << 7;
|
|
21
|
+
const STYLE_FLAG_OVERLINE = 1 << 8;
|
|
22
|
+
const DEFAULT_SCROLLBACK = 500;
|
|
23
|
+
const { Terminal } = xtermHeadless;
|
|
24
|
+
type HeadlessTerminal = InstanceType<typeof xtermHeadless.Terminal>;
|
|
25
|
+
|
|
26
|
+
interface MaterializedSnapshot {
|
|
27
|
+
lineKeys: string[];
|
|
28
|
+
snapshot: TerminalFrameSnapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface HeadlessTerminalFrameStateOptions {
|
|
32
|
+
cols: number;
|
|
33
|
+
maxLines?: number;
|
|
34
|
+
rows: number;
|
|
35
|
+
scrollback?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MAX_LINES = 500;
|
|
39
|
+
|
|
40
|
+
function sanitizeCols(cols: number): number {
|
|
41
|
+
return Number.isFinite(cols) ? Math.max(20, Math.min(Math.floor(cols), 400)) : 120;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sanitizeRows(rows: number): number {
|
|
45
|
+
return Number.isFinite(rows) ? Math.max(8, Math.min(Math.floor(rows), 200)) : 32;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createHeadlessTerminal(cols: number, rows: number, scrollback: number): HeadlessTerminal {
|
|
49
|
+
return new Terminal({
|
|
50
|
+
allowProposedApi: true,
|
|
51
|
+
cols,
|
|
52
|
+
rows,
|
|
53
|
+
convertEol: false,
|
|
54
|
+
cursorBlink: false,
|
|
55
|
+
cursorStyle: 'bar',
|
|
56
|
+
disableStdin: true,
|
|
57
|
+
logLevel: 'off',
|
|
58
|
+
scrollback
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildLineKey(line: TerminalFrameLine): string {
|
|
63
|
+
return `${line.wrapped ? '1' : '0'}:${line.runs.map((run) => `${run.style}:${run.text}`).join('|')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getChangedRange(previous: string[], next: string[]): {
|
|
67
|
+
deleteCount: number;
|
|
68
|
+
insertCount: number;
|
|
69
|
+
start: number;
|
|
70
|
+
} {
|
|
71
|
+
let start = 0;
|
|
72
|
+
const sharedLength = Math.min(previous.length, next.length);
|
|
73
|
+
while (start < sharedLength && previous[start] === next[start]) {
|
|
74
|
+
start += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let previousEnd = previous.length - 1;
|
|
78
|
+
let nextEnd = next.length - 1;
|
|
79
|
+
while (previousEnd >= start && nextEnd >= start && previous[previousEnd] === next[nextEnd]) {
|
|
80
|
+
previousEnd -= 1;
|
|
81
|
+
nextEnd -= 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
start,
|
|
86
|
+
deleteCount: Math.max(0, previousEnd - start + 1),
|
|
87
|
+
insertCount: Math.max(0, nextEnd - start + 1)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasMetaChanged(previous: TerminalFrameSnapshot, next: TerminalFrameSnapshot): boolean {
|
|
92
|
+
return (
|
|
93
|
+
previous.cols !== next.cols ||
|
|
94
|
+
previous.rows !== next.rows ||
|
|
95
|
+
previous.tailStart !== next.tailStart ||
|
|
96
|
+
previous.cursorX !== next.cursorX ||
|
|
97
|
+
previous.cursorY !== next.cursorY ||
|
|
98
|
+
previous.viewportY !== next.viewportY ||
|
|
99
|
+
previous.baseY !== next.baseY ||
|
|
100
|
+
previous.totalLines !== next.totalLines
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeStyleColor(mode: TerminalFrameStyle['fgMode'], value: number): number {
|
|
105
|
+
if (mode === 'default') {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class HeadlessTerminalFrameState {
|
|
112
|
+
private terminal: HeadlessTerminal;
|
|
113
|
+
|
|
114
|
+
private materializedSnapshot: MaterializedSnapshot;
|
|
115
|
+
|
|
116
|
+
private readonly maxLines: number;
|
|
117
|
+
|
|
118
|
+
private readonly scrollback: number;
|
|
119
|
+
|
|
120
|
+
private generation = 0;
|
|
121
|
+
|
|
122
|
+
private pendingWriteChain: Promise<void> = Promise.resolve();
|
|
123
|
+
|
|
124
|
+
constructor(options: HeadlessTerminalFrameStateOptions) {
|
|
125
|
+
const cols = sanitizeCols(options.cols);
|
|
126
|
+
const rows = sanitizeRows(options.rows);
|
|
127
|
+
this.maxLines = Number.isFinite(options.maxLines) ? Math.max(50, Math.floor(options.maxLines ?? DEFAULT_MAX_LINES)) : DEFAULT_MAX_LINES;
|
|
128
|
+
this.scrollback = options.scrollback ?? Math.max(DEFAULT_SCROLLBACK, this.maxLines);
|
|
129
|
+
this.terminal = createHeadlessTerminal(cols, rows, this.scrollback);
|
|
130
|
+
this.materializedSnapshot = this.materializeSnapshot(null, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
dispose(): void {
|
|
134
|
+
this.terminal.dispose();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getSnapshot(): TerminalFrameSnapshot {
|
|
138
|
+
return cloneTerminalFrameSnapshot(this.materializedSnapshot.snapshot);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async flush(): Promise<void> {
|
|
142
|
+
await this.pendingWriteChain;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
reset(sessionId: string | null): TerminalFramePatch {
|
|
146
|
+
const previousTerminal = this.terminal;
|
|
147
|
+
const previousWriteChain = this.pendingWriteChain;
|
|
148
|
+
this.generation += 1;
|
|
149
|
+
this.terminal = createHeadlessTerminal(
|
|
150
|
+
this.materializedSnapshot.snapshot.cols || 120,
|
|
151
|
+
this.materializedSnapshot.snapshot.rows || 32,
|
|
152
|
+
this.scrollback
|
|
153
|
+
);
|
|
154
|
+
void previousWriteChain.finally(() => {
|
|
155
|
+
previousTerminal.dispose();
|
|
156
|
+
});
|
|
157
|
+
this.materializedSnapshot = this.materializeSnapshot(sessionId, 0);
|
|
158
|
+
return this.createResetPatch();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resize(cols: number, rows: number): TerminalFramePatch | null {
|
|
162
|
+
const nextCols = sanitizeCols(cols);
|
|
163
|
+
const nextRows = sanitizeRows(rows);
|
|
164
|
+
if (this.materializedSnapshot.snapshot.cols === nextCols && this.materializedSnapshot.snapshot.rows === nextRows) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.terminal.resize(nextCols, nextRows);
|
|
169
|
+
return this.commitFrame({ forceReset: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
enqueueOutput(chunk: string): Promise<TerminalFramePatch | null> {
|
|
173
|
+
const writeGeneration = this.generation;
|
|
174
|
+
const writeTerminal = this.terminal;
|
|
175
|
+
const writeTask = this.pendingWriteChain
|
|
176
|
+
.catch(() => undefined)
|
|
177
|
+
.then(
|
|
178
|
+
() =>
|
|
179
|
+
new Promise<TerminalFramePatch | null>((resolve, reject) => {
|
|
180
|
+
if (writeGeneration !== this.generation || writeTerminal !== this.terminal) {
|
|
181
|
+
resolve(null);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
writeTerminal.write(chunk, () => {
|
|
186
|
+
if (writeGeneration !== this.generation || writeTerminal !== this.terminal) {
|
|
187
|
+
resolve(null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
resolve(this.commitFrame());
|
|
193
|
+
} catch (error) {
|
|
194
|
+
reject(error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
this.pendingWriteChain = writeTask.then(() => undefined, () => undefined);
|
|
201
|
+
return writeTask;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
createResetPatch(): TerminalFramePatch {
|
|
205
|
+
return {
|
|
206
|
+
sessionId: this.materializedSnapshot.snapshot.sessionId,
|
|
207
|
+
baseRevision: 0,
|
|
208
|
+
revision: this.materializedSnapshot.snapshot.revision,
|
|
209
|
+
ops: [
|
|
210
|
+
{
|
|
211
|
+
type: 'reset',
|
|
212
|
+
snapshot: cloneTerminalFrameSnapshot(this.materializedSnapshot.snapshot)
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private commitFrame(options: { forceReset?: boolean } = {}): TerminalFramePatch | null {
|
|
219
|
+
const previous = this.materializedSnapshot;
|
|
220
|
+
const nextRevision = previous.snapshot.revision + 1;
|
|
221
|
+
const next = this.materializeSnapshot(previous.snapshot.sessionId, nextRevision);
|
|
222
|
+
const metaChanged = hasMetaChanged(previous.snapshot, next.snapshot);
|
|
223
|
+
const shift = next.snapshot.tailStart - previous.snapshot.tailStart;
|
|
224
|
+
const shiftPatch = shift > 0 ? this.createTailShiftPatch(previous, next, shift) : null;
|
|
225
|
+
const range = shiftPatch ? null : getChangedRange(previous.lineKeys, next.lineKeys);
|
|
226
|
+
const linesChanged = shiftPatch !== null || (range ? range.deleteCount > 0 || range.insertCount > 0 : false);
|
|
227
|
+
|
|
228
|
+
if (!options.forceReset && !metaChanged && !linesChanged) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const changedSpan = shiftPatch
|
|
233
|
+
? shiftPatch.trimCount + shiftPatch.range.deleteCount + shiftPatch.range.insertCount
|
|
234
|
+
: (range?.deleteCount ?? 0) + (range?.insertCount ?? 0);
|
|
235
|
+
const shouldReset =
|
|
236
|
+
options.forceReset === true ||
|
|
237
|
+
previous.snapshot.sessionId !== next.snapshot.sessionId ||
|
|
238
|
+
next.snapshot.tailStart < previous.snapshot.tailStart ||
|
|
239
|
+
(shift > 0 && shiftPatch === null) ||
|
|
240
|
+
(linesChanged && changedSpan > Math.max(20, Math.floor(next.snapshot.lines.length * 0.6)));
|
|
241
|
+
|
|
242
|
+
this.materializedSnapshot = next;
|
|
243
|
+
|
|
244
|
+
if (shouldReset) {
|
|
245
|
+
return this.createResetPatch();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const ops: TerminalFramePatch['ops'] = [];
|
|
249
|
+
if (shiftPatch) {
|
|
250
|
+
if (shiftPatch.trimCount > 0) {
|
|
251
|
+
ops.push({
|
|
252
|
+
type: 'trimHead',
|
|
253
|
+
count: shiftPatch.trimCount
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (shiftPatch.range.deleteCount > 0 || shiftPatch.range.insertCount > 0) {
|
|
257
|
+
ops.push({
|
|
258
|
+
type: 'spliceLines',
|
|
259
|
+
start: shiftPatch.range.start,
|
|
260
|
+
deleteCount: shiftPatch.range.deleteCount,
|
|
261
|
+
lines: shiftPatch.lines.map((line) => ({
|
|
262
|
+
wrapped: line.wrapped,
|
|
263
|
+
runs: line.runs.map((run) => ({
|
|
264
|
+
style: run.style,
|
|
265
|
+
text: run.text
|
|
266
|
+
}))
|
|
267
|
+
}))
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} else if (range && (range.deleteCount > 0 || range.insertCount > 0)) {
|
|
271
|
+
ops.push({
|
|
272
|
+
type: 'spliceLines',
|
|
273
|
+
start: range.start,
|
|
274
|
+
deleteCount: range.deleteCount,
|
|
275
|
+
lines: next.snapshot.lines.slice(range.start, range.start + range.insertCount).map((line) => ({
|
|
276
|
+
wrapped: line.wrapped,
|
|
277
|
+
runs: line.runs.map((run) => ({
|
|
278
|
+
style: run.style,
|
|
279
|
+
text: run.text
|
|
280
|
+
}))
|
|
281
|
+
}))
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (metaChanged) {
|
|
286
|
+
ops.push({
|
|
287
|
+
type: 'meta',
|
|
288
|
+
cols: next.snapshot.cols,
|
|
289
|
+
rows: next.snapshot.rows,
|
|
290
|
+
tailStart: next.snapshot.tailStart,
|
|
291
|
+
cursorX: next.snapshot.cursorX,
|
|
292
|
+
cursorY: next.snapshot.cursorY,
|
|
293
|
+
viewportY: next.snapshot.viewportY,
|
|
294
|
+
baseY: next.snapshot.baseY,
|
|
295
|
+
totalLines: next.snapshot.totalLines
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
sessionId: next.snapshot.sessionId,
|
|
301
|
+
baseRevision: previous.snapshot.revision,
|
|
302
|
+
revision: next.snapshot.revision,
|
|
303
|
+
ops
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private createTailShiftPatch(
|
|
308
|
+
previous: MaterializedSnapshot,
|
|
309
|
+
next: MaterializedSnapshot,
|
|
310
|
+
shift: number
|
|
311
|
+
): {
|
|
312
|
+
lines: TerminalFrameLine[];
|
|
313
|
+
range: { deleteCount: number; insertCount: number; start: number };
|
|
314
|
+
trimCount: number;
|
|
315
|
+
} | null {
|
|
316
|
+
if (shift <= 0 || shift > previous.lineKeys.length) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const trimmedPrevious = previous.lineKeys.slice(shift);
|
|
321
|
+
const range = getChangedRange(trimmedPrevious, next.lineKeys);
|
|
322
|
+
const changedSpan = range.deleteCount + range.insertCount;
|
|
323
|
+
if (changedSpan > Math.max(8, Math.floor(next.lineKeys.length * 0.2))) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
trimCount: shift,
|
|
329
|
+
range,
|
|
330
|
+
lines: next.snapshot.lines.slice(range.start, range.start + range.insertCount)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private materializeSnapshot(sessionId: string | null, revision: number): MaterializedSnapshot {
|
|
335
|
+
const buffer = this.terminal.buffer.active;
|
|
336
|
+
const reusableCell = buffer.getNullCell();
|
|
337
|
+
const lines: TerminalFrameLine[] = [];
|
|
338
|
+
const lineKeys: string[] = [];
|
|
339
|
+
|
|
340
|
+
const tailCount = Math.min(this.maxLines, buffer.length);
|
|
341
|
+
const tailStart = Math.max(0, buffer.length - tailCount);
|
|
342
|
+
|
|
343
|
+
for (let lineIndex = tailStart; lineIndex < buffer.length; lineIndex += 1) {
|
|
344
|
+
const line = buffer.getLine(lineIndex);
|
|
345
|
+
if (!line) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const materializedLine = this.materializeLine(line, reusableCell);
|
|
349
|
+
lines.push(materializedLine);
|
|
350
|
+
lineKeys.push(buildLineKey(materializedLine));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
snapshot: {
|
|
355
|
+
sessionId,
|
|
356
|
+
revision,
|
|
357
|
+
cols: this.terminal.cols,
|
|
358
|
+
rows: this.terminal.rows,
|
|
359
|
+
tailStart,
|
|
360
|
+
cursorX: buffer.cursorX,
|
|
361
|
+
cursorY: buffer.cursorY,
|
|
362
|
+
viewportY: buffer.viewportY,
|
|
363
|
+
baseY: buffer.baseY,
|
|
364
|
+
totalLines: lines.length,
|
|
365
|
+
lines
|
|
366
|
+
},
|
|
367
|
+
lineKeys
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private materializeLine(line: IBufferLine, reusableCell: IBufferCell): TerminalFrameLine {
|
|
372
|
+
const endColumn = this.resolveLineEndColumn(line, reusableCell);
|
|
373
|
+
if (endColumn === 0) {
|
|
374
|
+
return {
|
|
375
|
+
wrapped: line.isWrapped,
|
|
376
|
+
runs: []
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const runs: TerminalFrameLine['runs'] = [];
|
|
381
|
+
let currentStyle = '';
|
|
382
|
+
let currentText = '';
|
|
383
|
+
|
|
384
|
+
for (let column = 0; column < endColumn; column += 1) {
|
|
385
|
+
const cell = line.getCell(column, reusableCell);
|
|
386
|
+
if (!cell || cell.getWidth() === 0) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const style = this.encodeCellStyle(cell);
|
|
391
|
+
const chars = cell.getChars() || ' '.repeat(Math.max(1, cell.getWidth()));
|
|
392
|
+
|
|
393
|
+
if (style === currentStyle) {
|
|
394
|
+
currentText += chars;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (currentText) {
|
|
399
|
+
runs.push({
|
|
400
|
+
style: currentStyle,
|
|
401
|
+
text: currentText
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
currentStyle = style;
|
|
406
|
+
currentText = chars;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (currentText || runs.length === 0) {
|
|
410
|
+
runs.push({
|
|
411
|
+
style: currentStyle,
|
|
412
|
+
text: currentText
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
wrapped: line.isWrapped,
|
|
418
|
+
runs
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private resolveLineEndColumn(line: IBufferLine, reusableCell: IBufferCell): number {
|
|
423
|
+
for (let column = line.length - 1; column >= 0; column -= 1) {
|
|
424
|
+
const cell = line.getCell(column, reusableCell);
|
|
425
|
+
if (!cell || cell.getWidth() === 0) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (cell.getChars() || !cell.isAttributeDefault()) {
|
|
429
|
+
return column + Math.max(1, cell.getWidth());
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private encodeCellStyle(cell: IBufferCell): string {
|
|
436
|
+
const fgMode = cell.isFgRGB() ? 'rgb' : cell.isFgPalette() ? 'palette' : 'default';
|
|
437
|
+
const bgMode = cell.isBgRGB() ? 'rgb' : cell.isBgPalette() ? 'palette' : 'default';
|
|
438
|
+
const style: TerminalFrameStyle = {
|
|
439
|
+
flags:
|
|
440
|
+
(cell.isBold() ? STYLE_FLAG_BOLD : 0) |
|
|
441
|
+
(cell.isItalic() ? STYLE_FLAG_ITALIC : 0) |
|
|
442
|
+
(cell.isDim() ? STYLE_FLAG_DIM : 0) |
|
|
443
|
+
(cell.isUnderline() ? STYLE_FLAG_UNDERLINE : 0) |
|
|
444
|
+
(cell.isBlink() ? STYLE_FLAG_BLINK : 0) |
|
|
445
|
+
(cell.isInverse() ? STYLE_FLAG_INVERSE : 0) |
|
|
446
|
+
(cell.isInvisible() ? STYLE_FLAG_INVISIBLE : 0) |
|
|
447
|
+
(cell.isStrikethrough() ? STYLE_FLAG_STRIKETHROUGH : 0) |
|
|
448
|
+
(cell.isOverline() ? STYLE_FLAG_OVERLINE : 0),
|
|
449
|
+
fgMode,
|
|
450
|
+
fg: normalizeStyleColor(fgMode, cell.getFgColor()),
|
|
451
|
+
bgMode,
|
|
452
|
+
bg: normalizeStyleColor(bgMode, cell.getBgColor())
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return encodeTerminalFrameStyle(style);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import type { ProjectSessionSummary } from '@lzdi/pty-remote-protocol/protocol.ts';
|
|
4
|
+
import { listProjectSessions } from './project-history.ts';
|
|
5
|
+
|
|
6
|
+
type ThreadsCommand = 'list' | 'ids';
|
|
7
|
+
|
|
8
|
+
interface ThreadsCliOptions {
|
|
9
|
+
command: ThreadsCommand;
|
|
10
|
+
cwd: string;
|
|
11
|
+
json: boolean;
|
|
12
|
+
limit: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function printUsage(): void {
|
|
16
|
+
console.log(`Usage:
|
|
17
|
+
node src/cli-main.ts threads [list|ids] [--cwd <path>] [--limit <n>] [--json]
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
node src/cli-main.ts threads
|
|
21
|
+
node src/cli-main.ts threads list --limit 20
|
|
22
|
+
node src/cli-main.ts threads ids --cwd .
|
|
23
|
+
node src/cli-main.ts threads list --json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function truncate(value: string, maxChars: number): string {
|
|
27
|
+
if (value.length <= maxChars) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatTimestamp(value: string): string {
|
|
34
|
+
const date = new Date(value);
|
|
35
|
+
if (Number.isNaN(date.getTime())) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const year = String(date.getFullYear());
|
|
40
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
41
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
42
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
43
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
44
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
45
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printSessionTable(sessions: ProjectSessionSummary[]): void {
|
|
49
|
+
const rows = sessions.map((session) => ({
|
|
50
|
+
sessionId: session.sessionId,
|
|
51
|
+
messages: String(session.messageCount),
|
|
52
|
+
updatedAt: formatTimestamp(session.updatedAt),
|
|
53
|
+
preview: truncate(session.preview, 96)
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const sessionIdWidth = Math.max('sessionId'.length, ...rows.map((row) => row.sessionId.length));
|
|
57
|
+
const messagesWidth = Math.max('msgs'.length, ...rows.map((row) => row.messages.length));
|
|
58
|
+
const updatedAtWidth = Math.max('updatedAt'.length, ...rows.map((row) => row.updatedAt.length));
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
`${'sessionId'.padEnd(sessionIdWidth)} ${'msgs'.padEnd(messagesWidth)} ${'updatedAt'.padEnd(updatedAtWidth)} preview`
|
|
62
|
+
);
|
|
63
|
+
console.log(
|
|
64
|
+
`${'-'.repeat(sessionIdWidth)} ${'-'.repeat(messagesWidth)} ${'-'.repeat(updatedAtWidth)} ${'-'.repeat(24)}`
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
console.log(
|
|
69
|
+
`${row.sessionId.padEnd(sessionIdWidth)} ${row.messages.padEnd(messagesWidth)} ${row.updatedAt.padEnd(updatedAtWidth)} ${row.preview}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseThreadsArgs(argv: string[]): ThreadsCliOptions | null {
|
|
75
|
+
let command: ThreadsCommand = 'list';
|
|
76
|
+
let cwd = process.cwd();
|
|
77
|
+
let limit = 12;
|
|
78
|
+
let json = false;
|
|
79
|
+
|
|
80
|
+
const args = [...argv];
|
|
81
|
+
if (args[0] === 'list' || args[0] === 'ids') {
|
|
82
|
+
command = args.shift() as ThreadsCommand;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
while (args.length > 0) {
|
|
86
|
+
const arg = args.shift();
|
|
87
|
+
if (!arg) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (arg === '--help' || arg === '-h') {
|
|
92
|
+
printUsage();
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arg === '--json') {
|
|
97
|
+
json = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (arg === '--cwd') {
|
|
102
|
+
const value = args.shift();
|
|
103
|
+
if (!value) {
|
|
104
|
+
throw new Error('Missing value for --cwd');
|
|
105
|
+
}
|
|
106
|
+
cwd = path.resolve(value);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (arg === '--limit') {
|
|
111
|
+
const value = args.shift();
|
|
112
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
113
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
114
|
+
throw new Error(`Invalid --limit value: ${value ?? '<empty>'}`);
|
|
115
|
+
}
|
|
116
|
+
limit = parsed;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
command,
|
|
125
|
+
cwd,
|
|
126
|
+
json,
|
|
127
|
+
limit
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function runThreadsCli(argv: string[]): Promise<number> {
|
|
132
|
+
const options = parseThreadsArgs(argv);
|
|
133
|
+
if (!options) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sessions = await listProjectSessions(options.cwd, options.limit);
|
|
138
|
+
|
|
139
|
+
if (options.command === 'ids') {
|
|
140
|
+
const ids = sessions.map((session) => session.sessionId);
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(ids, null, 2));
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const sessionId of ids) {
|
|
147
|
+
console.log(sessionId);
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sessions.length === 0) {
|
|
158
|
+
console.log(`No Claude sessions found for ${options.cwd}`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
printSessionTable(sessions);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|