skydive-cli 0.6.0-beta.18 → 0.6.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/dist/js/bin.mjs +20 -20
- package/dist/js/{boot-BcyHfvnh.mjs → boot-CPaxEwFQ.mjs} +722 -465
- package/dist/js/{client-DG0vUvWI.mjs → client-BPgSlxoE.mjs} +1 -1
- package/dist/js/client-BhSqd_K5.mjs +5 -0
- package/dist/js/{daemon-rHe1xapN.mjs → daemon-By6thwls.mjs} +1 -1
- package/dist/js/{daemon-DZ9YSkAh.mjs → daemon-DH8a0-D8.mjs} +2 -2
- package/dist/js/daemon-client-ClJWL3-E.mjs +8 -0
- package/dist/js/{daemon-client-D-k67MEB.mjs → daemon-client-CyYn91wO.mjs} +1 -1
- package/dist/js/{install-Dvl9vzTP.mjs → install-i9xERvoF.mjs} +2 -2
- package/dist/js/{print-CCXpClJw.mjs → print-BryBur1Q.mjs} +1 -1
- package/dist/js/{print-COTmamJX.mjs → print-CGP6esDJ.mjs} +2 -2
- package/dist/js/{print-share-34Iwxo39.mjs → print-share-DDAfbocP.mjs} +1 -1
- package/dist/js/{rest-D2MDvQ6c.mjs → rest-CbG5RvAI.mjs} +1 -1
- package/dist/js/{rest-DZCq0tZw.mjs → rest-iJiyyUZk.mjs} +1 -1
- package/package.json +5 -5
- package/dist/js/client-BpD6yakI.mjs +0 -5
- package/dist/js/daemon-client-BwyXZn0g.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-iJiyyUZk.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-BryBur1Q.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-i9xERvoF.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-BPgSlxoE.mjs";
|
|
8
8
|
import { t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
|
|
9
9
|
import "./api-BFQ4PQDA.mjs";
|
|
10
|
-
import "./daemon-
|
|
10
|
+
import "./daemon-By6thwls.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-CyYn91wO.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";
|
|
@@ -136,6 +136,7 @@ const useStore = create((set, get) => ({
|
|
|
136
136
|
portalClient: null,
|
|
137
137
|
toast: null,
|
|
138
138
|
seedPrompt: null,
|
|
139
|
+
seedAttachments: [],
|
|
139
140
|
autoGrantMachine: false,
|
|
140
141
|
forcedOnboarding: false,
|
|
141
142
|
earlyInput: "",
|
|
@@ -3120,6 +3121,393 @@ function WorkspacePicker({ appUrl, sessionToken, onSelect, onCancel }) {
|
|
|
3120
3121
|
});
|
|
3121
3122
|
}
|
|
3122
3123
|
|
|
3124
|
+
//#endregion
|
|
3125
|
+
//#region src/chat/paste.ts
|
|
3126
|
+
const MAX_PATH_PASTE_CHARS = 4096;
|
|
3127
|
+
/**
|
|
3128
|
+
* Decode a `file://` URL to a filesystem path, host-independently. Node's
|
|
3129
|
+
* `fileURLToPath` maps to the *running* OS's path shape, so a POSIX file URL
|
|
3130
|
+
* pasted on Windows would come back as a backslash drive-relative path. A
|
|
3131
|
+
* dropped-file URL is decided by its own shape, not by our host: a Windows
|
|
3132
|
+
* drive URL (`file:///C:/...`) yields a Windows path, anything else a POSIX
|
|
3133
|
+
* path. Returns null for a malformed URL.
|
|
3134
|
+
*/
|
|
3135
|
+
function fileUrlToPath(url) {
|
|
3136
|
+
let parsed;
|
|
3137
|
+
try {
|
|
3138
|
+
parsed = new URL(url);
|
|
3139
|
+
} catch (_error) {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
if (parsed.protocol !== "file:") return null;
|
|
3143
|
+
const decoded = decodeURIComponent(parsed.pathname);
|
|
3144
|
+
if (/^\/[a-zA-Z]:\//.test(decoded)) return decoded.slice(1).replace(/\//g, "\\");
|
|
3145
|
+
return decoded;
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* Parse the text a terminal emits for dropped files. This only identifies
|
|
3149
|
+
* path-shaped candidates; filesystem and content validation happen later.
|
|
3150
|
+
*/
|
|
3151
|
+
function parseDroppedPaths(text) {
|
|
3152
|
+
const trimmed = text.trim();
|
|
3153
|
+
if (!trimmed || trimmed.length > MAX_PATH_PASTE_CHARS || /[\r\n]/.test(trimmed)) return null;
|
|
3154
|
+
const tokens = [];
|
|
3155
|
+
let current = "";
|
|
3156
|
+
let quote = null;
|
|
3157
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
3158
|
+
const ch = trimmed.charAt(i);
|
|
3159
|
+
if (quote) {
|
|
3160
|
+
if (ch === quote) quote = null;
|
|
3161
|
+
else if (ch === "\\" && quote === "\"" && i + 1 < trimmed.length) {
|
|
3162
|
+
current += trimmed.charAt(i + 1);
|
|
3163
|
+
i += 1;
|
|
3164
|
+
} else current += ch;
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
3167
|
+
if (ch === "'" || ch === "\"") {
|
|
3168
|
+
quote = ch;
|
|
3169
|
+
continue;
|
|
3170
|
+
}
|
|
3171
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
3172
|
+
current += trimmed.charAt(i + 1);
|
|
3173
|
+
i += 1;
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
if (ch === " " || ch === " ") {
|
|
3177
|
+
if (current) {
|
|
3178
|
+
tokens.push(current);
|
|
3179
|
+
current = "";
|
|
3180
|
+
}
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
current += ch;
|
|
3184
|
+
}
|
|
3185
|
+
if (quote) return null;
|
|
3186
|
+
if (current) tokens.push(current);
|
|
3187
|
+
if (tokens.length === 0) return null;
|
|
3188
|
+
const paths = [];
|
|
3189
|
+
for (const token of tokens) {
|
|
3190
|
+
let path = token;
|
|
3191
|
+
if (path.startsWith("file://")) {
|
|
3192
|
+
const resolved = fileUrlToPath(path);
|
|
3193
|
+
if (resolved === null) return null;
|
|
3194
|
+
path = resolved;
|
|
3195
|
+
}
|
|
3196
|
+
if (path.startsWith("~/")) path = `${homedir()}${path.slice(1)}`;
|
|
3197
|
+
if (!isAbsolute(path) && !win32.isAbsolute(path) && !path.startsWith("./") && !path.startsWith("../")) return null;
|
|
3198
|
+
paths.push(path);
|
|
3199
|
+
}
|
|
3200
|
+
return paths;
|
|
3201
|
+
}
|
|
3202
|
+
const TEXT_MIME_BY_EXT = {
|
|
3203
|
+
".txt": "text/plain",
|
|
3204
|
+
".md": "text/markdown",
|
|
3205
|
+
".markdown": "text/markdown",
|
|
3206
|
+
".csv": "text/csv",
|
|
3207
|
+
".tsv": "text/tab-separated-values",
|
|
3208
|
+
".json": "application/json",
|
|
3209
|
+
".yaml": "application/yaml",
|
|
3210
|
+
".yml": "application/yaml",
|
|
3211
|
+
".xml": "application/xml",
|
|
3212
|
+
".html": "text/html",
|
|
3213
|
+
".htm": "text/html",
|
|
3214
|
+
".css": "text/css",
|
|
3215
|
+
".js": "text/javascript",
|
|
3216
|
+
".ts": "text/plain",
|
|
3217
|
+
".tsx": "text/plain",
|
|
3218
|
+
".jsx": "text/plain",
|
|
3219
|
+
".py": "text/x-python",
|
|
3220
|
+
".sh": "text/x-shellscript",
|
|
3221
|
+
".log": "text/plain",
|
|
3222
|
+
".svg": "image/svg+xml"
|
|
3223
|
+
};
|
|
3224
|
+
/** Decide how to handle a paste. `kind: 'binary'` events carry the mime of
|
|
3225
|
+
* bytes the terminal forwarded; text events carry the decoded paste text.
|
|
3226
|
+
*
|
|
3227
|
+
* The empty-text case is the fix for "can't paste an image": on macOS the
|
|
3228
|
+
* reflex is Cmd+V, which the terminal turns into a bracketed *text* paste, so
|
|
3229
|
+
* a screenshot on the clipboard arrives as empty bytes. We route that to an OS
|
|
3230
|
+
* clipboard read instead of dropping it, matching the explicit ctrl+v path. */
|
|
3231
|
+
function routePaste(input) {
|
|
3232
|
+
if (input.kind === "binary") {
|
|
3233
|
+
if (input.mimeType?.startsWith("image/")) return {
|
|
3234
|
+
kind: "binary-image",
|
|
3235
|
+
mediaType: input.mimeType
|
|
3236
|
+
};
|
|
3237
|
+
return { kind: "text" };
|
|
3238
|
+
}
|
|
3239
|
+
const paths = parseDroppedPaths(input.text);
|
|
3240
|
+
if (paths) return {
|
|
3241
|
+
kind: "dropped-paths",
|
|
3242
|
+
paths,
|
|
3243
|
+
text: input.text
|
|
3244
|
+
};
|
|
3245
|
+
if (input.text.trim() === "") return { kind: "clipboard-image" };
|
|
3246
|
+
return { kind: "text" };
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Resolve path candidates into attachable files. Every path must be an
|
|
3250
|
+
* existing regular file; otherwise the caller should restore the original
|
|
3251
|
+
* paste as text (an all-or-nothing rule so a paste meant as text — which
|
|
3252
|
+
* merely *looks* path-shaped — is never partially eaten).
|
|
3253
|
+
*
|
|
3254
|
+
* Any file type attaches. The mediaType comes from magic bytes when
|
|
3255
|
+
* detectable, an extension map for the plain-text formats magic can't see,
|
|
3256
|
+
* and application/octet-stream as the last resort — the attachments API
|
|
3257
|
+
* accepts any mediaType (100MB cap enforced server-side at presign).
|
|
3258
|
+
*/
|
|
3259
|
+
async function resolveDroppedFiles(paths) {
|
|
3260
|
+
try {
|
|
3261
|
+
const files = await Promise.all(paths.map(async (path) => {
|
|
3262
|
+
if (!(await stat(path)).isFile()) return null;
|
|
3263
|
+
const data = new Uint8Array(await readFile(path));
|
|
3264
|
+
const mediaType = (await fileTypeFromBuffer(data))?.mime ?? TEXT_MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
3265
|
+
return {
|
|
3266
|
+
fileName: basename(path),
|
|
3267
|
+
mediaType,
|
|
3268
|
+
data
|
|
3269
|
+
};
|
|
3270
|
+
}));
|
|
3271
|
+
if (!files.every((file) => file !== null)) return null;
|
|
3272
|
+
return files;
|
|
3273
|
+
} catch (_error) {
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
//#endregion
|
|
3279
|
+
//#region src/chat/tui/composer-attachments.tsx
|
|
3280
|
+
/**
|
|
3281
|
+
* Attachments a composer can stage for its next send, shared by every screen
|
|
3282
|
+
* that owns a composer (the chat transcript and the agent home). Staging
|
|
3283
|
+
* starts the upload immediately — the web composer's behavior — so submit
|
|
3284
|
+
* only awaits the in-flight uploads and collects their server ids.
|
|
3285
|
+
*/
|
|
3286
|
+
/** Files one composer can hold at once, so a stray multi-file drop can't
|
|
3287
|
+
* queue an unbounded pile of uploads. */
|
|
3288
|
+
const MAX_COMPOSER_ATTACHMENTS = 10;
|
|
3289
|
+
/**
|
|
3290
|
+
* Owns a composer's staged attachments and the paste routing that feeds them.
|
|
3291
|
+
*
|
|
3292
|
+
* `pasteDisabled` suppresses attaching while the screen has something else in
|
|
3293
|
+
* front of the composer (an overlay, a prompt, a channel with no composer at
|
|
3294
|
+
* all); the paste then falls through to whatever is focused, untouched.
|
|
3295
|
+
* `onPasteText` restores a paste that only *looked* like dropped file paths —
|
|
3296
|
+
* the filesystem check is async, so the text has to be held and put back.
|
|
3297
|
+
* `onError` receives user-facing failures (the cap, a failed upload) because
|
|
3298
|
+
* each screen surfaces them differently: the transcript takes an error row,
|
|
3299
|
+
* the home takes its status line.
|
|
3300
|
+
*/
|
|
3301
|
+
function useComposerAttachments(input) {
|
|
3302
|
+
const { rest, agentId, pasteDisabled, onPasteText, onError } = input;
|
|
3303
|
+
const [staged, setStaged] = useState([]);
|
|
3304
|
+
const uploadsRef = useRef(/* @__PURE__ */ new Map());
|
|
3305
|
+
const stagedRef = useRef(staged);
|
|
3306
|
+
stagedRef.current = staged;
|
|
3307
|
+
const stageFile = useCallback(({ fileName, mediaType, data }) => {
|
|
3308
|
+
if (!rest) return;
|
|
3309
|
+
if (stagedRef.current.length >= MAX_COMPOSER_ATTACHMENTS) {
|
|
3310
|
+
onError(`attachment limit reached (${MAX_COMPOSER_ATTACHMENTS}); ${fileName} skipped`);
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
const tempId = crypto.randomUUID();
|
|
3314
|
+
setStaged((prev) => [...prev, {
|
|
3315
|
+
tempId,
|
|
3316
|
+
fileName,
|
|
3317
|
+
sizeBytes: data.byteLength,
|
|
3318
|
+
status: "uploading",
|
|
3319
|
+
errorText: null
|
|
3320
|
+
}]);
|
|
3321
|
+
const promise = rest.uploadAttachment({
|
|
3322
|
+
agentId,
|
|
3323
|
+
fileName,
|
|
3324
|
+
mediaType,
|
|
3325
|
+
data
|
|
3326
|
+
});
|
|
3327
|
+
uploadsRef.current.set(tempId, promise);
|
|
3328
|
+
promise.then(() => {
|
|
3329
|
+
setStaged((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
3330
|
+
...a,
|
|
3331
|
+
status: "ready"
|
|
3332
|
+
} : a));
|
|
3333
|
+
}).catch((err) => {
|
|
3334
|
+
setStaged((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
3335
|
+
...a,
|
|
3336
|
+
status: "error",
|
|
3337
|
+
errorText: errorMessage(err)
|
|
3338
|
+
} : a));
|
|
3339
|
+
});
|
|
3340
|
+
}, [
|
|
3341
|
+
rest,
|
|
3342
|
+
agentId,
|
|
3343
|
+
onError
|
|
3344
|
+
]);
|
|
3345
|
+
const stageFiles = useCallback((files) => {
|
|
3346
|
+
for (const file of files) stageFile(file);
|
|
3347
|
+
}, [stageFile]);
|
|
3348
|
+
const stageClipboardImage = useCallback(async () => {
|
|
3349
|
+
const image = await readClipboardImage();
|
|
3350
|
+
if (!image) return;
|
|
3351
|
+
stageFile(image);
|
|
3352
|
+
}, [stageFile]);
|
|
3353
|
+
const dropLast = useCallback(() => {
|
|
3354
|
+
const last = stagedRef.current.at(-1);
|
|
3355
|
+
if (!last) return;
|
|
3356
|
+
uploadsRef.current.delete(last.tempId);
|
|
3357
|
+
setStaged((prev) => prev.filter((a) => a.tempId !== last.tempId));
|
|
3358
|
+
}, []);
|
|
3359
|
+
const clearStaged = useCallback(() => setStaged([]), []);
|
|
3360
|
+
const collectForSend = useCallback(async (staged) => {
|
|
3361
|
+
const files = [];
|
|
3362
|
+
const rows = [];
|
|
3363
|
+
const failed = [];
|
|
3364
|
+
const settled = await Promise.allSettled(staged.map((a) => uploadsRef.current.get(a.tempId)));
|
|
3365
|
+
for (const [i, result] of settled.entries()) {
|
|
3366
|
+
const row = staged[i];
|
|
3367
|
+
if (!row) continue;
|
|
3368
|
+
if (result.status === "fulfilled" && result.value) {
|
|
3369
|
+
files.push(result.value);
|
|
3370
|
+
rows.push({
|
|
3371
|
+
...row,
|
|
3372
|
+
status: "ready",
|
|
3373
|
+
errorText: null
|
|
3374
|
+
});
|
|
3375
|
+
} else {
|
|
3376
|
+
uploadsRef.current.delete(row.tempId);
|
|
3377
|
+
failed.push({
|
|
3378
|
+
attachment: row,
|
|
3379
|
+
reason: result.status === "rejected" ? result.reason : null
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return {
|
|
3384
|
+
files,
|
|
3385
|
+
rows,
|
|
3386
|
+
failed
|
|
3387
|
+
};
|
|
3388
|
+
}, []);
|
|
3389
|
+
const releaseUploaded = useCallback((rows) => {
|
|
3390
|
+
for (const row of rows) uploadsRef.current.delete(row.tempId);
|
|
3391
|
+
}, []);
|
|
3392
|
+
const adoptUploaded = useCallback((uploaded) => uploaded.map((file) => {
|
|
3393
|
+
const tempId = crypto.randomUUID();
|
|
3394
|
+
uploadsRef.current.set(tempId, Promise.resolve(file));
|
|
3395
|
+
return {
|
|
3396
|
+
tempId,
|
|
3397
|
+
fileName: file.fileName,
|
|
3398
|
+
sizeBytes: file.sizeBytes,
|
|
3399
|
+
status: "ready",
|
|
3400
|
+
errorText: null
|
|
3401
|
+
};
|
|
3402
|
+
}), []);
|
|
3403
|
+
const restoreStaged = useCallback((rows) => {
|
|
3404
|
+
const restore = rows.filter((a) => uploadsRef.current.has(a.tempId));
|
|
3405
|
+
if (restore.length === 0) return;
|
|
3406
|
+
setStaged((prev) => [...restore.map((a) => ({
|
|
3407
|
+
...a,
|
|
3408
|
+
status: "ready",
|
|
3409
|
+
errorText: null
|
|
3410
|
+
})), ...prev]);
|
|
3411
|
+
}, []);
|
|
3412
|
+
usePaste((event) => {
|
|
3413
|
+
if (pasteDisabled) return;
|
|
3414
|
+
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
3415
|
+
const route = routePaste({
|
|
3416
|
+
kind: event.metadata?.kind,
|
|
3417
|
+
mimeType: event.metadata?.mimeType,
|
|
3418
|
+
text
|
|
3419
|
+
});
|
|
3420
|
+
switch (route.kind) {
|
|
3421
|
+
case "binary-image":
|
|
3422
|
+
event.preventDefault();
|
|
3423
|
+
stageFiles([{
|
|
3424
|
+
fileName: pastedImageName(route.mediaType),
|
|
3425
|
+
mediaType: route.mediaType,
|
|
3426
|
+
data: event.bytes
|
|
3427
|
+
}]);
|
|
3428
|
+
return;
|
|
3429
|
+
case "clipboard-image":
|
|
3430
|
+
event.preventDefault();
|
|
3431
|
+
stageClipboardImage();
|
|
3432
|
+
return;
|
|
3433
|
+
case "dropped-paths":
|
|
3434
|
+
event.preventDefault();
|
|
3435
|
+
resolveDroppedFiles(route.paths).then((files) => {
|
|
3436
|
+
if (files) {
|
|
3437
|
+
stageFiles(files);
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
onPasteText(route.text);
|
|
3441
|
+
});
|
|
3442
|
+
return;
|
|
3443
|
+
case "text": return;
|
|
3444
|
+
}
|
|
3445
|
+
});
|
|
3446
|
+
return {
|
|
3447
|
+
staged,
|
|
3448
|
+
stagedRef,
|
|
3449
|
+
stageFiles,
|
|
3450
|
+
stageClipboardImage,
|
|
3451
|
+
dropLast,
|
|
3452
|
+
clearStaged,
|
|
3453
|
+
collectForSend,
|
|
3454
|
+
adoptUploaded,
|
|
3455
|
+
releaseUploaded,
|
|
3456
|
+
restoreStaged
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
/** Name for clipboard image bytes, which arrive with no filename of their
|
|
3460
|
+
* own: `pasted-image-<timestamp>.<ext>` from the paste's own media type. */
|
|
3461
|
+
function pastedImageName(mediaType) {
|
|
3462
|
+
const ext = mediaType.split("/")[1]?.split("+")[0] ?? "png";
|
|
3463
|
+
return `pasted-image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.${ext}`;
|
|
3464
|
+
}
|
|
3465
|
+
function formatBytes$2(bytes) {
|
|
3466
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3467
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
3468
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3469
|
+
}
|
|
3470
|
+
/** Status glyph for a staged attachment chip: uploading / ready / failed. */
|
|
3471
|
+
function attachmentGlyph(a) {
|
|
3472
|
+
switch (a.status) {
|
|
3473
|
+
case "uploading": return "↑";
|
|
3474
|
+
case "ready": return "✓";
|
|
3475
|
+
case "error": return `✗ ${a.errorText ?? "upload failed"}`;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
/** Rows a rendered tray occupies: one per attachment plus the hint line and
|
|
3479
|
+
* its top margin, so a screen can budget its own layout around it. */
|
|
3480
|
+
function attachmentTrayRows(count) {
|
|
3481
|
+
return count === 0 ? 0 : count + 2;
|
|
3482
|
+
}
|
|
3483
|
+
/** The staged-attachment tray under a composer: a chip per file with its
|
|
3484
|
+
* upload state, and the chord that removes the last one. */
|
|
3485
|
+
function AttachmentTray({ attachments }) {
|
|
3486
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
3487
|
+
style: {
|
|
3488
|
+
paddingLeft: 1,
|
|
3489
|
+
paddingRight: 1,
|
|
3490
|
+
flexShrink: 0,
|
|
3491
|
+
marginTop: 1,
|
|
3492
|
+
flexDirection: "column"
|
|
3493
|
+
},
|
|
3494
|
+
children: [attachments.map((a) => /* @__PURE__ */ jsxs("text", {
|
|
3495
|
+
fg: a.status === "error" ? theme.error : theme.muted,
|
|
3496
|
+
children: [
|
|
3497
|
+
"📎 ",
|
|
3498
|
+
a.fileName,
|
|
3499
|
+
" (",
|
|
3500
|
+
formatBytes$2(a.sizeBytes),
|
|
3501
|
+
") ",
|
|
3502
|
+
attachmentGlyph(a)
|
|
3503
|
+
]
|
|
3504
|
+
}, a.tempId)), /* @__PURE__ */ jsx("text", {
|
|
3505
|
+
fg: theme.dim,
|
|
3506
|
+
children: "ctrl+x remove last"
|
|
3507
|
+
})]
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3123
3511
|
//#endregion
|
|
3124
3512
|
//#region src/chat/tui/list-line.ts
|
|
3125
3513
|
/**
|
|
@@ -3474,6 +3862,17 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3474
3862
|
const [archiveOverrides, setArchiveOverrides] = useState(/* @__PURE__ */ new Map());
|
|
3475
3863
|
const [confirmId, setConfirmId] = useState(null);
|
|
3476
3864
|
const [error, setError] = useState(null);
|
|
3865
|
+
const [sending, setSending] = useState(false);
|
|
3866
|
+
const { staged: attachments, stageClipboardImage, dropLast: dropLastAttachment, collectForSend } = useComposerAttachments({
|
|
3867
|
+
rest,
|
|
3868
|
+
agentId: agent.id,
|
|
3869
|
+
pasteDisabled: confirmId !== null || isSearchInput || sending,
|
|
3870
|
+
onPasteText: (text) => {
|
|
3871
|
+
composerRef.current?.insertText(text);
|
|
3872
|
+
handleComposerChange();
|
|
3873
|
+
},
|
|
3874
|
+
onError: setError
|
|
3875
|
+
});
|
|
3477
3876
|
const isArchived = (c) => archiveOverrides.get(c.id) ?? c.viewerArchivedAt != null;
|
|
3478
3877
|
const list = ((isSearching && search?.query === debouncedQuery && search.state.kind === "ready" ? search.state.conversations : null) ?? conversations).filter((c) => !removedIds.has(c.id) && (showArchived || !isArchived(c)));
|
|
3479
3878
|
useEffect(() => {
|
|
@@ -3497,10 +3896,14 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3497
3896
|
const searchState = isSearching ? search?.state : void 0;
|
|
3498
3897
|
const searchStatusSegment = !isSearching ? "" : searchState?.kind === "loading" || search === null ? "searching…" : searchState?.kind === "error" ? `search failed: ${searchState.message}` : list.length === 0 ? "no matches" : "";
|
|
3499
3898
|
const trimmedMessage = isSearchInput ? "" : input.trim();
|
|
3500
|
-
const
|
|
3899
|
+
const hasMessage = trimmedMessage.length > 0 || attachments.length > 0;
|
|
3900
|
+
const enterHint = isSearchInput ? "filtering" : hasMessage ? "↵ send" : "↵ open";
|
|
3501
3901
|
const status = confirmConv ? {
|
|
3502
3902
|
fg: theme.error,
|
|
3503
3903
|
text: truncate(`delete “${confirmConv.title ?? "(untitled)"}”? y/n`, innerWidth)
|
|
3904
|
+
} : sending ? {
|
|
3905
|
+
fg: theme.muted,
|
|
3906
|
+
text: "uploading attachments…"
|
|
3504
3907
|
} : error ? {
|
|
3505
3908
|
fg: theme.error,
|
|
3506
3909
|
text: truncate(error, innerWidth)
|
|
@@ -3519,7 +3922,8 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3519
3922
|
].filter((segment) => segment !== ""), innerWidth)
|
|
3520
3923
|
};
|
|
3521
3924
|
const identityRows = height >= 24 ? 3 : 0;
|
|
3522
|
-
const
|
|
3925
|
+
const trayRows = attachmentTrayRows(attachments.length);
|
|
3926
|
+
const fixedRows = 6 + brandHeaderRows(height, width) + identityRows + trayRows;
|
|
3523
3927
|
const composerRows = Math.max(1, Math.min(composerLines, height - fixedRows - 2 - MIN_LIST_ROWS));
|
|
3524
3928
|
const visibleRows = Math.max(MIN_LIST_ROWS, height - fixedRows - (composerRows + 2));
|
|
3525
3929
|
const start = windowStart(clamped, rows.length, visibleRows);
|
|
@@ -3534,8 +3938,22 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3534
3938
|
conversation: row.hit.item
|
|
3535
3939
|
});
|
|
3536
3940
|
};
|
|
3537
|
-
const sendNewConversation = (message) => {
|
|
3538
|
-
|
|
3941
|
+
const sendNewConversation = async (message) => {
|
|
3942
|
+
setSending(true);
|
|
3943
|
+
const { files, failed } = await collectForSend(attachments);
|
|
3944
|
+
setSending(false);
|
|
3945
|
+
if (!message && files.length === 0) {
|
|
3946
|
+
setError("attachment upload failed");
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (failed.length > 0) {
|
|
3950
|
+
const plural = failed.length === 1 ? "" : "s";
|
|
3951
|
+
setError(`${failed.length} attachment${plural} failed to upload; sending without ${plural ? "them" : "it"}`);
|
|
3952
|
+
}
|
|
3953
|
+
useStore.setState({
|
|
3954
|
+
seedPrompt: message,
|
|
3955
|
+
seedAttachments: files
|
|
3956
|
+
});
|
|
3539
3957
|
onPick({
|
|
3540
3958
|
kind: "chat",
|
|
3541
3959
|
agent,
|
|
@@ -3559,7 +3977,8 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3559
3977
|
setHighlight(0);
|
|
3560
3978
|
};
|
|
3561
3979
|
const submit = () => {
|
|
3562
|
-
if (
|
|
3980
|
+
if (sending) return;
|
|
3981
|
+
if (hasMessage) sendNewConversation(trimmedMessage);
|
|
3563
3982
|
else openHighlighted();
|
|
3564
3983
|
};
|
|
3565
3984
|
/**
|
|
@@ -3653,6 +4072,15 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3653
4072
|
toggleArchived(row.hit.item);
|
|
3654
4073
|
}
|
|
3655
4074
|
key.preventDefault();
|
|
4075
|
+
} else if (key.name === "v" && key.ctrl) {
|
|
4076
|
+
if (confirmId === null && !isSearchInput) {
|
|
4077
|
+
setError(null);
|
|
4078
|
+
stageClipboardImage();
|
|
4079
|
+
}
|
|
4080
|
+
key.preventDefault();
|
|
4081
|
+
} else if (key.name === "x" && key.ctrl) {
|
|
4082
|
+
dropLastAttachment();
|
|
4083
|
+
key.preventDefault();
|
|
3656
4084
|
} else if (key.name === "tab" && key.shift) {
|
|
3657
4085
|
onToggleArchived();
|
|
3658
4086
|
key.preventDefault();
|
|
@@ -3736,6 +4164,7 @@ function ConversationHome({ agent, initialQuery, conversations, showAutomated, o
|
|
|
3736
4164
|
}
|
|
3737
4165
|
})
|
|
3738
4166
|
}),
|
|
4167
|
+
attachments.length > 0 ? /* @__PURE__ */ jsx(AttachmentTray, { attachments }) : null,
|
|
3739
4168
|
/* @__PURE__ */ jsx("text", {
|
|
3740
4169
|
fg: status.fg,
|
|
3741
4170
|
children: status.text
|
|
@@ -3967,166 +4396,12 @@ function formatTranscript(messages, format) {
|
|
|
3967
4396
|
if (format === "markdown") return `# Conversation transcript\n\n${entries.join("\n\n")}\n`;
|
|
3968
4397
|
return `${entries.join("\n\n")}\n`;
|
|
3969
4398
|
}
|
|
3970
|
-
async function exportTranscript(options) {
|
|
3971
|
-
const extension = options.format === "markdown" ? "md" : "txt";
|
|
3972
|
-
const filePath = path.join(options.cwd, `skydive-conversation-${options.conversationId}.${extension}`);
|
|
3973
|
-
await writeFile(filePath, formatTranscript(options.messages, options.format), "utf8");
|
|
3974
|
-
return filePath;
|
|
3975
|
-
}
|
|
3976
|
-
|
|
3977
|
-
//#endregion
|
|
3978
|
-
//#region src/chat/paste.ts
|
|
3979
|
-
const MAX_PATH_PASTE_CHARS = 4096;
|
|
3980
|
-
/**
|
|
3981
|
-
* Decode a `file://` URL to a filesystem path, host-independently. Node's
|
|
3982
|
-
* `fileURLToPath` maps to the *running* OS's path shape, so a POSIX file URL
|
|
3983
|
-
* pasted on Windows would come back as a backslash drive-relative path. A
|
|
3984
|
-
* dropped-file URL is decided by its own shape, not by our host: a Windows
|
|
3985
|
-
* drive URL (`file:///C:/...`) yields a Windows path, anything else a POSIX
|
|
3986
|
-
* path. Returns null for a malformed URL.
|
|
3987
|
-
*/
|
|
3988
|
-
function fileUrlToPath(url) {
|
|
3989
|
-
let parsed;
|
|
3990
|
-
try {
|
|
3991
|
-
parsed = new URL(url);
|
|
3992
|
-
} catch (_error) {
|
|
3993
|
-
return null;
|
|
3994
|
-
}
|
|
3995
|
-
if (parsed.protocol !== "file:") return null;
|
|
3996
|
-
const decoded = decodeURIComponent(parsed.pathname);
|
|
3997
|
-
if (/^\/[a-zA-Z]:\//.test(decoded)) return decoded.slice(1).replace(/\//g, "\\");
|
|
3998
|
-
return decoded;
|
|
3999
|
-
}
|
|
4000
|
-
/**
|
|
4001
|
-
* Parse the text a terminal emits for dropped files. This only identifies
|
|
4002
|
-
* path-shaped candidates; filesystem and content validation happen later.
|
|
4003
|
-
*/
|
|
4004
|
-
function parseDroppedPaths(text) {
|
|
4005
|
-
const trimmed = text.trim();
|
|
4006
|
-
if (!trimmed || trimmed.length > MAX_PATH_PASTE_CHARS || /[\r\n]/.test(trimmed)) return null;
|
|
4007
|
-
const tokens = [];
|
|
4008
|
-
let current = "";
|
|
4009
|
-
let quote = null;
|
|
4010
|
-
for (let i = 0; i < trimmed.length; i += 1) {
|
|
4011
|
-
const ch = trimmed.charAt(i);
|
|
4012
|
-
if (quote) {
|
|
4013
|
-
if (ch === quote) quote = null;
|
|
4014
|
-
else if (ch === "\\" && quote === "\"" && i + 1 < trimmed.length) {
|
|
4015
|
-
current += trimmed.charAt(i + 1);
|
|
4016
|
-
i += 1;
|
|
4017
|
-
} else current += ch;
|
|
4018
|
-
continue;
|
|
4019
|
-
}
|
|
4020
|
-
if (ch === "'" || ch === "\"") {
|
|
4021
|
-
quote = ch;
|
|
4022
|
-
continue;
|
|
4023
|
-
}
|
|
4024
|
-
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
4025
|
-
current += trimmed.charAt(i + 1);
|
|
4026
|
-
i += 1;
|
|
4027
|
-
continue;
|
|
4028
|
-
}
|
|
4029
|
-
if (ch === " " || ch === " ") {
|
|
4030
|
-
if (current) {
|
|
4031
|
-
tokens.push(current);
|
|
4032
|
-
current = "";
|
|
4033
|
-
}
|
|
4034
|
-
continue;
|
|
4035
|
-
}
|
|
4036
|
-
current += ch;
|
|
4037
|
-
}
|
|
4038
|
-
if (quote) return null;
|
|
4039
|
-
if (current) tokens.push(current);
|
|
4040
|
-
if (tokens.length === 0) return null;
|
|
4041
|
-
const paths = [];
|
|
4042
|
-
for (const token of tokens) {
|
|
4043
|
-
let path = token;
|
|
4044
|
-
if (path.startsWith("file://")) {
|
|
4045
|
-
const resolved = fileUrlToPath(path);
|
|
4046
|
-
if (resolved === null) return null;
|
|
4047
|
-
path = resolved;
|
|
4048
|
-
}
|
|
4049
|
-
if (path.startsWith("~/")) path = `${homedir()}${path.slice(1)}`;
|
|
4050
|
-
if (!isAbsolute(path) && !win32.isAbsolute(path) && !path.startsWith("./") && !path.startsWith("../")) return null;
|
|
4051
|
-
paths.push(path);
|
|
4052
|
-
}
|
|
4053
|
-
return paths;
|
|
4054
|
-
}
|
|
4055
|
-
const TEXT_MIME_BY_EXT = {
|
|
4056
|
-
".txt": "text/plain",
|
|
4057
|
-
".md": "text/markdown",
|
|
4058
|
-
".markdown": "text/markdown",
|
|
4059
|
-
".csv": "text/csv",
|
|
4060
|
-
".tsv": "text/tab-separated-values",
|
|
4061
|
-
".json": "application/json",
|
|
4062
|
-
".yaml": "application/yaml",
|
|
4063
|
-
".yml": "application/yaml",
|
|
4064
|
-
".xml": "application/xml",
|
|
4065
|
-
".html": "text/html",
|
|
4066
|
-
".htm": "text/html",
|
|
4067
|
-
".css": "text/css",
|
|
4068
|
-
".js": "text/javascript",
|
|
4069
|
-
".ts": "text/plain",
|
|
4070
|
-
".tsx": "text/plain",
|
|
4071
|
-
".jsx": "text/plain",
|
|
4072
|
-
".py": "text/x-python",
|
|
4073
|
-
".sh": "text/x-shellscript",
|
|
4074
|
-
".log": "text/plain",
|
|
4075
|
-
".svg": "image/svg+xml"
|
|
4076
|
-
};
|
|
4077
|
-
/** Decide how to handle a paste. `kind: 'binary'` events carry the mime of
|
|
4078
|
-
* bytes the terminal forwarded; text events carry the decoded paste text.
|
|
4079
|
-
*
|
|
4080
|
-
* The empty-text case is the fix for "can't paste an image": on macOS the
|
|
4081
|
-
* reflex is Cmd+V, which the terminal turns into a bracketed *text* paste, so
|
|
4082
|
-
* a screenshot on the clipboard arrives as empty bytes. We route that to an OS
|
|
4083
|
-
* clipboard read instead of dropping it, matching the explicit ctrl+v path. */
|
|
4084
|
-
function routePaste(input) {
|
|
4085
|
-
if (input.kind === "binary") {
|
|
4086
|
-
if (input.mimeType?.startsWith("image/")) return {
|
|
4087
|
-
kind: "binary-image",
|
|
4088
|
-
mediaType: input.mimeType
|
|
4089
|
-
};
|
|
4090
|
-
return { kind: "text" };
|
|
4091
|
-
}
|
|
4092
|
-
const paths = parseDroppedPaths(input.text);
|
|
4093
|
-
if (paths) return {
|
|
4094
|
-
kind: "dropped-paths",
|
|
4095
|
-
paths,
|
|
4096
|
-
text: input.text
|
|
4097
|
-
};
|
|
4098
|
-
if (input.text.trim() === "") return { kind: "clipboard-image" };
|
|
4099
|
-
return { kind: "text" };
|
|
4100
|
-
}
|
|
4101
|
-
/**
|
|
4102
|
-
* Resolve path candidates into attachable files. Every path must be an
|
|
4103
|
-
* existing regular file; otherwise the caller should restore the original
|
|
4104
|
-
* paste as text (an all-or-nothing rule so a paste meant as text — which
|
|
4105
|
-
* merely *looks* path-shaped — is never partially eaten).
|
|
4106
|
-
*
|
|
4107
|
-
* Any file type attaches. The mediaType comes from magic bytes when
|
|
4108
|
-
* detectable, an extension map for the plain-text formats magic can't see,
|
|
4109
|
-
* and application/octet-stream as the last resort — the attachments API
|
|
4110
|
-
* accepts any mediaType (100MB cap enforced server-side at presign).
|
|
4111
|
-
*/
|
|
4112
|
-
async function resolveDroppedFiles(paths) {
|
|
4113
|
-
try {
|
|
4114
|
-
const files = await Promise.all(paths.map(async (path) => {
|
|
4115
|
-
if (!(await stat(path)).isFile()) return null;
|
|
4116
|
-
const data = new Uint8Array(await readFile(path));
|
|
4117
|
-
const mediaType = (await fileTypeFromBuffer(data))?.mime ?? TEXT_MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
4118
|
-
return {
|
|
4119
|
-
fileName: basename(path),
|
|
4120
|
-
mediaType,
|
|
4121
|
-
data
|
|
4122
|
-
};
|
|
4123
|
-
}));
|
|
4124
|
-
if (!files.every((file) => file !== null)) return null;
|
|
4125
|
-
return files;
|
|
4126
|
-
} catch (_error) {
|
|
4127
|
-
return null;
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4399
|
+
async function exportTranscript(options) {
|
|
4400
|
+
const extension = options.format === "markdown" ? "md" : "txt";
|
|
4401
|
+
const filePath = path.join(options.cwd, `skydive-conversation-${options.conversationId}.${extension}`);
|
|
4402
|
+
await writeFile(filePath, formatTranscript(options.messages, options.format), "utf8");
|
|
4403
|
+
return filePath;
|
|
4404
|
+
}
|
|
4130
4405
|
|
|
4131
4406
|
//#endregion
|
|
4132
4407
|
//#region src/chat/save-file.ts
|
|
@@ -5994,9 +6269,9 @@ function stripLeadingMarker(s) {
|
|
|
5994
6269
|
}
|
|
5995
6270
|
const ELISION_MARKER_TAG = "⋯ elided";
|
|
5996
6271
|
function elisionMarker(bytes) {
|
|
5997
|
-
return `${ELISION_MARKER_TAG} ${formatBytes$
|
|
6272
|
+
return `${ELISION_MARKER_TAG} ${formatBytes$1(bytes)} to save memory ⋯`;
|
|
5998
6273
|
}
|
|
5999
|
-
function formatBytes$
|
|
6274
|
+
function formatBytes$1(n) {
|
|
6000
6275
|
if (n < 1024) return `${n} chars`;
|
|
6001
6276
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
6002
6277
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -7059,9 +7334,14 @@ function todosCanExpand(todos, cap) {
|
|
|
7059
7334
|
* reserve exactly this much and keep the composer pinned. Header + optional
|
|
7060
7335
|
* done-summary line + rendered rows + optional "+N more" tail + top margin.
|
|
7061
7336
|
* Must track `windowTodos` and `TodoCardView`'s layout.
|
|
7337
|
+
*
|
|
7338
|
+
* `minimized` is the card folded down to its header row (label, progress
|
|
7339
|
+
* counter and the chevron that brings the rows back) plus the top margin, and
|
|
7340
|
+
* nothing else, however long the list is.
|
|
7062
7341
|
*/
|
|
7063
|
-
function todoCardHeight(todos, { expanded }) {
|
|
7342
|
+
function todoCardHeight(todos, { expanded, minimized }) {
|
|
7064
7343
|
if (todos.length === 0) return 0;
|
|
7344
|
+
if (minimized) return 2;
|
|
7065
7345
|
const w = windowTodos(todos, {
|
|
7066
7346
|
cap: MAX_VISIBLE_ROWS$1,
|
|
7067
7347
|
expanded
|
|
@@ -7102,15 +7382,24 @@ function rowColor$1(status) {
|
|
|
7102
7382
|
}
|
|
7103
7383
|
}
|
|
7104
7384
|
/**
|
|
7105
|
-
* The plan card's checklist content. The caller owns outer spacing and
|
|
7106
|
-
*
|
|
7107
|
-
*
|
|
7385
|
+
* The plan card's checklist content. The caller owns outer spacing and both
|
|
7386
|
+
* view states — the chat screen has to reserve the card's rows, so it can't
|
|
7387
|
+
* discover the height from a toggle held down here.
|
|
7388
|
+
*
|
|
7389
|
+
* The card has two independent click targets, one per axis of "how much do I
|
|
7390
|
+
* show":
|
|
7391
|
+
* - the **header** minimizes the card to that one row and back
|
|
7392
|
+
* (`minimized` / `onToggleMinimized`), which is what keeps a long plan from
|
|
7393
|
+
* eating the screen above the composer. The chevron carries the affordance.
|
|
7394
|
+
* - the **body** unfolds the completed work the window hides
|
|
7395
|
+
* (`expanded` / `onToggleExpanded`), the same interaction reasoning and
|
|
7396
|
+
* tool output already use. Only clickable when something is actually folded.
|
|
7108
7397
|
*
|
|
7109
|
-
*
|
|
7110
|
-
*
|
|
7111
|
-
*
|
|
7398
|
+
* Each surface names the other in its hint, since a chevron alone can't say
|
|
7399
|
+
* which click does which: folded, the `✔ N done` row offers the unfold;
|
|
7400
|
+
* unfolded, the header says the rows are what folds back.
|
|
7112
7401
|
*/
|
|
7113
|
-
function TaskCardContent({ label, todos, isActive, expanded, onToggleExpanded }) {
|
|
7402
|
+
function TaskCardContent({ label, todos, isActive, minimized, expanded, onToggleMinimized, onToggleExpanded }) {
|
|
7114
7403
|
const completed = todos.filter((todo) => todo.status === "completed").length;
|
|
7115
7404
|
const canExpand = todosCanExpand(todos, MAX_VISIBLE_ROWS$1);
|
|
7116
7405
|
const { doneSummary, rows, moreCount } = windowTodos(todos, {
|
|
@@ -7119,66 +7408,76 @@ function TaskCardContent({ label, todos, isActive, expanded, onToggleExpanded })
|
|
|
7119
7408
|
});
|
|
7120
7409
|
return /* @__PURE__ */ jsxs("box", {
|
|
7121
7410
|
style: { flexDirection: "column" },
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
/* @__PURE__ */ jsx("text", {
|
|
7128
|
-
fg: isActive ? theme.accent : theme.muted,
|
|
7129
|
-
children: /* @__PURE__ */ jsx("b", { children: label })
|
|
7130
|
-
}),
|
|
7131
|
-
/* @__PURE__ */ jsxs("text", {
|
|
7132
|
-
fg: theme.dim,
|
|
7133
|
-
children: [
|
|
7134
|
-
" ",
|
|
7135
|
-
completed,
|
|
7136
|
-
"/",
|
|
7137
|
-
todos.length
|
|
7138
|
-
]
|
|
7139
|
-
}),
|
|
7140
|
-
canExpand ? /* @__PURE__ */ jsxs("text", {
|
|
7141
|
-
fg: theme.dim,
|
|
7142
|
-
children: [
|
|
7143
|
-
" ",
|
|
7144
|
-
"· click to ",
|
|
7145
|
-
expanded ? "collapse" : "expand"
|
|
7146
|
-
]
|
|
7147
|
-
}) : null
|
|
7148
|
-
]
|
|
7149
|
-
}),
|
|
7150
|
-
doneSummary > 0 ? /* @__PURE__ */ jsxs("box", {
|
|
7151
|
-
style: { flexDirection: "row" },
|
|
7152
|
-
children: [/* @__PURE__ */ jsx("text", {
|
|
7153
|
-
fg: theme.success,
|
|
7154
|
-
children: "✔ "
|
|
7155
|
-
}), /* @__PURE__ */ jsxs("text", {
|
|
7411
|
+
children: [/* @__PURE__ */ jsxs("box", {
|
|
7412
|
+
style: { flexDirection: "row" },
|
|
7413
|
+
onMouseDown: onToggleMinimized,
|
|
7414
|
+
children: [
|
|
7415
|
+
/* @__PURE__ */ jsx("text", {
|
|
7156
7416
|
fg: theme.dim,
|
|
7157
|
-
children:
|
|
7158
|
-
})
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7417
|
+
children: minimized ? "▸ " : "▾ "
|
|
7418
|
+
}),
|
|
7419
|
+
/* @__PURE__ */ jsx("text", {
|
|
7420
|
+
fg: isActive ? theme.accent : theme.muted,
|
|
7421
|
+
children: /* @__PURE__ */ jsx("b", { children: label })
|
|
7422
|
+
}),
|
|
7423
|
+
/* @__PURE__ */ jsxs("text", {
|
|
7424
|
+
fg: theme.dim,
|
|
7425
|
+
children: [
|
|
7426
|
+
" ",
|
|
7427
|
+
completed,
|
|
7428
|
+
"/",
|
|
7429
|
+
todos.length
|
|
7430
|
+
]
|
|
7431
|
+
}),
|
|
7432
|
+
!minimized && expanded && canExpand ? /* @__PURE__ */ jsxs("text", {
|
|
7433
|
+
fg: theme.dim,
|
|
7434
|
+
children: [" ", "· click a row to fold"]
|
|
7435
|
+
}) : null
|
|
7436
|
+
]
|
|
7437
|
+
}), minimized ? null : /* @__PURE__ */ jsxs("box", {
|
|
7438
|
+
style: { flexDirection: "column" },
|
|
7439
|
+
onMouseDown: canExpand ? onToggleExpanded : void 0,
|
|
7440
|
+
children: [
|
|
7441
|
+
doneSummary > 0 ? /* @__PURE__ */ jsxs("box", {
|
|
7163
7442
|
style: { flexDirection: "row" },
|
|
7164
|
-
children: [
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7443
|
+
children: [
|
|
7444
|
+
/* @__PURE__ */ jsx("text", {
|
|
7445
|
+
fg: theme.success,
|
|
7446
|
+
children: "✔ "
|
|
7447
|
+
}),
|
|
7448
|
+
/* @__PURE__ */ jsxs("text", {
|
|
7449
|
+
fg: theme.dim,
|
|
7450
|
+
children: [doneSummary, " done"]
|
|
7451
|
+
}),
|
|
7452
|
+
/* @__PURE__ */ jsxs("text", {
|
|
7453
|
+
fg: theme.dim,
|
|
7454
|
+
children: [" ", "· click to show"]
|
|
7455
|
+
})
|
|
7456
|
+
]
|
|
7457
|
+
}) : null,
|
|
7458
|
+
rows.map((todo) => {
|
|
7459
|
+
const g = glyph(todo.status);
|
|
7460
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
7461
|
+
style: { flexDirection: "row" },
|
|
7462
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
7463
|
+
fg: g.fg,
|
|
7464
|
+
children: [g.char, " "]
|
|
7465
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
7466
|
+
fg: rowColor$1(todo.status),
|
|
7467
|
+
children: todo.content
|
|
7468
|
+
})]
|
|
7469
|
+
}, todo.id);
|
|
7470
|
+
}),
|
|
7471
|
+
moreCount > 0 ? /* @__PURE__ */ jsxs("text", {
|
|
7472
|
+
fg: theme.dim,
|
|
7473
|
+
children: [
|
|
7474
|
+
" … +",
|
|
7475
|
+
moreCount,
|
|
7476
|
+
" more"
|
|
7477
|
+
]
|
|
7478
|
+
}) : null
|
|
7479
|
+
]
|
|
7480
|
+
})]
|
|
7182
7481
|
});
|
|
7183
7482
|
}
|
|
7184
7483
|
|
|
@@ -7193,9 +7492,12 @@ function TaskCardContent({ label, todos, isActive, expanded, onToggleExpanded })
|
|
|
7193
7492
|
* The card can't scroll like the web card, so it windows completed and pending
|
|
7194
7493
|
* work. `isActive` dims the header while the run is idle. `expanded` unfolds
|
|
7195
7494
|
* the completed work the window hides — a settled plan collapses to `✔ N done`
|
|
7196
|
-
* otherwise, and clicking the
|
|
7495
|
+
* otherwise, and clicking the body (or `/plan expand`) brings the rows back.
|
|
7496
|
+
* `minimized` is the card folded down to its header row, so a long plan can be
|
|
7497
|
+
* kept out of the way (click the header, or `/plan minimize`) without losing
|
|
7498
|
+
* the label and progress counter the way `/plan hide` does.
|
|
7197
7499
|
*/
|
|
7198
|
-
function TodoCardView({ todos, isActive, expanded, onToggleExpanded }) {
|
|
7500
|
+
function TodoCardView({ todos, isActive, minimized, expanded, onToggleMinimized, onToggleExpanded }) {
|
|
7199
7501
|
if (todos.length === 0) return null;
|
|
7200
7502
|
return /* @__PURE__ */ jsx("box", {
|
|
7201
7503
|
style: {
|
|
@@ -7209,7 +7511,9 @@ function TodoCardView({ todos, isActive, expanded, onToggleExpanded }) {
|
|
|
7209
7511
|
label: "Plan",
|
|
7210
7512
|
todos,
|
|
7211
7513
|
isActive,
|
|
7514
|
+
minimized,
|
|
7212
7515
|
expanded,
|
|
7516
|
+
onToggleMinimized,
|
|
7213
7517
|
onToggleExpanded
|
|
7214
7518
|
})
|
|
7215
7519
|
});
|
|
@@ -7552,8 +7856,14 @@ function SubagentRowView({ row, selected, width, nowMs, onOpen }) {
|
|
|
7552
7856
|
* ↵ to view (the chat screen owns those keys and the esc-back bookkeeping;
|
|
7553
7857
|
* the card reports its rows up through ConversationContext so the screen
|
|
7554
7858
|
* knows what's selectable).
|
|
7859
|
+
*
|
|
7860
|
+
* `minimized` folds the card down to its header row: a wide fan-out is 10 rows
|
|
7861
|
+
* of the terminal for as long as the conversation lives, so clicking the
|
|
7862
|
+
* header (or `/subagent collapse`) folds the rows away and leaves the label,
|
|
7863
|
+
* the live glyph and the N/M counter. The chat screen owns the flag because it
|
|
7864
|
+
* reserves the card's rows to keep the composer pinned.
|
|
7555
7865
|
*/
|
|
7556
|
-
function ToolSubagent({ items, isActive: _isActive, onHeightChange }) {
|
|
7866
|
+
function ToolSubagent({ items, isActive: _isActive, minimized, onToggleMinimized, onHeightChange }) {
|
|
7557
7867
|
const { rest, conversationId, openConversation, onSubagentBatch, subagentSelection } = useConversationContext();
|
|
7558
7868
|
const { width } = useTerminalDimensions();
|
|
7559
7869
|
const optimistic = /* @__PURE__ */ new Map();
|
|
@@ -7596,8 +7906,9 @@ function ToolSubagent({ items, isActive: _isActive, onHeightChange }) {
|
|
|
7596
7906
|
}, [live, rows.length]);
|
|
7597
7907
|
useEffect(() => {
|
|
7598
7908
|
const visibleRows = Math.min(rows.length, MAX_VISIBLE_ROWS);
|
|
7599
|
-
onHeightChange?.(rows.length === 0 ? items.length > 0 ? 1 : 0 : 1 + visibleRows + (rows.length > MAX_VISIBLE_ROWS ? 1 : 0));
|
|
7909
|
+
onHeightChange?.(rows.length === 0 ? items.length > 0 ? 1 : 0 : minimized ? 1 : 1 + visibleRows + (rows.length > MAX_VISIBLE_ROWS ? 1 : 0));
|
|
7600
7910
|
}, [
|
|
7911
|
+
minimized,
|
|
7601
7912
|
items.length,
|
|
7602
7913
|
onHeightChange,
|
|
7603
7914
|
rows.length
|
|
@@ -7656,32 +7967,40 @@ function ToolSubagent({ items, isActive: _isActive, onHeightChange }) {
|
|
|
7656
7967
|
const moreBelow = rows.length - start - visible.length;
|
|
7657
7968
|
return /* @__PURE__ */ jsxs("box", {
|
|
7658
7969
|
style: { flexDirection: "column" },
|
|
7659
|
-
children: [/* @__PURE__ */
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
"
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7970
|
+
children: [/* @__PURE__ */ jsx("box", {
|
|
7971
|
+
style: { flexDirection: "row" },
|
|
7972
|
+
onMouseDown: onToggleMinimized,
|
|
7973
|
+
children: /* @__PURE__ */ jsxs("text", { children: [
|
|
7974
|
+
/* @__PURE__ */ jsx("span", {
|
|
7975
|
+
fg: theme.dim,
|
|
7976
|
+
children: minimized ? "▸ " : "▾ "
|
|
7977
|
+
}),
|
|
7978
|
+
live ? /* @__PURE__ */ jsx(Spinner, { color: theme.accent }) : rows.every((row) => row.status === "completed") ? /* @__PURE__ */ jsx("span", {
|
|
7979
|
+
fg: theme.success,
|
|
7980
|
+
children: "✔"
|
|
7981
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
7982
|
+
fg: theme.error,
|
|
7983
|
+
children: "✗"
|
|
7984
|
+
}),
|
|
7985
|
+
/* @__PURE__ */ jsxs("span", {
|
|
7986
|
+
fg: theme.tool,
|
|
7987
|
+
children: [" ", /* @__PURE__ */ jsx("b", { children: headerLabel(rows) })]
|
|
7988
|
+
}),
|
|
7989
|
+
/* @__PURE__ */ jsxs("span", {
|
|
7990
|
+
fg: theme.dim,
|
|
7991
|
+
children: [
|
|
7992
|
+
" ",
|
|
7993
|
+
rows.filter((row) => row.status === "completed").length,
|
|
7994
|
+
"/",
|
|
7995
|
+
rows.length
|
|
7996
|
+
]
|
|
7997
|
+
}),
|
|
7998
|
+
openConversation && !minimized ? /* @__PURE__ */ jsxs("span", {
|
|
7999
|
+
fg: theme.dim,
|
|
8000
|
+
children: [" ", selectedIndex !== null ? "↑/↓ select · ↵ view · esc back" : "/subagent to browse"]
|
|
8001
|
+
}) : null
|
|
8002
|
+
] })
|
|
8003
|
+
}), minimized ? null : /* @__PURE__ */ jsxs("box", {
|
|
7685
8004
|
style: { flexDirection: "column" },
|
|
7686
8005
|
children: [visible.map((row, i) => {
|
|
7687
8006
|
const target = row.conversationId;
|
|
@@ -7707,8 +8026,12 @@ function ToolSubagent({ items, isActive: _isActive, onHeightChange }) {
|
|
|
7707
8026
|
* Delegated work pinned above the plan. Each batch owns its status polling;
|
|
7708
8027
|
* a settled batch keeps its rows — titles and drill-in — until the next
|
|
7709
8028
|
* fan-out replaces the group.
|
|
8029
|
+
*
|
|
8030
|
+
* `minimized` folds the rows away and leaves the header (click it, or
|
|
8031
|
+
* `/subagent minimize`), so a ten-task fan-out stops owning ten rows above the
|
|
8032
|
+
* composer for the rest of the conversation.
|
|
7710
8033
|
*/
|
|
7711
|
-
function SubagentCardView({ items, isActive, onHeightChange }) {
|
|
8034
|
+
function SubagentCardView({ items, isActive, minimized, onToggleMinimized, onHeightChange }) {
|
|
7712
8035
|
const [height, setHeight] = useState(0);
|
|
7713
8036
|
useEffect(() => onHeightChange?.(height > 0 ? height + 1 : 0), [height, onHeightChange]);
|
|
7714
8037
|
return /* @__PURE__ */ jsx("box", {
|
|
@@ -7723,6 +8046,8 @@ function SubagentCardView({ items, isActive, onHeightChange }) {
|
|
|
7723
8046
|
children: /* @__PURE__ */ jsx(ToolSubagent, {
|
|
7724
8047
|
items,
|
|
7725
8048
|
isActive,
|
|
8049
|
+
minimized,
|
|
8050
|
+
onToggleMinimized,
|
|
7726
8051
|
onHeightChange: setHeight
|
|
7727
8052
|
})
|
|
7728
8053
|
});
|
|
@@ -8751,12 +9076,20 @@ const keybindGroups = [
|
|
|
8751
9076
|
action: "jump back to your last message"
|
|
8752
9077
|
},
|
|
8753
9078
|
{
|
|
8754
|
-
keys: "click",
|
|
9079
|
+
keys: "click card header",
|
|
9080
|
+
action: "minimize the pinned plan or subagents card, and restore it"
|
|
9081
|
+
},
|
|
9082
|
+
{
|
|
9083
|
+
keys: "click card body",
|
|
8755
9084
|
action: "expand a folded plan card back to its finished rows"
|
|
8756
9085
|
},
|
|
8757
9086
|
{
|
|
8758
|
-
keys: "/plan [show|hide|expand|collapse]",
|
|
8759
|
-
action: "show / hide /
|
|
9087
|
+
keys: "/plan [show|hide|minimize|expand|collapse]",
|
|
9088
|
+
action: "show / hide / minimize the pinned plan card, or unfold it"
|
|
9089
|
+
},
|
|
9090
|
+
{
|
|
9091
|
+
keys: "/subagent [minimize]",
|
|
9092
|
+
action: "browse the pinned subagents card, or minimize it"
|
|
8760
9093
|
},
|
|
8761
9094
|
{
|
|
8762
9095
|
keys: "/rename <title>",
|
|
@@ -8910,6 +9243,18 @@ const keybindGroups = [
|
|
|
8910
9243
|
keys: "↵",
|
|
8911
9244
|
action: "open conversation"
|
|
8912
9245
|
},
|
|
9246
|
+
{
|
|
9247
|
+
keys: "ctrl+v",
|
|
9248
|
+
action: "paste image from clipboard"
|
|
9249
|
+
},
|
|
9250
|
+
{
|
|
9251
|
+
keys: "drag-drop",
|
|
9252
|
+
action: "attach image files dropped onto the terminal"
|
|
9253
|
+
},
|
|
9254
|
+
{
|
|
9255
|
+
keys: "ctrl+x",
|
|
9256
|
+
action: "drop the last staged image"
|
|
9257
|
+
},
|
|
8913
9258
|
{
|
|
8914
9259
|
keys: "ctrl+a",
|
|
8915
9260
|
action: "archive conversation (hides it for you only; again to restore)"
|
|
@@ -9461,8 +9806,8 @@ const slashCommands = [
|
|
|
9461
9806
|
{
|
|
9462
9807
|
name: "subagent",
|
|
9463
9808
|
aliases: ["subagents"],
|
|
9464
|
-
argsHint:
|
|
9465
|
-
description: "
|
|
9809
|
+
argsHint: "[minimize]",
|
|
9810
|
+
description: "browse or minimize the fan-out card",
|
|
9466
9811
|
action: { kind: "subagent" }
|
|
9467
9812
|
},
|
|
9468
9813
|
{
|
|
@@ -9482,8 +9827,8 @@ const slashCommands = [
|
|
|
9482
9827
|
{
|
|
9483
9828
|
name: "plan",
|
|
9484
9829
|
aliases: ["todos", "todo"],
|
|
9485
|
-
argsHint: "[show|hide|expand|collapse]",
|
|
9486
|
-
description: "show, hide, or
|
|
9830
|
+
argsHint: "[show|hide|minimize|expand|collapse]",
|
|
9831
|
+
description: "show, hide, minimize, or unfold the pinned plan card",
|
|
9487
9832
|
action: {
|
|
9488
9833
|
kind: "plan",
|
|
9489
9834
|
mode: "toggle"
|
|
@@ -11794,7 +12139,7 @@ function canPreview(file) {
|
|
|
11794
12139
|
if (/\/(json|xml|javascript|typescript|x-sh|yaml)$/.test(file.mediaType)) return true;
|
|
11795
12140
|
return /\.(md|mdx|txt|json|jsonl|ya?ml|toml|ini|env|csv|ts|tsx|js|jsx|mjs|cjs|css|scss|html|xml|svg|py|rb|rs|go|java|kt|swift|c|cc|cpp|h|hpp|sh|zsh|fish|sql|graphql|gql)$/i.test(file.path);
|
|
11796
12141
|
}
|
|
11797
|
-
function formatBytes
|
|
12142
|
+
function formatBytes(bytes) {
|
|
11798
12143
|
if (bytes < 1024) return `${bytes} B`;
|
|
11799
12144
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
11800
12145
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -11862,7 +12207,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
11862
12207
|
]);
|
|
11863
12208
|
const visibleFiles = useMemo(() => load.kind === "ready" ? filterReviewFiles(load.files, fileSearch ?? "", (file) => file.path) : [], [load, fileSearch]);
|
|
11864
12209
|
const rows = useMemo(() => buildWorkspaceTree(visibleFiles), [visibleFiles]);
|
|
11865
|
-
const navigatorPreferredWidth = reviewTreePreferredWidth(useMemo(() => buildReviewTree(visibleFiles, (file) => file.path), [visibleFiles]), (file) => ` · ${formatBytes
|
|
12210
|
+
const navigatorPreferredWidth = reviewTreePreferredWidth(useMemo(() => buildReviewTree(visibleFiles, (file) => file.path), [visibleFiles]), (file) => ` · ${formatBytes(file.sizeBytes)}`.length);
|
|
11866
12211
|
const shellDimensions = reviewShellDimensions({
|
|
11867
12212
|
width,
|
|
11868
12213
|
height: shellHeight,
|
|
@@ -11901,7 +12246,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
11901
12246
|
if (!canPreview(selectedFile)) {
|
|
11902
12247
|
setPreview({
|
|
11903
12248
|
kind: "none",
|
|
11904
|
-
message: `${selectedFile.mediaType} · ${formatBytes
|
|
12249
|
+
message: `${selectedFile.mediaType} · ${formatBytes(selectedFile.sizeBytes)}\npreview unavailable for binary or large files`
|
|
11905
12250
|
});
|
|
11906
12251
|
return;
|
|
11907
12252
|
}
|
|
@@ -12140,7 +12485,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
12140
12485
|
row.name,
|
|
12141
12486
|
/* @__PURE__ */ jsxs("span", {
|
|
12142
12487
|
fg: theme.faint,
|
|
12143
|
-
children: [" · ", formatBytes
|
|
12488
|
+
children: [" · ", formatBytes(row.file.sizeBytes)]
|
|
12144
12489
|
})
|
|
12145
12490
|
]
|
|
12146
12491
|
}, row.file.id);
|
|
@@ -13322,9 +13667,13 @@ function routeInput(raw) {
|
|
|
13322
13667
|
const arg = match.rest.toLowerCase();
|
|
13323
13668
|
return {
|
|
13324
13669
|
kind: "plan",
|
|
13325
|
-
mode: arg === "show" ? "show" : arg === "hide" ? "hide" : arg === "expand" ? "expand" : arg === "collapse" ? "collapse" : "toggle"
|
|
13670
|
+
mode: arg === "show" ? "show" : arg === "hide" ? "hide" : arg === "expand" ? "expand" : arg === "collapse" ? "collapse" : arg === "minimize" ? "minimize" : "toggle"
|
|
13326
13671
|
};
|
|
13327
13672
|
}
|
|
13673
|
+
case "subagent": return {
|
|
13674
|
+
kind: "subagent",
|
|
13675
|
+
mode: match.rest.toLowerCase() === "minimize" ? "minimize" : "browse"
|
|
13676
|
+
};
|
|
13328
13677
|
case "new-conversation":
|
|
13329
13678
|
case "fork":
|
|
13330
13679
|
case "archive-conversation":
|
|
@@ -13332,7 +13681,6 @@ function routeInput(raw) {
|
|
|
13332
13681
|
case "effort-picker":
|
|
13333
13682
|
case "theme-picker":
|
|
13334
13683
|
case "pr-picker":
|
|
13335
|
-
case "subagent":
|
|
13336
13684
|
case "status":
|
|
13337
13685
|
case "share":
|
|
13338
13686
|
case "browser":
|
|
@@ -13446,7 +13794,6 @@ const pageScrollFraction = .75;
|
|
|
13446
13794
|
const HISTORY_PAGE_SIZE = 60;
|
|
13447
13795
|
/** Cap composer growth; past this the textarea scrolls its content instead. */
|
|
13448
13796
|
const maxComposerRows = 8;
|
|
13449
|
-
const maxAttachments = 10;
|
|
13450
13797
|
/**
|
|
13451
13798
|
* A server timestamp as local epoch ms, or null when it is missing or
|
|
13452
13799
|
* unparseable so the caller can fall back to its own clock.
|
|
@@ -13456,19 +13803,6 @@ function parseStampMs(iso) {
|
|
|
13456
13803
|
const ms = Date.parse(iso);
|
|
13457
13804
|
return Number.isNaN(ms) ? null : ms;
|
|
13458
13805
|
}
|
|
13459
|
-
function formatBytes(bytes) {
|
|
13460
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
13461
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
13462
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13463
|
-
}
|
|
13464
|
-
/** Status glyph for a staged attachment chip: uploading / ready / failed. */
|
|
13465
|
-
function attachmentGlyph(a) {
|
|
13466
|
-
switch (a.status) {
|
|
13467
|
-
case "uploading": return "↑";
|
|
13468
|
-
case "ready": return "✓";
|
|
13469
|
-
case "error": return `✗ ${a.errorText ?? "upload failed"}`;
|
|
13470
|
-
}
|
|
13471
|
-
}
|
|
13472
13806
|
/**
|
|
13473
13807
|
* Open a URL in the user's local browser. Best-effort: over SSH there may be
|
|
13474
13808
|
* nothing to open — the card keeps showing the URL as the copyable fallback.
|
|
@@ -13524,6 +13858,11 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13524
13858
|
const [planHidden, setPlanHidden] = useState(false);
|
|
13525
13859
|
const [planExpanded, setPlanExpanded] = useState(false);
|
|
13526
13860
|
const togglePlanExpanded = useCallback(() => setPlanExpanded((current) => !current), []);
|
|
13861
|
+
const [planMinimized, setPlanMinimized] = useState(false);
|
|
13862
|
+
const togglePlanMinimized = useCallback(() => {
|
|
13863
|
+
setPlanMinimized((current) => !current);
|
|
13864
|
+
setPlanExpanded(false);
|
|
13865
|
+
}, []);
|
|
13527
13866
|
const [subagentRows, setSubagentRows] = useState(0);
|
|
13528
13867
|
const [anchoredSubagentIds, setAnchoredSubagentIds] = useState([]);
|
|
13529
13868
|
const [scrolledUp, setScrolledUp] = useState(false);
|
|
@@ -13555,6 +13894,11 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13555
13894
|
const [modelPickerView, setModelPickerView] = useState(null);
|
|
13556
13895
|
const modelPickerOpen = modelPickerView !== null;
|
|
13557
13896
|
const [subagentSelection, setSubagentSelection] = useState(null);
|
|
13897
|
+
const [subagentMinimized, setSubagentMinimized] = useState(false);
|
|
13898
|
+
const toggleSubagentMinimized = useCallback(() => {
|
|
13899
|
+
setSubagentMinimized((current) => !current);
|
|
13900
|
+
setSubagentSelection(null);
|
|
13901
|
+
}, []);
|
|
13558
13902
|
const [subagentBatches, setSubagentBatches] = useState(() => /* @__PURE__ */ new Map());
|
|
13559
13903
|
const onSubagentBatch = useCallback((report) => {
|
|
13560
13904
|
setSubagentBatches((current) => {
|
|
@@ -13588,8 +13932,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13588
13932
|
}, [initialConversationId]);
|
|
13589
13933
|
const [ctrlCArmed, setCtrlCArmed] = useState(false);
|
|
13590
13934
|
const [composerRows, setComposerRows] = useState(1);
|
|
13591
|
-
const [attachments, setAttachments] = useState([]);
|
|
13592
|
-
const uploadsRef = useRef(/* @__PURE__ */ new Map());
|
|
13593
13935
|
const [forking, setForking] = useState(false);
|
|
13594
13936
|
const forkingRef = useRef(false);
|
|
13595
13937
|
const [credPrompt, setCredPrompt] = useState(null);
|
|
@@ -13599,6 +13941,20 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13599
13941
|
const [askPrompt, setAskPrompt] = useState(null);
|
|
13600
13942
|
const askPromptOpen = askPrompt !== null;
|
|
13601
13943
|
const [restartConfirm, setRestartConfirm] = useState(false);
|
|
13944
|
+
const { staged: attachments, stagedRef: attachmentsRef, stageClipboardImage, dropLast: dropLastAttachment, clearStaged: clearAttachments, collectForSend, adoptUploaded, releaseUploaded, restoreStaged } = useComposerAttachments({
|
|
13945
|
+
rest,
|
|
13946
|
+
agentId: agent.id,
|
|
13947
|
+
pasteDisabled: cardPickerOpen || modelPickerOpen || themePickerOpen || prPickerOpen || helpOpen || creditsOpen || bgTasksOpen || credPromptOpen || Boolean(grantPrompt) || computePromptOpen || askPromptOpen || restartConfirm || nonWebChannel !== null,
|
|
13948
|
+
onPasteText: (text) => {
|
|
13949
|
+
composerRef.current?.insertText(text);
|
|
13950
|
+
handleComposerChange();
|
|
13951
|
+
},
|
|
13952
|
+
onError: (message) => setItems((prev) => [...prev, {
|
|
13953
|
+
kind: "error",
|
|
13954
|
+
id: crypto.randomUUID(),
|
|
13955
|
+
text: message
|
|
13956
|
+
}])
|
|
13957
|
+
});
|
|
13602
13958
|
const [menuDismissed, setMenuDismissed] = useState(false);
|
|
13603
13959
|
const [menuHighlight, setMenuHighlight] = useState(0);
|
|
13604
13960
|
const commandQuery = grantPrompt || credPrompt || computePrompt || askPrompt || restartConfirm ? null : detectCommandTrigger(input);
|
|
@@ -13611,8 +13967,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13611
13967
|
conversationIdRef.current = conversationId;
|
|
13612
13968
|
const itemsRef = useRef(items);
|
|
13613
13969
|
itemsRef.current = items;
|
|
13614
|
-
const attachmentsRef = useRef(attachments);
|
|
13615
|
-
attachmentsRef.current = attachments;
|
|
13616
13970
|
const inputRef = useRef(input);
|
|
13617
13971
|
inputRef.current = input;
|
|
13618
13972
|
const credPromptRef = useRef(credPrompt);
|
|
@@ -13699,7 +14053,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13699
14053
|
const paneDragRef = useRef(null);
|
|
13700
14054
|
const composerBoxHeight = composerRows + 2;
|
|
13701
14055
|
const pendingVisible = !grantPrompt && !credPrompt && !computePrompt && !askPrompt && !nonWebChannel && attachments.length > 0;
|
|
13702
|
-
const pendingRows = pendingVisible ? attachments.length
|
|
14056
|
+
const pendingRows = pendingVisible ? attachmentTrayRows(attachments.length) : 0;
|
|
13703
14057
|
const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
|
|
13704
14058
|
const nonWebNoticeHeight = 5;
|
|
13705
14059
|
const computePromptHeight = 4;
|
|
@@ -13720,7 +14074,10 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13720
14074
|
const inlineReviewHeight = reviewOpen && reviewPlacement === "below" ? reviewHeight : 0;
|
|
13721
14075
|
const menuRows = menuOpen ? completionMenuHeight(menuCommands.length) : 0;
|
|
13722
14076
|
const todoCardVisible = !credPrompt && !planHidden && !!todos && todos.length > 0;
|
|
13723
|
-
const todoRows = todoCardVisible ? todoCardHeight(todos, {
|
|
14077
|
+
const todoRows = todoCardVisible ? todoCardHeight(todos, {
|
|
14078
|
+
expanded: planExpanded,
|
|
14079
|
+
minimized: planMinimized
|
|
14080
|
+
}) : 0;
|
|
13724
14081
|
const subagentItems = useMemo(() => {
|
|
13725
14082
|
const ids = new Set(anchoredSubagentIds);
|
|
13726
14083
|
return items.filter((item) => item.kind === "tool" && item.toolName === "subagent" && ids.has(item.id));
|
|
@@ -14034,28 +14391,13 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14034
14391
|
startedAtMs: Date.now()
|
|
14035
14392
|
});
|
|
14036
14393
|
try {
|
|
14037
|
-
const
|
|
14038
|
-
const
|
|
14039
|
-
const
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
attachmentIds.push(result.value.id);
|
|
14045
|
-
restorable.push({
|
|
14046
|
-
...row,
|
|
14047
|
-
status: "ready",
|
|
14048
|
-
errorText: null
|
|
14049
|
-
});
|
|
14050
|
-
} else {
|
|
14051
|
-
uploadsRef.current.delete(row.tempId);
|
|
14052
|
-
setItems((prev) => [...prev, {
|
|
14053
|
-
kind: "error",
|
|
14054
|
-
id: crypto.randomUUID(),
|
|
14055
|
-
text: `attachment failed: ${row.fileName}${result.status === "rejected" ? ` (${errorDetail(result.reason)})` : ""}`
|
|
14056
|
-
}]);
|
|
14057
|
-
}
|
|
14058
|
-
}
|
|
14394
|
+
const { files, rows, failed } = await collectForSend(staged);
|
|
14395
|
+
const attachmentIds = files.map((file) => file.id);
|
|
14396
|
+
for (const { attachment, reason } of failed) setItems((prev) => [...prev, {
|
|
14397
|
+
kind: "error",
|
|
14398
|
+
id: crypto.randomUUID(),
|
|
14399
|
+
text: `attachment failed: ${attachment.fileName}${reason === null ? "" : ` (${errorDetail(reason)})`}`
|
|
14400
|
+
}]);
|
|
14059
14401
|
if (!trimmed && attachmentIds.length === 0) {
|
|
14060
14402
|
setItems((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
14061
14403
|
if (!wasStreaming) setRun({ kind: "idle" });
|
|
@@ -14068,7 +14410,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14068
14410
|
attachmentIds,
|
|
14069
14411
|
clientSurface: "tui"
|
|
14070
14412
|
});
|
|
14071
|
-
|
|
14413
|
+
releaseUploaded(rows);
|
|
14072
14414
|
if (result.isNewConversation) setConversationId(result.conversationId);
|
|
14073
14415
|
if (result.steered) {
|
|
14074
14416
|
const directiveId = result.directive?.id;
|
|
@@ -14098,27 +14440,26 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14098
14440
|
text: sendErrorMessage(err)
|
|
14099
14441
|
}]);
|
|
14100
14442
|
if (echo) setInput((existing) => existing ? existing : trimmed);
|
|
14101
|
-
|
|
14102
|
-
if (restore.length > 0) setAttachments((prev) => [...restore.map((a) => ({
|
|
14103
|
-
...a,
|
|
14104
|
-
status: "ready",
|
|
14105
|
-
errorText: null
|
|
14106
|
-
})), ...prev]);
|
|
14443
|
+
restoreStaged(staged);
|
|
14107
14444
|
if (!wasStreaming) setRun({ kind: "idle" });
|
|
14108
14445
|
}
|
|
14109
14446
|
}, [
|
|
14110
14447
|
rest,
|
|
14111
14448
|
agent.id,
|
|
14112
14449
|
conversationId,
|
|
14113
|
-
attachToRun
|
|
14450
|
+
attachToRun,
|
|
14451
|
+
collectForSend,
|
|
14452
|
+
releaseUploaded,
|
|
14453
|
+
restoreStaged
|
|
14114
14454
|
]);
|
|
14115
14455
|
const portalStatus = portal.status;
|
|
14116
14456
|
useEffect(() => {
|
|
14117
|
-
const { seedPrompt, autoGrantMachine } = useStore.getState();
|
|
14118
|
-
if (!seedPrompt || !isNewConversation(conversation)) return;
|
|
14457
|
+
const { seedPrompt, seedAttachments, autoGrantMachine } = useStore.getState();
|
|
14458
|
+
if (!seedPrompt && seedAttachments.length === 0 || !isNewConversation(conversation)) return;
|
|
14119
14459
|
if (autoGrantMachine && (portalStatus === "off" || portalStatus === "connecting")) return;
|
|
14120
14460
|
useStore.setState({
|
|
14121
14461
|
seedPrompt: null,
|
|
14462
|
+
seedAttachments: [],
|
|
14122
14463
|
autoGrantMachine: false
|
|
14123
14464
|
});
|
|
14124
14465
|
(async () => {
|
|
@@ -14136,7 +14477,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14136
14477
|
variant: "warning"
|
|
14137
14478
|
});
|
|
14138
14479
|
}
|
|
14139
|
-
await sendContent(seedPrompt,
|
|
14480
|
+
await sendContent(seedPrompt ?? "", adoptUploaded(seedAttachments));
|
|
14140
14481
|
})();
|
|
14141
14482
|
}, [
|
|
14142
14483
|
conversation,
|
|
@@ -14144,7 +14485,8 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14144
14485
|
portalClient,
|
|
14145
14486
|
agent.id,
|
|
14146
14487
|
agent.name,
|
|
14147
|
-
sendContent
|
|
14488
|
+
sendContent,
|
|
14489
|
+
adoptUploaded
|
|
14148
14490
|
]);
|
|
14149
14491
|
const cancelLatestSteer = useCallback(() => {
|
|
14150
14492
|
if (!rest) return false;
|
|
@@ -14551,95 +14893,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14551
14893
|
if (detectCommandTrigger(composer.plainText) === null) setMenuDismissed(false);
|
|
14552
14894
|
setComposerRows(Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), maxComposerRows));
|
|
14553
14895
|
}, []);
|
|
14554
|
-
/**
|
|
14555
|
-
* Stage a pasted/dropped file in the composer tray and start its upload
|
|
14556
|
-
* immediately (the web composer's behavior). Submit awaits the in-flight
|
|
14557
|
-
* uploads and carries the resulting attachment ids; the composer never
|
|
14558
|
-
* blocks while an upload runs.
|
|
14559
|
-
*/
|
|
14560
|
-
const stageAttachment = useCallback(({ fileName, mediaType, data }) => {
|
|
14561
|
-
if (!rest) return;
|
|
14562
|
-
if (attachmentsRef.current.length >= maxAttachments) {
|
|
14563
|
-
setItems((prev) => [...prev, {
|
|
14564
|
-
kind: "error",
|
|
14565
|
-
id: crypto.randomUUID(),
|
|
14566
|
-
text: `attachment limit reached (${maxAttachments}); ${fileName} skipped`
|
|
14567
|
-
}]);
|
|
14568
|
-
return;
|
|
14569
|
-
}
|
|
14570
|
-
const tempId = crypto.randomUUID();
|
|
14571
|
-
setAttachments((prev) => [...prev, {
|
|
14572
|
-
tempId,
|
|
14573
|
-
fileName,
|
|
14574
|
-
sizeBytes: data.byteLength,
|
|
14575
|
-
status: "uploading",
|
|
14576
|
-
errorText: null
|
|
14577
|
-
}]);
|
|
14578
|
-
const promise = rest.uploadAttachment({
|
|
14579
|
-
agentId: agent.id,
|
|
14580
|
-
fileName,
|
|
14581
|
-
mediaType,
|
|
14582
|
-
data
|
|
14583
|
-
});
|
|
14584
|
-
uploadsRef.current.set(tempId, promise);
|
|
14585
|
-
promise.then(() => {
|
|
14586
|
-
setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
14587
|
-
...a,
|
|
14588
|
-
status: "ready"
|
|
14589
|
-
} : a));
|
|
14590
|
-
}).catch((err) => {
|
|
14591
|
-
setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
14592
|
-
...a,
|
|
14593
|
-
status: "error",
|
|
14594
|
-
errorText: errorMessage(err)
|
|
14595
|
-
} : a));
|
|
14596
|
-
});
|
|
14597
|
-
}, [rest, agent.id]);
|
|
14598
|
-
const stageFiles = useCallback((files) => {
|
|
14599
|
-
for (const file of files) stageAttachment(file);
|
|
14600
|
-
}, [stageAttachment]);
|
|
14601
|
-
const pasteImage = useCallback(async () => {
|
|
14602
|
-
const image = await readClipboardImage();
|
|
14603
|
-
if (!image) return;
|
|
14604
|
-
stageAttachment(image);
|
|
14605
|
-
}, [stageAttachment]);
|
|
14606
|
-
usePaste((event) => {
|
|
14607
|
-
if (cardPickerOpen || modelPickerOpen || themePickerOpen || prPickerOpen || helpOpen || creditsOpen || bgTasksOpen || credPromptOpen || grantPrompt || computePromptOpen || askPromptOpen || restartConfirm || nonWebChannel) return;
|
|
14608
|
-
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
14609
|
-
const route = routePaste({
|
|
14610
|
-
kind: event.metadata?.kind,
|
|
14611
|
-
mimeType: event.metadata?.mimeType,
|
|
14612
|
-
text
|
|
14613
|
-
});
|
|
14614
|
-
switch (route.kind) {
|
|
14615
|
-
case "binary-image": {
|
|
14616
|
-
event.preventDefault();
|
|
14617
|
-
const ext = route.mediaType.split("/")[1]?.split("+")[0] ?? "png";
|
|
14618
|
-
stageFiles([{
|
|
14619
|
-
fileName: `pasted-image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.${ext}`,
|
|
14620
|
-
mediaType: route.mediaType,
|
|
14621
|
-
data: event.bytes
|
|
14622
|
-
}]);
|
|
14623
|
-
return;
|
|
14624
|
-
}
|
|
14625
|
-
case "clipboard-image":
|
|
14626
|
-
event.preventDefault();
|
|
14627
|
-
pasteImage();
|
|
14628
|
-
return;
|
|
14629
|
-
case "dropped-paths":
|
|
14630
|
-
event.preventDefault();
|
|
14631
|
-
resolveDroppedFiles(route.paths).then((files) => {
|
|
14632
|
-
if (files) {
|
|
14633
|
-
stageFiles(files);
|
|
14634
|
-
return;
|
|
14635
|
-
}
|
|
14636
|
-
composerRef.current?.insertText(route.text);
|
|
14637
|
-
handleComposerChange();
|
|
14638
|
-
});
|
|
14639
|
-
return;
|
|
14640
|
-
case "text": return;
|
|
14641
|
-
}
|
|
14642
|
-
});
|
|
14643
14896
|
const applyComposerText = useCallback((text, caret) => {
|
|
14644
14897
|
const composer = composerRef.current;
|
|
14645
14898
|
if (!composer) return;
|
|
@@ -14759,7 +15012,13 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14759
15012
|
const screenAtFork = useStore.getState().screen;
|
|
14760
15013
|
const stillHere = () => useStore.getState().screen === screenAtFork;
|
|
14761
15014
|
rest.forkConversation({ conversationId }).then((forked) => {
|
|
14762
|
-
if (!stillHere())
|
|
15015
|
+
if (!stillHere()) {
|
|
15016
|
+
forkingRef.current = false;
|
|
15017
|
+
setForking(false);
|
|
15018
|
+
return;
|
|
15019
|
+
}
|
|
15020
|
+
const current = runRef.current;
|
|
15021
|
+
if (current.kind === "streaming") current.abort.abort();
|
|
14763
15022
|
goTo({
|
|
14764
15023
|
kind: "chat",
|
|
14765
15024
|
agent,
|
|
@@ -14772,7 +15031,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14772
15031
|
});
|
|
14773
15032
|
useStore.getState().showToast({
|
|
14774
15033
|
variant: "success",
|
|
14775
|
-
message: "Forked conversation"
|
|
15034
|
+
message: current.kind === "idle" ? "Forked conversation" : `Forked conversation — ${agent.name} keeps working on the original; reopen to reattach`
|
|
14776
15035
|
});
|
|
14777
15036
|
}).catch((err) => {
|
|
14778
15037
|
forkingRef.current = false;
|
|
@@ -15107,10 +15366,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15107
15366
|
});
|
|
15108
15367
|
break;
|
|
15109
15368
|
case "fork":
|
|
15110
|
-
if (runRef.current.kind !== "idle") {
|
|
15111
|
-
useStore.getState().showToast({ message: "finish or cancel the run first (ctrl+c)" });
|
|
15112
|
-
break;
|
|
15113
|
-
}
|
|
15114
15369
|
if (!conversationId) {
|
|
15115
15370
|
useStore.getState().showToast({ message: "nothing to fork yet; send a message first" });
|
|
15116
15371
|
break;
|
|
@@ -15148,9 +15403,20 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15148
15403
|
else setPrPickerOpen(true);
|
|
15149
15404
|
break;
|
|
15150
15405
|
case "subagent": {
|
|
15406
|
+
if (routed.mode === "minimize") {
|
|
15407
|
+
const next = !subagentMinimized;
|
|
15408
|
+
setSubagentMinimized(next);
|
|
15409
|
+
if (next) setSubagentSelection(null);
|
|
15410
|
+
useStore.getState().showToast({ message: next ? "subagents minimized (/subagent minimize again to restore)" : "subagents restored" });
|
|
15411
|
+
break;
|
|
15412
|
+
}
|
|
15151
15413
|
const batch = anchoredSubagentBatchRef.current;
|
|
15152
|
-
if (!batch || batch.rows.length === 0)
|
|
15153
|
-
|
|
15414
|
+
if (!batch || batch.rows.length === 0) {
|
|
15415
|
+
useStore.getState().showToast({ message: "no subagents in this conversation yet" });
|
|
15416
|
+
break;
|
|
15417
|
+
}
|
|
15418
|
+
setSubagentMinimized(false);
|
|
15419
|
+
setSubagentSelection({
|
|
15154
15420
|
toolCallId: batch.toolCallId,
|
|
15155
15421
|
index: 0
|
|
15156
15422
|
});
|
|
@@ -15186,15 +15452,25 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15186
15452
|
useStore.getState().showToast({ message: "no plan to show yet" });
|
|
15187
15453
|
break;
|
|
15188
15454
|
}
|
|
15455
|
+
if (routed.mode === "minimize") {
|
|
15456
|
+
const next = !planMinimized;
|
|
15457
|
+
setPlanMinimized(next);
|
|
15458
|
+
setPlanHidden(false);
|
|
15459
|
+
setPlanExpanded(false);
|
|
15460
|
+
useStore.getState().showToast({ message: next ? "plan minimized (/plan minimize again to restore)" : "plan restored" });
|
|
15461
|
+
break;
|
|
15462
|
+
}
|
|
15189
15463
|
if (routed.mode === "expand" || routed.mode === "collapse") {
|
|
15190
15464
|
const nextExpanded = routed.mode === "expand";
|
|
15191
15465
|
setPlanExpanded(nextExpanded);
|
|
15192
|
-
|
|
15466
|
+
setPlanHidden(false);
|
|
15467
|
+
setPlanMinimized(false);
|
|
15193
15468
|
useStore.getState().showToast({ message: nextExpanded ? "plan expanded (/plan collapse)" : "plan collapsed" });
|
|
15194
15469
|
break;
|
|
15195
15470
|
}
|
|
15196
15471
|
const nextHidden = routed.mode === "toggle" ? !planHidden : routed.mode === "hide";
|
|
15197
15472
|
setPlanHidden(nextHidden);
|
|
15473
|
+
if (!nextHidden) setPlanMinimized(false);
|
|
15198
15474
|
useStore.getState().showToast({ message: nextHidden ? "plan hidden (/plan show)" : "plan shown" });
|
|
15199
15475
|
break;
|
|
15200
15476
|
}
|
|
@@ -15262,7 +15538,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15262
15538
|
setInput("");
|
|
15263
15539
|
composerRef.current?.clear();
|
|
15264
15540
|
setComposerRows(1);
|
|
15265
|
-
|
|
15541
|
+
clearAttachments();
|
|
15266
15542
|
sendContent(content, staged);
|
|
15267
15543
|
}, [
|
|
15268
15544
|
history,
|
|
@@ -15282,6 +15558,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15282
15558
|
compactConversation,
|
|
15283
15559
|
quit,
|
|
15284
15560
|
sendContent,
|
|
15561
|
+
clearAttachments,
|
|
15285
15562
|
goTo,
|
|
15286
15563
|
agent,
|
|
15287
15564
|
appUrl,
|
|
@@ -15293,6 +15570,8 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15293
15570
|
promptForCards,
|
|
15294
15571
|
todos,
|
|
15295
15572
|
planHidden,
|
|
15573
|
+
planMinimized,
|
|
15574
|
+
subagentMinimized,
|
|
15296
15575
|
prLinks.length
|
|
15297
15576
|
]);
|
|
15298
15577
|
const submit = useCallback(() => {
|
|
@@ -15535,16 +15814,12 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15535
15814
|
}
|
|
15536
15815
|
if (key.name === "v" && key.ctrl) {
|
|
15537
15816
|
if (nonWebChannel) return;
|
|
15538
|
-
|
|
15817
|
+
stageClipboardImage();
|
|
15539
15818
|
return;
|
|
15540
15819
|
}
|
|
15541
15820
|
if (key.name === "x" && key.ctrl || key.name === "backspace" && attachmentsRef.current.length > 0 && !composerRef.current?.plainText) {
|
|
15542
15821
|
if (nonWebChannel) return;
|
|
15543
|
-
|
|
15544
|
-
if (last) {
|
|
15545
|
-
uploadsRef.current.delete(last.tempId);
|
|
15546
|
-
setAttachments((prev) => prev.filter((a) => a.tempId !== last.tempId));
|
|
15547
|
-
}
|
|
15822
|
+
dropLastAttachment();
|
|
15548
15823
|
return;
|
|
15549
15824
|
}
|
|
15550
15825
|
if (key.name === "l" && key.ctrl) openInBrowser();
|
|
@@ -15939,12 +16214,16 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15939
16214
|
/* @__PURE__ */ jsx(SubagentCardView, {
|
|
15940
16215
|
items: subagentItems,
|
|
15941
16216
|
isActive: run.kind !== "idle",
|
|
16217
|
+
minimized: subagentMinimized,
|
|
16218
|
+
onToggleMinimized: toggleSubagentMinimized,
|
|
15942
16219
|
onHeightChange: setSubagentRows
|
|
15943
16220
|
}),
|
|
15944
16221
|
todoCardVisible && todos ? /* @__PURE__ */ jsx(TodoCardView, {
|
|
15945
16222
|
todos,
|
|
15946
16223
|
isActive: run.kind !== "idle",
|
|
16224
|
+
minimized: planMinimized,
|
|
15947
16225
|
expanded: planExpanded,
|
|
16226
|
+
onToggleMinimized: togglePlanMinimized,
|
|
15948
16227
|
onToggleExpanded: togglePlanExpanded
|
|
15949
16228
|
}) : null,
|
|
15950
16229
|
compacting ? /* @__PURE__ */ jsxs("box", {
|
|
@@ -15964,29 +16243,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15964
16243
|
children: [" ", "you can keep typing"]
|
|
15965
16244
|
})]
|
|
15966
16245
|
}) : null,
|
|
15967
|
-
pendingVisible ? /* @__PURE__ */
|
|
15968
|
-
style: {
|
|
15969
|
-
paddingLeft: 1,
|
|
15970
|
-
paddingRight: 1,
|
|
15971
|
-
flexShrink: 0,
|
|
15972
|
-
marginTop: 1,
|
|
15973
|
-
flexDirection: "column"
|
|
15974
|
-
},
|
|
15975
|
-
children: [attachments.map((a) => /* @__PURE__ */ jsxs("text", {
|
|
15976
|
-
fg: a.status === "error" ? theme.error : theme.muted,
|
|
15977
|
-
children: [
|
|
15978
|
-
"📎 ",
|
|
15979
|
-
a.fileName,
|
|
15980
|
-
" (",
|
|
15981
|
-
formatBytes(a.sizeBytes),
|
|
15982
|
-
") ",
|
|
15983
|
-
attachmentGlyph(a)
|
|
15984
|
-
]
|
|
15985
|
-
}, a.tempId)), /* @__PURE__ */ jsx("text", {
|
|
15986
|
-
fg: theme.dim,
|
|
15987
|
-
children: "ctrl+x remove last"
|
|
15988
|
-
})]
|
|
15989
|
-
}) : null,
|
|
16246
|
+
pendingVisible ? /* @__PURE__ */ jsx(AttachmentTray, { attachments }) : null,
|
|
15990
16247
|
menuOpen ? /* @__PURE__ */ jsx(CompletionMenu, {
|
|
15991
16248
|
items: menuCommands.map((c) => ({
|
|
15992
16249
|
id: c.name,
|