@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 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 as
253
- a sheet. An answer's Open shows what it wrote there, and a publish opens Preview.
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: _jsx(PrefsProvider, { children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: rail ? '$background' : undefined, children: _jsx(Screens, { rail: rail }) }) }) }));
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` sizes that rail: the edge the builder sizes its own
11
- * chat with.
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 './grip.tsx';
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` sizes that rail: the edge the builder sizes its own
11
- * chat with.
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 './grip.js';
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';