cosmoremote 2.1.0 → 2.1.2

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/session.js CHANGED
@@ -37,6 +37,10 @@ class CLISession extends events_1.EventEmitter {
37
37
  sessionId;
38
38
  process = null;
39
39
  cliBinary;
40
+ // Public so bridge.ts can compute the on-disk Claude session path
41
+ // (~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl) to verify whether a
42
+ // turn actually wrote conversation history before persisting the session
43
+ // id to session-contexts.json.
40
44
  workingDir;
41
45
  additionalDirs;
42
46
  model;
@@ -45,6 +49,7 @@ class CLISession extends events_1.EventEmitter {
45
49
  claudeSettingsPath = null;
46
50
  isClaude;
47
51
  isCodex;
52
+ isCursor;
48
53
  // Per-turn state — reset on every sendPrompt
49
54
  fullOutput = "";
50
55
  cleanOutput = "";
@@ -65,11 +70,13 @@ class CLISession extends events_1.EventEmitter {
65
70
  codexResponseDelivered = false;
66
71
  stdoutLineBuffer = "";
67
72
  stderrLineBuffer = "";
73
+ promptQueue = [];
68
74
  // Persistent-process state (Claude only)
69
75
  turnInFlight = false;
70
76
  idleTimer = null;
71
77
  codexThreadId = null;
72
78
  claudeSessionId = null;
79
+ cursorSessionId = null;
73
80
  // Optional persistent Codex adapter (experimental, opt-in via env var).
74
81
  codexPersistent = null;
75
82
  // True iff this session was constructed with a pre-existing claudeSessionId
@@ -86,8 +93,10 @@ class CLISession extends events_1.EventEmitter {
86
93
  this.claudeSettings = opts.claudeSettings ?? null;
87
94
  this.claudeSessionId = opts.initialClaudeSessionId ?? null;
88
95
  this.codexThreadId = opts.initialCodexThreadId ?? null;
96
+ this.cursorSessionId = opts.initialCursorSessionId ?? null;
89
97
  this.isClaude = this.cliBinary.includes("claude");
90
98
  this.isCodex = this.cliBinary.includes("codex");
99
+ this.isCursor = this.cliBinary.includes("cursor");
91
100
  // If we were given an existing Claude session id (continuation or imported
92
101
  // terminal conversation), spawn with `--resume` so Claude reloads history.
93
102
  this.claudeSessionResumable = this.isClaude && !!opts.initialClaudeSessionId;
@@ -105,9 +114,19 @@ class CLISession extends events_1.EventEmitter {
105
114
  }
106
115
  this.lastPromptContent = content;
107
116
  this.lastPromptAt = now;
117
+ if (this.turnInFlight) {
118
+ this.enqueuePrompt(content, attachments);
119
+ return;
120
+ }
121
+ this.dispatchPrompt(content, attachments);
122
+ }
123
+ dispatchPrompt(content, attachments) {
108
124
  if (this.isClaude) {
109
125
  this.sendPromptClaudePersistent(content, attachments);
110
126
  }
127
+ else if (this.isCursor) {
128
+ this.sendPromptCursorOneShot(content, attachments);
129
+ }
111
130
  else if (this.isCodex && CODEX_PERSISTENT_ENABLED) {
112
131
  this.sendPromptCodexPersistent(content, attachments);
113
132
  }
@@ -115,9 +134,29 @@ class CLISession extends events_1.EventEmitter {
115
134
  this.sendPromptOneShot(content, attachments);
116
135
  }
117
136
  }
137
+ enqueuePrompt(content, attachments) {
138
+ this.promptQueue.push({ content, attachments });
139
+ console.log(` [session:${this.sessionId.substring(0, 8)}] queued prompt position=${this.promptQueue.length}`);
140
+ this.emit("status", `Prompt queued (${this.promptQueue.length})`);
141
+ }
142
+ emitDoneAndContinue(result) {
143
+ this.emit("done", result);
144
+ if (this.promptQueue.length === 0)
145
+ return;
146
+ const next = this.promptQueue.shift();
147
+ if (!next)
148
+ return;
149
+ setImmediate(() => {
150
+ if (this.turnInFlight) {
151
+ this.promptQueue.unshift(next);
152
+ return;
153
+ }
154
+ this.dispatchPrompt(next.content, next.attachments);
155
+ });
156
+ }
118
157
  sendPromptCodexPersistent(content, attachments) {
119
158
  if (this.turnInFlight) {
120
- this.emit("error", "Previous Codex turn still running.");
159
+ this.enqueuePrompt(content, attachments);
121
160
  return;
122
161
  }
123
162
  if (attachments.length > 0) {
@@ -159,7 +198,7 @@ class CLISession extends events_1.EventEmitter {
159
198
  this.codexThreadId = this.codexPersistent.threadId;
160
199
  }
161
200
  this.flushOutput();
162
- this.emit("done", { code: result.code, fullOutput: this.cleanOutput });
201
+ this.emitDoneAndContinue({ code: result.code, fullOutput: this.cleanOutput });
163
202
  this.scheduleIdleShutdown();
164
203
  });
165
204
  }
@@ -181,8 +220,7 @@ class CLISession extends events_1.EventEmitter {
181
220
  // ---------------------------------------------------------------------------
182
221
  sendPromptClaudePersistent(content, attachments) {
183
222
  if (this.turnInFlight) {
184
- console.log(` [session:${this.sessionId.substring(0, 8)}] turn already in flight, queue not implemented — dropping prompt`);
185
- this.emit("error", "Previous turn still running. Please wait for it to finish before sending another prompt.");
223
+ this.enqueuePrompt(content, attachments);
186
224
  return;
187
225
  }
188
226
  this.resetTurnState();
@@ -280,7 +318,7 @@ class CLISession extends events_1.EventEmitter {
280
318
  if (code !== 0 && !this.cleanOutput.trim() && finalOutput.trim()) {
281
319
  this.emit("error", finalOutput);
282
320
  }
283
- this.emit("done", { code: code ?? -1, fullOutput: finalOutput });
321
+ this.emitDoneAndContinue({ code: code ?? -1, fullOutput: finalOutput });
284
322
  }
285
323
  this.cleanupAttachmentFiles();
286
324
  });
@@ -293,7 +331,7 @@ class CLISession extends events_1.EventEmitter {
293
331
  if (wasInFlight) {
294
332
  const finalOutput = this.rememberCliFailure(err.message) || err.message;
295
333
  this.emit("error", finalOutput);
296
- this.emit("done", { code: -1, fullOutput: finalOutput });
334
+ this.emitDoneAndContinue({ code: -1, fullOutput: finalOutput });
297
335
  }
298
336
  this.cleanupAttachmentFiles();
299
337
  });
@@ -397,7 +435,7 @@ class CLISession extends events_1.EventEmitter {
397
435
  if (this.cleanOutput.length === 0 && typeof event.result === "string" && event.result) {
398
436
  this.emit("output", event.result);
399
437
  }
400
- this.emit("done", { code: 0, fullOutput: finalText });
438
+ this.emitDoneAndContinue({ code: 0, fullOutput: finalText });
401
439
  this.scheduleIdleShutdown();
402
440
  return;
403
441
  }
@@ -446,6 +484,164 @@ class CLISession extends events_1.EventEmitter {
446
484
  }
447
485
  }
448
486
  // ---------------------------------------------------------------------------
487
+ // Cursor Agent path: headless one-shot process per prompt, resumed by chat id.
488
+ // ---------------------------------------------------------------------------
489
+ sendPromptCursorOneShot(content, attachments) {
490
+ if (this.turnInFlight) {
491
+ this.enqueuePrompt(content, attachments);
492
+ return;
493
+ }
494
+ this.resetTurnState();
495
+ this.turnInFlight = true;
496
+ this.startedAt = Date.now();
497
+ this.lastActivityAt = this.startedAt;
498
+ this.cancelIdleTimer();
499
+ const attachmentPaths = this.prepareAttachmentFiles(attachments);
500
+ const promptWithAttachments = this.buildPromptWithAttachments(content, attachmentPaths, true);
501
+ const args = [
502
+ "-p",
503
+ "--output-format", "stream-json",
504
+ "--stream-partial-output",
505
+ "--force",
506
+ "--sandbox", "disabled",
507
+ "--trust",
508
+ "--workspace", this.workingDir,
509
+ ];
510
+ if (this.cursorSessionId)
511
+ args.push("--resume", this.cursorSessionId);
512
+ if (this.model)
513
+ args.push("--model", this.model);
514
+ args.push(promptWithAttachments);
515
+ console.log(` [session:${this.sessionId.substring(0, 8)}] spawning Cursor binary=${this.cliBinary} cwd=${this.workingDir} cursorSessionId=${this.cursorSessionId ?? "new"}`);
516
+ this.process = (0, child_process_1.spawn)(this.cliBinary, args, {
517
+ cwd: this.workingDir,
518
+ env: { ...process.env, FORCE_COLOR: "0" },
519
+ detached: true,
520
+ stdio: ["ignore", "pipe", "pipe"],
521
+ });
522
+ console.log(` [session:${this.sessionId.substring(0, 8)}] Cursor args: ${args.map((a) => JSON.stringify(a)).join(" ")} pid=${this.process.pid ?? "unknown"}`);
523
+ this.startTurnWatchdog();
524
+ this.process.stdout?.on("data", (chunk) => {
525
+ const text = chunk.toString();
526
+ this.fullOutput += text;
527
+ this.lastActivityAt = Date.now();
528
+ this.stdoutLineBuffer += text;
529
+ const lines = this.drainBufferedLines("stdout");
530
+ for (const line of lines) {
531
+ this.handleCursorStreamLine(line);
532
+ }
533
+ if (lines.length === 0 && !text.trim().startsWith("{")) {
534
+ this.cleanOutput += text;
535
+ this.bufferOutput(text);
536
+ }
537
+ });
538
+ this.process.stderr?.on("data", (chunk) => {
539
+ const text = chunk.toString().trim();
540
+ if (!text)
541
+ return;
542
+ this.lastActivityAt = Date.now();
543
+ console.log(` [session:${this.sessionId.substring(0, 8)}] cursor stderr: ${text.replace(/\s+/g, " ").slice(0, 220)}`);
544
+ const isNoise = /FORCE_COLOR|trust.*workspace|sandbox/i.test(text);
545
+ if (isNoise)
546
+ return;
547
+ this.rememberCliFailure(text);
548
+ this.stderrLineBuffer += `${text}\n`;
549
+ });
550
+ this.process.on("close", (code) => {
551
+ const elapsed = this.startedAt ? Date.now() - this.startedAt : -1;
552
+ console.log(` [session:${this.sessionId.substring(0, 8)}] Cursor close code=${code} elapsedMs=${elapsed} cleanLen=${this.cleanOutput.length}`);
553
+ const wasInFlight = this.turnInFlight;
554
+ this.clearTurnWatchdog();
555
+ if (this.killTimeout)
556
+ clearTimeout(this.killTimeout);
557
+ this.killTimeout = null;
558
+ this.flushOutput();
559
+ if (wasInFlight) {
560
+ const finalOutput = this.finalOutputForClose(code);
561
+ if (code !== 0 && !this.cleanOutput.trim() && finalOutput.trim()) {
562
+ this.emit("error", finalOutput);
563
+ }
564
+ this.turnInFlight = false;
565
+ this.emitDoneAndContinue({ code: code ?? -1, fullOutput: finalOutput });
566
+ }
567
+ this.cleanupAttachmentFiles();
568
+ this.process = null;
569
+ this.startedAt = null;
570
+ this.lastActivityAt = null;
571
+ this.pendingErrorText = null;
572
+ this.lastMeaningfulStderr = null;
573
+ });
574
+ this.process.on("error", (err) => {
575
+ const elapsed = this.startedAt ? Date.now() - this.startedAt : -1;
576
+ console.log(` [session:${this.sessionId.substring(0, 8)}] Cursor error after ${elapsed}ms: ${err.message}`);
577
+ this.clearTurnWatchdog();
578
+ if (this.killTimeout)
579
+ clearTimeout(this.killTimeout);
580
+ this.killTimeout = null;
581
+ this.emit("error", err.message);
582
+ this.cleanupAttachmentFiles();
583
+ this.process = null;
584
+ this.startedAt = null;
585
+ this.lastActivityAt = null;
586
+ this.pendingErrorText = null;
587
+ this.lastMeaningfulStderr = null;
588
+ this.turnInFlight = false;
589
+ this.emitDoneAndContinue({ code: -1, fullOutput: err.message });
590
+ });
591
+ }
592
+ handleCursorStreamLine(line) {
593
+ let event;
594
+ try {
595
+ event = JSON.parse(line);
596
+ }
597
+ catch {
598
+ if (line.trim()) {
599
+ this.cleanOutput += line;
600
+ this.bufferOutput(line);
601
+ }
602
+ return;
603
+ }
604
+ const cursorSessionId = this.extractCursorSessionId(event);
605
+ if (cursorSessionId && this.cursorSessionId !== cursorSessionId) {
606
+ this.cursorSessionId = cursorSessionId;
607
+ console.log(` [session:${this.sessionId.substring(0, 8)}] cursor session_id=${cursorSessionId}`);
608
+ }
609
+ const tool = this.extractCursorTool(event);
610
+ if (tool) {
611
+ const label = this.formatToolLabel(tool.name, tool.input);
612
+ this.emit("status", label);
613
+ this.maybeEmitToolEvent({ name: tool.name, input: tool.input });
614
+ }
615
+ const status = this.formatCursorStatus(event);
616
+ if (status) {
617
+ this.emit("status", status);
618
+ }
619
+ const error = this.extractCursorError(event);
620
+ if (error) {
621
+ const message = this.rememberCliFailure(error) || error;
622
+ if (!this.cleanOutput.trim())
623
+ this.cleanOutput = message;
624
+ this.emit("error", message);
625
+ }
626
+ const text = this.extractCursorText(event);
627
+ if (text) {
628
+ this.cleanOutput += text;
629
+ this.bufferOutput(text);
630
+ }
631
+ if (this.isCursorTurnEnd(event)) {
632
+ const finalText = this.extractCursorFinalText(event) || this.cleanOutput;
633
+ console.log(` [session:${this.sessionId.substring(0, 8)}] Cursor turn complete len=${finalText.length}`);
634
+ this.clearTurnWatchdog();
635
+ this.turnInFlight = false;
636
+ this.cleanupAttachmentFiles();
637
+ this.flushOutput();
638
+ if (this.cleanOutput.length === 0 && finalText) {
639
+ this.emit("output", finalText);
640
+ }
641
+ this.emitDoneAndContinue({ code: 0, fullOutput: finalText });
642
+ }
643
+ }
644
+ // ---------------------------------------------------------------------------
449
645
  // One-shot path (Codex today; reused by anything non-persistent).
450
646
  // ---------------------------------------------------------------------------
451
647
  sendPromptOneShot(content, attachments) {
@@ -556,7 +752,7 @@ class CLISession extends events_1.EventEmitter {
556
752
  if (code !== 0 && !this.cleanOutput.trim() && finalOutput.trim()) {
557
753
  this.emit("error", finalOutput);
558
754
  }
559
- this.emit("done", { code: code ?? -1, fullOutput: finalOutput });
755
+ this.emitDoneAndContinue({ code: code ?? -1, fullOutput: finalOutput });
560
756
  this.cleanupAttachmentFiles();
561
757
  this.process = null;
562
758
  this.startedAt = null;
@@ -643,6 +839,7 @@ class CLISession extends events_1.EventEmitter {
643
839
  kill() {
644
840
  this.cancelIdleTimer();
645
841
  this.cleanupClaudeSettings();
842
+ this.promptQueue = [];
646
843
  if (this.codexPersistent) {
647
844
  void this.codexPersistent.interrupt().catch(() => { });
648
845
  this.codexPersistent.kill();
@@ -662,8 +859,7 @@ class CLISession extends events_1.EventEmitter {
662
859
  this.killProcessGroup("SIGKILL");
663
860
  setTimeout(() => {
664
861
  if (this.process && !this.process.killed) {
665
- console.log(` [session:${this.sessionId.substring(0, 8)}] SIGKILL didn't work either, orphaning pid=${this.process.pid}`);
666
- this.process = null;
862
+ console.log(` [session:${this.sessionId.substring(0, 8)}] SIGKILL did not confirm exit for pid=${this.process.pid}; keeping handle for later close/error and retry logs`);
667
863
  }
668
864
  }, 500);
669
865
  }
@@ -698,6 +894,9 @@ class CLISession extends events_1.EventEmitter {
698
894
  get hasTurnInFlight() {
699
895
  return this.turnInFlight;
700
896
  }
897
+ get hasQueuedPrompts() {
898
+ return this.promptQueue.length > 0;
899
+ }
701
900
  resetTurnState() {
702
901
  this.fullOutput = "";
703
902
  this.cleanOutput = "";
@@ -787,12 +986,17 @@ class CLISession extends events_1.EventEmitter {
787
986
  let label = "";
788
987
  switch (name.toLowerCase()) {
789
988
  case "read":
989
+ case "read_file":
990
+ case "list_dir":
790
991
  case "str_replace_editor":
791
992
  case "str_replace_based_edit_tool": {
792
993
  const cmd = input?.command;
793
- const path = input?.path;
994
+ const path = input?.path ?? input?.target_file;
794
995
  const file = path ? path.split("/").pop() : undefined;
795
- if (cmd === "view" || !cmd || name.toLowerCase() === "read") {
996
+ if (name.toLowerCase() === "list_dir") {
997
+ label = file ? `📁 Listing ${file}` : "📁 Listing directory";
998
+ }
999
+ else if (cmd === "view" || !cmd || name.toLowerCase() === "read" || name.toLowerCase() === "read_file") {
796
1000
  label = file ? `📖 Reading ${file}` : "📖 Reading file";
797
1001
  }
798
1002
  else if (cmd === "create") {
@@ -804,15 +1008,18 @@ class CLISession extends events_1.EventEmitter {
804
1008
  break;
805
1009
  }
806
1010
  case "bash":
807
- case "execute_bash": {
808
- const cmd = input?.command;
1011
+ case "execute_bash":
1012
+ case "run_terminal_cmd": {
1013
+ const cmd = input?.command ?? input?.cmd;
809
1014
  const preview = cmd ? cmd.replace(/\s+/g, " ").slice(0, 40) : "";
810
1015
  label = preview ? `⚙️ Running: ${preview}` : "⚙️ Running command";
811
1016
  break;
812
1017
  }
813
- case "edit": {
1018
+ case "edit":
1019
+ case "edit_file": {
814
1020
  const file = input?.path?.split("/").pop()
815
- ?? input?.file_path?.split("/").pop();
1021
+ ?? input?.file_path?.split("/").pop()
1022
+ ?? input?.target_file?.split("/").pop();
816
1023
  label = file ? `✏️ Editing ${file}` : "✏️ Editing file";
817
1024
  break;
818
1025
  }
@@ -826,8 +1033,11 @@ class CLISession extends events_1.EventEmitter {
826
1033
  label = pattern ? `🔍 Finding: ${pattern}` : "🔍 Finding files";
827
1034
  break;
828
1035
  }
829
- case "grep": {
830
- const pattern = input?.pattern?.slice(0, 30);
1036
+ case "grep":
1037
+ case "grep_search":
1038
+ case "file_search":
1039
+ case "codebase_search": {
1040
+ const pattern = (input?.pattern ?? input?.query)?.slice(0, 30);
831
1041
  label = pattern ? `🔎 Searching: ${pattern}` : "🔎 Searching files";
832
1042
  break;
833
1043
  }
@@ -958,6 +1168,140 @@ class CLISession extends events_1.EventEmitter {
958
1168
  return ".jpg";
959
1169
  }
960
1170
  }
1171
+ extractCursorSessionId(event) {
1172
+ const direct = event?.chatId ??
1173
+ event?.chat_id ??
1174
+ event?.sessionId ??
1175
+ event?.session_id ??
1176
+ event?.conversationId ??
1177
+ event?.conversation_id;
1178
+ if (typeof direct === "string" && direct.trim())
1179
+ return direct.trim();
1180
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1181
+ if ((type.includes("chat") || type.includes("session") || type.includes("conversation")) && typeof event?.id === "string") {
1182
+ return event.id.trim() || null;
1183
+ }
1184
+ return null;
1185
+ }
1186
+ extractCursorTool(event) {
1187
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1188
+ const item = event?.item;
1189
+ const itemType = typeof item?.type === "string" ? item.type.toLowerCase() : "";
1190
+ const name = event?.name ??
1191
+ event?.toolName ??
1192
+ event?.tool_name ??
1193
+ event?.tool?.name ??
1194
+ // cursor-agent shape: { type:"tool_call", tool_call:{ toolName, title, ... } }
1195
+ event?.tool_call?.toolName ??
1196
+ event?.tool_call?.name ??
1197
+ item?.name ??
1198
+ item?.toolName ??
1199
+ item?.tool_name;
1200
+ if (typeof name === "string" && name.trim() && (type.includes("tool") || itemType.includes("tool"))) {
1201
+ const input = event?.input ?? event?.args ?? event?.arguments ??
1202
+ event?.tool_call?.args ?? event?.tool_call?.input ??
1203
+ item?.input ?? item?.args ?? item?.arguments;
1204
+ return { name: name.trim(), input: typeof input === "object" && input ? input : undefined };
1205
+ }
1206
+ const blocks = Array.isArray(event?.message?.content) ? event.message.content : [];
1207
+ for (const block of blocks) {
1208
+ if (block?.type === "tool_use" && typeof block?.name === "string") {
1209
+ return { name: block.name, input: typeof block.input === "object" && block.input ? block.input : undefined };
1210
+ }
1211
+ }
1212
+ return null;
1213
+ }
1214
+ extractCursorText(event) {
1215
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1216
+ const delta = event?.delta;
1217
+ if (typeof event?.text === "string" && (type.includes("delta") || type === "text" || type.includes("partial"))) {
1218
+ return event.text;
1219
+ }
1220
+ if (typeof event?.content === "string" && (type.includes("delta") || type === "text" || type.includes("partial"))) {
1221
+ return event.content;
1222
+ }
1223
+ if (typeof delta === "string")
1224
+ return delta;
1225
+ if (typeof delta?.text === "string")
1226
+ return delta.text;
1227
+ if (typeof delta?.content === "string")
1228
+ return delta.content;
1229
+ if (event?.type === "stream_event" && event.event?.type === "content_block_delta") {
1230
+ const streamDelta = event.event.delta;
1231
+ if (streamDelta?.type === "text_delta" && typeof streamDelta.text === "string")
1232
+ return streamDelta.text;
1233
+ }
1234
+ if (type === "assistant" || type === "message" || type === "assistant_message") {
1235
+ // cursor-agent (with --stream-partial-output) emits one `assistant` frame per
1236
+ // token chunk, each carrying an incremental DELTA at message.content[].text.
1237
+ // Do NOT gate on cleanOutput being empty — that dropped every chunk after the
1238
+ // first, so the phone saw first-chunk → silence → full answer. The turn-end
1239
+ // `result` handler only re-emits full text when nothing was streamed, so
1240
+ // streaming every delta here does not double-render.
1241
+ const content = event?.message?.content ?? event?.content;
1242
+ if (typeof content === "string")
1243
+ return content;
1244
+ if (Array.isArray(content)) {
1245
+ return content
1246
+ .map((block) => typeof block?.text === "string" ? block.text : typeof block?.content === "string" ? block.content : "")
1247
+ .filter(Boolean)
1248
+ .join("");
1249
+ }
1250
+ }
1251
+ return null;
1252
+ }
1253
+ extractCursorFinalText(event) {
1254
+ const result = event?.result ?? event?.response ?? event?.message ?? event?.output;
1255
+ if (typeof result === "string" && result.trim())
1256
+ return result;
1257
+ if (typeof event?.text === "string" && event.text.trim())
1258
+ return event.text;
1259
+ if (typeof result?.content === "string" && result.content.trim())
1260
+ return result.content;
1261
+ if (Array.isArray(result?.content)) {
1262
+ const text = result.content
1263
+ .map((block) => typeof block?.text === "string" ? block.text : typeof block?.content === "string" ? block.content : "")
1264
+ .filter(Boolean)
1265
+ .join("");
1266
+ return text || null;
1267
+ }
1268
+ return null;
1269
+ }
1270
+ extractCursorError(event) {
1271
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1272
+ if (!type.includes("error") && !event?.error)
1273
+ return null;
1274
+ return this.extractCliFailureFromValue(event?.error) ??
1275
+ (typeof event?.message === "string" ? event.message : null) ??
1276
+ (typeof event?.detail === "string" ? event.detail : null);
1277
+ }
1278
+ isCursorTurnEnd(event) {
1279
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1280
+ const subtype = typeof event?.subtype === "string" ? event.subtype.toLowerCase() : "";
1281
+ return (type === "result" ||
1282
+ type === "done" ||
1283
+ type === "complete" ||
1284
+ type === "completed" ||
1285
+ type === "turn.completed" ||
1286
+ type === "response.completed" ||
1287
+ type === "chat.completed" ||
1288
+ type === "session.completed" ||
1289
+ subtype === "success");
1290
+ }
1291
+ formatCursorStatus(event) {
1292
+ const type = typeof event?.type === "string" ? event.type.toLowerCase() : "";
1293
+ if (!type)
1294
+ return null;
1295
+ if (type.includes("thinking") || type.includes("reasoning"))
1296
+ return "Thinking...";
1297
+ if (type.includes("model") || type.includes("request"))
1298
+ return "Calling model...";
1299
+ if (type.includes("completed") || type === "done" || type === "result")
1300
+ return "Turn complete";
1301
+ if (type.includes("started") || type.includes("created"))
1302
+ return "Starting conversation...";
1303
+ return null;
1304
+ }
961
1305
  hasWorkspacePermissionError() {
962
1306
  const haystack = [
963
1307
  this.cleanOutput,