skydive-cli 0.6.0-beta.19 → 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 +1 -1
- package/dist/js/bin.mjs +20 -20
- package/dist/js/{boot-BWiSsPQJ.mjs → boot-CPaxEwFQ.mjs} +667 -514
- package/dist/js/{client-HWCoL5Jm.mjs → client-BPgSlxoE.mjs} +1 -1
- package/dist/js/client-BhSqd_K5.mjs +5 -0
- package/dist/js/{daemon-Dl3nN-w4.mjs → daemon-By6thwls.mjs} +1 -1
- package/dist/js/{daemon-L_e8QFNp.mjs → daemon-DH8a0-D8.mjs} +2 -2
- package/dist/js/daemon-client-ClJWL3-E.mjs +8 -0
- package/dist/js/{daemon-client-BUakFQGS.mjs → daemon-client-CyYn91wO.mjs} +1 -1
- package/dist/js/{install-Bs9OISd4.mjs → install-i9xERvoF.mjs} +2 -2
- package/dist/js/{print-BC2o90gX.mjs → print-BryBur1Q.mjs} +1 -1
- package/dist/js/{print-iB47KC-f.mjs → print-CGP6esDJ.mjs} +2 -2
- package/dist/js/{print-share-CsIKrGto.mjs → print-share-DDAfbocP.mjs} +1 -1
- package/dist/js/{rest-DMNGNHgm.mjs → rest-CbG5RvAI.mjs} +1 -1
- package/dist/js/{rest-DQFiN7VL.mjs → rest-iJiyyUZk.mjs} +1 -1
- package/package.json +5 -5
- package/dist/js/client-BHXI6wfY.mjs +0 -5
- package/dist/js/daemon-client-BTkEdnyF.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
|
|
@@ -3886,246 +4315,92 @@ function channelGlyph(channel) {
|
|
|
3886
4315
|
* flush against the right edge; when the row is too narrow to fit the title
|
|
3887
4316
|
* AND the time, the time is dropped rather than wrapping the row.
|
|
3888
4317
|
*/
|
|
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
|
-
};
|
|
3905
|
-
}
|
|
3906
|
-
function relativeTime(iso) {
|
|
3907
|
-
const diffMs = Date.now() - new Date(iso).getTime();
|
|
3908
|
-
if (diffMs < 6e4) return "just now";
|
|
3909
|
-
if (diffMs < 36e5) return `${Math.floor(diffMs / 6e4)}m ago`;
|
|
3910
|
-
if (diffMs < 864e5) return `${Math.floor(diffMs / 36e5)}h ago`;
|
|
3911
|
-
return `${Math.floor(diffMs / 864e5)}d ago`;
|
|
3912
|
-
}
|
|
3913
|
-
|
|
3914
|
-
//#endregion
|
|
3915
|
-
//#region src/chat/conversation-ref.ts
|
|
3916
|
-
/**
|
|
3917
|
-
* Parsing the conversation reference a user hands `/conversation`.
|
|
3918
|
-
*
|
|
3919
|
-
* The whole point of the command is that you paste whatever you already have
|
|
3920
|
-
* in front of you, so every form the platform hands out has to work: a bare
|
|
3921
|
-
* id, the `https://skydive.com/c/<id>` link an agent replies with, or the
|
|
3922
|
-
* `skydive chat --resume <id>` line the TUI prints on exit. All three carry
|
|
3923
|
-
* exactly one id, so we pull the first uuid out of the string rather than
|
|
3924
|
-
* teaching this about URL shapes and command syntax.
|
|
3925
|
-
*
|
|
3926
|
-
* Validating here (instead of letting the API answer) is what turns a typo
|
|
3927
|
-
* into "not a conversation id" in the composer rather than a request that
|
|
3928
|
-
* cannot succeed.
|
|
3929
|
-
*/
|
|
3930
|
-
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
3931
|
-
/**
|
|
3932
|
-
* The conversation id in `input`, lowercased, or null when there isn't one.
|
|
3933
|
-
* Trailing punctuation, query strings and surrounding prose are tolerated —
|
|
3934
|
-
* a pasted link or command line is the expected input, not the exception.
|
|
3935
|
-
*/
|
|
3936
|
-
function parseConversationRef(input) {
|
|
3937
|
-
const match = uuidPattern.exec(input);
|
|
3938
|
-
return match ? match[0].toLowerCase() : null;
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
//#endregion
|
|
3942
|
-
//#region src/chat/transcript-export.ts
|
|
3943
|
-
function messageText(message) {
|
|
3944
|
-
const chunks = [];
|
|
3945
|
-
for (const part of message.parts) if ((part.type === "text" || part.type === "reasoning") && "text" in part && typeof part.text === "string") chunks.push(part.text);
|
|
3946
|
-
else if (part.type === "dynamic-tool" && "toolName" in part) chunks.push(`[tool: ${typeof part.toolName === "string" ? part.toolName : "tool"}]`);
|
|
3947
|
-
return chunks.join("\n").trim();
|
|
3948
|
-
}
|
|
3949
|
-
function messageLabel(message) {
|
|
3950
|
-
if (message.role === "user") return "User";
|
|
3951
|
-
if (message.role === "assistant") return message.metadata?.custom?.agentName ?? "Assistant";
|
|
3952
|
-
return message.role.charAt(0).toUpperCase() + message.role.slice(1);
|
|
3953
|
-
}
|
|
3954
|
-
function parseTranscriptFormat(value) {
|
|
3955
|
-
const normalized = value.trim().toLowerCase();
|
|
3956
|
-
if (!normalized || normalized === "md" || normalized === "markdown") return "markdown";
|
|
3957
|
-
if (normalized === "txt" || normalized === "text") return "text";
|
|
3958
|
-
return null;
|
|
3959
|
-
}
|
|
3960
|
-
function formatTranscript(messages, format) {
|
|
3961
|
-
const entries = messages.flatMap((message) => {
|
|
3962
|
-
const text = messageText(message);
|
|
3963
|
-
if (!text) return [];
|
|
3964
|
-
const label = messageLabel(message);
|
|
3965
|
-
return [format === "markdown" ? `## ${label}\n\n${text}` : `[${label}]\n${text}`];
|
|
3966
|
-
});
|
|
3967
|
-
if (format === "markdown") return `# Conversation transcript\n\n${entries.join("\n\n")}\n`;
|
|
3968
|
-
return `${entries.join("\n\n")}\n`;
|
|
3969
|
-
}
|
|
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
|
|
4318
|
+
function layoutRow(title, time, tag, width) {
|
|
4319
|
+
const GLYPH = 2;
|
|
4320
|
+
const tagCost = tag ? tag.length + 1 : 0;
|
|
4321
|
+
const timeCost = time.length + 1;
|
|
4322
|
+
const titleBudget = width - GLYPH - tagCost - timeCost;
|
|
4323
|
+
if (titleBudget < 8) return {
|
|
4324
|
+
title: truncate(title, Math.max(1, width - GLYPH - tagCost)),
|
|
4325
|
+
time: ""
|
|
4097
4326
|
};
|
|
4098
|
-
|
|
4099
|
-
|
|
4327
|
+
const fitTitle = truncate(title, titleBudget);
|
|
4328
|
+
const used = GLYPH + fitTitle.length + tagCost;
|
|
4329
|
+
const pad = Math.max(1, width - used - time.length);
|
|
4330
|
+
return {
|
|
4331
|
+
title: fitTitle,
|
|
4332
|
+
time: `${" ".repeat(pad)}${time}`
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4335
|
+
function relativeTime(iso) {
|
|
4336
|
+
const diffMs = Date.now() - new Date(iso).getTime();
|
|
4337
|
+
if (diffMs < 6e4) return "just now";
|
|
4338
|
+
if (diffMs < 36e5) return `${Math.floor(diffMs / 6e4)}m ago`;
|
|
4339
|
+
if (diffMs < 864e5) return `${Math.floor(diffMs / 36e5)}h ago`;
|
|
4340
|
+
return `${Math.floor(diffMs / 864e5)}d ago`;
|
|
4100
4341
|
}
|
|
4342
|
+
|
|
4343
|
+
//#endregion
|
|
4344
|
+
//#region src/chat/conversation-ref.ts
|
|
4101
4345
|
/**
|
|
4102
|
-
*
|
|
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).
|
|
4346
|
+
* Parsing the conversation reference a user hands `/conversation`.
|
|
4106
4347
|
*
|
|
4107
|
-
*
|
|
4108
|
-
*
|
|
4109
|
-
*
|
|
4110
|
-
*
|
|
4348
|
+
* The whole point of the command is that you paste whatever you already have
|
|
4349
|
+
* in front of you, so every form the platform hands out has to work: a bare
|
|
4350
|
+
* id, the `https://skydive.com/c/<id>` link an agent replies with, or the
|
|
4351
|
+
* `skydive chat --resume <id>` line the TUI prints on exit. All three carry
|
|
4352
|
+
* exactly one id, so we pull the first uuid out of the string rather than
|
|
4353
|
+
* teaching this about URL shapes and command syntax.
|
|
4354
|
+
*
|
|
4355
|
+
* Validating here (instead of letting the API answer) is what turns a typo
|
|
4356
|
+
* into "not a conversation id" in the composer rather than a request that
|
|
4357
|
+
* cannot succeed.
|
|
4111
4358
|
*/
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
}
|
|
4359
|
+
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
4360
|
+
/**
|
|
4361
|
+
* The conversation id in `input`, lowercased, or null when there isn't one.
|
|
4362
|
+
* Trailing punctuation, query strings and surrounding prose are tolerated —
|
|
4363
|
+
* a pasted link or command line is the expected input, not the exception.
|
|
4364
|
+
*/
|
|
4365
|
+
function parseConversationRef(input) {
|
|
4366
|
+
const match = uuidPattern.exec(input);
|
|
4367
|
+
return match ? match[0].toLowerCase() : null;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
//#endregion
|
|
4371
|
+
//#region src/chat/transcript-export.ts
|
|
4372
|
+
function messageText(message) {
|
|
4373
|
+
const chunks = [];
|
|
4374
|
+
for (const part of message.parts) if ((part.type === "text" || part.type === "reasoning") && "text" in part && typeof part.text === "string") chunks.push(part.text);
|
|
4375
|
+
else if (part.type === "dynamic-tool" && "toolName" in part) chunks.push(`[tool: ${typeof part.toolName === "string" ? part.toolName : "tool"}]`);
|
|
4376
|
+
return chunks.join("\n").trim();
|
|
4377
|
+
}
|
|
4378
|
+
function messageLabel(message) {
|
|
4379
|
+
if (message.role === "user") return "User";
|
|
4380
|
+
if (message.role === "assistant") return message.metadata?.custom?.agentName ?? "Assistant";
|
|
4381
|
+
return message.role.charAt(0).toUpperCase() + message.role.slice(1);
|
|
4382
|
+
}
|
|
4383
|
+
function parseTranscriptFormat(value) {
|
|
4384
|
+
const normalized = value.trim().toLowerCase();
|
|
4385
|
+
if (!normalized || normalized === "md" || normalized === "markdown") return "markdown";
|
|
4386
|
+
if (normalized === "txt" || normalized === "text") return "text";
|
|
4387
|
+
return null;
|
|
4388
|
+
}
|
|
4389
|
+
function formatTranscript(messages, format) {
|
|
4390
|
+
const entries = messages.flatMap((message) => {
|
|
4391
|
+
const text = messageText(message);
|
|
4392
|
+
if (!text) return [];
|
|
4393
|
+
const label = messageLabel(message);
|
|
4394
|
+
return [format === "markdown" ? `## ${label}\n\n${text}` : `[${label}]\n${text}`];
|
|
4395
|
+
});
|
|
4396
|
+
if (format === "markdown") return `# Conversation transcript\n\n${entries.join("\n\n")}\n`;
|
|
4397
|
+
return `${entries.join("\n\n")}\n`;
|
|
4398
|
+
}
|
|
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;
|
|
4129
4404
|
}
|
|
4130
4405
|
|
|
4131
4406
|
//#endregion
|
|
@@ -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`;
|
|
@@ -7060,13 +7335,13 @@ function todosCanExpand(todos, cap) {
|
|
|
7060
7335
|
* done-summary line + rendered rows + optional "+N more" tail + top margin.
|
|
7061
7336
|
* Must track `windowTodos` and `TodoCardView`'s layout.
|
|
7062
7337
|
*
|
|
7063
|
-
* `
|
|
7064
|
-
* and the chevron that brings the rows back) plus the top margin, and
|
|
7065
|
-
* else, however long the list is.
|
|
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.
|
|
7066
7341
|
*/
|
|
7067
|
-
function todoCardHeight(todos, { expanded,
|
|
7342
|
+
function todoCardHeight(todos, { expanded, minimized }) {
|
|
7068
7343
|
if (todos.length === 0) return 0;
|
|
7069
|
-
if (
|
|
7344
|
+
if (minimized) return 2;
|
|
7070
7345
|
const w = windowTodos(todos, {
|
|
7071
7346
|
cap: MAX_VISIBLE_ROWS$1,
|
|
7072
7347
|
expanded
|
|
@@ -7114,13 +7389,17 @@ function rowColor$1(status) {
|
|
|
7114
7389
|
* The card has two independent click targets, one per axis of "how much do I
|
|
7115
7390
|
* show":
|
|
7116
7391
|
* - the **header** minimizes the card to that one row and back
|
|
7117
|
-
* (`
|
|
7392
|
+
* (`minimized` / `onToggleMinimized`), which is what keeps a long plan from
|
|
7118
7393
|
* eating the screen above the composer. The chevron carries the affordance.
|
|
7119
7394
|
* - the **body** unfolds the completed work the window hides
|
|
7120
7395
|
* (`expanded` / `onToggleExpanded`), the same interaction reasoning and
|
|
7121
7396
|
* tool output already use. Only clickable when something is actually folded.
|
|
7397
|
+
*
|
|
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.
|
|
7122
7401
|
*/
|
|
7123
|
-
function TaskCardContent({ label, todos, isActive,
|
|
7402
|
+
function TaskCardContent({ label, todos, isActive, minimized, expanded, onToggleMinimized, onToggleExpanded }) {
|
|
7124
7403
|
const completed = todos.filter((todo) => todo.status === "completed").length;
|
|
7125
7404
|
const canExpand = todosCanExpand(todos, MAX_VISIBLE_ROWS$1);
|
|
7126
7405
|
const { doneSummary, rows, moreCount } = windowTodos(todos, {
|
|
@@ -7131,11 +7410,11 @@ function TaskCardContent({ label, todos, isActive, collapsed, expanded, onToggle
|
|
|
7131
7410
|
style: { flexDirection: "column" },
|
|
7132
7411
|
children: [/* @__PURE__ */ jsxs("box", {
|
|
7133
7412
|
style: { flexDirection: "row" },
|
|
7134
|
-
onMouseDown:
|
|
7413
|
+
onMouseDown: onToggleMinimized,
|
|
7135
7414
|
children: [
|
|
7136
7415
|
/* @__PURE__ */ jsx("text", {
|
|
7137
7416
|
fg: theme.dim,
|
|
7138
|
-
children:
|
|
7417
|
+
children: minimized ? "▸ " : "▾ "
|
|
7139
7418
|
}),
|
|
7140
7419
|
/* @__PURE__ */ jsx("text", {
|
|
7141
7420
|
fg: isActive ? theme.accent : theme.muted,
|
|
@@ -7149,9 +7428,13 @@ function TaskCardContent({ label, todos, isActive, collapsed, expanded, onToggle
|
|
|
7149
7428
|
"/",
|
|
7150
7429
|
todos.length
|
|
7151
7430
|
]
|
|
7152
|
-
})
|
|
7431
|
+
}),
|
|
7432
|
+
!minimized && expanded && canExpand ? /* @__PURE__ */ jsxs("text", {
|
|
7433
|
+
fg: theme.dim,
|
|
7434
|
+
children: [" ", "· click a row to fold"]
|
|
7435
|
+
}) : null
|
|
7153
7436
|
]
|
|
7154
|
-
}),
|
|
7437
|
+
}), minimized ? null : /* @__PURE__ */ jsxs("box", {
|
|
7155
7438
|
style: { flexDirection: "column" },
|
|
7156
7439
|
onMouseDown: canExpand ? onToggleExpanded : void 0,
|
|
7157
7440
|
children: [
|
|
@@ -7210,11 +7493,11 @@ function TaskCardContent({ label, todos, isActive, collapsed, expanded, onToggle
|
|
|
7210
7493
|
* work. `isActive` dims the header while the run is idle. `expanded` unfolds
|
|
7211
7494
|
* the completed work the window hides — a settled plan collapses to `✔ N done`
|
|
7212
7495
|
* otherwise, and clicking the body (or `/plan expand`) brings the rows back.
|
|
7213
|
-
* `
|
|
7214
|
-
* out of the way (click the header, or `/plan
|
|
7215
|
-
* label and progress counter the way `/plan hide` does.
|
|
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.
|
|
7216
7499
|
*/
|
|
7217
|
-
function TodoCardView({ todos, isActive,
|
|
7500
|
+
function TodoCardView({ todos, isActive, minimized, expanded, onToggleMinimized, onToggleExpanded }) {
|
|
7218
7501
|
if (todos.length === 0) return null;
|
|
7219
7502
|
return /* @__PURE__ */ jsx("box", {
|
|
7220
7503
|
style: {
|
|
@@ -7228,9 +7511,9 @@ function TodoCardView({ todos, isActive, collapsed, expanded, onToggleCollapsed,
|
|
|
7228
7511
|
label: "Plan",
|
|
7229
7512
|
todos,
|
|
7230
7513
|
isActive,
|
|
7231
|
-
|
|
7514
|
+
minimized,
|
|
7232
7515
|
expanded,
|
|
7233
|
-
|
|
7516
|
+
onToggleMinimized,
|
|
7234
7517
|
onToggleExpanded
|
|
7235
7518
|
})
|
|
7236
7519
|
});
|
|
@@ -7574,13 +7857,13 @@ function SubagentRowView({ row, selected, width, nowMs, onOpen }) {
|
|
|
7574
7857
|
* the card reports its rows up through ConversationContext so the screen
|
|
7575
7858
|
* knows what's selectable).
|
|
7576
7859
|
*
|
|
7577
|
-
* `
|
|
7860
|
+
* `minimized` folds the card down to its header row: a wide fan-out is 10 rows
|
|
7578
7861
|
* of the terminal for as long as the conversation lives, so clicking the
|
|
7579
7862
|
* header (or `/subagent collapse`) folds the rows away and leaves the label,
|
|
7580
7863
|
* the live glyph and the N/M counter. The chat screen owns the flag because it
|
|
7581
7864
|
* reserves the card's rows to keep the composer pinned.
|
|
7582
7865
|
*/
|
|
7583
|
-
function ToolSubagent({ items, isActive: _isActive,
|
|
7866
|
+
function ToolSubagent({ items, isActive: _isActive, minimized, onToggleMinimized, onHeightChange }) {
|
|
7584
7867
|
const { rest, conversationId, openConversation, onSubagentBatch, subagentSelection } = useConversationContext();
|
|
7585
7868
|
const { width } = useTerminalDimensions();
|
|
7586
7869
|
const optimistic = /* @__PURE__ */ new Map();
|
|
@@ -7623,9 +7906,9 @@ function ToolSubagent({ items, isActive: _isActive, collapsed, onToggleCollapsed
|
|
|
7623
7906
|
}, [live, rows.length]);
|
|
7624
7907
|
useEffect(() => {
|
|
7625
7908
|
const visibleRows = Math.min(rows.length, MAX_VISIBLE_ROWS);
|
|
7626
|
-
onHeightChange?.(rows.length === 0 ? items.length > 0 ? 1 : 0 :
|
|
7909
|
+
onHeightChange?.(rows.length === 0 ? items.length > 0 ? 1 : 0 : minimized ? 1 : 1 + visibleRows + (rows.length > MAX_VISIBLE_ROWS ? 1 : 0));
|
|
7627
7910
|
}, [
|
|
7628
|
-
|
|
7911
|
+
minimized,
|
|
7629
7912
|
items.length,
|
|
7630
7913
|
onHeightChange,
|
|
7631
7914
|
rows.length
|
|
@@ -7686,11 +7969,11 @@ function ToolSubagent({ items, isActive: _isActive, collapsed, onToggleCollapsed
|
|
|
7686
7969
|
style: { flexDirection: "column" },
|
|
7687
7970
|
children: [/* @__PURE__ */ jsx("box", {
|
|
7688
7971
|
style: { flexDirection: "row" },
|
|
7689
|
-
onMouseDown:
|
|
7972
|
+
onMouseDown: onToggleMinimized,
|
|
7690
7973
|
children: /* @__PURE__ */ jsxs("text", { children: [
|
|
7691
7974
|
/* @__PURE__ */ jsx("span", {
|
|
7692
7975
|
fg: theme.dim,
|
|
7693
|
-
children:
|
|
7976
|
+
children: minimized ? "▸ " : "▾ "
|
|
7694
7977
|
}),
|
|
7695
7978
|
live ? /* @__PURE__ */ jsx(Spinner, { color: theme.accent }) : rows.every((row) => row.status === "completed") ? /* @__PURE__ */ jsx("span", {
|
|
7696
7979
|
fg: theme.success,
|
|
@@ -7712,12 +7995,12 @@ function ToolSubagent({ items, isActive: _isActive, collapsed, onToggleCollapsed
|
|
|
7712
7995
|
rows.length
|
|
7713
7996
|
]
|
|
7714
7997
|
}),
|
|
7715
|
-
openConversation && !
|
|
7998
|
+
openConversation && !minimized ? /* @__PURE__ */ jsxs("span", {
|
|
7716
7999
|
fg: theme.dim,
|
|
7717
8000
|
children: [" ", selectedIndex !== null ? "↑/↓ select · ↵ view · esc back" : "/subagent to browse"]
|
|
7718
8001
|
}) : null
|
|
7719
8002
|
] })
|
|
7720
|
-
}),
|
|
8003
|
+
}), minimized ? null : /* @__PURE__ */ jsxs("box", {
|
|
7721
8004
|
style: { flexDirection: "column" },
|
|
7722
8005
|
children: [visible.map((row, i) => {
|
|
7723
8006
|
const target = row.conversationId;
|
|
@@ -7744,11 +8027,11 @@ function ToolSubagent({ items, isActive: _isActive, collapsed, onToggleCollapsed
|
|
|
7744
8027
|
* a settled batch keeps its rows — titles and drill-in — until the next
|
|
7745
8028
|
* fan-out replaces the group.
|
|
7746
8029
|
*
|
|
7747
|
-
* `
|
|
7748
|
-
* `/subagent
|
|
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
|
|
7749
8032
|
* composer for the rest of the conversation.
|
|
7750
8033
|
*/
|
|
7751
|
-
function SubagentCardView({ items, isActive,
|
|
8034
|
+
function SubagentCardView({ items, isActive, minimized, onToggleMinimized, onHeightChange }) {
|
|
7752
8035
|
const [height, setHeight] = useState(0);
|
|
7753
8036
|
useEffect(() => onHeightChange?.(height > 0 ? height + 1 : 0), [height, onHeightChange]);
|
|
7754
8037
|
return /* @__PURE__ */ jsx("box", {
|
|
@@ -7763,8 +8046,8 @@ function SubagentCardView({ items, isActive, collapsed, onToggleCollapsed, onHei
|
|
|
7763
8046
|
children: /* @__PURE__ */ jsx(ToolSubagent, {
|
|
7764
8047
|
items,
|
|
7765
8048
|
isActive,
|
|
7766
|
-
|
|
7767
|
-
|
|
8049
|
+
minimized,
|
|
8050
|
+
onToggleMinimized,
|
|
7768
8051
|
onHeightChange: setHeight
|
|
7769
8052
|
})
|
|
7770
8053
|
});
|
|
@@ -8793,20 +9076,20 @@ const keybindGroups = [
|
|
|
8793
9076
|
action: "jump back to your last message"
|
|
8794
9077
|
},
|
|
8795
9078
|
{
|
|
8796
|
-
keys: "click header",
|
|
8797
|
-
action: "minimize
|
|
9079
|
+
keys: "click card header",
|
|
9080
|
+
action: "minimize the pinned plan or subagents card, and restore it"
|
|
8798
9081
|
},
|
|
8799
9082
|
{
|
|
8800
|
-
keys: "click",
|
|
9083
|
+
keys: "click card body",
|
|
8801
9084
|
action: "expand a folded plan card back to its finished rows"
|
|
8802
9085
|
},
|
|
8803
9086
|
{
|
|
8804
|
-
keys: "/plan [show|hide|
|
|
8805
|
-
action: "show / hide / minimize
|
|
9087
|
+
keys: "/plan [show|hide|minimize|expand|collapse]",
|
|
9088
|
+
action: "show / hide / minimize the pinned plan card, or unfold it"
|
|
8806
9089
|
},
|
|
8807
9090
|
{
|
|
8808
|
-
keys: "/subagent [
|
|
8809
|
-
action: "browse the pinned subagents card, or minimize
|
|
9091
|
+
keys: "/subagent [minimize]",
|
|
9092
|
+
action: "browse the pinned subagents card, or minimize it"
|
|
8810
9093
|
},
|
|
8811
9094
|
{
|
|
8812
9095
|
keys: "/rename <title>",
|
|
@@ -8960,6 +9243,18 @@ const keybindGroups = [
|
|
|
8960
9243
|
keys: "↵",
|
|
8961
9244
|
action: "open conversation"
|
|
8962
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
|
+
},
|
|
8963
9258
|
{
|
|
8964
9259
|
keys: "ctrl+a",
|
|
8965
9260
|
action: "archive conversation (hides it for you only; again to restore)"
|
|
@@ -9511,12 +9806,9 @@ const slashCommands = [
|
|
|
9511
9806
|
{
|
|
9512
9807
|
name: "subagent",
|
|
9513
9808
|
aliases: ["subagents"],
|
|
9514
|
-
argsHint: "[
|
|
9515
|
-
description: "browse
|
|
9516
|
-
action: {
|
|
9517
|
-
kind: "subagent",
|
|
9518
|
-
mode: "browse"
|
|
9519
|
-
}
|
|
9809
|
+
argsHint: "[minimize]",
|
|
9810
|
+
description: "browse or minimize the fan-out card",
|
|
9811
|
+
action: { kind: "subagent" }
|
|
9520
9812
|
},
|
|
9521
9813
|
{
|
|
9522
9814
|
name: "status",
|
|
@@ -9535,8 +9827,8 @@ const slashCommands = [
|
|
|
9535
9827
|
{
|
|
9536
9828
|
name: "plan",
|
|
9537
9829
|
aliases: ["todos", "todo"],
|
|
9538
|
-
argsHint: "[show|hide|
|
|
9539
|
-
description: "show, hide, minimize or
|
|
9830
|
+
argsHint: "[show|hide|minimize|expand|collapse]",
|
|
9831
|
+
description: "show, hide, minimize, or unfold the pinned plan card",
|
|
9540
9832
|
action: {
|
|
9541
9833
|
kind: "plan",
|
|
9542
9834
|
mode: "toggle"
|
|
@@ -11847,7 +12139,7 @@ function canPreview(file) {
|
|
|
11847
12139
|
if (/\/(json|xml|javascript|typescript|x-sh|yaml)$/.test(file.mediaType)) return true;
|
|
11848
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);
|
|
11849
12141
|
}
|
|
11850
|
-
function formatBytes
|
|
12142
|
+
function formatBytes(bytes) {
|
|
11851
12143
|
if (bytes < 1024) return `${bytes} B`;
|
|
11852
12144
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
11853
12145
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -11915,7 +12207,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
11915
12207
|
]);
|
|
11916
12208
|
const visibleFiles = useMemo(() => load.kind === "ready" ? filterReviewFiles(load.files, fileSearch ?? "", (file) => file.path) : [], [load, fileSearch]);
|
|
11917
12209
|
const rows = useMemo(() => buildWorkspaceTree(visibleFiles), [visibleFiles]);
|
|
11918
|
-
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);
|
|
11919
12211
|
const shellDimensions = reviewShellDimensions({
|
|
11920
12212
|
width,
|
|
11921
12213
|
height: shellHeight,
|
|
@@ -11954,7 +12246,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
11954
12246
|
if (!canPreview(selectedFile)) {
|
|
11955
12247
|
setPreview({
|
|
11956
12248
|
kind: "none",
|
|
11957
|
-
message: `${selectedFile.mediaType} · ${formatBytes
|
|
12249
|
+
message: `${selectedFile.mediaType} · ${formatBytes(selectedFile.sizeBytes)}\npreview unavailable for binary or large files`
|
|
11958
12250
|
});
|
|
11959
12251
|
return;
|
|
11960
12252
|
}
|
|
@@ -12193,7 +12485,7 @@ function WorkspaceFilesTab({ agentId, viewportWidth, viewportHeight, active, she
|
|
|
12193
12485
|
row.name,
|
|
12194
12486
|
/* @__PURE__ */ jsxs("span", {
|
|
12195
12487
|
fg: theme.faint,
|
|
12196
|
-
children: [" · ", formatBytes
|
|
12488
|
+
children: [" · ", formatBytes(row.file.sizeBytes)]
|
|
12197
12489
|
})
|
|
12198
12490
|
]
|
|
12199
12491
|
}, row.file.id);
|
|
@@ -13375,16 +13667,13 @@ function routeInput(raw) {
|
|
|
13375
13667
|
const arg = match.rest.toLowerCase();
|
|
13376
13668
|
return {
|
|
13377
13669
|
kind: "plan",
|
|
13378
|
-
mode: arg === "show" ? "show" : arg === "hide" ? "hide" : arg === "expand" ? "expand" : arg === "collapse" ? "collapse" : "toggle"
|
|
13379
|
-
};
|
|
13380
|
-
}
|
|
13381
|
-
case "subagent": {
|
|
13382
|
-
const arg = match.rest.toLowerCase();
|
|
13383
|
-
return {
|
|
13384
|
-
kind: "subagent",
|
|
13385
|
-
mode: arg === "collapse" ? "collapse" : arg === "expand" ? "expand" : "browse"
|
|
13670
|
+
mode: arg === "show" ? "show" : arg === "hide" ? "hide" : arg === "expand" ? "expand" : arg === "collapse" ? "collapse" : arg === "minimize" ? "minimize" : "toggle"
|
|
13386
13671
|
};
|
|
13387
13672
|
}
|
|
13673
|
+
case "subagent": return {
|
|
13674
|
+
kind: "subagent",
|
|
13675
|
+
mode: match.rest.toLowerCase() === "minimize" ? "minimize" : "browse"
|
|
13676
|
+
};
|
|
13388
13677
|
case "new-conversation":
|
|
13389
13678
|
case "fork":
|
|
13390
13679
|
case "archive-conversation":
|
|
@@ -13505,7 +13794,6 @@ const pageScrollFraction = .75;
|
|
|
13505
13794
|
const HISTORY_PAGE_SIZE = 60;
|
|
13506
13795
|
/** Cap composer growth; past this the textarea scrolls its content instead. */
|
|
13507
13796
|
const maxComposerRows = 8;
|
|
13508
|
-
const maxAttachments = 10;
|
|
13509
13797
|
/**
|
|
13510
13798
|
* A server timestamp as local epoch ms, or null when it is missing or
|
|
13511
13799
|
* unparseable so the caller can fall back to its own clock.
|
|
@@ -13515,19 +13803,6 @@ function parseStampMs(iso) {
|
|
|
13515
13803
|
const ms = Date.parse(iso);
|
|
13516
13804
|
return Number.isNaN(ms) ? null : ms;
|
|
13517
13805
|
}
|
|
13518
|
-
function formatBytes(bytes) {
|
|
13519
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
13520
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
13521
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
13522
|
-
}
|
|
13523
|
-
/** Status glyph for a staged attachment chip: uploading / ready / failed. */
|
|
13524
|
-
function attachmentGlyph(a) {
|
|
13525
|
-
switch (a.status) {
|
|
13526
|
-
case "uploading": return "↑";
|
|
13527
|
-
case "ready": return "✓";
|
|
13528
|
-
case "error": return `✗ ${a.errorText ?? "upload failed"}`;
|
|
13529
|
-
}
|
|
13530
|
-
}
|
|
13531
13806
|
/**
|
|
13532
13807
|
* Open a URL in the user's local browser. Best-effort: over SSH there may be
|
|
13533
13808
|
* nothing to open — the card keeps showing the URL as the copyable fallback.
|
|
@@ -13583,12 +13858,10 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13583
13858
|
const [planHidden, setPlanHidden] = useState(false);
|
|
13584
13859
|
const [planExpanded, setPlanExpanded] = useState(false);
|
|
13585
13860
|
const togglePlanExpanded = useCallback(() => setPlanExpanded((current) => !current), []);
|
|
13586
|
-
const [
|
|
13587
|
-
const
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
return !current;
|
|
13591
|
-
});
|
|
13861
|
+
const [planMinimized, setPlanMinimized] = useState(false);
|
|
13862
|
+
const togglePlanMinimized = useCallback(() => {
|
|
13863
|
+
setPlanMinimized((current) => !current);
|
|
13864
|
+
setPlanExpanded(false);
|
|
13592
13865
|
}, []);
|
|
13593
13866
|
const [subagentRows, setSubagentRows] = useState(0);
|
|
13594
13867
|
const [anchoredSubagentIds, setAnchoredSubagentIds] = useState([]);
|
|
@@ -13621,12 +13894,10 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13621
13894
|
const [modelPickerView, setModelPickerView] = useState(null);
|
|
13622
13895
|
const modelPickerOpen = modelPickerView !== null;
|
|
13623
13896
|
const [subagentSelection, setSubagentSelection] = useState(null);
|
|
13624
|
-
const [
|
|
13625
|
-
const
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
return !current;
|
|
13629
|
-
});
|
|
13897
|
+
const [subagentMinimized, setSubagentMinimized] = useState(false);
|
|
13898
|
+
const toggleSubagentMinimized = useCallback(() => {
|
|
13899
|
+
setSubagentMinimized((current) => !current);
|
|
13900
|
+
setSubagentSelection(null);
|
|
13630
13901
|
}, []);
|
|
13631
13902
|
const [subagentBatches, setSubagentBatches] = useState(() => /* @__PURE__ */ new Map());
|
|
13632
13903
|
const onSubagentBatch = useCallback((report) => {
|
|
@@ -13661,8 +13932,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13661
13932
|
}, [initialConversationId]);
|
|
13662
13933
|
const [ctrlCArmed, setCtrlCArmed] = useState(false);
|
|
13663
13934
|
const [composerRows, setComposerRows] = useState(1);
|
|
13664
|
-
const [attachments, setAttachments] = useState([]);
|
|
13665
|
-
const uploadsRef = useRef(/* @__PURE__ */ new Map());
|
|
13666
13935
|
const [forking, setForking] = useState(false);
|
|
13667
13936
|
const forkingRef = useRef(false);
|
|
13668
13937
|
const [credPrompt, setCredPrompt] = useState(null);
|
|
@@ -13672,6 +13941,20 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13672
13941
|
const [askPrompt, setAskPrompt] = useState(null);
|
|
13673
13942
|
const askPromptOpen = askPrompt !== null;
|
|
13674
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
|
+
});
|
|
13675
13958
|
const [menuDismissed, setMenuDismissed] = useState(false);
|
|
13676
13959
|
const [menuHighlight, setMenuHighlight] = useState(0);
|
|
13677
13960
|
const commandQuery = grantPrompt || credPrompt || computePrompt || askPrompt || restartConfirm ? null : detectCommandTrigger(input);
|
|
@@ -13684,8 +13967,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13684
13967
|
conversationIdRef.current = conversationId;
|
|
13685
13968
|
const itemsRef = useRef(items);
|
|
13686
13969
|
itemsRef.current = items;
|
|
13687
|
-
const attachmentsRef = useRef(attachments);
|
|
13688
|
-
attachmentsRef.current = attachments;
|
|
13689
13970
|
const inputRef = useRef(input);
|
|
13690
13971
|
inputRef.current = input;
|
|
13691
13972
|
const credPromptRef = useRef(credPrompt);
|
|
@@ -13772,7 +14053,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13772
14053
|
const paneDragRef = useRef(null);
|
|
13773
14054
|
const composerBoxHeight = composerRows + 2;
|
|
13774
14055
|
const pendingVisible = !grantPrompt && !credPrompt && !computePrompt && !askPrompt && !nonWebChannel && attachments.length > 0;
|
|
13775
|
-
const pendingRows = pendingVisible ? attachments.length
|
|
14056
|
+
const pendingRows = pendingVisible ? attachmentTrayRows(attachments.length) : 0;
|
|
13776
14057
|
const credPromptHeight = 4 + (credPrompt && (credPrompt.error || credPrompt.submitting) ? 1 : 0);
|
|
13777
14058
|
const nonWebNoticeHeight = 5;
|
|
13778
14059
|
const computePromptHeight = 4;
|
|
@@ -13795,7 +14076,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
13795
14076
|
const todoCardVisible = !credPrompt && !planHidden && !!todos && todos.length > 0;
|
|
13796
14077
|
const todoRows = todoCardVisible ? todoCardHeight(todos, {
|
|
13797
14078
|
expanded: planExpanded,
|
|
13798
|
-
|
|
14079
|
+
minimized: planMinimized
|
|
13799
14080
|
}) : 0;
|
|
13800
14081
|
const subagentItems = useMemo(() => {
|
|
13801
14082
|
const ids = new Set(anchoredSubagentIds);
|
|
@@ -14110,28 +14391,13 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14110
14391
|
startedAtMs: Date.now()
|
|
14111
14392
|
});
|
|
14112
14393
|
try {
|
|
14113
|
-
const
|
|
14114
|
-
const
|
|
14115
|
-
const
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
attachmentIds.push(result.value.id);
|
|
14121
|
-
restorable.push({
|
|
14122
|
-
...row,
|
|
14123
|
-
status: "ready",
|
|
14124
|
-
errorText: null
|
|
14125
|
-
});
|
|
14126
|
-
} else {
|
|
14127
|
-
uploadsRef.current.delete(row.tempId);
|
|
14128
|
-
setItems((prev) => [...prev, {
|
|
14129
|
-
kind: "error",
|
|
14130
|
-
id: crypto.randomUUID(),
|
|
14131
|
-
text: `attachment failed: ${row.fileName}${result.status === "rejected" ? ` (${errorDetail(result.reason)})` : ""}`
|
|
14132
|
-
}]);
|
|
14133
|
-
}
|
|
14134
|
-
}
|
|
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
|
+
}]);
|
|
14135
14401
|
if (!trimmed && attachmentIds.length === 0) {
|
|
14136
14402
|
setItems((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
14137
14403
|
if (!wasStreaming) setRun({ kind: "idle" });
|
|
@@ -14144,7 +14410,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14144
14410
|
attachmentIds,
|
|
14145
14411
|
clientSurface: "tui"
|
|
14146
14412
|
});
|
|
14147
|
-
|
|
14413
|
+
releaseUploaded(rows);
|
|
14148
14414
|
if (result.isNewConversation) setConversationId(result.conversationId);
|
|
14149
14415
|
if (result.steered) {
|
|
14150
14416
|
const directiveId = result.directive?.id;
|
|
@@ -14174,27 +14440,26 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14174
14440
|
text: sendErrorMessage(err)
|
|
14175
14441
|
}]);
|
|
14176
14442
|
if (echo) setInput((existing) => existing ? existing : trimmed);
|
|
14177
|
-
|
|
14178
|
-
if (restore.length > 0) setAttachments((prev) => [...restore.map((a) => ({
|
|
14179
|
-
...a,
|
|
14180
|
-
status: "ready",
|
|
14181
|
-
errorText: null
|
|
14182
|
-
})), ...prev]);
|
|
14443
|
+
restoreStaged(staged);
|
|
14183
14444
|
if (!wasStreaming) setRun({ kind: "idle" });
|
|
14184
14445
|
}
|
|
14185
14446
|
}, [
|
|
14186
14447
|
rest,
|
|
14187
14448
|
agent.id,
|
|
14188
14449
|
conversationId,
|
|
14189
|
-
attachToRun
|
|
14450
|
+
attachToRun,
|
|
14451
|
+
collectForSend,
|
|
14452
|
+
releaseUploaded,
|
|
14453
|
+
restoreStaged
|
|
14190
14454
|
]);
|
|
14191
14455
|
const portalStatus = portal.status;
|
|
14192
14456
|
useEffect(() => {
|
|
14193
|
-
const { seedPrompt, autoGrantMachine } = useStore.getState();
|
|
14194
|
-
if (!seedPrompt || !isNewConversation(conversation)) return;
|
|
14457
|
+
const { seedPrompt, seedAttachments, autoGrantMachine } = useStore.getState();
|
|
14458
|
+
if (!seedPrompt && seedAttachments.length === 0 || !isNewConversation(conversation)) return;
|
|
14195
14459
|
if (autoGrantMachine && (portalStatus === "off" || portalStatus === "connecting")) return;
|
|
14196
14460
|
useStore.setState({
|
|
14197
14461
|
seedPrompt: null,
|
|
14462
|
+
seedAttachments: [],
|
|
14198
14463
|
autoGrantMachine: false
|
|
14199
14464
|
});
|
|
14200
14465
|
(async () => {
|
|
@@ -14212,7 +14477,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14212
14477
|
variant: "warning"
|
|
14213
14478
|
});
|
|
14214
14479
|
}
|
|
14215
|
-
await sendContent(seedPrompt,
|
|
14480
|
+
await sendContent(seedPrompt ?? "", adoptUploaded(seedAttachments));
|
|
14216
14481
|
})();
|
|
14217
14482
|
}, [
|
|
14218
14483
|
conversation,
|
|
@@ -14220,7 +14485,8 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14220
14485
|
portalClient,
|
|
14221
14486
|
agent.id,
|
|
14222
14487
|
agent.name,
|
|
14223
|
-
sendContent
|
|
14488
|
+
sendContent,
|
|
14489
|
+
adoptUploaded
|
|
14224
14490
|
]);
|
|
14225
14491
|
const cancelLatestSteer = useCallback(() => {
|
|
14226
14492
|
if (!rest) return false;
|
|
@@ -14627,95 +14893,6 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
14627
14893
|
if (detectCommandTrigger(composer.plainText) === null) setMenuDismissed(false);
|
|
14628
14894
|
setComposerRows(Math.min(Math.max(composer.editorView.getTotalVirtualLineCount(), 1), maxComposerRows));
|
|
14629
14895
|
}, []);
|
|
14630
|
-
/**
|
|
14631
|
-
* Stage a pasted/dropped file in the composer tray and start its upload
|
|
14632
|
-
* immediately (the web composer's behavior). Submit awaits the in-flight
|
|
14633
|
-
* uploads and carries the resulting attachment ids; the composer never
|
|
14634
|
-
* blocks while an upload runs.
|
|
14635
|
-
*/
|
|
14636
|
-
const stageAttachment = useCallback(({ fileName, mediaType, data }) => {
|
|
14637
|
-
if (!rest) return;
|
|
14638
|
-
if (attachmentsRef.current.length >= maxAttachments) {
|
|
14639
|
-
setItems((prev) => [...prev, {
|
|
14640
|
-
kind: "error",
|
|
14641
|
-
id: crypto.randomUUID(),
|
|
14642
|
-
text: `attachment limit reached (${maxAttachments}); ${fileName} skipped`
|
|
14643
|
-
}]);
|
|
14644
|
-
return;
|
|
14645
|
-
}
|
|
14646
|
-
const tempId = crypto.randomUUID();
|
|
14647
|
-
setAttachments((prev) => [...prev, {
|
|
14648
|
-
tempId,
|
|
14649
|
-
fileName,
|
|
14650
|
-
sizeBytes: data.byteLength,
|
|
14651
|
-
status: "uploading",
|
|
14652
|
-
errorText: null
|
|
14653
|
-
}]);
|
|
14654
|
-
const promise = rest.uploadAttachment({
|
|
14655
|
-
agentId: agent.id,
|
|
14656
|
-
fileName,
|
|
14657
|
-
mediaType,
|
|
14658
|
-
data
|
|
14659
|
-
});
|
|
14660
|
-
uploadsRef.current.set(tempId, promise);
|
|
14661
|
-
promise.then(() => {
|
|
14662
|
-
setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
14663
|
-
...a,
|
|
14664
|
-
status: "ready"
|
|
14665
|
-
} : a));
|
|
14666
|
-
}).catch((err) => {
|
|
14667
|
-
setAttachments((prev) => prev.map((a) => a.tempId === tempId ? {
|
|
14668
|
-
...a,
|
|
14669
|
-
status: "error",
|
|
14670
|
-
errorText: errorMessage(err)
|
|
14671
|
-
} : a));
|
|
14672
|
-
});
|
|
14673
|
-
}, [rest, agent.id]);
|
|
14674
|
-
const stageFiles = useCallback((files) => {
|
|
14675
|
-
for (const file of files) stageAttachment(file);
|
|
14676
|
-
}, [stageAttachment]);
|
|
14677
|
-
const pasteImage = useCallback(async () => {
|
|
14678
|
-
const image = await readClipboardImage();
|
|
14679
|
-
if (!image) return;
|
|
14680
|
-
stageAttachment(image);
|
|
14681
|
-
}, [stageAttachment]);
|
|
14682
|
-
usePaste((event) => {
|
|
14683
|
-
if (cardPickerOpen || modelPickerOpen || themePickerOpen || prPickerOpen || helpOpen || creditsOpen || bgTasksOpen || credPromptOpen || grantPrompt || computePromptOpen || askPromptOpen || restartConfirm || nonWebChannel) return;
|
|
14684
|
-
const text = event.metadata?.kind === "binary" ? "" : decodePasteBytes(event.bytes);
|
|
14685
|
-
const route = routePaste({
|
|
14686
|
-
kind: event.metadata?.kind,
|
|
14687
|
-
mimeType: event.metadata?.mimeType,
|
|
14688
|
-
text
|
|
14689
|
-
});
|
|
14690
|
-
switch (route.kind) {
|
|
14691
|
-
case "binary-image": {
|
|
14692
|
-
event.preventDefault();
|
|
14693
|
-
const ext = route.mediaType.split("/")[1]?.split("+")[0] ?? "png";
|
|
14694
|
-
stageFiles([{
|
|
14695
|
-
fileName: `pasted-image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.${ext}`,
|
|
14696
|
-
mediaType: route.mediaType,
|
|
14697
|
-
data: event.bytes
|
|
14698
|
-
}]);
|
|
14699
|
-
return;
|
|
14700
|
-
}
|
|
14701
|
-
case "clipboard-image":
|
|
14702
|
-
event.preventDefault();
|
|
14703
|
-
pasteImage();
|
|
14704
|
-
return;
|
|
14705
|
-
case "dropped-paths":
|
|
14706
|
-
event.preventDefault();
|
|
14707
|
-
resolveDroppedFiles(route.paths).then((files) => {
|
|
14708
|
-
if (files) {
|
|
14709
|
-
stageFiles(files);
|
|
14710
|
-
return;
|
|
14711
|
-
}
|
|
14712
|
-
composerRef.current?.insertText(route.text);
|
|
14713
|
-
handleComposerChange();
|
|
14714
|
-
});
|
|
14715
|
-
return;
|
|
14716
|
-
case "text": return;
|
|
14717
|
-
}
|
|
14718
|
-
});
|
|
14719
14896
|
const applyComposerText = useCallback((text, caret) => {
|
|
14720
14897
|
const composer = composerRef.current;
|
|
14721
14898
|
if (!composer) return;
|
|
@@ -15226,23 +15403,19 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15226
15403
|
else setPrPickerOpen(true);
|
|
15227
15404
|
break;
|
|
15228
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
|
+
}
|
|
15229
15413
|
const batch = anchoredSubagentBatchRef.current;
|
|
15230
15414
|
if (!batch || batch.rows.length === 0) {
|
|
15231
15415
|
useStore.getState().showToast({ message: "no subagents in this conversation yet" });
|
|
15232
15416
|
break;
|
|
15233
15417
|
}
|
|
15234
|
-
|
|
15235
|
-
setSubagentCollapsed(true);
|
|
15236
|
-
setSubagentSelection(null);
|
|
15237
|
-
useStore.getState().showToast({ message: "subagents collapsed (/subagent expand)" });
|
|
15238
|
-
break;
|
|
15239
|
-
}
|
|
15240
|
-
if (routed.mode === "expand") {
|
|
15241
|
-
setSubagentCollapsed(false);
|
|
15242
|
-
useStore.getState().showToast({ message: "subagents expanded" });
|
|
15243
|
-
break;
|
|
15244
|
-
}
|
|
15245
|
-
setSubagentCollapsed(false);
|
|
15418
|
+
setSubagentMinimized(false);
|
|
15246
15419
|
setSubagentSelection({
|
|
15247
15420
|
toolCallId: batch.toolCallId,
|
|
15248
15421
|
index: 0
|
|
@@ -15279,22 +15452,25 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15279
15452
|
useStore.getState().showToast({ message: "no plan to show yet" });
|
|
15280
15453
|
break;
|
|
15281
15454
|
}
|
|
15282
|
-
if (routed.mode === "
|
|
15283
|
-
|
|
15455
|
+
if (routed.mode === "minimize") {
|
|
15456
|
+
const next = !planMinimized;
|
|
15457
|
+
setPlanMinimized(next);
|
|
15458
|
+
setPlanHidden(false);
|
|
15284
15459
|
setPlanExpanded(false);
|
|
15285
|
-
useStore.getState().showToast({ message: "plan
|
|
15460
|
+
useStore.getState().showToast({ message: next ? "plan minimized (/plan minimize again to restore)" : "plan restored" });
|
|
15286
15461
|
break;
|
|
15287
15462
|
}
|
|
15288
|
-
if (routed.mode === "expand") {
|
|
15289
|
-
|
|
15290
|
-
setPlanExpanded(
|
|
15463
|
+
if (routed.mode === "expand" || routed.mode === "collapse") {
|
|
15464
|
+
const nextExpanded = routed.mode === "expand";
|
|
15465
|
+
setPlanExpanded(nextExpanded);
|
|
15291
15466
|
setPlanHidden(false);
|
|
15292
|
-
|
|
15467
|
+
setPlanMinimized(false);
|
|
15468
|
+
useStore.getState().showToast({ message: nextExpanded ? "plan expanded (/plan collapse)" : "plan collapsed" });
|
|
15293
15469
|
break;
|
|
15294
15470
|
}
|
|
15295
15471
|
const nextHidden = routed.mode === "toggle" ? !planHidden : routed.mode === "hide";
|
|
15296
15472
|
setPlanHidden(nextHidden);
|
|
15297
|
-
if (!nextHidden)
|
|
15473
|
+
if (!nextHidden) setPlanMinimized(false);
|
|
15298
15474
|
useStore.getState().showToast({ message: nextHidden ? "plan hidden (/plan show)" : "plan shown" });
|
|
15299
15475
|
break;
|
|
15300
15476
|
}
|
|
@@ -15362,7 +15538,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15362
15538
|
setInput("");
|
|
15363
15539
|
composerRef.current?.clear();
|
|
15364
15540
|
setComposerRows(1);
|
|
15365
|
-
|
|
15541
|
+
clearAttachments();
|
|
15366
15542
|
sendContent(content, staged);
|
|
15367
15543
|
}, [
|
|
15368
15544
|
history,
|
|
@@ -15382,6 +15558,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15382
15558
|
compactConversation,
|
|
15383
15559
|
quit,
|
|
15384
15560
|
sendContent,
|
|
15561
|
+
clearAttachments,
|
|
15385
15562
|
goTo,
|
|
15386
15563
|
agent,
|
|
15387
15564
|
appUrl,
|
|
@@ -15393,6 +15570,8 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15393
15570
|
promptForCards,
|
|
15394
15571
|
todos,
|
|
15395
15572
|
planHidden,
|
|
15573
|
+
planMinimized,
|
|
15574
|
+
subagentMinimized,
|
|
15396
15575
|
prLinks.length
|
|
15397
15576
|
]);
|
|
15398
15577
|
const submit = useCallback(() => {
|
|
@@ -15635,16 +15814,12 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
15635
15814
|
}
|
|
15636
15815
|
if (key.name === "v" && key.ctrl) {
|
|
15637
15816
|
if (nonWebChannel) return;
|
|
15638
|
-
|
|
15817
|
+
stageClipboardImage();
|
|
15639
15818
|
return;
|
|
15640
15819
|
}
|
|
15641
15820
|
if (key.name === "x" && key.ctrl || key.name === "backspace" && attachmentsRef.current.length > 0 && !composerRef.current?.plainText) {
|
|
15642
15821
|
if (nonWebChannel) return;
|
|
15643
|
-
|
|
15644
|
-
if (last) {
|
|
15645
|
-
uploadsRef.current.delete(last.tempId);
|
|
15646
|
-
setAttachments((prev) => prev.filter((a) => a.tempId !== last.tempId));
|
|
15647
|
-
}
|
|
15822
|
+
dropLastAttachment();
|
|
15648
15823
|
return;
|
|
15649
15824
|
}
|
|
15650
15825
|
if (key.name === "l" && key.ctrl) openInBrowser();
|
|
@@ -16039,16 +16214,16 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
16039
16214
|
/* @__PURE__ */ jsx(SubagentCardView, {
|
|
16040
16215
|
items: subagentItems,
|
|
16041
16216
|
isActive: run.kind !== "idle",
|
|
16042
|
-
|
|
16043
|
-
|
|
16217
|
+
minimized: subagentMinimized,
|
|
16218
|
+
onToggleMinimized: toggleSubagentMinimized,
|
|
16044
16219
|
onHeightChange: setSubagentRows
|
|
16045
16220
|
}),
|
|
16046
16221
|
todoCardVisible && todos ? /* @__PURE__ */ jsx(TodoCardView, {
|
|
16047
16222
|
todos,
|
|
16048
16223
|
isActive: run.kind !== "idle",
|
|
16049
|
-
|
|
16224
|
+
minimized: planMinimized,
|
|
16050
16225
|
expanded: planExpanded,
|
|
16051
|
-
|
|
16226
|
+
onToggleMinimized: togglePlanMinimized,
|
|
16052
16227
|
onToggleExpanded: togglePlanExpanded
|
|
16053
16228
|
}) : null,
|
|
16054
16229
|
compacting ? /* @__PURE__ */ jsxs("box", {
|
|
@@ -16068,29 +16243,7 @@ function ChatScreen({ agent, conversation, attachRunId, parentConversationId })
|
|
|
16068
16243
|
children: [" ", "you can keep typing"]
|
|
16069
16244
|
})]
|
|
16070
16245
|
}) : null,
|
|
16071
|
-
pendingVisible ? /* @__PURE__ */
|
|
16072
|
-
style: {
|
|
16073
|
-
paddingLeft: 1,
|
|
16074
|
-
paddingRight: 1,
|
|
16075
|
-
flexShrink: 0,
|
|
16076
|
-
marginTop: 1,
|
|
16077
|
-
flexDirection: "column"
|
|
16078
|
-
},
|
|
16079
|
-
children: [attachments.map((a) => /* @__PURE__ */ jsxs("text", {
|
|
16080
|
-
fg: a.status === "error" ? theme.error : theme.muted,
|
|
16081
|
-
children: [
|
|
16082
|
-
"📎 ",
|
|
16083
|
-
a.fileName,
|
|
16084
|
-
" (",
|
|
16085
|
-
formatBytes(a.sizeBytes),
|
|
16086
|
-
") ",
|
|
16087
|
-
attachmentGlyph(a)
|
|
16088
|
-
]
|
|
16089
|
-
}, a.tempId)), /* @__PURE__ */ jsx("text", {
|
|
16090
|
-
fg: theme.dim,
|
|
16091
|
-
children: "ctrl+x remove last"
|
|
16092
|
-
})]
|
|
16093
|
-
}) : null,
|
|
16246
|
+
pendingVisible ? /* @__PURE__ */ jsx(AttachmentTray, { attachments }) : null,
|
|
16094
16247
|
menuOpen ? /* @__PURE__ */ jsx(CompletionMenu, {
|
|
16095
16248
|
items: menuCommands.map((c) => ({
|
|
16096
16249
|
id: c.name,
|