@hanzo/build 0.2.81 → 0.2.83

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
@@ -118,9 +118,12 @@ French and right to left in Arabic.
118
118
  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
- and the choice kept in this browser (`hanzo.mind`, Enso and Medium until
122
- changed). `Tune` is the chip that changes it; Settings → Code writes the same
123
- choice. `@hanzo/build/mind` serves the store alone, without the builder.
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
125
+ writes the same choice. `@hanzo/build/mind` serves the store alone, without
126
+ the builder.
124
127
  - `prompt` / `Prompt` — the composer, as wide as the text above it (`measure`).
125
128
  - `Reply` — copy, Listen and Open under an answer.
126
129
  - `Panel` over `useDeck` — the side panel and its kept tabs.
@@ -255,8 +258,8 @@ machines follow under remote control, each linked with `hanzo link`), the
255
258
  codebase and the branch — the forge's repositories, filtered as you type, read
256
259
  again with Refresh list, and a way to Sync for a GitHub repository that is not
257
260
  there yet — then the ask, in the composer Chat draws too (`prompt`): the paperclip
258
- and Build or Plan at its start; the model and effort (`Tune`), dictation and
259
- send at its end. A codebase can be added to a project. Opening a codebase or an
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
263
  issue from its own screen lands here with that choice already made.
261
264
 
262
265
  **A run.** The transcript as it streams, drawn by what each part is: what the
@@ -351,7 +354,8 @@ picker and open-in-tab; Share and Publish; the console dock under it.
351
354
  `mode`, `model` and `effort` are sent with a run as asked — Enso as
352
355
  `enso-auto`, never left out — and `issue`, the number a run started from an
353
356
  issue closes; the platform honours them where it does and nothing here
354
- simulates them.
357
+ simulates them. A run takes `low`, `medium` or `high`, so Auto names `medium`
358
+ and High, XHigh and Max name `high` (`level`).
355
359
 
356
360
  ## Layout
357
361
 
@@ -369,7 +373,7 @@ src/
369
373
  prose.tsx markdown, drawn as text (markdown.ts reads it)
370
374
  desk.tsx the pane beside a run
371
375
  mind.ts the model and effort, one choice with Chat
372
- prompt.tsx the composer, and Tune, the model and effort chip
376
+ prompt.tsx the composer, and Tune, the effort and model row
373
377
  reply.tsx the row under an answer
374
378
  panel.tsx the side panel; tabs.ts its kept tabs
375
379
  side.tsx the column it stands in: width, the middle column, full screen
@@ -12,7 +12,7 @@
12
12
  * `conflict`, `pending` (not on the forge yet) or `paused` — and is absent when
13
13
  * the forge could not be read, which is not reported as healthy.
14
14
  *
15
- * Every link the sync app holds is also a row of GET /v1/projects (apps/sync
15
+ * Every link the sync app holds is also a row of GET /v1/project (apps/sync
16
16
  * project.go), so this list is the org's linked projects and not a copy of them.
17
17
  */
18
18
  import { type Target } from './call.ts';
package/lib/api/links.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * `conflict`, `pending` (not on the forge yet) or `paused` — and is absent when
13
13
  * the forge could not be read, which is not reported as healthy.
14
14
  *
15
- * Every link the sync app holds is also a row of GET /v1/projects (apps/sync
15
+ * Every link the sync app holds is also a row of GET /v1/project (apps/sync
16
16
  * project.go), so this list is the org's linked projects and not a copy of them.
17
17
  */
18
18
  import { call } from './call.js';
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The org's projects: what it has built, and where each one is served.
3
3
  *
4
- * GET /v1/projects → Project[]
5
- * PATCH /v1/projects/{slug} rename it, or make it public or private
6
- * DELETE /v1/projects/{slug} delete it and take its site down
7
- * GET /v1/projects/{slug}/deployments → Deployment[], newest first
4
+ * GET /v1/project → Project[]
5
+ * PATCH /v1/project/{slug} rename it, or make it public or private
6
+ * DELETE /v1/project/{slug} delete it and take its site down
7
+ * GET /v1/project/{slug}/deployments → Deployment[], newest first
8
8
  *
9
9
  * `slug` is the one key every surface shares — the address under /dev, the
10
10
  * site's name, the `project` a run is tagged with. A project belongs to the org,
@@ -52,7 +52,7 @@ export function project(raw) {
52
52
  };
53
53
  }
54
54
  export async function projects(t) {
55
- const raw = await call(t, 'GET', '/v1/projects');
55
+ const raw = await call(t, 'GET', '/v1/project');
56
56
  const rows = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
57
57
  return rows
58
58
  .map(project)
@@ -70,11 +70,11 @@ export async function change(t, slug, what) {
70
70
  }
71
71
  if (what.visibility !== undefined)
72
72
  body.visibility = what.visibility;
73
- return project(await call(t, 'PATCH', `/v1/projects/${seg(slug)}`, body));
73
+ return project(await call(t, 'PATCH', `/v1/project/${seg(slug)}`, body));
74
74
  }
75
75
  /** Delete a project. Its site stops answering and its slug is free again. */
76
76
  export async function remove(t, slug) {
77
- await call(t, 'DELETE', `/v1/projects/${seg(slug)}`);
77
+ await call(t, 'DELETE', `/v1/project/${seg(slug)}`);
78
78
  }
79
79
  export function deployment(raw) {
80
80
  const d = (raw && typeof raw === 'object' ? raw : {});
@@ -90,7 +90,7 @@ export function deployment(raw) {
90
90
  }
91
91
  /** A project's deployments, newest first: the first says how its latest build went. */
92
92
  export async function deployments(t, slug) {
93
- const raw = await call(t, 'GET', `/v1/projects/${seg(slug)}/deployments`);
93
+ const raw = await call(t, 'GET', `/v1/project/${seg(slug)}/deployments`);
94
94
  return (Array.isArray(raw) ? raw : [])
95
95
  .map(deployment)
96
96
  .filter((d) => d.id)
@@ -127,7 +127,7 @@ export async function templates(t) {
127
127
  * once, and answers `building` until it is `live` or its build failed.
128
128
  */
129
129
  export async function fork(t, slug) {
130
- return project(await call(t, 'POST', '/v1/projects/fork', { slug }));
130
+ return project(await call(t, 'POST', '/v1/project/fork', { slug }));
131
131
  }
132
132
  /** `owner/name` from a clone URL — its last two path segments, without `.git`. */
133
133
  export function address(clone) {
package/lib/host.d.ts CHANGED
@@ -107,6 +107,8 @@ 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;
110
112
  /** The default agents Customize › Agents offers to start from. Absent, none. */
111
113
  cast?: readonly Cast[];
112
114
  /** Every persona Customize › Agents offers to start from, read when Browse opens. Absent, none. */
package/lib/index.d.ts CHANGED
@@ -49,7 +49,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Scree
49
49
  * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
50
50
  * over a `Deck` of tabs).
51
51
  */
52
- 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 { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
53
53
  export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
54
54
  export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
55
55
  export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
package/lib/index.js CHANGED
@@ -44,7 +44,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
44
44
  * (`prompt`), the row under an answer (`Reply`), and the side panel (`Panel`
45
45
  * over a `Deck` of tabs).
46
46
  */
47
- export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
47
+ export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, useCatalog, useChoice, useMind } from './mind.js';
48
48
  export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
49
49
  export { Plus } from './plus.js';
50
50
  export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage } from './gear.js';
package/lib/landing.js CHANGED
@@ -36,7 +36,7 @@ import { ready, SANDBOX } from './api/places.js';
36
36
  import { asHub, isForge, isHub, settle } from './choice.js';
37
37
  import { useCached, useKept, usePlaces, useRead } from './data.js';
38
38
  import { useHost, useTarget } from './host.js';
39
- import { useMind } from './mind.js';
39
+ import { level, useMind } from './mind.js';
40
40
  import { usePrefs } from './prefs.js';
41
41
  import { Publish } from './publish.js';
42
42
  import { path } from './route.js';
@@ -231,7 +231,7 @@ export function Landing({ onStarted }) {
231
231
  targetId: place.id,
232
232
  mode,
233
233
  model: mind.model,
234
- effort: mind.effort,
234
+ effort: level(mind.effort),
235
235
  issue,
236
236
  });
237
237
  setDraft('');
@@ -297,7 +297,7 @@ export function Landing({ onStarted }) {
297
297
  };
298
298
  /** Start the agent that finds a codebase's environment, and open it: its run is the progress. A refusal is the dialog's to say. */
299
299
  const setup = async (repo) => {
300
- const run = await start(target, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
300
+ const run = await start(target, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: level(mind.effort) });
301
301
  onStarted(run.session);
302
302
  };
303
303
  /** The codebase a dialog chose, chosen here too. */
@@ -367,7 +367,7 @@ export function Landing({ onStarted }) {
367
367
  plugins: carry.plugins,
368
368
  devices,
369
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 }), mind: 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: () => {
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: () => {
371
371
  setOffering(false);
372
372
  env.reload();
373
373
  } })) : null] }));
package/lib/merge.d.ts CHANGED
@@ -42,7 +42,7 @@ export interface Project {
42
42
  synced: string;
43
43
  /** Its link's state: `synced`, `conflict`, `pending`, `paused`, `linked` when its forge copy could not be read; '' with no link. */
44
44
  status: string;
45
- /** The org's project (GET /v1/projects) that builds from it, by slug, or ''. */
45
+ /** The org's project (GET /v1/project) that builds from it, by slug, or ''. */
46
46
  slug: string;
47
47
  /** The name GitHub's listing says it takes on the forge once brought there, or ''. */
48
48
  codebase: string;
@@ -124,7 +124,7 @@ export interface Linked {
124
124
  status: string;
125
125
  branch: string;
126
126
  }
127
- /** One of the org's projects (GET /v1/projects) that builds from a repository. */
127
+ /** One of the org's projects (GET /v1/project) that builds from a repository. */
128
128
  export interface Site {
129
129
  slug: string;
130
130
  /** Its clone address. */
package/lib/mind.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { type Target } from './api/call.ts';
2
2
  import { type Model } from './api/models.ts';
3
- export type Effort = 'low' | 'medium' | 'high';
4
- /** The efforts, in English and marked (`phrase`): the composer draws each word through `t`. */
3
+ 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
5
  export declare const EFFORTS: readonly {
6
6
  id: Effort;
7
7
  label: string;
8
8
  note: string;
9
9
  }[];
10
+ /** The effort a coding run names (apps/coding takes low, medium or high): medium for Auto, high for the rest. */
11
+ export declare const level: (effort: Effort) => 'medium' | 'high';
10
12
  /** What a person chose: the model (Enso routes) and the effort. */
11
13
  export interface Choice {
12
14
  model: string;
@@ -17,14 +19,12 @@ export declare const FIRST: Choice;
17
19
  export declare const FREE = "enso-free";
18
20
  /** Whether a plan grants only the free lane. An unknown plan is not taken for Free. */
19
21
  export declare const isFree: (plan: string | null | undefined) => boolean;
20
- /** The kept choice, Enso and Medium where nothing is kept. Stable while the stored text is. */
22
+ /** The kept choice, Enso and Auto where nothing is kept. Stable while the stored text is. Low and Medium, offered once, read as Auto. */
21
23
  export declare function kept(): Choice;
22
24
  /** Keep a change, in this browser, for every surface that reads it. */
23
25
  export declare function keep(patch: Partial<Choice>): void;
24
26
  /** The kept choice and the way to change it. The server and the first render read the default. */
25
27
  export declare function useChoice(): readonly [Choice, (patch: Partial<Choice>) => void];
26
- /** Whether `model` takes an effort: the router and an undescribed model do, a listed one only if it reasons. */
27
- export declare function reasons(model: string, catalog: readonly Model[]): boolean;
28
28
  /** A model's name as the catalog spells it; Enso for the router. */
29
29
  export declare function nameOf(model: string, catalog: readonly Model[]): string;
30
30
  /** An id said as a name: its last path part, each word capitalised (`enso-flash` → Enso Flash). */
package/lib/mind.js CHANGED
@@ -5,16 +5,20 @@
5
5
  * GET /v1/models the catalog, read once per platform and shared
6
6
  * hanzo.mind the person's choice, in this browser: {model, effort}
7
7
  *
8
- * Effort is on the chip and the model is one press in. Enso routes every ask —
9
- * a message in Chat, a step in Dev — so naming a model is an override, and the
10
- * panel says so before it offers one. A premium model is picked for the screen
11
- * it was picked on (`usePick`) and never kept, so the next conversation or run
12
- * opens on what was kept, Enso until something else is.
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.
13
14
  *
14
- * The efforts are the set both planes take: `reasoning_effort` on a completion
15
- * and `effort` on a coding run (apps/coding `efforts`). A model the catalog
16
- * lists as not reasoning offers none; Enso and a model the catalog does not
17
- * describe offer all three.
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.
18
22
  */
19
23
  import { phrase } from '@hanzo/i18n';
20
24
  import { parseModels } from '@hanzo/ui/models/catalog';
@@ -22,13 +26,16 @@ import { useEffect, useState, useSyncExternalStore } from 'react';
22
26
  import { Refusal } from './api/call.js';
23
27
  import { ENSO, models as read } from './api/models.js';
24
28
  import { usePick } from './pick.js';
25
- /** The efforts, in English and marked (`phrase`): the composer draws each word through `t`. */
29
+ /** The efforts, in order, in English and marked (`phrase`): the composer draws each word through `t`. */
26
30
  export const EFFORTS = [
27
- { id: 'low', label: phrase('Low'), note: phrase('A quick pass') },
28
- { id: 'medium', label: phrase('Medium'), note: phrase('Thinks before it answers') },
29
- { id: 'high', label: phrase('High'), note: phrase('Slow and careful, for hard problems') },
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') },
30
35
  ];
31
- export const FIRST = { model: ENSO, effort: 'medium' };
36
+ /** The effort a coding run names (apps/coding takes low, medium or high): medium for Auto, high for the rest. */
37
+ export const level = (effort) => (effort === 'auto' ? 'medium' : 'high');
38
+ export const FIRST = { model: ENSO, effort: 'auto' };
32
39
  /** What Free answers on: the catalog's free lane, Enso's first. `free` is an alias of it. */
33
40
  export const FREE = 'enso-free';
34
41
  /** Whether a plan grants only the free lane. An unknown plan is not taken for Free. */
@@ -49,7 +56,7 @@ let raw;
49
56
  let held = FIRST;
50
57
  /** A choice the browser would not store, held for the page. */
51
58
  let memo = null;
52
- /** The kept choice, Enso and Medium where nothing is kept. Stable while the stored text is. */
59
+ /** The kept choice, Enso and Auto where nothing is kept. Stable while the stored text is. Low and Medium, offered once, read as Auto. */
53
60
  export function kept() {
54
61
  if (memo)
55
62
  return memo;
@@ -74,7 +81,8 @@ export function kept() {
74
81
  }
75
82
  /** Keep a change, in this browser, for every surface that reads it. */
76
83
  export function keep(patch) {
77
- const next = { ...kept(), ...patch };
84
+ const was = kept();
85
+ const next = { ...was, ...patch, effort: patch.effort === undefined ? was.effort : effortOf(patch.effort) };
78
86
  try {
79
87
  store()?.setItem(KEY, JSON.stringify(next));
80
88
  memo = null;
@@ -98,13 +106,6 @@ function subscribe(change) {
98
106
  export function useChoice() {
99
107
  return [useSyncExternalStore(subscribe, kept, () => FIRST), keep];
100
108
  }
101
- /** Whether `model` takes an effort: the router and an undescribed model do, a listed one only if it reasons. */
102
- export function reasons(model, catalog) {
103
- if (model === ENSO)
104
- return true;
105
- const row = catalog.find((m) => m.id === model);
106
- return !row || row.supports_reasoning === true;
107
- }
108
109
  /** A model's name as the catalog spells it; Enso for the router. */
109
110
  export function nameOf(model, catalog) {
110
111
  if (model === ENSO)
@@ -2,7 +2,7 @@
2
2
  * The plugins the composer offers, from `+` › Plugins and by typing `@`: only
3
3
  * those that run for this person, each on Hanzo's own services.
4
4
  *
5
- * Work in a project the org has a project (GET /v1/projects)
5
+ * Work in a project the org has a project (GET /v1/project)
6
6
  * Deep research signed in: web search, page reading and a Hanzo model, as the person
7
7
  * Sketch always: it draws in the page
8
8
  * Template Creator an org admin: only an admin writes the org's skills
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Work in a project: pick one of the org's projects (GET /v1/projects). A
3
+ * Work in a project: pick one of the org's projects (GET /v1/project). A
4
4
  * conversation does not belong to a project — work in one is a Dev run tagged
5
5
  * with it — so a message in the composer starts a Dev run on the picked project
6
6
  * (POST /v1/agent/coding, the person's saved model) and opens it; with nothing
package/lib/project.js CHANGED
@@ -64,7 +64,7 @@ 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 { useMind } from './mind.js';
67
+ import { level, useMind } from './mind.js';
68
68
  import { Out } from './out.js';
69
69
  import { Prompt, useEar } from './prompt.js';
70
70
  import { useSpoken } from './voice.js';
@@ -353,7 +353,7 @@ export function Project({ at }) {
353
353
  mode,
354
354
  // Enso named as itself: a run that names none takes the person's saved coding default.
355
355
  model: mind.model,
356
- effort: mind.effort,
356
+ effort: level(mind.effort),
357
357
  });
358
358
  setDraft('');
359
359
  setPicked([]);
@@ -424,7 +424,7 @@ export function Project({ at }) {
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
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
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'] }, mind: 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) }, 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] }));
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/prompt.d.ts CHANGED
@@ -57,8 +57,10 @@ export interface PromptOptions {
57
57
  /** Above the frame, at its width: what the draft is sent with. */
58
58
  head?: ReactNode;
59
59
  field?: ComposerProps['field'];
60
- /** The model and effort. Null where the next send uses neither (steering a run). */
61
- mind?: Mind | null;
60
+ /** The model chip: given only where the person turned the model selector on (the host's setting). Absent, no model is named. */
61
+ model?: Mind | null;
62
+ /** The effort selector (Auto, High, XHigh, Max). Null where the next send takes none (steering a run). */
63
+ effort?: Mind | null;
62
64
  limits?: PauseSource | null;
63
65
  /** A model the plan does not grant was asked for: the way to the plan that does. */
64
66
  onUpgrade?: (model: string) => void;
@@ -94,9 +96,10 @@ export interface PromptOptions {
94
96
  /** A phone: suggestions go above the frame and every target is 44px. */
95
97
  narrow?: boolean;
96
98
  }
97
- /** The row under the frame: where (Dev), then the model, the effort and the mode. */
98
- export declare function Tune({ mind, mode, where, onUpgrade, limits }: {
99
- mind?: Mind | null;
99
+ /** The row under the frame: where (Dev), then the model where the host shows it, the effort and the mode. */
100
+ export declare function Tune({ model, effort, mode, where, onUpgrade, limits, }: {
101
+ model?: Mind | null;
102
+ effort?: Mind | null;
100
103
  mode?: PromptOptions['mode'];
101
104
  where?: ReactNode;
102
105
  onUpgrade?: (model: string) => void;
package/lib/prompt.js CHANGED
@@ -6,10 +6,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * one round button that is voice mode while the draft is empty, send once it
7
7
  * has words, and stop while a turn runs. Nothing else is in the frame.
8
8
  *
9
- * Under the frame, at its end, one quiet row: the model, the effort (dim) and
10
- * the mode as a pill, each opening its choices; Dev puts where the run goes
11
- * and its repository at the row's start (`where`). Under that, the person's
12
- * suggestions (`below`; above the frame on a phone).
9
+ * Under the frame, at its end, one quiet row: the model (only where the host
10
+ * shows it), the effort (dim) and the mode as a pill, each opening its choices;
11
+ * Dev puts where the run goes and its repository at the row's start (`where`).
12
+ * Under that, the person's suggestions (`below`; above the frame on a phone).
13
13
  *
14
14
  * `/` opens the skills and `@` the plugins, in a list just above the frame.
15
15
  *
@@ -37,7 +37,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
37
37
  import { ENSO } from './api/models.js';
38
38
  import { MODES } from './gear.js';
39
39
  import { Dictation } from './mic.js';
40
- import { EFFORTS, grants, nameOf, reasons, ROUTER_IDS } from './mind.js';
40
+ import { EFFORTS, grants, nameOf, ROUTER_IDS } from './mind.js';
41
41
  import { Plus } from './plus.js';
42
42
  import { english } from './say.js';
43
43
  /** The reading measure: @hanzo/design's prose width, which Width (appearance) retunes. */
@@ -119,15 +119,14 @@ function Model({ mind, onUpgrade, limits }) {
119
119
  return (_jsx(DropdownMenuItem, { "data-slot": "model-row", "data-model": m.id, "data-paused": paused ? 'true' : undefined, onSelect: () => (ok ? mind.pick(m.id) : onUpgrade?.(m.id)), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: ok ? '$ink' : '$hint', numberOfLines: 1, flex: 1, children: nameOf(m.id, mind.models) }), ok ? null : (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 6, py: 1, rounded: 6, borderWidth: 1, borderColor: "$borderColor", children: t('Upgrade') })), ok && paused ? (_jsx(SizableText, { fontSize: 12, color: "$hint", children: t('Paused') })) : null, m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
120
120
  })] }, g.key))), more > 0 ? (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, py: 8, children: t('Type to search {count} more models', { count: more }) })) : null] })] })] }));
121
121
  }
122
- /** The row under the frame: where (Dev), then the model, the effort and the mode. */
123
- export function Tune({ mind, mode, where, onUpgrade, limits }) {
122
+ /** The row under the frame: where (Dev), then the model where the host shows it, the effort and the mode. */
123
+ export function Tune({ model, effort, mode, where, onUpgrade, limits, }) {
124
124
  const { t } = useI18n();
125
- const thinks = mind ? reasons(mind.model, mind.models) : false;
126
- const pace = mind ? EFFORTS.find((e) => e.id === mind.effort) : null;
125
+ const pace = effort ? EFFORTS.find((e) => e.id === effort.effort) : null;
127
126
  const now = mode ? MODES.find((m) => m.id === mode.value) : undefined;
128
127
  const way = now ? t(now.label) : '';
129
- return (_jsxs(XStack, { "data-slot": "tune", width: "100%", items: "center", gap: 2, flexWrap: "wrap", rowGap: 4, children: [where ? (_jsx(XStack, { items: "center", gap: 2, flexWrap: "wrap", minW: 0, "$max-sm": { width: '100%' }, children: where })) : null, _jsx(XStack, { flex: 1, "$max-sm": { display: 'none' } }), _jsxs(XStack, { items: "center", gap: 2, marginInlineStart: "auto", shrink: 0, children: [mind ? _jsx(Model, { mind: mind, onUpgrade: onUpgrade, limits: limits }) : null, mind && thinks && pace ? (_jsx(Opens, { label: t('Effort: {level}', { level: t(pace.label) }), word: t(pace.label), dim: true, children: (close) => (_jsx(Choices, { title: t('Effort'), items: EFFORTS, value: mind.effort, onPick: (e) => {
130
- mind.pace(e);
128
+ return (_jsxs(XStack, { "data-slot": "tune", width: "100%", items: "center", gap: 2, flexWrap: "wrap", rowGap: 4, children: [where ? (_jsx(XStack, { items: "center", gap: 2, flexWrap: "wrap", minW: 0, "$max-sm": { width: '100%' }, children: where })) : null, _jsx(XStack, { flex: 1, "$max-sm": { display: 'none' } }), _jsxs(XStack, { items: "center", gap: 2, marginInlineStart: "auto", shrink: 0, children: [model ? _jsx(Model, { mind: model, onUpgrade: onUpgrade, limits: limits }) : null, effort && pace ? (_jsx(Opens, { label: t('Effort: {level}', { level: t(pace.label) }), word: t(pace.label), dim: true, children: (close) => (_jsx(Choices, { title: t('Effort'), items: EFFORTS, value: effort.effort, onPick: (e) => {
129
+ effort.pace(e);
131
130
  close();
132
131
  } })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": t('Mode: {mode}', { mode: way }), items: "center", height: 24, px: 8, marginInlineStart: 4, rounded: 6, bg: "$hover", cursor: "pointer", hoverStyle: { bg: '$raised' }, focusVisibleStyle: { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' }, "$max-sm": { height: 44, px: 12 }, children: _jsx(SizableText, { fontSize: 14, color: "$ink", children: way }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: t('Mode'), items: mode.items ? MODES.filter((m) => mode.items.includes(m.id)) : MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
133
132
  }
@@ -163,7 +162,7 @@ export function prompt(o, state) {
163
162
  };
164
163
  const mention = listed && shelf && typed ? (_jsx(Mention, { shelf: shelf, rows: listed, index: index, onPick: choose, skills: typed.char === '/' })) : null;
165
164
  const head = o.head || o.note || mention || (narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: "$1.5", "data-slot": "prompt-head", children: [narrow ? o.below : null, o.head, mention, o.note ? (_jsx(SizableText, { "data-slot": "prompt-note", fontSize: 12, color: "$soft", text: "center", self: "center", children: o.note })) : null] })) : undefined;
166
- const foot = o.mind || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { mind: o.mind, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade, limits: o.limits }), narrow ? null : o.below] })) : undefined;
165
+ const foot = o.model || o.effort || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { model: o.model, effort: o.effort, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade, limits: o.limits }), narrow ? null : o.below] })) : undefined;
167
166
  const round = (_jsx(Control, { type: "button", size: narrow ? TAP : ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": t(busy ? 'Stop' : 'Send'), "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
168
167
  if (!live)
169
168
  return;
package/lib/run.js CHANGED
@@ -41,7 +41,7 @@ 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 { useMind } from './mind.js';
44
+ import { level, 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';
@@ -206,7 +206,7 @@ export function Run({ id }) {
206
206
  * itself: a run that names none takes the person's saved coding default
207
207
  * (apps/coding start.go), which is not what the composer shows.
208
208
  */
209
- const tuned = (a) => ({ ...a, model: mind.model, effort: mind.effort });
209
+ const tuned = (a) => ({ ...a, model: mind.model, effort: level(mind.effort) });
210
210
  // A setup run's work IS its environment: the panel opens on it.
211
211
  useEffect(() => {
212
212
  if (mode === 'setup')
@@ -386,7 +386,7 @@ export function Run({ id }) {
386
386
  ...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
387
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) },
388
388
  // What a new run from here is made with; words that steer this run use neither.
389
- mind: signed && !steering ? mind : null, limits: limits, voice: voice })] })] }), desk && !wide ? desk_(true) : null, desk && wide ? (_jsxs(YStack, { "data-slot": "desk", ref: split.aside, ...pane,
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,
390
390
  // A pane of its own beside the run's, the gutter between them (useSplit reads it as the inline-start margin).
391
391
  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
392
  if (e.key === 'Enter')
@@ -60,7 +60,7 @@ export function Code() {
60
60
  // The choices in the reader's words, kept by id.
61
61
  const efforts = EFFORTS.map((e) => ({ ...e, label: t(e.label) }));
62
62
  const modes = MODES.map((m) => ({ ...m, label: t(m.label), hint: t(m.hint) }));
63
- const effort = efforts.find((e) => e.id === choice.effort);
63
+ const effort = efforts.find((e) => e.id === choice.effort) ?? efforts[0];
64
64
  const mode = modes.find((m) => m.id === now.mode);
65
65
  return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Code'), detail: t('How a new run starts. The model and effort are the ones Chat uses too.') }), _jsxs(Group, { title: t('Defaults'), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: t('Model'), detail: t('Enso picks one for each step; any other model runs them all.'), trailing: _jsx(ModelPicker, { size: "sm", name: t('Default model'), models: offered, scope: "chat", value: choice.model || ENSO, onChange: (id) => choose({ model: id }), loading: catalog.loading, error: catalog.error }) }), _jsx(Row, { title: t('Effort'), detail: t('How long the agent thinks before it acts.'), trailing: _jsx(ChipSelect, { name: t('Default effort'), label: effort.label, chosen: effort, items: efforts, onChange: (e) => choose({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: t('Mode'), detail: mode.hint, trailing: _jsx(ChipSelect, { name: t('Default mode'), label: mode.label, chosen: mode, items: modes, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: t('Where it runs'), detail: t('The Hanzo sandbox, or one of your organization’s machines.'), trailing: _jsx(ChipSelect, { name: t('Default place'), label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: t('Search machines…'), placement: "bottom-end", loading: places.loading, error: places.error ? told(places.error, t) : null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place, t) })] }), _jsx(Group, { title: t('Workspace'), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: t('Environments'), detail: t('What a run installs and starts in each codebase, and the secrets it exports.'), trailing: _jsx(Link, { label: t('Open Environments'), onPress: () => go('environments') }) }), _jsx(Row, { title: t('Machines'), detail: t('Your organization’s own computers that take runs.'), trailing: _jsx(Link, { label: t('Open Machines'), onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
66
66
  }
package/lib/shelf.js CHANGED
@@ -3,9 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * Two shelves: the sites this org has deployed (Sites, drawn at the foot of the
4
4
  * Library — library.tsx), and the public starters (Templates).
5
5
  *
6
- * A project is a real row — `/v1/projects`, the same one a deployed site is
6
+ * A project is a real row — `/v1/project`, the same one a deployed site is
7
7
  * served from — so opening one opens its workspace at its own address. A
8
- * starter is taken as a COPY of its own repository (`POST /v1/projects/fork`),
8
+ * starter is taken as a COPY of its own repository (`POST /v1/project/fork`),
9
9
  * so what opens is that template, not a model's imitation of it.
10
10
  */
11
11
  import { SizableText, XStack, YStack } from '@hanzo/gui';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.81",
3
+ "version": "0.2.83",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",