@@ -6,7 +6,11 @@ import {
66 Loader2 ,
77 AlertCircle ,
88 Minus ,
9+ FastForward ,
910} from " lucide-vue-next" ;
11+ import { useI18n } from " vue-i18n" ;
12+
13+ const { t } = useI18n ();
1014
1115// Stepper for a streamer-pod session boot.
1216//
@@ -43,13 +47,21 @@ const props = withDefaults(
4347 progress? : number ;
4448 progress_stage? : string ;
4549 }>;
50+ // Operator-only: show a "Skip shaders" affordance while the pod is
51+ // still booting (the parent gates this on role + not-yet-live).
52+ canSkip? : boolean ;
53+ skipping? : boolean ;
4654 }>(),
4755 {
4856 headerLabel: " Session boot" ,
4957 statusHistory : () => [],
58+ canSkip: false ,
59+ skipping: false ,
5060 },
5161);
5262
63+ const emit = defineEmits <{ (e : " skip" ): void }>();
64+
5365const firedStatuses = computed (() => {
5466 const set = new Set <string >(props .statusHistory .map ((h ) => h .status ));
5567 // status push may race ahead of history fan-out.
@@ -147,6 +159,29 @@ function durationFor(stageKey: string): string {
147159 if (! next ) return " " ;
148160 return fmt (new Date (next .at ).getTime () - start );
149161}
162+
163+ // The shader compile is the one stage with rich progress — once a
164+ // percentage lands it gets a dedicated bar+count block instead of the
165+ // cramped inline meta every other stage uses.
166+ function shaderActive(stageKey : string , index : number ): boolean {
167+ return (
168+ stageKey === " processing_shaders" &&
169+ stateOf (index ) === " current" &&
170+ props .status !== " errored" &&
171+ progressFor (stageKey ) !== null
172+ );
173+ }
174+
175+ // "(26824 / 731082)" → "26,824 / 731,082". Falls back to the raw
176+ // string (sans wrapping parens) when it isn't an a/b count.
177+ function formatShaderCount(raw : string | null | undefined ): string {
178+ if (! raw ) return " " ;
179+ const m = raw .match (/ (\d [\d ,] * )\s * \/ \s * (\d [\d ,] * )/ );
180+ if (! m ) return raw .replace (/ ^ \( | \) $ / g , " " );
181+ const done = Number (m [1 ].replace (/ ,/ g , " " )).toLocaleString ();
182+ const total = Number (m [2 ].replace (/ ,/ g , " " )).toLocaleString ();
183+ return ` ${done } / ${total } ` ;
184+ }
150185 </script >
151186
152187<template >
@@ -162,7 +197,7 @@ function durationFor(stageKey: string): string {
162197 <li
163198 v-for =" { stage, index } in visibleStages"
164199 :key =" stage.key"
165- class =" flex items-center gap-2.5 text-xs transition-colors"
200+ class =" text-xs transition-colors"
166201 :class =" {
167202 'text-muted-foreground/60': stateOf(index) === 'pending',
168203 'text-muted-foreground/40 line-through decoration-muted-foreground/30':
@@ -174,52 +209,104 @@ function durationFor(stageKey: string): string {
174209 stateOf(index) === 'current' && status === 'errored',
175210 }"
176211 >
177- <span class =" w-4 h-4 inline-flex items-center justify-center shrink-0" >
178- <Check v-if =" stateOf (index ) === ' done' " class="w-3.5 h-3.5 " />
179- <Loader2
180- v-else-if =" stateOf (index ) === ' current' && status !== ' errored' "
181- class="w-3.5 h-3.5 animate-spin"
182- />
183- <AlertCircle
184- v-else-if =" stateOf (index ) === ' current' && status === ' errored' "
185- class="w-3.5 h-3.5 "
186- />
187- <Minus
188- v-else-if =" stateOf (index ) === ' skipped' "
189- class="w-3.5 h-3.5 opacity-50"
190- />
191- <CircleDashed v-else class="w-3.5 h-3.5 opacity-50" />
192- </span >
193- <span class =" flex-1" >{{ stage.label }}</span >
194- <span
195- v-if =" stateOf(index) === 'current' && progressFor(stage.key) !== null"
196- class =" font-mono text-[0.65rem] tabular-nums opacity-80"
212+ <div class =" flex items-center gap-2.5" >
213+ <span class =" w-4 h-4 inline-flex items-center justify-center shrink-0" >
214+ <Check v-if =" stateOf (index ) === ' done' " class="w-3.5 h-3.5 " />
215+ <Loader2
216+ v-else-if =" stateOf (index ) === ' current' && status !== ' errored' "
217+ class="w-3.5 h-3.5 animate-spin"
218+ />
219+ <AlertCircle
220+ v-else-if =" stateOf (index ) === ' current' && status === ' errored' "
221+ class="w-3.5 h-3.5 "
222+ />
223+ <Minus
224+ v-else-if =" stateOf (index ) === ' skipped' "
225+ class="w-3.5 h-3.5 opacity-50"
226+ />
227+ <CircleDashed v-else class="w-3.5 h-3.5 opacity-50" />
228+ </span >
229+ <span class =" flex-1 min-w-0 truncate" >{{ stage.label }}</span >
230+
231+ <!-- Inline meta for every stage except the active shader compile,
232+ which moves its numbers into the progress block below so the
233+ label never has to wrap. -->
234+ <template v-if =" ! shaderActive (stage .key , index )" >
235+ <span
236+ v-if ="
237+ stateOf(index) === 'current' && progressFor(stage.key) !== null
238+ "
239+ class =" font-mono text-[0.65rem] tabular-nums opacity-80"
240+ >
241+ {{ progressFor(stage.key)!.percent.toFixed(1) }}%
242+ </span >
243+ <span
244+ v-if =" stateOf(index) === 'current' && elapsedOnCurrent"
245+ class =" font-mono text-[0.65rem] tabular-nums opacity-70"
246+ >
247+ {{ elapsedOnCurrent }}
248+ </span >
249+ <span
250+ v-else-if =" stateOf(index) === 'done' && durationFor(stage.key)"
251+ class =" font-mono text-[0.65rem] tabular-nums opacity-60"
252+ >
253+ {{ durationFor(stage.key) }}
254+ </span >
255+ <span
256+ v-else-if =" stateOf(index) === 'skipped'"
257+ class =" font-mono text-[0.6rem] uppercase tracking-wider opacity-50"
258+ >
259+ skipped
260+ </span >
261+ </template >
262+
263+ <!-- Operator-only skip, pinned to the right of the shader row. -->
264+ <button
265+ v-if ="
266+ canSkip &&
267+ stage.key === 'processing_shaders' &&
268+ stateOf(index) === 'current'
269+ "
270+ type =" button"
271+ :disabled =" skipping"
272+ class =" ml-1 inline-flex shrink-0 items-center gap-1 rounded border border-border/60 bg-card/60 px-1.5 py-0.5 font-mono text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground transition-colors hover:bg-card hover:text-foreground disabled:opacity-50 cursor-pointer"
273+ @click.stop =" emit('skip')"
274+ >
275+ <Loader2 v-if =" skipping " class="w-2.5 h-2.5 animate-spin" />
276+ <FastForward v-else class="w-2.5 h-2.5 " />
277+ {{ t("live_stages.skip_shaders") }}
278+ </button >
279+ </div >
280+
281+ <!-- Shader compile detail: thin progress bar + count and elapsed,
282+ aligned under the label (icon width + gap = 1.625rem). -->
283+ <div
284+ v-if =" shaderActive(stage.key, index)"
285+ class =" mt-1.5 ml-[1.625rem] flex flex-col gap-1"
197286 >
198- {{ progressFor(stage.key)!.percent.toFixed(1) }}%<span
199- v-if =" progressFor(stage.key)!.stage"
200- class =" opacity-60"
287+ <div
288+ class =" h-1 w-full overflow-hidden rounded-full bg-muted-foreground/15"
201289 >
202- ({{ progressFor(stage.key)!.stage }})</span
290+ <div
291+ class =" h-full rounded-full bg-[hsl(var(--tac-amber))] transition-[width] duration-500 ease-out"
292+ :style =" { width: `${progressFor(stage.key)!.percent}%` }"
293+ />
294+ </div >
295+ <div
296+ class =" flex items-center justify-between gap-2 font-mono text-[0.6rem] tabular-nums text-muted-foreground"
203297 >
204- </span >
205- <span
206- v-if =" stateOf(index) === 'current' && elapsedOnCurrent"
207- class =" font-mono text-[0.65rem] tabular-nums opacity-70"
208- >
209- {{ elapsedOnCurrent }}
210- </span >
211- <span
212- v-else-if =" stateOf(index) === 'done' && durationFor(stage.key)"
213- class =" font-mono text-[0.65rem] tabular-nums opacity-60"
214- >
215- {{ durationFor(stage.key) }}
216- </span >
217- <span
218- v-else-if =" stateOf(index) === 'skipped'"
219- class =" font-mono text-[0.6rem] uppercase tracking-wider opacity-50"
220- >
221- skipped
222- </span >
298+ <span class =" truncate" >
299+ <span class =" text-foreground/80"
300+ >{{ progressFor(stage.key)!.percent.toFixed(1) }}%</span
301+ ><span v-if =" progressFor(stage.key)!.stage" class =" opacity-60" >
302+ · {{ formatShaderCount(progressFor(stage.key)!.stage) }}</span
303+ >
304+ </span >
305+ <span v-if =" elapsedOnCurrent" class =" shrink-0 opacity-70" >{{
306+ elapsedOnCurrent
307+ }}</span >
308+ </div >
309+ </div >
223310 </li >
224311 </ul >
225312
0 commit comments