dsh-code 1.0.4 → 1.0.6

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