skydive-cli 0.4.1-beta.6 → 0.5.0-beta.10

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.
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-94qrV53c.mjs";
3
- import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CbayCa87.mjs";
2
+ import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-Zs3BHURc.mjs";
3
+ import { D as getReviewStateDir, H as saveTheme, N as recordDefaultAgent, O as getSavedTheme, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL, z as resolveWebUrl } from "./print-ClPkgR9z.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
- import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-BY2nADw5.mjs";
5
+ import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-I3imNduB.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
- import { t as PortalClient } from "./client-mykp1DVb.mjs";
8
- import "./daemon-CfbpCjAw.mjs";
9
- import { t as SandboxStream } from "./client-Cn2af31H.mjs";
10
- import { t as PortalDaemonClient } from "./daemon-client-6xtta_n2.mjs";
11
- import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
7
+ import { t as PortalClient } from "./client-DabRpc_T.mjs";
8
+ import "./daemon-CYYE2BTu.mjs";
9
+ import "./api-DG5W6iwx.mjs";
10
+ import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client-3A9afTFC.mjs";
12
+ import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
12
13
  import * as os$1 from "node:os";
13
14
  import { homedir, platform, release, tmpdir } from "node:os";
14
15
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
@@ -109,6 +110,7 @@ const initialPortalUi = {
109
110
  machineName: null,
110
111
  error: null,
111
112
  grantedAgentIds: [],
113
+ declinedAgentIds: [],
112
114
  prompt: null
113
115
  };
114
116
  function initialChatTitle(screen) {
@@ -181,12 +183,13 @@ const useStore = create((set, get) => ({
181
183
  chatTitle: null
182
184
  }),
183
185
  setPortalClient: (portalClient) => set({ portalClient }),
184
- syncPortal: ({ status, machineName, error, grantedAgentIds }) => set((state) => ({ portal: {
186
+ syncPortal: ({ status, machineName, error, grantedAgentIds, declinedAgentIds }) => set((state) => ({ portal: {
185
187
  ...state.portal,
186
188
  status,
187
189
  machineName,
188
190
  error,
189
- grantedAgentIds
191
+ grantedAgentIds,
192
+ declinedAgentIds
190
193
  } })),
191
194
  setPortalPrompt: (prompt) => set((state) => ({ portal: {
192
195
  ...state.portal,
@@ -210,7 +213,17 @@ const useStore = create((set, get) => ({
210
213
  /**
211
214
  * Decide the screen the chat TUI boots into.
212
215
  *
213
- * With no selector we open the agent picker, exactly as before.
216
+ * With no selector we open the agent picker unless the config stores a
217
+ * `defaultAgent`, in which case a bare `skydive chat` opens a fresh
218
+ * conversation with that agent directly. The default is maintained
219
+ * automatically (every conversation the TUI opens records its agent, so a
220
+ * bare launch returns to whoever you talked to last — the dominant launch
221
+ * pattern), and `skydive config set defaultAgent` can seed or override it. A
222
+ * stored selector that no longer resolves degrades silently to the agent
223
+ * picker rather than an error screen: config goes stale (agent renamed,
224
+ * archived, another one claiming the same name), and unlike a mistyped
225
+ * `--agent` there is no command line to correct — and picking an agent
226
+ * there heals the default on its own.
214
227
  *
215
228
  * With `--agent` (`skydive chat --agent grace`, no `-p`) we resolve it
216
229
  * against the org roster and open that agent's conversation list, skipping
@@ -226,20 +239,31 @@ const useStore = create((set, get) => ({
226
239
  * `--agent`. When both are given, the conversation wins (it pins the agent
227
240
  * anyway).
228
241
  *
242
+ * None of these paths load the whole org agent roster. The only decision that
243
+ * needs every candidate is disambiguating a fuzzy name/slug selector; an id
244
+ * (or the `--resume` conversation's agentId) is exact and resolves with a
245
+ * single `getAgent`, and `--new` with no selector only needs "exactly one
246
+ * agent, or not", which a two-row page answers. A large org must never sit on
247
+ * the `connecting…` splash while the roster streams in.
248
+ *
229
249
  * Kept out of the React component so it's unit-testable under Node (the
230
250
  * component itself can't render without Bun + OpenTUI).
231
251
  */
232
- async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation }) {
252
+ /** A selector that is a bare uuid is an exact agent id: it can be resolved
253
+ * with a single `getAgent` fetch, so no boot path that carries one needs the
254
+ * org roster. A name/slug selector is the only case that can be ambiguous and
255
+ * therefore still needs the full candidate list to disambiguate. */
256
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
257
+ async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation, defaultAgentSelector }) {
233
258
  if (conversationId) {
234
- const [conversation, agents] = await Promise.all([rest.getConversation({ conversationId }).catch((err) => {
259
+ const conversation = await rest.getConversation({ conversationId }).catch((err) => {
235
260
  if (err instanceof HttpError && err.status === 404) throw new Error(`No conversation with id ${conversationId}. It may have been deleted, or belong to another workspace.`);
236
261
  throw err;
237
- }), rest.listAgents({
238
- scope: "org",
239
- onPage: null
240
- })]);
241
- const agent = agents.find((a) => a.id === conversation.agentId);
242
- if (!agent) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
262
+ });
263
+ const agent = await rest.getAgent({ agentId: conversation.agentId }).catch((err) => {
264
+ if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
265
+ throw err;
266
+ });
243
267
  return {
244
268
  kind: "chat",
245
269
  agent,
@@ -260,26 +284,59 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
260
284
  }
261
285
  };
262
286
  }
263
- if (!agentSelector && !newConversation) return { kind: "agent-picker" };
264
- const agents = await rest.listAgents({
265
- scope: "org",
266
- onPage: null
267
- });
268
- if (!agentSelector && newConversation) {
269
- const only = agents.length === 1 ? agents[0] : null;
270
- if (!only) return { kind: "agent-picker" };
271
- return {
287
+ if (!agentSelector && !newConversation && !defaultAgentSelector) return { kind: "agent-picker" };
288
+ if (!agentSelector && defaultAgentSelector) {
289
+ const agent = UUID_RE.test(defaultAgentSelector) ? await rest.getAgent({ agentId: defaultAgentSelector }).catch((err) => {
290
+ if (err instanceof HttpError && (err.status === 404 || err.status === 403)) return null;
291
+ throw err;
292
+ }) : resolveAgentOrNull(await rest.listAgents({
293
+ scope: "org",
294
+ onPage: null
295
+ }), defaultAgentSelector);
296
+ if (agent) return newChat(agent);
297
+ return { kind: "agent-picker" };
298
+ }
299
+ if (agentSelector && UUID_RE.test(agentSelector)) {
300
+ const agent = await rest.getAgent({ agentId: agentSelector }).catch((err) => {
301
+ if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`No agent with id ${agentSelector}. It may not exist, or belong to another workspace.`);
302
+ throw err;
303
+ });
304
+ if (newConversation) return {
272
305
  kind: "chat",
273
- agent: only,
306
+ agent,
274
307
  conversation: {
275
308
  kind: "new",
276
- agentId: only.id,
309
+ agentId: agent.id,
277
310
  nonce: crypto.randomUUID()
278
311
  }
279
312
  };
313
+ return {
314
+ kind: "conversation-picker",
315
+ agent
316
+ };
317
+ }
318
+ if (!agentSelector && newConversation) {
319
+ const firstTwo = await rest.listAgents({
320
+ scope: "org",
321
+ onPage: null,
322
+ limit: 2
323
+ });
324
+ const only = firstTwo.length === 1 ? firstTwo[0] : null;
325
+ if (!only) return { kind: "agent-picker" };
326
+ return newChat(only);
280
327
  }
281
- const agent = resolveAgent(agents, agentSelector);
282
- if (newConversation) return {
328
+ const agent = resolveAgent(await rest.listAgents({
329
+ scope: "org",
330
+ onPage: null
331
+ }), agentSelector);
332
+ if (newConversation) return newChat(agent);
333
+ return {
334
+ kind: "conversation-picker",
335
+ agent
336
+ };
337
+ }
338
+ function newChat(agent) {
339
+ return {
283
340
  kind: "chat",
284
341
  agent,
285
342
  conversation: {
@@ -288,10 +345,17 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
288
345
  nonce: crypto.randomUUID()
289
346
  }
290
347
  };
291
- return {
292
- kind: "conversation-picker",
293
- agent
294
- };
348
+ }
349
+ /** `resolveAgent` throws for a selector that matches nothing or matches
350
+ * ambiguously — both phrased for a human who can retype `--agent`. A stored
351
+ * default has no command line behind it, so both shapes just mean "no usable
352
+ * default". */
353
+ function resolveAgentOrNull(agents, selector) {
354
+ try {
355
+ return resolveAgent(agents, selector);
356
+ } catch {
357
+ return null;
358
+ }
295
359
  }
296
360
 
297
361
  //#endregion
@@ -317,21 +381,28 @@ function createPortalClient(opts) {
317
381
  if (portalDaemonDisabled()) {
318
382
  const client = new PortalClient({
319
383
  appUrl: opts.appUrl,
320
- sessionToken: opts.sessionToken,
384
+ credentials: () => ({
385
+ sessionToken: opts.sessionToken,
386
+ deviceToken: null
387
+ }),
321
388
  resolveCwd: () => process.cwd(),
322
389
  onState: (state) => opts.onState({
323
390
  status: state.status,
324
391
  machineName: state.machineName,
325
392
  friendlyName: state.friendlyName,
326
393
  error: state.error,
327
- grantedAgentIds: state.grantedAgentIds
394
+ grantedAgentIds: state.grantedAgentIds,
395
+ declinedAgentIds: []
328
396
  })
329
397
  });
330
398
  return {
331
399
  start: () => Promise.resolve(),
332
400
  enable: () => client.enable(),
333
401
  disable: () => client.disable(),
334
- grantAgent: (agentId) => client.grantAgent(agentId),
402
+ grantAgent: (agentId, conversationId) => client.grantAgent(agentId, conversationId),
403
+ decline: (_agentId, declined) => {
404
+ if (declined && client.grantedAgentIds().length === 0) client.disable();
405
+ },
335
406
  isGranted: (agentId) => client.isGranted(agentId),
336
407
  bind: () => {},
337
408
  dispose: () => client.dispose()
@@ -370,7 +441,8 @@ function usePortal({ appUrl, sessionToken, shareMachine, enabled = true }) {
370
441
  status: state.status,
371
442
  machineName: state.machineName,
372
443
  error: state.error,
373
- grantedAgentIds: state.grantedAgentIds
444
+ grantedAgentIds: state.grantedAgentIds,
445
+ declinedAgentIds: state.declinedAgentIds
374
446
  })
375
447
  });
376
448
  setPortalClient(client);
@@ -4519,20 +4591,23 @@ function RenderItem({ item }) {
4519
4591
  children: [item.text ? /* @__PURE__ */ jsx("text", {
4520
4592
  fg: theme.user,
4521
4593
  children: item.text
4522
- }) : null, item.attachments && item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
4594
+ }) : null, item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
4523
4595
  fg: theme.muted,
4524
4596
  children: ["⎘ ", item.attachments.join(" · ")]
4525
4597
  }) : null]
4526
4598
  });
4527
- case "pending-steer": return /* @__PURE__ */ jsx(Row, {
4599
+ case "pending-steer": return /* @__PURE__ */ jsxs(Row, {
4528
4600
  barColor: theme.dim,
4529
- children: /* @__PURE__ */ jsxs("text", {
4601
+ children: [/* @__PURE__ */ jsxs("text", {
4530
4602
  fg: theme.muted,
4531
4603
  children: [item.text, /* @__PURE__ */ jsx("span", {
4532
4604
  fg: theme.dim,
4533
4605
  children: " · steering… (esc to cancel)"
4534
4606
  })]
4535
- })
4607
+ }), item.attachments.length > 0 ? /* @__PURE__ */ jsxs("text", {
4608
+ fg: theme.dim,
4609
+ children: ["⎘ ", item.attachments.join(" · ")]
4610
+ }) : null]
4536
4611
  });
4537
4612
  case "assistant-text":
4538
4613
  if (item.done) return /* @__PURE__ */ jsx(Row, {
@@ -5267,7 +5342,8 @@ function applyChunk(items, chunk, agentName = null) {
5267
5342
  return items.map((m) => m.kind === "pending-steer" && ids.has(m.id) ? {
5268
5343
  kind: "user",
5269
5344
  id: m.id,
5270
- text: m.text
5345
+ text: m.text,
5346
+ attachments: m.attachments
5271
5347
  } : m);
5272
5348
  }
5273
5349
  case "data-anyone-render-spec": {
@@ -5508,7 +5584,7 @@ function uiMessagesToItems(messages) {
5508
5584
  kind: "user",
5509
5585
  id: m.id,
5510
5586
  text,
5511
- ...attachments.length > 0 ? { attachments } : {}
5587
+ attachments
5512
5588
  });
5513
5589
  continue;
5514
5590
  }
@@ -6009,6 +6085,58 @@ function createResponsePreview() {
6009
6085
  };
6010
6086
  }
6011
6087
 
6088
+ //#endregion
6089
+ //#region src/chat/tui/new-chat-splash.tsx
6090
+ /** Rows the splash art needs before it's worth painting: the 6-row mark, a
6091
+ * spacer, the two identity/hint lines, and a row of breathing room. Below
6092
+ * this only the identity + hint render (small panes must keep every
6093
+ * transcript row). */
6094
+ const MARK_MIN_ROWS = 10;
6095
+ /** Columns the full mark + wordmark lockup spans (~68), with margin; below
6096
+ * this the pinwheel mark renders alone instead of wrapping the wordmark. */
6097
+ const FULL_MARK_MIN_COLS = 70;
6098
+ /**
6099
+ * Brand splash filling an empty conversation's transcript area: the Skydive
6100
+ * mark centered above the agent's identity and the "say hello" hint, with
6101
+ * the composer live below. The identity line matters here because nothing
6102
+ * else on this screen names the agent — a bare `skydive chat` boots straight
6103
+ * into this view via the configured default. Rendered only while the
6104
+ * transcript is empty, so the first submission replaces it with the regular
6105
+ * conversation view. Degrades by available space: full lockup → mark only →
6106
+ * identity + hint only.
6107
+ */
6108
+ function NewChatSplash({ agentName, agentTitle, width, height }) {
6109
+ return /* @__PURE__ */ jsxs("box", {
6110
+ style: {
6111
+ height,
6112
+ flexDirection: "column",
6113
+ justifyContent: "center",
6114
+ alignItems: "center"
6115
+ },
6116
+ children: [
6117
+ height >= MARK_MIN_ROWS ? /* @__PURE__ */ jsx("box", {
6118
+ style: {
6119
+ flexDirection: "column",
6120
+ alignItems: "center",
6121
+ marginBottom: 1
6122
+ },
6123
+ children: /* @__PURE__ */ jsx(SkydiveMark, { variant: width >= FULL_MARK_MIN_COLS ? "full" : "mark" })
6124
+ }) : null,
6125
+ /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
6126
+ fg: theme.accent,
6127
+ children: agentName
6128
+ }), agentTitle ? /* @__PURE__ */ jsxs("span", {
6129
+ fg: theme.muted,
6130
+ children: [" · ", agentTitle]
6131
+ }) : null] }),
6132
+ /* @__PURE__ */ jsx("text", {
6133
+ fg: theme.dim,
6134
+ children: "say hello below"
6135
+ })
6136
+ ]
6137
+ });
6138
+ }
6139
+
6012
6140
  //#endregion
6013
6141
  //#region src/chat/tui/prompt-history.ts
6014
6142
  /**
@@ -10149,6 +10277,7 @@ function ChatScreen({ agent, conversation }) {
10149
10277
  const portalClient = useStore((s) => s.portalClient);
10150
10278
  const setPortalPrompt = useStore((s) => s.setPortalPrompt);
10151
10279
  const declinedRef = useRef(/* @__PURE__ */ new Set());
10280
+ const grantInFlightRef = useRef(/* @__PURE__ */ new Set());
10152
10281
  const grantPrompt = portal.prompt && portal.prompt.agentId === agent.id ? portal.prompt : null;
10153
10282
  const isShared = portal.status !== "off";
10154
10283
  const initialConversationId = isNewConversation(conversation) ? null : conversation.id;
@@ -10426,7 +10555,8 @@ function ChatScreen({ agent, conversation }) {
10426
10555
  setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
10427
10556
  kind: "user",
10428
10557
  id: m.id,
10429
- text: m.text
10558
+ text: m.text,
10559
+ attachments: m.attachments
10430
10560
  } : m));
10431
10561
  setRun({ kind: "idle" });
10432
10562
  agentHost.transition({
@@ -10503,7 +10633,7 @@ function ChatScreen({ agent, conversation }) {
10503
10633
  kind: "user",
10504
10634
  id: optimisticId,
10505
10635
  text: trimmed,
10506
- ...stagedNames.length > 0 ? { attachments: stagedNames } : {}
10636
+ attachments: stagedNames
10507
10637
  }]);
10508
10638
  const wasStreaming = runRef.current.kind === "streaming";
10509
10639
  if (!wasStreaming) setRun({
@@ -10552,7 +10682,8 @@ function ChatScreen({ agent, conversation }) {
10552
10682
  if (directiveId) setItems((prev) => prev.map((m) => m.id === optimisticId ? {
10553
10683
  kind: "pending-steer",
10554
10684
  id: directiveId,
10555
- text: trimmed
10685
+ text: trimmed,
10686
+ attachments: m.kind === "user" || m.kind === "pending-steer" ? m.attachments : []
10556
10687
  } : m));
10557
10688
  const current = runRef.current;
10558
10689
  if (!(current.kind === "streaming" && current.runId === result.runId)) attachToRun(result.runId, result.agentName ?? null);
@@ -10599,12 +10730,14 @@ function ChatScreen({ agent, conversation }) {
10599
10730
  });
10600
10731
  (async () => {
10601
10732
  if (autoGrantMachine && portalClient && portalStatus === "connected" && !portalClient.isGranted(agent.id)) try {
10602
- await portalClient.grantAgent(agent.id);
10733
+ grantInFlightRef.current.add(agent.id);
10734
+ await portalClient.grantAgent(agent.id, null);
10603
10735
  useStore.getState().showToast({
10604
- message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent ${agent.name}`,
10736
+ message: `Shared this machine with ${agent.name} for the import. The grant persists. Revoke anytime: skydive portal revoke --agent "${agent.name}"`,
10605
10737
  durationMs: 1e4
10606
10738
  });
10607
10739
  } catch (err) {
10740
+ grantInFlightRef.current.delete(agent.id);
10608
10741
  useStore.getState().showToast({
10609
10742
  message: `Couldn't grant machine access automatically (${errorMessage(err)}). The agent will ask in chat instead.`,
10610
10743
  variant: "warning"
@@ -10668,9 +10801,12 @@ function ChatScreen({ agent, conversation }) {
10668
10801
  if (!portalClient) throw new Error("portal is unavailable in this session");
10669
10802
  const justEnabled = portal.status === "off";
10670
10803
  if (justEnabled) portalClient.enable();
10804
+ grantInFlightRef.current.add(action.agentId);
10805
+ if (useStore.getState().portal.prompt?.agentId === action.agentId) setPortalPrompt(null);
10671
10806
  try {
10672
- await portalClient.grantAgent(action.agentId);
10807
+ await portalClient.grantAgent(action.agentId, action.conversationId);
10673
10808
  } catch (grantErr) {
10809
+ grantInFlightRef.current.delete(action.agentId);
10674
10810
  if (justEnabled) portalClient.disable();
10675
10811
  throw grantErr;
10676
10812
  }
@@ -10731,7 +10867,8 @@ function ChatScreen({ agent, conversation }) {
10731
10867
  appUrl,
10732
10868
  updateCard,
10733
10869
  portalClient,
10734
- portal.status
10870
+ portal.status,
10871
+ setPortalPrompt
10735
10872
  ]);
10736
10873
  const promptForCards = useCallback(() => {
10737
10874
  const targets = itemsRef.current.filter(isActionableCard);
@@ -11056,7 +11193,8 @@ function ChatScreen({ agent, conversation }) {
11056
11193
  setItems((prev) => prev.map((m) => m.kind === "pending-steer" ? {
11057
11194
  kind: "user",
11058
11195
  id: m.id,
11059
- text: m.text
11196
+ text: m.text,
11197
+ attachments: m.attachments
11060
11198
  } : m));
11061
11199
  useStore.getState().showToast({ message: "run cancelled" });
11062
11200
  }, [rest]);
@@ -11234,7 +11372,8 @@ function ChatScreen({ agent, conversation }) {
11234
11372
  rest,
11235
11373
  agentSelector: null,
11236
11374
  conversationId: target,
11237
- newConversation: false
11375
+ newConversation: false,
11376
+ defaultAgentSelector: null
11238
11377
  }));
11239
11378
  } catch (err) {
11240
11379
  setItems((prev) => [...prev, {
@@ -11255,6 +11394,7 @@ function ChatScreen({ agent, conversation }) {
11255
11394
  }
11256
11395
  if (portal.status === "off") {
11257
11396
  declinedRef.current.delete(agent.id);
11397
+ portalClient.decline(agent.id, false);
11258
11398
  portalClient.enable();
11259
11399
  if (conversationId) portalClient.bind(conversationId, shellSessionRef.current?.cwd ?? process.cwd());
11260
11400
  } else {
@@ -11272,25 +11412,35 @@ function ChatScreen({ agent, conversation }) {
11272
11412
  if (!portalClient) return;
11273
11413
  const agentName = agent.name;
11274
11414
  setPortalPrompt(null);
11275
- portalClient.grantAgent(agent.id).then(() => useStore.getState().showToast({
11415
+ grantInFlightRef.current.add(agent.id);
11416
+ portalClient.grantAgent(agent.id, conversationId).then(() => useStore.getState().showToast({
11276
11417
  variant: "success",
11277
11418
  message: `shared this machine with ${agentName}`
11278
- })).catch((err) => setItems((prev) => [...prev, {
11279
- kind: "error",
11280
- id: crypto.randomUUID(),
11281
- text: `couldn't share machine: ${errorDetail(err)}`
11282
- }]));
11419
+ })).catch((err) => {
11420
+ grantInFlightRef.current.delete(agent.id);
11421
+ setItems((prev) => [...prev, {
11422
+ kind: "error",
11423
+ id: crypto.randomUUID(),
11424
+ text: `couldn't share machine: ${errorDetail(err)}`
11425
+ }]);
11426
+ });
11283
11427
  }, [
11284
11428
  portalClient,
11285
11429
  agent.id,
11286
11430
  agent.name,
11287
- setPortalPrompt
11431
+ setPortalPrompt,
11432
+ conversationId
11288
11433
  ]);
11289
11434
  const declineGrant = useCallback(() => {
11290
11435
  declinedRef.current.add(agent.id);
11436
+ portalClient?.decline(agent.id, true);
11291
11437
  setPortalPrompt(null);
11292
11438
  useStore.getState().showToast({ message: "kept this machine private" });
11293
- }, [agent.id, setPortalPrompt]);
11439
+ }, [
11440
+ portalClient,
11441
+ agent.id,
11442
+ setPortalPrompt
11443
+ ]);
11294
11444
  const openPortalAccess = useCallback(() => {
11295
11445
  if (!portalClient) {
11296
11446
  useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
@@ -11299,6 +11449,7 @@ function ChatScreen({ agent, conversation }) {
11299
11449
  switch (portal.status) {
11300
11450
  case "off":
11301
11451
  declinedRef.current.delete(agent.id);
11452
+ portalClient.decline(agent.id, false);
11302
11453
  portalClient.enable();
11303
11454
  useStore.getState().showToast({ message: "enabling local access on this machine…" });
11304
11455
  break;
@@ -11311,6 +11462,7 @@ function ChatScreen({ agent, conversation }) {
11311
11462
  break;
11312
11463
  }
11313
11464
  declinedRef.current.delete(agent.id);
11465
+ portalClient.decline(agent.id, false);
11314
11466
  setPortalPrompt({
11315
11467
  agentId: agent.id,
11316
11468
  agentName: agent.name
@@ -11515,7 +11667,9 @@ function ChatScreen({ agent, conversation }) {
11515
11667
  useEffect(() => {
11516
11668
  if (!portalClient || portal.status !== "connected") return;
11517
11669
  if (portal.grantedAgentIds.includes(agent.id)) return;
11670
+ if (portal.declinedAgentIds.includes(agent.id)) return;
11518
11671
  if (declinedRef.current.has(agent.id)) return;
11672
+ if (grantInFlightRef.current.has(agent.id)) return;
11519
11673
  if (portal.prompt?.agentId === agent.id) return;
11520
11674
  setPortalPrompt({
11521
11675
  agentId: agent.id,
@@ -11532,12 +11686,21 @@ function ChatScreen({ agent, conversation }) {
11532
11686
  portalClient,
11533
11687
  portal.status,
11534
11688
  portal.grantedAgentIds,
11689
+ portal.declinedAgentIds,
11535
11690
  portal.prompt,
11536
11691
  agent.id,
11537
11692
  agent.name,
11538
11693
  setPortalPrompt,
11539
11694
  agentHost
11540
11695
  ]);
11696
+ useEffect(() => {
11697
+ for (const agentId of portal.grantedAgentIds) grantInFlightRef.current.delete(agentId);
11698
+ if (portal.prompt && portal.grantedAgentIds.includes(portal.prompt.agentId)) setPortalPrompt(null);
11699
+ }, [
11700
+ portal.grantedAgentIds,
11701
+ portal.prompt,
11702
+ setPortalPrompt
11703
+ ]);
11541
11704
  useKeyboard((key) => {
11542
11705
  if (reviewOpen) {
11543
11706
  if (key.name === "g" && key.ctrl) {
@@ -11879,13 +12042,11 @@ function ChatScreen({ agent, conversation }) {
11879
12042
  children: [!historyLoaded ? /* @__PURE__ */ jsx("text", {
11880
12043
  fg: theme.muted,
11881
12044
  children: "loading history…"
11882
- }) : items.length === 0 ? /* @__PURE__ */ jsxs("text", {
11883
- fg: theme.muted,
11884
- children: [
11885
- "say hello to ",
11886
- agent.name,
11887
- " below."
11888
- ]
12045
+ }) : items.length === 0 ? /* @__PURE__ */ jsx(NewChatSplash, {
12046
+ agentName: agent.name,
12047
+ agentTitle: agent.title ?? null,
12048
+ width: reviewView && reviewPlacement === "side" ? Math.max(1, bodyWidth - reviewWidth - 1) : bodyWidth,
12049
+ height: Math.max(1, scrollHeight - 1)
11889
12050
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [transcriptWindow.hiddenCount > 0 ? /* @__PURE__ */ jsx("box", {
11890
12051
  style: {
11891
12052
  paddingLeft: 1,
@@ -12339,9 +12500,12 @@ function ToastView() {
12339
12500
 
12340
12501
  //#endregion
12341
12502
  //#region src/chat/tui/app.tsx
12342
- function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation }) {
12503
+ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector }) {
12343
12504
  const renderer = useRenderer();
12344
12505
  const screen = useStore((s) => s.screen);
12506
+ useEffect(() => {
12507
+ if (screen.kind === "chat") recordDefaultAgent(screen.agent.id);
12508
+ }, [screen]);
12345
12509
  const chatTitle = useStore((s) => s.chatTitle);
12346
12510
  useStore((s) => s.themeId);
12347
12511
  const rest = useStore((s) => s.rest);
@@ -12424,7 +12588,8 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
12424
12588
  rest,
12425
12589
  agentSelector,
12426
12590
  conversationId,
12427
- newConversation: newConversation || useStore.getState().seedPrompt !== null
12591
+ newConversation: newConversation || useStore.getState().seedPrompt !== null,
12592
+ defaultAgentSelector
12428
12593
  });
12429
12594
  if (cancelled) return;
12430
12595
  goTo(screen);
@@ -12447,6 +12612,7 @@ function App({ appUrl, sessionToken, authKind, shareMachine, promptHistoryPath,
12447
12612
  agentSelector,
12448
12613
  conversationId,
12449
12614
  newConversation,
12615
+ defaultAgentSelector,
12450
12616
  goTo,
12451
12617
  setClients,
12452
12618
  fail
@@ -13025,7 +13191,7 @@ function onRenderCommit(id, phase, actualDuration, baseDuration, startTime, comm
13025
13191
  * module — and the heavy OpenTUI dependency graph it pulls in — is
13026
13192
  * dynamically imported and never loaded on the Node management path.
13027
13193
  */
13028
- async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, seedPrompt }) {
13194
+ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId, newConversation, defaultAgentSelector, seedPrompt }) {
13029
13195
  await registerGrammars();
13030
13196
  const watchdog = startMemoryWatchdog();
13031
13197
  installCrashHandler({ memoryProvider: () => watchdog.snapshot() });
@@ -13066,7 +13232,8 @@ async function runChat({ appUrl, sessionToken, authKind = "session", shareMachin
13066
13232
  notifications,
13067
13233
  agentSelector: agentSelector ?? null,
13068
13234
  conversationId: conversationId ?? null,
13069
- newConversation
13235
+ newConversation,
13236
+ defaultAgentSelector: defaultAgentSelector ?? null
13070
13237
  })
13071
13238
  }));
13072
13239
  }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-DabRpc_T.mjs";
3
+ import "./api-DG5W6iwx.mjs";
4
+
5
+ export { PortalClient };