skydive-cli 0.2.0-beta.576 → 0.2.0-beta.583

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
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
  ### Added
11
11
 
12
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.
13
14
 
14
15
  ## [0.2.0] - 2026-07-29
15
16
 
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.576";
23
+ var version = "0.2.0-beta.583";
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-DodmZA55.mjs");
1203
+ const { runChat } = await import("./boot-Be4wXLr5.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-DodmZA55.mjs");
1514
+ const { runChat } = await import("./boot-Be4wXLr5.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-DodmZA55.mjs");
1813
+ const { runWorkspacePicker } = await import("./boot-Be4wXLr5.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## [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\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";
@@ -407,6 +407,119 @@ function createTerminalTitleAdapter({ renderer }) {
407
407
  };
408
408
  }
409
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
+
410
523
  //#endregion
411
524
  //#region src/chat/agent-host/adapters/herdr/adapter.ts
412
525
  /** Maps one complete semantic transition to one herdr state report. */
@@ -577,6 +690,11 @@ function createAgentHost({ renderer, notifications }) {
577
690
  renderer,
578
691
  enabled: notifications
579
692
  }), createTerminalTitleAdapter({ renderer })];
693
+ const cmux = cmuxWorkspaceEnv();
694
+ if (cmux) adapters.push(createCmuxWorkspaceTitleAdapter({
695
+ socket: createCmuxSocket({ socketPath: cmux.socketPath }),
696
+ workspaceId: cmux.workspaceId
697
+ }));
580
698
  const herdr = createHerdrReporter();
581
699
  if (herdr) adapters.push(createHerdrAdapter(herdr));
582
700
  return composeAgentHost(adapters);
@@ -893,22 +1011,52 @@ function portalIndicator(portal) {
893
1011
  };
894
1012
  }
895
1013
  }
896
- 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) {
897
1021
  switch (screen.kind) {
898
- case "splash": return "";
899
- case "error": return "";
900
- case "agent-picker": return "pick an agent";
901
- case "agent-create": return "create an agent";
902
- case "workspace-picker": return "switch workspace";
903
- 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
+ };
904
1031
  case "chat": {
905
1032
  const c = screen.conversation;
906
1033
  const known = isNewConv(c) ? null : c.title ?? null;
907
- const label = chatTitle ?? known ?? (isNewConv(c) ? "new conversation" : "untitled");
908
- return `${screen.agent.name} · ${label}`;
1034
+ return {
1035
+ title: chatTitle ?? known ?? (isNewConv(c) ? "new conversation" : "untitled"),
1036
+ agentName: screen.agent.name
1037
+ };
909
1038
  }
910
1039
  }
911
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
+ }
912
1060
  function isNewConv(c) {
913
1061
  return "kind" in c && c.kind === "new";
914
1062
  }
@@ -4529,6 +4677,14 @@ const keybindGroups = [
4529
4677
  keys: "pgup / pgdn",
4530
4678
  action: "scroll transcript"
4531
4679
  },
4680
+ {
4681
+ keys: "end",
4682
+ action: "jump to latest (when scrolled up)"
4683
+ },
4684
+ {
4685
+ keys: "/plan [show|hide]",
4686
+ action: "show / hide the pinned plan card"
4687
+ },
4532
4688
  {
4533
4689
  keys: "/",
4534
4690
  action: "open the command menu (start of message)"
@@ -4995,6 +5151,16 @@ const slashCommands = [
4995
5151
  description: "show connection and session status",
4996
5152
  action: { kind: "status" }
4997
5153
  },
5154
+ {
5155
+ name: "plan",
5156
+ aliases: ["todos", "todo"],
5157
+ argsHint: "[show|hide]",
5158
+ description: "show or hide the pinned plan card",
5159
+ action: {
5160
+ kind: "plan",
5161
+ mode: "toggle"
5162
+ }
5163
+ },
4998
5164
  {
4999
5165
  name: "share",
5000
5166
  aliases: [],
@@ -5016,6 +5182,13 @@ const slashCommands = [
5016
5182
  description: "act on a waiting connect card (auth cards)",
5017
5183
  action: { kind: "connect" }
5018
5184
  },
5185
+ {
5186
+ name: "copy",
5187
+ aliases: ["cp"],
5188
+ argsHint: null,
5189
+ description: "copy the agent's last message to the clipboard",
5190
+ action: { kind: "copy" }
5191
+ },
5019
5192
  {
5020
5193
  name: "help",
5021
5194
  aliases: [],
@@ -8060,6 +8233,13 @@ function routeInput(raw) {
8060
8233
  kind: "open-conversation",
8061
8234
  ref: match.rest
8062
8235
  } : { kind: "conversation-picker" };
8236
+ case "plan": {
8237
+ const arg = match.rest.toLowerCase();
8238
+ return {
8239
+ kind: "plan",
8240
+ mode: arg === "show" ? "show" : arg === "hide" ? "hide" : "toggle"
8241
+ };
8242
+ }
8063
8243
  case "new-conversation":
8064
8244
  case "model-picker":
8065
8245
  case "theme-picker":
@@ -8067,6 +8247,7 @@ function routeInput(raw) {
8067
8247
  case "share":
8068
8248
  case "browser":
8069
8249
  case "connect":
8250
+ case "copy":
8070
8251
  case "help":
8071
8252
  case "quit": return { kind: action.kind };
8072
8253
  default: {
@@ -8261,6 +8442,14 @@ function ChatScreen({ agent, conversation }) {
8261
8442
  const [conversationId, setConversationId] = useState(initialConversationId);
8262
8443
  const [items, setItems] = useState([]);
8263
8444
  const [todos, setTodos] = useState(null);
8445
+ const [planHidden, setPlanHidden] = useState(false);
8446
+ const [scrolledUp, setScrolledUp] = useState(false);
8447
+ const hadTodosRef = useRef(false);
8448
+ useEffect(() => {
8449
+ const hasTodos = !!todos && todos.length > 0;
8450
+ if (hasTodos && !hadTodosRef.current) setPlanHidden(false);
8451
+ hadTodosRef.current = hasTodos;
8452
+ }, [todos]);
8264
8453
  const [historyLoaded, setHistoryLoaded] = useState(initialConversationId === null);
8265
8454
  const [input, setInput] = useState("");
8266
8455
  const [run, setRun] = useState({ kind: "idle" });
@@ -8327,6 +8516,25 @@ function ChatScreen({ agent, conversation }) {
8327
8516
  }, [agentHost, conversationId]);
8328
8517
  const scrollRef = useRef(null);
8329
8518
  const composerRef = useRef(null);
8519
+ const jumpToBottom = useCallback(() => {
8520
+ const box = scrollRef.current;
8521
+ if (!box) return;
8522
+ box.scrollTo({
8523
+ x: box.scrollLeft,
8524
+ y: box.scrollHeight
8525
+ });
8526
+ setScrolledUp(false);
8527
+ }, []);
8528
+ useEffect(() => {
8529
+ const id = setInterval(() => {
8530
+ const box = scrollRef.current;
8531
+ if (!box) return;
8532
+ const viewport = box.viewport.height;
8533
+ const atBottom = box.scrollTop + viewport >= box.scrollHeight - 1;
8534
+ setScrolledUp((prev) => prev === !atBottom ? prev : !atBottom);
8535
+ }, 150);
8536
+ return () => clearInterval(id);
8537
+ }, []);
8330
8538
  const reviewSideBoxRef = useRef(null);
8331
8539
  const reviewBelowBoxRef = useRef(null);
8332
8540
  const paneDragRef = useRef(null);
@@ -8350,9 +8558,11 @@ function ChatScreen({ agent, conversation }) {
8350
8558
  const chatHeight = bodyHeight;
8351
8559
  const inlineReviewHeight = reviewOpen && reviewPlacement === "below" ? reviewHeight : 0;
8352
8560
  const menuRows = menuOpen ? completionMenuHeight(menuCommands.length) : 0;
8353
- const todoCardVisible = !credPrompt && !!todos && todos.length > 0;
8561
+ const todoCardVisible = !credPrompt && !planHidden && !!todos && todos.length > 0;
8354
8562
  const todoRows = todoCardVisible ? todoCardHeight(todos) : 0;
8355
- const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - todoRows);
8563
+ const jumpHintVisible = scrolledUp && !credPrompt;
8564
+ const jumpHintRows = jumpHintVisible ? 1 : 0;
8565
+ const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - jumpHintRows - todoRows);
8356
8566
  useEffect(() => {
8357
8567
  if (reviewOpen && reviewFocused) {
8358
8568
  const composer = composerRef.current;
@@ -8494,10 +8704,7 @@ function ChatScreen({ agent, conversation }) {
8494
8704
  attachToRun(event.runId, null, parseStampMs(event.createdAt));
8495
8705
  return;
8496
8706
  }
8497
- if (event.title) {
8498
- setChatTitle(event.title);
8499
- agentHost.setTitle(event.title);
8500
- }
8707
+ if (event.title) setChatTitle(event.title);
8501
8708
  }
8502
8709
  }).catch(() => {});
8503
8710
  return () => abort.abort();
@@ -8505,8 +8712,7 @@ function ChatScreen({ agent, conversation }) {
8505
8712
  rest,
8506
8713
  conversationId,
8507
8714
  setChatTitle,
8508
- attachToRun,
8509
- agentHost
8715
+ attachToRun
8510
8716
  ]);
8511
8717
  const sendContent = useCallback(async (content, staged, opts) => {
8512
8718
  if (!rest) return;
@@ -9064,6 +9270,17 @@ function ChatScreen({ agent, conversation }) {
9064
9270
  } : m));
9065
9271
  useStore.getState().showToast({ message: "run cancelled" });
9066
9272
  }, [rest]);
9273
+ const copyLastMessage = useCallback(() => {
9274
+ const last = [...items].reverse().find((m) => m.kind === "assistant-text" && m.done && m.text.trim() !== "");
9275
+ if (!last) {
9276
+ useStore.getState().showToast({ message: "no agent message to copy yet" });
9277
+ return;
9278
+ }
9279
+ writeClipboardText(last.text).then((ok) => useStore.getState().showToast(ok ? {
9280
+ variant: "success",
9281
+ message: "copied last message to clipboard"
9282
+ } : { message: "clipboard unavailable" }));
9283
+ }, [items]);
9067
9284
  const openInBrowser = useCallback(() => {
9068
9285
  if (!appUrl) return;
9069
9286
  if (!conversationId) {
@@ -9275,12 +9492,25 @@ function ChatScreen({ agent, conversation }) {
9275
9492
  })
9276
9493
  }]);
9277
9494
  break;
9495
+ case "plan": {
9496
+ if (!(!!todos && todos.length > 0)) {
9497
+ useStore.getState().showToast({ message: "no plan to show yet" });
9498
+ break;
9499
+ }
9500
+ const nextHidden = routed.mode === "toggle" ? !planHidden : routed.mode === "hide";
9501
+ setPlanHidden(nextHidden);
9502
+ useStore.getState().showToast({ message: nextHidden ? "plan hidden (/plan show)" : "plan shown" });
9503
+ break;
9504
+ }
9278
9505
  case "share":
9279
9506
  toggleShare();
9280
9507
  break;
9281
9508
  case "browser":
9282
9509
  openInBrowser();
9283
9510
  break;
9511
+ case "copy":
9512
+ copyLastMessage();
9513
+ break;
9284
9514
  case "connect":
9285
9515
  if (!promptForCards()) useStore.getState().showToast({ message: "no connect card waiting for action" });
9286
9516
  break;
@@ -9313,6 +9543,7 @@ function ChatScreen({ agent, conversation }) {
9313
9543
  openConversationRef,
9314
9544
  toggleShare,
9315
9545
  openInBrowser,
9546
+ copyLastMessage,
9316
9547
  quit,
9317
9548
  sendContent,
9318
9549
  goTo,
@@ -9323,7 +9554,9 @@ function ChatScreen({ agent, conversation }) {
9323
9554
  model,
9324
9555
  portal.status,
9325
9556
  portal.grantedAgentIds,
9326
- promptForCards
9557
+ promptForCards,
9558
+ todos,
9559
+ planHidden
9327
9560
  ]);
9328
9561
  const submit = useCallback(() => {
9329
9562
  const staged = attachments.filter((a) => a.status !== "error");
@@ -9515,6 +9748,10 @@ function ChatScreen({ agent, conversation }) {
9515
9748
  scrollRef.current?.scrollBy(pageScrollFraction, "viewport");
9516
9749
  return;
9517
9750
  }
9751
+ if (key.name === "end" && scrolledUp) {
9752
+ jumpToBottom();
9753
+ return;
9754
+ }
9518
9755
  if (key.name === "c" && key.ctrl) {
9519
9756
  if (runRef.current.kind === "streaming") {
9520
9757
  cancelRun();
@@ -9711,6 +9948,23 @@ function ChatScreen({ agent, conversation }) {
9711
9948
  }) : null]
9712
9949
  })
9713
9950
  }),
9951
+ jumpHintVisible ? /* @__PURE__ */ jsxs("box", {
9952
+ onMouseDown: jumpToBottom,
9953
+ style: {
9954
+ flexDirection: "row",
9955
+ paddingLeft: 1,
9956
+ paddingRight: 1,
9957
+ height: 1,
9958
+ flexShrink: 0
9959
+ },
9960
+ children: [/* @__PURE__ */ jsx("text", {
9961
+ fg: theme.accent,
9962
+ children: "↓ jump to latest"
9963
+ }), /* @__PURE__ */ jsxs("text", {
9964
+ fg: theme.dim,
9965
+ children: [" ", "end"]
9966
+ })]
9967
+ }) : null,
9714
9968
  todoCardVisible && todos ? /* @__PURE__ */ jsx(TodoCardView, {
9715
9969
  todos,
9716
9970
  isActive: run.kind !== "idle"
@@ -10093,6 +10347,7 @@ function ToastView() {
10093
10347
  function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId }) {
10094
10348
  const renderer = useRenderer();
10095
10349
  const screen = useStore((s) => s.screen);
10350
+ const chatTitle = useStore((s) => s.chatTitle);
10096
10351
  useStore((s) => s.themeId);
10097
10352
  const rest = useStore((s) => s.rest);
10098
10353
  const goTo = useStore((s) => s.goTo);
@@ -10111,6 +10366,13 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
10111
10366
  agentHost.transition({ state: "idle" });
10112
10367
  return () => agentHost.dispose();
10113
10368
  }, [agentHost]);
10369
+ useEffect(() => {
10370
+ agentHost.setTitle(titleLabel(screen, chatTitle));
10371
+ }, [
10372
+ agentHost,
10373
+ screen,
10374
+ chatTitle
10375
+ ]);
10114
10376
  useEffect(() => {
10115
10377
  const selectionCopy = createSelectionCopy({
10116
10378
  renderer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.576",
3
+ "version": "0.2.0-beta.583",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",