dsh-code 1.0.5 → 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 +13 -12
- package/bin/deepseek.mjs +118 -3
- package/cordis.patch.yml +17 -7
- package/lib/index.mjs +1148 -347
- package/lib/types/app.d.ts +25 -6
- package/lib/types/attachments.d.ts +36 -4
- package/lib/types/index.d.ts +11 -2
- package/lib/types/provider-settings.d.ts +6 -11
- 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 +45 -4
- package/lib/types/session-directory.d.ts +48 -13
- package/lib/types/store.d.ts +3 -0
- package/package.json +168 -162
- package/src/app.ts +479 -198
- package/src/attachments.ts +110 -11
- package/src/commands.ts +35 -5
- package/src/index.ts +1868 -1779
- package/src/internals.ts +61 -40
- package/src/provider-settings.ts +12 -12
- package/src/render/animations.ts +606 -403
- package/src/render/export.ts +13 -3
- package/src/render/fuzzy.ts +83 -0
- package/src/render/projection.ts +1833 -1621
- package/src/session-directory.ts +94 -16
- 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 { appendFile, 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, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
|
|
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";
|
|
@@ -25083,6 +25083,41 @@ function appendStreamingTail(current, delta) {
|
|
|
25083
25083
|
const next = current + delta;
|
|
25084
25084
|
return next.length <= MAX_STREAMING_CHARS ? next : next.slice(-65536);
|
|
25085
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
|
+
}
|
|
25086
25121
|
/** Join the text blocks of a content list; non-text blocks contribute nothing. */
|
|
25087
25122
|
function textOf(content) {
|
|
25088
25123
|
return content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
@@ -25091,6 +25126,10 @@ function textOf(content) {
|
|
|
25091
25126
|
function imagesOf(content) {
|
|
25092
25127
|
return content.filter((block) => block.type === "image").map((block) => block.attachment);
|
|
25093
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
|
+
}
|
|
25094
25133
|
/** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
|
|
25095
25134
|
function imageLabels(images) {
|
|
25096
25135
|
if (images === void 0 || images.length === 0) return "";
|
|
@@ -25101,9 +25140,19 @@ function imageLabels(images) {
|
|
|
25101
25140
|
return `[image: ${name} · ${original === void 0 ? `${image.width}×${image.height}` : `${image.width}×${image.height} · original ${original.width}×${original.height}`} · ${image.bytes} B]`;
|
|
25102
25141
|
}).join("\n");
|
|
25103
25142
|
}
|
|
25104
|
-
/**
|
|
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. */
|
|
25105
25152
|
function promptDisplayText(entry) {
|
|
25106
|
-
const
|
|
25153
|
+
const imageText = imageLabels(entry.images);
|
|
25154
|
+
const fileText = fileLabels(entry.files);
|
|
25155
|
+
const labels = imageText === "" ? fileText : fileText === "" ? imageText : `${imageText}\n${fileText}`;
|
|
25107
25156
|
return entry.text === "" ? labels : labels === "" ? entry.text : `${entry.text}\n${labels}`;
|
|
25108
25157
|
}
|
|
25109
25158
|
/** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
|
|
@@ -25150,6 +25199,7 @@ function createReplayAccumulator() {
|
|
|
25150
25199
|
plan: false,
|
|
25151
25200
|
permission: "",
|
|
25152
25201
|
title: "",
|
|
25202
|
+
systemPrompt: "",
|
|
25153
25203
|
sandbox: "",
|
|
25154
25204
|
goal: void 0,
|
|
25155
25205
|
stats: {
|
|
@@ -25185,6 +25235,7 @@ function createReplayAccumulator() {
|
|
|
25185
25235
|
turnFiles: /* @__PURE__ */ new Map(),
|
|
25186
25236
|
turnSteps: /* @__PURE__ */ new Map(),
|
|
25187
25237
|
turnTools: /* @__PURE__ */ new Map(),
|
|
25238
|
+
systemNodes: /* @__PURE__ */ new Map(),
|
|
25188
25239
|
ops: 0
|
|
25189
25240
|
};
|
|
25190
25241
|
}
|
|
@@ -25267,6 +25318,17 @@ function retireReplayEntry(acc, index) {
|
|
|
25267
25318
|
* like the copy-on-write reducer returning its input view unchanged.
|
|
25268
25319
|
*/
|
|
25269
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
|
+
}
|
|
25270
25332
|
switch (event.type) {
|
|
25271
25333
|
case "user/message": {
|
|
25272
25334
|
const message = event.data;
|
|
@@ -25285,12 +25347,14 @@ function replayProjectEvent(acc, event) {
|
|
|
25285
25347
|
}
|
|
25286
25348
|
const text = textOf(message.content);
|
|
25287
25349
|
const images = imagesOf(message.content);
|
|
25350
|
+
const files = filesOf(message.content);
|
|
25288
25351
|
if (message.source.kind === "user") {
|
|
25289
25352
|
appendReplayEntry(acc, {
|
|
25290
25353
|
kind: "user",
|
|
25291
25354
|
text,
|
|
25292
25355
|
notice: false,
|
|
25293
|
-
...images.length === 0 ? {} : { images }
|
|
25356
|
+
...images.length === 0 ? {} : { images },
|
|
25357
|
+
...files.length === 0 ? {} : { files }
|
|
25294
25358
|
});
|
|
25295
25359
|
acc.stats = {
|
|
25296
25360
|
...acc.stats,
|
|
@@ -25335,45 +25399,68 @@ function replayProjectEvent(acc, event) {
|
|
|
25335
25399
|
ids.splice(start, 0, ...inserted.map((message) => message.id));
|
|
25336
25400
|
for (const message of inserted) {
|
|
25337
25401
|
const images = imagesOf(message.content);
|
|
25402
|
+
const files = filesOf(message.content);
|
|
25338
25403
|
appendReplayEntry(acc, {
|
|
25339
25404
|
kind: "pending",
|
|
25340
25405
|
messageId: message.id,
|
|
25341
25406
|
target,
|
|
25342
25407
|
text: pendingText(message.content),
|
|
25343
|
-
...images.length === 0 ? {} : { images }
|
|
25408
|
+
...images.length === 0 ? {} : { images },
|
|
25409
|
+
...files.length === 0 ? {} : { files }
|
|
25344
25410
|
});
|
|
25345
25411
|
indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
|
|
25346
25412
|
acc.ops += 1;
|
|
25347
25413
|
}
|
|
25348
25414
|
return true;
|
|
25349
25415
|
}
|
|
25350
|
-
case "
|
|
25351
|
-
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": {
|
|
25352
25431
|
const key = `${event.data.turn}:${event.data.step}`;
|
|
25353
|
-
|
|
25354
|
-
|
|
25355
|
-
const
|
|
25356
|
-
if (
|
|
25357
|
-
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
if (chunk.type === "reasoning-delta") {
|
|
25367
|
-
acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
|
|
25368
|
-
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
|
+
}
|
|
25369
25445
|
}
|
|
25370
|
-
|
|
25446
|
+
const streamed = acc.streaming !== "" || acc.streamingReasoning !== "";
|
|
25447
|
+
acc.streaming = "";
|
|
25448
|
+
acc.streamingReasoning = "";
|
|
25449
|
+
return changed || streamed;
|
|
25371
25450
|
}
|
|
25372
25451
|
case "assistant/message": {
|
|
25373
25452
|
const key = `${event.data.turn}:${event.data.step}`;
|
|
25374
25453
|
const started = acc.stepStart.get(key);
|
|
25375
25454
|
acc.stepStart.delete(key);
|
|
25376
|
-
|
|
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
|
+
}
|
|
25377
25464
|
acc.firstChunkAt.delete(key);
|
|
25378
25465
|
if (acc.turnSteps.get(event.data.turn) === key) acc.turnSteps.delete(event.data.turn);
|
|
25379
25466
|
const usage = event.data.usage;
|
|
@@ -25632,11 +25719,7 @@ function replayProjectEvent(acc, event) {
|
|
|
25632
25719
|
acc.model = `${config.provider}/${config.model}`;
|
|
25633
25720
|
acc.stats = {
|
|
25634
25721
|
...acc.stats,
|
|
25635
|
-
reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort)
|
|
25636
|
-
contextSegments: {
|
|
25637
|
-
...acc.stats.contextSegments,
|
|
25638
|
-
system: estimateTokens(event.data.header.system ?? "")
|
|
25639
|
-
}
|
|
25722
|
+
reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort)
|
|
25640
25723
|
};
|
|
25641
25724
|
return true;
|
|
25642
25725
|
}
|
|
@@ -25701,6 +25784,7 @@ function materializeReplayView(acc, copy) {
|
|
|
25701
25784
|
plan: acc.plan,
|
|
25702
25785
|
permission: acc.permission,
|
|
25703
25786
|
title: acc.title,
|
|
25787
|
+
systemPrompt: acc.systemPrompt,
|
|
25704
25788
|
sandbox: acc.sandbox,
|
|
25705
25789
|
goal: acc.goal,
|
|
25706
25790
|
pending: {
|
|
@@ -25716,11 +25800,58 @@ function materializeReplayView(acc, copy) {
|
|
|
25716
25800
|
lastPruneTokens: acc.lastPruneTokens,
|
|
25717
25801
|
turnFiles: new Map([...acc.turnFiles].map(([turn, files]) => [turn, new Set(files)])),
|
|
25718
25802
|
turnSteps: new Map(acc.turnSteps),
|
|
25719
|
-
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)
|
|
25720
25805
|
}
|
|
25721
25806
|
};
|
|
25722
25807
|
}
|
|
25723
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
|
+
/**
|
|
25724
25855
|
* The append-only flush boundary for a transcript view: the count of entries
|
|
25725
25856
|
* no later event can remove. Entries at or beyond this index are mutable and
|
|
25726
25857
|
* must stay in the live tree.
|
|
@@ -26365,8 +26496,60 @@ const DEEPSEEK_WAVE_STYLES = [
|
|
|
26365
26496
|
"aurora",
|
|
26366
26497
|
"pulse"
|
|
26367
26498
|
];
|
|
26368
|
-
/**
|
|
26369
|
-
|
|
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;
|
|
26370
26553
|
/** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
|
|
26371
26554
|
const SPARK_START_MS = 900;
|
|
26372
26555
|
const SPARK_FRAME_MS = 100;
|
|
@@ -26510,14 +26693,17 @@ function deepseekWaveStyleRandom(previous) {
|
|
|
26510
26693
|
return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
|
|
26511
26694
|
}
|
|
26512
26695
|
/**
|
|
26513
|
-
* Tier for a `provider/model` label: a
|
|
26696
|
+
* Tier for a `provider/model` label: a MODEL ID containing `flash` runs the
|
|
26514
26697
|
* single-band flash tier; everything else (pro/reasoner/chat) runs the
|
|
26515
|
-
* 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.
|
|
26516
26701
|
* @param model - the `provider/model` label of the applied model.
|
|
26517
26702
|
* @returns the wave tier for that model.
|
|
26518
26703
|
*/
|
|
26519
26704
|
function deepseekWaveTier(model) {
|
|
26520
|
-
|
|
26705
|
+
const slash = model.indexOf("/");
|
|
26706
|
+
return (slash < 0 ? model : model.slice(slash + 1)).toLowerCase().includes("flash") ? "flash" : "deepseek";
|
|
26521
26707
|
}
|
|
26522
26708
|
/**
|
|
26523
26709
|
* Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
|
|
@@ -26530,18 +26716,6 @@ function crest(distance) {
|
|
|
26530
26716
|
return .5 * (1 + Math.cos(Math.PI * distance));
|
|
26531
26717
|
}
|
|
26532
26718
|
/**
|
|
26533
|
-
* Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
|
|
26534
|
-
* the middle, so the crest accelerates and eases instead of sliding linearly.
|
|
26535
|
-
* @param progress - raw progress (clamped to 0..1).
|
|
26536
|
-
* @returns the eased progress in 0..1.
|
|
26537
|
-
*/
|
|
26538
|
-
function easeInOut(progress) {
|
|
26539
|
-
const p = Math.min(1, Math.max(0, progress));
|
|
26540
|
-
if (p < .5) return 4 * p * p * p;
|
|
26541
|
-
const inverse = -2 * p + 2;
|
|
26542
|
-
return 1 - inverse * inverse * inverse / 2;
|
|
26543
|
-
}
|
|
26544
|
-
/**
|
|
26545
26719
|
* Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
|
|
26546
26720
|
* at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
|
|
26547
26721
|
* total. The Wave style keeps the envelope at 1 (Codex paints Wave without
|
|
@@ -26559,38 +26733,98 @@ function envelope(elapsed, total, fadeIn, fadeOut) {
|
|
|
26559
26733
|
return Math.min(Math.max(Math.min(rise, fall), 0), 1);
|
|
26560
26734
|
}
|
|
26561
26735
|
/**
|
|
26562
|
-
* One band's
|
|
26563
|
-
*
|
|
26564
|
-
*
|
|
26565
|
-
*
|
|
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.
|
|
26566
26743
|
* @param style - the ignition style.
|
|
26567
26744
|
* @param band - the band triple (meaning depends on the style).
|
|
26568
26745
|
* @param elapsed - seconds since the animation started.
|
|
26569
26746
|
* @param column - column index in the content row (0..width-1).
|
|
26570
26747
|
* @param width - content-row width in columns.
|
|
26571
|
-
* @
|
|
26748
|
+
* @param context - band geometry (row position, undulation flag, pulse span).
|
|
26749
|
+
* @returns one or two `[hueIndex, strength, core]` contributions.
|
|
26572
26750
|
*/
|
|
26573
|
-
function bandSample(style, band, elapsed, column, width) {
|
|
26751
|
+
function bandSample(style, band, elapsed, column, width, context) {
|
|
26574
26752
|
const [first, second, third] = band;
|
|
26575
26753
|
switch (style) {
|
|
26576
26754
|
case "wave": {
|
|
26577
|
-
const
|
|
26578
|
-
|
|
26579
|
-
const
|
|
26580
|
-
|
|
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
|
+
]];
|
|
26581
26782
|
}
|
|
26582
26783
|
case "aurora": {
|
|
26583
26784
|
const center = (.5 + .38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width;
|
|
26584
26785
|
const halfWidth = Math.max(width * .22, 4);
|
|
26585
|
-
return [
|
|
26786
|
+
return [[
|
|
26787
|
+
Math.trunc(third),
|
|
26788
|
+
crest(Math.abs(column - center) / halfWidth),
|
|
26789
|
+
0
|
|
26790
|
+
]];
|
|
26586
26791
|
}
|
|
26587
26792
|
case "pulse": {
|
|
26588
|
-
const
|
|
26589
|
-
|
|
26590
|
-
const
|
|
26591
|
-
const
|
|
26592
|
-
|
|
26593
|
-
|
|
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
|
+
];
|
|
26594
26828
|
}
|
|
26595
26829
|
}
|
|
26596
26830
|
}
|
|
@@ -26603,9 +26837,10 @@ function blendRgb(fg, bg, alpha) {
|
|
|
26603
26837
|
];
|
|
26604
26838
|
}
|
|
26605
26839
|
/**
|
|
26606
|
-
*
|
|
26607
|
-
* and the bottom row last, sweeping down the band. 0.12
|
|
26608
|
-
* 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`).
|
|
26609
26844
|
*/
|
|
26610
26845
|
const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
26611
26846
|
/**
|
|
@@ -26616,9 +26851,13 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
|
26616
26851
|
* blends the mixed hue toward the blank-cell base at the style's alpha cap,
|
|
26617
26852
|
* and Aurora applies its own fade envelope. Returns `null` when the column
|
|
26618
26853
|
* should stay transparent, so the row returns to no `backgroundColor` on
|
|
26619
|
-
* both ends. With `rows > 1
|
|
26620
|
-
*
|
|
26621
|
-
*
|
|
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.
|
|
26622
26861
|
* @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
|
|
26623
26862
|
* @param column - column index in the content row (0..width-1).
|
|
26624
26863
|
* @param width - content-row width in columns.
|
|
@@ -26632,16 +26871,28 @@ const DEEPSEEK_WAVE_ROW_PHASE = .12;
|
|
|
26632
26871
|
*/
|
|
26633
26872
|
function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row = 0, rows = 1) {
|
|
26634
26873
|
const total = deepseekWaveBaseDuration(tier, style) / 1e3;
|
|
26635
|
-
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);
|
|
26636
26879
|
const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
|
|
26637
26880
|
const weights = [
|
|
26638
26881
|
0,
|
|
26639
26882
|
0,
|
|
26640
26883
|
0
|
|
26641
26884
|
];
|
|
26885
|
+
let bandIndex = 0;
|
|
26642
26886
|
for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
|
|
26643
|
-
const [hue, strength]
|
|
26644
|
-
|
|
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;
|
|
26645
26896
|
}
|
|
26646
26897
|
const weight = weights[0] + weights[1] + weights[2];
|
|
26647
26898
|
if (weight <= .01) return null;
|
|
@@ -26658,7 +26909,7 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row
|
|
|
26658
26909
|
Math.round(green / weight),
|
|
26659
26910
|
Math.round(blue / weight)
|
|
26660
26911
|
];
|
|
26661
|
-
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);
|
|
26662
26913
|
if (alpha < .02) return null;
|
|
26663
26914
|
return blendRgb(mixed, base, alpha);
|
|
26664
26915
|
}
|
|
@@ -26743,6 +26994,32 @@ function effortAboveHigh(effort) {
|
|
|
26743
26994
|
const rank = EFFORT_RANK[effort.trim().toLowerCase()];
|
|
26744
26995
|
return rank !== void 0 && rank > 3;
|
|
26745
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
|
+
}
|
|
26746
27023
|
//#endregion
|
|
26747
27024
|
//#region src/commands.ts
|
|
26748
27025
|
/**
|
|
@@ -26761,17 +27038,41 @@ function watchCommands(ctx) {
|
|
|
26761
27038
|
let error;
|
|
26762
27039
|
let loadedFor;
|
|
26763
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
|
+
};
|
|
26764
27059
|
const refresh = () => {
|
|
26765
27060
|
if (commands === void 0 || agent === void 0) return;
|
|
27061
|
+
let next;
|
|
27062
|
+
let nextError;
|
|
26766
27063
|
try {
|
|
26767
|
-
|
|
27064
|
+
next = commands.list(agent);
|
|
26768
27065
|
loadedFor = agent;
|
|
26769
|
-
error = void 0;
|
|
26770
27066
|
} catch (cause) {
|
|
26771
|
-
|
|
26772
|
-
|
|
27067
|
+
next = loadedFor === agent ? [...descriptors] : [];
|
|
27068
|
+
nextError = cause instanceof Error ? cause.message : String(cause);
|
|
26773
27069
|
}
|
|
26774
|
-
|
|
27070
|
+
if (!changed(next, nextError)) return;
|
|
27071
|
+
descriptors = next;
|
|
27072
|
+
error = nextError;
|
|
27073
|
+
lastFingerprint = descriptorFingerprint(next);
|
|
27074
|
+
lastNotifiedError = nextError;
|
|
27075
|
+
notify();
|
|
26775
27076
|
};
|
|
26776
27077
|
if (commands !== void 0) ctx.on("commands/change", () => refresh());
|
|
26777
27078
|
return {
|
|
@@ -26819,6 +27120,70 @@ function submissionPayload(line) {
|
|
|
26819
27120
|
return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines;
|
|
26820
27121
|
}
|
|
26821
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
|
|
26822
27187
|
//#region src/provider-settings.ts
|
|
26823
27188
|
/** Human text for a rejection value (mirrors the web page's `messageOf`). */
|
|
26824
27189
|
function messageOf$1(error) {
|
|
@@ -27028,7 +27393,8 @@ async function loadProviderSettings(ctx) {
|
|
|
27028
27393
|
active: active.has(entry.provider),
|
|
27029
27394
|
settingsNs: entry.settingsNs,
|
|
27030
27395
|
settingsPath: entry.settingsPath,
|
|
27031
|
-
...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) }
|
|
27032
27398
|
})), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
|
|
27033
27399
|
provider: provider.id,
|
|
27034
27400
|
displayName: provider.name,
|
|
@@ -27053,7 +27419,8 @@ async function loadProviderSettings(ctx) {
|
|
|
27053
27419
|
configuration: configurationOf(profile),
|
|
27054
27420
|
...credentialRef === void 0 ? {} : { credentialRef },
|
|
27055
27421
|
suggestedRef: deriveCredentialRef(base.provider),
|
|
27056
|
-
...base.declared === void 0 ? {} : { declared: base.declared }
|
|
27422
|
+
...base.declared === void 0 ? {} : { declared: base.declared },
|
|
27423
|
+
...base.error === void 0 ? {} : { diagnostic: base.error }
|
|
27057
27424
|
};
|
|
27058
27425
|
});
|
|
27059
27426
|
const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
|
|
@@ -27480,9 +27847,8 @@ function mergeSessionTitles(rows, observations) {
|
|
|
27480
27847
|
}
|
|
27481
27848
|
/**
|
|
27482
27849
|
* Encode a session id the way the JSONL backend does for its on-disk layout
|
|
27483
|
-
* (`encodeSegment`: safe units literal, everything else `~XXXX`). Used
|
|
27484
|
-
* validate
|
|
27485
|
-
* 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
|
|
27486
27852
|
* contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
|
|
27487
27853
|
*/
|
|
27488
27854
|
function encodeSessionSegment(raw) {
|
|
@@ -27498,21 +27864,91 @@ function encodeSessionSegment(raw) {
|
|
|
27498
27864
|
}
|
|
27499
27865
|
return out;
|
|
27500
27866
|
}
|
|
27501
|
-
/** The session-log artifact names the JSONL backend may create. */
|
|
27502
|
-
const SESSION_ARTIFACT_NAMES = ["session.jsonl", "session.jsonl.zstd"];
|
|
27503
27867
|
/**
|
|
27504
|
-
*
|
|
27505
|
-
*
|
|
27506
|
-
*
|
|
27507
|
-
*
|
|
27508
|
-
|
|
27509
|
-
|
|
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.
|
|
27510
27915
|
*/
|
|
27511
|
-
function
|
|
27512
|
-
|
|
27513
|
-
|
|
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.
|
|
27934
|
+
*/
|
|
27935
|
+
function sessionArtifactDirectory(dir, id) {
|
|
27514
27936
|
if (basename(dir) !== encodeSessionSegment(id)) return void 0;
|
|
27515
|
-
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;
|
|
27516
27952
|
}
|
|
27517
27953
|
/**
|
|
27518
27954
|
* Collect one session's deletion subtree: the id plus every record whose
|
|
@@ -30586,7 +31022,14 @@ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
|
|
|
30586
31022
|
}
|
|
30587
31023
|
//#endregion
|
|
30588
31024
|
//#region src/attachments.ts
|
|
30589
|
-
/** 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;
|
|
30590
31033
|
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
30591
31034
|
".png",
|
|
30592
31035
|
".jpg",
|
|
@@ -30608,24 +31051,58 @@ function detectImageMediaType(data) {
|
|
|
30608
31051
|
function looksLikeImagePath(path) {
|
|
30609
31052
|
return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
30610
31053
|
}
|
|
30611
|
-
/**
|
|
30612
|
-
|
|
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) {
|
|
30613
31067
|
const text = input.trim();
|
|
30614
|
-
if (text === "") return
|
|
30615
|
-
|
|
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);
|
|
30616
31075
|
for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
|
|
30617
31076
|
const token = match[1] ?? match[2] ?? match[3];
|
|
30618
31077
|
if (token === void 0) continue;
|
|
30619
31078
|
let path = token;
|
|
30620
|
-
if (path.startsWith("file://"))
|
|
30621
|
-
|
|
30622
|
-
|
|
30623
|
-
|
|
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;
|
|
30624
31091
|
}
|
|
30625
|
-
if (
|
|
30626
|
-
|
|
31092
|
+
if (looksLikeImagePath(path)) {
|
|
31093
|
+
images.push(path);
|
|
31094
|
+
continue;
|
|
31095
|
+
}
|
|
31096
|
+
if (!looksLikeDroppedFile(path)) return {
|
|
31097
|
+
images: [],
|
|
31098
|
+
files: []
|
|
31099
|
+
};
|
|
31100
|
+
files.push(path);
|
|
30627
31101
|
}
|
|
30628
|
-
return
|
|
31102
|
+
return {
|
|
31103
|
+
images,
|
|
31104
|
+
files
|
|
31105
|
+
};
|
|
30629
31106
|
}
|
|
30630
31107
|
/** Validate path, byte size and encoded signature without writing an attachment object. */
|
|
30631
31108
|
async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
|
|
@@ -30696,6 +31173,60 @@ async function saveImagePaths(paths, attachments, signal) {
|
|
|
30696
31173
|
attachment
|
|
30697
31174
|
}));
|
|
30698
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
|
+
}
|
|
30699
31230
|
//#endregion
|
|
30700
31231
|
//#region src/app.ts
|
|
30701
31232
|
/**
|
|
@@ -30785,6 +31316,10 @@ const LOCAL_COMMANDS = [
|
|
|
30785
31316
|
label: "/theme",
|
|
30786
31317
|
description: "switch the color theme"
|
|
30787
31318
|
},
|
|
31319
|
+
{
|
|
31320
|
+
label: "/animation",
|
|
31321
|
+
description: "toggle timed animations (/animation [on|off])"
|
|
31322
|
+
},
|
|
30788
31323
|
{
|
|
30789
31324
|
label: "/history",
|
|
30790
31325
|
description: "search and recall past prompts"
|
|
@@ -30844,12 +31379,21 @@ function padColumns(text, width) {
|
|
|
30844
31379
|
const clipped = truncateColumns(singleLineText(text), width);
|
|
30845
31380
|
return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
|
|
30846
31381
|
}
|
|
30847
|
-
/**
|
|
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
|
+
*/
|
|
30848
31388
|
function useFrames(intervalMs, active = true) {
|
|
30849
31389
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
30850
31390
|
(0, import_react.useEffect)(() => {
|
|
30851
31391
|
if (!active) return;
|
|
30852
|
-
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);
|
|
30853
31397
|
return () => {
|
|
30854
31398
|
clearInterval(id);
|
|
30855
31399
|
};
|
|
@@ -30870,14 +31414,17 @@ function useStableInput(handler, active) {
|
|
|
30870
31414
|
}, []);
|
|
30871
31415
|
useInput(stableHandler, { isActive: active });
|
|
30872
31416
|
}
|
|
30873
|
-
/**
|
|
30874
|
-
|
|
30875
|
-
|
|
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);
|
|
30876
31423
|
return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
|
|
30877
31424
|
}
|
|
30878
|
-
/** Blinking block caret appended to streaming text. */
|
|
30879
|
-
function Caret() {
|
|
30880
|
-
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);
|
|
30881
31428
|
return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
|
|
30882
31429
|
}
|
|
30883
31430
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
@@ -30904,17 +31451,19 @@ function useCursorBlink(active) {
|
|
|
30904
31451
|
* One bounded line painted with the deep-diving shimmer: a continuously
|
|
30905
31452
|
* moving blue gradient across graphemes, the `✻` glyph in the breathing
|
|
30906
31453
|
* spark color. Shared by the busy line and the collapsed thinking marker;
|
|
30907
|
-
* 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.
|
|
30908
31457
|
*/
|
|
30909
|
-
function ShimmerLine({ text }) {
|
|
30910
|
-
const tick = useFrames(33);
|
|
31458
|
+
function ShimmerLine({ text, animated = true }) {
|
|
31459
|
+
const tick = useFrames(33, animated);
|
|
30911
31460
|
const palette = getPalette();
|
|
30912
31461
|
const graphemes = splitGraphemes(text);
|
|
30913
31462
|
return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
|
|
30914
31463
|
const sparkle = grapheme.text === "✻";
|
|
30915
31464
|
return (0, import_react.createElement)(Text, {
|
|
30916
31465
|
key: `${grapheme.start}-${grapheme.end}`,
|
|
30917
|
-
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)),
|
|
30918
31467
|
bold: sparkle || void 0
|
|
30919
31468
|
}, grapheme.text);
|
|
30920
31469
|
}));
|
|
@@ -30925,10 +31474,13 @@ function ShimmerLine({ text }) {
|
|
|
30925
31474
|
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
30926
31475
|
* so a resumed mid-turn keeps the real time.
|
|
30927
31476
|
*/
|
|
30928
|
-
function DeepDivingLine({ since }) {
|
|
31477
|
+
function DeepDivingLine({ since, animated = true }) {
|
|
30929
31478
|
const elapsed = since === 0 ? 0 : Date.now() - since;
|
|
30930
31479
|
const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
|
|
30931
|
-
return (0, import_react.createElement)(ShimmerLine, {
|
|
31480
|
+
return (0, import_react.createElement)(ShimmerLine, {
|
|
31481
|
+
text,
|
|
31482
|
+
animated
|
|
31483
|
+
});
|
|
30932
31484
|
}
|
|
30933
31485
|
/**
|
|
30934
31486
|
* The streaming buffer rendered with a hard size cap: the live region must
|
|
@@ -32213,7 +32765,8 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
32213
32765
|
const identity = row.displayName === row.provider ? row.provider : row.displayName + " (" + row.provider + ")";
|
|
32214
32766
|
const authorization = authorizationForProvider(authorizations, row.provider);
|
|
32215
32767
|
const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? " · " + providerAuthorizationStatus(authorization) : "";
|
|
32216
|
-
const
|
|
32768
|
+
const diagnostic = row.diagnostic === void 0 ? "" : " · ! " + singleLineText(row.diagnostic);
|
|
32769
|
+
const label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "") + diagnostic;
|
|
32217
32770
|
const idleColor = row.configured ? inkColor(getPalette().brandMid) : inkColor(getPalette().dim);
|
|
32218
32771
|
itemRows.push((0, import_react.createElement)(Text, {
|
|
32219
32772
|
key: row.provider,
|
|
@@ -32581,7 +33134,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
32581
33134
|
color: zone === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
|
|
32582
33135
|
wrap: "truncate-end"
|
|
32583
33136
|
}, truncateColumns(" " + (zone === "url" ? ">" : " ") + " url " + (baseURL === "" ? "(official default)" : baseURL) + (zone === "url" ? "▏" : ""), viewport.contentColumns));
|
|
32584
|
-
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);
|
|
32585
33138
|
const first = selectionWindow(cursor, models.length + 1, rowBudget);
|
|
32586
33139
|
const modelRows = [];
|
|
32587
33140
|
for (let index = first; index < first + Math.max(0, Math.min(models.length + 1 - first, rowBudget)); index += 1) {
|
|
@@ -32614,7 +33167,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
32614
33167
|
color: inkColor(getPalette().brand),
|
|
32615
33168
|
bold: true,
|
|
32616
33169
|
wrap: "truncate-end"
|
|
32617
|
-
}, 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, {
|
|
32618
33175
|
color: inkColor(getPalette().dim),
|
|
32619
33176
|
wrap: "truncate-end"
|
|
32620
33177
|
}, truncateColumns("↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back", viewport.contentColumns)));
|
|
@@ -32954,6 +33511,167 @@ function waveRowSpans(cells) {
|
|
|
32954
33511
|
}
|
|
32955
33512
|
return spans;
|
|
32956
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
|
+
}
|
|
32957
33675
|
/**
|
|
32958
33676
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
32959
33677
|
* with independent history selection and content scrolling. The complete
|
|
@@ -33113,8 +33831,10 @@ function completionCandidates(value, descriptors, skills) {
|
|
|
33113
33831
|
seen.add(name);
|
|
33114
33832
|
all.push(candidate);
|
|
33115
33833
|
}
|
|
33116
|
-
|
|
33117
|
-
|
|
33834
|
+
return rankByName(all.map((candidate) => ({
|
|
33835
|
+
name: candidate.label.slice(1),
|
|
33836
|
+
candidate
|
|
33837
|
+
})), prefix).map((entry) => entry.candidate);
|
|
33118
33838
|
}
|
|
33119
33839
|
/**
|
|
33120
33840
|
* Shared completion-menu geometry: the menu view and the App's dynamic-row
|
|
@@ -33196,7 +33916,7 @@ function CompletionMenu({ active, mention, index, rows, error }) {
|
|
|
33196
33916
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
33197
33917
|
* box passes every key through untouched.
|
|
33198
33918
|
*/
|
|
33199
|
-
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 }) {
|
|
33200
33920
|
const columns = useStdout().stdout?.columns ?? 80;
|
|
33201
33921
|
const inputTerminalRows = useStdout().stdout?.rows ?? 30;
|
|
33202
33922
|
const editorColumns = Math.max(1, columns - 6);
|
|
@@ -33211,10 +33931,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33211
33931
|
const [draftImages, setDraftImages] = (0, import_react.useState)([]);
|
|
33212
33932
|
const draftImagesRef = (0, import_react.useRef)(draftImages);
|
|
33213
33933
|
draftImagesRef.current = draftImages;
|
|
33934
|
+
const [draftFiles, setDraftFiles] = (0, import_react.useState)([]);
|
|
33935
|
+
const draftFilesRef = (0, import_react.useRef)(draftFiles);
|
|
33936
|
+
draftFilesRef.current = draftFiles;
|
|
33214
33937
|
const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
|
|
33215
33938
|
const prepareAbortRef = (0, import_react.useRef)(void 0);
|
|
33216
33939
|
const prepareEpochRef = (0, import_react.useRef)(0);
|
|
33217
|
-
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
|
|
33940
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages && animations);
|
|
33218
33941
|
(0, import_react.useEffect)(() => () => {
|
|
33219
33942
|
prepareEpochRef.current += 1;
|
|
33220
33943
|
prepareAbortRef.current?.abort();
|
|
@@ -33238,6 +33961,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33238
33961
|
const safe = sanitizeDraftText(historyFill.text);
|
|
33239
33962
|
draftImagesRef.current = [];
|
|
33240
33963
|
setDraftImages([]);
|
|
33964
|
+
draftFilesRef.current = [];
|
|
33965
|
+
setDraftFiles([]);
|
|
33241
33966
|
valueRef.current = safe;
|
|
33242
33967
|
cursorRef.current = safe.length;
|
|
33243
33968
|
setValue(safe);
|
|
@@ -33264,6 +33989,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33264
33989
|
draftImagesRef.current = next;
|
|
33265
33990
|
return next.length === current.length ? current : next;
|
|
33266
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
|
+
});
|
|
33267
33997
|
}, [value]);
|
|
33268
33998
|
(0, import_react.useEffect)(() => {
|
|
33269
33999
|
if (stdin === void 0) return;
|
|
@@ -33307,12 +34037,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33307
34037
|
const [mentionError, setMentionError] = (0, import_react.useState)(void 0);
|
|
33308
34038
|
const mentionRequestRef = (0, import_react.useRef)(0);
|
|
33309
34039
|
const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
33310
|
-
const uniqueImageMarker = (name, source, reserved = []) => {
|
|
34040
|
+
const uniqueImageMarker = (name, source, reserved = [], kind = "image") => {
|
|
33311
34041
|
const safeName = singleLineText(sanitizeDraftText(name));
|
|
33312
|
-
|
|
34042
|
+
const label = kind === "file" ? "file" : "image";
|
|
34043
|
+
let marker = source === "mention" ? `@${safeName}` : `[${label}: ${safeName}]`;
|
|
33313
34044
|
let suffix = 2;
|
|
33314
|
-
|
|
33315
|
-
|
|
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}]`;
|
|
33316
34048
|
suffix += 1;
|
|
33317
34049
|
}
|
|
33318
34050
|
return marker;
|
|
@@ -33330,24 +34062,41 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33330
34062
|
setDraftImages(next);
|
|
33331
34063
|
return true;
|
|
33332
34064
|
};
|
|
33333
|
-
|
|
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) => {
|
|
33334
34071
|
const originalValue = valueRef.current;
|
|
33335
34072
|
const originalCursor = cursorRef.current;
|
|
33336
|
-
|
|
33337
|
-
|
|
33338
|
-
|
|
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 = [];
|
|
33339
34079
|
const markers = [];
|
|
33340
|
-
for (const inspection of
|
|
33341
|
-
if ([...draftImagesRef.current, ...
|
|
34080
|
+
for (const inspection of inspectedImages) {
|
|
34081
|
+
if ([...draftImagesRef.current, ...imageAdditions].some((image) => sameImagePath(image.path, inspection.path))) continue;
|
|
33342
34082
|
const marker = uniqueImageMarker(inspection.name, "drop", markers);
|
|
33343
|
-
|
|
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({
|
|
33344
34093
|
...inspection,
|
|
33345
34094
|
marker
|
|
33346
34095
|
});
|
|
33347
34096
|
markers.push(marker);
|
|
33348
34097
|
}
|
|
33349
|
-
if (
|
|
33350
|
-
notify("those
|
|
34098
|
+
if (imageAdditions.length === 0 && fileAdditions.length === 0) {
|
|
34099
|
+
notify("those attachments are already attached", "warning");
|
|
33351
34100
|
return;
|
|
33352
34101
|
}
|
|
33353
34102
|
const current = valueRef.current;
|
|
@@ -33356,7 +34105,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33356
34105
|
end: originalCursor
|
|
33357
34106
|
});
|
|
33358
34107
|
if (anchor === void 0) {
|
|
33359
|
-
notify("draft changed at the
|
|
34108
|
+
notify("draft changed at the attachment drop point; drop the files again", "warning");
|
|
33360
34109
|
return;
|
|
33361
34110
|
}
|
|
33362
34111
|
const at = anchor.start;
|
|
@@ -33368,12 +34117,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33368
34117
|
setValue(edit.value);
|
|
33369
34118
|
setCursor(nextCursor);
|
|
33370
34119
|
resetCursorBlink();
|
|
33371
|
-
const nextImages = [...draftImagesRef.current, ...
|
|
34120
|
+
const nextImages = [...draftImagesRef.current, ...imageAdditions];
|
|
33372
34121
|
draftImagesRef.current = nextImages;
|
|
33373
34122
|
setDraftImages(nextImages);
|
|
33374
|
-
|
|
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`);
|
|
33375
34128
|
}, (reason) => {
|
|
33376
|
-
notify(`
|
|
34129
|
+
notify(`attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
33377
34130
|
});
|
|
33378
34131
|
};
|
|
33379
34132
|
(0, import_react.useEffect)(() => {
|
|
@@ -33408,7 +34161,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33408
34161
|
]);
|
|
33409
34162
|
const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
|
|
33410
34163
|
const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
|
|
33411
|
-
|
|
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) => ({
|
|
33412
34174
|
label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
|
|
33413
34175
|
description: row.description,
|
|
33414
34176
|
origin: "mention"
|
|
@@ -33417,8 +34179,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33417
34179
|
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
33418
34180
|
const acceptMenuCandidate = () => {
|
|
33419
34181
|
if (mentionActive && mentionToken !== void 0) {
|
|
33420
|
-
if (
|
|
33421
|
-
const row =
|
|
34182
|
+
if (rankedMentionRows.length === 0) return;
|
|
34183
|
+
const row = rankedMentionRows[completionIndex % rankedMentionRows.length];
|
|
33422
34184
|
if (row !== void 0) {
|
|
33423
34185
|
if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
|
|
33424
34186
|
const tokenText = value.slice(mentionToken.start, cursor);
|
|
@@ -33621,6 +34383,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33621
34383
|
resetCursorBlink();
|
|
33622
34384
|
draftImagesRef.current = [];
|
|
33623
34385
|
setDraftImages([]);
|
|
34386
|
+
draftFilesRef.current = [];
|
|
34387
|
+
setDraftFiles([]);
|
|
33624
34388
|
setCompletionIndex(0);
|
|
33625
34389
|
setDismissedMenuValue(void 0);
|
|
33626
34390
|
} else quit();
|
|
@@ -33666,15 +34430,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33666
34430
|
}
|
|
33667
34431
|
const trimmed = liveValue.trim();
|
|
33668
34432
|
const text = submissionPayload(liveValue);
|
|
33669
|
-
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;
|
|
33670
34436
|
const controller = new AbortController();
|
|
33671
34437
|
const epoch = prepareEpochRef.current + 1;
|
|
33672
34438
|
prepareEpochRef.current = epoch;
|
|
33673
34439
|
prepareAbortRef.current = controller;
|
|
33674
34440
|
setPreparingImages(true);
|
|
33675
|
-
|
|
33676
|
-
const
|
|
33677
|
-
|
|
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]) => {
|
|
33678
34446
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
33679
34447
|
prepareAbortRef.current = void 0;
|
|
33680
34448
|
setPreparingImages(false);
|
|
@@ -33684,6 +34452,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33684
34452
|
setCursor(0);
|
|
33685
34453
|
draftImagesRef.current = [];
|
|
33686
34454
|
setDraftImages([]);
|
|
34455
|
+
draftFilesRef.current = [];
|
|
34456
|
+
setDraftFiles([]);
|
|
33687
34457
|
setCompletionIndex(0);
|
|
33688
34458
|
setDismissedMenuValue(void 0);
|
|
33689
34459
|
dismissNotice();
|
|
@@ -33692,13 +34462,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33692
34462
|
recordHistory(text);
|
|
33693
34463
|
}
|
|
33694
34464
|
recall.current = beginRecall(recallSpace, "");
|
|
33695
|
-
|
|
33696
|
-
|
|
34465
|
+
const blocks = [...images, ...files];
|
|
34466
|
+
if (busy) steer(text, blocks, originSession);
|
|
34467
|
+
else dispatch(text, blocks, originSession);
|
|
33697
34468
|
}, (reason) => {
|
|
33698
34469
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
|
|
33699
34470
|
prepareAbortRef.current = void 0;
|
|
33700
34471
|
setPreparingImages(false);
|
|
33701
|
-
notify(`
|
|
34472
|
+
notify(`attachment submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
|
|
33702
34473
|
});
|
|
33703
34474
|
return;
|
|
33704
34475
|
}
|
|
@@ -33805,6 +34576,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33805
34576
|
openTheme();
|
|
33806
34577
|
return;
|
|
33807
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
|
+
}
|
|
33808
34586
|
if (text === "/history") {
|
|
33809
34587
|
openHistory();
|
|
33810
34588
|
return;
|
|
@@ -33946,48 +34724,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
33946
34724
|
text = text.replaceAll(PASTE_END_MARKER, "");
|
|
33947
34725
|
}
|
|
33948
34726
|
if (text === "") return;
|
|
33949
|
-
|
|
33950
|
-
|
|
33951
|
-
|
|
33952
|
-
|
|
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
|
+
}
|
|
33953
34733
|
}
|
|
33954
34734
|
applyEdit(insertText(valueRef.current, cursorRef.current, text));
|
|
33955
34735
|
}
|
|
33956
34736
|
}, active);
|
|
33957
|
-
const
|
|
33958
|
-
const
|
|
33959
|
-
tier: null,
|
|
33960
|
-
style: null
|
|
33961
|
-
});
|
|
34737
|
+
const waveKey = waveTier !== null && waveStyle !== null ? `${waveTier}:${waveStyle}` : null;
|
|
34738
|
+
const [wavePlayedKey, setWavePlayedKey] = (0, import_react.useState)(null);
|
|
33962
34739
|
(0, import_react.useEffect)(() => {
|
|
33963
|
-
|
|
33964
|
-
wavePrevious.current = {
|
|
33965
|
-
tier: waveTier,
|
|
33966
|
-
style: waveStyle
|
|
33967
|
-
};
|
|
33968
|
-
if (waveTier === null) {
|
|
33969
|
-
setWaveTick(null);
|
|
33970
|
-
return;
|
|
33971
|
-
}
|
|
33972
|
-
if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
|
|
33973
|
-
}, [waveTier, waveStyle]);
|
|
33974
|
-
const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
|
|
33975
|
-
(0, import_react.useEffect)(() => {
|
|
33976
|
-
if (!waveActive) return;
|
|
33977
|
-
const id = setInterval(() => {
|
|
33978
|
-
setWaveTick((current) => current === null ? 0 : current + 1);
|
|
33979
|
-
}, 33);
|
|
33980
|
-
return () => {
|
|
33981
|
-
clearInterval(id);
|
|
33982
|
-
};
|
|
33983
|
-
}, [waveActive]);
|
|
33984
|
-
(0, import_react.useEffect)(() => {
|
|
33985
|
-
if (waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null);
|
|
34740
|
+
if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey);
|
|
33986
34741
|
}, [
|
|
33987
|
-
|
|
33988
|
-
|
|
33989
|
-
|
|
34742
|
+
animations,
|
|
34743
|
+
waveKey,
|
|
34744
|
+
wavePlayedKey
|
|
33990
34745
|
]);
|
|
34746
|
+
const waveArmed = waveKey !== null && waveKey !== wavePlayedKey;
|
|
33991
34747
|
const tierActive = waveTier !== null;
|
|
33992
34748
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
|
|
33993
34749
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
|
|
@@ -34059,7 +34815,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34059
34815
|
}, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
|
|
34060
34816
|
color: inkColor(getPalette().warn),
|
|
34061
34817
|
bold: true
|
|
34062
|
-
}, "… ") : 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, {
|
|
34063
34819
|
color: promptColor,
|
|
34064
34820
|
bold: tierActive ? true : void 0
|
|
34065
34821
|
}, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
|
|
@@ -34068,100 +34824,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
34068
34824
|
}, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
|
|
34069
34825
|
}
|
|
34070
34826
|
const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
|
|
34071
|
-
|
|
34072
|
-
|
|
34073
|
-
|
|
34074
|
-
|
|
34075
|
-
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
|
|
34084
|
-
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
let column = 0;
|
|
34092
|
-
for (let index = 0; index < cells.length; index += 1) {
|
|
34093
|
-
if (column === target) return index;
|
|
34094
|
-
column += cells[index].width ?? visibleColumns(cells[index].char);
|
|
34095
|
-
if (column > target) return void 0;
|
|
34096
|
-
}
|
|
34097
|
-
};
|
|
34098
|
-
const editorWaveRows = visibleRows.map((row, visibleIndex) => {
|
|
34099
|
-
const sourceIndex = editorWindowStart + visibleIndex;
|
|
34100
|
-
const bandRow = visibleIndex + 1;
|
|
34101
|
-
const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
|
|
34102
|
-
const placeholder = sourceIndex === 0 && value === "" && !busy;
|
|
34103
|
-
const cells = [];
|
|
34104
|
-
let usedColumns = 0;
|
|
34105
|
-
const push = (char, extra = {}) => {
|
|
34106
|
-
const width = visibleColumns(char);
|
|
34107
|
-
cells.push({
|
|
34108
|
-
char,
|
|
34109
|
-
width,
|
|
34110
|
-
backgroundColor: waveBg(bandRow, usedColumns),
|
|
34111
|
-
...extra
|
|
34112
|
-
});
|
|
34113
|
-
usedColumns += width;
|
|
34114
|
-
};
|
|
34115
|
-
if (sourceIndex === 0) {
|
|
34116
|
-
push(promptGlyph, {
|
|
34117
|
-
color: promptColor,
|
|
34118
|
-
bold: true
|
|
34119
|
-
});
|
|
34120
|
-
push(" ", { color: promptColor });
|
|
34121
|
-
} else {
|
|
34122
|
-
push(" ");
|
|
34123
|
-
push(" ");
|
|
34124
|
-
}
|
|
34125
|
-
for (const span of splitGraphemes(parts.before)) push(span.text);
|
|
34126
|
-
if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
|
|
34127
|
-
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
|
|
34128
|
-
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
|
|
34129
|
-
while (usedColumns < bandWidth) push(" ");
|
|
34130
|
-
const middleBandRow = Math.floor(totalBandRows / 2);
|
|
34131
|
-
if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
|
|
34132
|
-
const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
|
|
34133
|
-
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
|
|
34134
|
-
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
|
|
34135
|
-
if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
|
|
34136
|
-
const cell = cells[indices[at]];
|
|
34137
|
-
cell.char = word[at];
|
|
34138
|
-
cell.width = 1;
|
|
34139
|
-
cell.color = inkColor(deepseekWaveWordHue(at, hues));
|
|
34140
|
-
cell.bold = true;
|
|
34141
|
-
cell.dim = false;
|
|
34142
|
-
}
|
|
34143
|
-
}
|
|
34144
|
-
if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
|
|
34145
|
-
const spark = deepseekWaveSpark(waveTick);
|
|
34146
|
-
const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
|
|
34147
|
-
if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
|
|
34148
|
-
cells[lastIndex].char = spark;
|
|
34149
|
-
cells[lastIndex].color = promptColor;
|
|
34150
|
-
cells[lastIndex].bold = true;
|
|
34151
|
-
cells[lastIndex].dim = false;
|
|
34152
|
-
}
|
|
34153
|
-
}
|
|
34154
|
-
return (0, import_react.createElement)(Text, {
|
|
34155
|
-
key: `editor-${sourceIndex}`,
|
|
34156
|
-
wrap: "truncate-end"
|
|
34157
|
-
}, ...waveRowSpans(cells));
|
|
34158
|
-
});
|
|
34159
|
-
return (0, import_react.createElement)(Box, {
|
|
34160
|
-
flexDirection: "column",
|
|
34161
|
-
width: bandWidth
|
|
34162
|
-
}, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
|
|
34163
|
-
};
|
|
34164
|
-
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
|
+
}));
|
|
34165
34847
|
}
|
|
34166
34848
|
/** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
|
|
34167
34849
|
function buildSettledRow(entry, index, showReasoning, columns) {
|
|
@@ -34339,12 +35021,19 @@ function App(props) {
|
|
|
34339
35021
|
* to static while the prompt marker keeps the tier accent. The trigger
|
|
34340
35022
|
* follows the applied model label (what the status bar actually shows),
|
|
34341
35023
|
* never the initial paint, and the tier is derived from the label and
|
|
34342
|
-
* cached at the switch. The 33ms tick itself lives inside
|
|
34343
|
-
* sweep re-renders only the composer band, not the whole tree,
|
|
34344
|
-
* App owns the rarely-changing tier/style and
|
|
34345
|
-
*
|
|
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. */
|
|
34346
35029
|
const [waveTier, setWaveTier] = (0, import_react.useState)(null);
|
|
34347
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
|
+
};
|
|
34348
35037
|
const previousModel = (0, import_react.useRef)(void 0);
|
|
34349
35038
|
const previousEffort = (0, import_react.useRef)(props.effort);
|
|
34350
35039
|
const previousStyle = (0, import_react.useRef)(void 0);
|
|
@@ -34904,7 +35593,10 @@ function App(props) {
|
|
|
34904
35593
|
continuationPrefix: " ",
|
|
34905
35594
|
dim: true,
|
|
34906
35595
|
maxRows: auditedReasoningRows
|
|
34907
|
-
}) : 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, {
|
|
34908
35600
|
text: "Thinking… (Ctrl/Alt+R to expand)",
|
|
34909
35601
|
prefix: "✻ ",
|
|
34910
35602
|
continuationPrefix: " ",
|
|
@@ -34915,7 +35607,10 @@ function App(props) {
|
|
|
34915
35607
|
dim: false,
|
|
34916
35608
|
maxRows: auditedAnswerRows,
|
|
34917
35609
|
prefix: " "
|
|
34918
|
-
}, 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, {
|
|
34919
35614
|
rows: agentRows,
|
|
34920
35615
|
total: props.subagents.getTotalSeen()
|
|
34921
35616
|
}) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
|
|
@@ -35153,6 +35848,9 @@ function App(props) {
|
|
|
35153
35848
|
loadMentions: props.loadMentions,
|
|
35154
35849
|
inspectImages: props.inspectImages,
|
|
35155
35850
|
prepareImages: props.prepareImages,
|
|
35851
|
+
inspectFiles: props.inspectFiles,
|
|
35852
|
+
prepareFiles: props.prepareFiles,
|
|
35853
|
+
sessionKey: props.sessionKey,
|
|
35156
35854
|
cyclePermission: props.cyclePermission,
|
|
35157
35855
|
exportTranscript: props.exportTranscript,
|
|
35158
35856
|
renameTitle: props.renameTitle,
|
|
@@ -35164,6 +35862,8 @@ function App(props) {
|
|
|
35164
35862
|
cancelQueued: props.cancelQueued,
|
|
35165
35863
|
historyFill,
|
|
35166
35864
|
historyConsumed,
|
|
35865
|
+
animations,
|
|
35866
|
+
applyAnimations,
|
|
35167
35867
|
waveTier,
|
|
35168
35868
|
waveStyle,
|
|
35169
35869
|
maxRows: composerEditorCap,
|
|
@@ -35441,26 +36141,36 @@ const internals = {
|
|
|
35441
36141
|
mount: (element) => {
|
|
35442
36142
|
const keyboardEnhanced = shouldEnableKeyboardEnhancement();
|
|
35443
36143
|
const focusReporting = isVsCodeTerminalEnv();
|
|
35444
|
-
process.
|
|
35445
|
-
|
|
35446
|
-
|
|
35447
|
-
|
|
35448
|
-
|
|
35449
|
-
|
|
35450
|
-
|
|
35451
|
-
|
|
35452
|
-
|
|
35453
|
-
|
|
35454
|
-
|
|
35455
|
-
|
|
35456
|
-
|
|
35457
|
-
|
|
35458
|
-
|
|
35459
|
-
|
|
35460
|
-
|
|
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
|
+
}
|
|
35461
36168
|
}
|
|
35462
|
-
}
|
|
35463
|
-
}
|
|
36169
|
+
};
|
|
36170
|
+
} catch (error) {
|
|
36171
|
+
if (process.stdin.isTTY === true) process.stdin.setRawMode?.(false);
|
|
36172
|
+
throw error;
|
|
36173
|
+
}
|
|
35464
36174
|
},
|
|
35465
36175
|
stderr: process.stderr
|
|
35466
36176
|
};
|
|
@@ -35917,6 +36627,7 @@ function createTranscriptStore(replay) {
|
|
|
35917
36627
|
const listeners = /* @__PURE__ */ new Set();
|
|
35918
36628
|
let scheduled = false;
|
|
35919
36629
|
let lastNotifyAt = 0;
|
|
36630
|
+
const attemptKeys = /* @__PURE__ */ new Map();
|
|
35920
36631
|
const notify = () => {
|
|
35921
36632
|
if (scheduled) return;
|
|
35922
36633
|
scheduled = true;
|
|
@@ -35948,8 +36659,28 @@ function createTranscriptStore(replay) {
|
|
|
35948
36659
|
dirty = true;
|
|
35949
36660
|
notify();
|
|
35950
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
|
+
},
|
|
35951
36681
|
reset() {
|
|
35952
36682
|
acc = createReplayAccumulator();
|
|
36683
|
+
attemptKeys.clear();
|
|
35953
36684
|
dirty = true;
|
|
35954
36685
|
notify();
|
|
35955
36686
|
}
|
|
@@ -36017,12 +36748,27 @@ function foldSubagentRow(previous, sessionId, event) {
|
|
|
36017
36748
|
activity: "prompted",
|
|
36018
36749
|
updatedAt: event.time
|
|
36019
36750
|
};
|
|
36020
|
-
case "assistant/
|
|
36751
|
+
case "assistant/attempt": return {
|
|
36021
36752
|
...base,
|
|
36022
36753
|
state: "running",
|
|
36023
36754
|
activity: "thinking…",
|
|
36024
36755
|
updatedAt: event.time
|
|
36025
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
|
+
}
|
|
36026
36772
|
case "assistant/message": return {
|
|
36027
36773
|
...base,
|
|
36028
36774
|
state: "idle",
|
|
@@ -36098,7 +36844,7 @@ function createSubagentFeed() {
|
|
|
36098
36844
|
const counted = !seen.has(sessionId);
|
|
36099
36845
|
if (counted) seen.add(sessionId);
|
|
36100
36846
|
if (rows.length >= 8) {
|
|
36101
|
-
const evict = rows.findIndex((row) => row.state
|
|
36847
|
+
const evict = rows.findIndex((row) => row.state !== "running");
|
|
36102
36848
|
if (evict === -1) {
|
|
36103
36849
|
if (counted) notify();
|
|
36104
36850
|
return;
|
|
@@ -36158,8 +36904,8 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36158
36904
|
const listeners = /* @__PURE__ */ new Set();
|
|
36159
36905
|
const reload = () => {
|
|
36160
36906
|
const target = agent;
|
|
36161
|
-
if (skills === void 0
|
|
36162
|
-
Promise.resolve().then(() => skills.list({
|
|
36907
|
+
if (skills === void 0) return;
|
|
36908
|
+
Promise.resolve().then(() => skills.list(target === void 0 ? { cwd: fallbackCwd } : {
|
|
36163
36909
|
cwd: target.session.header.cwd ?? fallbackCwd,
|
|
36164
36910
|
scope: target
|
|
36165
36911
|
})).then((summaries) => {
|
|
@@ -36174,13 +36920,18 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36174
36920
|
for (const listener of listeners) listener();
|
|
36175
36921
|
}).catch((cause) => {
|
|
36176
36922
|
if (agent !== target) return;
|
|
36923
|
+
const nextError = cause instanceof Error ? cause.message : String(cause);
|
|
36177
36924
|
if (loadedFor !== target) rows = [];
|
|
36178
|
-
|
|
36179
|
-
error =
|
|
36925
|
+
const errorChanged = nextError !== error;
|
|
36926
|
+
error = nextError;
|
|
36927
|
+
if (!errorChanged) return;
|
|
36180
36928
|
for (const listener of listeners) listener();
|
|
36181
36929
|
});
|
|
36182
36930
|
};
|
|
36183
|
-
if (skills !== void 0)
|
|
36931
|
+
if (skills !== void 0) {
|
|
36932
|
+
ctx.on("skills/change", reload);
|
|
36933
|
+
reload();
|
|
36934
|
+
}
|
|
36184
36935
|
return {
|
|
36185
36936
|
get rows() {
|
|
36186
36937
|
return rows;
|
|
@@ -36215,16 +36966,22 @@ function watchSkills(ctx, fallbackCwd) {
|
|
|
36215
36966
|
* @param sessionId - the full session identity for the header.
|
|
36216
36967
|
* @returns the complete markdown text.
|
|
36217
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");
|
|
36218
36971
|
function buildExportMarkdown(view, sessionId) {
|
|
36219
36972
|
const out = [
|
|
36220
36973
|
view.title === "" ? `# dsh session ${sessionId}` : `# ${view.title}`,
|
|
36221
36974
|
`> session ${sessionId}`,
|
|
36222
36975
|
""
|
|
36223
36976
|
];
|
|
36977
|
+
if (view.systemPrompt !== "") out.push("<details><summary>system prompt</summary>", "", view.systemPrompt, "", "</details>", "");
|
|
36224
36978
|
for (const entry of view.entries) switch (entry.kind) {
|
|
36225
36979
|
case "user":
|
|
36226
36980
|
if (entry.notice) out.push(`> ⤷ context: ${entry.text}`, "");
|
|
36227
|
-
else
|
|
36981
|
+
else {
|
|
36982
|
+
const attachments = attachmentLabels(entry);
|
|
36983
|
+
out.push("## user", "", entry.text, ...attachments === "" ? [] : [attachments], "");
|
|
36984
|
+
}
|
|
36228
36985
|
break;
|
|
36229
36986
|
case "assistant":
|
|
36230
36987
|
if (entry.reasoning !== "") out.push("<details><summary>thinking</summary>", "", entry.reasoning, "", "</details>", "");
|
|
@@ -36257,7 +37014,7 @@ function buildExportMarkdown(view, sessionId) {
|
|
|
36257
37014
|
out.push(`> files changed: ${entry.paths.join(", ")}`, "");
|
|
36258
37015
|
break;
|
|
36259
37016
|
case "pending":
|
|
36260
|
-
out.push("## user", "", entry.text, ...
|
|
37017
|
+
out.push("## user", "", entry.text, ...attachmentLabels(entry) === "" ? [] : [attachmentLabels(entry)], "");
|
|
36261
37018
|
break;
|
|
36262
37019
|
default: assertNever(entry, "transcript entry kind");
|
|
36263
37020
|
}
|
|
@@ -37108,6 +37865,17 @@ async function runQuitSequence(steps, exit, onError) {
|
|
|
37108
37865
|
return started;
|
|
37109
37866
|
}
|
|
37110
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
|
+
/**
|
|
37111
37879
|
* Order-preserving gate for composer input while the startup prompt/images
|
|
37112
37880
|
* are still preparing. Anything submitted before the startup delivery settles
|
|
37113
37881
|
* queues and flushes afterwards in submit order, so the initial request can
|
|
@@ -37158,7 +37926,7 @@ async function resolveTarget(startup, persistence, cwd) {
|
|
|
37158
37926
|
};
|
|
37159
37927
|
if (startup.kind === "named") {
|
|
37160
37928
|
if (persistence !== void 0) {
|
|
37161
|
-
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`);
|
|
37162
37930
|
}
|
|
37163
37931
|
return {
|
|
37164
37932
|
sessionId: startup.sessionId,
|
|
@@ -37167,7 +37935,7 @@ async function resolveTarget(startup, persistence, cwd) {
|
|
|
37167
37935
|
};
|
|
37168
37936
|
}
|
|
37169
37937
|
if (persistence === void 0) throw new Error("cannot resolve the requested session: session persistence is not configured");
|
|
37170
|
-
const headers = await persistence.list();
|
|
37938
|
+
const headers = (await persistence.list()).map((snapshot) => snapshot.header);
|
|
37171
37939
|
if (startup.kind === "resume") {
|
|
37172
37940
|
const matched = matchSessionId(headers, startup.sessionId);
|
|
37173
37941
|
if (isSubagentSession(matched)) throw new Error("subagent conversations are read-only; resume a root session");
|
|
@@ -37226,12 +37994,12 @@ async function run(ctx, startup, io) {
|
|
|
37226
37994
|
const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
|
|
37227
37995
|
let mode = next.resume ? next.mode : next.mode ?? pendingMode;
|
|
37228
37996
|
if (!next.resume) mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
|
|
37229
|
-
const setup = async (agentCtx) => {
|
|
37230
|
-
const sessionPreset = next.resume ? resolvePreset(
|
|
37997
|
+
const setup = async (agentCtx, agent) => {
|
|
37998
|
+
const sessionPreset = next.resume ? resolvePreset(agent.session) : mode;
|
|
37231
37999
|
mode = (await presets.mount(agentCtx, sessionPreset)).id;
|
|
37232
38000
|
installModelSelection(agentCtx, {
|
|
37233
38001
|
get current() {
|
|
37234
|
-
return resolveEffectiveSelection(selectionState.picked,
|
|
38002
|
+
return resolveEffectiveSelection(selectionState.picked, agent.session.requestHeader()?.config, currentDefaults());
|
|
37235
38003
|
},
|
|
37236
38004
|
set current(value) {
|
|
37237
38005
|
selectionState.picked = value;
|
|
@@ -37257,8 +38025,9 @@ async function run(ctx, startup, io) {
|
|
|
37257
38025
|
cwd: nextCwd,
|
|
37258
38026
|
agentPreset: mode,
|
|
37259
38027
|
...next.parentSession === void 0 ? {} : { parentSession: next.parentSession },
|
|
37260
|
-
...next.seedLength === void 0 ? {} : {
|
|
38028
|
+
...next.seedLength === void 0 ? {} : { isSeeded: true }
|
|
37261
38029
|
},
|
|
38030
|
+
...next.seedLength === void 0 ? {} : { inheritedEventCount: SessionLogOffset(next.seedLength) },
|
|
37262
38031
|
...next.seed === void 0 ? {} : { seed: next.seed },
|
|
37263
38032
|
agentOptions: seedOptions,
|
|
37264
38033
|
signal: quitAbort.signal,
|
|
@@ -37335,10 +38104,15 @@ async function run(ctx, startup, io) {
|
|
|
37335
38104
|
if (session === void 0) return;
|
|
37336
38105
|
if (subject.id === session.id) {
|
|
37337
38106
|
store.apply(event);
|
|
38107
|
+
if (event.type === "subagent/catalog" && event.data.childId !== "") subagents.apply(event.data.childId, event);
|
|
37338
38108
|
return;
|
|
37339
38109
|
}
|
|
37340
38110
|
if (subject.header.parentSession === session.id && subject.header.origin === "subagent") subagents.apply(subject.id, event);
|
|
37341
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
|
+
});
|
|
37342
38116
|
const commands = watchCommands(ctx);
|
|
37343
38117
|
if (agent !== void 0) commands.setAgent(agent);
|
|
37344
38118
|
const skills = watchSkills(ctx, cwd);
|
|
@@ -37410,6 +38184,19 @@ async function run(ctx, startup, io) {
|
|
|
37410
38184
|
bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
37411
38185
|
});
|
|
37412
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
|
+
};
|
|
37413
38200
|
const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
|
|
37414
38201
|
let inputHistory = [];
|
|
37415
38202
|
let historyWriteChain = Promise.resolve();
|
|
@@ -37697,7 +38484,8 @@ async function run(ctx, startup, io) {
|
|
|
37697
38484
|
});
|
|
37698
38485
|
};
|
|
37699
38486
|
/** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
|
|
37700
|
-
const dispatch = (text, images = []) => {
|
|
38487
|
+
const dispatch = (text, images = [], origin) => {
|
|
38488
|
+
if (!submissionBelongsToSession(origin, session?.id)) return;
|
|
37701
38489
|
send(text, "followup", images);
|
|
37702
38490
|
};
|
|
37703
38491
|
/**
|
|
@@ -37705,7 +38493,8 @@ async function run(ctx, startup, io) {
|
|
|
37705
38493
|
* boundary (the inbox delivers between steps); an idle driver just starts
|
|
37706
38494
|
* a turn, so this doubles as the busy-state submit path.
|
|
37707
38495
|
*/
|
|
37708
|
-
const steer = (text, images = []) => {
|
|
38496
|
+
const steer = (text, images = [], origin) => {
|
|
38497
|
+
if (!submissionBelongsToSession(origin, session?.id)) return;
|
|
37709
38498
|
send(text, "steer", images);
|
|
37710
38499
|
};
|
|
37711
38500
|
/** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
|
|
@@ -37833,14 +38622,17 @@ async function run(ctx, startup, io) {
|
|
|
37833
38622
|
const loadSessions = async (options, signal) => {
|
|
37834
38623
|
if (sessionQuery === void 0) throw new Error("session query is unavailable in this profile");
|
|
37835
38624
|
const records = await sessionQuery.listSessions(signal);
|
|
38625
|
+
const root = jsonlSessionRoot(persistence);
|
|
37836
38626
|
const updated = /* @__PURE__ */ new Map();
|
|
37837
|
-
|
|
37838
|
-
const location = persistence?.locate(record.header);
|
|
37839
|
-
if (location === void 0) continue;
|
|
38627
|
+
if (root !== void 0) await Promise.all(records.map(async (record) => {
|
|
37840
38628
|
try {
|
|
37841
|
-
|
|
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);
|
|
37842
38634
|
} catch {}
|
|
37843
|
-
}
|
|
38635
|
+
}));
|
|
37844
38636
|
const projected = projectSessionRows(records, options, updated);
|
|
37845
38637
|
const page = projected.slice(0, 32);
|
|
37846
38638
|
if (page.length === 0) return projected;
|
|
@@ -37856,11 +38648,11 @@ async function run(ctx, startup, io) {
|
|
|
37856
38648
|
* 1. `planSessionDeletion` collects the subtree and refuses when the root
|
|
37857
38649
|
* or ANY member is live (a live child would outlive its deleted
|
|
37858
38650
|
* parent), ordering the plan children-first.
|
|
37859
|
-
* 2. Every plan node must
|
|
37860
|
-
* (`encodeSegment(id)
|
|
37861
|
-
*
|
|
37862
|
-
* file has been touched yet, so a backend or layout
|
|
37863
|
-
* 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.
|
|
37864
38656
|
* 3. Artifacts are removed children-first: only an I/O error mid-delete
|
|
37865
38657
|
* can stop it short (reported with removed/total counts), leaving the
|
|
37866
38658
|
* shallowest lineage intact.
|
|
@@ -37874,22 +38666,23 @@ async function run(ctx, startup, io) {
|
|
|
37874
38666
|
const records = await sessionQuery.listSessions();
|
|
37875
38667
|
const plan = planSessionDeletion(records, id);
|
|
37876
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)";
|
|
37877
38671
|
const byId = new Map(records.map((record) => [record.header.id, record]));
|
|
37878
38672
|
const dirs = /* @__PURE__ */ new Map();
|
|
37879
38673
|
for (const node of plan.nodes) {
|
|
37880
38674
|
const record = byId.get(node.id);
|
|
37881
38675
|
if (record === void 0) return `no persisted session matches "${node.id}"`;
|
|
37882
|
-
const
|
|
37883
|
-
if (
|
|
37884
|
-
const dir = sessionArtifactDirectory(location.path, node.id);
|
|
37885
|
-
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)}`;
|
|
37886
38678
|
dirs.set(node.id, dir);
|
|
37887
38679
|
}
|
|
37888
38680
|
let removed = 0;
|
|
37889
38681
|
for (const node of plan.nodes) {
|
|
37890
38682
|
const dir = dirs.get(node.id);
|
|
37891
38683
|
try {
|
|
37892
|
-
|
|
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 });
|
|
37893
38686
|
await rm(dir, {
|
|
37894
38687
|
force: true,
|
|
37895
38688
|
recursive: false
|
|
@@ -38127,6 +38920,7 @@ async function run(ctx, startup, io) {
|
|
|
38127
38920
|
const permission = permissionPresets === void 0 ? currentView.permission : effectivePermission(permissionPresets, session, pendingPermission);
|
|
38128
38921
|
return (0, import_react.createElement)(App, {
|
|
38129
38922
|
key: session?.id ?? "pending",
|
|
38923
|
+
sessionKey: session?.id ?? "",
|
|
38130
38924
|
store,
|
|
38131
38925
|
approval,
|
|
38132
38926
|
questions,
|
|
@@ -38164,6 +38958,8 @@ async function run(ctx, startup, io) {
|
|
|
38164
38958
|
loadMentions: (query, signal) => mentions.candidates(query, signal),
|
|
38165
38959
|
inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
|
|
38166
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),
|
|
38167
38963
|
cyclePermission: cyclePermission$1,
|
|
38168
38964
|
setPermission: setPermissionAction,
|
|
38169
38965
|
selectModel,
|
|
@@ -38202,6 +38998,8 @@ async function run(ctx, startup, io) {
|
|
|
38202
38998
|
saveStatusline,
|
|
38203
38999
|
applyEditorKeys,
|
|
38204
39000
|
saveTheme,
|
|
39001
|
+
animations: animationsEnabled,
|
|
39002
|
+
saveAnimations,
|
|
38205
39003
|
history: inputHistory,
|
|
38206
39004
|
recordHistory,
|
|
38207
39005
|
cancelQueued,
|
|
@@ -38240,6 +39038,9 @@ async function run(ctx, startup, io) {
|
|
|
38240
39038
|
if (themeWarning !== void 0) setTimeout(() => {
|
|
38241
39039
|
bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
|
|
38242
39040
|
}, 50);
|
|
39041
|
+
if (animationsWarning !== void 0) setTimeout(() => {
|
|
39042
|
+
bridge.notify("animations config unreadable, animations stay on: " + animationsWarning, "warning");
|
|
39043
|
+
}, 50);
|
|
38243
39044
|
resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
|
|
38244
39045
|
if (hint === void 0) return;
|
|
38245
39046
|
setTimeout(() => {
|
|
@@ -38287,4 +39088,4 @@ function apply(ctx, config) {
|
|
|
38287
39088
|
});
|
|
38288
39089
|
}
|
|
38289
39090
|
//#endregion
|
|
38290
|
-
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };
|
|
39091
|
+
export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence, submissionBelongsToSession };
|