skydive-cli 0.1.0-beta.45 → 0.1.0-beta.49

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/js/bin.mjs CHANGED
@@ -13,7 +13,7 @@ import fs from "node:fs";
13
13
  import zlib from "node:zlib";
14
14
 
15
15
  //#region package.json
16
- var version$1 = "0.1.0-beta.45";
16
+ var version$1 = "0.1.0-beta.49";
17
17
 
18
18
  //#endregion
19
19
  //#region src/types.ts
@@ -2046,7 +2046,7 @@ const chatCommand = {
2046
2046
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2047
2047
  process.exit(1);
2048
2048
  }
2049
- const { runChat } = await import("./boot-BQ8xC2R8.mjs");
2049
+ const { runChat } = await import("./boot-h5qiGgfB.mjs");
2050
2050
  await runChat({
2051
2051
  appUrl,
2052
2052
  sessionToken: session.value.sessionToken,
@@ -7,10 +7,11 @@ import open from "open";
7
7
  import { spawn } from "node:child_process";
8
8
  import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
9
9
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
10
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
11
11
  import { create } from "zustand";
12
12
  import { WebSocket } from "ws";
13
13
  import os, { homedir, platform, release, tmpdir } from "node:os";
14
+ import { createConnection } from "node:net";
14
15
  import { appendFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
15
16
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
16
17
  import { fileURLToPath } from "node:url";
@@ -65,7 +66,6 @@ const useStore = create((set, get) => ({
65
66
  chatTitle: null,
66
67
  portal: initialPortalUi,
67
68
  portalClient: null,
68
- attention: null,
69
69
  toast: null,
70
70
  goTo: (screen) => set({
71
71
  screen,
@@ -122,7 +122,6 @@ const useStore = create((set, get) => ({
122
122
  ...state.portal,
123
123
  prompt
124
124
  } })),
125
- setAttention: (attention) => set({ attention }),
126
125
  showToast: (input) => set({ toast: createToast(input) }),
127
126
  dismissToast: (id) => set((state) => state.toast?.id === id ? { toast: null } : {})
128
127
  }));
@@ -649,19 +648,49 @@ function usePortal({ appUrl, sessionToken, shareMachine }) {
649
648
  }
650
649
 
651
650
  //#endregion
652
- //#region src/chat/tui/attention.ts
653
- /**
654
- * Whether a notification should fire. We only ping when the terminal is
655
- * blurred — the whole point is to catch the user who has tabbed away. While
656
- * focus is still `unknown` (no focus/blur reported yet, or a terminal that
657
- * doesn't report focus at all) we stay silent rather than interrupt someone
658
- * who is very likely watching.
659
- */
651
+ //#region src/chat/agent-host/context.tsx
652
+ const noopAgentHost = {
653
+ transition() {},
654
+ setSession() {},
655
+ dispose() {}
656
+ };
657
+ const AgentHostContext = createContext(noopAgentHost);
658
+ function useAgentHost() {
659
+ return useContext(AgentHostContext);
660
+ }
661
+
662
+ //#endregion
663
+ //#region src/chat/agent-host/compose.ts
664
+ /** Fans semantic events out to every detected adapter. */
665
+ function composeAgentHost(adapters) {
666
+ let disposed = false;
667
+ return {
668
+ transition(transition) {
669
+ if (disposed) return;
670
+ for (const adapter of adapters) adapter.transition?.(transition);
671
+ },
672
+ setSession(sessionId) {
673
+ if (disposed) return;
674
+ for (const adapter of adapters) adapter.setSession?.(sessionId);
675
+ },
676
+ dispose() {
677
+ if (disposed) return;
678
+ disposed = true;
679
+ for (const adapter of adapters) adapter.dispose?.();
680
+ }
681
+ };
682
+ }
683
+
684
+ //#endregion
685
+ //#region src/chat/agent-host/adapters/terminal-notifications.ts
660
686
  function shouldNotify({ enabled, focus }) {
661
687
  return enabled && focus === "blurred";
662
688
  }
663
- /** The title and body for each attention event. */
664
- function attentionMessage(event, agentName) {
689
+ function notificationMessage({ event, agentName, message }) {
690
+ if (message) return {
691
+ title: agentName,
692
+ message
693
+ };
665
694
  switch (event) {
666
695
  case "run-finished": return {
667
696
  title: agentName,
@@ -674,12 +703,10 @@ function attentionMessage(event, agentName) {
674
703
  }
675
704
  }
676
705
  /**
677
- * Tracks terminal focus and fires a desktop notification when the agent wants
678
- * the user's attention while the terminal is in the background. The renderer's
679
- * focus/blur events are the source of truth for whether the terminal is in the
680
- * foreground.
706
+ * Sends focus-gated desktop notifications through OpenTUI. OpenTUI selects
707
+ * OSC 9/99/777 for the detected terminal; this adapter owns that transport.
681
708
  */
682
- function createAttention({ renderer, enabled }) {
709
+ function createTerminalNotificationsAdapter({ renderer, enabled }) {
683
710
  let focus = "unknown";
684
711
  const onFocus = () => {
685
712
  focus = "focused";
@@ -690,12 +717,12 @@ function createAttention({ renderer, enabled }) {
690
717
  renderer.on("focus", onFocus);
691
718
  renderer.on("blur", onBlur);
692
719
  return {
693
- notify(event, agentName) {
694
- if (!shouldNotify({
720
+ transition({ notification }) {
721
+ if (!notification || !shouldNotify({
695
722
  enabled,
696
723
  focus
697
724
  })) return;
698
- const { title, message } = attentionMessage(event, agentName);
725
+ const { title, message } = notificationMessage(notification);
699
726
  try {
700
727
  renderer.triggerNotification(message, title);
701
728
  } catch (_error) {}
@@ -707,6 +734,181 @@ function createAttention({ renderer, enabled }) {
707
734
  };
708
735
  }
709
736
 
737
+ //#endregion
738
+ //#region src/chat/agent-host/adapters/herdr/adapter.ts
739
+ /** Maps one complete semantic transition to one herdr state report. */
740
+ function createHerdrAdapter(herdr) {
741
+ let lastState;
742
+ return {
743
+ transition({ state, notification }) {
744
+ if (state === lastState && !notification) return;
745
+ lastState = state;
746
+ const message = state === "idle" && notification?.event === "run-finished" ? notification.message : void 0;
747
+ herdr.report(state, message);
748
+ },
749
+ setSession: herdr.setConversation,
750
+ dispose: herdr.dispose
751
+ };
752
+ }
753
+
754
+ //#endregion
755
+ //#region src/chat/agent-host/adapters/herdr/client.ts
756
+ /** How we identify ourselves to herdr. The agent label is what herdr shows
757
+ * in its sidebar/agents pane; unknown labels pass through verbatim (herdr
758
+ * normalizes only its built-in agents), so no herdr-side support is needed. */
759
+ const SOURCE = "skydive-cli";
760
+ const AGENT_LABEL = "skydive";
761
+ /** Cap on how long a report may hold a socket open. herdr answers in
762
+ * microseconds when healthy; past this we drop the report on the floor. */
763
+ const REQUEST_TIMEOUT_MS = 500;
764
+ /**
765
+ * The herdr environment, if this process is running inside a herdr pane.
766
+ * All three variables are injected by herdr into every pane it spawns;
767
+ * anything missing means "not in herdr" and reporting stays off.
768
+ */
769
+ function detectHerdrEnv(env = process.env) {
770
+ if (env.HERDR_ENV !== "1") return null;
771
+ const socketPath = env.HERDR_SOCKET_PATH;
772
+ const paneId = env.HERDR_PANE_ID;
773
+ if (!socketPath || !paneId) return null;
774
+ return {
775
+ socketPath,
776
+ paneId
777
+ };
778
+ }
779
+ function buildStateReport({ paneId, state, message, seq, conversationId }) {
780
+ return {
781
+ id: requestId(),
782
+ method: "pane.report_agent",
783
+ params: {
784
+ pane_id: paneId,
785
+ source: SOURCE,
786
+ agent: AGENT_LABEL,
787
+ state,
788
+ ...message ? { message } : {},
789
+ seq,
790
+ ...conversationId ? { agent_session_id: conversationId } : {}
791
+ }
792
+ };
793
+ }
794
+ function buildSessionReport({ paneId, seq, conversationId }) {
795
+ return {
796
+ id: requestId(),
797
+ method: "pane.report_agent_session",
798
+ params: {
799
+ pane_id: paneId,
800
+ source: SOURCE,
801
+ agent: AGENT_LABEL,
802
+ seq,
803
+ agent_session_id: conversationId
804
+ }
805
+ };
806
+ }
807
+ function buildRelease({ paneId, seq }) {
808
+ return {
809
+ id: requestId(),
810
+ method: "pane.release_agent",
811
+ params: {
812
+ pane_id: paneId,
813
+ source: SOURCE,
814
+ agent: AGENT_LABEL,
815
+ seq
816
+ }
817
+ };
818
+ }
819
+ let requestCounter = 0;
820
+ function requestId() {
821
+ requestCounter += 1;
822
+ return `${SOURCE}:${Date.now()}:${requestCounter}`;
823
+ }
824
+ const socketSend = (socketPath, request) => {
825
+ try {
826
+ const socket = createConnection(socketPath);
827
+ socket.unref();
828
+ let done = false;
829
+ const finish = () => {
830
+ if (done) return;
831
+ done = true;
832
+ clearTimeout(timeout);
833
+ socket.destroy();
834
+ };
835
+ socket.on("error", finish);
836
+ socket.on("data", finish);
837
+ socket.on("end", finish);
838
+ socket.on("connect", () => {
839
+ socket.write(`${JSON.stringify(request)}\n`);
840
+ });
841
+ const timeout = setTimeout(finish, REQUEST_TIMEOUT_MS);
842
+ timeout.unref?.();
843
+ } catch (_error) {}
844
+ };
845
+ /**
846
+ * The reporter, or null when not running inside herdr (the common case —
847
+ * callers hold `HerdrReporter | null` and optional-chain every call).
848
+ */
849
+ function createHerdrReporter({ env, send = socketSend } = { env: detectHerdrEnv() }) {
850
+ if (!env) return null;
851
+ const { socketPath, paneId } = env;
852
+ let seq = Date.now() * 1e3;
853
+ const nextSeq = () => {
854
+ seq += 1;
855
+ return seq;
856
+ };
857
+ let lastState;
858
+ let lastMessage;
859
+ let conversationId = null;
860
+ let disposed = false;
861
+ return {
862
+ report(state, message) {
863
+ if (disposed || state === lastState && message === lastMessage) return;
864
+ lastState = state;
865
+ lastMessage = message;
866
+ send(socketPath, buildStateReport({
867
+ paneId,
868
+ state,
869
+ message,
870
+ seq: nextSeq(),
871
+ conversationId
872
+ }));
873
+ },
874
+ setConversation(id) {
875
+ if (disposed || id === conversationId) return;
876
+ conversationId = id;
877
+ if (!id) return;
878
+ send(socketPath, buildSessionReport({
879
+ paneId,
880
+ seq: nextSeq(),
881
+ conversationId: id
882
+ }));
883
+ },
884
+ dispose() {
885
+ if (disposed) return;
886
+ disposed = true;
887
+ send(socketPath, buildRelease({
888
+ paneId,
889
+ seq: nextSeq()
890
+ }));
891
+ }
892
+ };
893
+ }
894
+
895
+ //#endregion
896
+ //#region src/chat/agent-host/create.ts
897
+ /**
898
+ * The sole integration assembly point. Adding a host protocol means adding an
899
+ * adapter and registering it here; chat lifecycle and generic host code stay
900
+ * unchanged.
901
+ */
902
+ function createAgentHost({ renderer, notifications }) {
903
+ const adapters = [createTerminalNotificationsAdapter({
904
+ renderer,
905
+ enabled: notifications
906
+ })];
907
+ const herdr = createHerdrReporter();
908
+ if (herdr) adapters.push(createHerdrAdapter(herdr));
909
+ return composeAgentHost(adapters);
910
+ }
911
+
710
912
  //#endregion
711
913
  //#region src/chat/clipboard.ts
712
914
  /** Spawns a command with no stdin and collects its stdout. Resolves (never
@@ -3409,6 +3611,36 @@ function reconcileMaskedInput(previousValue, displayed) {
3409
3611
  return previousValue.slice(0, kept) + displayed.slice(kept);
3410
3612
  }
3411
3613
 
3614
+ //#endregion
3615
+ //#region src/chat/notification-preview.ts
3616
+ const MAX_NOTIFICATION_PREVIEW_CHARS = 240;
3617
+ /**
3618
+ * Collects assistant `text-delta` chunks for one run and turns them into a
3619
+ * desktop-notification-sized preview. Reasoning, tool output, cards, errors,
3620
+ * and old transcript items are intentionally excluded.
3621
+ */
3622
+ function createResponsePreview() {
3623
+ let text = "";
3624
+ let lastPartId;
3625
+ return {
3626
+ addChunk(chunk) {
3627
+ if (chunk.type !== "text-delta") return;
3628
+ const delta = typeof chunk.delta === "string" ? chunk.delta : typeof chunk.text === "string" ? chunk.text : "";
3629
+ const partId = typeof chunk.id === "string" ? chunk.id : void 0;
3630
+ if (text && partId && lastPartId && partId !== lastPartId) text += " ";
3631
+ text += delta;
3632
+ lastPartId = partId ?? lastPartId;
3633
+ },
3634
+ get() {
3635
+ const normalized = text.replace(/\s+/g, " ").trim();
3636
+ if (!normalized) return void 0;
3637
+ const characters = Array.from(normalized);
3638
+ if (characters.length <= MAX_NOTIFICATION_PREVIEW_CHARS) return normalized;
3639
+ return `${characters.slice(0, MAX_NOTIFICATION_PREVIEW_CHARS - 1).join("")}…`;
3640
+ }
3641
+ };
3642
+ }
3643
+
3412
3644
  //#endregion
3413
3645
  //#region src/chat/tui/spinner-frames.ts
3414
3646
  /** Braille dot-cycle frames — the classic smooth terminal spinner. */
@@ -4486,6 +4718,7 @@ function ChatScreen({ agent, conversation }) {
4486
4718
  const grantPrompt = portal.prompt && portal.prompt.agentId === agent.id ? portal.prompt : null;
4487
4719
  const isShared = portal.status !== "off";
4488
4720
  const initialConversationId = isNewConversation(conversation) ? null : conversation.id;
4721
+ const agentHost = useAgentHost();
4489
4722
  const [conversationId, setConversationId] = useState(initialConversationId);
4490
4723
  const [items, setItems] = useState([]);
4491
4724
  const [historyLoaded, setHistoryLoaded] = useState(initialConversationId === null);
@@ -4509,6 +4742,17 @@ function ChatScreen({ agent, conversation }) {
4509
4742
  inputRef.current = input;
4510
4743
  const credPromptRef = useRef(credPrompt);
4511
4744
  credPromptRef.current = credPrompt;
4745
+ const hostState = grantPrompt || credPrompt ? "blocked" : run.kind === "idle" ? "idle" : "working";
4746
+ useEffect(() => {
4747
+ agentHost.transition({ state: hostState });
4748
+ }, [agentHost, hostState]);
4749
+ useEffect(() => () => {
4750
+ agentHost.transition({ state: "idle" });
4751
+ agentHost.setSession(null);
4752
+ }, [agentHost]);
4753
+ useEffect(() => {
4754
+ agentHost.setSession(conversationId);
4755
+ }, [agentHost, conversationId]);
4512
4756
  const scrollRef = useRef(null);
4513
4757
  const composerRef = useRef(null);
4514
4758
  const composerBoxHeight = composerRows + 2;
@@ -4563,6 +4807,7 @@ function ChatScreen({ agent, conversation }) {
4563
4807
  const attachToRun = useCallback((runId) => {
4564
4808
  if (!rest) return;
4565
4809
  const abort = new AbortController();
4810
+ const responsePreview = createResponsePreview();
4566
4811
  setRun({
4567
4812
  kind: "streaming",
4568
4813
  runId,
@@ -4573,6 +4818,7 @@ function ChatScreen({ agent, conversation }) {
4573
4818
  signal: abort.signal,
4574
4819
  onEvent: (event) => {
4575
4820
  if (event.kind === "chunk") {
4821
+ responsePreview.addChunk(event.chunk);
4576
4822
  setItems((prev) => applyChunk(prev, event.chunk));
4577
4823
  return;
4578
4824
  }
@@ -4588,7 +4834,14 @@ function ChatScreen({ agent, conversation }) {
4588
4834
  text: m.text
4589
4835
  } : m));
4590
4836
  setRun({ kind: "idle" });
4591
- useStore.getState().attention?.notify("run-finished", agent.name);
4837
+ agentHost.transition({
4838
+ state: "idle",
4839
+ notification: {
4840
+ event: "run-finished",
4841
+ agentName: agent.name,
4842
+ message: responsePreview.get()
4843
+ }
4844
+ });
4592
4845
  }
4593
4846
  }).catch((err) => {
4594
4847
  if (abort.signal.aborted) return;
@@ -4599,7 +4852,11 @@ function ChatScreen({ agent, conversation }) {
4599
4852
  }]);
4600
4853
  setRun({ kind: "idle" });
4601
4854
  });
4602
- }, [rest, agent.name]);
4855
+ }, [
4856
+ rest,
4857
+ agent.name,
4858
+ agentHost
4859
+ ]);
4603
4860
  useEffect(() => {
4604
4861
  if (!rest || !conversationId) return;
4605
4862
  if (initialConversationId !== null) return;
@@ -5062,7 +5319,13 @@ function ChatScreen({ agent, conversation }) {
5062
5319
  agentId: agent.id,
5063
5320
  agentName: agent.name
5064
5321
  });
5065
- useStore.getState().attention?.notify("needs-input", agent.name);
5322
+ agentHost.transition({
5323
+ state: "blocked",
5324
+ notification: {
5325
+ event: "needs-input",
5326
+ agentName: agent.name
5327
+ }
5328
+ });
5066
5329
  }, [
5067
5330
  portalClient,
5068
5331
  portal.status,
@@ -5070,7 +5333,8 @@ function ChatScreen({ agent, conversation }) {
5070
5333
  portal.prompt,
5071
5334
  agent.id,
5072
5335
  agent.name,
5073
- setPortalPrompt
5336
+ setPortalPrompt,
5337
+ agentHost
5074
5338
  ]);
5075
5339
  useKeyboard((key) => {
5076
5340
  if (modelPickerOpen || themePickerOpen || helpOpen) return;
@@ -5169,6 +5433,7 @@ function ChatScreen({ agent, conversation }) {
5169
5433
  }
5170
5434
  if (ctrlCArmed) {
5171
5435
  portalClient?.dispose();
5436
+ agentHost.dispose();
5172
5437
  renderer.destroy();
5173
5438
  return;
5174
5439
  }
@@ -5515,28 +5780,20 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
5515
5780
  const rest = useStore((s) => s.rest);
5516
5781
  const goTo = useStore((s) => s.goTo);
5517
5782
  const setClients = useStore((s) => s.setClients);
5518
- const setAttention = useStore((s) => s.setAttention);
5519
5783
  const showToast = useStore((s) => s.showToast);
5784
+ const [agentHost] = useState(() => createAgentHost({
5785
+ renderer,
5786
+ notifications
5787
+ }));
5520
5788
  usePortal({
5521
5789
  appUrl,
5522
5790
  sessionToken,
5523
5791
  shareMachine
5524
5792
  });
5525
5793
  useEffect(() => {
5526
- const attention = createAttention({
5527
- renderer,
5528
- enabled: notifications
5529
- });
5530
- setAttention(attention);
5531
- return () => {
5532
- attention.dispose();
5533
- setAttention(null);
5534
- };
5535
- }, [
5536
- renderer,
5537
- notifications,
5538
- setAttention
5539
- ]);
5794
+ agentHost.transition({ state: "idle" });
5795
+ return () => agentHost.dispose();
5796
+ }, [agentHost]);
5540
5797
  useEffect(() => {
5541
5798
  const selectionCopy = createSelectionCopy({
5542
5799
  renderer,
@@ -5615,37 +5872,40 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
5615
5872
  useKeyboard((key) => {
5616
5873
  if (key.name === "c" && key.ctrl && screen.kind !== "chat") {
5617
5874
  useStore.getState().portalClient?.dispose();
5618
- useStore.getState().attention?.dispose();
5875
+ agentHost.dispose();
5619
5876
  renderer.destroy();
5620
5877
  }
5621
5878
  });
5622
- return /* @__PURE__ */ jsxs("box", {
5623
- style: {
5624
- flexDirection: "column",
5625
- flexGrow: 1,
5626
- backgroundColor: theme.background
5627
- },
5628
- children: [
5629
- /* @__PURE__ */ jsx(StatusBar, {}),
5630
- /* @__PURE__ */ jsx(ToastView, {}),
5631
- /* @__PURE__ */ jsxs("box", {
5632
- style: {
5633
- flexGrow: 1,
5634
- padding: 1
5635
- },
5636
- children: [
5637
- screen.kind === "splash" && /* @__PURE__ */ jsx(Splash, {}),
5638
- screen.kind === "error" && /* @__PURE__ */ jsx(ErrorScreen, { message: screen.message }),
5639
- screen.kind === "agent-picker" && rest && /* @__PURE__ */ jsx(AgentPickerScreen, {}),
5640
- screen.kind === "agent-create" && rest && /* @__PURE__ */ jsx(AgentCreateScreen, {}),
5641
- screen.kind === "conversation-picker" && rest && /* @__PURE__ */ jsx(ConversationPickerScreen, { agent: screen.agent }),
5642
- screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
5643
- agent: screen.agent,
5644
- conversation: screen.conversation
5645
- })
5646
- ]
5647
- })
5648
- ]
5879
+ return /* @__PURE__ */ jsx(AgentHostContext.Provider, {
5880
+ value: agentHost,
5881
+ children: /* @__PURE__ */ jsxs("box", {
5882
+ style: {
5883
+ flexDirection: "column",
5884
+ flexGrow: 1,
5885
+ backgroundColor: theme.background
5886
+ },
5887
+ children: [
5888
+ /* @__PURE__ */ jsx(StatusBar, {}),
5889
+ /* @__PURE__ */ jsx(ToastView, {}),
5890
+ /* @__PURE__ */ jsxs("box", {
5891
+ style: {
5892
+ flexGrow: 1,
5893
+ padding: 1
5894
+ },
5895
+ children: [
5896
+ screen.kind === "splash" && /* @__PURE__ */ jsx(Splash, {}),
5897
+ screen.kind === "error" && /* @__PURE__ */ jsx(ErrorScreen, { message: screen.message }),
5898
+ screen.kind === "agent-picker" && rest && /* @__PURE__ */ jsx(AgentPickerScreen, {}),
5899
+ screen.kind === "agent-create" && rest && /* @__PURE__ */ jsx(AgentCreateScreen, {}),
5900
+ screen.kind === "conversation-picker" && rest && /* @__PURE__ */ jsx(ConversationPickerScreen, { agent: screen.agent }),
5901
+ screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
5902
+ agent: screen.agent,
5903
+ conversation: screen.conversation
5904
+ })
5905
+ ]
5906
+ })
5907
+ ]
5908
+ })
5649
5909
  });
5650
5910
  }
5651
5911
  function Splash() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.45",
3
+ "version": "0.1.0-beta.49",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",