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/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
- /** Prompt text with its durable image labels, without exposing local paths or bytes. */
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 labels = imageLabels(entry.images);
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 "assistant/chunk": {
25351
- const chunk = event.data.chunk;
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
- if ((chunk.type === "text-delta" || chunk.type === "reasoning-delta" ? chunk.text : "") !== "" && !acc.firstChunkAt.has(key)) {
25354
- acc.firstChunkAt.set(key, event.time);
25355
- const started = acc.stepStart.get(key);
25356
- if (started !== void 0) acc.stats = {
25357
- ...acc.stats,
25358
- ttftMs: acc.stats.ttftMs + Math.max(0, event.time - started),
25359
- ttftSteps: acc.stats.ttftSteps + 1
25360
- };
25361
- }
25362
- if (chunk.type === "text-delta") {
25363
- acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
25364
- return true;
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
- return false;
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
- const firstChunk = acc.firstChunkAt.get(key);
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
- /** Pulse ring half-width in columns — Codex PULSE_HALF_WIDTH (4.5). */
26369
- const PULSE_HALF_WIDTH = 4.5;
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 model id containing `flash` runs the
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
- return model.toLowerCase().includes("flash") ? "flash" : "deepseek";
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 contribution at a column — Codex `band_sample`, all three
26563
- * branches: Wave sweeps an eased crest across the row; Aurora drifts a
26564
- * sinusoidal center carrying a hue index; Pulse expands a ring from the row
26565
- * center with cubic ease and decaying strength.
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
- * @returns `[hueIndex, strength]`.
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 progress = (elapsed - first) / second;
26578
- if (progress < 0 || progress > 1) return [0, 0];
26579
- const center = easeInOut(progress) * (width + 18) - 9;
26580
- return [0, crest(Math.abs(column - center) / 9)];
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 [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)];
26786
+ return [[
26787
+ Math.trunc(third),
26788
+ crest(Math.abs(column - center) / halfWidth),
26789
+ 0
26790
+ ]];
26586
26791
  }
26587
26792
  case "pulse": {
26588
- const progress = (elapsed - first) / second;
26589
- if (progress < 0 || progress > 1) return [0, 0];
26590
- const inverse = 1 - progress;
26591
- const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH);
26592
- const distance = Math.abs(column - width / 2);
26593
- return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - .6 * progress)];
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
- * Per-row phase share of the duration: the crest reaches the top row first
26607
- * and the bottom row last, sweeping down the band. 0.12 keeps the bottom
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` each row samples the same timeline shifted by a
26620
- * per-row phase offset, so the crest cascades down the band instead of
26621
- * painting every row identically.
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] = bandSample(style, band, elapsed, column, width);
26644
- weights[hue] = style === "aurora" ? weights[hue] + strength : Math.max(weights[hue], strength);
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 * .55;
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
- descriptors = commands.list(agent);
27064
+ next = commands.list(agent);
26768
27065
  loadedFor = agent;
26769
- error = void 0;
26770
27066
  } catch (cause) {
26771
- descriptors = loadedFor === agent ? [...descriptors] : [];
26772
- error = cause instanceof Error ? cause.message : String(cause);
27067
+ next = loadedFor === agent ? [...descriptors] : [];
27068
+ nextError = cause instanceof Error ? cause.message : String(cause);
26773
27069
  }
26774
- for (const listener of listeners) listener();
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 ONLY to
27484
- * validate that a `locate()` path really is this session's directory before
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
- * Guard one `locate()` artifact path before deletion (codex's scoped-path
27505
- * check, adapted to the JSONL layout): the file must be a `session.jsonl`
27506
- * artifact sitting in the directory named exactly `encodeSegment(id)`.
27507
- * @param artifact - the path the persistence backend located.
27508
- * @param id - the session id the artifact claims to belong to.
27509
- * @returns the owning session directory, or undefined when the layout is unexpected.
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 sessionArtifactDirectory(artifact, id) {
27512
- if (basename(artifact) !== "session.jsonl" && basename(artifact) !== "session.jsonl.zstd") return void 0;
27513
- const dir = dirname(artifact);
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
- /** Parse a terminal paste/drop containing only one or more image paths. */
30612
- function parsePastedImagePaths(input) {
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
- const tokens = [];
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://")) try {
30621
- path = fileURLToPath(path);
30622
- } catch {
30623
- return [];
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 (!looksLikeImagePath(path)) return [];
30626
- tokens.push(path);
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 tokens;
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
- /** Interval-driven frame counter for one self-contained animated leaf. */
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 id = setInterval(() => setTick((current) => current + 1), intervalMs);
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
- /** The original web StateDot chase used by the busy composer marker. */
30874
- function BusyChase() {
30875
- const tick = useFrames(125);
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, { text });
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 label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "");
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)), (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, {
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
- if (prefix === "") return all;
33117
- return all.filter((candidate) => candidate.label.slice(1).startsWith(prefix));
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
- let marker = source === "mention" ? `@${safeName}` : `[image: ${safeName}]`;
34042
+ const label = kind === "file" ? "file" : "image";
34043
+ let marker = source === "mention" ? `@${safeName}` : `[${label}: ${safeName}]`;
33313
34044
  let suffix = 2;
33314
- while (valueRef.current.includes(marker) || draftImagesRef.current.some((image) => image.marker === marker) || reserved.includes(marker)) {
33315
- marker = source === "mention" ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`;
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
- const insertDroppedImages = (paths) => {
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
- notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
33337
- inspectImages(paths).then((inspected) => {
33338
- const additions = [];
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 inspected) {
33341
- if ([...draftImagesRef.current, ...additions].some((image) => sameImagePath(image.path, inspection.path))) continue;
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
- additions.push({
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 (additions.length === 0) {
33350
- notify("those images are already attached", "warning");
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 image drop point; drop the images again", "warning");
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, ...additions];
34120
+ const nextImages = [...draftImagesRef.current, ...imageAdditions];
33372
34121
  draftImagesRef.current = nextImages;
33373
34122
  setDraftImages(nextImages);
33374
- notify(`${additions.length} image${additions.length === 1 ? "" : "s"} ready for the next message`);
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(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
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
- const menuRows = mentionActive ? visibleMentionRows.map((row) => ({
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 (visibleMentionRows.length === 0) return;
33421
- const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
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
- notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
33676
- const snapshot = draftImagesRef.current;
33677
- prepareImages(snapshot.map((image) => image.path), controller.signal).then((images) => {
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
- if (busy) steer(text, images);
33696
- else dispatch(text, images);
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(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
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
- const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : [];
33950
- if (droppedPaths.length > 0) {
33951
- insertDroppedImages(droppedPaths);
33952
- return;
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 [waveTick, setWaveTick] = (0, import_react.useState)(null);
33958
- const wavePrevious = (0, import_react.useRef)({
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
- const previous = wavePrevious.current;
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
- waveTick,
33988
- waveTier,
33989
- waveStyle
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
- const waveRow = () => {
34072
- const hues = deepseekWaveHues(waveTier);
34073
- const style = waveStyle;
34074
- const bandRgb = getPalette().composerBand;
34075
- const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
34076
- const totalBandRows = visibleRows.length + 2;
34077
- const waveBg = (row, column) => {
34078
- const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, totalBandRows);
34079
- return rgb === null ? bandBg : inkColor(rgb);
34080
- };
34081
- const blankBandRow = (row) => {
34082
- const blanks = [];
34083
- for (let column = 0; column < bandWidth; column += 1) blanks.push({
34084
- char: " ",
34085
- width: 1,
34086
- backgroundColor: waveBg(row, column)
34087
- });
34088
- return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
34089
- };
34090
- const cellIndexAtColumn = (cells, target) => {
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 Input, so the
34343
- * sweep re-renders only the composer band, not the whole tree, at 30fps;
34344
- * App owns the rarely-changing tier/style and Input starts the sweep
34345
- * whenever that pair changes. */
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, { text: "✻ Thinking… (Ctrl/Alt+R to expand)" }) : (0, import_react.createElement)(StreamTail, {
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) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
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.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
35445
- const tuiStdin = createSplitStdin(process.stdin);
35446
- const instance = render(element, {
35447
- exitOnCtrlC: false,
35448
- stdin: tuiStdin.stdin,
35449
- stdout: process.stdout
35450
- });
35451
- return {
35452
- rerender(element) {
35453
- instance.rerender(element);
35454
- },
35455
- unmount() {
35456
- try {
35457
- instance.unmount();
35458
- } finally {
35459
- tuiStdin.dispose();
35460
- process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
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/chunk": return {
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 === "done");
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 || target === void 0) return;
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
- else rows = [...rows];
36179
- error = cause instanceof Error ? cause.message : String(cause);
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) ctx.on("skills/change", reload);
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 out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
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, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
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(agentCtx.agent.session) : mode;
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, agentCtx.agent?.session.requestHeader()?.config, currentDefaults());
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 ? {} : { seedLength: next.seedLength }
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
- for (const record of records) {
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
- updated.set(record.header.id, (await stat(location.path)).mtimeMs);
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 locate to a guarded artifact directory
37860
- * (`encodeSegment(id)`/`session.jsonl` layout). Backends without a
37861
- * locatable artifact (SQLite) refuse the WHOLE deletion here — no
37862
- * file has been touched yet, so a backend or layout surprise can
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 location = persistence?.locate(record.header);
37883
- if (location === void 0) return `session backend exposes no deletable artifact for ${node.id.slice(-12)} (deletion is unsupported on this backend)`;
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
- for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
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 };