@hanzo/build 0.2.60 → 0.2.62
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 +13 -3
- package/lib/api/conversations.d.ts +20 -0
- package/lib/api/conversations.js +23 -0
- package/lib/api/pref.d.ts +10 -0
- package/lib/api/pref.js +7 -0
- package/lib/builder.js +2 -2
- package/lib/carry.d.ts +17 -0
- package/lib/carry.js +95 -0
- package/lib/gear.d.ts +53 -0
- package/lib/gear.js +141 -0
- package/lib/index.d.ts +22 -5
- package/lib/index.js +20 -5
- package/lib/landing.js +30 -10
- package/lib/mic.d.ts +10 -0
- package/lib/mic.js +95 -0
- package/lib/mind.d.ts +18 -1
- package/lib/mind.js +48 -5
- package/lib/panel.js +14 -7
- package/lib/plugin.d.ts +56 -0
- package/lib/plugin.js +1 -0
- package/lib/plus.d.ts +46 -0
- package/lib/plus.js +58 -0
- package/lib/prefs.d.ts +14 -1
- package/lib/prefs.js +13 -6
- package/lib/project.js +4 -7
- package/lib/prompt.d.ts +68 -19
- package/lib/prompt.js +172 -95
- package/lib/run.js +14 -44
- package/lib/side.d.ts +68 -0
- package/lib/side.js +195 -0
- package/lib/suggest.d.ts +202 -0
- package/lib/suggest.js +329 -0
- package/lib/suggestions.d.ts +20 -0
- package/lib/suggestions.js +170 -0
- package/package.json +3 -3
- package/lib/grip.d.ts +0 -40
- package/lib/grip.js +0 -108
package/README.md
CHANGED
|
@@ -111,6 +111,12 @@ One of each, shared with Chat (@hanzo/rooms imports them):
|
|
|
111
111
|
- `prompt` / `Prompt` — the composer, as wide as the text above it (`measure`).
|
|
112
112
|
- `Reply` — copy, Listen and Open under an answer.
|
|
113
113
|
- `Panel` over `useDeck` — the side panel and its kept tabs.
|
|
114
|
+
- `useSplit` / `SideProvider` — the column the panel stands in: its edge (the
|
|
115
|
+
kit's `Grip`), the width the person drags kept in their preferences
|
|
116
|
+
(`side.width`, `PATCH /v1/pref`), the middle column hidden when the panel is
|
|
117
|
+
pulled over it (Show chat brings it back), and full screen from the strip
|
|
118
|
+
(Escape returns). `PrefsProvider` above it keeps the width; without one it
|
|
119
|
+
lasts the page.
|
|
114
120
|
|
|
115
121
|
## Addresses
|
|
116
122
|
|
|
@@ -249,8 +255,11 @@ Files, Diff (Diff, Review, Commits) and Terminal (a shell in the working tree,
|
|
|
249
255
|
reattached by the run's name, and the agent's log); `+` opens Desktop (the
|
|
250
256
|
sandbox's screen), Environment and any page. Tabs are dragged to reorder (or
|
|
251
257
|
moved with Ctrl/⌘+Shift+←/→), shut with their cross, and kept as left; the
|
|
252
|
-
header's Panel button and ⌘. / Ctrl+. open and shut it, and a phone opens it
|
|
253
|
-
|
|
258
|
+
header's Panel button and ⌘. / Ctrl+. open and shut it, and a phone opens it
|
|
259
|
+
over the whole window. Its edge sizes it, the width kept in the person's
|
|
260
|
+
preferences; pulled over the run it takes the run's room until Show run or a
|
|
261
|
+
pull back out, and its strip's button puts it full screen until Escape. An
|
|
262
|
+
answer's Open shows what it wrote there, and a publish opens Preview.
|
|
254
263
|
|
|
255
264
|
The desktop and the shell are the sandbox's, so they close when the run stops.
|
|
256
265
|
The setup bar says how long setup runs here have taken, once there are three.
|
|
@@ -305,7 +314,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
|
|
|
305
314
|
| `GET /v1/tool/plugins/authored` · `POST /v1/tool/plugins/build` · `DELETE …/authored/{id}` · `GET /v1/tool/plugins` | plugins, and what is mounted |
|
|
306
315
|
| `GET`/`POST /v1/agent` · `GET`/`PATCH`/`DELETE /v1/agent/{ref}` · `GET /v1/agent/chat/presets` | agents, and presets |
|
|
307
316
|
| `GET /v1/models` · `POST /v1/event` | the model list, a verdict |
|
|
308
|
-
| `GET`/`PATCH /v1/pref` | the person's own settings: theme, text size, motion, dictation language, what to call them, their instructions, the coding defaults |
|
|
317
|
+
| `GET`/`PATCH /v1/pref` | the person's own settings: theme, text size, motion, dictation language, what to call them, their instructions, the coding defaults, the side panel's width (`side`) |
|
|
309
318
|
| `PUT /v1/iam/account` · `POST /v1/account/avatar` | the person's name and photo |
|
|
310
319
|
| `GET`/`PUT /v1/iam/consent` | whether Hanzo may train on their data, and usage insights |
|
|
311
320
|
| `GET /v1/tool` · `PUT /v1/tool/activation` | what the agent may use, per kind of tool |
|
|
@@ -345,6 +354,7 @@ src/
|
|
|
345
354
|
prompt.tsx the composer, and Tune, the model and effort chip
|
|
346
355
|
reply.tsx the row under an answer
|
|
347
356
|
panel.tsx the side panel; tabs.ts its kept tabs
|
|
357
|
+
side.tsx the column it stands in: width, the middle column, full screen
|
|
348
358
|
pane.ts how a pane is cut, from the --pane-* tokens
|
|
349
359
|
door.tsx a run's desktop or shell, framed
|
|
350
360
|
git.tsx what a run pushed
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person's Chat conversations, as the agent service keeps them (the list
|
|
3
|
+
* @hanzo/ai `threads.list` and @hanzo/rooms read).
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/agents/chat/conversations → {conversations: [{id, title, updatedAt, pinned, archived}]}
|
|
6
|
+
*
|
|
7
|
+
* Pinned first, then most recently spoken in; archived ones are left out. Only
|
|
8
|
+
* the caller's own: another member's conversations are never listed.
|
|
9
|
+
*/
|
|
10
|
+
import { type Target } from './call.ts';
|
|
11
|
+
export interface Conversation {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Taken from the opening message until renamed. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** RFC 3339: when it was last spoken in, or ''. */
|
|
16
|
+
updated: string;
|
|
17
|
+
pinned: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function conversation(raw: unknown): Conversation | null;
|
|
20
|
+
export declare function conversations(t: Target, signal?: AbortSignal): Promise<Conversation[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person's Chat conversations, as the agent service keeps them (the list
|
|
3
|
+
* @hanzo/ai `threads.list` and @hanzo/rooms read).
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/agents/chat/conversations → {conversations: [{id, title, updatedAt, pinned, archived}]}
|
|
6
|
+
*
|
|
7
|
+
* Pinned first, then most recently spoken in; archived ones are left out. Only
|
|
8
|
+
* the caller's own: another member's conversations are never listed.
|
|
9
|
+
*/
|
|
10
|
+
import { call } from './call.js';
|
|
11
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
12
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
13
|
+
export function conversation(raw) {
|
|
14
|
+
const o = obj(raw);
|
|
15
|
+
const id = str(o.id);
|
|
16
|
+
if (!id)
|
|
17
|
+
return null;
|
|
18
|
+
return { id, title: str(o.title), updated: str(o.updatedAt), pinned: o.pinned === true };
|
|
19
|
+
}
|
|
20
|
+
export async function conversations(t, signal) {
|
|
21
|
+
const raw = obj(await call(t, 'GET', '/v1/agents/chat/conversations', undefined, { signal }));
|
|
22
|
+
return (Array.isArray(raw.conversations) ? raw.conversations : []).map(conversation).filter((c) => c !== null);
|
|
23
|
+
}
|
package/lib/api/pref.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* key another surface saved is left as it was. A value this version does not
|
|
11
11
|
* recognise reads as unset rather than as a choice.
|
|
12
12
|
*/
|
|
13
|
+
import { type Kept } from '../suggest.ts';
|
|
13
14
|
import { type Target } from './call.ts';
|
|
14
15
|
export type Theme = 'system' | 'light' | 'dark';
|
|
15
16
|
export type Text = 'small' | 'medium' | 'large';
|
|
@@ -24,6 +25,11 @@ export interface Code {
|
|
|
24
25
|
/** A machine's id, or '' for the sandbox. */
|
|
25
26
|
place?: string;
|
|
26
27
|
}
|
|
28
|
+
/** The side panel beside Chat and Dev, once for both. */
|
|
29
|
+
export interface Side {
|
|
30
|
+
/** How wide it is drawn, px, while the middle column shows. */
|
|
31
|
+
width?: number;
|
|
32
|
+
}
|
|
27
33
|
export interface Prefs {
|
|
28
34
|
theme?: Theme;
|
|
29
35
|
text?: Text;
|
|
@@ -37,12 +43,16 @@ export interface Prefs {
|
|
|
37
43
|
/** What every coding run is told about how they like to work. */
|
|
38
44
|
instructions?: string;
|
|
39
45
|
code?: Code;
|
|
46
|
+
side?: Side;
|
|
47
|
+
/** The suggestions under the composer: when made, from what, the rows, and those dismissed (suggest.ts). */
|
|
48
|
+
suggest?: Kept;
|
|
40
49
|
}
|
|
41
50
|
/** A change: a key set to a value, or to null to delete it. */
|
|
42
51
|
export type Patch = {
|
|
43
52
|
[K in keyof Prefs]?: Prefs[K] | null;
|
|
44
53
|
};
|
|
45
54
|
export declare function code(raw: unknown): Code | undefined;
|
|
55
|
+
export declare function side(raw: unknown): Side | undefined;
|
|
46
56
|
/** The builder's keys from a whole preference document. */
|
|
47
57
|
export declare function prefs(raw: unknown): Prefs;
|
|
48
58
|
/** What a document reads as once `patch` is merged into it — the platform's own rule. */
|
package/lib/api/pref.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* key another surface saved is left as it was. A value this version does not
|
|
11
11
|
* recognise reads as unset rather than as a choice.
|
|
12
12
|
*/
|
|
13
|
+
import { kept } from '../suggest.js';
|
|
13
14
|
import { call } from './call.js';
|
|
14
15
|
const THEMES = ['system', 'light', 'dark'];
|
|
15
16
|
const TEXTS = ['small', 'medium', 'large'];
|
|
@@ -30,6 +31,10 @@ export function code(raw) {
|
|
|
30
31
|
});
|
|
31
32
|
return Object.keys(out).length ? out : undefined;
|
|
32
33
|
}
|
|
34
|
+
export function side(raw) {
|
|
35
|
+
const w = obj(raw).width;
|
|
36
|
+
return typeof w === 'number' && Number.isFinite(w) && w > 0 ? { width: Math.round(w) } : undefined;
|
|
37
|
+
}
|
|
33
38
|
/** The builder's keys from a whole preference document. */
|
|
34
39
|
export function prefs(raw) {
|
|
35
40
|
const p = obj(raw);
|
|
@@ -42,6 +47,8 @@ export function prefs(raw) {
|
|
|
42
47
|
work: text(p.work),
|
|
43
48
|
instructions: text(p.instructions),
|
|
44
49
|
code: code(p.code),
|
|
50
|
+
side: side(p.side),
|
|
51
|
+
suggest: kept(p.suggest),
|
|
45
52
|
});
|
|
46
53
|
}
|
|
47
54
|
/** What a document reads as once `patch` is merged into it — the platform's own rule. */
|
package/lib/builder.js
CHANGED
|
@@ -41,7 +41,7 @@ import { Forge } from './forge.js';
|
|
|
41
41
|
import { HostProvider, useHost, useTarget } from './host.js';
|
|
42
42
|
import { Landing } from './landing.js';
|
|
43
43
|
import { nav, NEW } from './nav.js';
|
|
44
|
-
import { PrefsProvider } from './prefs.js';
|
|
44
|
+
import { Look, PrefsProvider } from './prefs.js';
|
|
45
45
|
import { Project } from './project.js';
|
|
46
46
|
import { Plans } from './plans.js';
|
|
47
47
|
import { path, route } from './route.js';
|
|
@@ -153,5 +153,5 @@ function Screens({ rail }) {
|
|
|
153
153
|
* the builder is then a pane of the host's, and paints no background of its own.
|
|
154
154
|
*/
|
|
155
155
|
export function Builder({ host, rail = true }) {
|
|
156
|
-
return (_jsx(HostProvider, { host: host, children:
|
|
156
|
+
return (_jsx(HostProvider, { host: host, children: _jsxs(PrefsProvider, { children: [_jsx(Look, {}), _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: rail ? '$background' : undefined, children: _jsx(Screens, { rail: rail }) })] }) }));
|
|
157
157
|
}
|
package/lib/carry.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Target } from './api/call.ts';
|
|
2
|
+
import { type GearState } from './gear.ts';
|
|
3
|
+
import type { Shelf } from './plus.tsx';
|
|
4
|
+
import type { Chip } from './prompt.tsx';
|
|
5
|
+
export interface Carry {
|
|
6
|
+
gear: GearState;
|
|
7
|
+
skills: Shelf;
|
|
8
|
+
/** The chips after `+`: the skills on, Research, and Plan. */
|
|
9
|
+
on: Chip[];
|
|
10
|
+
/** The words the gear adds to every message, '' when none. */
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
15
|
+
* `customize(tab)` opens Dev's Customize on that tab.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void): Carry;
|
package/lib/carry.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What a message carries beside its words, assembled once for Chat and Dev:
|
|
4
|
+
* the conversation's gear (gear.ts) drawn as the `+` menu's shelves and the
|
|
5
|
+
* chips after `+`, and `text` — the words the gear adds to every message, read
|
|
6
|
+
* again whenever the gear changes.
|
|
7
|
+
*
|
|
8
|
+
* skills each picked skill's SKILL.md (the org's own, or the brand's
|
|
9
|
+
* catalogue document): the platform's completion takes no skills
|
|
10
|
+
* field, so the text rides the message
|
|
11
|
+
* memory what Hanzo remembers about the person (GET /v1/ai/memory/list):
|
|
12
|
+
* the platform injects none itself
|
|
13
|
+
* mode Plan: lay the plan out and wait; Auto says nothing
|
|
14
|
+
*
|
|
15
|
+
* A shelf is offered only where its service answers; a surface adds its own
|
|
16
|
+
* (Chat its agents, Dev its devices) beside these.
|
|
17
|
+
*/
|
|
18
|
+
import { BookOpen, ListChecks, ScrollText } from '@hanzogui/lucide-icons-2';
|
|
19
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
20
|
+
import { call, query, unwrap } from './api/call.js';
|
|
21
|
+
import { active, authored, document, nameOf as skillOf } from './api/skills.js';
|
|
22
|
+
import { useRead } from './data.js';
|
|
23
|
+
import { useGear } from './gear.js';
|
|
24
|
+
/** The most of each skill a message carries. */
|
|
25
|
+
const SKILL_MAX = 16_000;
|
|
26
|
+
/** The memories every message carries at most, newest first. */
|
|
27
|
+
const RECALL = 20;
|
|
28
|
+
/** What Hanzo remembers about the person, newest first, as lines. */
|
|
29
|
+
async function remembered(t) {
|
|
30
|
+
const rows = unwrap(await call(t, 'GET', `/v1/ai/memory/list${query({ limit: RECALL })}`));
|
|
31
|
+
return (Array.isArray(rows) ? rows : [])
|
|
32
|
+
.map((r) => (r && typeof r === 'object' ? String(r.content ?? '') : ''))
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
/** The words `skills`, `memory` and `mode` add to a message. */
|
|
36
|
+
async function said(t, skills, memory, mode) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (skills.length) {
|
|
39
|
+
const own = await authored(t).catch(() => []);
|
|
40
|
+
for (const n of skills) {
|
|
41
|
+
const body = own.find((s) => s.name === n)?.content || (await document(t, n).catch(() => ''));
|
|
42
|
+
if (body)
|
|
43
|
+
parts.push(`Use the skill "${n}" where it applies:\n\n${body.slice(0, SKILL_MAX)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (memory) {
|
|
47
|
+
const lines = await remembered(t).catch(() => []);
|
|
48
|
+
if (lines.length)
|
|
49
|
+
parts.push(`What you remember about the person:\n${lines.map((l) => `- ${l}`).join('\n')}`);
|
|
50
|
+
}
|
|
51
|
+
if (mode === 'plan')
|
|
52
|
+
parts.push('Plan mode: lay out the plan — the steps, what each changes and why — and stop. Change nothing and produce no final work until the person says go.');
|
|
53
|
+
return parts.join('\n\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
57
|
+
* `customize(tab)` opens Dev's Customize on that tab.
|
|
58
|
+
*/
|
|
59
|
+
export function useCarry(t, key, customize) {
|
|
60
|
+
const gear = useGear(t, key);
|
|
61
|
+
const signed = Boolean(t.token());
|
|
62
|
+
const list = useRead(signed ? () => active(t) : null, [], [t, signed]);
|
|
63
|
+
const names = useMemo(() => list.value.map((s) => skillOf(s.name)).filter(Boolean), [list.value]);
|
|
64
|
+
const { skills, research, memory, mode } = gear.gear;
|
|
65
|
+
const shelf = {
|
|
66
|
+
rows: names.map((n) => ({ id: n, label: n, icon: _jsx(ScrollText, { size: 16 }), on: skills.includes(n) })),
|
|
67
|
+
pick: (id) => gear.toggle('skills', id),
|
|
68
|
+
loading: list.loading,
|
|
69
|
+
error: list.error ? 'The skills could not be read.' : null,
|
|
70
|
+
search: names.length > 8 ? 'Search skills' : undefined,
|
|
71
|
+
empty: 'No skills are on for this organization',
|
|
72
|
+
manage: { label: 'Manage skills', onPress: () => customize('skills') },
|
|
73
|
+
browse: { label: 'Browse skills', onPress: () => customize('skills') },
|
|
74
|
+
};
|
|
75
|
+
const on = [
|
|
76
|
+
...skills.map((n) => ({ id: `skill:${n}`, label: n, icon: _jsx(ScrollText, { size: 14 }), onRemove: () => gear.toggle('skills', n) })),
|
|
77
|
+
...(research ? [{ id: 'research', label: 'Research', icon: _jsx(BookOpen, { size: 14 }), onRemove: () => gear.set({ research: false }) }] : []),
|
|
78
|
+
...(mode === 'plan' ? [{ id: 'plan', label: 'Plan', icon: _jsx(ListChecks, { size: 14 }), onRemove: () => gear.set({ mode: 'auto' }) }] : []),
|
|
79
|
+
];
|
|
80
|
+
const signedIn = Boolean(t.token());
|
|
81
|
+
const [text, setText] = useState('');
|
|
82
|
+
const asked = skills.join('\n');
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!signedIn)
|
|
85
|
+
return setText(mode === 'plan' ? 'Plan mode: lay out the plan and stop until the person says go.' : '');
|
|
86
|
+
let live = true;
|
|
87
|
+
void said(t, skills, memory, mode).then((x) => live && setText(x));
|
|
88
|
+
return () => {
|
|
89
|
+
live = false;
|
|
90
|
+
};
|
|
91
|
+
// `skills` is read through `asked`, its contents.
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [t, signedIn, asked, memory, mode]);
|
|
94
|
+
return { gear, skills: shelf, on, text };
|
|
95
|
+
}
|
package/lib/gear.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Target } from './api/call.ts';
|
|
2
|
+
/** What a run or a turn may do on its own: the permission mode. */
|
|
3
|
+
export type Mode = 'auto' | 'plan';
|
|
4
|
+
/**
|
|
5
|
+
* The modes the platform honours. Manual and Accept edits are not here: a cloud
|
|
6
|
+
* run takes `build` or `plan` (apps/coding) and a chat turn asks before nothing,
|
|
7
|
+
* so offering them would promise a pause that never comes.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MODES: readonly {
|
|
10
|
+
id: Mode;
|
|
11
|
+
label: string;
|
|
12
|
+
note: string;
|
|
13
|
+
}[];
|
|
14
|
+
export interface Gear {
|
|
15
|
+
/** Skill names (`/v1/tool/skills`). */
|
|
16
|
+
skills: string[];
|
|
17
|
+
/** Connector ids (`/v1/tool/mcp/servers`). */
|
|
18
|
+
connectors: string[];
|
|
19
|
+
/** Plugin ids. */
|
|
20
|
+
plugins: string[];
|
|
21
|
+
/** A design system's id, or ''. */
|
|
22
|
+
design: string;
|
|
23
|
+
/** A machine's id, or '' for none (Chat) or the sandbox (Dev). */
|
|
24
|
+
device: string;
|
|
25
|
+
research: boolean;
|
|
26
|
+
memory: boolean;
|
|
27
|
+
mode: Mode;
|
|
28
|
+
}
|
|
29
|
+
export declare const BARE: Gear;
|
|
30
|
+
/** How many conversations keep a choice of their own: the document holds 16 KiB in all. */
|
|
31
|
+
export declare const KEEP = 30;
|
|
32
|
+
/** A stored value as a Gear, a field the reader does not know read as unset. */
|
|
33
|
+
export declare function gearOf(raw: unknown, base?: Gear): Gear;
|
|
34
|
+
/** `gears` with `key`'s entry first, cut to `KEEP`. */
|
|
35
|
+
export declare function shelve(gears: Record<string, Gear>, key: string, g: Gear): Record<string, Gear>;
|
|
36
|
+
export interface GearState {
|
|
37
|
+
gear: Gear;
|
|
38
|
+
/** Change this conversation's choice. */
|
|
39
|
+
set: (patch: Partial<Gear>) => void;
|
|
40
|
+
/** Add or take out one id of a list. */
|
|
41
|
+
toggle: (field: 'skills' | 'connectors' | 'plugins', id: string) => void;
|
|
42
|
+
/** Make this conversation's choice the person's default. */
|
|
43
|
+
keep: () => void;
|
|
44
|
+
/** Whether a save was refused, in the platform's words. */
|
|
45
|
+
error: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The gear of the conversation `key` names (`''` for one not begun: it reads and
|
|
49
|
+
* writes the default until it has an id). Signed out, it lives for the page.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useGear(t: Target, key?: string): GearState;
|
|
52
|
+
/** The dictation language the person keeps, null for Auto, and how to change it. */
|
|
53
|
+
export declare function useLanguage(t: Target): readonly [string | null, (id: string | null) => void];
|
package/lib/gear.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a conversation carries beside its words: the skills, connectors and
|
|
3
|
+
* plugins picked from the composer's `+`, the design system, the device, the
|
|
4
|
+
* Research and Memory switches and the mode. Chat and Dev keep it the same way.
|
|
5
|
+
*
|
|
6
|
+
* GET /v1/pref {prefs: {gear, gears}}
|
|
7
|
+
* PATCH /v1/pref {gear} — the person's default, what a new conversation opens with
|
|
8
|
+
* {gears: {<key>: Gear}} — each conversation's own, newest first, at most `KEEP`
|
|
9
|
+
*
|
|
10
|
+
* Both live in the account, so a choice follows the person to every device. A
|
|
11
|
+
* conversation that has none of its own reads the default; changing one writes
|
|
12
|
+
* that conversation's, and "Keep as default" writes the default.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
15
|
+
import { call } from './api/call.js';
|
|
16
|
+
/**
|
|
17
|
+
* The modes the platform honours. Manual and Accept edits are not here: a cloud
|
|
18
|
+
* run takes `build` or `plan` (apps/coding) and a chat turn asks before nothing,
|
|
19
|
+
* so offering them would promise a pause that never comes.
|
|
20
|
+
*/
|
|
21
|
+
export const MODES = [
|
|
22
|
+
{ id: 'auto', label: 'Auto', note: 'Hanzo works on its own and stops to ask only if something looks unsafe.' },
|
|
23
|
+
{ id: 'plan', label: 'Plan', note: 'Hanzo lays out the plan first and changes nothing until you say go.' },
|
|
24
|
+
];
|
|
25
|
+
export const BARE = {
|
|
26
|
+
skills: [],
|
|
27
|
+
connectors: [],
|
|
28
|
+
plugins: [],
|
|
29
|
+
design: '',
|
|
30
|
+
device: '',
|
|
31
|
+
research: false,
|
|
32
|
+
memory: true,
|
|
33
|
+
mode: 'auto',
|
|
34
|
+
};
|
|
35
|
+
/** How many conversations keep a choice of their own: the document holds 16 KiB in all. */
|
|
36
|
+
export const KEEP = 30;
|
|
37
|
+
const list = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x !== '') : []);
|
|
38
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
39
|
+
/** A stored value as a Gear, a field the reader does not know read as unset. */
|
|
40
|
+
export function gearOf(raw, base = BARE) {
|
|
41
|
+
const o = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
42
|
+
return {
|
|
43
|
+
skills: 'skills' in o ? list(o.skills) : base.skills,
|
|
44
|
+
connectors: 'connectors' in o ? list(o.connectors) : base.connectors,
|
|
45
|
+
plugins: 'plugins' in o ? list(o.plugins) : base.plugins,
|
|
46
|
+
design: 'design' in o ? text(o.design) : base.design,
|
|
47
|
+
device: 'device' in o ? text(o.device) : base.device,
|
|
48
|
+
research: typeof o.research === 'boolean' ? o.research : base.research,
|
|
49
|
+
memory: typeof o.memory === 'boolean' ? o.memory : base.memory,
|
|
50
|
+
mode: MODES.some((m) => m.id === o.mode) ? o.mode : base.mode,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** `gears` with `key`'s entry first, cut to `KEEP`. */
|
|
54
|
+
export function shelve(gears, key, g) {
|
|
55
|
+
const out = { [key]: g };
|
|
56
|
+
for (const [k, v] of Object.entries(gears)) {
|
|
57
|
+
if (k === key)
|
|
58
|
+
continue;
|
|
59
|
+
if (Object.keys(out).length >= KEEP)
|
|
60
|
+
break;
|
|
61
|
+
out[k] = v;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
let doc = { gear: BARE, gears: {}, language: null };
|
|
66
|
+
let loaded = null;
|
|
67
|
+
const listeners = new Set();
|
|
68
|
+
const emit = () => listeners.forEach((f) => f());
|
|
69
|
+
const subscribe = (f) => {
|
|
70
|
+
listeners.add(f);
|
|
71
|
+
return () => listeners.delete(f);
|
|
72
|
+
};
|
|
73
|
+
function load(t) {
|
|
74
|
+
const id = `${t.api}|${t.org ?? ''}|${t.token() ? 1 : 0}`;
|
|
75
|
+
if (loaded === id || !t.token())
|
|
76
|
+
return;
|
|
77
|
+
loaded = id;
|
|
78
|
+
void call(t, 'GET', '/v1/pref')
|
|
79
|
+
.then((r) => {
|
|
80
|
+
const raw = r?.prefs ?? {};
|
|
81
|
+
const gear = gearOf(raw.gear);
|
|
82
|
+
const all = raw.gears && typeof raw.gears === 'object' ? raw.gears : {};
|
|
83
|
+
const language = typeof raw.language === 'string' && raw.language ? raw.language : null;
|
|
84
|
+
doc = { gear, gears: Object.fromEntries(Object.entries(all).map(([k, v]) => [k, gearOf(v, gear)])), language };
|
|
85
|
+
emit();
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {
|
|
88
|
+
loaded = null;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The gear of the conversation `key` names (`''` for one not begun: it reads and
|
|
93
|
+
* writes the default until it has an id). Signed out, it lives for the page.
|
|
94
|
+
*/
|
|
95
|
+
export function useGear(t, key = '') {
|
|
96
|
+
useEffect(() => load(t), [t]);
|
|
97
|
+
const now = useSyncExternalStore(subscribe, () => doc, () => doc);
|
|
98
|
+
const [error, setError] = useState(null);
|
|
99
|
+
const gear = key ? (now.gears[key] ?? now.gear) : now.gear;
|
|
100
|
+
const write = useCallback((g) => {
|
|
101
|
+
const fresh = key ? { ...doc, gears: shelve(doc.gears, key, g) } : { ...doc, gear: g };
|
|
102
|
+
doc = fresh;
|
|
103
|
+
emit();
|
|
104
|
+
if (!t.token())
|
|
105
|
+
return;
|
|
106
|
+
const patch = key ? { gears: fresh.gears } : { gear: g };
|
|
107
|
+
call(t, 'PATCH', '/v1/pref', patch).then(() => setError(null), (e) => setError(e instanceof Error ? e.message : 'Your choice was not saved.'));
|
|
108
|
+
}, [key, t]);
|
|
109
|
+
return useMemo(() => ({
|
|
110
|
+
gear,
|
|
111
|
+
error,
|
|
112
|
+
set: (patch) => write({ ...gear, ...patch }),
|
|
113
|
+
toggle: (field, id) => {
|
|
114
|
+
const has = gear[field].includes(id);
|
|
115
|
+
write({ ...gear, [field]: has ? gear[field].filter((x) => x !== id) : [...gear[field], id] });
|
|
116
|
+
},
|
|
117
|
+
keep: () => {
|
|
118
|
+
doc = { ...doc, gear };
|
|
119
|
+
emit();
|
|
120
|
+
if (t.token())
|
|
121
|
+
call(t, 'PATCH', '/v1/pref', { gear }).catch((e) => setError(e instanceof Error ? e.message : 'Not saved.'));
|
|
122
|
+
},
|
|
123
|
+
}), [gear, error, write, t]);
|
|
124
|
+
}
|
|
125
|
+
/** The dictation language the person keeps, null for Auto, and how to change it. */
|
|
126
|
+
export function useLanguage(t) {
|
|
127
|
+
useEffect(() => load(t), [t]);
|
|
128
|
+
const now = useSyncExternalStore(subscribe, () => doc, () => doc);
|
|
129
|
+
const set = useCallback((id) => {
|
|
130
|
+
const was = doc.language;
|
|
131
|
+
doc = { ...doc, language: id };
|
|
132
|
+
emit();
|
|
133
|
+
// A refused save takes the choice back, so what is shown is what is kept.
|
|
134
|
+
if (t.token())
|
|
135
|
+
call(t, 'PATCH', '/v1/pref', { language: id }).catch(() => {
|
|
136
|
+
doc = { ...doc, language: was };
|
|
137
|
+
emit();
|
|
138
|
+
});
|
|
139
|
+
}, [t]);
|
|
140
|
+
return [now.language, set];
|
|
141
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* of its own mounts it with `rail={false}` and draws the builder's parts in that
|
|
8
8
|
* rail: its places (`nav`), its runs (`useSessions`, or `DevSection` for both),
|
|
9
9
|
* and at the foot the Slack card and the account menu (`Slack`, `Who`), under
|
|
10
|
-
* `HostProvider`. `Grip`
|
|
11
|
-
*
|
|
10
|
+
* `HostProvider`. `Grip` (the kit's, passed on) sizes that rail.
|
|
11
|
+
*
|
|
12
|
+
* The side panel's column, for a host that draws it beside its own middle
|
|
13
|
+
* column (Chat): `useSplit`, `SideProvider` and `PrefsProvider` above both, so
|
|
14
|
+
* the width the person drags is kept in their preferences (side.tsx).
|
|
12
15
|
*
|
|
13
16
|
* The plan, once for every surface that shows one: `label` names it (`Max` with
|
|
14
17
|
* `20x` beside it), `useStanding` reads where an account stands, `Meter` is the
|
|
@@ -26,7 +29,7 @@ export { nav, NEW, type Group } from './nav.tsx';
|
|
|
26
29
|
export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
27
30
|
export { HostProvider } from './host.tsx';
|
|
28
31
|
export { Slack, useWho, Who } from './foot.tsx';
|
|
29
|
-
export { Grip, type GripProps } from '
|
|
32
|
+
export { Grip, type GripProps } from '@hanzo/ui';
|
|
30
33
|
export { administers } from './claims.ts';
|
|
31
34
|
export { Meter, Title } from './meter.tsx';
|
|
32
35
|
export { Plan } from './settings/plan.tsx';
|
|
@@ -45,9 +48,23 @@ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Scree
|
|
|
45
48
|
* (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
|
|
46
49
|
* over a `Deck` of tabs).
|
|
47
50
|
*/
|
|
48
|
-
export { catalog, EFFORTS, FIRST, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
|
|
49
|
-
export { cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, Tune, useEar, type PromptOptions } from './prompt.tsx';
|
|
51
|
+
export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
|
|
52
|
+
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
|
|
53
|
+
export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
|
|
54
|
+
export { BARE, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
|
|
55
|
+
export { useCarry, type Carry } from './carry.tsx';
|
|
56
|
+
export { Dictation } from './mic.tsx';
|
|
57
|
+
export type { Composer as PluginComposer, Plugin, Prep, Turn } from './plugin.ts';
|
|
50
58
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker, type Artifact, type Listen } from './reply.tsx';
|
|
51
59
|
export { frames, Page, Panel, PanelToggle, type Kind, type PageProps, type PanelProps, type Suggestion } from './panel.tsx';
|
|
60
|
+
export { SideProvider, useSplit, type Split } from './side.tsx';
|
|
61
|
+
export { PrefsProvider } from './prefs.tsx';
|
|
52
62
|
export { kinds, Summary, useKinds, type Entry, type Section } from './summary.tsx';
|
|
53
63
|
export { address, destination, located, name, PAGE, SEARCH, useDeck, useKey, usePanel, useShown, type Deck, type Tab, type Tabs } from './tabs.ts';
|
|
64
|
+
/**
|
|
65
|
+
* Up to three suggestions under the composer (above it on a phone), each from
|
|
66
|
+
* the person's own activity: `useSuggestions` reads and makes them, `Suggestions`
|
|
67
|
+
* draws them. The row is `Hint` here: `Suggestion` is the side panel's.
|
|
68
|
+
*/
|
|
69
|
+
export { Suggestions, useSuggestions, type Suggested, type SuggestionsProps } from './suggestions.tsx';
|
|
70
|
+
export type { Suggestion as Hint } from './suggest.ts';
|
package/lib/index.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* of its own mounts it with `rail={false}` and draws the builder's parts in that
|
|
8
8
|
* rail: its places (`nav`), its runs (`useSessions`, or `DevSection` for both),
|
|
9
9
|
* and at the foot the Slack card and the account menu (`Slack`, `Who`), under
|
|
10
|
-
* `HostProvider`. `Grip`
|
|
11
|
-
*
|
|
10
|
+
* `HostProvider`. `Grip` (the kit's, passed on) sizes that rail.
|
|
11
|
+
*
|
|
12
|
+
* The side panel's column, for a host that draws it beside its own middle
|
|
13
|
+
* column (Chat): `useSplit`, `SideProvider` and `PrefsProvider` above both, so
|
|
14
|
+
* the width the person drags is kept in their preferences (side.tsx).
|
|
12
15
|
*
|
|
13
16
|
* The plan, once for every surface that shows one: `label` names it (`Max` with
|
|
14
17
|
* `20x` beside it), `useStanding` reads where an account stands, `Meter` is the
|
|
@@ -26,7 +29,7 @@ export { nav, NEW } from './nav.js';
|
|
|
26
29
|
export { DevSection, DOTS, useSessions } from './section.js';
|
|
27
30
|
export { HostProvider } from './host.js';
|
|
28
31
|
export { Slack, useWho, Who } from './foot.js';
|
|
29
|
-
export { Grip } from '
|
|
32
|
+
export { Grip } from '@hanzo/ui';
|
|
30
33
|
export { administers } from './claims.js';
|
|
31
34
|
export { Meter, Title } from './meter.js';
|
|
32
35
|
export { Plan } from './settings/plan.js';
|
|
@@ -40,9 +43,21 @@ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
|
|
|
40
43
|
* (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
|
|
41
44
|
* over a `Deck` of tabs).
|
|
42
45
|
*/
|
|
43
|
-
export { catalog, EFFORTS, FIRST, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
|
|
44
|
-
export { cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, Tune, useEar } from './prompt.js';
|
|
46
|
+
export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
|
|
47
|
+
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
|
|
48
|
+
export { Plus } from './plus.js';
|
|
49
|
+
export { BARE, gearOf, MODES, useGear, useLanguage } from './gear.js';
|
|
50
|
+
export { useCarry } from './carry.js';
|
|
51
|
+
export { Dictation } from './mic.js';
|
|
45
52
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
|
|
46
53
|
export { frames, Page, Panel, PanelToggle } from './panel.js';
|
|
54
|
+
export { SideProvider, useSplit } from './side.js';
|
|
55
|
+
export { PrefsProvider } from './prefs.js';
|
|
47
56
|
export { kinds, Summary, useKinds } from './summary.js';
|
|
48
57
|
export { address, destination, located, name, PAGE, SEARCH, useDeck, useKey, usePanel, useShown } from './tabs.js';
|
|
58
|
+
/**
|
|
59
|
+
* Up to three suggestions under the composer (above it on a phone), each from
|
|
60
|
+
* the person's own activity: `useSuggestions` reads and makes them, `Suggestions`
|
|
61
|
+
* draws them. The row is `Hint` here: `Suggestion` is the side panel's.
|
|
62
|
+
*/
|
|
63
|
+
export { Suggestions, useSuggestions } from './suggestions.js';
|