@crievo/omni 0.1.7 → 0.1.8
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/omnivideo.config.example.json +1 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/studio.md +1 -0
- package/studio/api/app.ts +4 -0
- package/studio/api/routes/cost.ts +13 -0
- package/studio/api/routes/me.ts +8 -0
- package/studio/api/services/agent-tools.ts +2 -2
- package/studio/api/services/cost.ts +22 -0
- package/studio/api/services/me.ts +67 -0
- package/studio/api/services/sessions.ts +8 -2
- package/studio/runner/runner.ts +57 -10
- package/studio/shared/omni-screens.ts +1 -1
- package/studio/shared/schemas/cost.ts +10 -0
- package/studio/shared/types/api.ts +74 -0
- package/studio/studio.ts +27 -2
- package/studio/web/components/AppShell.tsx +11 -3
- package/studio/web/components/FirstConnection.tsx +222 -0
- package/studio/web/components/OfflineNotice.tsx +59 -0
- package/studio/web/components/SetupSteps.tsx +63 -0
- package/studio/web/components/Sidebar.tsx +10 -25
- package/studio/web/components/StudioOffline.tsx +48 -50
- package/studio/web/components/UserCard.tsx +79 -0
- package/studio/web/components/icons.tsx +2 -0
- package/studio/web/hooks/useComputers.ts +55 -0
- package/studio/web/hooks/useCost.ts +11 -0
- package/studio/web/hooks/useMe.ts +32 -0
- package/studio/web/hooks/useSetupProgress.ts +54 -0
- package/studio/web/lib/me.ts +17 -0
- package/studio/web/lib/query-client.ts +6 -0
- package/studio/web/lib/settings-categories.ts +2 -2
- package/studio/web/lib/setup-retry.ts +29 -0
- package/studio/web/lib/studio-offline.ts +105 -59
- package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
- package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
- package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
- package/studio/web/pages/SettingsPage/index.tsx +15 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/setup-steps.css +163 -0
- package/studio/web/styles/components/user-card.css +121 -0
- package/studio/web/styles/index.css +2 -0
- package/studio/web/styles/pages/settings/shell.css +79 -0
- package/studio/web/styles/pages/settings/usage.css +47 -0
- package/studio/web/styles/pages/studio-offline.css +224 -38
- package/tools/cost.mjs +114 -0
- package/tools/lib/agents.mjs +3 -2
- package/tools/lib/channel.mjs +1 -1
- package/tools/lib/cloud-user.mjs +58 -0
- package/tools/lib/cost.mjs +357 -0
- package/tools/lib/deps.mjs +27 -12
- package/tools/lib/oauth-client.mjs +28 -14
- package/tools/lib/pages-style.mjs +67 -0
- package/tools/lib/pairing.mjs +1 -1
- package/tools/lib/schedule/schedule.mjs +1 -1
- package/tools/lib/self-update.mjs +122 -0
- package/tools/lib/setup-progress.mjs +315 -0
- package/tools/lib/up.mjs +176 -61
- package/tools/metrics.mjs +6 -1
- package/tools/omni.mjs +41 -16
- package/tools/omnichat.mjs +45 -9
- package/tools/platforms.mjs +75 -14
- package/tools/runner.mjs +6 -6
|
@@ -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
|
|
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
|
-
{
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* The menu's user card, under Omni's: the avatar, the name and the email of who is using the studio (hosted: the
|
|
2
|
+
Google account that signed in; local: the Omni login or the Claude Code account, hooks/useMe.ts). The Google
|
|
3
|
+
picture when there is one (or when it fails to load), else two initials. An icon button on the right signs out:
|
|
4
|
+
hosted, the Worker's POST /exit (the computers and the actions that went through are in Configurações → Agente
|
|
5
|
+
de IA); at 127.0.0.1, with the Omni account shown, `omni logout` for this computer, after a confirmation (a
|
|
6
|
+
Claude Code account has nothing to sign out of here). Collapsed menu: just the avatar, and the button takes its place while the pointer is on it,
|
|
7
|
+
as the sidebar toggle does over the logo. */
|
|
8
|
+
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { useMe } from "../hooks/useMe";
|
|
11
|
+
import { useRunner } from "../hooks/useRunner";
|
|
12
|
+
import { api, errorText, send } from "../lib/api";
|
|
13
|
+
import { displayNameOf, initialsOf } from "../lib/me";
|
|
14
|
+
import { queryClient, queryKeys } from "../lib/query-client";
|
|
15
|
+
import { toast, toastResult } from "../lib/toast";
|
|
16
|
+
import { SignOut } from "./icons";
|
|
17
|
+
|
|
18
|
+
const CONFIRM = "Sair da conta Omni neste computador? O plugin pede o login de novo na próxima sessão do Claude Code.";
|
|
19
|
+
|
|
20
|
+
/* the local studio's sign-out: asks first, then ends this computer's Omni login and rereads who is using the studio */
|
|
21
|
+
async function signOutHere() {
|
|
22
|
+
if (!confirm(CONFIRM)) return;
|
|
23
|
+
try {
|
|
24
|
+
toastResult(await send(api.me.logout.$post()), undefined, { always: true });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
toast.error("Algo deu errado", { body: errorText(e) });
|
|
27
|
+
}
|
|
28
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.me });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function UserCard({ collapsed }: { collapsed: boolean }) {
|
|
32
|
+
const me = useMe();
|
|
33
|
+
const { hosted } = useRunner();
|
|
34
|
+
const [broken, setBroken] = useState(false);
|
|
35
|
+
if (!me) return null;
|
|
36
|
+
const name = displayNameOf(me);
|
|
37
|
+
return (
|
|
38
|
+
<div className="menu-user" title={collapsed ? `${name} · ${me.email}` : undefined}>
|
|
39
|
+
{me.image && !broken ? (
|
|
40
|
+
<img
|
|
41
|
+
className="menu-user-photo"
|
|
42
|
+
src={me.image}
|
|
43
|
+
alt=""
|
|
44
|
+
width={36}
|
|
45
|
+
height={36}
|
|
46
|
+
referrerPolicy="no-referrer"
|
|
47
|
+
onError={() => setBroken(true)}
|
|
48
|
+
/>
|
|
49
|
+
) : (
|
|
50
|
+
<span className="menu-user-initials" aria-hidden="true">
|
|
51
|
+
{initialsOf(me)}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
<span className="menu-user-text menu-label">
|
|
55
|
+
<b>{name}</b>
|
|
56
|
+
<span>{me.email}</span>
|
|
57
|
+
</span>
|
|
58
|
+
{hosted ? (
|
|
59
|
+
<form method="post" action="/exit" className="menu-user-exit">
|
|
60
|
+
<button type="submit" className="menu-user-signout" title="Sair" aria-label="Sair">
|
|
61
|
+
<SignOut aria-hidden="true" />
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
) : (
|
|
65
|
+
me.source === "omni" && (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className="menu-user-signout"
|
|
69
|
+
title="Sair"
|
|
70
|
+
aria-label="Sair"
|
|
71
|
+
onClick={() => void signOutHere()}
|
|
72
|
+
>
|
|
73
|
+
<SignOut aria-hidden="true" />
|
|
74
|
+
</button>
|
|
75
|
+
)
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
FolderOpenIcon,
|
|
26
26
|
ImageDone01Icon,
|
|
27
27
|
Link01Icon,
|
|
28
|
+
Logout01Icon,
|
|
28
29
|
Plug01Icon,
|
|
29
30
|
PowerIcon,
|
|
30
31
|
SentIcon,
|
|
@@ -208,6 +209,7 @@ export const PlayCircle = icon(PlayCircleIcon, "PlayCircle");
|
|
|
208
209
|
export const Plug = icon(Plug01Icon, "Plug");
|
|
209
210
|
export const Plus = icon(Add01Icon, "Plus");
|
|
210
211
|
export const Power = icon(PowerIcon, "Power");
|
|
212
|
+
export const SignOut = icon(Logout01Icon, "SignOut");
|
|
211
213
|
export const Prohibit = icon(BlockedIcon, "Prohibit");
|
|
212
214
|
export const Queue = icon(LeftToRightListBulletIcon, "Queue");
|
|
213
215
|
export const Shuffle = icon(ShuffleIcon, "Shuffle");
|