skydive-cli 0.2.0-beta.610 → 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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)
|
|
695
|
-
socket
|
|
696
|
-
|
|
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
|
|
@@ -4636,6 +4830,50 @@ function TodoCardView({ todos, isActive }) {
|
|
|
4636
4830
|
});
|
|
4637
4831
|
}
|
|
4638
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
|
+
|
|
4639
4877
|
//#endregion
|
|
4640
4878
|
//#region src/chat/tui/chat/use-compaction.ts
|
|
4641
4879
|
const COMPACTING_INDICATOR_TIMEOUT_MS = 12e4;
|
|
@@ -8943,6 +9181,12 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8943
9181
|
agentHost.transition({ state: "idle" });
|
|
8944
9182
|
agentHost.setSession(null);
|
|
8945
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]);
|
|
8946
9190
|
useEffect(() => {
|
|
8947
9191
|
agentHost.setSession(conversationId);
|
|
8948
9192
|
setResumeConversation(conversationId);
|
|
@@ -9045,6 +9289,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9045
9289
|
text: recapText
|
|
9046
9290
|
});
|
|
9047
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
|
+
}
|
|
9048
9301
|
setTodos(latestTodosFromHistory(messages));
|
|
9049
9302
|
} catch (err) {
|
|
9050
9303
|
if (cancelled) return;
|
|
@@ -9060,12 +9313,22 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9060
9313
|
return () => {
|
|
9061
9314
|
cancelled = true;
|
|
9062
9315
|
};
|
|
9063
|
-
}, [
|
|
9316
|
+
}, [
|
|
9317
|
+
rest,
|
|
9318
|
+
initialConversationId,
|
|
9319
|
+
agentHost
|
|
9320
|
+
]);
|
|
9064
9321
|
const attachToRun = useCallback((runId, runAgentName, runStartedAtMs) => {
|
|
9065
9322
|
if (!rest) return;
|
|
9066
9323
|
seenRunsRef.current.add(runId);
|
|
9067
9324
|
const abort = new AbortController();
|
|
9068
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
|
+
});
|
|
9069
9332
|
const streamAgentName = runAgentName ?? agent.name;
|
|
9070
9333
|
setRun((prev) => ({
|
|
9071
9334
|
kind: "streaming",
|
|
@@ -9079,6 +9342,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9079
9342
|
onEvent: (event) => {
|
|
9080
9343
|
if (event.kind === "chunk") {
|
|
9081
9344
|
responsePreview.addChunk(event.chunk);
|
|
9345
|
+
prScanner.addChunk(event.chunk);
|
|
9082
9346
|
if (event.chunk.type === "data-anyone-todos-updated") {
|
|
9083
9347
|
const next = todosFromPayload(event.chunk.data);
|
|
9084
9348
|
if (next) setTodos(next.length > 0 ? next : null);
|
|
@@ -10634,6 +10898,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
10634
10898
|
fg: theme.muted,
|
|
10635
10899
|
children: statusText
|
|
10636
10900
|
}),
|
|
10901
|
+
prLinks.length > 0 ? /* @__PURE__ */ jsx(PrLinksView, { prs: prLinks }) : null,
|
|
10637
10902
|
/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
|
|
10638
10903
|
/* @__PURE__ */ jsx("text", {
|
|
10639
10904
|
fg: theme.dim,
|