@hanzo/build 0.2.51 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -55
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +42 -4
- package/lib/api/github.js +79 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -24
- package/lib/api/work.js +132 -27
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +21 -54
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +255 -92
- package/lib/host.d.ts +1 -1
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +79 -47
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +45 -0
- package/lib/panel.js +115 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.d.ts +16 -1
- package/lib/project.js +100 -32
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +28 -4
- package/lib/route.js +48 -1
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +23 -9
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
package/lib/plan.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plan as a person reads it, and how much of it is used.
|
|
3
|
+
*
|
|
4
|
+
* NAMED BY FAMILY. A slug is the catalog's rung (`max-20x`); a person reads the
|
|
5
|
+
* family — Free, Go, Pro, Max, Team, Agency, Enterprise — with the rung inside
|
|
6
|
+
* it as a small tag (`20x`). A slug outside the families reads as words
|
|
7
|
+
* (`growth-plus` is Growth Plus); one that is an id is never printed, and reads
|
|
8
|
+
* as the tier it is served under.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED IN SHARES. `GET /v1/ai/limits` (cloud apps/ai/limits_ops.go) answers
|
|
11
|
+
* every window as a percent used, its state and when it starts over — the
|
|
12
|
+
* session, the UTC day, and each class's billing period — never an amount.
|
|
13
|
+
*
|
|
14
|
+
* THREE KINDS OF ACCOUNT, three meters: on a PLAN its windows are the meter, on
|
|
15
|
+
* FREE the free allowance left today is, and with CREDITS and no plan (pay as
|
|
16
|
+
* you go) the balance is. A plan's meter never shows money.
|
|
17
|
+
*/
|
|
18
|
+
/** The families, by the slug's first word. `dev` is the slug Pro is sold under. */
|
|
19
|
+
const FAMILY = {
|
|
20
|
+
free: 'Free',
|
|
21
|
+
go: 'Go',
|
|
22
|
+
dev: 'Pro',
|
|
23
|
+
pro: 'Pro',
|
|
24
|
+
max: 'Max',
|
|
25
|
+
team: 'Team',
|
|
26
|
+
agency: 'Agency',
|
|
27
|
+
advisory: 'Agency',
|
|
28
|
+
dedicated: 'Agency',
|
|
29
|
+
pod: 'Agency',
|
|
30
|
+
enterprise: 'Enterprise',
|
|
31
|
+
};
|
|
32
|
+
/** The tier classes billing serves a rung under (`GET /v1/billing/tier` `tier.name`) that name a plan. */
|
|
33
|
+
const TIER = { pro: 'Pro', enterprise: 'Enterprise' };
|
|
34
|
+
/** A slug that is an id rather than a name: a UUID, a run of hex, a prefixed id, or digits. */
|
|
35
|
+
const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-|[0-9a-f]{12,}|^[a-z]+_[0-9a-z]{6,}$|^\d+$/i;
|
|
36
|
+
/** A word as a title reads it; a multiple (`2x`) stays as it is. */
|
|
37
|
+
const word = (w) => (/^\d+x$/.test(w) ? w : w.charAt(0).toUpperCase() + w.slice(1));
|
|
38
|
+
/**
|
|
39
|
+
* A plan as a person reads it, from the slug billing serves (`max-20x`) and the
|
|
40
|
+
* tier class beside it (`pro`). Null where neither names a plan: no slug and a
|
|
41
|
+
* free or trial tier, which is no plan at all.
|
|
42
|
+
*/
|
|
43
|
+
export function label(slug, tier) {
|
|
44
|
+
const raw = (slug ?? '').trim().toLowerCase();
|
|
45
|
+
const fallback = () => {
|
|
46
|
+
const t = TIER[(tier ?? '').trim().toLowerCase()];
|
|
47
|
+
return t ? { name: t, tag: '' } : null;
|
|
48
|
+
};
|
|
49
|
+
if (!raw)
|
|
50
|
+
return fallback();
|
|
51
|
+
// A rung that is an id still IS a rung: the account is on a plan, so it reads as one.
|
|
52
|
+
if (ID.test(raw))
|
|
53
|
+
return fallback() ?? { name: 'Paid plan', tag: '' };
|
|
54
|
+
const s = raw.replace(/[\s_]+/g, '-');
|
|
55
|
+
const [head, ...rest] = s.split('-').filter(Boolean);
|
|
56
|
+
const family = FAMILY[head];
|
|
57
|
+
if (!family)
|
|
58
|
+
return { name: s.split('-').filter(Boolean).map(word).join(' '), tag: '' };
|
|
59
|
+
const tail = rest.join('-');
|
|
60
|
+
const tag = /^\d+x$/.test(tail) ? tail : /^(annual|yearly|year)$/.test(tail) ? 'Annual' : tail ? rest.map(word).join(' ') : '';
|
|
61
|
+
return { name: family, tag };
|
|
62
|
+
}
|
|
63
|
+
/** A label as a sentence says it: `Max 20x`. */
|
|
64
|
+
export const said = (l) => (l ? (l.tag ? `${l.name} ${l.tag}` : l.name) : '');
|
|
65
|
+
const STATES = ['ok', 'near', 'limited'];
|
|
66
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
67
|
+
/**
|
|
68
|
+
* One window off the wire, or null for a window that bounds nothing. The
|
|
69
|
+
* server states a window's state only when the plan sets it a limit
|
|
70
|
+
* (limits_ops.go `window`), so a window with no state is unbounded and not drawn.
|
|
71
|
+
*/
|
|
72
|
+
export function share(v) {
|
|
73
|
+
const o = obj(v);
|
|
74
|
+
const state = STATES.find((s) => s === o?.state);
|
|
75
|
+
if (!o || !state || typeof o.percent !== 'number' || !Number.isFinite(o.percent))
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
percent: Math.min(100, Math.max(0, Math.round(o.percent))),
|
|
79
|
+
state,
|
|
80
|
+
resets: typeof o.resets_at === 'string' ? o.resets_at : '',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The meter for a plan slug (billing's or the limits', `free` or '' for none),
|
|
85
|
+
* the prepaid balance in cents (null where it could not be read) and the tier
|
|
86
|
+
* class billing serves the account under: a paid plan's windows — a rung, or a
|
|
87
|
+
* paid class with no rung (a plan recorded outside checkout, a partner org) —
|
|
88
|
+
* and with no plan, the balance where there is money in it, else the free
|
|
89
|
+
* allowance.
|
|
90
|
+
*/
|
|
91
|
+
export function kind(plan, balance, tier) {
|
|
92
|
+
const p = (plan ?? '').trim().toLowerCase();
|
|
93
|
+
if ((p && p !== 'free') || TIER[(tier ?? '').trim().toLowerCase()])
|
|
94
|
+
return 'plan';
|
|
95
|
+
return balance !== null && balance > 0 ? 'credits' : 'free';
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* When something starts over, as a sentence says it: a time for within a day
|
|
99
|
+
* (`5:00 PM`), else a date (`Nov 1`), periods ending on UTC days. '' for none.
|
|
100
|
+
*/
|
|
101
|
+
export function when(iso, now = Date.now()) {
|
|
102
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
103
|
+
if (Number.isNaN(t))
|
|
104
|
+
return '';
|
|
105
|
+
const d = new Date(t);
|
|
106
|
+
return t - now < 86_400_000
|
|
107
|
+
? d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
|
108
|
+
: d.toLocaleDateString('en-US', { timeZone: 'UTC', month: 'short', day: 'numeric' });
|
|
109
|
+
}
|
|
110
|
+
/** A class of model as a sentence names it. */
|
|
111
|
+
const CLASS = { premium: 'premium', ours: 'Hanzo' };
|
|
112
|
+
/**
|
|
113
|
+
* What a reader whose plan's included usage is used is told: the plan, which
|
|
114
|
+
* models, and when they come back — `Your Max 20x plan’s included premium model
|
|
115
|
+
* usage is used until 5:00 PM.`
|
|
116
|
+
*/
|
|
117
|
+
export function spent(l, classes, resets, now = Date.now()) {
|
|
118
|
+
const names = classes.map((c) => CLASS[c]).filter(Boolean);
|
|
119
|
+
const what = names.length ? `${names.join(' and ')} model usage` : 'usage';
|
|
120
|
+
const at = when(resets, now);
|
|
121
|
+
return `Your ${l ? `${said(l)} plan’s` : 'plan’s'} included ${what} is used${at ? ` until ${at}` : ''}.`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The ways past a spent allowance, in this order and two at most: keep going
|
|
125
|
+
* on credits — or, with none the server would let pay, add some — then the
|
|
126
|
+
* upgrade; and a switch to another model where the server offers one. Taken
|
|
127
|
+
* from what the server sent, so nothing is offered that it would refuse.
|
|
128
|
+
*/
|
|
129
|
+
export function ways(actions) {
|
|
130
|
+
const pick = (kind) => actions.find((a) => a.kind === kind);
|
|
131
|
+
const topup = pick('topup');
|
|
132
|
+
return [pick('credits') ?? (topup ? { ...topup, label: 'Add credits' } : undefined), pick('upgrade'), pick('switch')].filter((a) => a !== undefined);
|
|
133
|
+
}
|
package/lib/project.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Project as Row } from './api/projects.ts';
|
|
2
|
+
/** What a workspace is opened on: a repository on the forge, or a deployed site by its slug. */
|
|
3
|
+
export type Where = {
|
|
2
4
|
slug: string;
|
|
5
|
+
} | {
|
|
6
|
+
org: string;
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
/** Whether the site `p` is the repository `r`'s: built from it, or named as it is. */
|
|
10
|
+
export declare function siteOf(p: Row, r: {
|
|
11
|
+
org: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}): boolean;
|
|
14
|
+
/** A site's slug for a repository's name: lowercase letters, digits and hyphens, at most 40. */
|
|
15
|
+
export declare function slugOf(name: string): string;
|
|
16
|
+
export declare function Project({ at }: {
|
|
17
|
+
at: Where;
|
|
3
18
|
}): import("react").JSX.Element;
|
package/lib/project.js
CHANGED
|
@@ -3,6 +3,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* A project's workspace, in the same window: the chat on the left, what it
|
|
4
4
|
* built on the right.
|
|
5
5
|
*
|
|
6
|
+
* A project is a repository on the forge, addressed `<org>/<repo>`; the site it
|
|
7
|
+
* publishes to, when it has one, is the org's project row on that repository
|
|
8
|
+
* (or of the same name). A deployed site with no repository on the forge is
|
|
9
|
+
* opened by its slug (Artifacts), and works the same way.
|
|
10
|
+
*
|
|
6
11
|
* bar mark · project · history · chat toggle | Preview Files Code Layers |
|
|
7
12
|
* Share · Publish
|
|
8
13
|
* left the project's runs as a conversation, suggestions, and the ask
|
|
@@ -36,27 +41,32 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
36
41
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
37
42
|
import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
|
|
38
43
|
import { Attachments, CHAT, Console, DEVICES, Feedback, FileTabs, FileTree, ModeSelect, PageSelect, PreviewFrame, ProjectChip, Suggestions, SUGGESTIONS, VIEWS, Views, Workspace, fold, } from '@hanzo/ui/agents';
|
|
39
|
-
import {
|
|
44
|
+
import { Message } from '@hanzo/ui/chat';
|
|
40
45
|
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
41
46
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
47
|
+
import { useDictation, useVoice } from '@hanzo/voice';
|
|
42
48
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
43
49
|
import { read as pushed } from './api/changes.js';
|
|
50
|
+
import { codebases, one } from './api/codebases.js';
|
|
44
51
|
import { start } from './api/coding.js';
|
|
45
52
|
import { blob, tree } from './api/git.js';
|
|
46
53
|
import { declare } from './api/platform.js';
|
|
47
|
-
import { deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
54
|
+
import { address, deployments, name as repoName, ours, templates } from './api/projects.js';
|
|
48
55
|
import { list, message, stop } from './api/sessions.js';
|
|
49
|
-
import { decode, outcome, pull, said, who } from './api/turn.js';
|
|
56
|
+
import { decode, outcome, pull, said, scrub, who } from './api/turn.js';
|
|
50
57
|
import { verdict as record } from './api/verdict.js';
|
|
51
58
|
import { useKept, useProjects, useRead, useRun } from './data.js';
|
|
52
59
|
import { Merge } from './git.js';
|
|
53
60
|
import { Grip } from './grip.js';
|
|
54
61
|
import { useHost, useTarget } from './host.js';
|
|
55
62
|
import { MODES } from './landing.js';
|
|
63
|
+
import { useMind } from './mind.js';
|
|
56
64
|
import { Out } from './out.js';
|
|
57
|
-
import {
|
|
65
|
+
import { Prompt, useEar } from './prompt.js';
|
|
66
|
+
import { useSpoken } from './voice.js';
|
|
58
67
|
import { Publish } from './publish.js';
|
|
59
|
-
import {
|
|
68
|
+
import { path } from './route.js';
|
|
69
|
+
import { compose, Files, pick } from './tools.js';
|
|
60
70
|
const LIVE = new Set(['running', 'paused']);
|
|
61
71
|
/** How often a project being published is read again, ms. */
|
|
62
72
|
const AGAIN = 5000;
|
|
@@ -70,6 +80,20 @@ const CHAT_CEIL = 640;
|
|
|
70
80
|
const GUTTER = 12;
|
|
71
81
|
/** What the work keeps beside the chat however wide the chat was asked to be, px. */
|
|
72
82
|
const WORK = 400 + 2 * GUTTER;
|
|
83
|
+
/** Whether the site `p` is the repository `r`'s: built from it, or named as it is. */
|
|
84
|
+
export function siteOf(p, r) {
|
|
85
|
+
const want = `${r.org}/${r.name}`.toLowerCase();
|
|
86
|
+
return (p.repo !== '' && address(p.repo).toLowerCase() === want) || p.slug === r.name.toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
/** A site's slug for a repository's name: lowercase letters, digits and hyphens, at most 40. */
|
|
89
|
+
export function slugOf(name) {
|
|
90
|
+
return name
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
93
|
+
.replace(/^-+|-+$/g, '')
|
|
94
|
+
.slice(0, 40)
|
|
95
|
+
.replace(/-+$/, '');
|
|
96
|
+
}
|
|
73
97
|
/** One run in the conversation: what was asked, and what it came to. */
|
|
74
98
|
function Turn({ run, open, onOpen, project }) {
|
|
75
99
|
const t = useTarget();
|
|
@@ -83,12 +107,18 @@ function Turn({ run, open, onOpen, project }) {
|
|
|
83
107
|
});
|
|
84
108
|
} }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: note })) : null, !open ? (_jsx(XStack, { render: "button", onPress: onOpen, hitSlop: 8, children: _jsx(SizableText, { size: "$1", color: "$soft", textDecorationLine: "underline", children: "Show steps" }) })) : null] }), children: _jsx(SizableText, { size: "$2", color: "$ink", children: run.status === 'done' ? 'Done.' : run.status === 'error' ? 'This run hit an error.' : run.status === 'stopped' ? 'Stopped.' : 'Working…' }) })] }));
|
|
85
109
|
}
|
|
86
|
-
export function Project({
|
|
110
|
+
export function Project({ at }) {
|
|
87
111
|
const host = useHost();
|
|
88
112
|
const t = useTarget();
|
|
89
113
|
const signed = Boolean(host.person);
|
|
90
114
|
const all = useProjects(t, signed);
|
|
91
|
-
|
|
115
|
+
// The repository the address names, and the forge's record of it.
|
|
116
|
+
const named = 'slug' in at ? null : at;
|
|
117
|
+
const forge = useRead(named && signed ? () => one(t, named.name) : null, null, [t, signed, named?.name]);
|
|
118
|
+
const project = all.value.find((p) => (named ? siteOf(p, named) : 'slug' in at && p.slug === at.slug)) ?? null;
|
|
119
|
+
// The site this workspace publishes to: the project's, or the repository's name as one.
|
|
120
|
+
const slug = project?.slug ?? ('slug' in at ? at.slug : slugOf(named?.name ?? ''));
|
|
121
|
+
const title = project?.name ?? named?.name ?? slug;
|
|
92
122
|
// A project that has never served reads `building` while it is published, and
|
|
93
123
|
// is read again until it is live or its build failed.
|
|
94
124
|
const building = project?.status === 'building';
|
|
@@ -113,12 +143,15 @@ export function Project({ slug }) {
|
|
|
113
143
|
const stalled = building && ran > STALL;
|
|
114
144
|
// Publish again: the project's repository, built at its branch, the way Publish builds it.
|
|
115
145
|
const [redo, setRedo] = useState({ busy: false, error: '' });
|
|
146
|
+
// The repository's clone address: the forge's for a repository, the site's own otherwise.
|
|
147
|
+
const clone = named ? (forge.value?.clone ?? '') : (project?.repo ?? '');
|
|
148
|
+
const home = named ? (forge.value?.branch ?? 'main') : project?.branch || 'main';
|
|
116
149
|
const retry = async () => {
|
|
117
|
-
if (!
|
|
150
|
+
if (!clone || redo.busy)
|
|
118
151
|
return;
|
|
119
152
|
setRedo({ busy: true, error: '' });
|
|
120
153
|
try {
|
|
121
|
-
await declare(t, { repo:
|
|
154
|
+
await declare(t, { repo: clone, ref: home, name: slug, project: slug, mode: 'branch' });
|
|
122
155
|
setNow(Date.now());
|
|
123
156
|
again();
|
|
124
157
|
setRedo({ busy: false, error: '' });
|
|
@@ -127,11 +160,14 @@ export function Project({ slug }) {
|
|
|
127
160
|
setRedo({ busy: false, error: e.message });
|
|
128
161
|
}
|
|
129
162
|
};
|
|
130
|
-
|
|
163
|
+
// The site's runs, or the org's when the repository has no site yet.
|
|
164
|
+
const sited = !named || Boolean(project);
|
|
165
|
+
const listed = useRead(signed ? () => list(t, sited ? { kind: 'coding', project: slug, limit: 50 } : { kind: 'coding', limit: 50 }) : null, [], [t, signed, slug, sited]);
|
|
131
166
|
// Only the runs on this project's own repository: a record can be moved into
|
|
132
167
|
// any project, and a run's branch is what Files, Code and Publish read.
|
|
133
|
-
const
|
|
134
|
-
const
|
|
168
|
+
const mine = (r) => (named ? ours(r.repo, `${named.org}/${named.name}`) && Boolean(r.repo) : ours(r.repo, project?.repo ?? ''));
|
|
169
|
+
const runs = { ...listed, value: listed.value.filter(mine) };
|
|
170
|
+
const ordered = useMemo(() => [...runs.value].reverse(), [listed.value, project?.repo, named?.org, named?.name]);
|
|
135
171
|
const [chosen, setChosen] = useState(null);
|
|
136
172
|
const current = chosen ?? runs.value[0]?.id ?? null;
|
|
137
173
|
const run = useRun(t, current);
|
|
@@ -149,6 +185,12 @@ export function Project({ slug }) {
|
|
|
149
185
|
// Below md the chat and the work take turns, and Chat is one more view to switch back to.
|
|
150
186
|
const [pane, setPane] = useState('chat');
|
|
151
187
|
const wide = useMedia().md;
|
|
188
|
+
// A workspace too narrow for the chat at its floor and the work at its room
|
|
189
|
+
// (a tablet beside the host's column) shows the work, and the chat when asked:
|
|
190
|
+
// never both cut. Asked here is for this visit; the kept choice is the wide one's.
|
|
191
|
+
const cramped = wide && room > 0 && room < CHAT_FLOOR + WORK;
|
|
192
|
+
const [asked, setAsked] = useState(false);
|
|
193
|
+
const shut = cramped ? !asked : collapsed;
|
|
152
194
|
const [dock, setDock] = useKept('hanzo.build.dock', 36);
|
|
153
195
|
const [dismissed, setDismissed] = useKept(`hanzo.build.hints.${slug}`, false);
|
|
154
196
|
const [mode, setMode] = useKept('hanzo.build.mode', 'build');
|
|
@@ -158,12 +200,16 @@ export function Project({ slug }) {
|
|
|
158
200
|
const [note, setNote] = useState('');
|
|
159
201
|
const [picked, setPicked] = useState([]);
|
|
160
202
|
const [attached, setAttached] = useState([]);
|
|
203
|
+
// The model and effort Chat and New share, and dictation into the ask.
|
|
204
|
+
const mind = useMind(t);
|
|
205
|
+
const voice = useVoice({ speech: useEar(t), language: useSpoken(), ...useDictation(draft, setDraft) });
|
|
161
206
|
const [bridge, setBridge] = useState(false);
|
|
162
207
|
const [editing, setEditing] = useState(false);
|
|
163
208
|
const [pageLines, setPageLines] = useState([]);
|
|
164
209
|
const [history, setHistory] = useState(false);
|
|
165
210
|
const [publish, setPublish] = useState(null);
|
|
166
211
|
const [switcher, setSwitcher] = useState(false);
|
|
212
|
+
const repos = useRead(switcher && signed ? () => codebases(t) : null, [], [t, switcher, signed]);
|
|
167
213
|
const frame = useRef(null);
|
|
168
214
|
// What was published may be new files at the same address: the project is read
|
|
169
215
|
// for its address, and the frame loads again.
|
|
@@ -196,8 +242,8 @@ export function Project({ slug }) {
|
|
|
196
242
|
// Files is drawn again after a merge, from what the forge holds now.
|
|
197
243
|
const [round, setRound] = useState(0);
|
|
198
244
|
// Files and Code: native git, at the branch the open run pushed (or the project's own).
|
|
199
|
-
const repo = project?.repo ? repoName(project.repo) : '';
|
|
200
|
-
const ref = run.record?.branch ||
|
|
245
|
+
const repo = named ? named.name : project?.repo ? repoName(project.repo) : '';
|
|
246
|
+
const ref = run.record?.branch || home;
|
|
201
247
|
const [files, setFiles] = useState([]);
|
|
202
248
|
const [file, setFile] = useState(null);
|
|
203
249
|
useEffect(() => {
|
|
@@ -247,7 +293,7 @@ export function Project({ slug }) {
|
|
|
247
293
|
.map((e) => {
|
|
248
294
|
// The console is where a run's error is read whole; the chat says it in one plain line.
|
|
249
295
|
const body = decode(e.payload);
|
|
250
|
-
const error = e.kind === 'status' && body && typeof body === 'object' && typeof body.error === 'string' ? body.error : '';
|
|
296
|
+
const error = e.kind === 'status' && body && typeof body === 'object' && typeof body.error === 'string' ? scrub(body.error) : '';
|
|
251
297
|
return {
|
|
252
298
|
id: `r${e.seq || e.id}`,
|
|
253
299
|
level: error || (e.kind === 'status' && /error/.test(said(e, run.record?.mode).toLowerCase())) ? 'error' : e.kind === 'tool-call' ? 'info' : 'log',
|
|
@@ -294,11 +340,15 @@ export function Project({ slug }) {
|
|
|
294
340
|
// A project with no repository yet sends neither, and its first run makes one.
|
|
295
341
|
const next = await start(t, {
|
|
296
342
|
prompt: compose(`${ask}${context}`, attached),
|
|
297
|
-
|
|
343
|
+
// A repository with no site yet runs on the repository alone.
|
|
344
|
+
project: project?.slug,
|
|
298
345
|
repo,
|
|
299
|
-
base: project?.branch,
|
|
346
|
+
base: named ? forge.value?.branch : project?.branch,
|
|
300
347
|
after: current ?? undefined,
|
|
301
348
|
mode,
|
|
349
|
+
// Enso named as itself: a run that names none takes the person's saved coding default.
|
|
350
|
+
model: mind.model,
|
|
351
|
+
effort: mind.effort,
|
|
302
352
|
});
|
|
303
353
|
setDraft('');
|
|
304
354
|
setPicked([]);
|
|
@@ -337,10 +387,21 @@ export function Project({ slug }) {
|
|
|
337
387
|
if (!signed) {
|
|
338
388
|
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", children: [_jsx(SizableText, { size: "$4", color: "$ink", children: "Sign in to open this project." }), _jsx(Button, { onPress: () => host.signIn?.(), children: "Sign in" })] }));
|
|
339
389
|
}
|
|
340
|
-
|
|
341
|
-
|
|
390
|
+
// A repository of another organization is opened in that one.
|
|
391
|
+
if (named && host.org && named.org.toLowerCase() !== host.org.toLowerCase()) {
|
|
392
|
+
const member = host.memberships?.some((m) => m.toLowerCase() === named.org.toLowerCase()) && host.chooseOrg;
|
|
393
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: `${named.org}/${named.name} is in ${named.org}, and you are working in ${host.org}.` }), member ? (_jsx(Button, { variant: "outline", onPress: () => host.chooseOrg?.(named.org), children: `Switch to ${named.org}` })) : (_jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'projects' })), children: "See this organization\u2019s projects" }))] }));
|
|
394
|
+
}
|
|
395
|
+
if (named ? !forge.loading && !forge.value : !all.loading && !project && !all.error) {
|
|
396
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$3", px: "$4", children: [_jsx(SizableText, { size: "$4", color: "$ink", text: "center", children: named
|
|
397
|
+
? forge.error && !/not found|404/i.test(forge.error.message)
|
|
398
|
+
? forge.error.message
|
|
399
|
+
: `${named.org} has no repository named ${named.name}.`
|
|
400
|
+
: host.org
|
|
401
|
+
? `${host.org} has no project named ${slug}.`
|
|
402
|
+
: `There is no project named ${slug}.` }), _jsx(Button, { variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: named ? 'projects' : 'artifacts' })), children: named ? 'See this organization’s projects' : 'See what this organization has built' })] }));
|
|
342
403
|
}
|
|
343
|
-
const chat = (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3",
|
|
404
|
+
const chat = (_jsxs(YStack, { "data-slot": "project-chat", flex: 1, minH: 0, position: "relative", children: [_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$3", pt: "$2", pb: "$4", gap: "$4", $md: { pr: 0 }, children: ordered.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$ink", children: runs.loading
|
|
344
405
|
? 'Reading this project’s runs…'
|
|
345
406
|
: project?.live
|
|
346
407
|
? `${project.name} is loaded — it is in the preview, and every run on it is under the clock above. Say what to change and it gets built.`
|
|
@@ -354,12 +415,14 @@ export function Project({ slug }) {
|
|
|
354
415
|
? `${project?.name} is loaded — the preview shows the ${starter.title} starter until your copy is published. Say what to change and it gets built.`
|
|
355
416
|
: project
|
|
356
417
|
? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
|
|
357
|
-
:
|
|
418
|
+
: named
|
|
419
|
+
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
420
|
+
: `${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, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, 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, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
|
|
358
421
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
359
|
-
: undefined, busy: busy || (running && !draft.trim()),
|
|
422
|
+
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", onAttach: () => pick(attached, setAttached, setNote), tools: _jsx(ModeSelect, { modes: MODES, value: mode, onChange: (m) => setMode(m), bg: "transparent", minH: 24, px: "$1.5", self: "center" }), mind: running ? null : mind, voice: voice })] }), wide && !shut ? (_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] }));
|
|
360
423
|
// The preview stays loaded while another view is open, so coming back to it
|
|
361
424
|
// is the page as it was left.
|
|
362
|
-
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${
|
|
425
|
+
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${title} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
|
|
363
426
|
? 'Publishing is taking too long'
|
|
364
427
|
: building
|
|
365
428
|
? project?.forked
|
|
@@ -373,29 +436,34 @@ export function Project({ slug }) {
|
|
|
373
436
|
? 'Its page appears here when the build finishes.'
|
|
374
437
|
: failed
|
|
375
438
|
? why
|
|
376
|
-
: 'Publish this project and its page appears here.' }), (stalled || failed) &&
|
|
439
|
+
: 'Publish this project and its page appears here.' }), (stalled || failed) && clone ? (_jsx(Button, { variant: "outline", disabled: redo.busy, onPress: () => void retry(), children: "Publish again" })) : null, redo.error ? (_jsx(SizableText, { size: "$2", color: "$soft", text: "center", role: "alert", children: redo.error })) : null] }), toolbar: bridge ? (_jsx(XStack, { render: "button", "aria-pressed": editing, "aria-label": editing ? 'Stop picking elements' : 'Pick an element to edit', onPress: () => {
|
|
377
440
|
const next = !editing;
|
|
378
441
|
setEditing(next);
|
|
379
442
|
frame.current?.post({ type: 'preview:editable', active: next });
|
|
380
443
|
}, p: "$2", rounded: "$10", children: _jsx(MousePointerClick, { size: 16 }) })) : undefined })] }));
|
|
381
|
-
const body = view === 'preview' ? null : view === 'files' ? (repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
444
|
+
const body = view === 'preview' ? null : view === 'files' ? (repo && root.error ? (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", role: "status", children: `Nothing to show for ${repo} at ${ref}: the forge answered “${root.error.message}”.` })) : repo ? (_jsx(FileTree, { load: async (dir) => (await tree(t, repo, ref, dir)).map((e) => ({ path: e.path, kind: e.dir ? 'dir' : 'file' })), value: file, onSelect: (p) => void openFile(p), label: `${repo} at ${ref}` }, round)) : (_jsx(SizableText, { size: "$2", color: "$soft", p: "$4", children: "This project has no repository yet. Its first run creates one." }))) : view === 'code' ? (_jsx(FileTabs, { files: files, value: file, onSelect: setFile, onClose: (p) => {
|
|
382
445
|
setFiles((was) => was.filter((f) => f.path !== p));
|
|
383
446
|
if (file === p)
|
|
384
447
|
setFile(null);
|
|
385
448
|
}, empty: _jsxs(SizableText, { size: "$2", color: "$soft", p: "$4", children: ["Open a file from Files. Code here is read at ", ref, "; ask in the chat to change it."] }) })) : (_jsx(YStack, { p: "$4", gap: "$2", children: picked.length ? (picked.map((p) => (_jsxs(SizableText, { size: "$2", color: "$ink", children: [p.label, " \u2014 ", p.id] }, p.id)))) : (_jsx(SizableText, { size: "$2", color: "$soft", children: bridge
|
|
386
449
|
? 'Pick an element in the preview to see it here and attach it to your next ask.'
|
|
387
450
|
: 'Layers are read through the preview bridge, and this page does not run it. Its elements cannot be listed from here.' })) }));
|
|
388
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name:
|
|
451
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Workspace, { start: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, shrink: 1, children: [_jsx(XStack, { render: "button", "aria-label": "All runs", onPress: () => host.go(''), p: "$1", shrink: 0, children: _jsx(HanzoMark, { size: 20 }) }), _jsx(ProjectChip, { name: title, onPress: () => setSwitcher(true) }), _jsx(XStack, { render: "button", "aria-label": "History", onPress: () => setHistory(true), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Clock, { size: 16 }) }), _jsx(XStack, { render: "button", "aria-label": shut ? 'Show the chat' : 'Hide the chat', "aria-pressed": !shut, onPress: () => (cramped ? setAsked(!asked) : setCollapsed(!collapsed)), p: "$1.5", rounded: "$3", shrink: 0, hoverStyle: { bg: '$hover' }, "$max-md": { display: 'none' }, children: _jsx(PanelLeft, { size: 16 }) })] }), middle: _jsx(Views, { views: [CHAT, ...VIEWS], value: pane === 'chat' && !wide ? 'chat' : view, onChange: (v) => {
|
|
389
452
|
if (v === 'chat')
|
|
390
453
|
return setPane('chat');
|
|
391
454
|
setView(v);
|
|
392
455
|
setPane('view');
|
|
393
|
-
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", disabled: !
|
|
394
|
-
setPublish({ repo: project.repo, title: project.name, ref, name: project.slug, project: project.slug }), children: [_jsx(Upload, { size: 14 }), " Publish"] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: collapsed, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", project?.name ?? slug] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
456
|
+
}, label: "View" }), end: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsxs(Button, { variant: "outline", size: "sm", onPress: () => void share(), children: [_jsx(Share2, { size: 14 }), " Share"] }) }), _jsxs(Button, { size: "sm", "aria-label": "Publish", disabled: !clone || !slug, onPress: () => setPublish({ repo: clone, title, ref, name: slug, project: slug }), children: [_jsx(Upload, { size: 14 }), _jsx(SizableText, { size: "$2", color: "inherit", "$max-md": { display: 'none' }, children: "Publish" })] })] }), chat: chat, dock: _jsx(Console, { lines: lines, height: dock, onHeight: setDock, onClear: () => setPageLines([]), status: run.status || end.status || undefined, label: "Console" }), collapsed: shut, pane: pane, width: across, onLayout: (e) => setRoom(e.nativeEvent.layout.width), children: [preview, body] }), _jsx(Dialog, { open: history, onOpenChange: setHistory, children: _jsxs(DialogContent, { maxW: 480, children: [_jsxs(DialogTitle, { children: ["Runs on ", title] }), _jsx(YStack, { gap: "$1", children: runs.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No runs yet." })) : (runs.value.map((r) => (_jsxs(XStack, { render: "button", onPress: () => {
|
|
395
457
|
setChosen(r.id);
|
|
396
458
|
setHistory(false);
|
|
397
|
-
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
459
|
+
}, justify: "space-between", px: "$2", py: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, flex: 1, children: r.title || 'Untitled run' }), _jsx(SizableText, { size: "$1", color: "$soft", children: r.status })] }, r.id)))) })] }) }), _jsx(Dialog, { open: switcher, onOpenChange: setSwitcher, children: _jsxs(DialogContent, { maxW: 420, children: [_jsx(DialogTitle, { children: "Projects" }), _jsx(YStack, { gap: "$1", maxH: 420, overflow: "scroll", children: repos.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: repos.error.message })) : repos.loading && repos.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the forge\u2026" })) : (repos.value.map((c) => {
|
|
460
|
+
const here = Boolean(named) && c.name === named?.name;
|
|
461
|
+
return (_jsx(XStack, { render: "button", "aria-label": `Open ${c.org}/${c.name}`, "aria-current": here ? 'page' : undefined, onPress: () => {
|
|
462
|
+
setSwitcher(false);
|
|
463
|
+
host.go(path({ kind: 'repo', org: c.org || host.org || '', name: c.name }));
|
|
464
|
+
}, px: "$2", py: "$1.5", rounded: "$3", bg: here ? '$hover' : undefined, hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: c.name }) }, `${c.org}/${c.name}`));
|
|
465
|
+
})) }), _jsx(Button, { variant: "outline", size: "sm", onPress: () => {
|
|
466
|
+
setSwitcher(false);
|
|
467
|
+
host.go(path({ kind: 'screen', screen: 'projects' }));
|
|
468
|
+
}, children: "All projects" })] }) }), _jsx(Publish, { source: publish, onClose: () => setPublish(null), onLive: fresh })] }));
|
|
401
469
|
}
|
package/lib/prompt.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ComposerProps } from '@hanzo/ui/chat';
|
|
2
|
+
import { type PauseSource } from '@hanzo/ui/models';
|
|
3
|
+
import { type Machine, type Speech } from '@hanzo/voice';
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
|
+
import type { Target } from './api/call.ts';
|
|
6
|
+
import { type Mind } from './mind.ts';
|
|
7
|
+
/** The reading measure: @hanzo/design's prose width, which Width (appearance) retunes. */
|
|
8
|
+
export declare const MEASURE = "var(--container-prose, 48rem)";
|
|
9
|
+
/** The space between turns, px: one rhythm for Chat and Dev. */
|
|
10
|
+
export declare const GAP = 20;
|
|
11
|
+
/** The composer's width: the measure less the thread column's 0.75rem gutters, centred, so its edges are the text's. */
|
|
12
|
+
export declare const measure: {
|
|
13
|
+
readonly width: 'calc(100% - 1.5rem)';
|
|
14
|
+
readonly maxW: "calc(var(--container-prose, 48rem) - 1.5rem)";
|
|
15
|
+
readonly mx: 'auto';
|
|
16
|
+
};
|
|
17
|
+
/** The round controls' box, px: a line of the footer row. */
|
|
18
|
+
export declare const ROUND = 30;
|
|
19
|
+
/**
|
|
20
|
+
* The frame as a pane of its own, floating on the pane it is drawn in: the
|
|
21
|
+
* `--pane-*` cut on design's first paper rung (a second rung's drop reads as a
|
|
22
|
+
* gradient through a translucent pane).
|
|
23
|
+
*/
|
|
24
|
+
export declare const cut: {
|
|
25
|
+
readonly bg: 'var(--pane-fill, var(--sheet-1))';
|
|
26
|
+
readonly rounded: 'var(--pane-round, var(--radius-xl))';
|
|
27
|
+
readonly borderWidth: 1;
|
|
28
|
+
readonly borderColor: 'var(--pane-edge, var(--white-08))';
|
|
29
|
+
readonly backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))';
|
|
30
|
+
readonly boxShadow: 'var(--shadow-sheet-1)';
|
|
31
|
+
};
|
|
32
|
+
/** The platform's ear and voice for a target: dictation, talk and Listen all hear through it. */
|
|
33
|
+
export declare function useEar(t: Target): Speech;
|
|
34
|
+
export interface PromptOptions {
|
|
35
|
+
value: string;
|
|
36
|
+
onChange: (value: string) => void;
|
|
37
|
+
onSend: () => void;
|
|
38
|
+
/** Stop the turn in flight. Absent, busy only disables send. */
|
|
39
|
+
onStop?: () => void;
|
|
40
|
+
busy?: boolean;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Whether send is lit. Default: the draft has words. */
|
|
43
|
+
ready?: boolean;
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Above the frame, at its width: what the draft is sent with. */
|
|
47
|
+
head?: ReactNode;
|
|
48
|
+
field?: ComposerProps['field'];
|
|
49
|
+
/** The model and effort. Null where the next send uses neither (steering a run). */
|
|
50
|
+
mind?: Mind | null;
|
|
51
|
+
limits?: PauseSource | null;
|
|
52
|
+
/** The paperclip. Absent, there is none. */
|
|
53
|
+
onAttach?: () => void;
|
|
54
|
+
/** After the paperclip: what this surface alone offers (Dev's mode). */
|
|
55
|
+
tools?: ReactNode;
|
|
56
|
+
/** Dictation into the draft (@hanzo/voice `useVoice` + `useDictation`). */
|
|
57
|
+
voice?: Machine;
|
|
58
|
+
/** A spoken conversation (@hanzo/voice `useTalk`). */
|
|
59
|
+
talk?: Machine;
|
|
60
|
+
/** Drawn on a pane: the frame is cut as a pane of its own (`cut`). */
|
|
61
|
+
framed?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The chip beside send, and the panel it opens: the effort, a sentence saying
|
|
65
|
+
* what serves the ask, and the model.
|
|
66
|
+
*/
|
|
67
|
+
export declare function Tune({ mind, limits, disabled }: {
|
|
68
|
+
mind: Mind;
|
|
69
|
+
limits?: PauseSource | null;
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
}): import("react").JSX.Element;
|
|
72
|
+
/** @hanzo/ui `Composer` props for the one composer. */
|
|
73
|
+
export declare function prompt(o: PromptOptions): ComposerProps;
|
|
74
|
+
/** The composer drawn on its own. */
|
|
75
|
+
export declare function Prompt(o: PromptOptions): import("react").JSX.Element;
|