@lzdi/pty-remote-cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzdi/pty-remote-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "threads": "node bin/pty-remote-cli.js threads"
23
23
  },
24
24
  "dependencies": {
25
- "@lzdi/pty-remote-protocol": "0.1.3",
25
+ "@lzdi/pty-remote-protocol": "0.1.4",
26
26
  "@xterm/headless": "^6.0.0",
27
27
  "node-pty": "^1.1.0",
28
28
  "socket.io-client": "^4.8.3",
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ChatMessage,
3
+ ChatMessageMeta,
3
4
  ChatMessageBlock,
4
5
  TextChatMessageBlock,
5
6
  ToolResultChatMessageBlock,
@@ -36,12 +37,17 @@ interface CodexEventMsgPayload {
36
37
  message?: string;
37
38
  text?: string;
38
39
  phase?: string;
40
+ turn_id?: string;
41
+ }
42
+
43
+ interface CodexTurnContextPayload {
44
+ turn_id?: string;
39
45
  }
40
46
 
41
47
  interface CodexJsonlRecord {
42
48
  timestamp?: string;
43
49
  type?: string;
44
- payload?: CodexResponseItemPayload | CodexEventMsgPayload;
50
+ payload?: CodexResponseItemPayload | CodexEventMsgPayload | CodexTurnContextPayload;
45
51
  }
46
52
 
47
53
  export type CodexJsonlRuntimePhase = 'idle' | 'running';
@@ -50,6 +56,7 @@ export interface CodexJsonlMessagesState {
50
56
  orderedIds: string[];
51
57
  messagesById: Map<string, ChatMessage>;
52
58
  runtimePhase: CodexJsonlRuntimePhase;
59
+ activeTurnId: string | null;
53
60
  activityRevision: number;
54
61
  messageSequence: number;
55
62
  seenAssistantTextKeys: Set<string>;
@@ -61,6 +68,7 @@ export function createCodexJsonlMessagesState(): CodexJsonlMessagesState {
61
68
  orderedIds: [],
62
69
  messagesById: new Map<string, ChatMessage>(),
63
70
  runtimePhase: 'idle',
71
+ activeTurnId: null,
64
72
  activityRevision: 0,
65
73
  messageSequence: 0,
66
74
  seenAssistantTextKeys: new Set<string>(),
@@ -166,6 +174,25 @@ function normalizeCreatedAt(timestamp: string | undefined, sequence: number): st
166
174
  return new Date(sequence * 1_000).toISOString();
167
175
  }
168
176
 
177
+ function normalizeTurnId(turnId: string | undefined): string | null {
178
+ const normalized = turnId?.trim();
179
+ return normalized ? normalized : null;
180
+ }
181
+
182
+ function createMessageMeta(state: CodexJsonlMessagesState, phase?: string | null): ChatMessageMeta | undefined {
183
+ const normalizedPhase = phase?.trim() || null;
184
+ const turnId = state.activeTurnId;
185
+
186
+ if (!normalizedPhase && !turnId) {
187
+ return undefined;
188
+ }
189
+
190
+ return {
191
+ phase: normalizedPhase,
192
+ turnId
193
+ };
194
+ }
195
+
169
196
  function hasVisibleBlocks(blocks: ChatMessageBlock[]): boolean {
170
197
  return blocks.some((block) => {
171
198
  switch (block.type) {
@@ -214,27 +241,42 @@ function createToolResultBlock(callId: string, content: string, isError: boolean
214
241
  };
215
242
  }
216
243
 
217
- function getWebSearchQuery(action: CodexResponseItemPayload['action']): string {
244
+ function getWebSearchPreview(action: CodexResponseItemPayload['action']): string {
218
245
  if (!action || typeof action !== 'object') {
219
246
  return '';
220
247
  }
221
248
 
222
249
  const normalizedAction = action as {
250
+ pattern?: string;
223
251
  type?: string;
224
252
  query?: string;
225
253
  queries?: string[];
254
+ url?: string;
226
255
  };
227
- if (normalizedAction.type !== 'search') {
256
+ if (normalizedAction.type === 'search') {
257
+ if (typeof normalizedAction.query === 'string' && normalizedAction.query.trim()) {
258
+ return normalizedAction.query.trim();
259
+ }
260
+
261
+ if (Array.isArray(normalizedAction.queries)) {
262
+ const firstQuery = normalizedAction.queries.find((query) => typeof query === 'string' && query.trim());
263
+ return firstQuery?.trim() ?? '';
264
+ }
265
+
228
266
  return '';
229
267
  }
230
268
 
231
- if (typeof normalizedAction.query === 'string' && normalizedAction.query.trim()) {
232
- return normalizedAction.query.trim();
269
+ if (normalizedAction.type === 'open_page') {
270
+ return normalizedAction.url?.trim() ?? '';
233
271
  }
234
272
 
235
- if (Array.isArray(normalizedAction.queries)) {
236
- const firstQuery = normalizedAction.queries.find((query) => typeof query === 'string' && query.trim());
237
- return firstQuery?.trim() ?? '';
273
+ if (normalizedAction.type === 'find_in_page') {
274
+ const pattern = normalizedAction.pattern?.trim() ?? '';
275
+ const url = normalizedAction.url?.trim() ?? '';
276
+ if (pattern && url) {
277
+ return `${pattern} @ ${url}`;
278
+ }
279
+ return pattern || url;
238
280
  }
239
281
 
240
282
  return '';
@@ -282,6 +324,20 @@ function deriveMessageStatus(
282
324
  return 'complete';
283
325
  }
284
326
 
327
+ function mergeMessageMeta(existing: ChatMessageMeta | undefined, next: ChatMessageMeta | undefined): ChatMessageMeta | undefined {
328
+ const phase = next?.phase ?? existing?.phase ?? null;
329
+ const turnId = next?.turnId ?? existing?.turnId ?? null;
330
+
331
+ if (!phase && !turnId) {
332
+ return undefined;
333
+ }
334
+
335
+ return {
336
+ phase,
337
+ turnId
338
+ };
339
+ }
340
+
285
341
  export function refreshCodexJsonlMessageStatuses(state: CodexJsonlMessagesState): void {
286
342
  for (const [messageId, message] of state.messagesById.entries()) {
287
343
  const nextStatus = deriveMessageStatus(message.blocks, state.runtimePhase);
@@ -309,6 +365,7 @@ function upsertMessage(state: CodexJsonlMessagesState, nextMessage: ChatMessage)
309
365
  id: nextMessage.id,
310
366
  role: nextMessage.role,
311
367
  blocks,
368
+ meta: mergeMessageMeta(existing?.meta, nextMessage.meta),
312
369
  status: deriveMessageStatus(blocks, state.runtimePhase),
313
370
  createdAt: existing?.createdAt ?? nextMessage.createdAt
314
371
  };
@@ -369,6 +426,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
369
426
  id: messageId,
370
427
  role: 'user',
371
428
  blocks: [textBlock],
429
+ meta: createMessageMeta(state),
372
430
  status: 'complete',
373
431
  createdAt: normalizeCreatedAt(timestamp, sequence)
374
432
  });
@@ -395,6 +453,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
395
453
  id: messageId,
396
454
  role: 'assistant',
397
455
  blocks: [textBlock],
456
+ meta: createMessageMeta(state),
398
457
  status: 'complete',
399
458
  createdAt: normalizeCreatedAt(timestamp, sequence)
400
459
  });
@@ -421,6 +480,7 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
421
480
  id: messageId,
422
481
  role: 'assistant',
423
482
  blocks: [textBlock],
483
+ meta: createMessageMeta(state, payload?.phase),
424
484
  status: 'complete',
425
485
  createdAt: normalizeCreatedAt(timestamp, sequence)
426
486
  });
@@ -428,11 +488,16 @@ function applyEventMsg(state: CodexJsonlMessagesState, payload: CodexEventMsgPay
428
488
  }
429
489
 
430
490
  let nextPhase: CodexJsonlRuntimePhase | null = null;
491
+ const payloadTurnId = normalizeTurnId(payload?.turn_id);
431
492
 
432
493
  if (payloadType === 'task_started') {
433
494
  nextPhase = 'running';
495
+ state.activeTurnId = payloadTurnId ?? state.activeTurnId;
434
496
  } else if (payloadType === 'task_complete' || payloadType === 'turn_aborted') {
435
497
  nextPhase = 'idle';
498
+ if (!payloadTurnId || payloadTurnId === state.activeTurnId) {
499
+ state.activeTurnId = null;
500
+ }
436
501
  }
437
502
 
438
503
  if (!nextPhase || nextPhase === state.runtimePhase) {
@@ -496,6 +561,7 @@ function applyResponseItem(
496
561
  id: stableMessageId,
497
562
  role,
498
563
  blocks: stableBlocks,
564
+ meta: createMessageMeta(state, payload.phase),
499
565
  status: 'complete',
500
566
  createdAt: normalizeCreatedAt(timestamp, sequence)
501
567
  });
@@ -513,6 +579,7 @@ function applyResponseItem(
513
579
  id: `tool:${callId}`,
514
580
  role: 'assistant',
515
581
  blocks: [createToolUseBlock(callId, payload.name ?? 'unknown', stringifyUnknown(rawInput))],
582
+ meta: createMessageMeta(state),
516
583
  status: 'streaming',
517
584
  createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
518
585
  });
@@ -520,8 +587,8 @@ function applyResponseItem(
520
587
  }
521
588
 
522
589
  if (payloadType === 'web_search_call') {
523
- const query = getWebSearchQuery(payload.action);
524
- if (!query) {
590
+ const preview = getWebSearchPreview(payload.action);
591
+ if (!preview) {
525
592
  return;
526
593
  }
527
594
 
@@ -530,7 +597,8 @@ function applyResponseItem(
530
597
  upsertMessage(state, {
531
598
  id: `tool:${callId}`,
532
599
  role: 'assistant',
533
- blocks: [createToolUseBlock(callId, 'web_search', query)],
600
+ blocks: [createToolUseBlock(callId, 'web_search', preview)],
601
+ meta: createMessageMeta(state),
534
602
  status: 'complete',
535
603
  createdAt: normalizeCreatedAt(timestamp, sequence)
536
604
  });
@@ -549,6 +617,7 @@ function applyResponseItem(
549
617
  id: `tool:${callId}`,
550
618
  role: 'assistant',
551
619
  blocks: [createToolResultBlock(callId, stringifyUnknown(payload.output), isError)],
620
+ meta: createMessageMeta(state),
552
621
  status: isError ? 'error' : 'complete',
553
622
  createdAt: normalizeCreatedAt(timestamp, state.messageSequence++)
554
623
  });
@@ -578,6 +647,11 @@ export function applyCodexJsonlLine(state: CodexJsonlMessagesState, line: string
578
647
  return true;
579
648
  }
580
649
 
650
+ if (parsed.type === 'turn_context') {
651
+ state.activeTurnId = normalizeTurnId((parsed.payload as CodexTurnContextPayload | undefined)?.turn_id);
652
+ return true;
653
+ }
654
+
581
655
  return true;
582
656
  }
583
657
 
@@ -137,6 +137,7 @@ function messageEqual(left: ChatMessage | undefined, right: ChatMessage | undefi
137
137
  left.role === right.role &&
138
138
  left.status === right.status &&
139
139
  left.createdAt === right.createdAt &&
140
+ JSON.stringify(left.meta ?? null) === JSON.stringify(right.meta ?? null) &&
140
141
  JSON.stringify(left.blocks) === JSON.stringify(right.blocks)
141
142
  );
142
143
  }
@@ -158,6 +159,7 @@ function sleep(ms: number): Promise<void> {
158
159
  const AWAITING_JSONL_TURN_STALE_MS = 4000;
159
160
  const READY_TIMEOUT_MAX_RETRIES = 2;
160
161
  const TERMINAL_READY_BOTTOM_LINES = 8;
162
+ const INTERRUPT_READY_CHECK_DELAY_MS = 250;
161
163
 
162
164
  function materializeTerminalFrameLinesText(lines: TerminalFrameLine[]): string {
163
165
  return lines
@@ -408,50 +410,9 @@ export class CodexManager {
408
410
  return;
409
411
  }
410
412
 
411
- const previousMessages = handle.runtime.messages;
412
- const previousHasOlderMessages = handle.runtime.hasOlderMessages;
413
-
414
413
  this.clearLastError(handle);
415
- handle.awaitingJsonlTurn = false;
416
- if (handle.jsonlMessagesState.runtimePhase !== 'idle') {
417
- handle.jsonlMessagesState.runtimePhase = 'idle';
418
- handle.jsonlMessagesState.activityRevision += 1;
419
- refreshCodexJsonlMessageStatuses(handle.jsonlMessagesState);
420
- }
421
-
422
- const nextAllMessages = materializeCodexJsonlMessages(handle.jsonlMessagesState);
423
- const nextMessages = this.selectRecentMessages(nextAllMessages);
424
- const nextHasOlderMessages = nextAllMessages.length > nextMessages.length;
425
- const allMessagesChanged = !messagesEqual(handle.runtime.allMessages, nextAllMessages);
426
- const messagesChanged = !messagesEqual(handle.runtime.messages, nextMessages);
427
- const hasOlderMessagesChanged = handle.runtime.hasOlderMessages !== nextHasOlderMessages;
428
-
429
- if (allMessagesChanged) {
430
- handle.runtime.allMessages = nextAllMessages;
431
- }
432
- if (messagesChanged) {
433
- handle.runtime.messages = nextMessages;
434
- }
435
- if (allMessagesChanged || messagesChanged || hasOlderMessagesChanged) {
436
- handle.runtime.hasOlderMessages = nextHasOlderMessages;
437
- }
438
-
439
- this.stopHandlePtyPreservingReplay(handle, 'stop-active-run');
440
- this.setStatus(handle, 'idle', true);
441
-
442
- if (this.isActiveHandle(handle) && (allMessagesChanged || messagesChanged || hasOlderMessagesChanged)) {
443
- const upsertPayload = this.createMessagesUpsertPayload(
444
- handle,
445
- previousMessages,
446
- nextMessages,
447
- previousHasOlderMessages,
448
- nextHasOlderMessages
449
- );
450
- if (upsertPayload) {
451
- this.callbacks.emitMessagesUpsert(upsertPayload);
452
- }
453
- }
454
-
414
+ await this.sendInterruptSequence(handle);
415
+ await this.maybeMarkHandleReadyAfterInterrupt(handle);
455
416
  this.scheduleJsonlRefresh(0, 'stop-active-run');
456
417
  }
457
418
 
@@ -1543,6 +1504,80 @@ export class CodexManager {
1543
1504
  }
1544
1505
  }
1545
1506
 
1507
+ private async sendInterruptSequence(handle: CodexHandle): Promise<void> {
1508
+ if (!handle.pty) {
1509
+ return;
1510
+ }
1511
+
1512
+ this.log('info', 'sending codex interrupt sequence', {
1513
+ ...this.handleContext(handle),
1514
+ runtimeStatus: handle.runtime.status
1515
+ });
1516
+
1517
+ handle.pty.pty.write('\x1b');
1518
+ }
1519
+
1520
+ private async maybeMarkHandleReadyAfterInterrupt(handle: CodexHandle): Promise<void> {
1521
+ await sleep(INTERRUPT_READY_CHECK_DELAY_MS);
1522
+ if (!this.isActiveHandle(handle) || !handle.pty) {
1523
+ return;
1524
+ }
1525
+
1526
+ const previousMessages = handle.runtime.messages;
1527
+ const previousHasOlderMessages = handle.runtime.hasOlderMessages;
1528
+
1529
+ try {
1530
+ const { bottomText } = await this.getHandleTerminalView(handle);
1531
+ if (!looksReadyForInput(bottomText)) {
1532
+ return;
1533
+ }
1534
+
1535
+ handle.awaitingJsonlTurn = false;
1536
+ if (handle.jsonlMessagesState.runtimePhase !== 'idle') {
1537
+ handle.jsonlMessagesState.runtimePhase = 'idle';
1538
+ handle.jsonlMessagesState.activityRevision += 1;
1539
+ refreshCodexJsonlMessageStatuses(handle.jsonlMessagesState);
1540
+ }
1541
+
1542
+ const nextAllMessages = materializeCodexJsonlMessages(handle.jsonlMessagesState);
1543
+ const nextMessages = this.selectRecentMessages(nextAllMessages);
1544
+ const nextHasOlderMessages = nextAllMessages.length > nextMessages.length;
1545
+ const allMessagesChanged = !messagesEqual(handle.runtime.allMessages, nextAllMessages);
1546
+ const messagesChanged = !messagesEqual(handle.runtime.messages, nextMessages);
1547
+ const hasOlderMessagesChanged = handle.runtime.hasOlderMessages !== nextHasOlderMessages;
1548
+
1549
+ if (allMessagesChanged) {
1550
+ handle.runtime.allMessages = nextAllMessages;
1551
+ }
1552
+ if (messagesChanged) {
1553
+ handle.runtime.messages = nextMessages;
1554
+ }
1555
+ if (allMessagesChanged || messagesChanged || hasOlderMessagesChanged) {
1556
+ handle.runtime.hasOlderMessages = nextHasOlderMessages;
1557
+ }
1558
+
1559
+ this.setStatus(handle, 'idle', true);
1560
+
1561
+ if (this.isActiveHandle(handle) && (allMessagesChanged || messagesChanged || hasOlderMessagesChanged)) {
1562
+ const upsertPayload = this.createMessagesUpsertPayload(
1563
+ handle,
1564
+ previousMessages,
1565
+ nextMessages,
1566
+ previousHasOlderMessages,
1567
+ nextHasOlderMessages
1568
+ );
1569
+ if (upsertPayload) {
1570
+ this.callbacks.emitMessagesUpsert(upsertPayload);
1571
+ }
1572
+ }
1573
+ } catch (error) {
1574
+ this.log('warn', 'failed to confirm codex ready state after interrupt', {
1575
+ ...this.handleContext(handle),
1576
+ error: errorMessage(error, 'Failed to confirm ready state')
1577
+ });
1578
+ }
1579
+ }
1580
+
1546
1581
  private getLastActivityAt(handle: CodexHandle): number {
1547
1582
  return Math.max(
1548
1583
  handle.lastJsonlActivityAt ?? 0,