@hanzo/build 0.2.33 → 0.2.35

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 CHANGED
@@ -79,6 +79,12 @@ import { Builder, DevSection } from '@hanzo/build'
79
79
  <Builder host={host} rail={false} />
80
80
  ```
81
81
 
82
+ A run draws its own panes — the transcript, the side pane a gutter beside it
83
+ with the resize edge in that gutter, and each composer — from @hanzo/design's
84
+ pane tokens: `--pane-fill`, `--pane-round`, `--pane-edge`, `--pane-blur`,
85
+ `--pane-gap`, and `--shadow-sheet-2` for the drop. A host retunes the tokens;
86
+ it writes no rule against the builder's slots.
87
+
82
88
  `DevSection` is rows of `@hanzo/ui/chat`'s Sidebar; `useSessions(host)` is its
83
89
  data — the org's coding runs, kept live by the org's feed — for a host that
84
90
  draws the rows itself. `administers(token, org)` reads the org-admin bit off a
@@ -260,6 +266,7 @@ src/
260
266
  transcript.tsx a run's transcript, as cards
261
267
  prose.tsx markdown, drawn as text (markdown.ts reads it)
262
268
  desk.tsx the pane beside a run
269
+ pane.ts how a pane is cut, from the --pane-* tokens
263
270
  door.tsx a run's desktop or shell, framed
264
271
  git.tsx what a run pushed
265
272
  files.tsx a run's files and artifacts
@@ -4,7 +4,8 @@
4
4
  * GET /v1/provider/github/repos?q=&owner=&limit=&after= most recently pushed first
5
5
  * GET /v1/provider/github/repos/{owner}/{repo}/branches?q=&limit=&after= default first
6
6
  * GET /v1/provider/github/user the person's own connection
7
- * POST /v1/provider/github/user/connect → {authorizeUrl}
7
+ * POST /v1/provider/github/user/connect {return} → {authorizeUrl}
8
+ * POST /v1/provider/github/user/complete {grant} → the connection
8
9
  * POST /v1/provider/github/user/disconnect
9
10
  * GET /v1/provider/github/installations the accounts this org has bound the App on
10
11
  *
@@ -14,8 +15,9 @@
14
15
  *
15
16
  * `connected` false with repositories is an org admin seeing the org's
16
17
  * installations; false with none is the cue to connect. GitHub returns a
17
- * connecting person to the console's /connectors?complete=github&grant=<id>,
18
- * which completes it.
18
+ * connecting person to the page the connect named as its return (a Hanzo app
19
+ * page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
20
+ * and that page completes it (`complete`, and `landed` in ../back.ts).
19
21
  */
20
22
  import { type Target } from './call.ts';
21
23
  export interface Repo {
@@ -74,8 +76,14 @@ export interface BranchQuery {
74
76
  }
75
77
  export declare function branches(t: Target, owner: string, name: string, p?: BranchQuery, signal?: AbortSignal): Promise<Branches>;
76
78
  export declare function connection(t: Target): Promise<Connection>;
77
- /** Where to send the person to authorize the platform's GitHub App — github.com only. */
78
- export declare function connect(t: Target): Promise<string>;
79
+ /**
80
+ * Where to send the person to authorize the platform's GitHub App — github.com
81
+ * only. `back` is the page GitHub's return lands on; the platform keeps it only
82
+ * when it is a Hanzo app page.
83
+ */
84
+ export declare function connect(t: Target, back?: string): Promise<string>;
85
+ /** Finish the connect GitHub's return handed this page as `grant`. */
86
+ export declare function complete(t: Target, grant: string): Promise<Connection>;
79
87
  export declare function disconnect(t: Target): Promise<void>;
80
88
  /** A GitHub account the platform's App is installed on, as this org sees it. */
81
89
  export interface Installation {
package/lib/api/github.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * GET /v1/provider/github/repos?q=&owner=&limit=&after= most recently pushed first
5
5
  * GET /v1/provider/github/repos/{owner}/{repo}/branches?q=&limit=&after= default first
6
6
  * GET /v1/provider/github/user the person's own connection
7
- * POST /v1/provider/github/user/connect → {authorizeUrl}
7
+ * POST /v1/provider/github/user/connect {return} → {authorizeUrl}
8
+ * POST /v1/provider/github/user/complete {grant} → the connection
8
9
  * POST /v1/provider/github/user/disconnect
9
10
  * GET /v1/provider/github/installations the accounts this org has bound the App on
10
11
  *
@@ -14,8 +15,9 @@
14
15
  *
15
16
  * `connected` false with repositories is an org admin seeing the org's
16
17
  * installations; false with none is the cue to connect. GitHub returns a
17
- * connecting person to the console's /connectors?complete=github&grant=<id>,
18
- * which completes it.
18
+ * connecting person to the page the connect named as its return (a Hanzo app
19
+ * page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
20
+ * and that page completes it (`complete`, and `landed` in ../back.ts).
19
21
  */
20
22
  import { call, query, seg } from './call.js';
21
23
  const str = (v) => (typeof v === 'string' ? v : '');
@@ -57,18 +59,29 @@ export async function branches(t, owner, name, p = {}, signal) {
57
59
  total: num(raw.total),
58
60
  };
59
61
  }
62
+ function own(raw) {
63
+ const c = obj(raw);
64
+ return { configured: c.configured === true, connected: c.connected === true, login: str(c.login) };
65
+ }
60
66
  export async function connection(t) {
61
- const raw = obj(await call(t, 'GET', '/v1/provider/github/user'));
62
- return { configured: raw.configured === true, connected: raw.connected === true, login: str(raw.login) };
67
+ return own(await call(t, 'GET', '/v1/provider/github/user'));
63
68
  }
64
- /** Where to send the person to authorize the platform's GitHub App — github.com only. */
65
- export async function connect(t) {
66
- const raw = obj(await call(t, 'POST', '/v1/provider/github/user/connect'));
69
+ /**
70
+ * Where to send the person to authorize the platform's GitHub App — github.com
71
+ * only. `back` is the page GitHub's return lands on; the platform keeps it only
72
+ * when it is a Hanzo app page.
73
+ */
74
+ export async function connect(t, back) {
75
+ const raw = obj(await call(t, 'POST', '/v1/provider/github/user/connect', back ? { return: back } : undefined));
67
76
  const url = str(raw.authorizeUrl);
68
77
  if (!/^https:\/\/github\.com\//.test(url))
69
78
  throw new Error('The platform did not name a GitHub address to connect at');
70
79
  return url;
71
80
  }
81
+ /** Finish the connect GitHub's return handed this page as `grant`. */
82
+ export async function complete(t, grant) {
83
+ return own(await call(t, 'POST', '/v1/provider/github/user/complete', { grant }));
84
+ }
72
85
  export async function disconnect(t) {
73
86
  await call(t, 'POST', '/v1/provider/github/user/disconnect');
74
87
  }
@@ -3,7 +3,7 @@
3
3
  * platform's GitHub App is installed on.
4
4
  *
5
5
  * GET /v1/provider/{id} one connector and this org's connection to it
6
- * POST /v1/provider/{id}/connect {} → {authorizeUrl}: the provider's own consent page (org admin)
6
+ * POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
7
7
  * POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
8
8
  * GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
9
9
  *
@@ -25,8 +25,8 @@ export interface Connector {
25
25
  }
26
26
  export declare function connector(raw: unknown, id?: string): Connector;
27
27
  export declare function read(t: Target, id: string): Promise<Connector>;
28
- /** The provider's consent page for this org. */
29
- export declare function authorize(t: Target, id: string): Promise<string>;
28
+ /** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
29
+ export declare function authorize(t: Target, id: string, back?: string): Promise<string>;
30
30
  export declare function disconnect(t: Target, id: string): Promise<void>;
31
31
  export interface Channel {
32
32
  id: string;
@@ -3,7 +3,7 @@
3
3
  * platform's GitHub App is installed on.
4
4
  *
5
5
  * GET /v1/provider/{id} one connector and this org's connection to it
6
- * POST /v1/provider/{id}/connect {} → {authorizeUrl}: the provider's own consent page (org admin)
6
+ * POST /v1/provider/{id}/connect {return} → {authorizeUrl}: the provider's own consent page (org admin)
7
7
  * POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
8
8
  * GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
9
9
  *
@@ -35,9 +35,9 @@ const CONSENT = {
35
35
  slack: /^https:\/\/slack\.com\//,
36
36
  github: /^https:\/\/github\.com\//,
37
37
  };
38
- /** The provider's consent page for this org. */
39
- export async function authorize(t, id) {
40
- const url = str(obj(await call(t, 'POST', `/v1/provider/${seg(id)}/connect`, {})).authorizeUrl);
38
+ /** The provider's consent page for this org; its return lands on `back` when that is a Hanzo app page. */
39
+ export async function authorize(t, id, back) {
40
+ const url = str(obj(await call(t, 'POST', `/v1/provider/${seg(id)}/connect`, back ? { return: back } : {})).authorizeUrl);
41
41
  const host = CONSENT[id];
42
42
  if (!host || !host.test(url))
43
43
  throw new Error(`The platform did not name a ${id} address to connect at`);
package/lib/back.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Target } from './api/call.ts';
2
+ /** The query keys a return carries its answer in. */
3
+ export declare const ANSWER: readonly ['complete', 'grant', 'connected', 'account', 'error', 'reason'];
4
+ export type Landing = {
5
+ kind: 'grant';
6
+ provider: string;
7
+ grant: string;
8
+ } | {
9
+ kind: 'connected';
10
+ provider: string;
11
+ account: string;
12
+ } | {
13
+ kind: 'error';
14
+ provider: string;
15
+ reason: string;
16
+ };
17
+ /** The answer a return carried in `search`, or null when it carried none. */
18
+ export declare function landed(search: string): Landing | null;
19
+ /** `href` without the answer keys; `href` itself when it carries none. */
20
+ export declare function clean(href: string): string;
21
+ /** This page's address, as a connect's return. */
22
+ export declare function here(): string;
23
+ /** A provider id as people write it. */
24
+ export declare const named: (id: string) => string;
25
+ /**
26
+ * Finish what a return carried, once a person is signed in: `say` gets the
27
+ * sentence, and `after` runs once something was connected.
28
+ */
29
+ export declare function useBack(t: Target, signed: boolean, say: (note: string) => void, after: () => void): void;
package/lib/back.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Coming back from a provider's consent page.
3
+ *
4
+ * A connect names the page it starts on as its return (`here`). The platform
5
+ * keeps that address when it is a Hanzo app page, and its callback lands the
6
+ * person back on it with the answer in the query:
7
+ *
8
+ * ?complete=github&grant=<id> a GitHub connect to finish (POST /v1/provider/github/user/complete)
9
+ * ?connected=<provider>&account=<n> an organization's connector, connected
10
+ * ?error=<provider>&reason=<why> either, refused
11
+ *
12
+ * `useBack` reads that answer once, takes it off the address so a reload does
13
+ * not replay it, finishes a GitHub grant, and says what happened.
14
+ */
15
+ import { useEffect } from 'react';
16
+ import { complete } from './api/github.js';
17
+ /** The query keys a return carries its answer in. */
18
+ export const ANSWER = ['complete', 'grant', 'connected', 'account', 'error', 'reason'];
19
+ /** The answer a return carried in `search`, or null when it carried none. */
20
+ export function landed(search) {
21
+ const q = new URLSearchParams(search);
22
+ const error = q.get('error');
23
+ if (error)
24
+ return { kind: 'error', provider: error, reason: q.get('reason') ?? '' };
25
+ const grant = q.get('grant');
26
+ const done = q.get('complete');
27
+ if (done && grant)
28
+ return { kind: 'grant', provider: done, grant };
29
+ const connected = q.get('connected');
30
+ if (connected)
31
+ return { kind: 'connected', provider: connected, account: q.get('account') ?? '' };
32
+ return null;
33
+ }
34
+ /** `href` without the answer keys; `href` itself when it carries none. */
35
+ export function clean(href) {
36
+ const url = new URL(href);
37
+ if (!ANSWER.some((k) => url.searchParams.has(k)))
38
+ return href;
39
+ for (const k of ANSWER)
40
+ url.searchParams.delete(k);
41
+ return url.href;
42
+ }
43
+ /** This page's address, as a connect's return. */
44
+ export function here() {
45
+ return clean(window.location.href);
46
+ }
47
+ const NAMES = { github: 'GitHub', slack: 'Slack' };
48
+ /** A provider id as people write it. */
49
+ export const named = (id) => NAMES[id] ?? (id ? id[0].toUpperCase() + id.slice(1) : 'The connection');
50
+ /**
51
+ * Finish what a return carried, once a person is signed in: `say` gets the
52
+ * sentence, and `after` runs once something was connected.
53
+ */
54
+ export function useBack(t, signed, say, after) {
55
+ useEffect(() => {
56
+ if (!signed)
57
+ return;
58
+ const l = landed(window.location.search);
59
+ if (!l)
60
+ return;
61
+ // Off the address first: a grant completes once, and a reload must not try again.
62
+ const next = clean(window.location.href);
63
+ window.history.replaceState(window.history.state, '', next);
64
+ if (l.kind === 'error') {
65
+ say(`${named(l.provider)}: ${l.reason || 'not connected'}`);
66
+ return;
67
+ }
68
+ if (l.kind === 'connected') {
69
+ say(`${named(l.provider)} is connected${l.account ? ` as ${l.account}` : ''}`);
70
+ after();
71
+ return;
72
+ }
73
+ if (l.provider !== 'github')
74
+ return;
75
+ complete(t, l.grant).then((c) => {
76
+ say(c.connected ? `Connected as @${c.login}` : 'GitHub did not connect; connect again');
77
+ after();
78
+ }, (e) => say(e.message));
79
+ // Once per landing: the address is clean after the first run.
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, [signed]);
82
+ }
package/lib/desk.js CHANGED
@@ -39,7 +39,7 @@ export function Desk({ id, repo, branch, base, environment, mode, pr, title, pro
39
39
  const box = useBox(sandbox);
40
40
  // Nothing is said of the sandbox until it has been read.
41
41
  const held = box.value?.state ?? '';
42
- return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, borderLeftWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { px: "$2", py: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "browser", tab: tab, onPick: setTab, children: "Browser" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "artifacts", tab: tab, onPick: setTab, children: "Artifacts" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), sandbox ? _jsx(Held, { box: box, live: live }) : null, _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { session: id, sandbox: sandbox, repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'files' ? _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) : null] })] }));
42
+ return (_jsxs(YStack, { role: "complementary", "aria-label": "Run details", flex: 1, minW: 0, minH: 0, overflow: "hidden", style: { borderRadius: 'inherit' }, children: [_jsxs(XStack, { px: "$2", py: "$2", gap: "$1", borderBottomWidth: 1, borderColor: "$borderColor", items: "center", children: [_jsxs(XStack, { flex: 1, minW: 0, gap: "$1", overflow: "scroll", children: [_jsx(TabButton, { id: "browser", tab: tab, onPick: setTab, children: "Browser" }), _jsx(TabButton, { id: "desktop", tab: tab, onPick: setTab, children: "Desktop" }), _jsx(TabButton, { id: "terminal", tab: tab, onPick: setTab, children: "Terminal" }), _jsx(TabButton, { id: "artifacts", tab: tab, onPick: setTab, children: "Artifacts" }), _jsx(TabButton, { id: "files", tab: tab, onPick: setTab, children: "Files" }), _jsx(TabButton, { id: "git", tab: tab, onPick: setTab, children: "Git" }), _jsx(TabButton, { id: "environment", tab: tab, onPick: setTab, children: "Environment" })] }), _jsx(XStack, { render: "button", "aria-label": "Hide the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onHide, children: _jsx(PanelRightClose, { size: 16 }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": "Run actions", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [...menu, { key: 'details', label: 'Details', icon: _jsx(Info, { size: 16 }), onSelect: () => setTab('environment') }] })] }), sandbox ? _jsx(Held, { box: box, live: live }) : null, _jsxs(YStack, { flex: 1, minH: 0, overflow: bleed ? 'hidden' : 'scroll', px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: [tab === 'environment' ? (_jsxs(YStack, { gap: "$2", children: [_jsx(Environment, { repo: name, busy: live && mode === 'setup' }), _jsx(SizableText, { size: "$2", color: "$ink", pt: "$4", children: "This run" }), _jsx(Fact, { label: "Run", value: id }), _jsx(Fact, { label: "Repository", value: name || '—' }), _jsx(Fact, { label: "Branch", value: branch || '—' }), _jsx(Fact, { label: "Base", value: base || '—' }), _jsx(Fact, { label: "Runs on", value: environment || 'sandbox' }), _jsx(Fact, { label: "Sandbox", value: sandbox || '—' }), _jsx(Fact, { label: "Mode", value: mode || 'build' })] })) : null, tab === 'browser' ? _jsx(Browser, { sandbox: sandbox, held: held, site: site, live: live }, published) : null, tab === 'git' ? _jsx(Git, { session: id, title: title, live: live }) : null, tab === 'desktop' ? _jsx(Door, { which: "screen", sandbox: sandbox, held: held, session: id }) : null, tab === 'terminal' ? (_jsx(Terminal, { id: id, sandbox: sandbox, held: held, lines: lines, live: live, failed: failed, refused: refused, retry: retry, onRetry: onRetry })) : null, tab === 'artifacts' ? (_jsx(Artifacts, { session: id, sandbox: sandbox, repo: repo, branch: branch, mode: mode, project: project, site: site, pr: pr, onEnvironment: () => setTab('environment') })) : null, tab === 'files' ? _jsx(Files, { session: id, repo: repo, branch: branch, sandbox: sandbox, live: held === 'running' }) : null] })] }));
43
43
  }
44
44
  /**
45
45
  * The run's sandbox as it stands, and what moves it. It is read while the pane is
package/lib/foot.js CHANGED
@@ -15,7 +15,7 @@ import { path } from './route.js';
15
15
  export const DOCS = 'https://docs.hanzo.ai/docs/dev';
16
16
  /** Hanzo in Slack is set up under Settings; the card goes there, and once dismissed stays gone. */
17
17
  export function Slack({ onOpen, onDismiss }) {
18
- return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
18
+ return (_jsxs(XStack, { items: "center", gap: "$2.5", px: "$3", py: "$2.5", rounded: "var(--radius-lg)", borderWidth: 1, borderColor: "$borderColor", bg: "var(--glass)", children: [_jsx(Mark, { size: 16 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: "Try Hanzo in Slack" }), _jsx(XStack, { render: "button", "aria-label": "Set up Hanzo in Slack", onPress: onOpen, cursor: "pointer", children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Set up" }) })] }), _jsx(XStack, { render: "button", "aria-label": "Dismiss", onPress: onDismiss, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(X, { size: 14 }) })] }));
19
19
  }
20
20
  /**
21
21
  * The account menu's open state, for whatever row opens it.
package/lib/landing.js CHANGED
@@ -29,6 +29,7 @@ import { ready, SANDBOX } from './api/places.js';
29
29
  import { isForge } from './choice.js';
30
30
  import { useKept, usePlaces, useRead } from './data.js';
31
31
  import { useHost, useTarget } from './host.js';
32
+ import { pane } from './pane.js';
32
33
  import { usePrefs } from './prefs.js';
33
34
  import { Publish } from './publish.js';
34
35
  import { path } from './route.js';
@@ -211,7 +212,7 @@ export function Landing({ onStarted }) {
211
212
  const foot = (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", flexWrap: "wrap", rowGap: "$1", children: [_jsx(Attach, { files: files, onFiles: setFiles, onNote: setNote }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote }), _jsx(ModeSelect, { modes: MODES, value: kept.mode, onChange: (m) => set({ mode: m }), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), _jsx(XStack, { flex: 1 }), _jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === kept.model)?.label ?? 'Enso', chosen: catalog.value.find((m) => m.id === kept.model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => set({ model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: EFFORTS.find((e) => e.id === kept.effort)?.label ?? 'Medium', chosen: EFFORTS.find((e) => e.id === kept.effort) ?? null, items: EFFORTS, onChange: (e) => set({ effort: e.id }), placement: "top-end", width: 180 })] }));
212
213
  return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", px: "$4", pb: "$10", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: COLUMN, gap: "$5", children: [_jsxs(XStack, { role: "heading", "aria-level": 1, justify: "center", items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(HanzoMark, { size: 26 }), _jsx(SizableText, { size: "$8", color: "$ink", style: { textAlign: 'center' }, children: prefs.callName ? `What’s up next, ${prefs.callName}?` : 'What’s up next?' })] }), _jsxs(YStack, { gap: "$2", children: [env.value && env.value.state !== 'ready' && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
213
214
  ? `A proposed environment for ${codebase} is waiting for review.`
214
- : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
215
+ : `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(kept.mode, place.id) ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(kept.mode, place.id) })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: "Describe a task or ask a question", label: "Describe a task or ask a question", head: head, foot: foot })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), codebase ? (_jsx(SetupDialog, { repo: codebase, open: offering, onOpenChange: setOffering, onStart: setup, onSaved: () => {
215
216
  setOffering(false);
216
217
  env.reload();
217
218
  } })) : null] }));
package/lib/pane.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * How a pane is cut: a card floating on the host's ground, read from
3
+ * @hanzo/design's `--pane-*` tokens. Each falls back to the design value the
4
+ * token names, so a host that sets none still draws a pane.
5
+ *
6
+ * pane fill, corner, edge, lens and drop, as gui props
7
+ * gap the gutter between two panes, and the width of the edge in it
8
+ */
9
+ export declare const pane: {
10
+ readonly bg: 'var(--pane-fill, var(--sheet-1))';
11
+ readonly rounded: 'var(--pane-round, var(--radius-xl))';
12
+ readonly borderWidth: 1;
13
+ readonly borderColor: 'var(--pane-edge, var(--white-08))';
14
+ readonly backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))';
15
+ readonly boxShadow: 'var(--shadow-sheet-2)';
16
+ };
17
+ export declare const gap = "var(--pane-gap, var(--space-2))";
package/lib/pane.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * How a pane is cut: a card floating on the host's ground, read from
3
+ * @hanzo/design's `--pane-*` tokens. Each falls back to the design value the
4
+ * token names, so a host that sets none still draws a pane.
5
+ *
6
+ * pane fill, corner, edge, lens and drop, as gui props
7
+ * gap the gutter between two panes, and the width of the edge in it
8
+ */
9
+ export const pane = {
10
+ bg: 'var(--pane-fill, var(--sheet-1))',
11
+ rounded: 'var(--pane-round, var(--radius-xl))',
12
+ borderWidth: 1,
13
+ borderColor: 'var(--pane-edge, var(--white-08))',
14
+ backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))',
15
+ boxShadow: 'var(--shadow-sheet-2)',
16
+ };
17
+ export const gap = 'var(--pane-gap, var(--space-2))';
package/lib/project.js CHANGED
@@ -51,6 +51,7 @@ import { Grip } from './grip.js';
51
51
  import { useHost, useTarget } from './host.js';
52
52
  import { MODES } from './landing.js';
53
53
  import { Out } from './out.js';
54
+ import { pane as cut } from './pane.js';
54
55
  import { Publish } from './publish.js';
55
56
  import { Attach, compose, Dictate, Files } from './tools.js';
56
57
  const LIVE = new Set(['running', 'paused']);
@@ -319,7 +320,7 @@ export function Project({ slug }) {
319
320
  ? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
320
321
  : project
321
322
  ? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
322
- : `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
323
+ : `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { px: "$3", pb: "$3", gap: "$2", children: [!dismissed && !running ? _jsx(Suggestions, { items: SUGGESTIONS, onPick: (s) => void send(s), onDismiss: () => setDismissed(true) }) : null, note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsxs(Composer, { ...cut, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
323
324
  ? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
324
325
  : undefined, busy: busy || (running && !draft.trim()), rows: 3, placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", children: [_jsx(Attach, { files: attached, onFiles: setAttached, onNote: setNote }), _jsx(XStack, { flex: 1 }), _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), self: "center" }), _jsx(Dictate, { onText: (said) => setDraft((d) => (d ? `${d} ${said}` : said)), onNote: setNote })] })] }), wide && !collapsed ? (_jsx(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
325
326
  // The preview stays loaded while another view is open, so coming back to it
package/lib/run.js CHANGED
@@ -42,6 +42,7 @@ import { Grip } from './grip.js';
42
42
  import { useHost, useTarget } from './host.js';
43
43
  import { EFFORTS } from './landing.js';
44
44
  import { Out } from './out.js';
45
+ import { gap, pane } from './pane.js';
45
46
  import { usePrefs } from './prefs.js';
46
47
  import { Transcript } from './transcript.js';
47
48
  const LIVE = new Set(['running', 'paused', '']);
@@ -323,7 +324,7 @@ export function Run({ id }) {
323
324
  : 'Reading this run…';
324
325
  // What a new run from here is made with; words that steer this run use neither.
325
326
  const tuning = signed && !steering ? (_jsxs(XStack, { flex: 1, items: "center", gap: "$2", justify: "flex-end", flexWrap: "wrap", rowGap: "$1", children: [_jsx(ChipSelect, { quiet: true, name: "Model", label: catalog.value.find((m) => m.id === model)?.label ?? named(model), chosen: catalog.value.find((m) => m.id === model) ?? null, items: catalog.value, footer: _jsx(Access, { items: catalog.value }), onChange: (m) => choose({ ...chose, model: m.id }), placeholder: "Search models\u2026", placement: "top-end", loading: catalog.loading, error: catalog.error?.message ?? null }), _jsx(ChipSelect, { quiet: true, name: "Effort", label: pace.label, chosen: pace, items: EFFORTS, onChange: (e) => choose({ ...chose, effort: e.id }), placement: "top-end", width: 180 })] })) : undefined;
326
- return (_jsxs(XStack, { ref: measure, "data-slot": "run-split", flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { "data-slot": "run", flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
327
+ return (_jsxs(XStack, { ref: measure, "data-slot": "run-split", flex: 1, minH: 0, minW: 0, width: "100%", children: [_jsxs(YStack, { "data-slot": "run", ...pane, flex: 1, minH: 0, minW: 0, width: "100%", px: "$6", "$max-md": { px: '$4' }, children: [_jsxs(XStack, { pt: "$3", pb: "$2", gap: "$3", items: "center", minH: 44, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Manage this run", items: "center", gap: "$1.5", maxW: "100%", px: "$1.5", mx: "$-1.5", py: "$0.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { render: "h1", size: "$5", color: "$ink", numberOfLines: 1, minW: 0, children: title }), _jsx(ChevronDown, { size: 14 })] }), items: manage }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, maxW: "100%", children: [state || (detail.loading ? 'reading' : ''), record?.repo, record?.branch, record?.published ? 'shared' : ''].filter(Boolean).join(' · ') })] }), desk ? null : (_jsx(XStack, { render: "button", "aria-label": "Show the side pane", px: "$2", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: () => setDesk(true), "$max-md": { display: 'none' }, children: _jsx(PanelRight, { size: 16 }) })), pr.href ? (_jsx(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: _jsxs(XStack, { items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(GitPullRequest, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: pr.label }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })) : null, sharing.length ? (_jsx(DropdownMenu, { trigger: _jsxs(XStack, { render: "button", "aria-label": "Share", items: "center", gap: "$1.5", px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsx(Share, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$ink", children: "Share" })] }), items: sharing })) : null] }), _jsx(Transcript, { cards: shown,
327
328
  // A paused run's agent was stopped where it stood; what it was running is cut off.
328
329
  live: running && !paused, header: hidden ? (_jsx(SizableText, { size: "$1", color: "$soft", children: `${hidden} earlier ${hidden === 1 ? 'turn is' : 'turns are'} not shown. The run’s Git tab has everything it pushed.` })) : null, onApprove: planned && earlier ? (p) => void act(() => carry(p, 'approve')) : undefined, approving: busy, onVerdict: (v) => verdict(t, id, record?.project ?? '', v), empty: signed ? (_jsx(SizableText, { size: "$2", color: "$soft", children: detail.error
329
330
  ? detail.error.message
@@ -349,9 +350,13 @@ export function Run({ id }) {
349
350
  : 'This run is still working.' }), _jsx(Steer, { onCommand: command, withhold: [
350
351
  ...(paused ? ['pause'] : ['resume']),
351
352
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
352
- ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsxs(YStack, { "data-slot": "desk", ref: (el) => {
353
+ ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null, _jsx(Composer, { inline: true, ...pane, value: draft, onChange: setDraft, onSend: send, disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', foot: tuning }), _jsx(SizableText, { size: "$1", color: "$soft", style: { textAlign: 'center' }, children: "Hanzo is AI and can make mistakes." })] })] }), desk ? (_jsxs(YStack, { "data-slot": "desk", ref: (el) => {
353
354
  deskBox.current = el instanceof HTMLElement ? el : null;
354
- }, position: "relative", shrink: 0, minH: 0, display: "none", $md: { display: 'flex' }, style: { width: across, maxWidth: `calc(100% - ${TALK_FLOOR}px)`, minWidth: DESK_FLOOR }, children: [_jsx(Grip, { side: "right", span: across, floor: DESK_FLOOR, ceil: most, reset: Math.min(most, DESK_WIDTH), onSpan: (n) => deskBox.current?.style.setProperty('width', `${n}px`), onKeep: setSide, onShut: () => setDesk(false), label: "Resize the side pane" }), _jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', site: site, published: end.published, events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
355
+ }, ...pane,
356
+ // A pane of its own beside the run's, the gutter between them.
357
+ ml: gap, position: "relative", shrink: 0, minH: 0, display: "none", $md: { display: 'flex' }, style: { width: across, maxWidth: `calc(100% - ${TALK_FLOOR}px)`, minWidth: DESK_FLOOR }, children: [_jsx(Grip, { side: "right", span: across, floor: DESK_FLOOR, ceil: most, reset: Math.min(most, DESK_WIDTH), onSpan: (n) => deskBox.current?.style.setProperty('width', `${n}px`), onKeep: setSide, onShut: () => setDesk(false), label: "Resize the side pane",
358
+ // In the gutter between the two panes; `left` is measured inside the pane's 1px edge.
359
+ l: `calc(-1 * ${gap} - ${pane.borderWidth}px)`, width: gap }), _jsx(Desk, { id: id, repo: record?.repo ?? '', branch: record?.branch ?? '', base: record?.base ?? '', environment: record?.environment ?? '', mode: mode, pr: pr, title: title, project: record?.project ?? '', sandbox: record?.sandbox ?? '', site: site, published: end.published, events: events, live: running, menu: [...manage, ...sharing], refused: signed ? refused || (detail.error ? detail.error.message : '') : 'Sign in to follow this run.', retry: signed ? 'Retry' : 'Sign in', onRetry: signed ? detail.reload : () => host.signIn?.(), onHide: () => setDesk(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
355
360
  if (e.key === 'Enter')
356
361
  save();
357
362
  } }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
@@ -3,9 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Integrations: your own GitHub connection, the GitHub accounts the
4
4
  * organization has installed the platform's App on, and its Slack workspace.
5
5
  *
6
- * Connecting leaves this page once, for GitHub's or Slack's own consent screen.
7
- * Anyone connects their own GitHub; installing the App and connecting Slack are
8
- * an org admin's, which is the platform's rule.
6
+ * Connecting leaves this page once, for GitHub's or Slack's own consent screen,
7
+ * and the platform's callback brings the person back here with the answer
8
+ * (../back.ts), which this page finishes and reports. Anyone connects their own
9
+ * GitHub; installing the App and connecting Slack are an org admin's, which is
10
+ * the platform's rule.
9
11
  */
10
12
  import { SizableText, XStack, YStack } from '@hanzo/gui';
11
13
  import { Github, Hash, Lock, Slack } from '@hanzogui/lucide-icons-2';
@@ -13,6 +15,7 @@ import { Button } from '@hanzo/ui';
13
15
  import { useState } from 'react';
14
16
  import { connect, connection, disconnect as unlinkGithub, installations } from '../api/github.js';
15
17
  import { authorize, channels, disconnect, read } from '../api/provider.js';
18
+ import { here, useBack } from '../back.js';
16
19
  import { useRead } from '../data.js';
17
20
  import { useHost, useTarget } from '../host.js';
18
21
  import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
@@ -27,6 +30,11 @@ export function Integrations() {
27
30
  const rooms = useRead(signed && joined ? () => channels(t) : null, { channels: [], next: '' }, [t, signed, joined]);
28
31
  const [working, setWorking] = useState(false);
29
32
  const [note, setNote] = useState('');
33
+ useBack(t, signed, setNote, () => {
34
+ mine.reload();
35
+ installed.reload();
36
+ slack.reload();
37
+ });
30
38
  if (!signed) {
31
39
  return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Integrations" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s integrations." })] }));
32
40
  }
@@ -67,7 +75,7 @@ export function Integrations() {
67
75
  ? 'This deployment cannot connect GitHub yet.'
68
76
  : me.connected
69
77
  ? `Connected as @${me.login}`
70
- : 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(t), 'Your GitHub is disconnected', mine.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(t)), children: "Connect" }))) : null }), installed.error ? (_jsx(Row, { title: "App installations", detail: installed.error.message })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? 'every repository' : i.grant === 'selected' ? 'selected repositories' : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? 'Installed' : 'Removed on GitHub' }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: "No GitHub accounts installed", detail: "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(t, 'github')), children: "Install on a GitHub account" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin installs the App on a GitHub account." }))] }), _jsxs(Group, { title: "Slack", detail: "Talk to Hanzo from your workspace\u2019s 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
78
+ : 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(t), 'Your GitHub is disconnected', mine.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(t, here())), children: "Connect" }))) : null }), installed.error ? (_jsx(Row, { title: "App installations", detail: installed.error.message })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? 'every repository' : i.grant === 'selected' ? 'selected repositories' : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? 'Installed' : 'Removed on GitHub' }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: "No GitHub accounts installed", detail: "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(t, 'github', here())), children: "Install on a GitHub account" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin installs the App on a GitHub account." }))] }), _jsxs(Group, { title: "Slack", detail: "Talk to Hanzo from your workspace\u2019s 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
71
79
  ? slack.error.message
72
80
  : !s
73
81
  ? 'Reading…'
@@ -75,7 +83,7 @@ export function Integrations() {
75
83
  ? `Connected${s.since ? ` since ${day(s.since)}` : ''}`
76
84
  : s.available
77
85
  ? 'Not connected'
78
- : 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(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack')), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
86
+ : 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(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack', here())), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
79
87
  ? 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)))
80
88
  : null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the Slack workspace." })) : null] }), _jsx(Note, { children: note })] }));
81
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
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.",