skydive-cli 0.6.0-beta.16 → 0.6.0-beta.18
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 +19 -19
- package/dist/js/{boot-BqjMpVtf.mjs → boot-BcyHfvnh.mjs} +877 -560
- package/dist/js/client-BpD6yakI.mjs +5 -0
- package/dist/js/{client-CcN2hZil.mjs → client-DG0vUvWI.mjs} +1 -1
- package/dist/js/{daemon-Db75IWGi.mjs → daemon-DZ9YSkAh.mjs} +2 -2
- package/dist/js/daemon-client-BwyXZn0g.mjs +8 -0
- package/dist/js/{daemon-client-Bbjns6Wk.mjs → daemon-client-D-k67MEB.mjs} +1 -1
- package/dist/js/{daemon-BioFi0dP.mjs → daemon-rHe1xapN.mjs} +1 -1
- package/dist/js/{install-iZWrGKZJ.mjs → install-Dvl9vzTP.mjs} +2 -2
- package/dist/js/{print-CdbXPgQk.mjs → print-CCXpClJw.mjs} +1 -1
- package/dist/js/{print-BthJ13u6.mjs → print-COTmamJX.mjs} +2 -2
- package/dist/js/{print-share-BMuayLCB.mjs → print-share-34Iwxo39.mjs} +1 -1
- package/dist/js/{rest-C5WNKecM.mjs → rest-D2MDvQ6c.mjs} +1 -1
- package/dist/js/{rest-ToPnfmTk.mjs → rest-DZCq0tZw.mjs} +1 -1
- package/package.json +5 -5
- package/dist/js/client-CN1TUPnh.mjs +0 -5
- package/dist/js/daemon-client-QOL87qzQ.mjs +0 -8
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as errorMessage, i as sendErrorMessage, n as errorDetail, o as isRecord, t as createRestClient } from "./rest-
|
|
3
|
-
import { C as DEFAULT_APP_URL, E as getConfigPath, K as saveTheme, L as recordDefaultAgent, M as getSavedTheme, S as DEFAULT_API_URL, U as resolveWebUrl, _ as parseConnectCard, c as actionableCardVerb, d as parseOauthConnectParams, f as prioritizeActionableCards, g as computeSettledLabel, h as composeQuestionAnswer, i as resolveAgent, j as getReviewStateDir, l as cardActionErrorMessage, m as resolveConnectUrl, p as reconcileMaskedInput, s as MASK_CHAR, u as parseExternalOauthConnectParams, v as specKeyFor } from "./print-
|
|
4
|
-
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-
|
|
2
|
+
import { a as errorMessage, i as sendErrorMessage, n as errorDetail, o as isRecord, t as createRestClient } from "./rest-DZCq0tZw.mjs";
|
|
3
|
+
import { C as DEFAULT_APP_URL, E as getConfigPath, K as saveTheme, L as recordDefaultAgent, M as getSavedTheme, S as DEFAULT_API_URL, U as resolveWebUrl, _ as parseConnectCard, c as actionableCardVerb, d as parseOauthConnectParams, f as prioritizeActionableCards, g as computeSettledLabel, h as composeQuestionAnswer, i as resolveAgent, j as getReviewStateDir, l as cardActionErrorMessage, m as resolveConnectUrl, p as reconcileMaskedInput, s as MASK_CHAR, u as parseExternalOauthConnectParams, v as specKeyFor } from "./print-CCXpClJw.mjs";
|
|
4
|
+
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-Dvl9vzTP.mjs";
|
|
5
5
|
import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
|
|
6
6
|
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-D3l5kJlX.mjs";
|
|
7
|
-
import { t as PortalClient } from "./client-
|
|
7
|
+
import { t as PortalClient } from "./client-DG0vUvWI.mjs";
|
|
8
8
|
import { t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
|
|
9
9
|
import "./api-BFQ4PQDA.mjs";
|
|
10
|
-
import "./daemon-
|
|
10
|
+
import "./daemon-rHe1xapN.mjs";
|
|
11
11
|
import { t as SandboxStream } from "./client-l6orYfqq.mjs";
|
|
12
|
-
import { t as PortalDaemonClient } from "./daemon-client-
|
|
12
|
+
import { t as PortalDaemonClient } from "./daemon-client-D-k67MEB.mjs";
|
|
13
13
|
import { t as runRawPtyPassthrough } from "./raw-pty-dndn1Kxc.mjs";
|
|
14
14
|
import * as os$1 from "node:os";
|
|
15
15
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
@@ -137,7 +137,6 @@ const useStore = create((set, get) => ({
|
|
|
137
137
|
toast: null,
|
|
138
138
|
seedPrompt: null,
|
|
139
139
|
autoGrantMachine: false,
|
|
140
|
-
newConversationIntent: false,
|
|
141
140
|
forcedOnboarding: false,
|
|
142
141
|
earlyInput: "",
|
|
143
142
|
goTo: (screen) => set({
|
|
@@ -221,20 +220,20 @@ const useStore = create((set, get) => ({
|
|
|
221
220
|
/**
|
|
222
221
|
* Decide the screen the chat TUI boots into.
|
|
223
222
|
*
|
|
224
|
-
* With no selector we open the
|
|
225
|
-
* `defaultAgent`, in which case a bare
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* stored selector that no longer resolves degrades silently
|
|
231
|
-
*
|
|
232
|
-
* archived, another one claiming the same name), and unlike a
|
|
233
|
-
* `--agent` there is no command line to correct — and picking an
|
|
234
|
-
* there heals the default on its own.
|
|
223
|
+
* With no selector we open the welcome screen (screen 1 of the three-screen
|
|
224
|
+
* flow) — unless the config stores a `defaultAgent`, in which case a bare
|
|
225
|
+
* `skydive chat` opens that agent's conversation home directly (its centered
|
|
226
|
+
* composer + conversation list). The default is maintained automatically
|
|
227
|
+
* (every conversation the TUI opens records its agent, so a bare launch
|
|
228
|
+
* returns to whoever you talked to last — the dominant launch pattern), and
|
|
229
|
+
* `skydive config set defaultAgent` can seed or override it. A stored selector that no longer resolves degrades silently
|
|
230
|
+
* to the welcome screen rather than an error screen: config goes stale (agent
|
|
231
|
+
* renamed, archived, another one claiming the same name), and unlike a
|
|
232
|
+
* mistyped `--agent` there is no command line to correct — and picking an
|
|
233
|
+
* agent there heals the default on its own.
|
|
235
234
|
*
|
|
236
235
|
* With `--agent` (`skydive chat --agent grace`, no `-p`) we resolve it
|
|
237
|
-
* against the org roster and open that agent's conversation
|
|
236
|
+
* against the org roster and open that agent's conversation home, skipping
|
|
238
237
|
* the picker. Resolution reuses the same `resolveAgent` rules as the `-p`
|
|
239
238
|
* path so a given selector behaves identically in both modes: an id, or a
|
|
240
239
|
* unique case-insensitive slug/name; ambiguous or unknown selectors throw
|
|
@@ -251,8 +250,9 @@ const useStore = create((set, get) => ({
|
|
|
251
250
|
* needs every candidate is disambiguating a fuzzy name/slug selector; an id
|
|
252
251
|
* (or the `--resume` conversation's agentId) is exact and resolves with a
|
|
253
252
|
* single `getAgent`, and `--new` with no selector only needs "exactly one
|
|
254
|
-
* agent, or not", which a two-row page answers. A
|
|
255
|
-
* the
|
|
253
|
+
* agent, or not", which a two-row page answers. A bare launch lands straight
|
|
254
|
+
* on the welcome screen, which loads the roster itself. A large org must never
|
|
255
|
+
* sit on the `connecting…` splash while the roster streams in.
|
|
256
256
|
*
|
|
257
257
|
* Kept out of the React component so it's unit-testable under Node (the
|
|
258
258
|
* component itself can't render without Bun + OpenTUI).
|
|
@@ -262,7 +262,7 @@ const useStore = create((set, get) => ({
|
|
|
262
262
|
* org roster. A name/slug selector is the only case that can be ambiguous and
|
|
263
263
|
* therefore still needs the full candidate list to disambiguate. */
|
|
264
264
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
265
|
-
async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation, defaultAgentSelector, forceOnboarding }) {
|
|
265
|
+
async function resolveInitialScreen({ rest, agentSelector, conversationId, newConversation, seedPrompt, defaultAgentSelector, forceOnboarding }) {
|
|
266
266
|
if (conversationId) {
|
|
267
267
|
const conversation = await rest.getConversation({ conversationId }).catch((err) => {
|
|
268
268
|
if (err instanceof HttpError && err.status === 404) throw new Error(`No conversation with id ${conversationId}. It may have been deleted, or belong to another workspace.`);
|
|
@@ -294,14 +294,8 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
294
294
|
parentConversationId: conversation.parentConversationId ?? null
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
|
-
if (forceOnboarding && !agentSelector) return { kind: "
|
|
298
|
-
if (!agentSelector && !newConversation && !defaultAgentSelector) {
|
|
299
|
-
if ((await rest.listAgents({
|
|
300
|
-
scope: "org",
|
|
301
|
-
onPage: null
|
|
302
|
-
})).length === 0) return { kind: "first-run" };
|
|
303
|
-
return { kind: "agent-picker" };
|
|
304
|
-
}
|
|
297
|
+
if (forceOnboarding && !agentSelector) return { kind: "welcome" };
|
|
298
|
+
if (!agentSelector && !newConversation && seedPrompt === null && !defaultAgentSelector) return { kind: "welcome" };
|
|
305
299
|
if (!agentSelector && defaultAgentSelector) {
|
|
306
300
|
const agent = UUID_RE.test(defaultAgentSelector) ? await rest.getAgent({ agentId: defaultAgentSelector }).catch((err) => {
|
|
307
301
|
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) return null;
|
|
@@ -310,31 +304,13 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
310
304
|
scope: "org",
|
|
311
305
|
onPage: null
|
|
312
306
|
}), defaultAgentSelector);
|
|
313
|
-
if (agent) return
|
|
314
|
-
return { kind: "
|
|
315
|
-
}
|
|
316
|
-
if (agentSelector && UUID_RE.test(agentSelector)) {
|
|
317
|
-
const agent = await rest.getAgent({ agentId: agentSelector }).catch((err) => {
|
|
318
|
-
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`No agent with id ${agentSelector}. It may not exist, or belong to another workspace.`);
|
|
319
|
-
throw err;
|
|
320
|
-
});
|
|
321
|
-
if (newConversation) return {
|
|
322
|
-
kind: "chat",
|
|
323
|
-
agent,
|
|
324
|
-
attachRunId: null,
|
|
325
|
-
parentConversationId: null,
|
|
326
|
-
conversation: {
|
|
327
|
-
kind: "new",
|
|
328
|
-
agentId: agent.id,
|
|
329
|
-
nonce: crypto.randomUUID()
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
return {
|
|
333
|
-
kind: "conversation-picker",
|
|
334
|
-
agent,
|
|
335
|
-
initialQuery: ""
|
|
336
|
-
};
|
|
307
|
+
if (agent) return openAgentHome(agent, seedPrompt);
|
|
308
|
+
return { kind: "welcome" };
|
|
337
309
|
}
|
|
310
|
+
if (agentSelector && UUID_RE.test(agentSelector)) return openAgentHome(await rest.getAgent({ agentId: agentSelector }).catch((err) => {
|
|
311
|
+
if (err instanceof HttpError && (err.status === 404 || err.status === 403)) throw new Error(`No agent with id ${agentSelector}. It may not exist, or belong to another workspace.`);
|
|
312
|
+
throw err;
|
|
313
|
+
}), seedPrompt);
|
|
338
314
|
if (!agentSelector && newConversation) {
|
|
339
315
|
const firstTwo = await rest.listAgents({
|
|
340
316
|
scope: "org",
|
|
@@ -342,14 +318,24 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
|
|
|
342
318
|
limit: 2
|
|
343
319
|
});
|
|
344
320
|
const only = firstTwo.length === 1 ? firstTwo[0] : null;
|
|
345
|
-
if (!only) return { kind: "
|
|
346
|
-
return
|
|
321
|
+
if (!only) return { kind: "welcome" };
|
|
322
|
+
return openAgentHome(only, seedPrompt);
|
|
347
323
|
}
|
|
348
|
-
|
|
324
|
+
return openAgentHome(resolveAgent(await rest.listAgents({
|
|
349
325
|
scope: "org",
|
|
350
326
|
onPage: null
|
|
351
|
-
}), agentSelector);
|
|
352
|
-
|
|
327
|
+
}), agentSelector), seedPrompt);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Land on the agent's conversation home — the single new-conversation surface
|
|
331
|
+
* (centered composer + conversation list). The one exception is a seeded
|
|
332
|
+
* launch (`skydive import`): there is a message queued to auto-send, so boot
|
|
333
|
+
* straight into a fresh chat (`kind: 'new'`) that fires it. Without a seed
|
|
334
|
+
* there is nothing to send, so the composer on the home is where a new
|
|
335
|
+
* conversation starts — the legacy full-width empty-chat landing is gone.
|
|
336
|
+
*/
|
|
337
|
+
function openAgentHome(agent, seedPrompt) {
|
|
338
|
+
if (seedPrompt !== null) return newChat(agent);
|
|
353
339
|
return {
|
|
354
340
|
kind: "conversation-picker",
|
|
355
341
|
agent,
|
|
@@ -1831,9 +1817,7 @@ function screenLabelParts(screen, chatTitle) {
|
|
|
1831
1817
|
switch (screen.kind) {
|
|
1832
1818
|
case "splash": return { title: "" };
|
|
1833
1819
|
case "error": return { title: "" };
|
|
1834
|
-
case "
|
|
1835
|
-
case "agent-create": return { title: "create an agent" };
|
|
1836
|
-
case "first-run": return { title: "welcome" };
|
|
1820
|
+
case "welcome": return { title: "welcome" };
|
|
1837
1821
|
case "workspace-picker": return { title: "switch workspace" };
|
|
1838
1822
|
case "conversation-picker": return {
|
|
1839
1823
|
title: "pick a conversation",
|
|
@@ -2103,26 +2087,185 @@ function SkydiveMark({ variant = "full" }) {
|
|
|
2103
2087
|
}
|
|
2104
2088
|
|
|
2105
2089
|
//#endregion
|
|
2106
|
-
//#region src/chat/tui/
|
|
2107
|
-
|
|
2090
|
+
//#region src/chat/tui/use-terminal-ready.ts
|
|
2091
|
+
/**
|
|
2092
|
+
* The 80x24 size the CLI renderer boots on when it cannot read the terminal's
|
|
2093
|
+
* real size synchronously (emulators / multiplexers that report size via an
|
|
2094
|
+
* async `resize` after start). A terminal reporting its size synchronously
|
|
2095
|
+
* boots on its true size instead — which is almost never exactly 80x24. See
|
|
2096
|
+
* `@opentui/core`: the renderer seeds width from `process.stdout.columns || 80`
|
|
2097
|
+
* and height from `process.stdout.rows || 24`.
|
|
2098
|
+
*/
|
|
2099
|
+
const FALLBACK_WIDTH = 80;
|
|
2100
|
+
const FALLBACK_HEIGHT = 24;
|
|
2101
|
+
/**
|
|
2102
|
+
* Last-resort reveal delay for the ambiguous fallback boot (booted 80x24 with
|
|
2103
|
+
* no readable `process.stdout` size and no resize ever arriving — a non-tty
|
|
2104
|
+
* pipe, say). It is deliberately longer than the renderer's 100ms resize
|
|
2105
|
+
* debounce (`resizeDebounceDelay`), so a real async terminal's first resize
|
|
2106
|
+
* always wins this race and the brand's first *visible* paint is at the true
|
|
2107
|
+
* width. On the genuinely-80x24 path this timer never runs (we reveal from
|
|
2108
|
+
* `stdout.columns`), so its length costs a real terminal nothing.
|
|
2109
|
+
*/
|
|
2110
|
+
const REVEAL_FALLBACK_MS = 250;
|
|
2111
|
+
/** The terminal's real column/row count if the tty reports it synchronously,
|
|
2112
|
+
* else null. On an async-sizing emulator `process.stdout.columns` is populated
|
|
2113
|
+
* shortly after boot even while the renderer is still on its 80x24 fallback, so
|
|
2114
|
+
* this is the cheapest way to learn the true size without waiting on the
|
|
2115
|
+
* debounced resize. A non-tty (pipe/redirect) has no size and returns null. */
|
|
2116
|
+
function stdoutSize() {
|
|
2117
|
+
const width = process.stdout.columns;
|
|
2118
|
+
const height = process.stdout.rows;
|
|
2119
|
+
return typeof width === "number" && typeof height === "number" ? {
|
|
2120
|
+
width,
|
|
2121
|
+
height
|
|
2122
|
+
} : null;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Terminal dimensions plus a `ready` flag that gates the first centered paint,
|
|
2126
|
+
* to kill the welcome screen's left→center brand flash (PR #20856).
|
|
2127
|
+
*
|
|
2128
|
+
* `useTerminalDimensions` seeds from `renderer.width`/`height`. A terminal that
|
|
2129
|
+
* reports its size synchronously already has the real size on frame one, so
|
|
2130
|
+
* `ready` starts true and nothing is gated — no flash is possible because the
|
|
2131
|
+
* width never changes.
|
|
2132
|
+
*
|
|
2133
|
+
* A terminal that sizes asynchronously (tmux, cmux, some emulators) boots on
|
|
2134
|
+
* the 80x24 fallback and the real size lands via `resize` — but the renderer
|
|
2135
|
+
* DEBOUNCES that resize by 100ms, so a naive "reveal on the next tick" reveals
|
|
2136
|
+
* the brand centered-within-80 first and only jumps it to the real width a
|
|
2137
|
+
* tenth of a second later: exactly the flash reviewers saw. So on a fallback
|
|
2138
|
+
* boot we hold a blank full-size frame and reveal only once we actually know
|
|
2139
|
+
* the final width, from one of three signals, in order:
|
|
2140
|
+
*
|
|
2141
|
+
* 1. `process.stdout.columns` — the OS-reported tty size, populated well
|
|
2142
|
+
* before the debounced resize fires. If it reports a size other than
|
|
2143
|
+
* 80x24, that is the truth and the imminent resize will bring it, so we
|
|
2144
|
+
* stay gated until the resize actually lands (the width the brand centers
|
|
2145
|
+
* within must be the one on screen). If it reports exactly 80x24, the
|
|
2146
|
+
* terminal really is 80 wide — the 80-col centering is already final, so
|
|
2147
|
+
* reveal immediately with no flash.
|
|
2148
|
+
* 2. the `resize` event — the definitive real size for an async terminal;
|
|
2149
|
+
* reveal the instant it arrives, already at the final width.
|
|
2150
|
+
* 3. a `REVEAL_FALLBACK_MS` timer — only for a non-tty with no size and no
|
|
2151
|
+
* resize (a pipe), so it is never left blank forever. Longer than the
|
|
2152
|
+
* 100ms debounce so a real resize always beats it.
|
|
2153
|
+
*/
|
|
2154
|
+
function useTerminalReady() {
|
|
2155
|
+
const { width, height } = useTerminalDimensions();
|
|
2156
|
+
const renderer = useRenderer();
|
|
2157
|
+
const bootedOnFallback = width === FALLBACK_WIDTH && height === FALLBACK_HEIGHT;
|
|
2158
|
+
const [ready, setReady] = useState(() => {
|
|
2159
|
+
if (!bootedOnFallback) return true;
|
|
2160
|
+
const size = stdoutSize();
|
|
2161
|
+
return size?.width === FALLBACK_WIDTH && size?.height === FALLBACK_HEIGHT;
|
|
2162
|
+
});
|
|
2163
|
+
useEffect(() => {
|
|
2164
|
+
if (ready) return;
|
|
2165
|
+
let done = false;
|
|
2166
|
+
const settle = () => {
|
|
2167
|
+
if (done) return;
|
|
2168
|
+
done = true;
|
|
2169
|
+
setReady(true);
|
|
2170
|
+
};
|
|
2171
|
+
renderer.on("resize", settle);
|
|
2172
|
+
const size = stdoutSize();
|
|
2173
|
+
if (size?.width === FALLBACK_WIDTH && size?.height === FALLBACK_HEIGHT) settle();
|
|
2174
|
+
const t = setTimeout(settle, REVEAL_FALLBACK_MS);
|
|
2175
|
+
return () => {
|
|
2176
|
+
renderer.off("resize", settle);
|
|
2177
|
+
clearTimeout(t);
|
|
2178
|
+
};
|
|
2179
|
+
}, [renderer, ready]);
|
|
2180
|
+
return {
|
|
2181
|
+
width,
|
|
2182
|
+
height,
|
|
2183
|
+
ready
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
//#endregion
|
|
2188
|
+
//#region src/chat/tui/agent-name.ts
|
|
2189
|
+
function parseAgentName(value) {
|
|
2190
|
+
const name = value.trim();
|
|
2191
|
+
if (!name) return {
|
|
2192
|
+
ok: false,
|
|
2193
|
+
message: "Enter a name."
|
|
2194
|
+
};
|
|
2195
|
+
if (name.length > 120) return {
|
|
2196
|
+
ok: false,
|
|
2197
|
+
message: "Use 120 characters or fewer."
|
|
2198
|
+
};
|
|
2199
|
+
return {
|
|
2200
|
+
ok: true,
|
|
2201
|
+
name
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
//#endregion
|
|
2206
|
+
//#region src/chat/tui/screens/welcome.tsx
|
|
2207
|
+
/**
|
|
2208
|
+
* Screen 1 of the three-screen chat flow: **Welcome to Skydive**.
|
|
2209
|
+
*
|
|
2210
|
+
* A single centered screen (opencode fixed-width column) that is the entry
|
|
2211
|
+
* point whenever no agent is pinned. It folds together what used to be three
|
|
2212
|
+
* separate screens — the agent picker, the create-agent form, and the
|
|
2213
|
+
* first-run onboarding — so the flow is exactly: welcome → agent page → chat.
|
|
2214
|
+
*
|
|
2215
|
+
* Modes, all rendered in the same centered column:
|
|
2216
|
+
* - `list`: the workspace's agents, fuzzy-filterable. Enter opens the
|
|
2217
|
+
* highlighted agent's home (the agent page); tab flips mine/all scope;
|
|
2218
|
+
* ctrl+n switches to `create`; ctrl+w opens the workspace picker.
|
|
2219
|
+
* - `create`: an inline name field (prefilled from the server's suggestion).
|
|
2220
|
+
* Enter creates the agent, then offers a shell alias before opening it.
|
|
2221
|
+
* - `first-run`: reached automatically when the workspace has no agents at
|
|
2222
|
+
* all — auto-creates the account's first agent (server-named, like web
|
|
2223
|
+
* onboarding) and offers to have it learn from this machine's coding-agent
|
|
2224
|
+
* setup before dropping the user into its chat.
|
|
2225
|
+
*
|
|
2226
|
+
* Everything is centered both axes so a wide terminal gets even margins
|
|
2227
|
+
* instead of content stranded top-left.
|
|
2228
|
+
*/
|
|
2229
|
+
const CONTENT_MAX_COLS = 76;
|
|
2230
|
+
/**
|
|
2231
|
+
* Where to land after creating an agent (create mode / first-run). The
|
|
2232
|
+
* conversation home (centered composer + list) is the single new-conversation
|
|
2233
|
+
* surface, so a freshly created agent opens straight into its home. The one
|
|
2234
|
+
* exception is a seeded launch (`skydive import`): there is a message queued
|
|
2235
|
+
* to auto-send, so boot into a fresh chat (`kind: 'new'`) that fires it. A
|
|
2236
|
+
* brand-new agent has no conversations yet, so its home is just the composer
|
|
2237
|
+
* over an empty list — which is exactly the "start talking" prompt we want.
|
|
2238
|
+
*/
|
|
2239
|
+
function openAgentAfterCreate(agent) {
|
|
2240
|
+
if (useStore.getState().seedPrompt !== null) return {
|
|
2241
|
+
kind: "chat",
|
|
2242
|
+
agent,
|
|
2243
|
+
attachRunId: null,
|
|
2244
|
+
parentConversationId: null,
|
|
2245
|
+
conversation: {
|
|
2246
|
+
kind: "new",
|
|
2247
|
+
agentId: agent.id,
|
|
2248
|
+
nonce: crypto.randomUUID()
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
return {
|
|
2252
|
+
kind: "conversation-picker",
|
|
2253
|
+
agent,
|
|
2254
|
+
initialQuery: ""
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
function WelcomeScreen() {
|
|
2108
2258
|
const rest = useStore((s) => s.rest);
|
|
2109
2259
|
const goTo = useStore((s) => s.goTo);
|
|
2110
2260
|
const fail = useStore((s) => s.fail);
|
|
2261
|
+
const forcedOnboarding = useStore((s) => s.forcedOnboarding);
|
|
2111
2262
|
const [scope, setScope] = useState("mine");
|
|
2112
2263
|
const [cache, setCache] = useState({});
|
|
2113
2264
|
const [state, setState] = useState({ kind: "loading" });
|
|
2114
|
-
|
|
2115
|
-
if (key.name === "w" && key.ctrl) {
|
|
2116
|
-
if (useStore.getState().authKind === "api-key") {
|
|
2117
|
-
useStore.getState().showToast({ message: "API keys are pinned to one workspace. Run `skydive auth login --web` to switch workspaces." });
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
goTo({ kind: "workspace-picker" });
|
|
2121
|
-
} else if (key.name === "n" && key.ctrl) goTo({ kind: "agent-create" });
|
|
2122
|
-
else if (key.name === "tab") setScope((s) => s === "mine" ? "org" : "mine");
|
|
2123
|
-
});
|
|
2265
|
+
const [mode, setMode] = useState(forcedOnboarding ? { kind: "first-run" } : { kind: "list" });
|
|
2124
2266
|
useEffect(() => {
|
|
2125
2267
|
if (!rest) return;
|
|
2268
|
+
if (mode.kind !== "list") return;
|
|
2126
2269
|
const cached = cache[scope];
|
|
2127
2270
|
if (cached) {
|
|
2128
2271
|
setState({
|
|
@@ -2153,6 +2296,15 @@ function AgentPickerScreen() {
|
|
|
2153
2296
|
kind: "ready",
|
|
2154
2297
|
agents
|
|
2155
2298
|
});
|
|
2299
|
+
if (agents.length === 0 && scope === "mine") {
|
|
2300
|
+
const org = await rest.listAgents({
|
|
2301
|
+
scope: "org",
|
|
2302
|
+
onPage: null
|
|
2303
|
+
});
|
|
2304
|
+
if (cancelled) return;
|
|
2305
|
+
if (org.length === 0) setMode({ kind: "first-run" });
|
|
2306
|
+
else setScope("org");
|
|
2307
|
+
}
|
|
2156
2308
|
} catch (err) {
|
|
2157
2309
|
if (cancelled) return;
|
|
2158
2310
|
if (err instanceof HttpError && err.status === 401) {
|
|
@@ -2172,56 +2324,124 @@ function AgentPickerScreen() {
|
|
|
2172
2324
|
rest,
|
|
2173
2325
|
fail,
|
|
2174
2326
|
scope,
|
|
2175
|
-
cache
|
|
2327
|
+
cache,
|
|
2328
|
+
mode.kind
|
|
2176
2329
|
]);
|
|
2177
|
-
|
|
2178
|
-
if (
|
|
2330
|
+
if (mode.kind === "create") return /* @__PURE__ */ jsx(CreateAgentMode, { onCancel: () => setMode({ kind: "list" }) });
|
|
2331
|
+
if (mode.kind === "first-run") return /* @__PURE__ */ jsx(FirstRunMode, {});
|
|
2332
|
+
return /* @__PURE__ */ jsx(AgentListMode, {
|
|
2333
|
+
state,
|
|
2334
|
+
scope,
|
|
2335
|
+
onToggleScope: () => setScope((s) => s === "mine" ? "org" : "mine"),
|
|
2336
|
+
onCreate: () => setMode({ kind: "create" }),
|
|
2337
|
+
onSwitchWorkspace: () => {
|
|
2338
|
+
if (useStore.getState().authKind === "api-key") {
|
|
2339
|
+
useStore.getState().showToast({ message: "API keys are pinned to one workspace. Run `skydive auth login --web` to switch workspaces." });
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
goTo({ kind: "workspace-picker" });
|
|
2343
|
+
},
|
|
2344
|
+
onPick: goTo
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
/** The centered welcome frame every mode renders inside: the brand mark and a
|
|
2348
|
+
* "Welcome to Skydive" heading over the mode's own content, in a fixed-width
|
|
2349
|
+
* column centered on both axes.
|
|
2350
|
+
*
|
|
2351
|
+
* The first paint is GATED on the terminal's real size being known (`ready`).
|
|
2352
|
+
* The renderer boots on an 80x24 fallback when the emulator reports its size
|
|
2353
|
+
* asynchronously (tmux and friends), so an ungated screen centers the brand
|
|
2354
|
+
* within 80 cols first, then jumps to centered-within-the-real-width once the
|
|
2355
|
+
* size lands — the "weird flashing transition" from left to center reviewers
|
|
2356
|
+
* see. Crucially the renderer DEBOUNCES that resize by 100ms, so revealing on
|
|
2357
|
+
* the next tick would still flash; `useTerminalReady` instead reveals from the
|
|
2358
|
+
* OS-reported `process.stdout` size or the resize event itself, whichever
|
|
2359
|
+
* confirms the true width first. While `ready` is false we hold a blank but
|
|
2360
|
+
* full-size, same-background frame, so the brand's first *visible* paint is
|
|
2361
|
+
* already at its final centered position and never reflows. The home
|
|
2362
|
+
* (conversation-picker) and the empty-chat splash gate the same way, so the
|
|
2363
|
+
* flash is fixed on every brand-centered surface the launch flow can land on.
|
|
2364
|
+
* See `useTerminalReady`. */
|
|
2365
|
+
function WelcomeFrame({ contentWidth, children }) {
|
|
2366
|
+
const { width, height, ready } = useTerminalReady();
|
|
2367
|
+
if (!ready) return /* @__PURE__ */ jsx("box", { style: {
|
|
2368
|
+
flexDirection: "column",
|
|
2369
|
+
flexGrow: 1
|
|
2370
|
+
} });
|
|
2371
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2179
2372
|
style: {
|
|
2180
2373
|
flexDirection: "column",
|
|
2181
|
-
flexGrow: 1
|
|
2374
|
+
flexGrow: 1,
|
|
2375
|
+
alignItems: "center"
|
|
2182
2376
|
},
|
|
2183
|
-
children: [
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2377
|
+
children: [
|
|
2378
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
2379
|
+
flexGrow: 1,
|
|
2380
|
+
flexShrink: 1
|
|
2381
|
+
} }),
|
|
2382
|
+
/* @__PURE__ */ jsxs("box", {
|
|
2383
|
+
style: {
|
|
2384
|
+
flexDirection: "column",
|
|
2385
|
+
width: contentWidth
|
|
2386
|
+
},
|
|
2387
|
+
children: [/* @__PURE__ */ jsxs("box", {
|
|
2388
|
+
style: {
|
|
2389
|
+
flexDirection: "column",
|
|
2390
|
+
alignItems: "center"
|
|
2391
|
+
},
|
|
2392
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2393
|
+
height,
|
|
2394
|
+
width
|
|
2395
|
+
}), height >= 24 ? /* @__PURE__ */ jsxs("box", {
|
|
2396
|
+
style: {
|
|
2397
|
+
flexDirection: "column",
|
|
2398
|
+
alignItems: "center"
|
|
2399
|
+
},
|
|
2400
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
2401
|
+
fg: theme.accent,
|
|
2402
|
+
attributes: 1,
|
|
2403
|
+
children: "Welcome to Skydive"
|
|
2404
|
+
}), /* @__PURE__ */ jsx("box", { style: { height: 1 } })]
|
|
2405
|
+
}) : null]
|
|
2406
|
+
}), children]
|
|
2407
|
+
}),
|
|
2408
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
2409
|
+
flexGrow: 1,
|
|
2410
|
+
flexShrink: 1
|
|
2411
|
+
} })
|
|
2412
|
+
]
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
function AgentListMode({ state, scope, onToggleScope, onCreate, onSwitchWorkspace, onPick }) {
|
|
2416
|
+
const { width } = useTerminalDimensions();
|
|
2417
|
+
const contentWidth = Math.min(CONTENT_MAX_COLS, Math.max(1, width - 2));
|
|
2418
|
+
if (state.kind === "loading") return /* @__PURE__ */ jsx(WelcomeFrame, {
|
|
2419
|
+
contentWidth,
|
|
2420
|
+
children: /* @__PURE__ */ jsx("text", {
|
|
2187
2421
|
fg: theme.muted,
|
|
2188
2422
|
children: "loading agents…"
|
|
2189
|
-
})
|
|
2423
|
+
})
|
|
2190
2424
|
});
|
|
2191
|
-
if (state.kind === "error") return /* @__PURE__ */
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
flexGrow: 1
|
|
2195
|
-
},
|
|
2196
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2197
|
-
height,
|
|
2198
|
-
width
|
|
2199
|
-
}), /* @__PURE__ */ jsxs("text", {
|
|
2425
|
+
if (state.kind === "error") return /* @__PURE__ */ jsx(WelcomeFrame, {
|
|
2426
|
+
contentWidth,
|
|
2427
|
+
children: /* @__PURE__ */ jsxs("text", {
|
|
2200
2428
|
fg: theme.error,
|
|
2201
2429
|
children: ["error: ", state.message]
|
|
2202
|
-
})
|
|
2203
|
-
});
|
|
2204
|
-
if (state.agents.length === 0) return /* @__PURE__ */ jsxs("box", {
|
|
2205
|
-
style: {
|
|
2206
|
-
flexDirection: "column",
|
|
2207
|
-
flexGrow: 1
|
|
2208
|
-
},
|
|
2209
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
2210
|
-
height,
|
|
2211
|
-
width
|
|
2212
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
2213
|
-
fg: theme.muted,
|
|
2214
|
-
children: scope === "mine" ? "no agents of yours yet. press tab to see all agents, ctrl+w to switch workspace, or ctrl+n to create one." : "no agents in this workspace. press ctrl+w to switch workspace, or ctrl+n to create one."
|
|
2215
|
-
})]
|
|
2430
|
+
})
|
|
2216
2431
|
});
|
|
2217
2432
|
return /* @__PURE__ */ jsx(FilterableAgentList, {
|
|
2218
2433
|
agents: state.agents,
|
|
2219
2434
|
scope,
|
|
2220
|
-
|
|
2435
|
+
contentWidth,
|
|
2436
|
+
onToggleScope,
|
|
2437
|
+
onCreate,
|
|
2438
|
+
onSwitchWorkspace,
|
|
2439
|
+
onPick
|
|
2221
2440
|
});
|
|
2222
2441
|
}
|
|
2223
|
-
function FilterableAgentList({ agents, scope, onPick }) {
|
|
2442
|
+
function FilterableAgentList({ agents, scope, contentWidth, onToggleScope, onCreate, onSwitchWorkspace, onPick }) {
|
|
2224
2443
|
const { width, height } = useTerminalDimensions();
|
|
2444
|
+
const composerRef = useRef(null);
|
|
2225
2445
|
const [query, setQuery] = useState("");
|
|
2226
2446
|
const [highlight, setHighlight] = useState(0);
|
|
2227
2447
|
useEffect(() => {
|
|
@@ -2230,87 +2450,100 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
2230
2450
|
const hits = fuzzyFilter(query, agents, agentKeys);
|
|
2231
2451
|
const filtered = hits.map((h) => h.item);
|
|
2232
2452
|
const clamped = Math.min(highlight, Math.max(0, filtered.length - 1));
|
|
2233
|
-
const
|
|
2453
|
+
const identityRows = height >= 24 ? 2 : 0;
|
|
2454
|
+
const visibleRows = Math.max(3, height - 8 - brandHeaderRows(height, width) - identityRows);
|
|
2234
2455
|
const start = windowStart(clamped, filtered.length, visibleRows);
|
|
2235
2456
|
const windowed = hits.slice(start, start + visibleRows);
|
|
2457
|
+
const openAgent = () => {
|
|
2458
|
+
const agent = filtered[clamped];
|
|
2459
|
+
if (!agent) return;
|
|
2460
|
+
onPick(useStore.getState().seedPrompt !== null ? {
|
|
2461
|
+
kind: "chat",
|
|
2462
|
+
agent,
|
|
2463
|
+
attachRunId: null,
|
|
2464
|
+
parentConversationId: null,
|
|
2465
|
+
conversation: {
|
|
2466
|
+
kind: "new",
|
|
2467
|
+
agentId: agent.id,
|
|
2468
|
+
nonce: crypto.randomUUID()
|
|
2469
|
+
}
|
|
2470
|
+
} : {
|
|
2471
|
+
kind: "conversation-picker",
|
|
2472
|
+
agent,
|
|
2473
|
+
initialQuery: ""
|
|
2474
|
+
});
|
|
2475
|
+
};
|
|
2236
2476
|
useKeyboard((key) => {
|
|
2237
|
-
if (key.name === "up")
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
else if (key.name === "
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
initialQuery: ""
|
|
2259
|
-
});
|
|
2477
|
+
if (key.name === "up") {
|
|
2478
|
+
setHighlight(Math.max(0, clamped - 1));
|
|
2479
|
+
key.preventDefault();
|
|
2480
|
+
} else if (key.name === "down") {
|
|
2481
|
+
setHighlight(Math.min(Math.max(0, filtered.length - 1), clamped + 1));
|
|
2482
|
+
key.preventDefault();
|
|
2483
|
+
} else if (key.name === "pageup" || key.name === "pgup") {
|
|
2484
|
+
setHighlight(Math.max(0, clamped - Math.max(1, visibleRows - 1)));
|
|
2485
|
+
key.preventDefault();
|
|
2486
|
+
} else if (key.name === "pagedown" || key.name === "pgdn") {
|
|
2487
|
+
setHighlight(Math.min(Math.max(0, filtered.length - 1), clamped + Math.max(1, visibleRows - 1)));
|
|
2488
|
+
key.preventDefault();
|
|
2489
|
+
} else if (key.name === "n" && key.ctrl) {
|
|
2490
|
+
onCreate();
|
|
2491
|
+
key.preventDefault();
|
|
2492
|
+
} else if (key.name === "w" && key.ctrl) {
|
|
2493
|
+
onSwitchWorkspace();
|
|
2494
|
+
key.preventDefault();
|
|
2495
|
+
} else if (key.name === "tab") {
|
|
2496
|
+
onToggleScope();
|
|
2497
|
+
key.preventDefault();
|
|
2260
2498
|
}
|
|
2261
2499
|
});
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
flexGrow: 1
|
|
2266
|
-
},
|
|
2500
|
+
const empty = agents.length === 0;
|
|
2501
|
+
return /* @__PURE__ */ jsxs(WelcomeFrame, {
|
|
2502
|
+
contentWidth,
|
|
2267
2503
|
children: [
|
|
2268
|
-
/* @__PURE__ */ jsx(
|
|
2269
|
-
height,
|
|
2270
|
-
width
|
|
2271
|
-
}),
|
|
2272
|
-
/* @__PURE__ */ jsxs("box", {
|
|
2504
|
+
/* @__PURE__ */ jsx("box", {
|
|
2273
2505
|
style: {
|
|
2274
|
-
|
|
2275
|
-
|
|
2506
|
+
border: true,
|
|
2507
|
+
borderColor: theme.faint,
|
|
2508
|
+
paddingLeft: 1,
|
|
2509
|
+
paddingRight: 1,
|
|
2510
|
+
height: 3,
|
|
2511
|
+
flexShrink: 0,
|
|
2512
|
+
flexDirection: "column"
|
|
2276
2513
|
},
|
|
2277
|
-
children:
|
|
2278
|
-
|
|
2279
|
-
children: "search "
|
|
2280
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
2514
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
2515
|
+
ref: composerRef,
|
|
2281
2516
|
textColor: theme.fg,
|
|
2282
2517
|
focusedTextColor: theme.fg,
|
|
2283
2518
|
cursorColor: theme.cursor,
|
|
2284
2519
|
placeholderColor: theme.dim,
|
|
2285
|
-
placeholder: "
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
setQuery(value);
|
|
2520
|
+
placeholder: "Search agents…",
|
|
2521
|
+
keyBindings: searchKeyBindings,
|
|
2522
|
+
onContentChange: () => {
|
|
2523
|
+
setQuery(composerRef.current?.plainText ?? "");
|
|
2290
2524
|
setHighlight(0);
|
|
2525
|
+
},
|
|
2526
|
+
onSubmit: openAgent,
|
|
2527
|
+
focused: true,
|
|
2528
|
+
style: {
|
|
2529
|
+
height: 1,
|
|
2530
|
+
wrapMode: "word"
|
|
2291
2531
|
}
|
|
2292
|
-
})
|
|
2532
|
+
})
|
|
2293
2533
|
}),
|
|
2294
|
-
/* @__PURE__ */
|
|
2534
|
+
/* @__PURE__ */ jsx("text", {
|
|
2295
2535
|
fg: theme.dim,
|
|
2296
|
-
children:
|
|
2297
|
-
filtered.length,
|
|
2298
|
-
"/",
|
|
2299
|
-
agents.length,
|
|
2300
|
-
" · ",
|
|
2301
|
-
scope === "mine" ? "mine" : "all",
|
|
2302
|
-
" ",
|
|
2303
|
-
"· tab ",
|
|
2304
|
-
scope === "mine" ? "all" : "mine",
|
|
2305
|
-
" · ↑/↓ move · ↵ open · ctrl+w workspace · ctrl+n new"
|
|
2306
|
-
]
|
|
2536
|
+
children: truncate(`${scope === "mine" ? "mine" : "all agents"} · ↵ open · ctrl+n new agent · tab ${scope === "mine" ? "all" : "mine"} · ctrl+w workspace`, contentWidth)
|
|
2307
2537
|
}),
|
|
2308
2538
|
/* @__PURE__ */ jsx("box", {
|
|
2309
2539
|
style: {
|
|
2310
2540
|
flexDirection: "column",
|
|
2311
|
-
|
|
2541
|
+
flexShrink: 0
|
|
2312
2542
|
},
|
|
2313
|
-
children:
|
|
2543
|
+
children: empty ? /* @__PURE__ */ jsx("text", {
|
|
2544
|
+
fg: theme.muted,
|
|
2545
|
+
children: truncate(scope === "mine" ? "no agents of yours yet — press ctrl+n to create one, or tab to see all agents." : "no agents in this workspace — press ctrl+n to create one, or ctrl+w to switch workspace.", contentWidth)
|
|
2546
|
+
}) : windowed.length === 0 ? /* @__PURE__ */ jsxs("text", {
|
|
2314
2547
|
fg: theme.muted,
|
|
2315
2548
|
children: [
|
|
2316
2549
|
"no agents match “",
|
|
@@ -2339,45 +2572,12 @@ function FilterableAgentList({ agents, scope, onPick }) {
|
|
|
2339
2572
|
]
|
|
2340
2573
|
});
|
|
2341
2574
|
}
|
|
2342
|
-
|
|
2343
|
-
{
|
|
2344
|
-
get: (a) => a.name,
|
|
2345
|
-
weight: 1
|
|
2346
|
-
},
|
|
2347
|
-
{
|
|
2348
|
-
get: (a) => a.slug ?? "",
|
|
2349
|
-
weight: .6
|
|
2350
|
-
},
|
|
2351
|
-
{
|
|
2352
|
-
get: (a) => a.title ?? "",
|
|
2353
|
-
weight: .4
|
|
2354
|
-
}
|
|
2355
|
-
];
|
|
2356
|
-
|
|
2357
|
-
//#endregion
|
|
2358
|
-
//#region src/chat/tui/agent-name.ts
|
|
2359
|
-
function parseAgentName(value) {
|
|
2360
|
-
const name = value.trim();
|
|
2361
|
-
if (!name) return {
|
|
2362
|
-
ok: false,
|
|
2363
|
-
message: "Enter a name."
|
|
2364
|
-
};
|
|
2365
|
-
if (name.length > 120) return {
|
|
2366
|
-
ok: false,
|
|
2367
|
-
message: "Use 120 characters or fewer."
|
|
2368
|
-
};
|
|
2369
|
-
return {
|
|
2370
|
-
ok: true,
|
|
2371
|
-
name
|
|
2372
|
-
};
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
//#endregion
|
|
2376
|
-
//#region src/chat/tui/screens/agent-create.tsx
|
|
2377
|
-
function AgentCreateScreen() {
|
|
2575
|
+
function CreateAgentMode({ onCancel }) {
|
|
2378
2576
|
const rest = useStore((s) => s.rest);
|
|
2379
2577
|
const goTo = useStore((s) => s.goTo);
|
|
2380
2578
|
const fail = useStore((s) => s.fail);
|
|
2579
|
+
const { width } = useTerminalDimensions();
|
|
2580
|
+
const contentWidth = Math.min(CONTENT_MAX_COLS, Math.max(1, width - 2));
|
|
2381
2581
|
const [name, setName] = useState("");
|
|
2382
2582
|
const [state, setState] = useState({ kind: "idle" });
|
|
2383
2583
|
useEffect(() => {
|
|
@@ -2392,17 +2592,7 @@ function AgentCreateScreen() {
|
|
|
2392
2592
|
};
|
|
2393
2593
|
}, [rest]);
|
|
2394
2594
|
const openChat = (agent) => {
|
|
2395
|
-
goTo(
|
|
2396
|
-
kind: "chat",
|
|
2397
|
-
agent,
|
|
2398
|
-
attachRunId: null,
|
|
2399
|
-
parentConversationId: null,
|
|
2400
|
-
conversation: {
|
|
2401
|
-
kind: "new",
|
|
2402
|
-
agentId: agent.id,
|
|
2403
|
-
nonce: crypto.randomUUID()
|
|
2404
|
-
}
|
|
2405
|
-
});
|
|
2595
|
+
goTo(openAgentAfterCreate(agent));
|
|
2406
2596
|
};
|
|
2407
2597
|
const acceptAlias = (agent, alias) => {
|
|
2408
2598
|
try {
|
|
@@ -2441,7 +2631,7 @@ function AgentCreateScreen() {
|
|
|
2441
2631
|
openChat(state.agent);
|
|
2442
2632
|
return;
|
|
2443
2633
|
}
|
|
2444
|
-
if (key.name === "escape" && state.kind !== "creating")
|
|
2634
|
+
if (key.name === "escape" && state.kind !== "creating") onCancel();
|
|
2445
2635
|
});
|
|
2446
2636
|
const createAgent = async () => {
|
|
2447
2637
|
if (!rest || state.kind === "creating") return;
|
|
@@ -2468,51 +2658,25 @@ function AgentCreateScreen() {
|
|
|
2468
2658
|
fail("Session expired. Run `skydive auth login --web`, then retry.");
|
|
2469
2659
|
return;
|
|
2470
2660
|
}
|
|
2471
|
-
if (err instanceof HttpError && err.status === 403) {
|
|
2472
|
-
setState({
|
|
2473
|
-
kind: "error",
|
|
2474
|
-
message: "You don't have permission to create an agent here."
|
|
2475
|
-
});
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
if (err instanceof HttpError && err.status === 400) {
|
|
2479
|
-
setState({
|
|
2480
|
-
kind: "error",
|
|
2481
|
-
message: "Skydive could not create this agent. Check your active workspace and try again."
|
|
2482
|
-
});
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
if (err instanceof HttpError && err.status === 409) {
|
|
2486
|
-
setState({
|
|
2487
|
-
kind: "error",
|
|
2488
|
-
message: "That name is unavailable. Choose another name and try again."
|
|
2489
|
-
});
|
|
2490
|
-
return;
|
|
2491
|
-
}
|
|
2492
|
-
if (err instanceof HttpError && err.status === 429) {
|
|
2493
|
-
setState({
|
|
2494
|
-
kind: "error",
|
|
2495
|
-
message: "Too many requests. Wait a moment and try again."
|
|
2496
|
-
});
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
2661
|
setState({
|
|
2500
2662
|
kind: "error",
|
|
2501
|
-
message: err
|
|
2663
|
+
message: createErrorMessage(err)
|
|
2502
2664
|
});
|
|
2503
2665
|
}
|
|
2504
2666
|
};
|
|
2505
|
-
if (state.kind === "offer-alias") return /* @__PURE__ */ jsx(
|
|
2506
|
-
|
|
2507
|
-
|
|
2667
|
+
if (state.kind === "offer-alias") return /* @__PURE__ */ jsx(WelcomeFrame, {
|
|
2668
|
+
contentWidth,
|
|
2669
|
+
children: /* @__PURE__ */ jsx(AliasOffer, {
|
|
2670
|
+
agentName: state.agent.name,
|
|
2671
|
+
alias: state.alias
|
|
2672
|
+
})
|
|
2508
2673
|
});
|
|
2509
|
-
if (state.kind === "aliased") return /* @__PURE__ */ jsx(
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
},
|
|
2674
|
+
if (state.kind === "aliased") return /* @__PURE__ */ jsx(WelcomeFrame, {
|
|
2675
|
+
contentWidth,
|
|
2676
|
+
children: /* @__PURE__ */ jsx(AliasResult, { message: state.message })
|
|
2677
|
+
});
|
|
2678
|
+
return /* @__PURE__ */ jsxs(WelcomeFrame, {
|
|
2679
|
+
contentWidth,
|
|
2516
2680
|
children: [
|
|
2517
2681
|
/* @__PURE__ */ jsx("text", {
|
|
2518
2682
|
fg: theme.fg,
|
|
@@ -2522,6 +2686,7 @@ function AgentCreateScreen() {
|
|
|
2522
2686
|
fg: theme.muted,
|
|
2523
2687
|
children: "Choose a name. You can change it later."
|
|
2524
2688
|
}),
|
|
2689
|
+
/* @__PURE__ */ jsx("box", { style: { height: 1 } }),
|
|
2525
2690
|
/* @__PURE__ */ jsx("box", {
|
|
2526
2691
|
style: {
|
|
2527
2692
|
border: true,
|
|
@@ -2568,7 +2733,6 @@ function AliasOffer({ agentName, alias }) {
|
|
|
2568
2733
|
return /* @__PURE__ */ jsxs("box", {
|
|
2569
2734
|
style: {
|
|
2570
2735
|
flexDirection: "column",
|
|
2571
|
-
flexGrow: 1,
|
|
2572
2736
|
gap: 1
|
|
2573
2737
|
},
|
|
2574
2738
|
children: [
|
|
@@ -2600,7 +2764,6 @@ function AliasResult({ message }) {
|
|
|
2600
2764
|
return /* @__PURE__ */ jsxs("box", {
|
|
2601
2765
|
style: {
|
|
2602
2766
|
flexDirection: "column",
|
|
2603
|
-
flexGrow: 1,
|
|
2604
2767
|
gap: 1
|
|
2605
2768
|
},
|
|
2606
2769
|
children: [/* @__PURE__ */ jsx("text", {
|
|
@@ -2612,22 +2775,14 @@ function AliasResult({ message }) {
|
|
|
2612
2775
|
})]
|
|
2613
2776
|
});
|
|
2614
2777
|
}
|
|
2615
|
-
|
|
2616
|
-
//#endregion
|
|
2617
|
-
//#region src/chat/tui/screens/first-run.tsx
|
|
2618
|
-
/** Rows the mark lockup needs before it's worth painting (matches the
|
|
2619
|
-
* new-chat splash): the 6-row mark, a spacer, and a couple of content rows.
|
|
2620
|
-
* Below this the mark is dropped and only the message renders. */
|
|
2621
|
-
const MARK_MIN_ROWS$1 = 12;
|
|
2622
|
-
/** Columns the full mark + wordmark lockup spans; below this the pinwheel
|
|
2623
|
-
* mark renders alone instead of wrapping the wordmark. */
|
|
2624
|
-
const FULL_MARK_MIN_COLS$1 = 70;
|
|
2625
|
-
function FirstRunScreen() {
|
|
2778
|
+
function FirstRunMode() {
|
|
2626
2779
|
const rest = useStore((s) => s.rest);
|
|
2627
2780
|
const goTo = useStore((s) => s.goTo);
|
|
2628
2781
|
const fail = useStore((s) => s.fail);
|
|
2629
2782
|
const authKind = useStore((s) => s.authKind);
|
|
2630
2783
|
const forced = useStore((s) => s.forcedOnboarding);
|
|
2784
|
+
const { width } = useTerminalDimensions();
|
|
2785
|
+
const contentWidth = Math.min(CONTENT_MAX_COLS, Math.max(1, width - 2));
|
|
2631
2786
|
const [state, setState] = useState({ kind: "creating" });
|
|
2632
2787
|
const canShareMachine = authKind === "session";
|
|
2633
2788
|
useEffect(() => {
|
|
@@ -2658,17 +2813,7 @@ function FirstRunScreen() {
|
|
|
2658
2813
|
};
|
|
2659
2814
|
}, [rest, fail]);
|
|
2660
2815
|
const openNewChat = (agent) => {
|
|
2661
|
-
goTo(
|
|
2662
|
-
kind: "chat",
|
|
2663
|
-
agent,
|
|
2664
|
-
attachRunId: null,
|
|
2665
|
-
parentConversationId: null,
|
|
2666
|
-
conversation: {
|
|
2667
|
-
kind: "new",
|
|
2668
|
-
agentId: agent.id,
|
|
2669
|
-
nonce: crypto.randomUUID()
|
|
2670
|
-
}
|
|
2671
|
-
});
|
|
2816
|
+
goTo(openAgentAfterCreate(agent));
|
|
2672
2817
|
};
|
|
2673
2818
|
const openOnboardingChat = (agent, conversationId, title, attachRunId) => {
|
|
2674
2819
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2740,84 +2885,100 @@ function FirstRunScreen() {
|
|
|
2740
2885
|
if (key.name === "return" || key.name === "y") acceptImport(state.agent);
|
|
2741
2886
|
else if (key.name === "n" || key.name === "escape") declineImport(state.agent);
|
|
2742
2887
|
});
|
|
2743
|
-
return /* @__PURE__ */ jsx(
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2888
|
+
return /* @__PURE__ */ jsx(WelcomeFrame, {
|
|
2889
|
+
contentWidth,
|
|
2890
|
+
children: /* @__PURE__ */ jsx(FirstRunBody, {
|
|
2891
|
+
state,
|
|
2892
|
+
forced,
|
|
2893
|
+
canShareMachine
|
|
2894
|
+
})
|
|
2895
|
+
});
|
|
2748
2896
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2897
|
+
function FirstRunBody({ state, forced, canShareMachine }) {
|
|
2898
|
+
if (state.kind === "error") return /* @__PURE__ */ jsxs("box", {
|
|
2899
|
+
style: {
|
|
2900
|
+
flexDirection: "column",
|
|
2901
|
+
alignItems: "center",
|
|
2902
|
+
gap: 1
|
|
2903
|
+
},
|
|
2904
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
2905
|
+
fg: theme.error,
|
|
2906
|
+
children: state.message
|
|
2907
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2908
|
+
fg: theme.muted,
|
|
2909
|
+
children: "press ctrl+c to quit, then run `skydive` again to retry."
|
|
2910
|
+
})]
|
|
2911
|
+
});
|
|
2912
|
+
if (state.kind === "creating") return /* @__PURE__ */ jsx("box", {
|
|
2913
|
+
style: {
|
|
2914
|
+
flexDirection: "column",
|
|
2915
|
+
alignItems: "center",
|
|
2916
|
+
gap: 1
|
|
2917
|
+
},
|
|
2918
|
+
children: /* @__PURE__ */ jsx("text", {
|
|
2919
|
+
fg: theme.muted,
|
|
2920
|
+
children: forced ? "creating an agent…" : "setting up your first agent…"
|
|
2921
|
+
})
|
|
2922
|
+
});
|
|
2923
|
+
if (state.kind === "starting") return /* @__PURE__ */ jsx("text", {
|
|
2924
|
+
fg: theme.muted,
|
|
2925
|
+
children: "opening your agent…"
|
|
2926
|
+
});
|
|
2927
|
+
const { agent } = state;
|
|
2753
2928
|
return /* @__PURE__ */ jsxs("box", {
|
|
2754
2929
|
style: {
|
|
2755
|
-
flexGrow: 1,
|
|
2756
2930
|
flexDirection: "column",
|
|
2757
|
-
|
|
2758
|
-
|
|
2931
|
+
alignItems: "center",
|
|
2932
|
+
gap: 1
|
|
2759
2933
|
},
|
|
2760
|
-
children: [
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2934
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
2935
|
+
fg: theme.fg,
|
|
2936
|
+
children: forced ? `${agent.name} is ready.` : `Your first agent, ${agent.name}, is ready.`
|
|
2937
|
+
}), canShareMachine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", {
|
|
2938
|
+
fg: theme.muted,
|
|
2939
|
+
children: [
|
|
2940
|
+
"Want ",
|
|
2941
|
+
agent.name,
|
|
2942
|
+
" to learn how you already work? It can look at the coding agents set up on this machine to get up to speed fast. It will connect to your computer to read your setup, show you a plan first, and never copy credentials."
|
|
2943
|
+
]
|
|
2944
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
2945
|
+
fg: theme.dim,
|
|
2946
|
+
children: "↵/y yes, learn from my setup · n skip"
|
|
2947
|
+
})] }) : /* @__PURE__ */ jsx("text", {
|
|
2948
|
+
fg: theme.dim,
|
|
2949
|
+
children: "press any key to open the chat"
|
|
2774
2950
|
})]
|
|
2775
2951
|
});
|
|
2776
2952
|
}
|
|
2777
|
-
function renderBody({ state, forced, canShareMachine }) {
|
|
2778
|
-
if (state.kind === "error") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2779
|
-
fg: theme.error,
|
|
2780
|
-
children: state.message
|
|
2781
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
2782
|
-
fg: theme.muted,
|
|
2783
|
-
children: "press ctrl+c to quit, then run `skydive` again to retry."
|
|
2784
|
-
})] });
|
|
2785
|
-
if (state.kind === "creating") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2786
|
-
fg: theme.fg,
|
|
2787
|
-
children: "Welcome to Skydive."
|
|
2788
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
2789
|
-
fg: theme.muted,
|
|
2790
|
-
children: forced ? "creating an agent…" : "setting up your first agent…"
|
|
2791
|
-
})] });
|
|
2792
|
-
if (state.kind === "starting") return /* @__PURE__ */ jsx("text", {
|
|
2793
|
-
fg: theme.muted,
|
|
2794
|
-
children: "opening your agent…"
|
|
2795
|
-
});
|
|
2796
|
-
const { agent } = state;
|
|
2797
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
|
|
2798
|
-
fg: theme.fg,
|
|
2799
|
-
children: forced ? `${agent.name} is ready.` : `Your first agent, ${agent.name}, is ready.`
|
|
2800
|
-
}), canShareMachine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", {
|
|
2801
|
-
fg: theme.muted,
|
|
2802
|
-
children: [
|
|
2803
|
-
"Want ",
|
|
2804
|
-
agent.name,
|
|
2805
|
-
" to learn how you already work? It can look at the coding agents set up on this machine to get up to speed fast. It will connect to your computer to read your setup, show you a plan first, and never copy credentials."
|
|
2806
|
-
]
|
|
2807
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
2808
|
-
fg: theme.dim,
|
|
2809
|
-
children: "↵/y yes, learn from my setup · n skip"
|
|
2810
|
-
})] }) : /* @__PURE__ */ jsx("text", {
|
|
2811
|
-
fg: theme.dim,
|
|
2812
|
-
children: "press any key to open the chat"
|
|
2813
|
-
})] });
|
|
2814
|
-
}
|
|
2815
2953
|
function createErrorMessage(err) {
|
|
2816
2954
|
if (err instanceof HttpError && err.status === 403) return "You don't have permission to create an agent in this workspace.";
|
|
2817
|
-
if (err instanceof HttpError && err.status === 429) return "Too many requests. Wait a moment, then
|
|
2955
|
+
if (err instanceof HttpError && err.status === 429) return "Too many requests. Wait a moment, then try again.";
|
|
2956
|
+
if (err instanceof HttpError && err.status === 409) return "That name is unavailable. Choose another name and try again.";
|
|
2957
|
+
if (err instanceof HttpError && err.status === 400) return "Skydive could not create this agent. Check your active workspace and try again.";
|
|
2818
2958
|
if (err instanceof HttpError) return "Skydive could not create your agent. Try again in a moment.";
|
|
2819
2959
|
return "Could not reach Skydive. Check your connection and try again.";
|
|
2820
2960
|
}
|
|
2961
|
+
const searchKeyBindings = [{
|
|
2962
|
+
name: "return",
|
|
2963
|
+
action: "submit"
|
|
2964
|
+
}, {
|
|
2965
|
+
name: "kpenter",
|
|
2966
|
+
action: "submit"
|
|
2967
|
+
}];
|
|
2968
|
+
const agentKeys = [
|
|
2969
|
+
{
|
|
2970
|
+
get: (a) => a.name,
|
|
2971
|
+
weight: 1
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
get: (a) => a.slug ?? "",
|
|
2975
|
+
weight: .6
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
get: (a) => a.title ?? "",
|
|
2979
|
+
weight: .4
|
|
2980
|
+
}
|
|
2981
|
+
];
|
|
2821
2982
|
|
|
2822
2983
|
//#endregion
|
|
2823
2984
|
//#region src/chat/tui/screens/workspace-picker.tsx
|
|
@@ -2981,48 +3142,6 @@ function singleLine(text) {
|
|
|
2981
3142
|
return out;
|
|
2982
3143
|
}
|
|
2983
3144
|
/**
|
|
2984
|
-
* Fit a row's title, an optional mid-row `tag` and a trailing detail into
|
|
2985
|
-
* `width` columns, each rendered separated by one space. The title identifies
|
|
2986
|
-
* the row so it gets the space first; the tag comes next because it carries
|
|
2987
|
-
* information the detail does not; the detail takes what is left and is
|
|
2988
|
-
* dropped when nothing is.
|
|
2989
|
-
*/
|
|
2990
|
-
function fitRowText(title, detail, width, tag = "") {
|
|
2991
|
-
if (width <= 0) return {
|
|
2992
|
-
title: "",
|
|
2993
|
-
detail: "",
|
|
2994
|
-
tag: ""
|
|
2995
|
-
};
|
|
2996
|
-
const segment = (text) => text ? text.length + 1 : 0;
|
|
2997
|
-
if (title.length + segment(tag) + segment(detail) <= width) return {
|
|
2998
|
-
title,
|
|
2999
|
-
detail,
|
|
3000
|
-
tag
|
|
3001
|
-
};
|
|
3002
|
-
const room = width - title.length - 1;
|
|
3003
|
-
if (room < 2) return {
|
|
3004
|
-
title: truncate(title, width),
|
|
3005
|
-
detail: "",
|
|
3006
|
-
tag: ""
|
|
3007
|
-
};
|
|
3008
|
-
if (title.length + segment(tag) > width) return {
|
|
3009
|
-
title,
|
|
3010
|
-
detail: "",
|
|
3011
|
-
tag: truncate(tag, room)
|
|
3012
|
-
};
|
|
3013
|
-
const detailRoom = room - segment(tag);
|
|
3014
|
-
if (detailRoom < 2) return {
|
|
3015
|
-
title,
|
|
3016
|
-
detail: "",
|
|
3017
|
-
tag
|
|
3018
|
-
};
|
|
3019
|
-
return {
|
|
3020
|
-
title,
|
|
3021
|
-
detail: truncate(detail, detailRoom),
|
|
3022
|
-
tag
|
|
3023
|
-
};
|
|
3024
|
-
}
|
|
3025
|
-
/**
|
|
3026
3145
|
* Join hint segments with " · ", dropping trailing segments that do not fit
|
|
3027
3146
|
* rather than wrapping onto a second line — which would cost the list a row.
|
|
3028
3147
|
* The first segment is always kept (truncated if it has to be).
|
|
@@ -3130,6 +3249,7 @@ function forkedConversationRef({ agent, forked }) {
|
|
|
3130
3249
|
//#region src/chat/tui/screens/conversation-picker.tsx
|
|
3131
3250
|
const SEARCH_DEBOUNCE_MS = 250;
|
|
3132
3251
|
const SEARCH_QUERY_MAX_LENGTH = 200;
|
|
3252
|
+
const SEARCH_PREFIX = "/";
|
|
3133
3253
|
function ConversationPickerScreen({ agent, initialQuery }) {
|
|
3134
3254
|
const rest = useStore((s) => s.rest);
|
|
3135
3255
|
const goTo = useStore((s) => s.goTo);
|
|
@@ -3194,38 +3314,18 @@ function ConversationPickerScreen({ agent, initialQuery }) {
|
|
|
3194
3314
|
cacheKey,
|
|
3195
3315
|
cache
|
|
3196
3316
|
]);
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
children: [
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
3207
|
-
fg: theme.muted,
|
|
3208
|
-
children: "loading conversations…"
|
|
3209
|
-
})]
|
|
3210
|
-
});
|
|
3211
|
-
if (state.kind === "error") return /* @__PURE__ */ jsxs("box", {
|
|
3212
|
-
style: {
|
|
3213
|
-
flexDirection: "column",
|
|
3214
|
-
flexGrow: 1
|
|
3215
|
-
},
|
|
3216
|
-
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3217
|
-
height,
|
|
3218
|
-
width
|
|
3219
|
-
}), /* @__PURE__ */ jsxs("text", {
|
|
3220
|
-
fg: theme.error,
|
|
3221
|
-
children: ["error: ", state.message]
|
|
3222
|
-
})]
|
|
3223
|
-
});
|
|
3224
|
-
return /* @__PURE__ */ jsx(ConversationList, {
|
|
3317
|
+
if (state.kind === "loading") return /* @__PURE__ */ jsx(HomeFrame, { children: /* @__PURE__ */ jsx("text", {
|
|
3318
|
+
fg: theme.muted,
|
|
3319
|
+
children: "loading conversations…"
|
|
3320
|
+
}) });
|
|
3321
|
+
if (state.kind === "error") return /* @__PURE__ */ jsx(HomeFrame, { children: /* @__PURE__ */ jsxs("text", {
|
|
3322
|
+
fg: theme.error,
|
|
3323
|
+
children: ["error: ", state.message]
|
|
3324
|
+
}) });
|
|
3325
|
+
return /* @__PURE__ */ jsx(ConversationHome, {
|
|
3225
3326
|
agent,
|
|
3226
3327
|
initialQuery,
|
|
3227
3328
|
conversations: state.conversations,
|
|
3228
|
-
complete: state.complete,
|
|
3229
3329
|
showAutomated,
|
|
3230
3330
|
onToggleAutomated: () => setShowAutomated((shown) => !shown),
|
|
3231
3331
|
showArchived,
|
|
@@ -3233,12 +3333,84 @@ function ConversationPickerScreen({ agent, initialQuery }) {
|
|
|
3233
3333
|
onPick: goTo
|
|
3234
3334
|
});
|
|
3235
3335
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3336
|
+
/**
|
|
3337
|
+
* The gated, centered brand frame the home renders inside — shared by the
|
|
3338
|
+
* loading and error legs so every leg paints the brand in the SAME centered
|
|
3339
|
+
* position at the SAME (real) width. Without this the loading leg painted an
|
|
3340
|
+
* ungated, left-aligned brand and then jumped it to centered when the loaded
|
|
3341
|
+
* home mounted: the left→center flash James saw (PR #20856).
|
|
3342
|
+
*
|
|
3343
|
+
* It rides `useTerminalReady`: on an async-sizing terminal (tmux/cmux) the
|
|
3344
|
+
* renderer boots on the 80x24 fallback and the real width lands via a debounced
|
|
3345
|
+
* resize, so we hold a blank same-size frame until the true width is known and
|
|
3346
|
+
* only then paint the centered brand — first VISIBLE paint already at its final
|
|
3347
|
+
* position, no reflow. The loaded `ConversationHome` gates identically, so the
|
|
3348
|
+
* brand never moves across loading → loaded. See `useTerminalReady`.
|
|
3349
|
+
*/
|
|
3350
|
+
function HomeFrame({ children }) {
|
|
3351
|
+
const { width, height, ready } = useTerminalReady();
|
|
3352
|
+
const contentWidth = Math.min(76, Math.max(1, width - 2));
|
|
3353
|
+
if (!ready) return /* @__PURE__ */ jsx("box", { style: {
|
|
3354
|
+
flexDirection: "column",
|
|
3355
|
+
flexGrow: 1
|
|
3356
|
+
} });
|
|
3357
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
3358
|
+
style: {
|
|
3359
|
+
flexDirection: "column",
|
|
3360
|
+
flexGrow: 1,
|
|
3361
|
+
alignItems: "center"
|
|
3362
|
+
},
|
|
3363
|
+
children: [
|
|
3364
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
3365
|
+
flexGrow: 1,
|
|
3366
|
+
flexShrink: 1
|
|
3367
|
+
} }),
|
|
3368
|
+
/* @__PURE__ */ jsxs("box", {
|
|
3369
|
+
style: {
|
|
3370
|
+
flexDirection: "column",
|
|
3371
|
+
width: contentWidth
|
|
3372
|
+
},
|
|
3373
|
+
children: [/* @__PURE__ */ jsx("box", {
|
|
3374
|
+
style: {
|
|
3375
|
+
flexDirection: "column",
|
|
3376
|
+
alignItems: "center"
|
|
3377
|
+
},
|
|
3378
|
+
children: /* @__PURE__ */ jsx(BrandHeader, {
|
|
3379
|
+
height,
|
|
3380
|
+
width
|
|
3381
|
+
})
|
|
3382
|
+
}), children]
|
|
3383
|
+
}),
|
|
3384
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
3385
|
+
flexGrow: 1,
|
|
3386
|
+
flexShrink: 1
|
|
3387
|
+
} })
|
|
3388
|
+
]
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* The agent's home screen: a composer to start a new conversation (mirroring
|
|
3393
|
+
* the web home's "Ask <agent> anything" field) over the list of existing
|
|
3394
|
+
* conversations. The composer is always focused; ↑/↓ move the list, enter on
|
|
3395
|
+
* a non-empty composer sends a new message, enter on an empty composer opens
|
|
3396
|
+
* the highlighted conversation. A leading '/' turns the composer into a live
|
|
3397
|
+
* list filter (server-side search) instead of a message.
|
|
3398
|
+
*/
|
|
3399
|
+
function ConversationHome({ agent, initialQuery, conversations, showAutomated, onToggleAutomated, showArchived, onToggleArchived, onPick }) {
|
|
3400
|
+
const { width, height, ready } = useTerminalReady();
|
|
3238
3401
|
const rest = useStore((s) => s.rest);
|
|
3239
|
-
const
|
|
3402
|
+
const composerRef = useRef(null);
|
|
3403
|
+
const [input, setInput] = useState(initialQuery ? `${SEARCH_PREFIX}${initialQuery}` : "");
|
|
3404
|
+
const seededRef = useRef(false);
|
|
3405
|
+
useEffect(() => {
|
|
3406
|
+
if (seededRef.current) return;
|
|
3407
|
+
seededRef.current = true;
|
|
3408
|
+
if (initialQuery) composerRef.current?.setText(`${SEARCH_PREFIX}${initialQuery}`);
|
|
3409
|
+
}, [initialQuery]);
|
|
3240
3410
|
const [highlight, setHighlight] = useState(0);
|
|
3241
|
-
const
|
|
3411
|
+
const [composerLines, setComposerLines] = useState(1);
|
|
3412
|
+
const isSearchInput = input.startsWith(SEARCH_PREFIX);
|
|
3413
|
+
const debouncedQuery = useDebouncedValue((isSearchInput ? input.slice(1).slice(0, SEARCH_QUERY_MAX_LENGTH) : "").trim(), SEARCH_DEBOUNCE_MS);
|
|
3242
3414
|
const isSearching = debouncedQuery.length > 0;
|
|
3243
3415
|
const [search, setSearch] = useState(null);
|
|
3244
3416
|
useEffect(() => {
|
|
@@ -3307,19 +3479,15 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3307
3479
|
useEffect(() => {
|
|
3308
3480
|
setHighlight(0);
|
|
3309
3481
|
}, [showAutomated, showArchived]);
|
|
3310
|
-
const
|
|
3482
|
+
const rows = list.map((item) => ({
|
|
3311
3483
|
item,
|
|
3312
|
-
highlights: [
|
|
3313
|
-
|
|
3314
|
-
null,
|
|
3315
|
-
null
|
|
3316
|
-
]
|
|
3317
|
-
})) : fuzzyFilter(query, list, conversationKeys);
|
|
3318
|
-
const rows = [{ kind: "new" }, ...hits.map((hit) => ({
|
|
3484
|
+
highlights: [isSearching ? substringIndexes(singleLine(item.title ?? ""), debouncedQuery) : null]
|
|
3485
|
+
})).map((hit) => ({
|
|
3319
3486
|
kind: "conv",
|
|
3320
3487
|
hit
|
|
3321
|
-
}))
|
|
3322
|
-
const
|
|
3488
|
+
}));
|
|
3489
|
+
const contentWidth = Math.min(76, Math.max(1, width - 2));
|
|
3490
|
+
const innerWidth = contentWidth;
|
|
3323
3491
|
const rowWidth = Math.max(1, innerWidth - 2);
|
|
3324
3492
|
const clamped = Math.min(highlight, Math.max(0, rows.length - 1));
|
|
3325
3493
|
const highlightRow = rows[clamped];
|
|
@@ -3327,7 +3495,9 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3327
3495
|
const highlightAgentRun = highlightRow?.kind === "conv" && isAgentRunConversation(highlightRow.hit.item);
|
|
3328
3496
|
const confirmConv = confirmId !== null ? list.find((c) => c.id === confirmId) : void 0;
|
|
3329
3497
|
const searchState = isSearching ? search?.state : void 0;
|
|
3330
|
-
const
|
|
3498
|
+
const searchStatusSegment = !isSearching ? "" : searchState?.kind === "loading" || search === null ? "searching…" : searchState?.kind === "error" ? `search failed: ${searchState.message}` : list.length === 0 ? "no matches" : "";
|
|
3499
|
+
const trimmedMessage = isSearchInput ? "" : input.trim();
|
|
3500
|
+
const enterHint = isSearchInput ? "filtering" : trimmedMessage ? "↵ send" : "↵ open";
|
|
3331
3501
|
const status = confirmConv ? {
|
|
3332
3502
|
fg: theme.error,
|
|
3333
3503
|
text: truncate(`delete “${confirmConv.title ?? "(untitled)"}”? y/n`, innerWidth)
|
|
@@ -3337,39 +3507,61 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3337
3507
|
} : {
|
|
3338
3508
|
fg: theme.dim,
|
|
3339
3509
|
text: fitHintLine([
|
|
3340
|
-
|
|
3510
|
+
searchStatusSegment,
|
|
3511
|
+
enterHint,
|
|
3512
|
+
"/ search",
|
|
3341
3513
|
...highlightAgentRun ? [] : [`ctrl+a ${highlightArchived ? "unarchive" : "archive"}`],
|
|
3342
3514
|
"ctrl+d delete",
|
|
3343
3515
|
`tab ${showAutomated ? "hide" : "show"} agent runs`,
|
|
3344
3516
|
`shift+tab ${showArchived ? "hide" : "show"} archived`,
|
|
3345
|
-
"esc back",
|
|
3346
3517
|
"↑/↓ move",
|
|
3347
|
-
"
|
|
3348
|
-
], innerWidth)
|
|
3518
|
+
"esc back"
|
|
3519
|
+
].filter((segment) => segment !== ""), innerWidth)
|
|
3349
3520
|
};
|
|
3350
|
-
const
|
|
3521
|
+
const identityRows = height >= 24 ? 3 : 0;
|
|
3522
|
+
const fixedRows = 6 + brandHeaderRows(height, width) + identityRows;
|
|
3523
|
+
const composerRows = Math.max(1, Math.min(composerLines, height - fixedRows - 2 - MIN_LIST_ROWS));
|
|
3524
|
+
const visibleRows = Math.max(MIN_LIST_ROWS, height - fixedRows - (composerRows + 2));
|
|
3351
3525
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
3352
3526
|
const windowed = rows.slice(start, start + visibleRows);
|
|
3353
|
-
const
|
|
3354
|
-
|
|
3527
|
+
const openHighlighted = () => {
|
|
3528
|
+
const row = rows[clamped];
|
|
3529
|
+
if (row) onPick({
|
|
3355
3530
|
kind: "chat",
|
|
3356
3531
|
agent,
|
|
3357
3532
|
attachRunId: null,
|
|
3358
3533
|
parentConversationId: null,
|
|
3359
|
-
conversation:
|
|
3360
|
-
kind: "new",
|
|
3361
|
-
agentId: agent.id,
|
|
3362
|
-
nonce: crypto.randomUUID()
|
|
3363
|
-
}
|
|
3534
|
+
conversation: row.hit.item
|
|
3364
3535
|
});
|
|
3365
|
-
|
|
3536
|
+
};
|
|
3537
|
+
const sendNewConversation = (message) => {
|
|
3538
|
+
useStore.setState({ seedPrompt: message });
|
|
3539
|
+
onPick({
|
|
3366
3540
|
kind: "chat",
|
|
3367
3541
|
agent,
|
|
3368
3542
|
attachRunId: null,
|
|
3369
3543
|
parentConversationId: null,
|
|
3370
|
-
conversation:
|
|
3544
|
+
conversation: {
|
|
3545
|
+
kind: "new",
|
|
3546
|
+
agentId: agent.id,
|
|
3547
|
+
nonce: crypto.randomUUID()
|
|
3548
|
+
}
|
|
3371
3549
|
});
|
|
3372
3550
|
};
|
|
3551
|
+
const handleComposerChange = () => {
|
|
3552
|
+
const composer = composerRef.current;
|
|
3553
|
+
if (!composer) return;
|
|
3554
|
+
const raw = composer.plainText;
|
|
3555
|
+
const capped = raw.startsWith(SEARCH_PREFIX) ? raw.slice(0, 1 + SEARCH_QUERY_MAX_LENGTH) : raw;
|
|
3556
|
+
if (capped !== raw) composer.setText(capped);
|
|
3557
|
+
setInput(capped);
|
|
3558
|
+
setComposerLines(Math.max(1, composer.editorView.getTotalVirtualLineCount()));
|
|
3559
|
+
setHighlight(0);
|
|
3560
|
+
};
|
|
3561
|
+
const submit = () => {
|
|
3562
|
+
if (trimmedMessage) sendNewConversation(trimmedMessage);
|
|
3563
|
+
else openHighlighted();
|
|
3564
|
+
};
|
|
3373
3565
|
/**
|
|
3374
3566
|
* Optimistically drop a row, run `mutate`, and put the row back if it fails.
|
|
3375
3567
|
* Delete only — archive keeps the row and flips its mark instead.
|
|
@@ -3422,17 +3614,34 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3422
3614
|
else if (key.name === "n" || key.name === "escape") setConfirmId(null);
|
|
3423
3615
|
return;
|
|
3424
3616
|
}
|
|
3425
|
-
if (key.name === "escape")
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3617
|
+
if (key.name === "escape") {
|
|
3618
|
+
if (input) {
|
|
3619
|
+
composerRef.current?.setText("");
|
|
3620
|
+
setInput("");
|
|
3621
|
+
setComposerLines(1);
|
|
3622
|
+
setHighlight(0);
|
|
3623
|
+
} else onPick({ kind: "welcome" });
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
if (key.name === "up") {
|
|
3627
|
+
setHighlight(Math.max(0, clamped - 1));
|
|
3628
|
+
key.preventDefault();
|
|
3629
|
+
} else if (key.name === "down") {
|
|
3630
|
+
setHighlight(Math.min(Math.max(0, rows.length - 1), clamped + 1));
|
|
3631
|
+
key.preventDefault();
|
|
3632
|
+
} else if (key.name === "pageup" || key.name === "pgup") {
|
|
3633
|
+
setHighlight(Math.max(0, clamped - Math.max(1, visibleRows - 1)));
|
|
3634
|
+
key.preventDefault();
|
|
3635
|
+
} else if (key.name === "pagedown" || key.name === "pgdn") {
|
|
3636
|
+
setHighlight(Math.min(Math.max(0, rows.length - 1), clamped + Math.max(1, visibleRows - 1)));
|
|
3637
|
+
key.preventDefault();
|
|
3638
|
+
} else if (key.name === "d" && key.ctrl) {
|
|
3431
3639
|
const row = rows[clamped];
|
|
3432
3640
|
if (row?.kind === "conv") {
|
|
3433
3641
|
setError(null);
|
|
3434
3642
|
setConfirmId(row.hit.item.id);
|
|
3435
3643
|
}
|
|
3644
|
+
key.preventDefault();
|
|
3436
3645
|
} else if (key.name === "a" && key.ctrl) {
|
|
3437
3646
|
const row = rows[clamped];
|
|
3438
3647
|
if (row?.kind === "conv") {
|
|
@@ -3443,94 +3652,169 @@ function ConversationList({ agent, initialQuery, conversations, complete, showAu
|
|
|
3443
3652
|
setError(null);
|
|
3444
3653
|
toggleArchived(row.hit.item);
|
|
3445
3654
|
}
|
|
3446
|
-
|
|
3447
|
-
else if (key.name === "tab")
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3655
|
+
key.preventDefault();
|
|
3656
|
+
} else if (key.name === "tab" && key.shift) {
|
|
3657
|
+
onToggleArchived();
|
|
3658
|
+
key.preventDefault();
|
|
3659
|
+
} else if (key.name === "tab") {
|
|
3660
|
+
onToggleAutomated();
|
|
3661
|
+
key.preventDefault();
|
|
3451
3662
|
}
|
|
3452
3663
|
});
|
|
3664
|
+
if (!ready) return /* @__PURE__ */ jsx("box", { style: {
|
|
3665
|
+
flexDirection: "column",
|
|
3666
|
+
flexGrow: 1
|
|
3667
|
+
} });
|
|
3453
3668
|
return /* @__PURE__ */ jsxs("box", {
|
|
3454
3669
|
style: {
|
|
3455
3670
|
flexDirection: "column",
|
|
3456
|
-
flexGrow: 1
|
|
3671
|
+
flexGrow: 1,
|
|
3672
|
+
alignItems: "center"
|
|
3457
3673
|
},
|
|
3458
3674
|
children: [
|
|
3459
|
-
/* @__PURE__ */ jsx(
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
}),
|
|
3675
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
3676
|
+
flexGrow: 1,
|
|
3677
|
+
flexShrink: 1
|
|
3678
|
+
} }),
|
|
3463
3679
|
/* @__PURE__ */ jsxs("box", {
|
|
3464
|
-
style: {
|
|
3465
|
-
flexDirection: "row",
|
|
3466
|
-
height: 1
|
|
3467
|
-
},
|
|
3468
|
-
children: [/* @__PURE__ */ jsx("text", {
|
|
3469
|
-
fg: theme.muted,
|
|
3470
|
-
children: "search "
|
|
3471
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
3472
|
-
textColor: theme.fg,
|
|
3473
|
-
focusedTextColor: theme.fg,
|
|
3474
|
-
cursorColor: theme.cursor,
|
|
3475
|
-
placeholderColor: theme.dim,
|
|
3476
|
-
placeholder: "type to filter…",
|
|
3477
|
-
focused: confirmId === null,
|
|
3478
|
-
width: Math.max(10, width - 12),
|
|
3479
|
-
maxLength: SEARCH_QUERY_MAX_LENGTH,
|
|
3480
|
-
value: query,
|
|
3481
|
-
onInput: (value) => {
|
|
3482
|
-
setQuery(value.slice(0, SEARCH_QUERY_MAX_LENGTH));
|
|
3483
|
-
setHighlight(0);
|
|
3484
|
-
}
|
|
3485
|
-
})]
|
|
3486
|
-
}),
|
|
3487
|
-
/* @__PURE__ */ jsx("text", {
|
|
3488
|
-
fg: status.fg,
|
|
3489
|
-
children: status.text
|
|
3490
|
-
}),
|
|
3491
|
-
/* @__PURE__ */ jsx("box", {
|
|
3492
3680
|
style: {
|
|
3493
3681
|
flexDirection: "column",
|
|
3494
|
-
|
|
3682
|
+
width: contentWidth
|
|
3495
3683
|
},
|
|
3496
|
-
children:
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
children: [
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3684
|
+
children: [
|
|
3685
|
+
/* @__PURE__ */ jsxs("box", {
|
|
3686
|
+
style: {
|
|
3687
|
+
flexDirection: "column",
|
|
3688
|
+
alignItems: "center"
|
|
3689
|
+
},
|
|
3690
|
+
children: [/* @__PURE__ */ jsx(BrandHeader, {
|
|
3691
|
+
height,
|
|
3692
|
+
width
|
|
3693
|
+
}), height >= 24 ? /* @__PURE__ */ jsxs("box", {
|
|
3694
|
+
style: {
|
|
3695
|
+
flexDirection: "column",
|
|
3696
|
+
alignItems: "center"
|
|
3697
|
+
},
|
|
3698
|
+
children: [
|
|
3699
|
+
/* @__PURE__ */ jsx("text", {
|
|
3700
|
+
fg: theme.accent,
|
|
3701
|
+
attributes: 1,
|
|
3702
|
+
children: truncate(`Hey, I'm ${agent.name}`, innerWidth)
|
|
3703
|
+
}),
|
|
3704
|
+
identitySubtitle(agent) ? /* @__PURE__ */ jsx("text", {
|
|
3705
|
+
fg: theme.muted,
|
|
3706
|
+
children: truncate(identitySubtitle(agent), innerWidth)
|
|
3707
|
+
}) : null,
|
|
3708
|
+
/* @__PURE__ */ jsx("box", { style: { height: 1 } })
|
|
3709
|
+
]
|
|
3710
|
+
}) : null]
|
|
3711
|
+
}),
|
|
3712
|
+
/* @__PURE__ */ jsx("box", {
|
|
3713
|
+
style: {
|
|
3714
|
+
border: true,
|
|
3715
|
+
borderColor: theme.faint,
|
|
3716
|
+
paddingLeft: 1,
|
|
3717
|
+
paddingRight: 1,
|
|
3718
|
+
height: composerRows + 2,
|
|
3719
|
+
flexShrink: 0,
|
|
3720
|
+
flexDirection: "column"
|
|
3721
|
+
},
|
|
3722
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
3723
|
+
ref: composerRef,
|
|
3724
|
+
textColor: theme.fg,
|
|
3725
|
+
focusedTextColor: theme.fg,
|
|
3726
|
+
cursorColor: theme.cursor,
|
|
3727
|
+
placeholderColor: theme.dim,
|
|
3728
|
+
placeholder: `Ask ${agent.name} anything…`,
|
|
3729
|
+
keyBindings: composerKeyBindings$1,
|
|
3730
|
+
onContentChange: handleComposerChange,
|
|
3731
|
+
onSubmit: submit,
|
|
3732
|
+
focused: confirmId === null,
|
|
3733
|
+
style: {
|
|
3734
|
+
height: composerRows,
|
|
3735
|
+
wrapMode: "word"
|
|
3736
|
+
}
|
|
3737
|
+
})
|
|
3738
|
+
}),
|
|
3739
|
+
/* @__PURE__ */ jsx("text", {
|
|
3740
|
+
fg: status.fg,
|
|
3741
|
+
children: status.text
|
|
3742
|
+
}),
|
|
3743
|
+
/* @__PURE__ */ jsx("box", {
|
|
3744
|
+
style: {
|
|
3745
|
+
flexDirection: "column",
|
|
3746
|
+
flexShrink: 0
|
|
3747
|
+
},
|
|
3748
|
+
children: rows.length === 0 ? /* @__PURE__ */ jsx("text", {
|
|
3749
|
+
fg: theme.dim,
|
|
3750
|
+
children: isSearching ? "no conversations match." : `no conversations yet — type a message above to start one with ${agent.name}.`
|
|
3751
|
+
}) : windowed.map((row, i) => {
|
|
3752
|
+
const selected = start + i === clamped;
|
|
3753
|
+
const fg = selected ? theme.accent : theme.fg;
|
|
3754
|
+
const marker = selected ? "› " : " ";
|
|
3755
|
+
const archived = isArchived(row.hit.item);
|
|
3756
|
+
const { title, time } = layoutRow(`${archived ? "archived · " : ""}${singleLine(row.hit.item.title ?? "(untitled)")}`, relativeTime(row.hit.item.updatedAt), coAgentsTag(row.hit.item, agent.id), rowWidth);
|
|
3757
|
+
const glyph = channelGlyph(row.hit.item.channel);
|
|
3758
|
+
const tag = coAgentsTag(row.hit.item, agent.id);
|
|
3759
|
+
const unread = row.hit.item.unread === true;
|
|
3760
|
+
const titleText = /* @__PURE__ */ jsx(FuzzyText, {
|
|
3761
|
+
text: title,
|
|
3762
|
+
indexes: row.hit.item.title ? row.hit.highlights[0] : null
|
|
3763
|
+
});
|
|
3764
|
+
return /* @__PURE__ */ jsxs("text", {
|
|
3765
|
+
fg: archived && !selected ? theme.dim : fg,
|
|
3766
|
+
children: [
|
|
3767
|
+
!selected && unread ? /* @__PURE__ */ jsx("span", {
|
|
3768
|
+
fg: theme.accent,
|
|
3769
|
+
children: "● "
|
|
3770
|
+
}) : marker,
|
|
3771
|
+
/* @__PURE__ */ jsxs("span", {
|
|
3772
|
+
fg: selected ? theme.accent : theme.muted,
|
|
3773
|
+
children: [glyph, " "]
|
|
3774
|
+
}),
|
|
3775
|
+
unread ? /* @__PURE__ */ jsx("b", { children: titleText }) : titleText,
|
|
3776
|
+
tag ? /* @__PURE__ */ jsxs("span", {
|
|
3777
|
+
fg: theme.muted,
|
|
3778
|
+
children: [" ", tag]
|
|
3779
|
+
}) : null,
|
|
3780
|
+
/* @__PURE__ */ jsx("span", {
|
|
3781
|
+
fg: theme.dim,
|
|
3782
|
+
children: time
|
|
3783
|
+
})
|
|
3784
|
+
]
|
|
3785
|
+
}, row.hit.item.id);
|
|
3786
|
+
})
|
|
3787
|
+
})
|
|
3788
|
+
]
|
|
3789
|
+
}),
|
|
3790
|
+
/* @__PURE__ */ jsx("box", { style: {
|
|
3791
|
+
flexGrow: 1,
|
|
3792
|
+
flexShrink: 1
|
|
3793
|
+
} })
|
|
3531
3794
|
]
|
|
3532
3795
|
});
|
|
3533
3796
|
}
|
|
3797
|
+
const MIN_LIST_ROWS = 3;
|
|
3798
|
+
const composerKeyBindings$1 = [
|
|
3799
|
+
{
|
|
3800
|
+
name: "return",
|
|
3801
|
+
action: "submit"
|
|
3802
|
+
},
|
|
3803
|
+
{
|
|
3804
|
+
name: "kpenter",
|
|
3805
|
+
action: "submit"
|
|
3806
|
+
},
|
|
3807
|
+
{
|
|
3808
|
+
name: "return",
|
|
3809
|
+
shift: true,
|
|
3810
|
+
action: "newline"
|
|
3811
|
+
},
|
|
3812
|
+
{
|
|
3813
|
+
name: "kpenter",
|
|
3814
|
+
shift: true,
|
|
3815
|
+
action: "newline"
|
|
3816
|
+
}
|
|
3817
|
+
];
|
|
3534
3818
|
/**
|
|
3535
3819
|
* Tags a row with the thread's other agents (`+Grace, Ada`). The picker is
|
|
3536
3820
|
* entered per agent, so that agent stays implicit and only its co-participants
|
|
@@ -3547,20 +3831,6 @@ function coAgentsTag(conv, viewerAgentId) {
|
|
|
3547
3831
|
function conversationAgents(c) {
|
|
3548
3832
|
return c.agents && c.agents.length > 0 ? c.agents : [c.agent];
|
|
3549
3833
|
}
|
|
3550
|
-
const conversationKeys = [
|
|
3551
|
-
{
|
|
3552
|
-
get: (c) => singleLine(c.title ?? ""),
|
|
3553
|
-
weight: 1
|
|
3554
|
-
},
|
|
3555
|
-
{
|
|
3556
|
-
get: (c) => c.preview ?? "",
|
|
3557
|
-
weight: .6
|
|
3558
|
-
},
|
|
3559
|
-
{
|
|
3560
|
-
get: (c) => conversationAgents(c).map((a) => a.name).join(" "),
|
|
3561
|
-
weight: .4
|
|
3562
|
-
}
|
|
3563
|
-
];
|
|
3564
3834
|
/**
|
|
3565
3835
|
* Character indexes of the first case-insensitive occurrence of `needle` in
|
|
3566
3836
|
* `haystack`, for highlighting a literal title match under server search (the
|
|
@@ -3575,11 +3845,63 @@ function substringIndexes(haystack, needle) {
|
|
|
3575
3845
|
if (at < 0) return null;
|
|
3576
3846
|
return Array.from({ length: trimmed.length }, (_, i) => at + i);
|
|
3577
3847
|
}
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3848
|
+
/**
|
|
3849
|
+
* The subtitle under the “Hey, I'm <Agent>” greeting, mirroring the web home's
|
|
3850
|
+
* “<title> · <owner>'s agent” line. Either half may be missing (an older
|
|
3851
|
+
* server omits `title`/`creatorName`); we join whatever is present and return
|
|
3852
|
+
* an empty string when there's nothing to show, so the caller drops the row.
|
|
3853
|
+
*/
|
|
3854
|
+
function identitySubtitle(agent) {
|
|
3855
|
+
const parts = [];
|
|
3856
|
+
if (agent.title) parts.push(agent.title);
|
|
3857
|
+
if (agent.creatorName) parts.push(`${agent.creatorName}'s agent`);
|
|
3858
|
+
return parts.join(" · ");
|
|
3859
|
+
}
|
|
3860
|
+
/**
|
|
3861
|
+
* A single glyph marking a conversation's channel, echoing the web home's
|
|
3862
|
+
* leading channel icon. Web threads (the common case) get a globe; the other
|
|
3863
|
+
* human channels and agent-run channels get a recognizable mark, with a
|
|
3864
|
+
* neutral dot as the fallback so an unknown/again-renamed channel still lines
|
|
3865
|
+
* up. Kept to one display column so the row layout budget stays exact.
|
|
3866
|
+
*/
|
|
3867
|
+
function channelGlyph(channel) {
|
|
3868
|
+
switch (channel) {
|
|
3869
|
+
case "web":
|
|
3870
|
+
case null: return "◉";
|
|
3871
|
+
case "slack": return "▰";
|
|
3872
|
+
case "email": return "✉";
|
|
3873
|
+
case "imessage":
|
|
3874
|
+
case "sms": return "○";
|
|
3875
|
+
case "cron": return "⏲";
|
|
3876
|
+
default: return "·";
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Lay a row out web-home style: the title on the left, the relative time
|
|
3881
|
+
* flush-right, and an optional co-agent tag between them. The glyph (2 cols
|
|
3882
|
+
* incl. its trailing space) and marker (2 cols) are accounted for by the
|
|
3883
|
+
* caller via `width` (already `rowWidth`), so here we reserve the time (plus a
|
|
3884
|
+
* gap), the tag (plus a gap) and the glyph slot, then truncate the title into
|
|
3885
|
+
* what's left. The returned `time` carries its own leading padding so it sits
|
|
3886
|
+
* flush against the right edge; when the row is too narrow to fit the title
|
|
3887
|
+
* AND the time, the time is dropped rather than wrapping the row.
|
|
3888
|
+
*/
|
|
3889
|
+
function layoutRow(title, time, tag, width) {
|
|
3890
|
+
const GLYPH = 2;
|
|
3891
|
+
const tagCost = tag ? tag.length + 1 : 0;
|
|
3892
|
+
const timeCost = time.length + 1;
|
|
3893
|
+
const titleBudget = width - GLYPH - tagCost - timeCost;
|
|
3894
|
+
if (titleBudget < 8) return {
|
|
3895
|
+
title: truncate(title, Math.max(1, width - GLYPH - tagCost)),
|
|
3896
|
+
time: ""
|
|
3897
|
+
};
|
|
3898
|
+
const fitTitle = truncate(title, titleBudget);
|
|
3899
|
+
const used = GLYPH + fitTitle.length + tagCost;
|
|
3900
|
+
const pad = Math.max(1, width - used - time.length);
|
|
3901
|
+
return {
|
|
3902
|
+
title: fitTitle,
|
|
3903
|
+
time: `${" ".repeat(pad)}${time}`
|
|
3904
|
+
};
|
|
3583
3905
|
}
|
|
3584
3906
|
function relativeTime(iso) {
|
|
3585
3907
|
const diffMs = Date.now() - new Date(iso).getTime();
|
|
@@ -7393,7 +7715,6 @@ function SubagentCardView({ items, isActive, onHeightChange }) {
|
|
|
7393
7715
|
style: {
|
|
7394
7716
|
flexDirection: "column",
|
|
7395
7717
|
flexShrink: 0,
|
|
7396
|
-
height: height > 0 ? void 0 : 0,
|
|
7397
7718
|
marginTop: height > 0 ? 1 : 0,
|
|
7398
7719
|
paddingLeft: height > 0 ? 1 : 0,
|
|
7399
7720
|
paddingRight: height > 0 ? 1 : 0,
|
|
@@ -13163,6 +13484,7 @@ function isActionableCard(m) {
|
|
|
13163
13484
|
function ChatScreen({ agent, conversation, attachRunId, parentConversationId }) {
|
|
13164
13485
|
const renderer = useRenderer();
|
|
13165
13486
|
const { width, height } = useTerminalDimensions();
|
|
13487
|
+
const { ready: terminalReady } = useTerminalReady();
|
|
13166
13488
|
const rest = useStore((s) => s.rest);
|
|
13167
13489
|
const history = usePromptHistory({ filePath: useStore((s) => s.promptHistoryPath) });
|
|
13168
13490
|
const appUrl = useStore((s) => s.appUrl);
|
|
@@ -14590,6 +14912,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14590
14912
|
agentSelector: null,
|
|
14591
14913
|
conversationId: target,
|
|
14592
14914
|
newConversation: false,
|
|
14915
|
+
seedPrompt: null,
|
|
14593
14916
|
defaultAgentSelector: null,
|
|
14594
14917
|
forceOnboarding: false
|
|
14595
14918
|
});
|
|
@@ -14630,6 +14953,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14630
14953
|
agentSelector: null,
|
|
14631
14954
|
conversationId: target,
|
|
14632
14955
|
newConversation: false,
|
|
14956
|
+
seedPrompt: null,
|
|
14633
14957
|
defaultAgentSelector: null,
|
|
14634
14958
|
forceOnboarding: false
|
|
14635
14959
|
}));
|
|
@@ -14777,15 +15101,9 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14777
15101
|
break;
|
|
14778
15102
|
}
|
|
14779
15103
|
goTo({
|
|
14780
|
-
kind: "
|
|
15104
|
+
kind: "conversation-picker",
|
|
14781
15105
|
agent,
|
|
14782
|
-
|
|
14783
|
-
parentConversationId: null,
|
|
14784
|
-
conversation: {
|
|
14785
|
-
kind: "new",
|
|
14786
|
-
agentId: agent.id,
|
|
14787
|
-
nonce: crypto.randomUUID()
|
|
14788
|
-
}
|
|
15106
|
+
initialQuery: ""
|
|
14789
15107
|
});
|
|
14790
15108
|
break;
|
|
14791
15109
|
case "fork":
|
|
@@ -15289,6 +15607,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15289
15607
|
agentSelector: null,
|
|
15290
15608
|
conversationId: parentConversationId,
|
|
15291
15609
|
newConversation: false,
|
|
15610
|
+
seedPrompt: null,
|
|
15292
15611
|
defaultAgentSelector: null,
|
|
15293
15612
|
forceOnboarding: false
|
|
15294
15613
|
}).then((parentScreen) => goTo(parentScreen)).catch(() => {
|
|
@@ -15567,7 +15886,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15567
15886
|
children: [!historyLoaded ? /* @__PURE__ */ jsx("text", {
|
|
15568
15887
|
fg: theme.muted,
|
|
15569
15888
|
children: "loading history…"
|
|
15570
|
-
}) : items.length === 0 ? /* @__PURE__ */ jsx(NewChatSplash, {
|
|
15889
|
+
}) : items.length === 0 && !terminalReady ? /* @__PURE__ */ jsx("box", { style: { height: Math.max(1, scrollHeight - 1) } }) : items.length === 0 ? /* @__PURE__ */ jsx(NewChatSplash, {
|
|
15571
15890
|
agentName: agent.name,
|
|
15572
15891
|
agentTitle: agent.title ?? null,
|
|
15573
15892
|
width: reviewView && reviewPlacement === "side" ? Math.max(1, bodyWidth - reviewWidth - 1) : bodyWidth,
|
|
@@ -16216,7 +16535,8 @@ function App({ appUrl, sessionToken, authKind, workspaceId, shareMachine, prompt
|
|
|
16216
16535
|
rest,
|
|
16217
16536
|
agentSelector,
|
|
16218
16537
|
conversationId,
|
|
16219
|
-
newConversation
|
|
16538
|
+
newConversation,
|
|
16539
|
+
seedPrompt: useStore.getState().seedPrompt,
|
|
16220
16540
|
defaultAgentSelector,
|
|
16221
16541
|
forceOnboarding
|
|
16222
16542
|
});
|
|
@@ -16282,16 +16602,14 @@ function App({ appUrl, sessionToken, authKind, workspaceId, shareMachine, prompt
|
|
|
16282
16602
|
children: [
|
|
16283
16603
|
screen.kind === "splash" && /* @__PURE__ */ jsx(Splash, {}),
|
|
16284
16604
|
screen.kind === "error" && /* @__PURE__ */ jsx(ErrorScreen, { message: screen.message }),
|
|
16285
|
-
screen.kind === "
|
|
16286
|
-
screen.kind === "agent-create" && rest && /* @__PURE__ */ jsx(AgentCreateScreen, {}),
|
|
16287
|
-
screen.kind === "first-run" && rest && /* @__PURE__ */ jsx(FirstRunScreen, {}),
|
|
16605
|
+
screen.kind === "welcome" && rest && /* @__PURE__ */ jsx(WelcomeScreen, {}),
|
|
16288
16606
|
screen.kind === "workspace-picker" && /* @__PURE__ */ jsx(WorkspacePicker, {
|
|
16289
16607
|
appUrl,
|
|
16290
16608
|
sessionToken,
|
|
16291
|
-
onCancel: () => goTo({ kind: "
|
|
16609
|
+
onCancel: () => goTo({ kind: "welcome" }),
|
|
16292
16610
|
onSelect: (workspace) => {
|
|
16293
16611
|
setWorkspaceName(workspace.name);
|
|
16294
|
-
goTo({ kind: "
|
|
16612
|
+
goTo({ kind: "welcome" });
|
|
16295
16613
|
}
|
|
16296
16614
|
}),
|
|
16297
16615
|
screen.kind === "conversation-picker" && rest && /* @__PURE__ */ jsx(ConversationPickerScreen, {
|
|
@@ -16842,7 +17160,6 @@ async function runChat({ appUrl, sessionToken, authKind = "session", workspaceId
|
|
|
16842
17160
|
authKind,
|
|
16843
17161
|
seedPrompt: seedPrompt ?? null,
|
|
16844
17162
|
autoGrantMachine: Boolean(seedPrompt && shareMachine),
|
|
16845
|
-
newConversationIntent: newConversation,
|
|
16846
17163
|
forcedOnboarding: forceOnboarding
|
|
16847
17164
|
});
|
|
16848
17165
|
const store = useStore.getState();
|