@hanzo/build 0.2.79 → 0.2.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -4
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +36 -35
- package/lib/api/agents.js +66 -29
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +2 -1
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.d.ts +3 -0
- package/lib/customize/agents.js +152 -55
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +45 -14
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/host.d.ts +3 -1
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +6 -3
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +18 -15
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/settings/code.js
CHANGED
|
@@ -5,12 +5,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* — and the model and effort, which are the one choice Chat and Dev share
|
|
6
6
|
* (mind.tsx): changed here, changed in both composers. Beside them, the
|
|
7
7
|
* codebases' environments and the organization's machines, each on its own page.
|
|
8
|
+
*
|
|
9
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a model's and a
|
|
10
|
+
* machine's names are drawn as given.
|
|
8
11
|
*/
|
|
9
12
|
import { YStack } from '@hanzo/gui';
|
|
13
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
10
14
|
import { ChevronRight } from '@hanzogui/lucide-icons-2';
|
|
11
15
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
12
16
|
import { ModelPicker } from '@hanzo/ui/models';
|
|
13
17
|
import { useState } from 'react';
|
|
18
|
+
import { told } from '../api/call.js';
|
|
14
19
|
import { unhonoured } from '../api/coding.js';
|
|
15
20
|
import { ENSO } from '../api/models.js';
|
|
16
21
|
import { SANDBOX } from '../api/places.js';
|
|
@@ -20,29 +25,31 @@ import { MODES } from '../landing.js';
|
|
|
20
25
|
import { EFFORTS, useCatalog, useChoice } from '../mind.js';
|
|
21
26
|
import { usePrefs } from '../prefs.js';
|
|
22
27
|
import { path } from '../route.js';
|
|
28
|
+
import { said } from './machines.js';
|
|
23
29
|
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
24
30
|
export function Code() {
|
|
31
|
+
const { t } = useI18n();
|
|
25
32
|
const host = useHost();
|
|
26
|
-
const
|
|
33
|
+
const target = useTarget();
|
|
27
34
|
const signed = Boolean(host.person);
|
|
28
35
|
const { prefs, save } = usePrefs();
|
|
29
|
-
const catalog = useCatalog(
|
|
36
|
+
const catalog = useCatalog(target);
|
|
30
37
|
const [choice, choose] = useChoice();
|
|
31
|
-
const places = usePlaces(
|
|
38
|
+
const places = usePlaces(target, signed);
|
|
32
39
|
const [note, setNote] = useState('');
|
|
33
40
|
if (!signed) {
|
|
34
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
41
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Code') }), _jsx(Soft, { children: t('Sign in to set how your runs start.') })] }));
|
|
35
42
|
}
|
|
36
43
|
const now = { mode: 'build', place: '', ...prefs.code };
|
|
37
44
|
const keep = (change) => {
|
|
38
45
|
setNote('');
|
|
39
|
-
save({ code: { ...prefs.code, ...change } }).catch((e) => setNote(e
|
|
46
|
+
save({ code: { ...prefs.code, ...change } }).catch((e) => setNote(told(e, t)));
|
|
40
47
|
};
|
|
41
48
|
const where = (places.value.length ? places.value : [SANDBOX]).map((p) => ({
|
|
42
49
|
id: p.id || 'sandbox',
|
|
43
|
-
label: p.id ? p.label : 'Default',
|
|
50
|
+
label: p.id ? p.label : t('Default'),
|
|
44
51
|
// A machine offline now can still be the default; its status shows beside it.
|
|
45
|
-
hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : 'The platform’s own sandbox',
|
|
52
|
+
hint: p.id ? [said(p.status, t), p.capacity].filter(Boolean).join(' · ') : t('The platform’s own sandbox'),
|
|
46
53
|
place: p.id,
|
|
47
54
|
}));
|
|
48
55
|
// The sandbox is always listed, so a place not found is a machine that is not: it reads as its id.
|
|
@@ -50,10 +57,14 @@ export function Code() {
|
|
|
50
57
|
// A default is never premium: a premium model is picked for one run, in New.
|
|
51
58
|
const offered = catalog.models.filter((m) => m.class !== 'premium');
|
|
52
59
|
const go = (section) => host.go(path({ kind: 'settings', section }));
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
60
|
+
// The choices in the reader's words, kept by id.
|
|
61
|
+
const efforts = EFFORTS.map((e) => ({ ...e, label: t(e.label) }));
|
|
62
|
+
const modes = MODES.map((m) => ({ ...m, label: t(m.label), hint: t(m.hint) }));
|
|
63
|
+
const effort = efforts.find((e) => e.id === choice.effort);
|
|
64
|
+
const mode = modes.find((m) => m.id === now.mode);
|
|
65
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Code'), detail: t('How a new run starts. The model and effort are the ones Chat uses too.') }), _jsxs(Group, { title: t('Defaults'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: t('Model'), detail: t('Enso picks one for each step; any other model runs them all.'), trailing: _jsx(ModelPicker, { size: "sm", name: t('Default model'), models: offered, scope: "chat", value: choice.model || ENSO, onChange: (id) => choose({ model: id }), loading: catalog.loading, error: catalog.error }) }), _jsx(Row, { title: t('Effort'), detail: t('How long the agent thinks before it acts.'), trailing: _jsx(ChipSelect, { name: t('Default effort'), label: effort.label, chosen: effort, items: efforts, onChange: (e) => choose({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: t('Mode'), detail: mode.hint, trailing: _jsx(ChipSelect, { name: t('Default mode'), label: mode.label, chosen: mode, items: modes, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: t('Where it runs'), detail: t('The Hanzo sandbox, or one of your organization’s machines.'), trailing: _jsx(ChipSelect, { name: t('Default place'), label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: t('Search machines…'), placement: "bottom-end", loading: places.loading, error: places.error ? told(places.error, t) : null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place, t) })] }), _jsx(Group, { title: t('Workspace'), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: t('Environments'), detail: t('What a run installs and starts in each codebase, and the secrets it exports.'), trailing: _jsx(Link, { label: t('Open Environments'), onPress: () => go('environments') }) }), _jsx(Row, { title: t('Machines'), detail: t('Your organization’s own computers that take runs.'), trailing: _jsx(Link, { label: t('Open Machines'), onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
|
|
56
66
|
}
|
|
67
|
+
/** A row's way to its own page; `label` is what it opens, already said. */
|
|
57
68
|
function Link({ label, onPress }) {
|
|
58
|
-
return (_jsx(YStack, { render: "button", "aria-label":
|
|
69
|
+
return (_jsx(YStack, { render: "button", "aria-label": label, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onPress, children: _jsx(ChevronRight, { size: 16, 'data-i18n-mirror': '' }) }));
|
|
59
70
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/** What the credits section says first, on every surface
|
|
3
|
-
export declare const SEPARATE
|
|
4
|
-
export declare function Credits({ target
|
|
1
|
+
import { type Target } from '../api/call.ts';
|
|
2
|
+
/** What the credits section says first, on every surface: English, marked (`phrase`), drawn through `t`. */
|
|
3
|
+
export declare const SEPARATE: string;
|
|
4
|
+
export declare function Credits({ target, admin, add, onCard }: {
|
|
5
5
|
target: Target;
|
|
6
6
|
admin: boolean;
|
|
7
7
|
add?: string;
|
package/lib/settings/credits.js
CHANGED
|
@@ -13,18 +13,25 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
13
13
|
* Money moves two ways, the host's choice: `add` names a page that takes it (the
|
|
14
14
|
* pay site); without one a saved card is charged here (POST /v1/billing/topup)
|
|
15
15
|
* and a person with no card is sent to add one (`onCard`).
|
|
16
|
+
*
|
|
17
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a grant's, a limit's
|
|
18
|
+
* and a kind of spend's names are the ledger's, drawn as given.
|
|
16
19
|
*/
|
|
17
20
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
21
|
+
import { phrase } from '@hanzo/i18n';
|
|
22
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
18
23
|
import { Button, Input, Switch } from '@hanzo/ui';
|
|
19
24
|
import { useEffect, useState } from 'react';
|
|
20
25
|
import { arm, balance, caps, cents, chosen, credit, grants, label, lift, limit, methods, money, monthly, recharge, spend, topup, } from '../api/billing.js';
|
|
26
|
+
import { told } from '../api/call.js';
|
|
21
27
|
import { useRead } from '../data.js';
|
|
22
28
|
import { Card, day, Group, Note, Row, Soft } from './ui.js';
|
|
23
|
-
/** What the credits section says first, on every surface
|
|
24
|
-
export const SEPARATE = 'Credits are separate from your plan’s usage allowance. They pay for usage past it when you choose to continue with credits, and for all usage when there is no plan.';
|
|
29
|
+
/** What the credits section says first, on every surface: English, marked (`phrase`), drawn through `t`. */
|
|
30
|
+
export const SEPARATE = phrase('Credits are separate from your plan’s usage allowance. They pay for usage past it when you choose to continue with credits, and for all usage when there is no plan.');
|
|
25
31
|
/** A money figure as read: the amount, or why there is none. */
|
|
26
32
|
function Figure({ read }) {
|
|
27
|
-
|
|
33
|
+
const { t } = useI18n();
|
|
34
|
+
return (_jsx(SizableText, { size: "$2", color: read.error ? '$soft' : '$ink', numberOfLines: 1, maxW: 200, children: read.error ? t('Unavailable') : read.loading ? '…' : money(read.value) }));
|
|
28
35
|
}
|
|
29
36
|
/** A thin bar of a whole, red once a limit is used up. */
|
|
30
37
|
function Bar({ part, whole, limit }) {
|
|
@@ -35,15 +42,16 @@ function Meter({ title, used, of, says, first, limit = true }) {
|
|
|
35
42
|
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "baseline", justify: "space-between", flexWrap: "wrap", columnGap: "$3", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: says })] }), _jsx(Bar, { part: used, whole: of, limit: limit })] }));
|
|
36
43
|
}
|
|
37
44
|
/** `Resets Oct 1, 2026`, or '' for none. */
|
|
38
|
-
const resets = (iso) => (day(iso) ?
|
|
39
|
-
export function Credits({ target
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
45
|
+
const resets = (iso, t) => (day(iso) ? t('Resets {date}', { date: day(iso) }) : '');
|
|
46
|
+
export function Credits({ target, admin, add, onCard }) {
|
|
47
|
+
const { t } = useI18n();
|
|
48
|
+
const left = useRead(() => balance(target), 0, [target]);
|
|
49
|
+
const granted = useRead(() => credit(target), 0, [target]);
|
|
50
|
+
const rows = useRead(() => grants(target), [], [target]);
|
|
51
|
+
const spent = useRead(() => spend(target), null, [target]);
|
|
52
|
+
const rule = useRead(() => recharge(target), null, [target]);
|
|
53
|
+
const ceilings = useRead(() => caps(target), [], [target]);
|
|
54
|
+
const cards = useRead(add ? null : () => methods(target), [], [target, add]);
|
|
47
55
|
const [buying, setBuying] = useState(false);
|
|
48
56
|
const [amount, setAmount] = useState('25');
|
|
49
57
|
const [ceiling, setCeiling] = useState('');
|
|
@@ -67,7 +75,7 @@ export function Credits({ target: t, admin, add, onCard }) {
|
|
|
67
75
|
setNote(done);
|
|
68
76
|
}
|
|
69
77
|
catch (e) {
|
|
70
|
-
setNote(e
|
|
78
|
+
setNote(told(e, t));
|
|
71
79
|
}
|
|
72
80
|
finally {
|
|
73
81
|
setWorking(false);
|
|
@@ -82,8 +90,8 @@ export function Credits({ target: t, admin, add, onCard }) {
|
|
|
82
90
|
const live = rows.value.filter((g) => g.active && g.remaining > 0);
|
|
83
91
|
const buy = () => {
|
|
84
92
|
if (!adding)
|
|
85
|
-
return setNote('Enter an amount in dollars');
|
|
86
|
-
void act(() => topup(
|
|
93
|
+
return setNote(t('Enter an amount in dollars'));
|
|
94
|
+
void act(() => topup(target, adding, card.id), t('{amount} is added to the balance', { amount: money(adding) }), () => {
|
|
87
95
|
setBuying(false);
|
|
88
96
|
left.reload();
|
|
89
97
|
});
|
|
@@ -91,8 +99,8 @@ export function Credits({ target: t, admin, add, onCard }) {
|
|
|
91
99
|
const setLimit = () => {
|
|
92
100
|
const c = cents(ceiling);
|
|
93
101
|
if (!c)
|
|
94
|
-
return setNote('Enter a monthly limit in dollars');
|
|
95
|
-
void act(() => limit(
|
|
102
|
+
return setNote(t('Enter a monthly limit in dollars'));
|
|
103
|
+
void act(() => limit(target, ceilings.value, c), t('Spend stops at {amount} a month', { amount: money(c) }), () => {
|
|
96
104
|
setCeiling('');
|
|
97
105
|
ceilings.reload();
|
|
98
106
|
});
|
|
@@ -101,10 +109,15 @@ export function Credits({ target: t, admin, add, onCard }) {
|
|
|
101
109
|
const threshold = below.trim() === '' || below.trim() === '0' ? 0 : cents(below);
|
|
102
110
|
const each = cents(reload);
|
|
103
111
|
if (enabled && (threshold === null || !each))
|
|
104
|
-
return setNote('Enter the balance to reload below and the amount to add, in dollars');
|
|
105
|
-
void act(() => arm(
|
|
112
|
+
return setNote(t('Enter the balance to reload below and the amount to add, in dollars'));
|
|
113
|
+
void act(() => arm(target, { enabled, threshold: threshold ?? 0, amount: each ?? rule.value?.amount ?? 0 }), enabled ? t('Adds {amount} whenever the balance falls below {floor}', { amount: money(each ?? 0), floor: money(threshold ?? 0) }) : t('Auto-reload is off'), rule.reload);
|
|
106
114
|
};
|
|
107
|
-
return (_jsxs(YStack, { gap: "$6", "data-slot": "credits", children: [_jsxs(Group, { title:
|
|
115
|
+
return (_jsxs(YStack, { gap: "$6", "data-slot": "credits", children: [_jsxs(Group, { title: t('Credits'), detail: t(SEPARATE), action: add ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => window.open(add, '_blank', 'noopener'), children: t('Add funds') })) : buying || !admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: t('Buy more') })), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: t('Prepaid balance'), detail: t('Bought with a card'), trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: t('Granted credit'), detail: t('Given by Hanzo; some models take only prepaid'), trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: t('Spent this month'), detail: s && !s.known ? t('The ledger did not answer, so this is not a measurement') : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": t('Amount in dollars'), placeholder: "25", inputMode: "decimal" }) }), _jsx(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: t('Charged to {card}', { card: label(card) }) })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? t('Add {amount}', { amount: money(adding) }) : t('Add funds') })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? t('Reading cards…') : t('Add a card to buy more.') }), onCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onCard, children: t('Add card') })) : null] })) })) : null] }), live.length ? (_jsx(Card, { children: live.map((g, i) => (_jsx(Row, { first: i === 0, title: g.name || t('Credit grant'), detail: [
|
|
116
|
+
t('{left} left of {amount}', { left: money(g.remaining, g.currency), amount: money(g.amount, g.currency) }),
|
|
117
|
+
g.expires ? t('expires {date}', { date: day(g.expires) }) : '',
|
|
118
|
+
]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join(' · ') }, g.id))) })) : null] }), _jsx(Group, { title: t('Auto-reload'), detail: t('Tops the balance up from the card on file whenever it falls below a floor.'), children: rule.error ? (_jsx(Soft, { children: told(rule.error, t) })) : !rule.value ? (_jsx(Soft, { children: t('Reading auto-reload…') })) : (_jsxs(Card, { children: [_jsx(Row, { first: true, title: rule.value.enabled ? t('Adds {amount} below {floor}', { amount: money(rule.value.amount), floor: money(rule.value.threshold) }) : t('Off'), detail: rule.value.last ? t('Last reloaded {date}', { date: day(rule.value.last) }) : admin ? undefined : t('An org admin sets auto-reload.'), trailing: admin ? (_jsx(Switch, { "aria-label": t('Auto-reload'), checked: rule.value.enabled, disabled: working, onCheckedChange: (on) => saveRule(on) })) : undefined }), admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 150, children: _jsx(Input, { value: below, onChangeText: setBelow, "aria-label": t('Reload below, in dollars'), placeholder: t('Below {amount}', { amount: '$5' }), inputMode: "decimal" }) }), _jsx(YStack, { width: 150, children: _jsx(Input, { value: reload, onChangeText: setReload, "aria-label": t('Amount to add, in dollars'), placeholder: t('Add {amount}', { amount: '$25' }), inputMode: "decimal" }) }), _jsx(XStack, { flex: 1, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => saveRule(true), children: rule.value.enabled ? t('Save') : t('Turn on') }) })] })) : null] })) }), _jsx(Group, { title: t('Spend this month'), detail: t('What credits paid for, by kind.'), children: spent.error ? (_jsx(Soft, { children: told(spent.error, t) })) : !s ? (_jsx(Soft, { children: t('Reading spend…') })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: t('Nothing spent this month.') }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: t('Monthly limit'), detail: t('Spend from credits past it is refused until the month starts over.'), children: [ceilings.error ? (_jsx(Soft, { children: told(ceilings.error, t) })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: t('{amount} a month', { amount: money(cap.threshold) }), used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? t('Spend unknown') : t('{amount} spent', { amount: money(cap.spent) }), resets(cap.resets, t)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: t('No monthly limit'), detail: t('Spend is bounded by the balance alone.') })), others.map((c) => (_jsx(Row, { title: c.title || t('Limit'), detail: [c.project && t('project {name}', { name: c.project }), c.service && t('service {name}', { name: c.service }), c.enforce ? t('refuses spend past it') : t('warns only')]
|
|
108
121
|
.filter(Boolean)
|
|
109
|
-
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label":
|
|
122
|
+
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": t('Monthly limit in dollars'), placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(target, cap.id), t('The monthly limit is lifted'), ceilings.reload), children: t('Remove') })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? t('Change limit') : t('Set limit') })] })] })) : null] })), !admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin sets the monthly limit.') })) : null] }), _jsx(Note, { children: note })] }));
|
|
110
123
|
}
|
|
@@ -5,49 +5,56 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* One opens in the same editor a run shows beside it. A member reads; an org
|
|
6
6
|
* admin saves, sets secrets and forgets an environment, which is the platform's
|
|
7
7
|
* rule and not a choice made here.
|
|
8
|
+
*
|
|
9
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a codebase's name is
|
|
10
|
+
* its own, drawn as given.
|
|
8
11
|
*/
|
|
9
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
13
|
import { ChevronLeft, ChevronRight } from '@hanzogui/lucide-icons-2';
|
|
14
|
+
import { phrase } from '@hanzo/i18n';
|
|
15
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
11
16
|
import { Button } from '@hanzo/ui';
|
|
12
17
|
import { useState } from 'react';
|
|
18
|
+
import { told } from '../api/call.js';
|
|
13
19
|
import { environments, forget } from '../api/environment.js';
|
|
14
20
|
import { Confirm } from '../ask.js';
|
|
15
21
|
import { useRead } from '../data.js';
|
|
16
22
|
import { Environment } from '../environment.js';
|
|
17
23
|
import { useHost, useTarget } from '../host.js';
|
|
18
24
|
import { Card, Heading, Note, Row, Soft } from './ui.js';
|
|
19
|
-
const STATE = { none: 'Not set up', proposed: 'Proposed, waiting for review', ready: 'Ready' };
|
|
20
|
-
/** The line under a codebase: where it stands, its secrets, when it was saved. */
|
|
21
|
-
function about(e) {
|
|
22
|
-
const parts = [STATE[e.state]];
|
|
25
|
+
const STATE = { none: phrase('Not set up'), proposed: phrase('Proposed, waiting for review'), ready: phrase('Ready') };
|
|
26
|
+
/** The line under a codebase: where it stands, its secrets, when it was saved, each fact in the reader's words. */
|
|
27
|
+
function about(e, t) {
|
|
28
|
+
const parts = [t(STATE[e.state])];
|
|
23
29
|
if (e.secrets.length)
|
|
24
|
-
parts.push(e.secrets.length === 1 ? '1 secret' :
|
|
30
|
+
parts.push(e.secrets.length === 1 ? t('1 secret') : t('{n} secrets', { n: e.secrets.length }));
|
|
25
31
|
const when = e.updated ? new Date(e.updated) : null;
|
|
26
32
|
if (when && !Number.isNaN(when.getTime()))
|
|
27
|
-
parts.push(
|
|
33
|
+
parts.push(t('Updated {date}', { date: when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) }));
|
|
28
34
|
return parts.join(' · ');
|
|
29
35
|
}
|
|
30
36
|
export function Environments() {
|
|
37
|
+
const { t } = useI18n();
|
|
31
38
|
const host = useHost();
|
|
32
|
-
const
|
|
39
|
+
const target = useTarget();
|
|
33
40
|
const signed = Boolean(host.person);
|
|
34
|
-
const list = useRead(signed ? () => environments(
|
|
41
|
+
const list = useRead(signed ? () => environments(target) : null, [], [target, signed]);
|
|
35
42
|
const [open, setOpen] = useState('');
|
|
36
43
|
const [forgetting, setForgetting] = useState(false);
|
|
37
44
|
const [note, setNote] = useState('');
|
|
38
45
|
if (!signed) {
|
|
39
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
46
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Environments') }), _jsx(Soft, { children: t('Sign in to see your organization’s environments.') })] }));
|
|
40
47
|
}
|
|
41
48
|
if (open) {
|
|
42
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setOpen(''), children: [_jsx(ChevronLeft, { size: 14 }), " All environments
|
|
43
|
-
await forget(
|
|
44
|
-
setNote(
|
|
49
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setOpen(''), children: [_jsx(ChevronLeft, { size: 14, 'data-i18n-mirror': '' }), " ", t('All environments')] }), _jsx(XStack, { flex: 1 }), host.admin ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setForgetting(true), children: t('Forget') })) : null] }), _jsx(Environment, { repo: open, busy: false }), _jsx(Confirm, { open: forgetting, onOpenChange: setForgetting, title: t('Forget {repo}’s environment?', { repo: open }), says: t('Later runs on this codebase start bare: its scripts are dropped and every secret it holds is removed from KMS. This cannot be undone.'), act: t('Forget'), run: async () => {
|
|
50
|
+
await forget(target, open);
|
|
51
|
+
setNote(t('{repo}’s environment is forgotten', { repo: open }));
|
|
45
52
|
setOpen('');
|
|
46
53
|
list.reload();
|
|
47
54
|
} })] }));
|
|
48
55
|
}
|
|
49
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
56
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Environments'), detail: t('What a sandbox run does to a codebase before its agent starts: the install script, the start command, and the secrets it exports.') }), list.error ? (_jsx(Soft, { children: told(list.error, t) })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: t('Reading environments…') })) : list.value.length === 0 ? (_jsx(Soft, { children: t('No codebase has an environment yet. Set one up from New, or from a run’s Environment tab.') })) : (_jsx(Card, { children: list.value.map((e, i) => (_jsx(YStack, { render: "button", "aria-label": t('Open {repo}', { repo: e.repo }), onPress: () => {
|
|
50
57
|
setNote('');
|
|
51
58
|
setOpen(e.repo);
|
|
52
|
-
}, cursor: "pointer", hoverStyle: { bg: '$hover' },
|
|
59
|
+
}, cursor: "pointer", hoverStyle: { bg: '$hover' }, "$platform-web": { textAlign: 'start' }, children: _jsx(Row, { first: i === 0, title: e.repo, detail: about(e, t), trailing: _jsx(ChevronRight, { size: 16, opacity: 0.6, 'data-i18n-mirror': '' }) }) }, e.repo))) })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin saves an environment and sets its secrets.') })) : null, _jsx(Note, { children: note })] }));
|
|
53
60
|
}
|
package/lib/settings/general.js
CHANGED
|
@@ -3,43 +3,55 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* General: how the builder looks and listens — the theme, the text size, the
|
|
4
4
|
* motion, and the language dictation hears. Each choice is saved to the
|
|
5
5
|
* person's own settings as it is made, so it follows them to every device.
|
|
6
|
+
*
|
|
7
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a language is named
|
|
8
|
+
* in its own words, as given.
|
|
6
9
|
*/
|
|
7
10
|
import { YStack } from '@hanzo/gui';
|
|
11
|
+
import { phrase } from '@hanzo/i18n';
|
|
12
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
8
13
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
9
14
|
import { useState } from 'react';
|
|
15
|
+
import { told } from '../api/call.js';
|
|
10
16
|
import { useHost } from '../host.js';
|
|
11
17
|
import { usePrefs } from '../prefs.js';
|
|
12
18
|
import { LANGUAGES, spoken } from '../voice.js';
|
|
13
19
|
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
20
|
+
/** The choices, each word English and marked (`phrase`): drawn through `t`. */
|
|
14
21
|
const THEMES = [
|
|
15
|
-
{ id: 'system', label: 'System' },
|
|
16
|
-
{ id: 'light', label: 'Light' },
|
|
17
|
-
{ id: 'dark', label: 'Dark' },
|
|
22
|
+
{ id: 'system', label: phrase('System') },
|
|
23
|
+
{ id: 'light', label: phrase('Light') },
|
|
24
|
+
{ id: 'dark', label: phrase('Dark') },
|
|
18
25
|
];
|
|
19
26
|
const TEXTS = [
|
|
20
|
-
{ id: 'small', label: 'Small' },
|
|
21
|
-
{ id: 'medium', label: 'Medium' },
|
|
22
|
-
{ id: 'large', label: 'Large' },
|
|
27
|
+
{ id: 'small', label: phrase('Small') },
|
|
28
|
+
{ id: 'medium', label: phrase('Medium') },
|
|
29
|
+
{ id: 'large', label: phrase('Large') },
|
|
23
30
|
];
|
|
24
31
|
const MOTIONS = [
|
|
25
|
-
{ id: 'system', label: 'System', hint: 'As this device is set' },
|
|
26
|
-
{ id: 'reduced', label: 'Reduced', hint: 'No animation anywhere' },
|
|
32
|
+
{ id: 'system', label: phrase('System'), hint: phrase('As this device is set') },
|
|
33
|
+
{ id: 'reduced', label: phrase('Reduced'), hint: phrase('No animation anywhere') },
|
|
27
34
|
];
|
|
28
35
|
export function General() {
|
|
36
|
+
const { t } = useI18n();
|
|
29
37
|
const host = useHost();
|
|
30
38
|
const { prefs, save } = usePrefs();
|
|
31
39
|
const [note, setNote] = useState('');
|
|
32
40
|
if (!host.person) {
|
|
33
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
41
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('General') }), _jsx(Soft, { children: t('Sign in to see your settings.') })] }));
|
|
34
42
|
}
|
|
35
43
|
const keep = (patch) => {
|
|
36
44
|
setNote('');
|
|
37
|
-
save(patch).catch((e) => setNote(e
|
|
45
|
+
save(patch).catch((e) => setNote(told(e, t)));
|
|
38
46
|
};
|
|
47
|
+
// The choices in the reader's words, kept by id.
|
|
48
|
+
const themes = THEMES.map((x) => ({ ...x, label: t(x.label) }));
|
|
49
|
+
const texts = TEXTS.map((x) => ({ ...x, label: t(x.label) }));
|
|
50
|
+
const motions = MOTIONS.map((x) => ({ ...x, label: t(x.label), hint: t(x.hint) }));
|
|
39
51
|
// Each choice is one of its list: a saved one was read against it, and a host's theme is one of THEMES.
|
|
40
|
-
const theme =
|
|
41
|
-
const text =
|
|
42
|
-
const motion =
|
|
52
|
+
const theme = themes.find((x) => x.id === (prefs.theme ?? host.theme ?? 'dark'));
|
|
53
|
+
const text = texts.find((x) => x.id === (prefs.text ?? 'medium'));
|
|
54
|
+
const motion = motions.find((x) => x.id === (prefs.motion ?? 'system'));
|
|
43
55
|
const language = LANGUAGES.find((l) => l.id === prefs.language) ?? LANGUAGES.find((l) => l.id === spoken());
|
|
44
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
56
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('General'), detail: t('How Hanzo looks and listens. Saved to your account, so it follows you.') }), _jsx(Group, { title: t('Appearance'), children: _jsxs(Card, { children: [host.chooseTheme ? (_jsx(Row, { first: true, title: t('Theme'), detail: t('System follows this device’s light or dark setting.'), trailing: _jsx(ChipSelect, { name: t('Theme'), label: theme.label, chosen: theme, items: themes, onChange: (x) => keep({ theme: x.id }), placement: "bottom-end", width: 200 }) })) : null, _jsx(Row, { first: !host.chooseTheme, title: t('Text size'), detail: t('The transcript, the composer and every screen.'), trailing: _jsx(ChipSelect, { name: t('Text size'), label: text.label, chosen: text, items: texts, onChange: (x) => keep({ text: x.id === 'medium' ? null : x.id }), placement: "bottom-end", width: 200 }) }), _jsx(Row, { title: t('Motion'), detail: t('Reduced stops animations and transitions on this page.'), trailing: _jsx(ChipSelect, { name: t('Motion'), label: motion.label, chosen: motion, items: motions, onChange: (x) => keep({ motion: x.id === 'system' ? null : x.id }), placement: "bottom-end", width: 220 }) })] }) }), _jsx(Group, { title: t('Voice'), children: _jsx(Card, { children: _jsx(Row, { first: true, title: t('Dictation language'), detail: t('What the microphone in the composer listens for.'), trailing: _jsx(ChipSelect, { name: t('Dictation language'), label: language.label, chosen: language, items: LANGUAGES, onChange: (l) => keep({ language: l.id }), placeholder: t('Search languages…'), placement: "bottom-end", width: 220 }) }) }) }), _jsx(Note, { children: note })] }));
|
|
45
57
|
}
|
package/lib/settings/index.js
CHANGED
|
@@ -4,19 +4,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* workspace's environments, machines and keys, what the agent brings to a run,
|
|
5
5
|
* and the organization's integrations, members, usage and notifications. A
|
|
6
6
|
* section is its own address, so it can be linked to and comes back on reload.
|
|
7
|
+
* Its words are the reader's language (@hanzo/i18n `t`).
|
|
7
8
|
*/
|
|
8
9
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
9
11
|
import { useHost } from '../host.js';
|
|
10
12
|
import { path } from '../route.js';
|
|
11
13
|
import { heading } from './list.js';
|
|
12
14
|
import { ENTRIES, GROUPS } from './sections.js';
|
|
13
15
|
export function Settings({ section }) {
|
|
16
|
+
const { t } = useI18n();
|
|
14
17
|
const host = useHost();
|
|
15
18
|
// Every section the route grammar names has an entry.
|
|
16
19
|
const entry = ENTRIES.find((e) => e.id === section);
|
|
17
20
|
const go = (id) => host.go(path({ kind: 'settings', section: id }));
|
|
18
|
-
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [host.sections ? null : (_jsxs(YStack, { role: "navigation", "aria-label":
|
|
21
|
+
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, children: [host.sections ? null : (_jsxs(YStack, { role: "navigation", "aria-label": t('Settings'), width: 220, shrink: 0, borderInlineEndWidth: 1, borderColor: "$borderColor", px: "$2", py: "$4", gap: "$4", overflow: "scroll", "$max-md": { display: 'none' }, children: [_jsx(SizableText, { size: "$4", color: "$ink", px: "$2", children: t('Settings') }), GROUPS.map((g) => {
|
|
19
22
|
const list = ENTRIES.filter((e) => e.group === g);
|
|
20
|
-
return (_jsxs(YStack, { gap: "$0.5", children: [heading(g) ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g })) : null, list.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id)))] }, g));
|
|
21
|
-
})] })), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": e.label, onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: e.label }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
|
|
23
|
+
return (_jsxs(YStack, { gap: "$0.5", children: [heading(g) ? (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: t(heading(g)) })) : null, list.map((e) => (_jsx(XStack, { render: "button", "aria-label": t(e.label), "aria-current": e.id === entry.id ? 'page' : undefined, onPress: () => go(e.id), px: "$2", py: "$1.5", rounded: "$2", bg: e.id === entry.id ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: e.id === entry.id ? '$ink' : '$soft', children: t(e.label) }) }, e.id)))] }, g));
|
|
24
|
+
})] })), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, overflow: "scroll", children: [_jsx(XStack, { flexWrap: "wrap", gap: "$1.5", px: "$4", pt: "$3", $md: { display: 'none' }, children: ENTRIES.map((e) => (_jsx(XStack, { render: "button", "aria-label": t(e.label), onPress: () => go(e.id), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: e.id === entry.id ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: e.id === entry.id ? '$ink' : '$soft', children: t(e.label) }) }, e.id))) }), _jsx(YStack, { width: "100%", maxW: 760, self: "center", px: "$5", py: "$6", children: entry.body() })] })] }));
|
|
22
25
|
}
|
|
@@ -9,11 +9,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* (../back.ts), which this page finishes and reports. Anyone connects their own
|
|
10
10
|
* GitHub; installing the App and connecting Slack are an org admin's, which is
|
|
11
11
|
* the platform's rule.
|
|
12
|
+
*
|
|
13
|
+
* Its words are the reader's language (@hanzo/i18n `t`); an account's,
|
|
14
|
+
* a channel's and a provider's names, and the platform's own notes, are drawn
|
|
15
|
+
* as given.
|
|
12
16
|
*/
|
|
13
17
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
18
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
14
19
|
import { Github, Hash, Lock, Megaphone, Slack } from '@hanzogui/lucide-icons-2';
|
|
15
20
|
import { Button } from '@hanzo/ui';
|
|
16
21
|
import { useState } from 'react';
|
|
22
|
+
import { told } from '../api/call.js';
|
|
17
23
|
import { connect, connection, disconnect as unlinkGithub, installations } from '../api/github.js';
|
|
18
24
|
import { authorize, channels, disconnect, read, SOCIAL } from '../api/provider.js';
|
|
19
25
|
import { here, useBack } from '../back.js';
|
|
@@ -21,25 +27,26 @@ import { useRead } from '../data.js';
|
|
|
21
27
|
import { useHost, useTarget } from '../host.js';
|
|
22
28
|
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
23
29
|
export function Integrations() {
|
|
30
|
+
const { t } = useI18n();
|
|
24
31
|
const host = useHost();
|
|
25
|
-
const
|
|
32
|
+
const target = useTarget();
|
|
26
33
|
const signed = Boolean(host.person);
|
|
27
|
-
const mine = useRead(signed ? () => connection(
|
|
28
|
-
const installed = useRead(signed ? () => installations(
|
|
29
|
-
const slack = useRead(signed ? () => read(
|
|
34
|
+
const mine = useRead(signed ? () => connection(target) : null, null, [target, signed]);
|
|
35
|
+
const installed = useRead(signed ? () => installations(target) : null, [], [target, signed]);
|
|
36
|
+
const slack = useRead(signed ? () => read(target, 'slack') : null, null, [target, signed]);
|
|
30
37
|
const joined = slack.value?.connected ?? false;
|
|
31
|
-
const rooms = useRead(signed && joined ? () => channels(
|
|
38
|
+
const rooms = useRead(signed && joined ? () => channels(target) : null, { channels: [], next: '' }, [target, signed, joined]);
|
|
32
39
|
const social = useSocial(signed, SOCIAL.map((p) => p.id));
|
|
33
40
|
const [working, setWorking] = useState(false);
|
|
34
41
|
const [note, setNote] = useState('');
|
|
35
|
-
useBack(
|
|
42
|
+
useBack(target, signed, setNote, () => {
|
|
36
43
|
mine.reload();
|
|
37
44
|
installed.reload();
|
|
38
45
|
slack.reload();
|
|
39
46
|
social.reload();
|
|
40
47
|
});
|
|
41
48
|
if (!signed) {
|
|
42
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
49
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Integrations') }), _jsx(Soft, { children: t('Sign in to see this organization’s integrations.') })] }));
|
|
43
50
|
}
|
|
44
51
|
const act = async (what, done, after) => {
|
|
45
52
|
setWorking(true);
|
|
@@ -50,7 +57,7 @@ export function Integrations() {
|
|
|
50
57
|
setNote(done);
|
|
51
58
|
}
|
|
52
59
|
catch (e) {
|
|
53
|
-
setNote(e
|
|
60
|
+
setNote(told(e, t));
|
|
54
61
|
}
|
|
55
62
|
finally {
|
|
56
63
|
setWorking(false);
|
|
@@ -64,42 +71,49 @@ export function Integrations() {
|
|
|
64
71
|
host.open(await where());
|
|
65
72
|
}
|
|
66
73
|
catch (e) {
|
|
67
|
-
setNote(e
|
|
74
|
+
setNote(told(e, t));
|
|
68
75
|
setWorking(false);
|
|
69
76
|
}
|
|
70
77
|
};
|
|
71
78
|
const me = mine.value;
|
|
72
79
|
const s = slack.value;
|
|
73
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
74
|
-
? mine.error
|
|
80
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Integrations'), detail: host.org ? t('What {org} and you have connected.', { org: host.org }) : t('What this organization and you have connected.') }), _jsxs(Group, { title: "GitHub", detail: t('Your connection brings your repositories; the App brings an account’s to the organization.'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Github, { size: 16 }), title: t('Your GitHub'), detail: mine.error
|
|
81
|
+
? told(mine.error, t)
|
|
75
82
|
: !me
|
|
76
|
-
? 'Reading…'
|
|
83
|
+
? t('Reading…')
|
|
77
84
|
: !me.configured
|
|
78
|
-
? 'This deployment cannot connect GitHub yet.'
|
|
85
|
+
? t('This deployment cannot connect GitHub yet.')
|
|
79
86
|
: me.connected
|
|
80
|
-
?
|
|
81
|
-
: 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(
|
|
82
|
-
? slack.error
|
|
87
|
+
? t('Connected as @{login}', { login: me.login })
|
|
88
|
+
: t('Not connected'), trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(target), t('Your GitHub is disconnected'), mine.reload), children: t('Disconnect') })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(target, here())), children: t('Connect') }))) : null }), installed.error ? (_jsx(Row, { title: t('App installations'), detail: told(installed.error, t) })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? t('every repository') : i.grant === 'selected' ? t('selected repositories') : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? t('Installed') : t('Removed on GitHub') }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: t('No GitHub accounts installed'), detail: t('Install the App on a GitHub account to bring its repositories here.') })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(target, 'github', here())), children: t('Install on a GitHub account') }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin installs the App on a GitHub account.') }))] }), _jsxs(Group, { title: "Slack", detail: t('Talk to Hanzo from your workspace’s channels.'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
|
|
89
|
+
? told(slack.error, t)
|
|
83
90
|
: !s
|
|
84
|
-
? 'Reading…'
|
|
91
|
+
? t('Reading…')
|
|
85
92
|
: s.connected
|
|
86
|
-
?
|
|
93
|
+
? s.since
|
|
94
|
+
? t('Connected since {day}', { day: day(s.since) })
|
|
95
|
+
: t('Connected')
|
|
87
96
|
: s.available
|
|
88
|
-
? 'Not connected'
|
|
89
|
-
: s.note || 'This deployment cannot connect Slack yet.', trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(
|
|
90
|
-
? rooms.value.channels.map((c) => (_jsx(Row, { leading: c.private ? _jsx(Lock, { size: 14 }) : _jsx(Hash, { size: 14 }), title: c.name || c.id, trailing: _jsx(SizableText, { size: "$1", color: c.member ? '$ink' : '$soft', children: c.member ? 'Joined' : 'Not joined' }) }, c.id)))
|
|
91
|
-
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children:
|
|
97
|
+
? t('Not connected')
|
|
98
|
+
: s.note || t('This deployment cannot connect Slack yet.'), trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(target, 'slack'), t('{name} is disconnected', { name: 'Slack' }), slack.reload), children: t('Disconnect') })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(target, 'slack', here())), children: t('Connect') }))) : null }), joined && rooms.error ? _jsx(Row, { title: t('Channels'), detail: told(rooms.error, t) }) : null, joined
|
|
99
|
+
? rooms.value.channels.map((c) => (_jsx(Row, { leading: c.private ? _jsx(Lock, { size: 14 }) : _jsx(Hash, { size: 14 }), title: c.name || c.id, trailing: _jsx(SizableText, { size: "$1", color: c.member ? '$ink' : '$soft', children: c.member ? t('Joined') : t('Not joined') }) }, c.id)))
|
|
100
|
+
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin connects the Slack workspace.') })) : null] }), _jsxs(Group, { title: t('Social accounts'), detail: t('Where automations post what their agent writes.'), children: [_jsx(Card, { children: SOCIAL.map((p, i) => {
|
|
92
101
|
const c = social.value[p.id];
|
|
93
102
|
return (_jsx(Row, { first: i === 0, leading: _jsx(Megaphone, { size: 16 }), title: c?.connected && c.account ? `${p.name} · ${c.account}` : p.name, detail: social.error
|
|
94
|
-
? social.error
|
|
103
|
+
? told(social.error, t)
|
|
95
104
|
: !c
|
|
96
|
-
? 'Reading…'
|
|
105
|
+
? t('Reading…')
|
|
97
106
|
: c.connected
|
|
98
|
-
?
|
|
107
|
+
? // Connected, then what the account takes, as two sentences.
|
|
108
|
+
p.takes
|
|
109
|
+
? `${c.since ? t('Connected since {day}.', { day: day(c.since) }) : t('Connected.')} ${t(p.takes)}`
|
|
110
|
+
: c.since
|
|
111
|
+
? t('Connected since {day}', { day: day(c.since) })
|
|
112
|
+
: t('Connected')
|
|
99
113
|
: c.available
|
|
100
|
-
? p.takes
|
|
101
|
-
: p.takes ? `${p.takes} ${p.needs}` : p.needs, trailing: host.admin && c && (c.connected || c.available) ? (c.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(
|
|
102
|
-
}) }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children:
|
|
114
|
+
? p.takes ? t(p.takes) : t('Not connected')
|
|
115
|
+
: p.takes ? `${t(p.takes)} ${t(p.needs)}` : t(p.needs), trailing: host.admin && c && (c.connected || c.available) ? (c.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(target, p.id), t('{name} is disconnected', { name: p.name }), social.reload), children: t('Disconnect') })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(target, p.id, here())), "aria-label": t('Connect {name}', { name: p.name }), children: t('Connect') }))) : null }, p.id));
|
|
116
|
+
}) }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin connects the organization’s social accounts.') })) : null] }), _jsx(Note, { children: note })] }));
|
|
103
117
|
}
|
|
104
118
|
/**
|
|
105
119
|
* The named social connectors, by id: whether each can be connected here and
|