skydive-cli 0.5.0-beta.33 → 0.5.0-beta.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/js/bin.mjs +233 -187
- package/dist/js/{boot-C5HJfwRS.mjs → boot-CmInaUwJ.mjs} +922 -70
- package/dist/js/{client-BTQ1fwzM.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/{daemon-BPGZufS1.mjs → daemon-CH95gDij.mjs} +1 -1
- package/dist/js/{daemon-WQ8QYrFt.mjs → daemon-Dg89tpOE.mjs} +1 -1
- package/dist/js/{daemon-client-MpAGVkqP.mjs → daemon-client-BNDnEGw6.mjs} +1 -1
- package/dist/js/daemon-client-CC9sxWac.mjs +8 -0
- package/dist/js/{install-BPUAm8bS.mjs → install-aZ751iYf.mjs} +2 -2
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/{print-B9djx8GU.mjs → print-DV9L41G2.mjs} +2 -2
- package/dist/js/{print-Clgq46GU.mjs → print-h22UbIfZ.mjs} +1 -1
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-B9Bue8gg.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/{rest-C6E4mS2A.mjs → rest-ClVPZquN.mjs} +46 -9
- package/dist/js/{rest-CaWUSrnH.mjs → rest-v-7FQqLP.mjs} +1 -1
- package/package.json +11 -4
- package/dist/js/daemon-client-DTfIza0O.mjs +0 -8
- package/dist/js/raw-pty-g3kATVQe.mjs +0 -5
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-
|
|
3
|
-
import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-
|
|
2
|
+
import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-aZ751iYf.mjs";
|
|
3
|
+
import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-h22UbIfZ.mjs";
|
|
4
4
|
import { t as HttpError } from "./http-error-UHH3mVBF.mjs";
|
|
5
|
-
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-
|
|
5
|
+
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-ClVPZquN.mjs";
|
|
6
6
|
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-D3l5kJlX.mjs";
|
|
7
7
|
import { t as PortalClient } from "./client-d6K9qm6B.mjs";
|
|
8
|
-
import "./daemon-
|
|
8
|
+
import "./daemon-CH95gDij.mjs";
|
|
9
9
|
import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
|
|
10
10
|
import "./api-DQCaztBg.mjs";
|
|
11
|
-
import { t as SandboxStream } from "./client-
|
|
12
|
-
import { t as PortalDaemonClient } from "./daemon-client-
|
|
13
|
-
import { t as runRawPtyPassthrough } from "./raw-pty-
|
|
11
|
+
import { t as SandboxStream } from "./client-Btq6bMzX.mjs";
|
|
12
|
+
import { t as PortalDaemonClient } from "./daemon-client-BNDnEGw6.mjs";
|
|
13
|
+
import { t as runRawPtyPassthrough } from "./raw-pty-D5PhKZSl.mjs";
|
|
14
14
|
import * as os$1 from "node:os";
|
|
15
15
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
16
16
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
@@ -21,13 +21,13 @@ import { execFile, spawn } from "node:child_process";
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import { connect, createConnection } from "node:net";
|
|
23
23
|
import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
24
25
|
import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, addDefaultParsers, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
|
|
25
26
|
import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
26
27
|
import { Profiler, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
27
28
|
import { create } from "zustand";
|
|
28
29
|
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
29
30
|
import fuzzysort from "fuzzysort";
|
|
30
|
-
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { fileTypeFromBuffer } from "file-type";
|
|
32
32
|
import { structuredPatch } from "diff";
|
|
33
33
|
|
|
@@ -269,6 +269,7 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
269
269
|
return {
|
|
270
270
|
kind: "chat",
|
|
271
271
|
agent,
|
|
272
|
+
attachRunId: null,
|
|
272
273
|
conversation: {
|
|
273
274
|
id: conversation.id,
|
|
274
275
|
title: conversation.title,
|
|
@@ -313,6 +314,7 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
313
314
|
if (newConversation) return {
|
|
314
315
|
kind: "chat",
|
|
315
316
|
agent,
|
|
317
|
+
attachRunId: null,
|
|
316
318
|
conversation: {
|
|
317
319
|
kind: "new",
|
|
318
320
|
agentId: agent.id,
|
|
@@ -350,6 +352,7 @@ function newChat(agent) {
|
|
|
350
352
|
return {
|
|
351
353
|
kind: "chat",
|
|
352
354
|
agent,
|
|
355
|
+
attachRunId: null,
|
|
353
356
|
conversation: {
|
|
354
357
|
kind: "new",
|
|
355
358
|
agentId: agent.id,
|
|
@@ -2005,6 +2008,7 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
2005
2008
|
onPick(store.seedPrompt !== null || store.newConversationIntent ? {
|
|
2006
2009
|
kind: "chat",
|
|
2007
2010
|
agent,
|
|
2011
|
+
attachRunId: null,
|
|
2008
2012
|
conversation: {
|
|
2009
2013
|
kind: "new",
|
|
2010
2014
|
agentId: agent.id,
|
|
@@ -2152,6 +2156,7 @@ function AgentCreateScreen() {
|
|
|
2152
2156
|
goTo({
|
|
2153
2157
|
kind: "chat",
|
|
2154
2158
|
agent,
|
|
2159
|
+
attachRunId: null,
|
|
2155
2160
|
conversation: {
|
|
2156
2161
|
kind: "new",
|
|
2157
2162
|
agentId: agent.id,
|
|
@@ -2415,6 +2420,7 @@ function FirstRunScreen() {
|
|
|
2415
2420
|
goTo({
|
|
2416
2421
|
kind: "chat",
|
|
2417
2422
|
agent,
|
|
2423
|
+
attachRunId: null,
|
|
2418
2424
|
conversation: {
|
|
2419
2425
|
kind: "new",
|
|
2420
2426
|
agentId: agent.id,
|
|
@@ -2422,11 +2428,12 @@ function FirstRunScreen() {
|
|
|
2422
2428
|
}
|
|
2423
2429
|
});
|
|
2424
2430
|
};
|
|
2425
|
-
const openOnboardingChat = (agent, conversationId, title) => {
|
|
2431
|
+
const openOnboardingChat = (agent, conversationId, title, attachRunId) => {
|
|
2426
2432
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2427
2433
|
goTo({
|
|
2428
2434
|
kind: "chat",
|
|
2429
2435
|
agent,
|
|
2436
|
+
attachRunId,
|
|
2430
2437
|
conversation: {
|
|
2431
2438
|
id: conversationId,
|
|
2432
2439
|
title,
|
|
@@ -2450,7 +2457,7 @@ function FirstRunScreen() {
|
|
|
2450
2457
|
const { portalClient } = useStore.getState();
|
|
2451
2458
|
try {
|
|
2452
2459
|
const os = machineOsFromPlatform(process.platform);
|
|
2453
|
-
const { conversationId } = await rest.createOnboardingConversation({
|
|
2460
|
+
const { conversationId, runId } = await rest.createOnboardingConversation({
|
|
2454
2461
|
agentId: agent.id,
|
|
2455
2462
|
projectDir: process.cwd(),
|
|
2456
2463
|
os
|
|
@@ -2464,7 +2471,7 @@ function FirstRunScreen() {
|
|
|
2464
2471
|
});
|
|
2465
2472
|
});
|
|
2466
2473
|
}
|
|
2467
|
-
openOnboardingChat(agent, conversationId, `Onboarding ${agent.name}
|
|
2474
|
+
openOnboardingChat(agent, conversationId, `Onboarding ${agent.name}`, runId);
|
|
2468
2475
|
} catch (err) {
|
|
2469
2476
|
if (err instanceof HttpError && err.status === 401) {
|
|
2470
2477
|
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
@@ -3215,6 +3222,7 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3215
3222
|
if (row.kind === "new") onPick({
|
|
3216
3223
|
kind: "chat",
|
|
3217
3224
|
agent,
|
|
3225
|
+
attachRunId: null,
|
|
3218
3226
|
conversation: {
|
|
3219
3227
|
kind: "new",
|
|
3220
3228
|
agentId: agent.id,
|
|
@@ -3224,6 +3232,7 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3224
3232
|
else onPick({
|
|
3225
3233
|
kind: "chat",
|
|
3226
3234
|
agent,
|
|
3235
|
+
attachRunId: null,
|
|
3227
3236
|
conversation: row.hit.item
|
|
3228
3237
|
});
|
|
3229
3238
|
};
|
|
@@ -4900,8 +4909,6 @@ function toGauges(stats) {
|
|
|
4900
4909
|
function renderBar(fraction, width) {
|
|
4901
4910
|
const cells = Math.max(1, width);
|
|
4902
4911
|
const exact = Math.min(1, Math.max(0, fraction)) * cells;
|
|
4903
|
-
const full = Math.floor(exact);
|
|
4904
|
-
const remainder = exact - full;
|
|
4905
4912
|
const eighths = [
|
|
4906
4913
|
"",
|
|
4907
4914
|
"▏",
|
|
@@ -4912,7 +4919,12 @@ function renderBar(fraction, width) {
|
|
|
4912
4919
|
"▊",
|
|
4913
4920
|
"▉"
|
|
4914
4921
|
];
|
|
4915
|
-
|
|
4922
|
+
let full = Math.floor(exact);
|
|
4923
|
+
let partialIndex = Math.round((exact - full) * 8);
|
|
4924
|
+
if (partialIndex === 8) {
|
|
4925
|
+
full += 1;
|
|
4926
|
+
partialIndex = 0;
|
|
4927
|
+
}
|
|
4916
4928
|
const partial = full < cells ? eighths[partialIndex] : "";
|
|
4917
4929
|
const filled = "█".repeat(Math.min(full, cells)) + partial;
|
|
4918
4930
|
return (filled + "░".repeat(Math.max(0, cells - filled.length))).slice(0, cells);
|
|
@@ -6637,6 +6649,438 @@ function SubagentCardView({ items, isActive, onHeightChange, onBatchLiveChange }
|
|
|
6637
6649
|
});
|
|
6638
6650
|
}
|
|
6639
6651
|
|
|
6652
|
+
//#endregion
|
|
6653
|
+
//#region src/chat/tui/chat/bg-tasks.ts
|
|
6654
|
+
const FINISHED_GRACE_MS = 3e4;
|
|
6655
|
+
const MAX_RETAINED_FINISHED = 10;
|
|
6656
|
+
const bgTaskSchema = z.object({
|
|
6657
|
+
id: z.string(),
|
|
6658
|
+
command: z.string(),
|
|
6659
|
+
description: z.string().nullable(),
|
|
6660
|
+
startedAt: z.number(),
|
|
6661
|
+
state: z.enum(["running", "finished"]),
|
|
6662
|
+
exitCode: z.number().nullable(),
|
|
6663
|
+
killedReason: z.string().nullable(),
|
|
6664
|
+
outputTail: z.string()
|
|
6665
|
+
});
|
|
6666
|
+
const bgTasksPayloadSchema = z.object({ tasks: z.array(bgTaskSchema) });
|
|
6667
|
+
/**
|
|
6668
|
+
* The tasks carried by a `data-anyone-bg-tasks-updated` chunk/part `data`
|
|
6669
|
+
* payload, or null when the payload isn't the documented shape. The live stream
|
|
6670
|
+
* (chat.tsx) is the only producer today; history never carries these (bg tasks
|
|
6671
|
+
* are ephemeral live state, not persisted like todos).
|
|
6672
|
+
*/
|
|
6673
|
+
function bgTasksFromPayload(data) {
|
|
6674
|
+
const parsed = bgTasksPayloadSchema.safeParse(data);
|
|
6675
|
+
return parsed.success ? parsed.data.tasks : null;
|
|
6676
|
+
}
|
|
6677
|
+
/** The running subset — what the pinned pill counts as live shells. */
|
|
6678
|
+
function runningTasks(tasks) {
|
|
6679
|
+
return tasks.filter((t) => t.state === "running");
|
|
6680
|
+
}
|
|
6681
|
+
/** Retained-finished subset: finished tasks still inside the grace window. */
|
|
6682
|
+
function finishedTasks(tasks) {
|
|
6683
|
+
return tasks.filter((t) => t.state === "finished");
|
|
6684
|
+
}
|
|
6685
|
+
/**
|
|
6686
|
+
* Merge a fresh server snapshot into the client's current task list, keeping
|
|
6687
|
+
* finished shells around for a short grace window so one that exits while you
|
|
6688
|
+
* glanced away isn't gone instantly (Claude Code's PANEL_GRACE_MS behavior).
|
|
6689
|
+
*
|
|
6690
|
+
* The harness sends only live/just-changed tasks and drops a finished task from
|
|
6691
|
+
* its snapshot soon after; so we can't mirror the snapshot 1:1 or finished
|
|
6692
|
+
* shells vanish immediately. Instead:
|
|
6693
|
+
* - every task in the snapshot is taken as-is (running tasks, and the one
|
|
6694
|
+
* finished frame the harness emits on completion);
|
|
6695
|
+
* - a task the client had that is now ABSENT from the snapshot, and was
|
|
6696
|
+
* running, is carried over as finished (the harness dropped it before we saw
|
|
6697
|
+
* a finished frame — treat disappearance as completion);
|
|
6698
|
+
* - each finished task is stamped `finishedAt` once (first time we see it
|
|
6699
|
+
* finished), and evicted `FINISHED_GRACE_MS` after that;
|
|
6700
|
+
* - finished tasks are capped to the `MAX_RETAINED_FINISHED` most recent.
|
|
6701
|
+
*
|
|
6702
|
+
* Pure and deterministic given `now`, so it unit-tests without timers.
|
|
6703
|
+
*/
|
|
6704
|
+
function mergeBgTasks(prev, snapshot, now) {
|
|
6705
|
+
const snapshotIds = new Set(snapshot.map((t) => t.id));
|
|
6706
|
+
const prevById = new Map(prev.map((t) => [t.id, t]));
|
|
6707
|
+
const stampFinished = (t) => {
|
|
6708
|
+
if (t.state !== "finished") return {
|
|
6709
|
+
...t,
|
|
6710
|
+
finishedAt: void 0
|
|
6711
|
+
};
|
|
6712
|
+
const existing = prevById.get(t.id)?.finishedAt;
|
|
6713
|
+
return {
|
|
6714
|
+
...t,
|
|
6715
|
+
finishedAt: existing ?? now
|
|
6716
|
+
};
|
|
6717
|
+
};
|
|
6718
|
+
const merged = snapshot.map(stampFinished);
|
|
6719
|
+
for (const old of prev) {
|
|
6720
|
+
if (snapshotIds.has(old.id)) continue;
|
|
6721
|
+
if (old.state === "finished") merged.push({
|
|
6722
|
+
...old,
|
|
6723
|
+
finishedAt: old.finishedAt ?? now
|
|
6724
|
+
});
|
|
6725
|
+
else merged.push({
|
|
6726
|
+
...old,
|
|
6727
|
+
state: "finished",
|
|
6728
|
+
finishedAt: now
|
|
6729
|
+
});
|
|
6730
|
+
}
|
|
6731
|
+
const running = merged.filter((t) => t.state === "running");
|
|
6732
|
+
const finished = merged.filter((t) => t.state === "finished").filter((t) => now - (t.finishedAt ?? now) < FINISHED_GRACE_MS).sort((a, b) => (b.finishedAt ?? 0) - (a.finishedAt ?? 0)).slice(0, MAX_RETAINED_FINISHED);
|
|
6733
|
+
return [...running, ...finished];
|
|
6734
|
+
}
|
|
6735
|
+
/**
|
|
6736
|
+
* Whether the client should keep re-rendering to age out finished shells. True
|
|
6737
|
+
* while any finished task is retained, so the chat screen can run a light timer
|
|
6738
|
+
* that re-merges an empty snapshot to evict them on schedule.
|
|
6739
|
+
*/
|
|
6740
|
+
function hasRetainedFinished(tasks) {
|
|
6741
|
+
return tasks.some((t) => t.state === "finished");
|
|
6742
|
+
}
|
|
6743
|
+
/**
|
|
6744
|
+
* A task's human label: the agent's description, else the command (the pill and
|
|
6745
|
+
* list rows show this). Never a bare id.
|
|
6746
|
+
*/
|
|
6747
|
+
function taskLabel(task) {
|
|
6748
|
+
const raw = task.description?.trim() || task.command.trim();
|
|
6749
|
+
return raw.length > 0 ? raw : task.id;
|
|
6750
|
+
}
|
|
6751
|
+
/**
|
|
6752
|
+
* Elapsed runtime as a compact `1h2m`, `3m40s`, `12s` string, computed from
|
|
6753
|
+
* `startedAt` and a caller-supplied `now` (so the overlay can tick it locally
|
|
6754
|
+
* without any server round-trip). Clamped at zero for a startedAt in the future
|
|
6755
|
+
* (clock skew).
|
|
6756
|
+
*/
|
|
6757
|
+
function formatRuntime(startedAt, now) {
|
|
6758
|
+
const totalSec = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
6759
|
+
const h = Math.floor(totalSec / 3600);
|
|
6760
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
6761
|
+
const s = totalSec % 60;
|
|
6762
|
+
if (h > 0) return `${h}h${m}m`;
|
|
6763
|
+
if (m > 0) return `${m}m${s}s`;
|
|
6764
|
+
return `${s}s`;
|
|
6765
|
+
}
|
|
6766
|
+
/**
|
|
6767
|
+
* A finished task's terminal state as a short phrase for the list/detail:
|
|
6768
|
+
* `exit 0`, `exit 1`, `killed`, or `killed: <reason>`. A running task returns
|
|
6769
|
+
* `running`.
|
|
6770
|
+
*/
|
|
6771
|
+
function stateLabel(task) {
|
|
6772
|
+
if (task.state === "running") return "running";
|
|
6773
|
+
if (task.killedReason) return `killed: ${task.killedReason}`;
|
|
6774
|
+
if (task.exitCode !== null) return `exit ${task.exitCode}`;
|
|
6775
|
+
return "ended";
|
|
6776
|
+
}
|
|
6777
|
+
/** True when a finished task ended non-zero or was killed — rendered in error. */
|
|
6778
|
+
function isFailure(task) {
|
|
6779
|
+
return task.state === "finished" && (task.killedReason !== null || task.exitCode !== null && task.exitCode !== 0);
|
|
6780
|
+
}
|
|
6781
|
+
/**
|
|
6782
|
+
* The pinned indicator's summary line. Counts running shells and, separately,
|
|
6783
|
+
* finished shells still inside the grace window: `2 shells`, `2 shells · 1
|
|
6784
|
+
* done`, or `1 done` when only retained-finished remain. Returns null when
|
|
6785
|
+
* nothing is running and nothing is retained, so the indicator renders no
|
|
6786
|
+
* chrome.
|
|
6787
|
+
*/
|
|
6788
|
+
function pillSummary(tasks) {
|
|
6789
|
+
const running = runningTasks(tasks).length;
|
|
6790
|
+
const done = finishedTasks(tasks).length;
|
|
6791
|
+
const parts = [];
|
|
6792
|
+
if (running > 0) parts.push(`${running} shell${running === 1 ? "" : "s"}`);
|
|
6793
|
+
if (done > 0) parts.push(`${done} done`);
|
|
6794
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
//#endregion
|
|
6798
|
+
//#region src/chat/tui/chat/bg-tasks-chip.tsx
|
|
6799
|
+
/**
|
|
6800
|
+
* The background-shells indicator on the composer's bottom status row, placed
|
|
6801
|
+
* just before the PR chip (`· 2 shells · PR #123`) — the TUI analog of Claude
|
|
6802
|
+
* Code's shell count. Shows only while a shell is running; `ctrl+b` opens the
|
|
6803
|
+
* overlay (the binding lives in the chat screen). Renders nothing when nothing
|
|
6804
|
+
* is running, so an idle conversation shows no chrome for it.
|
|
6805
|
+
*
|
|
6806
|
+
* A plain `<text>` on the shared status row (not a pinned box above the
|
|
6807
|
+
* composer), matching PrLinksView — the `· ` dot separates it from the status
|
|
6808
|
+
* text to its left, and the PR chip's own leading `· ` separates it on the
|
|
6809
|
+
* right.
|
|
6810
|
+
*/
|
|
6811
|
+
function BgShellsChip({ tasks, onOpen }) {
|
|
6812
|
+
const summary = pillSummary(tasks);
|
|
6813
|
+
if (!summary) return null;
|
|
6814
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6815
|
+
style: { flexDirection: "row" },
|
|
6816
|
+
onMouseDown: onOpen,
|
|
6817
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
6818
|
+
fg: theme.dim,
|
|
6819
|
+
children: "· "
|
|
6820
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
6821
|
+
fg: theme.accent,
|
|
6822
|
+
children: ["◍ ", summary]
|
|
6823
|
+
})]
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
|
|
6827
|
+
//#endregion
|
|
6828
|
+
//#region src/chat/tui/screens/bg-tasks-overlay.tsx
|
|
6829
|
+
/**
|
|
6830
|
+
* The background-tasks overlay — the TUI analog of Claude Code's shells panel.
|
|
6831
|
+
* Rendered in place of the transcript+composer (like the model picker) while
|
|
6832
|
+
* the chat screen stays mounted, so the conversation and any live run survive.
|
|
6833
|
+
*
|
|
6834
|
+
* Two views:
|
|
6835
|
+
* - **list**: `Background` header + `N active shells`, one selectable row per
|
|
6836
|
+
* task (running first). ↑/↓ select, Enter opens detail, Esc closes.
|
|
6837
|
+
* - **detail**: one shell's Status / Runtime / Command / Output.
|
|
6838
|
+
* ← / Esc back to the list (or closes when we opened straight to detail).
|
|
6839
|
+
*
|
|
6840
|
+
* Claude-Code parity: when exactly one shell is running we skip the list and
|
|
6841
|
+
* open that shell's detail directly; ← from there closes the overlay rather
|
|
6842
|
+
* than dropping into a one-row list.
|
|
6843
|
+
*
|
|
6844
|
+
* View-only for now — `x to stop` needs a control channel back to the sandbox
|
|
6845
|
+
* (a follow-up), so we don't advertise or handle it here.
|
|
6846
|
+
*/
|
|
6847
|
+
function BgTasksOverlay({ tasks, onClose }) {
|
|
6848
|
+
const ordered = orderTasks(tasks);
|
|
6849
|
+
const running = runningTasks(ordered);
|
|
6850
|
+
const singleRunning = running.length === 1 ? running[0] ?? null : null;
|
|
6851
|
+
const [view, setView] = useState(singleRunning ? {
|
|
6852
|
+
kind: "detail",
|
|
6853
|
+
id: singleRunning.id,
|
|
6854
|
+
openedDirect: true
|
|
6855
|
+
} : { kind: "list" });
|
|
6856
|
+
const [selected, setSelected] = useState(0);
|
|
6857
|
+
const detailTask = view.kind === "detail" ? ordered.find((t) => t.id === view.id) ?? null : null;
|
|
6858
|
+
useEffect(() => {
|
|
6859
|
+
if (view.kind === "detail" && !detailTask) setView({ kind: "list" });
|
|
6860
|
+
}, [view, detailTask]);
|
|
6861
|
+
useEffect(() => {
|
|
6862
|
+
if (ordered.length === 0) onClose();
|
|
6863
|
+
}, [ordered.length, onClose]);
|
|
6864
|
+
if (view.kind === "detail" && detailTask) return /* @__PURE__ */ jsx(BgTaskDetail, {
|
|
6865
|
+
task: detailTask,
|
|
6866
|
+
onBack: () => {
|
|
6867
|
+
if (view.openedDirect) onClose();
|
|
6868
|
+
else setView({ kind: "list" });
|
|
6869
|
+
},
|
|
6870
|
+
onClose
|
|
6871
|
+
});
|
|
6872
|
+
return /* @__PURE__ */ jsx(BgTaskList, {
|
|
6873
|
+
tasks: ordered,
|
|
6874
|
+
runningCount: running.length,
|
|
6875
|
+
selected,
|
|
6876
|
+
onSelectedChange: setSelected,
|
|
6877
|
+
onOpen: (id) => setView({
|
|
6878
|
+
kind: "detail",
|
|
6879
|
+
id,
|
|
6880
|
+
openedDirect: false
|
|
6881
|
+
}),
|
|
6882
|
+
onClose
|
|
6883
|
+
});
|
|
6884
|
+
}
|
|
6885
|
+
/** Running first, then finished; within each group most-recently-started first. */
|
|
6886
|
+
function orderTasks(tasks) {
|
|
6887
|
+
return [...tasks].sort((a, b) => {
|
|
6888
|
+
const ar = a.state === "running" ? 0 : 1;
|
|
6889
|
+
const br = b.state === "running" ? 0 : 1;
|
|
6890
|
+
if (ar !== br) return ar - br;
|
|
6891
|
+
return b.startedAt - a.startedAt;
|
|
6892
|
+
});
|
|
6893
|
+
}
|
|
6894
|
+
function BgTaskList({ tasks, runningCount, selected, onSelectedChange, onOpen, onClose }) {
|
|
6895
|
+
const { height } = useTerminalDimensions();
|
|
6896
|
+
const now = useNow();
|
|
6897
|
+
const clamped = Math.min(selected, Math.max(0, tasks.length - 1));
|
|
6898
|
+
useKeyboard((key) => {
|
|
6899
|
+
if (key.name === "escape") {
|
|
6900
|
+
onClose();
|
|
6901
|
+
return;
|
|
6902
|
+
}
|
|
6903
|
+
if (key.name === "up") onSelectedChange(Math.max(0, clamped - 1));
|
|
6904
|
+
else if (key.name === "down") onSelectedChange(Math.min(Math.max(0, tasks.length - 1), clamped + 1));
|
|
6905
|
+
else if (key.name === "return") {
|
|
6906
|
+
const task = tasks[clamped];
|
|
6907
|
+
if (task) onOpen(task.id);
|
|
6908
|
+
}
|
|
6909
|
+
});
|
|
6910
|
+
const visibleRows = Math.max(3, height - 6);
|
|
6911
|
+
const start = windowStart(clamped, tasks.length, visibleRows);
|
|
6912
|
+
const windowed = tasks.slice(start, start + visibleRows);
|
|
6913
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6914
|
+
style: {
|
|
6915
|
+
flexDirection: "column",
|
|
6916
|
+
flexGrow: 1
|
|
6917
|
+
},
|
|
6918
|
+
children: [
|
|
6919
|
+
/* @__PURE__ */ jsx("text", {
|
|
6920
|
+
fg: theme.accent,
|
|
6921
|
+
children: /* @__PURE__ */ jsx("b", { children: "Background" })
|
|
6922
|
+
}),
|
|
6923
|
+
/* @__PURE__ */ jsxs("text", {
|
|
6924
|
+
fg: theme.muted,
|
|
6925
|
+
children: [
|
|
6926
|
+
runningCount,
|
|
6927
|
+
" active shell",
|
|
6928
|
+
runningCount === 1 ? "" : "s",
|
|
6929
|
+
tasks.length > runningCount ? ` · ${tasks.length - runningCount} finished` : ""
|
|
6930
|
+
]
|
|
6931
|
+
}),
|
|
6932
|
+
/* @__PURE__ */ jsx("box", {
|
|
6933
|
+
style: {
|
|
6934
|
+
flexDirection: "column",
|
|
6935
|
+
flexGrow: 1,
|
|
6936
|
+
marginTop: 1
|
|
6937
|
+
},
|
|
6938
|
+
children: windowed.map((task, i) => {
|
|
6939
|
+
const isSel = start + i === clamped;
|
|
6940
|
+
const running = task.state === "running";
|
|
6941
|
+
const stateFg = running ? theme.accent : isFailure(task) ? theme.error : theme.dim;
|
|
6942
|
+
const rt = running ? formatRuntime(task.startedAt, now) : null;
|
|
6943
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6944
|
+
style: { flexDirection: "row" },
|
|
6945
|
+
children: [
|
|
6946
|
+
/* @__PURE__ */ jsx("text", {
|
|
6947
|
+
fg: isSel ? theme.accent : theme.dim,
|
|
6948
|
+
children: isSel ? "› " : " "
|
|
6949
|
+
}),
|
|
6950
|
+
/* @__PURE__ */ jsx("text", {
|
|
6951
|
+
fg: running ? theme.success : theme.dim,
|
|
6952
|
+
children: "❯ "
|
|
6953
|
+
}),
|
|
6954
|
+
/* @__PURE__ */ jsx("text", {
|
|
6955
|
+
fg: isSel ? theme.fg : theme.muted,
|
|
6956
|
+
children: clampLabel(taskLabel(task))
|
|
6957
|
+
}),
|
|
6958
|
+
/* @__PURE__ */ jsxs("text", {
|
|
6959
|
+
fg: stateFg,
|
|
6960
|
+
children: [
|
|
6961
|
+
" ",
|
|
6962
|
+
"(",
|
|
6963
|
+
running ? `running · ${rt}` : stateLabel(task),
|
|
6964
|
+
")"
|
|
6965
|
+
]
|
|
6966
|
+
})
|
|
6967
|
+
]
|
|
6968
|
+
}, task.id);
|
|
6969
|
+
})
|
|
6970
|
+
}),
|
|
6971
|
+
/* @__PURE__ */ jsx("text", {
|
|
6972
|
+
fg: theme.dim,
|
|
6973
|
+
children: "↑/↓ select · ↵ view · esc close"
|
|
6974
|
+
})
|
|
6975
|
+
]
|
|
6976
|
+
});
|
|
6977
|
+
}
|
|
6978
|
+
function BgTaskDetail({ task, onBack, onClose }) {
|
|
6979
|
+
const { height } = useTerminalDimensions();
|
|
6980
|
+
const now = useNow();
|
|
6981
|
+
useKeyboard((key) => {
|
|
6982
|
+
if (key.name === "left") {
|
|
6983
|
+
onBack();
|
|
6984
|
+
return;
|
|
6985
|
+
}
|
|
6986
|
+
if (key.name === "escape") onClose();
|
|
6987
|
+
});
|
|
6988
|
+
const running = task.state === "running";
|
|
6989
|
+
const status = running ? "running" : stateLabel(task);
|
|
6990
|
+
const statusFg = running ? theme.accent : isFailure(task) ? theme.error : theme.success;
|
|
6991
|
+
const outputRows = Math.max(3, height - 9);
|
|
6992
|
+
const outputLines = (task.outputTail || "(no output yet)").split("\n").slice(-outputRows);
|
|
6993
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
6994
|
+
style: {
|
|
6995
|
+
flexDirection: "column",
|
|
6996
|
+
flexGrow: 1
|
|
6997
|
+
},
|
|
6998
|
+
children: [
|
|
6999
|
+
/* @__PURE__ */ jsx("text", {
|
|
7000
|
+
fg: theme.accent,
|
|
7001
|
+
children: /* @__PURE__ */ jsx("b", { children: "Shell details" })
|
|
7002
|
+
}),
|
|
7003
|
+
/* @__PURE__ */ jsxs("box", {
|
|
7004
|
+
style: {
|
|
7005
|
+
flexDirection: "column",
|
|
7006
|
+
marginTop: 1
|
|
7007
|
+
},
|
|
7008
|
+
children: [
|
|
7009
|
+
/* @__PURE__ */ jsxs("box", {
|
|
7010
|
+
style: { flexDirection: "row" },
|
|
7011
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
7012
|
+
fg: theme.muted,
|
|
7013
|
+
children: "Status "
|
|
7014
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
7015
|
+
fg: statusFg,
|
|
7016
|
+
children: status
|
|
7017
|
+
})]
|
|
7018
|
+
}),
|
|
7019
|
+
/* @__PURE__ */ jsxs("box", {
|
|
7020
|
+
style: { flexDirection: "row" },
|
|
7021
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
7022
|
+
fg: theme.muted,
|
|
7023
|
+
children: "Runtime "
|
|
7024
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
7025
|
+
fg: theme.fg,
|
|
7026
|
+
children: formatRuntime(task.startedAt, now)
|
|
7027
|
+
})]
|
|
7028
|
+
}),
|
|
7029
|
+
/* @__PURE__ */ jsxs("box", {
|
|
7030
|
+
style: { flexDirection: "row" },
|
|
7031
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
7032
|
+
fg: theme.muted,
|
|
7033
|
+
children: "Command "
|
|
7034
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
7035
|
+
fg: theme.fg,
|
|
7036
|
+
children: clampCommand(task.command)
|
|
7037
|
+
})]
|
|
7038
|
+
})
|
|
7039
|
+
]
|
|
7040
|
+
}),
|
|
7041
|
+
/* @__PURE__ */ jsx("text", {
|
|
7042
|
+
fg: theme.muted,
|
|
7043
|
+
style: { marginTop: 1 },
|
|
7044
|
+
children: "Output"
|
|
7045
|
+
}),
|
|
7046
|
+
/* @__PURE__ */ jsx("box", {
|
|
7047
|
+
style: {
|
|
7048
|
+
flexDirection: "column",
|
|
7049
|
+
flexGrow: 1
|
|
7050
|
+
},
|
|
7051
|
+
children: outputLines.map((line, i) => /* @__PURE__ */ jsx("text", {
|
|
7052
|
+
fg: theme.dim,
|
|
7053
|
+
children: line.length > 0 ? line : " "
|
|
7054
|
+
}, i))
|
|
7055
|
+
}),
|
|
7056
|
+
/* @__PURE__ */ jsx("text", {
|
|
7057
|
+
fg: theme.dim,
|
|
7058
|
+
children: "← back · esc close"
|
|
7059
|
+
})
|
|
7060
|
+
]
|
|
7061
|
+
});
|
|
7062
|
+
}
|
|
7063
|
+
/**
|
|
7064
|
+
* A 1Hz clock so the running Runtime ticks while the overlay is open, without
|
|
7065
|
+
* the server re-emitting a snapshot for elapsed time.
|
|
7066
|
+
*/
|
|
7067
|
+
function useNow() {
|
|
7068
|
+
const [now, setNow] = useState(() => Date.now());
|
|
7069
|
+
useEffect(() => {
|
|
7070
|
+
const id = setInterval(() => setNow(Date.now()), 1e3);
|
|
7071
|
+
return () => clearInterval(id);
|
|
7072
|
+
}, []);
|
|
7073
|
+
return now;
|
|
7074
|
+
}
|
|
7075
|
+
function clampLabel(text) {
|
|
7076
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
7077
|
+
return oneLine.length > 60 ? `${oneLine.slice(0, 59)}…` : oneLine;
|
|
7078
|
+
}
|
|
7079
|
+
function clampCommand(text) {
|
|
7080
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
7081
|
+
return oneLine.length > 100 ? `${oneLine.slice(0, 99)}…` : oneLine;
|
|
7082
|
+
}
|
|
7083
|
+
|
|
6640
7084
|
//#endregion
|
|
6641
7085
|
//#region src/chat/tui/chat/pr-links.tsx
|
|
6642
7086
|
/**
|
|
@@ -6669,7 +7113,7 @@ function prLinksStyledText(prs) {
|
|
|
6669
7113
|
if (i > 0) chunks.push(label(" "));
|
|
6670
7114
|
chunks.push(accent(link(pr.url)(`#${pr.number}`)));
|
|
6671
7115
|
});
|
|
6672
|
-
if (overflow > 0) chunks.push(label(` +${overflow}`));
|
|
7116
|
+
if (overflow > 0) chunks.push(label(` +${overflow} (/prs)`));
|
|
6673
7117
|
return new StyledText(chunks);
|
|
6674
7118
|
}
|
|
6675
7119
|
/**
|
|
@@ -6764,9 +7208,14 @@ function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer
|
|
|
6764
7208
|
case "streaming": return hasPendingSteer ? `${connectHint}↵ steer · esc cancel steer · ctrl+c cancel run` : `${connectHint}↵ steer · esc leave (keeps running) · ctrl+c cancel`;
|
|
6765
7209
|
}
|
|
6766
7210
|
}
|
|
6767
|
-
/**
|
|
6768
|
-
|
|
6769
|
-
|
|
7211
|
+
/**
|
|
7212
|
+
* Keep model switching discoverable beside the current model. The effort is
|
|
7213
|
+
* appended only once something has chosen one — an agent with no stored level
|
|
7214
|
+
* runs at the platform default, and naming a level it never picked would be a
|
|
7215
|
+
* claim the footer can't back.
|
|
7216
|
+
*/
|
|
7217
|
+
function formatModelHint(model, effort) {
|
|
7218
|
+
return `ctrl+p · ${model ?? "default"}${effort ? ` · ${effort}` : ""}`;
|
|
6770
7219
|
}
|
|
6771
7220
|
|
|
6772
7221
|
//#endregion
|
|
@@ -6776,15 +7225,16 @@ function formatModelHint(model) {
|
|
|
6776
7225
|
* bar and footer can't fit, one aligned `label value` row each. Pure so the
|
|
6777
7226
|
* layout is testable without a renderer.
|
|
6778
7227
|
*/
|
|
6779
|
-
function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, share }) {
|
|
7228
|
+
function formatStatusInfo({ appUrl, workspaceName, agentName, conversationId, model, effort, share }) {
|
|
6780
7229
|
const rows = [
|
|
6781
7230
|
["app", appUrl ?? "unknown"],
|
|
6782
7231
|
["workspace", workspaceName ?? "unknown"],
|
|
6783
7232
|
["agent", agentName],
|
|
6784
7233
|
["conversation", conversationId ?? "new (created when you send the first message)"],
|
|
6785
|
-
["model", model ?? "default"]
|
|
6786
|
-
["local access", formatShare(share)]
|
|
7234
|
+
["model", model ?? "default"]
|
|
6787
7235
|
];
|
|
7236
|
+
if (effort) rows.push(["effort", effort]);
|
|
7237
|
+
rows.push(["local access", formatShare(share)]);
|
|
6788
7238
|
const width = Math.max(...rows.map(([label]) => label.length));
|
|
6789
7239
|
return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join("\n");
|
|
6790
7240
|
}
|
|
@@ -7735,6 +8185,13 @@ const slashCommands = [
|
|
|
7735
8185
|
description: "switch the model for the next run",
|
|
7736
8186
|
action: { kind: "model-picker" }
|
|
7737
8187
|
},
|
|
8188
|
+
{
|
|
8189
|
+
name: "effort",
|
|
8190
|
+
aliases: ["reasoning"],
|
|
8191
|
+
argsHint: null,
|
|
8192
|
+
description: "change the current model's reasoning effort",
|
|
8193
|
+
action: { kind: "effort-picker" }
|
|
8194
|
+
},
|
|
7738
8195
|
{
|
|
7739
8196
|
name: "theme",
|
|
7740
8197
|
aliases: [],
|
|
@@ -7742,6 +8199,13 @@ const slashCommands = [
|
|
|
7742
8199
|
description: "switch the color theme",
|
|
7743
8200
|
action: { kind: "theme-picker" }
|
|
7744
8201
|
},
|
|
8202
|
+
{
|
|
8203
|
+
name: "prs",
|
|
8204
|
+
aliases: ["pr"],
|
|
8205
|
+
argsHint: null,
|
|
8206
|
+
description: "list this conversation's linked PRs; open or copy one",
|
|
8207
|
+
action: { kind: "pr-picker" }
|
|
8208
|
+
},
|
|
7745
8209
|
{
|
|
7746
8210
|
name: "status",
|
|
7747
8211
|
aliases: [],
|
|
@@ -8051,19 +8515,65 @@ function waitingOn(card) {
|
|
|
8051
8515
|
}
|
|
8052
8516
|
}
|
|
8053
8517
|
|
|
8518
|
+
//#endregion
|
|
8519
|
+
//#region src/chat/tui/screens/model-effort.ts
|
|
8520
|
+
/**
|
|
8521
|
+
* Effort a run uses when nothing has chosen one. Mirrors the harness's
|
|
8522
|
+
* DEFAULT_THINKING_LEVEL, so the picker shows the level a run would actually
|
|
8523
|
+
* take rather than leaving it blank.
|
|
8524
|
+
*/
|
|
8525
|
+
const DEFAULT_EFFORT = "medium";
|
|
8526
|
+
/**
|
|
8527
|
+
* Reasoning-effort levels a model can be run at, coarse to fine. Empty for a
|
|
8528
|
+
* model that takes no effort parameter at all — the catalog reports both the
|
|
8529
|
+
* capability flag and the level list, and a non-reasoning model must never be
|
|
8530
|
+
* sent an effort (providers reject it outright).
|
|
8531
|
+
*/
|
|
8532
|
+
function effortLevels(model) {
|
|
8533
|
+
if (!model.reasoning) return [];
|
|
8534
|
+
return model.thinkingLevels ?? [];
|
|
8535
|
+
}
|
|
8536
|
+
/**
|
|
8537
|
+
* The effort a run on `model` would use right now, or null when the model
|
|
8538
|
+
* takes none.
|
|
8539
|
+
*
|
|
8540
|
+
* `agentEffort` is the level stored on the agent — what its next run really
|
|
8541
|
+
* uses — so it wins, but only for the model the agent is actually on; the
|
|
8542
|
+
* caller passes null for every other row. `savedEffort` is the user's
|
|
8543
|
+
* remembered choice for this model and decides what a switch TO it applies.
|
|
8544
|
+
* A level the model doesn't offer is skipped rather than shown, which is how
|
|
8545
|
+
* an effort carried over from a different model degrades.
|
|
8546
|
+
*/
|
|
8547
|
+
function resolveEffort({ model, agentEffort, savedEffort }) {
|
|
8548
|
+
const levels = effortLevels(model);
|
|
8549
|
+
if (levels.length === 0) return null;
|
|
8550
|
+
for (const candidate of [
|
|
8551
|
+
agentEffort,
|
|
8552
|
+
savedEffort,
|
|
8553
|
+
DEFAULT_EFFORT
|
|
8554
|
+
]) if (candidate && levels.includes(candidate)) return candidate;
|
|
8555
|
+
return levels[0] ?? null;
|
|
8556
|
+
}
|
|
8557
|
+
/** Display name for a level ("high" → "High"), falling back to the raw id. */
|
|
8558
|
+
function effortLabel(model, level) {
|
|
8559
|
+
return model.thinkingLevelLabels?.[level] ?? level;
|
|
8560
|
+
}
|
|
8561
|
+
|
|
8054
8562
|
//#endregion
|
|
8055
8563
|
//#region src/chat/tui/screens/model-picker.tsx
|
|
8056
8564
|
/**
|
|
8057
|
-
* In-chat model switcher (opened with ctrl+p). Rendered as an
|
|
8058
|
-
* place of the transcript+composer while the chat screen stays
|
|
8059
|
-
* the conversation, input draft, and any live run survive picking
|
|
8565
|
+
* In-chat model switcher (opened with ctrl+p, or `/model`). Rendered as an
|
|
8566
|
+
* overlay in place of the transcript+composer while the chat screen stays
|
|
8567
|
+
* mounted, so the conversation, input draft, and any live run survive picking
|
|
8568
|
+
* a model.
|
|
8060
8569
|
*
|
|
8061
8570
|
* Selecting a model PATCHes the agent, which changes the model for the
|
|
8062
|
-
* agent's next run (send carries no per-message model). `
|
|
8063
|
-
*
|
|
8064
|
-
*
|
|
8571
|
+
* agent's next run (send carries no per-message model). `tab` on a reasoning
|
|
8572
|
+
* model opens its effort list instead, which is also where `/effort` lands
|
|
8573
|
+
* directly. Both callbacks hand the settled values back to the chat screen so
|
|
8574
|
+
* its status line updates without a round-trip.
|
|
8065
8575
|
*/
|
|
8066
|
-
function ModelPicker({ agentId, currentModel, modelLocked,
|
|
8576
|
+
function ModelPicker({ agentId, currentModel, currentEffort, modelLocked, initialView, onModelSelected, onEffortSelected, onCancel }) {
|
|
8067
8577
|
const rest = useStore((s) => s.rest);
|
|
8068
8578
|
const [state, setState] = useState({ kind: "loading" });
|
|
8069
8579
|
useEffect(() => {
|
|
@@ -8071,11 +8581,12 @@ function ModelPicker({ agentId, currentModel, modelLocked, onSelected, onCancel
|
|
|
8071
8581
|
let cancelled = false;
|
|
8072
8582
|
(async () => {
|
|
8073
8583
|
try {
|
|
8074
|
-
const models = await rest.listModels();
|
|
8584
|
+
const [models, savedEfforts] = await Promise.all([rest.listModels(), rest.listModelEfforts().catch(() => ({}))]);
|
|
8075
8585
|
if (cancelled) return;
|
|
8076
8586
|
setState({
|
|
8077
8587
|
kind: "ready",
|
|
8078
|
-
models
|
|
8588
|
+
models,
|
|
8589
|
+
savedEfforts
|
|
8079
8590
|
});
|
|
8080
8591
|
} catch (err) {
|
|
8081
8592
|
if (cancelled) return;
|
|
@@ -8103,12 +8614,16 @@ function ModelPicker({ agentId, currentModel, modelLocked, onSelected, onCancel
|
|
|
8103
8614
|
children: ["error: ", state.message]
|
|
8104
8615
|
})
|
|
8105
8616
|
});
|
|
8106
|
-
return /* @__PURE__ */ jsx(
|
|
8617
|
+
return /* @__PURE__ */ jsx(ModelSwitcher, {
|
|
8107
8618
|
models: state.models,
|
|
8619
|
+
initialSavedEfforts: state.savedEfforts,
|
|
8108
8620
|
currentModel,
|
|
8621
|
+
currentEffort,
|
|
8109
8622
|
modelLocked,
|
|
8623
|
+
initialView,
|
|
8110
8624
|
agentId,
|
|
8111
|
-
|
|
8625
|
+
onModelSelected,
|
|
8626
|
+
onEffortSelected,
|
|
8112
8627
|
onCancel
|
|
8113
8628
|
});
|
|
8114
8629
|
}
|
|
@@ -8138,38 +8653,126 @@ function ModelPickerFrame({ hint, children }) {
|
|
|
8138
8653
|
]
|
|
8139
8654
|
});
|
|
8140
8655
|
}
|
|
8141
|
-
function
|
|
8142
|
-
const
|
|
8143
|
-
const [
|
|
8144
|
-
const [
|
|
8656
|
+
function ModelSwitcher({ models, initialSavedEfforts, currentModel, currentEffort, modelLocked, initialView, agentId, onModelSelected, onEffortSelected, onCancel }) {
|
|
8657
|
+
const rest = useStore((s) => s.rest);
|
|
8658
|
+
const [savedEfforts, setSavedEfforts] = useState(initialSavedEfforts);
|
|
8659
|
+
const [view, setView] = useState(() => {
|
|
8660
|
+
if (initialView === "models") return { kind: "models" };
|
|
8661
|
+
const model = models.find((m) => m.id === currentModel);
|
|
8662
|
+
if (!model || effortLevels(model).length === 0) return { kind: "models" };
|
|
8663
|
+
return {
|
|
8664
|
+
kind: "effort",
|
|
8665
|
+
model
|
|
8666
|
+
};
|
|
8667
|
+
});
|
|
8145
8668
|
const [saving, setSaving] = useState(false);
|
|
8146
8669
|
const [saveError, setSaveError] = useState(null);
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8670
|
+
/** The effort this row would run at, or null when the model takes none. */
|
|
8671
|
+
function effortFor(model) {
|
|
8672
|
+
return resolveEffort({
|
|
8673
|
+
model,
|
|
8674
|
+
agentEffort: model.id === currentModel ? currentEffort : null,
|
|
8675
|
+
savedEffort: savedEfforts[model.id] ?? null
|
|
8676
|
+
});
|
|
8677
|
+
}
|
|
8678
|
+
function failed(err) {
|
|
8679
|
+
setSaveError(err instanceof HttpError ? parseApiError(err.body) ?? err.message : errorMessage(err));
|
|
8680
|
+
setSaving(false);
|
|
8681
|
+
}
|
|
8682
|
+
async function selectModel(model) {
|
|
8155
8683
|
if (!rest || saving) return;
|
|
8156
|
-
|
|
8684
|
+
const effort = effortFor(model);
|
|
8685
|
+
if (model.id === currentModel && effort === currentEffort) {
|
|
8157
8686
|
onCancel();
|
|
8158
8687
|
return;
|
|
8159
8688
|
}
|
|
8160
8689
|
setSaving(true);
|
|
8161
8690
|
setSaveError(null);
|
|
8162
8691
|
try {
|
|
8163
|
-
const
|
|
8692
|
+
const settled = await rest.updateAgentModel({
|
|
8164
8693
|
agentId,
|
|
8165
|
-
model: model.id
|
|
8694
|
+
model: model.id,
|
|
8695
|
+
thinkingLevel: effort
|
|
8696
|
+
});
|
|
8697
|
+
onModelSelected({
|
|
8698
|
+
model: settled.model ?? model.id,
|
|
8699
|
+
effort: settled.thinkingLevel
|
|
8166
8700
|
});
|
|
8167
|
-
onSelected(settled ?? model.id);
|
|
8168
8701
|
} catch (err) {
|
|
8169
|
-
|
|
8702
|
+
failed(err);
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
async function selectEffort(model, level) {
|
|
8706
|
+
if (!rest || saving) return;
|
|
8707
|
+
setSaving(true);
|
|
8708
|
+
setSaveError(null);
|
|
8709
|
+
try {
|
|
8710
|
+
await rest.setModelEffort({
|
|
8711
|
+
modelId: model.id,
|
|
8712
|
+
thinkingLevel: level
|
|
8713
|
+
});
|
|
8714
|
+
if (model.id === currentModel) {
|
|
8715
|
+
await rest.updateAgentEffort({
|
|
8716
|
+
agentId,
|
|
8717
|
+
thinkingLevel: level
|
|
8718
|
+
});
|
|
8719
|
+
onEffortSelected({
|
|
8720
|
+
effort: level,
|
|
8721
|
+
label: effortLabel(model, level)
|
|
8722
|
+
});
|
|
8723
|
+
return;
|
|
8724
|
+
}
|
|
8725
|
+
setSavedEfforts((prev) => ({
|
|
8726
|
+
...prev,
|
|
8727
|
+
[model.id]: level
|
|
8728
|
+
}));
|
|
8729
|
+
setView({ kind: "models" });
|
|
8170
8730
|
setSaving(false);
|
|
8731
|
+
} catch (err) {
|
|
8732
|
+
failed(err);
|
|
8171
8733
|
}
|
|
8172
8734
|
}
|
|
8735
|
+
if (view.kind === "effort") return /* @__PURE__ */ jsx(EffortList, {
|
|
8736
|
+
model: view.model,
|
|
8737
|
+
activeEffort: effortFor(view.model),
|
|
8738
|
+
isCurrentModel: view.model.id === currentModel,
|
|
8739
|
+
saving,
|
|
8740
|
+
saveError,
|
|
8741
|
+
onSelect: (level) => void selectEffort(view.model, level),
|
|
8742
|
+
onBack: () => {
|
|
8743
|
+
setSaveError(null);
|
|
8744
|
+
if (initialView === "effort") onCancel();
|
|
8745
|
+
else setView({ kind: "models" });
|
|
8746
|
+
}
|
|
8747
|
+
});
|
|
8748
|
+
return /* @__PURE__ */ jsx(ModelList, {
|
|
8749
|
+
models,
|
|
8750
|
+
currentModel,
|
|
8751
|
+
modelLocked,
|
|
8752
|
+
saving,
|
|
8753
|
+
saveError,
|
|
8754
|
+
effortFor,
|
|
8755
|
+
onSelect: (model) => void selectModel(model),
|
|
8756
|
+
onEditEffort: (model) => {
|
|
8757
|
+
setSaveError(null);
|
|
8758
|
+
setView({
|
|
8759
|
+
kind: "effort",
|
|
8760
|
+
model
|
|
8761
|
+
});
|
|
8762
|
+
},
|
|
8763
|
+
onCancel
|
|
8764
|
+
});
|
|
8765
|
+
}
|
|
8766
|
+
function ModelList({ models, currentModel, modelLocked, saving, saveError, effortFor, onSelect, onEditEffort, onCancel }) {
|
|
8767
|
+
const { width, height } = useTerminalDimensions();
|
|
8768
|
+
const [query, setQuery] = useState("");
|
|
8769
|
+
const [highlight, setHighlight] = useState(0);
|
|
8770
|
+
const hits = fuzzyFilter(query, models, modelKeys);
|
|
8771
|
+
const filtered = hits.map((h) => h.item);
|
|
8772
|
+
const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
|
|
8773
|
+
const visibleRows = Math.max(3, height - 8);
|
|
8774
|
+
const start = windowStart(clamped, filtered.length, visibleRows);
|
|
8775
|
+
const windowed = hits.slice(start, start + visibleRows);
|
|
8173
8776
|
useKeyboard((key) => {
|
|
8174
8777
|
if (saving) return;
|
|
8175
8778
|
if (key.name === "escape") {
|
|
@@ -8178,12 +8781,17 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
8178
8781
|
}
|
|
8179
8782
|
if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
|
|
8180
8783
|
else if (key.name === "down") setHighlight(Math.min(Math.max(0, filtered.length - 1), clamped + 1));
|
|
8181
|
-
else if (key.name === "
|
|
8784
|
+
else if (key.name === "tab") {
|
|
8785
|
+
const model = filtered[clamped];
|
|
8786
|
+
if (model && effortFor(model)) onEditEffort(model);
|
|
8787
|
+
} else if (key.name === "return") {
|
|
8182
8788
|
const model = filtered[clamped];
|
|
8183
|
-
if (model)
|
|
8789
|
+
if (model) onSelect(model);
|
|
8184
8790
|
}
|
|
8185
8791
|
});
|
|
8186
|
-
const
|
|
8792
|
+
const highlighted = filtered[clamped];
|
|
8793
|
+
const canEditEffort = !!highlighted && !!effortFor(highlighted);
|
|
8794
|
+
const statusLine = saving ? "saving…" : saveError ? saveError : `${filtered.length}/${models.length} · ↑/↓ move · ↵ select${canEditEffort ? " · tab effort" : ""} · esc cancel`;
|
|
8187
8795
|
return /* @__PURE__ */ jsxs("box", {
|
|
8188
8796
|
style: {
|
|
8189
8797
|
flexDirection: "column",
|
|
@@ -8236,6 +8844,7 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
8236
8844
|
const m = hit.item;
|
|
8237
8845
|
const selected = start + i === clamped;
|
|
8238
8846
|
const isCurrent = m.id === currentModel;
|
|
8847
|
+
const effort = effortFor(m);
|
|
8239
8848
|
return /* @__PURE__ */ jsxs("text", {
|
|
8240
8849
|
fg: selected ? theme.accent : theme.fg,
|
|
8241
8850
|
children: [
|
|
@@ -8253,7 +8862,11 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
8253
8862
|
/* @__PURE__ */ jsx(FuzzyText, {
|
|
8254
8863
|
text: m.id,
|
|
8255
8864
|
indexes: hit.highlights[2]
|
|
8256
|
-
})
|
|
8865
|
+
}),
|
|
8866
|
+
effort ? /* @__PURE__ */ jsx("span", {
|
|
8867
|
+
fg: theme.dim,
|
|
8868
|
+
children: ` ${effortLabel(m, effort)}`
|
|
8869
|
+
}) : null
|
|
8257
8870
|
]
|
|
8258
8871
|
}, m.id);
|
|
8259
8872
|
})
|
|
@@ -8265,6 +8878,60 @@ function FilterableModelList({ models, currentModel, modelLocked, agentId, onSel
|
|
|
8265
8878
|
]
|
|
8266
8879
|
});
|
|
8267
8880
|
}
|
|
8881
|
+
function EffortList({ model, activeEffort, isCurrentModel, saving, saveError, onSelect, onBack }) {
|
|
8882
|
+
const levels = effortLevels(model);
|
|
8883
|
+
const [highlight, setHighlight] = useState(() => {
|
|
8884
|
+
const index = activeEffort ? levels.indexOf(activeEffort) : -1;
|
|
8885
|
+
return index < 0 ? 0 : index;
|
|
8886
|
+
});
|
|
8887
|
+
const clamped = Math.min(highlight, Math.max(0, levels.length - 1));
|
|
8888
|
+
useKeyboard((key) => {
|
|
8889
|
+
if (saving) return;
|
|
8890
|
+
if (key.name === "escape") onBack();
|
|
8891
|
+
else if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
|
|
8892
|
+
else if (key.name === "down") setHighlight(Math.min(Math.max(0, levels.length - 1), clamped + 1));
|
|
8893
|
+
else if (key.name === "return") {
|
|
8894
|
+
const level = levels[clamped];
|
|
8895
|
+
if (level) onSelect(level);
|
|
8896
|
+
}
|
|
8897
|
+
});
|
|
8898
|
+
const statusLine = saving ? "saving…" : saveError ? saveError : "↑/↓ move · ↵ select · esc back";
|
|
8899
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
8900
|
+
style: {
|
|
8901
|
+
flexDirection: "column",
|
|
8902
|
+
flexGrow: 1
|
|
8903
|
+
},
|
|
8904
|
+
children: [
|
|
8905
|
+
/* @__PURE__ */ jsxs("text", {
|
|
8906
|
+
fg: theme.accent,
|
|
8907
|
+
children: ["effort · ", model.displayName]
|
|
8908
|
+
}),
|
|
8909
|
+
/* @__PURE__ */ jsx("text", {
|
|
8910
|
+
fg: theme.muted,
|
|
8911
|
+
children: isCurrentModel ? "applies to this agent from its next run" : "applies when you switch to this model"
|
|
8912
|
+
}),
|
|
8913
|
+
/* @__PURE__ */ jsx("box", {
|
|
8914
|
+
style: {
|
|
8915
|
+
flexDirection: "column",
|
|
8916
|
+
flexGrow: 1,
|
|
8917
|
+
marginTop: 1
|
|
8918
|
+
},
|
|
8919
|
+
children: levels.map((level, i) => /* @__PURE__ */ jsxs("text", {
|
|
8920
|
+
fg: i === clamped ? theme.accent : theme.fg,
|
|
8921
|
+
children: [
|
|
8922
|
+
i === clamped ? "› " : " ",
|
|
8923
|
+
level === activeEffort ? "● " : " ",
|
|
8924
|
+
effortLabel(model, level)
|
|
8925
|
+
]
|
|
8926
|
+
}, level))
|
|
8927
|
+
}),
|
|
8928
|
+
/* @__PURE__ */ jsx("text", {
|
|
8929
|
+
fg: saveError ? theme.error : theme.dim,
|
|
8930
|
+
children: statusLine
|
|
8931
|
+
})
|
|
8932
|
+
]
|
|
8933
|
+
});
|
|
8934
|
+
}
|
|
8268
8935
|
const modelKeys = [
|
|
8269
8936
|
{
|
|
8270
8937
|
get: (m) => m.displayName,
|
|
@@ -8291,6 +8958,106 @@ function parseApiError(body) {
|
|
|
8291
8958
|
return null;
|
|
8292
8959
|
}
|
|
8293
8960
|
|
|
8961
|
+
//#endregion
|
|
8962
|
+
//#region src/chat/tui/screens/pr-picker.tsx
|
|
8963
|
+
/**
|
|
8964
|
+
* The full list of PRs linked from the conversation, as a pickable overlay.
|
|
8965
|
+
*
|
|
8966
|
+
* The composer footer chip (see {@link ../chat/pr-links}) shows only the newest
|
|
8967
|
+
* few PR numbers before collapsing the rest into a `+N` count — there's no room
|
|
8968
|
+
* on that shared row for more, and no way from the chip to reach the ones the
|
|
8969
|
+
* `+N` swallowed. This overlay is the "expand the `+N`" surface: every PR the
|
|
8970
|
+
* conversation has linked, newest first, with the repo it belongs to, where you
|
|
8971
|
+
* can pick one to open in the browser or copy its URL.
|
|
8972
|
+
*
|
|
8973
|
+
* Rendered in place of the transcript like the model/theme/card pickers, so the
|
|
8974
|
+
* conversation and any live run survive underneath.
|
|
8975
|
+
*/
|
|
8976
|
+
function PrPicker({ prs, onOpen, onCopy, onCancel }) {
|
|
8977
|
+
const { height } = useTerminalDimensions();
|
|
8978
|
+
const ordered = [...prs].reverse();
|
|
8979
|
+
const [highlight, setHighlight] = useState(0);
|
|
8980
|
+
const clamped = Math.min(highlight, Math.max(0, ordered.length - 1));
|
|
8981
|
+
const visibleRows = Math.max(3, height - 6);
|
|
8982
|
+
const start = windowStart(clamped, ordered.length, visibleRows);
|
|
8983
|
+
const windowed = ordered.slice(start, start + visibleRows);
|
|
8984
|
+
useKeyboard((key) => {
|
|
8985
|
+
if (key.name === "escape") {
|
|
8986
|
+
onCancel();
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8989
|
+
if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
|
|
8990
|
+
else if (key.name === "down") setHighlight(Math.min(Math.max(0, ordered.length - 1), clamped + 1));
|
|
8991
|
+
else if (key.name === "return") {
|
|
8992
|
+
const pr = ordered[clamped];
|
|
8993
|
+
if (pr) onOpen(pr);
|
|
8994
|
+
} else if (key.name === "y") {
|
|
8995
|
+
const pr = ordered[clamped];
|
|
8996
|
+
if (pr) onCopy(pr);
|
|
8997
|
+
}
|
|
8998
|
+
});
|
|
8999
|
+
useEffect(() => {
|
|
9000
|
+
if (ordered.length === 0) onCancel();
|
|
9001
|
+
}, [ordered.length, onCancel]);
|
|
9002
|
+
if (ordered.length === 0) return null;
|
|
9003
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
9004
|
+
style: {
|
|
9005
|
+
flexDirection: "column",
|
|
9006
|
+
flexGrow: 1
|
|
9007
|
+
},
|
|
9008
|
+
children: [
|
|
9009
|
+
/* @__PURE__ */ jsx("text", {
|
|
9010
|
+
fg: theme.accent,
|
|
9011
|
+
children: "pull requests"
|
|
9012
|
+
}),
|
|
9013
|
+
/* @__PURE__ */ jsx("box", {
|
|
9014
|
+
style: {
|
|
9015
|
+
flexDirection: "column",
|
|
9016
|
+
flexGrow: 1,
|
|
9017
|
+
marginTop: 1
|
|
9018
|
+
},
|
|
9019
|
+
children: windowed.map((pr, i) => {
|
|
9020
|
+
const selected = start + i === clamped;
|
|
9021
|
+
const repo = repoLabel(pr.key);
|
|
9022
|
+
return /* @__PURE__ */ jsxs("text", {
|
|
9023
|
+
fg: selected ? theme.accent : theme.fg,
|
|
9024
|
+
children: [
|
|
9025
|
+
selected ? "› " : " ",
|
|
9026
|
+
"⇄ #",
|
|
9027
|
+
pr.number,
|
|
9028
|
+
repo ? /* @__PURE__ */ jsxs("span", {
|
|
9029
|
+
fg: theme.muted,
|
|
9030
|
+
children: [" · ", repo]
|
|
9031
|
+
}) : null
|
|
9032
|
+
]
|
|
9033
|
+
}, pr.key);
|
|
9034
|
+
})
|
|
9035
|
+
}),
|
|
9036
|
+
/* @__PURE__ */ jsxs("text", {
|
|
9037
|
+
fg: theme.dim,
|
|
9038
|
+
children: [
|
|
9039
|
+
ordered.length,
|
|
9040
|
+
" PR",
|
|
9041
|
+
ordered.length === 1 ? "" : "s",
|
|
9042
|
+
" · ↑/↓ move · ↵ open · y copy url · esc cancel"
|
|
9043
|
+
]
|
|
9044
|
+
})
|
|
9045
|
+
]
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
9048
|
+
/**
|
|
9049
|
+
* The `owner/repo` a PR belongs to, pulled from its `owner/repo#number`
|
|
9050
|
+
* identity key. Shown alongside the number so a conversation spanning several
|
|
9051
|
+
* repos never renders an ambiguous bare `#42`. Returns null if the key isn't in
|
|
9052
|
+
* the expected shape, so the row degrades to just the number rather than
|
|
9053
|
+
* printing a malformed label.
|
|
9054
|
+
*/
|
|
9055
|
+
function repoLabel(key) {
|
|
9056
|
+
const hash = key.lastIndexOf("#");
|
|
9057
|
+
if (hash <= 0) return null;
|
|
9058
|
+
return key.slice(0, hash);
|
|
9059
|
+
}
|
|
9060
|
+
|
|
8294
9061
|
//#endregion
|
|
8295
9062
|
//#region src/chat/tui/screens/theme-preview.tsx
|
|
8296
9063
|
/**
|
|
@@ -10953,7 +11720,9 @@ function routeInput(raw) {
|
|
|
10953
11720
|
case "fork":
|
|
10954
11721
|
case "archive-conversation":
|
|
10955
11722
|
case "model-picker":
|
|
11723
|
+
case "effort-picker":
|
|
10956
11724
|
case "theme-picker":
|
|
11725
|
+
case "pr-picker":
|
|
10957
11726
|
case "status":
|
|
10958
11727
|
case "computer":
|
|
10959
11728
|
case "share":
|
|
@@ -11093,7 +11862,7 @@ async function openUrlInBrowser(url) {
|
|
|
11093
11862
|
function isActionableCard(m) {
|
|
11094
11863
|
return m.kind === "card" && m.status !== "done" && m.status !== "busy" && m.card.button !== null;
|
|
11095
11864
|
}
|
|
11096
|
-
function ChatScreen({ agent, conversation }) {
|
|
11865
|
+
function ChatScreen({ agent, conversation, attachRunId }) {
|
|
11097
11866
|
const renderer = useRenderer();
|
|
11098
11867
|
const { width, height } = useTerminalDimensions();
|
|
11099
11868
|
const rest = useStore((s) => s.rest);
|
|
@@ -11121,6 +11890,16 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11121
11890
|
});
|
|
11122
11891
|
const [items, setItems] = useState([]);
|
|
11123
11892
|
const [todos, setTodos] = useState(null);
|
|
11893
|
+
const [bgTasks, setBgTasks] = useState([]);
|
|
11894
|
+
const [bgTasksOpen, setBgTasksOpen] = useState(false);
|
|
11895
|
+
const bgHasRetained = hasRetainedFinished(bgTasks);
|
|
11896
|
+
useEffect(() => {
|
|
11897
|
+
if (!bgHasRetained) return;
|
|
11898
|
+
const id = setInterval(() => {
|
|
11899
|
+
setBgTasks((prev) => mergeBgTasks(prev, prev, Date.now()));
|
|
11900
|
+
}, Math.min(FINISHED_GRACE_MS, 5e3));
|
|
11901
|
+
return () => clearInterval(id);
|
|
11902
|
+
}, [bgHasRetained]);
|
|
11124
11903
|
const [planHidden, setPlanHidden] = useState(false);
|
|
11125
11904
|
const [subagentRows, setSubagentRows] = useState(0);
|
|
11126
11905
|
const [anchoredSubagentIds, setAnchoredSubagentIds] = useState([]);
|
|
@@ -11139,10 +11918,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11139
11918
|
const [input, setInput] = useState(() => useStore.getState().takeEarlyInput());
|
|
11140
11919
|
const [run, setRun] = useState({ kind: "idle" });
|
|
11141
11920
|
const [model, setModel] = useState(agent.model ?? null);
|
|
11921
|
+
const [effort, setEffort] = useState(agent.thinkingLevel ?? null);
|
|
11142
11922
|
const [modelLocked, setModelLocked] = useState(agent.modelLocked ?? false);
|
|
11143
11923
|
const [cardPickerOpen, setCardPickerOpen] = useState(false);
|
|
11144
|
-
const [
|
|
11924
|
+
const [modelPickerView, setModelPickerView] = useState(null);
|
|
11925
|
+
const modelPickerOpen = modelPickerView !== null;
|
|
11145
11926
|
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
|
11927
|
+
const [prPickerOpen, setPrPickerOpen] = useState(false);
|
|
11146
11928
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
11147
11929
|
const [reviewOpen, setReviewOpen] = useState(false);
|
|
11148
11930
|
const [reviewFocused, setReviewFocused] = useState(false);
|
|
@@ -11274,6 +12056,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11274
12056
|
const ids = new Set(anchoredSubagentIds);
|
|
11275
12057
|
return items.filter((item) => item.kind === "tool" && item.toolName === "subagent" && ids.has(item.id));
|
|
11276
12058
|
}, [items, anchoredSubagentIds]);
|
|
12059
|
+
const bgTasksActive = bgTasks.length > 0;
|
|
11277
12060
|
const jumpHintVisible = scrolledUp && !credPrompt;
|
|
11278
12061
|
const jumpHintRows = jumpHintVisible ? 1 : 0;
|
|
11279
12062
|
const scrollHeight = Math.max(3, chatHeight - 2 - bottomBoxHeight - pendingRows - inlineReviewHeight - menuRows - jumpHintRows - subagentRows - todoRows);
|
|
@@ -11306,6 +12089,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11306
12089
|
cardPickerOpen,
|
|
11307
12090
|
modelPickerOpen,
|
|
11308
12091
|
themePickerOpen,
|
|
12092
|
+
prPickerOpen,
|
|
11309
12093
|
helpOpen,
|
|
11310
12094
|
reviewOpen,
|
|
11311
12095
|
reviewFocused,
|
|
@@ -11413,6 +12197,11 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11413
12197
|
if (next) setTodos(next.length > 0 ? next : null);
|
|
11414
12198
|
return;
|
|
11415
12199
|
}
|
|
12200
|
+
if (event.chunk.type === "data-anyone-bg-tasks-updated") {
|
|
12201
|
+
const next = bgTasksFromPayload(event.chunk.data);
|
|
12202
|
+
if (next) setBgTasks((prev) => mergeBgTasks(prev, next, Date.now()));
|
|
12203
|
+
return;
|
|
12204
|
+
}
|
|
11416
12205
|
setItems((prev) => applyChunk(prev, event.chunk, streamAgentName));
|
|
11417
12206
|
return;
|
|
11418
12207
|
}
|
|
@@ -11483,6 +12272,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11483
12272
|
if (event.kind === "agent") {
|
|
11484
12273
|
if (event.id !== agent.id) return;
|
|
11485
12274
|
setModel(event.model);
|
|
12275
|
+
setEffort(event.thinkingLevel ?? null);
|
|
11486
12276
|
setModelLocked(event.modelLocked);
|
|
11487
12277
|
return;
|
|
11488
12278
|
}
|
|
@@ -11500,6 +12290,13 @@ function ChatScreen({ agent, conversation }) {
|
|
|
11500
12290
|
attachToRun,
|
|
11501
12291
|
onCompactionEvent
|
|
11502
12292
|
]);
|
|
12293
|
+
const attachedSeedRunRef = useRef(false);
|
|
12294
|
+
useEffect(() => {
|
|
12295
|
+
if (!attachRunId || attachedSeedRunRef.current) return;
|
|
12296
|
+
if (seenRunsRef.current.has(attachRunId)) return;
|
|
12297
|
+
attachedSeedRunRef.current = true;
|
|
12298
|
+
attachToRun(attachRunId, null, null);
|
|
12299
|
+
}, [attachRunId, attachToRun]);
|
|
11503
12300
|
const sendContent = useCallback(async (content, staged, opts) => {
|
|
11504
12301
|
if (!rest) return;
|
|
11505
12302
|
const trimmed = content.trim();
|
|
@@ -12058,7 +12855,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12058
12855
|
stageAttachment(image);
|
|
12059
12856
|
}, [stageAttachment]);
|
|
12060
12857
|
usePaste((event) => {
|
|
12061
|
-
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt || computePromptOpen || nonWebChannel) return;
|
|
12858
|
+
if (cardPickerOpen || modelPickerOpen || themePickerOpen || prPickerOpen || helpOpen || bgTasksOpen || credPromptOpen || grantPrompt || computePromptOpen || nonWebChannel) return;
|
|
12062
12859
|
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
12063
12860
|
const route = routePaste({
|
|
12064
12861
|
kind: event.metadata?.kind,
|
|
@@ -12110,6 +12907,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12110
12907
|
cardPickerOpen,
|
|
12111
12908
|
modelPickerOpen,
|
|
12112
12909
|
themePickerOpen,
|
|
12910
|
+
prPickerOpen,
|
|
12113
12911
|
helpOpen,
|
|
12114
12912
|
credPromptOpen,
|
|
12115
12913
|
computePromptOpen,
|
|
@@ -12170,6 +12968,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12170
12968
|
goTo({
|
|
12171
12969
|
kind: "chat",
|
|
12172
12970
|
agent,
|
|
12971
|
+
attachRunId: null,
|
|
12173
12972
|
conversation: forkedConversationRef({
|
|
12174
12973
|
agent,
|
|
12175
12974
|
forked
|
|
@@ -12462,6 +13261,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12462
13261
|
goTo({
|
|
12463
13262
|
kind: "chat",
|
|
12464
13263
|
agent,
|
|
13264
|
+
attachRunId: null,
|
|
12465
13265
|
conversation: {
|
|
12466
13266
|
kind: "new",
|
|
12467
13267
|
agentId: agent.id,
|
|
@@ -12498,11 +13298,18 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12498
13298
|
openConversationRef(routed.ref);
|
|
12499
13299
|
break;
|
|
12500
13300
|
case "model-picker":
|
|
12501
|
-
|
|
13301
|
+
setModelPickerView("models");
|
|
13302
|
+
break;
|
|
13303
|
+
case "effort-picker":
|
|
13304
|
+
setModelPickerView("effort");
|
|
12502
13305
|
break;
|
|
12503
13306
|
case "theme-picker":
|
|
12504
13307
|
setThemePickerOpen(true);
|
|
12505
13308
|
break;
|
|
13309
|
+
case "pr-picker":
|
|
13310
|
+
if (prLinks.length === 0) useStore.getState().showToast({ message: "no linked PRs in this conversation yet" });
|
|
13311
|
+
else setPrPickerOpen(true);
|
|
13312
|
+
break;
|
|
12506
13313
|
case "status":
|
|
12507
13314
|
setItems((prev) => [...prev, {
|
|
12508
13315
|
kind: "info",
|
|
@@ -12514,6 +13321,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12514
13321
|
agentName: agent.name,
|
|
12515
13322
|
conversationId,
|
|
12516
13323
|
model,
|
|
13324
|
+
effort,
|
|
12517
13325
|
share: {
|
|
12518
13326
|
status: portal.status,
|
|
12519
13327
|
grantedToAgent: portal.grantedAgentIds.includes(agent.id)
|
|
@@ -12600,7 +13408,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12600
13408
|
portal.grantedAgentIds,
|
|
12601
13409
|
promptForCards,
|
|
12602
13410
|
todos,
|
|
12603
|
-
planHidden
|
|
13411
|
+
planHidden,
|
|
13412
|
+
prLinks.length
|
|
12604
13413
|
]);
|
|
12605
13414
|
const submit = useCallback(() => {
|
|
12606
13415
|
const staged = attachments.filter((a) => a.status !== "error");
|
|
@@ -12666,7 +13475,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12666
13475
|
}
|
|
12667
13476
|
if (reviewFocused) return;
|
|
12668
13477
|
}
|
|
12669
|
-
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen) return;
|
|
13478
|
+
if (cardPickerOpen || modelPickerOpen || themePickerOpen || prPickerOpen || helpOpen || bgTasksOpen) return;
|
|
12670
13479
|
if (grantPrompt) {
|
|
12671
13480
|
if (key.name === "y") {
|
|
12672
13481
|
confirmGrant();
|
|
@@ -12768,13 +13577,17 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12768
13577
|
return;
|
|
12769
13578
|
}
|
|
12770
13579
|
if (key.name === "p" && key.ctrl) {
|
|
12771
|
-
|
|
13580
|
+
setModelPickerView("models");
|
|
12772
13581
|
return;
|
|
12773
13582
|
}
|
|
12774
13583
|
if (key.name === "r" && key.ctrl) {
|
|
12775
13584
|
promptForCards();
|
|
12776
13585
|
return;
|
|
12777
13586
|
}
|
|
13587
|
+
if (key.name === "b" && key.ctrl) {
|
|
13588
|
+
if (bgTasksActive) setBgTasksOpen(true);
|
|
13589
|
+
return;
|
|
13590
|
+
}
|
|
12778
13591
|
if (key.name === "t" && key.ctrl) {
|
|
12779
13592
|
toggleShare();
|
|
12780
13593
|
return;
|
|
@@ -12869,10 +13682,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12869
13682
|
const boundCollectSandboxStats = useMemo(() => {
|
|
12870
13683
|
if (!rest) return null;
|
|
12871
13684
|
return () => collectSandboxStats({
|
|
12872
|
-
read: () => rest.computeLiveStats(),
|
|
13685
|
+
read: () => rest.computeLiveStats(agent.id),
|
|
12873
13686
|
now: () => Date.now()
|
|
12874
13687
|
});
|
|
12875
|
-
}, [rest]);
|
|
13688
|
+
}, [rest, agent.id]);
|
|
12876
13689
|
const conversationContext = useMemo(() => ({
|
|
12877
13690
|
rest,
|
|
12878
13691
|
conversationId,
|
|
@@ -12902,7 +13715,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12902
13715
|
actionableCards.length,
|
|
12903
13716
|
nonWebChannel
|
|
12904
13717
|
]);
|
|
12905
|
-
const modelHint = formatModelHint(model);
|
|
13718
|
+
const modelHint = formatModelHint(model, effort);
|
|
12906
13719
|
if (cardPickerOpen) return /* @__PURE__ */ jsx(CardPicker, {
|
|
12907
13720
|
cards: actionableCards,
|
|
12908
13721
|
onSelect: (card) => {
|
|
@@ -12911,22 +13724,56 @@ function ChatScreen({ agent, conversation }) {
|
|
|
12911
13724
|
},
|
|
12912
13725
|
onCancel: () => setCardPickerOpen(false)
|
|
12913
13726
|
});
|
|
12914
|
-
if (
|
|
13727
|
+
if (modelPickerView) return /* @__PURE__ */ jsx(ModelPicker, {
|
|
12915
13728
|
agentId: agent.id,
|
|
12916
13729
|
currentModel: model,
|
|
13730
|
+
currentEffort: effort,
|
|
12917
13731
|
modelLocked,
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
13732
|
+
initialView: modelPickerView,
|
|
13733
|
+
onModelSelected: (next) => {
|
|
13734
|
+
setModel(next.model);
|
|
13735
|
+
setEffort(next.effort);
|
|
13736
|
+
setModelPickerView(null);
|
|
12921
13737
|
useStore.getState().showToast({
|
|
12922
13738
|
variant: "success",
|
|
12923
|
-
message: `model changed to ${
|
|
13739
|
+
message: `model changed to ${next.model}`
|
|
12924
13740
|
});
|
|
12925
13741
|
},
|
|
12926
|
-
|
|
13742
|
+
onEffortSelected: (next) => {
|
|
13743
|
+
setEffort(next.effort);
|
|
13744
|
+
setModelPickerView(null);
|
|
13745
|
+
useStore.getState().showToast({
|
|
13746
|
+
variant: "success",
|
|
13747
|
+
message: `effort changed to ${next.label}`
|
|
13748
|
+
});
|
|
13749
|
+
},
|
|
13750
|
+
onCancel: () => setModelPickerView(null)
|
|
12927
13751
|
});
|
|
12928
13752
|
if (themePickerOpen) return /* @__PURE__ */ jsx(ThemePicker, { onClose: () => setThemePickerOpen(false) });
|
|
13753
|
+
if (prPickerOpen) return /* @__PURE__ */ jsx(PrPicker, {
|
|
13754
|
+
prs: prLinks,
|
|
13755
|
+
onOpen: (pr) => {
|
|
13756
|
+
setPrPickerOpen(false);
|
|
13757
|
+
openUrlInBrowser(pr.url);
|
|
13758
|
+
},
|
|
13759
|
+
onCopy: (pr) => {
|
|
13760
|
+
writeClipboardText(pr.url).then((ok) => {
|
|
13761
|
+
useStore.getState().showToast(ok ? {
|
|
13762
|
+
variant: "success",
|
|
13763
|
+
message: `copied #${pr.number} url`
|
|
13764
|
+
} : {
|
|
13765
|
+
variant: "error",
|
|
13766
|
+
message: "could not copy to clipboard"
|
|
13767
|
+
});
|
|
13768
|
+
});
|
|
13769
|
+
},
|
|
13770
|
+
onCancel: () => setPrPickerOpen(false)
|
|
13771
|
+
});
|
|
12929
13772
|
if (helpOpen) return /* @__PURE__ */ jsx(HelpModal, { onClose: () => setHelpOpen(false) });
|
|
13773
|
+
if (bgTasksOpen) return /* @__PURE__ */ jsx(BgTasksOverlay, {
|
|
13774
|
+
tasks: bgTasks,
|
|
13775
|
+
onClose: () => setBgTasksOpen(false)
|
|
13776
|
+
});
|
|
12930
13777
|
const reviewView = reviewOpen ? /* @__PURE__ */ jsx(ReviewPane, {
|
|
12931
13778
|
agentId: agent.id,
|
|
12932
13779
|
conversationId,
|
|
@@ -13292,6 +14139,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
13292
14139
|
fg: theme.muted,
|
|
13293
14140
|
children: statusText
|
|
13294
14141
|
}),
|
|
14142
|
+
bgTasksActive ? /* @__PURE__ */ jsx(BgShellsChip, {
|
|
14143
|
+
tasks: bgTasks,
|
|
14144
|
+
onOpen: () => setBgTasksOpen(true)
|
|
14145
|
+
}) : null,
|
|
13295
14146
|
prLinks.length > 0 ? /* @__PURE__ */ jsx(PrLinksView, { prs: prLinks }) : null,
|
|
13296
14147
|
/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
|
|
13297
14148
|
/* @__PURE__ */ jsx("text", {
|
|
@@ -13714,7 +14565,8 @@ function App({ appUrl, sessionToken, authKind, workspaceId, shareMachine, prompt
|
|
|
13714
14565
|
}),
|
|
13715
14566
|
screen.kind === "chat" && rest && /* @__PURE__ */ jsx(ChatScreen, {
|
|
13716
14567
|
agent: screen.agent,
|
|
13717
|
-
conversation: screen.conversation
|
|
14568
|
+
conversation: screen.conversation,
|
|
14569
|
+
attachRunId: screen.attachRunId
|
|
13718
14570
|
}, "id" in screen.conversation ? screen.conversation.id : screen.conversation.nonce)
|
|
13719
14571
|
]
|
|
13720
14572
|
})
|