@hanzo/build 0.2.83 → 0.2.84

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
@@ -119,12 +119,13 @@ One of each, shared with Chat (@hanzo/rooms imports them):
119
119
 
120
120
  - `useMind` — the model and effort: `GET /v1/models` read once per platform,
121
121
  and the choice kept in this browser (`hanzo.mind`, Enso and Auto until
122
- changed). `Tune` is the row that changes it: the effort (Auto, High, XHigh,
123
- Max) always, the model chip only where the host shows it (`Host.models`, a
124
- composer's `model`) — on hanzo.ai, Settings' model selector. Settings → Code
122
+ changed). The composer's row changes it: the effort (Auto, High, XHigh,
123
+ Max) always, the model chip only where the person turned the model selector
124
+ on (Settings → Appearance, `@hanzo/appearance` `composing`). Settings → Code
125
125
  writes the same choice. `@hanzo/build/mind` serves the store alone, without
126
- the builder.
127
- - `prompt` / `Prompt` — the composer, as wide as the text above it (`measure`).
126
+ the builder. `pacing(mind)` is the effort as the composer reads it.
127
+ - The composer is `@hanzo/ui/chat`'s `Composer`, once for Chat, Dev and Studio;
128
+ `useEar` is the platform's ear it dictates through.
128
129
  - `Reply` — copy, Listen and Open under an answer.
129
130
  - `Panel` over `useDeck` — the side panel and its kept tabs.
130
131
  - `useSplit` / `SideProvider` — the column the panel stands in: its edge (the
@@ -256,10 +257,12 @@ are any member's.
256
257
  (Cloud is the platform's sandbox with the codebase's environment; the org's
257
258
  machines follow under remote control, each linked with `hanzo link`), the
258
259
  codebase and the branch — the forge's repositories, filtered as you type, read
259
- again with Refresh list, and a way to Sync for a GitHub repository that is not
260
- there yet — then the ask, in the composer Chat draws too (`prompt`): the paperclip
261
- and Build or Plan at its start; the effort, the model where the host shows it
262
- (`Tune`), dictation and send at its end. A codebase can be added to a project. Opening a codebase or an
260
+ again each time the list opens, and a way to bring a GitHub repository that is
261
+ not there yet — then the ask, in the composer Chat draws too (`@hanzo/ui/chat`): the
262
+ paperclip and Build or Plan at its start; the effort, the model where the person
263
+ turned the selector on, dictation and send at its end. The three pills sit above
264
+ the field (environment, repository with its All / Organization / GitHub tabs, branch),
265
+ and Repos, Branches, Issues and Templates under it, each a press to its list. A codebase can be added to a project. Opening a codebase or an
263
266
  issue from its own screen lands here with that choice already made.
264
267
 
265
268
  **A run.** The transcript as it streams, drawn by what each part is: what the
@@ -373,7 +376,7 @@ src/
373
376
  prose.tsx markdown, drawn as text (markdown.ts reads it)
374
377
  desk.tsx the pane beside a run
375
378
  mind.ts the model and effort, one choice with Chat
376
- prompt.tsx the composer, and Tune, the effort and model row
379
+ voice.ts the language dictation listens in, and the platform's ear
377
380
  reply.tsx the row under an answer
378
381
  panel.tsx the side panel; tabs.ts its kept tabs
379
382
  side.tsx the column it stands in: width, the middle column, full screen
package/lib/carry.d.ts CHANGED
@@ -2,8 +2,7 @@ import { type ReactNode } from 'react';
2
2
  import { type Target } from './api/call.ts';
3
3
  import { type GearState, type Mode } from './gear.ts';
4
4
  import type { Composer, Prep, Turn } from './plugin.ts';
5
- import type { Shelf } from './plus.tsx';
6
- import type { Chip, PromptOptions } from './prompt.tsx';
5
+ import type { Carried, Modes, Shelf } from '@hanzo/ui/chat';
7
6
  /** What the surface lends the plugins: its draft, its files, its way about. */
8
7
  export type Lent = Omit<Composer, 'target' | 'show'>;
9
8
  export interface Carry {
@@ -20,9 +19,9 @@ export interface Carry {
20
19
  /** What the plugins that are on add to the message about to go. */
21
20
  prep: (turn: Omit<Turn, 'design'>, signal: AbortSignal) => Promise<Prep>;
22
21
  /** The chips after `+`: the skills on, Research, and Plan. */
23
- on: Chip[];
22
+ on: Carried[];
24
23
  /** The mode pill: this surface's modes, the conversation's choice among them. */
25
- mode: NonNullable<PromptOptions['mode']>;
24
+ mode: Modes<Mode>;
26
25
  /** Whether `what` may run: in Manual the person is asked (once, always in this conversation, or not); else yes. */
27
26
  allow: (what: Asked) => Promise<boolean>;
28
27
  /** The words the gear adds to every message, '' when none. */
package/lib/carry.js CHANGED
@@ -26,7 +26,7 @@ import { active, authored, document, nameOf as skillOf } from './api/skills.js';
26
26
  import { brand, brands } from './api/studio.js';
27
27
  import { useRead } from './data.js';
28
28
  import { said as designSaid } from './design.js';
29
- import { DEV_MODES, useGear } from './gear.js';
29
+ import { DEV_MODES, MODES, useGear } from './gear.js';
30
30
  import { active as lighted, prep, shelf as pluginShelf, usePlugins } from './plugins/index.js';
31
31
  /** The most of each skill a message carries. */
32
32
  const SKILL_MAX = 16_000;
@@ -171,7 +171,7 @@ export function useCarry(t, key, customize, lent, modes = DEV_MODES) {
171
171
  };
172
172
  return {
173
173
  gear,
174
- mode: { value: mode, set: (m) => gear.set({ mode: m }), items: modes },
174
+ mode: { value: mode, set: (m) => gear.set({ mode: m }), items: MODES.filter((x) => modes.includes(x.id)) },
175
175
  allow,
176
176
  skills: shelf,
177
177
  plugins,
package/lib/host.d.ts CHANGED
@@ -107,8 +107,6 @@ export interface Host {
107
107
  browser?: Browser;
108
108
  /** The host's sidebar lists Settings' sections and Customize's tabs, so the pages draw neither. */
109
109
  sections?: boolean;
110
- /** The composer names the model and offers its picker beside the effort: the person turned the model selector on in the host's settings. Absent, the effort alone. */
111
- models?: boolean;
112
110
  /** The default agents Customize › Agents offers to start from. Absent, none. */
113
111
  cast?: readonly Cast[];
114
112
  /** Every persona Customize › Agents offers to start from, read when Browse opens. Absent, none. */
package/lib/index.d.ts CHANGED
@@ -45,16 +45,14 @@ export type { Read } from './data.ts';
45
45
  export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
46
46
  /**
47
47
  * What Chat draws the same way (@hanzo/rooms imports these): the model and
48
- * effort (`useMind`, one choice kept for both, changed by `Tune`), the composer
49
- * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
50
- * over a `Deck` of tabs).
48
+ * effort (`useMind`, one choice kept for both), the row under an answer
49
+ * (`Reply`), and the side panel (`Panel` over a `Deck` of tabs). The composer is
50
+ * @hanzo/ui/chat's, for Chat and Dev alike.
51
51
  */
52
- export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
53
- export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
54
- export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
52
+ export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, level, nameOf, pacing, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
53
+ export { useEar } from './voice.ts';
55
54
  export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
56
55
  export { useCarry, type Carry } from './carry.tsx';
57
- export { Dictation } from './mic.tsx';
58
56
  export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker, type Artifact, type Listen } from './reply.tsx';
59
57
  export { frames, Page, Panel, PanelToggle, type Kind, type PageProps, type PanelProps, type Suggestion } from './panel.tsx';
60
58
  export { SideProvider, useSplit, type Split } from './side.tsx';
package/lib/index.js CHANGED
@@ -40,16 +40,14 @@ export { kind, label, said, share, spent, ways, when } from './plan.js';
40
40
  export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
41
41
  /**
42
42
  * What Chat draws the same way (@hanzo/rooms imports these): the model and
43
- * effort (`useMind`, one choice kept for both, changed by `Tune`), the composer
44
- * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
45
- * over a `Deck` of tabs).
43
+ * effort (`useMind`, one choice kept for both), the row under an answer
44
+ * (`Reply`), and the side panel (`Panel` over a `Deck` of tabs). The composer is
45
+ * @hanzo/ui/chat's, for Chat and Dev alike.
46
46
  */
47
- export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, useCatalog, useChoice, useMind } from './mind.js';
48
- export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
49
- export { Plus } from './plus.js';
47
+ export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, level, nameOf, pacing, useCatalog, useChoice, useMind } from './mind.js';
48
+ export { useEar } from './voice.js';
50
49
  export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage } from './gear.js';
51
50
  export { useCarry } from './carry.js';
52
- export { Dictation } from './mic.js';
53
51
  export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
54
52
  export { frames, Page, Panel, PanelToggle } from './panel.js';
55
53
  export { SideProvider, useSplit } from './side.js';
package/lib/landing.js CHANGED
@@ -1,11 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * New: the empty state. A heading at the top of the column, and at the foot of
4
4
  * the pane the composer — where the run goes (Cloud: the sandbox with the
5
5
  * codebase's environment; or one of the org's machines, under remote control),
6
6
  * which repository and branch it starts from, and the ask itself — the one
7
- * composer Chat draws too (prompt.tsx): attach and the mode at its start, the
8
- * model and effort, dictation and send at its end.
7
+ * composer Chat draws too (@hanzo/ui/chat): Dev hands it the three pills, the
8
+ * chips that go to its repositories, issues and templates, and its commands.
9
9
  *
10
10
  * Sending starts a coding run on a project and opens it. A project is a
11
11
  * repository — the org's on the forge, or one its GitHub grants — so a run
@@ -19,8 +19,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { SizableText, XStack, YStack } from '@hanzo/gui';
20
20
  import { phrase } from '@hanzo/i18n';
21
21
  import { useI18n } from '@hanzo/i18n/react';
22
- import { Cloud, Monitor } from '@hanzogui/lucide-icons-2';
22
+ import { CircleDot, Cloud, Code, GitBranch, LayoutTemplate, Monitor, Play } from '@hanzogui/lucide-icons-2';
23
23
  import { Button } from '@hanzo/ui';
24
+ import { Composer, MEASURE } from '@hanzo/ui/chat';
24
25
  import { BranchSelect, ChipSelect, FooterLink, RepoSelect } from '@hanzo/ui/product';
25
26
  import { useLimits } from '@hanzo/ui/product/useLimits';
26
27
  import { useDictation, useTalk, useVoice } from '@hanzo/voice';
@@ -36,7 +37,7 @@ import { ready, SANDBOX } from './api/places.js';
36
37
  import { asHub, isForge, isHub, settle } from './choice.js';
37
38
  import { useCached, useKept, usePlaces, useRead } from './data.js';
38
39
  import { useHost, useTarget } from './host.js';
39
- import { level, useMind } from './mind.js';
40
+ import { level, pacing, useMind } from './mind.js';
40
41
  import { usePrefs } from './prefs.js';
41
42
  import { Publish } from './publish.js';
42
43
  import { path } from './route.js';
@@ -44,10 +45,8 @@ import { useCarry } from './carry.js';
44
45
  import { DEV_MODES } from './gear.js';
45
46
  import { book, hand, Suggestions, taken, useSuggestions } from './suggestions.js';
46
47
  import { want } from './library.js';
47
- import { MEASURE, Prompt, useEar } from './prompt.js';
48
- import { Monitor as Device } from '@hanzogui/lucide-icons-2';
49
- import { compose, Files, pick, read as readFiles } from './tools.js';
50
- import { useSpoken } from './voice.js';
48
+ import { carrying, compose, pick, read as readFiles } from './tools.js';
49
+ import { useEar, useSpoken } from './voice.js';
51
50
  /** One vocabulary for the mode, on New and in a workspace: English, marked (`phrase`), drawn through `t`. */
52
51
  export const MODES = [
53
52
  { id: 'build', label: phrase('Build'), hint: phrase('Edits, commits and pushes a branch') },
@@ -104,9 +103,9 @@ export function Landing({ onStarted }) {
104
103
  const [note, setNote] = useState('');
105
104
  const [adding, setAdding] = useState(null);
106
105
  const [offering, setOffering] = useState(false);
107
- // The repository list is read again each time it opens; Refresh reads it again while open.
106
+ // The repository and branch lists are read again each time they open.
108
107
  const [picking, setPicking] = useState(false);
109
- const [round, setRound] = useState(0);
108
+ const [branching, setBranching] = useState(false);
110
109
  // A codebase or an issue hands its words over once. Left in the kept choice,
111
110
  // every later visit to New would put them back.
112
111
  useEffect(() => {
@@ -171,7 +170,7 @@ export function Landing({ onStarted }) {
171
170
  const github = (hub?.repos ?? []).map(asHub);
172
171
  for (const r of github)
173
172
  hubs.current.set(r.full_name.toLowerCase(), r);
174
- return { repos: [...repos, ...github], next: null };
173
+ return { repos: [...repos.map((r) => ({ ...r, source: 'forge' })), ...github.map((r) => ({ ...r, source: 'github' }))], next: null };
175
174
  }, [target]);
176
175
  // Drawn only once a forge codebase is chosen; one the forge no longer lists offers its default branch.
177
176
  const home = kept.repo?.default_branch || 'main';
@@ -306,27 +305,20 @@ export function Landing({ onStarted }) {
306
305
  setIssue(0);
307
306
  set({ repo: row, branch: row.default_branch });
308
307
  };
309
- // Cloud is the sandbox, set up by the chosen codebase's environment.
310
- const setUp = codebase && env.value
311
- ? env.value.state === 'ready'
312
- ? t('{repo} environment', { repo: codebase })
313
- : env.value.state === 'proposed'
314
- ? t('{repo} environment proposed', { repo: codebase })
315
- : unset(env.value)
316
- ? t('no {repo} environment yet', { repo: codebase })
317
- : t('{repo} environment, empty', { repo: codebase })
318
- : '';
308
+ // Cloud is the sandbox, set up by the chosen codebase's environment; the rest are the org's machines.
319
309
  const placeItems = useMemo(() => (places.value.length ? places.value : [SANDBOX]).map((p) => ({
320
310
  id: p.id || 'sandbox',
321
311
  label: p.id ? p.label : t('Cloud'),
322
- hint: p.id ? [t('Remote control'), p.status, p.capacity].filter(Boolean).join(' · ') : [t('Hanzo sandbox'), setUp].filter(Boolean).join(' · '),
312
+ hint: p.id ? [p.status, p.capacity].filter(Boolean).join(' · ') : '',
313
+ icon: p.id ? _jsx(Monitor, { size: 13, color: "$soft" }) : _jsx(Cloud, { size: 13, color: "$soft" }),
323
314
  disabled: !ready(p),
324
315
  place: p,
325
- })), [places.value, setUp, t]);
316
+ })), [places.value, t]);
326
317
  const branch = kept.branch || kept.repo?.default_branch || 'main';
327
- const where = (_jsxs(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: [_jsx(ChipSelect, { name: t('Where the run runs'), icon: place.id ? _jsx(Monitor, { size: 13 }) : _jsx(Cloud, { size: 13 }), label: place.id ? place.label : t('Cloud'), chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: t('Search machines…'), loading: places.loading, error: places.error?.message ?? null, footer: _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$ink", children: t('Set up remote control') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Run {command} on a machine: it joins this list, and a run there uses its checkout and credentials.', {
328
- command: (_jsx(SizableText, { size: "$1", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo link" })),
329
- }) })] }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) || isHub(kept.repo) ? kept.repo : null, onChange: (r) => {
318
+ const forge = isForge(kept.repo);
319
+ const screen = (to) => () => host.go(path({ kind: 'screen', screen: to }));
320
+ // The composer's pills: where the run goes, which repository, which branch.
321
+ const pills = (_jsxs(_Fragment, { children: [_jsx(ChipSelect, { name: t('Where the run runs'), icon: place.id ? _jsx(Monitor, { size: 13, color: "$soft" }) : _jsx(Cloud, { size: 13, color: "$soft" }), label: place.id ? place.label : t('Cloud'), chosen: placeItems.find((i) => i.place.id === place.id), items: placeItems, onChange: (i) => set({ place: i.place.id }), placeholder: t('Search machines…'), placement: "top-start", loading: places.loading, error: places.error?.message ?? null, footer: _jsx(FooterLink, { label: t('Manage…'), onPress: () => host.go(path({ kind: 'settings', section: 'machines' })) }) }), _jsx(RepoSelect, { open: picking, onOpenChange: setPicking, value: isForge(kept.repo) ? { ...kept.repo, source: 'forge' } : isHub(kept.repo) ? { ...kept.repo, source: 'github' } : null, onChange: (r) => {
330
322
  const hub = r.github ? hubs.current.get((r.full_name ?? "").toLowerCase()) : undefined;
331
323
  setIssue(0);
332
324
  if (hub) {
@@ -335,10 +327,11 @@ export function Landing({ onStarted }) {
335
327
  }
336
328
  const row = known.current.get(r.name) ?? chosen(r);
337
329
  set({ repo: row, branch: row.default_branch });
338
- }, load: loadRepos, troubleshoot: {
339
- label: t('Missing a GitHub repository? Bring it onto the forge'),
340
- onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })),
341
- }, connect: _jsx(XStack, { justify: "flex-end", children: _jsx(FooterLink, { label: t('Refresh list'), onPress: () => setRound((n) => n + 1) }) }), note: t("Repositories on the forge and on your organization's GitHub. Type to search."), action: {
330
+ }, load: loadRepos, tabs: [
331
+ { id: 'all', label: t('All') },
332
+ { id: 'forge', label: t('Organization') },
333
+ { id: 'github', label: 'GitHub' },
334
+ ], troubleshoot: { label: t('Bring a repository from GitHub'), onPress: screen('sync') }, action: {
342
335
  label: t('Add to project'),
343
336
  onPress: (r) => {
344
337
  const row = known.current.get(r.name);
@@ -349,25 +342,38 @@ export function Landing({ onStarted }) {
349
342
  name: r.name,
350
343
  });
351
344
  },
352
- }, placeholder: t('Choose a project'), disabled: !signed }, round), isForge(kept.repo) ? _jsx(BranchSelect, { value: branch, onChange: (b) => set({ branch: b }), load: loadBranches }) : null] }));
353
- const head = files.length ? (_jsx(XStack, { gap: 6, items: "center", flexWrap: "wrap", children: _jsx(Files, { files: files, onFiles: setFiles }) })) : undefined;
354
- // The machines a run can go to, as the `+` menu's Devices: the same choice as `where`'s.
345
+ }, placeholder: t('Repository'), placement: "top-start", disabled: !signed }), forge ? _jsx(BranchSelect, { open: branching, onOpenChange: setBranching, value: branch, onChange: (b) => set({ branch: b }), load: loadBranches, placement: "top-start" }) : null] }));
346
+ // Where a run goes from here: its repositories, branches, issues and templates, each one press away.
347
+ const chips = [
348
+ { id: 'repos', label: t('Repos'), icon: _jsx(Code, { size: 13, color: "$soft" }), onPress: () => setPicking(true) },
349
+ ...(forge ? [{ id: 'branches', label: t('Branches'), icon: _jsx(GitBranch, { size: 13, color: "$soft" }), onPress: () => setBranching(true) }] : []),
350
+ { id: 'issues', label: t('Issues'), icon: _jsx(CircleDot, { size: 13, color: "$soft" }), onPress: screen('issues') },
351
+ { id: 'templates', label: t('Templates'), icon: _jsx(LayoutTemplate, { size: 13, color: "$soft" }), onPress: screen('templates') },
352
+ ];
353
+ const commands = [
354
+ { id: 'run', label: t('Run'), keys: 'Ctrl+↵', icon: _jsx(Play, { size: 14, color: "$soft" }), disabled: !draft.trim(), run: () => void send() },
355
+ { id: 'issues', label: t('View issues'), icon: _jsx(CircleDot, { size: 14, color: "$soft" }), run: screen('issues') },
356
+ { id: 'templates', label: t('Browse templates'), icon: _jsx(LayoutTemplate, { size: 14, color: "$soft" }), run: screen('templates') },
357
+ { id: 'projects', label: t('Open projects'), icon: _jsx(Code, { size: 14, color: "$soft" }), run: screen('projects') },
358
+ { id: 'sync', label: t('Bring a repository from GitHub'), icon: _jsx(GitBranch, { size: 14, color: "$soft" }), run: screen('sync') },
359
+ ];
360
+ // The machines a run can go to, as the `+` menu's Devices: the same choice as the pill's.
355
361
  const devices = {
356
- rows: placeItems.map((i) => ({ id: i.place.id || 'sandbox', label: i.label, note: i.hint, icon: _jsx(Device, { size: 16 }), on: i.place.id === place.id })),
362
+ rows: placeItems.map((i) => ({ id: i.place.id || 'sandbox', label: i.label, note: i.hint, icon: _jsx(Monitor, { size: 16 }), on: i.place.id === place.id })),
357
363
  pick: (id) => set({ place: id === 'sandbox' ? '' : id }),
358
364
  loading: places.loading,
359
365
  error: places.error?.message ?? null,
360
366
  };
361
- return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: t('Build what’s next.') }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: t('{repo} is not one of this organization’s repositories.', { repo: codebase }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: t('Choose a repository') })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
362
- ? t('A proposed environment for {repo} is waiting for review.', { repo: codebase })
363
- : t('{repo} has no environment yet, so every run starts it bare.', { repo: codebase }) }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: t('Review') })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: t('Set up environment') }))] })) : null, note || unhonoured(mode, place.id, t) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id, t) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: t('Describe a task or ask a question'), head: head, plus: {
364
- upload: () => pick(files, setFiles, setNote, t),
365
- skills: carry.skills,
366
- design: carry.design,
367
- plugins: carry.plugins,
368
- devices,
369
- memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
370
- }, on: carry.on, slash: carry.skills, at: carry.plugins, mode: carry.mode, where: where, below: _jsx(Suggestions, { rows: hints.rows, onAct: act, onDismiss: hints.dismiss }), model: host.models ? mind : null, effort: mind, limits: limits, onUpgrade: () => host.go(path({ kind: 'screen', screen: 'plans' })), voice: voice, language: prefs.language ?? null, onLanguage: (id) => void savePrefs({ language: id }), talk: talk })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), carry.dialog, codebase ? (_jsx(SetupDialog, { repo: codebase, homes: homes, open: offering, onOpenChange: setOffering, onStart: setup, onRepo: choose, onSaved: () => {
367
+ return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", overflowX: "hidden", "data-slot": "new", children: [_jsx(YStack, { flex: 1, minH: 0, width: "100%", maxW: MEASURE, justify: "flex-end", pb: "$3", overflowY: "auto", children: _jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: t('Build what’s next.') }) }), _jsx(YStack, { width: "100%", maxW: MEASURE, gap: "$2", pb: 12, shrink: 0, children: _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: t('{repo} is not one of this organization’s repositories.', { repo: codebase }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: t('Choose a repository') })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
368
+ ? t('A proposed environment for {repo} is waiting for review.', { repo: codebase })
369
+ : t('{repo} has no environment yet, so every run starts it bare.', { repo: codebase }) }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: t('Review') })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: t('Set up environment') }))] })) : null, note || unhonoured(mode, place.id, t) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id, t) || mind.note })) : null, _jsx(Composer, { column: true, framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, placeholder: t('Ask, build, or run anything…'), label: t('Describe a task or ask a question'), plus: {
370
+ upload: () => pick(files, setFiles, setNote, t),
371
+ skills: carry.skills,
372
+ design: carry.design,
373
+ plugins: carry.plugins,
374
+ devices,
375
+ memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
376
+ }, carried: [...carry.on, ...carrying(files, setFiles)], slash: carry.skills, at: carry.plugins, pills: pills, chips: chips, commands: commands, starters: _jsx(Suggestions, { rows: hints.rows, onAct: act, onDismiss: hints.dismiss }), mind: mind, effort: pacing(mind), limits: limits, onUpgrade: () => host.go(path({ kind: 'screen', screen: 'plans' })), voice: voice, language: prefs.language ?? null, onLanguage: (id) => void savePrefs({ language: id }), talk: talk })] }) }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), carry.dialog, codebase ? (_jsx(SetupDialog, { repo: codebase, homes: homes, open: offering, onOpenChange: setOffering, onStart: setup, onRepo: choose, onSaved: () => {
371
377
  setOffering(false);
372
378
  env.reload();
373
379
  } })) : null] }));
package/lib/mind.d.ts CHANGED
@@ -1,12 +1,33 @@
1
+ /**
2
+ * Which model answers, and how hard it thinks: one store for Chat and Dev, and
3
+ * the catalog both offer. The composer (@hanzo/ui/chat) changes it.
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
+ * The effort is always under the composer; the model chip only where the person
9
+ * turned it on (Settings → Appearance › Model in composer). Enso
10
+ * routes every ask — a message in Chat, a step in Dev — so naming a model is an
11
+ * override, and the panel says so before it offers one. A premium model is
12
+ * picked for the screen it was picked on (`usePick`) and never kept, so the next
13
+ * conversation or run opens on what was kept, Enso until something else is.
14
+ *
15
+ * The efforts are Auto, High, XHigh and Max, Chat's words (Hanzo's
16
+ * ThinkingEffort), Auto until another is chosen. A coding run takes low, medium
17
+ * or high (apps/coding `efforts`), so `level` is what a run names: medium for
18
+ * Auto, what a run took before anyone chose, and high for the rest, the most it
19
+ * takes. Auto names one rather than none, because a run that names none takes
20
+ * an effort the person saved to their account once (apps/coding start.go) and
21
+ * no screen shows any more.
22
+ */
23
+ import type { Modes } from '@hanzo/ui/chat';
1
24
  import { type Target } from './api/call.ts';
2
25
  import { type Model } from './api/models.ts';
3
26
  export type Effort = 'auto' | 'high' | 'xhigh' | 'max';
4
- /** The efforts, in order, in English and marked (`phrase`): the composer draws each word through `t`. */
5
- export declare const EFFORTS: readonly {
27
+ /** The efforts a Dev run offers, in order: the composer's own list (Chat offers every one), less Low and Medium. Marked English, drawn through `t`. */
28
+ export declare const EFFORTS: (import("@hanzo/ui/chat").Way<import("@hanzo/ui/chat").Effort> & {
6
29
  id: Effort;
7
- label: string;
8
- note: string;
9
- }[];
30
+ })[];
10
31
  /** The effort a coding run names (apps/coding takes low, medium or high): medium for Auto, high for the rest. */
11
32
  export declare const level: (effort: Effort) => 'medium' | 'high';
12
33
  /** What a person chose: the model (Enso routes) and the effort. */
@@ -55,6 +76,8 @@ export interface Catalog {
55
76
  export declare function useCatalog(t: Target): Catalog;
56
77
  /** Whether `model` is one this plan may send: on Free, the catalog's free class alone. */
57
78
  export declare function grants(free: boolean, model: string, catalog: readonly Model[]): boolean;
79
+ /** The effort as the composer reads it: the one in force, its list, and the way to change it. */
80
+ export declare const pacing: (m: Pick<Mind, 'effort' | 'pace'>) => Modes<Effort>;
58
81
  /** The chip's whole state: the catalog, what answers next, and how to change either. */
59
82
  export interface Mind {
60
83
  model: string;
package/lib/mind.js CHANGED
@@ -1,38 +1,11 @@
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
- * The effort is always under the composer; the model chip only where the host
9
- * shows it (Hanzo's Advanced settings › model selector, `Host.models`). Enso
10
- * routes every ask — a message in Chat, a step in Dev — so naming a model is an
11
- * override, and the panel says so before it offers one. A premium model is
12
- * picked for the screen it was picked on (`usePick`) and never kept, so the next
13
- * conversation or run opens on what was kept, Enso until something else is.
14
- *
15
- * The efforts are Auto, High, XHigh and Max, Chat's words (Hanzo's
16
- * ThinkingEffort), Auto until another is chosen. A coding run takes low, medium
17
- * or high (apps/coding `efforts`), so `level` is what a run names: medium for
18
- * Auto, what a run took before anyone chose, and high for the rest, the most it
19
- * takes. Auto names one rather than none, because a run that names none takes
20
- * an effort the person saved to their account once (apps/coding start.go) and
21
- * no screen shows any more.
22
- */
23
- import { phrase } from '@hanzo/i18n';
1
+ import { EFFORTS as EVERY } from '@hanzo/ui/chat/pure';
24
2
  import { parseModels } from '@hanzo/ui/models/catalog';
25
3
  import { useEffect, useState, useSyncExternalStore } from 'react';
26
4
  import { Refusal } from './api/call.js';
27
5
  import { ENSO, models as read } from './api/models.js';
28
6
  import { usePick } from './pick.js';
29
- /** The efforts, in order, in English and marked (`phrase`): the composer draws each word through `t`. */
30
- export const EFFORTS = [
31
- { id: 'auto', label: phrase('Auto'), note: phrase('Adaptive reasoning based on task complexity') },
32
- { id: 'high', label: phrase('High'), note: phrase('Deep reasoning for complex problems') },
33
- { id: 'xhigh', label: phrase('XHigh'), note: phrase('Extended multi-step reasoning for hard tasks') },
34
- { id: 'max', label: phrase('Max'), note: phrase('Maximum compute and exhaustive verification') },
35
- ];
7
+ /** The efforts a Dev run offers, in order: the composer's own list (Chat offers every one), less Low and Medium. Marked English, drawn through `t`. */
8
+ export const EFFORTS = EVERY.filter((e) => ['auto', 'high', 'xhigh', 'max'].includes(e.id));
36
9
  /** The effort a coding run names (apps/coding takes low, medium or high): medium for Auto, high for the rest. */
37
10
  export const level = (effort) => (effort === 'auto' ? 'medium' : 'high');
38
11
  export const FIRST = { model: ENSO, effort: 'auto' };
@@ -205,6 +178,8 @@ export function grants(free, model, catalog) {
205
178
  return true;
206
179
  return catalog.find((m) => m.id === model)?.class === 'free';
207
180
  }
181
+ /** The effort as the composer reads it: the one in force, its list, and the way to change it. */
182
+ export const pacing = (m) => ({ value: m.effort, set: m.pace, items: EFFORTS });
208
183
  /**
209
184
  * The model and effort for the plan `plan` names (`GET /v1/ai/limits` `plan`).
210
185
  * On Free the default is `FREE` and a kept pick outside the free lane is set
@@ -19,7 +19,7 @@ import type { Say } from '@hanzo/i18n/react';
19
19
  import type { Target } from '../api/call.ts';
20
20
  import type { Gear, GearState } from '../gear.ts';
21
21
  import type { Composer, Plugin, Prep, Turn } from '../plugin.ts';
22
- import type { Shelf } from '../plus.tsx';
22
+ import type { Shelf } from '@hanzo/ui/chat';
23
23
  export { research } from './research.tsx';
24
24
  export interface Plugins {
25
25
  plugins: Plugin[];
package/lib/project.js CHANGED
@@ -46,7 +46,7 @@ import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
46
46
  import { useI18n } from '@hanzo/i18n/react';
47
47
  import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
48
48
  import { Attachments, CHAT, Console, DEVICES, Feedback, FileTabs, FileTree, PageSelect, PreviewFrame, ProjectChip, VIEWS, Views, Workspace, fold, } from '@hanzo/ui/agents';
49
- import { Message } from '@hanzo/ui/chat';
49
+ import { Composer, Message } from '@hanzo/ui/chat';
50
50
  import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
51
51
  import { HanzoMark } from '@hanzo/ui/product';
52
52
  import { useDictation, useVoice } from '@hanzo/voice';
@@ -64,13 +64,13 @@ import { useKept, useProjects, useRead, useRun } from './data.js';
64
64
  import { Merge } from './git.js';
65
65
  import { Gutter } from './gutter.js';
66
66
  import { useHost, useTarget } from './host.js';
67
- import { level, useMind } from './mind.js';
67
+ import { MODES } from './gear.js';
68
+ import { level, pacing, useMind } from './mind.js';
68
69
  import { Out } from './out.js';
69
- import { Prompt, useEar } from './prompt.js';
70
- import { useSpoken } from './voice.js';
70
+ import { useEar, useSpoken } from './voice.js';
71
71
  import { Publish } from './publish.js';
72
72
  import { path } from './route.js';
73
- import { compose, Files, pick } from './tools.js';
73
+ import { carrying, compose, pick } from './tools.js';
74
74
  const LIVE = new Set(['running', 'paused']);
75
75
  /** How often a project being published is read again, ms. */
76
76
  const AGAIN = 5000;
@@ -422,9 +422,9 @@ export function Project({ at }) {
422
422
  ? t('{name} is loaded and nothing is published yet. Say what to change and it gets built.', { name: project.name })
423
423
  : named
424
424
  ? t('{repo} is open on {branch}. Say what to change and it gets built there.', { repo: `${named.org}/${named.name}`, branch: home })
425
- : t('{name} is loaded. Say what to change and it gets built.', { name: slug }) })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", paddingInlineStart: "$2", borderInlineStartWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode, t) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: t('Open pull request {pr}', { pr: pr.label }), children: [_jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: t('Pull request {pr}', { pr: pr.label }) }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, children: [note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
425
+ : t('{name} is loaded. Say what to change and it gets built.', { name: slug }) })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", paddingInlineStart: "$2", borderInlineStartWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode, t) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: t('Open pull request {pr}', { pr: pr.label }), children: [_jsx(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: t('Pull request {pr}', { pr: pr.label }) }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, children: [note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, _jsx(Composer, { column: true, framed: true, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
426
426
  ? () => void stop(target, current).then(() => setNote(t('Stop requested — the run keeps its work on its branch')), (e) => setNote(e.message))
427
- : undefined, busy: busy || (running && !draft.trim()), placeholder: running ? t('Steer this run') : t('Ask Hanzo for edits'), label: t('Ask Hanzo for edits'), plus: { upload: () => pick(attached, setAttached, setNote, t) }, mode: running ? null : { value: mode === 'plan' ? 'plan' : 'auto', set: (m) => setMode(m === 'plan' ? 'plan' : 'build'), items: ['auto', 'plan'] }, model: running || !host.models ? null : mind, effort: running ? null : mind, voice: voice })] }), wide && !shut ? (_jsx(Gutter, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: t('Resize the chat'), gap: GUTTER })) : null] }));
427
+ : undefined, busy: busy || (running && !draft.trim()), placeholder: running ? t('Steer this run') : t('Ask Hanzo for edits'), label: t('Ask Hanzo for edits'), plus: { upload: () => pick(attached, setAttached, setNote, t) }, carried: carrying(attached, setAttached), mode: running ? null : { value: mode === 'plan' ? 'plan' : 'auto', set: (m) => setMode(m === 'plan' ? 'plan' : 'build'), items: MODES.filter((m) => m.id !== 'manual') }, mind: running ? null : mind, effort: running ? null : pacing(mind), voice: voice })] }), wide && !shut ? (_jsx(Gutter, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: t('Resize the chat'), gap: GUTTER })) : null] }));
428
428
  // The preview stays loaded while another view is open, so coming back to it
429
429
  // is the page as it was left.
430
430
  const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": t('Reload the preview'), onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: t('Device'), labels: "none" }), src ? (_jsx(Out, { href: src, label: t('Open in a new tab'), children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: t('{title} preview', { title }), device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
package/lib/run.js CHANGED
@@ -29,7 +29,7 @@ import { useI18n } from '@hanzo/i18n/react';
29
29
  import { ChevronDown, ChevronRight, Clock, Copy, ExternalLink, GitPullRequest, Globe, Pencil, Share } from '@hanzogui/lucide-icons-2';
30
30
  import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
31
31
  import { Steer } from '@hanzo/ui/agents';
32
- import { Failure } from '@hanzo/ui/chat';
32
+ import { Composer, Failure, measure } from '@hanzo/ui/chat';
33
33
  import { useLimits } from '@hanzo/ui/product/useLimits';
34
34
  import { useDictation, useVoice } from '@hanzo/voice';
35
35
  import { useEffect, useMemo, useState } from 'react';
@@ -41,18 +41,17 @@ import { verdict } from './api/verdict.js';
41
41
  import { useKept, useProjects, useRead, useRun } from './data.js';
42
42
  import { Desk, SEED } from './desk.js';
43
43
  import { useHost, useTarget } from './host.js';
44
- import { level, useMind } from './mind.js';
44
+ import { level, pacing, useMind } from './mind.js';
45
45
  import { Out } from './out.js';
46
46
  import { Gutter } from './gutter.js';
47
47
  import { gap, pane } from './pane.js';
48
- import { measure, Prompt, useEar } from './prompt.js';
49
- import { useSpoken } from './voice.js';
48
+ import { useEar, useSpoken } from './voice.js';
50
49
  import { speaker } from './reply.js';
51
50
  import { PanelToggle } from './panel.js';
52
51
  import { schedule } from './scheduled.js';
53
52
  import { SideProvider, useSplit } from './side.js';
54
53
  import { PAGE, useDeck, useKey, usePanel } from './tabs.js';
55
- import { compose, Files, pick } from './tools.js';
54
+ import { carrying, compose, pick } from './tools.js';
56
55
  import { Transcript } from './transcript.js';
57
56
  const LIVE = new Set(['running', 'paused', '']);
58
57
  /** A run's status as the line under its title says it; one the service adds later is drawn as it comes. */
@@ -384,9 +383,9 @@ export function Run({ id }) {
384
383
  : say('This run is still working.') }), _jsx(Steer, { onCommand: command, withhold: [
385
384
  ...(paused ? ['pause'] : ['resume']),
386
385
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
387
- ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? say('You will be notified') : say('Notify me') })] })) : null] }), _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: send, ready: Boolean(draft.trim()), disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? say('Steer this run') : say('Follow up on this run'), head: files.length ? _jsx(XStack, { gap: "$1.5", flexWrap: "wrap", children: _jsx(Files, { files: files, onFiles: setFiles }) }) : undefined, plus: { upload: () => pick(files, setFiles, setNote, say) },
386
+ ] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? say('You will be notified') : say('Notify me') })] })) : null] }), _jsx(Composer, { column: true, framed: true, value: draft, onChange: setDraft, onSend: send, ready: Boolean(draft.trim()), disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? say('Steer this run') : say('Follow up on this run'), plus: { upload: () => pick(files, setFiles, setNote, say) }, carried: carrying(files, setFiles),
388
387
  // What a new run from here is made with; words that steer this run use neither.
389
- model: signed && !steering && host.models ? mind : null, effort: signed && !steering ? mind : null, limits: limits, voice: voice })] })] }), desk && !wide ? desk_(true) : null, desk && wide ? (_jsxs(YStack, { "data-slot": "desk", ref: split.aside, ...pane,
388
+ mind: signed && !steering ? mind : null, effort: signed && !steering ? pacing(mind) : null, limits: limits, voice: voice })] })] }), desk && !wide ? desk_(true) : null, desk && wide ? (_jsxs(YStack, { "data-slot": "desk", ref: split.aside, ...pane,
390
389
  // A pane of its own beside the run's, the gutter between them (useSplit reads it as the inline-start margin).
391
390
  marginInlineStart: gap, position: "relative", shrink: 0, minH: 0, style: split.asideStyle, children: [split.grip ? (_jsx(Gutter, { ...split.grip, label: say('Resize the side panel'), inset: pane.borderWidth })) : null, _jsx(SideProvider, { value: split.side, children: desk_(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: say('Rename this run') }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": say('The run’s name'), maxLength: 512, onKeyDown: (e) => {
392
391
  if (e.key === 'Enter')
@@ -15,7 +15,8 @@ import { useState } from 'react';
15
15
  import { told } from '../api/call.js';
16
16
  import { useHost } from '../host.js';
17
17
  import { usePrefs } from '../prefs.js';
18
- import { LANGUAGES, spoken } from '../voice.js';
18
+ import { LANGUAGES } from '@hanzo/ui/chat/pure';
19
+ import { spoken } from '../voice.js';
19
20
  import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
20
21
  /** The choices, each word English and marked (`phrase`): drawn through `t`. */
21
22
  const THEMES = [
package/lib/tools.d.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { type Say } from '@hanzo/i18n/react';
1
+ /**
2
+ * Files with an ask, for every composer the builder draws: they ride the
3
+ * prompt as text — nothing is uploaded anywhere else. The paperclip is the
4
+ * composer's (@hanzo/ui/chat); this reads what it picks and says what is held.
5
+ */
6
+ import type { Say } from '@hanzo/i18n/react';
7
+ import type { Carried } from '@hanzo/ui/chat';
2
8
  export interface Attached {
3
9
  name: string;
4
10
  text: string;
@@ -12,8 +18,5 @@ export declare function read(have: Attached[], list: FileList, t?: Say): Promise
12
18
  }>;
13
19
  /** Ask the browser for files and read them in, within the caps; the picker is minted per press. */
14
20
  export declare function pick(have: Attached[], onFiles: (f: Attached[]) => void, onNote: (n: string) => void, t?: Say): void;
15
- /** The attached files as small removable marks. */
16
- export declare function Files({ files, onFiles }: {
17
- files: Attached[];
18
- onFiles: (f: Attached[]) => void;
19
- }): import("react").JSX.Element;
21
+ /** The attached files as what the composer carries: a removable chip each. */
22
+ export declare function carrying(files: Attached[], onFiles: (f: Attached[]) => void): Carried[];