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
@@ -1,23 +1,36 @@
1
- /**
1
+ /**
2
2
  * Terminal animation helpers derived from the web design language:
3
3
  * thinking uses Codex's slow shimmer sweep, the busy composer marker uses the
4
4
  * original braille chase, and the streaming caret blink is the Claude-Code
5
5
  * convention.
6
- *
7
- * The DeepSeek model-switch easter egg ports Codex's effort-ignition "Wave"
8
- * style (`codex-rs/tui/src/bottom_pane/effort_ignition_styles.rs`): switching
9
- * INTO an official DeepSeek route sweeps a blue wave across the composer's
10
- * input row — one column per cell, `backgroundColor` = the sampled wave
11
- * color — then, on the deepseek (Ultra-equivalent) tier, drops the `· ✦ ✧`
12
- * sparkle sequence into the rightmost blank cell before fading. The prompt
13
- * marker keeps the tier accent afterwards (persistent, like Codex's prompt
14
- * charge). Pure functions only — the Ink layer owns timers and colors.
15
- *
16
- * @module @deepseek-ai/dsh-code/render/animations
17
- */
18
-
19
- import type { RgbTriple } from '../theme.ts'
20
-
6
+ *
7
+ * The DeepSeek model-switch easter egg ports Codex's effort-ignition "Wave"
8
+ * style (`codex-rs/tui/src/bottom_pane/effort_ignition_styles.rs`): switching
9
+ * INTO an official DeepSeek route sweeps a blue wave across the composer's
10
+ * input row — one column per cell, `backgroundColor` = the sampled wave
11
+ * color — then, on the deepseek (Ultra-equivalent) tier, drops the `· ✦ ✧`
12
+ * sparkle sequence into the rightmost blank cell before fading. The prompt
13
+ * marker keeps the tier accent afterwards (persistent, like Codex's prompt
14
+ * charge). Pure functions only — the Ink layer owns timers and colors.
15
+ *
16
+ * Wave and Pulse deliberately extend the Codex port after in-terminal
17
+ * testing: the per-row phase cascade was removed (Codex tints each column
18
+ * across the whole band), Wave became a WATER SURFACE — one continuous sine
19
+ * swell spanning the band, mirror-symmetric about the center column, its
20
+ * crests flowing outward from the center with a symmetric fade envelope
21
+ * (the deepseek tier adds one faster harmonic crossing it), painted with
22
+ * Aurora's recipe: wide soft gradients, mirrored second-hue mixing, and a
23
+ * low alpha cap — no hard core line — while Pulse became true
24
+ * two-dimensional, cell-aspect-corrected detonations: soft wide rings whose
25
+ * color grades across their width, expanding outward through a symmetric
26
+ * fade envelope and each trailing an echo ripple in the next blue. Aurora
27
+ * keeps Codex's geometry verbatim.
28
+ *
29
+ * @module @deepseek-ai/dsh-code/render/animations
30
+ */
31
+
32
+ import type { RgbTriple } from '../theme.ts'
33
+
21
34
  /** Cadence for the original busy braille chase (8 frames × 125ms = 1s). */
22
35
  export const BUSY_CHASE_TICK_MS = 125
23
36
 
@@ -78,390 +91,580 @@ export function deepDivingSparkColor(tick: number, base: RgbTriple, highlight: R
78
91
  return blendRgb(highlight, base, deepDivingSparkIntensity(tick))
79
92
  }
80
93
 
81
- /** Caret visibility: half the ticks on, half off (530ms blink). */
82
- export function caretVisible(tick: number): boolean {
83
- return tick % 2 === 0
84
- }
85
-
86
- /**
87
- * The one-shot DeepSeek model-switch easter egg: when the status bar model
88
- * label switches to an official DeepSeek route, the composer's input row
89
- * plays Codex's effort-ignition "Wave" — a blue crest sweeping the content
90
- * row column by column (background tint 0.55 under the draft), plus the
91
- * Ultra-style ✧` sparkles on the deepseek tier — and the prompt marker
92
- * keeps the tier accent afterwards. The Ink layer owns the timer and reads
93
- * the ACTIVE palette anchors (`getPalette`); everything below is pure
94
- * interpolation over the colors it is given.
95
- */
96
-
97
- /** Frame cadence of the DeepSeek wave: Codex's IGNITION_FRAME_TICK (33ms ≈ 30fps). */
98
- export const DEEPSEEK_WAVE_TICK_MS = 33
99
-
100
- /**
101
- * The DeepSeek wave tiers. The concept maps Codex's reasoning tiers to
102
- * model ids: `flash` runs the Max parameters, `deepseek` (pro models) runs
103
- * the Ultra parameters (dual band + tail sparkles on the Wave style).
104
- * `unknown` is the "Into the Unknown" variant: it reuses the deepseek tier's
105
- * exact parameters (dual band, durations, sparkles) for NON-DeepSeek models
106
- * running a reasoning effort above high the wordmark renders differently
107
- * but the motion is identical.
108
- */
109
- export type DeepseekWaveTier = 'flash' | 'deepseek' | 'unknown'
110
-
111
- /**
112
- * The three ignition styles Codex `IgnitionStyle`: a traveling crest
113
- * (Wave), a drifting multi-hue band (Aurora), and an expanding ring (Pulse).
114
- * One style is picked at random per trigger and never repeats the previous.
115
- */
116
- export type DeepseekWaveStyle = 'wave' | 'aurora' | 'pulse'
117
-
118
- /** All styles in canonical order, for random selection. */
119
- const DEEPSEEK_WAVE_STYLES: readonly DeepseekWaveStyle[] = ['wave', 'aurora', 'pulse']
120
-
121
- /** Wave half-width in columns Codex WAVE_HALF_WIDTH (9). */
122
- export const WAVE_HALF_WIDTH = 9
123
-
124
- /** Pulse ring half-width in columns — Codex PULSE_HALF_WIDTH (4.5). */
125
- const PULSE_HALF_WIDTH = 4.5
126
-
127
- /** Sparkle start and frame cadence Codex SPARK_START / SPARK_FRAME. */
128
- const SPARK_START_MS = 900
129
- const SPARK_FRAME_MS = 100
130
-
131
- /** Sparkle glyphs in frame order Codex SPARK_GLYPHS (`· ✦ ✧`). */
132
- export const SPARK_GLYPHS = ['·', '✦', '✧'] as const
133
-
134
- /**
135
- * Band tables — Codex `bands(style, tier)`. Each entry is a triple whose
136
- * meaning depends on the style: Wave/Pulse use `(launch, travel, strength)`;
137
- * Aurora uses `(speed, phase, hueIndex)`.
138
- */
139
- export type DeepseekWaveBand = readonly [number, number, number]
140
- export const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Record<DeepseekWaveTier, readonly DeepseekWaveBand[]>>>> = {
141
- wave: {
142
- // Wave-Max: one band sweeping 0.10s..0.85s.
143
- flash: [[0.10, 0.75, 1.0]],
144
- // Wave-Ultra: two offset bands for a richer crest.
145
- deepseek: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
146
- // Into the Unknown reuses the Ultra parameters verbatim.
147
- unknown: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
148
- },
149
- aurora: {
150
- // Aurora-Max: two drifting bands (hues 0 and 1).
151
- flash: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0]],
152
- // Aurora-Ultra: a third band adds hue 2.
153
- deepseek: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
154
- unknown: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
155
- },
156
- pulse: {
157
- // Pulse-Max: one expanding ring.
158
- flash: [[0.10, 0.60, 1.0]],
159
- // Pulse-Ultra: two rings (inner weaker, outer stronger).
160
- deepseek: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
161
- unknown: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
162
- },
163
- }
164
-
165
- /** Extra display time applied to every Codex ignition style. */
166
- const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200
167
-
168
- /** Original Codex duration used as the animation's sampling timeline. */
169
- function deepseekWaveBaseDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
170
- // The unknown tier reuses the deepseek (pro) durations exactly.
171
- const pro = tier === 'deepseek' || tier === 'unknown'
172
- switch (style) {
173
- case 'aurora': return pro ? 1600 : 1300
174
- case 'pulse': return pro ? 1250 : 900
175
- case 'wave': return pro ? 1300 : 1000
176
- }
177
- }
178
-
179
- /**
180
- * Total visible duration: the Codex ignition duration plus 200ms so its motion
181
- * remains readable in a busy terminal.
182
- * @param tier - the active wave tier.
183
- * @param style - the active ignition style.
184
- * @returns the duration in milliseconds.
185
- */
186
- export function deepseekWaveDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): number {
187
- return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS
188
- }
189
-
190
- /** Map the extended display timeline back onto the original Codex samples. */
191
- function deepseekWaveSampleElapsedMs(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
192
- const base = deepseekWaveBaseDuration(tier, style)
193
- return tick * DEEPSEEK_WAVE_TICK_MS * base / deepseekWaveDuration(tier, style)
194
- }
195
-
196
- /**
197
- * Pick one ignition style at random, never repeating the previous one —
198
- * Codex `IgnitionStyle::random`. Falls back to the remaining styles.
199
- * @param previous - the style of the last trigger, if any.
200
- * @returns a style different from `previous`.
201
- */
202
- export function deepseekWaveStyleRandom(previous: DeepseekWaveStyle | undefined): DeepseekWaveStyle {
203
- const candidates = DEEPSEEK_WAVE_STYLES.filter(style => style !== previous)
204
- return candidates[Math.floor(Math.random() * candidates.length)] ?? 'wave'
205
- }
206
-
207
- /**
208
- * Tier for a `provider/model` label: a model id containing `flash` runs the
209
- * single-band flash tier; everything else (pro/reasoner/chat) runs the
210
- * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
211
- * @param model - the `provider/model` label of the applied model.
212
- * @returns the wave tier for that model.
213
- */
214
- export function deepseekWaveTier(model: string): DeepseekWaveTier {
215
- return model.toLowerCase().includes('flash') ? 'flash' : 'deepseek'
216
- }
217
-
218
- /**
219
- * Cosine window Codex `crest`: 1 exactly under the wave center, 0 from
220
- * one half-width away.
221
- * @param distance - distance from the crest center in half-widths.
222
- * @returns the crest strength in 0..1.
223
- */
224
- export function crest(distance: number): number {
225
- if (distance >= 1) return 0
226
- return 0.5 * (1 + Math.cos(Math.PI * distance))
227
- }
228
-
229
- /**
230
- * Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
231
- * the middle, so the crest accelerates and eases instead of sliding linearly.
232
- * @param progress - raw progress (clamped to 0..1).
233
- * @returns the eased progress in 0..1.
234
- */
235
- export function easeInOut(progress: number): number {
236
- const p = Math.min(1, Math.max(0, progress))
237
- if (p < 0.5) return 4 * p * p * p
238
- const inverse = -2 * p + 2
239
- return 1 - (inverse * inverse * inverse) / 2
240
- }
241
-
242
- /**
243
- * Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
244
- * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
245
- * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
246
- * an envelope); exported for the Aurora-style fades and for tests.
247
- * @param elapsed - seconds since the animation started.
248
- * @param total - total duration in seconds.
249
- * @param fadeIn - seconds of fade-in.
250
- * @param fadeOut - seconds of fade-out.
251
- * @returns the envelope value in 0..1.
252
- */
253
- export function envelope(elapsed: number, total: number, fadeIn: number, fadeOut: number): number {
254
- if (elapsed <= 0 || elapsed >= total) return 0
255
- const rise = elapsed / Math.max(fadeIn, Number.EPSILON)
256
- const fall = (total - elapsed) / Math.max(fadeOut, Number.EPSILON)
257
- return Math.min(Math.max(Math.min(rise, fall), 0), 1)
258
- }
259
-
260
- /**
261
- * One band's contribution at a column — Codex `band_sample`, all three
262
- * branches: Wave sweeps an eased crest across the row; Aurora drifts a
263
- * sinusoidal center carrying a hue index; Pulse expands a ring from the row
264
- * center with cubic ease and decaying strength.
265
- * @param style - the ignition style.
266
- * @param band - the band triple (meaning depends on the style).
267
- * @param elapsed - seconds since the animation started.
268
- * @param column - column index in the content row (0..width-1).
269
- * @param width - content-row width in columns.
270
- * @returns `[hueIndex, strength]`.
271
- */
272
- function bandSample(
273
- style: DeepseekWaveStyle,
274
- band: DeepseekWaveBand,
275
- elapsed: number,
276
- column: number,
277
- width: number,
278
- ): [number, number] {
279
- const [first, second, third] = band
280
- switch (style) {
281
- case 'wave': {
282
- const progress = (elapsed - first) / second
283
- if (progress < 0 || progress > 1) return [0, 0]
284
- const center = easeInOut(progress) * (width + 2 * WAVE_HALF_WIDTH) - WAVE_HALF_WIDTH
285
- return [0, crest(Math.abs(column - center) / WAVE_HALF_WIDTH)]
286
- }
287
- case 'aurora': {
288
- const center = (0.5 + 0.38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width
289
- const halfWidth = Math.max(width * 0.22, 4)
290
- return [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)]
291
- }
292
- case 'pulse': {
293
- const progress = (elapsed - first) / second
294
- if (progress < 0 || progress > 1) return [0, 0]
295
- const inverse = 1 - progress
296
- const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH)
297
- const distance = Math.abs(column - width / 2)
298
- return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - 0.6 * progress)]
299
- }
300
- }
301
- }
302
-
303
- /** Linear RGB blend — Codex `blend`: `fg * alpha + bg * (1 - alpha)`. */
304
- function blendRgb(fg: RgbTriple, bg: RgbTriple, alpha: number): RgbTriple {
305
- return [
306
- Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
307
- Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
308
- Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
309
- ]
310
- }
311
-
312
- /**
313
- * Per-row phase share of the duration: the crest reaches the top row first
314
- * and the bottom row last, sweeping down the band. 0.12 keeps the bottom
315
- * row's lag inside the 200ms duration extension.
316
- */
317
- const DEEPSEEK_WAVE_ROW_PHASE = 0.12
318
-
319
- /**
320
- * The background color for one composer-band column at a tick — Codex
321
- * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
322
- * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
323
- * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
324
- * blends the mixed hue toward the blank-cell base at the style's alpha cap,
325
- * and Aurora applies its own fade envelope. Returns `null` when the column
326
- * should stay transparent, so the row returns to no `backgroundColor` on
327
- * both ends. With `rows > 1` each row samples the same timeline shifted by a
328
- * per-row phase offset, so the crest cascades down the band instead of
329
- * painting every row identically.
330
- * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
331
- * @param column - column index in the content row (0..width-1).
332
- * @param width - content-row width in columns.
333
- * @param tier - the wave tier (flash = Max, deepseek = Ultra parameters).
334
- * @param style - the ignition style.
335
- * @param hues - the tier's three hues.
336
- * @param base - the blank-cell base color the tint blends toward.
337
- * @param row - row index in the band (0..rows-1; default 0 = old single-row).
338
- * @param rows - band height in rows (default 1).
339
- * @returns the blended RGB background, or null for transparent.
340
- */
341
- export function deepseekWaveColumnBg(
342
- tick: number,
343
- column: number,
344
- width: number,
345
- tier: DeepseekWaveTier,
346
- style: DeepseekWaveStyle,
347
- hues: readonly [RgbTriple, RgbTriple, RgbTriple],
348
- base: RgbTriple,
349
- row = 0,
350
- rows = 1,
351
- ): RgbTriple | null {
352
- const total = deepseekWaveBaseDuration(tier, style) / 1000
353
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
354
- - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE
355
- const fade = style === 'aurora' ? envelope(elapsed, total, 0.25, 0.40) : 1
356
- const weights = [0, 0, 0]
357
- for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
358
- const [hue, strength] = bandSample(style, band, elapsed, column, width)
359
- weights[hue] = style === 'aurora' ? weights[hue]! + strength : Math.max(weights[hue]!, strength)
360
- }
361
- const weight = weights[0]! + weights[1]! + weights[2]!
362
- if (weight <= 0.01) return null
363
- let red = 0
364
- let green = 0
365
- let blue = 0
366
- for (let index = 0; index < 3; index += 1) {
367
- red += weights[index]! * hues[index]![0]
368
- green += weights[index]! * hues[index]![1]
369
- blue += weights[index]! * hues[index]![2]
370
- }
371
- const mixed: RgbTriple = [
372
- Math.round(red / weight),
373
- Math.round(green / weight),
374
- Math.round(blue / weight),
375
- ]
376
- const alpha = style === 'aurora' ? Math.min(weight * 0.40, 0.50) * fade : weight * 0.55
377
- if (alpha < 0.02) return null
378
- return blendRgb(mixed, base, alpha)
379
- }
380
-
381
- /**
382
- * The sparkle glyph for a tick Codex `spark_frame`, sampled on the same
383
- * proportionally slowed DeepSeek Wave timeline as the composer background.
384
- * The Ink layer still must skip occupied cells.
385
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
386
- * @returns the sparkle glyph, or null outside the stretched tail window.
387
- */
388
- export function deepseekWaveSpark(tick: number): string | null {
389
- const elapsed = deepseekWaveSampleElapsedMs(tick, 'deepseek', 'wave')
390
- if (elapsed < SPARK_START_MS) return null
391
- const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS)
392
- return SPARK_GLYPHS[frame] ?? null
393
- }
394
-
395
- /**
396
- * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
397
- * shortly after the first crest launches and out before the wave settles,
398
- * so the brand name surfaces through the sweep's middle. The Ink layer
399
- * places it in the row's blank mid-section (never over real draft text).
400
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
401
- * @param tier - the wave tier.
402
- * @returns true while the wordmark should be visible.
403
- */
404
- export function deepseekWaveWordVisible(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): boolean {
405
- const total = deepseekWaveBaseDuration(tier, style) / 1000
406
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
407
- return envelope(elapsed, total, total * 0.2, total * 0.35) > 0.25
408
- }
409
-
410
- /**
411
- * The per-character color for the `deepseek` wordmark: the tier's hues
412
- * cycled per character (d→hue0, e→hue1, e→hue2, …), a brand-gradient text.
413
- * @param index - character index in the wordmark.
414
- * @param hues - the tier's three hues.
415
- * @returns the hue for that character.
416
- */
417
- export function deepseekWaveWordHue(index: number, hues: readonly [RgbTriple, RgbTriple, RgbTriple]): RgbTriple {
418
- return hues[index % hues.length]!
419
- }
420
-
421
- /**
422
- * True when a `provider/model` status label addresses the official DeepSeek
423
- * route: either segment contains `deepseek` (case-insensitive), covering the
424
- * `deepseek-official` provider route and its `deepseek-*` model ids.
425
- * @param label - the status bar model label (`provider/model`).
426
- * @returns whether the label names an official DeepSeek model.
427
- */
428
- export function isOfficialDeepSeekLabel(label: string): boolean {
429
- const slash = label.indexOf('/')
430
- const provider = slash < 0 ? label : label.slice(0, slash)
431
- const model = slash < 0 ? '' : label.slice(slash + 1)
432
- return provider.toLowerCase().includes('deepseek') || model.toLowerCase().includes('deepseek')
433
- }
434
-
435
- /**
436
- * Known reasoning-effort ranks in ascending order. Effort ids are opaque
437
- * adapter-owned strings, so the rank table covers the conventional names
438
- * (off low medium → high → xhigh → max/ultra); an unrecognized id
439
- * ranks as unknown (0), which never triggers the high-effort wave.
440
- */
441
- const EFFORT_RANK: Readonly<Record<string, number>> = {
442
- off: 0,
443
- none: 0,
444
- low: 1,
445
- medium: 2,
446
- med: 2,
447
- high: 3,
448
- xhigh: 4,
449
- 'x-high': 4,
450
- 'very-high': 4,
451
- max: 5,
452
- maximum: 5,
453
- ultra: 5,
454
- }
455
-
456
- /**
457
- * True when an effective reasoning effort is STRICTLY above `high` the
458
- * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
459
- * Absent efforts and unrecognized ids never qualify.
460
- * @param effort - the effective reasoning-effort id ('' or undefined when none).
461
- * @returns whether the effort ranks above high.
462
- */
463
- export function effortAboveHigh(effort: string | undefined): boolean {
464
- if (effort === undefined || effort === '') return false
465
- const rank = EFFORT_RANK[effort.trim().toLowerCase()]
466
- return rank !== undefined && rank > 3
467
- }
94
+ /** Caret blink cadence: one blink step (on or off) per tick. */
95
+ export const CARET_BLINK_TICK_MS = 530
96
+
97
+ /** Caret visibility: half the ticks on, half off (530ms blink). */
98
+ export function caretVisible(tick: number): boolean {
99
+ return tick % 2 === 0
100
+ }
101
+
102
+ /**
103
+ * The one-shot DeepSeek model-switch easter egg: when the status bar model
104
+ * label switches to an official DeepSeek route, the composer's input row
105
+ * plays Codex's effort-ignition "Wave" a blue crest sweeping the content
106
+ * row column by column (background tint 0.55 under the draft), plus the
107
+ * Ultra-style ✦ ✧` sparkles on the deepseek tier and the prompt marker
108
+ * keeps the tier accent afterwards. The Ink layer owns the timer and reads
109
+ * the ACTIVE palette anchors (`getPalette`); everything below is pure
110
+ * interpolation over the colors it is given.
111
+ */
112
+
113
+ /** Frame cadence of the DeepSeek wave: Codex's IGNITION_FRAME_TICK (33ms ≈ 30fps). */
114
+ export const DEEPSEEK_WAVE_TICK_MS = 33
115
+
116
+ /**
117
+ * The DeepSeek wave tiers. The concept maps Codex's reasoning tiers to
118
+ * model ids: `flash` runs the Max parameters, `deepseek` (pro models) runs
119
+ * the Ultra parameters (dual band + tail sparkles on the Wave style).
120
+ * `unknown` is the "Into the Unknown" variant: it reuses the deepseek tier's
121
+ * exact parameters (dual band, durations, sparkles) for NON-DeepSeek models
122
+ * running a reasoning effort above high — the wordmark renders differently
123
+ * but the motion is identical.
124
+ */
125
+ export type DeepseekWaveTier = 'flash' | 'deepseek' | 'unknown'
126
+
127
+ /**
128
+ * The three ignition styles — Codex `IgnitionStyle`: a traveling crest
129
+ * (Wave), a drifting multi-hue band (Aurora), and an expanding ring (Pulse).
130
+ * One style is picked at random per trigger and never repeats the previous.
131
+ */
132
+ export type DeepseekWaveStyle = 'wave' | 'aurora' | 'pulse'
133
+
134
+ /** All styles in canonical order, for random selection. */
135
+ const DEEPSEEK_WAVE_STYLES: readonly DeepseekWaveStyle[] = ['wave', 'aurora', 'pulse']
136
+
137
+ /**
138
+ * The water surface: ONE continuous sine line spanning the whole band,
139
+ * mirror-symmetric about the center column, its crests flowing OUTWARD from
140
+ * the center (phase k·|x center| ω·t). No sweep window, no return trip —
141
+ * the surface fades in, flows, and fades out, symmetric in both space and
142
+ * time. The deepseek tier adds one faster, finer HARMONIC line whose crests
143
+ * cross the fundamental's: interleaved richness with both lines still
144
+ * symmetric and still only ever flowing outward.
145
+ */
146
+ export const WAVE_SURFACE_AMPLITUDE = 0.8
147
+ export const WAVE_SURFACE_HARMONIC = 0.45
148
+ export const WAVE_SURFACE_WAVELENGTH = 40
149
+ export const WAVE_SURFACE_OMEGA = 9
150
+ /** Vertical thickness in lane units — Aurora-wide: soft gradients, no hard edges. */
151
+ export const WAVE_SURFACE_THICKNESS = 1.2
152
+ /**
153
+ * The mirrored second-hue profile: the space BELOW the surface carries a
154
+ * second blue at this strength, so color (not just brightness) varies
155
+ * continuously across the wave — Aurora-style hue mixing instead of a
156
+ * single flat tint.
157
+ */
158
+ export const WAVE_SURFACE_MIRROR = 0.6
159
+ /** Aurora-style soft alpha: low gain, capped well under the pulse ring's. */
160
+ export const WAVE_SURFACE_ALPHA_GAIN = 0.45
161
+ export const WAVE_SURFACE_ALPHA_CAP = 0.68
162
+
163
+ /**
164
+ * Pulse ring geometry, softened to the Wave standard: a WIDE band
165
+ * (half-width 5.5) with a moderate peak riding the radius — all inside the
166
+ * hue blend, no hard white line and an inner profile one hue over at a
167
+ * slightly smaller radius, so the ring's color grades continuously across
168
+ * its width (the radial analog of the water surface's mirrored hues).
169
+ */
170
+ const PULSE_HALF_WIDTH = 5.5
171
+ const PULSE_PEAK_HALF_WIDTH = 1.8
172
+ const PULSE_PEAK_GAIN = 0.35
173
+ const PULSE_INNER_OFFSET = 2.5
174
+ const PULSE_INNER_STRENGTH = 0.6
175
+ /** The inner edge of each ring carries the tier's third blue. */
176
+ const PULSE_INNER_HUE = 2
177
+
178
+ /**
179
+ * The trailing echo ripple: every pulse ring drags a second, weaker ring at
180
+ * a fraction of its radius in the NEXT hue of the tier's blues, fading in a
181
+ * little after the primary so the center hole opens first.
182
+ */
183
+ const PULSE_ECHO_RADIUS = 0.7
184
+ const PULSE_ECHO_STRENGTH = 0.65
185
+ const PULSE_ECHO_DELAY = 0.12
186
+
187
+ /** Aurora-grade soft alpha for the detonation — a notch above the swell. */
188
+ export const PULSE_ALPHA_GAIN = 0.45
189
+ export const PULSE_ALPHA_CAP = 0.72
190
+
191
+ /**
192
+ * Terminal cell aspect (row height ÷ column width, ≈2.2 for common fonts).
193
+ * A ring computed in raw cell units looks vertically squashed; weighting row
194
+ * distance by the aspect makes the Pulse ring appear circular on screen.
195
+ */
196
+ const PULSE_ROW_ASPECT = 2.2
197
+
198
+ /** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
199
+ const SPARK_START_MS = 900
200
+ const SPARK_FRAME_MS = 100
201
+
202
+ /** Sparkle glyphs in frame order — Codex SPARK_GLYPHS (`· ✦ ✧`). */
203
+ export const SPARK_GLYPHS = ['·', '✦', '✧'] as const
204
+
205
+ /**
206
+ * Band tables Codex `bands(style, tier)`. Each entry is a triple whose
207
+ * meaning depends on the style: Wave/Pulse use `(launch, travel, strength)`;
208
+ * Aurora uses `(speed, phase, hueIndex)`.
209
+ */
210
+ export type DeepseekWaveBand = readonly [number, number, number]
211
+ export const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Record<DeepseekWaveTier, readonly DeepseekWaveBand[]>>>> = {
212
+ wave: {
213
+ // Wave-Max: one band sweeping 0.10s..0.85s.
214
+ flash: [[0.10, 0.75, 1.0]],
215
+ // Wave-Ultra: two offset bands for a richer crest.
216
+ deepseek: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
217
+ // Into the Unknown reuses the Ultra parameters verbatim.
218
+ unknown: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
219
+ },
220
+ aurora: {
221
+ // Aurora-Max: two drifting bands (hues 0 and 1).
222
+ flash: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0]],
223
+ // Aurora-Ultra: a third band adds hue 2.
224
+ deepseek: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
225
+ unknown: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
226
+ },
227
+ pulse: {
228
+ // Pulse-Max: one expanding ring.
229
+ flash: [[0.10, 0.60, 1.0]],
230
+ // Pulse-Ultra: two rings (inner weaker, outer stronger).
231
+ deepseek: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
232
+ unknown: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
233
+ },
234
+ }
235
+
236
+ /** Extra display time applied to every Codex ignition style. */
237
+ const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200
238
+
239
+ /** Original Codex duration used as the animation's sampling timeline. */
240
+ function deepseekWaveBaseDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
241
+ // The unknown tier reuses the deepseek (pro) durations exactly.
242
+ const pro = tier === 'deepseek' || tier === 'unknown'
243
+ switch (style) {
244
+ case 'aurora': return pro ? 1600 : 1300
245
+ case 'pulse': return pro ? 1250 : 900
246
+ case 'wave': return pro ? 1300 : 1000
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Total visible duration: the Codex ignition duration plus 200ms so its motion
252
+ * remains readable in a busy terminal.
253
+ * @param tier - the active wave tier.
254
+ * @param style - the active ignition style.
255
+ * @returns the duration in milliseconds.
256
+ */
257
+ export function deepseekWaveDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): number {
258
+ return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS
259
+ }
260
+
261
+ /** Map the extended display timeline back onto the original Codex samples. */
262
+ function deepseekWaveSampleElapsedMs(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
263
+ const base = deepseekWaveBaseDuration(tier, style)
264
+ return tick * DEEPSEEK_WAVE_TICK_MS * base / deepseekWaveDuration(tier, style)
265
+ }
266
+
267
+ /**
268
+ * Pick one ignition style at random, never repeating the previous one —
269
+ * Codex `IgnitionStyle::random`. Falls back to the remaining styles.
270
+ * @param previous - the style of the last trigger, if any.
271
+ * @returns a style different from `previous`.
272
+ */
273
+ export function deepseekWaveStyleRandom(previous: DeepseekWaveStyle | undefined): DeepseekWaveStyle {
274
+ const candidates = DEEPSEEK_WAVE_STYLES.filter(style => style !== previous)
275
+ return candidates[Math.floor(Math.random() * candidates.length)] ?? 'wave'
276
+ }
277
+
278
+ /**
279
+ * Tier for a `provider/model` label: a MODEL ID containing `flash` runs the
280
+ * single-band flash tier; everything else (pro/reasoner/chat) runs the
281
+ * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping. Only the model
282
+ * segment (after the `/`) is matched, so a provider whose name contains
283
+ * `flash` cannot flip an unrelated model onto the flash tier.
284
+ * @param model - the `provider/model` label of the applied model.
285
+ * @returns the wave tier for that model.
286
+ */
287
+ export function deepseekWaveTier(model: string): DeepseekWaveTier {
288
+ const slash = model.indexOf('/')
289
+ const id = slash < 0 ? model : model.slice(slash + 1)
290
+ return id.toLowerCase().includes('flash') ? 'flash' : 'deepseek'
291
+ }
292
+
293
+ /**
294
+ * Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
295
+ * one half-width away.
296
+ * @param distance - distance from the crest center in half-widths.
297
+ * @returns the crest strength in 0..1.
298
+ */
299
+ export function crest(distance: number): number {
300
+ if (distance >= 1) return 0
301
+ return 0.5 * (1 + Math.cos(Math.PI * distance))
302
+ }
303
+
304
+ /**
305
+ * Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
306
+ * the middle, so the crest accelerates and eases instead of sliding linearly.
307
+ * @param progress - raw progress (clamped to 0..1).
308
+ * @returns the eased progress in 0..1.
309
+ */
310
+ export function easeInOut(progress: number): number {
311
+ const p = Math.min(1, Math.max(0, progress))
312
+ if (p < 0.5) return 4 * p * p * p
313
+ const inverse = -2 * p + 2
314
+ return 1 - (inverse * inverse * inverse) / 2
315
+ }
316
+
317
+ /**
318
+ * Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
319
+ * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
320
+ * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
321
+ * an envelope); exported for the Aurora-style fades and for tests.
322
+ * @param elapsed - seconds since the animation started.
323
+ * @param total - total duration in seconds.
324
+ * @param fadeIn - seconds of fade-in.
325
+ * @param fadeOut - seconds of fade-out.
326
+ * @returns the envelope value in 0..1.
327
+ */
328
+ export function envelope(elapsed: number, total: number, fadeIn: number, fadeOut: number): number {
329
+ if (elapsed <= 0 || elapsed >= total) return 0
330
+ const rise = elapsed / Math.max(fadeIn, Number.EPSILON)
331
+ const fall = (total - elapsed) / Math.max(fadeOut, Number.EPSILON)
332
+ return Math.min(Math.max(Math.min(rise, fall), 0), 1)
333
+ }
334
+
335
+ /** Per-band sampling context: everything geometry needs beyond the timeline. */
336
+ interface BandContext {
337
+ /** Aspect-weighted row offset from the band's center row (Pulse 2-D ring). */
338
+ dy: number
339
+ /** Normalized vertical position: -1 = top row, +1 = bottom row, 0 = middle
340
+ * (or a single-row band, where the undulation cannot render). */
341
+ u: number
342
+ /** Whether the band has more than one row to undulate across. */
343
+ undulating: boolean
344
+ /** The pulse ring radius that covers the band's far corner. */
345
+ pulseSpan: number
346
+ /** Band ordinal: pulse rings and the wave harmonic key off it. */
347
+ bandIndex: number
348
+ /** The style's base duration in seconds (envelope timelines). */
349
+ total: number
350
+ }
351
+
352
+ /**
353
+ * One band's contributions at a column — Codex `band_sample`, redesigned:
354
+ * Wave is a WATER SURFACE — one continuous sine line, mirror-symmetric
355
+ * about the center column, crests flowing outward from the center (band 1
356
+ * of the deepseek tier is a faster harmonic line crossing it). Pulse
357
+ * detonates in TWO dimensions: soft rings that keep expanding through a
358
+ * symmetric fade envelope, color grading across each ring's width, with a
359
+ * trailing echo ripple. Aurora matches Codex verbatim.
360
+ * @param style - the ignition style.
361
+ * @param band - the band triple (meaning depends on the style).
362
+ * @param elapsed - seconds since the animation started.
363
+ * @param column - column index in the content row (0..width-1).
364
+ * @param width - content-row width in columns.
365
+ * @param context - band geometry (row position, undulation flag, pulse span).
366
+ * @returns one or two `[hueIndex, strength, core]` contributions.
367
+ */
368
+ function bandSample(
369
+ style: DeepseekWaveStyle,
370
+ band: DeepseekWaveBand,
371
+ elapsed: number,
372
+ column: number,
373
+ width: number,
374
+ context: BandContext,
375
+ ): [number, number, number][] {
376
+ const [first, second, third] = band
377
+ switch (style) {
378
+ case 'wave': {
379
+ // The water surface: one continuous sine line across the whole band,
380
+ // mirror-symmetric about the center column (distance d = |x − center|
381
+ // drives the phase), crests flowing OUTWARD from the center — one
382
+ // direction, never a return trip. The band table's (launch, travel)
383
+ // becomes the line's fade envelope: fade in at `launch`, full until
384
+ // `launch + travel`, then fade out to the end — symmetric in time.
385
+ const fadeIn = first
386
+ const fadeOut = Math.max(0.05, context.total - (first + second))
387
+ const fade = envelope(elapsed, context.total, fadeIn, fadeOut)
388
+ if (fade <= 0.01) return [[0, 0, 0]]
389
+ // Band 0 is the fundamental swell; band 1 (deepseek tier) is a faster,
390
+ // finer harmonic crossing the fundamental's crests — two continuous
391
+ // symmetric lines interleaving instead of one shuttling back and forth.
392
+ const harmonic = context.bandIndex % 2 === 1
393
+ const wavelength = harmonic ? WAVE_SURFACE_WAVELENGTH / 1.5 : WAVE_SURFACE_WAVELENGTH
394
+ const omega = (harmonic ? 1.5 : 1) * WAVE_SURFACE_OMEGA
395
+ const amplitude = (harmonic ? WAVE_SURFACE_HARMONIC : 1) * WAVE_SURFACE_AMPLITUDE
396
+ const d = Math.abs(column - (width - 1) / 2)
397
+ const surface = amplitude * Math.sin(Math.PI * 2 * d / wavelength - omega * elapsed + (harmonic ? Math.PI / 2 : 0))
398
+ const fromSurface = Math.abs(context.u - surface)
399
+ const vertical = context.undulating ? crest(fromSurface / WAVE_SURFACE_THICKNESS) : 1
400
+ // Aurora-style hue mixing: the MIRRORED profile below the surface
401
+ // carries a second blue (the harmonic band carries the third), so the
402
+ // color distribution flows continuously with the wave instead of one
403
+ // flat tint broken by a hard core line.
404
+ const mirrorHue = harmonic ? 1 : 2
405
+ const below = context.undulating ? crest(Math.abs(context.u + surface) / WAVE_SURFACE_THICKNESS) : vertical
406
+ return [
407
+ [0, fade * vertical, 0],
408
+ [mirrorHue, fade * below * WAVE_SURFACE_MIRROR, 0],
409
+ ]
410
+ }
411
+ case 'aurora': {
412
+ const center = (0.5 + 0.38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width
413
+ const halfWidth = Math.max(width * 0.22, 4)
414
+ return [[Math.trunc(third), crest(Math.abs(column - center) / halfWidth), 0]]
415
+ }
416
+ case 'pulse': {
417
+ // Symmetric lifetime: the band table's (launch, travel) becomes the
418
+ // fade envelope (fade in at launch, full until launch+travel, then
419
+ // fade to the end) while the radius keeps expanding THROUGH the fade —
420
+ // a shockwave that never hits a wall, it dissolves mid-flight.
421
+ const launch = first
422
+ const travel = second
423
+ const fadeOut = Math.max(0.05, context.total - (launch + travel))
424
+ const fade = envelope(elapsed, context.total, launch, fadeOut)
425
+ if (fade <= 0.01) return [[0, 0, 0]]
426
+ const progress = (elapsed - launch) / travel
427
+ const radius = (1 - (1 - progress) ** 3) * context.pulseSpan
428
+ const decay = third * (1 - 0.35 * Math.min(Math.max(progress, 0), 1))
429
+ const distance = Math.hypot(column - width / 2, context.dy)
430
+ const fromRing = Math.abs(distance - radius)
431
+ // Wide soft band with a moderate peak riding the radius — all in the
432
+ // hue blend, no hard line — and an inner edge one hue over at a
433
+ // slightly smaller radius: the ring's color grades across its width.
434
+ const band = crest(fromRing / PULSE_HALF_WIDTH) + PULSE_PEAK_GAIN * crest(fromRing / PULSE_PEAK_HALF_WIDTH)
435
+ const inner = crest(Math.abs(distance - (radius - PULSE_INNER_OFFSET)) / PULSE_HALF_WIDTH)
436
+ // Trailing echo ripple in the next blue, fading in after the primary
437
+ // so the center hole opens first.
438
+ const echoGate = envelope(elapsed, context.total, launch + PULSE_ECHO_DELAY, fadeOut)
439
+ const echo = crest(Math.abs(distance - radius * PULSE_ECHO_RADIUS) / PULSE_HALF_WIDTH)
440
+ * PULSE_ECHO_STRENGTH * echoGate
441
+ return [
442
+ [context.bandIndex, fade * band * decay, 0],
443
+ [PULSE_INNER_HUE, fade * inner * decay * PULSE_INNER_STRENGTH, 0],
444
+ [context.bandIndex + 1, fade * echo * decay, 0],
445
+ ]
446
+ }
447
+ }
448
+ }
449
+
450
+ /** Linear RGB blend Codex `blend`: `fg * alpha + bg * (1 - alpha)`. */
451
+ function blendRgb(fg: RgbTriple, bg: RgbTriple, alpha: number): RgbTriple {
452
+ return [
453
+ Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
454
+ Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
455
+ Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
456
+ ]
457
+ }
458
+
459
+ /**
460
+ * Aurora-only per-row phase share of the duration: its drifting bands reach
461
+ * the top row first and the bottom row last, sweeping down the band. 0.12
462
+ * keeps the bottom row's lag inside the 200ms duration extension. Wave and
463
+ * Pulse deliberately share one timeline (see `deepseekWaveColumnBg`).
464
+ */
465
+ const DEEPSEEK_WAVE_ROW_PHASE = 0.12
466
+
467
+ /**
468
+ * The background color for one composer-band column at a tick — Codex
469
+ * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
470
+ * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
471
+ * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
472
+ * blends the mixed hue toward the blank-cell base at the style's alpha cap,
473
+ * and Aurora applies its own fade envelope. Returns `null` when the column
474
+ * should stay transparent, so the row returns to no `backgroundColor` on
475
+ * both ends. With `rows > 1`: Wave is a water surface — every column
476
+ * lights the row nearest the surface's current height, so the light reads
477
+ * as ONE continuous wavy line spanning the band, symmetric about the center
478
+ * column and flowing outward (a single-row band falls back to a flat glow);
479
+ * Pulse rings in two dimensions around the band's center cell with trailing
480
+ * echo ripples; only Aurora samples the timeline shifted by a per-row phase
481
+ * offset.
482
+ * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
483
+ * @param column - column index in the content row (0..width-1).
484
+ * @param width - content-row width in columns.
485
+ * @param tier - the wave tier (flash = Max, deepseek = Ultra parameters).
486
+ * @param style - the ignition style.
487
+ * @param hues - the tier's three hues.
488
+ * @param base - the blank-cell base color the tint blends toward.
489
+ * @param row - row index in the band (0..rows-1; default 0 = old single-row).
490
+ * @param rows - band height in rows (default 1).
491
+ * @returns the blended RGB background, or null for transparent.
492
+ */
493
+ export function deepseekWaveColumnBg(
494
+ tick: number,
495
+ column: number,
496
+ width: number,
497
+ tier: DeepseekWaveTier,
498
+ style: DeepseekWaveStyle,
499
+ hues: readonly [RgbTriple, RgbTriple, RgbTriple],
500
+ base: RgbTriple,
501
+ row = 0,
502
+ rows = 1,
503
+ ): RgbTriple | null {
504
+ const total = deepseekWaveBaseDuration(tier, style) / 1000
505
+ // Only Aurora keeps the per-row phase cascade (its drifting bands stagger
506
+ // nicely). Wave and Pulse sample ONE shared timeline — Codex paints each
507
+ // column across the whole band height (`tint_column`), and the cascade we
508
+ // added on top of the port tore Pulse into misaligned per-row rings and
509
+ // smeared Wave's crest into a diagonal.
510
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
511
+ - (style === 'aurora' ? (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE : 0)
512
+ const dy = style === 'pulse' ? (row - (rows - 1) / 2) * PULSE_ROW_ASPECT : 0
513
+ const undulating = rows > 1
514
+ const u = undulating ? (row - (rows - 1) / 2) / ((rows - 1) / 2) : 0
515
+ // The span is EXACTLY the band's far corner: the ring front reaches the
516
+ // edges as the fade envelope closes, so the detonation stays visible for
517
+ // its whole lifetime instead of rushing off the band mid-flight (the old
518
+ // +2·half-width margin made the ring's second half invisible).
519
+ const pulseSpan = Math.hypot(width / 2, ((rows - 1) / 2) * PULSE_ROW_ASPECT)
520
+ const fade = style === 'aurora' ? envelope(elapsed, total, 0.25, 0.40) : 1
521
+ const weights = [0, 0, 0]
522
+ let bandIndex = 0
523
+ for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
524
+ for (const [hue, strength] of bandSample(style, band, elapsed, column, width, { dy, u, undulating, pulseSpan, bandIndex, total })) {
525
+ weights[hue] = style === 'aurora' ? weights[hue]! + strength : Math.max(weights[hue]!, strength)
526
+ }
527
+ bandIndex += 1
528
+ }
529
+ const weight = weights[0]! + weights[1]! + weights[2]!
530
+ if (weight <= 0.01) return null
531
+ let red = 0
532
+ let green = 0
533
+ let blue = 0
534
+ for (let index = 0; index < 3; index += 1) {
535
+ red += weights[index]! * hues[index]![0]
536
+ green += weights[index]! * hues[index]![1]
537
+ blue += weights[index]! * hues[index]![2]
538
+ }
539
+ const mixed: RgbTriple = [
540
+ Math.round(red / weight),
541
+ Math.round(green / weight),
542
+ Math.round(blue / weight),
543
+ ]
544
+ // Alpha caps, all Aurora-grade now: Aurora ≤0.50, the swell ≤0.68, the
545
+ // detonation a notch above at ≤0.72 — soft gradients everywhere, no hard
546
+ // lines, the color work done by hue mixing instead of brightness spikes.
547
+ const alpha = style === 'aurora'
548
+ ? Math.min(weight * 0.40, 0.50) * fade
549
+ : style === 'wave'
550
+ ? Math.min(weight * WAVE_SURFACE_ALPHA_GAIN, WAVE_SURFACE_ALPHA_CAP)
551
+ : Math.min(weight * PULSE_ALPHA_GAIN, PULSE_ALPHA_CAP)
552
+ if (alpha < 0.02) return null
553
+ return blendRgb(mixed, base, alpha)
554
+ }
555
+
556
+ /**
557
+ * The sparkle glyph for a tick — Codex `spark_frame`, sampled on the same
558
+ * proportionally slowed DeepSeek Wave timeline as the composer background.
559
+ * The Ink layer still must skip occupied cells.
560
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
561
+ * @returns the sparkle glyph, or null outside the stretched tail window.
562
+ */
563
+ export function deepseekWaveSpark(tick: number): string | null {
564
+ const elapsed = deepseekWaveSampleElapsedMs(tick, 'deepseek', 'wave')
565
+ if (elapsed < SPARK_START_MS) return null
566
+ const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS)
567
+ return SPARK_GLYPHS[frame] ?? null
568
+ }
569
+
570
+ /**
571
+ * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
572
+ * shortly after the first crest launches and out before the wave settles,
573
+ * so the brand name surfaces through the sweep's middle. The Ink layer
574
+ * places it in the row's blank mid-section (never over real draft text).
575
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
576
+ * @param tier - the wave tier.
577
+ * @returns true while the wordmark should be visible.
578
+ */
579
+ export function deepseekWaveWordVisible(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): boolean {
580
+ const total = deepseekWaveBaseDuration(tier, style) / 1000
581
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
582
+ return envelope(elapsed, total, total * 0.2, total * 0.35) > 0.25
583
+ }
584
+
585
+ /**
586
+ * The per-character color for the `deepseek` wordmark: the tier's hues
587
+ * cycled per character (d→hue0, e→hue1, e→hue2, …), a brand-gradient text.
588
+ * @param index - character index in the wordmark.
589
+ * @param hues - the tier's three hues.
590
+ * @returns the hue for that character.
591
+ */
592
+ export function deepseekWaveWordHue(index: number, hues: readonly [RgbTriple, RgbTriple, RgbTriple]): RgbTriple {
593
+ return hues[index % hues.length]!
594
+ }
595
+
596
+ /**
597
+ * True when a `provider/model` status label addresses the official DeepSeek
598
+ * route: either segment contains `deepseek` (case-insensitive), covering the
599
+ * `deepseek-official` provider route and its `deepseek-*` model ids.
600
+ * @param label - the status bar model label (`provider/model`).
601
+ * @returns whether the label names an official DeepSeek model.
602
+ */
603
+ export function isOfficialDeepSeekLabel(label: string): boolean {
604
+ const slash = label.indexOf('/')
605
+ const provider = slash < 0 ? label : label.slice(0, slash)
606
+ const model = slash < 0 ? '' : label.slice(slash + 1)
607
+ return provider.toLowerCase().includes('deepseek') || model.toLowerCase().includes('deepseek')
608
+ }
609
+
610
+ /**
611
+ * Known reasoning-effort ranks in ascending order. Effort ids are opaque
612
+ * adapter-owned strings, so the rank table covers the conventional names
613
+ * (off → low → medium → high → xhigh → max/ultra); an unrecognized id
614
+ * ranks as unknown (0), which never triggers the high-effort wave.
615
+ */
616
+ const EFFORT_RANK: Readonly<Record<string, number>> = {
617
+ off: 0,
618
+ none: 0,
619
+ low: 1,
620
+ medium: 2,
621
+ med: 2,
622
+ high: 3,
623
+ xhigh: 4,
624
+ 'x-high': 4,
625
+ 'very-high': 4,
626
+ max: 5,
627
+ maximum: 5,
628
+ ultra: 5,
629
+ }
630
+
631
+ /**
632
+ * True when an effective reasoning effort is STRICTLY above `high` — the
633
+ * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
634
+ * Absent efforts and unrecognized ids never qualify.
635
+ * @param effort - the effective reasoning-effort id ('' or undefined when none).
636
+ * @returns whether the effort ranks above high.
637
+ */
638
+ export function effortAboveHigh(effort: string | undefined): boolean {
639
+ if (effort === undefined || effort === '') return false
640
+ const rank = EFFORT_RANK[effort.trim().toLowerCase()]
641
+ return rank !== undefined && rank > 3
642
+ }
643
+
644
+ /**
645
+ * Parse a persisted animations preference (`animations.json`): timed
646
+ * animations are on by default and only an explicit `false` disables them —
647
+ * a missing key, corrupt value, or absent file all mean enabled, so the
648
+ * /animation toggle degrades exactly like every other user preference.
649
+ * @param value - the raw parsed JSON value (expected boolean).
650
+ * @returns whether timed animations should run.
651
+ */
652
+ export function parseAnimationsPref(value: unknown): boolean {
653
+ return value !== false
654
+ }
655
+
656
+ /**
657
+ * One parsed `/animation` argument: '' toggles, `on|true|1` enables,
658
+ * `off|false|0` disables (case-insensitive, surrounding whitespace ignored),
659
+ * and anything else is a usage error the caller surfaces. Kept pure so the
660
+ * command's entire decision table is unit-testable.
661
+ * @param argument - the raw text after `/animation`.
662
+ * @returns `{ enabled }`, `'toggle'`, or `'usage'`.
663
+ */
664
+ export function parseAnimationsArgument(argument: string): { enabled: boolean } | 'toggle' | 'usage' {
665
+ const normalized = argument.trim().toLowerCase()
666
+ if (normalized === '') return 'toggle'
667
+ if (normalized === 'on' || normalized === 'true' || normalized === '1') return { enabled: true }
668
+ if (normalized === 'off' || normalized === 'false' || normalized === '0') return { enabled: false }
669
+ return 'usage'
670
+ }