skydive-cli 0.2.0-beta.608 → 0.2.0-beta.613

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as deleteConfig, B as resolveAppUrl, C as setActiveWorkspace, D as DEFAULT_API_URL, E as API_KEY_PREFIX, G as saveConfig, H as resolveManagementAuth, I as getShareMachineDefault, J as setLastSeenVersion, K as saveSession, L as getStoredApiKeyId, M as getLastSeenVersion, N as getPromptHistoryPath, R as getStoredApiKeyWorkspaceName, S as listWorkspaces, T as API_KEY_FAMILY_PREFIX, U as resolveSession, V as resolveConfig, X as version, Y as name, _ as themes, b as getActiveWorkspaceId, j as getConfigPath, k as DEFAULT_WEB_URL, o as brandHelpArt, t as installCrashHandler, w as API_KEYS_URL, x as getSessionIdentity, y as ensureActiveOrganization, z as getUpdateCheckDisabled } from "./install-B-9AJk0F.mjs";
2
+ import { A as deleteConfig, B as resolveAppUrl, C as setActiveWorkspace, D as DEFAULT_API_URL, E as API_KEY_PREFIX, G as saveConfig, H as resolveManagementAuth, I as getShareMachineDefault, J as setLastSeenVersion, K as saveSession, L as getStoredApiKeyId, M as getLastSeenVersion, N as getPromptHistoryPath, R as getStoredApiKeyWorkspaceName, S as listWorkspaces, T as API_KEY_FAMILY_PREFIX, U as resolveSession, V as resolveConfig, X as version, Y as name, _ as themes, b as getActiveWorkspaceId, j as getConfigPath, k as DEFAULT_WEB_URL, o as brandHelpArt, t as installCrashHandler, w as API_KEYS_URL, x as getSessionIdentity, y as ensureActiveOrganization, z as getUpdateCheckDisabled } from "./install-D1uhYunw.mjs";
3
3
  import { n as printError, r as printTable, t as output } from "./output-B4cW10Ph.mjs";
4
4
  import { n as createRestClient } from "./rest-DMpigwmN.mjs";
5
5
  import { i as resolveAgent } from "./print-6Bm51XhR.mjs";
@@ -1195,7 +1195,7 @@ const importCommand = {
1195
1195
  process.exit(1);
1196
1196
  }
1197
1197
  }
1198
- const { runChat } = await import("./boot-B1AReDlO.mjs");
1198
+ const { runChat } = await import("./boot-BnaFkGl4.mjs");
1199
1199
  await runChat({
1200
1200
  appUrl,
1201
1201
  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-B1AReDlO.mjs");
1514
+ const { runChat } = await import("./boot-BnaFkGl4.mjs");
1515
1515
  await runChat({
1516
1516
  appUrl,
1517
1517
  sessionToken: session.value.sessionToken,
@@ -1819,7 +1819,7 @@ const switchCommand = {
1819
1819
  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.");
1820
1820
  process.exit(1);
1821
1821
  }
1822
- const { runWorkspacePicker } = await import("./boot-B1AReDlO.mjs");
1822
+ const { runWorkspacePicker } = await import("./boot-BnaFkGl4.mjs");
1823
1823
  await runWorkspacePicker(session);
1824
1824
  return;
1825
1825
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, D as DEFAULT_API_URL, F as getSavedTheme, O as DEFAULT_APP_URL, P as getReviewStateDir, S as listWorkspaces, W as resolveWebUrl, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, j as getConfigPath, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, q as saveTheme, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-B-9AJk0F.mjs";
2
+ import { C as setActiveWorkspace, D as DEFAULT_API_URL, F as getSavedTheme, O as DEFAULT_APP_URL, P as getReviewStateDir, S as listWorkspaces, W as resolveWebUrl, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, j as getConfigPath, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, q as saveTheme, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-D1uhYunw.mjs";
3
3
  import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-DMpigwmN.mjs";
4
4
  import { 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 } from "./print-6Bm51XhR.mjs";
5
5
  import "./api-BGWcPuik.mjs";
@@ -14,7 +14,7 @@ import open from "open";
14
14
  import { execFile, spawn } from "node:child_process";
15
15
  import { createHash } from "node:crypto";
16
16
  import { constants } from "node:fs";
17
- import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
17
+ import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
18
18
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
19
19
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
20
20
  import { create } from "zustand";
@@ -273,6 +273,8 @@ const noopAgentHost = {
273
273
  transition() {},
274
274
  setSession() {},
275
275
  setTitle() {},
276
+ addPullRequest() {},
277
+ removePullRequest() {},
276
278
  dispose() {}
277
279
  };
278
280
  const AgentHostContext = createContext(noopAgentHost);
@@ -298,6 +300,14 @@ function composeAgentHost(adapters) {
298
300
  if (disposed) return;
299
301
  for (const adapter of adapters) adapter.setTitle?.(title);
300
302
  },
303
+ addPullRequest(pr) {
304
+ if (disposed) return;
305
+ for (const adapter of adapters) adapter.addPullRequest?.(pr);
306
+ },
307
+ removePullRequest(pr) {
308
+ if (disposed) return;
309
+ for (const adapter of adapters) adapter.removePullRequest?.(pr);
310
+ },
301
311
  dispose() {
302
312
  if (disposed) return;
303
313
  disposed = true;
@@ -463,6 +473,61 @@ function createCmuxWorkspaceTitleAdapter({ socket, workspaceId }) {
463
473
  } };
464
474
  }
465
475
 
476
+ //#endregion
477
+ //#region src/chat/agent-host/adapters/cmux/sidebar-pr.ts
478
+ /** Neutral gray — deliberately not green/purple; we don't render PR state. */
479
+ const PILL_COLOR = "#8b949e";
480
+ const PILL_ICON = "git-pull-request";
481
+ const PILL_PRIORITY = 90;
482
+ /**
483
+ * The cmux sidebar status key for a PR, derived from its repo-qualified
484
+ * identity (`owner/repo#number`) so PRs from different repos never collide on a
485
+ * shared `pr-<number>` slot. cmux keys are space-split tokens, so we sanitize
486
+ * to `pr-<owner>-<repo>-<number>` (any non-alphanumeric run becomes `-`).
487
+ */
488
+ function pillKey(prKey) {
489
+ return `pr-${prKey.replace(/[^a-z0-9]+/gi, "-")}`;
490
+ }
491
+ /**
492
+ * A V1 socket arg value. cmux's line protocol splits on spaces, so a value
493
+ * with spaces (our `PR #123` label) must be double-quoted; the flag values we
494
+ * send (hex color, url, integers, the workspace id) never contain spaces or
495
+ * quotes, so simple quoting of the label is sufficient and there is nothing to
496
+ * escape.
497
+ */
498
+ function quote(value) {
499
+ return `"${value}"`;
500
+ }
501
+ function createCmuxSidebarPrAdapter({ socket, workspaceId }) {
502
+ const applied = /* @__PURE__ */ new Set();
503
+ let chain = Promise.resolve();
504
+ const enqueue = (step) => {
505
+ chain = chain.then(step).catch((_error) => {});
506
+ };
507
+ const runSet = async (pr) => {
508
+ if (applied.has(pr.key)) return;
509
+ applied.add(pr.key);
510
+ const line = `set_status ${pillKey(pr.key)} ${quote(`PR #${pr.number}`)} --icon=${PILL_ICON} --color=${PILL_COLOR} --priority=${PILL_PRIORITY} --url=${pr.url} --tab=${workspaceId}`;
511
+ if (!await socket.command(line)) applied.delete(pr.key);
512
+ };
513
+ const runClear = async (prKey) => {
514
+ if (!applied.has(prKey)) return;
515
+ if (await socket.command(`clear_status ${pillKey(prKey)} --tab=${workspaceId}`)) applied.delete(prKey);
516
+ };
517
+ return {
518
+ addPullRequest(pr) {
519
+ enqueue(() => runSet(pr));
520
+ },
521
+ removePullRequest(pr) {
522
+ enqueue(() => runClear(pr.key));
523
+ },
524
+ dispose() {
525
+ const keys = [...applied];
526
+ for (const key of keys) enqueue(() => runClear(key));
527
+ }
528
+ };
529
+ }
530
+
466
531
  //#endregion
467
532
  //#region src/chat/agent-host/adapters/cmux/socket.ts
468
533
  const cmuxRpcResponseSchema = z.object({
@@ -474,50 +539,92 @@ const cmuxRpcResponseSchema = z.object({
474
539
  const RPC_TIMEOUT_MS = 1500;
475
540
  function createCmuxSocket({ socketPath, timeoutMs = RPC_TIMEOUT_MS }) {
476
541
  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));
542
+ const roundTrip = (requestLine, parseReply, onFailure) => new Promise((resolve) => {
543
+ let settled = false;
544
+ const finish = (value) => {
545
+ if (settled) return;
546
+ settled = true;
547
+ clearTimeout(timer);
548
+ try {
549
+ socket.destroy();
550
+ } catch (_error) {}
551
+ resolve(value);
552
+ };
553
+ const timer = setTimeout(() => finish(onFailure), timeoutMs);
554
+ let buffer = "";
555
+ const socket = connect(socketPath);
556
+ socket.setEncoding("utf8");
557
+ socket.on("connect", () => {
558
+ try {
559
+ socket.write(requestLine);
560
+ } catch (_error) {
561
+ finish(onFailure);
562
+ }
519
563
  });
520
- } };
564
+ socket.on("data", (chunk) => {
565
+ buffer += chunk;
566
+ const newline = buffer.indexOf("\n");
567
+ if (newline === -1) return;
568
+ const line = buffer.slice(0, newline);
569
+ try {
570
+ finish(parseReply(line));
571
+ } catch (_error) {
572
+ finish(onFailure);
573
+ }
574
+ });
575
+ socket.on("error", () => finish(onFailure));
576
+ socket.on("close", () => finish(onFailure));
577
+ });
578
+ return {
579
+ command(line) {
580
+ `${++seq}`;
581
+ return roundTrip(`${line}\n`, (reply) => reply.startsWith("OK") || reply === "OK", false);
582
+ },
583
+ request(method, params) {
584
+ return new Promise((resolve) => {
585
+ let settled = false;
586
+ const finish = (value) => {
587
+ if (settled) return;
588
+ settled = true;
589
+ clearTimeout(timer);
590
+ try {
591
+ socket.destroy();
592
+ } catch (_error) {}
593
+ resolve(value);
594
+ };
595
+ const timer = setTimeout(() => finish(null), timeoutMs);
596
+ const id = `skydive-${++seq}`;
597
+ let buffer = "";
598
+ const socket = connect(socketPath);
599
+ socket.setEncoding("utf8");
600
+ socket.on("connect", () => {
601
+ try {
602
+ socket.write(`${JSON.stringify({
603
+ id,
604
+ method,
605
+ params
606
+ })}\n`);
607
+ } catch (_error) {
608
+ finish(null);
609
+ }
610
+ });
611
+ socket.on("data", (chunk) => {
612
+ buffer += chunk;
613
+ const newline = buffer.indexOf("\n");
614
+ if (newline === -1) return;
615
+ const line = buffer.slice(0, newline);
616
+ try {
617
+ const parsed = cmuxRpcResponseSchema.safeParse(JSON.parse(line));
618
+ finish(parsed.success ? parsed.data : null);
619
+ } catch (_error) {
620
+ finish(null);
621
+ }
622
+ });
623
+ socket.on("error", () => finish(null));
624
+ socket.on("close", () => finish(null));
625
+ });
626
+ }
627
+ };
521
628
  }
522
629
 
523
630
  //#endregion
@@ -691,15 +798,102 @@ function createAgentHost({ renderer, notifications }) {
691
798
  enabled: notifications
692
799
  }), createTerminalTitleAdapter({ renderer })];
693
800
  const cmux = cmuxWorkspaceEnv();
694
- if (cmux) adapters.push(createCmuxWorkspaceTitleAdapter({
695
- socket: createCmuxSocket({ socketPath: cmux.socketPath }),
696
- workspaceId: cmux.workspaceId
697
- }));
801
+ if (cmux) {
802
+ const socket = createCmuxSocket({ socketPath: cmux.socketPath });
803
+ adapters.push(createCmuxWorkspaceTitleAdapter({
804
+ socket,
805
+ workspaceId: cmux.workspaceId
806
+ }), createCmuxSidebarPrAdapter({
807
+ socket,
808
+ workspaceId: cmux.workspaceId
809
+ }));
810
+ }
698
811
  const herdr = createHerdrReporter();
699
812
  if (herdr) adapters.push(createHerdrAdapter(herdr));
700
813
  return composeAgentHost(adapters);
701
814
  }
702
815
 
816
+ //#endregion
817
+ //#region src/chat/agent-host/pull-requests.ts
818
+ /**
819
+ * Matches a GitHub pull-request URL and captures its number. Scoped to
820
+ * **github.com** on purpose, and this is a scope decision, not an oversight:
821
+ *
822
+ * cmux's native sidebar badge also resolves GitHub Enterprise (GHES) PRs, but
823
+ * it does so from the workspace's **local git remote** (host-qualified
824
+ * `(host, owner, repo)` + a per-host `gh auth token`). We parse a transcript
825
+ * in a remote sandbox with no local `.git`, so from a bare URL we cannot tell
826
+ * `git.acme.com/o/r/pull/5` is a GHES host from any other tool's `/pull/`
827
+ * route — and a pill linking a misidentified URL is worse than no pill. So we
828
+ * take the one host we can identify unambiguously (github.com, incl. `www.`),
829
+ * which covers the common product case; GHES stays the native badge's job.
830
+ *
831
+ * Only the `/pull/<n>` form (not `/issues/`, not a bare `#123` — ambiguous with
832
+ * issues without local repo context we don't have). A trailing path/query/
833
+ * fragment (e.g. `/files`, `#discussion`) is allowed but not captured into the
834
+ * canonical URL. Also matches inside markdown links, backticks, and trailing
835
+ * punctuation.
836
+ */
837
+ const PR_URL_RE = /https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/gi;
838
+ /**
839
+ * Extracts every distinct GitHub PR referenced in a block of text, in first-seen
840
+ * order, de-duplicated by **repo-qualified identity** (`owner/repo#number`) —
841
+ * NOT by number, since `#42` can exist in many repos. Each ref carries a
842
+ * canonicalized URL (scheme + host + `/owner/repo/pull/number`, no trailing
843
+ * path/query/fragment) so the same PR linked twice with different fragments
844
+ * collapses to one pill.
845
+ */
846
+ function extractPullRequests(text) {
847
+ const byKey = /* @__PURE__ */ new Map();
848
+ for (const match of text.matchAll(PR_URL_RE)) {
849
+ const owner = match[1];
850
+ const repo = match[2];
851
+ const numberText = match[3];
852
+ if (!owner || !repo || !numberText) continue;
853
+ const number = Number.parseInt(numberText, 10);
854
+ if (!Number.isFinite(number) || number <= 0) continue;
855
+ const key = `${owner.toLowerCase()}/${repo.toLowerCase()}#${number}`;
856
+ if (byKey.has(key)) continue;
857
+ byKey.set(key, {
858
+ number,
859
+ key,
860
+ url: `https://github.com/${owner}/${repo}/pull/${number}`
861
+ });
862
+ }
863
+ return [...byKey.values()];
864
+ }
865
+ /**
866
+ * A streaming PR scanner: fed `text-delta` chunks as a run streams, it reports
867
+ * each distinct PR the moment its URL completes, exactly once, without ever
868
+ * rescanning the transcript. It accumulates text per part id (a URL can split
869
+ * across two deltas) and only scans the current part's buffer, so cost is
870
+ * bounded by one text block, not the whole scrollback.
871
+ *
872
+ * `onPullRequest` fires once per never-before-seen PR number for the life of
873
+ * the scanner. Construct one per run (or per conversation) and feed it the
874
+ * same `text-delta` chunks the transcript reducer sees.
875
+ */
876
+ function createPullRequestScanner(onPullRequest) {
877
+ let text = "";
878
+ let lastPartId;
879
+ const seen = /* @__PURE__ */ new Set();
880
+ return { addChunk(chunk) {
881
+ if (chunk["type"] !== "text-delta") return;
882
+ const delta = typeof chunk["delta"] === "string" ? chunk["delta"] : typeof chunk["text"] === "string" ? chunk["text"] : "";
883
+ if (!delta) return;
884
+ const partId = typeof chunk["id"] === "string" ? chunk["id"] : void 0;
885
+ if (partId && lastPartId && partId !== lastPartId) text = "";
886
+ text += delta;
887
+ lastPartId = partId ?? lastPartId;
888
+ if (!delta.includes("/pull/") && !/\/\d/.test(delta)) return;
889
+ for (const pr of extractPullRequests(text)) {
890
+ if (seen.has(pr.key)) continue;
891
+ seen.add(pr.key);
892
+ onPullRequest(pr);
893
+ }
894
+ } };
895
+ }
896
+
703
897
  //#endregion
704
898
  //#region src/chat/clipboard.ts
705
899
  /** Spawns a command with no stdin and collects its stdout. Resolves (never
@@ -1072,19 +1266,42 @@ function windowStart(highlight, total, visible) {
1072
1266
 
1073
1267
  //#endregion
1074
1268
  //#region src/chat/tui/fuzzy.ts
1269
+ function keyAccessor(key) {
1270
+ return typeof key === "function" ? key : key.get;
1271
+ }
1272
+ function keyWeight(key) {
1273
+ return typeof key === "function" ? 1 : key.weight ?? 1;
1274
+ }
1075
1275
  /**
1076
1276
  * Rank `items` against `query` across multiple keys with fuzzysort — the
1077
1277
  * OpenCode pattern. An empty/whitespace query returns everything in the
1078
1278
  * caller's original order (fuzzysort has nothing to rank); otherwise items
1079
1279
  * where no key matches are dropped and the rest come back best-first.
1280
+ *
1281
+ * fuzzysort's default multi-key score is the *best* single key's raw score,
1282
+ * with every key weighted equally — so an exact word sitting in a low-priority
1283
+ * field (a message preview, a co-participant's name) beats a genuine but
1284
+ * slightly-fuzzier match in the title. When keys carry weights we instead rank
1285
+ * by the best *weighted* key score, so a title match wins ties (and near-ties)
1286
+ * against secondary fields. Per-key highlight indexes are unchanged; only the
1287
+ * ordering shifts.
1080
1288
  */
1081
1289
  function fuzzyFilter(query, items, keys) {
1082
1290
  const needle = query.trim();
1291
+ const accessors = keys.map(keyAccessor);
1083
1292
  if (!needle) return items.map((item) => ({
1084
1293
  item,
1085
1294
  highlights: keys.map(() => null)
1086
1295
  }));
1087
- return fuzzysort.go(needle, items, { keys: [...keys] }).map((result) => ({
1296
+ const weights = keys.map(keyWeight);
1297
+ const weighted = weights.some((w) => w !== 1);
1298
+ return fuzzysort.go(needle, items, {
1299
+ keys: accessors,
1300
+ ...weighted ? { scoreFn: (r) => Math.max(...weights.map((w, i) => {
1301
+ const keyResult = r[i];
1302
+ return keyResult ? keyResult.score * w : 0;
1303
+ })) } : {}
1304
+ }).map((result) => ({
1088
1305
  item: result.obj,
1089
1306
  highlights: keys.map((_key, i) => {
1090
1307
  const indexes = result[i]?.indexes;
@@ -1414,9 +1631,18 @@ function FilterableAgentList({ agents, scope, onPick }) {
1414
1631
  });
1415
1632
  }
1416
1633
  const agentKeys = [
1417
- (a) => a.name,
1418
- (a) => a.slug ?? "",
1419
- (a) => a.title ?? ""
1634
+ {
1635
+ get: (a) => a.name,
1636
+ weight: 1
1637
+ },
1638
+ {
1639
+ get: (a) => a.slug ?? "",
1640
+ weight: .6
1641
+ },
1642
+ {
1643
+ get: (a) => a.title ?? "",
1644
+ weight: .4
1645
+ }
1420
1646
  ];
1421
1647
 
1422
1648
  //#endregion
@@ -2278,9 +2504,18 @@ function conversationAgents(c) {
2278
2504
  return c.agents && c.agents.length > 0 ? c.agents : [c.agent];
2279
2505
  }
2280
2506
  const conversationKeys = [
2281
- (c) => singleLine(c.title ?? ""),
2282
- (c) => c.preview ?? "",
2283
- (c) => conversationAgents(c).map((a) => a.name).join(" ")
2507
+ {
2508
+ get: (c) => singleLine(c.title ?? ""),
2509
+ weight: 1
2510
+ },
2511
+ {
2512
+ get: (c) => c.preview ?? "",
2513
+ weight: .6
2514
+ },
2515
+ {
2516
+ get: (c) => conversationAgents(c).map((a) => a.name).join(" "),
2517
+ weight: .4
2518
+ }
2284
2519
  ];
2285
2520
  /**
2286
2521
  * Character indexes of the first case-insensitive occurrence of `needle` in
@@ -4595,6 +4830,50 @@ function TodoCardView({ todos, isActive }) {
4595
4830
  });
4596
4831
  }
4597
4832
 
4833
+ //#endregion
4834
+ //#region src/chat/tui/chat/pr-links.tsx
4835
+ /**
4836
+ * Most PRs to show inline before collapsing the rest into a `+N` count. The
4837
+ * chip shares one row with the send/steer hint, so more than a couple would
4838
+ * crowd or wrap that row; the newest few are the useful ones.
4839
+ */
4840
+ const MAX_PR_CHIPS = 3;
4841
+ /**
4842
+ * Builds the styled `· PR #123 #124` chip as OpenTUI chunks. The number chunks
4843
+ * carry a first-class `link` (OpenTUI emits it as a zero-width OSC 8 hyperlink,
4844
+ * clickable in terminals that support it) — the same mechanism the transcript's
4845
+ * linkified markdown uses. We do NOT hand-roll OSC 8 escape strings into text
4846
+ * children: OpenTUI measures width by string length and emits its own SGR, so a
4847
+ * raw escape corrupts layout and leaks bytes on screen.
4848
+ *
4849
+ * Caps at {@link MAX_PR_CHIPS} shown; any beyond that collapse into a trailing
4850
+ * `+N` so the footer row can't be flooded by a run that links many PRs. Shows
4851
+ * the most recent (last-seen) PRs, since those are what's in play.
4852
+ */
4853
+ function prLinksStyledText(prs) {
4854
+ const dot = fg(theme.dim ?? "#565f89");
4855
+ const label = fg(theme.muted ?? "#7a7a7a");
4856
+ const accent = fg(theme.accent ?? "#7aa2f7");
4857
+ const shown = prs.slice(-MAX_PR_CHIPS);
4858
+ const overflow = prs.length - shown.length;
4859
+ const chunks = [dot("· "), label("PR ")];
4860
+ shown.forEach((pr, i) => {
4861
+ if (i > 0) chunks.push(label(" "));
4862
+ chunks.push(accent(link(pr.url)(`#${pr.number}`)));
4863
+ });
4864
+ if (overflow > 0) chunks.push(label(` +${overflow}`));
4865
+ return new StyledText(chunks);
4866
+ }
4867
+ /**
4868
+ * An inline chip for the conversation's linked PR(s), rendered on the
4869
+ * composer's status/hint row just after the send/steer hint — mirrors the
4870
+ * Claude Code affordance. Renders nothing when there are no PRs.
4871
+ */
4872
+ function PrLinksView({ prs }) {
4873
+ if (prs.length === 0) return null;
4874
+ return /* @__PURE__ */ jsx("text", { content: prLinksStyledText(prs) });
4875
+ }
4876
+
4598
4877
  //#endregion
4599
4878
  //#region src/chat/tui/chat/use-compaction.ts
4600
4879
  const COMPACTING_INDICATOR_TIMEOUT_MS = 12e4;
@@ -5960,9 +6239,18 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
5960
6239
  });
5961
6240
  }
5962
6241
  const modelKeys = [
5963
- (m) => m.displayName,
5964
- (m) => m.providerDisplay ?? "",
5965
- (m) => m.id
6242
+ {
6243
+ get: (m) => m.displayName,
6244
+ weight: 1
6245
+ },
6246
+ {
6247
+ get: (m) => m.providerDisplay ?? "",
6248
+ weight: .9
6249
+ },
6250
+ {
6251
+ get: (m) => m.id,
6252
+ weight: .9
6253
+ }
5966
6254
  ];
5967
6255
  /**
5968
6256
  * The REST error routes return `{ "error": "message" }`. Pull that out for a
@@ -8893,6 +9181,12 @@ function ChatScreen({ agent, conversation }) {
8893
9181
  agentHost.transition({ state: "idle" });
8894
9182
  agentHost.setSession(null);
8895
9183
  }, [agentHost]);
9184
+ const [prLinks, setPrLinks] = useState([]);
9185
+ const reportedPrsRef = useRef(/* @__PURE__ */ new Map());
9186
+ useEffect(() => () => {
9187
+ for (const pr of reportedPrsRef.current.values()) agentHost.removePullRequest(pr);
9188
+ reportedPrsRef.current = /* @__PURE__ */ new Map();
9189
+ }, [agentHost]);
8896
9190
  useEffect(() => {
8897
9191
  agentHost.setSession(conversationId);
8898
9192
  setResumeConversation(conversationId);
@@ -8995,6 +9289,15 @@ function ChatScreen({ agent, conversation }) {
8995
9289
  text: recapText
8996
9290
  });
8997
9291
  setItems(loaded);
9292
+ for (const item of loaded) {
9293
+ if (item.kind !== "assistant-text") continue;
9294
+ for (const pr of extractPullRequests(item.text)) {
9295
+ if (reportedPrsRef.current.has(pr.key)) continue;
9296
+ reportedPrsRef.current.set(pr.key, pr);
9297
+ setPrLinks((prev) => [...prev, pr]);
9298
+ agentHost.addPullRequest(pr);
9299
+ }
9300
+ }
8998
9301
  setTodos(latestTodosFromHistory(messages));
8999
9302
  } catch (err) {
9000
9303
  if (cancelled) return;
@@ -9010,12 +9313,22 @@ function ChatScreen({ agent, conversation }) {
9010
9313
  return () => {
9011
9314
  cancelled = true;
9012
9315
  };
9013
- }, [rest, initialConversationId]);
9316
+ }, [
9317
+ rest,
9318
+ initialConversationId,
9319
+ agentHost
9320
+ ]);
9014
9321
  const attachToRun = useCallback((runId, runAgentName, runStartedAtMs) => {
9015
9322
  if (!rest) return;
9016
9323
  seenRunsRef.current.add(runId);
9017
9324
  const abort = new AbortController();
9018
9325
  const responsePreview = createResponsePreview();
9326
+ const prScanner = createPullRequestScanner((pr) => {
9327
+ if (reportedPrsRef.current.has(pr.key)) return;
9328
+ reportedPrsRef.current.set(pr.key, pr);
9329
+ setPrLinks((prev) => [...prev, pr]);
9330
+ agentHost.addPullRequest(pr);
9331
+ });
9019
9332
  const streamAgentName = runAgentName ?? agent.name;
9020
9333
  setRun((prev) => ({
9021
9334
  kind: "streaming",
@@ -9029,6 +9342,7 @@ function ChatScreen({ agent, conversation }) {
9029
9342
  onEvent: (event) => {
9030
9343
  if (event.kind === "chunk") {
9031
9344
  responsePreview.addChunk(event.chunk);
9345
+ prScanner.addChunk(event.chunk);
9032
9346
  if (event.chunk.type === "data-anyone-todos-updated") {
9033
9347
  const next = todosFromPayload(event.chunk.data);
9034
9348
  if (next) setTodos(next.length > 0 ? next : null);
@@ -10584,6 +10898,7 @@ function ChatScreen({ agent, conversation }) {
10584
10898
  fg: theme.muted,
10585
10899
  children: statusText
10586
10900
  }),
10901
+ prLinks.length > 0 ? /* @__PURE__ */ jsx(PrLinksView, { prs: prLinks }) : null,
10587
10902
  /* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
10588
10903
  /* @__PURE__ */ jsx("text", {
10589
10904
  fg: theme.dim,
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.2.0-beta.608";
11
+ var version$1 = "0.2.0-beta.613";
12
12
 
13
13
  //#endregion
14
14
  //#region src/config.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.2.0-beta.608",
3
+ "version": "0.2.0-beta.613",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",