skydive-cli 0.2.0-beta.559 → 0.2.0-beta.577

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to the Skydive CLI are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Chat TUI sets the terminal title (OSC) to the live conversation title, so a terminal multiplexer (cmux, tmux, zellij) or window names its tab after the conversation. Follows renames from autoname, web, and Slack, and resets to a neutral default on exit.
13
+ - Every TUI screen (pickers included), not just chat, now drives the title, and inside cmux the workspace (sidebar entry) is renamed too via the cmux control socket. A workspace title you set by hand is left untouched. The tab and workspace title read title-first (title then agent name), while the in-app status bar stays agent-first.
14
+
8
15
  ## [0.2.0] - 2026-07-29
9
16
 
10
17
  ### Added
package/dist/js/bin.mjs CHANGED
@@ -20,7 +20,7 @@ import semver from "semver";
20
20
 
21
21
  //#region package.json
22
22
  var name = "skydive-cli";
23
- var version = "0.2.0-beta.559";
23
+ var version = "0.2.0-beta.577";
24
24
 
25
25
  //#endregion
26
26
  //#region src/types.ts
@@ -1200,7 +1200,7 @@ const importCommand = {
1200
1200
  process.exit(1);
1201
1201
  }
1202
1202
  }
1203
- const { runChat } = await import("./boot-CCz28zCI.mjs");
1203
+ const { runChat } = await import("./boot--whEK1H-.mjs");
1204
1204
  await runChat({
1205
1205
  appUrl,
1206
1206
  sessionToken: session.value.sessionToken,
@@ -1511,7 +1511,7 @@ const chatCommand = {
1511
1511
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1512
1512
  process.exit(1);
1513
1513
  }
1514
- const { runChat } = await import("./boot-CCz28zCI.mjs");
1514
+ const { runChat } = await import("./boot--whEK1H-.mjs");
1515
1515
  await runChat({
1516
1516
  appUrl,
1517
1517
  sessionToken: session.value.sessionToken,
@@ -1810,7 +1810,7 @@ const switchCommand = {
1810
1810
  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.");
1811
1811
  process.exit(1);
1812
1812
  }
1813
- const { runWorkspacePicker } = await import("./boot-CCz28zCI.mjs");
1813
+ const { runWorkspacePicker } = await import("./boot--whEK1H-.mjs");
1814
1814
  await runWorkspacePicker(session);
1815
1815
  return;
1816
1816
  }
@@ -2466,7 +2466,7 @@ function setupUpdateCheck(argv) {
2466
2466
 
2467
2467
  //#endregion
2468
2468
  //#region src/changelog.generated.ts
2469
- const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
2469
+ const CHANGELOG_MD = "# Changelog\n\nAll notable changes to the Skydive CLI are documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n- Chat TUI sets the terminal title (OSC) to the live conversation title, so a terminal multiplexer (cmux, tmux, zellij) or window names its tab after the conversation. Follows renames from autoname, web, and Slack, and resets to a neutral default on exit.\n- Every TUI screen (pickers included), not just chat, now drives the title, and inside cmux the workspace (sidebar entry) is renamed too via the cmux control socket. A workspace title you set by hand is left untouched. The tab and workspace title read title-first (title then agent name), while the in-app status bar stays agent-first.\n\n## [0.2.0] - 2026-07-29\n\n### Added\n\n**Sandbox & machine access**\n\n- Standalone `skydive sandbox` command for direct access to an agent's sandbox.\n- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.\n- Headless machine sharing via `skydive portal`, with self-registering portal grants.\n- `shareMachineDefault` config key for always-on portal sharing.\n\n**Chat TUI**\n\n- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.\n- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.\n- Fuzzy finder in the conversation and agent pickers.\n- Slash-command autocomplete menu.\n- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.\n- Run local shell commands with `!` in the composer.\n- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.\n- Working timer rolls up into minutes and hours.\n- Conversation picker paginates past 50 conversations and shows only your own conversations.\n- Esc leaves a live run; typing `exit` quits the chat.\n- Cursor Dark theme.\n- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.\n\n**Headless & scripting**\n\n- Resume a conversation by id.\n- `conversations list` and `conversations show` for transcript reads.\n- `messages get`, with run recovery keyed on message id.\n- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.\n\n**Authentication**\n\n- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.\n- Workspace picker on the device authorization page.\n- Account and workspace identity shown in `auth status`.\n\n**Platform**\n\n- Standalone binary builds compiling the CLI into a per-target executable.\n- Interactive workspace switcher; management commands follow the active workspace.\n- Terminal host integrations and agent notifications.\n\n### Fixed\n\n- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).\n- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.\n- Bare URLs in chat markdown are hyperlinked so they survive text wrap.\n- Composer draft is preserved across TUI overlays.\n- Relative connect links resolve before opening the browser.\n- Numbered markdown headings render colored in the TUI.\n- Chat transcript pages by 75% of a screen; picker rows stay on one line.\n- Run starts push to the TUI over the conversation stream.\n- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.\n\n## [0.1.0] - 2026-07-21\n\nInitial public release: `skydive chat` TUI, agent and conversation management,\ndevice authorization, and headless `-p` mode.\n";
2470
2470
 
2471
2471
  //#endregion
2472
2472
  //#region src/whats-new.ts
@@ -19,7 +19,7 @@ import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, d
19
19
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
20
20
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
21
21
  import { create } from "zustand";
22
- import { createConnection } from "node:net";
22
+ import { connect, createConnection } from "node:net";
23
23
  import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
24
24
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
25
25
  import fuzzysort from "fuzzysort";
@@ -272,6 +272,7 @@ function usePortal({ appUrl, sessionToken, shareMachine }) {
272
272
  const noopAgentHost = {
273
273
  transition() {},
274
274
  setSession() {},
275
+ setTitle() {},
275
276
  dispose() {}
276
277
  };
277
278
  const AgentHostContext = createContext(noopAgentHost);
@@ -293,6 +294,10 @@ function composeAgentHost(adapters) {
293
294
  if (disposed) return;
294
295
  for (const adapter of adapters) adapter.setSession?.(sessionId);
295
296
  },
297
+ setTitle(title) {
298
+ if (disposed) return;
299
+ for (const adapter of adapters) adapter.setTitle?.(title);
300
+ },
296
301
  dispose() {
297
302
  if (disposed) return;
298
303
  disposed = true;
@@ -354,6 +359,167 @@ function createTerminalNotificationsAdapter({ renderer, enabled }) {
354
359
  };
355
360
  }
356
361
 
362
+ //#endregion
363
+ //#region src/chat/agent-host/adapters/terminal-title.ts
364
+ /**
365
+ * The title shown when there is no conversation title yet, or after the chat
366
+ * closes. Keeps the tab identifiable as this CLI rather than leaving a stale
367
+ * conversation name behind.
368
+ */
369
+ const DEFAULT_TERMINAL_TITLE = "skydive";
370
+ /**
371
+ * Collapses a conversation title to a single terminal-title line. Titles arrive
372
+ * from autoname / web / Slack and can carry newlines, tabs, or other control
373
+ * characters (the same reason picker rows sanitize them); a raw newline in an
374
+ * OSC title terminates the sequence early and corrupts the tab, so flatten
375
+ * whitespace and drop C0 controls. Empty or whitespace-only input falls back to
376
+ * the default so the tab is never blanked.
377
+ */
378
+ function terminalTitleFor(title) {
379
+ if (!title) return DEFAULT_TERMINAL_TITLE;
380
+ return title.replace(/[\u0000-\u001f\u007f]+/g, " ").trim() || DEFAULT_TERMINAL_TITLE;
381
+ }
382
+ /**
383
+ * Mirrors the live conversation title onto the terminal title (OSC), so a
384
+ * multiplexer tab or window carries the conversation name instead of a bare
385
+ * shell/binary label. OpenTUI owns the escape transport via
386
+ * `setTerminalTitle`, so this never interleaves with the rendered frame.
387
+ *
388
+ * Writes are deduped, and the title is reset to a neutral default on dispose so
389
+ * leaving the chat doesn't strand the last conversation's name on the tab.
390
+ */
391
+ function createTerminalTitleAdapter({ renderer }) {
392
+ let last;
393
+ const apply = (next) => {
394
+ if (next === last) return;
395
+ last = next;
396
+ try {
397
+ renderer.setTerminalTitle(next);
398
+ } catch (_error) {}
399
+ };
400
+ return {
401
+ setTitle(title) {
402
+ apply(terminalTitleFor(title));
403
+ },
404
+ dispose() {
405
+ apply(DEFAULT_TERMINAL_TITLE);
406
+ }
407
+ };
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/chat/agent-host/adapters/cmux/workspace-title.ts
412
+ /**
413
+ * cmux is present only inside a cmux terminal. Reads the socket path and
414
+ * workspace id from the environment; returns null when either is missing, so
415
+ * the caller registers no adapter outside cmux.
416
+ */
417
+ function cmuxWorkspaceEnv(env = process.env) {
418
+ const socketPath = env.CMUX_SOCKET_PATH?.trim();
419
+ const workspaceId = env.CMUX_WORKSPACE_ID?.trim();
420
+ if (!socketPath || !workspaceId) return null;
421
+ if (!socketPath.startsWith("/")) return null;
422
+ return {
423
+ socketPath,
424
+ workspaceId
425
+ };
426
+ }
427
+ /** The fields of a `workspace.current` result we care about. */
428
+ const workspaceCurrentSchema = z.object({
429
+ workspace_id: z.string().optional(),
430
+ has_custom_title: z.boolean().optional()
431
+ });
432
+ /** True when the current workspace carries a user-set custom title. */
433
+ async function hasUserCustomTitle(socket, workspaceId) {
434
+ const res = await socket.request("workspace.current", {});
435
+ const parsed = res?.ok ? workspaceCurrentSchema.safeParse(res.result) : void 0;
436
+ if (!parsed?.success) return false;
437
+ const result = parsed.data;
438
+ if (result.workspace_id && result.workspace_id !== workspaceId) return false;
439
+ return result.has_custom_title === true;
440
+ }
441
+ function createCmuxWorkspaceTitleAdapter({ socket, workspaceId }) {
442
+ let lastApplied;
443
+ let chain = Promise.resolve();
444
+ let pending = null;
445
+ const flush = async () => {
446
+ const desired = pending;
447
+ pending = null;
448
+ if (desired === null || desired === lastApplied) return;
449
+ if (await hasUserCustomTitle(socket, workspaceId)) {
450
+ lastApplied = desired;
451
+ return;
452
+ }
453
+ if ((await socket.request("workspace.rename", {
454
+ workspace_id: workspaceId,
455
+ title: desired
456
+ }))?.ok) lastApplied = desired;
457
+ };
458
+ return { setTitle(title) {
459
+ const next = title?.trim();
460
+ if (!next) return;
461
+ pending = next;
462
+ chain = chain.then(flush, flush);
463
+ } };
464
+ }
465
+
466
+ //#endregion
467
+ //#region src/chat/agent-host/adapters/cmux/socket.ts
468
+ const cmuxRpcResponseSchema = z.object({
469
+ ok: z.boolean(),
470
+ result: z.unknown().optional(),
471
+ error: z.unknown().optional()
472
+ });
473
+ /** How long to wait for a single round-trip before giving up. */
474
+ const RPC_TIMEOUT_MS = 1500;
475
+ function createCmuxSocket({ socketPath, timeoutMs = RPC_TIMEOUT_MS }) {
476
+ let seq = 0;
477
+ return { request(method, params) {
478
+ return new Promise((resolve) => {
479
+ let settled = false;
480
+ const finish = (value) => {
481
+ if (settled) return;
482
+ settled = true;
483
+ clearTimeout(timer);
484
+ try {
485
+ socket.destroy();
486
+ } catch (_error) {}
487
+ resolve(value);
488
+ };
489
+ const timer = setTimeout(() => finish(null), timeoutMs);
490
+ const id = `skydive-${++seq}`;
491
+ let buffer = "";
492
+ const socket = connect(socketPath);
493
+ socket.setEncoding("utf8");
494
+ socket.on("connect", () => {
495
+ try {
496
+ socket.write(`${JSON.stringify({
497
+ id,
498
+ method,
499
+ params
500
+ })}\n`);
501
+ } catch (_error) {
502
+ finish(null);
503
+ }
504
+ });
505
+ socket.on("data", (chunk) => {
506
+ buffer += chunk;
507
+ const newline = buffer.indexOf("\n");
508
+ if (newline === -1) return;
509
+ const line = buffer.slice(0, newline);
510
+ try {
511
+ const parsed = cmuxRpcResponseSchema.safeParse(JSON.parse(line));
512
+ finish(parsed.success ? parsed.data : null);
513
+ } catch (_error) {
514
+ finish(null);
515
+ }
516
+ });
517
+ socket.on("error", () => finish(null));
518
+ socket.on("close", () => finish(null));
519
+ });
520
+ } };
521
+ }
522
+
357
523
  //#endregion
358
524
  //#region src/chat/agent-host/adapters/herdr/adapter.ts
359
525
  /** Maps one complete semantic transition to one herdr state report. */
@@ -523,7 +689,12 @@ function createAgentHost({ renderer, notifications }) {
523
689
  const adapters = [createTerminalNotificationsAdapter({
524
690
  renderer,
525
691
  enabled: notifications
526
- })];
692
+ }), createTerminalTitleAdapter({ renderer })];
693
+ const cmux = cmuxWorkspaceEnv();
694
+ if (cmux) adapters.push(createCmuxWorkspaceTitleAdapter({
695
+ socket: createCmuxSocket({ socketPath: cmux.socketPath }),
696
+ workspaceId: cmux.workspaceId
697
+ }));
527
698
  const herdr = createHerdrReporter();
528
699
  if (herdr) adapters.push(createHerdrAdapter(herdr));
529
700
  return composeAgentHost(adapters);
@@ -840,22 +1011,52 @@ function portalIndicator(portal) {
840
1011
  };
841
1012
  }
842
1013
  }
843
- function screenLabel(screen, chatTitle) {
1014
+ /**
1015
+ * The per-screen label, split into its parts: the screen's own `title` and,
1016
+ * when the screen belongs to an agent, that `agentName`. Kept structured so the
1017
+ * two surfaces can order the parts differently — the status bar reads
1018
+ * agent-first (a header), the terminal/workspace title reads title-first.
1019
+ */
1020
+ function screenLabelParts(screen, chatTitle) {
844
1021
  switch (screen.kind) {
845
- case "splash": return "";
846
- case "error": return "";
847
- case "agent-picker": return "pick an agent";
848
- case "agent-create": return "create an agent";
849
- case "workspace-picker": return "switch workspace";
850
- case "conversation-picker": return `${screen.agent.name} · pick a conversation`;
1022
+ case "splash": return { title: "" };
1023
+ case "error": return { title: "" };
1024
+ case "agent-picker": return { title: "pick an agent" };
1025
+ case "agent-create": return { title: "create an agent" };
1026
+ case "workspace-picker": return { title: "switch workspace" };
1027
+ case "conversation-picker": return {
1028
+ title: "pick a conversation",
1029
+ agentName: screen.agent.name
1030
+ };
851
1031
  case "chat": {
852
1032
  const c = screen.conversation;
853
1033
  const known = isNewConv(c) ? null : c.title ?? null;
854
- const label = chatTitle ?? known ?? (isNewConv(c) ? "new conversation" : "untitled");
855
- return `${screen.agent.name} · ${label}`;
1034
+ return {
1035
+ title: chatTitle ?? known ?? (isNewConv(c) ? "new conversation" : "untitled"),
1036
+ agentName: screen.agent.name
1037
+ };
856
1038
  }
857
1039
  }
858
1040
  }
1041
+ /**
1042
+ * Status-bar label: agent-first (`agent · title`), which reads as a header of
1043
+ * who you're talking to and where.
1044
+ */
1045
+ function screenLabel(screen, chatTitle) {
1046
+ const { title, agentName } = screenLabelParts(screen, chatTitle);
1047
+ if (!title) return "";
1048
+ return agentName ? `${agentName} · ${title}` : title;
1049
+ }
1050
+ /**
1051
+ * Terminal tab / cmux workspace title: title-first (`title · agent`), so the
1052
+ * conversation reads first in a tab or sidebar entry, with the agent as the
1053
+ * trailing qualifier.
1054
+ */
1055
+ function titleLabel(screen, chatTitle) {
1056
+ const { title, agentName } = screenLabelParts(screen, chatTitle);
1057
+ if (!title) return "";
1058
+ return agentName ? `${title} · ${agentName}` : title;
1059
+ }
859
1060
  function isNewConv(c) {
860
1061
  return "kind" in c && c.kind === "new";
861
1062
  }
@@ -10036,6 +10237,7 @@ function ToastView() {
10036
10237
  function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId }) {
10037
10238
  const renderer = useRenderer();
10038
10239
  const screen = useStore((s) => s.screen);
10240
+ const chatTitle = useStore((s) => s.chatTitle);
10039
10241
  useStore((s) => s.themeId);
10040
10242
  const rest = useStore((s) => s.rest);
10041
10243
  const goTo = useStore((s) => s.goTo);
@@ -10054,6 +10256,13 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
10054
10256
  agentHost.transition({ state: "idle" });
10055
10257
  return () => agentHost.dispose();
10056
10258
  }, [agentHost]);
10259
+ useEffect(() => {
10260
+ agentHost.setTitle(titleLabel(screen, chatTitle));
10261
+ }, [
10262
+ agentHost,
10263
+ screen,
10264
+ chatTitle
10265
+ ]);
10057
10266
  useEffect(() => {
10058
10267
  const selectionCopy = createSelectionCopy({
10059
10268
  renderer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.559",
3
+ "version": "0.2.0-beta.577",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",