dsh-code 1.0.0 → 1.0.1

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/lib/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { readFileSync, realpathSync } from "node:fs";
6
6
  import os, { homedir } from "node:os";
7
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
8
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
11
11
  import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
@@ -14,11 +14,14 @@ import { PassThrough, Stream } from "node:stream";
14
14
  import process$1, { cwd, env } from "node:process";
15
15
  import { EventEmitter } from "node:events";
16
16
  import { Buffer as Buffer$1 } from "node:buffer";
17
+ import { execFile, spawn } from "node:child_process";
18
+ import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
19
+ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
20
+ import { fileURLToPath } from "node:url";
17
21
  import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
18
22
  import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
19
23
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
20
24
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
21
- import { execFile, spawn } from "node:child_process";
22
25
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
23
26
  /**
24
27
  * @license React
@@ -24919,7 +24922,9 @@ function imageLabels(images) {
24919
24922
  if (images === void 0 || images.length === 0) return "";
24920
24923
  return images.map((image, index) => {
24921
24924
  const rawName = image.name?.trim() || `image ${index + 1}`;
24922
- return `[image: ${rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`} · ${image.width}×${image.height} · ${image.bytes} B]`;
24925
+ const name = rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`;
24926
+ const original = image.originalDimensions;
24927
+ return `[image: ${name} · ${original === void 0 ? `${image.width}×${image.height}` : `${image.width}×${image.height} · original ${original.width}×${original.height}`} · ${image.bytes} B]`;
24923
24928
  }).join("\n");
24924
24929
  }
24925
24930
  /** Prompt text with its durable image labels, without exposing local paths or bytes. */
@@ -27112,7 +27117,7 @@ function mergeSessionTitles(rows, observations) {
27112
27117
  const titles = /* @__PURE__ */ new Map();
27113
27118
  for (const observation of observations) {
27114
27119
  if (observation.status !== "fulfilled") continue;
27115
- const title = observation.value?.title?.title ?? observation.value?.title?.text;
27120
+ const title = observation.value?.title?.title;
27116
27121
  if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
27117
27122
  }
27118
27123
  return rows.map((row) => titles.has(row.id) ? {
@@ -27253,10 +27258,10 @@ function formatRate(n) {
27253
27258
  /**
27254
27259
  * Cache-hit share of billed prompt-side input.
27255
27260
  * @param usage - cumulative token totals.
27256
- * @returns rounded integer percent, or null when no input was billed.
27261
+ * @returns percent rounded to one decimal place, or null when no input was billed.
27257
27262
  */
27258
27263
  function cacheHitPercent(usage) {
27259
- return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 100);
27264
+ return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 1e3) / 10;
27260
27265
  }
27261
27266
  /** Separator between trailing state spans. */
27262
27267
  const STATUS_ITEM_SEPARATOR = " · ";
@@ -28398,7 +28403,7 @@ const PASTE_END_MARKER = "[201~";
28398
28403
  * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
28399
28404
  */
28400
28405
  function stripPasteMarkers(text) {
28401
- return text.replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28406
+ return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28402
28407
  }
28403
28408
  /** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
28404
28409
  const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u";
@@ -28409,7 +28414,6 @@ function legacyForKey(key) {
28409
28414
  const alt = (bits & 2) !== 0;
28410
28415
  const ctrl = (bits & 4) !== 0;
28411
28416
  if (key.code === 13) {
28412
- if (shift) return "\x1B[13;2u";
28413
28417
  if (ctrl) return "\n";
28414
28418
  if (alt) return "\x1B\r";
28415
28419
  return "\r";
@@ -29446,6 +29450,504 @@ function recallNewer(state) {
29446
29450
  };
29447
29451
  }
29448
29452
  //#endregion
29453
+ //#region src/authorization.ts
29454
+ /** Terminal adapter over the Harness provider-authorization and credential-record seams. */
29455
+ /** Record scope used by the upstream pi-ai adapter for provider logins. */
29456
+ const PI_AI_RECORD_SCOPE = "llm-pi-ai";
29457
+ /** Load only model-provider flows; unrelated future authorization domains stay out of `/model`. */
29458
+ async function loadProviderAuthorizations(ctx) {
29459
+ const authorization = ctx.get("authorization");
29460
+ const credentials = ctx.get("credentials");
29461
+ if (authorization === void 0 || credentials === void 0) return {
29462
+ rows: [],
29463
+ failures: []
29464
+ };
29465
+ const entries = authorization.list().filter((entry) => credentialKeyScope(entry.key) === PI_AI_RECORD_SCOPE);
29466
+ const failures = [];
29467
+ return {
29468
+ rows: await Promise.all(entries.map(async (entry) => {
29469
+ let record;
29470
+ try {
29471
+ record = await credentials.describeRecord(entry.key);
29472
+ } catch (error) {
29473
+ failures.push(`${entry.label}: ${error instanceof Error ? error.message : String(error)}`);
29474
+ record = {
29475
+ configured: false,
29476
+ writable: false
29477
+ };
29478
+ }
29479
+ return {
29480
+ key: entry.key,
29481
+ provider: credentialKeyId(entry.key),
29482
+ label: entry.label,
29483
+ methods: entry.methods,
29484
+ inFlight: entry.inFlight,
29485
+ record
29486
+ };
29487
+ })),
29488
+ failures
29489
+ };
29490
+ }
29491
+ /** Subscribe to login settlement and credential-record changes. */
29492
+ function subscribeProviderAuthorizations(ctx, listener) {
29493
+ const settled = ctx.on("authorization/settled", (key) => {
29494
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
29495
+ });
29496
+ const records = ctx.on("credentials/record-updated", (key) => {
29497
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
29498
+ });
29499
+ return () => {
29500
+ settled();
29501
+ records();
29502
+ };
29503
+ }
29504
+ /** Begin one provider login through the interaction surface owned by the caller. */
29505
+ async function beginProviderAuthorization(ctx, row, method, interaction, signal) {
29506
+ const authorization = ctx.get("authorization");
29507
+ if (authorization === void 0) throw new Error("provider login is unavailable in this profile");
29508
+ return (await authorization.begin({
29509
+ key: row.key,
29510
+ method,
29511
+ interaction,
29512
+ signal
29513
+ })).status;
29514
+ }
29515
+ /** Cancel the attempt currently serving this provider, if any. */
29516
+ function cancelProviderAuthorization(ctx, key) {
29517
+ ctx.get("authorization")?.cancel(key);
29518
+ }
29519
+ /** Remove an authorization record without changing the provider's settings profile. */
29520
+ async function logoutProviderAuthorization(ctx, row) {
29521
+ const credentials = ctx.get("credentials");
29522
+ if (credentials === void 0) throw new Error("credential storage is unavailable in this profile");
29523
+ const current = await credentials.describeRecord(row.key);
29524
+ if (!current.configured) return;
29525
+ if (!current.writable) throw new Error("this login record is read-only");
29526
+ await credentials.deleteRecord(row.key);
29527
+ }
29528
+ /** Open an authorization URL with the platform default browser, without invoking a shell. */
29529
+ function openAuthorizationUrl(raw) {
29530
+ let url;
29531
+ try {
29532
+ url = new URL(raw);
29533
+ } catch {
29534
+ return false;
29535
+ }
29536
+ if (url.protocol !== "https:" && url.protocol !== "http:") return false;
29537
+ const target = url.toString();
29538
+ try {
29539
+ const child = process.platform === "win32" ? spawn("explorer.exe", [target], {
29540
+ detached: true,
29541
+ stdio: "ignore"
29542
+ }) : process.platform === "darwin" ? spawn("/usr/bin/open", [target], {
29543
+ detached: true,
29544
+ stdio: "ignore"
29545
+ }) : spawn("xdg-open", [target], {
29546
+ detached: true,
29547
+ stdio: "ignore"
29548
+ });
29549
+ child.once("error", () => {});
29550
+ child.unref();
29551
+ return true;
29552
+ } catch {
29553
+ return false;
29554
+ }
29555
+ }
29556
+ /** Compact value-free status for the provider list. */
29557
+ function providerAuthorizationStatus(row) {
29558
+ if (row === void 0) return "login unavailable";
29559
+ if (row.inFlight) return "login in progress";
29560
+ if (!row.record.configured) return "not logged in";
29561
+ return row.record.kind === "grant" ? "OAuth" : "interactive API key";
29562
+ }
29563
+ /** Find a provider's login flow from a previously loaded directory. */
29564
+ function authorizationForProvider(directory, provider) {
29565
+ return directory?.rows.find((row) => row.provider === provider);
29566
+ }
29567
+ //#endregion
29568
+ //#region src/authorization-panel.ts
29569
+ /** Bounded Ink surfaces for provider login and logout. */
29570
+ /** Run one upstream authorization flow without letting notices or prompts exceed the panel budget. */
29571
+ function ProviderAuthorizationPanel(props) {
29572
+ const stdout = useStdout().stdout;
29573
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29574
+ const [phase, setPhase] = (0, import_react.useState)("methods");
29575
+ const [cursor, setCursor] = (0, import_react.useState)(0);
29576
+ const [notices, setNotices] = (0, import_react.useState)([]);
29577
+ const [prompt, setPrompt] = (0, import_react.useState)(void 0);
29578
+ const [draft, setDraft] = (0, import_react.useState)("");
29579
+ const [promptCursor, setPromptCursor] = (0, import_react.useState)(0);
29580
+ const [error, setError] = (0, import_react.useState)(void 0);
29581
+ const [copyState, setCopyState] = (0, import_react.useState)(void 0);
29582
+ const controllerRef = (0, import_react.useRef)(void 0);
29583
+ const replyRef = (0, import_react.useRef)(void 0);
29584
+ const openedUrls = (0, import_react.useRef)(/* @__PURE__ */ new Set());
29585
+ const clearReply = () => {
29586
+ replyRef.current?.detach();
29587
+ replyRef.current = void 0;
29588
+ setPrompt(void 0);
29589
+ setDraft("");
29590
+ setPromptCursor(0);
29591
+ };
29592
+ const decline = () => {
29593
+ const reply = replyRef.current;
29594
+ clearReply();
29595
+ reply?.reject(new AuthorizationDeclinedError());
29596
+ };
29597
+ const stop = () => {
29598
+ controllerRef.current?.abort();
29599
+ controllerRef.current = void 0;
29600
+ props.cancel(props.row.key);
29601
+ decline();
29602
+ };
29603
+ (0, import_react.useEffect)(() => () => {
29604
+ controllerRef.current?.abort();
29605
+ props.cancel(props.row.key);
29606
+ const reply = replyRef.current;
29607
+ replyRef.current = void 0;
29608
+ reply?.detach();
29609
+ reply?.reject(new AuthorizationDeclinedError());
29610
+ }, [props.row.key]);
29611
+ const start = (method) => {
29612
+ setPhase("running");
29613
+ setError(void 0);
29614
+ setNotices([]);
29615
+ setCopyState(void 0);
29616
+ const controller = new AbortController();
29617
+ controllerRef.current = controller;
29618
+ props.begin(props.row, method, {
29619
+ notify(notice) {
29620
+ setNotices((current) => [...current.slice(-19), notice]);
29621
+ if (notice.url !== void 0 && !openedUrls.current.has(notice.url)) {
29622
+ openedUrls.current.add(notice.url);
29623
+ props.openUrl(notice.url);
29624
+ }
29625
+ },
29626
+ prompt(next) {
29627
+ return new Promise((resolve, reject) => {
29628
+ const onWithdraw = () => {
29629
+ if (replyRef.current?.reject !== reject) return;
29630
+ clearReply();
29631
+ reject(/* @__PURE__ */ new Error("authorization prompt was withdrawn"));
29632
+ };
29633
+ next.signal?.addEventListener("abort", onWithdraw, { once: true });
29634
+ replyRef.current = {
29635
+ resolve,
29636
+ reject,
29637
+ detach: () => next.signal?.removeEventListener("abort", onWithdraw)
29638
+ };
29639
+ setPrompt(next);
29640
+ setDraft("");
29641
+ setPromptCursor(0);
29642
+ });
29643
+ }
29644
+ }, controller.signal).then((status) => {
29645
+ controllerRef.current = void 0;
29646
+ clearReply();
29647
+ if (status === "authorized") props.done();
29648
+ else props.back();
29649
+ }, (reason) => {
29650
+ controllerRef.current = void 0;
29651
+ clearReply();
29652
+ if (controller.signal.aborted) {
29653
+ props.back();
29654
+ return;
29655
+ }
29656
+ setError(reason instanceof Error ? reason.message : String(reason));
29657
+ setPhase("methods");
29658
+ });
29659
+ };
29660
+ const answer = (value) => {
29661
+ const reply = replyRef.current;
29662
+ clearReply();
29663
+ reply?.resolve(value);
29664
+ };
29665
+ useInput((input, key) => {
29666
+ if (phase === "methods") {
29667
+ if (key.escape || input === "q") {
29668
+ props.back();
29669
+ return;
29670
+ }
29671
+ if (props.row.methods.length === 0) return;
29672
+ if (key.upArrow) {
29673
+ setCursor((current) => (current + props.row.methods.length - 1) % props.row.methods.length);
29674
+ return;
29675
+ }
29676
+ if (key.downArrow) {
29677
+ setCursor((current) => (current + 1) % props.row.methods.length);
29678
+ return;
29679
+ }
29680
+ if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0].id);
29681
+ return;
29682
+ }
29683
+ if (key.escape) {
29684
+ stop();
29685
+ props.back();
29686
+ return;
29687
+ }
29688
+ const copyValue = notices.at(-1)?.code ?? notices.at(-1)?.url;
29689
+ if ((input === "c" || input === "C") && copyValue !== void 0) {
29690
+ props.copy(copyValue).then(() => setCopyState("copied"), (reason) => setCopyState(`copy failed: ${reason instanceof Error ? reason.message : String(reason)}`));
29691
+ return;
29692
+ }
29693
+ if (prompt === void 0) return;
29694
+ if (prompt.kind === "select") {
29695
+ if (prompt.options.length === 0) return;
29696
+ if (key.upArrow) {
29697
+ setPromptCursor((current) => (current + prompt.options.length - 1) % prompt.options.length);
29698
+ return;
29699
+ }
29700
+ if (key.downArrow) {
29701
+ setPromptCursor((current) => (current + 1) % prompt.options.length);
29702
+ return;
29703
+ }
29704
+ if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id);
29705
+ return;
29706
+ }
29707
+ if (key.backspace || key.delete) {
29708
+ setDraft((current) => [...current].slice(0, -1).join(""));
29709
+ return;
29710
+ }
29711
+ if (key.return) {
29712
+ if (draft.trim() !== "") answer(draft);
29713
+ return;
29714
+ }
29715
+ if (input !== "" && !key.ctrl && !key.meta) setDraft((current) => current + input);
29716
+ });
29717
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29718
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider login · esc cancel", viewport.contentColumns));
29719
+ const rows = [];
29720
+ if (phase === "methods") {
29721
+ if (error !== void 0) rows.push({
29722
+ key: "error",
29723
+ text: ` ${singleLineText(error)}`,
29724
+ color: inkColor(getPalette().error)
29725
+ });
29726
+ props.row.methods.forEach((method, index) => {
29727
+ rows.push({
29728
+ key: method.id,
29729
+ text: `${index === cursor ? "› " : " "}${displayText(method.label)}`,
29730
+ color: inkColor(index === cursor ? getPalette().brandBright : getPalette().dim)
29731
+ });
29732
+ });
29733
+ } else {
29734
+ notices.forEach((notice, index) => {
29735
+ rows.push({
29736
+ key: `notice-${index}`,
29737
+ text: ` ${displayText(notice.message)}`
29738
+ });
29739
+ if (notice.url !== void 0) rows.push({
29740
+ key: `url-${index}`,
29741
+ text: ` ${displayText(notice.url)}`,
29742
+ color: inkColor(getPalette().brandBright)
29743
+ });
29744
+ if (notice.code !== void 0) rows.push({
29745
+ key: `code-${index}`,
29746
+ text: ` code ${displayText(notice.code)}`,
29747
+ color: inkColor(getPalette().success),
29748
+ bold: true
29749
+ });
29750
+ });
29751
+ if (prompt !== void 0) {
29752
+ rows.push({
29753
+ key: "prompt",
29754
+ text: ` ${displayText(prompt.message)}`,
29755
+ color: inkColor(getPalette().brandBright)
29756
+ });
29757
+ if (prompt.kind === "select") prompt.options.forEach((option, index) => rows.push({
29758
+ key: `option-${option.id}`,
29759
+ text: `${index === promptCursor ? "› " : " "}${displayText(option.label)}${option.description === void 0 ? "" : ` · ${displayText(option.description)}`}`,
29760
+ color: inkColor(index === promptCursor ? getPalette().brandBright : getPalette().dim)
29761
+ }));
29762
+ else {
29763
+ const shown = prompt.kind === "secret" ? "•".repeat([...draft].length) : displayText(draft);
29764
+ rows.push({
29765
+ key: "draft",
29766
+ text: ` ${shown}▏`,
29767
+ color: inkColor(getPalette().text)
29768
+ });
29769
+ }
29770
+ } else rows.push({
29771
+ key: "waiting",
29772
+ text: " waiting for provider…",
29773
+ color: inkColor(getPalette().dim)
29774
+ });
29775
+ if (copyState !== void 0) rows.push({
29776
+ key: "copy",
29777
+ text: ` ${singleLineText(copyState)}`,
29778
+ color: inkColor(copyState === "copied" ? getPalette().success : getPalette().error)
29779
+ });
29780
+ }
29781
+ const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows));
29782
+ const footer = phase === "methods" ? "↑↓ choose · enter continue · esc/q back" : "enter answer · c copy URL/code · esc cancel login";
29783
+ return (0, import_react.createElement)(Box, {
29784
+ flexDirection: "column",
29785
+ width: viewport.outerColumns,
29786
+ paddingX: 1,
29787
+ borderStyle: "round",
29788
+ borderColor: inkColor(getPalette().brand)
29789
+ }, (0, import_react.createElement)(Text, {
29790
+ color: inkColor(getPalette().brand),
29791
+ bold: true,
29792
+ wrap: "truncate-end"
29793
+ }, truncateColumns(`/model · login ${displayText(props.row.label)}`, viewport.contentColumns)), ...visible.map((row) => (0, import_react.createElement)(Text, {
29794
+ key: row.key,
29795
+ color: row.color,
29796
+ bold: row.bold,
29797
+ wrap: "truncate-end"
29798
+ }, truncateColumns(row.text, viewport.contentColumns))), (0, import_react.createElement)(Text, {
29799
+ color: inkColor(getPalette().dim),
29800
+ wrap: "truncate-end"
29801
+ }, truncateColumns(footer, viewport.contentColumns)));
29802
+ }
29803
+ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
29804
+ const stdout = useStdout().stdout;
29805
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29806
+ const [busy, setBusy] = (0, import_react.useState)(false);
29807
+ const [error, setError] = (0, import_react.useState)(void 0);
29808
+ useInput((input, key) => {
29809
+ if (busy) return;
29810
+ if (key.escape || input === "n" || input === "N") {
29811
+ back();
29812
+ return;
29813
+ }
29814
+ if (input !== "y" && input !== "Y") return;
29815
+ setBusy(true);
29816
+ setError(void 0);
29817
+ confirm(row).then(done, (reason) => {
29818
+ setBusy(false);
29819
+ setError(reason instanceof Error ? reason.message : String(reason));
29820
+ });
29821
+ });
29822
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29823
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("y logout · n/esc back", viewport.contentColumns));
29824
+ return (0, import_react.createElement)(Box, {
29825
+ flexDirection: "column",
29826
+ width: viewport.outerColumns,
29827
+ paddingX: 1,
29828
+ borderStyle: "round",
29829
+ borderColor: inkColor(getPalette().warn)
29830
+ }, (0, import_react.createElement)(Text, {
29831
+ color: inkColor(getPalette().warn),
29832
+ bold: true,
29833
+ wrap: "truncate-end"
29834
+ }, truncateColumns("/model · logout provider", viewport.contentColumns)), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(` remove ${displayText(row.label)} login record`, viewport.contentColumns)), (0, import_react.createElement)(Text, {
29835
+ color: inkColor(getPalette().dim),
29836
+ wrap: "truncate-end"
29837
+ }, truncateColumns(" provider endpoint and model configuration stay unchanged", viewport.contentColumns)), error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
29838
+ color: inkColor(getPalette().error),
29839
+ wrap: "truncate-end"
29840
+ }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)), (0, import_react.createElement)(Text, {
29841
+ color: inkColor(getPalette().dim),
29842
+ wrap: "truncate-end"
29843
+ }, truncateColumns(busy ? "working…" : "y confirm · n/esc back", viewport.contentColumns)));
29844
+ }
29845
+ //#endregion
29846
+ //#region src/attachments.ts
29847
+ /** Terminal image-file adapter over the Harness durable attachment service. */
29848
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
29849
+ ".png",
29850
+ ".jpg",
29851
+ ".jpeg",
29852
+ ".webp",
29853
+ ".gif"
29854
+ ]);
29855
+ /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
29856
+ function detectImageMediaType(data) {
29857
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
29858
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
29859
+ if (data.length >= 6) {
29860
+ const signature = String.fromCharCode(...data.subarray(0, 6));
29861
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
29862
+ }
29863
+ if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
29864
+ }
29865
+ /** Whether a path-like token is worth probing as an image attachment. */
29866
+ function looksLikeImagePath(path) {
29867
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
29868
+ }
29869
+ /** Parse a terminal paste/drop containing only one or more image paths. */
29870
+ function parsePastedImagePaths(input) {
29871
+ const text = input.trim();
29872
+ if (text === "") return [];
29873
+ const tokens = [];
29874
+ for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
29875
+ const token = match[1] ?? match[2] ?? match[3];
29876
+ if (token === void 0) continue;
29877
+ let path = token;
29878
+ if (path.startsWith("file://")) try {
29879
+ path = fileURLToPath(path);
29880
+ } catch {
29881
+ return [];
29882
+ }
29883
+ if (!looksLikeImagePath(path)) return [];
29884
+ tokens.push(path);
29885
+ }
29886
+ return tokens;
29887
+ }
29888
+ /** Validate path, byte size and encoded signature without writing an attachment object. */
29889
+ async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
29890
+ if (paths.length === 0) return [];
29891
+ if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
29892
+ if (paths.length > attachments.imageLimits.maxImagesPerMessage) throw new Error(`too many images (${paths.length}; limit ${attachments.imageLimits.maxImagesPerMessage})`);
29893
+ const inspected = [];
29894
+ let totalBytes = 0;
29895
+ for (const raw of paths) {
29896
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
29897
+ let facts;
29898
+ try {
29899
+ facts = await stat(path);
29900
+ } catch (error) {
29901
+ throw new Error(`cannot read image "${raw}": ${error instanceof Error ? error.message : String(error)}`);
29902
+ }
29903
+ if (!facts.isFile()) throw new Error(`image path is not a file: "${raw}"`);
29904
+ if (facts.size > attachments.imageLimits.maxImageBytes) throw new Error(`image "${basename(path)}" is ${facts.size} bytes; limit ${attachments.imageLimits.maxImageBytes}`);
29905
+ totalBytes += facts.size;
29906
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error(`image batch is ${totalBytes} bytes; limit ${attachments.imageLimits.maxMessageImageBytes}`);
29907
+ const handle = await open(path, "r");
29908
+ try {
29909
+ const signature = /* @__PURE__ */ new Uint8Array(16);
29910
+ const { bytesRead } = await handle.read(signature, 0, signature.length, 0);
29911
+ const mediaType = detectImageMediaType(signature.subarray(0, bytesRead));
29912
+ if (mediaType === void 0 || !attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`unsupported image file "${raw}" (expected PNG, JPEG, WebP, or GIF)`);
29913
+ inspected.push({
29914
+ path,
29915
+ name: basename(path),
29916
+ mediaType,
29917
+ bytes: facts.size
29918
+ });
29919
+ } finally {
29920
+ await handle.close();
29921
+ }
29922
+ }
29923
+ return inspected;
29924
+ }
29925
+ /** Read, validate, and persist an ordered image path list as model content blocks. */
29926
+ async function saveImagePaths(paths, attachments) {
29927
+ if (paths.length === 0) return [];
29928
+ if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
29929
+ const inputs = [];
29930
+ for (const path of paths) {
29931
+ let data;
29932
+ try {
29933
+ data = await readFile(path);
29934
+ } catch (error) {
29935
+ throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
29936
+ }
29937
+ const mediaType = detectImageMediaType(data);
29938
+ if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
29939
+ inputs.push({
29940
+ data,
29941
+ mediaType,
29942
+ name: basename(path)
29943
+ });
29944
+ }
29945
+ return (await attachments.saveImages(inputs)).map((attachment) => ({
29946
+ type: "image",
29947
+ attachment
29948
+ }));
29949
+ }
29950
+ //#endregion
29449
29951
  //#region src/app.ts
29450
29952
  /**
29451
29953
  * The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
@@ -29490,6 +29992,106 @@ function readSettledRowCap() {
29490
29992
  const PASTE_BRACKET_TIMEOUT_MS = 1e3;
29491
29993
  /** Release the held frame after Ink has replayed the source-backed Static rows. */
29492
29994
  const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
29995
+ /** One source of truth for TUI-owned slash commands in completion and `/help`. */
29996
+ const LOCAL_COMMANDS = [
29997
+ {
29998
+ label: "/help",
29999
+ description: "show this overlay"
30000
+ },
30001
+ {
30002
+ label: "/model",
30003
+ description: "switch the model and manage providers"
30004
+ },
30005
+ {
30006
+ label: "/effort",
30007
+ description: "adjust reasoning effort for the current model"
30008
+ },
30009
+ {
30010
+ label: "/mode",
30011
+ description: "inspect or select the agent preset (/mode [preset])"
30012
+ },
30013
+ {
30014
+ label: "/permission",
30015
+ description: "inspect or select the permission preset (/permission [preset])"
30016
+ },
30017
+ {
30018
+ label: "/new",
30019
+ description: "create and switch to a fresh session (/new [preset])"
30020
+ },
30021
+ {
30022
+ label: "/fork",
30023
+ description: "fork at the latest completed turn (/fork [event-seq])"
30024
+ },
30025
+ {
30026
+ label: "/resume",
30027
+ description: "browse or switch root sessions (/resume [id|prefix])"
30028
+ },
30029
+ {
30030
+ label: "/plugin",
30031
+ description: "inspect the live plugin composition"
30032
+ },
30033
+ {
30034
+ label: "/jobs",
30035
+ description: "inspect background jobs"
30036
+ },
30037
+ {
30038
+ label: "/statusline",
30039
+ description: "customize the status line items"
30040
+ },
30041
+ {
30042
+ label: "/theme",
30043
+ description: "switch the color theme"
30044
+ },
30045
+ {
30046
+ label: "/history",
30047
+ description: "search and recall past prompts"
30048
+ },
30049
+ {
30050
+ label: "/agents",
30051
+ description: "inspect subagent sessions of this conversation"
30052
+ },
30053
+ {
30054
+ label: "/todos",
30055
+ description: "inspect the full todo list"
30056
+ },
30057
+ {
30058
+ label: "/subagent",
30059
+ description: "choose the model delegated subagents run on"
30060
+ },
30061
+ {
30062
+ label: "/delete",
30063
+ description: "delete a session and its subagent threads"
30064
+ },
30065
+ {
30066
+ label: "/clear",
30067
+ description: "clear the screen"
30068
+ },
30069
+ {
30070
+ label: "/export",
30071
+ description: "export the transcript to markdown (/export [path])"
30072
+ },
30073
+ {
30074
+ label: "/title",
30075
+ description: "rename this session (/title <text>)"
30076
+ },
30077
+ {
30078
+ label: "/copy",
30079
+ description: "copy the latest assistant response"
30080
+ },
30081
+ {
30082
+ label: "/diff",
30083
+ description: "inspect Git changes (/diff [--staged|ref])"
30084
+ },
30085
+ {
30086
+ label: "/review",
30087
+ description: "review Git changes under read-only permissions"
30088
+ },
30089
+ {
30090
+ label: "/quit",
30091
+ description: "exit"
30092
+ }
30093
+ ];
30094
+ const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map((command) => command.label.slice(1)));
29493
30095
  /** Pad text with spaces to a visible-column target (menu name column). */
29494
30096
  function padColumns(text, width) {
29495
30097
  const clipped = truncateColumns(singleLineText(text), width);
@@ -30544,7 +31146,8 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
30544
31146
  wrap: "truncate-end"
30545
31147
  }, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
30546
31148
  const index = rows.indexOf(row);
30547
- const label = displayText(`${row.providerName} · ${row.modelName}`);
31149
+ const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
31150
+ const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
30548
31151
  return (0, import_react.createElement)(Text, {
30549
31152
  key: `${row.provider}/${row.model}`,
30550
31153
  color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
@@ -30567,7 +31170,7 @@ function providerStateLabel(row) {
30567
31170
  return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
30568
31171
  }
30569
31172
  /** The provider-management stage reached from /model with `a`. */
30570
- function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, onRemove, onRetry, onBack }) {
31173
+ function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }) {
30571
31174
  const stdout = useStdout().stdout;
30572
31175
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
30573
31176
  const rows = directory?.rows ?? [];
@@ -30630,6 +31233,19 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30630
31233
  else onRemove(target);
30631
31234
  return;
30632
31235
  }
31236
+ const authorization = authorizationForProvider(authorizations, target.provider);
31237
+ if (input === "l" || input === "L") {
31238
+ if (authorization === void 0) setActionError("this provider offers no interactive login flow");
31239
+ else if (authorization.inFlight) setActionError("a login attempt is already running for this provider");
31240
+ else onLogin(target, authorization);
31241
+ return;
31242
+ }
31243
+ if (input === "o" || input === "O") {
31244
+ if (authorization === void 0 || !authorization.record.configured) setActionError("this provider has no login record to remove");
31245
+ else if (!authorization.record.writable) setActionError("this login record is read-only");
31246
+ else onLogout(target, authorization);
31247
+ return;
31248
+ }
30633
31249
  if (key.return) {
30634
31250
  if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
30635
31251
  else if (target.credential?.kind === "error") setActionError("credential status is unavailable; retry before writing");
@@ -30659,6 +31275,16 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30659
31275
  color: inkColor(getPalette().warn),
30660
31276
  wrap: "truncate-end"
30661
31277
  }, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
31278
+ ...authorizationError === void 0 ? [] : [(0, import_react.createElement)(Text, {
31279
+ key: "authorization-error",
31280
+ color: inkColor(getPalette().warn),
31281
+ wrap: "truncate-end"
31282
+ }, truncateColumns(` login status unavailable: ${singleLineText(authorizationError)}`, viewport.contentColumns))],
31283
+ ...(authorizations?.failures ?? []).map((failure, index) => (0, import_react.createElement)(Text, {
31284
+ key: `authorization-failure-${index}`,
31285
+ color: inkColor(getPalette().warn),
31286
+ wrap: "truncate-end"
31287
+ }, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
30662
31288
  ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
30663
31289
  key: "empty",
30664
31290
  color: inkColor(getPalette().dim),
@@ -30680,7 +31306,10 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30680
31306
  wrap: "truncate-end"
30681
31307
  }, truncateColumns(`/model — providers${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
30682
31308
  const index = rows.indexOf(row);
30683
- const label = `${row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`} · ${providerStateLabel(row)}${row.removable ? " · custom" : ""}`;
31309
+ const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`;
31310
+ const authorization = authorizationForProvider(authorizations, row.provider);
31311
+ const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? ` · ${providerAuthorizationStatus(authorization)}` : "";
31312
+ const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? " · custom" : ""}`;
30684
31313
  return (0, import_react.createElement)(Text, {
30685
31314
  key: row.provider,
30686
31315
  color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
@@ -30689,7 +31318,7 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30689
31318
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
30690
31319
  color: inkColor(getPalette().dim),
30691
31320
  wrap: "truncate-end"
30692
- }, truncateColumns("↑↓ move · tab configure · enter add/update key · d remove key · x remove custom provider · r retry · esc back", viewport.contentColumns)));
31321
+ }, truncateColumns("↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
30693
31322
  }
30694
31323
  /** Provider configuration editor: only explicit models are written to settings. */
30695
31324
  function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
@@ -31002,7 +31631,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31002
31631
  key: "key-submit",
31003
31632
  dimColor: true,
31004
31633
  wrap: "truncate-end"
31005
- }, " enter submit · alt+enter / ctrl+j newline · up/down history · tab complete"),
31634
+ }, " enter submit · up/down history · tab complete"),
31006
31635
  (0, import_react.createElement)(Text, {
31007
31636
  key: "key-mentions",
31008
31637
  dimColor: true,
@@ -31039,31 +31668,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31039
31668
  color: inkColor(getPalette().error),
31040
31669
  wrap: "truncate-end"
31041
31670
  }, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
31042
- (0, import_react.createElement)(Box, { key: "local-help" }, row("/help", "show this overlay")),
31043
- (0, import_react.createElement)(Box, { key: "local-model" }, row("/model", "switch the model")),
31044
- (0, import_react.createElement)(Box, { key: "local-effort" }, row("/effort", "adjust reasoning effort for the current model")),
31045
- (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
31046
- (0, import_react.createElement)(Box, { key: "local-permission" }, row("/permission", "inspect or select the permission preset (/permission [preset])")),
31047
- (0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
31048
- (0, import_react.createElement)(Box, { key: "local-fork" }, row("/fork", "fork at the latest completed turn (/fork [event-seq])")),
31049
- (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
31050
- (0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
31051
- (0, import_react.createElement)(Box, { key: "local-jobs" }, row("/jobs", "inspect background jobs")),
31052
- (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
31053
- (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
31054
- (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
31055
- (0, import_react.createElement)(Box, { key: "local-agents" }, row("/agents", "inspect subagent sessions of this conversation")),
31056
- (0, import_react.createElement)(Box, { key: "local-todos" }, row("/todos", "inspect the full todo list")),
31057
- (0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
31058
- (0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
31059
- (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
31060
- (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
31061
- (0, import_react.createElement)(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
31062
- (0, import_react.createElement)(Box, { key: "local-copy" }, row("/copy", "copy the latest assistant response")),
31063
- (0, import_react.createElement)(Box, { key: "local-diff" }, row("/diff", "inspect Git changes (/diff [--staged|ref])")),
31064
- (0, import_react.createElement)(Box, { key: "local-review" }, row("/review", "review Git changes under read-only permissions")),
31065
- (0, import_react.createElement)(Box, { key: "local-quit" }, row("/quit", "exit")),
31066
- ...descriptors.map((descriptor) => (0, import_react.createElement)(Text, {
31671
+ ...LOCAL_COMMANDS.map((command) => (0, import_react.createElement)(Box, { key: `local-${command.label.slice(1)}` }, row(command.label, command.description))),
31672
+ ...descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => (0, import_react.createElement)(Text, {
31067
31673
  key: `command-${descriptor.name}`,
31068
31674
  dimColor: true,
31069
31675
  wrap: "truncate-end"
@@ -31332,130 +31938,11 @@ const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
31332
31938
  function completionCandidates(value, descriptors, skills) {
31333
31939
  if (!value.startsWith("/")) return [];
31334
31940
  const prefix = value.slice(1).split(" ")[0] ?? "";
31335
- const local = [
31336
- {
31337
- label: "/help",
31338
- description: "show commands",
31339
- origin: "command"
31340
- },
31341
- {
31342
- label: "/model",
31343
- description: "switch the model",
31344
- origin: "command"
31345
- },
31346
- {
31347
- label: "/effort",
31348
- description: "adjust reasoning effort for the current model",
31349
- origin: "command"
31350
- },
31351
- {
31352
- label: "/mode",
31353
- description: "select the agent preset",
31354
- origin: "command"
31355
- },
31356
- {
31357
- label: "/permission",
31358
- description: "inspect or select the permission preset",
31359
- origin: "command"
31360
- },
31361
- {
31362
- label: "/new",
31363
- description: "start a fresh session",
31364
- origin: "command"
31365
- },
31366
- {
31367
- label: "/fork",
31368
- description: "fork at a completed turn",
31369
- origin: "command"
31370
- },
31371
- {
31372
- label: "/resume",
31373
- description: "browse or switch sessions",
31374
- origin: "command"
31375
- },
31376
- {
31377
- label: "/plugin",
31378
- description: "inspect the plugin composition",
31379
- origin: "command"
31380
- },
31381
- {
31382
- label: "/jobs",
31383
- description: "inspect background jobs",
31384
- origin: "command"
31385
- },
31386
- {
31387
- label: "/statusline",
31388
- description: "customize the status line",
31389
- origin: "command"
31390
- },
31391
- {
31392
- label: "/theme",
31393
- description: "switch the color theme",
31394
- origin: "command"
31395
- },
31396
- {
31397
- label: "/history",
31398
- description: "search and recall past prompts",
31399
- origin: "command"
31400
- },
31401
- {
31402
- label: "/agents",
31403
- description: "inspect subagent sessions of this conversation",
31404
- origin: "command"
31405
- },
31406
- {
31407
- label: "/todos",
31408
- description: "inspect the full todo list",
31409
- origin: "command"
31410
- },
31411
- {
31412
- label: "/subagent",
31413
- description: "choose the model delegated subagents run on",
31414
- origin: "command"
31415
- },
31416
- {
31417
- label: "/delete",
31418
- description: "delete a session and its subagent threads",
31419
- origin: "command"
31420
- },
31421
- {
31422
- label: "/clear",
31423
- description: "clear the screen",
31424
- origin: "command"
31425
- },
31426
- {
31427
- label: "/export",
31428
- description: "export the transcript to markdown",
31429
- origin: "command"
31430
- },
31431
- {
31432
- label: "/title",
31433
- description: "rename this session",
31434
- origin: "command"
31435
- },
31436
- {
31437
- label: "/copy",
31438
- description: "copy the latest assistant response",
31439
- origin: "command"
31440
- },
31441
- {
31442
- label: "/diff",
31443
- description: "inspect Git changes",
31444
- origin: "command"
31445
- },
31446
- {
31447
- label: "/review",
31448
- description: "review changes read-only",
31449
- origin: "command"
31450
- },
31451
- {
31452
- label: "/quit",
31453
- description: "exit",
31454
- origin: "command"
31455
- }
31456
- ];
31457
- const localNames = new Set(local.map((candidate) => candidate.label.slice(1)));
31458
- const registry = descriptors.filter((descriptor) => !localNames.has(descriptor.name)).map((descriptor) => ({
31941
+ const local = LOCAL_COMMANDS.map((command) => ({
31942
+ ...command,
31943
+ origin: "command"
31944
+ }));
31945
+ const registry = descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => ({
31459
31946
  label: `/${descriptor.name}`,
31460
31947
  description: descriptor.description,
31461
31948
  origin: "command"
@@ -31534,11 +32021,19 @@ function CompletionMenu({ active, mention, index, rows }) {
31534
32021
  * While a modal (approval / question / model panel) owns the keys, the
31535
32022
  * box passes every key through untouched.
31536
32023
  */
31537
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32024
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
31538
32025
  const columns = useStdout().stdout?.columns ?? 80;
31539
32026
  const stdin = useStdin().stdin;
31540
32027
  const [value, setValue] = (0, import_react.useState)("");
31541
32028
  const [cursor, setCursor] = (0, import_react.useState)(0);
32029
+ const valueRef = (0, import_react.useRef)(value);
32030
+ const cursorRef = (0, import_react.useRef)(cursor);
32031
+ valueRef.current = value;
32032
+ cursorRef.current = cursor;
32033
+ const [draftImages, setDraftImages] = (0, import_react.useState)([]);
32034
+ const draftImagesRef = (0, import_react.useRef)(draftImages);
32035
+ draftImagesRef.current = draftImages;
32036
+ const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
31542
32037
  const killRef = (0, import_react.useRef)("");
31543
32038
  const preferredColumnRef = (0, import_react.useRef)(null);
31544
32039
  const editorScrollRef = (0, import_react.useRef)(0);
@@ -31551,6 +32046,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31551
32046
  (0, import_react.useEffect)(() => {
31552
32047
  if (historyFill === void 0) return;
31553
32048
  const safe = sanitizeDraftText(historyFill.text);
32049
+ draftImagesRef.current = [];
32050
+ setDraftImages([]);
31554
32051
  setValue(safe);
31555
32052
  setCursor(safe.length);
31556
32053
  preferredColumnRef.current = null;
@@ -31567,6 +32064,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31567
32064
  recallSpace,
31568
32065
  historyConsumed
31569
32066
  ]);
32067
+ (0, import_react.useEffect)(() => {
32068
+ setDraftImages((current) => {
32069
+ const next = current.filter((image) => value.includes(image.marker));
32070
+ draftImagesRef.current = next;
32071
+ return next.length === current.length ? current : next;
32072
+ });
32073
+ }, [value]);
31570
32074
  (0, import_react.useEffect)(() => {
31571
32075
  if (stdin === void 0) return;
31572
32076
  const originalRead = stdin.read.bind(stdin);
@@ -31602,6 +32106,64 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31602
32106
  };
31603
32107
  const mentionActive = mentionToken !== void 0;
31604
32108
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
32109
+ const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
32110
+ const uniqueImageMarker = (name, source, reserved = []) => {
32111
+ const safeName = singleLineText(sanitizeDraftText(name));
32112
+ let marker = source === "mention" ? `@${safeName}` : `[image: ${safeName}]`;
32113
+ let suffix = 2;
32114
+ while (valueRef.current.includes(marker) || draftImagesRef.current.some((image) => image.marker === marker) || reserved.includes(marker)) {
32115
+ marker = source === "mention" ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`;
32116
+ suffix += 1;
32117
+ }
32118
+ return marker;
32119
+ };
32120
+ const registerDraftImage = (inspection, marker) => {
32121
+ if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32122
+ notify(`${inspection.name} is already attached`, "warning");
32123
+ return false;
32124
+ }
32125
+ const next = [...draftImagesRef.current, {
32126
+ ...inspection,
32127
+ marker
32128
+ }];
32129
+ draftImagesRef.current = next;
32130
+ setDraftImages(next);
32131
+ return true;
32132
+ };
32133
+ const insertDroppedImages = (paths) => {
32134
+ notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
32135
+ inspectImages(paths).then((inspected) => {
32136
+ const additions = [];
32137
+ const markers = [];
32138
+ for (const inspection of inspected) {
32139
+ if ([...draftImagesRef.current, ...additions].some((image) => sameImagePath(image.path, inspection.path))) continue;
32140
+ const marker = uniqueImageMarker(inspection.name, "drop", markers);
32141
+ additions.push({
32142
+ ...inspection,
32143
+ marker
32144
+ });
32145
+ markers.push(marker);
32146
+ }
32147
+ if (additions.length === 0) {
32148
+ notify("those images are already attached", "warning");
32149
+ return;
32150
+ }
32151
+ const at = cursorRef.current;
32152
+ const current = valueRef.current;
32153
+ const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? " " : ""}${markers.join(" ")}${current.slice(at) === "" ? "" : " "}`;
32154
+ const next = current.slice(0, at) + insertion + current.slice(at);
32155
+ valueRef.current = next;
32156
+ cursorRef.current = at + insertion.length;
32157
+ setValue(next);
32158
+ setCursor(cursorRef.current);
32159
+ const nextImages = [...draftImagesRef.current, ...additions];
32160
+ draftImagesRef.current = nextImages;
32161
+ setDraftImages(nextImages);
32162
+ notify(`${additions.length} image${additions.length === 1 ? "" : "s"} ready for the next message`);
32163
+ }, (reason) => {
32164
+ notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32165
+ });
32166
+ };
31605
32167
  (0, import_react.useEffect)(() => {
31606
32168
  if (!active || !mentionActive) {
31607
32169
  setMentionRows([]);
@@ -31629,6 +32191,41 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31629
32191
  if (mentionActive && mentionToken !== void 0) {
31630
32192
  const row = mentionRows[completionIndex % mentionRows.length];
31631
32193
  if (row !== void 0) {
32194
+ if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
32195
+ const tokenText = value.slice(mentionToken.start, cursor);
32196
+ const start = mentionToken.start;
32197
+ notify(`checking image ${basename(row.path)}…`);
32198
+ inspectImages([row.path]).then((inspected) => {
32199
+ const inspection = inspected[0];
32200
+ if (inspection === void 0) return;
32201
+ const current = valueRef.current;
32202
+ if (current.slice(start, start + tokenText.length) !== tokenText) return;
32203
+ if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32204
+ const next = current.slice(0, start) + current.slice(start + tokenText.length);
32205
+ valueRef.current = next;
32206
+ cursorRef.current = start;
32207
+ setValue(next);
32208
+ setCursor(start);
32209
+ setDismissedMenuValue(next);
32210
+ notify(`${inspection.name} is already attached`, "warning");
32211
+ return;
32212
+ }
32213
+ const marker = uniqueImageMarker(inspection.name, "mention");
32214
+ const next = current.slice(0, start) + marker + current.slice(start + tokenText.length);
32215
+ valueRef.current = next;
32216
+ cursorRef.current = start + marker.length;
32217
+ setValue(next);
32218
+ setCursor(cursorRef.current);
32219
+ setDismissedMenuValue(next);
32220
+ registerDraftImage(inspection, marker);
32221
+ notify(`${inspection.name} ready for the next message`);
32222
+ }, (reason) => {
32223
+ notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32224
+ });
32225
+ setCompletionIndex(0);
32226
+ setDismissedMenuValue(void 0);
32227
+ return;
32228
+ }
31632
32229
  const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
31633
32230
  setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
31634
32231
  setCursor(mentionToken.start + insertion.length);
@@ -31660,6 +32257,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31660
32257
  };
31661
32258
  useInput((input, key) => {
31662
32259
  if (!active) return;
32260
+ if (preparingImages) return;
31663
32261
  if (deleteConfirm !== void 0) {
31664
32262
  if (input === "y" || input === "Y") confirmDelete();
31665
32263
  else cancelDelete();
@@ -31687,6 +32285,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31687
32285
  else if (value !== "") {
31688
32286
  setValue("");
31689
32287
  setCursor(0);
32288
+ draftImagesRef.current = [];
32289
+ setDraftImages([]);
31690
32290
  setCompletionIndex(0);
31691
32291
  setDismissedMenuValue(void 0);
31692
32292
  } else quit();
@@ -31717,18 +32317,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31717
32317
  cancelQueued(queued[queued.length - 1].messageId);
31718
32318
  return;
31719
32319
  }
31720
- const shiftEnterSequence = input === "[13;2u" || input === "[27;2;13~";
31721
- if (shiftEnterSequence || key.return) {
32320
+ if (key.return) {
31722
32321
  if (pasteBracketRef.current) {
31723
32322
  applyEdit(insertText(value, cursor, "\n"));
31724
32323
  return;
31725
32324
  }
31726
- if (shiftEnterSequence || key.shift || key.meta || key.ctrl && input === "j") {
31727
- setValue(value.slice(0, cursor) + "\n" + value.slice(cursor));
31728
- setCursor(cursor + 1);
31729
- setDismissedMenuValue(void 0);
31730
- return;
31731
- }
31732
32325
  if (menuActive) {
31733
32326
  if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
31734
32327
  acceptMenuCandidate();
@@ -31736,6 +32329,34 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31736
32329
  }
31737
32330
  }
31738
32331
  const text = value.trim();
32332
+ if (draftImagesRef.current.length > 0) {
32333
+ setPreparingImages(true);
32334
+ notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
32335
+ const snapshot = draftImagesRef.current;
32336
+ prepareImages(snapshot.map((image) => image.path)).then((images) => {
32337
+ setPreparingImages(false);
32338
+ valueRef.current = "";
32339
+ cursorRef.current = 0;
32340
+ setValue("");
32341
+ setCursor(0);
32342
+ draftImagesRef.current = [];
32343
+ setDraftImages([]);
32344
+ setCompletionIndex(0);
32345
+ setDismissedMenuValue(void 0);
32346
+ dismissNotice();
32347
+ if (text !== "") {
32348
+ recordLocal(text);
32349
+ recordHistory(text);
32350
+ }
32351
+ recall.current = beginRecall(recallSpace, "");
32352
+ if (busy) steer(text, images);
32353
+ else dispatch(text, images);
32354
+ }, (reason) => {
32355
+ setPreparingImages(false);
32356
+ notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32357
+ });
32358
+ return;
32359
+ }
31739
32360
  setValue("");
31740
32361
  setCursor(0);
31741
32362
  setCompletionIndex(0);
@@ -31863,6 +32484,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31863
32484
  dispatch(text);
31864
32485
  return;
31865
32486
  }
32487
+ if (input === "\n" || input === "\r") return;
31866
32488
  if (menuActive && key.upArrow) {
31867
32489
  setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
31868
32490
  return;
@@ -32011,6 +32633,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32011
32633
  text = text.replaceAll(PASTE_END_MARKER, "");
32012
32634
  }
32013
32635
  if (text === "") return;
32636
+ const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : [];
32637
+ if (droppedPaths.length > 0) {
32638
+ insertDroppedImages(droppedPaths);
32639
+ return;
32640
+ }
32014
32641
  applyEdit(insertText(value, cursor, text));
32015
32642
  }
32016
32643
  });
@@ -32429,6 +33056,8 @@ function App(props) {
32429
33056
  const [modelError, setModelError] = (0, import_react.useState)(void 0);
32430
33057
  const [providerDirectory, setProviderDirectory] = (0, import_react.useState)(void 0);
32431
33058
  const [providerError, setProviderError] = (0, import_react.useState)(void 0);
33059
+ const [authorizationDirectory, setAuthorizationDirectory] = (0, import_react.useState)(void 0);
33060
+ const [authorizationError, setAuthorizationError] = (0, import_react.useState)(void 0);
32432
33061
  const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
32433
33062
  const [notice, setNotice] = (0, import_react.useState)(void 0);
32434
33063
  const notify = (0, import_react.useCallback)((text, tone = "info") => {
@@ -32476,6 +33105,24 @@ function App(props) {
32476
33105
  modelLoadEpoch,
32477
33106
  props.loadModelProviders
32478
33107
  ]);
33108
+ (0, import_react.useEffect)(() => {
33109
+ if (!modelOpen || props.loadProviderAuthorizations === void 0) return;
33110
+ let cancelled = false;
33111
+ setAuthorizationDirectory(void 0);
33112
+ setAuthorizationError(void 0);
33113
+ Promise.resolve().then(() => props.loadProviderAuthorizations()).then((loaded) => {
33114
+ if (!cancelled) setAuthorizationDirectory(loaded);
33115
+ }, (error) => {
33116
+ if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error));
33117
+ });
33118
+ return () => {
33119
+ cancelled = true;
33120
+ };
33121
+ }, [
33122
+ modelOpen,
33123
+ modelLoadEpoch,
33124
+ props.loadProviderAuthorizations
33125
+ ]);
32479
33126
  (0, import_react.useEffect)(() => {
32480
33127
  const subscribe = props.subscribeModelProviders;
32481
33128
  if (!modelOpen || subscribe === void 0) return;
@@ -32485,6 +33132,15 @@ function App(props) {
32485
33132
  setProviderError(error instanceof Error ? error.message : String(error));
32486
33133
  }
32487
33134
  }, [modelOpen, props.subscribeModelProviders]);
33135
+ (0, import_react.useEffect)(() => {
33136
+ const subscribe = props.subscribeProviderAuthorizations;
33137
+ if (!modelOpen || subscribe === void 0) return;
33138
+ try {
33139
+ return subscribe(() => setModelLoadEpoch((epoch) => epoch + 1));
33140
+ } catch (error) {
33141
+ setAuthorizationError(error instanceof Error ? error.message : String(error));
33142
+ }
33143
+ }, [modelOpen, props.subscribeProviderAuthorizations]);
32488
33144
  const busy = view.busy;
32489
33145
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
32490
33146
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
@@ -32681,13 +33337,16 @@ function App(props) {
32681
33337
  busy,
32682
33338
  streamingActive
32683
33339
  ]);
33340
+ const sessionHasImages = (0, import_react.useMemo)(() => view.entries.some((entry) => (entry.kind === "user" || entry.kind === "pending") && (entry.images?.length ?? 0) > 0), [view.entries]);
32684
33341
  /** Apply one /model pick: record the selection, close the panel, report via notice. */
32685
33342
  const applyModel = (row, effortId) => {
32686
33343
  try {
32687
33344
  const label = props.selectModel(row, effortId);
32688
33345
  setModelLabel(label);
32689
33346
  setEffortLabel(effortId);
32690
- notify(`model next step uses ${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`);
33347
+ const selected = `${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`;
33348
+ if (sessionHasImages && row.inputModalities !== void 0 && !row.inputModalities.includes("image")) notify(`model → ${selected} · image history will be sent as text placeholders`, "warning");
33349
+ else notify(`model → next step uses ${selected}`);
32691
33350
  setModelOpen(false);
32692
33351
  setProviderOpen(false);
32693
33352
  setProviderAction(void 0);
@@ -32707,7 +33366,37 @@ function App(props) {
32707
33366
  };
32708
33367
  let modelSurface;
32709
33368
  if (modelOpen && !approvalPending && !questionPending) {
32710
- if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
33369
+ if (providerAction?.kind === "login" && props.beginProviderAuthorization !== void 0 && props.cancelProviderAuthorization !== void 0 && props.openAuthorizationUrl !== void 0 && props.copyTextValue !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationPanel, {
33370
+ row: providerAction.authorization,
33371
+ begin: props.beginProviderAuthorization,
33372
+ cancel: () => props.cancelProviderAuthorization(providerAction.authorization),
33373
+ openUrl: props.openAuthorizationUrl,
33374
+ copy: props.copyTextValue,
33375
+ done: () => {
33376
+ const authorization = providerAction.authorization;
33377
+ setProviderAction(void 0);
33378
+ setProviderOpen(false);
33379
+ reloadModelSurfaces();
33380
+ notify(`logged in to ${authorization.label}; select a model`);
33381
+ },
33382
+ back: () => {
33383
+ setProviderAction(void 0);
33384
+ setProviderOpen(true);
33385
+ }
33386
+ });
33387
+ else if (providerAction?.kind === "logout" && props.logoutProviderAuthorization !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationLogoutPanel, {
33388
+ row: providerAction.authorization,
33389
+ confirm: props.logoutProviderAuthorization,
33390
+ done: () => {
33391
+ const authorization = providerAction.authorization;
33392
+ setProviderAction(void 0);
33393
+ setProviderOpen(true);
33394
+ reloadModelSurfaces();
33395
+ notify(`logged out from ${authorization.label}`);
33396
+ },
33397
+ back: () => setProviderAction(void 0)
33398
+ });
33399
+ else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
32711
33400
  target: providerAction.target,
32712
33401
  catalog: directory?.rows ?? [],
32713
33402
  save: props.saveModelProviderConfiguration,
@@ -32761,6 +33450,8 @@ function App(props) {
32761
33450
  else if (providerOpen) modelSurface = (0, import_react.createElement)(ProviderPanel, {
32762
33451
  directory: providerDirectory,
32763
33452
  error: providerError,
33453
+ authorizations: authorizationDirectory,
33454
+ authorizationError,
32764
33455
  onCredential: (target) => {
32765
33456
  if (props.saveModelProviderCredential === void 0) {
32766
33457
  notify("API key storage is unavailable in this profile", "warning");
@@ -32801,6 +33492,32 @@ function App(props) {
32801
33492
  target
32802
33493
  });
32803
33494
  },
33495
+ onLogin: (target, authorization) => {
33496
+ if (busy) {
33497
+ notify("provider login is available only while the agent is idle", "warning");
33498
+ return;
33499
+ }
33500
+ if (props.beginProviderAuthorization === void 0 || props.cancelProviderAuthorization === void 0 || props.openAuthorizationUrl === void 0 || props.copyTextValue === void 0) {
33501
+ notify("provider login is unavailable in this profile", "warning");
33502
+ return;
33503
+ }
33504
+ setProviderAction({
33505
+ kind: "login",
33506
+ target,
33507
+ authorization
33508
+ });
33509
+ },
33510
+ onLogout: (target, authorization) => {
33511
+ if (props.logoutProviderAuthorization === void 0) {
33512
+ notify("provider logout is unavailable in this profile", "warning");
33513
+ return;
33514
+ }
33515
+ setProviderAction({
33516
+ kind: "logout",
33517
+ target,
33518
+ authorization
33519
+ });
33520
+ },
32804
33521
  onRetry: reloadModelSurfaces,
32805
33522
  onBack: () => setProviderOpen(false)
32806
33523
  });
@@ -32983,6 +33700,8 @@ function App(props) {
32983
33700
  setModelError(void 0);
32984
33701
  setProviderDirectory(void 0);
32985
33702
  setProviderError(void 0);
33703
+ setAuthorizationDirectory(void 0);
33704
+ setAuthorizationError(void 0);
32986
33705
  setProviderOpen(false);
32987
33706
  setProviderAction(void 0);
32988
33707
  setEffortFor(void 0);
@@ -33070,6 +33789,8 @@ function App(props) {
33070
33789
  if (!busy && !streamingActive) refreshScreen();
33071
33790
  },
33072
33791
  loadMentions: props.loadMentions,
33792
+ inspectImages: props.inspectImages,
33793
+ prepareImages: props.prepareImages,
33073
33794
  cyclePermission: props.cyclePermission,
33074
33795
  exportTranscript: props.exportTranscript,
33075
33796
  renameTitle: props.renameTitle,
@@ -33394,14 +34115,16 @@ async function loadModelDirectory(ctx) {
33394
34115
  provider: provider.id,
33395
34116
  providerName: provider.name,
33396
34117
  model: model.id,
33397
- modelName: model.name
34118
+ modelName: model.name,
34119
+ ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
33398
34120
  };
33399
34121
  if (llmResolve.resolveModelInfo === void 0) return row;
33400
34122
  try {
33401
34123
  const resolved = await llmResolve.resolveModelInfo(provider.id, model.id);
33402
- return resolved.reasoning === void 0 ? row : {
34124
+ return {
33403
34125
  ...row,
33404
- reasoning: mapReasoning(resolved.reasoning)
34126
+ ...resolved.inputModalities === void 0 ? {} : { inputModalities: [...resolved.inputModalities] },
34127
+ ...resolved.reasoning === void 0 ? {} : { reasoning: mapReasoning(resolved.reasoning) }
33405
34128
  };
33406
34129
  } catch {
33407
34130
  reasoningFailures.push(`${provider.id}/${model.id}`);
@@ -33503,7 +34226,7 @@ function deriveCredentialRef(provider) {
33503
34226
  }
33504
34227
  /** Events that invalidate the official Models provider/settings/credential join. */
33505
34228
  const PROVIDER_SETTINGS_EVENTS = [
33506
- "credentials/updated",
34229
+ "credentials/reference-updated",
33507
34230
  "settings/document-updated",
33508
34231
  "llm/adapters-updated"
33509
34232
  ];
@@ -33821,7 +34544,8 @@ function createMentions(ctx, agent, cwd) {
33821
34544
  const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
33822
34545
  label: candidate.path,
33823
34546
  description: candidate.kind === "directory" ? "Folder" : "File",
33824
- kind: candidate.kind
34547
+ kind: candidate.kind,
34548
+ ...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
33825
34549
  }));
33826
34550
  const sessionRows = sessions.map((candidate) => ({
33827
34551
  label: formatSessionReferenceMention(candidate),
@@ -34284,44 +35008,6 @@ function buildExportMarkdown(view, sessionId) {
34284
35008
  return out.join("\n");
34285
35009
  }
34286
35010
  //#endregion
34287
- //#region src/attachments.ts
34288
- /** Terminal image-file adapter over the Harness durable attachment service. */
34289
- /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
34290
- function detectImageMediaType(data) {
34291
- if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
34292
- if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
34293
- if (data.length >= 6) {
34294
- const signature = String.fromCharCode(...data.subarray(0, 6));
34295
- if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
34296
- }
34297
- if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
34298
- }
34299
- /** Read, validate, and persist an ordered image path list as model content blocks. */
34300
- async function saveImagePaths(paths, attachments) {
34301
- if (paths.length === 0) return [];
34302
- if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
34303
- const inputs = [];
34304
- for (const path of paths) {
34305
- let data;
34306
- try {
34307
- data = await readFile(path);
34308
- } catch (error) {
34309
- throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
34310
- }
34311
- const mediaType = detectImageMediaType(data);
34312
- if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
34313
- inputs.push({
34314
- data,
34315
- mediaType,
34316
- name: basename(path)
34317
- });
34318
- }
34319
- return (await attachments.saveImages(inputs)).map((attachment) => ({
34320
- type: "image",
34321
- attachment
34322
- }));
34323
- }
34324
- //#endregion
34325
35011
  //#region src/editor.ts
34326
35012
  /** Host editor and clipboard adapters used by the terminal surface. */
34327
35013
  function waitForProcess(command, args, input) {
@@ -34602,7 +35288,6 @@ function applyPendingPermission(service, session, pending) {
34602
35288
  */
34603
35289
  function listPermissionRows(service) {
34604
35290
  return service.names.map((id) => {
34605
- if (service.optionOf === void 0) return { id };
34606
35291
  try {
34607
35292
  return {
34608
35293
  id,
@@ -35216,16 +35901,16 @@ async function run(ctx, startup, io) {
35216
35901
  deliverLine(line, mode, images);
35217
35902
  };
35218
35903
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
35219
- const dispatch = (text) => {
35220
- send(text, "followup");
35904
+ const dispatch = (text, images = []) => {
35905
+ send(text, "followup", images);
35221
35906
  };
35222
35907
  /**
35223
35908
  * Submit steering: a running driver consumes the text at its next step
35224
35909
  * boundary (the inbox delivers between steps); an idle driver just starts
35225
35910
  * a turn, so this doubles as the busy-state submit path.
35226
35911
  */
35227
- const steer = (text) => {
35228
- send(text, "steer");
35912
+ const steer = (text, images = []) => {
35913
+ send(text, "steer", images);
35229
35914
  };
35230
35915
  /** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
35231
35916
  const interrupt = () => {
@@ -35637,7 +36322,16 @@ async function run(ctx, startup, io) {
35637
36322
  saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
35638
36323
  unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
35639
36324
  removeModelProvider: (target) => removeProviderSettings(ctx, target),
36325
+ loadProviderAuthorizations: () => loadProviderAuthorizations(ctx),
36326
+ subscribeProviderAuthorizations: (listener) => subscribeProviderAuthorizations(ctx, listener),
36327
+ beginProviderAuthorization: (row, method, interaction, signal) => beginProviderAuthorization(ctx, row, method, interaction, signal),
36328
+ cancelProviderAuthorization: (row) => cancelProviderAuthorization(ctx, row.key),
36329
+ logoutProviderAuthorization: (row) => logoutProviderAuthorization(ctx, row),
36330
+ openAuthorizationUrl,
36331
+ copyTextValue: copyText,
35640
36332
  loadMentions: (query, signal) => mentions.candidates(query, signal),
36333
+ inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
36334
+ prepareImages: (paths) => saveImagePaths(paths, ctx.get("attachments")),
35641
36335
  cyclePermission: cyclePermission$1,
35642
36336
  setPermission: setPermissionAction,
35643
36337
  selectModel,
@@ -35687,7 +36381,13 @@ async function run(ctx, startup, io) {
35687
36381
  mountRef.current?.rerender(appElement());
35688
36382
  };
35689
36383
  mountRef.current = io.mount(appElement());
35690
- if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => send(startup.prompt ?? "", "followup", images), (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
36384
+ if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) {
36385
+ if ((startup.images?.length ?? 0) > 0) bridge.notify(`processing ${startup.images.length} startup image${startup.images.length === 1 ? "" : "s"}…`);
36386
+ saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => {
36387
+ if (images.length > 0) bridge.notify(`${images.length} startup image${images.length === 1 ? "" : "s"} attached`);
36388
+ send(startup.prompt ?? "", "followup", images);
36389
+ }, (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
36390
+ }
35691
36391
  async function copyLastResponse() {
35692
36392
  const text = latestAssistantText(store.getView());
35693
36393
  if (text === void 0) return "nothing to copy yet";