skydive-cli 0.2.0-beta.482 → 0.2.0-beta.509
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/README.md +7 -0
- package/dist/js/bin.mjs +4 -4
- package/dist/js/{boot-B8ownO3n.mjs → boot-C-wL4WoP.mjs} +227 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,6 +176,13 @@ internal API. On launch it walks you through:
|
|
|
176
176
|
formatting, collapsed reasoning, and rich rendering of tool calls
|
|
177
177
|
(`bash`, `edit`/`write` diffs, `read`, `grep`/`glob`).
|
|
178
178
|
|
|
179
|
+
You can change conversation without leaving chat. `/conversation` (aliases
|
|
180
|
+
`/conversations`, `/conv`) reopens the conversation list, and
|
|
181
|
+
`/conversation <id>` — an id, its `/c/<id>` link, or a pasted
|
|
182
|
+
`skydive chat --resume <id>` line — jumps straight into that thread, including
|
|
183
|
+
one belonging to a **different** agent. Either way the run you leave behind
|
|
184
|
+
keeps going server-side; reopening it reattaches to the stream.
|
|
185
|
+
|
|
179
186
|
Runs under **Node** like every other command. The TUI renders through
|
|
180
187
|
OpenTUI, whose native core needs the [Bun](https://bun.sh) runtime, so the
|
|
181
188
|
first `chat` run downloads a private, pinned Bun and re-execs under it
|
package/dist/js/bin.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import semver from "semver";
|
|
|
20
20
|
|
|
21
21
|
//#region package.json
|
|
22
22
|
var name = "skydive-cli";
|
|
23
|
-
var version = "0.2.0-beta.
|
|
23
|
+
var version = "0.2.0-beta.509";
|
|
24
24
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/types.ts
|
|
@@ -1200,7 +1200,7 @@ const importCommand = {
|
|
|
1200
1200
|
process.exit(1);
|
|
1201
1201
|
}
|
|
1202
1202
|
}
|
|
1203
|
-
const { runChat } = await import("./boot-
|
|
1203
|
+
const { runChat } = await import("./boot-C-wL4WoP.mjs");
|
|
1204
1204
|
await runChat({
|
|
1205
1205
|
appUrl,
|
|
1206
1206
|
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-C-wL4WoP.mjs");
|
|
1515
1515
|
await runChat({
|
|
1516
1516
|
appUrl,
|
|
1517
1517
|
sessionToken: session.value.sessionToken,
|
|
@@ -1810,7 +1810,7 @@ const switchCommand = {
|
|
|
1810
1810
|
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.");
|
|
1811
1811
|
process.exit(1);
|
|
1812
1812
|
}
|
|
1813
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
1813
|
+
const { runWorkspacePicker } = await import("./boot-C-wL4WoP.mjs");
|
|
1814
1814
|
await runWorkspacePicker(session);
|
|
1815
1815
|
return;
|
|
1816
1816
|
}
|
|
@@ -2003,6 +2003,33 @@ function relativeTime(iso) {
|
|
|
2003
2003
|
return `${Math.floor(diffMs / 864e5)}d ago`;
|
|
2004
2004
|
}
|
|
2005
2005
|
|
|
2006
|
+
//#endregion
|
|
2007
|
+
//#region src/chat/conversation-ref.ts
|
|
2008
|
+
/**
|
|
2009
|
+
* Parsing the conversation reference a user hands `/conversation`.
|
|
2010
|
+
*
|
|
2011
|
+
* The whole point of the command is that you paste whatever you already have
|
|
2012
|
+
* in front of you, so every form the platform hands out has to work: a bare
|
|
2013
|
+
* id, the `https://skydive.com/c/<id>` link an agent replies with, or the
|
|
2014
|
+
* `skydive chat --resume <id>` line the TUI prints on exit. All three carry
|
|
2015
|
+
* exactly one id, so we pull the first uuid out of the string rather than
|
|
2016
|
+
* teaching this about URL shapes and command syntax.
|
|
2017
|
+
*
|
|
2018
|
+
* Validating here (instead of letting the API answer) is what turns a typo
|
|
2019
|
+
* into "not a conversation id" in the composer rather than a request that
|
|
2020
|
+
* cannot succeed.
|
|
2021
|
+
*/
|
|
2022
|
+
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
2023
|
+
/**
|
|
2024
|
+
* The conversation id in `input`, lowercased, or null when there isn't one.
|
|
2025
|
+
* Trailing punctuation, query strings and surrounding prose are tolerated —
|
|
2026
|
+
* a pasted link or command line is the expected input, not the exception.
|
|
2027
|
+
*/
|
|
2028
|
+
function parseConversationRef(input) {
|
|
2029
|
+
const match = uuidPattern.exec(input);
|
|
2030
|
+
return match ? match[0].toLowerCase() : null;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2006
2033
|
//#endregion
|
|
2007
2034
|
//#region src/chat/paste.ts
|
|
2008
2035
|
const MAX_PATH_PASTE_CHARS = 4096;
|
|
@@ -4095,8 +4122,8 @@ function TodoCardView({ todos, isActive }) {
|
|
|
4095
4122
|
//#endregion
|
|
4096
4123
|
//#region src/chat/tui/chat/footer-hints.ts
|
|
4097
4124
|
/** Build the chat footer copy independently of its colors and layout. */
|
|
4098
|
-
function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer,
|
|
4099
|
-
const connectHint =
|
|
4125
|
+
function formatChatFooterStatus({ runKind, ctrlCArmed, isShared, hasPendingSteer, actionableCardCount, nonWebChannel }) {
|
|
4126
|
+
const connectHint = actionableCardCount > 1 ? `ctrl+r connect (${actionableCardCount}) · ` : actionableCardCount === 1 ? "ctrl+r connect · " : "";
|
|
4100
4127
|
switch (runKind) {
|
|
4101
4128
|
case "idle":
|
|
4102
4129
|
if (ctrlCArmed) return "press ctrl+c again to quit";
|
|
@@ -4443,7 +4470,7 @@ const keybindGroups = [
|
|
|
4443
4470
|
},
|
|
4444
4471
|
{
|
|
4445
4472
|
keys: "ctrl+r",
|
|
4446
|
-
action: "
|
|
4473
|
+
action: "act on a waiting connect card (auth cards)"
|
|
4447
4474
|
},
|
|
4448
4475
|
{
|
|
4449
4476
|
keys: "pgup / pgdn",
|
|
@@ -4887,6 +4914,13 @@ const slashCommands = [
|
|
|
4887
4914
|
description: "start a new conversation with this agent",
|
|
4888
4915
|
action: { kind: "new-conversation" }
|
|
4889
4916
|
},
|
|
4917
|
+
{
|
|
4918
|
+
name: "conversation",
|
|
4919
|
+
aliases: ["conversations", "conv"],
|
|
4920
|
+
argsHint: "[id or link]",
|
|
4921
|
+
description: "switch conversation (no id: pick from the list)",
|
|
4922
|
+
action: { kind: "conversation" }
|
|
4923
|
+
},
|
|
4890
4924
|
{
|
|
4891
4925
|
name: "model",
|
|
4892
4926
|
aliases: [],
|
|
@@ -4926,7 +4960,7 @@ const slashCommands = [
|
|
|
4926
4960
|
name: "connect",
|
|
4927
4961
|
aliases: [],
|
|
4928
4962
|
argsHint: null,
|
|
4929
|
-
description: "
|
|
4963
|
+
description: "act on a waiting connect card (auth cards)",
|
|
4930
4964
|
action: { kind: "connect" }
|
|
4931
4965
|
},
|
|
4932
4966
|
{
|
|
@@ -4951,7 +4985,7 @@ const slashCommands = [
|
|
|
4951
4985
|
*
|
|
4952
4986
|
* An argument-less command only matches as the exact line — `/help me write
|
|
4953
4987
|
* a plan` stays an ordinary message rather than swallowing the user's text
|
|
4954
|
-
* into the help modal. Only `sandbox`
|
|
4988
|
+
* into the help modal. Only `sandbox` and `conversation` accept a rest.
|
|
4955
4989
|
*/
|
|
4956
4990
|
function matchSlashCommand(line) {
|
|
4957
4991
|
if (!line.startsWith("/")) return null;
|
|
@@ -5024,6 +5058,93 @@ function printResumeHint(write = (text) => process.stdout.write(text)) {
|
|
|
5024
5058
|
lastConversationId = null;
|
|
5025
5059
|
}
|
|
5026
5060
|
|
|
5061
|
+
//#endregion
|
|
5062
|
+
//#region src/chat/tui/screens/card-picker.tsx
|
|
5063
|
+
/**
|
|
5064
|
+
* Asks which connect card ctrl+r should act on. A conversation can hold
|
|
5065
|
+
* several cards waiting at once (a batch of `platform auth configure` calls),
|
|
5066
|
+
* and the chord can't guess which one the user means — picking the newest
|
|
5067
|
+
* silently strands the rest, since a card left in `launched` or `error` stays
|
|
5068
|
+
* actionable forever. Rendered as an overlay in place of the transcript, like
|
|
5069
|
+
* the model and theme pickers, so the conversation and any live run survive.
|
|
5070
|
+
*/
|
|
5071
|
+
function CardPicker({ cards, onSelect, onCancel }) {
|
|
5072
|
+
const { height } = useTerminalDimensions();
|
|
5073
|
+
const [highlight, setHighlight] = useState(0);
|
|
5074
|
+
const clamped = Math.min(highlight, Math.max(0, cards.length - 1));
|
|
5075
|
+
const visibleRows = Math.max(3, height - 6);
|
|
5076
|
+
const start = windowStart(clamped, cards.length, visibleRows);
|
|
5077
|
+
const windowed = cards.slice(start, start + visibleRows);
|
|
5078
|
+
useKeyboard((key) => {
|
|
5079
|
+
if (key.name === "escape") {
|
|
5080
|
+
onCancel();
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
if (key.name === "up") setHighlight(Math.max(0, clamped - 1));
|
|
5084
|
+
else if (key.name === "down") setHighlight(Math.min(Math.max(0, cards.length - 1), clamped + 1));
|
|
5085
|
+
else if (key.name === "return") {
|
|
5086
|
+
const card = cards[clamped];
|
|
5087
|
+
if (card) onSelect(card);
|
|
5088
|
+
}
|
|
5089
|
+
});
|
|
5090
|
+
useEffect(() => {
|
|
5091
|
+
if (cards.length === 0) onCancel();
|
|
5092
|
+
}, [cards.length, onCancel]);
|
|
5093
|
+
if (cards.length === 0) return null;
|
|
5094
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
5095
|
+
style: {
|
|
5096
|
+
flexDirection: "column",
|
|
5097
|
+
flexGrow: 1
|
|
5098
|
+
},
|
|
5099
|
+
children: [
|
|
5100
|
+
/* @__PURE__ */ jsx("text", {
|
|
5101
|
+
fg: theme.accent,
|
|
5102
|
+
children: "connect"
|
|
5103
|
+
}),
|
|
5104
|
+
/* @__PURE__ */ jsx("box", {
|
|
5105
|
+
style: {
|
|
5106
|
+
flexDirection: "column",
|
|
5107
|
+
flexGrow: 1,
|
|
5108
|
+
marginTop: 1
|
|
5109
|
+
},
|
|
5110
|
+
children: windowed.map((card, i) => {
|
|
5111
|
+
const selected = start + i === clamped;
|
|
5112
|
+
const waiting = waitingOn(card);
|
|
5113
|
+
const label = card.card.button?.label ?? "";
|
|
5114
|
+
return /* @__PURE__ */ jsxs("text", {
|
|
5115
|
+
fg: selected ? theme.accent : theme.fg,
|
|
5116
|
+
children: [
|
|
5117
|
+
selected ? "› " : " ",
|
|
5118
|
+
"⚿ ",
|
|
5119
|
+
card.card.title,
|
|
5120
|
+
label ? /* @__PURE__ */ jsxs("span", {
|
|
5121
|
+
fg: theme.muted,
|
|
5122
|
+
children: [" · ", label]
|
|
5123
|
+
}) : null,
|
|
5124
|
+
waiting ? /* @__PURE__ */ jsxs("span", {
|
|
5125
|
+
fg: theme.dim,
|
|
5126
|
+
children: [" · ", waiting]
|
|
5127
|
+
}) : null
|
|
5128
|
+
]
|
|
5129
|
+
}, card.id);
|
|
5130
|
+
})
|
|
5131
|
+
}),
|
|
5132
|
+
/* @__PURE__ */ jsxs("text", {
|
|
5133
|
+
fg: theme.dim,
|
|
5134
|
+
children: [cards.length, " waiting · ↑/↓ move · ↵ select · esc cancel"]
|
|
5135
|
+
})
|
|
5136
|
+
]
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
5139
|
+
/** Why a card is still on the list, for the rows that aren't simply untouched. */
|
|
5140
|
+
function waitingOn(card) {
|
|
5141
|
+
switch (card.status) {
|
|
5142
|
+
case "launched": return "finish in your browser";
|
|
5143
|
+
case "error": return card.detail ?? "failed";
|
|
5144
|
+
default: return null;
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5027
5148
|
//#endregion
|
|
5028
5149
|
//#region src/chat/tui/screens/model-picker.tsx
|
|
5029
5150
|
/**
|
|
@@ -7882,6 +8003,10 @@ function routeInput(raw) {
|
|
|
7882
8003
|
kind: "sandbox-exec",
|
|
7883
8004
|
command: match.rest
|
|
7884
8005
|
} : { kind: "sandbox-pty" };
|
|
8006
|
+
case "conversation": return match.rest ? {
|
|
8007
|
+
kind: "open-conversation",
|
|
8008
|
+
ref: match.rest
|
|
8009
|
+
} : { kind: "conversation-picker" };
|
|
7885
8010
|
case "new-conversation":
|
|
7886
8011
|
case "model-picker":
|
|
7887
8012
|
case "theme-picker":
|
|
@@ -8087,6 +8212,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8087
8212
|
const [input, setInput] = useState("");
|
|
8088
8213
|
const [run, setRun] = useState({ kind: "idle" });
|
|
8089
8214
|
const [model, setModel] = useState(agent.model ?? null);
|
|
8215
|
+
const [cardPickerOpen, setCardPickerOpen] = useState(false);
|
|
8090
8216
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
8091
8217
|
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
|
8092
8218
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
@@ -8200,6 +8326,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8200
8326
|
};
|
|
8201
8327
|
}, [
|
|
8202
8328
|
grantPrompt,
|
|
8329
|
+
cardPickerOpen,
|
|
8203
8330
|
modelPickerOpen,
|
|
8204
8331
|
themePickerOpen,
|
|
8205
8332
|
helpOpen,
|
|
@@ -8555,10 +8682,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8555
8682
|
portalClient,
|
|
8556
8683
|
portal.status
|
|
8557
8684
|
]);
|
|
8558
|
-
const
|
|
8559
|
-
const
|
|
8560
|
-
|
|
8561
|
-
|
|
8685
|
+
const promptForCards = useCallback(() => {
|
|
8686
|
+
const targets = itemsRef.current.filter(isActionableCard);
|
|
8687
|
+
const [only] = targets;
|
|
8688
|
+
if (!only) return false;
|
|
8689
|
+
if (targets.length === 1) {
|
|
8690
|
+
runCardAction(only);
|
|
8691
|
+
return true;
|
|
8692
|
+
}
|
|
8693
|
+
setCardPickerOpen(true);
|
|
8562
8694
|
return true;
|
|
8563
8695
|
}, [runCardAction]);
|
|
8564
8696
|
const handleCredInput = useCallback((displayed) => {
|
|
@@ -8803,7 +8935,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8803
8935
|
stageAttachment(image);
|
|
8804
8936
|
}, [stageAttachment]);
|
|
8805
8937
|
usePaste((event) => {
|
|
8806
|
-
if (modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt || nonWebChannel) return;
|
|
8938
|
+
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen || credPromptOpen || grantPrompt || nonWebChannel) return;
|
|
8807
8939
|
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
8808
8940
|
const route = routePaste({
|
|
8809
8941
|
kind: event.metadata?.kind,
|
|
@@ -8852,6 +8984,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8852
8984
|
if (composer.plainText === "" && inputRef.current !== "") applyComposerText(inputRef.current, "end");
|
|
8853
8985
|
}, [
|
|
8854
8986
|
grantPrompt,
|
|
8987
|
+
cardPickerOpen,
|
|
8855
8988
|
modelPickerOpen,
|
|
8856
8989
|
themePickerOpen,
|
|
8857
8990
|
helpOpen,
|
|
@@ -8922,6 +9055,64 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8922
9055
|
agent,
|
|
8923
9056
|
goTo
|
|
8924
9057
|
]);
|
|
9058
|
+
/**
|
|
9059
|
+
* Leave this conversation for another screen without ending its run: drop
|
|
9060
|
+
* our own SSE subscription and nothing else — never `rest.cancelRun` — so
|
|
9061
|
+
* the agent keeps working server-side and reopening the thread reattaches.
|
|
9062
|
+
* Same contract as `esc`, which is why `/conversation` is allowed mid-run
|
|
9063
|
+
* where `/new` is not: what you land on still lists the thread you left,
|
|
9064
|
+
* so the run is never stranded out of reach.
|
|
9065
|
+
*/
|
|
9066
|
+
const leaveForScreen = useCallback((next) => {
|
|
9067
|
+
const current = runRef.current;
|
|
9068
|
+
if (current.kind === "streaming") current.abort.abort();
|
|
9069
|
+
if (current.kind !== "idle") useStore.getState().showToast({ message: `${agent.name} keeps working — reopen to reattach` });
|
|
9070
|
+
goTo(next);
|
|
9071
|
+
}, [agent.name, goTo]);
|
|
9072
|
+
/**
|
|
9073
|
+
* `/conversation <id or link>`: open that conversation without leaving the
|
|
9074
|
+
* TUI — the point being that the id usually arrives *from* an agent, in the
|
|
9075
|
+
* session you started to go find it.
|
|
9076
|
+
*
|
|
9077
|
+
* Resolution goes through `resolveInitialScreen`, the same path
|
|
9078
|
+
* `--resume <id>` takes, so an id belonging to a different agent opens
|
|
9079
|
+
* under that agent and the not-found / no-access wording lives in one
|
|
9080
|
+
* place.
|
|
9081
|
+
*/
|
|
9082
|
+
const openConversationRef = useCallback(async (ref) => {
|
|
9083
|
+
const target = parseConversationRef(ref);
|
|
9084
|
+
if (!target) {
|
|
9085
|
+
setItems((prev) => [...prev, {
|
|
9086
|
+
kind: "error",
|
|
9087
|
+
id: crypto.randomUUID(),
|
|
9088
|
+
text: `not a conversation id: ${ref} — pass an id, its /c/<id> link, or run /conversation on its own to pick from the list`
|
|
9089
|
+
}]);
|
|
9090
|
+
return;
|
|
9091
|
+
}
|
|
9092
|
+
if (!rest) return;
|
|
9093
|
+
if (target === conversationId) {
|
|
9094
|
+
useStore.getState().showToast({ message: "already in this conversation" });
|
|
9095
|
+
return;
|
|
9096
|
+
}
|
|
9097
|
+
try {
|
|
9098
|
+
leaveForScreen(await resolveInitialScreen({
|
|
9099
|
+
rest,
|
|
9100
|
+
agentSelector: null,
|
|
9101
|
+
conversationId: target,
|
|
9102
|
+
newConversation: false
|
|
9103
|
+
}));
|
|
9104
|
+
} catch (err) {
|
|
9105
|
+
setItems((prev) => [...prev, {
|
|
9106
|
+
kind: "error",
|
|
9107
|
+
id: crypto.randomUUID(),
|
|
9108
|
+
text: `couldn't open ${target}: ${errorMessage(err)}`
|
|
9109
|
+
}]);
|
|
9110
|
+
}
|
|
9111
|
+
}, [
|
|
9112
|
+
rest,
|
|
9113
|
+
conversationId,
|
|
9114
|
+
leaveForScreen
|
|
9115
|
+
]);
|
|
8925
9116
|
const toggleShare = useCallback(() => {
|
|
8926
9117
|
if (!portalClient) return;
|
|
8927
9118
|
if (portal.status === "off") {
|
|
@@ -8993,6 +9184,15 @@ function ChatScreen({ agent, conversation }) {
|
|
|
8993
9184
|
}
|
|
8994
9185
|
});
|
|
8995
9186
|
break;
|
|
9187
|
+
case "conversation-picker":
|
|
9188
|
+
leaveForScreen({
|
|
9189
|
+
kind: "conversation-picker",
|
|
9190
|
+
agent
|
|
9191
|
+
});
|
|
9192
|
+
break;
|
|
9193
|
+
case "open-conversation":
|
|
9194
|
+
openConversationRef(routed.ref);
|
|
9195
|
+
break;
|
|
8996
9196
|
case "model-picker":
|
|
8997
9197
|
setModelPickerOpen(true);
|
|
8998
9198
|
break;
|
|
@@ -9024,7 +9224,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9024
9224
|
openInBrowser();
|
|
9025
9225
|
break;
|
|
9026
9226
|
case "connect":
|
|
9027
|
-
if (!
|
|
9227
|
+
if (!promptForCards()) useStore.getState().showToast({ message: "no connect card waiting for action" });
|
|
9028
9228
|
break;
|
|
9029
9229
|
case "help":
|
|
9030
9230
|
setHelpOpen(true);
|
|
@@ -9051,6 +9251,8 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9051
9251
|
runLocalShell,
|
|
9052
9252
|
runSandboxExec,
|
|
9053
9253
|
runSandboxPty,
|
|
9254
|
+
leaveForScreen,
|
|
9255
|
+
openConversationRef,
|
|
9054
9256
|
toggleShare,
|
|
9055
9257
|
openInBrowser,
|
|
9056
9258
|
quit,
|
|
@@ -9063,7 +9265,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9063
9265
|
model,
|
|
9064
9266
|
portal.status,
|
|
9065
9267
|
portal.grantedAgentIds,
|
|
9066
|
-
|
|
9268
|
+
promptForCards
|
|
9067
9269
|
]);
|
|
9068
9270
|
const submit = useCallback(() => {
|
|
9069
9271
|
const staged = attachments.filter((a) => a.status !== "error");
|
|
@@ -9118,7 +9320,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9118
9320
|
}
|
|
9119
9321
|
if (reviewFocused) return;
|
|
9120
9322
|
}
|
|
9121
|
-
if (modelPickerOpen || themePickerOpen || helpOpen) return;
|
|
9323
|
+
if (cardPickerOpen || modelPickerOpen || themePickerOpen || helpOpen) return;
|
|
9122
9324
|
if (grantPrompt) {
|
|
9123
9325
|
if (key.name === "y") {
|
|
9124
9326
|
confirmGrant();
|
|
@@ -9210,7 +9412,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9210
9412
|
return;
|
|
9211
9413
|
}
|
|
9212
9414
|
if (key.name === "r" && key.ctrl) {
|
|
9213
|
-
|
|
9415
|
+
promptForCards();
|
|
9214
9416
|
return;
|
|
9215
9417
|
}
|
|
9216
9418
|
if (key.name === "t" && key.ctrl) {
|
|
@@ -9286,23 +9488,31 @@ function ChatScreen({ agent, conversation }) {
|
|
|
9286
9488
|
}
|
|
9287
9489
|
});
|
|
9288
9490
|
const hasPendingSteer = items.some((m) => m.kind === "pending-steer");
|
|
9289
|
-
const
|
|
9491
|
+
const actionableCards = useMemo(() => items.filter(isActionableCard), [items]);
|
|
9290
9492
|
const statusText = useMemo(() => formatChatFooterStatus({
|
|
9291
9493
|
runKind: run.kind,
|
|
9292
9494
|
ctrlCArmed,
|
|
9293
9495
|
isShared,
|
|
9294
9496
|
hasPendingSteer,
|
|
9295
|
-
|
|
9497
|
+
actionableCardCount: actionableCards.length,
|
|
9296
9498
|
nonWebChannel: nonWebChannel !== null
|
|
9297
9499
|
}), [
|
|
9298
9500
|
run.kind,
|
|
9299
9501
|
ctrlCArmed,
|
|
9300
9502
|
isShared,
|
|
9301
9503
|
hasPendingSteer,
|
|
9302
|
-
|
|
9504
|
+
actionableCards.length,
|
|
9303
9505
|
nonWebChannel
|
|
9304
9506
|
]);
|
|
9305
9507
|
const modelHint = formatModelHint(model);
|
|
9508
|
+
if (cardPickerOpen) return /* @__PURE__ */ jsx(CardPicker, {
|
|
9509
|
+
cards: actionableCards,
|
|
9510
|
+
onSelect: (card) => {
|
|
9511
|
+
setCardPickerOpen(false);
|
|
9512
|
+
runCardAction(card);
|
|
9513
|
+
},
|
|
9514
|
+
onCancel: () => setCardPickerOpen(false)
|
|
9515
|
+
});
|
|
9306
9516
|
if (modelPickerOpen) return /* @__PURE__ */ jsx(ModelPicker, {
|
|
9307
9517
|
agentId: agent.id,
|
|
9308
9518
|
currentModel: model,
|