@hanzo/build 0.2.52 → 0.2.53
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/README.md +103 -49
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +24 -4
- package/lib/api/github.js +60 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -38
- package/lib/api/work.js +132 -44
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +12 -52
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +139 -71
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +70 -38
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +45 -0
- package/lib/panel.js +115 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.js +16 -6
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +16 -0
- package/lib/route.js +40 -0
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +22 -8
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
package/lib/standing.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an account stands: which plan it is on, which meter it reads, and the
|
|
3
|
+
* reads behind it — the tier for the plan's name, the limits for its windows,
|
|
4
|
+
* the free allowance, and the prepaid balance. One read for every surface that
|
|
5
|
+
* shows a plan (the account menus, Usage, Billing), so they cannot disagree.
|
|
6
|
+
*
|
|
7
|
+
* Each read stands alone: one that fails leaves its part empty and the rest
|
|
8
|
+
* drawn. Only when none answers is the whole read an error.
|
|
9
|
+
*/
|
|
10
|
+
import { allowance, balance, tier } from './api/billing.js';
|
|
11
|
+
import { limits } from './api/limits.js';
|
|
12
|
+
import { useRead } from './data.js';
|
|
13
|
+
import { kind, label, when } from './plan.js';
|
|
14
|
+
const got = (r) => (r.status === 'fulfilled' ? r.value : null);
|
|
15
|
+
/** Every read at once. Rejects only when all of them were refused, with the first refusal. */
|
|
16
|
+
export async function stand(t) {
|
|
17
|
+
const all = await Promise.allSettled([tier(t), limits(t), allowance(t), balance(t)]);
|
|
18
|
+
if (all.every((r) => r.status === 'rejected'))
|
|
19
|
+
throw all[0].reason;
|
|
20
|
+
const [billed, used, free, money] = [got(all[0]), got(all[1]), got(all[2]), got(all[3])];
|
|
21
|
+
// Billing names what was bought; the limits name the rung the gate counts. A
|
|
22
|
+
// plan either one names is the plan.
|
|
23
|
+
const slug = billed?.plan || (used && used.plan !== 'free' ? used.plan : '');
|
|
24
|
+
const k = kind(slug, money, billed?.tier);
|
|
25
|
+
return {
|
|
26
|
+
kind: k,
|
|
27
|
+
label: k === 'plan' ? label(slug, billed?.tier) : k === 'free' ? { name: 'Free', tag: '' } : { name: 'Pay as you go', tag: '' },
|
|
28
|
+
limits: used,
|
|
29
|
+
allowance: free,
|
|
30
|
+
balance: money,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The standing for `t`, read when `enabled` (a menu that is open) and again when the org changes. */
|
|
34
|
+
export function useStanding(t, enabled = true) {
|
|
35
|
+
return useRead(enabled && t.token() ? () => stand(t) : null, null, [t, enabled]);
|
|
36
|
+
}
|
|
37
|
+
const RANK = { ok: 0, near: 1, limited: 2 };
|
|
38
|
+
/**
|
|
39
|
+
* A plan's windows, shortest first: the session, the UTC day, and the month —
|
|
40
|
+
* the billing period of whichever class of model has used the most of it,
|
|
41
|
+
* because that is the one that stops a request first.
|
|
42
|
+
*/
|
|
43
|
+
export function rows(l, now = Date.now()) {
|
|
44
|
+
if (!l)
|
|
45
|
+
return [];
|
|
46
|
+
const month = Object.values(l.classes).reduce((a, c) => (!c ? a : !a || c.percent > a.percent || (c.percent === a.percent && RANK[c.state] > RANK[a.state]) ? c : a), null);
|
|
47
|
+
const out = [];
|
|
48
|
+
const add = (name, w) => {
|
|
49
|
+
if (!w)
|
|
50
|
+
return;
|
|
51
|
+
const at = when(w.resets, now);
|
|
52
|
+
out.push({ name, percent: w.percent, state: w.state, resets: at ? `Resets ${at}` : '' });
|
|
53
|
+
};
|
|
54
|
+
add('Session', l.session);
|
|
55
|
+
add('Today', l.day);
|
|
56
|
+
add('Month', month);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** The share of the free allowance left in its window, 0 to 100, or null where none bounds the account. */
|
|
60
|
+
export function left(a) {
|
|
61
|
+
if (!a || a.limit <= 0)
|
|
62
|
+
return null;
|
|
63
|
+
return Math.max(0, 100 - Math.round((Math.min(a.used, a.limit) / a.limit) * 100));
|
|
64
|
+
}
|
package/lib/sync.js
CHANGED
|
@@ -11,7 +11,10 @@ import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
|
11
11
|
import { Check, ChevronLeft, ChevronRight, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
12
12
|
import { Button, Input } from '@hanzo/ui';
|
|
13
13
|
import { useEffect, useMemo, useState } from 'react';
|
|
14
|
-
import {
|
|
14
|
+
import { here, useBack } from './back.js';
|
|
15
|
+
import { Refusal } from './api/call.js';
|
|
16
|
+
import { accounts, bring, grants } from './api/github.js';
|
|
17
|
+
import { authorize } from './api/provider.js';
|
|
15
18
|
import { readPending, writePending } from './choice.js';
|
|
16
19
|
import { useRead } from './data.js';
|
|
17
20
|
import { useHost, useTarget } from './host.js';
|
|
@@ -47,24 +50,35 @@ export function Sync() {
|
|
|
47
50
|
const all = names.every((n) => picked.includes(n));
|
|
48
51
|
setPicked((cur) => (all ? cur.filter((n) => !names.includes(n)) : [...new Set([...cur, ...names])]));
|
|
49
52
|
};
|
|
53
|
+
// Coming back from GitHub's App install lands here, says what happened, and reads the list again.
|
|
54
|
+
useBack(t, signed, setError, list.reload);
|
|
55
|
+
/**
|
|
56
|
+
* Install the platform's GitHub App on another account — an organization admin's
|
|
57
|
+
* act — and come back to this page: the App, not a person's own sign-in, is what
|
|
58
|
+
* grants an organization's repositories.
|
|
59
|
+
*/
|
|
50
60
|
const addOrg = async () => {
|
|
51
61
|
setError('');
|
|
62
|
+
if (!host.admin) {
|
|
63
|
+
setError('An organization admin installs the GitHub App on another account.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
52
66
|
try {
|
|
53
|
-
|
|
54
|
-
host.open(url);
|
|
67
|
+
host.open(await authorize(t, 'github', here()));
|
|
55
68
|
}
|
|
56
69
|
catch (e) {
|
|
57
|
-
setError(e.
|
|
70
|
+
setError(e instanceof Refusal && e.status === 403 ? 'An organization admin installs the GitHub App on another account.' : 'GitHub could not be reached to install the App. Try again.');
|
|
58
71
|
}
|
|
59
72
|
};
|
|
60
73
|
const send = async () => {
|
|
61
74
|
setBusy(true);
|
|
62
75
|
setError('');
|
|
63
76
|
try {
|
|
64
|
-
await bring(t, picked);
|
|
77
|
+
const queued = await bring(t, picked);
|
|
78
|
+
// Each waits under the name it takes on the forge, as the import says it, else as GitHub's listing says it will.
|
|
65
79
|
const next = [
|
|
66
80
|
...readPending(host.org).filter((p) => !picked.includes(p.fullName)),
|
|
67
|
-
...chosen.map((r) => ({ fullName: r.fullName, name: r.name })),
|
|
81
|
+
...chosen.map((r) => ({ fullName: r.fullName, name: queued.codebases[r.fullName.toLowerCase()] || r.codebase || r.name })),
|
|
68
82
|
];
|
|
69
83
|
writePending(host.org, next);
|
|
70
84
|
setSyncing(true);
|
|
@@ -102,7 +116,7 @@ export function Sync() {
|
|
|
102
116
|
window.clearInterval(id);
|
|
103
117
|
};
|
|
104
118
|
}, [syncing, t, host.org, host.go, picked]);
|
|
105
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to projects", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Projects" }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: "/" }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Sync" })] }), _jsxs(YStack, { flex: 1, items: "center", justify: "center", px: "$6", pb: "$10", gap: "$5", children: [_jsxs(XStack, { justify: "center", gap: "$6", children: [_jsx(Step, { label: "Select repositories", done: step === 'access', current: step === 'pick' }), _jsx(Step, { label: "Repository access", done: false, current: step === 'access' })] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$5", bg: "$panel", px: "$6", py: "$6", gap: "$4", width: "100%", maxW: 460, children: [step === 'pick' && !owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search organizations", "aria-label": "Search organizations", disabled: !signed }), signed ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${orgs.length} ${orgs.length === 1 ? 'organization' : 'organizations'}` })) : null, list.error ? _jsx(Soft, { children:
|
|
119
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [_jsxs(XStack, { px: "$6", pt: "$5", pb: "$2", gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Back to projects", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Projects" }) }), _jsx(SizableText, { size: "$2", color: "$soft", children: "/" }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Sync" })] }), _jsxs(YStack, { flex: 1, items: "center", justify: "center", px: "$6", pb: "$10", gap: "$5", children: [_jsxs(XStack, { justify: "center", gap: "$6", children: [_jsx(Step, { label: "Select repositories", done: step === 'access', current: step === 'pick' }), _jsx(Step, { label: "Repository access", done: false, current: step === 'access' })] }), _jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$5", bg: "$panel", px: "$6", py: "$6", gap: "$4", width: "100%", maxW: 460, children: [step === 'pick' && !owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search organizations", "aria-label": "Search organizations", disabled: !signed }), signed ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${orgs.length} ${orgs.length === 1 ? 'organization' : 'organizations'}` })) : null, list.error ? _jsx(Soft, { children: "The GitHub connection could not be read right now." }) : null, !signed ? (_jsx(Soft, { children: "Sign in to choose repositories the GitHub connection can read." })) : list.loading && rows.length === 0 && orgs.length === 0 ? (_jsx(Soft, { children: "Reading the connection\u2026" })) : shownOrgs.length === 0 ? (_jsx(Soft, { children: orgs.length === 0 ? 'No organizations on this connection yet.' : 'Nothing matches.' })) : (shownOrgs.map((a) => (_jsxs(XStack, { render: "button", "aria-label": a.blocked ? `${a.name} needs an organization admin` : `Open ${a.name}`, disabled: a.blocked, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: () => {
|
|
106
120
|
setOwner(a.name);
|
|
107
121
|
setQ('');
|
|
108
122
|
}, children: [_jsx(SizableText, { flex: 1, size: "$2", color: a.blocked ? '$soft' : '$ink', numberOfLines: 1, children: a.name }), a.blocked ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Needs organization admin" })) : (_jsx(ChevronRight, { size: 14 }))] }, a.name)))), _jsxs(XStack, { render: "button", "aria-label": "Add organization", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), disabled: !signed, children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add organization" })] })] })] })) : null, step === 'pick' && owner ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Select repositories" }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Choose repositories to bring into ", dest] })] }), _jsxs(Panel, { children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { render: "button", "aria-label": "All organizations", onPress: () => setOwner(''), children: _jsx(ChevronLeft, { size: 16 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: owner })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Search repositories", "aria-label": "Search repositories" }), _jsxs(XStack, { justify: "space-between", items: "center", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [ofOwner.length, " ", ofOwner.length === 1 ? 'repository' : 'repositories'] }), _jsx(XStack, { render: "button", "aria-label": "Select all", onPress: selectShown, disabled: shownRepos.length === 0, children: _jsx(SizableText, { size: "$1", color: "$ink", children: "Select all" }) })] }), shownRepos.length === 0 ? (_jsx(Soft, { children: "Nothing matches." })) : (shownRepos.map((r) => (_jsx(RepoRow, { repo: r, on: picked.includes(r.fullName), onPress: () => toggle(r.fullName) }, r.fullName)))), _jsxs(XStack, { render: "button", "aria-label": "Grant more repositories", items: "center", gap: "$2", py: "$2", onPress: () => void addOrg(), children: [_jsx(Plus, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Add repositories" })] })] })] })) : null, step === 'access' ? (_jsxs(_Fragment, { children: [_jsxs(YStack, { gap: "$1", items: "center", children: [_jsx(SizableText, { size: "$5", color: "$ink", children: "Repository access" }), _jsx(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: "Review who can read these repositories" })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsxs(XStack, { flex: 1, items: "center", gap: "$2", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", "aria-label": "Clear the selection", onPress: () => { setPicked([]); setStep('pick'); }, children: _jsx(X, { size: 12 }) }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: more ? `${lead}, +${more} more` : lead })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setStep('pick'), children: "Edit" })] }), _jsxs(XStack, { justify: "space-between", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Who gets access" }), _jsxs(SizableText, { size: "$2", color: "$ink", children: [host.person?.name || host.person?.email || 'You', " \u00B7 You"] })] }), _jsxs(YStack, { gap: "$1", items: "flex-end", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Role" }), _jsx(SizableText, { size: "$2", color: "$ink", children: host.admin ? 'Repository admin' : 'Member' })] })] }), _jsxs(SizableText, { size: "$2", color: "$soft", style: { textAlign: 'center' }, children: ["Anyone with access to ", dest] })] })) : null, error ? _jsx(Soft, { children: error }) : null, step === 'pick' ? (_jsx(Button, { width: "100%", disabled: !owner || picked.length === 0, onPress: () => setStep('access'), children: "Continue" })) : (_jsxs(YStack, { gap: "$3", items: "center", width: "100%", children: [_jsx(Button, { width: "100%", disabled: busy || syncing || picked.length === 0, onPress: () => void send(), children: syncing || busy ? 'Syncing…' : `Sync ${picked.length} ${picked.length === 1 ? 'repository' : 'repositories'} to ${dest}` }), syncing ? null : (_jsx(XStack, { render: "button", "aria-label": "Sync later", onPress: back, children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Sync later" }) }))] })), !signed ? (_jsx(Button, { width: "100%", onPress: () => host.signIn?.(), children: "Sign in" })) : null] })] })] }));
|
|
@@ -117,5 +131,5 @@ function Soft({ children }) {
|
|
|
117
131
|
return (_jsx(SizableText, { size: "$2", color: "$soft", children: children }));
|
|
118
132
|
}
|
|
119
133
|
function RepoRow({ repo, on, onPress }) {
|
|
120
|
-
return (_jsxs(XStack, { render: "button", "aria-label": on ? `Selected ${repo.name}` : `Select ${repo.name}`, "aria-pressed": on, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: onPress, children: [_jsx(YStack, { width: 14, height: 14, rounded: "$1", borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', bg: on ? '$ink' : 'transparent' }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: repo.name }), repo.imported ? (_jsx(SizableText, { size: "$1", color: "$soft", children:
|
|
134
|
+
return (_jsxs(XStack, { render: "button", "aria-label": on ? `Selected ${repo.name}` : `Select ${repo.name}`, "aria-pressed": on, items: "center", gap: "$2", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", onPress: onPress, children: [_jsx(YStack, { width: 14, height: 14, rounded: "$1", borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', bg: on ? '$ink' : 'transparent' }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: repo.name }), repo.imported || repo.status === 'pending' ? (_jsx(SizableText, { size: "$1", color: "$soft", children: repo.imported ? (repo.status === 'conflict' ? 'On Forge · diverged' : 'On Forge') : 'Syncing…' })) : null] }));
|
|
121
135
|
}
|
package/lib/tabs.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** One tab. A kind's id is the kind; a page's is minted. */
|
|
2
|
+
export interface Tab {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** A page's address. */
|
|
7
|
+
url?: string;
|
|
8
|
+
/** A page's own bytes, rendered as `type`. */
|
|
9
|
+
body?: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The tabs in a scope, in order, and the chosen one. */
|
|
13
|
+
export interface Tabs {
|
|
14
|
+
tabs: Tab[];
|
|
15
|
+
at: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare const PAGE = "page";
|
|
18
|
+
/** A scope as kept, or null when nothing is: the surface's seed stands in. A bad entry is dropped. */
|
|
19
|
+
export declare function read(scope: string): Tabs | null;
|
|
20
|
+
/** A tab opened: a kind already open is chosen, a page already open on the same thing is chosen. */
|
|
21
|
+
export declare function opened(was: Tabs, tab: Omit<Tab, 'id'> & {
|
|
22
|
+
id?: string;
|
|
23
|
+
}): Tabs;
|
|
24
|
+
/** A tab shut: the one to its right is chosen, else the one to its left, as a browser does. */
|
|
25
|
+
export declare function shut(was: Tabs, id: string): Tabs;
|
|
26
|
+
/** A tab moved to `to`, the others closing up around it. */
|
|
27
|
+
export declare function moved(was: Tabs, id: string, to: number): Tabs;
|
|
28
|
+
export interface Deck extends Tabs {
|
|
29
|
+
open: (tab: Omit<Tab, 'id'> & {
|
|
30
|
+
id?: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
pick: (id: string) => void;
|
|
33
|
+
shut: (id: string) => void;
|
|
34
|
+
move: (id: string, to: number) => void;
|
|
35
|
+
/** Point a page somewhere else. */
|
|
36
|
+
go: (id: string, url: string) => void;
|
|
37
|
+
}
|
|
38
|
+
/** A scope's tabs: as kept, else `seed`, and every change kept. */
|
|
39
|
+
export declare function useDeck(scope: string, seed: Tabs): Deck;
|
|
40
|
+
/** Whether the panel is open in `mode`, kept; `first` before anyone has said. */
|
|
41
|
+
export declare function useShown(mode: string, first: boolean): readonly [boolean, (open: boolean) => void];
|
|
42
|
+
/**
|
|
43
|
+
* Whether the panel is open: kept per mode where it is a column (`wide`), and
|
|
44
|
+
* for this page only where it is a sheet over everything — a phone opens on
|
|
45
|
+
* the conversation, never on a sheet it did not ask for.
|
|
46
|
+
*/
|
|
47
|
+
export declare function usePanel(mode: string, first: boolean, wide: boolean): readonly [boolean, (open: boolean) => void];
|
|
48
|
+
/** ⌘. on a Mac, Ctrl+. elsewhere: the panel, from anywhere on the page. */
|
|
49
|
+
export declare function useKey(toggle: () => void): void;
|
|
50
|
+
/** The host and path of an address: as much as a title can honestly be. */
|
|
51
|
+
export declare function name(url: string): string;
|
|
52
|
+
/** An address as typed, made one that opens: https for a host, http for this machine. */
|
|
53
|
+
export declare function address(typed: string): string;
|
package/lib/tabs.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The side panel's tabs, once, for Chat and Dev: what is open, in what order,
|
|
3
|
+
* which one is chosen, and whether the panel is open at all — kept as it was
|
|
4
|
+
* left. The panel that draws them is `Panel` (panel.tsx).
|
|
5
|
+
*
|
|
6
|
+
* hanzo.side.<mode> whether the panel is open in Chat or in Dev
|
|
7
|
+
* hanzo.side.<scope> its tabs, their order, and the chosen one
|
|
8
|
+
*
|
|
9
|
+
* A tab is a KIND the surface offers (Artifacts, Preview, Files, Diff,
|
|
10
|
+
* Terminal, Sources, …), one of each, or a PAGE: an address, or the bytes of
|
|
11
|
+
* something an answer wrote, rendered. `+` opens a kind that is not open or a
|
|
12
|
+
* new page; a tab is dragged to reorder (Ctrl/⌘+Shift+←/→ from the keyboard)
|
|
13
|
+
* and shut with its cross (Delete). A page's own bytes are kept with it, so an
|
|
14
|
+
* artifact's tab survives a reload; an object URL does not and is not kept.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
18
|
+
export const PAGE = 'page';
|
|
19
|
+
/** Bytes kept with a page tab, at most: a page an answer wrote, not a dump. */
|
|
20
|
+
const KEEP = 200_000;
|
|
21
|
+
const where = (scope) => `hanzo.side.${scope}`;
|
|
22
|
+
function store() {
|
|
23
|
+
try {
|
|
24
|
+
return typeof window === 'undefined' ? null : window.localStorage;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// The scopes this document has read, so a switch back costs no parse.
|
|
31
|
+
const live = new Map();
|
|
32
|
+
const listeners = new Set();
|
|
33
|
+
const announce = () => {
|
|
34
|
+
for (const l of listeners)
|
|
35
|
+
l();
|
|
36
|
+
};
|
|
37
|
+
const subscribe = (l) => {
|
|
38
|
+
listeners.add(l);
|
|
39
|
+
return () => {
|
|
40
|
+
listeners.delete(l);
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const isTab = (t) => !!t && typeof t.id === 'string' && typeof t.kind === 'string' && typeof t.title === 'string';
|
|
44
|
+
/** A scope as kept, or null when nothing is: the surface's seed stands in. A bad entry is dropped. */
|
|
45
|
+
export function read(scope) {
|
|
46
|
+
if (live.has(scope))
|
|
47
|
+
return live.get(scope);
|
|
48
|
+
let held = null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = store()?.getItem(where(scope));
|
|
51
|
+
if (raw) {
|
|
52
|
+
const v = JSON.parse(raw);
|
|
53
|
+
// An object URL dies with the document that minted it: a page holding only one is not kept.
|
|
54
|
+
const tabs = (Array.isArray(v.tabs) ? v.tabs.filter(isTab) : []).filter((t) => t.kind !== PAGE || t.body !== undefined || !t.url?.startsWith('blob:'));
|
|
55
|
+
held = { tabs, at: tabs.some((t) => t.id === v.at) ? v.at : (tabs[0]?.id ?? null) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
held = null;
|
|
60
|
+
}
|
|
61
|
+
live.set(scope, held);
|
|
62
|
+
return held;
|
|
63
|
+
}
|
|
64
|
+
function write(scope, next) {
|
|
65
|
+
live.set(scope, next);
|
|
66
|
+
try {
|
|
67
|
+
const kept = { ...next, tabs: next.tabs.map((t) => (t.body !== undefined && t.body.length > KEEP ? { ...t, body: undefined } : t)) };
|
|
68
|
+
store()?.setItem(where(scope), JSON.stringify(kept));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A browser that refuses storage keeps the tabs for this page.
|
|
72
|
+
}
|
|
73
|
+
announce();
|
|
74
|
+
}
|
|
75
|
+
let minted = 0;
|
|
76
|
+
const mint = () => `p${Date.now().toString(36)}${(minted++).toString(36)}`;
|
|
77
|
+
/** A tab opened: a kind already open is chosen, a page already open on the same thing is chosen. */
|
|
78
|
+
export function opened(was, tab) {
|
|
79
|
+
const same = was.tabs.find((t) => tab.kind === PAGE ? t.kind === PAGE && ((tab.url && t.url === tab.url) || (tab.body !== undefined && t.body === tab.body && t.title === tab.title)) : t.kind === tab.kind);
|
|
80
|
+
if (same)
|
|
81
|
+
return { ...was, at: same.id };
|
|
82
|
+
const id = tab.id ?? (tab.kind === PAGE ? mint() : tab.kind);
|
|
83
|
+
return { tabs: [...was.tabs, { ...tab, id }], at: id };
|
|
84
|
+
}
|
|
85
|
+
/** A tab shut: the one to its right is chosen, else the one to its left, as a browser does. */
|
|
86
|
+
export function shut(was, id) {
|
|
87
|
+
const gone = was.tabs.findIndex((t) => t.id === id);
|
|
88
|
+
if (gone < 0)
|
|
89
|
+
return was;
|
|
90
|
+
const tabs = was.tabs.filter((t) => t.id !== id);
|
|
91
|
+
if (was.at !== id)
|
|
92
|
+
return { ...was, tabs };
|
|
93
|
+
return { tabs, at: (tabs[gone] ?? tabs[gone - 1])?.id ?? null };
|
|
94
|
+
}
|
|
95
|
+
/** A tab moved to `to`, the others closing up around it. */
|
|
96
|
+
export function moved(was, id, to) {
|
|
97
|
+
const from = was.tabs.findIndex((t) => t.id === id);
|
|
98
|
+
if (from < 0)
|
|
99
|
+
return was;
|
|
100
|
+
const tabs = was.tabs.slice();
|
|
101
|
+
const [tab] = tabs.splice(from, 1);
|
|
102
|
+
tabs.splice(Math.max(0, Math.min(tabs.length, to)), 0, tab);
|
|
103
|
+
return { ...was, tabs };
|
|
104
|
+
}
|
|
105
|
+
/** A scope's tabs: as kept, else `seed`, and every change kept. */
|
|
106
|
+
export function useDeck(scope, seed) {
|
|
107
|
+
const kept = useSyncExternalStore(subscribe, () => read(scope), () => null);
|
|
108
|
+
const now = kept ?? seed;
|
|
109
|
+
const change = (f) => write(scope, f(read(scope) ?? seed));
|
|
110
|
+
return {
|
|
111
|
+
...now,
|
|
112
|
+
open: (tab) => change((was) => opened(was, tab)),
|
|
113
|
+
pick: (id) => change((was) => ({ ...was, at: id })),
|
|
114
|
+
shut: (id) => change((was) => shut(was, id)),
|
|
115
|
+
move: (id, to) => change((was) => moved(was, id, to)),
|
|
116
|
+
go: (id, url) => change((was) => ({ ...was, tabs: was.tabs.map((t) => (t.id === id ? { ...t, url, title: name(url) } : t)) })),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const shownKey = (mode) => where(`${mode}.open`);
|
|
120
|
+
const shownLive = new Map();
|
|
121
|
+
function shownOf(mode, first) {
|
|
122
|
+
if (shownLive.has(mode))
|
|
123
|
+
return shownLive.get(mode);
|
|
124
|
+
let v = first;
|
|
125
|
+
try {
|
|
126
|
+
const raw = store()?.getItem(shownKey(mode));
|
|
127
|
+
if (raw === '1' || raw === '0')
|
|
128
|
+
v = raw === '1';
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
v = first;
|
|
132
|
+
}
|
|
133
|
+
shownLive.set(mode, v);
|
|
134
|
+
return v;
|
|
135
|
+
}
|
|
136
|
+
/** Whether the panel is open in `mode`, kept; `first` before anyone has said. */
|
|
137
|
+
export function useShown(mode, first) {
|
|
138
|
+
const open = useSyncExternalStore(subscribe, () => shownOf(mode, first), () => first);
|
|
139
|
+
const set = (next) => {
|
|
140
|
+
shownLive.set(mode, next);
|
|
141
|
+
try {
|
|
142
|
+
store()?.setItem(shownKey(mode), next ? '1' : '0');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Kept for this page.
|
|
146
|
+
}
|
|
147
|
+
announce();
|
|
148
|
+
};
|
|
149
|
+
return [open, set];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether the panel is open: kept per mode where it is a column (`wide`), and
|
|
153
|
+
* for this page only where it is a sheet over everything — a phone opens on
|
|
154
|
+
* the conversation, never on a sheet it did not ask for.
|
|
155
|
+
*/
|
|
156
|
+
export function usePanel(mode, first, wide) {
|
|
157
|
+
const [kept, keep] = useShown(mode, first);
|
|
158
|
+
const [sheet, setSheet] = useState(false);
|
|
159
|
+
return wide ? [kept, keep] : [sheet, setSheet];
|
|
160
|
+
}
|
|
161
|
+
/** ⌘. on a Mac, Ctrl+. elsewhere: the panel, from anywhere on the page. */
|
|
162
|
+
export function useKey(toggle) {
|
|
163
|
+
const now = useRef(toggle);
|
|
164
|
+
now.current = toggle;
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
const press = (e) => {
|
|
167
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key === '.') {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
now.current();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
window.addEventListener('keydown', press);
|
|
173
|
+
return () => window.removeEventListener('keydown', press);
|
|
174
|
+
}, []);
|
|
175
|
+
}
|
|
176
|
+
/** The host and path of an address: as much as a title can honestly be. */
|
|
177
|
+
export function name(url) {
|
|
178
|
+
try {
|
|
179
|
+
const u = new URL(url);
|
|
180
|
+
return u.host + (u.pathname === '/' ? '' : u.pathname);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return url;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** An address as typed, made one that opens: https for a host, http for this machine. */
|
|
187
|
+
export function address(typed) {
|
|
188
|
+
const t = typed.trim();
|
|
189
|
+
if (!t)
|
|
190
|
+
return '';
|
|
191
|
+
// This machine first: `localhost:3000` reads as a scheme to the rule below.
|
|
192
|
+
if (/^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(t))
|
|
193
|
+
return `http://${t}`;
|
|
194
|
+
if (/^[a-z][a-z0-9+.-]*:(?!\d)/i.test(t))
|
|
195
|
+
return t;
|
|
196
|
+
return `https://${t}`;
|
|
197
|
+
}
|
package/lib/tools.d.ts
CHANGED
|
@@ -9,17 +9,10 @@ export declare function read(have: Attached[], list: FileList): Promise<{
|
|
|
9
9
|
files: Attached[];
|
|
10
10
|
note: string;
|
|
11
11
|
}>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
onFiles: (f: Attached[]) => void;
|
|
15
|
-
onNote: (n: string) => void;
|
|
16
|
-
}): import("react").JSX.Element;
|
|
12
|
+
/** Ask the browser for files and read them in, within the caps; the picker is minted per press. */
|
|
13
|
+
export declare function pick(have: Attached[], onFiles: (f: Attached[]) => void, onNote: (n: string) => void): void;
|
|
17
14
|
/** The attached files as small removable marks. */
|
|
18
15
|
export declare function Files({ files, onFiles }: {
|
|
19
16
|
files: Attached[];
|
|
20
17
|
onFiles: (f: Attached[]) => void;
|
|
21
18
|
}): import("react").JSX.Element;
|
|
22
|
-
export declare function Dictate({ onText, onNote }: {
|
|
23
|
-
onText: (text: string) => void;
|
|
24
|
-
onNote: (note: string) => void;
|
|
25
|
-
}): import("react").JSX.Element;
|
package/lib/tools.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Files with an ask, for every composer the builder draws: they ride the
|
|
4
|
+
* prompt as text — nothing is uploaded anywhere else. The paperclip is the
|
|
5
|
+
* composer's (prompt.tsx); this reads what it picks and draws the chips.
|
|
6
6
|
*/
|
|
7
7
|
import { SizableText, XStack } from '@hanzo/gui';
|
|
8
|
-
import {
|
|
8
|
+
import { Paperclip, X } from '@hanzogui/lucide-icons-2';
|
|
9
9
|
import { ComposerTool } from '@hanzo/ui/chat';
|
|
10
|
-
import { ChipSelect } from '@hanzo/ui/product';
|
|
11
|
-
import { useRef } from 'react';
|
|
12
|
-
import { useTarget } from './host.js';
|
|
13
|
-
import { useDictation } from './voice.js';
|
|
14
10
|
const EACH = 100_000;
|
|
15
11
|
const ALL = 200_000;
|
|
16
12
|
/** The ask with its attachments appended as fenced text. */
|
|
@@ -39,25 +35,24 @@ export async function read(have, list) {
|
|
|
39
35
|
}
|
|
40
36
|
return { files, note };
|
|
41
37
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
/** Ask the browser for files and read them in, within the caps; the picker is minted per press. */
|
|
39
|
+
export function pick(have, onFiles, onNote) {
|
|
40
|
+
if (typeof document === 'undefined')
|
|
41
|
+
return;
|
|
42
|
+
const ask = document.createElement('input');
|
|
43
|
+
ask.type = 'file';
|
|
44
|
+
ask.multiple = true;
|
|
45
|
+
ask.onchange = () => {
|
|
46
|
+
if (!ask.files?.length)
|
|
47
|
+
return;
|
|
48
|
+
void read(have, ask.files).then(({ files, note }) => {
|
|
49
|
+
onFiles(files);
|
|
50
|
+
onNote(note);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
ask.click();
|
|
55
54
|
}
|
|
56
55
|
/** The attached files as small removable marks. */
|
|
57
56
|
export function Files({ files, onFiles }) {
|
|
58
57
|
return (_jsx(_Fragment, { children: files.map((f) => (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", height: 24, rounded: "$2", bg: "$raised", children: [_jsx(Paperclip, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, maxW: 140, children: f.name }), _jsx(ComposerTool, { label: `Remove ${f.name}`, icon: _jsx(X, { size: 12 }), onPress: () => onFiles(files.filter((x) => x !== f)) })] }, f.name))) }));
|
|
59
58
|
}
|
|
60
|
-
export function Dictate({ onText, onNote }) {
|
|
61
|
-
const voice = useDictation(useTarget(), onText, onNote);
|
|
62
|
-
return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language), items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
|
|
63
|
-
}
|
package/lib/transcript.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Verdict } from '@hanzo/ui/agents';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
3
|
import type { Card } from './api/turn.ts';
|
|
4
|
-
|
|
4
|
+
import { type Artifact, type Listen } from './reply.tsx';
|
|
5
|
+
export declare function Transcript({ cards, live, empty, header, onApprove, approving, onVerdict, onOpen, onDiff, listen, end, }: {
|
|
5
6
|
cards: Card[];
|
|
6
7
|
/** The run is still working: a card still running shows it; otherwise it was cut off. */
|
|
7
8
|
live: boolean;
|
|
@@ -12,4 +13,12 @@ export declare function Transcript({ cards, live, empty, header, onApprove, appr
|
|
|
12
13
|
approving?: boolean;
|
|
13
14
|
/** Record a verdict on what the agent said; throws when it did not land. */
|
|
14
15
|
onVerdict: (v: Verdict) => Promise<void>;
|
|
16
|
+
/** Open what an answer wrote in the side panel. */
|
|
17
|
+
onOpen?: (a: Artifact) => void;
|
|
18
|
+
/** Open the run's diff in the side panel. */
|
|
19
|
+
onDiff?: () => void;
|
|
20
|
+
/** Read an answer aloud. */
|
|
21
|
+
listen?: Listen;
|
|
22
|
+
/** After the last card: how the run ended, when it ended on an error. */
|
|
23
|
+
end?: ReactNode;
|
|
15
24
|
}): import("react").JSX.Element;
|
package/lib/transcript.js
CHANGED
|
@@ -9,26 +9,41 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
*
|
|
10
10
|
* It follows the run to the bottom as it streams and stops when the reader
|
|
11
11
|
* scrolls up (Thread). Everything drawn is text.
|
|
12
|
+
*
|
|
13
|
+
* It reads as Chat reads: the same measure (`MEASURE`), the same turns and
|
|
14
|
+
* prose sizes, the same row under an answer (`Reply`: copy, listen, and open
|
|
15
|
+
* what it wrote in the side panel), the same caret while an answer streams and
|
|
16
|
+
* the same `Failure` when a run stops on an error. What only a run has — its
|
|
17
|
+
* commands, edits and steps — are cards of their own between the turns.
|
|
12
18
|
*/
|
|
13
19
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
20
|
import { Button } from '@hanzo/ui';
|
|
15
21
|
import { Feedback } from '@hanzo/ui/agents';
|
|
16
22
|
import { Code, Message, Step, Thread } from '@hanzo/ui/chat';
|
|
17
23
|
import { useState } from 'react';
|
|
24
|
+
import { GAP, MEASURE } from './prompt.js';
|
|
18
25
|
import { Prose } from './prose.js';
|
|
26
|
+
import { artifacts, lead, Reply } from './reply.js';
|
|
19
27
|
/** A card's output, its tail: what a command said last is why it stopped. */
|
|
20
28
|
const TAIL = 6000;
|
|
21
29
|
const tail = (s) => (s.length > TAIL ? `…${s.slice(-TAIL)}` : s);
|
|
22
30
|
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
23
31
|
/** What a thought is about: its first line, without the markdown that bolds it. */
|
|
24
32
|
const firstLine = (text) => text.trim().split('\n')[0].replace(/[*_#`]/g, '').slice(0, 120);
|
|
25
|
-
export function Transcript({ cards, live, empty, header, onApprove, approving = false, onVerdict, }) {
|
|
33
|
+
export function Transcript({ cards, live, empty, header, onApprove, approving = false, onVerdict, onOpen, onDiff, listen, end, }) {
|
|
26
34
|
// A card the run never finished is cut off once the run has ended, not still going.
|
|
27
35
|
const state = (ran) => (ran === 'running' && !live ? 'cancelled' : ran);
|
|
28
|
-
|
|
36
|
+
// The answer still arriving: the last thing the agent said while the run works.
|
|
37
|
+
const last = live ? [...cards].reverse().find((c) => c.kind === 'said' && c.who === 'agent')?.key : undefined;
|
|
38
|
+
return (_jsxs(Thread, { gap: GAP, column: { maxW: MEASURE }, "aria-label": "Transcript", children: [header, cards.length === 0 ? _jsx(YStack, { py: "$2", children: empty }) : null, cards.map((c) => {
|
|
29
39
|
switch (c.kind) {
|
|
30
|
-
case 'said':
|
|
31
|
-
|
|
40
|
+
case 'said': {
|
|
41
|
+
if (c.who === 'person')
|
|
42
|
+
return (_jsx(Message, { role: "user", children: _jsx(Prose, { text: c.text }) }, c.key));
|
|
43
|
+
const made = artifacts(c.text);
|
|
44
|
+
const first = lead(made);
|
|
45
|
+
return (_jsx(Message, { role: "assistant", busy: c.key === last, children: _jsxs(YStack, { gap: "$1", children: [_jsx(Prose, { text: c.text }), c.key === last ? null : (_jsx(Reply, { text: c.text, listen: listen, onOpen: first && onOpen ? () => onOpen(first) : undefined, children: _jsx(Judge, { onVerdict: onVerdict }) }))] }) }, c.key));
|
|
46
|
+
}
|
|
32
47
|
case 'think':
|
|
33
48
|
return (_jsx(Step, { name: "Thought", detail: firstLine(c.text), status: "done", "aria-label": "Thought", children: _jsx(Prose, { text: c.text }) }, c.key));
|
|
34
49
|
case 'todo':
|
|
@@ -36,18 +51,18 @@ export function Transcript({ cards, live, empty, header, onApprove, approving =
|
|
|
36
51
|
case 'shell':
|
|
37
52
|
return (_jsx(Step, { name: "Shell", detail: c.command, status: state(c.ran), "aria-label": `Command ${c.command}`, children: _jsx(Code, { language: "shell", value: c.output ? `$ ${c.command}\n${c.output}` : `$ ${c.command}`, children: c.output ? `$ ${c.command}\n${tail(c.output)}` : `$ ${c.command}` }) }, c.key));
|
|
38
53
|
case 'edit':
|
|
39
|
-
return (_jsx(Step, { name: c.files.length === 1 ? 'Edited' : c.files.length ? `Edited ${c.files.length} files` : 'Edited files', detail: c.files.join(', '), status: state(c.ran), "aria-label": `Edited ${c.files.join(', ') || 'files'}`, children: _jsx(SizableText, { size: "$1", color: "$soft", children: c.files.length ?
|
|
54
|
+
return (_jsx(Step, { name: c.files.length === 1 ? 'Edited' : c.files.length ? `Edited ${c.files.length} files` : 'Edited files', detail: c.files.join(', '), status: state(c.ran), "aria-label": `Edited ${c.files.join(', ') || 'files'}`, children: _jsxs(YStack, { gap: "$2", items: "flex-start", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { whiteSpace: 'pre-wrap' }, children: c.files.length ? c.files.join('\n') : 'The agent named no files.' }), onDiff ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onDiff, children: "Open the diff" })) : null] }) }, c.key));
|
|
40
55
|
case 'step':
|
|
41
56
|
return (_jsx(Step, { name: c.name, detail: c.detail, status: state(c.ran), defaultOpen: c.name === 'Error', "aria-label": `${c.name} ${c.detail}`.trim(), children: c.name === 'Error' ? (_jsx(SizableText, { size: "$2", color: "$ink", style: { ...mono, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: c.output })) : c.output.trim() ? (_jsx(Code, { language: "output", value: c.output, children: tail(c.output.replace(/\n+$/, '')) })) : undefined }, c.key));
|
|
42
57
|
case 'note':
|
|
43
58
|
return (_jsx(SizableText, { size: "$1", color: "$soft", role: "note", children: c.text }, c.key));
|
|
44
59
|
case 'plan':
|
|
45
|
-
return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [onApprove ? (_jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' })) : null, _jsx(Judge, {
|
|
60
|
+
return (_jsxs(YStack, { gap: "$3", p: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", "aria-label": "Plan", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Plan" }), _jsx(Prose, { text: c.text }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Reply, { text: c.text, listen: listen }), onApprove ? (_jsx(Button, { size: "sm", disabled: approving, onPress: () => onApprove(c.text), children: approving ? 'Starting the build…' : 'Approve and build' })) : null, _jsx(Judge, { onVerdict: onVerdict })] })] }, c.key));
|
|
46
61
|
}
|
|
47
|
-
})] }));
|
|
62
|
+
}), end] }));
|
|
48
63
|
}
|
|
49
|
-
/**
|
|
50
|
-
function Judge({
|
|
64
|
+
/** Say whether what the agent said was good. A verdict that did not land is taken back and says why. */
|
|
65
|
+
function Judge({ onVerdict }) {
|
|
51
66
|
const [verdict, setVerdict] = useState(null);
|
|
52
67
|
const [note, setNote] = useState('');
|
|
53
68
|
const judge = (next) => {
|
|
@@ -58,5 +73,5 @@ function Judge({ text, onVerdict }) {
|
|
|
58
73
|
setNote(e.message);
|
|
59
74
|
});
|
|
60
75
|
};
|
|
61
|
-
return (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Feedback, { text:
|
|
76
|
+
return (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Feedback, { text: "", verdict: verdict, onVerdict: judge }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null] }));
|
|
62
77
|
}
|
package/lib/voice.d.ts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
import type { Target } from './api/call.ts';
|
|
2
1
|
export declare const LANGUAGES: {
|
|
3
2
|
id: string;
|
|
4
3
|
label: string;
|
|
5
4
|
}[];
|
|
6
5
|
/** The language this browser is set to, when it is one dictation offers; English otherwise. */
|
|
7
6
|
export declare function spoken(): string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
busy: boolean;
|
|
11
|
-
able: boolean;
|
|
12
|
-
toggle: () => Promise<void>;
|
|
13
|
-
language: string;
|
|
14
|
-
setLanguage: (id: string) => Promise<void>;
|
|
15
|
-
languages: {
|
|
16
|
-
id: string;
|
|
17
|
-
label: string;
|
|
18
|
-
}[];
|
|
19
|
-
};
|
|
7
|
+
/** The person's saved language (Settings → General), else this browser's, else English. */
|
|
8
|
+
export declare function useSpoken(): string;
|