dsh-code 0.9.1 → 1.0.1

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 (67) hide show
  1. package/README.en.md +278 -249
  2. package/README.md +131 -102
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +36 -1
  5. package/lib/index.mjs +3055 -819
  6. package/lib/startup.mjs +21 -11
  7. package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
  8. package/lib/types/app.d.ts +84 -16
  9. package/lib/types/attachments.d.ts +20 -0
  10. package/lib/types/authorization-panel.d.ts +22 -0
  11. package/lib/types/authorization.d.ts +36 -0
  12. package/lib/types/editor.d.ts +6 -0
  13. package/lib/types/fork.d.ts +8 -0
  14. package/lib/types/git-workflow.d.ts +23 -0
  15. package/lib/types/index.d.ts +6 -0
  16. package/lib/types/kernel-panels.d.ts +39 -0
  17. package/lib/types/keyboard.d.ts +41 -0
  18. package/lib/types/mentions.d.ts +30 -38
  19. package/lib/types/models.d.ts +3 -1
  20. package/lib/types/permissions.d.ts +4 -14
  21. package/lib/types/presets.d.ts +5 -20
  22. package/lib/types/provider-settings.d.ts +16 -0
  23. package/lib/types/render/animations.d.ts +10 -39
  24. package/lib/types/render/editor.d.ts +137 -0
  25. package/lib/types/render/export.d.ts +1 -1
  26. package/lib/types/render/lines.d.ts +6 -2
  27. package/lib/types/render/markdown.d.ts +3 -1
  28. package/lib/types/render/projection.d.ts +29 -3
  29. package/lib/types/render/status.d.ts +6 -13
  30. package/lib/types/session-directory.d.ts +1 -3
  31. package/lib/types/startup.d.ts +14 -11
  32. package/lib/types/store.d.ts +11 -9
  33. package/lib/types/subagents.d.ts +3 -3
  34. package/lib/types/theme.d.ts +14 -1
  35. package/lib/types/version.d.ts +15 -2
  36. package/package.json +159 -141
  37. package/src/app.ts +1490 -663
  38. package/src/attachments.ts +128 -0
  39. package/src/authorization-panel.ts +285 -0
  40. package/src/authorization.ts +147 -0
  41. package/src/editor.ts +51 -0
  42. package/src/fork.ts +31 -0
  43. package/src/git-workflow.ts +87 -0
  44. package/src/index.ts +1523 -1374
  45. package/src/internals.ts +14 -1
  46. package/src/kernel-panels.ts +914 -798
  47. package/src/keyboard.ts +126 -0
  48. package/src/mentions.ts +78 -117
  49. package/src/models.ts +20 -14
  50. package/src/permissions.ts +5 -13
  51. package/src/presets.ts +6 -22
  52. package/src/provider-settings.ts +95 -1
  53. package/src/render/animations.ts +420 -450
  54. package/src/render/editor.ts +398 -0
  55. package/src/render/export.ts +79 -79
  56. package/src/render/lines.ts +342 -236
  57. package/src/render/markdown.ts +99 -26
  58. package/src/render/projection.ts +106 -19
  59. package/src/render/status.ts +713 -650
  60. package/src/render/text.ts +150 -150
  61. package/src/render/tool-detail.ts +3 -1
  62. package/src/session-directory.ts +4 -4
  63. package/src/startup.ts +136 -119
  64. package/src/store.ts +23 -11
  65. package/src/subagents.ts +13 -5
  66. package/src/theme.ts +214 -206
  67. package/src/version.ts +58 -1
@@ -1,450 +1,420 @@
1
- /**
2
- * Terminal animation frame tables derived from the web design language:
3
- * the StateDot "ongoing" pixel chase (3×3 ring, 125ms flat-hold brightness
4
- * steps, 1s cycle) becomes the single-cell stepped pulse below and the
5
- * full-ring clockwise braille chase in {@link BUSY_CHASE_FRAMES}, and the
6
- * streaming caret blink is the Claude-Code convention.
7
- *
8
- * The DeepSeek model-switch easter egg ports Codex's effort-ignition "Wave"
9
- * style (`codex-rs/tui/src/bottom_pane/effort_ignition_styles.rs`): switching
10
- * INTO an official DeepSeek route sweeps a blue wave across the composer's
11
- * input row — one column per cell, `backgroundColor` = the sampled wave
12
- * color — then, on the deepseek (Ultra-equivalent) tier, drops the `· ✦ ✧`
13
- * sparkle sequence into the rightmost blank cell before fading. The prompt
14
- * marker keeps the tier accent afterwards (persistent, like Codex's prompt
15
- * charge). Pure functions only — the Ink layer owns timers and colors.
16
- *
17
- * @module @deepseek-ai/dsh-code/render/animations
18
- */
19
-
20
- import type { RgbTriple } from '../theme.ts'
21
-
22
- /** Single-cell stepped pulse: flat holds mirroring the web's 125ms keyframes. */
23
- export const PULSE_FRAMES = ['█', '█', '▆', '▃', '▁', '▃', '▆', '█'] as const
24
-
25
- /** Pulse frame for a monotonic tick. */
26
- export function pulseFrame(tick: number): string {
27
- return PULSE_FRAMES[tick % PULSE_FRAMES.length] ?? PULSE_FRAMES[0]
28
- }
29
-
30
- /**
31
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
32
- * ring trail clockwise around the eight outer positions, one braille glyph
33
- * per step — 8 frames × 125ms = the web's 1s cycle.
34
- */
35
- export const BUSY_CHASE_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] as const
36
-
37
- /** Chase frame for a monotonic tick (the busy composer/Deep-diving marker). */
38
- export function busyChaseFrame(tick: number): string {
39
- return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0]
40
- }
41
-
42
- /** Caret visibility: half the ticks on, half off (530ms blink). */
43
- export function caretVisible(tick: number): boolean {
44
- return tick % 2 === 0
45
- }
46
-
47
- /**
48
- * The one-shot DeepSeek model-switch easter egg: when the status bar model
49
- * label switches to an official DeepSeek route, the composer's input row
50
- * plays Codex's effort-ignition "Wave" a blue crest sweeping the content
51
- * row column by column (background tint ≤ 0.55 under the draft), plus the
52
- * Ultra-style `· ✦ ✧` sparkles on the deepseek tier — and the prompt marker
53
- * keeps the tier accent afterwards. The Ink layer owns the timer and reads
54
- * the ACTIVE palette anchors (`getPalette`); everything below is pure
55
- * interpolation over the colors it is given.
56
- */
57
-
58
- /** Frame cadence of the DeepSeek wave: Codex's IGNITION_FRAME_TICK (33ms ≈ 30fps). */
59
- export const DEEPSEEK_WAVE_TICK_MS = 33
60
-
61
- /**
62
- * The DeepSeek wave tiers. The concept maps Codex's reasoning tiers to
63
- * model ids: `flash` runs the Max parameters, `deepseek` (pro models) runs
64
- * the Ultra parameters (dual band + tail sparkles on the Wave style).
65
- * `unknown` is the "Into the Unknown" variant: it reuses the deepseek tier's
66
- * exact parameters (dual band, durations, sparkles) for NON-DeepSeek models
67
- * running a reasoning effort above high the wordmark renders differently
68
- * but the motion is identical.
69
- */
70
- export type DeepseekWaveTier = 'flash' | 'deepseek' | 'unknown'
71
-
72
- /**
73
- * The three ignition styles — Codex `IgnitionStyle`: a traveling crest
74
- * (Wave), a drifting multi-hue band (Aurora), and an expanding ring (Pulse).
75
- * One style is picked at random per trigger and never repeats the previous.
76
- */
77
- export type DeepseekWaveStyle = 'wave' | 'aurora' | 'pulse'
78
-
79
- /** All styles in canonical order, for random selection and tests. */
80
- export const DEEPSEEK_WAVE_STYLES: readonly DeepseekWaveStyle[] = ['wave', 'aurora', 'pulse']
81
-
82
- /** Wave half-width in columns — Codex WAVE_HALF_WIDTH (9). */
83
- export const WAVE_HALF_WIDTH = 9
84
-
85
- /** Pulse ring half-width in columns Codex PULSE_HALF_WIDTH (4.5). */
86
- export const PULSE_HALF_WIDTH = 4.5
87
-
88
- /** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
89
- export const SPARK_START_MS = 900
90
- export const SPARK_FRAME_MS = 100
91
-
92
- /** Sparkle glyphs in frame order Codex SPARK_GLYPHS (`· ✦ ✧`). */
93
- export const SPARK_GLYPHS = ['·', '✦', '✧'] as const
94
-
95
- /**
96
- * Band tables — Codex `bands(style, tier)`. Each entry is a triple whose
97
- * meaning depends on the style: Wave/Pulse use `(launch, travel, strength)`;
98
- * Aurora uses `(speed, phase, hueIndex)`.
99
- */
100
- export type DeepseekWaveBand = readonly [number, number, number]
101
- export const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Record<DeepseekWaveTier, readonly DeepseekWaveBand[]>>>> = {
102
- wave: {
103
- // Wave-Max: one band sweeping 0.10s..0.85s.
104
- flash: [[0.10, 0.75, 1.0]],
105
- // Wave-Ultra: two offset bands for a richer crest.
106
- deepseek: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
107
- // Into the Unknown reuses the Ultra parameters verbatim.
108
- unknown: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
109
- },
110
- aurora: {
111
- // Aurora-Max: two drifting bands (hues 0 and 1).
112
- flash: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0]],
113
- // Aurora-Ultra: a third band adds hue 2.
114
- deepseek: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
115
- unknown: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
116
- },
117
- pulse: {
118
- // Pulse-Max: one expanding ring.
119
- flash: [[0.10, 0.60, 1.0]],
120
- // Pulse-Ultra: two rings (inner weaker, outer stronger).
121
- deepseek: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
122
- unknown: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
123
- },
124
- }
125
-
126
- /** Extra display time applied to every Codex ignition style. */
127
- export const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200
128
-
129
- /** Original Codex duration used as the animation's sampling timeline. */
130
- function deepseekWaveBaseDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
131
- // The unknown tier reuses the deepseek (pro) durations exactly.
132
- const pro = tier === 'deepseek' || tier === 'unknown'
133
- switch (style) {
134
- case 'aurora': return pro ? 1600 : 1300
135
- case 'pulse': return pro ? 1250 : 900
136
- case 'wave': return pro ? 1300 : 1000
137
- }
138
- }
139
-
140
- /**
141
- * Total visible duration: the Codex ignition duration plus 200ms so its motion
142
- * remains readable in a busy terminal.
143
- * @param tier - the active wave tier.
144
- * @param style - the active ignition style.
145
- * @returns the duration in milliseconds.
146
- */
147
- export function deepseekWaveDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): number {
148
- return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS
149
- }
150
-
151
- /** Map the extended display timeline back onto the original Codex samples. */
152
- function deepseekWaveSampleElapsedMs(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
153
- const base = deepseekWaveBaseDuration(tier, style)
154
- return tick * DEEPSEEK_WAVE_TICK_MS * base / deepseekWaveDuration(tier, style)
155
- }
156
-
157
- /**
158
- * Pick one ignition style at random, never repeating the previous one —
159
- * Codex `IgnitionStyle::random`. Falls back to the remaining styles.
160
- * @param previous - the style of the last trigger, if any.
161
- * @returns a style different from `previous`.
162
- */
163
- export function deepseekWaveStyleRandom(previous: DeepseekWaveStyle | undefined): DeepseekWaveStyle {
164
- const candidates = DEEPSEEK_WAVE_STYLES.filter(style => style !== previous)
165
- return candidates[Math.floor(Math.random() * candidates.length)] ?? 'wave'
166
- }
167
-
168
- /**
169
- * Blank-cell background the wave tint blends toward — fixed approximations
170
- * of the terminal's default background, mirroring Codex's
171
- * `user_message_bg_rgb` (which derives a near-black / near-white bubble tint
172
- * from the terminal background). The Ink layer picks the active theme's one.
173
- */
174
- export const WAVE_BASE_DARK: RgbTriple = [16, 18, 24]
175
- export const WAVE_BASE_LIGHT: RgbTriple = [242, 244, 248]
176
-
177
- /**
178
- * Tier for a `provider/model` label: a model id containing `flash` runs the
179
- * single-band flash tier; everything else (pro/reasoner/chat) runs the
180
- * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
181
- * @param model - the `provider/model` label of the applied model.
182
- * @returns the wave tier for that model.
183
- */
184
- export function deepseekWaveTier(model: string): DeepseekWaveTier {
185
- return model.toLowerCase().includes('flash') ? 'flash' : 'deepseek'
186
- }
187
-
188
- /**
189
- * Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
190
- * one half-width away.
191
- * @param distance - distance from the crest center in half-widths.
192
- * @returns the crest strength in 0..1.
193
- */
194
- export function crest(distance: number): number {
195
- if (distance >= 1) return 0
196
- return 0.5 * (1 + Math.cos(Math.PI * distance))
197
- }
198
-
199
- /**
200
- * Cubic ease-in-out Codex `ease_in_out`: flat at both ends, steepest in
201
- * the middle, so the crest accelerates and eases instead of sliding linearly.
202
- * @param progress - raw progress (clamped to 0..1).
203
- * @returns the eased progress in 0..1.
204
- */
205
- export function easeInOut(progress: number): number {
206
- const p = Math.min(1, Math.max(0, progress))
207
- if (p < 0.5) return 4 * p * p * p
208
- const inverse = -2 * p + 2
209
- return 1 - (inverse * inverse * inverse) / 2
210
- }
211
-
212
- /**
213
- * Fade-in/fade-out envelope — Codex `envelope`: linear ramp over `fadeIn`
214
- * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
215
- * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
216
- * an envelope); exported for the Aurora-style fades and for tests.
217
- * @param elapsed - seconds since the animation started.
218
- * @param total - total duration in seconds.
219
- * @param fadeIn - seconds of fade-in.
220
- * @param fadeOut - seconds of fade-out.
221
- * @returns the envelope value in 0..1.
222
- */
223
- export function envelope(elapsed: number, total: number, fadeIn: number, fadeOut: number): number {
224
- if (elapsed <= 0 || elapsed >= total) return 0
225
- const rise = elapsed / Math.max(fadeIn, Number.EPSILON)
226
- const fall = (total - elapsed) / Math.max(fadeOut, Number.EPSILON)
227
- return Math.min(Math.max(Math.min(rise, fall), 0), 1)
228
- }
229
-
230
- /**
231
- * One band's contribution at a column — Codex `band_sample`, all three
232
- * branches: Wave sweeps an eased crest across the row; Aurora drifts a
233
- * sinusoidal center carrying a hue index; Pulse expands a ring from the row
234
- * center with cubic ease and decaying strength.
235
- * @param style - the ignition style.
236
- * @param band - the band triple (meaning depends on the style).
237
- * @param elapsed - seconds since the animation started.
238
- * @param column - column index in the content row (0..width-1).
239
- * @param width - content-row width in columns.
240
- * @returns `[hueIndex, strength]`.
241
- */
242
- function bandSample(
243
- style: DeepseekWaveStyle,
244
- band: DeepseekWaveBand,
245
- elapsed: number,
246
- column: number,
247
- width: number,
248
- ): [number, number] {
249
- const [first, second, third] = band
250
- switch (style) {
251
- case 'wave': {
252
- const progress = (elapsed - first) / second
253
- if (progress < 0 || progress > 1) return [0, 0]
254
- const center = easeInOut(progress) * (width + 2 * WAVE_HALF_WIDTH) - WAVE_HALF_WIDTH
255
- return [0, crest(Math.abs(column - center) / WAVE_HALF_WIDTH)]
256
- }
257
- case 'aurora': {
258
- const center = (0.5 + 0.38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width
259
- const halfWidth = Math.max(width * 0.22, 4)
260
- return [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)]
261
- }
262
- case 'pulse': {
263
- const progress = (elapsed - first) / second
264
- if (progress < 0 || progress > 1) return [0, 0]
265
- const inverse = 1 - progress
266
- const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH)
267
- const distance = Math.abs(column - width / 2)
268
- return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - 0.6 * progress)]
269
- }
270
- }
271
- }
272
-
273
- /** Linear RGB blend Codex `blend`: `fg * alpha + bg * (1 - alpha)`. */
274
- function blendRgb(fg: RgbTriple, bg: RgbTriple, alpha: number): RgbTriple {
275
- return [
276
- Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
277
- Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
278
- Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
279
- ]
280
- }
281
-
282
- /**
283
- * The background color for one composer-row column at a tick — Codex
284
- * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
285
- * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
286
- * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
287
- * blends the mixed hue toward the blank-cell base at the style's alpha cap,
288
- * and Aurora applies its own fade envelope. Returns `null` when the column
289
- * should stay transparent, so the row returns to no `backgroundColor` on
290
- * both ends.
291
- * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
292
- * @param column - column index in the content row (0..width-1).
293
- * @param width - content-row width in columns.
294
- * @param tier - the wave tier (flash = Max, deepseek = Ultra parameters).
295
- * @param style - the ignition style.
296
- * @param hues - the tier's three hues.
297
- * @param base - the blank-cell base color the tint blends toward.
298
- * @returns the blended RGB background, or null for transparent.
299
- */
300
- export function deepseekWaveColumnBg(
301
- tick: number,
302
- column: number,
303
- width: number,
304
- tier: DeepseekWaveTier,
305
- style: DeepseekWaveStyle,
306
- hues: readonly [RgbTriple, RgbTriple, RgbTriple],
307
- base: RgbTriple,
308
- ): RgbTriple | null {
309
- const total = deepseekWaveBaseDuration(tier, style) / 1000
310
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
311
- const fade = style === 'aurora' ? envelope(elapsed, total, 0.25, 0.40) : 1
312
- const weights = [0, 0, 0]
313
- for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
314
- const [hue, strength] = bandSample(style, band, elapsed, column, width)
315
- weights[hue] = style === 'aurora' ? weights[hue]! + strength : Math.max(weights[hue]!, strength)
316
- }
317
- const weight = weights[0]! + weights[1]! + weights[2]!
318
- if (weight <= 0.01) return null
319
- let red = 0
320
- let green = 0
321
- let blue = 0
322
- for (let index = 0; index < 3; index += 1) {
323
- red += weights[index]! * hues[index]![0]
324
- green += weights[index]! * hues[index]![1]
325
- blue += weights[index]! * hues[index]![2]
326
- }
327
- const mixed: RgbTriple = [
328
- Math.round(red / weight),
329
- Math.round(green / weight),
330
- Math.round(blue / weight),
331
- ]
332
- const alpha = style === 'aurora' ? Math.min(weight * 0.40, 0.50) * fade : weight * 0.55
333
- if (alpha < 0.02) return null
334
- return blendRgb(mixed, base, alpha)
335
- }
336
-
337
- /**
338
- * The sparkle glyph for a tick Codex `spark_frame`, sampled on the same
339
- * proportionally slowed DeepSeek Wave timeline as the composer background.
340
- * The Ink layer still must skip occupied cells.
341
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
342
- * @returns the sparkle glyph, or null outside the stretched tail window.
343
- */
344
- export function deepseekWaveSpark(tick: number): string | null {
345
- const elapsed = deepseekWaveSampleElapsedMs(tick, 'deepseek', 'wave')
346
- if (elapsed < SPARK_START_MS) return null
347
- const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS)
348
- return SPARK_GLYPHS[frame] ?? null
349
- }
350
-
351
- /**
352
- * The composer BORDER color at a tick: the frame breathes with the wave —
353
- * the palette's static dim blends toward the tier accent as the crest is
354
- * alive and back, so the frame glows up while the wave sweeps and settles to
355
- * dim on both ends (first==last frame==dim, no hard jump).
356
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
357
- * @param tier - the wave tier.
358
- * @param hues - the tier's three hues; the border blends toward hues[0].
359
- * @param dim - the palette's static dim RGB (the resting border color).
360
- * @returns the blended border RGB.
361
- */
362
- export function deepseekWaveBorderColor(
363
- tick: number,
364
- tier: DeepseekWaveTier,
365
- style: DeepseekWaveStyle,
366
- hues: readonly [RgbTriple, RgbTriple, RgbTriple],
367
- dim: RgbTriple,
368
- ): RgbTriple {
369
- const total = deepseekWaveBaseDuration(tier, style) / 1000
370
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
371
- // The border stays visibly on the tier accent for the whole sweep (a
372
- // floor keeps it glowing, not just cresting mid-wave): it ramps in as the
373
- // first band launches and relaxes after the last crest passes.
374
- const glow = envelope(elapsed, total, total * 0.25, total * 0.4)
375
- return blendRgb(hues[0], dim, 0.25 + glow * 0.6)
376
- }
377
-
378
- /**
379
- * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
380
- * shortly after the first crest launches and out before the wave settles,
381
- * so the brand name surfaces through the sweep's middle. The Ink layer
382
- * places it in the row's blank mid-section (never over real draft text).
383
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
384
- * @param tier - the wave tier.
385
- * @returns true while the wordmark should be visible.
386
- */
387
- export function deepseekWaveWordVisible(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): boolean {
388
- const total = deepseekWaveBaseDuration(tier, style) / 1000
389
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
390
- return envelope(elapsed, total, total * 0.2, total * 0.35) > 0.25
391
- }
392
-
393
- /**
394
- * The per-character color for the `deepseek` wordmark: the tier's hues
395
- * cycled per character (d→hue0, e→hue1, e→hue2, …), a brand-gradient text.
396
- * @param index - character index in the wordmark.
397
- * @param hues - the tier's three hues.
398
- * @returns the hue for that character.
399
- */
400
- export function deepseekWaveWordHue(index: number, hues: readonly [RgbTriple, RgbTriple, RgbTriple]): RgbTriple {
401
- return hues[index % hues.length]!
402
- }
403
-
404
- /**
405
- * True when a `provider/model` status label addresses the official DeepSeek
406
- * route: either segment contains `deepseek` (case-insensitive), covering the
407
- * `deepseek-official` provider route and its `deepseek-*` model ids.
408
- * @param label - the status bar model label (`provider/model`).
409
- * @returns whether the label names an official DeepSeek model.
410
- */
411
- export function isOfficialDeepSeekLabel(label: string): boolean {
412
- const slash = label.indexOf('/')
413
- const provider = slash < 0 ? label : label.slice(0, slash)
414
- const model = slash < 0 ? '' : label.slice(slash + 1)
415
- return provider.toLowerCase().includes('deepseek') || model.toLowerCase().includes('deepseek')
416
- }
417
-
418
- /**
419
- * Known reasoning-effort ranks in ascending order. Effort ids are opaque
420
- * adapter-owned strings, so the rank table covers the conventional names
421
- * (off → low → medium → high → xhigh → max/ultra); an unrecognized id
422
- * ranks as unknown (0), which never triggers the high-effort wave.
423
- */
424
- const EFFORT_RANK: Readonly<Record<string, number>> = {
425
- off: 0,
426
- none: 0,
427
- low: 1,
428
- medium: 2,
429
- med: 2,
430
- high: 3,
431
- xhigh: 4,
432
- 'x-high': 4,
433
- 'very-high': 4,
434
- max: 5,
435
- maximum: 5,
436
- ultra: 5,
437
- }
438
-
439
- /**
440
- * True when an effective reasoning effort is STRICTLY above `high` — the
441
- * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
442
- * Absent efforts and unrecognized ids never qualify.
443
- * @param effort - the effective reasoning-effort id ('' or undefined when none).
444
- * @returns whether the effort ranks above high.
445
- */
446
- export function effortAboveHigh(effort: string | undefined): boolean {
447
- if (effort === undefined || effort === '') return false
448
- const rank = EFFORT_RANK[effort.trim().toLowerCase()]
449
- return rank !== undefined && rank > 3
450
- }
1
+ /**
2
+ * Terminal animation frame tables derived from the web design language:
3
+ * the StateDot "ongoing" pixel chase (3×3 ring, 125ms flat-hold brightness
4
+ * steps, 1s cycle) becomes the full-ring clockwise braille chase in
5
+ * {@link BUSY_CHASE_FRAMES}, and the streaming caret blink is the
6
+ * Claude-Code convention.
7
+ *
8
+ * The DeepSeek model-switch easter egg ports Codex's effort-ignition "Wave"
9
+ * style (`codex-rs/tui/src/bottom_pane/effort_ignition_styles.rs`): switching
10
+ * INTO an official DeepSeek route sweeps a blue wave across the composer's
11
+ * input row — one column per cell, `backgroundColor` = the sampled wave
12
+ * color — then, on the deepseek (Ultra-equivalent) tier, drops the `· ✦ ✧`
13
+ * sparkle sequence into the rightmost blank cell before fading. The prompt
14
+ * marker keeps the tier accent afterwards (persistent, like Codex's prompt
15
+ * charge). Pure functions only — the Ink layer owns timers and colors.
16
+ *
17
+ * @module @deepseek-ai/dsh-code/render/animations
18
+ */
19
+
20
+ import type { RgbTriple } from '../theme.ts'
21
+
22
+ /**
23
+ * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
24
+ * ring trail clockwise around the eight outer positions, one braille glyph
25
+ * per step 8 frames × 125ms = the web's 1s cycle.
26
+ */
27
+ export const BUSY_CHASE_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] as const
28
+
29
+ /** Chase frame for a monotonic tick (the busy composer/Deep-diving marker). */
30
+ export function busyChaseFrame(tick: number): string {
31
+ return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0]
32
+ }
33
+
34
+ /** Caret visibility: half the ticks on, half off (530ms blink). */
35
+ export function caretVisible(tick: number): boolean {
36
+ return tick % 2 === 0
37
+ }
38
+
39
+ /**
40
+ * The one-shot DeepSeek model-switch easter egg: when the status bar model
41
+ * label switches to an official DeepSeek route, the composer's input row
42
+ * plays Codex's effort-ignition "Wave" a blue crest sweeping the content
43
+ * row column by column (background tint ≤ 0.55 under the draft), plus the
44
+ * Ultra-style ✧` sparkles on the deepseek tier — and the prompt marker
45
+ * keeps the tier accent afterwards. The Ink layer owns the timer and reads
46
+ * the ACTIVE palette anchors (`getPalette`); everything below is pure
47
+ * interpolation over the colors it is given.
48
+ */
49
+
50
+ /** Frame cadence of the DeepSeek wave: Codex's IGNITION_FRAME_TICK (33ms 30fps). */
51
+ export const DEEPSEEK_WAVE_TICK_MS = 33
52
+
53
+ /**
54
+ * The DeepSeek wave tiers. The concept maps Codex's reasoning tiers to
55
+ * model ids: `flash` runs the Max parameters, `deepseek` (pro models) runs
56
+ * the Ultra parameters (dual band + tail sparkles on the Wave style).
57
+ * `unknown` is the "Into the Unknown" variant: it reuses the deepseek tier's
58
+ * exact parameters (dual band, durations, sparkles) for NON-DeepSeek models
59
+ * running a reasoning effort above high — the wordmark renders differently
60
+ * but the motion is identical.
61
+ */
62
+ export type DeepseekWaveTier = 'flash' | 'deepseek' | 'unknown'
63
+
64
+ /**
65
+ * The three ignition styles Codex `IgnitionStyle`: a traveling crest
66
+ * (Wave), a drifting multi-hue band (Aurora), and an expanding ring (Pulse).
67
+ * One style is picked at random per trigger and never repeats the previous.
68
+ */
69
+ export type DeepseekWaveStyle = 'wave' | 'aurora' | 'pulse'
70
+
71
+ /** All styles in canonical order, for random selection. */
72
+ const DEEPSEEK_WAVE_STYLES: readonly DeepseekWaveStyle[] = ['wave', 'aurora', 'pulse']
73
+
74
+ /** Wave half-width in columns Codex WAVE_HALF_WIDTH (9). */
75
+ export const WAVE_HALF_WIDTH = 9
76
+
77
+ /** Pulse ring half-width in columns Codex PULSE_HALF_WIDTH (4.5). */
78
+ const PULSE_HALF_WIDTH = 4.5
79
+
80
+ /** Sparkle start and frame cadence Codex SPARK_START / SPARK_FRAME. */
81
+ const SPARK_START_MS = 900
82
+ const SPARK_FRAME_MS = 100
83
+
84
+ /** Sparkle glyphs in frame order — Codex SPARK_GLYPHS (`· ✦ ✧`). */
85
+ export const SPARK_GLYPHS = ['·', '✦', '✧'] as const
86
+
87
+ /**
88
+ * Band tables — Codex `bands(style, tier)`. Each entry is a triple whose
89
+ * meaning depends on the style: Wave/Pulse use `(launch, travel, strength)`;
90
+ * Aurora uses `(speed, phase, hueIndex)`.
91
+ */
92
+ export type DeepseekWaveBand = readonly [number, number, number]
93
+ export const DEEPSEEK_WAVE_BANDS: Readonly<Record<DeepseekWaveStyle, Readonly<Record<DeepseekWaveTier, readonly DeepseekWaveBand[]>>>> = {
94
+ wave: {
95
+ // Wave-Max: one band sweeping 0.10s..0.85s.
96
+ flash: [[0.10, 0.75, 1.0]],
97
+ // Wave-Ultra: two offset bands for a richer crest.
98
+ deepseek: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
99
+ // Into the Unknown reuses the Ultra parameters verbatim.
100
+ unknown: [[0.10, 0.70, 1.0], [0.35, 0.55, 1.0]],
101
+ },
102
+ aurora: {
103
+ // Aurora-Max: two drifting bands (hues 0 and 1).
104
+ flash: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0]],
105
+ // Aurora-Ultra: a third band adds hue 2.
106
+ deepseek: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
107
+ unknown: [[0.35, 0.15, 0.0], [-0.50, 0.60, 1.0], [0.75, 0.35, 2.0]],
108
+ },
109
+ pulse: {
110
+ // Pulse-Max: one expanding ring.
111
+ flash: [[0.10, 0.60, 1.0]],
112
+ // Pulse-Ultra: two rings (inner weaker, outer stronger).
113
+ deepseek: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
114
+ unknown: [[0.10, 0.55, 0.8], [0.45, 0.55, 1.1]],
115
+ },
116
+ }
117
+
118
+ /** Extra display time applied to every Codex ignition style. */
119
+ const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200
120
+
121
+ /** Original Codex duration used as the animation's sampling timeline. */
122
+ function deepseekWaveBaseDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
123
+ // The unknown tier reuses the deepseek (pro) durations exactly.
124
+ const pro = tier === 'deepseek' || tier === 'unknown'
125
+ switch (style) {
126
+ case 'aurora': return pro ? 1600 : 1300
127
+ case 'pulse': return pro ? 1250 : 900
128
+ case 'wave': return pro ? 1300 : 1000
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Total visible duration: the Codex ignition duration plus 200ms so its motion
134
+ * remains readable in a busy terminal.
135
+ * @param tier - the active wave tier.
136
+ * @param style - the active ignition style.
137
+ * @returns the duration in milliseconds.
138
+ */
139
+ export function deepseekWaveDuration(tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): number {
140
+ return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS
141
+ }
142
+
143
+ /** Map the extended display timeline back onto the original Codex samples. */
144
+ function deepseekWaveSampleElapsedMs(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle): number {
145
+ const base = deepseekWaveBaseDuration(tier, style)
146
+ return tick * DEEPSEEK_WAVE_TICK_MS * base / deepseekWaveDuration(tier, style)
147
+ }
148
+
149
+ /**
150
+ * Pick one ignition style at random, never repeating the previous one —
151
+ * Codex `IgnitionStyle::random`. Falls back to the remaining styles.
152
+ * @param previous - the style of the last trigger, if any.
153
+ * @returns a style different from `previous`.
154
+ */
155
+ export function deepseekWaveStyleRandom(previous: DeepseekWaveStyle | undefined): DeepseekWaveStyle {
156
+ const candidates = DEEPSEEK_WAVE_STYLES.filter(style => style !== previous)
157
+ return candidates[Math.floor(Math.random() * candidates.length)] ?? 'wave'
158
+ }
159
+
160
+ /**
161
+ * Tier for a `provider/model` label: a model id containing `flash` runs the
162
+ * single-band flash tier; everything else (pro/reasoner/chat) runs the
163
+ * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
164
+ * @param model - the `provider/model` label of the applied model.
165
+ * @returns the wave tier for that model.
166
+ */
167
+ export function deepseekWaveTier(model: string): DeepseekWaveTier {
168
+ return model.toLowerCase().includes('flash') ? 'flash' : 'deepseek'
169
+ }
170
+
171
+ /**
172
+ * Cosine window Codex `crest`: 1 exactly under the wave center, 0 from
173
+ * one half-width away.
174
+ * @param distance - distance from the crest center in half-widths.
175
+ * @returns the crest strength in 0..1.
176
+ */
177
+ export function crest(distance: number): number {
178
+ if (distance >= 1) return 0
179
+ return 0.5 * (1 + Math.cos(Math.PI * distance))
180
+ }
181
+
182
+ /**
183
+ * Cubic ease-in-out — Codex `ease_in_out`: flat at both ends, steepest in
184
+ * the middle, so the crest accelerates and eases instead of sliding linearly.
185
+ * @param progress - raw progress (clamped to 0..1).
186
+ * @returns the eased progress in 0..1.
187
+ */
188
+ export function easeInOut(progress: number): number {
189
+ const p = Math.min(1, Math.max(0, progress))
190
+ if (p < 0.5) return 4 * p * p * p
191
+ const inverse = -2 * p + 2
192
+ return 1 - (inverse * inverse * inverse) / 2
193
+ }
194
+
195
+ /**
196
+ * Fade-in/fade-out envelope Codex `envelope`: linear ramp over `fadeIn`
197
+ * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
198
+ * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
199
+ * an envelope); exported for the Aurora-style fades and for tests.
200
+ * @param elapsed - seconds since the animation started.
201
+ * @param total - total duration in seconds.
202
+ * @param fadeIn - seconds of fade-in.
203
+ * @param fadeOut - seconds of fade-out.
204
+ * @returns the envelope value in 0..1.
205
+ */
206
+ export function envelope(elapsed: number, total: number, fadeIn: number, fadeOut: number): number {
207
+ if (elapsed <= 0 || elapsed >= total) return 0
208
+ const rise = elapsed / Math.max(fadeIn, Number.EPSILON)
209
+ const fall = (total - elapsed) / Math.max(fadeOut, Number.EPSILON)
210
+ return Math.min(Math.max(Math.min(rise, fall), 0), 1)
211
+ }
212
+
213
+ /**
214
+ * One band's contribution at a column Codex `band_sample`, all three
215
+ * branches: Wave sweeps an eased crest across the row; Aurora drifts a
216
+ * sinusoidal center carrying a hue index; Pulse expands a ring from the row
217
+ * center with cubic ease and decaying strength.
218
+ * @param style - the ignition style.
219
+ * @param band - the band triple (meaning depends on the style).
220
+ * @param elapsed - seconds since the animation started.
221
+ * @param column - column index in the content row (0..width-1).
222
+ * @param width - content-row width in columns.
223
+ * @returns `[hueIndex, strength]`.
224
+ */
225
+ function bandSample(
226
+ style: DeepseekWaveStyle,
227
+ band: DeepseekWaveBand,
228
+ elapsed: number,
229
+ column: number,
230
+ width: number,
231
+ ): [number, number] {
232
+ const [first, second, third] = band
233
+ switch (style) {
234
+ case 'wave': {
235
+ const progress = (elapsed - first) / second
236
+ if (progress < 0 || progress > 1) return [0, 0]
237
+ const center = easeInOut(progress) * (width + 2 * WAVE_HALF_WIDTH) - WAVE_HALF_WIDTH
238
+ return [0, crest(Math.abs(column - center) / WAVE_HALF_WIDTH)]
239
+ }
240
+ case 'aurora': {
241
+ const center = (0.5 + 0.38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width
242
+ const halfWidth = Math.max(width * 0.22, 4)
243
+ return [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)]
244
+ }
245
+ case 'pulse': {
246
+ const progress = (elapsed - first) / second
247
+ if (progress < 0 || progress > 1) return [0, 0]
248
+ const inverse = 1 - progress
249
+ const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH)
250
+ const distance = Math.abs(column - width / 2)
251
+ return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - 0.6 * progress)]
252
+ }
253
+ }
254
+ }
255
+
256
+ /** Linear RGB blend — Codex `blend`: `fg * alpha + bg * (1 - alpha)`. */
257
+ function blendRgb(fg: RgbTriple, bg: RgbTriple, alpha: number): RgbTriple {
258
+ return [
259
+ Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
260
+ Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
261
+ Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
262
+ ]
263
+ }
264
+
265
+ /**
266
+ * Per-row phase share of the duration: the crest reaches the top row first
267
+ * and the bottom row last, sweeping down the band. 0.12 keeps the bottom
268
+ * row's lag inside the 200ms duration extension.
269
+ */
270
+ const DEEPSEEK_WAVE_ROW_PHASE = 0.12
271
+
272
+ /**
273
+ * The background color for one composer-band column at a tick Codex
274
+ * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
275
+ * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
276
+ * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
277
+ * blends the mixed hue toward the blank-cell base at the style's alpha cap,
278
+ * and Aurora applies its own fade envelope. Returns `null` when the column
279
+ * should stay transparent, so the row returns to no `backgroundColor` on
280
+ * both ends. With `rows > 1` each row samples the same timeline shifted by a
281
+ * per-row phase offset, so the crest cascades down the band instead of
282
+ * painting every row identically.
283
+ * @param tick - wave frame (0, 1, at DEEPSEEK_WAVE_TICK_MS).
284
+ * @param column - column index in the content row (0..width-1).
285
+ * @param width - content-row width in columns.
286
+ * @param tier - the wave tier (flash = Max, deepseek = Ultra parameters).
287
+ * @param style - the ignition style.
288
+ * @param hues - the tier's three hues.
289
+ * @param base - the blank-cell base color the tint blends toward.
290
+ * @param row - row index in the band (0..rows-1; default 0 = old single-row).
291
+ * @param rows - band height in rows (default 1).
292
+ * @returns the blended RGB background, or null for transparent.
293
+ */
294
+ export function deepseekWaveColumnBg(
295
+ tick: number,
296
+ column: number,
297
+ width: number,
298
+ tier: DeepseekWaveTier,
299
+ style: DeepseekWaveStyle,
300
+ hues: readonly [RgbTriple, RgbTriple, RgbTriple],
301
+ base: RgbTriple,
302
+ row = 0,
303
+ rows = 1,
304
+ ): RgbTriple | null {
305
+ const total = deepseekWaveBaseDuration(tier, style) / 1000
306
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
307
+ - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE
308
+ const fade = style === 'aurora' ? envelope(elapsed, total, 0.25, 0.40) : 1
309
+ const weights = [0, 0, 0]
310
+ for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
311
+ const [hue, strength] = bandSample(style, band, elapsed, column, width)
312
+ weights[hue] = style === 'aurora' ? weights[hue]! + strength : Math.max(weights[hue]!, strength)
313
+ }
314
+ const weight = weights[0]! + weights[1]! + weights[2]!
315
+ if (weight <= 0.01) return null
316
+ let red = 0
317
+ let green = 0
318
+ let blue = 0
319
+ for (let index = 0; index < 3; index += 1) {
320
+ red += weights[index]! * hues[index]![0]
321
+ green += weights[index]! * hues[index]![1]
322
+ blue += weights[index]! * hues[index]![2]
323
+ }
324
+ const mixed: RgbTriple = [
325
+ Math.round(red / weight),
326
+ Math.round(green / weight),
327
+ Math.round(blue / weight),
328
+ ]
329
+ const alpha = style === 'aurora' ? Math.min(weight * 0.40, 0.50) * fade : weight * 0.55
330
+ if (alpha < 0.02) return null
331
+ return blendRgb(mixed, base, alpha)
332
+ }
333
+
334
+ /**
335
+ * The sparkle glyph for a tick — Codex `spark_frame`, sampled on the same
336
+ * proportionally slowed DeepSeek Wave timeline as the composer background.
337
+ * The Ink layer still must skip occupied cells.
338
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
339
+ * @returns the sparkle glyph, or null outside the stretched tail window.
340
+ */
341
+ export function deepseekWaveSpark(tick: number): string | null {
342
+ const elapsed = deepseekWaveSampleElapsedMs(tick, 'deepseek', 'wave')
343
+ if (elapsed < SPARK_START_MS) return null
344
+ const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS)
345
+ return SPARK_GLYPHS[frame] ?? null
346
+ }
347
+
348
+ /**
349
+ * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
350
+ * shortly after the first crest launches and out before the wave settles,
351
+ * so the brand name surfaces through the sweep's middle. The Ink layer
352
+ * places it in the row's blank mid-section (never over real draft text).
353
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
354
+ * @param tier - the wave tier.
355
+ * @returns true while the wordmark should be visible.
356
+ */
357
+ export function deepseekWaveWordVisible(tick: number, tier: DeepseekWaveTier, style: DeepseekWaveStyle = 'wave'): boolean {
358
+ const total = deepseekWaveBaseDuration(tier, style) / 1000
359
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1000
360
+ return envelope(elapsed, total, total * 0.2, total * 0.35) > 0.25
361
+ }
362
+
363
+ /**
364
+ * The per-character color for the `deepseek` wordmark: the tier's hues
365
+ * cycled per character (d→hue0, e→hue1, e→hue2, …), a brand-gradient text.
366
+ * @param index - character index in the wordmark.
367
+ * @param hues - the tier's three hues.
368
+ * @returns the hue for that character.
369
+ */
370
+ export function deepseekWaveWordHue(index: number, hues: readonly [RgbTriple, RgbTriple, RgbTriple]): RgbTriple {
371
+ return hues[index % hues.length]!
372
+ }
373
+
374
+ /**
375
+ * True when a `provider/model` status label addresses the official DeepSeek
376
+ * route: either segment contains `deepseek` (case-insensitive), covering the
377
+ * `deepseek-official` provider route and its `deepseek-*` model ids.
378
+ * @param label - the status bar model label (`provider/model`).
379
+ * @returns whether the label names an official DeepSeek model.
380
+ */
381
+ export function isOfficialDeepSeekLabel(label: string): boolean {
382
+ const slash = label.indexOf('/')
383
+ const provider = slash < 0 ? label : label.slice(0, slash)
384
+ const model = slash < 0 ? '' : label.slice(slash + 1)
385
+ return provider.toLowerCase().includes('deepseek') || model.toLowerCase().includes('deepseek')
386
+ }
387
+
388
+ /**
389
+ * Known reasoning-effort ranks in ascending order. Effort ids are opaque
390
+ * adapter-owned strings, so the rank table covers the conventional names
391
+ * (off → low → medium → high → xhigh → max/ultra); an unrecognized id
392
+ * ranks as unknown (0), which never triggers the high-effort wave.
393
+ */
394
+ const EFFORT_RANK: Readonly<Record<string, number>> = {
395
+ off: 0,
396
+ none: 0,
397
+ low: 1,
398
+ medium: 2,
399
+ med: 2,
400
+ high: 3,
401
+ xhigh: 4,
402
+ 'x-high': 4,
403
+ 'very-high': 4,
404
+ max: 5,
405
+ maximum: 5,
406
+ ultra: 5,
407
+ }
408
+
409
+ /**
410
+ * True when an effective reasoning effort is STRICTLY above `high` — the
411
+ * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
412
+ * Absent efforts and unrecognized ids never qualify.
413
+ * @param effort - the effective reasoning-effort id ('' or undefined when none).
414
+ * @returns whether the effort ranks above high.
415
+ */
416
+ export function effortAboveHigh(effort: string | undefined): boolean {
417
+ if (effort === undefined || effort === '') return false
418
+ const rank = EFFORT_RANK[effort.trim().toLowerCase()]
419
+ return rank !== undefined && rank > 3
420
+ }