@crievo/omni 0.1.7 → 0.1.9

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 (76) hide show
  1. package/omnivideo.config.example.json +2 -0
  2. package/package.json +1 -1
  3. package/reference/cloud.md +15 -1
  4. package/reference/learnings.md +7 -0
  5. package/reference/scenes-contract.md +3 -2
  6. package/reference/studio.md +3 -2
  7. package/studio/api/app.ts +4 -0
  8. package/studio/api/routes/cost.ts +13 -0
  9. package/studio/api/routes/me.ts +8 -0
  10. package/studio/api/services/agent-persona.ts +1 -0
  11. package/studio/api/services/agent-tools.ts +2 -2
  12. package/studio/api/services/cost.ts +22 -0
  13. package/studio/api/services/me.ts +67 -0
  14. package/studio/api/services/sessions.ts +8 -2
  15. package/studio/api/services/sound.ts +17 -6
  16. package/studio/runner/runner.ts +57 -10
  17. package/studio/shared/omni-screens.ts +1 -1
  18. package/studio/shared/schemas/cost.ts +10 -0
  19. package/studio/shared/types/api.ts +77 -1
  20. package/studio/studio.ts +27 -2
  21. package/studio/web/components/AppShell.tsx +11 -3
  22. package/studio/web/components/FirstConnection.tsx +222 -0
  23. package/studio/web/components/OfflineNotice.tsx +59 -0
  24. package/studio/web/components/SetupSteps.tsx +63 -0
  25. package/studio/web/components/Sidebar.tsx +10 -25
  26. package/studio/web/components/StudioOffline.tsx +48 -50
  27. package/studio/web/components/UserCard.tsx +79 -0
  28. package/studio/web/components/icons.tsx +2 -0
  29. package/studio/web/hooks/useComputers.ts +55 -0
  30. package/studio/web/hooks/useCost.ts +11 -0
  31. package/studio/web/hooks/useMe.ts +32 -0
  32. package/studio/web/hooks/useSetupProgress.ts +54 -0
  33. package/studio/web/lib/me.ts +17 -0
  34. package/studio/web/lib/query-client.ts +6 -0
  35. package/studio/web/lib/settings-categories.ts +2 -2
  36. package/studio/web/lib/setup-retry.ts +29 -0
  37. package/studio/web/lib/sound-timeline.ts +33 -0
  38. package/studio/web/lib/studio-offline.ts +105 -59
  39. package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
  40. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
  41. package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
  42. package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
  43. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
  44. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
  45. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
  46. package/studio/web/pages/SettingsPage/index.tsx +15 -1
  47. package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
  48. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +10 -8
  49. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +31 -18
  50. package/studio/web/router.tsx +8 -0
  51. package/studio/web/styles/components/setup-steps.css +163 -0
  52. package/studio/web/styles/components/user-card.css +121 -0
  53. package/studio/web/styles/index.css +2 -0
  54. package/studio/web/styles/pages/settings/shell.css +79 -0
  55. package/studio/web/styles/pages/settings/usage.css +47 -0
  56. package/studio/web/styles/pages/studio-offline.css +224 -38
  57. package/tools/cost.mjs +114 -0
  58. package/tools/lib/agents.mjs +3 -2
  59. package/tools/lib/channel.mjs +1 -1
  60. package/tools/lib/cloud-user.mjs +58 -0
  61. package/tools/lib/cost.mjs +357 -0
  62. package/tools/lib/deps.mjs +27 -12
  63. package/tools/lib/oauth-client.mjs +28 -14
  64. package/tools/lib/pages-style.mjs +67 -0
  65. package/tools/lib/pairing.mjs +1 -1
  66. package/tools/lib/schedule/schedule.mjs +1 -1
  67. package/tools/lib/self-update.mjs +122 -0
  68. package/tools/lib/setup-progress.mjs +315 -0
  69. package/tools/lib/up.mjs +176 -61
  70. package/tools/metrics.mjs +6 -1
  71. package/tools/omni.mjs +41 -16
  72. package/tools/omnichat.mjs +45 -9
  73. package/tools/platforms.mjs +75 -14
  74. package/tools/runner.mjs +6 -6
  75. package/tools/setup.mjs +18 -5
  76. package/tools/sound.mjs +198 -94
package/studio/studio.ts CHANGED
@@ -35,7 +35,9 @@ import { resumeAfterApproval, watchDeadline } from "./api/services/approval";
35
35
  import { startRunner } from "./runner/runner";
36
36
  import { cloudSettings, env, writeEnv } from "../tools/platforms.mjs";
37
37
  import { checkKey, finishPairing, startPairing } from "../tools/lib/pairing.mjs";
38
- import { ACCESS_PREFIX, accessToken } from "../tools/lib/oauth-client.mjs";
38
+ import { ACCESS_PREFIX, accessToken, login, readTokens } from "../tools/lib/oauth-client.mjs";
39
+ import { createSelfUpdater } from "../tools/lib/self-update.mjs";
40
+ import { setupRunning } from "../tools/lib/setup-progress.mjs";
39
41
  import { openUrl } from "../tools/lib/platform.mjs";
40
42
 
41
43
  const args = process.argv.slice(2);
@@ -146,6 +148,29 @@ if (args.includes("--runner") || (hosted.on && option("port") === undefined)) {
146
148
  };
147
149
  /* only a Phase 1 key is repaired by pairing; an OAuth login refreshes itself or asks for omni login */
148
150
  const check = async (t: string) => (t.startsWith(ACCESS_PREFIX) ? { valid: null } : checkKey(url, t));
149
- startRunner({ url, token, port, handle, checkKey: check, pair });
151
+ /* no usable login (never entered, signed out, disconnected): the approval page opens on this computer by itself */
152
+ const signIn = async () => {
153
+ /* the plugin's hook may be asking for this very login: wait for it instead of opening a second page */
154
+ for (let i = 0; i < 144 && setupRunning(); i++) await new Promise(r => setTimeout(r, 5000));
155
+ if (readTokens(url)) return;
156
+ await login(url, { deviceName: hostname().replace(/\.local$/, "") });
157
+ };
158
+ /* a machine behind the hosted studio updates itself: the hub closes the socket with UPGRADE, and this asks npm
159
+ whether a newer CLI exists (tools/lib/self-update.mjs); never on a checkout, once an hour */
160
+ const update = createSelfUpdater({
161
+ log: line => console.log(line),
162
+ busy: setupRunning,
163
+ loggedIn: () => !!readTokens(url),
164
+ });
165
+ startRunner({
166
+ url,
167
+ token,
168
+ port,
169
+ handle,
170
+ checkKey: check,
171
+ pair,
172
+ login: signIn,
173
+ onOutdated: async () => void (await update()),
174
+ });
150
175
  }
151
176
  }
@@ -1,10 +1,14 @@
1
1
  /* The root route component: the top banners, the menu, the page (<Outlet/>) inside the scrolling
2
- .content, the lightbox, the approval notice, Omni, the toasts and, over everything, the screen for a studio that lost its connection, the voice agent (its panel opens from the sidebar button). */
2
+ .content, the lightbox, the approval notice, Omni (the voice agent, its panel opens from the sidebar button) and
3
+ the toasts. While the studio isn't connected, its screen takes the page's place inside .content, with the menu
4
+ still there; the page stays mounted underneath, hidden, so what it held is back when the studio is. Settings →
5
+ Agente de IA stays reachable: the computers and the actions there come from the Worker, not from the studio. */
3
6
 
4
7
  import { useEffect, useRef } from "react";
5
8
  import { Outlet, useLocation, useMatchRoute } from "@tanstack/react-router";
6
9
  import { useMenuCollapsed } from "../hooks/useMenuCollapsed";
7
10
  import { useMissingSetupSteps } from "../hooks/useMissingSetupSteps";
11
+ import { useRunner } from "../hooks/useRunner";
8
12
  import { ApprovalNotice } from "./ApprovalNotice";
9
13
  import { TopBanners } from "./TopBanners";
10
14
  import { Lightbox } from "./Lightbox";
@@ -32,6 +36,9 @@ export function AppShell() {
32
36
  const [collapsed, toggleMenu] = useMenuCollapsed();
33
37
  const showGettingStarted = location.pathname === "/getting-started" || !!missing?.length;
34
38
  const withSidebar = !!matchRoute({ to: "/settings", fuzzy: true }) || !!matchRoute({ to: "/usage" });
39
+ const { online } = useRunner();
40
+ const onAgentSettings = !!matchRoute({ to: "/settings/$category", params: { category: "agent" } });
41
+ const offlineScreen = online === false && !onAgentSettings;
35
42
 
36
43
  /* the content panel scrolls, not the window: back to the top on every page change */
37
44
  const pageKey = pageKeyOf(location.pathname, location.searchStr);
@@ -51,15 +58,16 @@ export function AppShell() {
51
58
  showGettingStarted={showGettingStarted}
52
59
  />
53
60
  <div className="content" ref={content}>
54
- <main className={withSidebar ? "with-sidebar" : ""}>
61
+ <main className={withSidebar ? "with-sidebar" : ""} hidden={offlineScreen}>
55
62
  <Outlet />
56
63
  </main>
64
+ {/* always mounted: it is what sends the first time on to Primeiros passos once the studio answers */}
65
+ <StudioOffline suppressed={onAgentSettings} />
57
66
  </div>
58
67
  </div>
59
68
  <Lightbox />
60
69
  <ApprovalNotice />
61
70
  <OmniWidget menuCollapsed={collapsed} />
62
- <StudioOffline />
63
71
  <Toaster />
64
72
  </>
65
73
  );
@@ -0,0 +1,222 @@
1
+ /* The first time, inside the app (the sidebar stays). For a user who may never have opened a terminal: short lines,
2
+ icons and numbered steps, no paragraphs. A three-line welcome, then one vertical stepper that follows the whole
3
+ first run: until the computer reports, the three things to do (open Claude, paste the command, sign in), with what
4
+ the command does under its card; from then on, the two done and the setup's steps as the CLI finishes them. Nothing
5
+ here names the CLI's package or a script: the plugin is the way in, and its hook opens the login and runs the
6
+ setup by itself. */
7
+
8
+ import { useState } from "react";
9
+ import { useNow } from "../hooks/useNow";
10
+ import { toast } from "../lib/toast";
11
+ import { PLUGIN_INSTALL } from "../lib/studio-offline";
12
+ import type { SetupRecord, SetupStep } from "../types/api";
13
+ import { Badge } from "./ui/badge";
14
+ import { Panel } from "./ui/panel";
15
+ import { SetupSteps, type StepRetry } from "./SetupSteps";
16
+ import { Check, Copy, Plug, VideoCamera, Warning } from "./icons";
17
+
18
+ const WELCOME = [
19
+ { Icon: VideoCamera, title: "Faz o vídeo inteiro", text: "Roteiro, animação, voz e legendas" },
20
+ { Icon: Plug, title: "Roda no seu computador", text: "Com o seu Claude Code" },
21
+ { Icon: Check, title: "Você só aprova", text: "O roteiro e as cenas" },
22
+ ];
23
+
24
+ /* how long after the last step the studio may take to answer before the screen says something is off */
25
+ const SLOW_MS = 90_000;
26
+
27
+ function Command({ command }: { command: string }) {
28
+ const [copied, setCopied] = useState(false);
29
+ const copy = async () => {
30
+ try {
31
+ await navigator.clipboard.writeText(command);
32
+ setCopied(true);
33
+ setTimeout(() => setCopied(false), 1800);
34
+ } catch {
35
+ toast.error("Não consegui copiar", { body: "Selecione o comando e copie à mão." });
36
+ }
37
+ };
38
+ return (
39
+ <div className="connect-command">
40
+ <code>{command}</code>
41
+ <button type="button" className="button outlined" onClick={copy}>
42
+ {copied ? (
43
+ <Check className="ic" weight="bold" aria-hidden="true" />
44
+ ) : (
45
+ <Copy className="ic" aria-hidden="true" />
46
+ )}
47
+ {copied ? "Copiado" : "Copiar"}
48
+ </button>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ /* the little window of the approval page, so the user knows what to look for */
54
+ function ApprovalMock() {
55
+ return (
56
+ <div className="connect-mock" aria-hidden="true">
57
+ <div className="connect-mock-bar">
58
+ <i />
59
+ <i />
60
+ <i />
61
+ </div>
62
+ <div className="connect-mock-body">
63
+ <b>Conectar este computador?</b>
64
+ <span className="connect-mock-lines">
65
+ <i />
66
+ <i />
67
+ </span>
68
+ <span className="connect-mock-buttons">
69
+ <em>Conectar</em>
70
+ <em className="outline">Recusar</em>
71
+ </span>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ /* before the computer reports anything: what to do, one thing per step */
78
+ function InstallSteps() {
79
+ return (
80
+ <ol className="setup-steps install-steps" aria-label="Como conectar">
81
+ <li className="setup-step todo">
82
+ <span className="setup-mark" aria-hidden="true">
83
+ 1
84
+ </span>
85
+ <span className="setup-text">
86
+ <b>Abra uma sessão do Claude Code</b>
87
+ <span className="setup-detail">Uma conversa nova, como você já faz.</span>
88
+ </span>
89
+ </li>
90
+ <li className="setup-step todo">
91
+ <span className="setup-mark" aria-hidden="true">
92
+ 2
93
+ </span>
94
+ <span className="setup-text">
95
+ <b>Cole e rode este comando</b>
96
+ <Command command={PLUGIN_INSTALL} />
97
+ <span className="connect-does-title">O que ele faz</span>
98
+ <ul className="connect-does">
99
+ <li>
100
+ <Check weight="bold" aria-hidden="true" /> Adiciona o Omni ao seu Claude Code
101
+ </li>
102
+ <li>
103
+ <Check weight="bold" aria-hidden="true" /> Prepara o computador sozinho, sem você digitar mais nada
104
+ </li>
105
+ </ul>
106
+ </span>
107
+ </li>
108
+ <li className="setup-step todo">
109
+ <span className="setup-mark" aria-hidden="true">
110
+ 3
111
+ </span>
112
+ <span className="setup-text">
113
+ <b>Entre na sua conta</b>
114
+ <span className="setup-detail">Uma janela abre sozinha: entre com o Google e clique em Conectar.</span>
115
+ <ApprovalMock />
116
+ </span>
117
+ </li>
118
+ </ol>
119
+ );
120
+ }
121
+
122
+ /* the plugin and the login are behind us once the computer reports: they head the stepper, done */
123
+ const DONE_BEFORE = (device: string): SetupStep[] => [
124
+ { id: "plugin", label: "Plugin instalado", detail: "O Omni já está no seu Claude Code.", state: "done" },
125
+ {
126
+ id: "login",
127
+ label: "Conta conectada",
128
+ detail: device ? `Computador: ${device}` : "Este computador entrou.",
129
+ state: "done",
130
+ },
131
+ ];
132
+
133
+ export function FirstConnection({ setup, retry }: { setup: SetupRecord | null; retry?: StepRetry }) {
134
+ const now = useNow(5000);
135
+ const finished = setup?.steps.filter(s => s.state === "done").length ?? 0;
136
+ const failed = setup?.state === "error";
137
+ const waitingStudio = setup?.state === "done";
138
+ const slow = waitingStudio && now - setup.updated_at > SLOW_MS;
139
+ const asking = !!retry?.pending;
140
+ return (
141
+ <div className="page connect">
142
+ <header className="header">
143
+ <h1>
144
+ <em>Bem-vindo</em> ao Omni
145
+ </h1>
146
+ <span className="subtle">Vamos conectar o seu computador. Leva uns minutos, só uma vez.</span>
147
+ </header>
148
+
149
+ <ul className="connect-welcome" aria-label="O que o Omni faz">
150
+ {WELCOME.map(({ Icon, title, text }) => (
151
+ <li key={title}>
152
+ <span className="connect-welcome-icon" aria-hidden="true">
153
+ <Icon />
154
+ </span>
155
+ <b>{title}</b>
156
+ <span>{text}</span>
157
+ </li>
158
+ ))}
159
+ </ul>
160
+
161
+ {!setup && (
162
+ <Panel title="Conectar o seu computador" extra={<Badge variant="running">3 passos</Badge>}>
163
+ <InstallSteps />
164
+ <p className="connect-status" role="status">
165
+ <span className="setup-spin" aria-hidden="true" /> Esperando o computador. Esta tela avança sozinha.
166
+ </p>
167
+ </Panel>
168
+ )}
169
+
170
+ {setup && (
171
+ <Panel
172
+ title="Preparando o seu computador"
173
+ extra={
174
+ failed ? (
175
+ <Badge variant="error">Uma etapa não terminou</Badge>
176
+ ) : waitingStudio ? (
177
+ <Badge variant="running">
178
+ <span className="spinning" aria-hidden="true" /> Conectando
179
+ </Badge>
180
+ ) : (
181
+ <Badge variant="running">
182
+ {finished} de {setup.steps.length}
183
+ </Badge>
184
+ )
185
+ }
186
+ >
187
+ <SetupSteps steps={[...DONE_BEFORE(setup.device), ...setup.steps]} retry={retry} />
188
+ {failed && asking ? (
189
+ <p className="connect-status" role="status">
190
+ <span className="setup-spin" aria-hidden="true" /> Pedi para o computador tentar de novo.
191
+ </p>
192
+ ) : failed && retry?.listening ? (
193
+ <p className="connect-status warning" role="status">
194
+ <Warning aria-hidden="true" /> Algo não deu certo. Use “Tentar de novo” na etapa marcada.
195
+ </p>
196
+ ) : failed ? (
197
+ <p className="connect-status warning" role="status">
198
+ <Warning aria-hidden="true" /> O computador parou de responder. Abra uma nova sessão do Claude Code: o
199
+ plugin retoma de onde parou.
200
+ </p>
201
+ ) : waitingStudio ? (
202
+ <p className={`connect-status${slow ? " warning" : ""}`} role="status">
203
+ {slow ? (
204
+ <>
205
+ <Warning aria-hidden="true" /> Está demorando. Abra uma nova sessão do Claude Code.
206
+ </>
207
+ ) : (
208
+ <>
209
+ <span className="setup-spin" aria-hidden="true" /> Conectando ao studio, em até 30 segundos.
210
+ </>
211
+ )}
212
+ </p>
213
+ ) : (
214
+ <p className="connect-status" role="status">
215
+ <span className="setup-spin" aria-hidden="true" /> Pode deixar rodando. Só acontece desta vez.
216
+ </p>
217
+ )}
218
+ </Panel>
219
+ )}
220
+ </div>
221
+ );
222
+ }
@@ -0,0 +1,59 @@
1
+ /* "O studio perdeu a conexão" and its kin, as a pure view (the advice comes from lib/studio-offline.ts): what
2
+ happened in one line, what the computer is doing to come back as the same vertical stepper as the setup's, and the
3
+ two buttons. The last-resort line shows only after the trouble has lasted a while. StudioOffline.tsx feeds it from
4
+ the hub; the preview page (pages/ConnectPreviewPage) feeds it samples. */
5
+
6
+ import { Link } from "@tanstack/react-router";
7
+ import { useNow } from "../hooks/useNow";
8
+ import type { OfflineAdvice } from "../lib/studio-offline";
9
+ import { ArrowClockwise, Plug, Warning } from "./icons";
10
+ import { SetupSteps } from "./SetupSteps";
11
+ import { Panel } from "./ui/panel";
12
+
13
+ export function OfflineNotice({
14
+ advice,
15
+ checking,
16
+ onCheck,
17
+ }: {
18
+ advice: OfflineAdvice;
19
+ checking: boolean;
20
+ onCheck: () => void;
21
+ }) {
22
+ const now = useNow(5000);
23
+ const since = advice.since ? new Date(advice.since).getTime() : null;
24
+ const stuck = since !== null && now - since >= advice.fallbackAfterMs;
25
+ return (
26
+ <div className="page studio-offline" role="alert">
27
+ <div className="studio-offline-body">
28
+ <span className="studio-offline-icon" aria-hidden="true">
29
+ <Plug />
30
+ </span>
31
+ <h1>{advice.title}</h1>
32
+ <p className="studio-offline-text">{advice.text}</p>
33
+ <Panel title="O que está acontecendo" className="studio-offline-card">
34
+ <SetupSteps steps={advice.steps} />
35
+ <p className={`connect-status${stuck ? " warning" : ""}`} role="status">
36
+ {stuck ? (
37
+ <>
38
+ <Warning aria-hidden="true" /> {advice.fallback}
39
+ </>
40
+ ) : (
41
+ <>
42
+ <span className="setup-spin" aria-hidden="true" /> Esta tela atualiza sozinha.
43
+ </>
44
+ )}
45
+ </p>
46
+ </Panel>
47
+ <div className="actions studio-offline-actions">
48
+ <button type="button" className="button primary" onClick={onCheck} disabled={checking}>
49
+ <ArrowClockwise className={`ic${checking ? " spin" : ""}`} aria-hidden="true" />{" "}
50
+ {checking ? "Verificando…" : "Verificar agora"}
51
+ </button>
52
+ <Link to="/settings/$category" params={{ category: "agent" }} className="button">
53
+ <Plug className="ic" aria-hidden="true" /> Computadores conectados
54
+ </Link>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,63 @@
1
+ /* The first-time setup as a vertical stepper (docs: tools/lib/setup-progress.mjs reports it): a circle on the
2
+ left of each step, loading while it runs and a check, in emerald, when it is done. The steps are named for
3
+ what they are for ("Configuração para extrair o estilo visual"), never for the program behind them. A step that
4
+ failed offers "Tentar de novo" while the computer is there to hear it (lib/setup-retry.ts); asked, it waits for
5
+ the computer and loads again. */
6
+
7
+ import { isWaiting } from "../lib/setup-retry";
8
+ import type { SetupStep, SetupStepState } from "../types/api";
9
+ import { ArrowClockwise, Check, X } from "./icons";
10
+
11
+ const SAID: Record<SetupStepState, string> = {
12
+ pending: "Aguardando",
13
+ running: "Em andamento",
14
+ done: "Concluído",
15
+ error: "Não terminou",
16
+ };
17
+
18
+ export type StepRetry = {
19
+ /* someone is on the computer to hear a request */
20
+ listening: boolean;
21
+ /* the step asked for again and not answered yet ("all" included) */
22
+ pending: string | null;
23
+ onRetry: (stepId: string) => void;
24
+ };
25
+
26
+ export function SetupSteps({ steps, retry }: { steps: SetupStep[]; retry?: StepRetry }) {
27
+ return (
28
+ <ol className="setup-steps" aria-label="Etapas da configuração" aria-live="polite">
29
+ {steps.map(s => {
30
+ /* asked again: it loads while the computer picks it up, before the report says so */
31
+ const waiting = isWaiting(s.id, s.state, retry?.pending ?? null);
32
+ const state: SetupStepState = waiting ? "running" : s.state;
33
+ return (
34
+ <li key={s.id} className={`setup-step ${state}`} aria-current={state === "running" ? "step" : undefined}>
35
+ <span className="setup-mark" aria-hidden="true">
36
+ {state === "done" && <Check weight="bold" />}
37
+ {state === "error" && <X weight="bold" />}
38
+ {state === "running" && <span className="setup-spin" />}
39
+ </span>
40
+ <span className="setup-text">
41
+ <b>{s.label}</b>
42
+ <span className="setup-detail">
43
+ {state === "error"
44
+ ? "Não consegui terminar esta etapa."
45
+ : waiting
46
+ ? "Pedi para o computador tentar de novo."
47
+ : s.detail}
48
+ </span>
49
+ {state === "error" && s.error && <code className="setup-error">{s.error}</code>}
50
+ </span>
51
+ {state === "error" && retry?.listening ? (
52
+ <button type="button" className="button outlined setup-retry" onClick={() => retry.onRetry(s.id)}>
53
+ <ArrowClockwise className="ic" aria-hidden="true" /> Tentar de novo
54
+ </button>
55
+ ) : (
56
+ <span className="setup-said">{waiting ? "Aguardando o computador" : SAID[state]}</span>
57
+ )}
58
+ </li>
59
+ );
60
+ })}
61
+ </ol>
62
+ );
63
+ }
@@ -2,8 +2,9 @@
2
2
  Gerenciamento below, then Configurações in a section of its own, with no title. Each item: a duotone
3
3
  icon, like the ElevenLabs sidebar. The queue has no item: it lives in the "Agente, fila e serviços"
4
4
  panel and opens at /queue from its link.
5
- At the bottom sits Omni's card (the avatar and "Falar com Omni"), with Primeiros passos above it only
6
- while setup is missing: it opens the agent's panel (components/Omni).
5
+ At the bottom sits Omni's card (the avatar and "Falar com Omni"), with Primeiros passos and, for the admin, Admin
6
+ above it (Primeiros passos only while setup is missing): it opens the agent's panel (components/Omni). Under it,
7
+ the user's card (who is using the studio; in the hosted studio, with the Sair icon on its right).
7
8
  Active state: useMatchRoute over each item's routes. */
8
9
 
9
10
  import type { ReactNode } from "react";
@@ -11,12 +12,12 @@ import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
11
12
  import { useAdmin } from "../hooks/useAdmin";
12
13
  import { BenchoScope } from "./bencho/BenchoScope";
13
14
  import { CreateMenu, type CreateMenuItem } from "./bencho/CreateMenu";
14
- import { useRunner } from "../hooks/useRunner";
15
15
  import { omniLevel, toggleOmniPanel, useOmniPanel } from "../lib/omni-panel";
16
16
  import { OmniGlow } from "./Omni/OmniGlow";
17
17
  import { OMNI_AVATAR } from "./Omni/OmniWidget";
18
18
  import { AvatarGroupIcon } from "./AvatarGroupIcon";
19
19
  import { BrandLogo } from "./BrandLogo";
20
+ import { UserCard } from "./UserCard";
20
21
  import { VoiceAvatar } from "./VoiceAvatar";
21
22
  import {
22
23
  CalendarBlank,
@@ -26,9 +27,7 @@ import {
26
27
  ArrowExpand,
27
28
  GridFour,
28
29
  Minus,
29
- Plug,
30
30
  Play,
31
- Power,
32
31
  ShieldCheck,
33
32
  SidebarLeft,
34
33
  Sparkle,
@@ -205,7 +204,6 @@ export function Sidebar({
205
204
  }) {
206
205
  const isActive = useIsActive();
207
206
  const createItems = useCreateItems();
208
- const { hosted } = useRunner();
209
207
  const { isAdmin } = useAdmin();
210
208
  const omni = useOmniPanel();
211
209
  return (
@@ -269,6 +267,11 @@ export function Sidebar({
269
267
  </NavLink>
270
268
  </nav>
271
269
  )}
270
+ {isAdmin && (
271
+ <nav aria-label={ADMIN_NAV.name}>
272
+ <NavLink item={ADMIN_NAV} collapsed={collapsed} active={isActive(ADMIN_NAV)} />
273
+ </nav>
274
+ )}
272
275
  {omni.available && (
273
276
  <OmniGlow
274
277
  state={collapsed ? "off" : omni.glow}
@@ -303,25 +306,7 @@ export function Sidebar({
303
306
  </button>
304
307
  </OmniGlow>
305
308
  )}
306
- {isAdmin && (
307
- <nav aria-label={ADMIN_NAV.name}>
308
- <NavLink item={ADMIN_NAV} collapsed={collapsed} active={isActive(ADMIN_NAV)} />
309
- </nav>
310
- )}
311
- {/* the hosted studio's own pages (the Worker renders them, not this app): the paired Macs and the log of
312
- actions, and signing out */}
313
- {hosted && (
314
- <nav aria-label="Conta">
315
- <a href="/runner" title={collapsed ? "Macs e ações" : undefined}>
316
- <Plug aria-hidden="true" />
317
- <span className="menu-label">Macs e ações</span>
318
- </a>
319
- <a href="/exit" title={collapsed ? "Sair" : undefined}>
320
- <Power aria-hidden="true" />
321
- <span className="menu-label">Sair</span>
322
- </a>
323
- </nav>
324
- )}
309
+ <UserCard collapsed={collapsed} />
325
310
  </div>
326
311
  </aside>
327
312
  );
@@ -1,23 +1,39 @@
1
- /* The hosted studio while the studio isn't connected to it (the Mac that runs it is off, asleep or
2
- offline). A screen of its own, like the 404, over the whole app: nothing behind it can act, so the page
3
- says what happened, what to do on the Mac to resume (lib/studio-offline.ts, from what the hub knows: a
4
- refused key, an old version, a disconnect on purpose), and checks again by itself every few seconds.
5
- When the studio answers, the screen goes away and the app underneath (kept mounted, with its state)
6
- rereads everything. Not shown at 127.0.0.1 (the page's own server is the studio). */
1
+ /* The hosted studio while the studio isn't connected to it (the Mac that runs it is off, asleep, offline, or was
2
+ never connected). It is a screen inside the app, in the content area with the sidebar next to it, not a cover
3
+ over everything: the app underneath stays mounted (hidden, with its state), and when the studio answers this
4
+ goes away and everything rereads. Not shown at 127.0.0.1 (the page's own server is the studio).
7
5
 
8
- import { useEffect } from "react";
6
+ Two cases. The first time (no computer ever entered this account, or the CLI is reporting its setup) is
7
+ components/FirstConnection.tsx: a welcome, the plugin and the setup's steps. A computer that was there and is
8
+ away gets what happened and what brings it back (lib/studio-offline.ts), always through the plugin. Either way
9
+ it asks again every few seconds, and the first time hands over to Primeiros passos once the studio is up. */
10
+
11
+ import { useEffect, useRef } from "react";
12
+ import { useNavigate } from "@tanstack/react-router";
9
13
  import { useQueryClient } from "@tanstack/react-query";
10
14
  import { useRunner } from "../hooks/useRunner";
15
+ import { useSetupProgress, useSetupRetry } from "../hooks/useSetupProgress";
11
16
  import { queryKeys } from "../lib/query-client";
12
- import { offlineAdvice } from "../lib/studio-offline";
13
- import { ArrowClockwise, Plug, Power } from "./icons";
17
+ import { currentSetup, isFirstConnection, offlineAdvice } from "../lib/studio-offline";
18
+ import { FirstConnection } from "./FirstConnection";
19
+ import { OfflineNotice } from "./OfflineNotice";
14
20
 
15
21
  const CHECK_MS = 5000;
16
22
 
17
- export function StudioOffline() {
23
+ export function StudioOffline({ suppressed = false }: { suppressed?: boolean }) {
18
24
  const { online, name, lastSeen, problem, checking, check } = useRunner();
19
25
  const qc = useQueryClient();
26
+ const navigate = useNavigate();
20
27
  const offline = online === false;
28
+ const { setup: reported, retryRequest } = useSetupProgress(offline);
29
+ /* an account no computer ever entered keeps showing its setup however old it is (the screen says it is slow);
30
+ one that had a computer only counts a recent one, so last week's finished setup isn't today's outage */
31
+ const never = isFirstConnection({ name, lastSeen, problem });
32
+ const setup = never ? reported : currentSetup(reported);
33
+ const first = never || !!setup;
34
+ const retry = useSetupRetry(setup, retryRequest);
35
+ /* the first time ends in Primeiros passos, not wherever the page happened to be */
36
+ const wasFirst = useRef(false);
21
37
 
22
38
  /* not connected: ask again until it answers; back: everything on screen is stale */
23
39
  useEffect(() => {
@@ -30,51 +46,33 @@ export function StudioOffline() {
30
46
  }, [offline, qc]);
31
47
 
32
48
  useEffect(() => {
33
- if (!offline) return;
49
+ if (offline && first) wasFirst.current = true;
50
+ if (online && wasFirst.current) {
51
+ wasFirst.current = false;
52
+ void navigate({ to: "/getting-started" });
53
+ }
54
+ }, [offline, online, first, navigate]);
55
+
56
+ useEffect(() => {
57
+ if (!offline || suppressed) return;
34
58
  const before = document.title;
35
- document.title = "Studio sem conexão · Omni";
59
+ document.title = `${first ? "Conectar o computador" : "Studio sem conexão"} · Omni`;
36
60
  return () => {
37
61
  document.title = before;
38
62
  };
39
- }, [offline]);
63
+ }, [offline, suppressed, first]);
64
+
65
+ if (!offline || suppressed) return null;
66
+ if (first)
67
+ return (
68
+ <main>
69
+ <FirstConnection setup={setup} retry={retry} />
70
+ </main>
71
+ );
40
72
 
41
- if (!offline) return null;
42
- const advice = offlineAdvice({ name, lastSeen, problem });
43
73
  return (
44
- <div className="studio-offline" role="alert">
45
- <div className="studio-offline-body">
46
- <span className="studio-offline-icon" aria-hidden="true">
47
- <Plug />
48
- </span>
49
- <h1>{advice.title}</h1>
50
- <p className="studio-offline-text">{advice.text}</p>
51
- <ol className="studio-offline-steps">
52
- {advice.steps.map(st => (
53
- <li key={st.text}>
54
- {st.text}
55
- {st.code && (
56
- <>
57
- {" "}
58
- <code>{st.code}</code>
59
- </>
60
- )}
61
- </li>
62
- ))}
63
- </ol>
64
- <div className="actions studio-offline-actions">
65
- <button type="button" className="button primary" onClick={check} disabled={checking}>
66
- <ArrowClockwise className={`ic${checking ? " spin" : ""}`} aria-hidden="true" />{" "}
67
- {checking ? "Verificando…" : "Tentar de novo"}
68
- </button>
69
- <a href="/runner" className="button">
70
- <Plug className="ic" aria-hidden="true" /> Macs e ações
71
- </a>
72
- <a href="/exit" className="button">
73
- <Power className="ic" aria-hidden="true" /> Sair
74
- </a>
75
- </div>
76
- <p className="studio-offline-hint">Verifico sozinho a cada 5 segundos e volto para onde você estava.</p>
77
- </div>
78
- </div>
74
+ <main>
75
+ <OfflineNotice advice={offlineAdvice({ name, lastSeen, problem })} checking={checking} onCheck={check} />
76
+ </main>
79
77
  );
80
78
  }