@hanzo/rooms 0.1.2 → 0.1.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.
package/dist/Meet.js CHANGED
@@ -1,27 +1,31 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef, useCallback } from 'react';
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { Video, VideoOff, Mic, MicOff, PhoneOff, Copy, Check, Plus, Calendar, ExternalLink, Bot, Volume2, Square, Send, UserPlus, Tv, LayoutGrid, ArrowRight, } from 'lucide-react';
5
5
  import { Text, View, XStack, YStack } from '@hanzo/gui';
6
6
  import { Button } from '@hanzo/ui';
7
+ import { useIamToken } from '@hanzo/iam/react';
8
+ import { REFUSED, speech, useVoice } from '@hanzo/voice';
7
9
  import { Face, voiceOf } from './cast.js';
8
10
  import { member } from './team.js';
9
- import { speakAgent, stopAgentSpeech, AudioWave, useAgentSpeech } from './speech.js';
11
+ import { cleanForSpeech, speakAgent, stopAgentSpeech, AudioWave, useAgentSpeech } from './speech.js';
10
12
  import { GOOD } from './lib/mix.js';
13
+ import { api } from './lib/api.js';
14
+ import { ENSO, base } from './lib/ai.js';
11
15
  /** A core member as a meeting seats them: the team's name and part, the cast's voice. */
12
16
  const seated = (id) => {
13
17
  const one = member(id);
14
18
  return { id, name: one.name, role: one.role, voice: voiceOf(id) };
15
19
  };
16
20
  export const AVAILABLE_MEETING_AGENTS = [
17
- { id: 'hanzo-coder', name: 'Hanzo Coder', role: 'Engineering Lead & Coder', voice: 'ash' },
18
- { id: 'hanzo-researcher', name: 'Hanzo Researcher', role: 'Research & DeSci Scientist', voice: 'fable' },
21
+ { id: 'hanzo-coder', name: 'Hanzo Coder', role: 'Engineering Lead & Coder', voice: voiceOf('Hanzo Coder') },
22
+ { id: 'hanzo-researcher', name: 'Hanzo Researcher', role: 'Research & DeSci Scientist', voice: voiceOf('Hanzo Researcher') },
19
23
  seated('maya'),
20
24
  seated('des'),
21
25
  seated('vi'),
22
26
  seated('nora'),
23
27
  seated('einstein'),
24
- { id: 'zach', name: 'Zach', role: 'Quantitative Analyst', voice: 'onyx' },
28
+ { id: 'zach', name: 'Zach', role: 'Quantitative Analyst', voice: voiceOf('zach') },
25
29
  ];
26
30
  const DEFAULT_MEETINGS = [
27
31
  {
@@ -64,65 +68,37 @@ const DEFAULT_MEETINGS = [
64
68
  url: 'https://meet.hanzo.ai/security-sync',
65
69
  },
66
70
  ];
67
- async function generateAgentMeetingResponse(prompt, agentName) {
68
- const token = typeof window !== 'undefined' ? localStorage.getItem('hanzo_iam_access_token') : null;
69
- if (token) {
70
- try {
71
- const res = await fetch('https://api.hanzo.ai/v1/chat/completions', {
72
- method: 'POST',
73
- headers: {
74
- 'Content-Type': 'application/json',
75
- Authorization: `Bearer ${token}`,
71
+ /**
72
+ * The co-pilot's answer to one thing said in the call, in a sentence or two a
73
+ * voice can read. Asked of the gateway as the reader (`token`, from
74
+ * `useIamToken`) through the one API host; a refusal is thrown with what the
75
+ * gateway said, never answered with a line nobody wrote.
76
+ */
77
+ async function generateAgentMeetingResponse(prompt, agentName, token) {
78
+ if (!token)
79
+ throw new Error('Sign in to ask the co-pilot.');
80
+ const res = await fetch(`${api()}/v1/chat/completions`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
83
+ body: JSON.stringify({
84
+ model: ENSO,
85
+ messages: [
86
+ {
87
+ role: 'system',
88
+ content: `You are ${agentName}, participating in a live Hanzo Meet video call. Respond succinctly, conversationally, and informatively in 1 to 2 sentences suitable for voice synthesis.`,
76
89
  },
77
- body: JSON.stringify({
78
- model: 'gpt-4o-mini',
79
- messages: [
80
- {
81
- role: 'system',
82
- content: `You are ${agentName}, participating in a live Hanzo Meet video call. Respond succinctly, conversationally, and informatively in 1 to 2 sentences suitable for voice synthesis.`,
83
- },
84
- {
85
- role: 'user',
86
- content: prompt,
87
- },
88
- ],
89
- max_tokens: 150,
90
- }),
91
- });
92
- if (res.ok) {
93
- const data = await res.json();
94
- const text = data.choices?.[0]?.message?.content?.trim();
95
- if (text)
96
- return text;
97
- }
98
- }
99
- catch { }
100
- }
101
- // Domain-specific intelligent persona responses
102
- const lower = prompt.toLowerCase();
103
- const agent = agentName.toLowerCase();
104
- if (agent.includes('coder')) {
105
- if (lower.includes('audio') || lower.includes('webrtc') || lower.includes('media')) {
106
- return 'The WebRTC audio track is streaming over LiveKit SFU at 24kHz with PCM capture and real-time turn-taking.';
107
- }
108
- if (lower.includes('deploy') || lower.includes('build') || lower.includes('status')) {
109
- return 'All services and container builds are verified green across Kubernetes pods with zero regressions.';
110
- }
111
- return `As Hanzo Coder, I am tracking that: our engineering pipeline is synchronized and ready for production deployment.`;
112
- }
113
- if (agent.includes('researcher')) {
114
- return `From our DeSci research benchmarks, model convergence and validation loss show optimal scaling across the cluster.`;
115
- }
116
- if (agent.includes('maya')) {
117
- return `I have recorded this decision and scheduled the follow-up milestone in our calendar.`;
118
- }
119
- if (agent.includes('des')) {
120
- return `The UI hierarchy, contrast ratios, and layout flow are optimized for clarity across both desktop and mobile.`;
121
- }
122
- if (agent.includes('vi')) {
123
- return `Cluster health, P99 response latencies, and auto-scaling ingress controllers are reporting 100% operational.`;
124
- }
125
- return `Understood. I have logged that item for our meeting sync and will monitor the execution.`;
90
+ { role: 'user', content: prompt },
91
+ ],
92
+ max_tokens: 150,
93
+ }),
94
+ });
95
+ if (!res.ok)
96
+ throw new Error(`The co-pilot is unavailable (${res.status}).`);
97
+ const data = await res.json();
98
+ const text = data.choices?.[0]?.message?.content?.trim();
99
+ if (!text)
100
+ throw new Error('The co-pilot answered with nothing.');
101
+ return text;
126
102
  }
127
103
  /** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
128
104
  const FLOOR = 10;
@@ -143,16 +119,11 @@ export function Meet() {
143
119
  const [meetingView, setMeetingView] = useState('iframe');
144
120
  const [agentQuestion, setAgentQuestion] = useState('');
145
121
  // Agent Speech in Call
122
+ const { token } = useIamToken();
146
123
  const { speaking, speak, toggle } = useAgentSpeech(activeCoPilotAgent);
147
124
  const [audioTesting, setAudioTesting] = useState(false);
148
- const [handsFree, setHandsFree] = useState(false);
149
- const recognitionRef = useRef(null);
150
- const [liveNotes, setLiveNotes] = useState([
151
- '18:02: Meeting started with participants. AI Bot transcription active.',
152
- '18:04: Discussed latency metrics on local S3 drive server (port 9005). Average response: 1.2ms.',
153
- '18:08: Agreed on channel connector architecture: channels connect via bot instances exclusively.',
154
- '18:12: Action Item assigned to Hanzo Coder: Integrate Cal.com scheduling engine across bot triggers.',
155
- ]);
125
+ // The call's notes: what was said and answered here, and nothing before it.
126
+ const [liveNotes, setLiveNotes] = useState([]);
156
127
  const videoRef = useRef(null);
157
128
  const localStreamRef = useRef(null);
158
129
  useEffect(() => {
@@ -230,80 +201,37 @@ export function Meet() {
230
201
  return;
231
202
  }
232
203
  setAudioTesting(true);
233
- speakAgent(`Welcome to Hanzo Meet. Speaker test complete. All agent audio and video systems are ready for this call.`, {
204
+ speakAgent(`Welcome to Hanzo Meet. This is ${activeCoPilotAgent}'s voice. If you can hear this, your speakers are working.`, {
234
205
  agent: activeCoPilotAgent,
206
+ token,
235
207
  onEnd: () => setAudioTesting(false),
236
208
  onError: () => setAudioTesting(false),
237
209
  });
238
210
  };
211
+ // HANDS-FREE is one spoken conversation with the co-pilot, on @hanzo/voice:
212
+ // the mic records, each pause goes to Hanzo's /v1/audio/transcriptions, the
213
+ // utterance is answered, and the answer is read in the co-pilot's cast voice
214
+ // by /v1/audio/speech. Speaking over the reply stops it (the machine's
215
+ // barge-in). Every browser that can record takes part, not only the ones with
216
+ // a working SpeechRecognition — Chromium on Linux and Firefox have none.
217
+ const ear = useMemo(() => speech({ ...base(), ...(token ? { token } : {}) }), [token]);
218
+ const handsFree = useVoice({
219
+ speech: ear,
220
+ onUtterance: (transcript) => void handleSpokenInput(transcript),
221
+ });
222
+ const handsFreeSay = handsFree.say;
239
223
  const handleSpokenInput = useCallback(async (transcript) => {
240
224
  const time = new Date().toLocaleTimeString().slice(0, 5);
241
- const userNote = `${time}: You (voice): ${transcript}`;
242
- setLiveNotes((prev) => [...prev, userNote]);
243
- const reply = await generateAgentMeetingResponse(transcript, activeCoPilotAgent);
244
- const agentNote = `${time}: ${activeCoPilotAgent} (AI): ${reply}`;
245
- setLiveNotes((prev) => [...prev, agentNote]);
246
- speak(reply, activeCoPilotAgent);
247
- }, [activeCoPilotAgent, speak]);
248
- // Real-time hands-free speech recognition & barge-in turn-taking
249
- useEffect(() => {
250
- if (!handsFree || !activeMeetingRoom) {
251
- if (recognitionRef.current) {
252
- try {
253
- recognitionRef.current.stop();
254
- }
255
- catch { }
256
- recognitionRef.current = null;
257
- }
258
- return;
259
- }
260
- if (typeof window === 'undefined')
261
- return;
262
- const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
263
- if (!SpeechRec)
264
- return;
265
- const recognition = new SpeechRec();
266
- recognition.continuous = true;
267
- recognition.interimResults = true;
268
- recognition.lang = 'en-US';
269
- recognition.onresult = (event) => {
270
- // Barge-in: if human starts speaking while agent is talking, halt speech immediately!
271
- if (speaking) {
272
- stopAgentSpeech();
273
- }
274
- for (let i = event.resultIndex; i < event.results.length; i++) {
275
- if (event.results[i].isFinal) {
276
- const transcript = event.results[i][0]?.transcript?.trim();
277
- if (transcript) {
278
- handleSpokenInput(transcript);
279
- }
280
- }
281
- }
282
- };
283
- recognition.onerror = () => { };
284
- recognition.onend = () => {
285
- if (handsFree && activeMeetingRoom) {
286
- try {
287
- recognition.start();
288
- }
289
- catch { }
290
- }
291
- };
225
+ setLiveNotes((prev) => [...prev, `${time}: You (voice): ${transcript}`]);
292
226
  try {
293
- recognition.start();
294
- recognitionRef.current = recognition;
227
+ const reply = await generateAgentMeetingResponse(transcript, activeCoPilotAgent, token);
228
+ setLiveNotes((prev) => [...prev, `${time}: ${activeCoPilotAgent} (AI): ${reply}`]);
229
+ void handsFreeSay(cleanForSpeech(reply), voiceOf(activeCoPilotAgent));
295
230
  }
296
- catch { }
297
- return () => {
298
- if (recognitionRef.current) {
299
- try {
300
- recognitionRef.current.stop();
301
- }
302
- catch { }
303
- recognitionRef.current = null;
304
- }
305
- };
306
- }, [handsFree, activeMeetingRoom, speaking, handleSpokenInput]);
231
+ catch (err) {
232
+ setLiveNotes((prev) => [...prev, `${time}: ${activeCoPilotAgent} did not answer: ${err.message}`]);
233
+ }
234
+ }, [activeCoPilotAgent, token, handsFreeSay]);
307
235
  const handleAskAgent = async (e) => {
308
236
  e.preventDefault();
309
237
  const q = agentQuestion.trim();
@@ -311,26 +239,37 @@ export function Meet() {
311
239
  return;
312
240
  setAgentQuestion('');
313
241
  const time = new Date().toLocaleTimeString().slice(0, 5);
314
- const userNote = `${time}: You: ${q}`;
315
- setLiveNotes((prev) => [...prev, userNote]);
316
- const reply = await generateAgentMeetingResponse(q, activeCoPilotAgent);
317
- const agentNote = `${time}: ${activeCoPilotAgent} (AI): ${reply}`;
318
- setLiveNotes((prev) => [...prev, agentNote]);
319
- speak(reply, activeCoPilotAgent);
242
+ setLiveNotes((prev) => [...prev, `${time}: You: ${q}`]);
243
+ try {
244
+ const reply = await generateAgentMeetingResponse(q, activeCoPilotAgent, token);
245
+ setLiveNotes((prev) => [...prev, `${time}: ${activeCoPilotAgent} (AI): ${reply}`]);
246
+ speak(reply, activeCoPilotAgent);
247
+ }
248
+ catch (err) {
249
+ setLiveNotes((prev) => [...prev, `${time}: ${activeCoPilotAgent} did not answer: ${err.message}`]);
250
+ }
320
251
  };
252
+ // The briefing is the call's own notes, read aloud; with none there is nothing to brief.
321
253
  const handleReadBriefing = () => {
322
- const summary = `Executive briefing for ${activeMeetingRoom || 'call'}. Key items discussed: latency metrics on local S3 drive server average 1.2ms, channel connector architecture verified, Cal scheduling integration underway.`;
323
- toggle(summary, activeCoPilotAgent);
254
+ if (!liveNotes.length)
255
+ return;
256
+ toggle(`Briefing for ${activeMeetingRoom || 'this call'}. ${liveNotes.join('. ')}`, activeCoPilotAgent);
324
257
  };
258
+ // What the hands-free control says: why it cannot run, or that Hanzo's speech
259
+ // refused and the browser stood in (or could not) — never silence.
260
+ const handsFreeNote = handsFree.reason ?? (handsFree.refusal ? REFUSED[handsFree.refusal.covered ? 'covered' : 'lost'] : null);
325
261
  return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
326
262
  ['iframe', 'SFU Stage', Tv],
327
263
  ['grid', 'Agent Grid', LayoutGrid],
328
264
  ].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy Room Link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
329
265
  stopAgentSpeech();
266
+ // Leaving the call hangs up the hands-free conversation with it.
267
+ if (handsFree.open)
268
+ handsFree.toggle();
330
269
  setActiveMeetingRoom(null);
331
270
  }, children: [_jsx(PhoneOff, { size: 14 }), "Leave Call"] })] })) : (_jsx(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", children: _jsx(Button, { asChild: true, variant: "linkMuted", size: "sm", px: 0, children: _jsxs("a", { href: "https://meet.hanzo.ai", target: "_blank", rel: "noopener noreferrer", children: ["meet.hanzo.ai", _jsx(ExternalLink, { size: 12 })] }) }) }))] }), activeMeetingRoom ? (_jsxs(XStack, { flex: 1, overflow: "hidden", position: "relative", children: [_jsx(XStack, { flex: 1, minW: "auto", position: "relative", bg: "$sunken", children: meetingView === 'iframe' ? (_jsx(View, { render: _jsx("iframe", { src: `https://meet.hanzo.ai/${activeMeetingRoom}#config.prejoinPageEnabled=false&config.startWithAudioMuted=${!micOn}&config.startWithVideoMuted=${!cameraOn}`, allow: "camera; microphone; display-capture; autoplay; clipboard-write; speaker-selection; screen-wake-lock", title: `Hanzo Meet Room: ${activeMeetingRoom}` }), width: "100%", height: "100%", borderWidth: 0, bg: "$sunken" }, "stage")) : (_jsxs(View, { flex: 1, display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 12, p: 16, overflowY: "auto", bg: "$background", children: [_jsx(YStack, { bg: "$panel", rounded: 12, borderWidth: 1, borderColor: "$borderColor", position: "relative", overflow: "hidden", items: "center", justify: "center", minH: 220, children: _jsxs(YStack, { items: "center", children: [_jsx(YStack, { width: 72, height: 72, rounded: 9999, bg: "$raised", items: "center", justify: "center", mb: 8, children: _jsx(Text, { fontSize: "$8", fontWeight: "700", color: "$ink", children: "You" }) }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Alex (Host)" }), _jsx(Text, { fontSize: "$1", color: micOn ? GOOD : '$soft', children: micOn ? 'Mic Active' : 'Muted' })] }) }), invitedAgents.map((agentName) => {
332
271
  const voice = voiceOf(agentName);
333
- const isThisSpeaking = speaking && activeCoPilotAgent.toLowerCase() === agentName.toLowerCase();
272
+ const isThisSpeaking = (speaking || handsFree.state === 'speaking') && activeCoPilotAgent.toLowerCase() === agentName.toLowerCase();
334
273
  return (_jsxs(YStack, { bg: "$panel", rounded: 12, borderWidth: isThisSpeaking ? 2 : 1, borderColor: isThisSpeaking ? 'var(--state-success)' : '$borderColor', position: "relative", overflow: "hidden", items: "center", justify: "center", p: 20, minH: 220, transition: "200ms", children: [_jsxs(View, { position: "relative", mb: 10, children: [_jsx(Face, { name: agentName, size: 72 }), isThisSpeaking ? (_jsx(View, { position: "absolute", b: -4, r: -4, bg: "var(--popover)", borderWidth: 1, borderColor: "var(--state-success)", rounded: 9999, p: 4, children: _jsx(Volume2, { size: 12, color: GOOD }) })) : null] }), _jsx(Text, { fontSize: "$3", fontWeight: "700", color: "$ink", mb: 2, children: agentName }), _jsxs(Text, { fontSize: "$1", color: "$soft", mb: 8, children: ["Voice: ", voice] }), _jsx(XStack, { items: "center", gap: 6, bg: "$hover", px: 10, py: 4, rounded: 12, children: isThisSpeaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), _jsx(Text, { fontSize: "$1", fontWeight: "600", color: GOOD, children: "Speaking in Call" })] })) : (_jsxs(_Fragment, { children: [_jsx(Bot, { size: 11, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$quiet", children: "AI Agent \u00B7 In Call" })] })) })] }, agentName));
335
274
  })] }, "grid")) }), botAssisting ? (_jsxs(YStack, { id: "meet-copilot", width: 360, minW: 360, bg: "$panel", borderLeftWidth: 1, borderLeftColor: "$borderColor", p: 16, gap: 12, "$max-md": {
336
275
  display: copilot ? 'flex' : 'none',
@@ -343,9 +282,13 @@ export function Meet() {
343
282
  minW: 0,
344
283
  z: 'var(--z-sheet)',
345
284
  borderLeftWidth: 0,
346
- }, children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Face, { name: activeCoPilotAgent, size: 28 }), _jsxs(YStack, { children: [_jsx(Text, { fontSize: "$2", fontWeight: "700", color: "$ink", children: activeCoPilotAgent }), _jsxs(Text, { fontSize: FLOOR, color: "$faint", children: ["AI Co-pilot \u00B7 Voice: ", voiceOf(activeCoPilotAgent)] })] })] }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", color: GOOD, bg: "$hover", px: 6, py: 2, rounded: 4, children: "Live in call" })] }), _jsx(XStack, { gap: 4, flexWrap: "wrap", children: invitedAgents.map((agent) => (_jsx(Button, { size: "sm", variant: activeCoPilotAgent === agent ? 'secondary' : 'ghost', color: activeCoPilotAgent === agent ? '$ink' : '$soft', "aria-pressed": activeCoPilotAgent === agent, onClick: () => setActiveCoPilotAgent(agent), children: agent }, agent))) }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, variant: speaking ? 'secondary' : 'default', onClick: handleReadBriefing, children: speaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), "Stop Briefing", _jsx(Square, { size: 11, fill: "currentColor" })] })) : (_jsxs(_Fragment, { children: [_jsx(Volume2, { size: 14 }), "Briefing"] })) }), _jsxs(Button, { size: "sm", variant: handsFree ? 'secondary' : 'default', "data-testid": "hands-free-voice-toggle", "aria-pressed": handsFree, title: handsFree ? 'Hands-Free Voice Active (Speaks and interrupts on voice)' : 'Enable Hands-Free Voice Mode', onClick: () => setHandsFree(!handsFree), children: [_jsx(Mic, { size: 14, color: handsFree ? 'var(--state-success)' : 'currentColor' }), handsFree ? 'Hands-Free On' : 'Hands-Free'] })] }), _jsx(YStack, { flex: 1, overflowY: "auto", bg: "$background", rounded: 8, borderWidth: 1, borderColor: "$borderColor", p: 12, gap: 8, children: liveNotes.map((note, i) => (_jsx(Text, { fontSize: "$1", color: "$quiet", lineHeight: 16.5, pb: 8, borderBottomWidth: 1, borderBottomColor: "$borderColor", children: note }, i))) }), _jsxs(XStack, { render: "form", onSubmit: handleAskAgent, gap: 6, children: [_jsx(Text, { render: _jsx("input", { type: "text", value: agentQuestion, onChange: (e) => setAgentQuestion(e.target.value), placeholder: `Ask ${activeCoPilotAgent} in meeting...` }), flex: 1, bg: "$background", borderWidth: 1, borderColor: "var(--border-control)", rounded: 6, px: 10, py: 8, color: "$ink", fontSize: "$1", outlineStyle: "none" }), _jsx(Button, { type: "submit", variant: "primary", size: "icon-sm", disabled: !agentQuestion.trim(), "aria-label": "Ask", children: _jsx(Send, { size: 12 }) })] }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, onClick: () => {
347
- const sample = `${new Date().toLocaleTimeString().slice(0, 5)}: Participant discussed deployment timeline for next release.`;
348
- setLiveNotes((prev) => [...prev, sample]);
285
+ }, children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Face, { name: activeCoPilotAgent, size: 28 }), _jsxs(YStack, { children: [_jsx(Text, { fontSize: "$2", fontWeight: "700", color: "$ink", children: activeCoPilotAgent }), _jsxs(Text, { fontSize: FLOOR, color: "$faint", children: ["AI Co-pilot \u00B7 Voice: ", voiceOf(activeCoPilotAgent)] })] })] }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", color: GOOD, bg: "$hover", px: 6, py: 2, rounded: 4, children: "Live in call" })] }), _jsx(XStack, { gap: 4, flexWrap: "wrap", children: invitedAgents.map((agent) => (_jsx(Button, { size: "sm", variant: activeCoPilotAgent === agent ? 'secondary' : 'ghost', color: activeCoPilotAgent === agent ? '$ink' : '$soft', "aria-pressed": activeCoPilotAgent === agent, onClick: () => setActiveCoPilotAgent(agent), children: agent }, agent))) }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, variant: speaking ? 'secondary' : 'default', disabled: !speaking && !liveNotes.length, onClick: handleReadBriefing, children: speaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), "Stop Briefing", _jsx(Square, { size: 11, fill: "currentColor" })] })) : (_jsxs(_Fragment, { children: [_jsx(Volume2, { size: 14 }), "Briefing"] })) }), _jsxs(Button, { size: "sm", variant: handsFree.open ? 'secondary' : 'default', "data-testid": "hands-free-voice-toggle", "data-state": handsFree.state, "data-refusal": handsFree.refusal ? handsFree.refusal.service : undefined, "aria-pressed": handsFree.open, disabled: !!handsFree.blocked, title: handsFreeNote ??
286
+ (handsFree.open ? 'Hands-Free Voice Active (Speaks and interrupts on voice)' : 'Enable Hands-Free Voice Mode'), onClick: handsFree.toggle, children: [_jsx(Mic, { size: 14, color: handsFree.open ? 'var(--state-success)' : 'currentColor' }), handsFree.open ? 'Hands-Free On' : 'Hands-Free'] })] }), handsFreeNote ? (_jsx(Text, { fontSize: "$1", color: "$soft", role: "status", children: handsFreeNote })) : null, _jsx(YStack, { flex: 1, overflowY: "auto", bg: "$background", rounded: 8, borderWidth: 1, borderColor: "$borderColor", p: 12, gap: 8, children: liveNotes.map((note, i) => (_jsx(Text, { fontSize: "$1", color: "$quiet", lineHeight: 16.5, pb: 8, borderBottomWidth: 1, borderBottomColor: "$borderColor", children: note }, i))) }), _jsxs(XStack, { render: "form", onSubmit: handleAskAgent, gap: 6, children: [_jsx(Text, { render: _jsx("input", { type: "text", value: agentQuestion, onChange: (e) => setAgentQuestion(e.target.value), placeholder: `Ask ${activeCoPilotAgent} in meeting...` }), flex: 1, bg: "$background", borderWidth: 1, borderColor: "var(--border-control)", rounded: 6, px: 10, py: 8, color: "$ink", fontSize: "$1", outlineStyle: "none" }), _jsx(Button, { type: "submit", variant: "primary", size: "icon-sm", disabled: !agentQuestion.trim(), "aria-label": "Ask", children: _jsx(Send, { size: 12 }) })] }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, disabled: !agentQuestion.trim(), title: "Add what is typed above to the notes, without asking the co-pilot", onClick: () => {
287
+ const note = agentQuestion.trim();
288
+ if (!note)
289
+ return;
290
+ setAgentQuestion('');
291
+ setLiveNotes((prev) => [...prev, `${new Date().toLocaleTimeString().slice(0, 5)}: You (note): ${note}`]);
349
292
  }, children: "+ Add Live Note" }), _jsx(Button, { variant: "primary", size: "sm", onClick: () => {
350
293
  const text = liveNotes.join('\n');
351
294
  navigator.clipboard.writeText(text);
package/dist/Profile.js CHANGED
@@ -55,7 +55,7 @@ export function Profile({ open, onClose }) {
55
55
  return (_jsxs(YStack, { ...pane(sheet(2).backgroundColor), width: WIDTH, maxW: "100%", shrink: 0, minH: 0,
56
56
  // A column on a laptop. On a phone the room is already the whole screen,
57
57
  // so this covers it rather than squeezing it to nothing.
58
- position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-panel)", $lg: { position: 'relative', z: 'var(--z-base)', l: 'auto' }, children: [_jsxs(XStack, { items: "center", px: "$4", py: "$3", gap: "$2", children: [_jsx(Text, { flex: 1, fontSize: "$5", fontWeight: "600", color: "$ink", children: "Profile" }), _jsx(Box, { render: "button", onClick: onClose, "aria-label": "Close profile", p: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$4", pb: "$5", gap: "$4", children: [_jsx("input", { ref: picker, type: "file", accept: "image/png,image/jpeg,image/gif,image/webp", hidden: true, onChange: (e) => {
58
+ position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-panel)", $lg: { position: 'relative', z: 'var(--z-base)', l: 'auto' }, children: [_jsxs(XStack, { items: "center", px: "$4", py: "$3", gap: "$2", children: [_jsx(Text, { flex: 1, fontSize: "$5", fontWeight: "600", color: "$ink", children: "Profile" }), _jsx(Box, { render: "button", onClick: onClose, "aria-label": "Close profile", p: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", px: "$4", pb: "$5", gap: "$4", children: [_jsx("input", { ref: picker, type: "file", accept: "image/png,image/jpeg,image/gif,image/webp", hidden: true, onChange: (e) => {
59
59
  const file = e.target.files?.[0];
60
60
  e.target.value = '';
61
61
  void pick(file);
package/dist/RightPane.js CHANGED
@@ -152,7 +152,7 @@ export function RightPane({ said = [], onClose, }) {
152
152
  // having to tell it that the conversation changed.
153
153
  const at = channel(open);
154
154
  const pane = usePane(at);
155
- return (_jsxs(YStack, { flex: 1, minH: 0, bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", children: [pane.pinned ? _jsx(Summary, { at: at, said: said, held: pane.held }) : null, _jsx(Browser, { at: at, pane: pane, onClose: onClose })] }));
155
+ return (_jsxs(YStack, { flex: 1, minH: 320, bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", children: [pane.pinned ? _jsx(Summary, { at: at, said: said, held: pane.held }) : null, _jsx(Browser, { at: at, pane: pane, onClose: onClose })] }));
156
156
  }
157
157
  /** The two cards: out of the conversation, and into it. */
158
158
  function Summary({ at, said, held }) {
@@ -165,7 +165,7 @@ function Summary({ at, said, held }) {
165
165
  // it would take the browser's half, and scrolls after that — so two rows
166
166
  // do not reserve a screen and thirty do not push the browser off the
167
167
  // bottom of the column.
168
- maxH: "45%", overflow: "scroll", p: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Marks, { at: at, kind: "outputs", title: "Outputs", icon: _jsx(FileCode, { size: 16, "aria-hidden": true }), marks: made, children: "This conversation has produced nothing yet." }), _jsx(Marks, { at: at, kind: "sources", title: "Sources", icon: _jsx(Paperclip, { size: 16, "aria-hidden": true }), marks: took, children: "Nothing has been put into this conversation yet." })] }));
168
+ maxH: "45%", overflowY: "auto", overflowX: "hidden", p: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Marks, { at: at, kind: "outputs", title: "Outputs", icon: _jsx(FileCode, { size: 16, "aria-hidden": true }), marks: made, children: "This conversation has produced nothing yet." }), _jsx(Marks, { at: at, kind: "sources", title: "Sources", icon: _jsx(Paperclip, { size: 16, "aria-hidden": true }), marks: took, children: "Nothing has been put into this conversation yet." })] }));
169
169
  }
170
170
  /**
171
171
  * One card of things, the first {@link FEW} of them, and the honest number of
@@ -338,7 +338,7 @@ function Browser({ at, pane, onClose, }) {
338
338
  , {
339
339
  // 44 IS THE FLOOR A THUMB NEEDS, and the strip scrolls rather than
340
340
  // squeezing its tabs under it — the rule `tabs.tsx` exists to hold.
341
- minH: 44, shrink: 0, items: "center", gap: "$1", px: "$2", overflow: "scroll", borderBottomWidth: 1, borderColor: "$borderColor", children: [pane.tabs.map((t) => (_jsx(Sheet, { at: at, leaf: t, on: t.id === pane.at }, t.id))), _jsx(Box, { render: "button", onClick: () => openTab(at, 'https://hanzo.ai'), "aria-label": "New tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }), _jsx(Box, { flex: 1, minW: "$1" }), leaf ? (_jsxs(_Fragment, { children: [_jsx(Box, { render: "button", onClick: () => reload((n) => n + 1), "aria-label": "Reload this tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(RotateCw, { size: 16, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => window.open(leaf.url, '_blank', 'noopener,noreferrer'), "aria-label": "Open this tab in a new window", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16, "aria-hidden": true }) })] })) : null, onClose ? (_jsx(Box, { render: "button", onClick: onClose, "aria-label": "Hide side panel", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(PanelRightClose, { size: 16, "aria-hidden": true }) })) : null] }), leaf ? _jsx(Where, { at: at, leaf: leaf }) : null, _jsx(YStack, { flex: 1, minH: 0, children: leaf ? (_jsx(Preview, { leaf: leaf, held: pane.held }, `${leaf.id}.${again}`)) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(Quiet, { children: "Nothing open here." }), _jsx(Quiet, { children: "Press an output or a source to look at it, or open a tab." })] })) })] }));
341
+ minH: 44, shrink: 0, items: "center", gap: "$1", px: "$2", overflowX: "auto", overflowY: "hidden", borderBottomWidth: 1, borderColor: "$borderColor", children: [pane.tabs.map((t) => (_jsx(Sheet, { at: at, leaf: t, on: t.id === pane.at }, t.id))), _jsx(Box, { render: "button", onClick: () => openTab(at, 'https://hanzo.ai'), "aria-label": "New tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }), _jsx(Box, { flex: 1, minW: "$1" }), leaf ? (_jsxs(_Fragment, { children: [_jsx(Box, { render: "button", onClick: () => reload((n) => n + 1), "aria-label": "Reload this tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(RotateCw, { size: 16, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => window.open(leaf.url, '_blank', 'noopener,noreferrer'), "aria-label": "Open this tab in a new window", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16, "aria-hidden": true }) })] })) : null, onClose ? (_jsx(Box, { render: "button", onClick: onClose, "aria-label": "Hide side panel", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(PanelRightClose, { size: 16, "aria-hidden": true }) })) : null] }), leaf ? _jsx(Where, { at: at, leaf: leaf }) : null, _jsx(YStack, { flex: 1, minH: 0, children: leaf ? (_jsx(Preview, { leaf: leaf, held: pane.held }, `${leaf.id}.${again}`)) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(Quiet, { children: "Nothing open here." }), _jsx(Quiet, { children: "Press an output or a source to look at it, or open a tab." })] })) })] }));
342
342
  }
343
343
  /** One tab: the thing you press, and the cross that shuts it. */
344
344
  function Sheet({ at, leaf, on }) {
@@ -385,7 +385,7 @@ function Preview({ leaf, held }) {
385
385
  const type = mine?.type ?? '';
386
386
  const ext = suffix(mine?.name ?? leaf.title);
387
387
  if (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg'].includes(ext)) {
388
- return (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflow: "scroll", children: _jsx(View, { render: _jsx("img", { src: leaf.url, alt: leaf.title }), maxW: "100%", maxH: "100%", objectFit: "contain" }) }));
388
+ return (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflowY: "auto", overflowX: "hidden", children: _jsx(View, { render: _jsx("img", { src: leaf.url, alt: leaf.title }), maxW: "100%", maxH: "100%", objectFit: "contain" }) }));
389
389
  }
390
390
  // THE LIST IS OF WHAT A FRAME CAN RENDER, not of what a file might be. A type
391
391
  // absent from it is unsupported, which is the honest default: guessing that
package/dist/Settings.js CHANGED
@@ -139,7 +139,7 @@ export function SettingsList({ active, filter, onPick, onBack, }) {
139
139
  */
140
140
  export function SettingsPane({ id }) {
141
141
  const row = ROWS.find((r) => r.id === id);
142
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", children: _jsxs(YStack, { p: "$5", gap: "$4", maxW: 860, children: [_jsx(Text, { fontSize: "$6", fontWeight: "600", color: "$ink", children: row?.label ?? 'Settings' }), _jsx(Pane, { id: id, note: row?.note })] }) }));
142
+ return (_jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", children: _jsxs(YStack, { p: "$5", gap: "$4", maxW: 860, children: [_jsx(Text, { fontSize: "$6", fontWeight: "600", color: "$ink", children: row?.label ?? 'Settings' }), _jsx(Pane, { id: id, note: row?.note })] }) }));
143
143
  }
144
144
  /** One pane. */
145
145
  function Pane({ id, note }) {
package/dist/Shell.js CHANGED
@@ -887,9 +887,13 @@ export function Paned({ children, swap, framed = false, bare = false, }) {
887
887
  // In the frame the gutter stands between the pane and this column.
888
888
  ml: framed ? "var(--pane-gap)" : undefined, ref: beside.ref,
889
889
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
890
- style: beside.style, position: "relative", width: "var(--span)", maxW: "50%", shrink: 0, minH: 0, display: "none", $lg: { display: "flex" }, children: [_jsx(Grip, { side: "right", span: beside.span, floor: FLOOR, ceil: CEIL, reset: BESIDE_WIDTH, onSpan: beside.move, onKeep: beside.keep, onShut: () => showAside(null), label: "Resize side panel",
890
+ style: beside.style, position: "relative", width: "var(--span)",
891
+ // Half the WINDOW. Half of an `auto` grid track is half of this
892
+ // column's own width, so the rooms' shell drew it at half its span
893
+ // beside an empty strip of the same size.
894
+ maxW: "50vw", shrink: 0, minH: 0, display: "none", $lg: { display: "flex" }, children: [_jsx(Grip, { side: "right", span: beside.span, floor: FLOOR, ceil: CEIL, reset: BESIDE_WIDTH, onSpan: beside.move, onKeep: beside.keep, onShut: () => showAside(null), label: "Resize side panel",
891
895
  // In the gutter the frame leaves between the panes; a room has none.
892
- l: GUTTER_START, width: GUTTER }), _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", rounded: "var(--pane-round)", ref: (el) => setSlot(el instanceof HTMLElement ? el : null) })] })) : null] }));
896
+ l: GUTTER_START, width: GUTTER }), _jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", rounded: "var(--pane-round)", ref: (el) => setSlot(el instanceof HTMLElement ? el : null) })] })) : null] }));
893
897
  }
894
898
  /**
895
899
  * THE RAIL: where you can go, as marks with their names under them. It is the
package/dist/Talk.js CHANGED
@@ -77,7 +77,7 @@ export function Talk({ room }) {
77
77
  }
78
78
  };
79
79
  const name = (of) => setDraft((held) => mention(held, of));
80
- return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(ChannelHeader, { name: titleOf(room), children: [room.topic ? (_jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: room.topic })) : null, _jsx(Box, { render: "button", "aria-label": aside === 'roster' ? 'Hide directory & roster' : 'Show directory & roster', "aria-pressed": aside === 'roster', onClick: () => showAside(aside === 'roster' ? null : 'roster'), ml: "auto", p: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(Users, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$3", children: [failed ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: say(failed, 'this room') })) : messages === null ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the room." })) : messages.length === 0 && pending.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing has been said here yet." })) : (messages.map((one) => (_jsx(Line, { member: memberOf(members, one.author), fallback: one.author, stamp: when(one.createdOn), text: one.text }, one.id)))), pending
80
+ return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(ChannelHeader, { name: titleOf(room), children: [room.topic ? (_jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: room.topic })) : null, _jsx(Box, { render: "button", "aria-label": aside === 'roster' ? 'Hide directory & roster' : 'Show directory & roster', "aria-pressed": aside === 'roster', onClick: () => showAside(aside === 'roster' ? null : 'roster'), ml: "auto", p: "$2", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(Users, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", p: "$4", gap: "$3", children: [failed ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: say(failed, 'this room') })) : messages === null ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Reading the room." })) : messages.length === 0 && pending.length === 0 ? (_jsx(Text, { fontSize: "$3", color: "$soft", children: "Nothing has been said here yet." })) : (messages.map((one) => (_jsx(Line, { member: memberOf(members, one.author), fallback: one.author, stamp: when(one.createdOn), text: one.text }, one.id)))), pending
81
81
  .filter((one) => !messages?.some((said) => said.author === me && said.text === one.text))
82
82
  .map((one) => (_jsx(Line, { member: mine, fallback: "You", text: one.text, soft: true }, `pending-${one.key}`)))] }), _jsxs(YStack, { p: "$3", gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Composer, { value: draft, onChange: setDraft, onSend: () => void speak(), placeholder: `Message ${titleOf(room)}`, label: `Message ${titleOf(room)}`, hint: agents.length ? 'Mention an agent with @name and it answers.' : undefined, children: agents.length ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", shrink: 1, minW: 0, children: agents.map((one) => (_jsx(Box, { render: "button", "aria-label": `Mention ${one.name}`, onClick: () => name(one.name), px: "$2", py: "$1", rounded: "$10", hoverStyle: { bg: '$hover' }, children: _jsxs(Text, { fontSize: "$2", color: "$soft", children: ["@", one.name] }) }, one.id))) })) : null }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] }), _jsx(Beside, { children: aside === 'roster' ? _jsx(Roster, { members: members, wrong: wrong, onMention: name }) : null })] }));
83
83
  }
package/dist/Vibe.js CHANGED
@@ -166,7 +166,7 @@ function Vibe() {
166
166
  .catch((e) => setTurns((prev) => prev.map((t) => (t.id === id ? { ...t, refused: plainly(e, 'this run') } : t))))
167
167
  .finally(() => setBusy(false));
168
168
  };
169
- return (_jsxs(YStack, { "data-vibe": true, flex: 1, minH: 0, height: "100vh", width: "100%", maxW: "100%", overflowX: "auto", overflowY: "hidden", bg: "$background", pb: 56, children: [_jsxs(XStack, { height: 52, items: "center", justify: "space-between", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", z: 30, shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(TopOrgSelector, {}), _jsxs(XStack, { display: "none", $md: { display: 'flex' }, items: "center", gap: "$2", py: 4, px: 10, rounded: 20, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(View, { width: 7, height: 7, rounded: 4, bg: busy ? 'var(--state-online)' : '$faint' }), _jsx(Text, { fontSize: 11, color: "$quiet", fontWeight: "500", children: busy ? 'Starting a run' : 'Idle' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { $md: { display: 'none' }, bg: "$panel", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", children: ['stream', 'preview', 'team'].map((tab) => (_jsx(Button, { size: "sm", px: "$2", variant: activeTabMobile === tab ? 'secondary' : 'ghost', color: activeTabMobile === tab ? '$ink' : '$soft', "aria-pressed": activeTabMobile === tab, onClick: () => setActiveTabMobile(tab), children: tab === 'stream' ? 'Stream' : tab === 'preview' ? 'Preview' : 'Team' }, tab))) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", children: _jsxs(Link, { href: invite(), children: [_jsx(UserPlus, { size: 13 }), "Invite"] }) })] })] }), _jsxs(View, { flex: 1, minH: 0, width: "100%", height: "100%", overflow: "hidden", display: "grid", gridTemplateRows: "100%", gridTemplateColumns: "100%", $md: { gridTemplateColumns: 'minmax(320px, 380px) minmax(0, 1fr)' }, $lg: { gridTemplateColumns: 'minmax(340px, 400px) minmax(360px, 1fr) minmax(300px, 340px)' }, $xl: { gridTemplateColumns: 'minmax(380px, 440px) minmax(480px, 1fr) minmax(340px, 380px)' }, children: [_jsxs(YStack, { height: "100%", borderRightWidth: 1, borderColor: "$borderColor", bg: "$background", minW: 0, display: activeTabMobile === 'stream' ? 'flex' : 'none', $md: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Sparkles, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "Runs" }), turns.length > 0 && (_jsx(View, { px: 6, py: 2, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", children: turns.length }) }))] }), _jsxs(YStack, { flex: 1, overflow: "scroll", p: "$3.5", gap: "$4", children: [turns.length === 0 ? (_jsx(Quiet, { children: "Nothing has been sent from this room yet. Describe a task below and the run it opens appears here." })) : null, turns.map((turn) => (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(XStack, { gap: "$2.5", children: [_jsx(View, { width: 24, height: 24, rounded: 12, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(Text, { fontSize: 10, fontWeight: "700", color: "$ink", children: turn.who.charAt(0).toUpperCase() }) }), _jsxs(YStack, { flex: 1, gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: 12, fontWeight: "700", color: "$ink", children: turn.who }), _jsx(Text, { fontSize: 11, color: "$soft", children: turn.time }), _jsx(View, { px: 5, py: 1, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", fontFamily: "$mono", children: turn.model }) })] }), _jsx(Text, { fontSize: 12, color: "$quiet", lineHeight: 18, children: turn.prompt })] })] }), _jsx(YStack, { ml: "$4", gap: "$1.5", children: turn.refused ? (_jsx(Text, { fontSize: 11, color: "$ink", lineHeight: 16, children: turn.refused })) : !turn.run ? (_jsx(Quiet, { children: "Starting." })) : (_jsxs(YStack, { p: "$2", rounded: 8, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", gap: "$1", children: [_jsx(Text, { fontSize: 11, color: "$quiet", fontFamily: "$mono", children: turn.run.sessionId }), _jsxs(Text, { fontSize: 11, color: "$soft", children: [turn.run.routed
169
+ return (_jsxs(YStack, { "data-vibe": true, flex: 1, minH: 0, height: "100vh", width: "100%", maxW: "100%", overflowX: "auto", overflowY: "hidden", bg: "$background", pb: 56, children: [_jsxs(XStack, { height: 52, items: "center", justify: "space-between", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", z: 30, shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(TopOrgSelector, {}), _jsxs(XStack, { display: "none", $md: { display: 'flex' }, items: "center", gap: "$2", py: 4, px: 10, rounded: 20, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(View, { width: 7, height: 7, rounded: 4, bg: busy ? 'var(--state-online)' : '$faint' }), _jsx(Text, { fontSize: 11, color: "$quiet", fontWeight: "500", children: busy ? 'Starting a run' : 'Idle' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { $md: { display: 'none' }, bg: "$panel", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", children: ['stream', 'preview', 'team'].map((tab) => (_jsx(Button, { size: "sm", px: "$2", variant: activeTabMobile === tab ? 'secondary' : 'ghost', color: activeTabMobile === tab ? '$ink' : '$soft', "aria-pressed": activeTabMobile === tab, onClick: () => setActiveTabMobile(tab), children: tab === 'stream' ? 'Stream' : tab === 'preview' ? 'Preview' : 'Team' }, tab))) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", children: _jsxs(Link, { href: invite(), children: [_jsx(UserPlus, { size: 13 }), "Invite"] }) })] })] }), _jsxs(View, { flex: 1, minH: 0, width: "100%", height: "100%", overflow: "hidden", display: "grid", gridTemplateRows: "100%", gridTemplateColumns: "100%", $md: { gridTemplateColumns: 'minmax(320px, 380px) minmax(0, 1fr)' }, $lg: { gridTemplateColumns: 'minmax(340px, 400px) minmax(360px, 1fr) minmax(300px, 340px)' }, $xl: { gridTemplateColumns: 'minmax(380px, 440px) minmax(480px, 1fr) minmax(340px, 380px)' }, children: [_jsxs(YStack, { height: "100%", borderRightWidth: 1, borderColor: "$borderColor", bg: "$background", minW: 0, display: activeTabMobile === 'stream' ? 'flex' : 'none', $md: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Sparkles, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "Runs" }), turns.length > 0 && (_jsx(View, { px: 6, py: 2, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", children: turns.length }) }))] }), _jsxs(YStack, { flex: 1, overflowY: "auto", overflowX: "hidden", p: "$3.5", gap: "$4", children: [turns.length === 0 ? (_jsx(Quiet, { children: "Nothing has been sent from this room yet. Describe a task below and the run it opens appears here." })) : null, turns.map((turn) => (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(XStack, { gap: "$2.5", children: [_jsx(View, { width: 24, height: 24, rounded: 12, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(Text, { fontSize: 10, fontWeight: "700", color: "$ink", children: turn.who.charAt(0).toUpperCase() }) }), _jsxs(YStack, { flex: 1, gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: 12, fontWeight: "700", color: "$ink", children: turn.who }), _jsx(Text, { fontSize: 11, color: "$soft", children: turn.time }), _jsx(View, { px: 5, py: 1, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", fontFamily: "$mono", children: turn.model }) })] }), _jsx(Text, { fontSize: 12, color: "$quiet", lineHeight: 18, children: turn.prompt })] })] }), _jsx(YStack, { ml: "$4", gap: "$1.5", children: turn.refused ? (_jsx(Text, { fontSize: 11, color: "$ink", lineHeight: 16, children: turn.refused })) : !turn.run ? (_jsx(Quiet, { children: "Starting." })) : (_jsxs(YStack, { p: "$2", rounded: 8, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", gap: "$1", children: [_jsx(Text, { fontSize: 11, color: "$quiet", fontFamily: "$mono", children: turn.run.sessionId }), _jsxs(Text, { fontSize: 11, color: "$soft", children: [turn.run.routed
170
170
  ? `Running on ${turn.run.targetId}`
171
171
  : 'Running in a sandbox', turn.run.branch ? ` · ${turn.run.branch}` : ''] }), _jsx(Text, { render: _jsx(Link, { href: dev(turn.run.sessionId) }), color: "$ink", fontSize: "$1", children: "Watch it work" })] })) })] }, turn.id))), _jsx("div", { ref: bottom })] }), _jsxs(YStack, { p: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsxs(Picker, { value: model, onChange: (e) => setWanted(e.target.value), "aria-label": "Model", maxW: 200, children: [models.length === 0 ? _jsx("option", { value: model, children: model }) : null, models.map((m) => (_jsx("option", { value: m.id, children: m.id }, m.id)))] }), _jsx(Text, { fontSize: 10, color: "$faint", children: "Press Enter to send" })] }), _jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, overflow: "hidden", children: [_jsx(Text, { render: _jsx("textarea", { value: agentInput, onChange: (e) => setAgentInput(e.target.value), onKeyDown: (e) => {
172
172
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -177,7 +177,7 @@ function Vibe() {
177
177
  ['desktop', 'Desktop', Monitor],
178
178
  ['tablet', 'Tablet', Tablet],
179
179
  ['mobile', 'Mobile', Smartphone],
180
- ].map(([device, label, Glyph]) => (_jsx(Button, { variant: previewDevice === device ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": label, title: label, "aria-pressed": previewDevice === device, onClick: () => setPreviewDevice(device), children: _jsx(Glyph, { size: 12, color: previewDevice === device ? 'var(--foreground)' : 'var(--muted-foreground)' }) }, device))) }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Reload preview", title: "Reload preview", onClick: () => setPreviewKey((k) => k + 1), children: _jsx(RefreshCw, { size: 12, color: "var(--muted-foreground)" }) })] })] }), _jsx(YStack, { flex: 1, items: "center", justify: "center", bg: "$background", p: "$2", children: previewUrl.trim() === '' ? (_jsx(Quiet, { children: "Nothing to show. Put the address of what you are building in the bar." })) : (_jsx(View, { width: previewDevice === 'desktop' ? '100%' : previewDevice === 'tablet' ? 768 : 375, maxW: "100%", height: "100%", bg: "$background", rounded: previewDevice === 'desktop' ? 0 : 8, overflow: "hidden", borderWidth: previewDevice === 'desktop' ? 0 : 1, borderColor: "var(--border-control)", boxShadow: "var(--shadow-lg)", children: _jsx(View, { render: _jsx("iframe", { src: previewUrl, sandbox: "allow-scripts allow-forms allow-popups allow-same-origin", title: "Preview" }), width: "100%", height: "100%", borderWidth: 0 }) }, previewKey)) })] }), _jsxs(YStack, { height: "100%", bg: "$background", minW: 0, display: activeTabMobile === 'team' ? 'flex' : 'none', $md: { display: 'none' }, $lg: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Users, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "People" })] }), _jsx(YStack, { flex: 1, overflow: "scroll", p: "$3", gap: "$3", children: _jsx(Team, {}) })] })] })] }));
180
+ ].map(([device, label, Glyph]) => (_jsx(Button, { variant: previewDevice === device ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": label, title: label, "aria-pressed": previewDevice === device, onClick: () => setPreviewDevice(device), children: _jsx(Glyph, { size: 12, color: previewDevice === device ? 'var(--foreground)' : 'var(--muted-foreground)' }) }, device))) }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Reload preview", title: "Reload preview", onClick: () => setPreviewKey((k) => k + 1), children: _jsx(RefreshCw, { size: 12, color: "var(--muted-foreground)" }) })] })] }), _jsx(YStack, { flex: 1, items: "center", justify: "center", bg: "$background", p: "$2", children: previewUrl.trim() === '' ? (_jsx(Quiet, { children: "Nothing to show. Put the address of what you are building in the bar." })) : (_jsx(View, { width: previewDevice === 'desktop' ? '100%' : previewDevice === 'tablet' ? 768 : 375, maxW: "100%", height: "100%", bg: "$background", rounded: previewDevice === 'desktop' ? 0 : 8, overflow: "hidden", borderWidth: previewDevice === 'desktop' ? 0 : 1, borderColor: "var(--border-control)", boxShadow: "var(--shadow-lg)", children: _jsx(View, { render: _jsx("iframe", { src: previewUrl, sandbox: "allow-scripts allow-forms allow-popups allow-same-origin", title: "Preview" }), width: "100%", height: "100%", borderWidth: 0 }) }, previewKey)) })] }), _jsxs(YStack, { height: "100%", bg: "$background", minW: 0, display: activeTabMobile === 'team' ? 'flex' : 'none', $md: { display: 'none' }, $lg: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Users, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "People" })] }), _jsx(YStack, { flex: 1, overflowY: "auto", overflowX: "hidden", p: "$3", gap: "$3", children: _jsx(Team, {}) })] })] })] }));
181
181
  }
182
182
  // THE GATE IS NOT A ROOM'S TO REMEMBER. This room draws its own shell rather
183
183
  // than `Room`, which is where every other room picks the organization gate up —
package/dist/Work.js CHANGED
@@ -157,7 +157,7 @@ function Watching({ idle }) {
157
157
  */
158
158
  function Idle({ runs }) {
159
159
  if (runs === 0) {
160
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", children: _jsxs(YStack, { width: "100%", maxW: 640, mx: "auto", px: "$5", pt: "$10", gap: "$4", children: [_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { render: "h1", fontSize: "$7", fontWeight: "500", color: "$ink", children: "Start a run" }), _jsx(Text, { fontSize: "$3", color: "$soft", maxW: 520, children: "Give an agent a task. Its plan, progress, and output will appear here." })] }), _jsx(Start, {}), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Runs stay visible in Working so you can inspect or steer them at any time." })] }) }));
160
+ return (_jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", children: _jsxs(YStack, { width: "100%", maxW: 640, mx: "auto", px: "$5", pt: "$10", gap: "$4", children: [_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { render: "h1", fontSize: "$7", fontWeight: "500", color: "$ink", children: "Start a run" }), _jsx(Text, { fontSize: "$3", color: "$soft", maxW: 520, children: "Give an agent a task. Its plan, progress, and output will appear here." })] }), _jsx(Start, {}), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Runs stay visible in Working so you can inspect or steer them at any time." })] }) }));
161
161
  }
162
162
  return (_jsx(YStack, { flex: 1, items: "center", justify: "center", p: "$6", children: _jsx(Text, { fontSize: "$3", color: "$soft", children: "Pick a run to watch it work." }) }));
163
163
  }
@@ -230,7 +230,7 @@ function Watch({ id }) {
230
230
  setNote('Could not reach this run');
231
231
  }
232
232
  }, [ai, draft, id]);
233
- return (_jsxs(XStack, { flex: 1, minH: 0, flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, children: [_jsx(ChannelHeader, { name: session?.title || session?.agent || id, artifacts: 0 }), _jsx(XStack, { px: "$4", pb: "$2", children: _jsx(Bar, { run: session }) }), _jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$4", children: _jsx(ProgressBlock, { steps: shown, done: finished, children: message ? (_jsx(Text, { fontSize: "$4", color: "$ink", children: message })) : null }) }), _jsxs(YStack, { p: "$3", gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Steer, { onCommand: (c) => void steer(c), withhold: finished ? ['pause', 'resume', 'stop'] : [], note: note }), _jsx(Composer, { value: draft, onChange: setDraft, onSend: () => void send(), placeholder: "Steer this run\u2026", label: "Steer this run" })] })] }), _jsx(YStack, { width: "100%", p: "$3", borderTopWidth: 1, $sm: { width: 420, shrink: 0, borderTopWidth: 0, borderLeftWidth: 1 }, borderColor: "$borderColor", children: _jsx(Pane, { tabs: [
233
+ return (_jsxs(XStack, { flex: 1, minH: 0, flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minH: 0, minW: 0, children: [_jsx(ChannelHeader, { name: session?.title || session?.agent || id, artifacts: 0 }), _jsx(XStack, { px: "$4", pb: "$2", children: _jsx(Bar, { run: session }) }), _jsx(YStack, { flex: 1, minH: 0, overflowY: "auto", overflowX: "hidden", p: "$4", gap: "$4", children: _jsx(ProgressBlock, { steps: shown, done: finished, children: message ? (_jsx(Text, { fontSize: "$4", color: "$ink", children: message })) : null }) }), _jsxs(YStack, { p: "$3", gap: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Steer, { onCommand: (c) => void steer(c), withhold: finished ? ['pause', 'resume', 'stop'] : [], note: note }), _jsx(Composer, { value: draft, onChange: setDraft, onSend: () => void send(), placeholder: "Steer this run\u2026", label: "Steer this run" })] })] }), _jsx(YStack, { width: "100%", p: "$3", borderTopWidth: 1, $sm: { width: 420, shrink: 0, borderTopWidth: 0, borderLeftWidth: 1 }, borderColor: "$borderColor", children: _jsx(Pane, { tabs: [
234
234
  { id: 'code', label: 'Code', kind: 'code', content: _jsx(Nothing, { what: "changes" }) },
235
235
  { id: 'view', label: 'View', kind: 'view', content: _jsx(Nothing, { what: "preview" }) },
236
236
  { id: 'canvas', label: 'Canvas', kind: 'canvas', content: _jsx(Nothing, { what: "summary" }) },
package/dist/cast.d.ts CHANGED
@@ -24,18 +24,17 @@ interface Role {
24
24
  */
25
25
  ground?: string;
26
26
  /**
27
- * The voice the platform reads this character's replies in, by
28
- * /v1/audio/speech name.
27
+ * The voice the platform reads this character's replies in: a speech-service
28
+ * voice id, sent to /v1/audio/speech as it is. The first letter is the accent
29
+ * (a American, b British), the second the register (f female, m male).
29
30
  *
30
- * ONE VOICE PER CHARACTER, and never two who would be in the same room. The
31
- * service serves eleven and there are fourteen of them, so three must double
32
- * up — the question is which three, and the answer is people who work apart:
33
- * the five the demo puts in one call are all distinct. `echo` was reading for
34
- * three at once, which in a call is one person doing three parts.
31
+ * ONE VOICE PER CHARACTER, and never two who would be in the same room. A
32
+ * house part shares the voice of the character whose face it wears, so Hanzo
33
+ * Coder sounds like Dev; no two characters share one.
35
34
  *
36
35
  * CAST, NOT COPIED. These are the speech service's own voices, chosen for fit
37
- * — a warm animated reader for the physicist, something low and dry for the
38
- * founder. None of them is anybody's actual voice and none is trying to be:
36
+ * — a playful reader for the physicist, something low and measured for the
37
+ * quant. None of them is anybody's actual voice and none is trying to be:
39
38
  * synthesising a real person saying words they never said is a different act
40
39
  * from casting a part, and this is the second one.
41
40
  */
@@ -88,18 +87,22 @@ export declare function speakers(text: string, names: string[]): {
88
87
  who: string | null;
89
88
  text: string;
90
89
  }[];
90
+ /**
91
+ * Every American and British voice the speech service reads in, which is the
92
+ * pool an agent nobody cast is given a voice from. `am_santa` is left out: it
93
+ * is a character voice, not a reader.
94
+ */
91
95
  export declare const VOICES: string[];
96
+ /** A voice for a name nobody cast: the same name always gets the same one. */
92
97
  export declare function fallbackVoice(name: string | undefined): string;
93
98
  /** The voice to read a character's replies in. Always returns a valid voice. */
94
99
  export declare const voiceOf: (name: string | undefined) => string;
100
+ /** Who a voice sounds like, read off its id — what a browser voice standing in is matched on. */
95
101
  export interface VoiceProfile {
96
102
  voice: string;
97
- pitch: number;
98
- rate: number;
99
- description: string;
100
- gender: 'male' | 'female' | 'neutral';
103
+ gender: 'male' | 'female';
104
+ accent: 'american' | 'british';
101
105
  }
102
- export declare const VOICE_PROFILES: Record<string, VoiceProfile>;
103
106
  export declare function voiceProfileOf(name: string | undefined): VoiceProfile;
104
107
  export declare function fallbackMemoji(name: string | undefined): string;
105
108
  /**