dsh-code 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +278 -265
- package/README.md +277 -264
- package/cordis.patch.yml +7 -0
- package/lib/index.mjs +1649 -575
- package/lib/types/app.d.ts +19 -14
- package/lib/types/attachments.d.ts +14 -1
- package/lib/types/authorization-panel.d.ts +22 -0
- package/lib/types/authorization.d.ts +36 -0
- package/lib/types/keyboard.d.ts +32 -3
- package/lib/types/mentions.d.ts +4 -0
- package/lib/types/models.d.ts +3 -1
- package/lib/types/permissions.d.ts +4 -14
- package/lib/types/presets.d.ts +4 -17
- package/lib/types/render/animations.d.ts +27 -11
- package/lib/types/render/editor.d.ts +32 -7
- package/lib/types/render/lines.d.ts +1 -1
- package/lib/types/render/status.d.ts +1 -1
- package/package.json +49 -43
- package/src/app.ts +4570 -4188
- package/src/attachments.ts +93 -2
- package/src/authorization-panel.ts +285 -0
- package/src/authorization.ts +147 -0
- package/src/index.ts +34 -21
- package/src/internals.ts +26 -9
- package/src/keyboard.ts +164 -39
- package/src/mentions.ts +12 -1
- package/src/models.ts +20 -14
- package/src/permissions.ts +5 -13
- package/src/presets.ts +5 -18
- package/src/provider-settings.ts +1 -1
- package/src/render/animations.ts +447 -400
- package/src/render/editor.ts +125 -25
- package/src/render/lines.ts +16 -2
- package/src/render/projection.ts +7 -3
- package/src/render/status.ts +2 -2
- package/src/session-directory.ts +1 -1
package/src/render/animations.ts
CHANGED
|
@@ -1,420 +1,467 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal animation
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* per step — 8 frames × 125ms = the web's 1s cycle.
|
|
26
|
-
*/
|
|
1
|
+
/**
|
|
2
|
+
* Terminal animation helpers derived from the web design language:
|
|
3
|
+
* thinking uses Codex's slow shimmer sweep, the busy composer marker uses the
|
|
4
|
+
* original braille chase, and the streaming caret blink is the Claude-Code
|
|
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
|
+
|
|
21
|
+
/** Cadence for the original busy braille chase (8 frames × 125ms = 1s). */
|
|
22
|
+
export const BUSY_CHASE_TICK_MS = 125
|
|
23
|
+
|
|
24
|
+
/** Original terminal StateDot chase frames. */
|
|
27
25
|
export const BUSY_CHASE_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] as const
|
|
28
26
|
|
|
29
|
-
/** Chase frame for a monotonic tick
|
|
27
|
+
/** Chase frame for a monotonic tick. */
|
|
30
28
|
export function busyChaseFrame(tick: number): string {
|
|
31
29
|
return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0]
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/**
|
|
35
|
-
export
|
|
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
|
-
}
|
|
32
|
+
/** Clock cadence for the Codex-style Deep diving shimmer. */
|
|
33
|
+
export const DEEP_DIVING_SHIMMER_TICK_MS = 33
|
|
194
34
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
}
|
|
35
|
+
/** Codex shimmer timing and geometry. */
|
|
36
|
+
export const DEEP_DIVING_SHIMMER_DURATION_MS = 2_000
|
|
37
|
+
export const DEEP_DIVING_SHIMMER_PADDING = 10
|
|
38
|
+
export const DEEP_DIVING_SHIMMER_HALF_WIDTH = 5
|
|
39
|
+
export const DEEP_DIVING_SPARK_BREATH_DURATION_MS = 2_000
|
|
212
40
|
|
|
213
41
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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]`.
|
|
42
|
+
* Codex's 2-second shimmer sweep, expressed in terminal ticks. The sweep has
|
|
43
|
+
* ten virtual columns of padding on either side and a five-column cosine
|
|
44
|
+
* highlight band, so the text changes gently rather than cycling rapidly.
|
|
224
45
|
*/
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
elapsed
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
const
|
|
233
|
-
|
|
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
|
-
]
|
|
46
|
+
export function deepDivingShimmerIntensity(index: number, tick: number, graphemeCount: number): number {
|
|
47
|
+
if (graphemeCount <= 0) return 0
|
|
48
|
+
const period = graphemeCount + DEEP_DIVING_SHIMMER_PADDING * 2
|
|
49
|
+
const elapsed = ((tick * DEEP_DIVING_SHIMMER_TICK_MS) % DEEP_DIVING_SHIMMER_DURATION_MS + DEEP_DIVING_SHIMMER_DURATION_MS) % DEEP_DIVING_SHIMMER_DURATION_MS
|
|
50
|
+
const position = elapsed / DEEP_DIVING_SHIMMER_DURATION_MS * period
|
|
51
|
+
const distance = Math.abs(index + DEEP_DIVING_SHIMMER_PADDING - position)
|
|
52
|
+
if (distance > DEEP_DIVING_SHIMMER_HALF_WIDTH) return 0
|
|
53
|
+
const angle = Math.PI * distance / DEEP_DIVING_SHIMMER_HALF_WIDTH
|
|
54
|
+
return 0.5 * (1 + Math.cos(angle))
|
|
263
55
|
}
|
|
264
56
|
|
|
265
|
-
/**
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
57
|
+
/** Blue RGB color for one grapheme in the Codex-style shimmer. */
|
|
58
|
+
export function deepDivingGradientColor(
|
|
59
|
+
index: number,
|
|
295
60
|
tick: number,
|
|
296
|
-
|
|
297
|
-
width: number,
|
|
298
|
-
tier: DeepseekWaveTier,
|
|
299
|
-
style: DeepseekWaveStyle,
|
|
300
|
-
hues: readonly [RgbTriple, RgbTriple, RgbTriple],
|
|
61
|
+
graphemeCount: number,
|
|
301
62
|
base: RgbTriple,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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]!
|
|
63
|
+
highlight: RgbTriple,
|
|
64
|
+
): RgbTriple {
|
|
65
|
+
return blendRgb(highlight, base, deepDivingShimmerIntensity(index, tick, graphemeCount))
|
|
372
66
|
}
|
|
373
67
|
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
*
|
|
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')
|
|
68
|
+
/** Smooth breathing intensity for the always-visible Deep diving sparkle. */
|
|
69
|
+
export function deepDivingSparkIntensity(tick: number): number {
|
|
70
|
+
const elapsed = ((tick * DEEP_DIVING_SHIMMER_TICK_MS) % DEEP_DIVING_SPARK_BREATH_DURATION_MS
|
|
71
|
+
+ DEEP_DIVING_SPARK_BREATH_DURATION_MS) % DEEP_DIVING_SPARK_BREATH_DURATION_MS
|
|
72
|
+
const phase = elapsed / DEEP_DIVING_SPARK_BREATH_DURATION_MS
|
|
73
|
+
return 0.2 + 0.8 * (0.5 + 0.5 * Math.cos(Math.PI * 2 * phase))
|
|
386
74
|
}
|
|
387
75
|
|
|
388
|
-
/**
|
|
389
|
-
|
|
390
|
-
|
|
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,
|
|
76
|
+
/** Blue RGB color for the breathing Deep diving sparkle. */
|
|
77
|
+
export function deepDivingSparkColor(tick: number, base: RgbTriple, highlight: RgbTriple): RgbTriple {
|
|
78
|
+
return blendRgb(highlight, base, deepDivingSparkIntensity(tick))
|
|
407
79
|
}
|
|
408
80
|
|
|
409
|
-
/**
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
}
|