skydive-cli 0.1.0-beta.122 → 0.1.0-beta.180

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/README.md CHANGED
@@ -142,18 +142,19 @@ skydive chat -p "status?" --agent grace --json # structured envelo
142
142
  browser login — sign in first with `skydive auth login --web` or set
143
143
  `SKYDIVE_SESSION_TOKEN`.
144
144
  - `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
145
- runId, text }` instead of streaming the raw text.
145
+ messageId, text }` instead of streaming the raw text.
146
146
  - **Recovering a dropped run.** A long run's stream can be cut off at the edge
147
- (a gateway 502/504) after the run was created. The run keeps going
148
- server-side, so a blind retry would re-execute the agent. Instead, note the
149
- `runId` (always in `--json` output) and fetch the result once it's done:
147
+ (a gateway 502/504) after the message was accepted. The agent keeps working
148
+ server-side, so a blind retry would re-execute it. Instead, note the
149
+ `messageId` (always in `--json` output) and fetch the result once it's done:
150
150
 
151
151
  ```sh
152
- skydive runs get <runId> # prints the reply; --json for the envelope
152
+ skydive messages get <messageId> # prints the reply; --json for the envelope
153
153
  ```
154
154
 
155
- `runs get` re-attaches to the run and replays its full reply whether it's
156
- still streaming or already finished.
155
+ `messages get` re-attaches to the exchange behind that message id and replays
156
+ the full reply whether it's still streaming or already finished. Message ids
157
+ are the handle this API works in; run ids stay server-side.
157
158
 
158
159
  It defaults to the production API (`https://api.skydive.com`). For local
159
160
  dev, point it at your stack:
package/dist/js/bin.mjs CHANGED
@@ -31,7 +31,7 @@ var __exportAll = (all, no_symbols) => {
31
31
 
32
32
  //#endregion
33
33
  //#region package.json
34
- var version$1 = "0.1.0-beta.122";
34
+ var version$1 = "0.1.0-beta.180";
35
35
 
36
36
  //#endregion
37
37
  //#region src/types.ts
@@ -886,7 +886,7 @@ function truncate(value, max) {
886
886
  if (trimmed.length <= max) return trimmed || "-";
887
887
  return `${trimmed.slice(0, max - 1)}\u2026`;
888
888
  }
889
- const getCommand = {
889
+ const getCommand$1 = {
890
890
  command: "get <id>",
891
891
  describe: "Get agent details",
892
892
  builder: (y) => y.positional("id", {
@@ -956,7 +956,7 @@ const createCommand$1 = {
956
956
  const agentsCommand = {
957
957
  command: "agents",
958
958
  describe: "Manage agents",
959
- builder: (y) => y.command(listCommand$4).command(getCommand).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
959
+ builder: (y) => y.command(listCommand$4).command(getCommand$1).command(createCommand$1).demandCommand(1, "Specify a subcommand: list, get, create"),
960
960
  handler: () => {}
961
961
  };
962
962
 
@@ -2152,7 +2152,7 @@ const chatCommand = {
2152
2152
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2153
2153
  process.exit(1);
2154
2154
  }
2155
- const { runChat } = await import("./boot-BvkqTAxQ.mjs");
2155
+ const { runChat } = await import("./boot-B-eA8M6t.mjs");
2156
2156
  await runChat({
2157
2157
  appUrl,
2158
2158
  sessionToken: session.value.sessionToken,
@@ -2199,6 +2199,60 @@ async function runPrintMode({ argv, appUrl }) {
2199
2199
  }
2200
2200
  }
2201
2201
 
2202
+ //#endregion
2203
+ //#region src/commands/messages.ts
2204
+ /**
2205
+ * `skydive messages get <messageId>` re-attaches to an exchange by message id
2206
+ * and prints the agent's reply.
2207
+ *
2208
+ * This is the recovery path for `chat -p`: a long run's stream can drop at the
2209
+ * edge (Cloudflare 502/504) after the message was accepted, leaving the caller
2210
+ * with a messageId but no result. The agent keeps working server-side, so a
2211
+ * blind retry would double-execute an agent that may have write access.
2212
+ * Instead, fetch the finished result by message id here — the server resolves
2213
+ * the run behind the message and replays it from its persisted event log, so
2214
+ * this works whether the run is still live or already done.
2215
+ *
2216
+ * Message ids are the currency of this API: `chat -p --json` reports the
2217
+ * `messageId` and this command consumes it. Run ids stay server-side.
2218
+ */
2219
+ const getCommand = {
2220
+ command: "get <message-id>",
2221
+ describe: "Fetch a message by id and print the reply (recovers a timed-out -p)",
2222
+ builder: (y) => y.positional("message-id", {
2223
+ type: "string",
2224
+ demandOption: true,
2225
+ describe: "Message id (the `messageId` from a prior `chat -p --json`)"
2226
+ }),
2227
+ handler: async (argv) => {
2228
+ const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2229
+ const session = resolveSession({ appUrl });
2230
+ if (session.isErr()) {
2231
+ printError(`${session.error.message} Run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
2232
+ process.exit(1);
2233
+ }
2234
+ const { messageGet } = await Promise.resolve().then(() => print_exports);
2235
+ try {
2236
+ const result = await messageGet({
2237
+ appUrl,
2238
+ sessionToken: session.value.sessionToken,
2239
+ messageId: argv["message-id"],
2240
+ json: argv.json
2241
+ });
2242
+ if (argv.json) output(argv, result);
2243
+ } catch (error) {
2244
+ printError(error instanceof Error ? error.message : String(error));
2245
+ process.exit(1);
2246
+ }
2247
+ }
2248
+ };
2249
+ const messagesCommand = {
2250
+ command: "messages",
2251
+ describe: "Inspect chat messages and re-fetch agent replies",
2252
+ builder: (y) => y.command(getCommand).demandCommand(1, "Specify a subcommand: get"),
2253
+ handler: () => {}
2254
+ };
2255
+
2202
2256
  //#endregion
2203
2257
  //#region src/chat/api/rest.ts
2204
2258
  var HttpError = class extends Error {
@@ -2239,6 +2293,65 @@ function createRestClient({ appUrl, sessionToken }) {
2239
2293
  });
2240
2294
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2241
2295
  }
2296
+ const streamEvents = async ({ path, label, signal, onEvent }) => {
2297
+ let lastEventId = null;
2298
+ let finished = false;
2299
+ let reconnects = 0;
2300
+ for (;;) {
2301
+ if (signal.aborted) return;
2302
+ try {
2303
+ const headers = {
2304
+ authorization: `Bearer ${sessionToken}`,
2305
+ accept: "text/event-stream"
2306
+ };
2307
+ if (lastEventId) headers["last-event-id"] = lastEventId;
2308
+ const res = await fetch(`${appUrl}${path}`, {
2309
+ headers,
2310
+ signal
2311
+ });
2312
+ if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
2313
+ reconnects = 0;
2314
+ const parser = createParser({ onEvent: (message) => {
2315
+ if (message.id) lastEventId = message.id;
2316
+ if (message.event === "error") {
2317
+ const { error } = streamErrorSchema.parse(JSON.parse(message.data));
2318
+ throw new Error(error);
2319
+ }
2320
+ const event = runStreamEventSchema.parse(JSON.parse(message.data));
2321
+ if (event.kind === "finished") finished = true;
2322
+ onEvent(event.kind === "finished" ? {
2323
+ ...event,
2324
+ error: event.error ?? null
2325
+ } : event);
2326
+ } });
2327
+ const decoder = new TextDecoder();
2328
+ const reader = res.body.getReader();
2329
+ try {
2330
+ for (;;) {
2331
+ const { done, value } = await reader.read();
2332
+ if (done) break;
2333
+ parser.feed(decoder.decode(value, { stream: true }));
2334
+ if (finished) return;
2335
+ }
2336
+ } finally {
2337
+ try {
2338
+ await reader.cancel();
2339
+ } catch (_error) {}
2340
+ }
2341
+ } catch (err) {
2342
+ if (signal.aborted) return;
2343
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) throw err;
2344
+ reconnects += 1;
2345
+ if (reconnects > MAX_STREAM_RECONNECTS) throw err;
2346
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2347
+ continue;
2348
+ }
2349
+ if (finished) return;
2350
+ reconnects += 1;
2351
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error(`${label} stream ended unexpectedly`);
2352
+ await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2353
+ }
2354
+ };
2242
2355
  return {
2243
2356
  listAgents: async ({ scope, onPage }) => {
2244
2357
  const all = [];
@@ -2365,36 +2478,35 @@ function createRestClient({ appUrl, sessionToken }) {
2365
2478
  });
2366
2479
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
2367
2480
  },
2368
- streamRun: async ({ runId, signal, onEvent }) => {
2369
- let lastEventId = null;
2370
- let finished = false;
2481
+ streamRun: async ({ runId, signal, onEvent }) => streamEvents({
2482
+ path: `/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`,
2483
+ label: "run",
2484
+ signal,
2485
+ onEvent
2486
+ }),
2487
+ streamMessage: async ({ messageId, signal, onEvent }) => streamEvents({
2488
+ path: `/api/v1/chat/messages/${encodeURIComponent(messageId)}/stream`,
2489
+ label: "message",
2490
+ signal,
2491
+ onEvent
2492
+ }),
2493
+ streamConversation: async ({ conversationId, signal, onEvent }) => {
2371
2494
  let reconnects = 0;
2372
2495
  for (;;) {
2373
2496
  if (signal.aborted) return;
2374
2497
  try {
2375
- const headers = {
2376
- authorization: `Bearer ${sessionToken}`,
2377
- accept: "text/event-stream"
2378
- };
2379
- if (lastEventId) headers["last-event-id"] = lastEventId;
2380
- const res = await fetch(`${appUrl}/api/v1/chat/runs/${encodeURIComponent(runId)}/stream`, {
2381
- headers,
2498
+ const res = await fetch(`${appUrl}/api/v1/chat/conversations/${encodeURIComponent(conversationId)}/stream`, {
2499
+ headers: {
2500
+ authorization: `Bearer ${sessionToken}`,
2501
+ accept: "text/event-stream"
2502
+ },
2382
2503
  signal
2383
2504
  });
2384
2505
  if (!res.ok || !res.body) throw new HttpError(res.status, await res.text().catch(() => ""));
2385
2506
  reconnects = 0;
2386
2507
  const parser = createParser({ onEvent: (message) => {
2387
- if (message.id) lastEventId = message.id;
2388
- if (message.event === "error") {
2389
- const { error } = streamErrorSchema.parse(JSON.parse(message.data));
2390
- throw new Error(error);
2391
- }
2392
- const event = runStreamEventSchema.parse(JSON.parse(message.data));
2393
- if (event.kind === "finished") finished = true;
2394
- onEvent(event.kind === "finished" ? {
2395
- ...event,
2396
- error: event.error ?? null
2397
- } : event);
2508
+ const parsed = conversationStreamEventSchema.safeParse(JSON.parse(message.data));
2509
+ if (parsed.success) onEvent(parsed.data);
2398
2510
  } });
2399
2511
  const decoder = new TextDecoder();
2400
2512
  const reader = res.body.getReader();
@@ -2403,7 +2515,6 @@ function createRestClient({ appUrl, sessionToken }) {
2403
2515
  const { done, value } = await reader.read();
2404
2516
  if (done) break;
2405
2517
  parser.feed(decoder.decode(value, { stream: true }));
2406
- if (finished) return;
2407
2518
  }
2408
2519
  } finally {
2409
2520
  try {
@@ -2418,9 +2529,9 @@ function createRestClient({ appUrl, sessionToken }) {
2418
2529
  await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2419
2530
  continue;
2420
2531
  }
2421
- if (finished) return;
2532
+ if (signal.aborted) return;
2422
2533
  reconnects += 1;
2423
- if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("run stream ended unexpectedly");
2534
+ if (reconnects > MAX_STREAM_RECONNECTS) throw new Error("conversation stream ended unexpectedly");
2424
2535
  await sleep(Math.min(500 * 2 ** reconnects, 5e3));
2425
2536
  }
2426
2537
  }
@@ -2509,6 +2620,7 @@ const recapResponseSchema = z.object({ recap: z.object({ text: z.string() }).nul
2509
2620
  const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
2510
2621
  const sendResultSchema = z.object({
2511
2622
  runId: z.string(),
2623
+ messageId: z.string().uuid().nullish(),
2512
2624
  conversationId: z.string().uuid(),
2513
2625
  isNewConversation: z.boolean(),
2514
2626
  steered: z.boolean().optional(),
@@ -2537,6 +2649,11 @@ const runStreamEventSchema = z.union([z.object({
2537
2649
  error: z.string().nullish()
2538
2650
  })]);
2539
2651
  const streamErrorSchema = z.object({ error: z.string() });
2652
+ const conversationStreamEventSchema = z.object({
2653
+ kind: z.literal("conversation"),
2654
+ id: z.string(),
2655
+ title: z.string().nullable()
2656
+ });
2540
2657
 
2541
2658
  //#endregion
2542
2659
  //#region src/chat/util.ts
@@ -2828,9 +2945,12 @@ function formatConnectCard(card) {
2828
2945
  //#endregion
2829
2946
  //#region src/chat/print.ts
2830
2947
  var print_exports = /* @__PURE__ */ __exportAll({
2948
+ collectRunText: () => collectRunText,
2949
+ messageGet: () => messageGet,
2831
2950
  readStdin: () => readStdin,
2832
2951
  resolveAgent: () => resolveAgent,
2833
- runPrint: () => runPrint
2952
+ runPrint: () => runPrint,
2953
+ toPrintError: () => toPrintError
2834
2954
  });
2835
2955
  /**
2836
2956
  * Non-interactive chat, à la `claude -p`. Sends a single prompt to an
@@ -2852,48 +2972,129 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
2852
2972
  scope: "org",
2853
2973
  onPage: null
2854
2974
  }), agentSelector);
2855
- const send = await client.sendMessage({
2856
- agentId: agent.id,
2857
- conversationId,
2858
- content: prompt,
2859
- attachmentIds: [],
2860
- clientSurface: "cli"
2975
+ let send;
2976
+ try {
2977
+ send = await client.sendMessage({
2978
+ agentId: agent.id,
2979
+ conversationId,
2980
+ content: prompt,
2981
+ attachmentIds: [],
2982
+ clientSurface: "cli"
2983
+ });
2984
+ } catch (err) {
2985
+ throw toPrintError(err);
2986
+ }
2987
+ const { text, connectCards } = await collectRunText({
2988
+ client,
2989
+ appUrl,
2990
+ target: {
2991
+ kind: "run",
2992
+ id: send.runId
2993
+ },
2994
+ onText: json ? null : (delta) => process.stdout.write(delta),
2995
+ messageIdForHint: send.messageId ?? null
2861
2996
  });
2997
+ if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
2998
+ if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
2999
+ return {
3000
+ agentId: agent.id,
3001
+ agentName: agent.name,
3002
+ conversationId: send.conversationId,
3003
+ isNewConversation: send.isNewConversation,
3004
+ messageId: send.messageId ?? null,
3005
+ text,
3006
+ connectCards
3007
+ };
3008
+ }
3009
+ /**
3010
+ * Turn a transport failure into an actionable CLI error. A Cloudflare edge
3011
+ * 5xx (502/504) on a long run otherwise leaks a raw HTML/JSON error page to
3012
+ * stdout, which is impossible to act on. When a messageId is known and
3013
+ * recoverable we point the user at `skydive messages get <messageId>` rather
3014
+ * than a blind retry (a retry re-executes an agent that may have write access).
3015
+ */
3016
+ function toPrintError(err, messageId) {
3017
+ if (err instanceof HttpError && err.status >= 500) {
3018
+ const recovery = messageId ? ` The run may still be completing server-side. Do NOT blindly retry (it would re-run the agent). Fetch the result with: skydive messages get ${messageId}` : "";
3019
+ return /* @__PURE__ */ new Error(`The request to Skydive timed out at the edge (HTTP ${err.status}).${recovery}`);
3020
+ }
3021
+ return err instanceof Error ? err : new Error(String(err));
3022
+ }
3023
+ /**
3024
+ * Stream a run to completion, folding text-delta chunks into the reply and
3025
+ * collecting any connect cards (OAuth / MCP / credential requests) the run
3026
+ * posts. Shared by `chat -p` (streaming the run it just created) and `messages
3027
+ * get` (re-attaching by message id — the server replays a finished run from its
3028
+ * persisted log, so this works whether the run is live or already done).
3029
+ */
3030
+ async function collectRunText({ client, appUrl, target, onText, messageIdForHint }) {
2862
3031
  let text = "";
2863
3032
  const controller = new AbortController();
2864
3033
  let streamError = null;
2865
3034
  const connectCards = [];
2866
- await client.streamRun({
2867
- runId: send.runId,
2868
- signal: controller.signal,
2869
- onEvent: (event) => {
2870
- if (event.kind === "finished") {
2871
- if (event.error) streamError = event.error;
2872
- return;
2873
- }
2874
- const chunk = event.chunk;
2875
- if (chunk["type"] === "text-delta") {
2876
- const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
2877
- if (delta) {
2878
- text += delta;
2879
- if (!json) process.stdout.write(delta);
2880
- }
2881
- } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
2882
- else {
2883
- const card = connectCardFromChunk(chunk, appUrl);
2884
- if (card) connectCards.push(card);
3035
+ const onEvent = (event) => {
3036
+ if (event.kind === "finished") {
3037
+ if (event.error) streamError = event.error;
3038
+ return;
3039
+ }
3040
+ const chunk = event.chunk;
3041
+ if (chunk["type"] === "text-delta") {
3042
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
3043
+ if (delta) {
3044
+ text += delta;
3045
+ if (onText) onText(delta);
2885
3046
  }
3047
+ } else if (chunk["type"] === "error") streamError = typeof chunk["errorText"] === "string" ? chunk["errorText"] : "unknown error";
3048
+ else {
3049
+ const card = connectCardFromChunk(chunk, appUrl);
3050
+ if (card) connectCards.push(card);
2886
3051
  }
2887
- });
3052
+ };
3053
+ try {
3054
+ if (target.kind === "message") await client.streamMessage({
3055
+ messageId: target.id,
3056
+ signal: controller.signal,
3057
+ onEvent
3058
+ });
3059
+ else await client.streamRun({
3060
+ runId: target.id,
3061
+ signal: controller.signal,
3062
+ onEvent
3063
+ });
3064
+ } catch (err) {
3065
+ throw toPrintError(err, messageIdForHint);
3066
+ }
2888
3067
  if (streamError) throw new Error(streamError);
3068
+ return {
3069
+ text,
3070
+ connectCards
3071
+ };
3072
+ }
3073
+ /**
3074
+ * Re-attach to an exchange by message id and print its reply. Backs `skydive
3075
+ * messages get <messageId>` — the recovery path when a `chat -p` stream dropped
3076
+ * at the edge after the message was accepted. The server resolves the run
3077
+ * behind the message and replays it from its persisted event log, so this
3078
+ * returns the full reply whether the run is still live or already done.
3079
+ */
3080
+ async function messageGet({ appUrl, sessionToken, messageId, json }) {
3081
+ const { text, connectCards } = await collectRunText({
3082
+ client: createRestClient({
3083
+ appUrl,
3084
+ sessionToken
3085
+ }),
3086
+ appUrl,
3087
+ target: {
3088
+ kind: "message",
3089
+ id: messageId
3090
+ },
3091
+ onText: json ? null : (delta) => process.stdout.write(delta),
3092
+ messageIdForHint: null
3093
+ });
2889
3094
  if (!json && text && !text.endsWith("\n")) process.stdout.write("\n");
2890
3095
  if (!json) for (const card of connectCards) process.stdout.write(`${formatConnectCard(card)}\n`);
2891
3096
  return {
2892
- agentId: agent.id,
2893
- agentName: agent.name,
2894
- conversationId: send.conversationId,
2895
- isNewConversation: send.isNewConversation,
2896
- runId: send.runId,
3097
+ messageId,
2897
3098
  text,
2898
3099
  connectCards
2899
3100
  };
@@ -3129,7 +3330,7 @@ const switchCommand = {
3129
3330
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
3130
3331
  process.exit(1);
3131
3332
  }
3132
- const { runWorkspacePicker } = await import("./boot-BvkqTAxQ.mjs");
3333
+ const { runWorkspacePicker } = await import("./boot-B-eA8M6t.mjs");
3133
3334
  await runWorkspacePicker(session);
3134
3335
  return;
3135
3336
  }
@@ -3183,7 +3384,7 @@ function createCli(argv) {
3183
3384
  type: "string",
3184
3385
  global: true,
3185
3386
  describe: "Override API base URL"
3186
- }).command(authCommand).command(chatCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
3387
+ }).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
3187
3388
  printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
3188
3389
  process.exit(1);
3189
3390
  });
@@ -508,7 +508,7 @@ var PortalClient = class {
508
508
  this.setStatus("error", errorMessage(err));
509
509
  }
510
510
  if (!this.enabled || this.disposed) break;
511
- await sleep$1(backoff);
511
+ await sleep(backoff);
512
512
  backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
513
513
  }
514
514
  }
@@ -554,7 +554,7 @@ var PortalClient = class {
554
554
  for (let attempt = 0; attempt < 10; attempt += 1) {
555
555
  await this.refreshDevice();
556
556
  if (this.deviceId) return this.deviceId;
557
- await sleep$1(300);
557
+ await sleep(300);
558
558
  }
559
559
  throw new Error("this machine is not connected yet");
560
560
  }
@@ -590,7 +590,7 @@ function toBuffer(data) {
590
590
  function shortBody(res) {
591
591
  return res.text().then((text) => text.slice(0, 120)).catch(() => "");
592
592
  }
593
- function sleep$1(ms) {
593
+ function sleep(ms) {
594
594
  return new Promise((resolve) => setTimeout(resolve, ms));
595
595
  }
596
596
 
@@ -4639,9 +4639,6 @@ function ThemePicker({ onClose }) {
4639
4639
 
4640
4640
  //#endregion
4641
4641
  //#region src/chat/tui/screens/chat.tsx
4642
- function sleep(ms) {
4643
- return new Promise((resolve) => setTimeout(resolve, ms));
4644
- }
4645
4642
  /**
4646
4643
  * Composer key bindings: Enter sends the message (matching the rest of the
4647
4644
  * app), Shift+Enter inserts a hard newline. Everything else falls back to the
@@ -4858,41 +4855,18 @@ function ChatScreen({ agent, conversation }) {
4858
4855
  ]);
4859
4856
  useEffect(() => {
4860
4857
  if (!rest || !conversationId) return;
4861
- if (initialConversationId !== null) return;
4862
- let cancelled = false;
4863
- (async () => {
4864
- const delays = [
4865
- 0,
4866
- 800,
4867
- 1500,
4868
- 2500,
4869
- 4e3,
4870
- 6e3
4871
- ];
4872
- let last = null;
4873
- for (const delay of delays) {
4874
- if (delay) await sleep(delay);
4875
- if (cancelled) return;
4876
- let title;
4877
- try {
4878
- ({title} = await rest.getConversation({ conversationId }));
4879
- } catch (_error) {
4880
- continue;
4881
- }
4882
- if (cancelled) return;
4883
- if (title && title !== last) {
4884
- last = title;
4885
- setChatTitle(title);
4886
- }
4858
+ const abort = new AbortController();
4859
+ rest.streamConversation({
4860
+ conversationId,
4861
+ signal: abort.signal,
4862
+ onEvent: (event) => {
4863
+ if (event.title) setChatTitle(event.title);
4887
4864
  }
4888
- })();
4889
- return () => {
4890
- cancelled = true;
4891
- };
4865
+ }).catch(() => {});
4866
+ return () => abort.abort();
4892
4867
  }, [
4893
4868
  rest,
4894
4869
  conversationId,
4895
- initialConversationId,
4896
4870
  setChatTitle
4897
4871
  ]);
4898
4872
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.122",
3
+ "version": "0.1.0-beta.180",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",