dsh-code 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +278 -265
- package/README.md +277 -264
- package/cordis.patch.yml +7 -0
- package/lib/index.mjs +1649 -575
- package/lib/types/app.d.ts +19 -14
- package/lib/types/attachments.d.ts +14 -1
- package/lib/types/authorization-panel.d.ts +22 -0
- package/lib/types/authorization.d.ts +36 -0
- package/lib/types/keyboard.d.ts +32 -3
- package/lib/types/mentions.d.ts +4 -0
- package/lib/types/models.d.ts +3 -1
- package/lib/types/permissions.d.ts +4 -14
- package/lib/types/presets.d.ts +4 -17
- package/lib/types/render/animations.d.ts +27 -11
- package/lib/types/render/editor.d.ts +32 -7
- package/lib/types/render/lines.d.ts +1 -1
- package/lib/types/render/status.d.ts +1 -1
- package/package.json +49 -43
- package/src/app.ts +4570 -4188
- package/src/attachments.ts +93 -2
- package/src/authorization-panel.ts +285 -0
- package/src/authorization.ts +147 -0
- package/src/index.ts +34 -21
- package/src/internals.ts +26 -9
- package/src/keyboard.ts +164 -39
- package/src/mentions.ts +12 -1
- package/src/models.ts +20 -14
- package/src/permissions.ts +5 -13
- package/src/presets.ts +5 -18
- package/src/provider-settings.ts +1 -1
- package/src/render/animations.ts +447 -400
- package/src/render/editor.ts +125 -25
- package/src/render/lines.ts +16 -2
- package/src/render/projection.ts +7 -3
- package/src/render/status.ts +2 -2
- package/src/session-directory.ts +1 -1
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";
|
|
@@ -16,9 +16,12 @@ import { EventEmitter } from "node:events";
|
|
|
16
16
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
17
17
|
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
18
|
import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
|
|
19
|
+
import { execFile, spawn } from "node:child_process";
|
|
20
|
+
import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
|
|
21
|
+
import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
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
|
-
|
|
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. */
|
|
@@ -26628,11 +26633,7 @@ function renderMarkdown(text, width, options = {}) {
|
|
|
26628
26633
|
}
|
|
26629
26634
|
//#endregion
|
|
26630
26635
|
//#region src/render/animations.ts
|
|
26631
|
-
/**
|
|
26632
|
-
* The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
|
|
26633
|
-
* ring trail clockwise around the eight outer positions, one braille glyph
|
|
26634
|
-
* per step — 8 frames × 125ms = the web's 1s cycle.
|
|
26635
|
-
*/
|
|
26636
|
+
/** Original terminal StateDot chase frames. */
|
|
26636
26637
|
const BUSY_CHASE_FRAMES = [
|
|
26637
26638
|
"⣾",
|
|
26638
26639
|
"⣽",
|
|
@@ -26643,10 +26644,40 @@ const BUSY_CHASE_FRAMES = [
|
|
|
26643
26644
|
"⣯",
|
|
26644
26645
|
"⣷"
|
|
26645
26646
|
];
|
|
26646
|
-
/** Chase frame for a monotonic tick
|
|
26647
|
+
/** Chase frame for a monotonic tick. */
|
|
26647
26648
|
function busyChaseFrame(tick) {
|
|
26648
26649
|
return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0];
|
|
26649
26650
|
}
|
|
26651
|
+
/** Codex shimmer timing and geometry. */
|
|
26652
|
+
const DEEP_DIVING_SHIMMER_DURATION_MS = 2e3;
|
|
26653
|
+
const DEEP_DIVING_SPARK_BREATH_DURATION_MS = 2e3;
|
|
26654
|
+
/**
|
|
26655
|
+
* Codex's 2-second shimmer sweep, expressed in terminal ticks. The sweep has
|
|
26656
|
+
* ten virtual columns of padding on either side and a five-column cosine
|
|
26657
|
+
* highlight band, so the text changes gently rather than cycling rapidly.
|
|
26658
|
+
*/
|
|
26659
|
+
function deepDivingShimmerIntensity(index, tick, graphemeCount) {
|
|
26660
|
+
if (graphemeCount <= 0) return 0;
|
|
26661
|
+
const period = graphemeCount + 20;
|
|
26662
|
+
const position = (tick * 33 % DEEP_DIVING_SHIMMER_DURATION_MS + DEEP_DIVING_SHIMMER_DURATION_MS) % DEEP_DIVING_SHIMMER_DURATION_MS / DEEP_DIVING_SHIMMER_DURATION_MS * period;
|
|
26663
|
+
const distance = Math.abs(index + 10 - position);
|
|
26664
|
+
if (distance > 5) return 0;
|
|
26665
|
+
const angle = Math.PI * distance / 5;
|
|
26666
|
+
return .5 * (1 + Math.cos(angle));
|
|
26667
|
+
}
|
|
26668
|
+
/** Blue RGB color for one grapheme in the Codex-style shimmer. */
|
|
26669
|
+
function deepDivingGradientColor(index, tick, graphemeCount, base, highlight) {
|
|
26670
|
+
return blendRgb(highlight, base, deepDivingShimmerIntensity(index, tick, graphemeCount));
|
|
26671
|
+
}
|
|
26672
|
+
/** Smooth breathing intensity for the always-visible Deep diving sparkle. */
|
|
26673
|
+
function deepDivingSparkIntensity(tick) {
|
|
26674
|
+
const phase = (tick * 33 % DEEP_DIVING_SPARK_BREATH_DURATION_MS + DEEP_DIVING_SPARK_BREATH_DURATION_MS) % DEEP_DIVING_SPARK_BREATH_DURATION_MS / DEEP_DIVING_SPARK_BREATH_DURATION_MS;
|
|
26675
|
+
return .2 + .8 * (.5 + .5 * Math.cos(Math.PI * 2 * phase));
|
|
26676
|
+
}
|
|
26677
|
+
/** Blue RGB color for the breathing Deep diving sparkle. */
|
|
26678
|
+
function deepDivingSparkColor(tick, base, highlight) {
|
|
26679
|
+
return blendRgb(highlight, base, deepDivingSparkIntensity(tick));
|
|
26680
|
+
}
|
|
26650
26681
|
/** Caret visibility: half the ticks on, half off (530ms blink). */
|
|
26651
26682
|
function caretVisible(tick) {
|
|
26652
26683
|
return tick % 2 === 0;
|
|
@@ -27036,6 +27067,87 @@ function effortAboveHigh(effort) {
|
|
|
27036
27067
|
return rank !== void 0 && rank > 3;
|
|
27037
27068
|
}
|
|
27038
27069
|
//#endregion
|
|
27070
|
+
//#region src/mentions.ts
|
|
27071
|
+
/** Menu cap on file rows; the service owns ranking and default rows. */
|
|
27072
|
+
const MAX_FILE_ROWS = 20;
|
|
27073
|
+
/** Whether a mention token is already navigating a filesystem path. */
|
|
27074
|
+
function isPathLikeMentionQuery(query) {
|
|
27075
|
+
return /[\\/]/u.test(query);
|
|
27076
|
+
}
|
|
27077
|
+
/**
|
|
27078
|
+
* Create the mention API for one agent's workspace. A missing
|
|
27079
|
+
* `fileReferences` service (with an agent present) or `sessionReferenceResolver`
|
|
27080
|
+
* degrades that half to empty rows; `prepare` passes text through untouched
|
|
27081
|
+
* without references. An undefined agent (a bare launch before any session
|
|
27082
|
+
* exists) runs the official WorkspaceFileSearch over the launch cwd — the
|
|
27083
|
+
* same class the mounted service uses per agent — so `@` file completion
|
|
27084
|
+
* works from the first keystroke; session references wait for the session.
|
|
27085
|
+
*
|
|
27086
|
+
* `candidates` never reaches for `this` — the runner hands it to the input
|
|
27087
|
+
* editor as a detached callback, and a `this`-bound method would throw on
|
|
27088
|
+
* every `@` key.
|
|
27089
|
+
* @param ctx - context carrying the optional `fileReferences` and
|
|
27090
|
+
* `sessionReferenceResolver` services.
|
|
27091
|
+
* @param agent - the session owner; excluded from its own session candidates.
|
|
27092
|
+
* @param cwd - launch working directory; bounds the pre-session search.
|
|
27093
|
+
*/
|
|
27094
|
+
function createMentions(ctx, agent, cwd) {
|
|
27095
|
+
const resolver = ctx.get("sessionReferenceResolver");
|
|
27096
|
+
const fileReferences = ctx.get("fileReferences");
|
|
27097
|
+
const sessionCapable = agent !== void 0 && resolver !== void 0;
|
|
27098
|
+
let preSessionSearch;
|
|
27099
|
+
const preSessionFiles = (query, signal) => {
|
|
27100
|
+
preSessionSearch ??= new WorkspaceFileSearch(cwd, {
|
|
27101
|
+
maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
27102
|
+
maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
27103
|
+
excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
|
|
27104
|
+
});
|
|
27105
|
+
return preSessionSearch.list(query, signal ?? new AbortController().signal);
|
|
27106
|
+
};
|
|
27107
|
+
return {
|
|
27108
|
+
async candidates(query, signal) {
|
|
27109
|
+
const needle = query.trim();
|
|
27110
|
+
const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && !isPathLikeMentionQuery(needle) && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
|
|
27111
|
+
const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
|
|
27112
|
+
label: candidate.path,
|
|
27113
|
+
description: candidate.kind === "directory" ? "Folder" : "File",
|
|
27114
|
+
kind: candidate.kind,
|
|
27115
|
+
...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
|
|
27116
|
+
}));
|
|
27117
|
+
const sessionRows = sessions.map((candidate) => ({
|
|
27118
|
+
label: formatSessionReferenceMention(candidate),
|
|
27119
|
+
description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
|
|
27120
|
+
kind: "session"
|
|
27121
|
+
}));
|
|
27122
|
+
return [...fileRows, ...sessionRows];
|
|
27123
|
+
},
|
|
27124
|
+
parse(text) {
|
|
27125
|
+
return parseSessionReferenceText(text);
|
|
27126
|
+
},
|
|
27127
|
+
async prepare(parsed, signal) {
|
|
27128
|
+
if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
|
|
27129
|
+
text: parsed.text,
|
|
27130
|
+
references: parsed.references
|
|
27131
|
+
};
|
|
27132
|
+
const prepared = await resolver.prepare(agent, [{
|
|
27133
|
+
type: "text",
|
|
27134
|
+
text: parsed.text
|
|
27135
|
+
}], parsed.references, signal);
|
|
27136
|
+
return {
|
|
27137
|
+
text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
|
|
27138
|
+
references: parsed.references,
|
|
27139
|
+
additionalContext: prepared.additionalContext
|
|
27140
|
+
};
|
|
27141
|
+
},
|
|
27142
|
+
sessionMention(candidate) {
|
|
27143
|
+
return formatSessionReferenceMention({
|
|
27144
|
+
sessionId: candidate.sessionId,
|
|
27145
|
+
label: candidate.label
|
|
27146
|
+
});
|
|
27147
|
+
}
|
|
27148
|
+
};
|
|
27149
|
+
}
|
|
27150
|
+
//#endregion
|
|
27039
27151
|
//#region src/session-directory.ts
|
|
27040
27152
|
/** Lightweight session-directory projection for the /resume picker. */
|
|
27041
27153
|
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
@@ -27112,7 +27224,7 @@ function mergeSessionTitles(rows, observations) {
|
|
|
27112
27224
|
const titles = /* @__PURE__ */ new Map();
|
|
27113
27225
|
for (const observation of observations) {
|
|
27114
27226
|
if (observation.status !== "fulfilled") continue;
|
|
27115
|
-
const title = observation.value?.title?.title
|
|
27227
|
+
const title = observation.value?.title?.title;
|
|
27116
27228
|
if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
|
|
27117
27229
|
}
|
|
27118
27230
|
return rows.map((row) => titles.has(row.id) ? {
|
|
@@ -27253,10 +27365,10 @@ function formatRate(n) {
|
|
|
27253
27365
|
/**
|
|
27254
27366
|
* Cache-hit share of billed prompt-side input.
|
|
27255
27367
|
* @param usage - cumulative token totals.
|
|
27256
|
-
* @returns rounded
|
|
27368
|
+
* @returns percent rounded to one decimal place, or null when no input was billed.
|
|
27257
27369
|
*/
|
|
27258
27370
|
function cacheHitPercent(usage) {
|
|
27259
|
-
return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens *
|
|
27371
|
+
return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 1e3) / 10;
|
|
27260
27372
|
}
|
|
27261
27373
|
/** Separator between trailing state spans. */
|
|
27262
27374
|
const STATUS_ITEM_SEPARATOR = " · ";
|
|
@@ -27965,13 +28077,20 @@ function toolDetailLines(detail, columns) {
|
|
|
27965
28077
|
default: return detail;
|
|
27966
28078
|
}
|
|
27967
28079
|
}
|
|
28080
|
+
/** Default compact tool-card window used while the Ctrl+R fold is closed. */
|
|
28081
|
+
const DEFAULT_TOOL_ROWS = 3;
|
|
28082
|
+
/** Keep the invocation visible while making hidden tool output discoverable. */
|
|
28083
|
+
function compactToolLines(lines, columns) {
|
|
28084
|
+
if (lines.length <= DEFAULT_TOOL_ROWS) return lines;
|
|
28085
|
+
return [...lines.slice(0, 2), ...textLines(" … output hidden · Ctrl+R", columns, "dim").slice(0, 1)];
|
|
28086
|
+
}
|
|
27968
28087
|
/**
|
|
27969
28088
|
* Convert one durable transcript entry to its complete scrollable row model.
|
|
27970
28089
|
* The source entry stays intact; only the caller's visible slice is rendered.
|
|
27971
28090
|
* Wrapped continuations keep a hanging indent aligned under each row's
|
|
27972
28091
|
* content (Codex history-cell alignment) instead of resetting to column 0.
|
|
27973
28092
|
*/
|
|
27974
|
-
function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
|
|
28093
|
+
function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true, showToolDetails = showReasoning) {
|
|
27975
28094
|
const width = Math.max(1, Math.floor(columns));
|
|
27976
28095
|
switch (entry.kind) {
|
|
27977
28096
|
case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
|
|
@@ -27993,7 +28112,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
|
|
|
27993
28112
|
const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
|
|
27994
28113
|
const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
|
|
27995
28114
|
const summaryStyle = entry.state === "error" ? "error" : "dim";
|
|
27996
|
-
|
|
28115
|
+
const lines = [
|
|
27997
28116
|
...hangingStyledLines([
|
|
27998
28117
|
lineSegment(`[${entry.ordinal}] `, "dim"),
|
|
27999
28118
|
lineSegment(entry.name, "brand"),
|
|
@@ -28003,6 +28122,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
|
|
|
28003
28122
|
...entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle),
|
|
28004
28123
|
...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
|
|
28005
28124
|
];
|
|
28125
|
+
return showToolDetails ? lines : compactToolLines(lines, width);
|
|
28006
28126
|
}
|
|
28007
28127
|
case "command": {
|
|
28008
28128
|
const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
|
|
@@ -28020,7 +28140,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
|
|
|
28020
28140
|
}
|
|
28021
28141
|
/** Settled-history variant carrying the Ctrl+R reasoning fold. */
|
|
28022
28142
|
function settledEntryLines(entry, columns, showReasoning) {
|
|
28023
|
-
return transcriptEntryLines(entry, columns, showReasoning, false);
|
|
28143
|
+
return transcriptEntryLines(entry, columns, showReasoning, false, showReasoning);
|
|
28024
28144
|
}
|
|
28025
28145
|
//#endregion
|
|
28026
28146
|
//#region src/render/editor.ts
|
|
@@ -28034,9 +28154,8 @@ function settledEntryLines(entry, columns, showReasoning) {
|
|
|
28034
28154
|
* grapheme boundaries) so the React state stays two primitives
|
|
28035
28155
|
* (value, cursor) and every operation here stays pure and testable.
|
|
28036
28156
|
*
|
|
28037
|
-
* Word motion
|
|
28038
|
-
*
|
|
28039
|
-
* single word (two hanzi are one Alt+B step, not two).
|
|
28157
|
+
* Word motion follows Codex's piece semantics: whitespace separates runs,
|
|
28158
|
+
* punctuation runs stay atomic, and each Han grapheme is its own boundary.
|
|
28040
28159
|
*
|
|
28041
28160
|
* @module @deepseek-ai/dsh-code/render/editor
|
|
28042
28161
|
*/
|
|
@@ -28189,6 +28308,28 @@ function editorModel(value, columns) {
|
|
|
28189
28308
|
length: value.length
|
|
28190
28309
|
};
|
|
28191
28310
|
}
|
|
28311
|
+
/** Split one row around the authoritative caret; every other row stays whole. */
|
|
28312
|
+
function editorRowParts(row, rowIndex, caretRow, cursor, caretEnabled = true) {
|
|
28313
|
+
if (!caretEnabled || rowIndex !== caretRow) return {
|
|
28314
|
+
before: "",
|
|
28315
|
+
caret: "",
|
|
28316
|
+
after: row.text,
|
|
28317
|
+
hasCaret: false
|
|
28318
|
+
};
|
|
28319
|
+
const at = row.offsets.indexOf(cursor);
|
|
28320
|
+
if (at < 0) return {
|
|
28321
|
+
before: "",
|
|
28322
|
+
caret: "",
|
|
28323
|
+
after: row.text,
|
|
28324
|
+
hasCaret: false
|
|
28325
|
+
};
|
|
28326
|
+
return {
|
|
28327
|
+
before: at > 0 ? row.text.slice(0, row.cuts[at]) : "",
|
|
28328
|
+
caret: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at], row.cuts[at + 1]) : " ",
|
|
28329
|
+
after: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at + 1]) : "",
|
|
28330
|
+
hasCaret: true
|
|
28331
|
+
};
|
|
28332
|
+
}
|
|
28192
28333
|
/** Map a cursor offset to its caret site on the wrapped rows. */
|
|
28193
28334
|
function caretSite(model, offset) {
|
|
28194
28335
|
const target = Math.max(0, Math.min(model.length, offset));
|
|
@@ -28213,7 +28354,9 @@ function caretSite(model, offset) {
|
|
|
28213
28354
|
*/
|
|
28214
28355
|
function moveCursorVertically(model, offset, preferredColumn, delta) {
|
|
28215
28356
|
const target = caretSite(model, offset).row + delta;
|
|
28216
|
-
if (
|
|
28357
|
+
if (delta === 0) return offset;
|
|
28358
|
+
if (target < 0) return 0;
|
|
28359
|
+
if (target >= model.rows.length) return model.length;
|
|
28217
28360
|
const row = model.rows[target];
|
|
28218
28361
|
const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
|
|
28219
28362
|
let best = 0;
|
|
@@ -28233,24 +28376,28 @@ function lineBounds(value, offset) {
|
|
|
28233
28376
|
}
|
|
28234
28377
|
/** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
|
|
28235
28378
|
const WORD_SEPARATORS = /* @__PURE__ */ new Set("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?");
|
|
28379
|
+
const HAN_GRAPHEME = /^\p{Script=Han}(?:\p{Mark}|\uFE0F)*$/u;
|
|
28380
|
+
const UNICODE_PUNCTUATION = /^\p{P}+$/u;
|
|
28236
28381
|
function classifyGrapheme(text) {
|
|
28237
28382
|
if (/^\s$/u.test(text)) return "space";
|
|
28238
|
-
return WORD_SEPARATORS.has(text) ? "punct" : "word";
|
|
28383
|
+
return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
|
|
28239
28384
|
}
|
|
28240
|
-
/** Maximal same-class runs
|
|
28385
|
+
/** Maximal same-class runs; Han graphemes deliberately stay one run each. */
|
|
28241
28386
|
function pieceRuns(value) {
|
|
28242
28387
|
const runs = [];
|
|
28243
28388
|
let current;
|
|
28244
28389
|
for (const span of splitGraphemes(value)) {
|
|
28245
28390
|
const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
|
|
28246
|
-
|
|
28391
|
+
const atomic = klass === "word" && HAN_GRAPHEME.test(span.text);
|
|
28392
|
+
if (current !== void 0 && current.class === klass && !current.atomic && !atomic) {
|
|
28247
28393
|
current.end = span.end;
|
|
28248
28394
|
continue;
|
|
28249
28395
|
}
|
|
28250
28396
|
current = {
|
|
28251
28397
|
start: span.start,
|
|
28252
28398
|
end: span.end,
|
|
28253
|
-
class: klass
|
|
28399
|
+
class: klass,
|
|
28400
|
+
atomic
|
|
28254
28401
|
};
|
|
28255
28402
|
runs.push(current);
|
|
28256
28403
|
}
|
|
@@ -28261,10 +28408,9 @@ function pieceRuns(value) {
|
|
|
28261
28408
|
* START of the trailing non-space piece (extending over separator pieces).
|
|
28262
28409
|
*/
|
|
28263
28410
|
function moveWordLeft(value, offset) {
|
|
28264
|
-
const cursor =
|
|
28411
|
+
const cursor = clampCursor(value, offset);
|
|
28265
28412
|
const runs = pieceRuns(value);
|
|
28266
|
-
let index = runs.
|
|
28267
|
-
while (index >= 0 && runs[index].end > cursor) index -= 1;
|
|
28413
|
+
let index = runs.findLastIndex((run) => run.start < cursor);
|
|
28268
28414
|
if (index < 0) return 0;
|
|
28269
28415
|
if (runs[index].class === "space") {
|
|
28270
28416
|
index -= 1;
|
|
@@ -28279,10 +28425,9 @@ function moveWordLeft(value, offset) {
|
|
|
28279
28425
|
* the leading non-space piece (extending over separator pieces).
|
|
28280
28426
|
*/
|
|
28281
28427
|
function moveWordRight(value, offset) {
|
|
28282
|
-
const cursor =
|
|
28428
|
+
const cursor = clampCursor(value, offset);
|
|
28283
28429
|
const runs = pieceRuns(value);
|
|
28284
|
-
let index =
|
|
28285
|
-
while (index < runs.length && runs[index].start < cursor) index += 1;
|
|
28430
|
+
let index = runs.findIndex((run) => run.end > cursor);
|
|
28286
28431
|
if (index >= runs.length) return value.length;
|
|
28287
28432
|
if (runs[index].class === "space") {
|
|
28288
28433
|
index += 1;
|
|
@@ -28371,6 +28516,62 @@ function insertText(value, cursor, text) {
|
|
|
28371
28516
|
killed: void 0
|
|
28372
28517
|
};
|
|
28373
28518
|
}
|
|
28519
|
+
/** Ctrl+A: current logical line start, then the previous line start on repeat. */
|
|
28520
|
+
function moveToLineStart(value, cursor, crossOnRepeat) {
|
|
28521
|
+
const site = clampCursor(value, cursor);
|
|
28522
|
+
const bounds = lineBounds(value, site);
|
|
28523
|
+
if (!crossOnRepeat || site !== bounds.start || bounds.start === 0) return bounds.start;
|
|
28524
|
+
return lineBounds(value, bounds.start - 1).start;
|
|
28525
|
+
}
|
|
28526
|
+
/** Ctrl+E: current logical line end, then the next line end on repeat. */
|
|
28527
|
+
function moveToLineEnd(value, cursor, crossOnRepeat) {
|
|
28528
|
+
const site = clampCursor(value, cursor);
|
|
28529
|
+
const bounds = lineBounds(value, site);
|
|
28530
|
+
if (!crossOnRepeat || site !== bounds.end || bounds.end >= value.length) return bounds.end;
|
|
28531
|
+
return lineBounds(value, bounds.end + 1).end;
|
|
28532
|
+
}
|
|
28533
|
+
/**
|
|
28534
|
+
* Remap a captured range when all intervening edits are wholly before or
|
|
28535
|
+
* wholly after it. An edit overlapping either boundary invalidates the
|
|
28536
|
+
* anchor instead of guessing and inserting content at a surprising place.
|
|
28537
|
+
*/
|
|
28538
|
+
function remapStableRange(original, current, range) {
|
|
28539
|
+
const start = Math.max(0, Math.min(original.length, range.start));
|
|
28540
|
+
const end = Math.max(start, Math.min(original.length, range.end));
|
|
28541
|
+
if (original === current) return {
|
|
28542
|
+
start,
|
|
28543
|
+
end
|
|
28544
|
+
};
|
|
28545
|
+
let prefix = 0;
|
|
28546
|
+
const shared = Math.min(original.length, current.length);
|
|
28547
|
+
while (prefix < shared && original[prefix] === current[prefix]) prefix += 1;
|
|
28548
|
+
let suffix = 0;
|
|
28549
|
+
while (suffix < original.length - prefix && suffix < current.length - prefix && original[original.length - 1 - suffix] === current[current.length - 1 - suffix]) suffix += 1;
|
|
28550
|
+
const oldChangedEnd = original.length - suffix;
|
|
28551
|
+
if (prefix >= end) return {
|
|
28552
|
+
start,
|
|
28553
|
+
end
|
|
28554
|
+
};
|
|
28555
|
+
if (oldChangedEnd <= start) {
|
|
28556
|
+
const delta = current.length - original.length;
|
|
28557
|
+
return {
|
|
28558
|
+
start: start + delta,
|
|
28559
|
+
end: end + delta
|
|
28560
|
+
};
|
|
28561
|
+
}
|
|
28562
|
+
}
|
|
28563
|
+
/** Replace a current range while preserving a cursor moved after capture. */
|
|
28564
|
+
function replaceRangePreservingCursor(value, cursor, range, replacement) {
|
|
28565
|
+
const start = Math.max(0, Math.min(value.length, range.start));
|
|
28566
|
+
const end = Math.max(start, Math.min(value.length, range.end));
|
|
28567
|
+
const site = clampCursor(value, cursor);
|
|
28568
|
+
const nextCursor = site <= start ? site : site >= end ? site + replacement.length - (end - start) : start + replacement.length;
|
|
28569
|
+
return {
|
|
28570
|
+
value: value.slice(0, start) + replacement + value.slice(end),
|
|
28571
|
+
cursor: nextCursor,
|
|
28572
|
+
killed: void 0
|
|
28573
|
+
};
|
|
28574
|
+
}
|
|
28374
28575
|
/**
|
|
28375
28576
|
* Composer editor row budget: the editor itself never grows past this many
|
|
28376
28577
|
* physical rows; deeper drafts scroll internally to keep the caret visible.
|
|
@@ -28380,14 +28581,73 @@ function composerMaxRows(terminalRows) {
|
|
|
28380
28581
|
return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
|
|
28381
28582
|
}
|
|
28382
28583
|
/**
|
|
28383
|
-
*
|
|
28384
|
-
*
|
|
28385
|
-
*
|
|
28584
|
+
* History navigation starts with Up on an empty draft, or after visual
|
|
28585
|
+
* movement has reached the directional text edge of an unchanged recalled
|
|
28586
|
+
* entry. Every other position remains under textarea movement.
|
|
28386
28587
|
*/
|
|
28387
|
-
function shouldRecallNavigate(value, cursor, lastRecalled) {
|
|
28388
|
-
if (value === "") return
|
|
28389
|
-
if (
|
|
28390
|
-
return
|
|
28588
|
+
function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
|
|
28589
|
+
if (value === "") return direction < 0;
|
|
28590
|
+
if (lastRecalled !== value) return false;
|
|
28591
|
+
return direction < 0 ? cursor === 0 : cursor === value.length;
|
|
28592
|
+
}
|
|
28593
|
+
//#endregion
|
|
28594
|
+
//#region src/keyboard.ts
|
|
28595
|
+
/**
|
|
28596
|
+
* Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
|
|
28597
|
+
* kitty CSI-u normalization layer.
|
|
28598
|
+
*
|
|
28599
|
+
* The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
|
|
28600
|
+
* and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
|
|
28601
|
+
* deliberately NOT requested: Ink 5's parser cannot decode the
|
|
28602
|
+
* `:event-type` suffix, and repeat/release reporting buys this surface
|
|
28603
|
+
* nothing.
|
|
28604
|
+
*
|
|
28605
|
+
* Ink 5 also cannot parse most CSI-u forms at all — they fall through its
|
|
28606
|
+
* regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
|
|
28607
|
+
* stdin read patch therefore rewrites every CSI-u form it can decode back
|
|
28608
|
+
* to the legacy byte or canonical sequence the existing key handling
|
|
28609
|
+
* already understands, before Ink ever parses the chunk.
|
|
28610
|
+
* @module @deepseek-ai/dsh-code/keyboard
|
|
28611
|
+
*/
|
|
28612
|
+
/** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
|
|
28613
|
+
const KEYBOARD_ENHANCE_ENABLE = "\x1B[>4;0m\x1B[>5u";
|
|
28614
|
+
/** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
|
|
28615
|
+
const KEYBOARD_ENHANCE_DISABLE = "\x1B[<u\x1B[>4;0m";
|
|
28616
|
+
const DSH_ENABLE_KEYBOARD_ENHANCEMENT = "DSH_ENABLE_KEYBOARD_ENHANCEMENT";
|
|
28617
|
+
function parseBooleanEnv(value) {
|
|
28618
|
+
if (value === void 0) return void 0;
|
|
28619
|
+
const normalized = value.trim().toLowerCase();
|
|
28620
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes") return true;
|
|
28621
|
+
if (normalized === "0" || normalized === "false" || normalized === "no") return false;
|
|
28622
|
+
}
|
|
28623
|
+
/** True when the process is running inside the VS Code integrated terminal. */
|
|
28624
|
+
function isVsCodeTerminalEnv(env = process.env) {
|
|
28625
|
+
return env.TERM_PROGRAM?.trim().toLowerCase() === "vscode" || env.VSCODE_INJECTION === "1";
|
|
28626
|
+
}
|
|
28627
|
+
/** Whether to push Kitty keyboard enhancement for the current terminal. */
|
|
28628
|
+
function shouldEnableKeyboardEnhancement(env = process.env) {
|
|
28629
|
+
if (parseBooleanEnv(env["DSH_DISABLE_KEYBOARD_ENHANCEMENT"]) === true) return false;
|
|
28630
|
+
const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT]);
|
|
28631
|
+
if (explicitEnable !== void 0) return explicitEnable;
|
|
28632
|
+
return !isVsCodeTerminalEnv(env);
|
|
28633
|
+
}
|
|
28634
|
+
/** Enable bracketed paste reporting. */
|
|
28635
|
+
const BRACKETED_PASTE_ENABLE = "\x1B[?2004h";
|
|
28636
|
+
/** Disable bracketed paste reporting. */
|
|
28637
|
+
const BRACKETED_PASTE_DISABLE = "\x1B[?2004l";
|
|
28638
|
+
/** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
|
|
28639
|
+
const TERMINAL_FOCUS_REPORT_ENABLE = "\x1B[?1004h";
|
|
28640
|
+
/** Disable terminal focus-in/focus-out reporting. */
|
|
28641
|
+
const TERMINAL_FOCUS_REPORT_DISABLE = "\x1B[?1004l";
|
|
28642
|
+
/**
|
|
28643
|
+
* Remove xterm focus reports from one input chunk and update the caller's
|
|
28644
|
+
* focus state. Focus reports are terminal protocol, not composer text.
|
|
28645
|
+
*/
|
|
28646
|
+
function stripTerminalFocusEvents(chunk, onFocus) {
|
|
28647
|
+
return chunk.replace(/\x1b\[(I|O)/gu, (_whole, event) => {
|
|
28648
|
+
onFocus(event === "I");
|
|
28649
|
+
return "";
|
|
28650
|
+
});
|
|
28391
28651
|
}
|
|
28392
28652
|
/** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
|
|
28393
28653
|
const PASTE_START_MARKER = "[200~";
|
|
@@ -28398,7 +28658,7 @@ const PASTE_END_MARKER = "[201~";
|
|
|
28398
28658
|
* "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
|
|
28399
28659
|
*/
|
|
28400
28660
|
function stripPasteMarkers(text) {
|
|
28401
|
-
return text.replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
|
|
28661
|
+
return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
|
|
28402
28662
|
}
|
|
28403
28663
|
/** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
|
|
28404
28664
|
const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u";
|
|
@@ -28409,7 +28669,6 @@ function legacyForKey(key) {
|
|
|
28409
28669
|
const alt = (bits & 2) !== 0;
|
|
28410
28670
|
const ctrl = (bits & 4) !== 0;
|
|
28411
28671
|
if (key.code === 13) {
|
|
28412
|
-
if (shift) return "\x1B[13;2u";
|
|
28413
28672
|
if (ctrl) return "\n";
|
|
28414
28673
|
if (alt) return "\x1B\r";
|
|
28415
28674
|
return "\r";
|
|
@@ -28460,6 +28719,98 @@ function normalizeKeyboardChunk(chunk) {
|
|
|
28460
28719
|
}) ?? whole;
|
|
28461
28720
|
});
|
|
28462
28721
|
}
|
|
28722
|
+
const HOME_SEQUENCES = [
|
|
28723
|
+
"\x1B[H",
|
|
28724
|
+
"\x1B[1~",
|
|
28725
|
+
"\x1B[7~",
|
|
28726
|
+
"\x1BOH"
|
|
28727
|
+
];
|
|
28728
|
+
const END_SEQUENCES = [
|
|
28729
|
+
"\x1B[F",
|
|
28730
|
+
"\x1B[4~",
|
|
28731
|
+
"\x1B[8~",
|
|
28732
|
+
"\x1BOF"
|
|
28733
|
+
];
|
|
28734
|
+
/** Parse one CSI functional-key sequence at `offset`. */
|
|
28735
|
+
function functionalToken(chunk, offset) {
|
|
28736
|
+
const tail = chunk.slice(offset);
|
|
28737
|
+
for (const sequence of HOME_SEQUENCES) if (tail.startsWith(sequence)) return {
|
|
28738
|
+
token: { kind: "home" },
|
|
28739
|
+
length: sequence.length
|
|
28740
|
+
};
|
|
28741
|
+
for (const sequence of END_SEQUENCES) if (tail.startsWith(sequence)) return {
|
|
28742
|
+
token: { kind: "end" },
|
|
28743
|
+
length: sequence.length
|
|
28744
|
+
};
|
|
28745
|
+
const modifiedHome = /^\x1b\[1;(\d+)H/u.exec(tail);
|
|
28746
|
+
if (modifiedHome !== null) return {
|
|
28747
|
+
token: { kind: "home" },
|
|
28748
|
+
length: modifiedHome[0].length
|
|
28749
|
+
};
|
|
28750
|
+
const modifiedEnd = /^\x1b\[1;(\d+)F/u.exec(tail);
|
|
28751
|
+
if (modifiedEnd !== null) return {
|
|
28752
|
+
token: { kind: "end" },
|
|
28753
|
+
length: modifiedEnd[0].length
|
|
28754
|
+
};
|
|
28755
|
+
const modifiedDelete = /^\x1b\[3(?:;(\d+))?~/u.exec(tail);
|
|
28756
|
+
if (modifiedDelete !== null) {
|
|
28757
|
+
const modifiers = Number.parseInt(modifiedDelete[1] ?? "1", 10) - 1;
|
|
28758
|
+
return {
|
|
28759
|
+
token: { kind: (modifiers & 2) !== 0 || (modifiers & 4) !== 0 ? "delete-word-forward" : "delete-forward" },
|
|
28760
|
+
length: modifiedDelete[0].length
|
|
28761
|
+
};
|
|
28762
|
+
}
|
|
28763
|
+
}
|
|
28764
|
+
/**
|
|
28765
|
+
* Tokenize a stdin chunk containing at least one editor-only key. Ink calls
|
|
28766
|
+
* `useInput` once for a pasted/batched chunk, so preserving each action here
|
|
28767
|
+
* prevents repeated Backspace/Home/End/Delete presses from collapsing into
|
|
28768
|
+
* one blurred key event. Unknown escape sequences return undefined and stay
|
|
28769
|
+
* under Ink's ownership.
|
|
28770
|
+
*/
|
|
28771
|
+
function tokenizeRawEditorChunk(chunk) {
|
|
28772
|
+
const tokens = [];
|
|
28773
|
+
let text = "";
|
|
28774
|
+
let special = false;
|
|
28775
|
+
const flushText = () => {
|
|
28776
|
+
if (text === "") return;
|
|
28777
|
+
tokens.push({
|
|
28778
|
+
kind: "text",
|
|
28779
|
+
text
|
|
28780
|
+
});
|
|
28781
|
+
text = "";
|
|
28782
|
+
};
|
|
28783
|
+
for (let offset = 0; offset < chunk.length;) {
|
|
28784
|
+
if (chunk.startsWith("\x1B", offset) || chunk.startsWith("\x1B\b", offset)) {
|
|
28785
|
+
flushText();
|
|
28786
|
+
tokens.push({ kind: "delete-word-backward" });
|
|
28787
|
+
special = true;
|
|
28788
|
+
offset += 2;
|
|
28789
|
+
continue;
|
|
28790
|
+
}
|
|
28791
|
+
const functional = functionalToken(chunk, offset);
|
|
28792
|
+
if (functional !== void 0) {
|
|
28793
|
+
flushText();
|
|
28794
|
+
tokens.push(functional.token);
|
|
28795
|
+
special = true;
|
|
28796
|
+
offset += functional.length;
|
|
28797
|
+
continue;
|
|
28798
|
+
}
|
|
28799
|
+
const char = chunk[offset];
|
|
28800
|
+
if (char === "" || char === "\b") {
|
|
28801
|
+
flushText();
|
|
28802
|
+
tokens.push({ kind: "delete-backward" });
|
|
28803
|
+
special = true;
|
|
28804
|
+
offset += 1;
|
|
28805
|
+
continue;
|
|
28806
|
+
}
|
|
28807
|
+
if (char === "\x1B") return void 0;
|
|
28808
|
+
text += char;
|
|
28809
|
+
offset += 1;
|
|
28810
|
+
}
|
|
28811
|
+
flushText();
|
|
28812
|
+
return special ? tokens : void 0;
|
|
28813
|
+
}
|
|
28463
28814
|
//#endregion
|
|
28464
28815
|
//#region src/kernel-panels.ts
|
|
28465
28816
|
/** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
|
|
@@ -29446,6 +29797,511 @@ function recallNewer(state) {
|
|
|
29446
29797
|
};
|
|
29447
29798
|
}
|
|
29448
29799
|
//#endregion
|
|
29800
|
+
//#region src/authorization.ts
|
|
29801
|
+
/** Terminal adapter over the Harness provider-authorization and credential-record seams. */
|
|
29802
|
+
/** Record scope used by the upstream pi-ai adapter for provider logins. */
|
|
29803
|
+
const PI_AI_RECORD_SCOPE = "llm-pi-ai";
|
|
29804
|
+
/** Load only model-provider flows; unrelated future authorization domains stay out of `/model`. */
|
|
29805
|
+
async function loadProviderAuthorizations(ctx) {
|
|
29806
|
+
const authorization = ctx.get("authorization");
|
|
29807
|
+
const credentials = ctx.get("credentials");
|
|
29808
|
+
if (authorization === void 0 || credentials === void 0) return {
|
|
29809
|
+
rows: [],
|
|
29810
|
+
failures: []
|
|
29811
|
+
};
|
|
29812
|
+
const entries = authorization.list().filter((entry) => credentialKeyScope(entry.key) === PI_AI_RECORD_SCOPE);
|
|
29813
|
+
const failures = [];
|
|
29814
|
+
return {
|
|
29815
|
+
rows: await Promise.all(entries.map(async (entry) => {
|
|
29816
|
+
let record;
|
|
29817
|
+
try {
|
|
29818
|
+
record = await credentials.describeRecord(entry.key);
|
|
29819
|
+
} catch (error) {
|
|
29820
|
+
failures.push(`${entry.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
29821
|
+
record = {
|
|
29822
|
+
configured: false,
|
|
29823
|
+
writable: false
|
|
29824
|
+
};
|
|
29825
|
+
}
|
|
29826
|
+
return {
|
|
29827
|
+
key: entry.key,
|
|
29828
|
+
provider: credentialKeyId(entry.key),
|
|
29829
|
+
label: entry.label,
|
|
29830
|
+
methods: entry.methods,
|
|
29831
|
+
inFlight: entry.inFlight,
|
|
29832
|
+
record
|
|
29833
|
+
};
|
|
29834
|
+
})),
|
|
29835
|
+
failures
|
|
29836
|
+
};
|
|
29837
|
+
}
|
|
29838
|
+
/** Subscribe to login settlement and credential-record changes. */
|
|
29839
|
+
function subscribeProviderAuthorizations(ctx, listener) {
|
|
29840
|
+
const settled = ctx.on("authorization/settled", (key) => {
|
|
29841
|
+
if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
|
|
29842
|
+
});
|
|
29843
|
+
const records = ctx.on("credentials/record-updated", (key) => {
|
|
29844
|
+
if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
|
|
29845
|
+
});
|
|
29846
|
+
return () => {
|
|
29847
|
+
settled();
|
|
29848
|
+
records();
|
|
29849
|
+
};
|
|
29850
|
+
}
|
|
29851
|
+
/** Begin one provider login through the interaction surface owned by the caller. */
|
|
29852
|
+
async function beginProviderAuthorization(ctx, row, method, interaction, signal) {
|
|
29853
|
+
const authorization = ctx.get("authorization");
|
|
29854
|
+
if (authorization === void 0) throw new Error("provider login is unavailable in this profile");
|
|
29855
|
+
return (await authorization.begin({
|
|
29856
|
+
key: row.key,
|
|
29857
|
+
method,
|
|
29858
|
+
interaction,
|
|
29859
|
+
signal
|
|
29860
|
+
})).status;
|
|
29861
|
+
}
|
|
29862
|
+
/** Cancel the attempt currently serving this provider, if any. */
|
|
29863
|
+
function cancelProviderAuthorization(ctx, key) {
|
|
29864
|
+
ctx.get("authorization")?.cancel(key);
|
|
29865
|
+
}
|
|
29866
|
+
/** Remove an authorization record without changing the provider's settings profile. */
|
|
29867
|
+
async function logoutProviderAuthorization(ctx, row) {
|
|
29868
|
+
const credentials = ctx.get("credentials");
|
|
29869
|
+
if (credentials === void 0) throw new Error("credential storage is unavailable in this profile");
|
|
29870
|
+
const current = await credentials.describeRecord(row.key);
|
|
29871
|
+
if (!current.configured) return;
|
|
29872
|
+
if (!current.writable) throw new Error("this login record is read-only");
|
|
29873
|
+
await credentials.deleteRecord(row.key);
|
|
29874
|
+
}
|
|
29875
|
+
/** Open an authorization URL with the platform default browser, without invoking a shell. */
|
|
29876
|
+
function openAuthorizationUrl(raw) {
|
|
29877
|
+
let url;
|
|
29878
|
+
try {
|
|
29879
|
+
url = new URL(raw);
|
|
29880
|
+
} catch {
|
|
29881
|
+
return false;
|
|
29882
|
+
}
|
|
29883
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
|
29884
|
+
const target = url.toString();
|
|
29885
|
+
try {
|
|
29886
|
+
const child = process.platform === "win32" ? spawn("explorer.exe", [target], {
|
|
29887
|
+
detached: true,
|
|
29888
|
+
stdio: "ignore"
|
|
29889
|
+
}) : process.platform === "darwin" ? spawn("/usr/bin/open", [target], {
|
|
29890
|
+
detached: true,
|
|
29891
|
+
stdio: "ignore"
|
|
29892
|
+
}) : spawn("xdg-open", [target], {
|
|
29893
|
+
detached: true,
|
|
29894
|
+
stdio: "ignore"
|
|
29895
|
+
});
|
|
29896
|
+
child.once("error", () => {});
|
|
29897
|
+
child.unref();
|
|
29898
|
+
return true;
|
|
29899
|
+
} catch {
|
|
29900
|
+
return false;
|
|
29901
|
+
}
|
|
29902
|
+
}
|
|
29903
|
+
/** Compact value-free status for the provider list. */
|
|
29904
|
+
function providerAuthorizationStatus(row) {
|
|
29905
|
+
if (row === void 0) return "login unavailable";
|
|
29906
|
+
if (row.inFlight) return "login in progress";
|
|
29907
|
+
if (!row.record.configured) return "not logged in";
|
|
29908
|
+
return row.record.kind === "grant" ? "OAuth" : "interactive API key";
|
|
29909
|
+
}
|
|
29910
|
+
/** Find a provider's login flow from a previously loaded directory. */
|
|
29911
|
+
function authorizationForProvider(directory, provider) {
|
|
29912
|
+
return directory?.rows.find((row) => row.provider === provider);
|
|
29913
|
+
}
|
|
29914
|
+
//#endregion
|
|
29915
|
+
//#region src/authorization-panel.ts
|
|
29916
|
+
/** Bounded Ink surfaces for provider login and logout. */
|
|
29917
|
+
/** Run one upstream authorization flow without letting notices or prompts exceed the panel budget. */
|
|
29918
|
+
function ProviderAuthorizationPanel(props) {
|
|
29919
|
+
const stdout = useStdout().stdout;
|
|
29920
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
29921
|
+
const [phase, setPhase] = (0, import_react.useState)("methods");
|
|
29922
|
+
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
29923
|
+
const [notices, setNotices] = (0, import_react.useState)([]);
|
|
29924
|
+
const [prompt, setPrompt] = (0, import_react.useState)(void 0);
|
|
29925
|
+
const [draft, setDraft] = (0, import_react.useState)("");
|
|
29926
|
+
const [promptCursor, setPromptCursor] = (0, import_react.useState)(0);
|
|
29927
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
29928
|
+
const [copyState, setCopyState] = (0, import_react.useState)(void 0);
|
|
29929
|
+
const controllerRef = (0, import_react.useRef)(void 0);
|
|
29930
|
+
const replyRef = (0, import_react.useRef)(void 0);
|
|
29931
|
+
const openedUrls = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
29932
|
+
const clearReply = () => {
|
|
29933
|
+
replyRef.current?.detach();
|
|
29934
|
+
replyRef.current = void 0;
|
|
29935
|
+
setPrompt(void 0);
|
|
29936
|
+
setDraft("");
|
|
29937
|
+
setPromptCursor(0);
|
|
29938
|
+
};
|
|
29939
|
+
const decline = () => {
|
|
29940
|
+
const reply = replyRef.current;
|
|
29941
|
+
clearReply();
|
|
29942
|
+
reply?.reject(new AuthorizationDeclinedError());
|
|
29943
|
+
};
|
|
29944
|
+
const stop = () => {
|
|
29945
|
+
controllerRef.current?.abort();
|
|
29946
|
+
controllerRef.current = void 0;
|
|
29947
|
+
props.cancel(props.row.key);
|
|
29948
|
+
decline();
|
|
29949
|
+
};
|
|
29950
|
+
(0, import_react.useEffect)(() => () => {
|
|
29951
|
+
controllerRef.current?.abort();
|
|
29952
|
+
props.cancel(props.row.key);
|
|
29953
|
+
const reply = replyRef.current;
|
|
29954
|
+
replyRef.current = void 0;
|
|
29955
|
+
reply?.detach();
|
|
29956
|
+
reply?.reject(new AuthorizationDeclinedError());
|
|
29957
|
+
}, [props.row.key]);
|
|
29958
|
+
const start = (method) => {
|
|
29959
|
+
setPhase("running");
|
|
29960
|
+
setError(void 0);
|
|
29961
|
+
setNotices([]);
|
|
29962
|
+
setCopyState(void 0);
|
|
29963
|
+
const controller = new AbortController();
|
|
29964
|
+
controllerRef.current = controller;
|
|
29965
|
+
props.begin(props.row, method, {
|
|
29966
|
+
notify(notice) {
|
|
29967
|
+
setNotices((current) => [...current.slice(-19), notice]);
|
|
29968
|
+
if (notice.url !== void 0 && !openedUrls.current.has(notice.url)) {
|
|
29969
|
+
openedUrls.current.add(notice.url);
|
|
29970
|
+
props.openUrl(notice.url);
|
|
29971
|
+
}
|
|
29972
|
+
},
|
|
29973
|
+
prompt(next) {
|
|
29974
|
+
return new Promise((resolve, reject) => {
|
|
29975
|
+
const onWithdraw = () => {
|
|
29976
|
+
if (replyRef.current?.reject !== reject) return;
|
|
29977
|
+
clearReply();
|
|
29978
|
+
reject(/* @__PURE__ */ new Error("authorization prompt was withdrawn"));
|
|
29979
|
+
};
|
|
29980
|
+
next.signal?.addEventListener("abort", onWithdraw, { once: true });
|
|
29981
|
+
replyRef.current = {
|
|
29982
|
+
resolve,
|
|
29983
|
+
reject,
|
|
29984
|
+
detach: () => next.signal?.removeEventListener("abort", onWithdraw)
|
|
29985
|
+
};
|
|
29986
|
+
setPrompt(next);
|
|
29987
|
+
setDraft("");
|
|
29988
|
+
setPromptCursor(0);
|
|
29989
|
+
});
|
|
29990
|
+
}
|
|
29991
|
+
}, controller.signal).then((status) => {
|
|
29992
|
+
controllerRef.current = void 0;
|
|
29993
|
+
clearReply();
|
|
29994
|
+
if (status === "authorized") props.done();
|
|
29995
|
+
else props.back();
|
|
29996
|
+
}, (reason) => {
|
|
29997
|
+
controllerRef.current = void 0;
|
|
29998
|
+
clearReply();
|
|
29999
|
+
if (controller.signal.aborted) {
|
|
30000
|
+
props.back();
|
|
30001
|
+
return;
|
|
30002
|
+
}
|
|
30003
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
30004
|
+
setPhase("methods");
|
|
30005
|
+
});
|
|
30006
|
+
};
|
|
30007
|
+
const answer = (value) => {
|
|
30008
|
+
const reply = replyRef.current;
|
|
30009
|
+
clearReply();
|
|
30010
|
+
reply?.resolve(value);
|
|
30011
|
+
};
|
|
30012
|
+
useInput((input, key) => {
|
|
30013
|
+
if (phase === "methods") {
|
|
30014
|
+
if (key.escape || input === "q") {
|
|
30015
|
+
props.back();
|
|
30016
|
+
return;
|
|
30017
|
+
}
|
|
30018
|
+
if (props.row.methods.length === 0) return;
|
|
30019
|
+
if (key.upArrow) {
|
|
30020
|
+
setCursor((current) => (current + props.row.methods.length - 1) % props.row.methods.length);
|
|
30021
|
+
return;
|
|
30022
|
+
}
|
|
30023
|
+
if (key.downArrow) {
|
|
30024
|
+
setCursor((current) => (current + 1) % props.row.methods.length);
|
|
30025
|
+
return;
|
|
30026
|
+
}
|
|
30027
|
+
if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0].id);
|
|
30028
|
+
return;
|
|
30029
|
+
}
|
|
30030
|
+
if (key.escape) {
|
|
30031
|
+
stop();
|
|
30032
|
+
props.back();
|
|
30033
|
+
return;
|
|
30034
|
+
}
|
|
30035
|
+
const copyValue = notices.at(-1)?.code ?? notices.at(-1)?.url;
|
|
30036
|
+
if ((input === "c" || input === "C") && copyValue !== void 0) {
|
|
30037
|
+
props.copy(copyValue).then(() => setCopyState("copied"), (reason) => setCopyState(`copy failed: ${reason instanceof Error ? reason.message : String(reason)}`));
|
|
30038
|
+
return;
|
|
30039
|
+
}
|
|
30040
|
+
if (prompt === void 0) return;
|
|
30041
|
+
if (prompt.kind === "select") {
|
|
30042
|
+
if (prompt.options.length === 0) return;
|
|
30043
|
+
if (key.upArrow) {
|
|
30044
|
+
setPromptCursor((current) => (current + prompt.options.length - 1) % prompt.options.length);
|
|
30045
|
+
return;
|
|
30046
|
+
}
|
|
30047
|
+
if (key.downArrow) {
|
|
30048
|
+
setPromptCursor((current) => (current + 1) % prompt.options.length);
|
|
30049
|
+
return;
|
|
30050
|
+
}
|
|
30051
|
+
if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id);
|
|
30052
|
+
return;
|
|
30053
|
+
}
|
|
30054
|
+
if (key.backspace || key.delete) {
|
|
30055
|
+
setDraft((current) => [...current].slice(0, -1).join(""));
|
|
30056
|
+
return;
|
|
30057
|
+
}
|
|
30058
|
+
if (key.return) {
|
|
30059
|
+
if (draft.trim() !== "") answer(draft);
|
|
30060
|
+
return;
|
|
30061
|
+
}
|
|
30062
|
+
if (input !== "" && !key.ctrl && !key.meta) setDraft((current) => current + input);
|
|
30063
|
+
});
|
|
30064
|
+
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
30065
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider login · esc cancel", viewport.contentColumns));
|
|
30066
|
+
const rows = [];
|
|
30067
|
+
if (phase === "methods") {
|
|
30068
|
+
if (error !== void 0) rows.push({
|
|
30069
|
+
key: "error",
|
|
30070
|
+
text: ` ${singleLineText(error)}`,
|
|
30071
|
+
color: inkColor(getPalette().error)
|
|
30072
|
+
});
|
|
30073
|
+
props.row.methods.forEach((method, index) => {
|
|
30074
|
+
rows.push({
|
|
30075
|
+
key: method.id,
|
|
30076
|
+
text: `${index === cursor ? "› " : " "}${displayText(method.label)}`,
|
|
30077
|
+
color: inkColor(index === cursor ? getPalette().brandBright : getPalette().dim)
|
|
30078
|
+
});
|
|
30079
|
+
});
|
|
30080
|
+
} else {
|
|
30081
|
+
notices.forEach((notice, index) => {
|
|
30082
|
+
rows.push({
|
|
30083
|
+
key: `notice-${index}`,
|
|
30084
|
+
text: ` ${displayText(notice.message)}`
|
|
30085
|
+
});
|
|
30086
|
+
if (notice.url !== void 0) rows.push({
|
|
30087
|
+
key: `url-${index}`,
|
|
30088
|
+
text: ` ${displayText(notice.url)}`,
|
|
30089
|
+
color: inkColor(getPalette().brandBright)
|
|
30090
|
+
});
|
|
30091
|
+
if (notice.code !== void 0) rows.push({
|
|
30092
|
+
key: `code-${index}`,
|
|
30093
|
+
text: ` code ${displayText(notice.code)}`,
|
|
30094
|
+
color: inkColor(getPalette().success),
|
|
30095
|
+
bold: true
|
|
30096
|
+
});
|
|
30097
|
+
});
|
|
30098
|
+
if (prompt !== void 0) {
|
|
30099
|
+
rows.push({
|
|
30100
|
+
key: "prompt",
|
|
30101
|
+
text: ` ${displayText(prompt.message)}`,
|
|
30102
|
+
color: inkColor(getPalette().brandBright)
|
|
30103
|
+
});
|
|
30104
|
+
if (prompt.kind === "select") prompt.options.forEach((option, index) => rows.push({
|
|
30105
|
+
key: `option-${option.id}`,
|
|
30106
|
+
text: `${index === promptCursor ? "› " : " "}${displayText(option.label)}${option.description === void 0 ? "" : ` · ${displayText(option.description)}`}`,
|
|
30107
|
+
color: inkColor(index === promptCursor ? getPalette().brandBright : getPalette().dim)
|
|
30108
|
+
}));
|
|
30109
|
+
else {
|
|
30110
|
+
const shown = prompt.kind === "secret" ? "•".repeat([...draft].length) : displayText(draft);
|
|
30111
|
+
rows.push({
|
|
30112
|
+
key: "draft",
|
|
30113
|
+
text: ` ${shown}▏`,
|
|
30114
|
+
color: inkColor(getPalette().text)
|
|
30115
|
+
});
|
|
30116
|
+
}
|
|
30117
|
+
} else rows.push({
|
|
30118
|
+
key: "waiting",
|
|
30119
|
+
text: " waiting for provider…",
|
|
30120
|
+
color: inkColor(getPalette().dim)
|
|
30121
|
+
});
|
|
30122
|
+
if (copyState !== void 0) rows.push({
|
|
30123
|
+
key: "copy",
|
|
30124
|
+
text: ` ${singleLineText(copyState)}`,
|
|
30125
|
+
color: inkColor(copyState === "copied" ? getPalette().success : getPalette().error)
|
|
30126
|
+
});
|
|
30127
|
+
}
|
|
30128
|
+
const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows));
|
|
30129
|
+
const footer = phase === "methods" ? "↑↓ choose · enter continue · esc/q back" : "enter answer · c copy URL/code · esc cancel login";
|
|
30130
|
+
return (0, import_react.createElement)(Box, {
|
|
30131
|
+
flexDirection: "column",
|
|
30132
|
+
width: viewport.outerColumns,
|
|
30133
|
+
paddingX: 1,
|
|
30134
|
+
borderStyle: "round",
|
|
30135
|
+
borderColor: inkColor(getPalette().brand)
|
|
30136
|
+
}, (0, import_react.createElement)(Text, {
|
|
30137
|
+
color: inkColor(getPalette().brand),
|
|
30138
|
+
bold: true,
|
|
30139
|
+
wrap: "truncate-end"
|
|
30140
|
+
}, truncateColumns(`/model · login ${displayText(props.row.label)}`, viewport.contentColumns)), ...visible.map((row) => (0, import_react.createElement)(Text, {
|
|
30141
|
+
key: row.key,
|
|
30142
|
+
color: row.color,
|
|
30143
|
+
bold: row.bold,
|
|
30144
|
+
wrap: "truncate-end"
|
|
30145
|
+
}, truncateColumns(row.text, viewport.contentColumns))), (0, import_react.createElement)(Text, {
|
|
30146
|
+
color: inkColor(getPalette().dim),
|
|
30147
|
+
wrap: "truncate-end"
|
|
30148
|
+
}, truncateColumns(footer, viewport.contentColumns)));
|
|
30149
|
+
}
|
|
30150
|
+
function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
|
|
30151
|
+
const stdout = useStdout().stdout;
|
|
30152
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
30153
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
30154
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
30155
|
+
useInput((input, key) => {
|
|
30156
|
+
if (busy) return;
|
|
30157
|
+
if (key.escape || input === "n" || input === "N") {
|
|
30158
|
+
back();
|
|
30159
|
+
return;
|
|
30160
|
+
}
|
|
30161
|
+
if (input !== "y" && input !== "Y") return;
|
|
30162
|
+
setBusy(true);
|
|
30163
|
+
setError(void 0);
|
|
30164
|
+
confirm(row).then(done, (reason) => {
|
|
30165
|
+
setBusy(false);
|
|
30166
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
30167
|
+
});
|
|
30168
|
+
});
|
|
30169
|
+
if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
|
|
30170
|
+
if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("y logout · n/esc back", viewport.contentColumns));
|
|
30171
|
+
return (0, import_react.createElement)(Box, {
|
|
30172
|
+
flexDirection: "column",
|
|
30173
|
+
width: viewport.outerColumns,
|
|
30174
|
+
paddingX: 1,
|
|
30175
|
+
borderStyle: "round",
|
|
30176
|
+
borderColor: inkColor(getPalette().warn)
|
|
30177
|
+
}, (0, import_react.createElement)(Text, {
|
|
30178
|
+
color: inkColor(getPalette().warn),
|
|
30179
|
+
bold: true,
|
|
30180
|
+
wrap: "truncate-end"
|
|
30181
|
+
}, 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, {
|
|
30182
|
+
color: inkColor(getPalette().dim),
|
|
30183
|
+
wrap: "truncate-end"
|
|
30184
|
+
}, truncateColumns(" provider endpoint and model configuration stay unchanged", viewport.contentColumns)), error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
|
|
30185
|
+
color: inkColor(getPalette().error),
|
|
30186
|
+
wrap: "truncate-end"
|
|
30187
|
+
}, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)), (0, import_react.createElement)(Text, {
|
|
30188
|
+
color: inkColor(getPalette().dim),
|
|
30189
|
+
wrap: "truncate-end"
|
|
30190
|
+
}, truncateColumns(busy ? "working…" : "y confirm · n/esc back", viewport.contentColumns)));
|
|
30191
|
+
}
|
|
30192
|
+
//#endregion
|
|
30193
|
+
//#region src/attachments.ts
|
|
30194
|
+
/** Terminal image-file adapter over the Harness durable attachment service. */
|
|
30195
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
30196
|
+
".png",
|
|
30197
|
+
".jpg",
|
|
30198
|
+
".jpeg",
|
|
30199
|
+
".webp",
|
|
30200
|
+
".gif"
|
|
30201
|
+
]);
|
|
30202
|
+
/** Detect the supported encoded raster formats from bytes, never from a path suffix. */
|
|
30203
|
+
function detectImageMediaType(data) {
|
|
30204
|
+
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";
|
|
30205
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
30206
|
+
if (data.length >= 6) {
|
|
30207
|
+
const signature = String.fromCharCode(...data.subarray(0, 6));
|
|
30208
|
+
if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
|
|
30209
|
+
}
|
|
30210
|
+
if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
|
|
30211
|
+
}
|
|
30212
|
+
/** Whether a path-like token is worth probing as an image attachment. */
|
|
30213
|
+
function looksLikeImagePath(path) {
|
|
30214
|
+
return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
30215
|
+
}
|
|
30216
|
+
/** Parse a terminal paste/drop containing only one or more image paths. */
|
|
30217
|
+
function parsePastedImagePaths(input) {
|
|
30218
|
+
const text = input.trim();
|
|
30219
|
+
if (text === "") return [];
|
|
30220
|
+
const tokens = [];
|
|
30221
|
+
for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
|
|
30222
|
+
const token = match[1] ?? match[2] ?? match[3];
|
|
30223
|
+
if (token === void 0) continue;
|
|
30224
|
+
let path = token;
|
|
30225
|
+
if (path.startsWith("file://")) try {
|
|
30226
|
+
path = fileURLToPath(path);
|
|
30227
|
+
} catch {
|
|
30228
|
+
return [];
|
|
30229
|
+
}
|
|
30230
|
+
if (!looksLikeImagePath(path)) return [];
|
|
30231
|
+
tokens.push(path);
|
|
30232
|
+
}
|
|
30233
|
+
return tokens;
|
|
30234
|
+
}
|
|
30235
|
+
/** Validate path, byte size and encoded signature without writing an attachment object. */
|
|
30236
|
+
async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
|
|
30237
|
+
if (paths.length === 0) return [];
|
|
30238
|
+
if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
|
|
30239
|
+
if (paths.length > attachments.imageLimits.maxImagesPerMessage) throw new Error(`too many images (${paths.length}; limit ${attachments.imageLimits.maxImagesPerMessage})`);
|
|
30240
|
+
const inspected = [];
|
|
30241
|
+
let totalBytes = 0;
|
|
30242
|
+
for (const raw of paths) {
|
|
30243
|
+
const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
|
|
30244
|
+
let facts;
|
|
30245
|
+
try {
|
|
30246
|
+
facts = await stat(path);
|
|
30247
|
+
} catch (error) {
|
|
30248
|
+
throw new Error(`cannot read image "${raw}": ${error instanceof Error ? error.message : String(error)}`);
|
|
30249
|
+
}
|
|
30250
|
+
if (!facts.isFile()) throw new Error(`image path is not a file: "${raw}"`);
|
|
30251
|
+
if (facts.size > attachments.imageLimits.maxImageBytes) throw new Error(`image "${basename(path)}" is ${facts.size} bytes; limit ${attachments.imageLimits.maxImageBytes}`);
|
|
30252
|
+
totalBytes += facts.size;
|
|
30253
|
+
if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error(`image batch is ${totalBytes} bytes; limit ${attachments.imageLimits.maxMessageImageBytes}`);
|
|
30254
|
+
const handle = await open(path, "r");
|
|
30255
|
+
try {
|
|
30256
|
+
const signature = /* @__PURE__ */ new Uint8Array(16);
|
|
30257
|
+
const { bytesRead } = await handle.read(signature, 0, signature.length, 0);
|
|
30258
|
+
const mediaType = detectImageMediaType(signature.subarray(0, bytesRead));
|
|
30259
|
+
if (mediaType === void 0 || !attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`unsupported image file "${raw}" (expected PNG, JPEG, WebP, or GIF)`);
|
|
30260
|
+
inspected.push({
|
|
30261
|
+
path,
|
|
30262
|
+
name: basename(path),
|
|
30263
|
+
mediaType,
|
|
30264
|
+
bytes: facts.size
|
|
30265
|
+
});
|
|
30266
|
+
} finally {
|
|
30267
|
+
await handle.close();
|
|
30268
|
+
}
|
|
30269
|
+
}
|
|
30270
|
+
return inspected;
|
|
30271
|
+
}
|
|
30272
|
+
/** Read, validate, and persist an ordered image path list as model content blocks. */
|
|
30273
|
+
async function saveImagePaths(paths, attachments, signal) {
|
|
30274
|
+
if (paths.length === 0) return [];
|
|
30275
|
+
if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
|
|
30276
|
+
const checkCancelled = () => {
|
|
30277
|
+
if (signal?.aborted === true) throw new Error("image submission cancelled");
|
|
30278
|
+
};
|
|
30279
|
+
const inputs = [];
|
|
30280
|
+
for (const path of paths) {
|
|
30281
|
+
checkCancelled();
|
|
30282
|
+
let data;
|
|
30283
|
+
try {
|
|
30284
|
+
data = await readFile(path);
|
|
30285
|
+
} catch (error) {
|
|
30286
|
+
throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
|
|
30287
|
+
}
|
|
30288
|
+
const mediaType = detectImageMediaType(data);
|
|
30289
|
+
if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
|
|
30290
|
+
inputs.push({
|
|
30291
|
+
data,
|
|
30292
|
+
mediaType,
|
|
30293
|
+
name: basename(path)
|
|
30294
|
+
});
|
|
30295
|
+
}
|
|
30296
|
+
checkCancelled();
|
|
30297
|
+
const refs = await attachments.saveImages(inputs);
|
|
30298
|
+
checkCancelled();
|
|
30299
|
+
return refs.map((attachment) => ({
|
|
30300
|
+
type: "image",
|
|
30301
|
+
attachment
|
|
30302
|
+
}));
|
|
30303
|
+
}
|
|
30304
|
+
//#endregion
|
|
29449
30305
|
//#region src/app.ts
|
|
29450
30306
|
/**
|
|
29451
30307
|
* The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
|
|
@@ -29490,20 +30346,121 @@ function readSettledRowCap() {
|
|
|
29490
30346
|
const PASTE_BRACKET_TIMEOUT_MS = 1e3;
|
|
29491
30347
|
/** Release the held frame after Ink has replayed the source-backed Static rows. */
|
|
29492
30348
|
const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
|
|
30349
|
+
/** One source of truth for TUI-owned slash commands in completion and `/help`. */
|
|
30350
|
+
const LOCAL_COMMANDS = [
|
|
30351
|
+
{
|
|
30352
|
+
label: "/help",
|
|
30353
|
+
description: "show this overlay"
|
|
30354
|
+
},
|
|
30355
|
+
{
|
|
30356
|
+
label: "/model",
|
|
30357
|
+
description: "switch the model and manage providers"
|
|
30358
|
+
},
|
|
30359
|
+
{
|
|
30360
|
+
label: "/effort",
|
|
30361
|
+
description: "adjust reasoning effort for the current model"
|
|
30362
|
+
},
|
|
30363
|
+
{
|
|
30364
|
+
label: "/mode",
|
|
30365
|
+
description: "inspect or select the agent preset (/mode [preset])"
|
|
30366
|
+
},
|
|
30367
|
+
{
|
|
30368
|
+
label: "/permission",
|
|
30369
|
+
description: "inspect or select the permission preset (/permission [preset])"
|
|
30370
|
+
},
|
|
30371
|
+
{
|
|
30372
|
+
label: "/new",
|
|
30373
|
+
description: "create and switch to a fresh session (/new [preset])"
|
|
30374
|
+
},
|
|
30375
|
+
{
|
|
30376
|
+
label: "/fork",
|
|
30377
|
+
description: "fork at the latest completed turn (/fork [event-seq])"
|
|
30378
|
+
},
|
|
30379
|
+
{
|
|
30380
|
+
label: "/resume",
|
|
30381
|
+
description: "browse or switch root sessions (/resume [id|prefix])"
|
|
30382
|
+
},
|
|
30383
|
+
{
|
|
30384
|
+
label: "/plugin",
|
|
30385
|
+
description: "inspect the live plugin composition"
|
|
30386
|
+
},
|
|
30387
|
+
{
|
|
30388
|
+
label: "/jobs",
|
|
30389
|
+
description: "inspect background jobs"
|
|
30390
|
+
},
|
|
30391
|
+
{
|
|
30392
|
+
label: "/statusline",
|
|
30393
|
+
description: "customize the status line items"
|
|
30394
|
+
},
|
|
30395
|
+
{
|
|
30396
|
+
label: "/theme",
|
|
30397
|
+
description: "switch the color theme"
|
|
30398
|
+
},
|
|
30399
|
+
{
|
|
30400
|
+
label: "/history",
|
|
30401
|
+
description: "search and recall past prompts"
|
|
30402
|
+
},
|
|
30403
|
+
{
|
|
30404
|
+
label: "/agents",
|
|
30405
|
+
description: "inspect subagent sessions of this conversation"
|
|
30406
|
+
},
|
|
30407
|
+
{
|
|
30408
|
+
label: "/todos",
|
|
30409
|
+
description: "inspect the full todo list"
|
|
30410
|
+
},
|
|
30411
|
+
{
|
|
30412
|
+
label: "/subagent",
|
|
30413
|
+
description: "choose the model delegated subagents run on"
|
|
30414
|
+
},
|
|
30415
|
+
{
|
|
30416
|
+
label: "/delete",
|
|
30417
|
+
description: "delete a session and its subagent threads"
|
|
30418
|
+
},
|
|
30419
|
+
{
|
|
30420
|
+
label: "/clear",
|
|
30421
|
+
description: "clear the screen"
|
|
30422
|
+
},
|
|
30423
|
+
{
|
|
30424
|
+
label: "/export",
|
|
30425
|
+
description: "export the transcript to markdown (/export [path])"
|
|
30426
|
+
},
|
|
30427
|
+
{
|
|
30428
|
+
label: "/title",
|
|
30429
|
+
description: "rename this session (/title <text>)"
|
|
30430
|
+
},
|
|
30431
|
+
{
|
|
30432
|
+
label: "/copy",
|
|
30433
|
+
description: "copy the latest assistant response"
|
|
30434
|
+
},
|
|
30435
|
+
{
|
|
30436
|
+
label: "/diff",
|
|
30437
|
+
description: "inspect Git changes (/diff [--staged|ref])"
|
|
30438
|
+
},
|
|
30439
|
+
{
|
|
30440
|
+
label: "/review",
|
|
30441
|
+
description: "review Git changes under read-only permissions"
|
|
30442
|
+
},
|
|
30443
|
+
{
|
|
30444
|
+
label: "/quit",
|
|
30445
|
+
description: "exit"
|
|
30446
|
+
}
|
|
30447
|
+
];
|
|
30448
|
+
const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map((command) => command.label.slice(1)));
|
|
29493
30449
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
29494
30450
|
function padColumns(text, width) {
|
|
29495
30451
|
const clipped = truncateColumns(singleLineText(text), width);
|
|
29496
30452
|
return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
|
|
29497
30453
|
}
|
|
29498
30454
|
/** Interval-driven frame counter for one self-contained animated leaf. */
|
|
29499
|
-
function useFrames(intervalMs) {
|
|
30455
|
+
function useFrames(intervalMs, active = true) {
|
|
29500
30456
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
29501
30457
|
(0, import_react.useEffect)(() => {
|
|
30458
|
+
if (!active) return;
|
|
29502
30459
|
const id = setInterval(() => setTick((current) => current + 1), intervalMs);
|
|
29503
30460
|
return () => {
|
|
29504
30461
|
clearInterval(id);
|
|
29505
30462
|
};
|
|
29506
|
-
}, [intervalMs]);
|
|
30463
|
+
}, [active, intervalMs]);
|
|
29507
30464
|
return tick;
|
|
29508
30465
|
}
|
|
29509
30466
|
/**
|
|
@@ -29520,12 +30477,7 @@ function useStableInput(handler, active) {
|
|
|
29520
30477
|
}, []);
|
|
29521
30478
|
useInput(stableHandler, { isActive: active });
|
|
29522
30479
|
}
|
|
29523
|
-
/**
|
|
29524
|
-
* The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
|
|
29525
|
-
* ring trail clockwise around the eight outer positions (8 frames × 125ms =
|
|
29526
|
-
* the web's 1s cycle). Replaces the plain busy ellipsis as the composer's
|
|
29527
|
-
* prompt marker and leads the Deep-diving line.
|
|
29528
|
-
*/
|
|
30480
|
+
/** The original web StateDot chase used by the busy composer marker. */
|
|
29529
30481
|
function BusyChase() {
|
|
29530
30482
|
const tick = useFrames(125);
|
|
29531
30483
|
return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
|
|
@@ -29535,21 +30487,46 @@ function Caret() {
|
|
|
29535
30487
|
const tick = useFrames(530);
|
|
29536
30488
|
return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
|
|
29537
30489
|
}
|
|
29538
|
-
/**
|
|
29539
|
-
function
|
|
29540
|
-
const
|
|
29541
|
-
|
|
30490
|
+
/** One resettable input-caret phase shared by the entire composer. */
|
|
30491
|
+
function useCursorBlink(active) {
|
|
30492
|
+
const [epoch, setEpoch] = (0, import_react.useState)(0);
|
|
30493
|
+
const [visible, setVisible] = (0, import_react.useState)(true);
|
|
30494
|
+
(0, import_react.useEffect)(() => {
|
|
30495
|
+
setVisible(true);
|
|
30496
|
+
if (!active) return;
|
|
30497
|
+
const id = setInterval(() => setVisible((current) => !current), 530);
|
|
30498
|
+
return () => {
|
|
30499
|
+
clearInterval(id);
|
|
30500
|
+
};
|
|
30501
|
+
}, [active, epoch]);
|
|
30502
|
+
return {
|
|
30503
|
+
visible,
|
|
30504
|
+
reset: (0, import_react.useCallback)(() => {
|
|
30505
|
+
setVisible(true);
|
|
30506
|
+
setEpoch((current) => current + 1);
|
|
30507
|
+
}, [])
|
|
30508
|
+
};
|
|
29542
30509
|
}
|
|
29543
30510
|
/**
|
|
29544
|
-
* The busy line, web TurnStatus contract:
|
|
29545
|
-
* `Deep diving...` label, with the elapsed clock appended
|
|
29546
|
-
* has clearly been running (15s) — anchored to `turn/start`
|
|
29547
|
-
* mid-turn keeps the real time.
|
|
30511
|
+
* The busy line, web TurnStatus contract: a continuously moving blue gradient
|
|
30512
|
+
* paints the complete `Deep diving...` label, with the elapsed clock appended
|
|
30513
|
+
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
30514
|
+
* so a resumed mid-turn keeps the real time.
|
|
29548
30515
|
*/
|
|
29549
30516
|
function DeepDivingLine({ since }) {
|
|
29550
|
-
useFrames(
|
|
30517
|
+
const tick = useFrames(33);
|
|
29551
30518
|
const elapsed = since === 0 ? 0 : Date.now() - since;
|
|
29552
|
-
|
|
30519
|
+
const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
|
|
30520
|
+
const palette = getPalette();
|
|
30521
|
+
const graphemes = splitGraphemes(text);
|
|
30522
|
+
return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
|
|
30523
|
+
const sparkle = grapheme.text === "✻";
|
|
30524
|
+
return (0, import_react.createElement)(Text, {
|
|
30525
|
+
key: `${grapheme.start}-${grapheme.end}`,
|
|
30526
|
+
color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
|
|
30527
|
+
bold: sparkle || void 0
|
|
30528
|
+
}, grapheme.text);
|
|
30529
|
+
}));
|
|
29553
30530
|
}
|
|
29554
30531
|
/**
|
|
29555
30532
|
* The streaming buffer rendered with a hard size cap: the live region must
|
|
@@ -30544,7 +31521,8 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
30544
31521
|
wrap: "truncate-end"
|
|
30545
31522
|
}, 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
31523
|
const index = rows.indexOf(row);
|
|
30547
|
-
const
|
|
31524
|
+
const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
|
|
31525
|
+
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
|
|
30548
31526
|
return (0, import_react.createElement)(Text, {
|
|
30549
31527
|
key: `${row.provider}/${row.model}`,
|
|
30550
31528
|
color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
@@ -30567,7 +31545,7 @@ function providerStateLabel(row) {
|
|
|
30567
31545
|
return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
|
|
30568
31546
|
}
|
|
30569
31547
|
/** The provider-management stage reached from /model with `a`. */
|
|
30570
|
-
function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, onRemove, onRetry, onBack }) {
|
|
31548
|
+
function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }) {
|
|
30571
31549
|
const stdout = useStdout().stdout;
|
|
30572
31550
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
30573
31551
|
const rows = directory?.rows ?? [];
|
|
@@ -30630,6 +31608,19 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
|
|
|
30630
31608
|
else onRemove(target);
|
|
30631
31609
|
return;
|
|
30632
31610
|
}
|
|
31611
|
+
const authorization = authorizationForProvider(authorizations, target.provider);
|
|
31612
|
+
if (input === "l" || input === "L") {
|
|
31613
|
+
if (authorization === void 0) setActionError("this provider offers no interactive login flow");
|
|
31614
|
+
else if (authorization.inFlight) setActionError("a login attempt is already running for this provider");
|
|
31615
|
+
else onLogin(target, authorization);
|
|
31616
|
+
return;
|
|
31617
|
+
}
|
|
31618
|
+
if (input === "o" || input === "O") {
|
|
31619
|
+
if (authorization === void 0 || !authorization.record.configured) setActionError("this provider has no login record to remove");
|
|
31620
|
+
else if (!authorization.record.writable) setActionError("this login record is read-only");
|
|
31621
|
+
else onLogout(target, authorization);
|
|
31622
|
+
return;
|
|
31623
|
+
}
|
|
30633
31624
|
if (key.return) {
|
|
30634
31625
|
if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
|
|
30635
31626
|
else if (target.credential?.kind === "error") setActionError("credential status is unavailable; retry before writing");
|
|
@@ -30659,6 +31650,16 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
|
|
|
30659
31650
|
color: inkColor(getPalette().warn),
|
|
30660
31651
|
wrap: "truncate-end"
|
|
30661
31652
|
}, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
|
|
31653
|
+
...authorizationError === void 0 ? [] : [(0, import_react.createElement)(Text, {
|
|
31654
|
+
key: "authorization-error",
|
|
31655
|
+
color: inkColor(getPalette().warn),
|
|
31656
|
+
wrap: "truncate-end"
|
|
31657
|
+
}, truncateColumns(` login status unavailable: ${singleLineText(authorizationError)}`, viewport.contentColumns))],
|
|
31658
|
+
...(authorizations?.failures ?? []).map((failure, index) => (0, import_react.createElement)(Text, {
|
|
31659
|
+
key: `authorization-failure-${index}`,
|
|
31660
|
+
color: inkColor(getPalette().warn),
|
|
31661
|
+
wrap: "truncate-end"
|
|
31662
|
+
}, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
|
|
30662
31663
|
...rows.length === 0 ? [(0, import_react.createElement)(Text, {
|
|
30663
31664
|
key: "empty",
|
|
30664
31665
|
color: inkColor(getPalette().dim),
|
|
@@ -30680,7 +31681,10 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
|
|
|
30680
31681
|
wrap: "truncate-end"
|
|
30681
31682
|
}, 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
31683
|
const index = rows.indexOf(row);
|
|
30683
|
-
const
|
|
31684
|
+
const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`;
|
|
31685
|
+
const authorization = authorizationForProvider(authorizations, row.provider);
|
|
31686
|
+
const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? ` · ${providerAuthorizationStatus(authorization)}` : "";
|
|
31687
|
+
const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? " · custom" : ""}`;
|
|
30684
31688
|
return (0, import_react.createElement)(Text, {
|
|
30685
31689
|
key: row.provider,
|
|
30686
31690
|
color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
@@ -30689,7 +31693,7 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
|
|
|
30689
31693
|
}), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
30690
31694
|
color: inkColor(getPalette().dim),
|
|
30691
31695
|
wrap: "truncate-end"
|
|
30692
|
-
}, truncateColumns("↑↓ move ·
|
|
31696
|
+
}, truncateColumns("↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
|
|
30693
31697
|
}
|
|
30694
31698
|
/** Provider configuration editor: only explicit models are written to settings. */
|
|
30695
31699
|
function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
|
|
@@ -31002,7 +32006,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
|
31002
32006
|
key: "key-submit",
|
|
31003
32007
|
dimColor: true,
|
|
31004
32008
|
wrap: "truncate-end"
|
|
31005
|
-
}, " enter submit ·
|
|
32009
|
+
}, " enter submit · up/down history · tab complete"),
|
|
31006
32010
|
(0, import_react.createElement)(Text, {
|
|
31007
32011
|
key: "key-mentions",
|
|
31008
32012
|
dimColor: true,
|
|
@@ -31039,31 +32043,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
|
31039
32043
|
color: inkColor(getPalette().error),
|
|
31040
32044
|
wrap: "truncate-end"
|
|
31041
32045
|
}, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
|
|
31042
|
-
(0, import_react.createElement)(Box, { key:
|
|
31043
|
-
(0, import_react.createElement)(
|
|
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, {
|
|
32046
|
+
...LOCAL_COMMANDS.map((command) => (0, import_react.createElement)(Box, { key: `local-${command.label.slice(1)}` }, row(command.label, command.description))),
|
|
32047
|
+
...descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => (0, import_react.createElement)(Text, {
|
|
31067
32048
|
key: `command-${descriptor.name}`,
|
|
31068
32049
|
dimColor: true,
|
|
31069
32050
|
wrap: "truncate-end"
|
|
@@ -31124,49 +32105,6 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
|
|
|
31124
32105
|
function verboseLine(text, columns) {
|
|
31125
32106
|
return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
|
|
31126
32107
|
}
|
|
31127
|
-
/** Identify one whole-chunk key sequence Ink drops or blurs. */
|
|
31128
|
-
function annotateRawKey(chunk) {
|
|
31129
|
-
switch (chunk) {
|
|
31130
|
-
case "": return "delete-backward";
|
|
31131
|
-
case "\x1B":
|
|
31132
|
-
case "\x1B\b": return "delete-word-backward";
|
|
31133
|
-
case "\x1B[3~":
|
|
31134
|
-
case "\x1B[3;2~": return "delete-forward";
|
|
31135
|
-
case "\x1B[3;3~":
|
|
31136
|
-
case "\x1B[3;5~": return "delete-word-forward";
|
|
31137
|
-
case "\x1B[H":
|
|
31138
|
-
case "\x1B[1~":
|
|
31139
|
-
case "\x1B[7~":
|
|
31140
|
-
case "\x1BOH": return "home";
|
|
31141
|
-
case "\x1B[F":
|
|
31142
|
-
case "\x1B[4~":
|
|
31143
|
-
case "\x1B[8~":
|
|
31144
|
-
case "\x1BOF": return "end";
|
|
31145
|
-
default: return;
|
|
31146
|
-
}
|
|
31147
|
-
}
|
|
31148
|
-
/**
|
|
31149
|
-
* One-row editor window keeping the logical cursor visible in long drafts.
|
|
31150
|
-
* The caret and its surroundings slice at grapheme boundaries: splitting a
|
|
31151
|
-
* star-plane surrogate pair would render an isolated half under the block
|
|
31152
|
-
* caret with a width the terminal never draws.
|
|
31153
|
-
*/
|
|
31154
|
-
function editorWindow(value, cursor, columns) {
|
|
31155
|
-
const width = Math.max(1, columns);
|
|
31156
|
-
const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
|
|
31157
|
-
const site = clampCursor(value, cursor);
|
|
31158
|
-
const caretSpan = splitGraphemes(value).find((span) => span.start === site);
|
|
31159
|
-
const caret = caretSpan === void 0 ? " " : normalize(caretSpan.text);
|
|
31160
|
-
const rest = value.slice(caretSpan === void 0 ? site : caretSpan.end);
|
|
31161
|
-
const remaining = Math.max(0, width - visibleColumns(caret));
|
|
31162
|
-
const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)));
|
|
31163
|
-
const beforeBudget = Math.max(0, remaining - afterBudget);
|
|
31164
|
-
return {
|
|
31165
|
-
before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text,
|
|
31166
|
-
caret,
|
|
31167
|
-
after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
|
|
31168
|
-
};
|
|
31169
|
-
}
|
|
31170
32108
|
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
31171
32109
|
const COMPOSER_PLACEHOLDER = "type a message · / commands · @ mentions";
|
|
31172
32110
|
/** Adjacent cells with identical styling merge into one styled Text span. */
|
|
@@ -31332,130 +32270,11 @@ const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
|
|
|
31332
32270
|
function completionCandidates(value, descriptors, skills) {
|
|
31333
32271
|
if (!value.startsWith("/")) return [];
|
|
31334
32272
|
const prefix = value.slice(1).split(" ")[0] ?? "";
|
|
31335
|
-
const local =
|
|
31336
|
-
|
|
31337
|
-
|
|
31338
|
-
|
|
31339
|
-
|
|
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) => ({
|
|
32273
|
+
const local = LOCAL_COMMANDS.map((command) => ({
|
|
32274
|
+
...command,
|
|
32275
|
+
origin: "command"
|
|
32276
|
+
}));
|
|
32277
|
+
const registry = descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => ({
|
|
31459
32278
|
label: `/${descriptor.name}`,
|
|
31460
32279
|
description: descriptor.description,
|
|
31461
32280
|
origin: "command"
|
|
@@ -31534,25 +32353,52 @@ function CompletionMenu({ active, mention, index, rows }) {
|
|
|
31534
32353
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
31535
32354
|
* box passes every key through untouched.
|
|
31536
32355
|
*/
|
|
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 }) {
|
|
32356
|
+
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
32357
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
32358
|
+
const editorColumns = Math.max(1, columns - 6);
|
|
31539
32359
|
const stdin = useStdin().stdin;
|
|
32360
|
+
const focusReporting = isVsCodeTerminalEnv();
|
|
31540
32361
|
const [value, setValue] = (0, import_react.useState)("");
|
|
31541
32362
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
32363
|
+
const valueRef = (0, import_react.useRef)(value);
|
|
32364
|
+
const cursorRef = (0, import_react.useRef)(cursor);
|
|
32365
|
+
valueRef.current = value;
|
|
32366
|
+
cursorRef.current = cursor;
|
|
32367
|
+
const [draftImages, setDraftImages] = (0, import_react.useState)([]);
|
|
32368
|
+
const draftImagesRef = (0, import_react.useRef)(draftImages);
|
|
32369
|
+
draftImagesRef.current = draftImages;
|
|
32370
|
+
const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
|
|
32371
|
+
const prepareAbortRef = (0, import_react.useRef)(void 0);
|
|
32372
|
+
const prepareEpochRef = (0, import_react.useRef)(0);
|
|
32373
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
|
|
32374
|
+
(0, import_react.useEffect)(() => () => {
|
|
32375
|
+
prepareEpochRef.current += 1;
|
|
32376
|
+
prepareAbortRef.current?.abort();
|
|
32377
|
+
}, []);
|
|
31542
32378
|
const killRef = (0, import_react.useRef)("");
|
|
31543
32379
|
const preferredColumnRef = (0, import_react.useRef)(null);
|
|
31544
32380
|
const editorScrollRef = (0, import_react.useRef)(0);
|
|
31545
32381
|
const pasteBracketRef = (0, import_react.useRef)(false);
|
|
31546
32382
|
/** Cancels the pending lost-paste safety timer (undefined when disarmed). */
|
|
31547
32383
|
const pasteBracketCancelRef = (0, import_react.useRef)(void 0);
|
|
31548
|
-
/**
|
|
31549
|
-
const
|
|
32384
|
+
/** Ordered editor tokens from the stdin chunk Ink is about to deliver. */
|
|
32385
|
+
const rawEditorTokens = (0, import_react.useRef)(void 0);
|
|
32386
|
+
/** VS Code focus state from xterm focus-report events; starts focused. */
|
|
32387
|
+
const terminalFocusedRef = (0, import_react.useRef)(true);
|
|
31550
32388
|
const recall = (0, import_react.useRef)(beginRecall([], ""));
|
|
32389
|
+
(0, import_react.useEffect)(() => {
|
|
32390
|
+
preferredColumnRef.current = null;
|
|
32391
|
+
}, [editorColumns]);
|
|
31551
32392
|
(0, import_react.useEffect)(() => {
|
|
31552
32393
|
if (historyFill === void 0) return;
|
|
31553
32394
|
const safe = sanitizeDraftText(historyFill.text);
|
|
32395
|
+
draftImagesRef.current = [];
|
|
32396
|
+
setDraftImages([]);
|
|
32397
|
+
valueRef.current = safe;
|
|
32398
|
+
cursorRef.current = safe.length;
|
|
31554
32399
|
setValue(safe);
|
|
31555
32400
|
setCursor(safe.length);
|
|
32401
|
+
resetCursorBlink();
|
|
31556
32402
|
preferredColumnRef.current = null;
|
|
31557
32403
|
setDismissedMenuValue(void 0);
|
|
31558
32404
|
recall.current = {
|
|
@@ -31565,8 +32411,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31565
32411
|
}, [
|
|
31566
32412
|
historyFill,
|
|
31567
32413
|
recallSpace,
|
|
31568
|
-
historyConsumed
|
|
32414
|
+
historyConsumed,
|
|
32415
|
+
resetCursorBlink
|
|
31569
32416
|
]);
|
|
32417
|
+
(0, import_react.useEffect)(() => {
|
|
32418
|
+
setDraftImages((current) => {
|
|
32419
|
+
const next = current.filter((image) => value.includes(image.marker));
|
|
32420
|
+
draftImagesRef.current = next;
|
|
32421
|
+
return next.length === current.length ? current : next;
|
|
32422
|
+
});
|
|
32423
|
+
}, [value]);
|
|
31570
32424
|
(0, import_react.useEffect)(() => {
|
|
31571
32425
|
if (stdin === void 0) return;
|
|
31572
32426
|
const originalRead = stdin.read.bind(stdin);
|
|
@@ -31574,13 +32428,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31574
32428
|
const chunk = originalRead(...args);
|
|
31575
32429
|
if (chunk === null) return chunk;
|
|
31576
32430
|
const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
|
|
31577
|
-
|
|
31578
|
-
|
|
32431
|
+
const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
|
|
32432
|
+
terminalFocusedRef.current = focused;
|
|
32433
|
+
}) : normalized;
|
|
32434
|
+
rawEditorTokens.current = tokenizeRawEditorChunk(input);
|
|
32435
|
+
return input;
|
|
31579
32436
|
};
|
|
31580
32437
|
return () => {
|
|
31581
32438
|
stdin.read = originalRead;
|
|
31582
32439
|
};
|
|
31583
|
-
}, [stdin]);
|
|
32440
|
+
}, [focusReporting, stdin]);
|
|
31584
32441
|
if (recall.current.entries !== recallSpace) {
|
|
31585
32442
|
const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
|
|
31586
32443
|
recall.current = {
|
|
@@ -31602,15 +32459,93 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31602
32459
|
};
|
|
31603
32460
|
const mentionActive = mentionToken !== void 0;
|
|
31604
32461
|
const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
|
|
32462
|
+
const mentionRequestRef = (0, import_react.useRef)(0);
|
|
32463
|
+
const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
32464
|
+
const uniqueImageMarker = (name, source, reserved = []) => {
|
|
32465
|
+
const safeName = singleLineText(sanitizeDraftText(name));
|
|
32466
|
+
let marker = source === "mention" ? `@${safeName}` : `[image: ${safeName}]`;
|
|
32467
|
+
let suffix = 2;
|
|
32468
|
+
while (valueRef.current.includes(marker) || draftImagesRef.current.some((image) => image.marker === marker) || reserved.includes(marker)) {
|
|
32469
|
+
marker = source === "mention" ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`;
|
|
32470
|
+
suffix += 1;
|
|
32471
|
+
}
|
|
32472
|
+
return marker;
|
|
32473
|
+
};
|
|
32474
|
+
const registerDraftImage = (inspection, marker) => {
|
|
32475
|
+
if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
|
|
32476
|
+
notify(`${inspection.name} is already attached`, "warning");
|
|
32477
|
+
return false;
|
|
32478
|
+
}
|
|
32479
|
+
const next = [...draftImagesRef.current, {
|
|
32480
|
+
...inspection,
|
|
32481
|
+
marker
|
|
32482
|
+
}];
|
|
32483
|
+
draftImagesRef.current = next;
|
|
32484
|
+
setDraftImages(next);
|
|
32485
|
+
return true;
|
|
32486
|
+
};
|
|
32487
|
+
const insertDroppedImages = (paths) => {
|
|
32488
|
+
const originalValue = valueRef.current;
|
|
32489
|
+
const originalCursor = cursorRef.current;
|
|
32490
|
+
notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
|
|
32491
|
+
inspectImages(paths).then((inspected) => {
|
|
32492
|
+
const additions = [];
|
|
32493
|
+
const markers = [];
|
|
32494
|
+
for (const inspection of inspected) {
|
|
32495
|
+
if ([...draftImagesRef.current, ...additions].some((image) => sameImagePath(image.path, inspection.path))) continue;
|
|
32496
|
+
const marker = uniqueImageMarker(inspection.name, "drop", markers);
|
|
32497
|
+
additions.push({
|
|
32498
|
+
...inspection,
|
|
32499
|
+
marker
|
|
32500
|
+
});
|
|
32501
|
+
markers.push(marker);
|
|
32502
|
+
}
|
|
32503
|
+
if (additions.length === 0) {
|
|
32504
|
+
notify("those images are already attached", "warning");
|
|
32505
|
+
return;
|
|
32506
|
+
}
|
|
32507
|
+
const current = valueRef.current;
|
|
32508
|
+
const anchor = remapStableRange(originalValue, current, {
|
|
32509
|
+
start: originalCursor,
|
|
32510
|
+
end: originalCursor
|
|
32511
|
+
});
|
|
32512
|
+
if (anchor === void 0) {
|
|
32513
|
+
notify("draft changed at the image drop point; drop the images again", "warning");
|
|
32514
|
+
return;
|
|
32515
|
+
}
|
|
32516
|
+
const at = anchor.start;
|
|
32517
|
+
const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? " " : ""}${markers.join(" ")}${current.slice(at) === "" ? "" : " "}`;
|
|
32518
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, insertion);
|
|
32519
|
+
const nextCursor = current === originalValue && cursorRef.current === originalCursor ? at + insertion.length : edit.cursor;
|
|
32520
|
+
valueRef.current = edit.value;
|
|
32521
|
+
cursorRef.current = nextCursor;
|
|
32522
|
+
setValue(edit.value);
|
|
32523
|
+
setCursor(nextCursor);
|
|
32524
|
+
resetCursorBlink();
|
|
32525
|
+
const nextImages = [...draftImagesRef.current, ...additions];
|
|
32526
|
+
draftImagesRef.current = nextImages;
|
|
32527
|
+
setDraftImages(nextImages);
|
|
32528
|
+
notify(`${additions.length} image${additions.length === 1 ? "" : "s"} ready for the next message`);
|
|
32529
|
+
}, (reason) => {
|
|
32530
|
+
notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
32531
|
+
});
|
|
32532
|
+
};
|
|
31605
32533
|
(0, import_react.useEffect)(() => {
|
|
32534
|
+
const requestId = mentionRequestRef.current + 1;
|
|
32535
|
+
mentionRequestRef.current = requestId;
|
|
31606
32536
|
if (!active || !mentionActive) {
|
|
31607
32537
|
setMentionRows([]);
|
|
31608
32538
|
return;
|
|
31609
32539
|
}
|
|
31610
32540
|
const controller = new AbortController();
|
|
31611
|
-
|
|
31612
|
-
|
|
32541
|
+
const query = mentionToken.query;
|
|
32542
|
+
const timer = setTimeout(() => {
|
|
32543
|
+
loadMentions(query, controller.signal).then((rows) => {
|
|
32544
|
+
if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows);
|
|
32545
|
+
}, () => {});
|
|
32546
|
+
}, 50);
|
|
31613
32547
|
return () => {
|
|
32548
|
+
clearTimeout(timer);
|
|
31614
32549
|
controller.abort();
|
|
31615
32550
|
};
|
|
31616
32551
|
}, [
|
|
@@ -31618,8 +32553,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31618
32553
|
mentionActive,
|
|
31619
32554
|
mentionToken?.query
|
|
31620
32555
|
]);
|
|
31621
|
-
const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
|
|
31622
|
-
const
|
|
32556
|
+
const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
|
|
32557
|
+
const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
|
|
32558
|
+
const menuRows = mentionActive ? visibleMentionRows.map((row) => ({
|
|
31623
32559
|
label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
|
|
31624
32560
|
description: row.description,
|
|
31625
32561
|
origin: "mention"
|
|
@@ -31627,17 +32563,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31627
32563
|
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
31628
32564
|
const acceptMenuCandidate = () => {
|
|
31629
32565
|
if (mentionActive && mentionToken !== void 0) {
|
|
31630
|
-
|
|
32566
|
+
if (visibleMentionRows.length === 0) return;
|
|
32567
|
+
const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
|
|
31631
32568
|
if (row !== void 0) {
|
|
32569
|
+
if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
|
|
32570
|
+
const tokenText = value.slice(mentionToken.start, cursor);
|
|
32571
|
+
const start = mentionToken.start;
|
|
32572
|
+
const originalValue = value;
|
|
32573
|
+
notify(`checking image ${basename(row.path)}…`);
|
|
32574
|
+
inspectImages([row.path]).then((inspected) => {
|
|
32575
|
+
const inspection = inspected[0];
|
|
32576
|
+
if (inspection === void 0) return;
|
|
32577
|
+
const current = valueRef.current;
|
|
32578
|
+
const anchor = remapStableRange(originalValue, current, {
|
|
32579
|
+
start,
|
|
32580
|
+
end: start + tokenText.length
|
|
32581
|
+
});
|
|
32582
|
+
if (anchor === void 0 || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
32583
|
+
notify("draft changed around the image mention; select it again", "warning");
|
|
32584
|
+
return;
|
|
32585
|
+
}
|
|
32586
|
+
if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
|
|
32587
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, "");
|
|
32588
|
+
valueRef.current = edit.value;
|
|
32589
|
+
cursorRef.current = edit.cursor;
|
|
32590
|
+
setValue(edit.value);
|
|
32591
|
+
setCursor(edit.cursor);
|
|
32592
|
+
resetCursorBlink();
|
|
32593
|
+
setDismissedMenuValue(edit.value);
|
|
32594
|
+
notify(`${inspection.name} is already attached`, "warning");
|
|
32595
|
+
return;
|
|
32596
|
+
}
|
|
32597
|
+
const marker = uniqueImageMarker(inspection.name, "mention");
|
|
32598
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, marker);
|
|
32599
|
+
valueRef.current = edit.value;
|
|
32600
|
+
cursorRef.current = edit.cursor;
|
|
32601
|
+
setValue(edit.value);
|
|
32602
|
+
setCursor(edit.cursor);
|
|
32603
|
+
resetCursorBlink();
|
|
32604
|
+
setDismissedMenuValue(edit.value);
|
|
32605
|
+
registerDraftImage(inspection, marker);
|
|
32606
|
+
notify(`${inspection.name} ready for the next message`);
|
|
32607
|
+
}, (reason) => {
|
|
32608
|
+
notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
32609
|
+
});
|
|
32610
|
+
setCompletionIndex(0);
|
|
32611
|
+
setDismissedMenuValue(void 0);
|
|
32612
|
+
return;
|
|
32613
|
+
}
|
|
31632
32614
|
const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
|
|
31633
|
-
|
|
31634
|
-
|
|
32615
|
+
const nextValue = value.slice(0, mentionToken.start) + insertion + value.slice(cursor);
|
|
32616
|
+
const nextCursor = mentionToken.start + insertion.length;
|
|
32617
|
+
valueRef.current = nextValue;
|
|
32618
|
+
cursorRef.current = nextCursor;
|
|
32619
|
+
setValue(nextValue);
|
|
32620
|
+
setCursor(nextCursor);
|
|
32621
|
+
resetCursorBlink();
|
|
31635
32622
|
}
|
|
31636
32623
|
} else {
|
|
32624
|
+
if (candidates.length === 0) return;
|
|
31637
32625
|
const candidate = candidates[completionIndex % candidates.length];
|
|
31638
32626
|
if (candidate !== void 0) {
|
|
31639
|
-
|
|
31640
|
-
|
|
32627
|
+
const nextValue = `${candidate.label} `;
|
|
32628
|
+
const nextCursor = candidate.label.length + 1;
|
|
32629
|
+
valueRef.current = nextValue;
|
|
32630
|
+
cursorRef.current = nextCursor;
|
|
32631
|
+
setValue(nextValue);
|
|
32632
|
+
setCursor(nextCursor);
|
|
32633
|
+
resetCursorBlink();
|
|
31641
32634
|
}
|
|
31642
32635
|
}
|
|
31643
32636
|
setCompletionIndex(0);
|
|
@@ -31646,20 +32639,101 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31646
32639
|
/** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
|
|
31647
32640
|
const applyEdit = (edit) => {
|
|
31648
32641
|
if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
|
|
32642
|
+
valueRef.current = edit.value;
|
|
32643
|
+
cursorRef.current = edit.cursor;
|
|
31649
32644
|
setValue(edit.value);
|
|
31650
32645
|
setCursor(edit.cursor);
|
|
32646
|
+
resetCursorBlink();
|
|
31651
32647
|
preferredColumnRef.current = null;
|
|
31652
32648
|
setCompletionIndex(0);
|
|
31653
32649
|
setDismissedMenuValue(void 0);
|
|
31654
32650
|
};
|
|
31655
32651
|
/** Move the cursor without editing; horizontal moves clear the column preference. */
|
|
31656
32652
|
const moveCursorTo = (next) => {
|
|
31657
|
-
|
|
32653
|
+
resetCursorBlink();
|
|
32654
|
+
if (next === cursorRef.current) return;
|
|
32655
|
+
cursorRef.current = next;
|
|
31658
32656
|
setCursor(next);
|
|
31659
32657
|
preferredColumnRef.current = null;
|
|
31660
32658
|
};
|
|
31661
|
-
|
|
32659
|
+
/** Apply an ordered raw-key batch against one current draft snapshot. */
|
|
32660
|
+
const applyRawEditorTokens = (tokens) => {
|
|
32661
|
+
let nextValue = valueRef.current;
|
|
32662
|
+
let nextCursor = cursorRef.current;
|
|
32663
|
+
for (const token of tokens) {
|
|
32664
|
+
if (token.kind === "text") {
|
|
32665
|
+
const edit = insertText(nextValue, nextCursor, token.text);
|
|
32666
|
+
nextValue = edit.value;
|
|
32667
|
+
nextCursor = edit.cursor;
|
|
32668
|
+
continue;
|
|
32669
|
+
}
|
|
32670
|
+
if (token.kind === "home") {
|
|
32671
|
+
nextCursor = moveToLineStart(nextValue, nextCursor, false);
|
|
32672
|
+
continue;
|
|
32673
|
+
}
|
|
32674
|
+
if (token.kind === "end") {
|
|
32675
|
+
nextCursor = moveToLineEnd(nextValue, nextCursor, false);
|
|
32676
|
+
continue;
|
|
32677
|
+
}
|
|
32678
|
+
const edit = token.kind === "delete-backward" ? deleteBackward(nextValue, nextCursor) : token.kind === "delete-word-backward" ? deleteWordBackward(nextValue, nextCursor) : token.kind === "delete-forward" ? deleteForward(nextValue, nextCursor) : deleteWordForward(nextValue, nextCursor);
|
|
32679
|
+
if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
|
|
32680
|
+
nextValue = edit.value;
|
|
32681
|
+
nextCursor = edit.cursor;
|
|
32682
|
+
}
|
|
32683
|
+
valueRef.current = nextValue;
|
|
32684
|
+
cursorRef.current = nextCursor;
|
|
32685
|
+
setValue(nextValue);
|
|
32686
|
+
setCursor(nextCursor);
|
|
32687
|
+
resetCursorBlink();
|
|
32688
|
+
preferredColumnRef.current = null;
|
|
32689
|
+
setCompletionIndex(0);
|
|
32690
|
+
setDismissedMenuValue(void 0);
|
|
32691
|
+
};
|
|
32692
|
+
const cancelImageSubmission = () => {
|
|
32693
|
+
prepareEpochRef.current += 1;
|
|
32694
|
+
prepareAbortRef.current?.abort();
|
|
32695
|
+
prepareAbortRef.current = void 0;
|
|
32696
|
+
setPreparingImages(false);
|
|
32697
|
+
dismissNotice();
|
|
32698
|
+
notify("image submission cancelled", "warning");
|
|
32699
|
+
};
|
|
32700
|
+
/** Move through visual rows first, then cross history at the true edge. */
|
|
32701
|
+
const navigateVertical = (direction) => {
|
|
32702
|
+
const currentValue = valueRef.current;
|
|
32703
|
+
const currentCursor = cursorRef.current;
|
|
32704
|
+
const model = editorModel(currentValue, editorColumns);
|
|
32705
|
+
const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
|
|
32706
|
+
const next = moveCursorVertically(model, currentCursor, preferred, direction);
|
|
32707
|
+
if (next !== currentCursor) {
|
|
32708
|
+
cursorRef.current = next;
|
|
32709
|
+
setCursor(next);
|
|
32710
|
+
resetCursorBlink();
|
|
32711
|
+
preferredColumnRef.current = preferred;
|
|
32712
|
+
return;
|
|
32713
|
+
}
|
|
32714
|
+
if (recall.current.entries.length > 0 && shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
|
|
32715
|
+
const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current);
|
|
32716
|
+
recall.current = step.state;
|
|
32717
|
+
if (step.entry !== void 0) {
|
|
32718
|
+
const safe = sanitizeDraftText(step.entry);
|
|
32719
|
+
valueRef.current = safe;
|
|
32720
|
+
cursorRef.current = safe.length;
|
|
32721
|
+
setValue(safe);
|
|
32722
|
+
setCursor(safe.length);
|
|
32723
|
+
preferredColumnRef.current = null;
|
|
32724
|
+
setDismissedMenuValue(void 0);
|
|
32725
|
+
}
|
|
32726
|
+
}
|
|
32727
|
+
resetCursorBlink();
|
|
32728
|
+
};
|
|
32729
|
+
useStableInput((input, key) => {
|
|
31662
32730
|
if (!active) return;
|
|
32731
|
+
const liveValue = valueRef.current;
|
|
32732
|
+
const liveCursor = cursorRef.current;
|
|
32733
|
+
if (preparingImages) {
|
|
32734
|
+
if (key.escape || key.ctrl && input === "c") cancelImageSubmission();
|
|
32735
|
+
return;
|
|
32736
|
+
}
|
|
31663
32737
|
if (deleteConfirm !== void 0) {
|
|
31664
32738
|
if (input === "y" || input === "Y") confirmDelete();
|
|
31665
32739
|
else cancelDelete();
|
|
@@ -31675,6 +32749,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31675
32749
|
return;
|
|
31676
32750
|
}
|
|
31677
32751
|
if (key.ctrl && input === "r") {
|
|
32752
|
+
if (focusReporting && !terminalFocusedRef.current) return;
|
|
31678
32753
|
toggleReasoning();
|
|
31679
32754
|
return;
|
|
31680
32755
|
}
|
|
@@ -31684,17 +32759,22 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31684
32759
|
}
|
|
31685
32760
|
if (key.ctrl && input === "c") {
|
|
31686
32761
|
if (busy) interrupt();
|
|
31687
|
-
else if (
|
|
32762
|
+
else if (liveValue !== "") {
|
|
32763
|
+
valueRef.current = "";
|
|
32764
|
+
cursorRef.current = 0;
|
|
31688
32765
|
setValue("");
|
|
31689
32766
|
setCursor(0);
|
|
32767
|
+
resetCursorBlink();
|
|
32768
|
+
draftImagesRef.current = [];
|
|
32769
|
+
setDraftImages([]);
|
|
31690
32770
|
setCompletionIndex(0);
|
|
31691
32771
|
setDismissedMenuValue(void 0);
|
|
31692
32772
|
} else quit();
|
|
31693
32773
|
return;
|
|
31694
32774
|
}
|
|
31695
32775
|
if (key.ctrl && input === "d") {
|
|
31696
|
-
if (
|
|
31697
|
-
applyEdit(deleteForward(
|
|
32776
|
+
if (liveValue !== "") {
|
|
32777
|
+
applyEdit(deleteForward(liveValue, liveCursor));
|
|
31698
32778
|
return;
|
|
31699
32779
|
}
|
|
31700
32780
|
if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
|
|
@@ -31703,7 +32783,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31703
32783
|
}
|
|
31704
32784
|
if (key.escape) {
|
|
31705
32785
|
if (menuActive) {
|
|
31706
|
-
setDismissedMenuValue(
|
|
32786
|
+
setDismissedMenuValue(liveValue);
|
|
31707
32787
|
return;
|
|
31708
32788
|
}
|
|
31709
32789
|
if (hasNotice) {
|
|
@@ -31713,31 +32793,63 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31713
32793
|
if (busy) interrupt();
|
|
31714
32794
|
return;
|
|
31715
32795
|
}
|
|
31716
|
-
if (key.delete &&
|
|
32796
|
+
if (key.delete && liveValue === "" && queued.length > 0) {
|
|
31717
32797
|
cancelQueued(queued[queued.length - 1].messageId);
|
|
31718
32798
|
return;
|
|
31719
32799
|
}
|
|
31720
|
-
|
|
31721
|
-
if (shiftEnterSequence || key.return) {
|
|
32800
|
+
if (key.return) {
|
|
31722
32801
|
if (pasteBracketRef.current) {
|
|
31723
|
-
applyEdit(insertText(
|
|
31724
|
-
return;
|
|
31725
|
-
}
|
|
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);
|
|
32802
|
+
applyEdit(insertText(liveValue, liveCursor, "\n"));
|
|
31730
32803
|
return;
|
|
31731
32804
|
}
|
|
31732
32805
|
if (menuActive) {
|
|
31733
|
-
if (!(!mentionActive && candidates.some((candidate) => candidate.label ===
|
|
32806
|
+
if (!(!mentionActive && candidates.some((candidate) => candidate.label === liveValue))) {
|
|
31734
32807
|
acceptMenuCandidate();
|
|
31735
32808
|
return;
|
|
31736
32809
|
}
|
|
31737
32810
|
}
|
|
31738
|
-
const text =
|
|
32811
|
+
const text = liveValue.trim();
|
|
32812
|
+
if (draftImagesRef.current.length > 0) {
|
|
32813
|
+
const controller = new AbortController();
|
|
32814
|
+
const epoch = prepareEpochRef.current + 1;
|
|
32815
|
+
prepareEpochRef.current = epoch;
|
|
32816
|
+
prepareAbortRef.current = controller;
|
|
32817
|
+
setPreparingImages(true);
|
|
32818
|
+
notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
|
|
32819
|
+
const snapshot = draftImagesRef.current;
|
|
32820
|
+
prepareImages(snapshot.map((image) => image.path), controller.signal).then((images) => {
|
|
32821
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
32822
|
+
prepareAbortRef.current = void 0;
|
|
32823
|
+
setPreparingImages(false);
|
|
32824
|
+
valueRef.current = "";
|
|
32825
|
+
cursorRef.current = 0;
|
|
32826
|
+
setValue("");
|
|
32827
|
+
setCursor(0);
|
|
32828
|
+
draftImagesRef.current = [];
|
|
32829
|
+
setDraftImages([]);
|
|
32830
|
+
setCompletionIndex(0);
|
|
32831
|
+
setDismissedMenuValue(void 0);
|
|
32832
|
+
dismissNotice();
|
|
32833
|
+
if (text !== "") {
|
|
32834
|
+
recordLocal(text);
|
|
32835
|
+
recordHistory(text);
|
|
32836
|
+
}
|
|
32837
|
+
recall.current = beginRecall(recallSpace, "");
|
|
32838
|
+
if (busy) steer(text, images);
|
|
32839
|
+
else dispatch(text, images);
|
|
32840
|
+
}, (reason) => {
|
|
32841
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
32842
|
+
prepareAbortRef.current = void 0;
|
|
32843
|
+
setPreparingImages(false);
|
|
32844
|
+
notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
32845
|
+
});
|
|
32846
|
+
return;
|
|
32847
|
+
}
|
|
32848
|
+
valueRef.current = "";
|
|
32849
|
+
cursorRef.current = 0;
|
|
31739
32850
|
setValue("");
|
|
31740
32851
|
setCursor(0);
|
|
32852
|
+
resetCursorBlink();
|
|
31741
32853
|
setCompletionIndex(0);
|
|
31742
32854
|
setDismissedMenuValue(void 0);
|
|
31743
32855
|
if (text === "") return;
|
|
@@ -31863,6 +32975,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31863
32975
|
dispatch(text);
|
|
31864
32976
|
return;
|
|
31865
32977
|
}
|
|
32978
|
+
if (input === "\n" || input === "\r") return;
|
|
32979
|
+
if (menuActive && (key.tab || input.startsWith(" "))) {
|
|
32980
|
+
const remainder = key.tab ? "" : input.slice(1);
|
|
32981
|
+
acceptMenuCandidate();
|
|
32982
|
+
if (remainder !== "") applyEdit(insertText(valueRef.current, cursorRef.current, remainder));
|
|
32983
|
+
return;
|
|
32984
|
+
}
|
|
31866
32985
|
if (menuActive && key.upArrow) {
|
|
31867
32986
|
setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
|
|
31868
32987
|
return;
|
|
@@ -31871,119 +32990,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
31871
32990
|
setCompletionIndex((index) => (index + 1) % menuRows.length);
|
|
31872
32991
|
return;
|
|
31873
32992
|
}
|
|
31874
|
-
const
|
|
31875
|
-
|
|
31876
|
-
|
|
31877
|
-
|
|
31878
|
-
else if (rawKey === "delete-backward") applyEdit(deleteBackward(value, cursor));
|
|
31879
|
-
else if (rawKey === "delete-word-backward") applyEdit(deleteWordBackward(value, cursor));
|
|
31880
|
-
else if (rawKey === "delete-forward") applyEdit(deleteForward(value, cursor));
|
|
31881
|
-
else applyEdit(deleteWordForward(value, cursor));
|
|
32993
|
+
const rawTokens = rawEditorTokens.current;
|
|
32994
|
+
rawEditorTokens.current = void 0;
|
|
32995
|
+
if (rawTokens !== void 0) {
|
|
32996
|
+
applyRawEditorTokens(rawTokens);
|
|
31882
32997
|
return;
|
|
31883
32998
|
}
|
|
31884
32999
|
if (key.upArrow || key.downArrow) {
|
|
31885
|
-
|
|
31886
|
-
const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current);
|
|
31887
|
-
recall.current = step.state;
|
|
31888
|
-
if (step.entry !== void 0) {
|
|
31889
|
-
const safe = sanitizeDraftText(step.entry);
|
|
31890
|
-
setValue(safe);
|
|
31891
|
-
setCursor(safe.length);
|
|
31892
|
-
preferredColumnRef.current = null;
|
|
31893
|
-
setDismissedMenuValue(void 0);
|
|
31894
|
-
}
|
|
31895
|
-
return;
|
|
31896
|
-
}
|
|
31897
|
-
const model = editorModel(value, Math.max(1, columns - 6));
|
|
31898
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
|
|
31899
|
-
const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1);
|
|
31900
|
-
if (next !== cursor) {
|
|
31901
|
-
setCursor(next);
|
|
31902
|
-
preferredColumnRef.current = preferred;
|
|
31903
|
-
}
|
|
33000
|
+
navigateVertical(key.upArrow ? -1 : 1);
|
|
31904
33001
|
return;
|
|
31905
33002
|
}
|
|
31906
33003
|
if (key.ctrl && (input === "p" || input === "n")) {
|
|
31907
|
-
|
|
31908
|
-
if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
|
|
31909
|
-
const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current);
|
|
31910
|
-
recall.current = step.state;
|
|
31911
|
-
if (step.entry !== void 0) {
|
|
31912
|
-
const safe = sanitizeDraftText(step.entry);
|
|
31913
|
-
setValue(safe);
|
|
31914
|
-
setCursor(safe.length);
|
|
31915
|
-
preferredColumnRef.current = null;
|
|
31916
|
-
setDismissedMenuValue(void 0);
|
|
31917
|
-
}
|
|
31918
|
-
return;
|
|
31919
|
-
}
|
|
31920
|
-
const model = editorModel(value, Math.max(1, columns - 6));
|
|
31921
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
|
|
31922
|
-
const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1);
|
|
31923
|
-
if (next !== cursor) {
|
|
31924
|
-
setCursor(next);
|
|
31925
|
-
preferredColumnRef.current = preferred;
|
|
31926
|
-
}
|
|
31927
|
-
return;
|
|
31928
|
-
}
|
|
31929
|
-
if (key.tab && menuActive) {
|
|
31930
|
-
acceptMenuCandidate();
|
|
33004
|
+
navigateVertical(input === "p" ? -1 : 1);
|
|
31931
33005
|
return;
|
|
31932
33006
|
}
|
|
31933
33007
|
if (key.leftArrow) {
|
|
31934
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(
|
|
33008
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, -1));
|
|
31935
33009
|
return;
|
|
31936
33010
|
}
|
|
31937
33011
|
if (key.rightArrow) {
|
|
31938
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordRight(
|
|
33012
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordRight(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, 1));
|
|
31939
33013
|
return;
|
|
31940
33014
|
}
|
|
31941
33015
|
if (key.meta && input === "b") {
|
|
31942
|
-
moveCursorTo(moveWordLeft(
|
|
33016
|
+
moveCursorTo(moveWordLeft(liveValue, liveCursor));
|
|
31943
33017
|
return;
|
|
31944
33018
|
}
|
|
31945
33019
|
if (key.meta && input === "f") {
|
|
31946
|
-
moveCursorTo(moveWordRight(
|
|
33020
|
+
moveCursorTo(moveWordRight(liveValue, liveCursor));
|
|
31947
33021
|
return;
|
|
31948
33022
|
}
|
|
31949
33023
|
if (key.ctrl && input === "b") {
|
|
31950
|
-
moveCursorTo(moveCursorBy(
|
|
33024
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, -1));
|
|
31951
33025
|
return;
|
|
31952
33026
|
}
|
|
31953
33027
|
if (key.ctrl && input === "f") {
|
|
31954
|
-
moveCursorTo(moveCursorBy(
|
|
33028
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, 1));
|
|
31955
33029
|
return;
|
|
31956
33030
|
}
|
|
31957
33031
|
if (key.ctrl && input === "w") {
|
|
31958
|
-
applyEdit(deleteWordBackward(
|
|
33032
|
+
applyEdit(deleteWordBackward(liveValue, liveCursor));
|
|
31959
33033
|
return;
|
|
31960
33034
|
}
|
|
31961
33035
|
if (key.meta && input === "d") {
|
|
31962
|
-
applyEdit(deleteWordForward(
|
|
33036
|
+
applyEdit(deleteWordForward(liveValue, liveCursor));
|
|
31963
33037
|
return;
|
|
31964
33038
|
}
|
|
31965
33039
|
if (key.backspace || key.delete) {
|
|
31966
|
-
applyEdit(deleteBackward(
|
|
33040
|
+
applyEdit(deleteBackward(liveValue, liveCursor));
|
|
31967
33041
|
return;
|
|
31968
33042
|
}
|
|
31969
33043
|
if (key.ctrl && input === "a") {
|
|
31970
|
-
moveCursorTo(
|
|
33044
|
+
moveCursorTo(moveToLineStart(liveValue, liveCursor, true));
|
|
31971
33045
|
return;
|
|
31972
33046
|
}
|
|
31973
33047
|
if (key.ctrl && input === "e") {
|
|
31974
|
-
moveCursorTo(
|
|
33048
|
+
moveCursorTo(moveToLineEnd(liveValue, liveCursor, true));
|
|
31975
33049
|
return;
|
|
31976
33050
|
}
|
|
31977
33051
|
if (key.ctrl && input === "u") {
|
|
31978
|
-
applyEdit(killToLineStart(
|
|
33052
|
+
applyEdit(killToLineStart(liveValue, liveCursor));
|
|
31979
33053
|
return;
|
|
31980
33054
|
}
|
|
31981
33055
|
if (key.ctrl && input === "k") {
|
|
31982
|
-
applyEdit(killToLineEnd(
|
|
33056
|
+
applyEdit(killToLineEnd(liveValue, liveCursor));
|
|
31983
33057
|
return;
|
|
31984
33058
|
}
|
|
31985
33059
|
if (key.ctrl && input === "y") {
|
|
31986
|
-
if (killRef.current !== "") applyEdit(insertText(
|
|
33060
|
+
if (killRef.current !== "") applyEdit(insertText(liveValue, liveCursor, killRef.current));
|
|
31987
33061
|
return;
|
|
31988
33062
|
}
|
|
31989
33063
|
if (key.ctrl && input === "l") {
|
|
@@ -32011,9 +33085,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
32011
33085
|
text = text.replaceAll(PASTE_END_MARKER, "");
|
|
32012
33086
|
}
|
|
32013
33087
|
if (text === "") return;
|
|
32014
|
-
|
|
33088
|
+
const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : [];
|
|
33089
|
+
if (droppedPaths.length > 0) {
|
|
33090
|
+
insertDroppedImages(droppedPaths);
|
|
33091
|
+
return;
|
|
33092
|
+
}
|
|
33093
|
+
applyEdit(insertText(valueRef.current, cursorRef.current, text));
|
|
32015
33094
|
}
|
|
32016
|
-
});
|
|
33095
|
+
}, active);
|
|
32017
33096
|
const [waveTick, setWaveTick] = (0, import_react.useState)(null);
|
|
32018
33097
|
const wavePrevious = (0, import_react.useRef)({
|
|
32019
33098
|
tier: null,
|
|
@@ -32031,7 +33110,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
32031
33110
|
}
|
|
32032
33111
|
if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
|
|
32033
33112
|
}, [waveTier, waveStyle]);
|
|
32034
|
-
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
33113
|
+
const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
32035
33114
|
(0, import_react.useEffect)(() => {
|
|
32036
33115
|
if (!waveActive) return;
|
|
32037
33116
|
const id = setInterval(() => {
|
|
@@ -32052,7 +33131,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
32052
33131
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
|
|
32053
33132
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
|
|
32054
33133
|
const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
|
|
32055
|
-
const editorViewModel = editorModel(value,
|
|
33134
|
+
const editorViewModel = editorModel(value, editorColumns);
|
|
32056
33135
|
const clampedCursor = clampCursor(value, cursor);
|
|
32057
33136
|
const caret = caretSite(editorViewModel, clampedCursor);
|
|
32058
33137
|
const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows));
|
|
@@ -32103,123 +33182,120 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
32103
33182
|
const editorRows = [];
|
|
32104
33183
|
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
32105
33184
|
const row = editorViewModel.rows[index];
|
|
32106
|
-
const
|
|
32107
|
-
const
|
|
32108
|
-
const
|
|
32109
|
-
const
|
|
32110
|
-
const placeholder = index === 0 && value === "" && !busy;
|
|
32111
|
-
const tail = placeholder ? COMPOSER_PLACEHOLDER : after;
|
|
32112
|
-
const consumed = 2 + visibleColumns(before) + visibleColumns(caretChar) + visibleColumns(tail);
|
|
33185
|
+
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages);
|
|
33186
|
+
const placeholder = index === 0 && value === "" && !busy && !preparingImages;
|
|
33187
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
|
|
33188
|
+
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail);
|
|
32113
33189
|
editorRows.push((0, import_react.createElement)(Text, {
|
|
32114
33190
|
key: index,
|
|
32115
33191
|
backgroundColor: bandBg,
|
|
32116
33192
|
wrap: "truncate-end"
|
|
32117
|
-
}, index === 0 ?
|
|
33193
|
+
}, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
|
|
33194
|
+
color: inkColor(getPalette().warn),
|
|
33195
|
+
bold: true
|
|
33196
|
+
}, "… ") : busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
|
|
32118
33197
|
color: promptColor,
|
|
32119
33198
|
bold: tierActive ? true : void 0
|
|
32120
|
-
}, `${promptGlyph} `) : " ", before, (0, import_react.createElement)(
|
|
33199
|
+
}, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
|
|
32121
33200
|
key: "caret",
|
|
32122
|
-
|
|
32123
|
-
}), placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : after, bandFill(consumed)));
|
|
33201
|
+
inverse: cursorVisible || void 0
|
|
33202
|
+
}, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
|
|
32124
33203
|
}
|
|
32125
33204
|
const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
|
|
32126
33205
|
const waveRow = () => {
|
|
32127
33206
|
const hues = deepseekWaveHues(waveTier);
|
|
32128
33207
|
const style = waveStyle;
|
|
32129
33208
|
const bandRgb = getPalette().composerBand;
|
|
33209
|
+
const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
|
|
33210
|
+
const totalBandRows = visibleRows.length + 2;
|
|
32130
33211
|
const waveBg = (row, column) => {
|
|
32131
|
-
const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row,
|
|
33212
|
+
const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, totalBandRows);
|
|
32132
33213
|
return rgb === null ? bandBg : inkColor(rgb);
|
|
32133
33214
|
};
|
|
32134
33215
|
const blankBandRow = (row) => {
|
|
32135
33216
|
const blanks = [];
|
|
32136
|
-
|
|
33217
|
+
for (let column = 0; column < bandWidth; column += 1) blanks.push({
|
|
32137
33218
|
char: " ",
|
|
32138
|
-
|
|
33219
|
+
width: 1,
|
|
33220
|
+
backgroundColor: waveBg(row, column)
|
|
32139
33221
|
});
|
|
32140
|
-
return (0, import_react.createElement)(Text, { key: row }, ...waveRowSpans(blanks));
|
|
33222
|
+
return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
|
|
32141
33223
|
};
|
|
32142
|
-
const
|
|
32143
|
-
|
|
32144
|
-
{
|
|
32145
|
-
|
|
32146
|
-
|
|
32147
|
-
|
|
32148
|
-
{
|
|
32149
|
-
char: " ",
|
|
32150
|
-
backgroundColor: waveBg(1, 1)
|
|
32151
|
-
},
|
|
32152
|
-
{
|
|
32153
|
-
char: promptGlyph,
|
|
32154
|
-
color: promptColor,
|
|
32155
|
-
bold: true,
|
|
32156
|
-
backgroundColor: waveBg(1, 2)
|
|
32157
|
-
},
|
|
32158
|
-
{
|
|
32159
|
-
char: " ",
|
|
32160
|
-
color: promptColor,
|
|
32161
|
-
backgroundColor: waveBg(1, 3)
|
|
33224
|
+
const cellIndexAtColumn = (cells, target) => {
|
|
33225
|
+
let column = 0;
|
|
33226
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
33227
|
+
if (column === target) return index;
|
|
33228
|
+
column += cells[index].width ?? visibleColumns(cells[index].char);
|
|
33229
|
+
if (column > target) return void 0;
|
|
32162
33230
|
}
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
|
|
32166
|
-
|
|
32167
|
-
|
|
32168
|
-
|
|
32169
|
-
|
|
32170
|
-
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
|
|
32176
|
-
|
|
32177
|
-
|
|
32178
|
-
|
|
32179
|
-
|
|
32180
|
-
|
|
32181
|
-
|
|
32182
|
-
|
|
32183
|
-
|
|
32184
|
-
|
|
33231
|
+
};
|
|
33232
|
+
const editorWaveRows = visibleRows.map((row, visibleIndex) => {
|
|
33233
|
+
const sourceIndex = editorWindowStart + visibleIndex;
|
|
33234
|
+
const bandRow = visibleIndex + 1;
|
|
33235
|
+
const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
|
|
33236
|
+
const placeholder = sourceIndex === 0 && value === "" && !busy;
|
|
33237
|
+
const cells = [];
|
|
33238
|
+
let usedColumns = 0;
|
|
33239
|
+
const push = (char, extra = {}) => {
|
|
33240
|
+
const width = visibleColumns(char);
|
|
33241
|
+
cells.push({
|
|
33242
|
+
char,
|
|
33243
|
+
width,
|
|
33244
|
+
backgroundColor: waveBg(bandRow, usedColumns),
|
|
33245
|
+
...extra
|
|
33246
|
+
});
|
|
33247
|
+
usedColumns += width;
|
|
33248
|
+
};
|
|
33249
|
+
if (sourceIndex === 0) {
|
|
33250
|
+
push(promptGlyph, {
|
|
33251
|
+
color: promptColor,
|
|
33252
|
+
bold: true
|
|
33253
|
+
});
|
|
33254
|
+
push(" ", { color: promptColor });
|
|
33255
|
+
} else {
|
|
33256
|
+
push(" ");
|
|
33257
|
+
push(" ");
|
|
33258
|
+
}
|
|
33259
|
+
for (const span of splitGraphemes(parts.before)) push(span.text);
|
|
33260
|
+
if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
|
|
33261
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
|
|
33262
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
|
|
33263
|
+
while (usedColumns < bandWidth) push(" ");
|
|
33264
|
+
const middleBandRow = Math.floor(totalBandRows / 2);
|
|
33265
|
+
if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
|
|
33266
|
+
const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
|
|
33267
|
+
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
|
|
33268
|
+
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
|
|
33269
|
+
if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
|
|
33270
|
+
const cell = cells[indices[at]];
|
|
33271
|
+
cell.char = word[at];
|
|
33272
|
+
cell.width = 1;
|
|
33273
|
+
cell.color = inkColor(deepseekWaveWordHue(at, hues));
|
|
33274
|
+
cell.bold = true;
|
|
33275
|
+
cell.dim = false;
|
|
33276
|
+
}
|
|
33277
|
+
}
|
|
33278
|
+
if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
|
|
33279
|
+
const spark = deepseekWaveSpark(waveTick);
|
|
33280
|
+
const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
|
|
33281
|
+
if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
|
|
33282
|
+
cells[lastIndex].char = spark;
|
|
33283
|
+
cells[lastIndex].color = promptColor;
|
|
33284
|
+
cells[lastIndex].bold = true;
|
|
33285
|
+
cells[lastIndex].dim = false;
|
|
33286
|
+
}
|
|
33287
|
+
}
|
|
33288
|
+
return (0, import_react.createElement)(Text, {
|
|
33289
|
+
key: `editor-${sourceIndex}`,
|
|
33290
|
+
wrap: "truncate-end"
|
|
33291
|
+
}, ...waveRowSpans(cells));
|
|
32185
33292
|
});
|
|
32186
|
-
if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
|
|
32187
|
-
const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
|
|
32188
|
-
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
|
|
32189
|
-
let clear = true;
|
|
32190
|
-
for (let at = 0; at < word.length; at += 1) {
|
|
32191
|
-
const cell = cells[start + at];
|
|
32192
|
-
if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
|
|
32193
|
-
clear = false;
|
|
32194
|
-
break;
|
|
32195
|
-
}
|
|
32196
|
-
}
|
|
32197
|
-
if (clear) for (let at = 0; at < word.length; at += 1) {
|
|
32198
|
-
const cell = cells[start + at];
|
|
32199
|
-
cell.char = word[at];
|
|
32200
|
-
cell.color = inkColor(deepseekWaveWordHue(at, hues));
|
|
32201
|
-
cell.bold = true;
|
|
32202
|
-
cell.dim = false;
|
|
32203
|
-
}
|
|
32204
|
-
}
|
|
32205
|
-
if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
|
|
32206
|
-
const spark = deepseekWaveSpark(waveTick);
|
|
32207
|
-
if (spark !== null) {
|
|
32208
|
-
const last = cells[cells.length - 1];
|
|
32209
|
-
if (last !== void 0 && last.char === " ") {
|
|
32210
|
-
last.char = spark;
|
|
32211
|
-
last.color = promptColor;
|
|
32212
|
-
last.bold = true;
|
|
32213
|
-
last.dim = false;
|
|
32214
|
-
}
|
|
32215
|
-
}
|
|
32216
|
-
}
|
|
32217
33293
|
return (0, import_react.createElement)(Box, {
|
|
32218
33294
|
flexDirection: "column",
|
|
32219
33295
|
width: bandWidth
|
|
32220
|
-
}, blankBandRow(0),
|
|
33296
|
+
}, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
|
|
32221
33297
|
};
|
|
32222
|
-
return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : band(staticEditor));
|
|
33298
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages ? waveRow() : band(staticEditor));
|
|
32223
33299
|
}
|
|
32224
33300
|
/** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
|
|
32225
33301
|
function buildSettledRow(entry, index, showReasoning, columns) {
|
|
@@ -32429,6 +33505,8 @@ function App(props) {
|
|
|
32429
33505
|
const [modelError, setModelError] = (0, import_react.useState)(void 0);
|
|
32430
33506
|
const [providerDirectory, setProviderDirectory] = (0, import_react.useState)(void 0);
|
|
32431
33507
|
const [providerError, setProviderError] = (0, import_react.useState)(void 0);
|
|
33508
|
+
const [authorizationDirectory, setAuthorizationDirectory] = (0, import_react.useState)(void 0);
|
|
33509
|
+
const [authorizationError, setAuthorizationError] = (0, import_react.useState)(void 0);
|
|
32432
33510
|
const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
|
|
32433
33511
|
const [notice, setNotice] = (0, import_react.useState)(void 0);
|
|
32434
33512
|
const notify = (0, import_react.useCallback)((text, tone = "info") => {
|
|
@@ -32476,6 +33554,24 @@ function App(props) {
|
|
|
32476
33554
|
modelLoadEpoch,
|
|
32477
33555
|
props.loadModelProviders
|
|
32478
33556
|
]);
|
|
33557
|
+
(0, import_react.useEffect)(() => {
|
|
33558
|
+
if (!modelOpen || props.loadProviderAuthorizations === void 0) return;
|
|
33559
|
+
let cancelled = false;
|
|
33560
|
+
setAuthorizationDirectory(void 0);
|
|
33561
|
+
setAuthorizationError(void 0);
|
|
33562
|
+
Promise.resolve().then(() => props.loadProviderAuthorizations()).then((loaded) => {
|
|
33563
|
+
if (!cancelled) setAuthorizationDirectory(loaded);
|
|
33564
|
+
}, (error) => {
|
|
33565
|
+
if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error));
|
|
33566
|
+
});
|
|
33567
|
+
return () => {
|
|
33568
|
+
cancelled = true;
|
|
33569
|
+
};
|
|
33570
|
+
}, [
|
|
33571
|
+
modelOpen,
|
|
33572
|
+
modelLoadEpoch,
|
|
33573
|
+
props.loadProviderAuthorizations
|
|
33574
|
+
]);
|
|
32479
33575
|
(0, import_react.useEffect)(() => {
|
|
32480
33576
|
const subscribe = props.subscribeModelProviders;
|
|
32481
33577
|
if (!modelOpen || subscribe === void 0) return;
|
|
@@ -32485,6 +33581,15 @@ function App(props) {
|
|
|
32485
33581
|
setProviderError(error instanceof Error ? error.message : String(error));
|
|
32486
33582
|
}
|
|
32487
33583
|
}, [modelOpen, props.subscribeModelProviders]);
|
|
33584
|
+
(0, import_react.useEffect)(() => {
|
|
33585
|
+
const subscribe = props.subscribeProviderAuthorizations;
|
|
33586
|
+
if (!modelOpen || subscribe === void 0) return;
|
|
33587
|
+
try {
|
|
33588
|
+
return subscribe(() => setModelLoadEpoch((epoch) => epoch + 1));
|
|
33589
|
+
} catch (error) {
|
|
33590
|
+
setAuthorizationError(error instanceof Error ? error.message : String(error));
|
|
33591
|
+
}
|
|
33592
|
+
}, [modelOpen, props.subscribeProviderAuthorizations]);
|
|
32488
33593
|
const busy = view.busy;
|
|
32489
33594
|
const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
|
|
32490
33595
|
const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
|
|
@@ -32681,13 +33786,16 @@ function App(props) {
|
|
|
32681
33786
|
busy,
|
|
32682
33787
|
streamingActive
|
|
32683
33788
|
]);
|
|
33789
|
+
const sessionHasImages = (0, import_react.useMemo)(() => view.entries.some((entry) => (entry.kind === "user" || entry.kind === "pending") && (entry.images?.length ?? 0) > 0), [view.entries]);
|
|
32684
33790
|
/** Apply one /model pick: record the selection, close the panel, report via notice. */
|
|
32685
33791
|
const applyModel = (row, effortId) => {
|
|
32686
33792
|
try {
|
|
32687
33793
|
const label = props.selectModel(row, effortId);
|
|
32688
33794
|
setModelLabel(label);
|
|
32689
33795
|
setEffortLabel(effortId);
|
|
32690
|
-
|
|
33796
|
+
const selected = `${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`;
|
|
33797
|
+
if (sessionHasImages && row.inputModalities !== void 0 && !row.inputModalities.includes("image")) notify(`model → ${selected} · image history will be sent as text placeholders`, "warning");
|
|
33798
|
+
else notify(`model → next step uses ${selected}`);
|
|
32691
33799
|
setModelOpen(false);
|
|
32692
33800
|
setProviderOpen(false);
|
|
32693
33801
|
setProviderAction(void 0);
|
|
@@ -32707,7 +33815,37 @@ function App(props) {
|
|
|
32707
33815
|
};
|
|
32708
33816
|
let modelSurface;
|
|
32709
33817
|
if (modelOpen && !approvalPending && !questionPending) {
|
|
32710
|
-
if (providerAction?.kind === "
|
|
33818
|
+
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, {
|
|
33819
|
+
row: providerAction.authorization,
|
|
33820
|
+
begin: props.beginProviderAuthorization,
|
|
33821
|
+
cancel: () => props.cancelProviderAuthorization(providerAction.authorization),
|
|
33822
|
+
openUrl: props.openAuthorizationUrl,
|
|
33823
|
+
copy: props.copyTextValue,
|
|
33824
|
+
done: () => {
|
|
33825
|
+
const authorization = providerAction.authorization;
|
|
33826
|
+
setProviderAction(void 0);
|
|
33827
|
+
setProviderOpen(false);
|
|
33828
|
+
reloadModelSurfaces();
|
|
33829
|
+
notify(`logged in to ${authorization.label}; select a model`);
|
|
33830
|
+
},
|
|
33831
|
+
back: () => {
|
|
33832
|
+
setProviderAction(void 0);
|
|
33833
|
+
setProviderOpen(true);
|
|
33834
|
+
}
|
|
33835
|
+
});
|
|
33836
|
+
else if (providerAction?.kind === "logout" && props.logoutProviderAuthorization !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationLogoutPanel, {
|
|
33837
|
+
row: providerAction.authorization,
|
|
33838
|
+
confirm: props.logoutProviderAuthorization,
|
|
33839
|
+
done: () => {
|
|
33840
|
+
const authorization = providerAction.authorization;
|
|
33841
|
+
setProviderAction(void 0);
|
|
33842
|
+
setProviderOpen(true);
|
|
33843
|
+
reloadModelSurfaces();
|
|
33844
|
+
notify(`logged out from ${authorization.label}`);
|
|
33845
|
+
},
|
|
33846
|
+
back: () => setProviderAction(void 0)
|
|
33847
|
+
});
|
|
33848
|
+
else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
|
|
32711
33849
|
target: providerAction.target,
|
|
32712
33850
|
catalog: directory?.rows ?? [],
|
|
32713
33851
|
save: props.saveModelProviderConfiguration,
|
|
@@ -32761,6 +33899,8 @@ function App(props) {
|
|
|
32761
33899
|
else if (providerOpen) modelSurface = (0, import_react.createElement)(ProviderPanel, {
|
|
32762
33900
|
directory: providerDirectory,
|
|
32763
33901
|
error: providerError,
|
|
33902
|
+
authorizations: authorizationDirectory,
|
|
33903
|
+
authorizationError,
|
|
32764
33904
|
onCredential: (target) => {
|
|
32765
33905
|
if (props.saveModelProviderCredential === void 0) {
|
|
32766
33906
|
notify("API key storage is unavailable in this profile", "warning");
|
|
@@ -32801,6 +33941,32 @@ function App(props) {
|
|
|
32801
33941
|
target
|
|
32802
33942
|
});
|
|
32803
33943
|
},
|
|
33944
|
+
onLogin: (target, authorization) => {
|
|
33945
|
+
if (busy) {
|
|
33946
|
+
notify("provider login is available only while the agent is idle", "warning");
|
|
33947
|
+
return;
|
|
33948
|
+
}
|
|
33949
|
+
if (props.beginProviderAuthorization === void 0 || props.cancelProviderAuthorization === void 0 || props.openAuthorizationUrl === void 0 || props.copyTextValue === void 0) {
|
|
33950
|
+
notify("provider login is unavailable in this profile", "warning");
|
|
33951
|
+
return;
|
|
33952
|
+
}
|
|
33953
|
+
setProviderAction({
|
|
33954
|
+
kind: "login",
|
|
33955
|
+
target,
|
|
33956
|
+
authorization
|
|
33957
|
+
});
|
|
33958
|
+
},
|
|
33959
|
+
onLogout: (target, authorization) => {
|
|
33960
|
+
if (props.logoutProviderAuthorization === void 0) {
|
|
33961
|
+
notify("provider logout is unavailable in this profile", "warning");
|
|
33962
|
+
return;
|
|
33963
|
+
}
|
|
33964
|
+
setProviderAction({
|
|
33965
|
+
kind: "logout",
|
|
33966
|
+
target,
|
|
33967
|
+
authorization
|
|
33968
|
+
});
|
|
33969
|
+
},
|
|
32804
33970
|
onRetry: reloadModelSurfaces,
|
|
32805
33971
|
onBack: () => setProviderOpen(false)
|
|
32806
33972
|
});
|
|
@@ -32983,6 +34149,8 @@ function App(props) {
|
|
|
32983
34149
|
setModelError(void 0);
|
|
32984
34150
|
setProviderDirectory(void 0);
|
|
32985
34151
|
setProviderError(void 0);
|
|
34152
|
+
setAuthorizationDirectory(void 0);
|
|
34153
|
+
setAuthorizationError(void 0);
|
|
32986
34154
|
setProviderOpen(false);
|
|
32987
34155
|
setProviderAction(void 0);
|
|
32988
34156
|
setEffortFor(void 0);
|
|
@@ -33070,6 +34238,8 @@ function App(props) {
|
|
|
33070
34238
|
if (!busy && !streamingActive) refreshScreen();
|
|
33071
34239
|
},
|
|
33072
34240
|
loadMentions: props.loadMentions,
|
|
34241
|
+
inspectImages: props.inspectImages,
|
|
34242
|
+
prepareImages: props.prepareImages,
|
|
33073
34243
|
cyclePermission: props.cyclePermission,
|
|
33074
34244
|
exportTranscript: props.exportTranscript,
|
|
33075
34245
|
renameTitle: props.renameTitle,
|
|
@@ -33271,7 +34441,9 @@ function isSlashLine(line) {
|
|
|
33271
34441
|
/** Substitutable runner effects; production values write to the real terminal. */
|
|
33272
34442
|
const internals = {
|
|
33273
34443
|
mount: (element) => {
|
|
33274
|
-
|
|
34444
|
+
const keyboardEnhanced = shouldEnableKeyboardEnhancement();
|
|
34445
|
+
const focusReporting = isVsCodeTerminalEnv();
|
|
34446
|
+
process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
|
|
33275
34447
|
const instance = render(element, { exitOnCtrlC: false });
|
|
33276
34448
|
return {
|
|
33277
34449
|
rerender(element) {
|
|
@@ -33279,7 +34451,7 @@ const internals = {
|
|
|
33279
34451
|
},
|
|
33280
34452
|
unmount() {
|
|
33281
34453
|
instance.unmount();
|
|
33282
|
-
process.stdout.write("
|
|
34454
|
+
process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
|
|
33283
34455
|
}
|
|
33284
34456
|
};
|
|
33285
34457
|
},
|
|
@@ -33394,14 +34566,16 @@ async function loadModelDirectory(ctx) {
|
|
|
33394
34566
|
provider: provider.id,
|
|
33395
34567
|
providerName: provider.name,
|
|
33396
34568
|
model: model.id,
|
|
33397
|
-
modelName: model.name
|
|
34569
|
+
modelName: model.name,
|
|
34570
|
+
...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
|
|
33398
34571
|
};
|
|
33399
34572
|
if (llmResolve.resolveModelInfo === void 0) return row;
|
|
33400
34573
|
try {
|
|
33401
34574
|
const resolved = await llmResolve.resolveModelInfo(provider.id, model.id);
|
|
33402
|
-
return
|
|
34575
|
+
return {
|
|
33403
34576
|
...row,
|
|
33404
|
-
|
|
34577
|
+
...resolved.inputModalities === void 0 ? {} : { inputModalities: [...resolved.inputModalities] },
|
|
34578
|
+
...resolved.reasoning === void 0 ? {} : { reasoning: mapReasoning(resolved.reasoning) }
|
|
33405
34579
|
};
|
|
33406
34580
|
} catch {
|
|
33407
34581
|
reasoningFailures.push(`${provider.id}/${model.id}`);
|
|
@@ -33503,7 +34677,7 @@ function deriveCredentialRef(provider) {
|
|
|
33503
34677
|
}
|
|
33504
34678
|
/** Events that invalidate the official Models provider/settings/credential join. */
|
|
33505
34679
|
const PROVIDER_SETTINGS_EVENTS = [
|
|
33506
|
-
"credentials/updated",
|
|
34680
|
+
"credentials/reference-updated",
|
|
33507
34681
|
"settings/document-updated",
|
|
33508
34682
|
"llm/adapters-updated"
|
|
33509
34683
|
];
|
|
@@ -33781,82 +34955,6 @@ async function removeProviderSettings(ctx, target) {
|
|
|
33781
34955
|
}
|
|
33782
34956
|
}
|
|
33783
34957
|
//#endregion
|
|
33784
|
-
//#region src/mentions.ts
|
|
33785
|
-
/** Menu cap on file rows; the service owns ranking and default rows. */
|
|
33786
|
-
const MAX_FILE_ROWS = 20;
|
|
33787
|
-
/**
|
|
33788
|
-
* Create the mention API for one agent's workspace. A missing
|
|
33789
|
-
* `fileReferences` service (with an agent present) or `sessionReferenceResolver`
|
|
33790
|
-
* degrades that half to empty rows; `prepare` passes text through untouched
|
|
33791
|
-
* without references. An undefined agent (a bare launch before any session
|
|
33792
|
-
* exists) runs the official WorkspaceFileSearch over the launch cwd — the
|
|
33793
|
-
* same class the mounted service uses per agent — so `@` file completion
|
|
33794
|
-
* works from the first keystroke; session references wait for the session.
|
|
33795
|
-
*
|
|
33796
|
-
* `candidates` never reaches for `this` — the runner hands it to the input
|
|
33797
|
-
* editor as a detached callback, and a `this`-bound method would throw on
|
|
33798
|
-
* every `@` key.
|
|
33799
|
-
* @param ctx - context carrying the optional `fileReferences` and
|
|
33800
|
-
* `sessionReferenceResolver` services.
|
|
33801
|
-
* @param agent - the session owner; excluded from its own session candidates.
|
|
33802
|
-
* @param cwd - launch working directory; bounds the pre-session search.
|
|
33803
|
-
*/
|
|
33804
|
-
function createMentions(ctx, agent, cwd) {
|
|
33805
|
-
const resolver = ctx.get("sessionReferenceResolver");
|
|
33806
|
-
const fileReferences = ctx.get("fileReferences");
|
|
33807
|
-
const sessionCapable = agent !== void 0 && resolver !== void 0;
|
|
33808
|
-
let preSessionSearch;
|
|
33809
|
-
const preSessionFiles = (query, signal) => {
|
|
33810
|
-
preSessionSearch ??= new WorkspaceFileSearch(cwd, {
|
|
33811
|
-
maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
33812
|
-
maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
33813
|
-
excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
|
|
33814
|
-
});
|
|
33815
|
-
return preSessionSearch.list(query, signal ?? new AbortController().signal);
|
|
33816
|
-
};
|
|
33817
|
-
return {
|
|
33818
|
-
async candidates(query, signal) {
|
|
33819
|
-
const needle = query.trim();
|
|
33820
|
-
const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
|
|
33821
|
-
const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
|
|
33822
|
-
label: candidate.path,
|
|
33823
|
-
description: candidate.kind === "directory" ? "Folder" : "File",
|
|
33824
|
-
kind: candidate.kind
|
|
33825
|
-
}));
|
|
33826
|
-
const sessionRows = sessions.map((candidate) => ({
|
|
33827
|
-
label: formatSessionReferenceMention(candidate),
|
|
33828
|
-
description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
|
|
33829
|
-
kind: "session"
|
|
33830
|
-
}));
|
|
33831
|
-
return [...fileRows, ...sessionRows];
|
|
33832
|
-
},
|
|
33833
|
-
parse(text) {
|
|
33834
|
-
return parseSessionReferenceText(text);
|
|
33835
|
-
},
|
|
33836
|
-
async prepare(parsed, signal) {
|
|
33837
|
-
if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
|
|
33838
|
-
text: parsed.text,
|
|
33839
|
-
references: parsed.references
|
|
33840
|
-
};
|
|
33841
|
-
const prepared = await resolver.prepare(agent, [{
|
|
33842
|
-
type: "text",
|
|
33843
|
-
text: parsed.text
|
|
33844
|
-
}], parsed.references, signal);
|
|
33845
|
-
return {
|
|
33846
|
-
text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
|
|
33847
|
-
references: parsed.references,
|
|
33848
|
-
additionalContext: prepared.additionalContext
|
|
33849
|
-
};
|
|
33850
|
-
},
|
|
33851
|
-
sessionMention(candidate) {
|
|
33852
|
-
return formatSessionReferenceMention({
|
|
33853
|
-
sessionId: candidate.sessionId,
|
|
33854
|
-
label: candidate.label
|
|
33855
|
-
});
|
|
33856
|
-
}
|
|
33857
|
-
};
|
|
33858
|
-
}
|
|
33859
|
-
//#endregion
|
|
33860
34958
|
//#region src/questions.ts
|
|
33861
34959
|
const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
|
|
33862
34960
|
/**
|
|
@@ -34284,44 +35382,6 @@ function buildExportMarkdown(view, sessionId) {
|
|
|
34284
35382
|
return out.join("\n");
|
|
34285
35383
|
}
|
|
34286
35384
|
//#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
35385
|
//#region src/editor.ts
|
|
34326
35386
|
/** Host editor and clipboard adapters used by the terminal surface. */
|
|
34327
35387
|
function waitForProcess(command, args, input) {
|
|
@@ -34602,7 +35662,6 @@ function applyPendingPermission(service, session, pending) {
|
|
|
34602
35662
|
*/
|
|
34603
35663
|
function listPermissionRows(service) {
|
|
34604
35664
|
return service.names.map((id) => {
|
|
34605
|
-
if (service.optionOf === void 0) return { id };
|
|
34606
35665
|
try {
|
|
34607
35666
|
return {
|
|
34608
35667
|
id,
|
|
@@ -35216,16 +36275,16 @@ async function run(ctx, startup, io) {
|
|
|
35216
36275
|
deliverLine(line, mode, images);
|
|
35217
36276
|
};
|
|
35218
36277
|
/** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
|
|
35219
|
-
const dispatch = (text) => {
|
|
35220
|
-
send(text, "followup");
|
|
36278
|
+
const dispatch = (text, images = []) => {
|
|
36279
|
+
send(text, "followup", images);
|
|
35221
36280
|
};
|
|
35222
36281
|
/**
|
|
35223
36282
|
* Submit steering: a running driver consumes the text at its next step
|
|
35224
36283
|
* boundary (the inbox delivers between steps); an idle driver just starts
|
|
35225
36284
|
* a turn, so this doubles as the busy-state submit path.
|
|
35226
36285
|
*/
|
|
35227
|
-
const steer = (text) => {
|
|
35228
|
-
send(text, "steer");
|
|
36286
|
+
const steer = (text, images = []) => {
|
|
36287
|
+
send(text, "steer", images);
|
|
35229
36288
|
};
|
|
35230
36289
|
/** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
|
|
35231
36290
|
const interrupt = () => {
|
|
@@ -35637,7 +36696,16 @@ async function run(ctx, startup, io) {
|
|
|
35637
36696
|
saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
|
|
35638
36697
|
unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
|
|
35639
36698
|
removeModelProvider: (target) => removeProviderSettings(ctx, target),
|
|
36699
|
+
loadProviderAuthorizations: () => loadProviderAuthorizations(ctx),
|
|
36700
|
+
subscribeProviderAuthorizations: (listener) => subscribeProviderAuthorizations(ctx, listener),
|
|
36701
|
+
beginProviderAuthorization: (row, method, interaction, signal) => beginProviderAuthorization(ctx, row, method, interaction, signal),
|
|
36702
|
+
cancelProviderAuthorization: (row) => cancelProviderAuthorization(ctx, row.key),
|
|
36703
|
+
logoutProviderAuthorization: (row) => logoutProviderAuthorization(ctx, row),
|
|
36704
|
+
openAuthorizationUrl,
|
|
36705
|
+
copyTextValue: copyText,
|
|
35640
36706
|
loadMentions: (query, signal) => mentions.candidates(query, signal),
|
|
36707
|
+
inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
|
|
36708
|
+
prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
|
|
35641
36709
|
cyclePermission: cyclePermission$1,
|
|
35642
36710
|
setPermission: setPermissionAction,
|
|
35643
36711
|
selectModel,
|
|
@@ -35687,7 +36755,13 @@ async function run(ctx, startup, io) {
|
|
|
35687
36755
|
mountRef.current?.rerender(appElement());
|
|
35688
36756
|
};
|
|
35689
36757
|
mountRef.current = io.mount(appElement());
|
|
35690
|
-
if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0)
|
|
36758
|
+
if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) {
|
|
36759
|
+
if ((startup.images?.length ?? 0) > 0) bridge.notify(`processing ${startup.images.length} startup image${startup.images.length === 1 ? "" : "s"}…`);
|
|
36760
|
+
saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => {
|
|
36761
|
+
if (images.length > 0) bridge.notify(`${images.length} startup image${images.length === 1 ? "" : "s"} attached`);
|
|
36762
|
+
send(startup.prompt ?? "", "followup", images);
|
|
36763
|
+
}, (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
|
|
36764
|
+
}
|
|
35691
36765
|
async function copyLastResponse() {
|
|
35692
36766
|
const text = latestAssistantText(store.getView());
|
|
35693
36767
|
if (text === void 0) return "nothing to copy yet";
|