@hanzo/build 0.2.69 → 0.2.72
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/lib/carry.d.ts +15 -4
- package/lib/carry.js +40 -7
- package/lib/gear.d.ts +10 -4
- package/lib/gear.js +12 -5
- package/lib/gutter.d.ts +17 -0
- package/lib/gutter.js +25 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/landing.js +10 -4
- package/lib/plugins/index.d.ts +1 -1
- package/lib/plugins/index.js +4 -2
- package/lib/project.js +3 -2
- package/lib/prompt.d.ts +1 -0
- package/lib/prompt.js +1 -1
- package/lib/run.js +4 -9
- package/lib/suggest.d.ts +4 -0
- package/lib/suggest.js +15 -2
- package/lib/suggestions.d.ts +1 -1
- package/lib/suggestions.js +3 -2
- package/lib/summary.js +8 -5
- package/package.json +1 -1
package/lib/carry.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type Target } from './api/call.ts';
|
|
3
|
-
import { type GearState } from './gear.ts';
|
|
3
|
+
import { type GearState, type Mode } from './gear.ts';
|
|
4
4
|
import type { Composer, Prep, Turn } from './plugin.ts';
|
|
5
5
|
import type { Shelf } from './plus.tsx';
|
|
6
|
-
import type { Chip } from './prompt.tsx';
|
|
6
|
+
import type { Chip, PromptOptions } from './prompt.tsx';
|
|
7
7
|
/** What the surface lends the plugins: its draft, its files, its way about. */
|
|
8
8
|
export type Lent = Omit<Composer, 'target' | 'show'>;
|
|
9
9
|
export interface Carry {
|
|
@@ -21,11 +21,22 @@ export interface Carry {
|
|
|
21
21
|
prep: (turn: Omit<Turn, 'design'>, signal: AbortSignal) => Promise<Prep>;
|
|
22
22
|
/** The chips after `+`: the skills on, Research, and Plan. */
|
|
23
23
|
on: Chip[];
|
|
24
|
+
/** The mode pill: this surface's modes, the conversation's choice among them. */
|
|
25
|
+
mode: NonNullable<PromptOptions['mode']>;
|
|
26
|
+
/** Whether `what` may run: in Manual the person is asked (once, always in this conversation, or not); else yes. */
|
|
27
|
+
allow: (what: Asked) => Promise<boolean>;
|
|
24
28
|
/** The words the gear adds to every message, '' when none. */
|
|
25
29
|
text: string;
|
|
26
30
|
}
|
|
31
|
+
/** What Manual asks about: a plugin, or a Dev run. `id` is what "always" remembers. */
|
|
32
|
+
export interface Asked {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
note: string;
|
|
36
|
+
}
|
|
27
37
|
/**
|
|
28
38
|
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
29
|
-
* `customize(tab)` opens Dev's Customize on that tab
|
|
39
|
+
* `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
|
|
40
|
+
* surface offers (a choice it does not offer reads as Auto).
|
|
30
41
|
*/
|
|
31
|
-
export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent): Carry;
|
|
42
|
+
export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent, modes: readonly Mode[]): Carry;
|
package/lib/carry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* What a message carries beside its words, assembled once for Chat and Dev:
|
|
4
4
|
* the conversation's gear (gear.ts) drawn as the `+` menu's shelves and the
|
|
@@ -10,12 +10,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
* field, so the text rides the message
|
|
11
11
|
* memory what Hanzo remembers about the person (GET /v1/ai/memory/list):
|
|
12
12
|
* the platform injects none itself
|
|
13
|
-
* mode Plan: lay the plan out and wait; Auto says nothing
|
|
13
|
+
* mode Plan: lay the plan out and wait; Auto says nothing; Manual
|
|
14
|
+
* asks the person before each plugin runs for a message (`Allow`)
|
|
14
15
|
*
|
|
15
16
|
* A shelf is offered only where its service answers; a surface adds its own
|
|
16
17
|
* (Chat its agents, Dev its devices) beside these.
|
|
17
18
|
*/
|
|
19
|
+
import { SizableText, XStack } from '@hanzo/gui';
|
|
18
20
|
import { BookOpen, ListChecks, Palette, ScrollText } from '@hanzogui/lucide-icons-2';
|
|
21
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle } from '@hanzo/ui';
|
|
19
22
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
23
|
import { call, query, unwrap } from './api/call.js';
|
|
21
24
|
import { active, authored, document, nameOf as skillOf } from './api/skills.js';
|
|
@@ -60,11 +63,16 @@ async function said(t, skills, memory, mode, design) {
|
|
|
60
63
|
parts.push('Plan mode: lay out the plan — the steps, what each changes and why — and stop. Change nothing and produce no final work until the person says go.');
|
|
61
64
|
return parts.join('\n\n');
|
|
62
65
|
}
|
|
66
|
+
/** Manual's pause: what is about to run, what it does, and the three answers. Closing it is a no. */
|
|
67
|
+
function Allow({ what, onAnswer }) {
|
|
68
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onAnswer('deny'), children: _jsxs(DialogContent, { maxW: 440, width: "100%", "data-slot": "allow", children: [_jsx(DialogTitle, { children: `Allow ${what.name}?` }), _jsx(DialogDescription, { children: what.note ? `${what.note}. Manual asks before each one runs.` : 'Manual asks before each one runs.' }), _jsxs(XStack, { justify: "flex-end", gap: "$2", pt: "$3", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onAnswer('deny'), children: "Deny" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAnswer('always'), children: _jsx(SizableText, { fontSize: 14, children: "Always in this chat" }) }), _jsx(Button, { size: "sm", onPress: () => onAnswer('once'), children: "Allow once" })] })] }) }));
|
|
69
|
+
}
|
|
63
70
|
/**
|
|
64
71
|
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
65
|
-
* `customize(tab)` opens Dev's Customize on that tab
|
|
72
|
+
* `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
|
|
73
|
+
* surface offers (a choice it does not offer reads as Auto).
|
|
66
74
|
*/
|
|
67
|
-
export function useCarry(t, key, customize, lent) {
|
|
75
|
+
export function useCarry(t, key, customize, lent, modes) {
|
|
68
76
|
const gear = useGear(t, key);
|
|
69
77
|
const [dialog, show] = useState(null);
|
|
70
78
|
// The composer a plugin acts on, read when it acts: the draft moves under it.
|
|
@@ -84,7 +92,8 @@ export function useCarry(t, key, customize, lent) {
|
|
|
84
92
|
const signed = Boolean(t.token());
|
|
85
93
|
const list = useRead(signed ? () => active(t) : null, [], [t, signed]);
|
|
86
94
|
const names = useMemo(() => list.value.map((s) => skillOf(s.name)).filter(Boolean), [list.value]);
|
|
87
|
-
const { skills, research, memory,
|
|
95
|
+
const { skills, research, memory, design } = gear.gear;
|
|
96
|
+
const mode = modes.includes(gear.gear.mode) ? gear.gear.mode : 'auto';
|
|
88
97
|
const shelf = {
|
|
89
98
|
rows: names.map((n) => ({ id: n, label: n, icon: _jsx(ScrollText, { size: 16 }), on: skills.includes(n) })),
|
|
90
99
|
pick: (id) => gear.toggle('skills', id),
|
|
@@ -141,8 +150,26 @@ export function useCarry(t, key, customize, lent) {
|
|
|
141
150
|
// `skills` is read through `asked`, its contents.
|
|
142
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
152
|
}, [t, signedIn, asked, memory, mode, design]);
|
|
153
|
+
// Manual: what is about to run is asked of the person, unless allowed for the rest of the conversation.
|
|
154
|
+
const kept = useRef([]);
|
|
155
|
+
kept.current = gear.gear.allowed;
|
|
156
|
+
const allow = async (what) => {
|
|
157
|
+
if (mode !== 'manual' || kept.current.includes(what.id))
|
|
158
|
+
return true;
|
|
159
|
+
const v = await new Promise((done) => show(_jsx(Allow, { what: what, onAnswer: (x) => {
|
|
160
|
+
show(null);
|
|
161
|
+
done(x);
|
|
162
|
+
} })));
|
|
163
|
+
if (v === 'always') {
|
|
164
|
+
kept.current = [...kept.current, what.id];
|
|
165
|
+
gear.set({ allowed: kept.current });
|
|
166
|
+
}
|
|
167
|
+
return v !== 'deny';
|
|
168
|
+
};
|
|
144
169
|
return {
|
|
145
170
|
gear,
|
|
171
|
+
mode: { value: mode, set: (m) => gear.set({ mode: m }), items: modes },
|
|
172
|
+
allow,
|
|
146
173
|
skills: shelf,
|
|
147
174
|
plugins,
|
|
148
175
|
dialog,
|
|
@@ -151,10 +178,16 @@ export function useCarry(t, key, customize, lent) {
|
|
|
151
178
|
preparing: lit.length > 0,
|
|
152
179
|
design: designs,
|
|
153
180
|
// A design system a plugin made is attached at once, and the maker turns off: it made one.
|
|
154
|
-
prep:
|
|
181
|
+
prep: async (turn, signal) => {
|
|
182
|
+
// One plugin at a time, so each pause is its own question.
|
|
183
|
+
const denied = [];
|
|
184
|
+
for (const x of lit)
|
|
185
|
+
if (!signal.aborted && !(await allow({ id: x.id, name: x.name, note: x.note })))
|
|
186
|
+
denied.push(x.id);
|
|
187
|
+
const p = await prep(found.plugins, gear.gear, { ...turn, design }, composer, signal, denied);
|
|
155
188
|
if (p.design)
|
|
156
189
|
gear.set({ design: p.design, plugins: gear.gear.plugins.filter((x) => x !== 'design') });
|
|
157
190
|
return p;
|
|
158
|
-
}
|
|
191
|
+
},
|
|
159
192
|
};
|
|
160
193
|
}
|
package/lib/gear.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { type Target } from './api/call.ts';
|
|
2
2
|
/** What a run or a turn may do on its own: the permission mode. */
|
|
3
|
-
export type Mode = 'auto' | 'plan';
|
|
3
|
+
export type Mode = 'auto' | 'manual' | 'plan';
|
|
4
4
|
/**
|
|
5
|
-
* The modes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The modes, one line each. Chat offers Auto and Manual; Dev adds Plan. Manual
|
|
6
|
+
* is a real pause: before a plugin runs for a message (a search, a site read, a
|
|
7
|
+
* deck), the person allows it once, always in this conversation, or not, and a
|
|
8
|
+
* Dev run is asked for before it starts.
|
|
8
9
|
*/
|
|
9
10
|
export declare const MODES: readonly {
|
|
10
11
|
id: Mode;
|
|
11
12
|
label: string;
|
|
12
13
|
note: string;
|
|
13
14
|
}[];
|
|
15
|
+
/** The modes each surface offers. */
|
|
16
|
+
export declare const CHAT_MODES: readonly Mode[];
|
|
17
|
+
export declare const DEV_MODES: readonly Mode[];
|
|
14
18
|
export interface Gear {
|
|
15
19
|
/** Skill names (`/v1/tool/skills`). */
|
|
16
20
|
skills: string[];
|
|
@@ -25,6 +29,8 @@ export interface Gear {
|
|
|
25
29
|
research: boolean;
|
|
26
30
|
memory: boolean;
|
|
27
31
|
mode: Mode;
|
|
32
|
+
/** In Manual, the plugins allowed for the rest of this conversation. */
|
|
33
|
+
allowed: string[];
|
|
28
34
|
}
|
|
29
35
|
export declare const BARE: Gear;
|
|
30
36
|
/** How many conversations keep a choice of their own: the document holds 16 KiB in all. */
|
package/lib/gear.js
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
19
|
import { call } from './api/call.js';
|
|
20
20
|
/**
|
|
21
|
-
* The modes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The modes, one line each. Chat offers Auto and Manual; Dev adds Plan. Manual
|
|
22
|
+
* is a real pause: before a plugin runs for a message (a search, a site read, a
|
|
23
|
+
* deck), the person allows it once, always in this conversation, or not, and a
|
|
24
|
+
* Dev run is asked for before it starts.
|
|
24
25
|
*/
|
|
25
26
|
export const MODES = [
|
|
26
|
-
{ id: 'auto', label: 'Auto', note: '
|
|
27
|
-
{ id: '
|
|
27
|
+
{ id: 'auto', label: 'Auto', note: 'Runs tools on its own; pauses only for anything risky.' },
|
|
28
|
+
{ id: 'manual', label: 'Manual', note: 'Asks before each tool, connector or site it opens.' },
|
|
29
|
+
{ id: 'plan', label: 'Plan', note: 'Writes the plan and waits for your go before changing anything.' },
|
|
28
30
|
];
|
|
31
|
+
/** The modes each surface offers. */
|
|
32
|
+
export const CHAT_MODES = ['auto', 'manual'];
|
|
33
|
+
export const DEV_MODES = ['auto', 'manual', 'plan'];
|
|
29
34
|
export const BARE = {
|
|
30
35
|
skills: [],
|
|
31
36
|
connectors: [],
|
|
@@ -35,6 +40,7 @@ export const BARE = {
|
|
|
35
40
|
research: false,
|
|
36
41
|
memory: true,
|
|
37
42
|
mode: 'auto',
|
|
43
|
+
allowed: [],
|
|
38
44
|
};
|
|
39
45
|
/** What a new conversation carries over from the last: the switches, never a pick. */
|
|
40
46
|
const STICKY = ['research', 'memory', 'mode'];
|
|
@@ -55,6 +61,7 @@ export function gearOf(raw, base = BARE) {
|
|
|
55
61
|
research: typeof o.research === 'boolean' ? o.research : base.research,
|
|
56
62
|
memory: typeof o.memory === 'boolean' ? o.memory : base.memory,
|
|
57
63
|
mode: MODES.some((m) => m.id === o.mode) ? o.mode : base.mode,
|
|
64
|
+
allowed: 'allowed' in o ? list(o.allowed) : base.allowed,
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
67
|
/** `gears` with `key`'s entry first, cut to `KEEP`. */
|
package/lib/gutter.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grip between two panes, one for every edge: the sidebar's, the side
|
|
3
|
+
* panel's, a run's chat against its work. The kit's Grip with its `inline` bar
|
|
4
|
+
* (drawn only while it is reached for), its band centred across the gutter and
|
|
5
|
+
* the bar halfway down it, so each edge looks and moves the same.
|
|
6
|
+
*
|
|
7
|
+
* side the edge the box it sits in is pinned to (Grip's): `left` for a
|
|
8
|
+
* sidebar, whose free edge is its right, `right` for an aside
|
|
9
|
+
* gap the gutter's width; the frame's `--pane-gap` by default
|
|
10
|
+
* inset the box's own border, px: an absolute place is measured inside it
|
|
11
|
+
*/
|
|
12
|
+
import { type GripProps } from '@hanzo/ui';
|
|
13
|
+
export type GutterProps = Omit<GripProps, 'bar'> & {
|
|
14
|
+
gap?: string | number;
|
|
15
|
+
inset?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function Gutter({ side, gap, inset, ...rest }: GutterProps): import("react").JSX.Element;
|
package/lib/gutter.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The grip between two panes, one for every edge: the sidebar's, the side
|
|
4
|
+
* panel's, a run's chat against its work. The kit's Grip with its `inline` bar
|
|
5
|
+
* (drawn only while it is reached for), its band centred across the gutter and
|
|
6
|
+
* the bar halfway down it, so each edge looks and moves the same.
|
|
7
|
+
*
|
|
8
|
+
* side the edge the box it sits in is pinned to (Grip's): `left` for a
|
|
9
|
+
* sidebar, whose free edge is its right, `right` for an aside
|
|
10
|
+
* gap the gutter's width; the frame's `--pane-gap` by default
|
|
11
|
+
* inset the box's own border, px: an absolute place is measured inside it
|
|
12
|
+
*/
|
|
13
|
+
import { Grip } from '@hanzo/ui';
|
|
14
|
+
import { gap as GAP } from './pane.js';
|
|
15
|
+
export function Gutter({ side, gap = GAP, inset = 0, ...rest }) {
|
|
16
|
+
const g = typeof gap === 'number' ? `${gap}px` : gap;
|
|
17
|
+
// A CSS length gui takes as written; its types name only tokens and numbers.
|
|
18
|
+
const band = `max(8px, ${g})`;
|
|
19
|
+
// The band's start, so its middle is the gutter's: half the gutter and half the band out from the edge.
|
|
20
|
+
const at = (w) => `calc(-1 * (${g} + ${w}) / 2 - ${inset}px)`;
|
|
21
|
+
const edge = side === 'left' ? 'r' : 'l';
|
|
22
|
+
return (_jsx(Grip, { side: side, bar: "inline", justify: "center", width: band, [edge]: at(band),
|
|
23
|
+
// A finger's band, centred on the same line.
|
|
24
|
+
$touchable: { width: 44, [edge]: at('44px') }, ...rest }));
|
|
25
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
|
30
30
|
export { HostProvider } from './host.tsx';
|
|
31
31
|
export { Slack, useWho, Who } from './foot.tsx';
|
|
32
32
|
export { Grip, type GripProps } from '@hanzo/ui';
|
|
33
|
+
export { Gutter, type GutterProps } from './gutter.tsx';
|
|
33
34
|
export { administers } from './claims.ts';
|
|
34
35
|
export { Meter, Title } from './meter.tsx';
|
|
35
36
|
export { Plan } from './settings/plan.tsx';
|
|
@@ -51,7 +52,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Scree
|
|
|
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
53
|
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
|
|
53
54
|
export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
|
|
54
|
-
export { BARE, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
|
|
55
|
+
export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
|
|
55
56
|
export { useCarry, type Carry } from './carry.tsx';
|
|
56
57
|
export { Dictation } from './mic.tsx';
|
|
57
58
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker, type Artifact, type Listen } from './reply.tsx';
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { DevSection, DOTS, useSessions } from './section.js';
|
|
|
30
30
|
export { HostProvider } from './host.js';
|
|
31
31
|
export { Slack, useWho, Who } from './foot.js';
|
|
32
32
|
export { Grip } from '@hanzo/ui';
|
|
33
|
+
export { Gutter } from './gutter.js';
|
|
33
34
|
export { administers } from './claims.js';
|
|
34
35
|
export { Meter, Title } from './meter.js';
|
|
35
36
|
export { Plan } from './settings/plan.js';
|
|
@@ -46,7 +47,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
|
|
|
46
47
|
export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
|
|
47
48
|
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
|
|
48
49
|
export { Plus } from './plus.js';
|
|
49
|
-
export { BARE, gearOf, MODES, useGear, useLanguage } from './gear.js';
|
|
50
|
+
export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage } from './gear.js';
|
|
50
51
|
export { useCarry } from './carry.js';
|
|
51
52
|
export { Dictation } from './mic.js';
|
|
52
53
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
|
package/lib/landing.js
CHANGED
|
@@ -36,6 +36,7 @@ import { usePrefs } from './prefs.js';
|
|
|
36
36
|
import { Publish } from './publish.js';
|
|
37
37
|
import { path } from './route.js';
|
|
38
38
|
import { useCarry } from './carry.js';
|
|
39
|
+
import { DEV_MODES } from './gear.js';
|
|
39
40
|
import { Suggestions, useSuggestions } from './suggestions.js';
|
|
40
41
|
import { MEASURE, Prompt, useEar } from './prompt.js';
|
|
41
42
|
import { Monitor as Device } from '@hanzogui/lucide-icons-2';
|
|
@@ -134,8 +135,8 @@ export function Landing({ onStarted }) {
|
|
|
134
135
|
}
|
|
135
136
|
},
|
|
136
137
|
go: (href) => (href.startsWith('http') ? host.open(href) : host.go(href)),
|
|
137
|
-
});
|
|
138
|
-
const mode = carry.
|
|
138
|
+
}, DEV_MODES);
|
|
139
|
+
const mode = carry.mode.value === 'plan' ? 'plan' : 'build';
|
|
139
140
|
const hints = useSuggestions(t);
|
|
140
141
|
// The chosen codebase's environment, so New can say when it has none yet.
|
|
141
142
|
const codebase = isForge(kept.repo) ? kept.repo.name : '';
|
|
@@ -209,6 +210,11 @@ export function Landing({ onStarted }) {
|
|
|
209
210
|
setNote(prepped.reply.split('```')[0].trim());
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
213
|
+
// Manual: the run is asked for before it starts; it edits, commits and pushes on its own once it has.
|
|
214
|
+
if (!(await carry.allow({ id: 'run', name: 'Dev to start this run', note: 'It edits, tests, commits and pushes its own branch' }))) {
|
|
215
|
+
setNote('Not started.');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
212
218
|
const run = await start(t, {
|
|
213
219
|
prompt: compose([draft.trim(), carry.text, prepped.system ?? ''].filter(Boolean).join('\n\n---\n'), files),
|
|
214
220
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
@@ -304,7 +310,7 @@ export function Landing({ onStarted }) {
|
|
|
304
310
|
loading: places.loading,
|
|
305
311
|
error: places.error?.message ?? null,
|
|
306
312
|
};
|
|
307
|
-
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: "$
|
|
313
|
+
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: "Build what\u2019s 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: `${codebase} is not one of this organization’s repositories.` }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: "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'
|
|
308
314
|
? `A proposed environment for ${codebase} is waiting for review.`
|
|
309
315
|
: `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(mode, place.id) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: "Describe a task or ask a question", head: head, plus: {
|
|
310
316
|
upload: () => pick(files, setFiles, setNote),
|
|
@@ -313,7 +319,7 @@ export function Landing({ onStarted }) {
|
|
|
313
319
|
plugins: carry.plugins,
|
|
314
320
|
devices,
|
|
315
321
|
memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
|
|
316
|
-
}, on: carry.on, slash: carry.skills, at: carry.plugins, mode:
|
|
322
|
+
}, on: carry.on, slash: carry.skills, at: carry.plugins, mode: carry.mode, where: where, below: _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, 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: () => {
|
|
317
323
|
setOffering(false);
|
|
318
324
|
env.reload();
|
|
319
325
|
} })) : null] }));
|
package/lib/plugins/index.d.ts
CHANGED
|
@@ -24,4 +24,4 @@ export declare function merge(preps: readonly Prep[]): Prep;
|
|
|
24
24
|
* fails the message, with its name and its reason: a report asked for and not
|
|
25
25
|
* researched must not arrive looking researched.
|
|
26
26
|
*/
|
|
27
|
-
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal): Promise<Prep>;
|
|
27
|
+
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal, denied?: readonly string[]): Promise<Prep>;
|
package/lib/plugins/index.js
CHANGED
|
@@ -116,8 +116,10 @@ export function merge(preps) {
|
|
|
116
116
|
* fails the message, with its name and its reason: a report asked for and not
|
|
117
117
|
* researched must not arrive looking researched.
|
|
118
118
|
*/
|
|
119
|
-
export async function prep(plugins, g, turn, c, signal) {
|
|
120
|
-
const preps = await Promise.all(active(plugins, g)
|
|
119
|
+
export async function prep(plugins, g, turn, c, signal, denied = []) {
|
|
120
|
+
const preps = await Promise.all(active(plugins, g)
|
|
121
|
+
.filter((p) => !denied.includes(p.id))
|
|
122
|
+
.map((p) => p.prepare(turn, c, signal).catch((e) => {
|
|
121
123
|
if (signal.aborted)
|
|
122
124
|
throw e;
|
|
123
125
|
throw new Error(`${p.name}: ${say(e)}`);
|
package/lib/project.js
CHANGED
|
@@ -42,7 +42,7 @@ import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
|
42
42
|
import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
|
|
43
43
|
import { Attachments, CHAT, Console, DEVICES, Feedback, FileTabs, FileTree, PageSelect, PreviewFrame, ProjectChip, VIEWS, Views, Workspace, fold, } from '@hanzo/ui/agents';
|
|
44
44
|
import { Message } from '@hanzo/ui/chat';
|
|
45
|
-
import { Button, Dialog, DialogContent, DialogTitle
|
|
45
|
+
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
46
46
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
47
47
|
import { useDictation, useVoice } from '@hanzo/voice';
|
|
48
48
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -57,6 +57,7 @@ import { decode, outcome, pull, said, scrub, who } from './api/turn.js';
|
|
|
57
57
|
import { verdict as record } from './api/verdict.js';
|
|
58
58
|
import { useKept, useProjects, useRead, useRun } from './data.js';
|
|
59
59
|
import { Merge } from './git.js';
|
|
60
|
+
import { Gutter } from './gutter.js';
|
|
60
61
|
import { useHost, useTarget } from './host.js';
|
|
61
62
|
import { useMind } from './mind.js';
|
|
62
63
|
import { Out } from './out.js';
|
|
@@ -416,7 +417,7 @@ export function Project({ at }) {
|
|
|
416
417
|
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
417
418
|
: `${slug} is loaded. Say what to change and it gets built.` })) : (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", pl: "$2", borderLeftWidth: 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) })).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: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", 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
|
|
418
419
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
419
|
-
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", plus: { upload: () => pick(attached, setAttached, setNote) }, mode: running ? null : { value: mode === 'plan' ? 'plan' : 'auto', set: (m) => setMode(m === 'plan' ? 'plan' : 'build') }, mind: running ? null : mind, voice: voice })] }), wide && !shut ? (_jsx(
|
|
420
|
+
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", plus: { upload: () => pick(attached, setAttached, setNote) }, 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: "Resize the chat", gap: GUTTER })) : null] }));
|
|
420
421
|
// The preview stays loaded while another view is open, so coming back to it
|
|
421
422
|
// is the page as it was left.
|
|
422
423
|
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": "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: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "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: `${title} preview`, 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
package/lib/prompt.js
CHANGED
|
@@ -113,7 +113,7 @@ export function Tune({ mind, mode, where, onUpgrade }) {
|
|
|
113
113
|
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, ml: "auto", shrink: 0, children: [mind ? _jsx(Model, { mind: mind, onUpgrade: onUpgrade }) : null, mind && thinks && pace ? (_jsx(Opens, { label: `Effort: ${pace.label}`, word: pace.label, dim: true, children: (close) => (_jsx(Choices, { title: "Effort", items: EFFORTS, value: mind.effort, onPick: (e) => {
|
|
114
114
|
mind.pace(e);
|
|
115
115
|
close();
|
|
116
|
-
} })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": `Mode: ${MODES.find((m) => m.id === mode.value)?.label}`, items: "center", height: 24, px: 8, ml: 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: MODES.find((m) => m.id === mode.value)?.label }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: "Mode", items: MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
|
|
116
|
+
} })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": `Mode: ${MODES.find((m) => m.id === mode.value)?.label}`, items: "center", height: 24, px: 8, ml: 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: MODES.find((m) => m.id === mode.value)?.label }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: "Mode", items: mode.items ? MODES.filter((m) => mode.items.includes(m.id)) : MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
|
|
117
117
|
}
|
|
118
118
|
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
119
119
|
export function trigger(value) {
|
package/lib/run.js
CHANGED
|
@@ -25,7 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
*/
|
|
26
26
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
27
27
|
import { ChevronDown, ChevronRight, Clock, Copy, ExternalLink, GitPullRequest, Globe, Pencil, Share } from '@hanzogui/lucide-icons-2';
|
|
28
|
-
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu,
|
|
28
|
+
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
29
29
|
import { Steer } from '@hanzo/ui/agents';
|
|
30
30
|
import { Failure } from '@hanzo/ui/chat';
|
|
31
31
|
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
@@ -41,6 +41,7 @@ import { Desk, SEED } from './desk.js';
|
|
|
41
41
|
import { useHost, useTarget } from './host.js';
|
|
42
42
|
import { useMind } from './mind.js';
|
|
43
43
|
import { Out } from './out.js';
|
|
44
|
+
import { Gutter } from './gutter.js';
|
|
44
45
|
import { gap, pane } from './pane.js';
|
|
45
46
|
import { measure, Prompt, useEar } from './prompt.js';
|
|
46
47
|
import { useSpoken } from './voice.js';
|
|
@@ -372,15 +373,9 @@ export function Run({ id }) {
|
|
|
372
373
|
...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
|
|
373
374
|
] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : '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 ? 'Steer this run' : '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) },
|
|
374
375
|
// What a new run from here is made with; words that steer this run use neither.
|
|
375
|
-
mind: signed && !steering ? mind : null, limits: limits, voice: voice
|
|
376
|
+
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,
|
|
376
377
|
// A pane of its own beside the run's, the gutter between them.
|
|
377
|
-
ml: gap, position: "relative", shrink: 0, minH: 0, style: split.asideStyle, children: [split.grip ? (_jsx(
|
|
378
|
-
// The handle halfway down the edge, where a hand looks for it.
|
|
379
|
-
justify: "center",
|
|
380
|
-
// In the gutter between the two panes; `left` is measured inside the pane's 1px edge.
|
|
381
|
-
l: `calc(-1 * ${gap} - ${pane.borderWidth}px)`, width: gap,
|
|
382
|
-
// A finger's band, centred on the gutter.
|
|
383
|
-
$touchable: { l: `calc(-22px - ${gap} / 2 - ${pane.borderWidth}px)`, width: 44 } })) : 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: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
|
|
378
|
+
ml: gap, position: "relative", shrink: 0, minH: 0, style: split.asideStyle, children: [split.grip ? (_jsx(Gutter, { ...split.grip, label: "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: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
|
|
384
379
|
if (e.key === 'Enter')
|
|
385
380
|
save();
|
|
386
381
|
} }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
|
package/lib/suggest.d.ts
CHANGED
|
@@ -196,6 +196,10 @@ export declare function answer(raw: unknown, inputs: Input[]): Suggestion[];
|
|
|
196
196
|
export declare function kept(raw: unknown): Kept | undefined;
|
|
197
197
|
/** The rows to draw: those not dismissed. */
|
|
198
198
|
export declare const shown: (k: Kept | null | undefined) => Suggestion[];
|
|
199
|
+
/** What Hanzo does, three ways in: what fills the rows a person's activity and signup do not. */
|
|
200
|
+
export declare const STARTERS: readonly Suggestion[];
|
|
201
|
+
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
202
|
+
export declare const fill: (k: Kept | null | undefined) => Suggestion[];
|
|
199
203
|
/** `k` with `id` dismissed: remembered first, the oldest forgotten past `REMEMBER`. */
|
|
200
204
|
export declare function dismissed(k: Kept, id: string): Kept;
|
|
201
205
|
/** The addresses and titles of dismissed rows the model is told to skip. */
|
package/lib/suggest.js
CHANGED
|
@@ -190,9 +190,9 @@ const SYSTEM = [
|
|
|
190
190
|
'- prompt: the message the person would send Hanzo to do the step, first person, complete on its own, naming the source and its address when it has one.',
|
|
191
191
|
'- href: copied exactly from the input the row is about, or "" when that input has none. Never write an address that is not in the input.',
|
|
192
192
|
'- icon: github for a github.com address, agent for a run or an automation, chat for a conversation, file for a file, task for any other issue or task.',
|
|
193
|
-
'
|
|
193
|
+
'Answer three rows. Take them from the activity first; fill the rest from what the person told us at signup (their role, their work): first things to ask Hanzo, with href "".',
|
|
194
194
|
'Never repeat an item listed under skip. Never suggest the same item twice.',
|
|
195
|
-
'
|
|
195
|
+
'With neither activity nor anything from signup worth a row, answer fewer; never pad with a weak one.',
|
|
196
196
|
].join('\n');
|
|
197
197
|
/** The chat completion that asks for rows, or null when there is nothing to make them from. */
|
|
198
198
|
export function request(inputs, seed, skip, now) {
|
|
@@ -309,6 +309,19 @@ export function kept(raw) {
|
|
|
309
309
|
}
|
|
310
310
|
/** The rows to draw: those not dismissed. */
|
|
311
311
|
export const shown = (k) => (k ? k.rows.filter((r) => !k.dismissed.includes(r.id)).slice(0, MOST) : []);
|
|
312
|
+
/** What Hanzo does, three ways in: what fills the rows a person's activity and signup do not. */
|
|
313
|
+
export const STARTERS = [
|
|
314
|
+
{ id: 'start:chat', icon: 'chat', title: 'Think something through', href: '', summary: '', action: 'Ask Hanzo', prompt: 'Help me think through ' },
|
|
315
|
+
{ id: 'start:dev', icon: 'agent', title: 'Build an app or a site', href: '', summary: '', action: 'Describe it', prompt: 'Build me ' },
|
|
316
|
+
{ id: 'start:research', icon: 'file', title: 'Research a topic', href: '', summary: '', action: 'Get a cited brief', prompt: 'Research this and give me a short brief with sources: ' },
|
|
317
|
+
];
|
|
318
|
+
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
319
|
+
export const fill = (k) => {
|
|
320
|
+
if (!k)
|
|
321
|
+
return [];
|
|
322
|
+
const rows = shown(k);
|
|
323
|
+
return [...rows, ...STARTERS.filter((r) => !k.dismissed.includes(r.id))].slice(0, Math.max(rows.length, MOST));
|
|
324
|
+
};
|
|
312
325
|
/** `k` with `id` dismissed: remembered first, the oldest forgotten past `REMEMBER`. */
|
|
313
326
|
export function dismissed(k, id) {
|
|
314
327
|
return { ...k, dismissed: [id, ...k.dismissed.filter((x) => x !== id)].slice(0, REMEMBER) };
|
package/lib/suggestions.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Target } from './api/call.ts';
|
|
2
2
|
import { type Suggestion } from './suggest.ts';
|
|
3
3
|
export interface Suggested {
|
|
4
|
-
/** The rows to draw:
|
|
4
|
+
/** The rows to draw: three when any are, none dismissed. */
|
|
5
5
|
rows: Suggestion[];
|
|
6
6
|
/** Dismiss one, remembered by the account. */
|
|
7
7
|
dismiss: (id: string) => void;
|
package/lib/suggestions.js
CHANGED
|
@@ -33,7 +33,7 @@ import { list } from './api/sessions.js';
|
|
|
33
33
|
import { everything } from './api/work.js';
|
|
34
34
|
import { claims } from './claims.js';
|
|
35
35
|
import { Out } from './out.js';
|
|
36
|
-
import { answer, dismissed, due, inputs, request, seed,
|
|
36
|
+
import { answer, dismissed, due, fill, inputs, request, seed, sig, skip, stale, TAKE } from './suggest.js';
|
|
37
37
|
/** A source that refuses this person (a 4xx other than an expired sign-in or a rate) has nothing for them; any other failure leaves it unknown. */
|
|
38
38
|
function soft(p, none) {
|
|
39
39
|
return p.catch((e) => {
|
|
@@ -120,7 +120,8 @@ export function useSuggestions(t) {
|
|
|
120
120
|
load(t).then(() => setError(null), (e) => setError(said(e)));
|
|
121
121
|
}, [t]);
|
|
122
122
|
const now = useSyncExternalStore(subscribe, () => doc, () => doc);
|
|
123
|
-
|
|
123
|
+
// Filled to three with what Hanzo does, so a new account and a quiet one never see one lonely row.
|
|
124
|
+
const rows = useMemo(() => fill(now), [now]);
|
|
124
125
|
const dismiss = useCallback((id) => {
|
|
125
126
|
if (!doc)
|
|
126
127
|
return;
|
package/lib/summary.js
CHANGED
|
@@ -12,18 +12,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
*/
|
|
13
13
|
import { useMemo, useState } from 'react';
|
|
14
14
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
15
|
-
import { ListTodo } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { FilePlus2, ListTodo } from '@hanzogui/lucide-icons-2';
|
|
16
16
|
import { Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
|
|
17
|
+
/** A row's pitch and inset, px: the kit menu's, so this panel reads as every other one. */
|
|
18
|
+
const ROW_H = 32;
|
|
19
|
+
const ROW_PX = 8;
|
|
17
20
|
/** A row of the summary: its mark and its name, one press from a tab. */
|
|
18
21
|
function Line({ icon, children, detail, onPress, quiet = false }) {
|
|
19
|
-
return (_jsxs(XStack, { render: onPress ? 'button' : undefined, role: onPress ? 'button' : undefined, onPress: onPress, items: "center",
|
|
22
|
+
return (_jsxs(XStack, { render: onPress ? 'button' : undefined, role: onPress ? 'button' : undefined, onPress: onPress, items: "center", justify: "flex-start", gap: ROW_PX, px: ROW_PX, minH: ROW_H, width: "100%", rounded: 4, cursor: onPress ? 'pointer' : undefined, hoverStyle: onPress ? { bg: '$hover' } : undefined, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [icon ? (_jsx(XStack, { width: 16, justify: "center", shrink: 0, opacity: 0.75, children: icon })) : null, _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", py: detail ? '$1' : 0, children: [_jsx(SizableText, { fontSize: 14, color: quiet ? '$soft' : '$ink', numberOfLines: 1, style: { textAlign: 'left' }, children: children }), detail ? (_jsx(SizableText, { fontSize: 12, color: "$soft", numberOfLines: 2, style: { textAlign: 'left' }, children: detail })) : null] })] }));
|
|
20
23
|
}
|
|
21
24
|
/** A section's heading, with what it adds at its end. */
|
|
22
|
-
const Head = ({ children, action }) => (_jsxs(XStack, { items: "center", justify: "space-between", px:
|
|
25
|
+
const Head = ({ children, action }) => (_jsxs(XStack, { items: "center", justify: "space-between", px: ROW_PX, minH: ROW_H, children: [_jsx(SizableText, { fontSize: 13, color: "$soft", children: children }), action] }));
|
|
23
26
|
/** One section's rows; `done` runs after a row is pressed (the popover puts itself away). */
|
|
24
27
|
function Rows({ section, done, details = false }) {
|
|
25
28
|
const { entries, empty, lead } = section;
|
|
26
|
-
return (_jsxs(_Fragment, { children: [lead ? (_jsx(Line, {
|
|
29
|
+
return (_jsxs(_Fragment, { children: [lead ? (_jsx(Line, { icon: _jsx(FilePlus2, { size: 15, "aria-hidden": true }), onPress: () => {
|
|
27
30
|
done?.();
|
|
28
31
|
lead.run();
|
|
29
32
|
}, children: lead.title })) : null, entries.map((e) => (_jsx(Line, { icon: e.icon, detail: details ? e.detail : undefined, onPress: e.open
|
|
@@ -43,7 +46,7 @@ export function Summary({ sections, onOpen, label = 'Summary' }) {
|
|
|
43
46
|
setOpen(false);
|
|
44
47
|
onOpen?.();
|
|
45
48
|
};
|
|
46
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { children: _jsx(XStack, { render: "button", "data-slot": "summary-toggle", "aria-label": label, "aria-haspopup": "dialog", "aria-expanded": open, ...{ title: label }, width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(ListTodo, { size: 16, "aria-hidden": true }) }) }), _jsx(PopoverContent, { solid: true, align: "end", p:
|
|
49
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { children: _jsx(XStack, { render: "button", "data-slot": "summary-toggle", "aria-label": label, "aria-haspopup": "dialog", "aria-expanded": open, ...{ title: label }, width: 32, height: 32, rounded: "$3", items: "center", justify: "center", cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(ListTodo, { size: 16, "aria-hidden": true }) }) }), _jsx(PopoverContent, { solid: true, align: "end", sideOffset: 8, p: 4, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(YStack, { "data-slot": "summary", role: "dialog", "aria-label": label, self: "stretch", width: "100%", maxH: "min(70dvh, 520px)", overflowY: "auto", overflowX: "hidden", children: sections.map((s, i) => (_jsxs(YStack, { children: [i ? _jsx(XStack, { height: 1, bg: "$borderColor", my: 4, mx: 4 }) : null, _jsx(Head, { action: s.action, children: s.title }), _jsx(Rows, { section: s, done: done })] }, s.id))) }) })] }));
|
|
47
50
|
}
|
|
48
51
|
/** Each section as a kind of tab for `Panel`: the same rows, with their second lines. */
|
|
49
52
|
export function kinds(sections, icons = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.72",
|
|
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.",
|