@hanzo/build 0.2.48 → 0.2.49

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.
@@ -90,6 +90,15 @@ export declare function subscribe(t: Target, p: Purchase): Promise<{
90
90
  /** End a plan when its paid period does, not before. */
91
91
  export declare function cancel(t: Target, id: string): Promise<Subscription>;
92
92
  export declare function reactivate(t: Target, id: string): Promise<Subscription>;
93
+ export interface Address {
94
+ name?: string;
95
+ line1?: string;
96
+ line2?: string;
97
+ city?: string;
98
+ state?: string;
99
+ postalCode?: string;
100
+ country?: string;
101
+ }
93
102
  export interface Method {
94
103
  id: string;
95
104
  type: string;
@@ -100,16 +109,23 @@ export interface Method {
100
109
  default: boolean;
101
110
  /** What the platform calls it when it is not a card. */
102
111
  name: string;
112
+ billingAddress?: Address;
103
113
  }
104
114
  export declare function method(raw: unknown): Method;
115
+ /** Formats a billing address as a readable line: `Acme Inc · 123 Main St · San Francisco CA 94105 · US`. */
116
+ export declare function formatAddress(a?: Address): string;
105
117
  /** How a saved method reads in a row: `Visa •••• 4242`. */
106
118
  export declare function label(m: Method): string;
107
119
  export declare function methods(t: Target): Promise<Method[]>;
108
120
  /** The card a charge uses: the default, or the newest. */
109
121
  export declare function chosen(list: Method[]): Method | null;
110
- /** Save a card from the single-use token the processor's form returned. */
111
- export declare function save(t: Target, token: string): Promise<Method>;
122
+ /** Save a card from the single-use token the processor's form returned, optionally with billing info. */
123
+ export declare function save(t: Target, token: string, billingAddress?: Address): Promise<Method>;
124
+ export declare function updateMethod(t: Target, id: string, what: {
125
+ billingAddress?: Address;
126
+ }): Promise<Method>;
112
127
  export declare function detach(t: Target, id: string): Promise<void>;
128
+ export declare function makeDefault(t: Target, id: string): Promise<Method>;
113
129
  export interface Processor {
114
130
  /** `square`, or '' where none is configured. */
115
131
  provider: string;
@@ -125,6 +125,7 @@ export async function reactivate(t, id) {
125
125
  export function method(raw) {
126
126
  const m = obj(raw);
127
127
  const c = obj(m.card);
128
+ const a = obj(m.billingAddress);
128
129
  const month = num(c.expMonth);
129
130
  const year = num(c.expYear);
130
131
  return {
@@ -135,8 +136,31 @@ export function method(raw) {
135
136
  expires: month && year ? `${String(month).padStart(2, '0')}/${String(year % 100).padStart(2, '0')}` : '',
136
137
  default: m.isDefault === true,
137
138
  name: str(m.name),
139
+ billingAddress: a && (a.line1 || a.city || a.country || a.name || a.postalCode || a.state)
140
+ ? {
141
+ name: str(a.name),
142
+ line1: str(a.line1),
143
+ line2: str(a.line2),
144
+ city: str(a.city),
145
+ state: str(a.state),
146
+ postalCode: str(a.postalCode),
147
+ country: str(a.country),
148
+ }
149
+ : undefined,
138
150
  };
139
151
  }
152
+ /** Formats a billing address as a readable line: `Acme Inc · 123 Main St · San Francisco CA 94105 · US`. */
153
+ export function formatAddress(a) {
154
+ if (!a)
155
+ return '';
156
+ const parts = [
157
+ a.name,
158
+ [a.line1, a.line2].filter(Boolean).join(', '),
159
+ [a.city, a.state, a.postalCode].filter(Boolean).join(' '),
160
+ a.country,
161
+ ].filter(Boolean);
162
+ return parts.join(' · ');
163
+ }
140
164
  /** How a saved method reads in a row: `Visa •••• 4242`. */
141
165
  export function label(m) {
142
166
  if (m.last4)
@@ -152,15 +176,25 @@ export async function methods(t) {
152
176
  export function chosen(list) {
153
177
  return list.find((m) => m.default) ?? list[0] ?? null;
154
178
  }
155
- /** Save a card from the single-use token the processor's form returned. */
156
- export async function save(t, token) {
179
+ /** Save a card from the single-use token the processor's form returned, optionally with billing info. */
180
+ export async function save(t, token, billingAddress) {
157
181
  if (!token)
158
182
  throw new Error('The card form returned no token');
159
- return method(await call(t, 'POST', '/v1/billing/methods', { type: 'card', providerRef: token }));
183
+ const body = { type: 'card', providerRef: token };
184
+ if (billingAddress && Object.values(billingAddress).some(Boolean)) {
185
+ body.billingAddress = billingAddress;
186
+ }
187
+ return method(await call(t, 'POST', '/v1/billing/methods', body));
188
+ }
189
+ export async function updateMethod(t, id, what) {
190
+ return method(await call(t, 'PATCH', `/v1/billing/methods/${seg(id)}`, what));
160
191
  }
161
192
  export async function detach(t, id) {
162
193
  await call(t, 'DELETE', `/v1/billing/methods/${seg(id)}`);
163
194
  }
195
+ export async function makeDefault(t, id) {
196
+ return method(await call(t, 'POST', `/v1/billing/methods/${seg(id)}/default`));
197
+ }
164
198
  export async function processor(t) {
165
199
  const p = obj(await call(t, 'GET', '/v1/billing/settings'));
166
200
  return {
@@ -16,6 +16,46 @@
16
16
  import { type Target } from './call.ts';
17
17
  export declare const KINDS: readonly ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
18
18
  export type Kind = (typeof KINDS)[number];
19
+ export interface GPU {
20
+ vendor?: string;
21
+ model?: string;
22
+ memory?: number;
23
+ }
24
+ export interface MachineSpec {
25
+ os?: string;
26
+ arch?: string;
27
+ cpus?: number;
28
+ memory?: number;
29
+ gpus?: GPU[];
30
+ }
31
+ export interface MachineMetrics {
32
+ load1?: number;
33
+ load5?: number;
34
+ load15?: number;
35
+ memUsed?: number;
36
+ memFree?: number;
37
+ gpuUtil?: number;
38
+ cpuUtil?: number;
39
+ cpuTemp?: number;
40
+ gpuTemp?: number;
41
+ gpuPower?: number;
42
+ gpuPowerLimit?: number;
43
+ gpuMemUsed?: number;
44
+ gpuMemTotal?: number;
45
+ diskUsed?: number;
46
+ diskTotal?: number;
47
+ diskRead?: number;
48
+ diskWrite?: number;
49
+ netRx?: number;
50
+ netTx?: number;
51
+ model?: string;
52
+ decode?: number;
53
+ prefill?: number;
54
+ ttft?: number;
55
+ running?: number;
56
+ waiting?: number;
57
+ at?: number;
58
+ }
19
59
  export interface Machine {
20
60
  /** `tgt_…` */
21
61
  id: string;
@@ -33,7 +73,16 @@ export interface Machine {
33
73
  running: number;
34
74
  /** When it last beat, RFC 3339, or '' when it never has. */
35
75
  seen: string;
76
+ serving?: boolean;
77
+ createdAt?: string;
78
+ updatedAt?: string;
79
+ spec?: MachineSpec;
80
+ metrics?: MachineMetrics;
36
81
  }
82
+ export declare function formatBytes(bytes?: number): string;
83
+ export declare function formatPercent(fraction?: number): string;
84
+ export declare function formatRelative(isoDate?: string): string;
85
+ export declare function specSummary(m: Machine): string;
37
86
  export declare function machine(raw: unknown): Machine;
38
87
  export declare function machines(t: Target): Promise<Machine[]>;
39
88
  /**
@@ -44,6 +93,7 @@ export declare function add(t: Target, what: {
44
93
  label: string;
45
94
  kind: Kind;
46
95
  host?: string;
96
+ capacity?: string;
47
97
  }): Promise<Machine>;
48
98
  /**
49
99
  * A machine's status as a person reads it. The platform reports a hand-registered
@@ -55,6 +105,9 @@ export declare function state(m: Machine): string;
55
105
  export declare function change(t: Target, id: string, what: {
56
106
  label?: string;
57
107
  status?: 'online' | 'draining';
108
+ capacity?: string;
109
+ kind?: Kind;
110
+ host?: string;
58
111
  }): Promise<Machine>;
59
112
  export declare function remove(t: Target, id: string): Promise<void>;
60
113
  /**
@@ -17,9 +17,101 @@ import { call, Refusal, seg } from './call.js';
17
17
  export const KINDS = ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
18
18
  const str = (v) => (typeof v === 'string' ? v : '');
19
19
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
20
+ const obj = (v) => (v && typeof v === 'object' ? v : {});
21
+ export function formatBytes(bytes) {
22
+ if (!bytes || bytes <= 0)
23
+ return '';
24
+ const k = 1024;
25
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
26
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
27
+ const val = bytes / Math.pow(k, i);
28
+ return `${val >= 10 || i === 0 ? Math.round(val) : val.toFixed(1)} ${sizes[i]}`;
29
+ }
30
+ export function formatPercent(fraction) {
31
+ if (fraction === undefined || !Number.isFinite(fraction))
32
+ return '';
33
+ return `${Math.round(fraction * 100)}%`;
34
+ }
35
+ export function formatRelative(isoDate) {
36
+ if (!isoDate)
37
+ return '';
38
+ const t = new Date(isoDate).getTime();
39
+ if (Number.isNaN(t))
40
+ return '';
41
+ const diffSec = Math.max(0, Math.floor((Date.now() - t) / 1000));
42
+ if (diffSec < 60)
43
+ return `${diffSec}s ago`;
44
+ if (diffSec < 3600)
45
+ return `${Math.floor(diffSec / 60)}m ago`;
46
+ if (diffSec < 86400)
47
+ return `${Math.floor(diffSec / 3600)}h ago`;
48
+ return `${Math.floor(diffSec / 86400)}d ago`;
49
+ }
50
+ export function specSummary(m) {
51
+ if (m.spec) {
52
+ const parts = [];
53
+ if (m.spec.cpus)
54
+ parts.push(`${m.spec.cpus} vCPU`);
55
+ if (m.spec.memory)
56
+ parts.push(formatBytes(m.spec.memory));
57
+ if (m.spec.gpus && m.spec.gpus.length > 0) {
58
+ const g = m.spec.gpus[0];
59
+ parts.push(`${m.spec.gpus.length}× ${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`);
60
+ }
61
+ if (parts.length > 0)
62
+ return parts.join(' · ');
63
+ }
64
+ return m.capacity || '';
65
+ }
20
66
  export function machine(raw) {
21
67
  const o = (raw && typeof raw === 'object' ? raw : {});
22
- return {
68
+ const specObj = obj(o.spec);
69
+ const metricsObj = obj(o.metrics);
70
+ let spec;
71
+ if (Object.keys(specObj).length > 0) {
72
+ const rawGpus = Array.isArray(specObj.gpus) ? specObj.gpus : [];
73
+ spec = {
74
+ os: str(specObj.os),
75
+ arch: str(specObj.arch),
76
+ cpus: num(specObj.cpus),
77
+ memory: num(specObj.memory),
78
+ gpus: rawGpus.map((g) => {
79
+ const go = obj(g);
80
+ return { vendor: str(go.vendor), model: str(go.model), memory: num(go.memory) };
81
+ }),
82
+ };
83
+ }
84
+ let metrics;
85
+ if (Object.keys(metricsObj).length > 0) {
86
+ metrics = {
87
+ load1: num(metricsObj.load1),
88
+ load5: num(metricsObj.load5),
89
+ load15: num(metricsObj.load15),
90
+ memUsed: num(metricsObj.memUsed),
91
+ memFree: num(metricsObj.memFree),
92
+ gpuUtil: num(metricsObj.gpuUtil),
93
+ cpuUtil: num(metricsObj.cpuUtil),
94
+ cpuTemp: num(metricsObj.cpuTemp),
95
+ gpuTemp: num(metricsObj.gpuTemp),
96
+ gpuPower: num(metricsObj.gpuPower),
97
+ gpuPowerLimit: num(metricsObj.gpuPowerLimit),
98
+ gpuMemUsed: num(metricsObj.gpuMemUsed),
99
+ gpuMemTotal: num(metricsObj.gpuMemTotal),
100
+ diskUsed: num(metricsObj.diskUsed),
101
+ diskTotal: num(metricsObj.diskTotal),
102
+ diskRead: num(metricsObj.diskRead),
103
+ diskWrite: num(metricsObj.diskWrite),
104
+ netRx: num(metricsObj.netRx),
105
+ netTx: num(metricsObj.netTx),
106
+ model: str(metricsObj.model),
107
+ decode: num(metricsObj.decode),
108
+ prefill: num(metricsObj.prefill),
109
+ ttft: num(metricsObj.ttft),
110
+ running: num(metricsObj.running),
111
+ waiting: num(metricsObj.waiting),
112
+ };
113
+ }
114
+ const m = {
23
115
  id: str(o.id),
24
116
  label: str(o.label) || str(o.host) || str(o.id),
25
117
  kind: str(o.kind),
@@ -30,6 +122,14 @@ export function machine(raw) {
30
122
  running: num(o.running),
31
123
  seen: str(o.metricsAt),
32
124
  };
125
+ Object.defineProperties(m, {
126
+ serving: { value: o.serving === true, enumerable: false, writable: true },
127
+ createdAt: { value: str(o.createdAt), enumerable: false, writable: true },
128
+ updatedAt: { value: str(o.updatedAt), enumerable: false, writable: true },
129
+ spec: { value: spec, enumerable: false, writable: true },
130
+ metrics: { value: metrics, enumerable: false, writable: true },
131
+ });
132
+ return m;
33
133
  }
34
134
  export async function machines(t) {
35
135
  const raw = (await call(t, 'GET', '/v1/agent/targets')) ?? {};
@@ -55,7 +155,12 @@ export async function add(t, what) {
55
155
  const label = what.label.trim() || host;
56
156
  if (!label)
57
157
  throw new Error('A machine needs a name or a hostname');
58
- return machine(await call(t, 'POST', '/v1/agent/targets', { label, kind: what.kind, ...(host ? { host } : {}) }));
158
+ const body = { label, kind: what.kind };
159
+ if (host)
160
+ body.host = host;
161
+ if (what.capacity?.trim())
162
+ body.capacity = what.capacity.trim();
163
+ return machine(await call(t, 'POST', '/v1/agent/targets', body));
59
164
  }
60
165
  /**
61
166
  * A machine's status as a person reads it. The platform reports a hand-registered
@@ -76,6 +181,12 @@ export async function change(t, id, what) {
76
181
  }
77
182
  if (what.status !== undefined)
78
183
  body.status = what.status;
184
+ if (what.capacity !== undefined)
185
+ body.capacity = what.capacity.trim();
186
+ if (what.kind !== undefined)
187
+ body.kind = what.kind;
188
+ if (what.host !== undefined)
189
+ body.host = what.host.trim();
79
190
  return machine(await owned(call(t, 'PATCH', `/v1/agent/targets/${seg(id)}`, body)));
80
191
  }
81
192
  export async function remove(t, id) {
@@ -110,7 +110,7 @@ function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
110
110
  }
111
111
  };
112
112
  const pick = (name, yes) => set({ tools: yes ? [...d.tools, name] : d.tools.filter((n) => n !== name) });
113
- return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), placeholder: "helper", "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
113
+ return (_jsx(Sheet, { title: agent ? agent.name : 'New agent', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: agent && full.error && !full.value ? (_jsx(Soft, { children: full.error.message })) : !ready ? (_jsx(Soft, { children: "Reading the agent\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Name", hint: agent ? 'An agent keeps its name.' : 'Letters, digits, . _ or -. Other agents call it as agent_<name>.', children: _jsx(Input, { value: d.name, onChangeText: (v) => set({ name: v }), "aria-label": "Name", disabled: Boolean(agent), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The line another agent reads to decide whether to call this one.", children: _jsx(Input, { value: d.description, onChangeText: (v) => set({ description: v }), placeholder: "Answers questions about our codebase", "aria-label": "Description" }) }), _jsx(Field, { label: "Model", children: _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(ModelPicker, { size: "sm", name: "Model", placeholder: "The deployment\u2019s default", models: catalog.value, scope: "chat", value: d.model || undefined, onChange: (id) => set({ model: id }), loading: catalog.loading, error: catalog.error?.message ?? null }), d.model && !agent ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => set({ model: '' }), children: "Use the default" })) : null] }) }), _jsx(Field, { label: "Instructions", hint: "The system prompt: what the model reads before every run.", children: _jsx(Textarea, { value: d.instructions, onChangeText: (v) => set({ instructions: v }), placeholder: "Be terse, and cite the file you read.", "aria-label": "Instructions", rows: 8 }) }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Tools" }), _jsx(SizableText, { size: "$1", color: "$soft", children: every ? 'Every tool the fleet’s MCP server serves when it runs.' : d.tools.length ? `${d.tools.length} chosen. It can call only these.` : 'None chosen: it can call no tool.' })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Every tool" }), _jsx(Switch, { checked: every, onCheckedChange: (v) => set({ tools: v ? [ALL] : [] }), "aria-label": "Every tool" })] }), every ? null : (_jsxs(_Fragment, { children: [_jsx(Input, { value: find, onChangeText: setFind, placeholder: "Find a tool\u2026", "aria-label": "Find a tool" }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", maxH: 240, overflow: "scroll", children: on.loading && !on.value.length ? (_jsx(Soft, { children: "Reading the tools that are on\u2026" })) : on.error && !on.value.length ? (_jsx(Soft, { children: on.error.message })) : !shownTools.length ? (_jsx(Soft, { children: choices.length ? 'No tool matches.' : 'No tool is on for your organization. Switch skills and connectors on first.' })) : (shownTools.map((x, i) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", color: "$ink", style: mono, numberOfLines: 1, children: x.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [x.source, x.description].filter(Boolean).join(' · ') })] }), _jsx(Switch, { checked: d.tools.includes(x.name), onCheckedChange: (v) => pick(x.name, v), "aria-label": `Use ${x.name}` })] }, x.name)))) })] }))] }), _jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Budget" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", rowGap: "$2", items: "flex-end", children: [_jsxs(YStack, { gap: "$1", width: 140, children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Each ", d.period, ", USD"] }), _jsx(Input, { value: d.cap, onChangeText: (v) => set({ cap: v }), placeholder: "10", "aria-label": "Budget each period", inputMode: "decimal" })] }), _jsxs(YStack, { gap: "$1", width: 140, children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "One run, USD" }), _jsx(Input, { value: d.task, onChangeText: (v) => set({ task: v }), placeholder: "1", "aria-label": "Budget for one run", inputMode: "decimal" })] }), _jsx(Choice, { label: "Period", value: d.period, options: PERIODS.map((p) => ({ id: p, label: p[0].toUpperCase() + p.slice(1) })), onChange: (p) => set({ period: p }) })] }), _jsxs(SizableText, { size: "$1", color: "$soft", children: [full.value && full.value.cap ? `Spent ${money(full.value.spent)} this ${full.value.period}. ` : '', "A run stops at either limit."] })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [agent && onDeleted ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: agent ? 'Save' : 'Create' })] }), agent && onDeleted ? (_jsx(Confirm, { what: agent.name, says: "The agent and every run recorded against it are removed.", open: asking, onOpenChange: setAsking, onYes: async () => {
114
114
  await remove(t, agent.id || agent.name);
115
115
  onDeleted();
116
116
  } })) : null] })) }));
@@ -78,7 +78,7 @@ function Build({ onClose, onBuilt }) {
78
78
  setWorking(false);
79
79
  }
80
80
  };
81
- return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -. The runtime loads it by this name.", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "acme", "aria-label": "Name", autoCapitalize: "none" }) }), _jsx(Field, { label: "Connector", hint: "Optional. The connector whose credential the plugin reads when it runs. A plugin never holds a key itself.", children: _jsx(Input, { value: provider, onChangeText: setProvider, placeholder: "acme", "aria-label": "Connector", autoCapitalize: "none" }) }), _jsx(Choice, { label: "Build from", value: way, options: WAYS, onChange: setWay }), _jsx(Field, { label: way === 'source' ? 'Source' : 'The API', hint: way === 'source'
81
+ return (_jsxs(Sheet, { title: "Build a plugin", open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [_jsx(Field, { label: "Name", hint: "One lowercase word: letters, digits, _ or -. The runtime loads it by this name.", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name", autoCapitalize: "none" }) }), _jsx(Field, { label: "Connector", hint: "Optional. The connector whose credential the plugin reads when it runs. A plugin never holds a key itself.", children: _jsx(Input, { value: provider, onChangeText: setProvider, "aria-label": "Connector", autoCapitalize: "none" }) }), _jsx(Choice, { label: "Build from", value: way, options: WAYS, onChange: setWay }), _jsx(Field, { label: way === 'source' ? 'Source' : 'The API', hint: way === 'source'
82
82
  ? 'It is bundled and compiled in the runtime that will run it, and kept only if both succeed.'
83
83
  : 'An OpenAPI document, or prose naming the endpoints. A model writes the TypeScript; you read it before it runs.', children: _jsx(Textarea, { value: way === 'source' ? source : spec, onChangeText: way === 'source' ? setSource : setSpec, "aria-label": way === 'source' ? 'Source' : 'The API', placeholder: way === 'source' ? 'export const acme = createProvider({ … })' : 'POST /v1/things creates a thing…', rows: 14, style: mono }) }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: "Build" })] })] }));
84
84
  }
@@ -120,7 +120,7 @@ function Editor({ skill, may, onClose, onSaved, onDeleted, }) {
120
120
  setWorking(false);
121
121
  }
122
122
  };
123
- return (_jsxs(Sheet, { title: skill ? skill.name : 'New skill', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [skill?.source ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["Read from ", skill.source, ". The next push of that repository replaces what is saved here."] })) : null, _jsx(Field, { label: "Name", hint: skill ? 'A skill’s name is its id; save under another name to make a new one.' : 'One lowercase word: letters, digits, _ or -. Agents name it as skill_<name>.', children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "triage", "aria-label": "Name", disabled: Boolean(skill), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The one line an agent reads to decide whether it needs this skill.", children: _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "How we triage an incoming issue", "aria-label": "Description" }) }), _jsx(Field, { label: "SKILL.md", hint: "Markdown: what the skill is for, when to use it, and the steps.", children: _jsx(Textarea, { value: content, onChangeText: setContent, placeholder: '# Triage\n\n1. Read the issue…', "aria-label": "SKILL.md", rows: 14, style: mono }) }), _jsx(Line, { children: note || (may ? '' : 'An org admin writes and deletes the organization’s skills.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [skill && may ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: may ? 'Cancel' : 'Close' }), may ? (_jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save" })) : null] }), skill ? (_jsx(Confirm, { what: skill.name, says: "The skill leaves your organization and every agent that names it.", open: asking, onOpenChange: setAsking, onYes: async () => {
123
+ return (_jsxs(Sheet, { title: skill ? skill.name : 'New skill', open: true, onOpenChange: (o) => !o && onClose(), width: 680, children: [skill?.source ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["Read from ", skill.source, ". The next push of that repository replaces what is saved here."] })) : null, _jsx(Field, { label: "Name", hint: skill ? 'A skill’s name is its id; save under another name to make a new one.' : 'One lowercase word: letters, digits, _ or -. Agents name it as skill_<name>.', children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name", disabled: Boolean(skill), autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", hint: "The one line an agent reads to decide whether it needs this skill.", children: _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "How we triage an incoming issue", "aria-label": "Description" }) }), _jsx(Field, { label: "SKILL.md", hint: "Markdown: what the skill is for, when to use it, and the steps.", children: _jsx(Textarea, { value: content, onChangeText: setContent, placeholder: '# Triage\n\n1. Read the issue…', "aria-label": "SKILL.md", rows: 14, style: mono }) }), _jsx(Line, { children: note || (may ? '' : 'An org admin writes and deletes the organization’s skills.') }), _jsxs(XStack, { gap: "$2", items: "center", children: [skill && may ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAsking(true), children: "Delete" })) : null, _jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: may ? 'Cancel' : 'Close' }), may ? (_jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save" })) : null] }), skill ? (_jsx(Confirm, { what: skill.name, says: "The skill leaves your organization and every agent that names it.", open: asking, onOpenChange: setAsking, onYes: async () => {
124
124
  await remove(t, skill.id);
125
125
  onDeleted(skill.name);
126
126
  } })) : null] }));
package/lib/forge.js CHANGED
@@ -60,11 +60,12 @@ function Codebases() {
60
60
  const [q, setQ] = useState('');
61
61
  const [making, setMaking] = useState(false);
62
62
  const [sort, setSort] = useState('name');
63
+ const [filter, setFilter] = useState('all');
63
64
  const [page, setPage] = useState(0);
64
65
  const [landed, setLanded] = useState(0);
65
66
  useEffect(() => {
66
67
  setPage(0);
67
- }, [q, sort]);
68
+ }, [q, sort, filter]);
68
69
  const list = useRead(signed ? () => codebases(t) : null, [], [t, signed, making, landed]);
69
70
  // Which codebases have an environment. A codebase absent here has none.
70
71
  const envs = useRead(signed ? () => environments(t) : null, [], [t, signed]);
@@ -109,6 +110,13 @@ function Codebases() {
109
110
  branches: ['main'],
110
111
  })),
111
112
  ]
113
+ .filter((c) => {
114
+ if (filter === 'ready')
115
+ return envOf.get(c.name) === 'ready';
116
+ if (filter === 'syncing')
117
+ return syncing.has(c.name);
118
+ return true;
119
+ })
112
120
  .filter((c) => !needle || `${c.org}/${c.name} ${c.description}`.toLowerCase().includes(needle))
113
121
  .slice()
114
122
  .sort((a, b) => {
@@ -128,7 +136,7 @@ function Codebases() {
128
136
  pinCodebase(host.org, c);
129
137
  host.go('');
130
138
  };
131
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : ago(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
139
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", shrink: 0, items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => host.go('-/settings/environments'), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Filter repositories", onPress: () => setFilter((f) => (f === 'all' ? 'ready' : f === 'ready' ? 'syncing' : 'all')), px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(SizableText, { size: "$2", color: "$ink", children: filter === 'ready' ? 'Ready env' : filter === 'syncing' ? 'Syncing' : 'All repos' }) }), _jsx(YStack, { flex: 1, "$max-md": { display: 'none' } }), _jsx(YStack, { width: 240, minW: 0, shrink: 1, "$max-md": { width: 'auto', flex: 1 }, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' }), list.value.length === 0 ? (_jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "Create repository"] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" }), _jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.go('-/templates'), children: "Start from template" })] })) : null] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: "Environment" }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 96, "$max-md": { display: 'none' }, children: envOf.get(c.name) === 'ready' ? 'Ready' : envOf.get(c.name) === 'proposed' ? 'To review' : '—' }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : ago(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
132
140
  const row = await create(t, name, description);
133
141
  pinCodebase(host.org, row);
134
142
  setMaking(false);
@@ -140,6 +148,13 @@ function NewRepo({ open, onOpenChange, onCreate, }) {
140
148
  const [description, setDescription] = useState('');
141
149
  const [busy, setBusy] = useState(false);
142
150
  const [error, setError] = useState('');
151
+ useEffect(() => {
152
+ if (!open) {
153
+ setName('');
154
+ setDescription('');
155
+ setError('');
156
+ }
157
+ }, [open]);
143
158
  const submit = async () => {
144
159
  setBusy(true);
145
160
  setError('');
@@ -166,7 +181,7 @@ function Projects() {
166
181
  pinBoard(host.org, b.key);
167
182
  host.go(path({ kind: 'screen', screen: 'issues' }));
168
183
  };
169
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
184
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Projects", says: "Boards on the forge. A board is a repository that has work on it." }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's boards." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "No boards yet. A repository shows up here once it has an issue." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync from GitHub" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((b) => (_jsx(Row, { label: `Open ${b.name}`, onPress: () => open(b), children: _jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: b.name }), b.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: b.description })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: b.key }))] }) }, b.key))) }))] }) }));
170
185
  }
171
186
  function Issues() {
172
187
  const host = useHost();
@@ -196,5 +211,5 @@ function Issues() {
196
211
  }
197
212
  host.go('');
198
213
  };
199
- return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsx(Note, { children: "Nothing open." })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, w.id))) }))] }) }));
214
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 720, gap: "$3", children: [_jsx(Head, { title: "Issues", says: board ? `Open work on ${board}. Choosing one starts the next run on that codebase.` : 'Open work across every board. Choosing one starts the next run on that codebase.' }), board ? (_jsx(XStack, { render: "button", onPress: () => setBoard(''), "aria-label": "Show every board", self: "flex-start", children: _jsx(SizableText, { size: "$1", color: "$soft", children: "All boards" }) })) : null, list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's issues." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", py: "$2", children: [_jsx(Note, { children: "Nothing open." }), _jsxs(XStack, { gap: "$2", pt: "$1", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'codebases' })), children: "View repositories" }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "Start a new run" })] })] })) : (_jsx(YStack, { gap: "$2", children: list.value.map((w) => (_jsxs(Row, { label: `Build ${w.identifier || w.title}`, onPress: () => open(w), children: [_jsx(SizableText, { size: "$1", color: "$soft", width: 88, numberOfLines: 1, children: w.identifier || w.kind }), _jsx(SizableText, { size: "$3", color: "$ink", flex: 1, minW: 0, numberOfLines: 1, children: w.title }), _jsx(SizableText, { size: "$1", color: "$soft", children: STATUS[w.status] ?? w.status })] }, w.id))) }))] }) }));
200
215
  }
@@ -6,14 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  */
7
7
  import { SizableText, XStack, YStack } from '@hanzo/gui';
8
8
  import { CreditCard, Download, Plus, X } from '@hanzogui/lucide-icons-2';
9
- import { Button, Dialog, DialogContent, DialogTitle, toast } from '@hanzo/ui';
10
- import { useState } from 'react';
11
- import { cancel, current, detach, invoices, label, methods, money, pdf, reactivate, subscriptions, } from '../api/billing.js';
9
+ import { Button, Dialog, DialogContent, DialogTitle, Input, toast } from '@hanzo/ui';
10
+ import { useEffect, useState } from 'react';
11
+ import { cancel, chosen, current, detach, formatAddress, invoices, label, makeDefault, methods, money, pdf, reactivate, subscriptions, updateMethod, } from '../api/billing.js';
12
12
  import { useRead } from '../data.js';
13
13
  import { useHost, useTarget } from '../host.js';
14
14
  import { path } from '../route.js';
15
15
  import { AddCard } from './card.js';
16
- import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
16
+ import { Card, day, Field, Group, Heading, Note, Row, Soft } from './ui.js';
17
17
  export function Billing() {
18
18
  const host = useHost();
19
19
  const t = useTarget();
@@ -23,6 +23,7 @@ export function Billing() {
23
23
  const bills = useRead(signed ? () => invoices(t) : null, [], [t, signed]);
24
24
  const [adding, setAdding] = useState(false);
25
25
  const [ending, setEnding] = useState(false);
26
+ const [editingBilling, setEditingBilling] = useState(null);
26
27
  const [open, setOpen] = useState(null);
27
28
  const [working, setWorking] = useState(false);
28
29
  const [note, setNote] = useState('');
@@ -31,6 +32,7 @@ export function Billing() {
31
32
  }
32
33
  const plan = current(subs.value);
33
34
  const empty = !cards.error && !cards.loading && cards.value.length === 0;
35
+ const defaultCard = chosen(cards.value);
34
36
  // A done act is a toast; a refused one stays on the page, beside what was tried.
35
37
  const act = async (what, done, after) => {
36
38
  setWorking(true);
@@ -51,14 +53,85 @@ export function Billing() {
51
53
  ? subs.error.message
52
54
  : plan
53
55
  ? terms(plan)
54
- : 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [m.expires ? `Expires ${m.expires}` : '', m.default ? 'Default' : ''].filter(Boolean).join(' · ') || undefined, trailing: host.admin ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${label(m)}`, disabled: working, onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: "Remove" })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin adds and removes cards." })) : null] }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
56
+ : 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [
57
+ m.expires ? `Expires ${m.expires}` : '',
58
+ m.default ? 'Default' : '',
59
+ m.billingAddress ? formatAddress(m.billingAddress) : '',
60
+ ]
61
+ .filter(Boolean)
62
+ .join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Make ${label(m)} default`, disabled: working, onPress: () => void act(() => makeDefault(t, m.id), `${label(m)} is now the default`, cards.reload), children: "Make default" })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Edit billing info for ${label(m)}`, disabled: working, onPress: () => setEditingBilling(m), children: "Edit info" }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${label(m)}`, disabled: working, onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: "Remove" })] })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin adds and removes cards." })) : null] }), _jsx(Group, { title: "Billing details", detail: "Information printed on invoices and payment receipts.", action: host.admin && defaultCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditingBilling(defaultCard), children: defaultCard.billingAddress ? 'Edit details' : 'Add billing info' })) : undefined, children: defaultCard?.billingAddress ? (_jsx(Card, { children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [defaultCard.billingAddress.name ? (_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: defaultCard.billingAddress.name })) : null, defaultCard.billingAddress.line1 ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.line1, defaultCard.billingAddress.line2 ? `, ${defaultCard.billingAddress.line2}` : ''] })) : null, defaultCard.billingAddress.city || defaultCard.billingAddress.state || defaultCard.billingAddress.postalCode ? (_jsx(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.city, defaultCard.billingAddress.state, defaultCard.billingAddress.postalCode]
63
+ .filter(Boolean)
64
+ .join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "No billing address on file." }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Add your company name and address so they appear on monthly invoices and receipts." })] }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => (defaultCard ? setEditingBilling(defaultCard) : setAdding(true)), children: [_jsx(Plus, { size: 14 }), " ", defaultCard ? 'Add billing info' : 'Set billing info'] })) : null] })) }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
55
65
  cards.reload();
56
66
  toast.success(`${label(m)} is saved`);
67
+ } }), _jsx(EditBilling, { open: editingBilling !== null, onOpenChange: (o) => !o && setEditingBilling(null), card: editingBilling, onSaved: () => {
68
+ cards.reload();
57
69
  } }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
58
70
  setEnding(false);
59
71
  subs.reload();
60
72
  }) })) : null, open ? _jsx(Bill, { invoice: open, onClose: () => setOpen(null) }) : null] }));
61
73
  }
74
+ function EditBilling({ open, onOpenChange, card, onSaved, }) {
75
+ const t = useTarget();
76
+ const [name, setName] = useState('');
77
+ const [line1, setLine1] = useState('');
78
+ const [line2, setLine2] = useState('');
79
+ const [city, setCity] = useState('');
80
+ const [stateVal, setStateVal] = useState('');
81
+ const [postalCode, setPostalCode] = useState('');
82
+ const [country, setCountry] = useState('US');
83
+ const [working, setWorking] = useState(false);
84
+ const [note, setNote] = useState('');
85
+ useEffect(() => {
86
+ if (card?.billingAddress) {
87
+ setName(card.billingAddress.name ?? '');
88
+ setLine1(card.billingAddress.line1 ?? '');
89
+ setLine2(card.billingAddress.line2 ?? '');
90
+ setCity(card.billingAddress.city ?? '');
91
+ setStateVal(card.billingAddress.state ?? '');
92
+ setPostalCode(card.billingAddress.postalCode ?? '');
93
+ setCountry(card.billingAddress.country ?? 'US');
94
+ }
95
+ else {
96
+ setName('');
97
+ setLine1('');
98
+ setLine2('');
99
+ setCity('');
100
+ setStateVal('');
101
+ setPostalCode('');
102
+ setCountry('US');
103
+ }
104
+ setNote('');
105
+ }, [card]);
106
+ const save = async () => {
107
+ if (!card)
108
+ return;
109
+ setWorking(true);
110
+ setNote('');
111
+ try {
112
+ const address = {
113
+ name: name.trim() || undefined,
114
+ line1: line1.trim() || undefined,
115
+ line2: line2.trim() || undefined,
116
+ city: city.trim() || undefined,
117
+ state: stateVal.trim() || undefined,
118
+ postalCode: postalCode.trim() || undefined,
119
+ country: country.trim() || undefined,
120
+ };
121
+ await updateMethod(t, card.id, { billingAddress: address });
122
+ onSaved();
123
+ onOpenChange(false);
124
+ toast.success('Billing details updated');
125
+ }
126
+ catch (e) {
127
+ setNote(e.message);
128
+ }
129
+ finally {
130
+ setWorking(false);
131
+ }
132
+ };
133
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Billing details" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "These details appear on your invoices and receipts." }), _jsx(Field, { label: "Business or personal name", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "e.g. Webby AI, Inc. or Joshua Lynch" }) }), _jsx(Field, { label: "Address line 1", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Street address or P.O. Box" }) }), _jsx(Field, { label: "Address line 2", hint: "Optional", children: _jsx(Input, { value: line2, onChangeText: setLine2, placeholder: "Suite, unit, building, floor" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 100, children: _jsx(Field, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State / Prov" }) }) })] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "Postal / ZIP code", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP or Postal Code" }) }) }), _jsx(YStack, { width: 110, children: _jsx(Field, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] }), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", pt: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save details" })] })] })] }) }));
134
+ }
62
135
  /** What the plan costs, and when it renews or ends. */
63
136
  function terms(p) {
64
137
  const cost = `${money(p.price * p.seats, p.currency)} a ${p.interval}${p.seats > 1 ? ` for ${p.seats} seats` : ''}.`;
@@ -8,12 +8,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
8
8
  * it will be charged on.
9
9
  */
10
10
  import { SizableText, XStack, YStack } from '@hanzo/gui';
11
- import { X } from '@hanzogui/lucide-icons-2';
12
- import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
11
+ import { ChevronDown, ChevronUp, X } from '@hanzogui/lucide-icons-2';
12
+ import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
13
13
  import { useEffect, useRef, useState } from 'react';
14
14
  import { processor, save } from '../api/billing.js';
15
15
  import { useRead } from '../data.js';
16
16
  import { useTarget } from '../host.js';
17
+ import { Field as FormField } from './ui.js';
17
18
  /** Anything but production loads the sandbox SDK, so a misread setting never arms real charges. */
18
19
  export const sdk = (environment) => environment === 'production' ? 'https://web.squarecdn.com/v1/square.js' : 'https://sandbox.web.squarecdn.com/v1/square.js';
19
20
  const loads = new Map();
@@ -47,6 +48,13 @@ export function AddCard({ open, onOpenChange, onSaved }) {
47
48
  const [mounted, setMounted] = useState(false);
48
49
  const [working, setWorking] = useState(false);
49
50
  const [note, setNote] = useState('');
51
+ const [name, setName] = useState('');
52
+ const [postalCode, setPostalCode] = useState('');
53
+ const [moreAddress, setMoreAddress] = useState(false);
54
+ const [line1, setLine1] = useState('');
55
+ const [city, setCity] = useState('');
56
+ const [stateVal, setStateVal] = useState('');
57
+ const [country, setCountry] = useState('US');
50
58
  const p = cfg.value;
51
59
  useEffect(() => {
52
60
  if (!open || !box || !ready(p))
@@ -87,7 +95,17 @@ export function AddCard({ open, onOpenChange, onSaved }) {
87
95
  const r = await f.tokenize();
88
96
  if (r.status !== 'OK' || !r.token)
89
97
  throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || 'The card details are incomplete');
90
- const m = await save(t, r.token);
98
+ const billingAddress = name.trim() || postalCode.trim() || line1.trim() || city.trim() || stateVal.trim()
99
+ ? {
100
+ name: name.trim() || undefined,
101
+ postalCode: postalCode.trim() || undefined,
102
+ line1: line1.trim() || undefined,
103
+ city: city.trim() || undefined,
104
+ state: stateVal.trim() || undefined,
105
+ country: country.trim() || 'US',
106
+ }
107
+ : undefined;
108
+ const m = await save(t, r.token, billingAddress);
91
109
  onSaved(m);
92
110
  onOpenChange(false);
93
111
  }
@@ -99,7 +117,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
99
117
  }
100
118
  };
101
119
  const why = cfg.error ? cfg.error.message : !cfg.loading && !ready(p) ? 'This deployment takes no cards yet.' : '';
102
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Add a card" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": "Card details", children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Loading the card form\u2026" })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
120
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Add a card" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": "Card details", children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Loading the card form\u2026" })) : null] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "Name on card", hint: "Optional", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "Name or business" }) }) }), _jsx(YStack, { width: 120, children: _jsx(FormField, { label: "Postal code", hint: "Optional", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP / Postal" }) }) })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setMoreAddress(!moreAddress), children: [moreAddress ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), moreAddress ? 'Hide address' : 'Add billing address'] }) }), moreAddress ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsx(FormField, { label: "Street address", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Address line 1" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] })] })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
103
121
  ? 'Sandbox: use a test card. Nothing is charged.'
104
122
  : 'The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.' })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: "Save card" })] })] })] }) }));
105
123
  }
@@ -7,11 +7,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  * mints its claim key or removes it.
8
8
  */
9
9
  import { SizableText, XStack, YStack } from '@hanzo/gui';
10
- import { MoreHorizontal, Plus } from '@hanzogui/lucide-icons-2';
10
+ import { Activity, ChevronDown, ChevronUp, Cloud, Cpu, HardDrive, Laptop, MoreHorizontal, Plus, Server, Terminal, Zap, } from '@hanzogui/lucide-icons-2';
11
11
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
12
12
  import { CopyButton } from '@hanzo/ui/product';
13
13
  import { useEffect, useState } from 'react';
14
- import { add, change, KINDS, key, machines, remove, state } from '../api/machines.js';
14
+ import { add, change, formatBytes, formatPercent, formatRelative, key, KINDS, machines, remove, state, } from '../api/machines.js';
15
15
  import { Confirm, Rename } from '../ask.js';
16
16
  import { useRead } from '../data.js';
17
17
  import { useHost, useTarget } from '../host.js';
@@ -32,6 +32,44 @@ function Dot({ m }) {
32
32
  const on = state(m) === 'online';
33
33
  return (_jsx(YStack, { width: 8, height: 8, rounded: 999, shrink: 0, bg: on ? '$green10' : 'transparent', borderWidth: on ? 0 : 1, borderColor: m.status === 'draining' ? '$yellow10' : '$soft', "aria-hidden": true }));
34
34
  }
35
+ function KindIcon({ kind, size = 16 }) {
36
+ switch (kind) {
37
+ case 'laptop':
38
+ return _jsx(Laptop, { size: size });
39
+ case 'gpu':
40
+ return _jsx(Cpu, { size: size });
41
+ case 'cloud':
42
+ return _jsx(Cloud, { size: size });
43
+ case 'cluster':
44
+ return _jsx(Server, { size: size });
45
+ default:
46
+ return _jsx(Terminal, { size: size });
47
+ }
48
+ }
49
+ function StatusBadge({ m }) {
50
+ const s = state(m);
51
+ if (!s)
52
+ return null;
53
+ let color = '$soft';
54
+ let text = s;
55
+ if (s === 'online') {
56
+ color = '$green10';
57
+ text = 'Online';
58
+ }
59
+ else if (s === 'draining') {
60
+ color = '$yellow10';
61
+ text = 'Draining';
62
+ }
63
+ else if (s === 'not seen yet') {
64
+ color = '$blue10';
65
+ text = 'Not seen yet';
66
+ }
67
+ else if (s === 'offline') {
68
+ color = '$soft';
69
+ text = 'Offline';
70
+ }
71
+ return (_jsxs(XStack, { px: "$2", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: color, items: "center", gap: "$1.5", children: [_jsx(Dot, { m: m }), _jsx(SizableText, { size: "$1", color: color, style: { fontWeight: 500 }, children: text })] }));
72
+ }
35
73
  export function Machines() {
36
74
  const host = useHost();
37
75
  const t = useTarget();
@@ -42,6 +80,8 @@ export function Machines() {
42
80
  const [removing, setRemoving] = useState(null);
43
81
  const [keying, setKeying] = useState(null);
44
82
  const [minted, setMinted] = useState(null);
83
+ const [expandedId, setExpandedId] = useState(null);
84
+ const [showAdvancedLink, setShowAdvancedLink] = useState(false);
45
85
  const [note, setNote] = useState('');
46
86
  // Read again on the agent's beat; "Reading machines…" stays for the first read only.
47
87
  const again = list.reload;
@@ -65,15 +105,20 @@ export function Machines() {
65
105
  setNote(e.message);
66
106
  }
67
107
  };
68
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsxs(Group, { title: "Linked", children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? list.error.message : 'Reading machines…' })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { m: m }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
69
- { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
70
- m.status === 'draining'
71
- ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
72
- : { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
73
- { key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
74
- { type: 'separator' },
75
- { key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
76
- ] }) }, m.id))) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error.message }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
108
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }), _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setShowAdvancedLink(!showAdvancedLink), children: [showAdvancedLink ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), showAdvancedLink ? 'Hide options' : 'More connection options'] })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Supported: macOS, Linux, Windows, NVIDIA CUDA GPUs, Cloud VMs" })] }), showAdvancedLink ? (_jsxs(YStack, { gap: "$2.5", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "1. Background daemon (system service)" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Run worker continuously in the background without keeping a terminal open:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "hanzo code --serve" }), _jsx(CopyButton, { value: "hanzo code --serve", label: "Copy daemon command" })] })] }), _jsxs(YStack, { gap: "$1", pt: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: "2. Install CLI from shell" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: mono, children: "curl -fsSL https://hanzo.sh | sh" }), _jsx(CopyButton, { value: "curl -fsSL https://hanzo.sh | sh", label: "Copy install command" })] })] })] })) : null] }), _jsxs(Group, { title: "Linked", children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? list.error.message : 'Reading machines…' })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Soft, { children: "No machine is linked yet." }), _jsx(Card, { children: _jsxs(YStack, { p: "$4", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Server, { size: 20 }), _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: "Connect your first computer or GPU server" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Execute code, runs, and local models directly on your hardware with full privacy and zero cloud fees." })] })] }), _jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "1." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Install Hanzo CLI: ", _jsx("span", { style: mono, children: "curl -fsSL https://hanzo.sh | sh" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "2." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Sign in with your account: ", _jsx("span", { style: mono, children: "hanzo login" })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "3." }), _jsxs(SizableText, { size: "$2", color: "$ink", children: ["Link the machine: ", _jsx("span", { style: mono, children: "hanzo link" })] })] })] }), _jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }) })] })) : (_jsx(Card, { children: list.value.map((m, i) => {
109
+ const isExpanded = expandedId === m.id;
110
+ const hasTelemetry = Boolean(m.metrics || m.spec);
111
+ return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children: "Serving" }) })) : null, m.running > 0 ? (_jsxs(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", bg: "$raised", items: "center", gap: "$1", children: [_jsx(Activity, { size: 12, color: "$green10" }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.running, " active"] })] })) : null, _jsx(CopyButton, { value: `hanzo link --target ${m.id}`, label: `Copy link command for ${m.label}` }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
112
+ { key: 'details', label: isExpanded ? 'Hide details' : 'Show details', onSelect: () => setExpandedId(isExpanded ? null : m.id) },
113
+ { key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
114
+ m.status === 'draining'
115
+ ? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
116
+ : { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
117
+ { key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
118
+ { type: 'separator' },
119
+ { key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
120
+ ] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.cpus, " cores"] })] })) : null, m.spec.memory ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(HardDrive, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [formatBytes(m.spec.memory), " RAM"] })] })) : null, m.spec.gpus && m.spec.gpus.length > 0 ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Zap, { size: 12, color: "$yellow10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: m.spec.gpus.map((g) => `${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`).join(', ') })] })) : null] })) : null, m.metrics && (m.metrics.cpuUtil || m.metrics.load1 || m.metrics.memUsed || m.metrics.gpuUtil) ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$3", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", children: [m.metrics.cpuUtil ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "CPU Utilization" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.cpuUtil), " ", m.metrics.cpuTemp ? `· ${m.metrics.cpuTemp}°C` : ''] })] })) : m.metrics.load1 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Load (1m, 5m, 15m)" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.load1, ", ", m.metrics.load5 ?? 0, ", ", m.metrics.load15 ?? 0] })] })) : null, m.metrics.memUsed ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Memory" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatBytes(m.metrics.memUsed), " ", m.spec?.memory ? `/ ${formatBytes(m.spec.memory)}` : 'used'] })] })) : null, m.metrics.gpuUtil !== undefined && m.metrics.gpuUtil > 0 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "GPU" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.gpuUtil), " ", m.metrics.gpuTemp ? `· ${m.metrics.gpuTemp}°C` : '', " ", m.metrics.gpuPower ? `· ${m.metrics.gpuPower}W` : ''] })] })) : null, m.metrics.model ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Serving Model" }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.model, " ", m.metrics.decode ? `(${m.metrics.decode} tok/s)` : ''] })] })) : null] })) : null, _jsxs(XStack, { justify: "space-between", items: "center", flexWrap: "wrap", gap: "$2", pt: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsxs(SizableText, { size: "$1", color: "$soft", style: mono, children: ["ID: ", m.id] }), m.seen ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Last heartbeat: ", formatRelative(m.seen)] })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Run directly on this machine:" }), _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["hanzo run --target ", m.id] }), _jsx(CopyButton, { value: `hanzo run --target ${m.id}`, label: `Copy run command for ${m.label}` })] })] })] })] })) : null] }, m.id));
121
+ }) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error.message }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
77
122
  setNote(`${m.label} is registered`);
78
123
  list.reload();
79
124
  } }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '',
@@ -89,7 +134,7 @@ export function Machines() {
89
134
  } }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
90
135
  const k = await key(t, keying.id);
91
136
  setMinted({ machine: keying, key: k });
92
- } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
137
+ } }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) }), _jsxs(YStack, { gap: "$1", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: { fontWeight: 600 }, children: "Start background worker with this key:" }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2", py: "$1", rounded: "$2", bg: "$raised", children: [_jsxs(SizableText, { size: "$1", color: "$ink", style: mono, children: ["export HANZO_TARGET_KEY=\"", minted.key, "\""] }), _jsx(CopyButton, { value: `export HANZO_TARGET_KEY="${minted.key}"\nhanzo code --serve --target ${minted.machine.id}`, label: "Copy key command" })] })] })] })) : null, _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
93
138
  }
94
139
  /** Registering a machine by hand: its name, what it is, and the hostname its runs report. */
95
140
  function Adding({ open, onOpenChange, onAdded }) {
@@ -97,6 +142,7 @@ function Adding({ open, onOpenChange, onAdded }) {
97
142
  const [label, setLabel] = useState('');
98
143
  const [kind, setKind] = useState('machine');
99
144
  const [hostname, setHostname] = useState('');
145
+ const [capacity, setCapacity] = useState('');
100
146
  const [working, setWorking] = useState(false);
101
147
  const [note, setNote] = useState('');
102
148
  const close = () => {
@@ -105,6 +151,7 @@ function Adding({ open, onOpenChange, onAdded }) {
105
151
  setLabel('');
106
152
  setKind('machine');
107
153
  setHostname('');
154
+ setCapacity('');
108
155
  setNote('');
109
156
  onOpenChange(false);
110
157
  };
@@ -112,7 +159,10 @@ function Adding({ open, onOpenChange, onAdded }) {
112
159
  setWorking(true);
113
160
  setNote('');
114
161
  try {
115
- const m = await add(t, { label, kind, host: hostname });
162
+ const targetLabel = label.trim() || hostname.trim();
163
+ if (!targetLabel)
164
+ throw new Error('A machine needs a name or a hostname');
165
+ const m = await add(t, { label: targetLabel, kind, host: hostname, capacity: capacity.trim() || undefined });
116
166
  setWorking(false);
117
167
  onAdded(m);
118
168
  close();
@@ -122,5 +172,5 @@ function Adding({ open, onOpenChange, onAdded }) {
122
172
  setWorking(false);
123
173
  }
124
174
  };
125
- return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "Left empty, it takes the hostname.", children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k }) }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": "Hostname", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
175
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", hint: "Left empty, it takes the hostname.", children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsxs(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', items: "center", gap: "$1.5", children: [_jsx(KindIcon, { kind: k, size: 14 }), _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k })] }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": "Hostname", autoCapitalize: "none" }) }), _jsx(Field, { label: "Capacity / Hardware", hint: "Optional human summary (e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090).", children: _jsx(Input, { value: capacity, onChangeText: setCapacity, "aria-label": "Capacity", placeholder: "e.g. 16 vCPU / 64G / 1\u00D7 RTX 4090" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
126
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.48",
3
+ "version": "0.2.49",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",