@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Vibe.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* /vibe — three columns: the runs started here, a frame pointed at whatever you
|
|
5
|
+
* are building, and the org's people.
|
|
6
|
+
*
|
|
7
|
+
* WHAT IS DRAWN WAS READ, OR IT IS NOT DRAWN. This room shipped as a stage set.
|
|
8
|
+
* It opened holding three members nobody had invited — one of them an
|
|
9
|
+
* individual, named in full at a live domain — all stamped "online" by a
|
|
10
|
+
* literal and counted in the header as "3 online". Under them sat two finished
|
|
11
|
+
* agent runs attributed to those people, priced at $0.0485 and $0.0192 over
|
|
12
|
+
* 146.0s and 32.1s, and three chat messages carrying a poll whose 3-to-1 tally
|
|
13
|
+
* nobody had voted in. The address bar named a host on another org's domain
|
|
14
|
+
* while the frame below it loaded /ocean, a path this site does not serve.
|
|
15
|
+
* Every one of those was typed into the file.
|
|
16
|
+
*
|
|
17
|
+
* The reads that answer are IAM's people, the platform's agents and models, and
|
|
18
|
+
* POST /v1/agents/coding. The chat column went entirely: nothing it displayed
|
|
19
|
+
* came from anywhere, and nothing typed into it ever left the browser — a room
|
|
20
|
+
* that only ever talked to itself. The preview column kept its frame and lost
|
|
21
|
+
* its claims: it now shows the URL in the bar, which is the one thing about it
|
|
22
|
+
* that was ever true.
|
|
23
|
+
*/
|
|
24
|
+
import { dev } from './lib/host.js';
|
|
25
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
26
|
+
import { useRooms } from './host.js';
|
|
27
|
+
import { ArrowUp, Globe, Monitor, RefreshCw, Smartphone, Sparkles, Tablet, UserPlus, Users, } from 'lucide-react';
|
|
28
|
+
import { View, XStack, YStack, Text } from '@hanzo/gui';
|
|
29
|
+
import { Button, Picker } from '@hanzo/ui';
|
|
30
|
+
import { AiProvider, useAgents, useModels, usePeople } from '@hanzo/ai/react';
|
|
31
|
+
import { sku } from './lib/limits.js';
|
|
32
|
+
import { useIam, useOrganizations } from '@hanzo/iam/react';
|
|
33
|
+
import { hasSession } from './lib/session.js';
|
|
34
|
+
import { useHydrated } from './lib/hydrated.js';
|
|
35
|
+
import { ENSO, FREE, served, useAi } from './lib/ai.js';
|
|
36
|
+
import { TopOrgSelector } from './OrgSwitcher.js';
|
|
37
|
+
import { plainly, say } from './failure.js';
|
|
38
|
+
import { invite } from './lib/home.js';
|
|
39
|
+
import { startRun } from './lib/coding.js';
|
|
40
|
+
import { Orgs } from './Orgs.js';
|
|
41
|
+
const clock = () => new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
42
|
+
/** The one grey line, so every empty state in this room is written the same. */
|
|
43
|
+
function Quiet({ children }) {
|
|
44
|
+
return (_jsx(Text, { fontSize: 11, color: "$soft", lineHeight: 16, children: children }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The org's people and its agents.
|
|
48
|
+
*
|
|
49
|
+
* SPLIT OUT because `usePeople` needs an owner and a hook cannot be called
|
|
50
|
+
* conditionally — the same reason Home keeps `Mates` apart from `Team`.
|
|
51
|
+
*
|
|
52
|
+
* NOBODY IS MARKED PRESENT. Nothing here subscribes to presence, so "online"
|
|
53
|
+
* was a field set to the same string for everyone the moment the file loaded.
|
|
54
|
+
* A directory can say who belongs to the org; it cannot say who is looking.
|
|
55
|
+
*/
|
|
56
|
+
function Members({ owner }) {
|
|
57
|
+
const { people, loading, error } = usePeople({ owner, service: false });
|
|
58
|
+
const { agents, loading: counting, error: refused } = useAgents();
|
|
59
|
+
const wrong = error ?? refused;
|
|
60
|
+
if (wrong)
|
|
61
|
+
return _jsx(Quiet, { children: say(wrong, 'your team') });
|
|
62
|
+
const rows = [
|
|
63
|
+
...people.map((p) => ({
|
|
64
|
+
key: p.id,
|
|
65
|
+
name: p.displayName || p.name,
|
|
66
|
+
detail: p.email || '',
|
|
67
|
+
// A ROLE IS A CLAIM IAM MAKES OR NOBODY DOES.
|
|
68
|
+
role: p.admin ? 'Admin' : '',
|
|
69
|
+
mark: (p.displayName || p.name || '?').charAt(0).toUpperCase(),
|
|
70
|
+
})),
|
|
71
|
+
...agents.map((a) => ({
|
|
72
|
+
key: a.id,
|
|
73
|
+
name: a.name,
|
|
74
|
+
detail: a.model || '',
|
|
75
|
+
role: 'Agent',
|
|
76
|
+
mark: a.emoji || a.name.charAt(0).toUpperCase(),
|
|
77
|
+
})),
|
|
78
|
+
];
|
|
79
|
+
if (rows.length === 0)
|
|
80
|
+
return _jsx(Quiet, { children: loading || counting ? 'Reading.' : 'Nobody in this org yet.' });
|
|
81
|
+
return (_jsx(YStack, { gap: "$1.5", children: rows.map((r) => (_jsxs(XStack, { items: "center", justify: "space-between", p: "$2", rounded: 8, bg: "$panel", children: [_jsxs(XStack, { items: "center", gap: "$2.5", flex: 1, overflow: "hidden", 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: r.mark }) }), _jsxs(YStack, { flex: 1, overflow: "hidden", children: [_jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", numberOfLines: 1, children: r.name }), r.detail ? (_jsx(Text, { fontSize: 10, color: "$soft", numberOfLines: 1, children: r.detail })) : null] })] }), r.role ? (_jsx(View, { px: 6, py: 2, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", children: r.role }) })) : null] }, r.key))) }));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The session is asked FIRST, because it is the state that never resolves — a
|
|
85
|
+
* reader nobody will answer would sit on "Reading." until they closed the tab.
|
|
86
|
+
*
|
|
87
|
+
* `useHydrated` keeps the server's render and the first client one agreeing:
|
|
88
|
+
* `useOrganizations` resolves synchronously in the browser and never on the
|
|
89
|
+
* server, and React answers a text mismatch by throwing the tree away.
|
|
90
|
+
*/
|
|
91
|
+
function Team() {
|
|
92
|
+
const hydrated = useHydrated();
|
|
93
|
+
const { isAuthenticated } = useIam();
|
|
94
|
+
const signedIn = isAuthenticated || hasSession();
|
|
95
|
+
const { currentOrgId } = useOrganizations() || {};
|
|
96
|
+
if (!hydrated)
|
|
97
|
+
return _jsx(Quiet, { children: "\u00A0" });
|
|
98
|
+
if (!signedIn)
|
|
99
|
+
return _jsx(Quiet, { children: "Sign in to see who is in this workspace." });
|
|
100
|
+
if (!currentOrgId)
|
|
101
|
+
return _jsx(Quiet, { children: "Reading." });
|
|
102
|
+
return _jsx(Members, { owner: currentOrgId });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* THIS ROOM MOUNTS ITS OWN CLIENT.
|
|
106
|
+
*
|
|
107
|
+
* Every other room on this site arrives under `Room` → `Workspace`, which
|
|
108
|
+
* supplies the `AiProvider` for the whole shell. `/vibe` is routed straight to
|
|
109
|
+
* this component, so nothing above it does — and the moment anything here reads
|
|
110
|
+
* the platform, `useAi` raises and the route answers 500. The provider prefers
|
|
111
|
+
* the `client` prop over the one it builds itself, so a session that settles
|
|
112
|
+
* after mount reaches the hooks below; signed out, its own anonymous client
|
|
113
|
+
* stands in and the org-scoped reads simply answer nothing.
|
|
114
|
+
*
|
|
115
|
+
* The reading half is a separate component because a hook cannot run in the
|
|
116
|
+
* component that mounts its provider.
|
|
117
|
+
*/
|
|
118
|
+
function VibeWorkspaceInner() {
|
|
119
|
+
const { client } = useAi();
|
|
120
|
+
return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(Vibe, {}) }));
|
|
121
|
+
}
|
|
122
|
+
function Vibe() {
|
|
123
|
+
const { Link } = useRooms();
|
|
124
|
+
const { user } = useIam();
|
|
125
|
+
// Hanzo models alone, as every picker here offers them (`sku`).
|
|
126
|
+
const { models: listed } = useModels();
|
|
127
|
+
const models = useMemo(() => listed.filter((m) => sku(m.id)), [listed]);
|
|
128
|
+
const [activeTabMobile, setActiveTabMobile] = useState('stream');
|
|
129
|
+
// The frame shows what the bar says. It starts empty because a run publishes
|
|
130
|
+
// no URL — the 202 names a session, not an address — so this room has nothing
|
|
131
|
+
// to point at until somebody points it.
|
|
132
|
+
const [previewUrl, setPreviewUrl] = useState('');
|
|
133
|
+
const [previewDevice, setPreviewDevice] = useState('desktop');
|
|
134
|
+
const [previewKey, setPreviewKey] = useState(0);
|
|
135
|
+
const [agentInput, setAgentInput] = useState('');
|
|
136
|
+
// The house name for the lane, resolved against what the gateway serves —
|
|
137
|
+
// `served` falls to a model that answers rather than sending one nobody has.
|
|
138
|
+
const [wanted, setWanted] = useState(user ? ENSO : FREE);
|
|
139
|
+
const model = served(models, wanted);
|
|
140
|
+
const [busy, setBusy] = useState(false);
|
|
141
|
+
const [turns, setTurns] = useState([]);
|
|
142
|
+
const bottom = useRef(null);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
bottom.current?.scrollIntoView({ behavior: 'smooth' });
|
|
145
|
+
}, [turns]);
|
|
146
|
+
const who = user?.displayName || user?.name || user?.email || 'You';
|
|
147
|
+
/**
|
|
148
|
+
* SEND THE PROMPT, AND SAY WHAT CAME BACK.
|
|
149
|
+
*
|
|
150
|
+
* `repo` is no longer sent. It carried the room's name — which defaulted to
|
|
151
|
+
* `ocean-genesis-room` and was editable free text — into a field the contract
|
|
152
|
+
* defines as `owner/name` in the caller's own org, so a label typed in a
|
|
153
|
+
* header reached a real write as a repository. Omitting it is what the
|
|
154
|
+
* contract means by optional: the server picks the default.
|
|
155
|
+
*/
|
|
156
|
+
const send = () => {
|
|
157
|
+
const task = agentInput.trim();
|
|
158
|
+
if (!task || busy)
|
|
159
|
+
return;
|
|
160
|
+
const id = `t${Date.now()}`;
|
|
161
|
+
setAgentInput('');
|
|
162
|
+
setBusy(true);
|
|
163
|
+
setTurns((prev) => [...prev, { id, time: clock(), who, prompt: task, model }]);
|
|
164
|
+
startRun({ prompt: task, model })
|
|
165
|
+
.then((run) => setTurns((prev) => prev.map((t) => (t.id === id ? { ...t, run } : t))))
|
|
166
|
+
.catch((e) => setTurns((prev) => prev.map((t) => (t.id === id ? { ...t, refused: plainly(e, 'this run') } : t))))
|
|
167
|
+
.finally(() => setBusy(false));
|
|
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
|
|
170
|
+
? `Running on ${turn.run.targetId}`
|
|
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
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
send();
|
|
175
|
+
}
|
|
176
|
+
}, placeholder: "Describe a task and an agent starts on it\u2026", rows: 2, disabled: busy }), "$platform-web": { resize: 'none' }, width: "100%", bg: "transparent", px: 12, py: 10, color: "$ink", fontSize: "$2", outlineStyle: "none", borderWidth: 0 }), _jsx(XStack, { items: "center", justify: "flex-end", px: "$2.5", pb: "$2", children: _jsxs(Button, { variant: "primary", size: "sm", onClick: send, disabled: !agentInput.trim() || busy, children: ["Send", _jsx(ArrowUp, { size: 13 })] }) })] })] })] }), _jsxs(YStack, { height: "100%", borderRightWidth: 1, borderColor: "$borderColor", bg: "$background", minW: 0, display: activeTabMobile === 'preview' ? 'flex' : 'none', $md: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", justify: "space-between", gap: "$2", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsxs(XStack, { items: "center", gap: "$2", flex: 1, maxW: 420, bg: "$panel", rounded: 6, px: "$2.5", py: 3, borderWidth: 1, borderColor: "var(--border-control)", children: [_jsx(Globe, { size: 13, color: "var(--muted-foreground)" }), _jsx(Text, { render: _jsx("input", { type: "text", value: previewUrl, onChange: (e) => setPreviewUrl(e.target.value), placeholder: "https://\u2026" }), flex: 1, bg: "transparent", borderWidth: 0, color: "$ink", fontSize: "$1", fontFamily: "$mono", outlineStyle: "none" })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { items: "center", gap: 2, bg: "$panel", rounded: 6, p: 2, borderWidth: 1, borderColor: "$borderColor", children: [
|
|
177
|
+
['desktop', 'Desktop', Monitor],
|
|
178
|
+
['tablet', 'Tablet', Tablet],
|
|
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, {}) })] })] })] }));
|
|
181
|
+
}
|
|
182
|
+
// THE GATE IS NOT A ROOM'S TO REMEMBER. This room draws its own shell rather
|
|
183
|
+
// than `Room`, which is where every other room picks the organization gate up —
|
|
184
|
+
// so it was reachable, and usable, by an account that had never paid. Wrapping
|
|
185
|
+
// the export means the route cannot mount the room without it.
|
|
186
|
+
export function VibeWorkspace() {
|
|
187
|
+
return (_jsx(Orgs, { children: _jsx(VibeWorkspaceInner, {}) }));
|
|
188
|
+
}
|
package/dist/Work.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The board, and the session its reads need.
|
|
4
|
+
*
|
|
5
|
+
* SPLIT so the hooks below never run for a reader with none: sessions and their
|
|
6
|
+
* trees are the org's, answered 403 without a session, and a hook cannot be
|
|
7
|
+
* called conditionally — so the condition is a component boundary rather than
|
|
8
|
+
* an `if` the rules forbid.
|
|
9
|
+
*/
|
|
10
|
+
export declare function Work({ idle }: {
|
|
11
|
+
idle?: ReactNode;
|
|
12
|
+
}): import("react").JSX.Element;
|
package/dist/Work.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The working view: what is running, what it did, and what it made.
|
|
4
|
+
//
|
|
5
|
+
// Every piece here comes from `@hanzo/ui/agents` and every fact from
|
|
6
|
+
// `@hanzo/ai`. This file is arrangement — it owns no pixels and no transport,
|
|
7
|
+
// which is the test of whether the lift actually landed: a stranger with the
|
|
8
|
+
// two packages can assemble this room, and a channel workspace will assemble
|
|
9
|
+
// the same one from the same parts.
|
|
10
|
+
//
|
|
11
|
+
// THE BOARD IS A TREE, not a list. `sessions.list` answers ROOT sessions only,
|
|
12
|
+
// so a fan-out is invisible until you ask for it: each root with `children > 0`
|
|
13
|
+
// gets one `tree` read, and `nest` is not needed here because the server
|
|
14
|
+
// already materialises the nesting. A root with no children costs no request.
|
|
15
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
16
|
+
import { useAi, useSession, useSessions } from '@hanzo/ai/react';
|
|
17
|
+
import { useOpen } from './open.js';
|
|
18
|
+
import { AgentBoard, ChannelHeader, Pane, ProgressBlock, Steer, steps, } from '@hanzo/ui/agents';
|
|
19
|
+
import { Text, XStack, YStack } from '@hanzo/ui';
|
|
20
|
+
import { useIam } from '@hanzo/iam/react';
|
|
21
|
+
import { Composer } from '@hanzo/ui/chat';
|
|
22
|
+
import { Start } from './Start.js';
|
|
23
|
+
import { Bar } from './progress.js';
|
|
24
|
+
/** The server's four statuses, plus the honest fallback for anything else. */
|
|
25
|
+
const STATUS = (s) => s === 'running' || s === 'paused' || s === 'done' || s === 'error' ? s : 'unknown';
|
|
26
|
+
/**
|
|
27
|
+
* A session as the board reads it.
|
|
28
|
+
*
|
|
29
|
+
* `doing` is `lastEvent.preview` — the first 240 BYTES of a payload, cut
|
|
30
|
+
* without regard for JSON or rune boundaries. It is rendered, never parsed, and
|
|
31
|
+
* never re-cut here: a second truncation of an already-truncated string is how
|
|
32
|
+
* a preview becomes a fragment of a fragment.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* What the run is doing, in words.
|
|
36
|
+
*
|
|
37
|
+
* The preview is the first 240 bytes of the raw event, so for a machine payload
|
|
38
|
+
* it arrives as `{"type":"done","finishRea…` — a fragment of JSON with the
|
|
39
|
+
* closing brace cut off. Rendering that put twenty-five lines of escaped JSON
|
|
40
|
+
* down the board where the summaries belong, which reads as a crash.
|
|
41
|
+
*
|
|
42
|
+
* It cannot be parsed back (it is truncated by construction), so this reads the
|
|
43
|
+
* one field that survives the cut — `type` is written first by every producer —
|
|
44
|
+
* and says it. Anything that does not look like a payload is human text already
|
|
45
|
+
* and passes through untouched.
|
|
46
|
+
*/
|
|
47
|
+
const say = (preview) => {
|
|
48
|
+
if (!preview)
|
|
49
|
+
return undefined;
|
|
50
|
+
const raw = preview.trim();
|
|
51
|
+
if (!raw.startsWith('{') && !raw.startsWith('"{') && !raw.startsWith('['))
|
|
52
|
+
return preview;
|
|
53
|
+
const kind = /"type"\s*:\s*\\?"([a-z_]+)/i.exec(raw)?.[1];
|
|
54
|
+
switch (kind) {
|
|
55
|
+
case 'done': return 'Finished';
|
|
56
|
+
case 'error': return 'Failed';
|
|
57
|
+
case 'start': return 'Starting';
|
|
58
|
+
case 'tool':
|
|
59
|
+
case 'tool_call': return 'Using a tool';
|
|
60
|
+
case 'text':
|
|
61
|
+
case 'delta': return 'Writing';
|
|
62
|
+
case undefined: return undefined;
|
|
63
|
+
default: return kind.replace(/_/g, ' ');
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const toRun = (s, children = []) => ({
|
|
67
|
+
id: s.id,
|
|
68
|
+
agent: s.agent || s.title || s.id,
|
|
69
|
+
status: STATUS(s.status),
|
|
70
|
+
doing: say(s.lastEvent?.preview),
|
|
71
|
+
target: s.target,
|
|
72
|
+
project: s.project,
|
|
73
|
+
children,
|
|
74
|
+
});
|
|
75
|
+
/** Flatten the server's tree into the board's, dropping the node we asked for. */
|
|
76
|
+
const kidsOf = (node) =>
|
|
77
|
+
// `children` is NULL on a leaf, not `[]` — the guard is load-bearing.
|
|
78
|
+
(node?.children ?? []).map((child) => child.session ? toRun(child.session, kidsOf(child)) : null).filter((r) => r !== null);
|
|
79
|
+
function Watching({ idle }) {
|
|
80
|
+
const ai = useAi();
|
|
81
|
+
const { sessions, loading, reload } = useSessions({ limit: 100 });
|
|
82
|
+
// THE OPEN RUN IS THE SHELL'S, not this pane's. `Start` announces a new run by
|
|
83
|
+
// writing `openSession`, which is how /dev picks it up; this pane kept its own
|
|
84
|
+
// useState and so never heard, and the one control the idle state offers spent
|
|
85
|
+
// money on a real run and left the reader reading "No runs yet". Same store,
|
|
86
|
+
// and the list is re-read when the id changes so the new row is there to open.
|
|
87
|
+
const { session: open, openSession: setOpen } = useOpen();
|
|
88
|
+
const [trees, setTrees] = useState({});
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (open)
|
|
91
|
+
reload();
|
|
92
|
+
}, [open, reload]);
|
|
93
|
+
// One `tree` read per root that says it has children. Roots without a
|
|
94
|
+
// fan-out are left alone, so an org of solo runs costs exactly one request.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const safeSessions = Array.isArray(sessions) ? sessions : [];
|
|
97
|
+
const swarms = safeSessions.filter((s) => (s?.children ?? 0) > 0);
|
|
98
|
+
if (!swarms.length)
|
|
99
|
+
return;
|
|
100
|
+
const stop = new AbortController();
|
|
101
|
+
void Promise.all(swarms.map((s) => ai.sessions
|
|
102
|
+
.tree(s.id, { signal: stop.signal })
|
|
103
|
+
.then((node) => [s.id, kidsOf(node)])
|
|
104
|
+
.catch(() => [s.id, []]))).then((pairs) => {
|
|
105
|
+
if (!stop.signal.aborted)
|
|
106
|
+
setTrees(Object.fromEntries(pairs));
|
|
107
|
+
});
|
|
108
|
+
return () => stop.abort();
|
|
109
|
+
}, [ai, sessions]);
|
|
110
|
+
const runs = useMemo(() => sessions.map((s) => toRun(s, trees[s.id] ?? [])), [sessions, trees]);
|
|
111
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", $lg: { flexDirection: 'row' }, children: [_jsx(YStack
|
|
112
|
+
// AN EMPTY BOARD DRAWS NOTHING ACROSS THE TOP. As a full-width strip it
|
|
113
|
+
// reserved 270px and a hairline for one line of grey text, and pushed
|
|
114
|
+
// the pane's own content most of a screen down — a narrow window's
|
|
115
|
+
// whole budget spent saying there is nothing to show. As a COLUMN it is
|
|
116
|
+
// still worth drawing empty, because a column with a heading is where a
|
|
117
|
+
// reader looks for the list; a strip is not.
|
|
118
|
+
, {
|
|
119
|
+
// AN EMPTY BOARD DRAWS NOTHING ACROSS THE TOP. As a full-width strip it
|
|
120
|
+
// reserved 270px and a hairline for one line of grey text, and pushed
|
|
121
|
+
// the pane's own content most of a screen down — a narrow window's
|
|
122
|
+
// whole budget spent saying there is nothing to show. As a COLUMN it is
|
|
123
|
+
// still worth drawing empty, because a column with a heading is where a
|
|
124
|
+
// reader looks for the list; a strip is not.
|
|
125
|
+
display: runs.length === 0 ? 'none' : 'flex', width: "100%",
|
|
126
|
+
// A STRIP HAS A HEIGHT, and `maxHeight` could not give it one. The
|
|
127
|
+
// board inside is a ScrollView at `flex: 1 1 0` — a zero basis — so in
|
|
128
|
+
// a column with an auto height it measured 1px and the strip vanished.
|
|
129
|
+
// "Up to 220" is not expressible over a child that contributes nothing;
|
|
130
|
+
// 220 is.
|
|
131
|
+
height: 220, shrink: 0, borderBottomWidth: 1,
|
|
132
|
+
// A COLUMN NEEDS A WINDOW WIDE ENOUGH FOR TWO. At 640 this took 300 of
|
|
133
|
+
// the 316px left after the rail and the sidebar, so the pane beside it
|
|
134
|
+
// was 32px wide and the run itself was unreadable — a narrow strip with
|
|
135
|
+
// a rule down it, which is what a squeezed column looks like. Above
|
|
136
|
+
// 1024 there is room for both; below it the board is a strip across the
|
|
137
|
+
// top and the run has the whole width under it.
|
|
138
|
+
//
|
|
139
|
+
// STATE THE VALUE, NEVER `undefined`. gui emits nothing for an
|
|
140
|
+
// undefined value, so the base cap this block used to lift SURVIVED
|
|
141
|
+
// into `$lg` and the full-height column described here was a 220px stub
|
|
142
|
+
// at every width — measured in the browser at 1280.
|
|
143
|
+
$lg: {
|
|
144
|
+
display: 'flex',
|
|
145
|
+
width: 300,
|
|
146
|
+
shrink: 0,
|
|
147
|
+
height: '100%',
|
|
148
|
+
borderBottomWidth: 0,
|
|
149
|
+
borderRightWidth: 1,
|
|
150
|
+
}, borderColor: "$borderColor", children: _jsx(AgentBoard, { runs: runs, active: open ?? undefined, onOpen: setOpen, empty: _jsx(Text, { fontSize: "$2", color: "$soft", children: loading ? 'Reading runs…' : 'No runs yet. Start one and it appears here.' }) }) }), _jsx(YStack, { width: "100%", flex: 1, minW: 0, minH: 0, $lg: { width: 'auto' }, children: open ? _jsx(Watch, { id: open }, open) : (idle ?? _jsx(Idle, { runs: runs.length })) })] }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Nothing is open. What that MEANS depends on whether there is anything to
|
|
154
|
+
* open: with runs on the board it is an instruction, and with none the pane is
|
|
155
|
+
* the whole width (the board draws nothing) and the honest thing to show is the
|
|
156
|
+
* home — the composer and what to do next.
|
|
157
|
+
*/
|
|
158
|
+
function Idle({ runs }) {
|
|
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." })] }) }));
|
|
161
|
+
}
|
|
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
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One run: its steps on the left, whatever it produced on the right.
|
|
166
|
+
*
|
|
167
|
+
* KEYED BY THE RUN at the call site, so opening a different one REMOUNTS — the
|
|
168
|
+
* live tail, the draft and the last confirmation all start empty rather than
|
|
169
|
+
* carrying the previous run's.
|
|
170
|
+
*/
|
|
171
|
+
function Watch({ id }) {
|
|
172
|
+
const ai = useAi();
|
|
173
|
+
const { session } = useSession(id);
|
|
174
|
+
const [live, setLive] = useState([]);
|
|
175
|
+
const [draft, setDraft] = useState('');
|
|
176
|
+
const [note, setNote] = useState(null);
|
|
177
|
+
// The live feed, narrowed to this run's tree. There is no per-session stream
|
|
178
|
+
// — `/v1/agents/sessions/{id}/stream` does not exist — so `root` is how one
|
|
179
|
+
// run is watched, and it brings its sub-agents' turns with it.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const stop = new AbortController();
|
|
182
|
+
void (async () => {
|
|
183
|
+
try {
|
|
184
|
+
for await (const frame of ai.sessions.stream({ root: id }, { signal: stop.signal })) {
|
|
185
|
+
if (stop.signal.aborted)
|
|
186
|
+
return;
|
|
187
|
+
if (frame.kind === 'event')
|
|
188
|
+
setLive((prev) => [...prev, frame.event]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// A dropped feed is ordinary. What is on screen stands.
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
return () => stop.abort();
|
|
196
|
+
}, [ai, id]);
|
|
197
|
+
// The 50 the detail carries, then everything the feed added, deduped by seq —
|
|
198
|
+
// the two overlap whenever a turn lands between the read and the subscribe.
|
|
199
|
+
const events = useMemo(() => {
|
|
200
|
+
const seen = new Map();
|
|
201
|
+
for (const [i, e] of [...(session?.recentEvents ?? []), ...live].entries()) {
|
|
202
|
+
seen.set(e.seq !== undefined ? `s${e.seq}` : (e.id ?? `i${i}`), e);
|
|
203
|
+
}
|
|
204
|
+
return [...seen.values()].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
205
|
+
}, [session?.recentEvents, live]);
|
|
206
|
+
const { steps: shown, message } = useMemo(() => steps(events), [events]);
|
|
207
|
+
const status = STATUS(session?.status);
|
|
208
|
+
const finished = status === 'done' || status === 'error';
|
|
209
|
+
const steer = useCallback(async (command) => {
|
|
210
|
+
setNote(null);
|
|
211
|
+
try {
|
|
212
|
+
await ai.sessions.steer(id, command);
|
|
213
|
+
setNote(`${command} queued — it takes effect between turns`);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
setNote(`Could not ${command} this run`);
|
|
217
|
+
}
|
|
218
|
+
}, [ai, id]);
|
|
219
|
+
const send = useCallback(async () => {
|
|
220
|
+
const text = draft.trim();
|
|
221
|
+
if (!text)
|
|
222
|
+
return;
|
|
223
|
+
setDraft('');
|
|
224
|
+
setNote(null);
|
|
225
|
+
try {
|
|
226
|
+
await ai.sessions.steer(id, 'message', { message: text });
|
|
227
|
+
setNote('Sent — the agent reads it between turns');
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
setNote('Could not reach this run');
|
|
231
|
+
}
|
|
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: [
|
|
234
|
+
{ id: 'code', label: 'Code', kind: 'code', content: _jsx(Nothing, { what: "changes" }) },
|
|
235
|
+
{ id: 'view', label: 'View', kind: 'view', content: _jsx(Nothing, { what: "preview" }) },
|
|
236
|
+
{ id: 'canvas', label: 'Canvas', kind: 'canvas', content: _jsx(Nothing, { what: "summary" }) },
|
|
237
|
+
] }) })] }));
|
|
238
|
+
}
|
|
239
|
+
function Nothing({ what }) {
|
|
240
|
+
return (_jsxs(Text, { fontSize: "$2", color: "$soft", children: ["This run has produced no ", what, " yet."] }));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The board, and the session its reads need.
|
|
244
|
+
*
|
|
245
|
+
* SPLIT so the hooks below never run for a reader with none: sessions and their
|
|
246
|
+
* trees are the org's, answered 403 without a session, and a hook cannot be
|
|
247
|
+
* called conditionally — so the condition is a component boundary rather than
|
|
248
|
+
* an `if` the rules forbid.
|
|
249
|
+
*/
|
|
250
|
+
export function Work({ idle }) {
|
|
251
|
+
const { user, isLoading } = useIam();
|
|
252
|
+
if (isLoading || !user) {
|
|
253
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$2", children: [_jsx(Text, { render: "h1", fontSize: "$5", fontWeight: "600", color: "$ink", children: "Agents" }), _jsx(Text, { fontSize: "$3", color: "$soft", text: "center", children: isLoading ? '\u00a0' : 'Sign in to watch your agents work.' })] }));
|
|
254
|
+
}
|
|
255
|
+
return _jsx(Watching, { idle: idle });
|
|
256
|
+
}
|