dsh-code 1.0.4 → 1.0.6
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 +287 -286
- package/README.md +16 -13
- package/bin/deepseek.mjs +336 -11
- package/cordis.patch.yml +26 -18
- package/lib/index.mjs +1310 -439
- package/lib/types/app.d.ts +25 -6
- package/lib/types/attachments.d.ts +36 -4
- package/lib/types/git-workflow.d.ts +7 -2
- package/lib/types/history.d.ts +18 -11
- package/lib/types/index.d.ts +11 -2
- package/lib/types/presets.d.ts +4 -1
- package/lib/types/provider-settings.d.ts +6 -11
- package/lib/types/questions.d.ts +16 -12
- package/lib/types/render/animations.d.ts +74 -7
- package/lib/types/render/export.d.ts +0 -6
- package/lib/types/render/fuzzy.d.ts +21 -0
- package/lib/types/render/projection.d.ts +47 -5
- package/lib/types/session-directory.d.ts +48 -13
- package/lib/types/settings-file.d.ts +8 -0
- package/lib/types/store.d.ts +3 -0
- package/package.json +168 -159
- package/src/app.ts +480 -199
- package/src/attachments.ts +110 -11
- package/src/commands.ts +35 -5
- package/src/git-workflow.ts +29 -10
- package/src/history.ts +22 -13
- package/src/index.ts +1868 -1752
- package/src/internals.ts +61 -40
- package/src/permissions.ts +1 -1
- package/src/presets.ts +19 -6
- package/src/provider-settings.ts +12 -12
- package/src/questions.ts +57 -74
- package/src/render/animations.ts +606 -403
- package/src/render/export.ts +20 -10
- package/src/render/fuzzy.ts +83 -0
- package/src/render/projection.ts +1833 -1620
- package/src/session-directory.ts +94 -16
- package/src/settings-file.ts +38 -6
- package/src/skills.ts +23 -9
- package/src/store.ts +39 -1
- package/src/subagents.ts +26 -3
package/lib/index.mjs
CHANGED
|
@@ -4,12 +4,12 @@ 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, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
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
|
-
import { MessageId, ReasoningEffortId,
|
|
12
|
-
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
11
|
+
import { MessageId, ReasoningEffortId, assistantStreamFirstTokenTime, boundContextSummary, createUserMessage, isTokenDelta, normalizeApiKey } from "@deepseek-ai/dsh-llm";
|
|
12
|
+
import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
13
13
|
import { PassThrough, Stream } from "node:stream";
|
|
14
14
|
import process$1, { cwd, env } from "node:process";
|
|
15
15
|
import { EventEmitter } from "node:events";
|
|
@@ -22,6 +22,7 @@ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
24
24
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
25
|
+
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
25
26
|
//#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
|
|
26
27
|
/**
|
|
27
28
|
* @license React
|
|
@@ -25082,6 +25083,41 @@ function appendStreamingTail(current, delta) {
|
|
|
25082
25083
|
const next = current + delta;
|
|
25083
25084
|
return next.length <= MAX_STREAMING_CHARS ? next : next.slice(-65536);
|
|
25084
25085
|
}
|
|
25086
|
+
/** Assemble the effective system prompt from surface nodes: head text plus every later non-empty node. */
|
|
25087
|
+
function assembleSystemPrompt(nodes) {
|
|
25088
|
+
if (nodes.size === 0) return "";
|
|
25089
|
+
return [...nodes.entries()].sort((left, right) => left[0] - right[0]).map(([, text]) => text).filter((text) => text !== "").join("\n\n");
|
|
25090
|
+
}
|
|
25091
|
+
/**
|
|
25092
|
+
* Apply one surface event's replace to the live system nodes. Any surface
|
|
25093
|
+
* event may shadow system nodes — the kernel's compaction summary lands as a
|
|
25094
|
+
* `user/message` replace whose range can cover later system nodes (only node
|
|
25095
|
+
* 0 is compaction-protected upstream) — so every surface fold retires covered
|
|
25096
|
+
* nodes, not just `system/message` itself.
|
|
25097
|
+
* @param nodes - the live system-node map (mutated when the event replaces).
|
|
25098
|
+
* @param surfaceOp - the surface operation the event carries, when it is a
|
|
25099
|
+
* surface event (log-only events have none and change nothing).
|
|
25100
|
+
* @returns the reassembled prompt when nodes were retired, `changed: false`
|
|
25101
|
+
* when the event shadows nothing.
|
|
25102
|
+
*/
|
|
25103
|
+
function retireShadowedSystemNodes(nodes, surfaceOp) {
|
|
25104
|
+
if (surfaceOp === void 0 || surfaceOp === "append") return {
|
|
25105
|
+
prompt: "",
|
|
25106
|
+
changed: false
|
|
25107
|
+
};
|
|
25108
|
+
let changed = false;
|
|
25109
|
+
for (const seq of nodes.keys()) if (seq >= surfaceOp.startSeq && seq <= surfaceOp.endSeq) {
|
|
25110
|
+
nodes.delete(seq);
|
|
25111
|
+
changed = true;
|
|
25112
|
+
}
|
|
25113
|
+
return changed ? {
|
|
25114
|
+
prompt: assembleSystemPrompt(nodes),
|
|
25115
|
+
changed
|
|
25116
|
+
} : {
|
|
25117
|
+
prompt: "",
|
|
25118
|
+
changed: false
|
|
25119
|
+
};
|
|
25120
|
+
}
|
|
25085
25121
|
/** Join the text blocks of a content list; non-text blocks contribute nothing. */
|
|
25086
25122
|
function textOf(content) {
|
|
25087
25123
|
return content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
@@ -25090,6 +25126,10 @@ function textOf(content) {
|
|
|
25090
25126
|
function imagesOf(content) {
|
|
25091
25127
|
return content.filter((block) => block.type === "image").map((block) => block.attachment);
|
|
25092
25128
|
}
|
|
25129
|
+
/** Durable file references in their model-visible order. */
|
|
25130
|
+
function filesOf(content) {
|
|
25131
|
+
return content.filter((block) => block.type === "file").map((block) => block.attachment);
|
|
25132
|
+
}
|
|
25093
25133
|
/** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
|
|
25094
25134
|
function imageLabels(images) {
|
|
25095
25135
|
if (images === void 0 || images.length === 0) return "";
|
|
@@ -25100,9 +25140,19 @@ function imageLabels(images) {
|
|
|
25100
25140
|
return `[image: ${name} · ${original === void 0 ? `${image.width}×${image.height}` : `${image.width}×${image.height} · original ${original.width}×${original.height}`} · ${image.bytes} B]`;
|
|
25101
25141
|
}).join("\n");
|
|
25102
25142
|
}
|
|
25103
|
-
/**
|
|
25143
|
+
/** Human-readable bounded file labels for the same surfaces (0.1.5 file blocks). */
|
|
25144
|
+
function fileLabels(files) {
|
|
25145
|
+
if (files === void 0 || files.length === 0) return "";
|
|
25146
|
+
return files.map((file, index) => {
|
|
25147
|
+
const rawName = file.name?.trim() || `file ${index + 1}`;
|
|
25148
|
+
return `[file: ${rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`} · ${file.bytes} B]`;
|
|
25149
|
+
}).join("\n");
|
|
25150
|
+
}
|
|
25151
|
+
/** Prompt text with its durable image and file labels, without exposing local paths or bytes. */
|
|
25104
25152
|
function promptDisplayText(entry) {
|
|
25105
|
-
const
|
|
25153
|
+
const imageText = imageLabels(entry.images);
|
|
25154
|
+
const fileText = fileLabels(entry.files);
|
|
25155
|
+
const labels = imageText === "" ? fileText : fileText === "" ? imageText : `${imageText}\n${fileText}`;
|
|
25106
25156
|
return entry.text === "" ? labels : labels === "" ? entry.text : `${entry.text}\n${labels}`;
|
|
25107
25157
|
}
|
|
25108
25158
|
/** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
|
|
@@ -25149,6 +25199,7 @@ function createReplayAccumulator() {
|
|
|
25149
25199
|
plan: false,
|
|
25150
25200
|
permission: "",
|
|
25151
25201
|
title: "",
|
|
25202
|
+
systemPrompt: "",
|
|
25152
25203
|
sandbox: "",
|
|
25153
25204
|
goal: void 0,
|
|
25154
25205
|
stats: {
|
|
@@ -25184,6 +25235,7 @@ function createReplayAccumulator() {
|
|
|
25184
25235
|
turnFiles: /* @__PURE__ */ new Map(),
|
|
25185
25236
|
turnSteps: /* @__PURE__ */ new Map(),
|
|
25186
25237
|
turnTools: /* @__PURE__ */ new Map(),
|
|
25238
|
+
systemNodes: /* @__PURE__ */ new Map(),
|
|
25187
25239
|
ops: 0
|
|
25188
25240
|
};
|
|
25189
25241
|
}
|
|
@@ -25266,6 +25318,17 @@ function retireReplayEntry(acc, index) {
|
|
|
25266
25318
|
* like the copy-on-write reducer returning its input view unchanged.
|
|
25267
25319
|
*/
|
|
25268
25320
|
function replayProjectEvent(acc, event) {
|
|
25321
|
+
const shadow = retireShadowedSystemNodes(acc.systemNodes, event.surfaceOp);
|
|
25322
|
+
if (shadow.changed) {
|
|
25323
|
+
acc.systemPrompt = shadow.prompt;
|
|
25324
|
+
acc.stats = {
|
|
25325
|
+
...acc.stats,
|
|
25326
|
+
contextSegments: {
|
|
25327
|
+
...acc.stats.contextSegments,
|
|
25328
|
+
system: estimateTokens(shadow.prompt)
|
|
25329
|
+
}
|
|
25330
|
+
};
|
|
25331
|
+
}
|
|
25269
25332
|
switch (event.type) {
|
|
25270
25333
|
case "user/message": {
|
|
25271
25334
|
const message = event.data;
|
|
@@ -25284,12 +25347,14 @@ function replayProjectEvent(acc, event) {
|
|
|
25284
25347
|
}
|
|
25285
25348
|
const text = textOf(message.content);
|
|
25286
25349
|
const images = imagesOf(message.content);
|
|
25350
|
+
const files = filesOf(message.content);
|
|
25287
25351
|
if (message.source.kind === "user") {
|
|
25288
25352
|
appendReplayEntry(acc, {
|
|
25289
25353
|
kind: "user",
|
|
25290
25354
|
text,
|
|
25291
25355
|
notice: false,
|
|
25292
|
-
...images.length === 0 ? {} : { images }
|
|
25356
|
+
...images.length === 0 ? {} : { images },
|
|
25357
|
+
...files.length === 0 ? {} : { files }
|
|
25293
25358
|
});
|
|
25294
25359
|
acc.stats = {
|
|
25295
25360
|
...acc.stats,
|
|
@@ -25334,45 +25399,68 @@ function replayProjectEvent(acc, event) {
|
|
|
25334
25399
|
ids.splice(start, 0, ...inserted.map((message) => message.id));
|
|
25335
25400
|
for (const message of inserted) {
|
|
25336
25401
|
const images = imagesOf(message.content);
|
|
25402
|
+
const files = filesOf(message.content);
|
|
25337
25403
|
appendReplayEntry(acc, {
|
|
25338
25404
|
kind: "pending",
|
|
25339
25405
|
messageId: message.id,
|
|
25340
25406
|
target,
|
|
25341
25407
|
text: pendingText(message.content),
|
|
25342
|
-
...images.length === 0 ? {} : { images }
|
|
25408
|
+
...images.length === 0 ? {} : { images },
|
|
25409
|
+
...files.length === 0 ? {} : { files }
|
|
25343
25410
|
});
|
|
25344
25411
|
indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
|
|
25345
25412
|
acc.ops += 1;
|
|
25346
25413
|
}
|
|
25347
25414
|
return true;
|
|
25348
25415
|
}
|
|
25349
|
-
case "
|
|
25350
|
-
const
|
|
25416
|
+
case "system/message": {
|
|
25417
|
+
const text = textOf(event.data.message.content);
|
|
25418
|
+
retireShadowedSystemNodes(acc.systemNodes, event.surfaceOp);
|
|
25419
|
+
acc.systemNodes.set(event.seq, text);
|
|
25420
|
+
acc.systemPrompt = assembleSystemPrompt(acc.systemNodes);
|
|
25421
|
+
acc.stats = {
|
|
25422
|
+
...acc.stats,
|
|
25423
|
+
contextSegments: {
|
|
25424
|
+
...acc.stats.contextSegments,
|
|
25425
|
+
system: estimateTokens(acc.systemPrompt)
|
|
25426
|
+
}
|
|
25427
|
+
};
|
|
25428
|
+
return true;
|
|
25429
|
+
}
|
|
25430
|
+
case "assistant/attempt": {
|
|
25351
25431
|
const key = `${event.data.turn}:${event.data.step}`;
|
|
25352
|
-
|
|
25353
|
-
|
|
25354
|
-
const
|
|
25355
|
-
if (
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
if (chunk.type === "reasoning-delta") {
|
|
25366
|
-
acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
|
|
25367
|
-
return true;
|
|
25432
|
+
let changed = false;
|
|
25433
|
+
if (!acc.firstChunkAt.has(key)) {
|
|
25434
|
+
const first = assistantStreamFirstTokenTime(event.data.stream ?? []);
|
|
25435
|
+
if (first !== void 0) {
|
|
25436
|
+
acc.firstChunkAt.set(key, first);
|
|
25437
|
+
const started = acc.stepStart.get(key);
|
|
25438
|
+
if (started !== void 0) acc.stats = {
|
|
25439
|
+
...acc.stats,
|
|
25440
|
+
ttftMs: acc.stats.ttftMs + Math.max(0, first - started),
|
|
25441
|
+
ttftSteps: acc.stats.ttftSteps + 1
|
|
25442
|
+
};
|
|
25443
|
+
changed = true;
|
|
25444
|
+
}
|
|
25368
25445
|
}
|
|
25369
|
-
|
|
25446
|
+
const streamed = acc.streaming !== "" || acc.streamingReasoning !== "";
|
|
25447
|
+
acc.streaming = "";
|
|
25448
|
+
acc.streamingReasoning = "";
|
|
25449
|
+
return changed || streamed;
|
|
25370
25450
|
}
|
|
25371
25451
|
case "assistant/message": {
|
|
25372
25452
|
const key = `${event.data.turn}:${event.data.step}`;
|
|
25373
25453
|
const started = acc.stepStart.get(key);
|
|
25374
25454
|
acc.stepStart.delete(key);
|
|
25375
|
-
|
|
25455
|
+
let firstChunk = acc.firstChunkAt.get(key);
|
|
25456
|
+
if (firstChunk === void 0) {
|
|
25457
|
+
firstChunk = assistantStreamFirstTokenTime(event.data.stream ?? []);
|
|
25458
|
+
if (firstChunk !== void 0 && started !== void 0) acc.stats = {
|
|
25459
|
+
...acc.stats,
|
|
25460
|
+
ttftMs: acc.stats.ttftMs + Math.max(0, firstChunk - started),
|
|
25461
|
+
ttftSteps: acc.stats.ttftSteps + 1
|
|
25462
|
+
};
|
|
25463
|
+
}
|
|
25376
25464
|
acc.firstChunkAt.delete(key);
|
|
25377
25465
|
if (acc.turnSteps.get(event.data.turn) === key) acc.turnSteps.delete(event.data.turn);
|
|
25378
25466
|
const usage = event.data.usage;
|
|
@@ -25631,11 +25719,7 @@ function replayProjectEvent(acc, event) {
|
|
|
25631
25719
|
acc.model = `${config.provider}/${config.model}`;
|
|
25632
25720
|
acc.stats = {
|
|
25633
25721
|
...acc.stats,
|
|
25634
|
-
reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort)
|
|
25635
|
-
contextSegments: {
|
|
25636
|
-
...acc.stats.contextSegments,
|
|
25637
|
-
system: estimateTokens(event.data.header.system ?? "")
|
|
25638
|
-
}
|
|
25722
|
+
reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort)
|
|
25639
25723
|
};
|
|
25640
25724
|
return true;
|
|
25641
25725
|
}
|
|
@@ -25700,6 +25784,7 @@ function materializeReplayView(acc, copy) {
|
|
|
25700
25784
|
plan: acc.plan,
|
|
25701
25785
|
permission: acc.permission,
|
|
25702
25786
|
title: acc.title,
|
|
25787
|
+
systemPrompt: acc.systemPrompt,
|
|
25703
25788
|
sandbox: acc.sandbox,
|
|
25704
25789
|
goal: acc.goal,
|
|
25705
25790
|
pending: {
|
|
@@ -25715,11 +25800,58 @@ function materializeReplayView(acc, copy) {
|
|
|
25715
25800
|
lastPruneTokens: acc.lastPruneTokens,
|
|
25716
25801
|
turnFiles: new Map([...acc.turnFiles].map(([turn, files]) => [turn, new Set(files)])),
|
|
25717
25802
|
turnSteps: new Map(acc.turnSteps),
|
|
25718
|
-
turnTools: new Map([...acc.turnTools].map(([turn, tools]) => [turn, new Set(tools)]))
|
|
25803
|
+
turnTools: new Map([...acc.turnTools].map(([turn, tools]) => [turn, new Set(tools)])),
|
|
25804
|
+
systemNodes: new Map(acc.systemNodes)
|
|
25719
25805
|
}
|
|
25720
25806
|
};
|
|
25721
25807
|
}
|
|
25722
25808
|
/**
|
|
25809
|
+
* Fold one process-local assistant-stream chunk frame (session-log v2+ keeps
|
|
25810
|
+
* durable logs settlement-only; live typing rides the `agent/assistant-stream`
|
|
25811
|
+
* agent event). Same first-token anchoring the durable `assistant/chunk` event
|
|
25812
|
+
* used to carry: the first non-empty delta anchors the TTFT and empty
|
|
25813
|
+
* keep-alive deltas do not count. The caller maps the frame's attempt to the
|
|
25814
|
+
* `turn:step` key (the start frame owns turn/step; chunk frames do not).
|
|
25815
|
+
* @param acc - the live replay accumulator.
|
|
25816
|
+
* @param key - the `turn:step` key the attempt's start frame declared.
|
|
25817
|
+
* @param time - the frame's safe-integer timestamp.
|
|
25818
|
+
* @param chunk - the model chunk the frame carries.
|
|
25819
|
+
* @returns whether the accumulator changed (the store stays silent otherwise).
|
|
25820
|
+
*/
|
|
25821
|
+
function applyAssistantStreamChunk(acc, key, time, chunk) {
|
|
25822
|
+
if (isTokenDelta(chunk) && !acc.firstChunkAt.has(key)) {
|
|
25823
|
+
acc.firstChunkAt.set(key, time);
|
|
25824
|
+
const started = acc.stepStart.get(key);
|
|
25825
|
+
if (started !== void 0) acc.stats = {
|
|
25826
|
+
...acc.stats,
|
|
25827
|
+
ttftMs: acc.stats.ttftMs + Math.max(0, time - started),
|
|
25828
|
+
ttftSteps: acc.stats.ttftSteps + 1
|
|
25829
|
+
};
|
|
25830
|
+
}
|
|
25831
|
+
if (chunk.type === "text-delta") {
|
|
25832
|
+
acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
|
|
25833
|
+
return true;
|
|
25834
|
+
}
|
|
25835
|
+
if (chunk.type === "reasoning-delta") {
|
|
25836
|
+
acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
|
|
25837
|
+
return true;
|
|
25838
|
+
}
|
|
25839
|
+
return false;
|
|
25840
|
+
}
|
|
25841
|
+
/**
|
|
25842
|
+
* Drop the live streaming tails without a settlement (an `agent/assistant-stream`
|
|
25843
|
+
* end frame with an `abandoned` outcome, or a session switch). The next start
|
|
25844
|
+
* frame rebuilds from scratch.
|
|
25845
|
+
* @param acc - the live replay accumulator.
|
|
25846
|
+
* @returns whether any tail text was discarded.
|
|
25847
|
+
*/
|
|
25848
|
+
function clearAssistantStream(acc) {
|
|
25849
|
+
const streamed = acc.streaming !== "" || acc.streamingReasoning !== "";
|
|
25850
|
+
acc.streaming = "";
|
|
25851
|
+
acc.streamingReasoning = "";
|
|
25852
|
+
return streamed;
|
|
25853
|
+
}
|
|
25854
|
+
/**
|
|
25723
25855
|
* The append-only flush boundary for a transcript view: the count of entries
|
|
25724
25856
|
* no later event can remove. Entries at or beyond this index are mutable and
|
|
25725
25857
|
* must stay in the live tree.
|
|
@@ -26364,8 +26496,60 @@ const DEEPSEEK_WAVE_STYLES = [
|
|
|
26364
26496
|
"aurora",
|
|
26365
26497
|
"pulse"
|
|
26366
26498
|
];
|
|
26367
|
-
/**
|
|
26368
|
-
|
|
26499
|
+
/**
|
|
26500
|
+
* The water surface: ONE continuous sine line spanning the whole band,
|
|
26501
|
+
* mirror-symmetric about the center column, its crests flowing OUTWARD from
|
|
26502
|
+
* the center (phase k·|x − center| − ω·t). No sweep window, no return trip —
|
|
26503
|
+
* the surface fades in, flows, and fades out, symmetric in both space and
|
|
26504
|
+
* time. The deepseek tier adds one faster, finer HARMONIC line whose crests
|
|
26505
|
+
* cross the fundamental's: interleaved richness with both lines still
|
|
26506
|
+
* symmetric and still only ever flowing outward.
|
|
26507
|
+
*/
|
|
26508
|
+
const WAVE_SURFACE_AMPLITUDE = .8;
|
|
26509
|
+
const WAVE_SURFACE_HARMONIC = .45;
|
|
26510
|
+
/** Vertical thickness in lane units — Aurora-wide: soft gradients, no hard edges. */
|
|
26511
|
+
const WAVE_SURFACE_THICKNESS = 1.2;
|
|
26512
|
+
/**
|
|
26513
|
+
* The mirrored second-hue profile: the space BELOW the surface carries a
|
|
26514
|
+
* second blue at this strength, so color (not just brightness) varies
|
|
26515
|
+
* continuously across the wave — Aurora-style hue mixing instead of a
|
|
26516
|
+
* single flat tint.
|
|
26517
|
+
*/
|
|
26518
|
+
const WAVE_SURFACE_MIRROR = .6;
|
|
26519
|
+
/** Aurora-style soft alpha: low gain, capped well under the pulse ring's. */
|
|
26520
|
+
const WAVE_SURFACE_ALPHA_GAIN = .45;
|
|
26521
|
+
const WAVE_SURFACE_ALPHA_CAP = .68;
|
|
26522
|
+
/**
|
|
26523
|
+
* Pulse ring geometry, softened to the Wave standard: a WIDE band
|
|
26524
|
+
* (half-width 5.5) with a moderate peak riding the radius — all inside the
|
|
26525
|
+
* hue blend, no hard white line — and an inner profile one hue over at a
|
|
26526
|
+
* slightly smaller radius, so the ring's color grades continuously across
|
|
26527
|
+
* its width (the radial analog of the water surface's mirrored hues).
|
|
26528
|
+
*/
|
|
26529
|
+
const PULSE_HALF_WIDTH = 5.5;
|
|
26530
|
+
const PULSE_PEAK_HALF_WIDTH = 1.8;
|
|
26531
|
+
const PULSE_PEAK_GAIN = .35;
|
|
26532
|
+
const PULSE_INNER_OFFSET = 2.5;
|
|
26533
|
+
const PULSE_INNER_STRENGTH = .6;
|
|
26534
|
+
/** The inner edge of each ring carries the tier's third blue. */
|
|
26535
|
+
const PULSE_INNER_HUE = 2;
|
|
26536
|
+
/**
|
|
26537
|
+
* The trailing echo ripple: every pulse ring drags a second, weaker ring at
|
|
26538
|
+
* a fraction of its radius in the NEXT hue of the tier's blues, fading in a
|
|
26539
|
+
* little after the primary so the center hole opens first.
|
|
26540
|
+
*/
|
|
26541
|
+
const PULSE_ECHO_RADIUS = .7;
|
|
26542
|
+
const PULSE_ECHO_STRENGTH = .65;
|
|
26543
|
+
const PULSE_ECHO_DELAY = .12;
|
|
26544
|
+
/** Aurora-grade soft alpha for the detonation — a notch above the swell. */
|
|
26545
|
+
const PULSE_ALPHA_GAIN = .45;
|
|
26546
|
+
const PULSE_ALPHA_CAP = .72;
|
|
26547
|
+
/**
|
|
26548
|
+
* Terminal cell aspect (row height ÷ column width, ≈2.2 for common fonts).
|
|
26549
|
+
* A ring computed in raw cell units looks vertically squashed; weighting row
|
|
26550
|
+
* distance by the aspect makes the Pulse ring appear circular on screen.
|
|
26551
|
+
*/
|
|
26552
|
+
const PULSE_ROW_ASPECT = 2.2;
|
|
26369
26553
|
/** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
|
|
26370
26554
|
const SPARK_START_MS = 900;
|
|
26371
26555
|
const SPARK_FRAME_MS = 100;
|
|
@@ -26509,14 +26693,17 @@ function deepseekWaveStyleRandom(previous) {
|
|
|
26509
26693
|
return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
|
|
26510
26694
|
}
|
|
26511
26695
|
/**
|
|
26512
|
-
* Tier for a `provider/model` label: a
|
|
26696
|
+
* Tier for a `provider/model` label: a MODEL ID containing `flash` runs the
|
|
26513
26697
|
* single-band flash tier; everything else (pro/reasoner/chat) runs the
|
|
26514
|
-
* dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
|
|
26698
|
+
* dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping. Only the model
|
|
26699
|
+
* segment (after the `/`) is matched, so a provider whose name contains
|
|
26700
|
+
* `flash` cannot flip an unrelated model onto the flash tier.
|
|
26515
26701
|
* @param model - the `provider/model` label of the applied model.
|
|
26516
26702
|
* @returns the wave tier for that model.
|
|
26517
26703
|
*/
|
|
26518
26704
|
function deepseekWaveTier(model) {
|
|
26519
|
-
|
|
26705
|
+
const slash = model.indexOf("/");
|
|
26706
|
+
return (slash < 0 ? model : model.slice(slash + 1)).toLowerCase().includes("flash") ? "flash" : "deepseek";
|
|
26520
26707
|
}
|
|
26521
26708
|
/**
|
|
26522
26709
|
* Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
|
|
@@ -26529,18 +26716,6 @@ function crest(distance) {
|
|
|
26529
26716
|
return .5 * (1 + Math.cos(Math.PI * distance));
|
|
26530
26717
|
}
|
|
26531
26718
|
/**
|
|
26532
|
-
* Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
|
|
26533
|
-
* the middle, so the crest accelerates and eases instead of sliding linearly.
|
|
26534
|
-
* @param progress - raw progress (clamped to 0..1).
|
|
26535
|
-
* @returns the eased progress in 0..1.
|
|
26536
|
-
*/
|
|
26537
|
-
function easeInOut(progress) {
|
|
26538
|
-
const p = Math.min(1, Math.max(0, progress));
|
|
26539
|
-
if (p < .5) return 4 * p * p * p;
|
|
26540
|
-
const inverse = -2 * p + 2;
|
|
26541
|
-
return 1 - inverse * inverse * inverse / 2;
|
|
26542
|
-
}
|
|
26543
|
-
/**
|
|
26544
26719
|
* Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
|
|
26545
26720
|
* at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
|
|
26546
26721
|
* total. The Wave style keeps the envelope at 1 (Codex paints Wave without
|
|
@@ -26558,38 +26733,98 @@ function envelope(elapsed, total, fadeIn, fadeOut) {
|
|
|
26558
26733
|
return Math.min(Math.max(Math.min(rise, fall), 0), 1);
|
|
26559
26734
|
}
|
|
26560
26735
|
/**
|
|
26561
|
-
* One band's
|
|
26562
|
-
*
|
|
26563
|
-
*
|
|
26564
|
-
*
|
|
26736
|
+
* One band's contributions at a column — Codex `band_sample`, redesigned:
|
|
26737
|
+
* Wave is a WATER SURFACE — one continuous sine line, mirror-symmetric
|
|
26738
|
+
* about the center column, crests flowing outward from the center (band 1
|
|
26739
|
+
* of the deepseek tier is a faster harmonic line crossing it). Pulse
|
|
26740
|
+
* detonates in TWO dimensions: soft rings that keep expanding through a
|
|
26741
|
+
* symmetric fade envelope, color grading across each ring's width, with a
|
|
26742
|
+
* trailing echo ripple. Aurora matches Codex verbatim.
|
|
26565
26743
|
* @param style - the ignition style.
|
|
26566
26744
|
* @param band - the band triple (meaning depends on the style).
|
|
26567
26745
|
* @param elapsed - seconds since the animation started.
|
|
26568
26746
|
* @param column - column index in the content row (0..width-1).
|
|
26569
26747
|
* @param width - content-row width in columns.
|
|
26570
|
-
* @
|
|
26748
|
+
* @param context - band geometry (row position, undulation flag, pulse span).
|
|
26749
|
+
* @returns one or two `[hueIndex, strength, core]` contributions.
|
|
26571
26750
|
*/
|
|
26572
|
-
function bandSample(style, band, elapsed, column, width) {
|
|
26751
|
+
function bandSample(style, band, elapsed, column, width, context) {
|
|
26573
26752
|
const [first, second, third] = band;
|
|
26574
26753
|
switch (style) {
|
|
26575
26754
|
case "wave": {
|
|
26576
|
-
const
|
|
26577
|
-
|
|
26578
|
-
const
|
|
26579
|
-
|
|
26755
|
+
const fadeIn = first;
|
|
26756
|
+
const fadeOut = Math.max(.05, context.total - (first + second));
|
|
26757
|
+
const fade = envelope(elapsed, context.total, fadeIn, fadeOut);
|
|
26758
|
+
if (fade <= .01) return [[
|
|
26759
|
+
0,
|
|
26760
|
+
0,
|
|
26761
|
+
0
|
|
26762
|
+
]];
|
|
26763
|
+
const harmonic = context.bandIndex % 2 === 1;
|
|
26764
|
+
const wavelength = harmonic ? 40 / 1.5 : 40;
|
|
26765
|
+
const omega = (harmonic ? 1.5 : 1) * 9;
|
|
26766
|
+
const amplitude = (harmonic ? WAVE_SURFACE_HARMONIC : 1) * WAVE_SURFACE_AMPLITUDE;
|
|
26767
|
+
const d = Math.abs(column - (width - 1) / 2);
|
|
26768
|
+
const surface = amplitude * Math.sin(Math.PI * 2 * d / wavelength - omega * elapsed + (harmonic ? Math.PI / 2 : 0));
|
|
26769
|
+
const fromSurface = Math.abs(context.u - surface);
|
|
26770
|
+
const vertical = context.undulating ? crest(fromSurface / WAVE_SURFACE_THICKNESS) : 1;
|
|
26771
|
+
const mirrorHue = harmonic ? 1 : 2;
|
|
26772
|
+
const below = context.undulating ? crest(Math.abs(context.u + surface) / WAVE_SURFACE_THICKNESS) : vertical;
|
|
26773
|
+
return [[
|
|
26774
|
+
0,
|
|
26775
|
+
fade * vertical,
|
|
26776
|
+
0
|
|
26777
|
+
], [
|
|
26778
|
+
mirrorHue,
|
|
26779
|
+
fade * below * WAVE_SURFACE_MIRROR,
|
|
26780
|
+
0
|
|
26781
|
+
]];
|
|
26580
26782
|
}
|
|
26581
26783
|
case "aurora": {
|
|
26582
26784
|
const center = (.5 + .38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width;
|
|
26583
26785
|
const halfWidth = Math.max(width * .22, 4);
|
|
26584
|
-
return [
|
|
26786
|
+
return [[
|
|
26787
|
+
Math.trunc(third),
|
|
26788
|
+
crest(Math.abs(column - center) / halfWidth),
|
|
26789
|
+
0
|
|
26790
|
+
]];
|
|
26585
26791
|
}
|
|
26586
26792
|
case "pulse": {
|
|
26587
|
-
const
|
|
26588
|
-
|
|
26589
|
-
const
|
|
26590
|
-
const
|
|
26591
|
-
|
|
26592
|
-
|
|
26793
|
+
const launch = first;
|
|
26794
|
+
const travel = second;
|
|
26795
|
+
const fadeOut = Math.max(.05, context.total - (launch + travel));
|
|
26796
|
+
const fade = envelope(elapsed, context.total, launch, fadeOut);
|
|
26797
|
+
if (fade <= .01) return [[
|
|
26798
|
+
0,
|
|
26799
|
+
0,
|
|
26800
|
+
0
|
|
26801
|
+
]];
|
|
26802
|
+
const progress = (elapsed - launch) / travel;
|
|
26803
|
+
const radius = (1 - (1 - progress) ** 3) * context.pulseSpan;
|
|
26804
|
+
const decay = third * (1 - .35 * Math.min(Math.max(progress, 0), 1));
|
|
26805
|
+
const distance = Math.hypot(column - width / 2, context.dy);
|
|
26806
|
+
const fromRing = Math.abs(distance - radius);
|
|
26807
|
+
const band = crest(fromRing / PULSE_HALF_WIDTH) + PULSE_PEAK_GAIN * crest(fromRing / PULSE_PEAK_HALF_WIDTH);
|
|
26808
|
+
const inner = crest(Math.abs(distance - (radius - PULSE_INNER_OFFSET)) / PULSE_HALF_WIDTH);
|
|
26809
|
+
const echoGate = envelope(elapsed, context.total, launch + PULSE_ECHO_DELAY, fadeOut);
|
|
26810
|
+
const echo = crest(Math.abs(distance - radius * PULSE_ECHO_RADIUS) / PULSE_HALF_WIDTH) * PULSE_ECHO_STRENGTH * echoGate;
|
|
26811
|
+
return [
|
|
26812
|
+
[
|
|
26813
|
+
context.bandIndex,
|
|
26814
|
+
fade * band * decay,
|
|
26815
|
+
0
|
|
26816
|
+
],
|
|
26817
|
+
[
|
|
26818
|
+
PULSE_INNER_HUE,
|
|
26819
|
+
fade * inner * decay * PULSE_INNER_STRENGTH,
|
|
26820
|
+
0
|
|
26821
|
+
],
|
|
26822
|
+
[
|
|
26823
|
+
context.bandIndex + 1,
|
|
26824
|
+
fade * echo * decay,
|
|
26825
|
+
0
|
|
26826
|
+
]
|
|
26827
|
+
];
|
|
26593
26828
|
}
|
|
26594
26829
|
}
|
|
26595
26830
|
}
|
|
@@ -26602,9 +26837,10 @@ function blendRgb(fg, bg, alpha) {
|
|
|
26602
26837
|
];
|
|
26603
26838
|
}
|
|
26604
26839
|
/**
|
|
26605
|
-
*
|
|
26606
|
-
* and the bottom row last, sweeping down the band. 0.12
|
|
26607
|
-
* row's lag inside the 200ms duration extension.
|
|
26840
|
+
* Aurora-only per-row phase share of the duration: its drifting bands reach
|
|
26841
|
+
* the top row first and the bottom row last, sweeping down the band. 0.12
|
|
26842
|
+
* keeps the bottom row's lag inside the 200ms duration extension. Wave and
|
|
26843
|
+
* Pulse deliberately share one timeline (see `deepseekWaveColumnBg`).
|
|
26608
26844
|
*/
|
|
26609
26845
|
const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
26610
26846
|
/**
|
|
@@ -26615,9 +26851,13 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
|
26615
26851
|
* blends the mixed hue toward the blank-cell base at the style's alpha cap,
|
|
26616
26852
|
* and Aurora applies its own fade envelope. Returns `null` when the column
|
|
26617
26853
|
* should stay transparent, so the row returns to no `backgroundColor` on
|
|
26618
|
-
* both ends. With `rows > 1
|
|
26619
|
-
*
|
|
26620
|
-
*
|
|
26854
|
+
* both ends. With `rows > 1`: Wave is a water surface — every column
|
|
26855
|
+
* lights the row nearest the surface's current height, so the light reads
|
|
26856
|
+
* as ONE continuous wavy line spanning the band, symmetric about the center
|
|
26857
|
+
* column and flowing outward (a single-row band falls back to a flat glow);
|
|
26858
|
+
* Pulse rings in two dimensions around the band's center cell with trailing
|
|
26859
|
+
* echo ripples; only Aurora samples the timeline shifted by a per-row phase
|
|
26860
|
+
* offset.
|
|
26621
26861
|
* @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
|
|
26622
26862
|
* @param column - column index in the content row (0..width-1).
|
|
26623
26863
|
* @param width - content-row width in columns.
|
|
@@ -26631,16 +26871,28 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
|
26631
26871
|
*/
|
|
26632
26872
|
function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row = 0, rows = 1) {
|
|
26633
26873
|
const total = deepseekWaveBaseDuration(tier, style) / 1e3;
|
|
26634
|
-
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE;
|
|
26874
|
+
const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (style === "aurora" ? (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE : 0);
|
|
26875
|
+
const dy = style === "pulse" ? (row - (rows - 1) / 2) * PULSE_ROW_ASPECT : 0;
|
|
26876
|
+
const undulating = rows > 1;
|
|
26877
|
+
const u = undulating ? (row - (rows - 1) / 2) / ((rows - 1) / 2) : 0;
|
|
26878
|
+
const pulseSpan = Math.hypot(width / 2, (rows - 1) / 2 * PULSE_ROW_ASPECT);
|
|
26635
26879
|
const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
|
|
26636
26880
|
const weights = [
|
|
26637
26881
|
0,
|
|
26638
26882
|
0,
|
|
26639
26883
|
0
|
|
26640
26884
|
];
|
|
26885
|
+
let bandIndex = 0;
|
|
26641
26886
|
for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
|
|
26642
|
-
const [hue, strength]
|
|
26643
|
-
|
|
26887
|
+
for (const [hue, strength] of bandSample(style, band, elapsed, column, width, {
|
|
26888
|
+
dy,
|
|
26889
|
+
u,
|
|
26890
|
+
undulating,
|
|
26891
|
+
pulseSpan,
|
|
26892
|
+
bandIndex,
|
|
26893
|
+
total
|
|
26894
|
+
})) weights[hue] = style === "aurora" ? weights[hue] + strength : Math.max(weights[hue], strength);
|
|
26895
|
+
bandIndex += 1;
|
|
26644
26896
|
}
|
|
26645
26897
|
const weight = weights[0] + weights[1] + weights[2];
|
|
26646
26898
|
if (weight <= .01) return null;
|
|
@@ -26657,7 +26909,7 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row
|
|
|
26657
26909
|
Math.round(green / weight),
|
|
26658
26910
|
Math.round(blue / weight)
|
|
26659
26911
|
];
|
|
26660
|
-
const alpha = style === "aurora" ? Math.min(weight * .4, .5) * fade : weight * .
|
|
26912
|
+
const alpha = style === "aurora" ? Math.min(weight * .4, .5) * fade : style === "wave" ? Math.min(weight * WAVE_SURFACE_ALPHA_GAIN, WAVE_SURFACE_ALPHA_CAP) : Math.min(weight * PULSE_ALPHA_GAIN, PULSE_ALPHA_CAP);
|
|
26661
26913
|
if (alpha < .02) return null;
|
|
26662
26914
|
return blendRgb(mixed, base, alpha);
|
|
26663
26915
|
}
|
|
@@ -26742,6 +26994,32 @@ function effortAboveHigh(effort) {
|
|
|
26742
26994
|
const rank = EFFORT_RANK[effort.trim().toLowerCase()];
|
|
26743
26995
|
return rank !== void 0 && rank > 3;
|
|
26744
26996
|
}
|
|
26997
|
+
/**
|
|
26998
|
+
* Parse a persisted animations preference (`animations.json`): timed
|
|
26999
|
+
* animations are on by default and only an explicit `false` disables them —
|
|
27000
|
+
* a missing key, corrupt value, or absent file all mean enabled, so the
|
|
27001
|
+
* /animation toggle degrades exactly like every other user preference.
|
|
27002
|
+
* @param value - the raw parsed JSON value (expected boolean).
|
|
27003
|
+
* @returns whether timed animations should run.
|
|
27004
|
+
*/
|
|
27005
|
+
function parseAnimationsPref(value) {
|
|
27006
|
+
return value !== false;
|
|
27007
|
+
}
|
|
27008
|
+
/**
|
|
27009
|
+
* One parsed `/animation` argument: '' toggles, `on|true|1` enables,
|
|
27010
|
+
* `off|false|0` disables (case-insensitive, surrounding whitespace ignored),
|
|
27011
|
+
* and anything else is a usage error the caller surfaces. Kept pure so the
|
|
27012
|
+
* command's entire decision table is unit-testable.
|
|
27013
|
+
* @param argument - the raw text after `/animation`.
|
|
27014
|
+
* @returns `{ enabled }`, `'toggle'`, or `'usage'`.
|
|
27015
|
+
*/
|
|
27016
|
+
function parseAnimationsArgument(argument) {
|
|
27017
|
+
const normalized = argument.trim().toLowerCase();
|
|
27018
|
+
if (normalized === "") return "toggle";
|
|
27019
|
+
if (normalized === "on" || normalized === "true" || normalized === "1") return { enabled: true };
|
|
27020
|
+
if (normalized === "off" || normalized === "false" || normalized === "0") return { enabled: false };
|
|
27021
|
+
return "usage";
|
|
27022
|
+
}
|
|
26745
27023
|
//#endregion
|
|
26746
27024
|
//#region src/commands.ts
|
|
26747
27025
|
/**
|
|
@@ -26760,17 +27038,41 @@ function watchCommands(ctx) {
|
|
|
26760
27038
|
let error;
|
|
26761
27039
|
let loadedFor;
|
|
26762
27040
|
const listeners = /* @__PURE__ */ new Set();
|
|
27041
|
+
const descriptorFingerprint = (list) => JSON.stringify(list.map((descriptor) => [
|
|
27042
|
+
descriptor.name,
|
|
27043
|
+
descriptor.description,
|
|
27044
|
+
descriptor.input?.hint ?? "",
|
|
27045
|
+
descriptor.input?.attachments === true
|
|
27046
|
+
]));
|
|
27047
|
+
let lastFingerprint = "[]";
|
|
27048
|
+
let lastNotifiedError;
|
|
27049
|
+
const changed = (next, nextError) => descriptorFingerprint(next) !== lastFingerprint || nextError !== lastNotifiedError;
|
|
27050
|
+
let notifyScheduled = false;
|
|
27051
|
+
const notify = () => {
|
|
27052
|
+
if (notifyScheduled) return;
|
|
27053
|
+
notifyScheduled = true;
|
|
27054
|
+
setImmediate(() => {
|
|
27055
|
+
notifyScheduled = false;
|
|
27056
|
+
for (const listener of listeners) listener();
|
|
27057
|
+
});
|
|
27058
|
+
};
|
|
26763
27059
|
const refresh = () => {
|
|
26764
27060
|
if (commands === void 0 || agent === void 0) return;
|
|
27061
|
+
let next;
|
|
27062
|
+
let nextError;
|
|
26765
27063
|
try {
|
|
26766
|
-
|
|
27064
|
+
next = commands.list(agent);
|
|
26767
27065
|
loadedFor = agent;
|
|
26768
|
-
error = void 0;
|
|
26769
27066
|
} catch (cause) {
|
|
26770
|
-
|
|
26771
|
-
|
|
27067
|
+
next = loadedFor === agent ? [...descriptors] : [];
|
|
27068
|
+
nextError = cause instanceof Error ? cause.message : String(cause);
|
|
26772
27069
|
}
|
|
26773
|
-
|
|
27070
|
+
if (!changed(next, nextError)) return;
|
|
27071
|
+
descriptors = next;
|
|
27072
|
+
error = nextError;
|
|
27073
|
+
lastFingerprint = descriptorFingerprint(next);
|
|
27074
|
+
lastNotifiedError = nextError;
|
|
27075
|
+
notify();
|
|
26774
27076
|
};
|
|
26775
27077
|
if (commands !== void 0) ctx.on("commands/change", () => refresh());
|
|
26776
27078
|
return {
|
|
@@ -26818,6 +27120,70 @@ function submissionPayload(line) {
|
|
|
26818
27120
|
return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines;
|
|
26819
27121
|
}
|
|
26820
27122
|
//#endregion
|
|
27123
|
+
//#region src/render/fuzzy.ts
|
|
27124
|
+
/** Extra weight for name starts and separator boundaries. */
|
|
27125
|
+
function boundaryBonus(name, index) {
|
|
27126
|
+
return index === 0 || name.charAt(index - 1) === "-" || name.charAt(index - 1) === "_" ? 8 : 0;
|
|
27127
|
+
}
|
|
27128
|
+
/**
|
|
27129
|
+
* Score the strongest ordered-subsequence alignment in O(name × query).
|
|
27130
|
+
* Boundary and adjacent matches earn weight; skipped and leading characters
|
|
27131
|
+
* cost weight. Undefined when the query is not a subsequence of the name.
|
|
27132
|
+
*/
|
|
27133
|
+
function alignmentScore(name, query) {
|
|
27134
|
+
if (query.length > name.length) return void 0;
|
|
27135
|
+
const noMatch = Number.NEGATIVE_INFINITY;
|
|
27136
|
+
let previous = Array(name.length).fill(noMatch);
|
|
27137
|
+
for (let index = 0; index < name.length; index++) if (name.charAt(index) === query.charAt(0)) previous[index] = 1 + boundaryBonus(name, index) - index;
|
|
27138
|
+
for (let queryIndex = 1; queryIndex < query.length; queryIndex++) {
|
|
27139
|
+
const current = Array(name.length).fill(noMatch);
|
|
27140
|
+
let left = noMatch;
|
|
27141
|
+
let leftLeft = noMatch;
|
|
27142
|
+
let bestGapped = noMatch;
|
|
27143
|
+
for (const [index, prior] of previous.entries()) {
|
|
27144
|
+
if (leftLeft !== noMatch) bestGapped = Math.max(bestGapped, leftLeft + index - 2);
|
|
27145
|
+
if (name.charAt(index) === query.charAt(queryIndex)) {
|
|
27146
|
+
const bonus = 1 + boundaryBonus(name, index);
|
|
27147
|
+
let score = noMatch;
|
|
27148
|
+
if (left !== noMatch) score = left + bonus + 4;
|
|
27149
|
+
if (bestGapped !== noMatch) score = Math.max(score, bestGapped + bonus + 1 - index);
|
|
27150
|
+
current[index] = score;
|
|
27151
|
+
}
|
|
27152
|
+
leftLeft = left;
|
|
27153
|
+
left = prior;
|
|
27154
|
+
}
|
|
27155
|
+
previous = current;
|
|
27156
|
+
}
|
|
27157
|
+
let best = noMatch;
|
|
27158
|
+
for (const score of previous) best = Math.max(best, score);
|
|
27159
|
+
return best === noMatch ? void 0 : best;
|
|
27160
|
+
}
|
|
27161
|
+
/**
|
|
27162
|
+
* Rank named items by a menu query.
|
|
27163
|
+
* @param items - candidates in source order (the caller's composition order
|
|
27164
|
+
* is the final tie-breaker, e.g. local commands before registry entries).
|
|
27165
|
+
* @param rawQuery - the text typed after the trigger, matched case-insensitively.
|
|
27166
|
+
* @returns the matching items: prefix hits first, then by alignment score,
|
|
27167
|
+
* then in source order. The input list itself for an empty query.
|
|
27168
|
+
*/
|
|
27169
|
+
function rankByName(items, rawQuery) {
|
|
27170
|
+
const query = rawQuery.toLowerCase();
|
|
27171
|
+
if (query === "") return items;
|
|
27172
|
+
const ranked = [];
|
|
27173
|
+
items.forEach((item, index) => {
|
|
27174
|
+
const name = item.name.toLowerCase();
|
|
27175
|
+
const score = alignmentScore(name, query);
|
|
27176
|
+
if (score !== void 0) ranked.push({
|
|
27177
|
+
item,
|
|
27178
|
+
index,
|
|
27179
|
+
prefix: name.startsWith(query),
|
|
27180
|
+
score
|
|
27181
|
+
});
|
|
27182
|
+
});
|
|
27183
|
+
ranked.sort((left, right) => Number(right.prefix) - Number(left.prefix) || right.score - left.score || left.index - right.index);
|
|
27184
|
+
return ranked.map((match) => match.item);
|
|
27185
|
+
}
|
|
27186
|
+
//#endregion
|
|
26821
27187
|
//#region src/provider-settings.ts
|
|
26822
27188
|
/** Human text for a rejection value (mirrors the web page's `messageOf`). */
|
|
26823
27189
|
function messageOf$1(error) {
|
|
@@ -27027,7 +27393,8 @@ async function loadProviderSettings(ctx) {
|
|
|
27027
27393
|
active: active.has(entry.provider),
|
|
27028
27394
|
settingsNs: entry.settingsNs,
|
|
27029
27395
|
settingsPath: entry.settingsPath,
|
|
27030
|
-
...entry.declared === void 0 ? {} : { declared: entry.declared }
|
|
27396
|
+
...entry.declared === void 0 ? {} : { declared: entry.declared },
|
|
27397
|
+
...entry.error === void 0 ? {} : { error: singleLine$1(entry.error) }
|
|
27031
27398
|
})), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
|
|
27032
27399
|
provider: provider.id,
|
|
27033
27400
|
displayName: provider.name,
|
|
@@ -27052,7 +27419,8 @@ async function loadProviderSettings(ctx) {
|
|
|
27052
27419
|
configuration: configurationOf(profile),
|
|
27053
27420
|
...credentialRef === void 0 ? {} : { credentialRef },
|
|
27054
27421
|
suggestedRef: deriveCredentialRef(base.provider),
|
|
27055
|
-
...base.declared === void 0 ? {} : { declared: base.declared }
|
|
27422
|
+
...base.declared === void 0 ? {} : { declared: base.declared },
|
|
27423
|
+
...base.error === void 0 ? {} : { diagnostic: base.error }
|
|
27056
27424
|
};
|
|
27057
27425
|
});
|
|
27058
27426
|
const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
|
|
@@ -27479,9 +27847,8 @@ function mergeSessionTitles(rows, observations) {
|
|
|
27479
27847
|
}
|
|
27480
27848
|
/**
|
|
27481
27849
|
* Encode a session id the way the JSONL backend does for its on-disk layout
|
|
27482
|
-
* (`encodeSegment`: safe units literal, everything else `~XXXX`). Used
|
|
27483
|
-
* validate
|
|
27484
|
-
* any deletion touches the filesystem — a local copy of the pure upstream
|
|
27850
|
+
* (`encodeSegment`: safe units literal, everything else `~XXXX`). Used to
|
|
27851
|
+
* validate and derive session directories — a local copy of the pure upstream
|
|
27485
27852
|
* contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
|
|
27486
27853
|
*/
|
|
27487
27854
|
function encodeSessionSegment(raw) {
|
|
@@ -27497,21 +27864,91 @@ function encodeSessionSegment(raw) {
|
|
|
27497
27864
|
}
|
|
27498
27865
|
return out;
|
|
27499
27866
|
}
|
|
27500
|
-
/** The session-log artifact names the JSONL backend may create. */
|
|
27501
|
-
const SESSION_ARTIFACT_NAMES = ["session.jsonl", "session.jsonl.zstd"];
|
|
27502
27867
|
/**
|
|
27503
|
-
*
|
|
27504
|
-
*
|
|
27505
|
-
*
|
|
27506
|
-
*
|
|
27507
|
-
|
|
27508
|
-
|
|
27868
|
+
* Encode a project cwd the way the JSONL backend groups sessions on disk
|
|
27869
|
+
* (`projectKey`: separators collapse to one `-`, everything else mirrors
|
|
27870
|
+
* `encodeSegment`, bounded to 251 chars). A local copy of the pure upstream
|
|
27871
|
+
* contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
|
|
27872
|
+
*/
|
|
27873
|
+
function encodeProjectKey(cwd) {
|
|
27874
|
+
if (cwd.length === 0) throw new Error("cannot encode an empty project path");
|
|
27875
|
+
let readable = "";
|
|
27876
|
+
let separatorRun = false;
|
|
27877
|
+
for (let i = 0; i < cwd.length; i += 1) {
|
|
27878
|
+
const code = cwd.charCodeAt(i);
|
|
27879
|
+
const ch = String.fromCharCode(code);
|
|
27880
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
27881
|
+
if (!separatorRun) readable += "-";
|
|
27882
|
+
separatorRun = true;
|
|
27883
|
+
} else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
27884
|
+
readable += ch;
|
|
27885
|
+
separatorRun = false;
|
|
27886
|
+
} else {
|
|
27887
|
+
readable += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
27888
|
+
separatorRun = false;
|
|
27889
|
+
}
|
|
27890
|
+
}
|
|
27891
|
+
return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
|
|
27892
|
+
}
|
|
27893
|
+
/** The project-level directory name the JSONL backend uses for a missing cwd. */
|
|
27894
|
+
const NO_CWD_DIRECTORY = "_no-cwd";
|
|
27895
|
+
/**
|
|
27896
|
+
* Derive one session's artifact directory under the JSONL backend root,
|
|
27897
|
+
* mirroring the upstream `<root>/<projectKey(cwd)>/<encodeSegment(id)>/`
|
|
27898
|
+
* layout (0.1.5 `sessionDir`/`projectDir`).
|
|
27899
|
+
* @param root - the JSONL backend's configured session root.
|
|
27900
|
+
* @param cwd - the session's pinned working directory, when the header has one.
|
|
27901
|
+
* @param id - the session id.
|
|
27902
|
+
* @returns the absolute session directory path.
|
|
27903
|
+
*/
|
|
27904
|
+
function sessionDirectoryFor(root, cwd, id) {
|
|
27905
|
+
const project = cwd === void 0 || cwd === "" ? NO_CWD_DIRECTORY : encodeProjectKey(cwd);
|
|
27906
|
+
return resolve(root, project, encodeSessionSegment(id));
|
|
27907
|
+
}
|
|
27908
|
+
/**
|
|
27909
|
+
* The canonical session-log artifact filenames the JSONL backend may create:
|
|
27910
|
+
* format v0 writes the bare `session.jsonl` name; v1+ write
|
|
27911
|
+
* `session.vN.jsonl`, each generation optionally zstd-compressed. Multiple
|
|
27912
|
+
* immutable generations may coexist in one session directory (0.1.5). The
|
|
27913
|
+
* range follows the installed session package's `SESSION_FORMAT_VERSION`, so
|
|
27914
|
+
* a future generation joins the enumeration with the dependency bump.
|
|
27915
|
+
*/
|
|
27916
|
+
function sessionArtifactNames() {
|
|
27917
|
+
const names = ["session.jsonl", "session.jsonl.zstd"];
|
|
27918
|
+
for (let version = 1; version <= SESSION_FORMAT_VERSION; version += 1) names.push(`session.v${version}.jsonl`, `session.v${version}.jsonl.zstd`);
|
|
27919
|
+
return names;
|
|
27920
|
+
}
|
|
27921
|
+
/** Canonical generation-log filenames as a lookup set (bare v0 or `vN`-suffixed, ± zstd). */
|
|
27922
|
+
const SESSION_ARTIFACT_NAME_SET = new Set(sessionArtifactNames());
|
|
27923
|
+
/** True for one canonical session-log artifact filename the backend may own. */
|
|
27924
|
+
function isSessionArtifactName(name) {
|
|
27925
|
+
return SESSION_ARTIFACT_NAME_SET.has(name);
|
|
27926
|
+
}
|
|
27927
|
+
/**
|
|
27928
|
+
* Guard a derived session directory before deletion (codex's scoped-path
|
|
27929
|
+
* check, adapted to the JSONL layout): the directory's base name must be
|
|
27930
|
+
* exactly `encodeSegment(id)` beneath its project grouping.
|
|
27931
|
+
* @param dir - the derived session artifact directory.
|
|
27932
|
+
* @param id - the session id the directory claims to belong to.
|
|
27933
|
+
* @returns the guarded directory, or undefined when the layout is unexpected.
|
|
27509
27934
|
*/
|
|
27510
|
-
function sessionArtifactDirectory(
|
|
27511
|
-
if (basename(artifact) !== "session.jsonl" && basename(artifact) !== "session.jsonl.zstd") return void 0;
|
|
27512
|
-
const dir = dirname(artifact);
|
|
27935
|
+
function sessionArtifactDirectory(dir, id) {
|
|
27513
27936
|
if (basename(dir) !== encodeSessionSegment(id)) return void 0;
|
|
27514
|
-
return dir;
|
|
27937
|
+
if (basename(dirname(dir)) === NO_CWD_DIRECTORY) return dir;
|
|
27938
|
+
return /^--.*--$|^~/.test(basename(dirname(dir))) ? dir : void 0;
|
|
27939
|
+
}
|
|
27940
|
+
/**
|
|
27941
|
+
* The JSONL backend's configured session root, when the mounted backend
|
|
27942
|
+
* exposes one. The upstream service contract dropped `locate()` in 0.1.5
|
|
27943
|
+
* (artifact paths are backend-private; only refusal diagnostics carry them),
|
|
27944
|
+
* so the TUI derives artifact paths from the backend's public plugin config.
|
|
27945
|
+
* Backends without a JSONL-style config (or a foreign shape) yield undefined
|
|
27946
|
+
* and callers degrade: mtime sorting falls back to createdAt and /delete
|
|
27947
|
+
* refuses, exactly as before.
|
|
27948
|
+
*/
|
|
27949
|
+
function jsonlSessionRoot(persistence) {
|
|
27950
|
+
const root = persistence?.config?.root;
|
|
27951
|
+
return typeof root === "string" && root !== "" ? root : void 0;
|
|
27515
27952
|
}
|
|
27516
27953
|
/**
|
|
27517
27954
|
* Collect one session's deletion subtree: the id plus every record whose
|
|
@@ -30061,6 +30498,26 @@ function parseHistoryFile(raw, max = 100) {
|
|
|
30061
30498
|
return kept.slice(-max);
|
|
30062
30499
|
}
|
|
30063
30500
|
/**
|
|
30501
|
+
* The append unit for the persistent file: one JSON line, so a multi-line
|
|
30502
|
+
* draft still occupies exactly one physical line. Each submission appends
|
|
30503
|
+
* this unit at the end of the file, so concurrent terminals add entries
|
|
30504
|
+
* after each other. Node chunks one append at 512 KiB: a pasted entry
|
|
30505
|
+
* beyond that size could interleave mid-line with another writer's
|
|
30506
|
+
* chunks, and the damaged line then drops out at the next parse —
|
|
30507
|
+
* recall tolerates the loss by design.
|
|
30508
|
+
*/
|
|
30509
|
+
function historyLine(text) {
|
|
30510
|
+
return serializeHistoryEntry(text) + "\n";
|
|
30511
|
+
}
|
|
30512
|
+
/**
|
|
30513
|
+
* Whether the file on disk differs from its canonical form (deduped and
|
|
30514
|
+
* capped). True means stale lines have accumulated and the next boot
|
|
30515
|
+
* should rewrite it once, atomically.
|
|
30516
|
+
*/
|
|
30517
|
+
function needsCompaction(raw, max = 100) {
|
|
30518
|
+
return serializeHistoryList(parseHistoryFile(raw, max)) !== raw;
|
|
30519
|
+
}
|
|
30520
|
+
/**
|
|
30064
30521
|
* Record one in-session submission: empty text is ignored and an adjacent
|
|
30065
30522
|
* duplicate collapses (Codex `record_local_submission` semantics). The local
|
|
30066
30523
|
* pool shares the persistent pool's cap so the recall space stays bounded.
|
|
@@ -30075,10 +30532,10 @@ function recordLocalEntry(local, text, max = 100) {
|
|
|
30075
30532
|
return [...local, text].slice(-max);
|
|
30076
30533
|
}
|
|
30077
30534
|
/**
|
|
30078
|
-
* Serialize a capped entry list to the history file format (one JSON line
|
|
30079
|
-
* entry, trailing newline). The
|
|
30080
|
-
*
|
|
30081
|
-
*
|
|
30535
|
+
* Serialize a capped entry list to the history file format (one JSON line
|
|
30536
|
+
* per entry, trailing newline). The boot-time compaction writes this
|
|
30537
|
+
* canonical form once when stale lines have accumulated; submissions
|
|
30538
|
+
* themselves only ever append a single line.
|
|
30082
30539
|
* @param entries - the entries to persist, oldest first.
|
|
30083
30540
|
* @returns the file content, '' for an empty list.
|
|
30084
30541
|
*/
|
|
@@ -30565,7 +31022,14 @@ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
|
|
|
30565
31022
|
}
|
|
30566
31023
|
//#endregion
|
|
30567
31024
|
//#region src/attachments.ts
|
|
30568
|
-
/** Terminal image-file adapter over the Harness durable attachment service. */
|
|
31025
|
+
/** Terminal image- and file-attachment adapter over the Harness durable attachment service. */
|
|
31026
|
+
/**
|
|
31027
|
+
* Terminal-side file admission bounds. Upstream exposes image limits through
|
|
31028
|
+
* the attachment service but no file limits (files ride verbatim storage);
|
|
31029
|
+
* these keep a dragged file from silently ingesting a disk-sized blob and
|
|
31030
|
+
* bound one message the way the image batch is bounded.
|
|
31031
|
+
*/
|
|
31032
|
+
const MAX_FILE_BYTES = 8388608;
|
|
30569
31033
|
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
30570
31034
|
".png",
|
|
30571
31035
|
".jpg",
|
|
@@ -30587,24 +31051,58 @@ function detectImageMediaType(data) {
|
|
|
30587
31051
|
function looksLikeImagePath(path) {
|
|
30588
31052
|
return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
30589
31053
|
}
|
|
30590
|
-
/**
|
|
30591
|
-
|
|
31054
|
+
/**
|
|
31055
|
+
* Parse a paste/drop into its image and file paths: image-suffixed tokens
|
|
31056
|
+
* stay images, other path-shaped tokens ride as file attachments (0.1.5
|
|
31057
|
+
* file blocks), and anything that is neither leaves both empty — the caller
|
|
31058
|
+
* then treats the paste as plain text.
|
|
31059
|
+
*
|
|
31060
|
+
* File tokens are held to an absolute-path-with-shape bar (drive/backslash
|
|
31061
|
+
* or a dot-suffixed leaf after a separator): a dropped terminal path always
|
|
31062
|
+
* carries one of those, while prose, slash commands, and option flags never
|
|
31063
|
+
* do. A POSIX absolute path without any dot-suffixed leaf falls through as
|
|
31064
|
+
* text — the @ mention route still attaches such files deliberately.
|
|
31065
|
+
*/
|
|
31066
|
+
function parsePastedAttachmentPaths(input) {
|
|
30592
31067
|
const text = input.trim();
|
|
30593
|
-
if (text === "") return
|
|
30594
|
-
|
|
31068
|
+
if (text === "") return {
|
|
31069
|
+
images: [],
|
|
31070
|
+
files: []
|
|
31071
|
+
};
|
|
31072
|
+
const images = [];
|
|
31073
|
+
const files = [];
|
|
31074
|
+
const looksLikeDroppedFile = (path) => /^[A-Za-z]:[\\/]/u.test(path) || /^\\\\/u.test(path) || /^\/|^\.\.?\//u.test(path) && /\.[A-Za-z0-9]{1,16}$/u.test(path);
|
|
30595
31075
|
for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
|
|
30596
31076
|
const token = match[1] ?? match[2] ?? match[3];
|
|
30597
31077
|
if (token === void 0) continue;
|
|
30598
31078
|
let path = token;
|
|
30599
|
-
if (path.startsWith("file://"))
|
|
30600
|
-
|
|
30601
|
-
|
|
30602
|
-
|
|
31079
|
+
if (path.startsWith("file://")) {
|
|
31080
|
+
try {
|
|
31081
|
+
path = fileURLToPath(path);
|
|
31082
|
+
} catch {
|
|
31083
|
+
return {
|
|
31084
|
+
images: [],
|
|
31085
|
+
files: []
|
|
31086
|
+
};
|
|
31087
|
+
}
|
|
31088
|
+
if (looksLikeImagePath(path)) images.push(path);
|
|
31089
|
+
else files.push(path);
|
|
31090
|
+
continue;
|
|
31091
|
+
}
|
|
31092
|
+
if (looksLikeImagePath(path)) {
|
|
31093
|
+
images.push(path);
|
|
31094
|
+
continue;
|
|
30603
31095
|
}
|
|
30604
|
-
if (!
|
|
30605
|
-
|
|
31096
|
+
if (!looksLikeDroppedFile(path)) return {
|
|
31097
|
+
images: [],
|
|
31098
|
+
files: []
|
|
31099
|
+
};
|
|
31100
|
+
files.push(path);
|
|
30606
31101
|
}
|
|
30607
|
-
return
|
|
31102
|
+
return {
|
|
31103
|
+
images,
|
|
31104
|
+
files
|
|
31105
|
+
};
|
|
30608
31106
|
}
|
|
30609
31107
|
/** Validate path, byte size and encoded signature without writing an attachment object. */
|
|
30610
31108
|
async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
|
|
@@ -30675,6 +31173,60 @@ async function saveImagePaths(paths, attachments, signal) {
|
|
|
30675
31173
|
attachment
|
|
30676
31174
|
}));
|
|
30677
31175
|
}
|
|
31176
|
+
/** Validate path and byte size for non-image file attachments without writing. */
|
|
31177
|
+
async function inspectFilePaths(paths, attachments, cwd = process.cwd()) {
|
|
31178
|
+
if (paths.length === 0) return [];
|
|
31179
|
+
if (attachments === void 0) throw new Error("file attachments are unavailable in this profile");
|
|
31180
|
+
if (paths.length > 8) throw new Error(`too many files (${paths.length}; limit 8)`);
|
|
31181
|
+
const inspected = [];
|
|
31182
|
+
for (const raw of paths) {
|
|
31183
|
+
const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
|
|
31184
|
+
let facts;
|
|
31185
|
+
try {
|
|
31186
|
+
facts = await stat(path);
|
|
31187
|
+
} catch (error) {
|
|
31188
|
+
throw new Error(`cannot read file "${raw}": ${error instanceof Error ? error.message : String(error)}`);
|
|
31189
|
+
}
|
|
31190
|
+
if (!facts.isFile()) throw new Error(`file path is not a file: "${raw}"`);
|
|
31191
|
+
if (facts.size > 8388608) throw new Error(`file "${basename(path)}" is ${facts.size} bytes; limit ${MAX_FILE_BYTES}`);
|
|
31192
|
+
inspected.push({
|
|
31193
|
+
path,
|
|
31194
|
+
name: basename(path),
|
|
31195
|
+
bytes: facts.size
|
|
31196
|
+
});
|
|
31197
|
+
}
|
|
31198
|
+
return inspected;
|
|
31199
|
+
}
|
|
31200
|
+
/** Read and persist an ordered non-image file path list as model file blocks. */
|
|
31201
|
+
async function saveFilePaths(paths, attachments, signal) {
|
|
31202
|
+
if (paths.length === 0) return [];
|
|
31203
|
+
if (attachments === void 0) throw new Error("file attachments are unavailable in this profile");
|
|
31204
|
+
await inspectFilePaths(paths, attachments);
|
|
31205
|
+
const checkCancelled = () => {
|
|
31206
|
+
if (signal?.aborted === true) throw new Error("file submission cancelled");
|
|
31207
|
+
};
|
|
31208
|
+
const inputs = [];
|
|
31209
|
+
for (const path of paths) {
|
|
31210
|
+
checkCancelled();
|
|
31211
|
+
let data;
|
|
31212
|
+
try {
|
|
31213
|
+
data = await readFile(path);
|
|
31214
|
+
} catch (error) {
|
|
31215
|
+
throw new Error(`cannot read file "${path}": ${error instanceof Error ? error.message : String(error)}`);
|
|
31216
|
+
}
|
|
31217
|
+
inputs.push({
|
|
31218
|
+
data,
|
|
31219
|
+
name: basename(path)
|
|
31220
|
+
});
|
|
31221
|
+
}
|
|
31222
|
+
checkCancelled();
|
|
31223
|
+
const refs = await Promise.all(inputs.map((input) => attachments.saveFile(input)));
|
|
31224
|
+
checkCancelled();
|
|
31225
|
+
return refs.map((attachment) => ({
|
|
31226
|
+
type: "file",
|
|
31227
|
+
attachment
|
|
31228
|
+
}));
|
|
31229
|
+
}
|
|
30678
31230
|
//#endregion
|
|
30679
31231
|
//#region src/app.ts
|
|
30680
31232
|
/**
|
|
@@ -30764,6 +31316,10 @@ const LOCAL_COMMANDS = [
|
|
|
30764
31316
|
label: "/theme",
|
|
30765
31317
|
description: "switch the color theme"
|
|
30766
31318
|
},
|
|
31319
|
+
{
|
|
31320
|
+
label: "/animation",
|
|
31321
|
+
description: "toggle timed animations (/animation [on|off])"
|
|
31322
|
+
},
|
|
30767
31323
|
{
|
|
30768
31324
|
label: "/history",
|
|
30769
31325
|
description: "search and recall past prompts"
|
|
@@ -30823,12 +31379,21 @@ function padColumns(text, width) {
|
|
|
30823
31379
|
const clipped = truncateColumns(singleLineText(text), width);
|
|
30824
31380
|
return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
|
|
30825
31381
|
}
|
|
30826
|
-
/**
|
|
31382
|
+
/**
|
|
31383
|
+
* Wall-clock frame counter for one self-contained animated leaf. Each fire
|
|
31384
|
+
* derives the tick from elapsed time instead of counting intervals, so a
|
|
31385
|
+
* stretched interval (busy event loop, slow SSH) skips the animation ahead
|
|
31386
|
+
* rather than slowing it down; the tick always tracks real time.
|
|
31387
|
+
*/
|
|
30827
31388
|
function useFrames(intervalMs, active = true) {
|
|
30828
31389
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
30829
31390
|
(0, import_react.useEffect)(() => {
|
|
30830
31391
|
if (!active) return;
|
|
30831
|
-
const
|
|
31392
|
+
const startedAt = Date.now();
|
|
31393
|
+
setTick(0);
|
|
31394
|
+
const id = setInterval(() => {
|
|
31395
|
+
setTick(Math.max(0, Math.floor((Date.now() - startedAt) / intervalMs)));
|
|
31396
|
+
}, intervalMs);
|
|
30832
31397
|
return () => {
|
|
30833
31398
|
clearInterval(id);
|
|
30834
31399
|
};
|
|
@@ -30849,14 +31414,17 @@ function useStableInput(handler, active) {
|
|
|
30849
31414
|
}, []);
|
|
30850
31415
|
useInput(stableHandler, { isActive: active });
|
|
30851
31416
|
}
|
|
30852
|
-
/**
|
|
30853
|
-
|
|
30854
|
-
|
|
31417
|
+
/**
|
|
31418
|
+
* The original web StateDot chase used by the busy composer marker. With
|
|
31419
|
+
* animations off it freezes on the first frame (still visibly busy).
|
|
31420
|
+
*/
|
|
31421
|
+
function BusyChase({ animated = true }) {
|
|
31422
|
+
const tick = useFrames(125, animated);
|
|
30855
31423
|
return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
|
|
30856
31424
|
}
|
|
30857
|
-
/** Blinking block caret appended to streaming text. */
|
|
30858
|
-
function Caret() {
|
|
30859
|
-
const tick = useFrames(530);
|
|
31425
|
+
/** Blinking block caret appended to streaming text; solid when frozen. */
|
|
31426
|
+
function Caret({ animated = true }) {
|
|
31427
|
+
const tick = useFrames(530, animated);
|
|
30860
31428
|
return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
|
|
30861
31429
|
}
|
|
30862
31430
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
@@ -30883,17 +31451,19 @@ function useCursorBlink(active) {
|
|
|
30883
31451
|
* One bounded line painted with the deep-diving shimmer: a continuously
|
|
30884
31452
|
* moving blue gradient across graphemes, the `✻` glyph in the breathing
|
|
30885
31453
|
* spark color. Shared by the busy line and the collapsed thinking marker;
|
|
30886
|
-
* always exactly one row (truncate-end) so the live budget stays exact.
|
|
31454
|
+
* always exactly one row (truncate-end) so the live budget stays exact. With
|
|
31455
|
+
* animations off the same spans render in fixed colors — no timer, no
|
|
31456
|
+
* per-frame repaint, the `✻` keeps its highlight.
|
|
30887
31457
|
*/
|
|
30888
|
-
function ShimmerLine({ text }) {
|
|
30889
|
-
const tick = useFrames(33);
|
|
31458
|
+
function ShimmerLine({ text, animated = true }) {
|
|
31459
|
+
const tick = useFrames(33, animated);
|
|
30890
31460
|
const palette = getPalette();
|
|
30891
31461
|
const graphemes = splitGraphemes(text);
|
|
30892
31462
|
return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
|
|
30893
31463
|
const sparkle = grapheme.text === "✻";
|
|
30894
31464
|
return (0, import_react.createElement)(Text, {
|
|
30895
31465
|
key: `${grapheme.start}-${grapheme.end}`,
|
|
30896
|
-
color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
|
|
31466
|
+
color: inkColor(!animated ? sparkle ? palette.brandBright : palette.brandDeep : sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
|
|
30897
31467
|
bold: sparkle || void 0
|
|
30898
31468
|
}, grapheme.text);
|
|
30899
31469
|
}));
|
|
@@ -30904,10 +31474,13 @@ function ShimmerLine({ text }) {
|
|
|
30904
31474
|
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
30905
31475
|
* so a resumed mid-turn keeps the real time.
|
|
30906
31476
|
*/
|
|
30907
|
-
function DeepDivingLine({ since }) {
|
|
31477
|
+
function DeepDivingLine({ since, animated = true }) {
|
|
30908
31478
|
const elapsed = since === 0 ? 0 : Date.now() - since;
|
|
30909
31479
|
const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
|
|
30910
|
-
return (0, import_react.createElement)(ShimmerLine, {
|
|
31480
|
+
return (0, import_react.createElement)(ShimmerLine, {
|
|
31481
|
+
text,
|
|
31482
|
+
animated
|
|
31483
|
+
});
|
|
30911
31484
|
}
|
|
30912
31485
|
/**
|
|
30913
31486
|
* The streaming buffer rendered with a hard size cap: the live region must
|
|
@@ -32192,7 +32765,8 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
32192
32765
|
const identity = row.displayName === row.provider ? row.provider : row.displayName + " (" + row.provider + ")";
|
|
32193
32766
|
const authorization = authorizationForProvider(authorizations, row.provider);
|
|
32194
32767
|
const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? " · " + providerAuthorizationStatus(authorization) : "";
|
|
32195
|
-
const
|
|
32768
|
+
const diagnostic = row.diagnostic === void 0 ? "" : " · ! " + singleLineText(row.diagnostic);
|
|
32769
|
+
const label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "") + diagnostic;
|
|
32196
32770
|
const idleColor = row.configured ? inkColor(getPalette().brandMid) : inkColor(getPalette().dim);
|
|
32197
32771
|
itemRows.push((0, import_react.createElement)(Text, {
|
|
32198
32772
|
key: row.provider,
|
|
@@ -32560,7 +33134,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
32560
33134
|
color: zone === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
32561
33135
|
wrap: "truncate-end"
|
|
32562
33136
|
}, truncateColumns(" " + (zone === "url" ? ">" : " ") + " url " + (baseURL === "" ? "(official default)" : baseURL) + (zone === "url" ? "▏" : ""), viewport.contentColumns));
|
|
32563
|
-
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 3);
|
|
33137
|
+
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - (target.diagnostic === void 0 ? 0 : 1) - 3);
|
|
32564
33138
|
const first = selectionWindow(cursor, models.length + 1, rowBudget);
|
|
32565
33139
|
const modelRows = [];
|
|
32566
33140
|
for (let index = first; index < first + Math.max(0, Math.min(models.length + 1 - first, rowBudget)); index += 1) {
|
|
@@ -32593,7 +33167,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
32593
33167
|
color: inkColor(getPalette().brand),
|
|
32594
33168
|
bold: true,
|
|
32595
33169
|
wrap: "truncate-end"
|
|
32596
|
-
}, truncateColumns("/model — configure " + target.displayName, viewport.contentColumns)),
|
|
33170
|
+
}, truncateColumns("/model — configure " + target.displayName, viewport.contentColumns)), ...target.diagnostic === void 0 ? [] : [(0, import_react.createElement)(Text, {
|
|
33171
|
+
key: "diagnostic",
|
|
33172
|
+
color: inkColor(getPalette().warn),
|
|
33173
|
+
wrap: "truncate-end"
|
|
33174
|
+
}, truncateColumns("! " + displayText(singleLineText(target.diagnostic)), viewport.contentColumns))], (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), keyRow, urlRow, ...stateRows, ...modelRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
32597
33175
|
color: inkColor(getPalette().dim),
|
|
32598
33176
|
wrap: "truncate-end"
|
|
32599
33177
|
}, truncateColumns("↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back", viewport.contentColumns)));
|
|
@@ -32933,6 +33511,167 @@ function waveRowSpans(cells) {
|
|
|
32933
33511
|
}
|
|
32934
33512
|
return spans;
|
|
32935
33513
|
}
|
|
33514
|
+
/** Index of the cell STARTING at a display column, if one does. */
|
|
33515
|
+
function cellIndexAtColumn(cells, target) {
|
|
33516
|
+
let column = 0;
|
|
33517
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
33518
|
+
if (column === target) return index;
|
|
33519
|
+
column += cells[index].width ?? visibleColumns(cells[index].char);
|
|
33520
|
+
if (column > target) return void 0;
|
|
33521
|
+
}
|
|
33522
|
+
}
|
|
33523
|
+
/**
|
|
33524
|
+
* Wall-clock wave frames — strictly ONE sweep per MOUNT; the mount-spanning
|
|
33525
|
+
* one-shot latch (surviving modal unmounts) lives in Input as `wavePlayedKey`.
|
|
33526
|
+
* The first gate-off after the sweep has started (it completed, a turn went
|
|
33527
|
+
* busy, image preparation began, animations were toggled off) latches `done`
|
|
33528
|
+
* for this mount, so the same mount can never resume or replay. A trigger
|
|
33529
|
+
* that lands while the gate is already down stays pending until the gate
|
|
33530
|
+
* rises once, then plays.
|
|
33531
|
+
*/
|
|
33532
|
+
function useWaveFrames(active, durationMs) {
|
|
33533
|
+
const [tick, setTick] = (0, import_react.useState)(0);
|
|
33534
|
+
const [done, setDone] = (0, import_react.useState)(false);
|
|
33535
|
+
const startedRef = (0, import_react.useRef)(false);
|
|
33536
|
+
(0, import_react.useEffect)(() => {
|
|
33537
|
+
if (done) return;
|
|
33538
|
+
if (!active) {
|
|
33539
|
+
if (startedRef.current) setDone(true);
|
|
33540
|
+
return;
|
|
33541
|
+
}
|
|
33542
|
+
startedRef.current = true;
|
|
33543
|
+
const startedAt = Date.now();
|
|
33544
|
+
const id = setInterval(() => {
|
|
33545
|
+
const elapsed = Date.now() - startedAt;
|
|
33546
|
+
if (elapsed >= durationMs) {
|
|
33547
|
+
clearInterval(id);
|
|
33548
|
+
setDone(true);
|
|
33549
|
+
return;
|
|
33550
|
+
}
|
|
33551
|
+
setTick(Math.max(0, Math.floor(elapsed / 33)));
|
|
33552
|
+
}, 33);
|
|
33553
|
+
return () => {
|
|
33554
|
+
clearInterval(id);
|
|
33555
|
+
};
|
|
33556
|
+
}, [
|
|
33557
|
+
active,
|
|
33558
|
+
durationMs,
|
|
33559
|
+
done
|
|
33560
|
+
]);
|
|
33561
|
+
return {
|
|
33562
|
+
tick,
|
|
33563
|
+
done
|
|
33564
|
+
};
|
|
33565
|
+
}
|
|
33566
|
+
/**
|
|
33567
|
+
* The self-contained wave leaf: it owns its 33ms tick, so the sweep
|
|
33568
|
+
* re-renders ONLY this component at ~30fps — Input's derived editor state
|
|
33569
|
+
* never re-runs per frame. Graphemes stay atomic and every background sample
|
|
33570
|
+
* advances by terminal display columns, so CJK and emoji cannot move the
|
|
33571
|
+
* caret or wrap the band. The duration gate renders the fallback band on the
|
|
33572
|
+
* frame the sweep completes.
|
|
33573
|
+
*/
|
|
33574
|
+
function ComposerWave(props) {
|
|
33575
|
+
const { tier, style } = props;
|
|
33576
|
+
const durationMs = deepseekWaveDuration(tier, style);
|
|
33577
|
+
const { tick, done } = useWaveFrames(props.active, durationMs);
|
|
33578
|
+
const settledRef = (0, import_react.useRef)(false);
|
|
33579
|
+
const onSettledRef = (0, import_react.useRef)(props.onSettled);
|
|
33580
|
+
onSettledRef.current = props.onSettled;
|
|
33581
|
+
const settle = () => {
|
|
33582
|
+
if (settledRef.current) return;
|
|
33583
|
+
settledRef.current = true;
|
|
33584
|
+
onSettledRef.current();
|
|
33585
|
+
};
|
|
33586
|
+
(0, import_react.useEffect)(() => {
|
|
33587
|
+
if (done) settle();
|
|
33588
|
+
}, [done]);
|
|
33589
|
+
(0, import_react.useEffect)(() => () => {
|
|
33590
|
+
settle();
|
|
33591
|
+
}, []);
|
|
33592
|
+
if (!props.active || done || tick * 33 >= durationMs) return props.fallback;
|
|
33593
|
+
const hues = deepseekWaveHues(tier);
|
|
33594
|
+
const bandRgb = getPalette().composerBand;
|
|
33595
|
+
const totalBandRows = props.rows.length + 2;
|
|
33596
|
+
const waveBg = (row, column) => {
|
|
33597
|
+
const rgb = deepseekWaveColumnBg(tick, column, props.bandWidth, tier, style, hues, bandRgb, row, totalBandRows);
|
|
33598
|
+
return rgb === null ? props.bandBg : inkColor(rgb);
|
|
33599
|
+
};
|
|
33600
|
+
const blankBandRow = (row) => {
|
|
33601
|
+
const blanks = [];
|
|
33602
|
+
for (let column = 0; column < props.bandWidth; column += 1) blanks.push({
|
|
33603
|
+
char: " ",
|
|
33604
|
+
width: 1,
|
|
33605
|
+
backgroundColor: waveBg(row, column)
|
|
33606
|
+
});
|
|
33607
|
+
return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
|
|
33608
|
+
};
|
|
33609
|
+
const editorWaveRows = props.rows.map((row, visibleIndex) => {
|
|
33610
|
+
const sourceIndex = props.windowStart + visibleIndex;
|
|
33611
|
+
const bandRow = visibleIndex + 1;
|
|
33612
|
+
const parts = editorRowParts(row, sourceIndex, props.caretRow, props.cursor);
|
|
33613
|
+
const placeholder = sourceIndex === 0 && props.value === "";
|
|
33614
|
+
const cells = [];
|
|
33615
|
+
let usedColumns = 0;
|
|
33616
|
+
const push = (char, extra = {}) => {
|
|
33617
|
+
const width = visibleColumns(char);
|
|
33618
|
+
cells.push({
|
|
33619
|
+
char,
|
|
33620
|
+
width,
|
|
33621
|
+
backgroundColor: waveBg(bandRow, usedColumns),
|
|
33622
|
+
...extra
|
|
33623
|
+
});
|
|
33624
|
+
usedColumns += width;
|
|
33625
|
+
};
|
|
33626
|
+
if (sourceIndex === 0) {
|
|
33627
|
+
push(props.promptGlyph, {
|
|
33628
|
+
color: props.promptColor,
|
|
33629
|
+
bold: true
|
|
33630
|
+
});
|
|
33631
|
+
push(" ", { color: props.promptColor });
|
|
33632
|
+
} else {
|
|
33633
|
+
push(" ");
|
|
33634
|
+
push(" ");
|
|
33635
|
+
}
|
|
33636
|
+
for (const span of splitGraphemes(parts.before)) push(span.text);
|
|
33637
|
+
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible });
|
|
33638
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
|
|
33639
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
|
|
33640
|
+
while (usedColumns < props.bandWidth) push(" ");
|
|
33641
|
+
const middleBandRow = Math.floor(totalBandRows / 2);
|
|
33642
|
+
if (bandRow === middleBandRow && deepseekWaveWordVisible(tick, tier, style)) {
|
|
33643
|
+
const word = tier === "unknown" ? "Into the Unknown" : "deepseek";
|
|
33644
|
+
const start = Math.max(2, Math.floor((props.bandWidth - word.length) / 2));
|
|
33645
|
+
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
|
|
33646
|
+
if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
|
|
33647
|
+
const cell = cells[indices[at]];
|
|
33648
|
+
cell.char = word[at];
|
|
33649
|
+
cell.width = 1;
|
|
33650
|
+
cell.color = inkColor(deepseekWaveWordHue(at, hues));
|
|
33651
|
+
cell.bold = true;
|
|
33652
|
+
cell.dim = false;
|
|
33653
|
+
}
|
|
33654
|
+
}
|
|
33655
|
+
if (bandRow === middleBandRow && (tier === "deepseek" || tier === "unknown") && style === "wave") {
|
|
33656
|
+
const spark = deepseekWaveSpark(tick);
|
|
33657
|
+
const lastIndex = cellIndexAtColumn(cells, props.bandWidth - 1);
|
|
33658
|
+
if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
|
|
33659
|
+
cells[lastIndex].char = spark;
|
|
33660
|
+
cells[lastIndex].color = props.promptColor;
|
|
33661
|
+
cells[lastIndex].bold = true;
|
|
33662
|
+
cells[lastIndex].dim = false;
|
|
33663
|
+
}
|
|
33664
|
+
}
|
|
33665
|
+
return (0, import_react.createElement)(Text, {
|
|
33666
|
+
key: `editor-${sourceIndex}`,
|
|
33667
|
+
wrap: "truncate-end"
|
|
33668
|
+
}, ...waveRowSpans(cells));
|
|
33669
|
+
});
|
|
33670
|
+
return (0, import_react.createElement)(Box, {
|
|
33671
|
+
flexDirection: "column",
|
|
33672
|
+
width: props.bandWidth
|
|
33673
|
+
}, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
|
|
33674
|
+
}
|
|
32936
33675
|
/**
|
|
32937
33676
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
32938
33677
|
* with independent history selection and content scrolling. The complete
|
|
@@ -33092,8 +33831,10 @@ function completionCandidates(value, descriptors, skills) {
|
|
|
33092
33831
|
seen.add(name);
|
|
33093
33832
|
all.push(candidate);
|
|
33094
33833
|
}
|
|
33095
|
-
|
|
33096
|
-
|
|
33834
|
+
return rankByName(all.map((candidate) => ({
|
|
33835
|
+
name: candidate.label.slice(1),
|
|
33836
|
+
candidate
|
|
33837
|
+
})), prefix).map((entry) => entry.candidate);
|
|
33097
33838
|
}
|
|
33098
33839
|
/**
|
|
33099
33840
|
* Shared completion-menu geometry: the menu view and the App's dynamic-row
|
|
@@ -33175,7 +33916,7 @@ function CompletionMenu({ active, mention, index, rows, error }) {
|
|
|
33175
33916
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
33176
33917
|
* box passes every key through untouched.
|
|
33177
33918
|
*/
|
|
33178
|
-
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, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows }) {
|
|
33919
|
+
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, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, animations, applyAnimations, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows, sessionKey }) {
|
|
33179
33920
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
33180
33921
|
const inputTerminalRows = useStdout().stdout?.rows ?? 30;
|
|
33181
33922
|
const editorColumns = Math.max(1, columns - 6);
|
|
@@ -33190,10 +33931,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33190
33931
|
const [draftImages, setDraftImages] = (0, import_react.useState)([]);
|
|
33191
33932
|
const draftImagesRef = (0, import_react.useRef)(draftImages);
|
|
33192
33933
|
draftImagesRef.current = draftImages;
|
|
33934
|
+
const [draftFiles, setDraftFiles] = (0, import_react.useState)([]);
|
|
33935
|
+
const draftFilesRef = (0, import_react.useRef)(draftFiles);
|
|
33936
|
+
draftFilesRef.current = draftFiles;
|
|
33193
33937
|
const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
|
|
33194
33938
|
const prepareAbortRef = (0, import_react.useRef)(void 0);
|
|
33195
33939
|
const prepareEpochRef = (0, import_react.useRef)(0);
|
|
33196
|
-
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
|
|
33940
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages && animations);
|
|
33197
33941
|
(0, import_react.useEffect)(() => () => {
|
|
33198
33942
|
prepareEpochRef.current += 1;
|
|
33199
33943
|
prepareAbortRef.current?.abort();
|
|
@@ -33217,6 +33961,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33217
33961
|
const safe = sanitizeDraftText(historyFill.text);
|
|
33218
33962
|
draftImagesRef.current = [];
|
|
33219
33963
|
setDraftImages([]);
|
|
33964
|
+
draftFilesRef.current = [];
|
|
33965
|
+
setDraftFiles([]);
|
|
33220
33966
|
valueRef.current = safe;
|
|
33221
33967
|
cursorRef.current = safe.length;
|
|
33222
33968
|
setValue(safe);
|
|
@@ -33243,6 +33989,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33243
33989
|
draftImagesRef.current = next;
|
|
33244
33990
|
return next.length === current.length ? current : next;
|
|
33245
33991
|
});
|
|
33992
|
+
setDraftFiles((current) => {
|
|
33993
|
+
const next = current.filter((file) => value.includes(file.marker));
|
|
33994
|
+
draftFilesRef.current = next;
|
|
33995
|
+
return next.length === current.length ? current : next;
|
|
33996
|
+
});
|
|
33246
33997
|
}, [value]);
|
|
33247
33998
|
(0, import_react.useEffect)(() => {
|
|
33248
33999
|
if (stdin === void 0) return;
|
|
@@ -33286,12 +34037,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33286
34037
|
const [mentionError, setMentionError] = (0, import_react.useState)(void 0);
|
|
33287
34038
|
const mentionRequestRef = (0, import_react.useRef)(0);
|
|
33288
34039
|
const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
33289
|
-
const uniqueImageMarker = (name, source, reserved = []) => {
|
|
34040
|
+
const uniqueImageMarker = (name, source, reserved = [], kind = "image") => {
|
|
33290
34041
|
const safeName = singleLineText(sanitizeDraftText(name));
|
|
33291
|
-
|
|
34042
|
+
const label = kind === "file" ? "file" : "image";
|
|
34043
|
+
let marker = source === "mention" ? `@${safeName}` : `[${label}: ${safeName}]`;
|
|
33292
34044
|
let suffix = 2;
|
|
33293
|
-
|
|
33294
|
-
|
|
34045
|
+
const taken = (candidate) => valueRef.current.includes(candidate) || draftImagesRef.current.some((image) => image.marker === candidate) || draftFilesRef.current.some((file) => file.marker === candidate) || reserved.includes(candidate);
|
|
34046
|
+
while (taken(marker)) {
|
|
34047
|
+
marker = source === "mention" ? `@${safeName} (${suffix})` : `[${label}: ${safeName} ${suffix}]`;
|
|
33295
34048
|
suffix += 1;
|
|
33296
34049
|
}
|
|
33297
34050
|
return marker;
|
|
@@ -33309,24 +34062,41 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33309
34062
|
setDraftImages(next);
|
|
33310
34063
|
return true;
|
|
33311
34064
|
};
|
|
33312
|
-
|
|
34065
|
+
/**
|
|
34066
|
+
* Attach a paste/drop split into image and non-image paths: images ride the
|
|
34067
|
+
* durable image blocks, files ride the 0.1.5 file blocks, and both register
|
|
34068
|
+
* visible draft markers anchored at the drop point.
|
|
34069
|
+
*/
|
|
34070
|
+
const insertDroppedAttachments = (imagePaths, filePaths) => {
|
|
33313
34071
|
const originalValue = valueRef.current;
|
|
33314
34072
|
const originalCursor = cursorRef.current;
|
|
33315
|
-
|
|
33316
|
-
|
|
33317
|
-
|
|
34073
|
+
const total = imagePaths.length + filePaths.length;
|
|
34074
|
+
if (total === 0) return;
|
|
34075
|
+
notify(`checking ${total} attachment${total === 1 ? "" : "s"}…`);
|
|
34076
|
+
Promise.all([imagePaths.length === 0 ? Promise.resolve([]) : inspectImages(imagePaths), filePaths.length === 0 ? Promise.resolve([]) : inspectFiles(filePaths)]).then(([inspectedImages, inspectedFiles]) => {
|
|
34077
|
+
const imageAdditions = [];
|
|
34078
|
+
const fileAdditions = [];
|
|
33318
34079
|
const markers = [];
|
|
33319
|
-
for (const inspection of
|
|
33320
|
-
if ([...draftImagesRef.current, ...
|
|
34080
|
+
for (const inspection of inspectedImages) {
|
|
34081
|
+
if ([...draftImagesRef.current, ...imageAdditions].some((image) => sameImagePath(image.path, inspection.path))) continue;
|
|
33321
34082
|
const marker = uniqueImageMarker(inspection.name, "drop", markers);
|
|
33322
|
-
|
|
34083
|
+
imageAdditions.push({
|
|
34084
|
+
...inspection,
|
|
34085
|
+
marker
|
|
34086
|
+
});
|
|
34087
|
+
markers.push(marker);
|
|
34088
|
+
}
|
|
34089
|
+
for (const inspection of inspectedFiles) {
|
|
34090
|
+
if ([...draftFilesRef.current, ...fileAdditions].some((file) => sameImagePath(file.path, inspection.path))) continue;
|
|
34091
|
+
const marker = uniqueImageMarker(inspection.name, "drop", markers, "file");
|
|
34092
|
+
fileAdditions.push({
|
|
33323
34093
|
...inspection,
|
|
33324
34094
|
marker
|
|
33325
34095
|
});
|
|
33326
34096
|
markers.push(marker);
|
|
33327
34097
|
}
|
|
33328
|
-
if (
|
|
33329
|
-
notify("those
|
|
34098
|
+
if (imageAdditions.length === 0 && fileAdditions.length === 0) {
|
|
34099
|
+
notify("those attachments are already attached", "warning");
|
|
33330
34100
|
return;
|
|
33331
34101
|
}
|
|
33332
34102
|
const current = valueRef.current;
|
|
@@ -33335,7 +34105,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33335
34105
|
end: originalCursor
|
|
33336
34106
|
});
|
|
33337
34107
|
if (anchor === void 0) {
|
|
33338
|
-
notify("draft changed at the
|
|
34108
|
+
notify("draft changed at the attachment drop point; drop the files again", "warning");
|
|
33339
34109
|
return;
|
|
33340
34110
|
}
|
|
33341
34111
|
const at = anchor.start;
|
|
@@ -33347,12 +34117,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33347
34117
|
setValue(edit.value);
|
|
33348
34118
|
setCursor(nextCursor);
|
|
33349
34119
|
resetCursorBlink();
|
|
33350
|
-
const nextImages = [...draftImagesRef.current, ...
|
|
34120
|
+
const nextImages = [...draftImagesRef.current, ...imageAdditions];
|
|
33351
34121
|
draftImagesRef.current = nextImages;
|
|
33352
34122
|
setDraftImages(nextImages);
|
|
33353
|
-
|
|
34123
|
+
const nextFiles = [...draftFilesRef.current, ...fileAdditions];
|
|
34124
|
+
draftFilesRef.current = nextFiles;
|
|
34125
|
+
setDraftFiles(nextFiles);
|
|
34126
|
+
const count = imageAdditions.length + fileAdditions.length;
|
|
34127
|
+
notify(`${count} attachment${count === 1 ? "" : "s"} ready for the next message`);
|
|
33354
34128
|
}, (reason) => {
|
|
33355
|
-
notify(`
|
|
34129
|
+
notify(`attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
33356
34130
|
});
|
|
33357
34131
|
};
|
|
33358
34132
|
(0, import_react.useEffect)(() => {
|
|
@@ -33387,7 +34161,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33387
34161
|
]);
|
|
33388
34162
|
const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
|
|
33389
34163
|
const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
|
|
33390
|
-
|
|
34164
|
+
let rankedMentionRows = visibleMentionRows;
|
|
34165
|
+
if (mentionToken !== void 0 && !isPathLikeMentionQuery(mentionToken.query) && mentionToken.query !== "") {
|
|
34166
|
+
const hits = rankByName(visibleMentionRows.map((row) => ({
|
|
34167
|
+
name: row.label.replace(/^@/u, ""),
|
|
34168
|
+
row
|
|
34169
|
+
})), mentionToken.query).map((entry) => entry.row);
|
|
34170
|
+
const hitSet = new Set(hits);
|
|
34171
|
+
rankedMentionRows = [...hits, ...visibleMentionRows.filter((row) => !hitSet.has(row))];
|
|
34172
|
+
}
|
|
34173
|
+
const menuRows = mentionActive ? rankedMentionRows.map((row) => ({
|
|
33391
34174
|
label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
|
|
33392
34175
|
description: row.description,
|
|
33393
34176
|
origin: "mention"
|
|
@@ -33396,8 +34179,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33396
34179
|
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
33397
34180
|
const acceptMenuCandidate = () => {
|
|
33398
34181
|
if (mentionActive && mentionToken !== void 0) {
|
|
33399
|
-
if (
|
|
33400
|
-
const row =
|
|
34182
|
+
if (rankedMentionRows.length === 0) return;
|
|
34183
|
+
const row = rankedMentionRows[completionIndex % rankedMentionRows.length];
|
|
33401
34184
|
if (row !== void 0) {
|
|
33402
34185
|
if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
|
|
33403
34186
|
const tokenText = value.slice(mentionToken.start, cursor);
|
|
@@ -33600,6 +34383,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33600
34383
|
resetCursorBlink();
|
|
33601
34384
|
draftImagesRef.current = [];
|
|
33602
34385
|
setDraftImages([]);
|
|
34386
|
+
draftFilesRef.current = [];
|
|
34387
|
+
setDraftFiles([]);
|
|
33603
34388
|
setCompletionIndex(0);
|
|
33604
34389
|
setDismissedMenuValue(void 0);
|
|
33605
34390
|
} else quit();
|
|
@@ -33645,15 +34430,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33645
34430
|
}
|
|
33646
34431
|
const trimmed = liveValue.trim();
|
|
33647
34432
|
const text = submissionPayload(liveValue);
|
|
33648
|
-
if (draftImagesRef.current.length > 0) {
|
|
34433
|
+
if (draftImagesRef.current.length > 0 || draftFilesRef.current.length > 0) {
|
|
34434
|
+
if (isSlashLine(text)) notify("commands cannot carry attachments; the line will be sent to the model as a prompt", "warning");
|
|
34435
|
+
const originSession = sessionKey;
|
|
33649
34436
|
const controller = new AbortController();
|
|
33650
34437
|
const epoch = prepareEpochRef.current + 1;
|
|
33651
34438
|
prepareEpochRef.current = epoch;
|
|
33652
34439
|
prepareAbortRef.current = controller;
|
|
33653
34440
|
setPreparingImages(true);
|
|
33654
|
-
|
|
33655
|
-
const
|
|
33656
|
-
|
|
34441
|
+
const imageSnapshot = draftImagesRef.current;
|
|
34442
|
+
const fileSnapshot = draftFilesRef.current;
|
|
34443
|
+
const total = imageSnapshot.length + fileSnapshot.length;
|
|
34444
|
+
notify(`processing ${total} attachment${total === 1 ? "" : "s"}…`);
|
|
34445
|
+
Promise.all([imageSnapshot.length === 0 ? Promise.resolve([]) : prepareImages(imageSnapshot.map((image) => image.path), controller.signal), fileSnapshot.length === 0 ? Promise.resolve([]) : prepareFiles(fileSnapshot.map((file) => file.path), controller.signal)]).then(([images, files]) => {
|
|
33657
34446
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
33658
34447
|
prepareAbortRef.current = void 0;
|
|
33659
34448
|
setPreparingImages(false);
|
|
@@ -33663,6 +34452,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33663
34452
|
setCursor(0);
|
|
33664
34453
|
draftImagesRef.current = [];
|
|
33665
34454
|
setDraftImages([]);
|
|
34455
|
+
draftFilesRef.current = [];
|
|
34456
|
+
setDraftFiles([]);
|
|
33666
34457
|
setCompletionIndex(0);
|
|
33667
34458
|
setDismissedMenuValue(void 0);
|
|
33668
34459
|
dismissNotice();
|
|
@@ -33671,13 +34462,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33671
34462
|
recordHistory(text);
|
|
33672
34463
|
}
|
|
33673
34464
|
recall.current = beginRecall(recallSpace, "");
|
|
33674
|
-
|
|
33675
|
-
|
|
34465
|
+
const blocks = [...images, ...files];
|
|
34466
|
+
if (busy) steer(text, blocks, originSession);
|
|
34467
|
+
else dispatch(text, blocks, originSession);
|
|
33676
34468
|
}, (reason) => {
|
|
33677
34469
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
33678
34470
|
prepareAbortRef.current = void 0;
|
|
33679
34471
|
setPreparingImages(false);
|
|
33680
|
-
notify(`
|
|
34472
|
+
notify(`attachment submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
33681
34473
|
});
|
|
33682
34474
|
return;
|
|
33683
34475
|
}
|
|
@@ -33784,6 +34576,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33784
34576
|
openTheme();
|
|
33785
34577
|
return;
|
|
33786
34578
|
}
|
|
34579
|
+
if (text === "/animation" || text.startsWith("/animation ")) {
|
|
34580
|
+
const parsed = parseAnimationsArgument(text.slice(10));
|
|
34581
|
+
if (parsed === "toggle") applyAnimations(!animations);
|
|
34582
|
+
else if (parsed === "usage") notify("usage: /animation [on|off]", "info");
|
|
34583
|
+
else applyAnimations(parsed.enabled);
|
|
34584
|
+
return;
|
|
34585
|
+
}
|
|
33787
34586
|
if (text === "/history") {
|
|
33788
34587
|
openHistory();
|
|
33789
34588
|
return;
|
|
@@ -33925,48 +34724,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33925
34724
|
text = text.replaceAll(PASTE_END_MARKER, "");
|
|
33926
34725
|
}
|
|
33927
34726
|
if (text === "") return;
|
|
33928
|
-
|
|
33929
|
-
|
|
33930
|
-
|
|
33931
|
-
|
|
34727
|
+
if (text.length > 1) {
|
|
34728
|
+
const dropped = parsePastedAttachmentPaths(text);
|
|
34729
|
+
if (dropped.images.length > 0 || dropped.files.length > 0) {
|
|
34730
|
+
insertDroppedAttachments(dropped.images, dropped.files);
|
|
34731
|
+
return;
|
|
34732
|
+
}
|
|
33932
34733
|
}
|
|
33933
34734
|
applyEdit(insertText(valueRef.current, cursorRef.current, text));
|
|
33934
34735
|
}
|
|
33935
34736
|
}, active);
|
|
33936
|
-
const
|
|
33937
|
-
const
|
|
33938
|
-
tier: null,
|
|
33939
|
-
style: null
|
|
33940
|
-
});
|
|
33941
|
-
(0, import_react.useEffect)(() => {
|
|
33942
|
-
const previous = wavePrevious.current;
|
|
33943
|
-
wavePrevious.current = {
|
|
33944
|
-
tier: waveTier,
|
|
33945
|
-
style: waveStyle
|
|
33946
|
-
};
|
|
33947
|
-
if (waveTier === null) {
|
|
33948
|
-
setWaveTick(null);
|
|
33949
|
-
return;
|
|
33950
|
-
}
|
|
33951
|
-
if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
|
|
33952
|
-
}, [waveTier, waveStyle]);
|
|
33953
|
-
const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
33954
|
-
(0, import_react.useEffect)(() => {
|
|
33955
|
-
if (!waveActive) return;
|
|
33956
|
-
const id = setInterval(() => {
|
|
33957
|
-
setWaveTick((current) => current === null ? 0 : current + 1);
|
|
33958
|
-
}, 33);
|
|
33959
|
-
return () => {
|
|
33960
|
-
clearInterval(id);
|
|
33961
|
-
};
|
|
33962
|
-
}, [waveActive]);
|
|
34737
|
+
const waveKey = waveTier !== null && waveStyle !== null ? `${waveTier}:${waveStyle}` : null;
|
|
34738
|
+
const [wavePlayedKey, setWavePlayedKey] = (0, import_react.useState)(null);
|
|
33963
34739
|
(0, import_react.useEffect)(() => {
|
|
33964
|
-
if (
|
|
34740
|
+
if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey);
|
|
33965
34741
|
}, [
|
|
33966
|
-
|
|
33967
|
-
|
|
33968
|
-
|
|
34742
|
+
animations,
|
|
34743
|
+
waveKey,
|
|
34744
|
+
wavePlayedKey
|
|
33969
34745
|
]);
|
|
34746
|
+
const waveArmed = waveKey !== null && waveKey !== wavePlayedKey;
|
|
33970
34747
|
const tierActive = waveTier !== null;
|
|
33971
34748
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
|
|
33972
34749
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
|
|
@@ -34038,7 +34815,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34038
34815
|
}, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
|
|
34039
34816
|
color: inkColor(getPalette().warn),
|
|
34040
34817
|
bold: true
|
|
34041
|
-
}, "… ") : busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
|
|
34818
|
+
}, "… ") : busy ? (0, import_react.createElement)(BusyChase, { animated: animations }) : (0, import_react.createElement)(Text, {
|
|
34042
34819
|
color: promptColor,
|
|
34043
34820
|
bold: tierActive ? true : void 0
|
|
34044
34821
|
}, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
|
|
@@ -34047,100 +34824,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34047
34824
|
}, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
|
|
34048
34825
|
}
|
|
34049
34826
|
const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
|
|
34050
|
-
|
|
34051
|
-
|
|
34052
|
-
|
|
34053
|
-
|
|
34054
|
-
|
|
34055
|
-
|
|
34056
|
-
|
|
34057
|
-
|
|
34058
|
-
|
|
34059
|
-
|
|
34060
|
-
|
|
34061
|
-
|
|
34062
|
-
|
|
34063
|
-
|
|
34064
|
-
|
|
34065
|
-
|
|
34066
|
-
|
|
34067
|
-
|
|
34068
|
-
|
|
34069
|
-
|
|
34070
|
-
let column = 0;
|
|
34071
|
-
for (let index = 0; index < cells.length; index += 1) {
|
|
34072
|
-
if (column === target) return index;
|
|
34073
|
-
column += cells[index].width ?? visibleColumns(cells[index].char);
|
|
34074
|
-
if (column > target) return void 0;
|
|
34075
|
-
}
|
|
34076
|
-
};
|
|
34077
|
-
const editorWaveRows = visibleRows.map((row, visibleIndex) => {
|
|
34078
|
-
const sourceIndex = editorWindowStart + visibleIndex;
|
|
34079
|
-
const bandRow = visibleIndex + 1;
|
|
34080
|
-
const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
|
|
34081
|
-
const placeholder = sourceIndex === 0 && value === "" && !busy;
|
|
34082
|
-
const cells = [];
|
|
34083
|
-
let usedColumns = 0;
|
|
34084
|
-
const push = (char, extra = {}) => {
|
|
34085
|
-
const width = visibleColumns(char);
|
|
34086
|
-
cells.push({
|
|
34087
|
-
char,
|
|
34088
|
-
width,
|
|
34089
|
-
backgroundColor: waveBg(bandRow, usedColumns),
|
|
34090
|
-
...extra
|
|
34091
|
-
});
|
|
34092
|
-
usedColumns += width;
|
|
34093
|
-
};
|
|
34094
|
-
if (sourceIndex === 0) {
|
|
34095
|
-
push(promptGlyph, {
|
|
34096
|
-
color: promptColor,
|
|
34097
|
-
bold: true
|
|
34098
|
-
});
|
|
34099
|
-
push(" ", { color: promptColor });
|
|
34100
|
-
} else {
|
|
34101
|
-
push(" ");
|
|
34102
|
-
push(" ");
|
|
34103
|
-
}
|
|
34104
|
-
for (const span of splitGraphemes(parts.before)) push(span.text);
|
|
34105
|
-
if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
|
|
34106
|
-
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
|
|
34107
|
-
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
|
|
34108
|
-
while (usedColumns < bandWidth) push(" ");
|
|
34109
|
-
const middleBandRow = Math.floor(totalBandRows / 2);
|
|
34110
|
-
if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
|
|
34111
|
-
const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
|
|
34112
|
-
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
|
|
34113
|
-
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
|
|
34114
|
-
if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
|
|
34115
|
-
const cell = cells[indices[at]];
|
|
34116
|
-
cell.char = word[at];
|
|
34117
|
-
cell.width = 1;
|
|
34118
|
-
cell.color = inkColor(deepseekWaveWordHue(at, hues));
|
|
34119
|
-
cell.bold = true;
|
|
34120
|
-
cell.dim = false;
|
|
34121
|
-
}
|
|
34122
|
-
}
|
|
34123
|
-
if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
|
|
34124
|
-
const spark = deepseekWaveSpark(waveTick);
|
|
34125
|
-
const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
|
|
34126
|
-
if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
|
|
34127
|
-
cells[lastIndex].char = spark;
|
|
34128
|
-
cells[lastIndex].color = promptColor;
|
|
34129
|
-
cells[lastIndex].bold = true;
|
|
34130
|
-
cells[lastIndex].dim = false;
|
|
34131
|
-
}
|
|
34132
|
-
}
|
|
34133
|
-
return (0, import_react.createElement)(Text, {
|
|
34134
|
-
key: `editor-${sourceIndex}`,
|
|
34135
|
-
wrap: "truncate-end"
|
|
34136
|
-
}, ...waveRowSpans(cells));
|
|
34137
|
-
});
|
|
34138
|
-
return (0, import_react.createElement)(Box, {
|
|
34139
|
-
flexDirection: "column",
|
|
34140
|
-
width: bandWidth
|
|
34141
|
-
}, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
|
|
34142
|
-
};
|
|
34143
|
-
return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages ? waveRow() : band(staticEditor));
|
|
34827
|
+
return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, (0, import_react.createElement)(ComposerWave, {
|
|
34828
|
+
key: waveKey ?? "static",
|
|
34829
|
+
tier: waveTier ?? "deepseek",
|
|
34830
|
+
style: waveStyle ?? "wave",
|
|
34831
|
+
active: waveTier !== null && waveStyle !== null && !busy && !preparingImages && animations && waveArmed,
|
|
34832
|
+
onSettled: () => {
|
|
34833
|
+
if (waveKey !== null) setWavePlayedKey(waveKey);
|
|
34834
|
+
},
|
|
34835
|
+
fallback: band(staticEditor),
|
|
34836
|
+
bandWidth,
|
|
34837
|
+
bandBg,
|
|
34838
|
+
rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
|
|
34839
|
+
windowStart: editorWindowStart,
|
|
34840
|
+
caretRow: caret.row,
|
|
34841
|
+
cursor: clampedCursor,
|
|
34842
|
+
caretVisible: cursorVisible,
|
|
34843
|
+
value,
|
|
34844
|
+
promptGlyph,
|
|
34845
|
+
promptColor
|
|
34846
|
+
}));
|
|
34144
34847
|
}
|
|
34145
34848
|
/** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
|
|
34146
34849
|
function buildSettledRow(entry, index, showReasoning, columns) {
|
|
@@ -34318,12 +35021,19 @@ function App(props) {
|
|
|
34318
35021
|
* to static while the prompt marker keeps the tier accent. The trigger
|
|
34319
35022
|
* follows the applied model label (what the status bar actually shows),
|
|
34320
35023
|
* never the initial paint, and the tier is derived from the label and
|
|
34321
|
-
* cached at the switch. The 33ms tick itself lives inside
|
|
34322
|
-
* sweep re-renders only the composer band, not the whole tree,
|
|
34323
|
-
* App owns the rarely-changing tier/style and
|
|
34324
|
-
*
|
|
35024
|
+
* cached at the switch. The 33ms tick itself lives inside the ComposerWave
|
|
35025
|
+
* leaf, so the sweep re-renders only the composer band, not the whole tree,
|
|
35026
|
+
* at 30fps; App owns the rarely-changing tier/style and the leaf plays the
|
|
35027
|
+
* sweep exactly ONCE per pair change — an unchanged model+effort pair
|
|
35028
|
+
* (ordinary turns, image preparation, /animation toggles) never replays. */
|
|
34325
35029
|
const [waveTier, setWaveTier] = (0, import_react.useState)(null);
|
|
34326
35030
|
const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
|
|
35031
|
+
const [animations, setAnimations] = (0, import_react.useState)(props.animations ?? true);
|
|
35032
|
+
const applyAnimations = (enabled) => {
|
|
35033
|
+
setAnimations(enabled);
|
|
35034
|
+
props.saveAnimations?.(enabled);
|
|
35035
|
+
notify(`animations ${enabled ? "on" : "off"}`);
|
|
35036
|
+
};
|
|
34327
35037
|
const previousModel = (0, import_react.useRef)(void 0);
|
|
34328
35038
|
const previousEffort = (0, import_react.useRef)(props.effort);
|
|
34329
35039
|
const previousStyle = (0, import_react.useRef)(void 0);
|
|
@@ -34883,7 +35593,10 @@ function App(props) {
|
|
|
34883
35593
|
continuationPrefix: " ",
|
|
34884
35594
|
dim: true,
|
|
34885
35595
|
maxRows: auditedReasoningRows
|
|
34886
|
-
}) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, {
|
|
35596
|
+
}) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, {
|
|
35597
|
+
text: "✻ Thinking… (Ctrl/Alt+R to expand)",
|
|
35598
|
+
animated: animations
|
|
35599
|
+
}) : (0, import_react.createElement)(StreamTail, {
|
|
34887
35600
|
text: "Thinking… (Ctrl/Alt+R to expand)",
|
|
34888
35601
|
prefix: "✻ ",
|
|
34889
35602
|
continuationPrefix: " ",
|
|
@@ -34894,7 +35607,10 @@ function App(props) {
|
|
|
34894
35607
|
dim: false,
|
|
34895
35608
|
maxRows: auditedAnswerRows,
|
|
34896
35609
|
prefix: " "
|
|
34897
|
-
}, busy ? (0, import_react.createElement)(Caret
|
|
35610
|
+
}, busy ? (0, import_react.createElement)(Caret, { animated: animations }) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, {
|
|
35611
|
+
since: view.busySince,
|
|
35612
|
+
animated: animations
|
|
35613
|
+
}) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
|
|
34898
35614
|
rows: agentRows,
|
|
34899
35615
|
total: props.subagents.getTotalSeen()
|
|
34900
35616
|
}) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
|
|
@@ -35132,6 +35848,9 @@ function App(props) {
|
|
|
35132
35848
|
loadMentions: props.loadMentions,
|
|
35133
35849
|
inspectImages: props.inspectImages,
|
|
35134
35850
|
prepareImages: props.prepareImages,
|
|
35851
|
+
inspectFiles: props.inspectFiles,
|
|
35852
|
+
prepareFiles: props.prepareFiles,
|
|
35853
|
+
sessionKey: props.sessionKey,
|
|
35135
35854
|
cyclePermission: props.cyclePermission,
|
|
35136
35855
|
exportTranscript: props.exportTranscript,
|
|
35137
35856
|
renameTitle: props.renameTitle,
|
|
@@ -35143,6 +35862,8 @@ function App(props) {
|
|
|
35143
35862
|
cancelQueued: props.cancelQueued,
|
|
35144
35863
|
historyFill,
|
|
35145
35864
|
historyConsumed,
|
|
35865
|
+
animations,
|
|
35866
|
+
applyAnimations,
|
|
35146
35867
|
waveTier,
|
|
35147
35868
|
waveStyle,
|
|
35148
35869
|
maxRows: composerEditorCap,
|
|
@@ -35420,26 +36141,36 @@ const internals = {
|
|
|
35420
36141
|
mount: (element) => {
|
|
35421
36142
|
const keyboardEnhanced = shouldEnableKeyboardEnhancement();
|
|
35422
36143
|
const focusReporting = isVsCodeTerminalEnv();
|
|
35423
|
-
process.
|
|
35424
|
-
|
|
35425
|
-
|
|
35426
|
-
|
|
35427
|
-
|
|
35428
|
-
|
|
35429
|
-
|
|
35430
|
-
|
|
35431
|
-
|
|
35432
|
-
|
|
35433
|
-
|
|
35434
|
-
|
|
35435
|
-
|
|
35436
|
-
|
|
35437
|
-
|
|
35438
|
-
|
|
35439
|
-
|
|
36144
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(true);
|
|
36145
|
+
try {
|
|
36146
|
+
process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
|
|
36147
|
+
const tuiStdin = createSplitStdin(process.stdin);
|
|
36148
|
+
const instance = render(element, {
|
|
36149
|
+
exitOnCtrlC: false,
|
|
36150
|
+
stdin: tuiStdin.stdin,
|
|
36151
|
+
stdout: process.stdout
|
|
36152
|
+
});
|
|
36153
|
+
return {
|
|
36154
|
+
rerender(element) {
|
|
36155
|
+
instance.rerender(element);
|
|
36156
|
+
},
|
|
36157
|
+
unmount() {
|
|
36158
|
+
try {
|
|
36159
|
+
process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
|
|
36160
|
+
} finally {
|
|
36161
|
+
try {
|
|
36162
|
+
instance.unmount();
|
|
36163
|
+
} finally {
|
|
36164
|
+
tuiStdin.dispose();
|
|
36165
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false);
|
|
36166
|
+
}
|
|
36167
|
+
}
|
|
35440
36168
|
}
|
|
35441
|
-
}
|
|
35442
|
-
}
|
|
36169
|
+
};
|
|
36170
|
+
} catch (error) {
|
|
36171
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false);
|
|
36172
|
+
throw error;
|
|
36173
|
+
}
|
|
35443
36174
|
},
|
|
35444
36175
|
stderr: process.stderr
|
|
35445
36176
|
};
|
|
@@ -35789,13 +36520,15 @@ async function syncModelCapabilities(ctx, notify) {
|
|
|
35789
36520
|
//#region src/questions.ts
|
|
35790
36521
|
const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
|
|
35791
36522
|
/**
|
|
35792
|
-
* Mount the
|
|
35793
|
-
* @param ctx - context
|
|
35794
|
-
* @
|
|
35795
|
-
*
|
|
36523
|
+
* Mount the `user-questions/request` answerer over a FIFO queue.
|
|
36524
|
+
* @param ctx - plugin context whose event bus carries the waterfall.
|
|
36525
|
+
* @param owns - agents this terminal answers for; every other request is
|
|
36526
|
+
* deferred back into the waterfall (`next()`), so sibling answerers stay
|
|
36527
|
+
* usable. Agent-less asks are claimed: this TUI is the only human surface
|
|
36528
|
+
* in the process.
|
|
36529
|
+
* @returns the store the renderer subscribes to.
|
|
35796
36530
|
*/
|
|
35797
|
-
function mountQuestionProvider(ctx) {
|
|
35798
|
-
const service = ctx.get("userQuestions");
|
|
36531
|
+
function mountQuestionProvider(ctx, owns) {
|
|
35799
36532
|
let snapshot = { pending: void 0 };
|
|
35800
36533
|
let active;
|
|
35801
36534
|
const queue = [];
|
|
@@ -35810,54 +36543,40 @@ function mountQuestionProvider(ctx) {
|
|
|
35810
36543
|
active = next;
|
|
35811
36544
|
set({ pending: next });
|
|
35812
36545
|
};
|
|
35813
|
-
|
|
35814
|
-
if (
|
|
35815
|
-
|
|
35816
|
-
|
|
35817
|
-
|
|
35818
|
-
|
|
35819
|
-
|
|
35820
|
-
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
|
|
35824
|
-
submit() {},
|
|
35825
|
-
cancel() {}
|
|
35826
|
-
};
|
|
35827
|
-
service.registerProvider({ ask(request) {
|
|
35828
|
-
return new Promise((resolve, reject) => {
|
|
35829
|
-
const onAbort = () => {
|
|
35830
|
-
if (active === pending) {
|
|
35831
|
-
active = void 0;
|
|
35832
|
-
set({ pending: void 0 });
|
|
35833
|
-
advance();
|
|
35834
|
-
} else {
|
|
35835
|
-
const at = queue.indexOf(pending);
|
|
35836
|
-
if (at >= 0) queue.splice(at, 1);
|
|
35837
|
-
}
|
|
35838
|
-
reject(ABORT_ERROR);
|
|
35839
|
-
};
|
|
35840
|
-
const detachAbort = () => {
|
|
35841
|
-
if (request.signal !== void 0) request.signal.removeEventListener("abort", onAbort);
|
|
35842
|
-
};
|
|
35843
|
-
const pending = {
|
|
35844
|
-
request,
|
|
35845
|
-
resolve,
|
|
35846
|
-
reject,
|
|
35847
|
-
detachAbort
|
|
35848
|
-
};
|
|
35849
|
-
if (request.signal?.aborted === true) {
|
|
35850
|
-
reject(ABORT_ERROR);
|
|
35851
|
-
return;
|
|
36546
|
+
ctx.on("user-questions/request", (request, next) => {
|
|
36547
|
+
if (request.agent !== void 0 && !owns(request.agent)) return next();
|
|
36548
|
+
return new Promise((resolve, reject) => {
|
|
36549
|
+
const onAbort = () => {
|
|
36550
|
+
if (active === pending) {
|
|
36551
|
+
active = void 0;
|
|
36552
|
+
set({ pending: void 0 });
|
|
36553
|
+
advance();
|
|
36554
|
+
} else {
|
|
36555
|
+
const at = queue.indexOf(pending);
|
|
36556
|
+
if (at >= 0) queue.splice(at, 1);
|
|
35852
36557
|
}
|
|
35853
|
-
|
|
35854
|
-
|
|
35855
|
-
|
|
35856
|
-
|
|
35857
|
-
|
|
35858
|
-
|
|
35859
|
-
|
|
35860
|
-
|
|
36558
|
+
reject(ABORT_ERROR);
|
|
36559
|
+
};
|
|
36560
|
+
const detachAbort = () => {
|
|
36561
|
+
if (request.signal !== void 0) request.signal.removeEventListener("abort", onAbort);
|
|
36562
|
+
};
|
|
36563
|
+
const pending = {
|
|
36564
|
+
request,
|
|
36565
|
+
resolve,
|
|
36566
|
+
reject,
|
|
36567
|
+
detachAbort
|
|
36568
|
+
};
|
|
36569
|
+
if (request.signal?.aborted === true) {
|
|
36570
|
+
reject(ABORT_ERROR);
|
|
36571
|
+
return;
|
|
36572
|
+
}
|
|
36573
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
36574
|
+
if (active === void 0) {
|
|
36575
|
+
active = pending;
|
|
36576
|
+
set({ pending });
|
|
36577
|
+
} else queue.push(pending);
|
|
36578
|
+
});
|
|
36579
|
+
});
|
|
35861
36580
|
return {
|
|
35862
36581
|
subscribe(listener) {
|
|
35863
36582
|
listeners.add(listener);
|
|
@@ -35908,6 +36627,7 @@ function createTranscriptStore(replay) {
|
|
|
35908
36627
|
const listeners = /* @__PURE__ */ new Set();
|
|
35909
36628
|
let scheduled = false;
|
|
35910
36629
|
let lastNotifyAt = 0;
|
|
36630
|
+
const attemptKeys = /* @__PURE__ */ new Map();
|
|
35911
36631
|
const notify = () => {
|
|
35912
36632
|
if (scheduled) return;
|
|
35913
36633
|
scheduled = true;
|
|
@@ -35939,8 +36659,28 @@ function createTranscriptStore(replay) {
|
|
|
35939
36659
|
dirty = true;
|
|
35940
36660
|
notify();
|
|
35941
36661
|
},
|
|
36662
|
+
applyStreamFrame(frame) {
|
|
36663
|
+
if (frame.type === "start") {
|
|
36664
|
+
attemptKeys.set(frame.attemptId, `${frame.turn}:${frame.step}`);
|
|
36665
|
+
return;
|
|
36666
|
+
}
|
|
36667
|
+
if (frame.type === "chunk") {
|
|
36668
|
+
const key = attemptKeys.get(frame.attemptId);
|
|
36669
|
+
if (key === void 0) return;
|
|
36670
|
+
if (!applyAssistantStreamChunk(acc, key, frame.time, frame.chunk)) return;
|
|
36671
|
+
dirty = true;
|
|
36672
|
+
notify();
|
|
36673
|
+
return;
|
|
36674
|
+
}
|
|
36675
|
+
attemptKeys.delete(frame.attemptId);
|
|
36676
|
+
if (frame.outcome.kind === "abandoned" && clearAssistantStream(acc)) {
|
|
36677
|
+
dirty = true;
|
|
36678
|
+
notify();
|
|
36679
|
+
}
|
|
36680
|
+
},
|
|
35942
36681
|
reset() {
|
|
35943
36682
|
acc = createReplayAccumulator();
|
|
36683
|
+
attemptKeys.clear();
|
|
35944
36684
|
dirty = true;
|
|
35945
36685
|
notify();
|
|
35946
36686
|
}
|
|
@@ -36008,12 +36748,27 @@ function foldSubagentRow(previous, sessionId, event) {
|
|
|
36008
36748
|
activity: "prompted",
|
|
36009
36749
|
updatedAt: event.time
|
|
36010
36750
|
};
|
|
36011
|
-
case "assistant/
|
|
36751
|
+
case "assistant/attempt": return {
|
|
36012
36752
|
...base,
|
|
36013
36753
|
state: "running",
|
|
36014
36754
|
activity: "thinking…",
|
|
36015
36755
|
updatedAt: event.time
|
|
36016
36756
|
};
|
|
36757
|
+
case "subagent/catalog": {
|
|
36758
|
+
const mode = event.data.mode === "continuable" ? "continuable" : "one-shot";
|
|
36759
|
+
const label = event.data.label !== void 0 && event.data.label.trim() !== "" ? bound(event.data.label) : void 0;
|
|
36760
|
+
const nextLabel = label === void 0 ? base.label : label;
|
|
36761
|
+
const activity = label === void 0 ? `catalog · ${mode}` : `catalog · ${mode} · ${label}`;
|
|
36762
|
+
const state = previous === void 0 ? "idle" : base.state;
|
|
36763
|
+
if (nextLabel === base.label && state === base.state && activity === base.activity) return base;
|
|
36764
|
+
return {
|
|
36765
|
+
...base,
|
|
36766
|
+
state,
|
|
36767
|
+
label: nextLabel,
|
|
36768
|
+
activity,
|
|
36769
|
+
updatedAt: event.time
|
|
36770
|
+
};
|
|
36771
|
+
}
|
|
36017
36772
|
case "assistant/message": return {
|
|
36018
36773
|
...base,
|
|
36019
36774
|
state: "idle",
|
|
@@ -36089,7 +36844,7 @@ function createSubagentFeed() {
|
|
|
36089
36844
|
const counted = !seen.has(sessionId);
|
|
36090
36845
|
if (counted) seen.add(sessionId);
|
|
36091
36846
|
if (rows.length >= 8) {
|
|
36092
|
-
const evict = rows.findIndex((row) => row.state
|
|
36847
|
+
const evict = rows.findIndex((row) => row.state !== "running");
|
|
36093
36848
|
if (evict === -1) {
|
|
36094
36849
|
if (counted) notify();
|
|
36095
36850
|
return;
|
|
@@ -36149,8 +36904,8 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36149
36904
|
const listeners = /* @__PURE__ */ new Set();
|
|
36150
36905
|
const reload = () => {
|
|
36151
36906
|
const target = agent;
|
|
36152
|
-
if (skills === void 0
|
|
36153
|
-
Promise.resolve().then(() => skills.list({
|
|
36907
|
+
if (skills === void 0) return;
|
|
36908
|
+
Promise.resolve().then(() => skills.list(target === void 0 ? { cwd: fallbackCwd } : {
|
|
36154
36909
|
cwd: target.session.header.cwd ?? fallbackCwd,
|
|
36155
36910
|
scope: target
|
|
36156
36911
|
})).then((summaries) => {
|
|
@@ -36165,13 +36920,18 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36165
36920
|
for (const listener of listeners) listener();
|
|
36166
36921
|
}).catch((cause) => {
|
|
36167
36922
|
if (agent !== target) return;
|
|
36923
|
+
const nextError = cause instanceof Error ? cause.message : String(cause);
|
|
36168
36924
|
if (loadedFor !== target) rows = [];
|
|
36169
|
-
|
|
36170
|
-
error =
|
|
36925
|
+
const errorChanged = nextError !== error;
|
|
36926
|
+
error = nextError;
|
|
36927
|
+
if (!errorChanged) return;
|
|
36171
36928
|
for (const listener of listeners) listener();
|
|
36172
36929
|
});
|
|
36173
36930
|
};
|
|
36174
|
-
if (skills !== void 0)
|
|
36931
|
+
if (skills !== void 0) {
|
|
36932
|
+
ctx.on("skills/change", reload);
|
|
36933
|
+
reload();
|
|
36934
|
+
}
|
|
36175
36935
|
return {
|
|
36176
36936
|
get rows() {
|
|
36177
36937
|
return rows;
|
|
@@ -36206,16 +36966,22 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36206
36966
|
* @param sessionId - the full session identity for the header.
|
|
36207
36967
|
* @returns the complete markdown text.
|
|
36208
36968
|
*/
|
|
36969
|
+
/** Both user and queued rows export the same attachment label block. */
|
|
36970
|
+
const attachmentLabels = (entry) => [imageLabels(entry.images), fileLabels(entry.files)].filter((label) => label !== "").join("\n");
|
|
36209
36971
|
function buildExportMarkdown(view, sessionId) {
|
|
36210
36972
|
const out = [
|
|
36211
36973
|
view.title === "" ? `# dsh session ${sessionId}` : `# ${view.title}`,
|
|
36212
36974
|
`> session ${sessionId}`,
|
|
36213
36975
|
""
|
|
36214
36976
|
];
|
|
36977
|
+
if (view.systemPrompt !== "") out.push("<details><summary>system prompt</summary>", "", view.systemPrompt, "", "</details>", "");
|
|
36215
36978
|
for (const entry of view.entries) switch (entry.kind) {
|
|
36216
36979
|
case "user":
|
|
36217
36980
|
if (entry.notice) out.push(`> ⤷ context: ${entry.text}`, "");
|
|
36218
|
-
else
|
|
36981
|
+
else {
|
|
36982
|
+
const attachments = attachmentLabels(entry);
|
|
36983
|
+
out.push("## user", "", entry.text, ...attachments === "" ? [] : [attachments], "");
|
|
36984
|
+
}
|
|
36219
36985
|
break;
|
|
36220
36986
|
case "assistant":
|
|
36221
36987
|
if (entry.reasoning !== "") out.push("<details><summary>thinking</summary>", "", entry.reasoning, "", "</details>", "");
|
|
@@ -36248,7 +37014,7 @@ function buildExportMarkdown(view, sessionId) {
|
|
|
36248
37014
|
out.push(`> files changed: ${entry.paths.join(", ")}`, "");
|
|
36249
37015
|
break;
|
|
36250
37016
|
case "pending":
|
|
36251
|
-
out.push("## user", "", entry.text, ...
|
|
37017
|
+
out.push("## user", "", entry.text, ...attachmentLabels(entry) === "" ? [] : [attachmentLabels(entry)], "");
|
|
36252
37018
|
break;
|
|
36253
37019
|
default: assertNever(entry, "transcript entry kind");
|
|
36254
37020
|
}
|
|
@@ -36633,7 +37399,12 @@ function selectForkSeed(events, atSeq) {
|
|
|
36633
37399
|
}
|
|
36634
37400
|
//#endregion
|
|
36635
37401
|
//#region src/git-workflow.ts
|
|
36636
|
-
/**
|
|
37402
|
+
/**
|
|
37403
|
+
* Read-only Git inspection used by /diff and /review. Every diff
|
|
37404
|
+
* invocation carries --no-ext-diff and --no-textconv, so configured
|
|
37405
|
+
* external diff drivers and text converters can never execute as a
|
|
37406
|
+
* side effect of reading a diff.
|
|
37407
|
+
*/
|
|
36637
37408
|
/** Split Git's stable `diff --git` framing without interpreting patch content. */
|
|
36638
37409
|
function parseGitDiffFiles(text) {
|
|
36639
37410
|
if (text === "") return [];
|
|
@@ -36656,6 +37427,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36656
37427
|
args: [
|
|
36657
37428
|
"diff",
|
|
36658
37429
|
"--no-ext-diff",
|
|
37430
|
+
"--no-textconv",
|
|
36659
37431
|
"--unified=3",
|
|
36660
37432
|
"HEAD",
|
|
36661
37433
|
"--"
|
|
@@ -36666,6 +37438,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36666
37438
|
args: [
|
|
36667
37439
|
"diff",
|
|
36668
37440
|
"--no-ext-diff",
|
|
37441
|
+
"--no-textconv",
|
|
36669
37442
|
"--unified=3",
|
|
36670
37443
|
"--cached",
|
|
36671
37444
|
"--"
|
|
@@ -36677,6 +37450,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36677
37450
|
args: [
|
|
36678
37451
|
"diff",
|
|
36679
37452
|
"--no-ext-diff",
|
|
37453
|
+
"--no-textconv",
|
|
36680
37454
|
"--unified=3",
|
|
36681
37455
|
value,
|
|
36682
37456
|
"--"
|
|
@@ -36700,8 +37474,25 @@ function executeGit(cwd, args, signal) {
|
|
|
36700
37474
|
});
|
|
36701
37475
|
});
|
|
36702
37476
|
}
|
|
37477
|
+
/** Arguments for the unstaged-only fallback below. */
|
|
37478
|
+
const UNSTAGED_DIFF_ARGS = [
|
|
37479
|
+
"diff",
|
|
37480
|
+
"--no-ext-diff",
|
|
37481
|
+
"--no-textconv",
|
|
37482
|
+
"--unified=3",
|
|
37483
|
+
"--"
|
|
37484
|
+
];
|
|
37485
|
+
/** Whether the repository has at least one commit (a HEAD revision). */
|
|
37486
|
+
function hasHeadRevision(cwd, signal) {
|
|
37487
|
+
return executeGit(cwd, [
|
|
37488
|
+
"rev-parse",
|
|
37489
|
+
"--verify",
|
|
37490
|
+
"--quiet",
|
|
37491
|
+
"HEAD"
|
|
37492
|
+
], signal).then(() => true).catch(() => false);
|
|
37493
|
+
}
|
|
36703
37494
|
/**
|
|
36704
|
-
* Load one complete textual diff without invoking external
|
|
37495
|
+
* Load one complete textual diff without invoking external programs.
|
|
36705
37496
|
* @param signal - aborted by the caller on session switches/quit, killing the
|
|
36706
37497
|
* git subprocess instead of letting a stale repository's diff land later.
|
|
36707
37498
|
*/
|
|
@@ -36714,15 +37505,10 @@ async function loadGitDiff(cwd, argument, signal) {
|
|
|
36714
37505
|
files: parseGitDiffFiles(text)
|
|
36715
37506
|
};
|
|
36716
37507
|
} catch (error) {
|
|
36717
|
-
if (argument.trim() !== "") throw error;
|
|
37508
|
+
if (argument.trim() !== "" || signal?.aborted === true || await hasHeadRevision(cwd, signal)) throw error;
|
|
36718
37509
|
return {
|
|
36719
|
-
title: "git diff - working tree",
|
|
36720
|
-
files: parseGitDiffFiles(await executeGit(cwd,
|
|
36721
|
-
"diff",
|
|
36722
|
-
"--no-ext-diff",
|
|
36723
|
-
"--unified=3",
|
|
36724
|
-
"--"
|
|
36725
|
-
], signal))
|
|
37510
|
+
title: "git diff - working tree (no commits yet)",
|
|
37511
|
+
files: parseGitDiffFiles(await executeGit(cwd, UNSTAGED_DIFF_ARGS, signal))
|
|
36726
37512
|
};
|
|
36727
37513
|
}
|
|
36728
37514
|
}
|
|
@@ -36797,16 +37583,24 @@ function agentPresetsFrom(ctx) {
|
|
|
36797
37583
|
function isBlankSession(events) {
|
|
36798
37584
|
return !events.some((event) => event.type === "turn/start");
|
|
36799
37585
|
}
|
|
37586
|
+
/** Upstream renamed the shipped `code` preset to `ptc` in 0.1.2-rc.1; sessions
|
|
37587
|
+
* and CLI choices recorded before the rename keep resolving through this map. */
|
|
37588
|
+
const LEGACY_PRESET_IDS = { code: "ptc" };
|
|
37589
|
+
function normalizePresetId(id) {
|
|
37590
|
+
return id === void 0 ? void 0 : LEGACY_PRESET_IDS[id] ?? id;
|
|
37591
|
+
}
|
|
36800
37592
|
/** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
|
|
36801
37593
|
function resolvePreset(session) {
|
|
36802
|
-
|
|
36803
|
-
|
|
36804
|
-
|
|
37594
|
+
const events = session.snapshotEvents();
|
|
37595
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
37596
|
+
const event = events[index];
|
|
37597
|
+
if (event.type === "agent-preset/selected" && event.data?.agentPreset !== void 0) return normalizePresetId(event.data.agentPreset);
|
|
36805
37598
|
}
|
|
36806
|
-
return session.header.agentPreset ?? "standard";
|
|
37599
|
+
return normalizePresetId(session.header.agentPreset) ?? "standard";
|
|
36807
37600
|
}
|
|
36808
37601
|
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
36809
37602
|
async function selectPreset(service, agent, presetId) {
|
|
37603
|
+
presetId = normalizePresetId(presetId);
|
|
36810
37604
|
if (agent !== void 0) return switchPreset(service, agent, presetId);
|
|
36811
37605
|
const preset = await service.resolve(presetId);
|
|
36812
37606
|
if (preset.broken !== void 0) throw new Error(preset.broken);
|
|
@@ -36814,7 +37608,7 @@ async function selectPreset(service, agent, presetId) {
|
|
|
36814
37608
|
}
|
|
36815
37609
|
/** Recompose atomically from the caller's perspective, logging only success. */
|
|
36816
37610
|
async function switchPreset(service, agent, presetId) {
|
|
36817
|
-
if (!isBlankSession(agent.session.
|
|
37611
|
+
if (!isBlankSession(agent.session.snapshotEvents())) throw new Error("mode is locked after the first turn; use /new <mode>");
|
|
36818
37612
|
const preset = await service.recompose(agent.ctx, presetId);
|
|
36819
37613
|
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
36820
37614
|
return preset;
|
|
@@ -36827,7 +37621,7 @@ function permissionPresetsFrom(ctx) {
|
|
|
36827
37621
|
}
|
|
36828
37622
|
/** Effective label for either an active session or the not-yet-created first one. */
|
|
36829
37623
|
function effectivePermission(service, session, pending) {
|
|
36830
|
-
return session === void 0 ? pending ?? service.defaultPreset : service.current(session
|
|
37624
|
+
return session === void 0 ? pending ?? service.defaultPreset : service.current(session);
|
|
36831
37625
|
}
|
|
36832
37626
|
/** Validate a preset and write it only when a durable session already exists. */
|
|
36833
37627
|
function selectPermission(service, session, preset) {
|
|
@@ -36909,6 +37703,38 @@ function listPluginRows(ctx) {
|
|
|
36909
37703
|
* @module @deepseek-ai/dsh-code/settings-file
|
|
36910
37704
|
*/
|
|
36911
37705
|
/**
|
|
37706
|
+
* Run one file operation with a bounded retry: one initial try plus at
|
|
37707
|
+
* most `retries` more. Creating or replacing a file can fail transiently
|
|
37708
|
+
* with EPERM/EACCES while an antivirus scanner or search indexer holds
|
|
37709
|
+
* it — the standard graceful-fs remedy, not a workaround for a
|
|
37710
|
+
* persistent permission problem. A save that still fails leaves its
|
|
37711
|
+
* uniquely named temp file behind, so repeated crashed saves accumulate
|
|
37712
|
+
* distinct leftovers rather than corrupting a shared one.
|
|
37713
|
+
*/
|
|
37714
|
+
async function withTransientRetry(operation, retries = 5) {
|
|
37715
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
37716
|
+
await operation();
|
|
37717
|
+
return;
|
|
37718
|
+
} catch (error) {
|
|
37719
|
+
const code = error.code;
|
|
37720
|
+
if (attempt >= retries || code !== "EPERM" && code !== "EACCES") throw error;
|
|
37721
|
+
await new Promise((resolve) => setTimeout(resolve, 30 * (attempt + 1)));
|
|
37722
|
+
}
|
|
37723
|
+
}
|
|
37724
|
+
/**
|
|
37725
|
+
* Write one file atomically: create the parent directory, write to a
|
|
37726
|
+
* uniquely named temp file, and rename it into place. A crash midway
|
|
37727
|
+
* can never leave a half-written document behind. Unique temp names
|
|
37728
|
+
* keep concurrent writers (two terminals, two chains in one process)
|
|
37729
|
+
* from sharing one temp path.
|
|
37730
|
+
*/
|
|
37731
|
+
async function writeFileAtomically(path, text) {
|
|
37732
|
+
await mkdir(dirname(path), { recursive: true });
|
|
37733
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
37734
|
+
await withTransientRetry(() => writeFile(temp, text, "utf8"));
|
|
37735
|
+
await withTransientRetry(() => rename(temp, path));
|
|
37736
|
+
}
|
|
37737
|
+
/**
|
|
36912
37738
|
* Create the shared settings-write chain. One instance per process keeps
|
|
36913
37739
|
* every user-level JSON file mutually serialized.
|
|
36914
37740
|
* @returns the persistence handle.
|
|
@@ -36917,12 +37743,7 @@ function createUserSettingsPersistence() {
|
|
|
36917
37743
|
let chain = Promise.resolve();
|
|
36918
37744
|
return {
|
|
36919
37745
|
save(path, text) {
|
|
36920
|
-
const write = chain.then(
|
|
36921
|
-
await mkdir(dirname(path), { recursive: true });
|
|
36922
|
-
const temp = `${path}.tmp`;
|
|
36923
|
-
await writeFile(temp, text, "utf8");
|
|
36924
|
-
await rename(temp, path);
|
|
36925
|
-
});
|
|
37746
|
+
const write = chain.then(() => writeFileAtomically(path, text));
|
|
36926
37747
|
chain = write.catch(() => {});
|
|
36927
37748
|
return write;
|
|
36928
37749
|
},
|
|
@@ -37044,6 +37865,17 @@ async function runQuitSequence(steps, exit, onError) {
|
|
|
37044
37865
|
return started;
|
|
37045
37866
|
}
|
|
37046
37867
|
/**
|
|
37868
|
+
* Whether a tagged submission still belongs to the active session. Attachment
|
|
37869
|
+
* prepares resolve on the microtask timeline, while a queued session switch
|
|
37870
|
+
* remounts the app asynchronously — the composing instance's unmount cleanup
|
|
37871
|
+
* runs too late to abort, so the delivery itself carries the composing
|
|
37872
|
+
* session's full id and the runner drops it here when the world moved on.
|
|
37873
|
+
* An untagged (synchronous) or pending-session ('') submission always passes.
|
|
37874
|
+
*/
|
|
37875
|
+
function submissionBelongsToSession(origin, activeSessionId) {
|
|
37876
|
+
return origin === void 0 || origin === "" || origin === activeSessionId;
|
|
37877
|
+
}
|
|
37878
|
+
/**
|
|
37047
37879
|
* Order-preserving gate for composer input while the startup prompt/images
|
|
37048
37880
|
* are still preparing. Anything submitted before the startup delivery settles
|
|
37049
37881
|
* queues and flushes afterwards in submit order, so the initial request can
|
|
@@ -37094,7 +37926,7 @@ async function resolveTarget(startup, persistence, cwd) {
|
|
|
37094
37926
|
};
|
|
37095
37927
|
if (startup.kind === "named") {
|
|
37096
37928
|
if (persistence !== void 0) {
|
|
37097
|
-
if ((await persistence.list()).some((header) => header.id === startup.sessionId)) throw new Error(`session "${startup.sessionId}" already exists; use --resume to continue it`);
|
|
37929
|
+
if ((await persistence.list()).map((snapshot) => snapshot.header).some((header) => header.id === startup.sessionId)) throw new Error(`session "${startup.sessionId}" already exists; use --resume to continue it`);
|
|
37098
37930
|
}
|
|
37099
37931
|
return {
|
|
37100
37932
|
sessionId: startup.sessionId,
|
|
@@ -37103,7 +37935,7 @@ async function resolveTarget(startup, persistence, cwd) {
|
|
|
37103
37935
|
};
|
|
37104
37936
|
}
|
|
37105
37937
|
if (persistence === void 0) throw new Error("cannot resolve the requested session: session persistence is not configured");
|
|
37106
|
-
const headers = await persistence.list();
|
|
37938
|
+
const headers = (await persistence.list()).map((snapshot) => snapshot.header);
|
|
37107
37939
|
if (startup.kind === "resume") {
|
|
37108
37940
|
const matched = matchSessionId(headers, startup.sessionId);
|
|
37109
37941
|
if (isSubagentSession(matched)) throw new Error("subagent conversations are read-only; resume a root session");
|
|
@@ -37161,13 +37993,13 @@ async function run(ctx, startup, io) {
|
|
|
37161
37993
|
const nextCwd = next.cwd ?? cwd;
|
|
37162
37994
|
const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
|
|
37163
37995
|
let mode = next.resume ? next.mode : next.mode ?? pendingMode;
|
|
37164
|
-
if (!next.resume) mode = (await presets.resolve(mode)).id;
|
|
37165
|
-
const setup = async (agentCtx) => {
|
|
37166
|
-
const sessionPreset = next.resume ? resolvePreset(
|
|
37996
|
+
if (!next.resume) mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
|
|
37997
|
+
const setup = async (agentCtx, agent) => {
|
|
37998
|
+
const sessionPreset = next.resume ? resolvePreset(agent.session) : mode;
|
|
37167
37999
|
mode = (await presets.mount(agentCtx, sessionPreset)).id;
|
|
37168
38000
|
installModelSelection(agentCtx, {
|
|
37169
38001
|
get current() {
|
|
37170
|
-
return resolveEffectiveSelection(selectionState.picked,
|
|
38002
|
+
return resolveEffectiveSelection(selectionState.picked, agent.session.requestHeader()?.config, currentDefaults());
|
|
37171
38003
|
},
|
|
37172
38004
|
set current(value) {
|
|
37173
38005
|
selectionState.picked = value;
|
|
@@ -37193,8 +38025,9 @@ async function run(ctx, startup, io) {
|
|
|
37193
38025
|
cwd: nextCwd,
|
|
37194
38026
|
agentPreset: mode,
|
|
37195
38027
|
...next.parentSession === void 0 ? {} : { parentSession: next.parentSession },
|
|
37196
|
-
...next.seedLength === void 0 ? {} : {
|
|
38028
|
+
...next.seedLength === void 0 ? {} : { isSeeded: true }
|
|
37197
38029
|
},
|
|
38030
|
+
...next.seedLength === void 0 ? {} : { inheritedEventCount: SessionLogOffset(next.seedLength) },
|
|
37198
38031
|
...next.seed === void 0 ? {} : { seed: next.seed },
|
|
37199
38032
|
agentOptions: seedOptions,
|
|
37200
38033
|
signal: quitAbort.signal,
|
|
@@ -37206,7 +38039,7 @@ async function run(ctx, startup, io) {
|
|
|
37206
38039
|
handle,
|
|
37207
38040
|
agent: handle.agent,
|
|
37208
38041
|
session,
|
|
37209
|
-
store: createTranscriptStore(session.
|
|
38042
|
+
store: createTranscriptStore(session.snapshotEvents()),
|
|
37210
38043
|
mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
|
|
37211
38044
|
mode: mode ?? "standard",
|
|
37212
38045
|
selection: selectionState,
|
|
@@ -37271,10 +38104,15 @@ async function run(ctx, startup, io) {
|
|
|
37271
38104
|
if (session === void 0) return;
|
|
37272
38105
|
if (subject.id === session.id) {
|
|
37273
38106
|
store.apply(event);
|
|
38107
|
+
if (event.type === "subagent/catalog" && event.data.childId !== "") subagents.apply(event.data.childId, event);
|
|
37274
38108
|
return;
|
|
37275
38109
|
}
|
|
37276
38110
|
if (subject.header.parentSession === session.id && subject.header.origin === "subagent") subagents.apply(subject.id, event);
|
|
37277
38111
|
});
|
|
38112
|
+
ctx.on("agent/assistant-stream", ({ agent: source, frame }) => {
|
|
38113
|
+
if (agent === void 0 || source.id !== agent.id) return;
|
|
38114
|
+
store.applyStreamFrame(frame);
|
|
38115
|
+
});
|
|
37278
38116
|
const commands = watchCommands(ctx);
|
|
37279
38117
|
if (agent !== void 0) commands.setAgent(agent);
|
|
37280
38118
|
const skills = watchSkills(ctx, cwd);
|
|
@@ -37290,7 +38128,7 @@ async function run(ctx, startup, io) {
|
|
|
37290
38128
|
const picked = subagentOverride ?? resolveEffectiveSelection(belongsToActive && activeAgent !== void 0 ? activeAgent.selection.picked ?? pendingSelection : void 0, subject.session.requestHeader()?.config, currentDefaults());
|
|
37291
38129
|
return next().then((resolved) => applyModelSelectionToConfig(resolved, picked));
|
|
37292
38130
|
});
|
|
37293
|
-
const questions = mountQuestionProvider(ctx);
|
|
38131
|
+
const questions = mountQuestionProvider(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id);
|
|
37294
38132
|
const bridge = { notify: () => {} };
|
|
37295
38133
|
const capabilitySyncDebounceMs = 400;
|
|
37296
38134
|
const runCapabilitySync = () => {
|
|
@@ -37346,19 +38184,42 @@ async function run(ctx, startup, io) {
|
|
|
37346
38184
|
bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
37347
38185
|
});
|
|
37348
38186
|
};
|
|
38187
|
+
const animationsPath = join(homedir(), ".dsh", "dsh-code", "animations.json");
|
|
38188
|
+
let animationsEnabled = true;
|
|
38189
|
+
let animationsWarning;
|
|
38190
|
+
try {
|
|
38191
|
+
animationsEnabled = parseAnimationsPref((JSON.parse(readFileSync(animationsPath, "utf8")) ?? {}).animations);
|
|
38192
|
+
} catch (error) {
|
|
38193
|
+
if (error.code !== "ENOENT") animationsWarning = error instanceof Error ? error.message : String(error);
|
|
38194
|
+
}
|
|
38195
|
+
const saveAnimations = (enabled) => {
|
|
38196
|
+
settingsPersistence.save(animationsPath, JSON.stringify({ animations: enabled }, null, 2) + "\n").catch((writeError) => {
|
|
38197
|
+
bridge.notify("animations save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
38198
|
+
});
|
|
38199
|
+
};
|
|
37349
38200
|
const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
|
|
37350
38201
|
let inputHistory = [];
|
|
38202
|
+
let historyWriteChain = Promise.resolve();
|
|
37351
38203
|
try {
|
|
37352
|
-
|
|
38204
|
+
const rawHistory = readFileSync(historyPath, "utf8");
|
|
38205
|
+
inputHistory = parseHistoryFile(rawHistory);
|
|
38206
|
+
if (needsCompaction(rawHistory)) historyWriteChain = historyWriteChain.then(() => writeFileAtomically(historyPath, serializeHistoryList(inputHistory))).catch(() => {});
|
|
37353
38207
|
} catch {
|
|
37354
38208
|
inputHistory = [];
|
|
37355
38209
|
}
|
|
37356
|
-
/**
|
|
37357
|
-
|
|
38210
|
+
/**
|
|
38211
|
+
* Serialized history writes: each submission appends one JSON line at the
|
|
38212
|
+
* end of the file, so concurrent terminals add entries after each other
|
|
38213
|
+
* instead of overwriting snapshots they read at their own boot. A
|
|
38214
|
+
* multi-line draft still occupies one physical line (JSON escapes the
|
|
38215
|
+
* newline), and a regular-length line reaches the disk as one positioned
|
|
38216
|
+
* write; an oversized paste may interleave mid-line, which the next
|
|
38217
|
+
* parse simply drops.
|
|
38218
|
+
*/
|
|
37358
38219
|
const recordHistory = (text) => {
|
|
37359
38220
|
if (text === "") return;
|
|
37360
38221
|
inputHistory = [...inputHistory, text].slice(-100);
|
|
37361
|
-
historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() =>
|
|
38222
|
+
historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() => appendFile(historyPath, historyLine(text), "utf8")).catch((writeError) => {
|
|
37362
38223
|
bridge.notify("history save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
37363
38224
|
});
|
|
37364
38225
|
};
|
|
@@ -37623,7 +38484,8 @@ async function run(ctx, startup, io) {
|
|
|
37623
38484
|
});
|
|
37624
38485
|
};
|
|
37625
38486
|
/** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
|
|
37626
|
-
const dispatch = (text, images = []) => {
|
|
38487
|
+
const dispatch = (text, images = [], origin) => {
|
|
38488
|
+
if (!submissionBelongsToSession(origin, session?.id)) return;
|
|
37627
38489
|
send(text, "followup", images);
|
|
37628
38490
|
};
|
|
37629
38491
|
/**
|
|
@@ -37631,7 +38493,8 @@ async function run(ctx, startup, io) {
|
|
|
37631
38493
|
* boundary (the inbox delivers between steps); an idle driver just starts
|
|
37632
38494
|
* a turn, so this doubles as the busy-state submit path.
|
|
37633
38495
|
*/
|
|
37634
|
-
const steer = (text, images = []) => {
|
|
38496
|
+
const steer = (text, images = [], origin) => {
|
|
38497
|
+
if (!submissionBelongsToSession(origin, session?.id)) return;
|
|
37635
38498
|
send(text, "steer", images);
|
|
37636
38499
|
};
|
|
37637
38500
|
/** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
|
|
@@ -37759,14 +38622,17 @@ async function run(ctx, startup, io) {
|
|
|
37759
38622
|
const loadSessions = async (options, signal) => {
|
|
37760
38623
|
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
37761
38624
|
const records = await sessionQuery.listSessions(signal);
|
|
38625
|
+
const root = jsonlSessionRoot(persistence);
|
|
37762
38626
|
const updated = /* @__PURE__ */ new Map();
|
|
37763
|
-
|
|
37764
|
-
const location = persistence?.locate(record.header);
|
|
37765
|
-
if (location === void 0) continue;
|
|
38627
|
+
if (root !== void 0) await Promise.all(records.map(async (record) => {
|
|
37766
38628
|
try {
|
|
37767
|
-
|
|
38629
|
+
const dir = sessionDirectoryFor(root, record.header.cwd, record.header.id);
|
|
38630
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
38631
|
+
const stats = await Promise.all(entries.filter((entry) => entry.isFile() && isSessionArtifactName(entry.name)).map((entry) => stat(join(dir, entry.name))));
|
|
38632
|
+
const newest = Math.max(...stats.map((info) => info.mtimeMs));
|
|
38633
|
+
if (Number.isFinite(newest)) updated.set(record.header.id, newest);
|
|
37768
38634
|
} catch {}
|
|
37769
|
-
}
|
|
38635
|
+
}));
|
|
37770
38636
|
const projected = projectSessionRows(records, options, updated);
|
|
37771
38637
|
const page = projected.slice(0, 32);
|
|
37772
38638
|
if (page.length === 0) return projected;
|
|
@@ -37782,11 +38648,11 @@ async function run(ctx, startup, io) {
|
|
|
37782
38648
|
* 1. `planSessionDeletion` collects the subtree and refuses when the root
|
|
37783
38649
|
* or ANY member is live (a live child would outlive its deleted
|
|
37784
38650
|
* parent), ordering the plan children-first.
|
|
37785
|
-
* 2. Every plan node must
|
|
37786
|
-
* (`encodeSegment(id)
|
|
37787
|
-
*
|
|
37788
|
-
* file has been touched yet, so a backend or layout
|
|
37789
|
-
* never strand a half-deleted subtree.
|
|
38651
|
+
* 2. Every plan node must derive to a guarded artifact directory
|
|
38652
|
+
* (`encodeSegment(id)` layout beneath the backend's config root).
|
|
38653
|
+
* Backends without a derivable artifact (non-JSONL) refuse the WHOLE
|
|
38654
|
+
* deletion here — no file has been touched yet, so a backend or layout
|
|
38655
|
+
* surprise can never strand a half-deleted subtree.
|
|
37790
38656
|
* 3. Artifacts are removed children-first: only an I/O error mid-delete
|
|
37791
38657
|
* can stop it short (reported with removed/total counts), leaving the
|
|
37792
38658
|
* shallowest lineage intact.
|
|
@@ -37800,22 +38666,23 @@ async function run(ctx, startup, io) {
|
|
|
37800
38666
|
const records = await sessionQuery.listSessions();
|
|
37801
38667
|
const plan = planSessionDeletion(records, id);
|
|
37802
38668
|
if (!plan.ok) return plan.reason;
|
|
38669
|
+
const root = jsonlSessionRoot(persistence);
|
|
38670
|
+
if (root === void 0) return "session backend exposes no deletable artifact (deletion is unsupported on this backend)";
|
|
37803
38671
|
const byId = new Map(records.map((record) => [record.header.id, record]));
|
|
37804
38672
|
const dirs = /* @__PURE__ */ new Map();
|
|
37805
38673
|
for (const node of plan.nodes) {
|
|
37806
38674
|
const record = byId.get(node.id);
|
|
37807
38675
|
if (record === void 0) return `no persisted session matches "${node.id}"`;
|
|
37808
|
-
const
|
|
37809
|
-
if (
|
|
37810
|
-
const dir = sessionArtifactDirectory(location.path, node.id);
|
|
37811
|
-
if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
|
|
38676
|
+
const dir = sessionArtifactDirectory(sessionDirectoryFor(root, record.header.cwd, node.id), node.id);
|
|
38677
|
+
if (dir === void 0) return `refusing to delete: unexpected artifact layout for ${node.id.slice(-12)}`;
|
|
37812
38678
|
dirs.set(node.id, dir);
|
|
37813
38679
|
}
|
|
37814
38680
|
let removed = 0;
|
|
37815
38681
|
for (const node of plan.nodes) {
|
|
37816
38682
|
const dir = dirs.get(node.id);
|
|
37817
38683
|
try {
|
|
37818
|
-
|
|
38684
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
38685
|
+
for (const entry of entries) if (entry.isFile() && isSessionArtifactName(entry.name)) await rm(join(dir, entry.name), { force: true });
|
|
37819
38686
|
await rm(dir, {
|
|
37820
38687
|
force: true,
|
|
37821
38688
|
recursive: false
|
|
@@ -37977,10 +38844,6 @@ async function run(ctx, startup, io) {
|
|
|
37977
38844
|
};
|
|
37978
38845
|
const reviewChanges = (argument) => {
|
|
37979
38846
|
const currentAgent = agent;
|
|
37980
|
-
if (currentAgent === void 0) {
|
|
37981
|
-
bridge.notify("no session yet - submit a message to start", "warning");
|
|
37982
|
-
return;
|
|
37983
|
-
}
|
|
37984
38847
|
const atEpoch = epoch;
|
|
37985
38848
|
const reviewCwd = session?.header.cwd ?? cwd;
|
|
37986
38849
|
const controller = new AbortController();
|
|
@@ -38014,7 +38877,7 @@ async function run(ctx, startup, io) {
|
|
|
38014
38877
|
const text = argument.trim();
|
|
38015
38878
|
const atSeq = text === "" ? void 0 : Number(text);
|
|
38016
38879
|
if (text !== "" && (!Number.isSafeInteger(atSeq) || (atSeq ?? -1) < 0)) throw new Error("usage: /fork [event-seq]");
|
|
38017
|
-
const seed = selectForkSeed(session.
|
|
38880
|
+
const seed = selectForkSeed(session.snapshotEvents(), atSeq);
|
|
38018
38881
|
const id = `session-${randomUUID()}`;
|
|
38019
38882
|
requestSwitch({
|
|
38020
38883
|
target: {
|
|
@@ -38057,6 +38920,7 @@ async function run(ctx, startup, io) {
|
|
|
38057
38920
|
const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
|
|
38058
38921
|
return (0, import_react.createElement)(App, {
|
|
38059
38922
|
key: session?.id ?? "pending",
|
|
38923
|
+
sessionKey: session?.id ?? "",
|
|
38060
38924
|
store,
|
|
38061
38925
|
approval,
|
|
38062
38926
|
questions,
|
|
@@ -38070,7 +38934,7 @@ async function run(ctx, startup, io) {
|
|
|
38070
38934
|
branch: gitBranch(sessionCwd),
|
|
38071
38935
|
sessionId: session === void 0 ? "" : session.id.slice(-8),
|
|
38072
38936
|
resumed: active?.resumed ?? false,
|
|
38073
|
-
mode: active?.mode ?? pendingMode ?? presets.defaultId,
|
|
38937
|
+
mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
|
|
38074
38938
|
permission,
|
|
38075
38939
|
dispatch,
|
|
38076
38940
|
steer,
|
|
@@ -38094,6 +38958,8 @@ async function run(ctx, startup, io) {
|
|
|
38094
38958
|
loadMentions: (query, signal) => mentions.candidates(query, signal),
|
|
38095
38959
|
inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
|
|
38096
38960
|
prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
|
|
38961
|
+
inspectFiles: (paths) => inspectFilePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
|
|
38962
|
+
prepareFiles: (paths, signal) => saveFilePaths(paths, ctx.get("attachments"), signal),
|
|
38097
38963
|
cyclePermission: cyclePermission$1,
|
|
38098
38964
|
setPermission: setPermissionAction,
|
|
38099
38965
|
selectModel,
|
|
@@ -38132,6 +38998,8 @@ async function run(ctx, startup, io) {
|
|
|
38132
38998
|
saveStatusline,
|
|
38133
38999
|
applyEditorKeys,
|
|
38134
39000
|
saveTheme,
|
|
39001
|
+
animations: animationsEnabled,
|
|
39002
|
+
saveAnimations,
|
|
38135
39003
|
history: inputHistory,
|
|
38136
39004
|
recordHistory,
|
|
38137
39005
|
cancelQueued,
|
|
@@ -38170,6 +39038,9 @@ async function run(ctx, startup, io) {
|
|
|
38170
39038
|
if (themeWarning !== void 0) setTimeout(() => {
|
|
38171
39039
|
bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
|
|
38172
39040
|
}, 50);
|
|
39041
|
+
if (animationsWarning !== void 0) setTimeout(() => {
|
|
39042
|
+
bridge.notify("animations config unreadable, animations stay on: " + animationsWarning, "warning");
|
|
39043
|
+
}, 50);
|
|
38173
39044
|
resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
|
|
38174
39045
|
if (hint === void 0) return;
|
|
38175
39046
|
setTimeout(() => {
|
|
@@ -38217,4 +39088,4 @@ function apply(ctx, config) {
|
|
|
38217
39088
|
});
|
|
38218
39089
|
}
|
|
38219
39090
|
//#endregion
|
|
38220
|
-
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };
|
|
39091
|
+
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence, submissionBelongsToSession };
|