@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/mind.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model answers, and how hard it thinks: one store for Chat and Dev, and
|
|
3
|
+
* the catalog both offer. The chip that changes it is `Tune` (prompt.tsx).
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/models the catalog, read once per platform and shared
|
|
6
|
+
* hanzo.mind the person's choice, in this browser: {model, effort}
|
|
7
|
+
*
|
|
8
|
+
* Effort is on the chip and the model is one press in. Enso routes every ask —
|
|
9
|
+
* a message in Chat, a step in Dev — so naming a model is an override, and the
|
|
10
|
+
* panel says so before it offers one. A premium model is picked for the screen
|
|
11
|
+
* it was picked on (`usePick`) and never kept, so the next conversation or run
|
|
12
|
+
* opens on what was kept, Enso until something else is.
|
|
13
|
+
*
|
|
14
|
+
* The efforts are the set both planes take: `reasoning_effort` on a completion
|
|
15
|
+
* and `effort` on a coding run (apps/coding `efforts`). A model the catalog
|
|
16
|
+
* lists as not reasoning offers none; Enso and a model the catalog does not
|
|
17
|
+
* describe offer all three.
|
|
18
|
+
*/
|
|
19
|
+
import { modelName, parseModels } from '@hanzo/ui/models/catalog';
|
|
20
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
21
|
+
import { Refusal } from './api/call.js';
|
|
22
|
+
import { ENSO, models as read } from './api/models.js';
|
|
23
|
+
import { usePick } from './pick.js';
|
|
24
|
+
export const EFFORTS = [
|
|
25
|
+
{ id: 'low', label: 'Low', note: 'A quick pass' },
|
|
26
|
+
{ id: 'medium', label: 'Medium', note: 'Thinks before it answers' },
|
|
27
|
+
{ id: 'high', label: 'High', note: 'Slow and careful, for hard problems' },
|
|
28
|
+
];
|
|
29
|
+
export const FIRST = { model: ENSO, effort: 'medium' };
|
|
30
|
+
const KEY = 'hanzo.mind';
|
|
31
|
+
const CHANGE = 'hanzo:mind';
|
|
32
|
+
const effortOf = (v) => (EFFORTS.some((e) => e.id === v) ? v : FIRST.effort);
|
|
33
|
+
/** The store, or null where the browser gives none (a server render, blocked site data). */
|
|
34
|
+
function store() {
|
|
35
|
+
try {
|
|
36
|
+
return typeof window === 'undefined' ? null : window.localStorage;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let raw;
|
|
43
|
+
let held = FIRST;
|
|
44
|
+
/** A choice the browser would not store, held for the page. */
|
|
45
|
+
let memo = null;
|
|
46
|
+
/** The kept choice, Enso and Medium where nothing is kept. Stable while the stored text is. */
|
|
47
|
+
export function kept() {
|
|
48
|
+
if (memo)
|
|
49
|
+
return memo;
|
|
50
|
+
let now = null;
|
|
51
|
+
try {
|
|
52
|
+
now = store()?.getItem(KEY) ?? null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
now = null;
|
|
56
|
+
}
|
|
57
|
+
if (now === raw)
|
|
58
|
+
return held;
|
|
59
|
+
raw = now;
|
|
60
|
+
try {
|
|
61
|
+
const v = JSON.parse(now ?? 'null');
|
|
62
|
+
held = { model: typeof v?.model === 'string' && v.model.trim() ? v.model : FIRST.model, effort: effortOf(v?.effort) };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
held = FIRST;
|
|
66
|
+
}
|
|
67
|
+
return held;
|
|
68
|
+
}
|
|
69
|
+
/** Keep a change, in this browser, for every surface that reads it. */
|
|
70
|
+
export function keep(patch) {
|
|
71
|
+
const next = { ...kept(), ...patch };
|
|
72
|
+
try {
|
|
73
|
+
store()?.setItem(KEY, JSON.stringify(next));
|
|
74
|
+
memo = null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// A browser that refuses storage keeps the choice for this page.
|
|
78
|
+
memo = next;
|
|
79
|
+
}
|
|
80
|
+
if (typeof window !== 'undefined')
|
|
81
|
+
window.dispatchEvent(new Event(CHANGE));
|
|
82
|
+
}
|
|
83
|
+
function subscribe(change) {
|
|
84
|
+
window.addEventListener(CHANGE, change);
|
|
85
|
+
window.addEventListener('storage', change);
|
|
86
|
+
return () => {
|
|
87
|
+
window.removeEventListener(CHANGE, change);
|
|
88
|
+
window.removeEventListener('storage', change);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** The kept choice and the way to change it. The server and the first render read the default. */
|
|
92
|
+
export function useChoice() {
|
|
93
|
+
return [useSyncExternalStore(subscribe, kept, () => FIRST), keep];
|
|
94
|
+
}
|
|
95
|
+
/** Whether `model` takes an effort: the router and an undescribed model do, a listed one only if it reasons. */
|
|
96
|
+
export function reasons(model, catalog) {
|
|
97
|
+
if (model === ENSO)
|
|
98
|
+
return true;
|
|
99
|
+
const row = catalog.find((m) => m.id === model);
|
|
100
|
+
return !row || row.supports_reasoning === true;
|
|
101
|
+
}
|
|
102
|
+
/** A model's name as the catalog spells it; Enso for the router. */
|
|
103
|
+
export function nameOf(model, catalog) {
|
|
104
|
+
if (model === ENSO)
|
|
105
|
+
return 'Enso';
|
|
106
|
+
const row = catalog.find((m) => m.id === model);
|
|
107
|
+
return modelName(row ?? { id: model });
|
|
108
|
+
}
|
|
109
|
+
// The catalog, once per platform for the page: Chat and Dev read the same list.
|
|
110
|
+
const catalogs = new Map();
|
|
111
|
+
const settled = new Map();
|
|
112
|
+
/** Where this browser keeps the last list it read. */
|
|
113
|
+
const LAST = 'hanzo.build.models';
|
|
114
|
+
/** The router alone: what a chooser offers before any list was ever read, so it reads "Enso" and not an id. */
|
|
115
|
+
const ROUTER = parseModels([{ id: ENSO, name: 'Enso', family: 'enso', class: 'ours' }]);
|
|
116
|
+
/** The last list this browser read, or none. */
|
|
117
|
+
function last() {
|
|
118
|
+
try {
|
|
119
|
+
return parseModels(JSON.parse(store()?.getItem(LAST) ?? 'null'));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Which catalog a target reads: the platform, and whether anyone is signed in. */
|
|
126
|
+
const shelf = (t) => `${t.api}|${t.token() ? 'in' : 'out'}`;
|
|
127
|
+
/** `GET /v1/models` for a target, asked once and shared; a refusal is not kept, so the next ask goes again. */
|
|
128
|
+
export function catalog(t) {
|
|
129
|
+
const at = shelf(t);
|
|
130
|
+
let ask = catalogs.get(at);
|
|
131
|
+
if (!ask) {
|
|
132
|
+
ask = read(t).then((models) => {
|
|
133
|
+
settled.set(at, models);
|
|
134
|
+
try {
|
|
135
|
+
if (models.length)
|
|
136
|
+
store()?.setItem(LAST, JSON.stringify(models));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// The next page reads the list again.
|
|
140
|
+
}
|
|
141
|
+
return models;
|
|
142
|
+
});
|
|
143
|
+
catalogs.set(at, ask);
|
|
144
|
+
ask.catch(() => catalogs.delete(at));
|
|
145
|
+
}
|
|
146
|
+
return ask;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The platform's catalog, as a hook: what was read already is there on the
|
|
150
|
+
* first render. The gateway answers 429 once the org's quota is spent, and a
|
|
151
|
+
* list that vanishes then leaves nothing to choose from, so a failed read shows
|
|
152
|
+
* the last list this browser kept, with a note; with none kept, Enso alone, and
|
|
153
|
+
* the reason in the chooser — never the platform's own words.
|
|
154
|
+
*/
|
|
155
|
+
export function useCatalog(t) {
|
|
156
|
+
const at = shelf(t);
|
|
157
|
+
const [state, setState] = useState(() => settled.has(at) ? { at, models: settled.get(at), error: null } : null);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
let live = true;
|
|
160
|
+
catalog(t).then((models) => live && setState({ at, models, error: null }), (e) => live && setState({ at, models: [], error: e }));
|
|
161
|
+
return () => {
|
|
162
|
+
live = false;
|
|
163
|
+
};
|
|
164
|
+
// The target is read through `at`: the origin and whether anyone is signed in.
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
}, [at]);
|
|
167
|
+
const now = state?.at === at ? state : settled.has(at) ? { at, models: settled.get(at), error: null } : null;
|
|
168
|
+
if (!now?.error)
|
|
169
|
+
return { models: now?.models ?? [], list: now?.models ?? [], loading: !now, error: null, note: '' };
|
|
170
|
+
const kept = last();
|
|
171
|
+
if (kept.length)
|
|
172
|
+
return { models: kept, list: kept, loading: false, error: null, note: 'The model list could not be refreshed; these are the models as last read.' };
|
|
173
|
+
const spent = now.error instanceof Refusal && now.error.status === 429;
|
|
174
|
+
return {
|
|
175
|
+
models: ROUTER,
|
|
176
|
+
list: [],
|
|
177
|
+
loading: false,
|
|
178
|
+
error: spent ? 'The model list is resting: this organization’s quota is spent.' : 'The model list could not be read right now.',
|
|
179
|
+
note: '',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function useMind(t) {
|
|
183
|
+
const [choice, change] = useChoice();
|
|
184
|
+
const { models, list, loading, error, note } = useCatalog(t);
|
|
185
|
+
const [model, pick] = usePick(choice.model, (id) => change({ model: id }), list);
|
|
186
|
+
return { model, effort: choice.effort, models, loading, error, note, pick, pace: (effort) => change({ effort }) };
|
|
187
|
+
}
|
package/lib/nav.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RailLink } from '@hanzo/ui/chat';
|
|
2
|
+
import type { Host } from './host.tsx';
|
|
3
|
+
/** A run of places under one quiet label. */
|
|
4
|
+
export interface Group {
|
|
5
|
+
id: 'work' | 'make' | 'run' | 'setup';
|
|
6
|
+
label: string;
|
|
7
|
+
links: RailLink[];
|
|
8
|
+
}
|
|
9
|
+
/** The id of the row that starts a new run: a rail with a New control of its own draws that instead. */
|
|
10
|
+
export declare const NEW = "new";
|
|
11
|
+
export declare function nav(host: Host, go: (path: string) => void): Group[];
|
package/lib/nav.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The builder's places, once, for every rail that lists them: the builder's own
|
|
4
|
+
* rail (builder.tsx), `DevSection` (section.tsx) and a host's column (the Hanzo
|
|
5
|
+
* app's, @hanzo/rooms Column). Each place is a first-class row — nothing hides
|
|
6
|
+
* behind a disclosure — grouped in the order a first-time customer asks:
|
|
7
|
+
* what do I do, what did it make, where does it run, how do I set it up.
|
|
8
|
+
*
|
|
9
|
+
* Every row moves the builder through `go` with the address `path` writes, so
|
|
10
|
+
* where a place lives is said here and in route.ts and nowhere else.
|
|
11
|
+
*/
|
|
12
|
+
import { Blocks, BookOpen, CircleDot, Container, Cpu, FolderGit2, LayoutTemplate, SlidersHorizontal, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
|
|
13
|
+
import { pinBoard } from './choice.js';
|
|
14
|
+
import { DOCS } from './foot.js';
|
|
15
|
+
import { path, route } from './route.js';
|
|
16
|
+
/** The id of the row that starts a new run: a rail with a New control of its own draws that instead. */
|
|
17
|
+
export const NEW = 'new';
|
|
18
|
+
export function nav(host, go) {
|
|
19
|
+
const r = route(host.path);
|
|
20
|
+
const screen = r.kind === 'screen' ? r.screen : '';
|
|
21
|
+
const section = r.kind === 'settings' ? r.section : '';
|
|
22
|
+
const to = (p) => () => go(p);
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: 'work',
|
|
26
|
+
label: 'Work',
|
|
27
|
+
links: [
|
|
28
|
+
{ id: NEW, label: 'New run', icon: _jsx(SquarePen, { size: 16 }), onPress: to(''), active: r.kind === 'new' },
|
|
29
|
+
// A project is a repository: its list, linking one from GitHub, and one's workspace.
|
|
30
|
+
{
|
|
31
|
+
id: 'projects',
|
|
32
|
+
label: 'Projects',
|
|
33
|
+
icon: _jsx(FolderGit2, { size: 16 }),
|
|
34
|
+
onPress: to(path({ kind: 'screen', screen: 'projects' })),
|
|
35
|
+
active: screen === 'projects' || screen === 'sync' || r.kind === 'repo',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'issues',
|
|
39
|
+
label: 'Issues',
|
|
40
|
+
icon: _jsx(CircleDot, { size: 16 }),
|
|
41
|
+
onPress: () => {
|
|
42
|
+
pinBoard(host.org, '');
|
|
43
|
+
go(path({ kind: 'screen', screen: 'issues' }));
|
|
44
|
+
},
|
|
45
|
+
active: screen === 'issues',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'make',
|
|
51
|
+
label: 'Make',
|
|
52
|
+
links: [
|
|
53
|
+
{ id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
|
|
54
|
+
{ id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
|
|
55
|
+
{ id: 'automations', label: 'Automations', icon: _jsx(Workflow, { size: 16 }), onPress: to(path({ kind: 'screen', screen: 'automations' })), active: screen === 'automations' || r.kind === 'automation' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'run',
|
|
60
|
+
label: 'Run',
|
|
61
|
+
links: [
|
|
62
|
+
{ id: 'machines', label: 'Machines', icon: _jsx(Cpu, { size: 16 }), onPress: to(path({ kind: 'settings', section: 'machines' })), active: section === 'machines' },
|
|
63
|
+
{ id: 'environments', label: 'Environments', icon: _jsx(Container, { size: 16 }), onPress: to(path({ kind: 'settings', section: 'environments' })), active: section === 'environments' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'setup',
|
|
68
|
+
label: 'Setup',
|
|
69
|
+
links: [
|
|
70
|
+
{ id: 'customize', label: 'Customize', icon: _jsx(SlidersHorizontal, { size: 16 }), onPress: to(path({ kind: 'customize', tab: 'skills' })), active: r.kind === 'customize' || screen === 'mcp' },
|
|
71
|
+
{ id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open(DOCS, '_blank', 'noopener,noreferrer') },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
package/lib/panel.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Deck, type Tab } from './tabs.ts';
|
|
3
|
+
/** A kind of tab the surface offers. */
|
|
4
|
+
export interface Kind {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
render: () => ReactNode;
|
|
8
|
+
/** Drawn edge to edge, scrolling itself: a terminal, a frame. */
|
|
9
|
+
bleed?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The header's Panel button: a word beside the mark while the panel is shut on
|
|
13
|
+
* a laptop, because a glyph alone was a control nobody found; the mark alone
|
|
14
|
+
* below `lg`, where the header has no room for the word, and while it is open,
|
|
15
|
+
* when the panel and its own close are in view (below `lg` it steps aside).
|
|
16
|
+
*/
|
|
17
|
+
export declare function PanelToggle({ open, onToggle }: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onToggle: () => void;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* A page tab: an address with its bar, or bytes rendered as their type.
|
|
23
|
+
*
|
|
24
|
+
* Two sandboxes. Bytes and an object URL take THIS page's origin, so a script
|
|
25
|
+
* an answer wrote runs without `allow-same-origin`, never with our storage. A
|
|
26
|
+
* remote page takes its own origin, and withholding same-origin breaks it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Page({ tab, onGo }: {
|
|
29
|
+
tab: Tab;
|
|
30
|
+
onGo: (url: string) => void;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
export interface PanelProps {
|
|
33
|
+
side: Deck;
|
|
34
|
+
kinds: readonly Kind[];
|
|
35
|
+
onHide: () => void;
|
|
36
|
+
/** A sheet over the page (a phone) rather than the host's column. */
|
|
37
|
+
sheet?: boolean;
|
|
38
|
+
/** Under the tabs, over every tab: a line about what they all read (Dev's sandbox). */
|
|
39
|
+
bar?: ReactNode;
|
|
40
|
+
/** At the strip's end, before hide: the surface's own menu. */
|
|
41
|
+
menu?: ReactNode;
|
|
42
|
+
label?: string;
|
|
43
|
+
}
|
|
44
|
+
/** The panel: the strip, and the chosen tab. */
|
|
45
|
+
export declare function Panel({ side, kinds, onHide, sheet, bar, menu, label }: PanelProps): import("react").JSX.Element;
|
package/lib/panel.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The side panel, once, for Chat and Dev: tabs over what a conversation or a
|
|
4
|
+
* run made, read and served (tabs.ts), opened and shut in one press — the
|
|
5
|
+
* header's Panel button (`PanelToggle`), or ⌘. / Ctrl+. (`useKey`).
|
|
6
|
+
*
|
|
7
|
+
* The column the panel stands in is the host's: its width and edge. On a phone
|
|
8
|
+
* the panel is a sheet over the page instead (`sheet`).
|
|
9
|
+
*/
|
|
10
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { ExternalLink, PanelRight, PanelRightClose, Plus, RotateCw, X } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { DropdownMenu } from '@hanzo/ui';
|
|
13
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { createPortal } from 'react-dom';
|
|
15
|
+
import { address, PAGE } from './tabs.js';
|
|
16
|
+
/** The shortcut, as this platform writes it. */
|
|
17
|
+
const KEY = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? '⌘.' : 'Ctrl+.';
|
|
18
|
+
/**
|
|
19
|
+
* The header's Panel button: a word beside the mark while the panel is shut on
|
|
20
|
+
* a laptop, because a glyph alone was a control nobody found; the mark alone
|
|
21
|
+
* below `lg`, where the header has no room for the word, and while it is open,
|
|
22
|
+
* when the panel and its own close are in view (below `lg` it steps aside).
|
|
23
|
+
*/
|
|
24
|
+
export function PanelToggle({ open, onToggle }) {
|
|
25
|
+
return (_jsxs(XStack, { render: "button", role: "button", "data-slot": "side-toggle", "aria-label": open ? 'Close the side panel' : 'Open the side panel', "aria-pressed": open, "aria-keyshortcuts": "Meta+Period Control+Period", ...{ title: `${open ? 'Close' : 'Open'} the side panel (${KEY})` }, onPress: onToggle, items: "center", gap: "$1.5", px: "$2", $lg: { px: open ? '$2' : '$2.5' }, "$max-lg": open ? { display: 'none' } : undefined, height: 30, rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: open ? '$hover' : 'transparent', cursor: "pointer", shrink: 0, hoverStyle: { bg: '$hover' }, children: [open ? _jsx(PanelRightClose, { size: 14 }) : _jsx(PanelRight, { size: 14 }), open ? null : (_jsx(SizableText, { size: "$2", color: "$ink", "$max-lg": { display: 'none' }, children: "Panel" }))] }));
|
|
26
|
+
}
|
|
27
|
+
const suffix = (s) => {
|
|
28
|
+
const dot = s.lastIndexOf('.');
|
|
29
|
+
return dot > 0 ? s.slice(dot + 1).toLowerCase() : '';
|
|
30
|
+
};
|
|
31
|
+
const PICTURES = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg'];
|
|
32
|
+
const FRAMED = ['html', 'htm', 'txt', 'md', 'json', 'csv', 'pdf', 'css', 'js', 'ts'];
|
|
33
|
+
/**
|
|
34
|
+
* A page tab: an address with its bar, or bytes rendered as their type.
|
|
35
|
+
*
|
|
36
|
+
* Two sandboxes. Bytes and an object URL take THIS page's origin, so a script
|
|
37
|
+
* an answer wrote runs without `allow-same-origin`, never with our storage. A
|
|
38
|
+
* remote page takes its own origin, and withholding same-origin breaks it.
|
|
39
|
+
*/
|
|
40
|
+
export function Page({ tab, onGo }) {
|
|
41
|
+
const own = tab.body !== undefined;
|
|
42
|
+
const src = useMemo(() => (own ? URL.createObjectURL(new Blob([tab.body], { type: tab.type || 'text/plain' })) : (tab.url ?? '')), [own, tab.body, tab.type, tab.url]);
|
|
43
|
+
useEffect(() => () => (own ? URL.revokeObjectURL(src) : undefined), [own, src]);
|
|
44
|
+
const [round, again] = useState(0);
|
|
45
|
+
const [typed, setTyped] = useState(tab.url ?? '');
|
|
46
|
+
const local = own || src.startsWith('blob:');
|
|
47
|
+
const type = tab.type ?? '';
|
|
48
|
+
const ext = suffix(tab.title);
|
|
49
|
+
const picture = type.startsWith('image/') || PICTURES.includes(ext);
|
|
50
|
+
const shows = !local || own || !type || type.startsWith('text/') || type === 'application/pdf' || type === 'application/json' || FRAMED.includes(ext);
|
|
51
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, "data-slot": "side-page", children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1.5", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(XStack, { render: "button", role: "button", "aria-label": "Reload this tab", onPress: () => again((n) => n + 1), p: "$1.5", rounded: "$2", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: _jsx(RotateCw, { size: 14 }) }), local ? (_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, children: tab.title })) : (_jsx(XStack, { flex: 1, minW: 0, items: "center", px: "$2", height: 28, rounded: "$2", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: _jsx("input", { defaultValue: tab.url ?? '', placeholder: "Type an address", "aria-label": "Address", spellCheck: false, autoFocus: !tab.url, onChange: (e) => setTyped(e.target.value), onKeyDown: (e) => {
|
|
52
|
+
if (e.key === 'Enter' && address(typed))
|
|
53
|
+
onGo(address(typed));
|
|
54
|
+
}, style: { width: '100%', border: 0, outline: 'none', background: 'transparent', color: 'var(--foreground)', font: 'inherit', fontSize: 13 } }, tab.id) })), src ? (_jsx(XStack, { render: "button", role: "button", "aria-label": "Open this tab in a new window", onPress: () => window.open(src, '_blank', 'noopener,noreferrer'), p: "$1.5", rounded: "$2", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 14 }) })) : null] }), !src ? (_jsx(YStack, { flex: 1, items: "center", justify: "center", p: "$5", children: _jsx(SizableText, { size: "$2", color: "$soft", children: "Type an address to open a page here." }) })) : picture && !own ? (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflowY: "auto", overflowX: "hidden", children: _jsx("img", { src: src, alt: tab.title, style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' } }, round) })) : shows ? (_jsx("iframe", { src: src, title: tab.title, sandbox: local ? 'allow-scripts allow-popups allow-forms' : 'allow-same-origin allow-scripts allow-popups allow-forms', style: { border: 0, width: '100%', height: '100%', display: 'block', background: 'var(--background)' } }, round)) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${(ext || type || 'This').toUpperCase()} previews aren’t supported yet.` }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Open it in a new window to view it." })] }))] }));
|
|
55
|
+
}
|
|
56
|
+
const DRAG = 'text/x-hanzo-tab';
|
|
57
|
+
/** One tab in the strip: what you press, and its cross. */
|
|
58
|
+
function Leaf({ tab, on, side, index, label }) {
|
|
59
|
+
return (_jsxs(XStack, { "data-slot": "side-tab", "data-tab": tab.id, items: "center", gap: "$0.5", pl: "$2.5", pr: "$1", height: 30, maxW: 160, shrink: 0, rounded: "$3", bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover' }, ...{
|
|
60
|
+
draggable: true,
|
|
61
|
+
onDragStart: (e) => {
|
|
62
|
+
e.dataTransfer.setData(DRAG, tab.id);
|
|
63
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
64
|
+
},
|
|
65
|
+
onDragOver: (e) => {
|
|
66
|
+
if (Array.from(e.dataTransfer.types).includes(DRAG))
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
},
|
|
69
|
+
onDrop: (e) => {
|
|
70
|
+
const id = e.dataTransfer.getData(DRAG);
|
|
71
|
+
if (!id || id === tab.id)
|
|
72
|
+
return;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
side.move(id, index);
|
|
75
|
+
},
|
|
76
|
+
}, children: [_jsx(XStack, { render: "button", role: "tab", id: `side-tab-${tab.id}`, "aria-selected": on, "aria-controls": "side-body", tabIndex: on ? 0 : -1, onPress: () => side.pick(tab.id), onKeyDown: (e) => {
|
|
77
|
+
const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
|
|
78
|
+
if (step && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
79
|
+
e.preventDefault?.();
|
|
80
|
+
side.move(tab.id, index + step);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (step) {
|
|
84
|
+
e.preventDefault?.();
|
|
85
|
+
const next = side.tabs[(index + step + side.tabs.length) % side.tabs.length];
|
|
86
|
+
if (!next)
|
|
87
|
+
return;
|
|
88
|
+
side.pick(next.id);
|
|
89
|
+
queueMicrotask(() => document.getElementById(`side-tab-${next.id}`)?.focus());
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
93
|
+
e.preventDefault?.();
|
|
94
|
+
side.shut(tab.id);
|
|
95
|
+
}
|
|
96
|
+
}, flex: 1, minW: 0, cursor: "pointer", children: _jsx(SizableText, { size: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: label }) }), _jsx(XStack, { render: "button", role: "button", "aria-label": `Close ${label}`, tabIndex: -1, onPress: () => side.shut(tab.id), p: "$1", rounded: "$2", cursor: "pointer", opacity: on ? 0.8 : 0.45, hoverStyle: { bg: '$hover', opacity: 1 }, children: _jsx(X, { size: 12 }) })] }));
|
|
97
|
+
}
|
|
98
|
+
/** The panel: the strip, and the chosen tab. */
|
|
99
|
+
export function Panel({ side, kinds, onHide, sheet = false, bar, menu, label = 'Side panel' }) {
|
|
100
|
+
const tab = side.tabs.find((t) => t.id === side.at) ?? null;
|
|
101
|
+
const kind = tab && tab.kind !== PAGE ? kinds.find((k) => k.id === tab.kind) : undefined;
|
|
102
|
+
const bleed = !tab || tab.kind === PAGE || Boolean(kind?.bleed);
|
|
103
|
+
const closed = kinds.filter((k) => !side.tabs.some((t) => t.kind === k.id));
|
|
104
|
+
const title = (t) => (t.kind === PAGE ? t.title || 'New page' : (kinds.find((k) => k.id === t.kind)?.label ?? t.title));
|
|
105
|
+
const body = (_jsxs(YStack, { role: "complementary", "aria-label": label, "data-slot": "side", flex: 1, minW: 0, minH: 0, overflow: "hidden", style: { borderRadius: 'inherit' }, children: [_jsxs(XStack, { px: "$2", py: "$1.5", gap: "$1", items: "center", borderBottomWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(XStack, { role: "tablist", "aria-label": "Tabs", flex: 1, minW: 0, gap: "$1", items: "center", overflowX: "auto", overflowY: "hidden", "$platform-web": { scrollbarWidth: 'none' }, children: side.tabs.map((t, i) => (_jsx(Leaf, { tab: t, on: t.id === side.at, side: side, index: i, label: title(t) }, t.id))) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", role: "button", "aria-label": "Open a tab", p: "$1.5", rounded: "$2", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16 }) }), items: [
|
|
106
|
+
...closed.map((k) => ({ key: k.id, label: k.label, onSelect: () => side.open({ kind: k.id, title: k.label }) })),
|
|
107
|
+
{ key: PAGE, label: 'Page', description: 'An address, framed here', onSelect: () => side.open({ kind: PAGE, title: 'New page', url: '' }) },
|
|
108
|
+
] }), menu, _jsx(XStack, { render: "button", role: "button", "aria-label": "Close the side panel", onPress: onHide, p: "$1.5", rounded: "$2", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: _jsx(PanelRightClose, { size: 16 }) })] }), bar, _jsx(YStack, { id: "side-body", role: "tabpanel", "aria-labelledby": tab ? `side-tab-${tab.id}` : undefined,
|
|
109
|
+
// What a tab lays over itself (a door's "Connecting…") stays inside it, under the strip.
|
|
110
|
+
position: "relative", flex: 1, minH: 0, overflowY: bleed ? 'hidden' : 'auto', overflowX: "hidden", px: bleed ? 0 : '$3', py: bleed ? 0 : '$3', children: !tab ? (_jsxs(YStack, { flex: 1, items: "center", justify: "center", gap: "$2", p: "$5", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing open here." }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Press + to open a tab." })] })) : tab.kind === PAGE ? (_jsx(Page, { tab: tab, onGo: (url) => side.go(tab.id, url) }, tab.id)) : kind ? (kind.render()) : (_jsx(SizableText, { size: "$2", color: "$soft", children: "This tab is not offered here." })) })] }));
|
|
111
|
+
if (!sheet || typeof document === 'undefined')
|
|
112
|
+
return body;
|
|
113
|
+
// A phone: a sheet from the foot of the window over a dim, the same panel in it.
|
|
114
|
+
return createPortal(_jsxs(YStack, { "data-slot": "side-sheet", position: "fixed", t: 0, r: 0, b: 0, l: 0, z: 60, children: [_jsx(YStack, { render: "button", "aria-label": "Close the side panel", onPress: onHide, position: "absolute", t: 0, r: 0, l: 0, height: "12dvh", bg: "var(--surface-scrim)" }), _jsx(YStack, { position: "absolute", l: 0, r: 0, b: 0, height: "88dvh", bg: "$background", borderTopWidth: 1, borderColor: "$borderColor", style: { borderTopLeftRadius: 16, borderTopRightRadius: 16 }, overflow: "hidden", children: body })] }), document.body);
|
|
115
|
+
}
|
package/lib/pick.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import { type Model } from './api/models.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What a run starts on with nothing picked on this screen: Enso when Enso is
|
|
4
|
+
* kept — the router serves every ask whether or not a catalog lists it — else
|
|
5
|
+
* the kept model while the catalog still offers it (or has not answered yet),
|
|
6
|
+
* else the catalog's own default for a conversation, else Enso. Never '' — an
|
|
7
|
+
* empty chooser reads "Choose a model" and the run would name nothing on purpose.
|
|
8
|
+
*/
|
|
9
|
+
export declare function standing(kept: string, catalog: readonly Model[]): string;
|
|
2
10
|
export declare function usePick(kept: string, keep: (id: string) => void, catalog: readonly Model[]): readonly [string, (id: string) => void];
|
package/lib/pick.js
CHANGED
|
@@ -7,7 +7,24 @@
|
|
|
7
7
|
* before this rule) is not a default either, so Enso stands in for it.
|
|
8
8
|
*/
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
+
import { defaultModel } from '@hanzo/ui/models/catalog';
|
|
10
11
|
import { ENSO } from './api/models.js';
|
|
12
|
+
/**
|
|
13
|
+
* What a run starts on with nothing picked on this screen: Enso when Enso is
|
|
14
|
+
* kept — the router serves every ask whether or not a catalog lists it — else
|
|
15
|
+
* the kept model while the catalog still offers it (or has not answered yet),
|
|
16
|
+
* else the catalog's own default for a conversation, else Enso. Never '' — an
|
|
17
|
+
* empty chooser reads "Choose a model" and the run would name nothing on purpose.
|
|
18
|
+
*/
|
|
19
|
+
export function standing(kept, catalog) {
|
|
20
|
+
if (kept === ENSO)
|
|
21
|
+
return ENSO;
|
|
22
|
+
const premium = catalog.find((m) => m.id === kept)?.class === 'premium';
|
|
23
|
+
const offered = catalog.length === 0 || catalog.some((m) => m.id === kept);
|
|
24
|
+
if (kept && !premium && offered)
|
|
25
|
+
return kept;
|
|
26
|
+
return defaultModel(catalog, 'chat') || ENSO;
|
|
27
|
+
}
|
|
11
28
|
export function usePick(kept, keep, catalog) {
|
|
12
29
|
const [once, setOnce] = useState(null);
|
|
13
30
|
const premium = (id) => catalog.find((m) => m.id === id)?.class === 'premium';
|
|
@@ -17,5 +34,5 @@ export function usePick(kept, keep, catalog) {
|
|
|
17
34
|
setOnce(null);
|
|
18
35
|
keep(id);
|
|
19
36
|
};
|
|
20
|
-
return [once ?? (
|
|
37
|
+
return [once ?? standing(kept, catalog), pick];
|
|
21
38
|
}
|
package/lib/plan.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
export interface Label {
|
|
19
|
+
/** The family: `Max`. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** The rung inside it, drawn small beside the name: `20x`; '' for none. */
|
|
22
|
+
tag: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A plan as a person reads it, from the slug billing serves (`max-20x`) and the
|
|
26
|
+
* tier class beside it (`pro`). Null where neither names a plan: no slug and a
|
|
27
|
+
* free or trial tier, which is no plan at all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function label(slug?: string | null, tier?: string | null): Label | null;
|
|
30
|
+
/** A label as a sentence says it: `Max 20x`. */
|
|
31
|
+
export declare const said: (l: Label | null) => string;
|
|
32
|
+
export type State = 'ok' | 'near' | 'limited';
|
|
33
|
+
/** One window, as a share used. */
|
|
34
|
+
export interface Share {
|
|
35
|
+
/** Used, 0 to 100. */
|
|
36
|
+
percent: number;
|
|
37
|
+
state: State;
|
|
38
|
+
/** When it starts over, RFC 3339; '' for a session not running, which starts with the next request. */
|
|
39
|
+
resets: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One window off the wire, or null for a window that bounds nothing. The
|
|
43
|
+
* server states a window's state only when the plan sets it a limit
|
|
44
|
+
* (limits_ops.go `window`), so a window with no state is unbounded and not drawn.
|
|
45
|
+
*/
|
|
46
|
+
export declare function share(v: unknown): Share | null;
|
|
47
|
+
/** Which meter an account reads. */
|
|
48
|
+
export type Kind = 'plan' | 'free' | 'credits';
|
|
49
|
+
/**
|
|
50
|
+
* The meter for a plan slug (billing's or the limits', `free` or '' for none),
|
|
51
|
+
* the prepaid balance in cents (null where it could not be read) and the tier
|
|
52
|
+
* class billing serves the account under: a paid plan's windows — a rung, or a
|
|
53
|
+
* paid class with no rung (a plan recorded outside checkout, a partner org) —
|
|
54
|
+
* and with no plan, the balance where there is money in it, else the free
|
|
55
|
+
* allowance.
|
|
56
|
+
*/
|
|
57
|
+
export declare function kind(plan: string | null | undefined, balance: number | null, tier?: string | null): Kind;
|
|
58
|
+
/**
|
|
59
|
+
* When something starts over, as a sentence says it: a time for within a day
|
|
60
|
+
* (`5:00 PM`), else a date (`Nov 1`), periods ending on UTC days. '' for none.
|
|
61
|
+
*/
|
|
62
|
+
export declare function when(iso: string, now?: number): string;
|
|
63
|
+
/**
|
|
64
|
+
* What a reader whose plan's included usage is used is told: the plan, which
|
|
65
|
+
* models, and when they come back — `Your Max 20x plan’s included premium model
|
|
66
|
+
* usage is used until 5:00 PM.`
|
|
67
|
+
*/
|
|
68
|
+
export declare function spent(l: Label | null, classes: readonly string[], resets: string, now?: number): string;
|
|
69
|
+
/**
|
|
70
|
+
* The ways past a spent allowance, in this order and two at most: keep going
|
|
71
|
+
* on credits — or, with none the server would let pay, add some — then the
|
|
72
|
+
* upgrade; and a switch to another model where the server offers one. Taken
|
|
73
|
+
* from what the server sent, so nothing is offered that it would refuse.
|
|
74
|
+
*/
|
|
75
|
+
export declare function ways<A extends {
|
|
76
|
+
kind: string;
|
|
77
|
+
label: string;
|
|
78
|
+
}>(actions: readonly A[]): A[];
|