@hugpy/video-intelligence-ui 0.3.2 → 0.3.4

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 (152) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +9 -0
  3. package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
  4. package/dist/assets/index-DMoT8G1q.css +1 -0
  5. package/dist/assets/index-GeD1mo16.js +4190 -0
  6. package/dist/index.html +71 -0
  7. package/entry.tsx +6 -27
  8. package/index.html +4 -4
  9. package/package.json +44 -5
  10. package/src/assets/hugpy-mark.png +0 -0
  11. package/src/config.ts +11 -27
  12. package/src/demo/DemoBanner.tsx +0 -0
  13. package/src/demo/demo.css +0 -0
  14. package/src/demo/demoFetch.ts +0 -9
  15. package/src/demo/fixtures.ts +0 -0
  16. package/src/demo/identityFixture.ts +0 -0
  17. package/src/demo/index.ts +0 -0
  18. package/src/demo/mediaBase.ts +0 -0
  19. package/src/demo/mode.ts +0 -0
  20. package/src/demo/promptAssist.ts +0 -0
  21. package/src/demo/sampleData.ts +0 -0
  22. package/src/demo/sampleSets.ts +0 -0
  23. package/src/demo/seed.ts +0 -0
  24. package/src/nav/BrandMark.css +0 -0
  25. package/src/nav/BrandMark.tsx +0 -0
  26. package/src/nav/Navbar.css +0 -0
  27. package/src/nav/Navbar.tsx +8 -3
  28. package/src/regions/SpatialRegionEditor.tsx +0 -0
  29. package/src/regions/TemporalRegionEditor.tsx +0 -0
  30. package/src/regions/types.ts +0 -0
  31. package/src/session.ts +8 -43
  32. package/src/share.ts +0 -0
  33. package/src/stations/ActivePanel.tsx +0 -0
  34. package/src/stations/ActiveProcessesStation.tsx +0 -0
  35. package/src/stations/AddFileBar.tsx +0 -0
  36. package/src/stations/AudioCropStation.tsx +0 -0
  37. package/src/stations/ComfyPanel.tsx +0 -0
  38. package/src/stations/ComfyStation.tsx +0 -0
  39. package/src/stations/CondensedKnobStrip.tsx +0 -0
  40. package/src/stations/ConsolePanel.tsx +0 -0
  41. package/src/stations/FrameExtractStation.tsx +0 -0
  42. package/src/stations/GenIdentityBar.tsx +0 -0
  43. package/src/stations/GenerateStation.tsx +10 -6
  44. package/src/stations/GoalComposer.tsx +0 -0
  45. package/src/stations/ImageCropStation.tsx +0 -0
  46. package/src/stations/KnobInfo.tsx +0 -0
  47. package/src/stations/PlaceholderStation.tsx +0 -0
  48. package/src/stations/ScriptFirstStation.tsx +4 -0
  49. package/src/stations/SectionTabs.tsx +0 -0
  50. package/src/stations/SharePanel.tsx +4 -4
  51. package/src/stations/SidebarRegistry.tsx +0 -0
  52. package/src/stations/StationShell.tsx +0 -0
  53. package/src/stations/StudioClipsStation.tsx +0 -0
  54. package/src/stations/TemplateStepsPanel.tsx +190 -0
  55. package/src/stations/ToksPanel.tsx +1 -2
  56. package/src/stations/WorkbenchDrawer.tsx +0 -0
  57. package/src/stations/WorkbenchSidebar.tsx +8 -261
  58. package/src/stations/WorkbenchStation.tsx +0 -0
  59. package/src/stations/comfyEmbed.ts +0 -0
  60. package/src/stations/drawerShell.tsx +0 -0
  61. package/src/stations/ops/AudioCropCore.tsx +0 -0
  62. package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
  63. package/src/stations/ops/FrameExtractCore.tsx +0 -0
  64. package/src/stations/ops/ImageCropCore.tsx +0 -0
  65. package/src/stations/ops/VideoCropCore.tsx +0 -0
  66. package/src/stations/ops/useVideoCropJobs.ts +0 -0
  67. package/src/stations/oracle/DagPanel.tsx +0 -0
  68. package/src/stations/oracle/OracleStation.tsx +2 -0
  69. package/src/stations/oracle/PerformanceComposer.tsx +119 -0
  70. package/src/stations/oracle/SpatialTab.tsx +0 -0
  71. package/src/stations/oracle/StewardPanel.tsx +0 -0
  72. package/src/stations/oracle/oracle.css +33 -0
  73. package/src/stations/oracle/oracleShared.tsx +0 -0
  74. package/src/stations/oracle/oracleTypes.ts +0 -0
  75. package/src/stations/oracle/useOracle.ts +0 -0
  76. package/src/stations/pollCaps.ts +0 -0
  77. package/src/stations/registry.ts +0 -0
  78. package/src/stations/scriptFirst.css +23 -0
  79. package/src/stations/studio/AngleRingEditor.tsx +0 -0
  80. package/src/stations/studio/IdentitiesStation.tsx +86 -76
  81. package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
  82. package/src/stations/studio/IdentityProfileControls.tsx +0 -0
  83. package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
  84. package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
  85. package/src/stations/studio/StudioGenerateMode.tsx +3 -2
  86. package/src/stations/studio/StudioGenerateTab.tsx +278 -190
  87. package/src/stations/studio/StudioItemCard.tsx +0 -0
  88. package/src/stations/studio/StudioLibraryTab.tsx +0 -0
  89. package/src/stations/studio/StudioMovieComposer.tsx +321 -46
  90. package/src/stations/studio/StudioMovieSessions.tsx +12 -19
  91. package/src/stations/studio/StudioPlane.tsx +28 -5
  92. package/src/stations/studio/StudioSplicedRow.tsx +0 -0
  93. package/src/stations/studio/StudioViewer.tsx +6 -11
  94. package/src/stations/studio/TurntableViewer.tsx +0 -0
  95. package/src/stations/studio/coordinationBadge.tsx +0 -0
  96. package/src/stations/studio/identityUpload.ts +0 -0
  97. package/src/stations/studio/movieTimeline.ts +0 -0
  98. package/src/stations/studio/promptCard.tsx +0 -0
  99. package/src/stations/studio/studioItem.ts +0 -0
  100. package/src/stations/studio/studioShared.tsx +14 -2
  101. package/src/stations/studio/useIdentityCreateJob.ts +26 -21
  102. package/src/stations/studio/useIdentityProfiles.ts +0 -0
  103. package/src/stations/studio/useReconstruction.ts +11 -5
  104. package/src/stations/types.ts +0 -0
  105. package/src/stations/useAudioCropJobs.ts +0 -0
  106. package/src/stations/useAudioExtractJob.ts +0 -0
  107. package/src/stations/useCropJobs.ts +0 -0
  108. package/src/stations/useFrameJobs.ts +0 -0
  109. package/src/stations/useGenerateJob.ts +0 -0
  110. package/src/stations/useGenerateMovieJob.ts +0 -0
  111. package/src/stations/useGenerateRowJobs.ts +0 -0
  112. package/src/stations/useGenerateSceneJob.ts +0 -0
  113. package/src/stations/useGenerateSceneRowJobs.ts +0 -0
  114. package/src/stations/useScriptFirstRun.ts +0 -0
  115. package/src/style/app.css +49 -0
  116. package/src/transport/client.ts +0 -0
  117. package/src/transport/telemetry.ts +0 -0
  118. package/src/video/DropReceptacle.tsx +0 -0
  119. package/src/video/LengthRow.tsx +0 -0
  120. package/src/video/LibraryGroups.tsx +0 -0
  121. package/src/video/PromptAssistButtons.tsx +5 -4
  122. package/src/video/UnnamedWarn.tsx +0 -0
  123. package/src/video/composerBridge.ts +0 -0
  124. package/src/video/contract.ts +0 -0
  125. package/src/video/coordinationReport.ts +0 -0
  126. package/src/video/jobClock.ts +0 -0
  127. package/src/video/jobStream.ts +119 -0
  128. package/src/video/jobTracker.ts +67 -76
  129. package/src/video/mediaLibrary.ts +0 -0
  130. package/src/video/modelsFeed.ts +105 -0
  131. package/src/video/renderCompat.ts +0 -0
  132. package/src/video/sessionForm.ts +0 -0
  133. package/src/video/spreadAssist.ts +0 -0
  134. package/src/video/studioBridge.ts +0 -0
  135. package/src/video/studioOracle.ts +48 -0
  136. package/src/video/useMediaJobs.ts +17 -3
  137. package/src/video/useModels.ts +12 -53
  138. package/src/video/useMoviePresets.ts +0 -0
  139. package/src/video/useMovieSessions.ts +15 -6
  140. package/src/video/useOperatorHistory.ts +11 -101
  141. package/src/video/usePresets.ts +0 -0
  142. package/src/video/useProjects.ts +0 -0
  143. package/src/video/usePromptAssist.ts +70 -25
  144. package/src/video/useRenderPresets.ts +0 -0
  145. package/src/video/useStudioAssistLog.ts +0 -0
  146. package/src/video/useStudioPresets.ts +0 -0
  147. package/src/video/useToks.ts +14 -6
  148. package/src/video/watchVideoJob.ts +43 -0
  149. package/tsconfig.json +0 -0
  150. package/vite.config.ts +0 -0
  151. package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
  152. package/src/video/useWorkerCalls.ts +0 -165
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Template steps — the ordered task outline of a registry task-template
3
+ * (settings NS `task_templates`), rendered so the operator can SEE every step
4
+ * and CHOOSE the model for each one. This is the missing cockpit: the
5
+ * template existed only as API data with no screen behind Generate.
6
+ *
7
+ * Reads: GET /llm/templates (open). Saves one slot at a time via
8
+ * POST /llm/templates/<id>/tasks/<task> {model} (operator-gated) — a 401/403
9
+ * or any refusal is rendered VERBATIM on the row it belongs to; the panel
10
+ * itself never hides. Model options come from the assist discovery endpoint,
11
+ * which already excludes operator-blocked and mistagged models, so a choice
12
+ * made here is a choice the resolver will actually serve.
13
+ */
14
+ import { useCallback, useEffect, useState } from "react";
15
+
16
+ import { request, okValue, errorOf, describeAppError } from "../transport/client";
17
+ import { hugpyConfig } from "../config";
18
+
19
+ type TemplateTask = { name: string; desc: string; model: string | null };
20
+ type TemplateRec = {
21
+ id: string;
22
+ name: string;
23
+ active: boolean;
24
+ groups: string[];
25
+ tasks: TemplateTask[];
26
+ };
27
+
28
+ const TEMPLATES_URL = `${hugpyConfig.apiBase}/llm/templates`;
29
+
30
+ export function TemplateStepsPanel({ templateId }: { templateId?: string }) {
31
+ const [templates, setTemplates] = useState<TemplateRec[]>([]);
32
+ const [picked, setPicked] = useState<string | null>(templateId ?? null);
33
+ const [models, setModels] = useState<string[]>([]);
34
+ const [rowBusy, setRowBusy] = useState<string | null>(null);
35
+ const [rowError, setRowError] = useState<Record<string, string>>({});
36
+ const [loadError, setLoadError] = useState<string | null>(null);
37
+
38
+ const load = useCallback(async () => {
39
+ const res = await request<unknown>(TEMPLATES_URL, {
40
+ method: "GET",
41
+ timeoutMs: 15_000,
42
+ meta: { operation: "templates.list" },
43
+ });
44
+ if (!res.ok) {
45
+ setLoadError(describeAppError(errorOf(res)));
46
+ return;
47
+ }
48
+ const value = okValue(res) as { templates?: TemplateRec[] } | null;
49
+ const rows = Array.isArray(value?.templates) ? value.templates : [];
50
+ setTemplates(rows);
51
+ setLoadError(null);
52
+ // Keep the caller's pick when it still exists; otherwise prefer the
53
+ // active template, then the first one. Never invent an id.
54
+ setPicked((cur) =>
55
+ cur && rows.some((t) => t.id === cur)
56
+ ? cur
57
+ : rows.find((t) => t.active)?.id ?? rows[0]?.id ?? null,
58
+ );
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ void load();
63
+ }, [load]);
64
+
65
+ // Offerable generators — same feed the assist picker uses (blocked models
66
+ // are excluded server-side). A discovery failure leaves the dropdowns with
67
+ // just the current value; saving is still possible via "unset".
68
+ useEffect(() => {
69
+ let alive = true;
70
+ (async () => {
71
+ const res = await request<unknown>(hugpyConfig.promptAssistModelsUrl, {
72
+ method: "GET",
73
+ timeoutMs: 15_000,
74
+ meta: { operation: "templates.models" },
75
+ });
76
+ if (!alive || !res.ok) return;
77
+ const value = okValue(res) as { models?: { model: string }[] } | null;
78
+ setModels(
79
+ (Array.isArray(value?.models) ? value.models : []).map((m) => m.model),
80
+ );
81
+ })();
82
+ return () => {
83
+ alive = false;
84
+ };
85
+ }, []);
86
+
87
+ const tpl = templates.find((t) => t.id === picked) ?? null;
88
+
89
+ const saveSlot = useCallback(
90
+ async (taskName: string, model: string | null) => {
91
+ if (!tpl) return;
92
+ setRowBusy(taskName);
93
+ const url = `${TEMPLATES_URL}/${encodeURIComponent(
94
+ tpl.id,
95
+ )}/tasks/${encodeURIComponent(taskName)}`;
96
+ const res = await request<unknown>(url, {
97
+ method: "POST",
98
+ body: JSON.stringify({ model }),
99
+ headers: { "Content-Type": "application/json" },
100
+ timeoutMs: 15_000,
101
+ meta: { operation: "templates.fill" },
102
+ });
103
+ setRowBusy(null);
104
+ if (!res.ok) {
105
+ setRowError((e) => ({ ...e, [taskName]: describeAppError(errorOf(res)) }));
106
+ return;
107
+ }
108
+ setRowError((e) => {
109
+ const { [taskName]: _gone, ...rest } = e;
110
+ return rest;
111
+ });
112
+ void load();
113
+ },
114
+ [tpl, load],
115
+ );
116
+
117
+ return (
118
+ <section className="vi-script-panel">
119
+ <h3>
120
+ Template steps
121
+ <span className="vi-script-sub">
122
+ the ordered plan — every step, and the model it calls
123
+ </span>
124
+ </h3>
125
+
126
+ {loadError && <p className="vi-script-note">{loadError}</p>}
127
+
128
+ <div className="vi-script-row">
129
+ {templates.map((t) => (
130
+ <button
131
+ key={t.id}
132
+ type="button"
133
+ className={`vi-btn vi-btn-sm${t.id === picked ? " vi-btn-on" : ""}`}
134
+ onClick={() => setPicked(t.id)}
135
+ >
136
+ {t.name}
137
+ {t.active ? " · active" : ""}
138
+ </button>
139
+ ))}
140
+ {templates.length === 0 && !loadError && (
141
+ <span className="vi-script-note">No templates in the registry.</span>
142
+ )}
143
+ </div>
144
+
145
+ {tpl && (
146
+ <>
147
+ <p className="vi-script-note">
148
+ groups (stage order): <code>{tpl.groups.join(" → ") || "—"}</code>
149
+ </p>
150
+ <ol className="vi-template-steps">
151
+ {tpl.tasks.map((task) => (
152
+ <li key={task.name} className="vi-template-step">
153
+ <div className="vi-template-step-head">
154
+ <code>{task.name}</code>
155
+ <select
156
+ className="vi-knob-input"
157
+ value={task.model ?? ""}
158
+ disabled={rowBusy === task.name}
159
+ onChange={(e) =>
160
+ void saveSlot(task.name, e.target.value || null)
161
+ }
162
+ >
163
+ <option value="">unset — router picks from groups</option>
164
+ {/* Keep a pinned model visible even when discovery
165
+ doesn't offer it right now (worker offline). */}
166
+ {task.model && !models.includes(task.model) && (
167
+ <option value={task.model}>{task.model}</option>
168
+ )}
169
+ {models.map((m) => (
170
+ <option key={m} value={m}>
171
+ {m}
172
+ </option>
173
+ ))}
174
+ </select>
175
+ {rowBusy === task.name && (
176
+ <span className="vi-script-note">saving…</span>
177
+ )}
178
+ </div>
179
+ <p className="vi-template-step-desc">{task.desc}</p>
180
+ {rowError[task.name] && (
181
+ <p className="vi-script-note">{rowError[task.name]}</p>
182
+ )}
183
+ </li>
184
+ ))}
185
+ </ol>
186
+ </>
187
+ )}
188
+ </section>
189
+ );
190
+ }
@@ -18,7 +18,6 @@ import {
18
18
  useToks,
19
19
  rollupByWorker,
20
20
  tokAgo,
21
- TOKS_POLL_MS,
22
21
  type TokEntry,
23
22
  type TokGroup,
24
23
  } from "../video/useToks";
@@ -153,7 +152,7 @@ export function ToksStation() {
153
152
  className={`vi-assistlog-dot ${live ? "vi-assistlog-dot--live" : "vi-assistlog-dot--disconnected"}`}
154
153
  aria-hidden
155
154
  />
156
- {live ? "live" : "paused"} · every {Math.round(TOKS_POLL_MS / 1000)}s
155
+ {live ? "live" : "paused"} · on activity
157
156
  </span>
158
157
  </div>
159
158
  <p className="vi-knob-hint">
File without changes
@@ -42,21 +42,18 @@ import {
42
42
  cancelJob,
43
43
  isJobActive,
44
44
  isJobTerminal,
45
- dismissAllTerminal,
46
45
  type TrackedJob,
47
46
  type StationId,
48
47
  } from "../video/jobTracker";
49
48
  import { STILL_RUNNING_MESSAGE } from "./pollCaps";
50
49
  import { requestAddPart, requestStage } from "../video/composerBridge";
51
50
  import { requestStudioStage } from "../video/studioBridge";
52
- import { useWorkerCalls, type WorkerCall } from "../video/useWorkerCalls";
53
51
  import { useSidebarRegistry } from "./SidebarRegistry";
54
52
  import { WorkbenchDrawer } from "./WorkbenchDrawer";
55
53
  import { SharePanel } from "./SharePanel";
56
54
  import {
57
55
  mediaBytesUrl,
58
56
  studioClipArchiveUrl,
59
- llmJobCancelUrl,
60
57
  } from "../config";
61
58
  import { request, errorOf, describeAppError } from "../transport/client";
62
59
  import type { MediaRef } from "../video/contract";
@@ -73,6 +70,8 @@ const STATION_NAME: Record<StationId, string> = {
73
70
  frames: "Frames & Models",
74
71
  generate: "Generate",
75
72
  "generate-tester": "Generate (tester)",
73
+ "generate-scene-parts": "Generate (scene parts)",
74
+ studio: "Studio Clip",
76
75
  };
77
76
 
78
77
  // The route a process row links to. It is the station id by default (id === route
@@ -82,6 +81,7 @@ const STATION_NAME: Record<StationId, string> = {
82
81
  // default studio sub-tab.
83
82
  const STATION_ROUTE: Partial<Record<StationId, string>> = {
84
83
  "video-crop": "studio-clips",
84
+ studio: "studio-clips",
85
85
  // The tester is a surface INSIDE the Generate station, not a route of its own —
86
86
  // without this the default (id === path) would link a tester row to /generate-tester,
87
87
  // which resolves to nothing.
@@ -190,182 +190,10 @@ function fmtAgo(ms: number): string {
190
190
  return `${m}m ${(total % 60).toString().padStart(2, "0")}s`;
191
191
  }
192
192
 
193
- // The scrolling log tail in the expanded panel: a monospace <pre> capped by CSS
194
- // max-height with its own scroll, pinned to the BOTTOM (newest line) whenever the
195
- // tail changes — so a live render's newest output is always in view without the
196
- // operator chasing it. Empty/absent tail degrades to a quiet "no log yet".
197
- function WorkerCallLog({ lines }: { lines: string[] }) {
198
- const ref = useRef<HTMLPreElement | null>(null);
199
- // Re-pin only when the content actually changes (count + newest line), not on
200
- // every parent re-render — otherwise a manual scroll-up would be yanked back
201
- // down every poll tick.
202
- const key = lines.length ? `${lines.length}:${lines[lines.length - 1]}` : "";
203
- useEffect(() => {
204
- const el = ref.current;
205
- if (el) el.scrollTop = el.scrollHeight;
206
- }, [key]);
207
- if (lines.length === 0) {
208
- return <p className="vi-proc-log-empty">No log yet.</p>;
209
- }
210
- return (
211
- <pre className="vi-proc-log" ref={ref} aria-label="Recent log output">
212
- {lines.join("\n")}
213
- </pre>
214
- );
215
- }
216
-
217
- // One in-flight WORKER CALL row (from the fleet-wide /llm/jobs feed) — the calls
218
- // this browser session did NOT launch (identity reconstruction, another station's
219
- // render, another transport). Presentational sibling of ProcessRow: no NavLink
220
- // (these jobs have no station route here), a unified hard-Cancel, and — unlike the
221
- // old flat row that read green-active even when wedged — an EXPANDABLE panel with a
222
- // live progress bar, current stage, an honest stalled state, and a scrolling log.
223
- function WorkerCallRow({
224
- call,
225
- now,
226
- cancelling,
227
- expanded,
228
- onToggle,
229
- onCancel,
230
- }: {
231
- call: WorkerCall;
232
- now: number;
233
- cancelling: boolean;
234
- expanded: boolean;
235
- onToggle: (id: string) => void;
236
- onCancel: (call: WorkerCall) => void;
237
- }) {
238
- const meta = [call.model, call.worker].filter(Boolean).join(" · ");
239
- // Honest stalled signal: the server said so, OR movement has been silent past the
240
- // stall window. Recomputed off `now` (the ~1s panel tick), so a live render that
241
- // freezes flips to stalled on its own without a status change.
242
- const lastMoveMs = call.updatedAt != null ? Math.max(0, now - call.updatedAt) : null;
243
- const stalled =
244
- call.stalled || (lastMoveMs != null && lastMoveMs > WORKER_CALL_STALL_MS);
245
- const pct = call.progress != null ? Math.round(call.progress * 100) : null;
246
- // A requested-and-acknowledged cancel also latches the button into "cancelling…"
247
- // (survives across polls until the row drops), not just the local in-flight click.
248
- const cancelBusy = cancelling || call.cancelRequested;
249
- return (
250
- <li className="vi-proc-item vi-proc-item-worker">
251
- <div className="vi-proc-top">
252
- <button
253
- type="button"
254
- className="vi-proc-toggle"
255
- aria-expanded={expanded}
256
- aria-label={expanded ? "Collapse details" : "Expand — progress, stage, log"}
257
- onClick={() => onToggle(call.id)}
258
- >
259
- <span className="vi-proc-caret" aria-hidden>
260
- {expanded ? "▾" : "▸"}
261
- </span>
262
- </button>
263
- {stalled ? (
264
- <span className="vi-proc-stall-dot" aria-hidden />
265
- ) : (
266
- <span className="vi-proc-spinner" aria-hidden />
267
- )}
268
- <span className="vi-proc-station">{humanizeWorkerKind(call.kind)}</span>
269
- <span className={`vi-status vi-status-${call.status}`}>{call.status}</span>
270
- {stalled && <span className="vi-proc-stall-badge">stalled</span>}
271
- {pct != null && <span className="vi-proc-progress">{pct}%</span>}
272
- <span className="vi-proc-elapsed">{fmtElapsed(now - call.startedAt)}</span>
273
- </div>
274
- {meta && <span className="vi-proc-label">{meta}</span>}
275
-
276
- {expanded && (
277
- <div className="vi-proc-detail">
278
- {/* Progress bar: determinate when the feed gives a fraction, otherwise an
279
- indeterminate "working" sweep — never a fabricated percentage. Goes
280
- amber when stalled so a frozen bar doesn't read as healthy progress. */}
281
- <div className="vi-proc-progbar-row">
282
- <div
283
- className={`vi-proc-progbar${
284
- pct == null ? " vi-proc-progbar-indeterminate" : ""
285
- }${stalled ? " vi-proc-progbar-stalled" : ""}`}
286
- role="progressbar"
287
- aria-valuemin={0}
288
- aria-valuemax={100}
289
- {...(pct != null ? { "aria-valuenow": pct } : {})}
290
- >
291
- <span
292
- className="vi-proc-progbar-fill"
293
- style={pct != null ? { width: `${pct}%` } : undefined}
294
- />
295
- </div>
296
- <span className="vi-proc-stage">
297
- {call.stage ? call.stage : pct == null ? "working…" : `${pct}%`}
298
- </span>
299
- </div>
300
-
301
- {call.message && <p className="vi-proc-detail-msg">{call.message}</p>}
302
-
303
- <div className="vi-proc-detail-meta">
304
- <span
305
- className={`vi-proc-lastmove${
306
- stalled ? " vi-proc-lastmove-stalled" : ""
307
- }`}
308
- >
309
- {lastMoveMs != null
310
- ? `last movement ${fmtAgo(lastMoveMs)} ago`
311
- : "last movement unknown"}
312
- </span>
313
- </div>
314
-
315
- <WorkerCallLog lines={call.logTail} />
316
- </div>
317
- )}
318
-
319
- <div className="vi-proc-msg">
320
- <button
321
- type="button"
322
- className="vi-btn vi-btn-sm vi-proc-btn"
323
- disabled={cancelBusy}
324
- onClick={() => onCancel(call)}
325
- title="Stop this worker call — a step already in flight finishes first"
326
- >
327
- {cancelBusy ? "Cancelling…" : "✕ Cancel"}
328
- </button>
329
- </div>
330
- </li>
331
- );
332
- }
333
-
334
- // STUDIO SCOPE (operator, 2026-07-31): the studio workbench's Active Processes must
335
- // list ONLY studio-originated work — NEVER a general fleet model call (a discord chat,
336
- // a web /v1 completion, another transport's LLM call). Those must not be visible here
337
- // and must not be cancellable from inside the studio. The fleet-wide /llm/jobs feed
338
- // carries every transport, so we keep only rows that are RENDER/MEDIA work:
339
- // * transport === "media" — the media/render bus (the authoritative signal: every
340
- // job on it is video-arm work — studio clips/movies, scene/image gen, identity
341
- // reconstruction/mesh/char360, mlt render), OR
342
- // * a known render/media job KIND (defensive fallback if a row's transport is blank).
343
- // A general LLM call (transport web/v1/discord/cli, kind chat/completion/…) is dropped.
344
- // The console-wide ActiveProcessesStation still shows the fleet superset — this scoping
345
- // is the studio sidebar's alone.
346
- const STUDIO_MEDIA_KINDS = new Set([
347
- "studio_i2v",
348
- "generate_studio_movie",
349
- "generate_scene",
350
- "generate_movie",
351
- "generate_image",
352
- "identity_reconstruction",
353
- "identity_mesh_build",
354
- "identity_video_extract",
355
- "mlt_render",
356
- "crop",
357
- "frame_extract",
358
- "audio_extract",
359
- ]);
360
-
361
- function isStudioOriginated(c: WorkerCall): boolean {
362
- return c.transport === "media" || STUDIO_MEDIA_KINDS.has(c.kind);
363
- }
364
-
365
- /** Non-done tracked jobs, newest first — the Active Processes tab body. */
193
+ /** Only live tracked jobs belong in Active Processes. */
366
194
  function activeJobsOf(records: TrackedJob[]): TrackedJob[] {
367
195
  return records
368
- .filter((r) => r.status !== "done")
196
+ .filter((r) => !isJobTerminal(r))
369
197
  .slice()
370
198
  .sort((a, b) => b.enqueuedAt - a.enqueuedAt);
371
199
  }
@@ -378,64 +206,8 @@ function ProcessesPanel() {
378
206
  const records = useJobTracker();
379
207
  const jobs = useMemo(() => activeJobsOf(records), [records]);
380
208
 
381
- // The fleet-wide in-flight worker calls (identity reconstruction, other stations'
382
- // renders, calls from any transport) — the work THIS browser session never
383
- // launched, so the session jobTracker can't see it. Merged below, DEDUPED against
384
- // the tracker: a /llm/jobs row whose id equals a TrackedJob.jobId is dropped (the
385
- // tracker row is richer — station link, progress, provenance — so it wins).
386
- const { calls, refetch } = useWorkerCalls();
387
- const trackerIds = useMemo(() => new Set(jobs.map((j) => j.jobId)), [jobs]);
388
- const workerCalls = useMemo(
389
- // Deduped against the session tracker AND scoped to studio-originated (media/
390
- // render) work only — a general fleet model call never surfaces or cancels here.
391
- () => calls.filter((c) => !trackerIds.has(c.id) && isStudioOriginated(c)),
392
- [calls, trackerIds],
393
- );
394
-
395
- // Terminal rows (failed / cancelled / expired) linger in this list after the
396
- // active ones drop off — this drives the opt-in "Clear finished" broom below.
397
- const hasTerminal = useMemo(() => jobs.some(isJobTerminal), [jobs]);
398
-
399
- // Per-id busy flag for the worker-call Cancel buttons (immediate feedback; the
400
- // next poll / the forced refetch drops the row once the server observes it).
401
- const [cancelling, setCancelling] = useState<Set<string>>(new Set());
402
- const onCancelWorkerCall = useCallback(
403
- async (call: WorkerCall) => {
404
- setCancelling((prev) => new Set(prev).add(call.id));
405
- // ONE unified hard-cancel for every transport: the feed's own cancel route
406
- // (POST /llm/jobs/<id>/cancel → {cancelled,status}) fans the stop out to the
407
- // right bus server-side, so the row no longer has to route media vs chat itself.
408
- await request<unknown>(llmJobCancelUrl(call.id), {
409
- method: "POST",
410
- meta: { specKey: "workers", operation: "worker.call.cancel" },
411
- });
412
- refetch();
413
- setCancelling((prev) => {
414
- const n = new Set(prev);
415
- n.delete(call.id);
416
- return n;
417
- });
418
- },
419
- [refetch],
420
- );
421
-
422
- // Per-id expanded state for the worker-call rows (progress / stage / stalled /
423
- // log). Keyed by job id so a row's panel stays open across poll ticks; a job that
424
- // finishes and drops from the feed just leaves a stale id in the set (harmless).
425
- const [expanded, setExpanded] = useState<Set<string>>(new Set());
426
- const toggleExpanded = useCallback((id: string) => {
427
- setExpanded((prev) => {
428
- const n = new Set(prev);
429
- if (n.has(id)) n.delete(id);
430
- else n.add(id);
431
- return n;
432
- });
433
- }, []);
434
-
435
- // Tick ~1s while anything is live so every elapsed clock stays current: tracker
436
- // actives/capped OR any in-flight worker call (those are always live).
437
- const ticking =
438
- jobs.some((r) => isJobActive(r) || r.capped) || workerCalls.length > 0;
209
+ // Tick ~1s while anything is live so every elapsed clock stays current.
210
+ const ticking = jobs.some((r) => isJobActive(r) || r.capped);
439
211
  const [now, setNow] = useState(() => Date.now());
440
212
  useEffect(() => {
441
213
  if (!ticking) {
@@ -452,7 +224,7 @@ function ProcessesPanel() {
452
224
  // target is stable; the empty copy is suppressed when a station has portaled rows in.
453
225
  const extraHost = <div className="vi-proc-extra" ref={reg.setActiveExtraHost} />;
454
226
 
455
- const total = jobs.length + workerCalls.length;
227
+ const total = jobs.length;
456
228
 
457
229
  if (total === 0) {
458
230
  return (
@@ -473,36 +245,11 @@ function ProcessesPanel() {
473
245
  <div className="vi-proc-header">
474
246
  <h2 className="vi-proc-title">Active processes</h2>
475
247
  <span className="vi-proc-count">{total}</span>
476
- {/* Only shown when there's a finished/failed row to clear; forgets the
477
- LOCAL terminal rows only (never server work), and leaves every
478
- still-running job polling and visible. */}
479
- {hasTerminal && (
480
- <button
481
- type="button"
482
- className="vi-btn vi-btn-sm vi-btn-ghost"
483
- onClick={() => dismissAllTerminal()}
484
- title="Remove finished and failed items from this view — your saved files on disk are kept."
485
- aria-label="Remove finished and failed jobs from this list — still-running jobs stay, and your saved files on disk are kept."
486
- >
487
- Clear finished
488
- </button>
489
- )}
490
248
  </div>
491
249
  <ul className="vi-proc-list">
492
250
  {jobs.map((job) => (
493
251
  <ProcessRow key={job.jobId} job={job} now={now} />
494
252
  ))}
495
- {workerCalls.map((call) => (
496
- <WorkerCallRow
497
- key={call.id}
498
- call={call}
499
- now={now}
500
- cancelling={cancelling.has(call.id)}
501
- expanded={expanded.has(call.id)}
502
- onToggle={toggleExpanded}
503
- onCancel={onCancelWorkerCall}
504
- />
505
- ))}
506
253
  </ul>
507
254
  {extraHost}
508
255
  </>
File without changes
File without changes
File without changes
File without changes