@hanzo/build 0.2.77 → 0.2.79
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/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/landing.js +46 -6
- package/lib/mic.js +4 -1
- package/lib/prompt.d.ts +2 -1
- package/lib/prompt.js +12 -8
- package/lib/suggest.d.ts +58 -23
- package/lib/suggest.js +104 -50
- package/lib/suggestions.d.ts +18 -4
- package/lib/suggestions.js +56 -25
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -74,5 +74,5 @@ export { address, destination, located, name, PAGE, SEARCH, useDeck, useKey, use
|
|
|
74
74
|
* the person's own activity: `useSuggestions` reads and makes them, `Suggestions`
|
|
75
75
|
* draws them. The row is `Hint` here: `Suggestion` is the side panel's.
|
|
76
76
|
*/
|
|
77
|
-
export { Suggestions, useSuggestions, type Suggested, type SuggestionsProps } from './suggestions.tsx';
|
|
78
|
-
export type { Suggestion as Hint } from './suggest.ts';
|
|
77
|
+
export { book, hand, Suggestions, taken, useSuggestions, type Suggested, type SuggestionsProps } from './suggestions.tsx';
|
|
78
|
+
export type { Act, Suggestion as Hint } from './suggest.ts';
|
package/lib/index.js
CHANGED
|
@@ -68,4 +68,4 @@ export { address, destination, located, name, PAGE, SEARCH, useDeck, useKey, use
|
|
|
68
68
|
* the person's own activity: `useSuggestions` reads and makes them, `Suggestions`
|
|
69
69
|
* draws them. The row is `Hint` here: `Suggestion` is the side panel's.
|
|
70
70
|
*/
|
|
71
|
-
export { Suggestions, useSuggestions } from './suggestions.js';
|
|
71
|
+
export { book, hand, Suggestions, taken, useSuggestions } from './suggestions.js';
|
package/lib/landing.js
CHANGED
|
@@ -37,7 +37,8 @@ import { Publish } from './publish.js';
|
|
|
37
37
|
import { path } from './route.js';
|
|
38
38
|
import { useCarry } from './carry.js';
|
|
39
39
|
import { DEV_MODES } from './gear.js';
|
|
40
|
-
import { Suggestions, useSuggestions } from './suggestions.js';
|
|
40
|
+
import { book, hand, Suggestions, taken, useSuggestions } from './suggestions.js';
|
|
41
|
+
import { want } from './library.js';
|
|
41
42
|
import { MEASURE, Prompt, useEar } from './prompt.js';
|
|
42
43
|
import { Monitor as Device } from '@hanzogui/lucide-icons-2';
|
|
43
44
|
import { compose, Files, pick, read as readFiles } from './tools.js';
|
|
@@ -175,9 +176,9 @@ export function Landing({ onStarted }) {
|
|
|
175
176
|
return { branches: names.map((name) => ({ name })), next: null };
|
|
176
177
|
}, [t, codebase, home]);
|
|
177
178
|
// The composer sends only a draft with words in it, and not while a send is out.
|
|
178
|
-
const send = async () => {
|
|
179
|
+
const send = async (said = draft) => {
|
|
179
180
|
if (!signed) {
|
|
180
|
-
hold({ draft, files, sent: true });
|
|
181
|
+
hold({ draft: said, files, sent: true });
|
|
181
182
|
host.signIn?.();
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
@@ -203,7 +204,7 @@ export function Landing({ onStarted }) {
|
|
|
203
204
|
setNote('');
|
|
204
205
|
try {
|
|
205
206
|
// What the plugins that are on add: words for the run, or an answer of their own.
|
|
206
|
-
const prepped = carry.preparing ? await carry.prep({ text:
|
|
207
|
+
const prepped = carry.preparing ? await carry.prep({ text: said.trim(), files: [], thread: '' }, new AbortController().signal) : {};
|
|
207
208
|
// An answer of a plugin's own (a deck) is the answer: no run is started for it.
|
|
208
209
|
if (prepped.reply) {
|
|
209
210
|
// Its words, without the page it drew (Chat shows that in the side panel).
|
|
@@ -216,7 +217,7 @@ export function Landing({ onStarted }) {
|
|
|
216
217
|
return;
|
|
217
218
|
}
|
|
218
219
|
const run = await start(t, {
|
|
219
|
-
prompt: compose([
|
|
220
|
+
prompt: compose([said.trim(), carry.text, prepped.system ?? ''].filter(Boolean).join('\n\n---\n'), files),
|
|
220
221
|
// The name alone. The org rides the request, and a slash is not a repo name.
|
|
221
222
|
// An empty base or place is not sent: the default branch, and the sandbox.
|
|
222
223
|
repo: repo?.name ?? hub?.full_name,
|
|
@@ -249,6 +250,45 @@ export function Landing({ onStarted }) {
|
|
|
249
250
|
// Sent once, on the sign-in that it waited for.
|
|
250
251
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
251
252
|
}, [signed]);
|
|
253
|
+
// A run a suggestion in Chat left for Dev starts on arrival, once its person is known.
|
|
254
|
+
const [handed] = useState(() => ({ prompt: taken('task') }));
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (!signed || !handed.prompt)
|
|
257
|
+
return;
|
|
258
|
+
const p = handed.prompt;
|
|
259
|
+
handed.prompt = '';
|
|
260
|
+
setDraft(p);
|
|
261
|
+
void send(p);
|
|
262
|
+
// Started once, on the sign-in that it waited for.
|
|
263
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
264
|
+
}, [signed]);
|
|
265
|
+
/** What pressing a suggestion does: open what it names, chat its prompt, start its run, or make its scheduled task. */
|
|
266
|
+
const act = (a) => {
|
|
267
|
+
switch (a.kind) {
|
|
268
|
+
case 'open':
|
|
269
|
+
if (a.item)
|
|
270
|
+
want(a.item);
|
|
271
|
+
if (/^https:\/\//.test(a.to))
|
|
272
|
+
window.open(a.to, '_blank', 'noopener,noreferrer');
|
|
273
|
+
else if (a.to.startsWith('/'))
|
|
274
|
+
host.open(a.to);
|
|
275
|
+
else
|
|
276
|
+
host.go(a.to);
|
|
277
|
+
return;
|
|
278
|
+
case 'send':
|
|
279
|
+
hand('send', a.prompt);
|
|
280
|
+
host.open('/chat');
|
|
281
|
+
return;
|
|
282
|
+
case 'task':
|
|
283
|
+
setDraft(a.prompt);
|
|
284
|
+
void send(a.prompt);
|
|
285
|
+
return;
|
|
286
|
+
case 'schedule':
|
|
287
|
+
setNote('');
|
|
288
|
+
void book(t, a, 'dev').then((made) => host.go(path({ kind: 'automation', id: made.id })), (e) => setNote(e instanceof Error && e.message ? e.message : 'The scheduled task could not be made.'));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
252
292
|
/** 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. */
|
|
253
293
|
const setup = async (repo) => {
|
|
254
294
|
const run = await start(t, { prompt: SETUP, repo, mode: 'setup', model: mind.model, effort: mind.effort });
|
|
@@ -319,7 +359,7 @@ export function Landing({ onStarted }) {
|
|
|
319
359
|
plugins: carry.plugins,
|
|
320
360
|
devices,
|
|
321
361
|
memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
|
|
322
|
-
}, on: carry.on, slash: carry.skills, at: carry.plugins, mode: carry.mode, where: where, below: _jsx(Suggestions, { rows: hints.rows,
|
|
362
|
+
}, 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: () => {
|
|
323
363
|
setOffering(false);
|
|
324
364
|
env.reload();
|
|
325
365
|
} })) : null] }));
|
package/lib/mic.js
CHANGED
|
@@ -119,7 +119,10 @@ export function Dictation({ voice, disabled = false, narrow = false, language =
|
|
|
119
119
|
const { inputs, allowed, using } = useInputs(open);
|
|
120
120
|
const [device, setDevice] = useState(() => (typeof window === 'undefined' ? null : kept()));
|
|
121
121
|
const spoken = LANGUAGES.find((l) => l.id === language);
|
|
122
|
-
|
|
122
|
+
// The chevron is drawn while the mic is reached for (pointer, keyboard, menu open); at rest the row is the mic alone.
|
|
123
|
+
const [near, setNear] = useState(false);
|
|
124
|
+
const shown = open || near;
|
|
125
|
+
return (_jsxs(XStack, { "data-slot": "dictation", items: "center", rounded: 8, borderWidth: 1, borderColor: open ? '$borderColor' : 'transparent', hoverStyle: { borderColor: '$borderColor' }, shrink: 0, onPointerEnter: () => setNear(true), onPointerLeave: () => setNear(false), onFocus: () => setNear(true), onBlur: () => setNear(false), children: [_jsx(Control, { asChild: true, size: narrow ? 44 : 30, ...PLAIN, children: _jsx(Voice, { voice: voice, disabled: disabled, says: { idle: 'Dictate', listening: 'Dictating — click to stop' }, children: (state) => _jsx(MicIcon, { size: 16, color: "var(--foreground)", fill: state === 'idle' ? 'none' : 'var(--foreground)' }) }) }), _jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, disabled: disabled, children: _jsx(XStack, { render: "button", "aria-label": "Dictation settings", "data-slot": "dictation-menu", width: 16, height: 30, items: "center", justify: "center", rounded: 6, cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, ...(narrow ? { width: 44, height: 44 } : null), children: _jsx(ChevronDown, { size: 14, color: "$soft", opacity: shown || narrow ? 1 : 0 }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 200, "data-slot": "dictation-panel", children: [_jsx(Meter, { device: device, on: open && allowed }), inputs.length === 0 ? (_jsx(DropdownMenuLabel, { children: _jsx(SizableText, { fontSize: 14, color: "$hint", children: "No devices found" }) })) : (inputs.map((d, i) => {
|
|
123
126
|
const on = device ? d.deviceId === device : d.groupId === using;
|
|
124
127
|
return (_jsx(DropdownMenuItem, { preventCloseOnSelect: true, onSelect: () => {
|
|
125
128
|
setDevice(d.deviceId);
|
package/lib/prompt.d.ts
CHANGED
|
@@ -94,11 +94,12 @@ export interface PromptOptions {
|
|
|
94
94
|
narrow?: boolean;
|
|
95
95
|
}
|
|
96
96
|
/** The row under the frame: where (Dev), then the model, the effort and the mode. */
|
|
97
|
-
export declare function Tune({ mind, mode, where, onUpgrade }: {
|
|
97
|
+
export declare function Tune({ mind, mode, where, onUpgrade, limits }: {
|
|
98
98
|
mind?: Mind | null;
|
|
99
99
|
mode?: PromptOptions['mode'];
|
|
100
100
|
where?: ReactNode;
|
|
101
101
|
onUpgrade?: (model: string) => void;
|
|
102
|
+
limits?: PauseSource | null;
|
|
102
103
|
}): import("react").JSX.Element;
|
|
103
104
|
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
104
105
|
export declare function trigger(value: string): {
|
package/lib/prompt.js
CHANGED
|
@@ -23,7 +23,7 @@ import { ArrowUp, AudioLines, Check, Square, X } from '@hanzogui/lucide-icons-2'
|
|
|
23
23
|
import { Control } from '@hanzo/composer';
|
|
24
24
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSearch, DropdownMenuTrigger, Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
|
|
25
25
|
import { Composer } from '@hanzo/ui/chat';
|
|
26
|
-
import { groupModels, matchesModel } from '@hanzo/ui/models';
|
|
26
|
+
import { groupModels, isPaused, matchesModel } from '@hanzo/ui/models';
|
|
27
27
|
import { speech, Voice } from '@hanzo/voice';
|
|
28
28
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
29
29
|
import { ENSO } from './api/models.js';
|
|
@@ -81,7 +81,7 @@ function Opens({ label, word, dim, children }) {
|
|
|
81
81
|
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { children: _jsx(Word, { label: label, dim: dim, open: open, children: word }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: children(() => setOpen(false)) })] }));
|
|
82
82
|
}
|
|
83
83
|
/** The model's name, and the catalog behind it: Free's lane to pick from, the rest an upgrade. */
|
|
84
|
-
function Model({ mind, onUpgrade }) {
|
|
84
|
+
function Model({ mind, onUpgrade, limits }) {
|
|
85
85
|
const [open, setOpen] = useState(false);
|
|
86
86
|
const [q, setQ] = useState('');
|
|
87
87
|
// The catalog is hundreds long, and drawing all of it is what made the menu slow
|
|
@@ -103,14 +103,16 @@ function Model({ mind, onUpgrade }) {
|
|
|
103
103
|
const routed = mind.model === ENSO;
|
|
104
104
|
return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, placement: "top-end", children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { "data-slot": "mind", children: _jsx(Word, { label: `Model: ${nameOf(mind.model, mind.models)}`, open: open, children: nameOf(mind.model, mind.models) }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 280, "data-slot": "model-menu", children: [_jsx(DropdownMenuSearch, { value: q, onValueChange: setQ, placeholder: "Search models" }), _jsxs(YStack, { maxH: "min(420px, 60vh)", overflowY: "auto", overflowX: "hidden", "data-slot": "model-rows", children: [!mind.free && !q.trim() ? (_jsx(DropdownMenuItem, { onSelect: () => mind.pick(ENSO), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsxs(YStack, { flex: 1, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", children: "Enso" }), _jsx(SizableText, { fontSize: 12, color: "$hint", children: "Picks the model for each message and step" })] }), routed ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) })) : null, groups.map((g) => (_jsxs(YStack, { role: "group", "aria-label": g.label, children: [_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, pt: 8, pb: 2, children: g.label }), g.models.map((m) => {
|
|
105
105
|
const ok = grants(mind.free, m.id, mind.models);
|
|
106
|
-
|
|
106
|
+
// Paused by the plan's usage: still listed and still picked; the gateway answers from Enso.
|
|
107
|
+
const paused = isPaused(m, limits);
|
|
108
|
+
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: "Upgrade" })), ok && paused ? (_jsx(SizableText, { fontSize: 12, color: "$hint", children: "Paused" })) : null, m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
|
|
107
109
|
})] }, g.key))), more > 0 ? (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, py: 8, children: `Type to search ${more} more models` })) : null] })] })] }));
|
|
108
110
|
}
|
|
109
111
|
/** The row under the frame: where (Dev), then the model, the effort and the mode. */
|
|
110
|
-
export function Tune({ mind, mode, where, onUpgrade }) {
|
|
112
|
+
export function Tune({ mind, mode, where, onUpgrade, limits }) {
|
|
111
113
|
const thinks = mind ? reasons(mind.model, mind.models) : false;
|
|
112
114
|
const pace = mind ? EFFORTS.find((e) => e.id === mind.effort) : null;
|
|
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) => {
|
|
115
|
+
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, limits: limits }) : 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
116
|
mind.pace(e);
|
|
115
117
|
close();
|
|
116
118
|
} })) })) : 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] })] }));
|
|
@@ -144,7 +146,7 @@ export function prompt(o, state) {
|
|
|
144
146
|
};
|
|
145
147
|
const mention = listed && shelf && typed ? (_jsx(Mention, { shelf: shelf, rows: listed, index: index, onPick: choose, tag: typed.char === '/' ? 'Skill' : 'Plugin', name: typed.char === '/' ? 'Skills' : 'Plugins' })) : null;
|
|
146
148
|
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;
|
|
147
|
-
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 }), narrow ? null : o.below] })) : undefined;
|
|
149
|
+
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;
|
|
148
150
|
const round = (_jsx(Control, { type: "button", size: narrow ? TAP : ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": busy ? 'Stop' : 'Send', "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
|
|
149
151
|
if (!live)
|
|
150
152
|
return;
|
|
@@ -166,14 +168,16 @@ export function prompt(o, state) {
|
|
|
166
168
|
disabled,
|
|
167
169
|
placeholder: o.placeholder ?? ASK,
|
|
168
170
|
...(o.label ? { label: o.label } : {}),
|
|
169
|
-
rows:
|
|
171
|
+
// At rest two rows: one line of text, then the tools. It grows with the text to the kit's ceiling, then scrolls.
|
|
172
|
+
rows: 1,
|
|
170
173
|
field: {
|
|
171
174
|
...o.field,
|
|
172
175
|
fontSize: 16,
|
|
173
176
|
lineHeight: 24,
|
|
174
177
|
px: 8,
|
|
175
178
|
pt: 2,
|
|
176
|
-
|
|
179
|
+
pb: 0,
|
|
180
|
+
minH: 26,
|
|
177
181
|
bg: 'transparent',
|
|
178
182
|
},
|
|
179
183
|
head,
|
package/lib/suggest.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Three suggestions under the composer, each one person's own and each a thing
|
|
3
|
+
* done at once when pressed (`Act`): open what it names (a conversation, a run,
|
|
4
|
+
* a Studio creation, an item, an issue), send a new chat, start a Dev run, or
|
|
5
|
+
* schedule a task. None only fills the composer.
|
|
5
6
|
*
|
|
6
7
|
* Nothing here is written by hand. The rows are generated by Hanzo's own model
|
|
7
8
|
* (`MODEL`, POST /v1/chat/completions with the person's bearer, so the call is
|
|
@@ -24,28 +25,49 @@
|
|
|
24
25
|
* This file is the pure half: inputs to a request, an answer to validated rows,
|
|
25
26
|
* the fingerprint and the policy. suggestions.tsx reads the sources and draws them.
|
|
26
27
|
*/
|
|
27
|
-
import type { Automation } from './api/auto.ts';
|
|
28
|
+
import type { Automation, Schedule } from './api/auto.ts';
|
|
28
29
|
import type { Conversation } from './api/conversations.ts';
|
|
29
30
|
import type { Item } from './api/library.ts';
|
|
30
31
|
import type { Session } from './api/sessions.ts';
|
|
31
32
|
import type { Issue } from './merge.ts';
|
|
32
33
|
export type Icon = 'github' | 'agent' | 'chat' | 'file' | 'task';
|
|
33
34
|
export declare const ICONS: readonly Icon[];
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* What pressing a row does, at once:
|
|
37
|
+
*
|
|
38
|
+
* open the thing it names: `to` is an app address (`/chat/<id>`,
|
|
39
|
+
* `/studio/c/<id>`), a builder path (`sess_…`, `-/automations/<id>`,
|
|
40
|
+
* `-/artifacts` with `item`), or another site's (a new tab)
|
|
41
|
+
* send a new chat, asked `prompt`
|
|
42
|
+
* task a new Dev run on `prompt`
|
|
43
|
+
* schedule a scheduled task, made and shown
|
|
44
|
+
*/
|
|
45
|
+
export type Act = {
|
|
46
|
+
kind: 'open';
|
|
47
|
+
to: string;
|
|
48
|
+
item?: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'send';
|
|
51
|
+
prompt: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'task';
|
|
54
|
+
prompt: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'schedule';
|
|
57
|
+
name: string;
|
|
58
|
+
prompt: string;
|
|
59
|
+
schedule: Schedule;
|
|
60
|
+
};
|
|
61
|
+
/** One row: one short action in plain words, and what pressing it does. */
|
|
35
62
|
export interface Suggestion {
|
|
36
|
-
/** Stable across generations:
|
|
63
|
+
/** Stable across generations: what it opens, or its words. */
|
|
37
64
|
id: string;
|
|
38
65
|
icon: Icon;
|
|
39
|
-
/**
|
|
66
|
+
/** The action, a few plain words: "Review poster.png in Studio". */
|
|
40
67
|
title: string;
|
|
41
|
-
/** The source's own https address, copied from an input, or '' for none. */
|
|
42
|
-
href: string;
|
|
43
68
|
/** What the source says, a sentence or three: the hover card. */
|
|
44
69
|
summary: string;
|
|
45
|
-
|
|
46
|
-
action: string;
|
|
47
|
-
/** What the composer is given when the row is chosen. */
|
|
48
|
-
prompt: string;
|
|
70
|
+
act: Act;
|
|
49
71
|
}
|
|
50
72
|
/** One piece of the person's activity, as the model is shown it. */
|
|
51
73
|
export interface Input {
|
|
@@ -59,6 +81,10 @@ export interface Input {
|
|
|
59
81
|
text: string;
|
|
60
82
|
/** Its own https address, or ''. */
|
|
61
83
|
href: string;
|
|
84
|
+
/** Where opening it goes (`Act` open's `to`), or '' when nothing opens it. */
|
|
85
|
+
to: string;
|
|
86
|
+
/** A Library item's id, for an item the Library opens at. */
|
|
87
|
+
item?: string;
|
|
62
88
|
/** RFC 3339, or ''. */
|
|
63
89
|
updated: string;
|
|
64
90
|
}
|
|
@@ -142,6 +168,8 @@ export declare function due(kept: Kept | null, fingerprint: string, now: number)
|
|
|
142
168
|
export declare function stale(kept: Kept | null, now: number): boolean;
|
|
143
169
|
/** The icon a row about `i` carries: GitHub's mark only for GitHub's own address. */
|
|
144
170
|
export declare function iconOf(i: Input): Icon;
|
|
171
|
+
/** What a row does, as the model names it. */
|
|
172
|
+
export declare const ACTIONS: readonly ['open', 'send', 'task', 'schedule'];
|
|
145
173
|
/** The answer's shape, decoded strictly by the model. */
|
|
146
174
|
export declare const SCHEMA: {
|
|
147
175
|
readonly type: 'object';
|
|
@@ -154,7 +182,7 @@ export declare const SCHEMA: {
|
|
|
154
182
|
readonly items: {
|
|
155
183
|
readonly type: 'object';
|
|
156
184
|
readonly additionalProperties: false;
|
|
157
|
-
readonly required: readonly ['icon', 'title', '
|
|
185
|
+
readonly required: readonly ['icon', 'title', 'summary', 'action', 'source', 'prompt', 'every'];
|
|
158
186
|
readonly properties: {
|
|
159
187
|
readonly icon: {
|
|
160
188
|
readonly type: 'string';
|
|
@@ -163,18 +191,22 @@ export declare const SCHEMA: {
|
|
|
163
191
|
readonly title: {
|
|
164
192
|
readonly type: 'string';
|
|
165
193
|
};
|
|
166
|
-
readonly href: {
|
|
167
|
-
readonly type: 'string';
|
|
168
|
-
};
|
|
169
194
|
readonly summary: {
|
|
170
195
|
readonly type: 'string';
|
|
171
196
|
};
|
|
172
197
|
readonly action: {
|
|
173
198
|
readonly type: 'string';
|
|
199
|
+
readonly enum: readonly ["open", "send", "task", "schedule"];
|
|
200
|
+
};
|
|
201
|
+
readonly source: {
|
|
202
|
+
readonly type: 'string';
|
|
174
203
|
};
|
|
175
204
|
readonly prompt: {
|
|
176
205
|
readonly type: 'string';
|
|
177
206
|
};
|
|
207
|
+
readonly every: {
|
|
208
|
+
readonly type: 'string';
|
|
209
|
+
};
|
|
178
210
|
};
|
|
179
211
|
};
|
|
180
212
|
};
|
|
@@ -182,21 +214,24 @@ export declare const SCHEMA: {
|
|
|
182
214
|
};
|
|
183
215
|
/** The chat completion that asks for rows, or null when there is nothing to make them from. */
|
|
184
216
|
export declare function request(inputs: Input[], seed: Seed, skip: string[], now: Date): Record<string, unknown> | null;
|
|
217
|
+
/** `every` as a schedule, or null for words that are not one. */
|
|
218
|
+
export declare function every(v: string): Schedule | null;
|
|
185
219
|
/** The text of a chat completion's first choice, or ''. */
|
|
186
220
|
export declare function content(raw: unknown): string;
|
|
187
|
-
/** A row's id:
|
|
188
|
-
export declare const idOf: (
|
|
221
|
+
/** A row's id: what it opens, or its words. */
|
|
222
|
+
export declare const idOf: (to: string, title: string) => string;
|
|
189
223
|
/**
|
|
190
|
-
* The rows of a chat completion, validated: each with
|
|
191
|
-
*
|
|
192
|
-
* per
|
|
224
|
+
* The rows of a chat completion, validated: each with words and an action that
|
|
225
|
+
* binds (an open names an input's key; a send, a task or a schedule carries its
|
|
226
|
+
* prompt, and a schedule a time it reads), one row per thing, at most `MOST`.
|
|
227
|
+
* Anything else is dropped, never repaired.
|
|
193
228
|
*/
|
|
194
229
|
export declare function answer(raw: unknown, inputs: Input[]): Suggestion[];
|
|
195
230
|
/** The `suggest` key of the preference document, or undefined when it holds none. */
|
|
196
231
|
export declare function kept(raw: unknown): Kept | undefined;
|
|
197
232
|
/** The rows to draw: those not dismissed. */
|
|
198
233
|
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. */
|
|
234
|
+
/** What Hanzo does, three ways in, each done at once: what fills the rows a person's activity and signup do not. */
|
|
200
235
|
export declare const STARTERS: readonly Suggestion[];
|
|
201
236
|
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
202
237
|
export declare const fill: (k: Kept | null | undefined) => Suggestion[];
|
package/lib/suggest.js
CHANGED
|
@@ -51,7 +51,7 @@ const first = (s) => s.split('\n').find((l) => l.trim()) ?? '';
|
|
|
51
51
|
/** The person's activity as inputs: their own runs only, each kind's newest `TAKE`. */
|
|
52
52
|
export function inputs(a) {
|
|
53
53
|
const all = [
|
|
54
|
-
...a.conversations.map((c) => ({ kind: 'conversation', key: c.id, title: c.title, state: '', text: '', href: '', updated: c.updated })),
|
|
54
|
+
...a.conversations.map((c) => ({ kind: 'conversation', key: c.id, title: c.title, state: '', text: '', href: '', to: `/chat/${encodeURIComponent(c.id)}`, updated: c.updated })),
|
|
55
55
|
...a.issues.map((i) => ({
|
|
56
56
|
kind: i.pull ? 'pull' : 'issue',
|
|
57
57
|
key: i.key,
|
|
@@ -59,6 +59,7 @@ export function inputs(a) {
|
|
|
59
59
|
state: i.state === 'closed' ? 'closed' : i.status || 'open',
|
|
60
60
|
text: [i.repo || i.forge || i.board, first(i.body)].filter(Boolean).join(': '),
|
|
61
61
|
href: https(i.url),
|
|
62
|
+
to: https(i.url),
|
|
62
63
|
updated: i.updated,
|
|
63
64
|
})),
|
|
64
65
|
...a.runs
|
|
@@ -70,6 +71,7 @@ export function inputs(a) {
|
|
|
70
71
|
state: r.status,
|
|
71
72
|
text: [r.repo, r.branch].filter(Boolean).join(' @ '),
|
|
72
73
|
href: https(r.pr),
|
|
74
|
+
to: r.id,
|
|
73
75
|
updated: r.updatedAt || r.createdAt,
|
|
74
76
|
})),
|
|
75
77
|
...a.automations
|
|
@@ -81,9 +83,11 @@ export function inputs(a) {
|
|
|
81
83
|
state: x.last?.status || (x.enabled ? 'on' : 'off'),
|
|
82
84
|
text: x.last?.summary || first(x.instructions),
|
|
83
85
|
href: '',
|
|
86
|
+
to: `-/automations/${x.id}`,
|
|
84
87
|
updated: x.last?.at || x.updated,
|
|
85
88
|
})),
|
|
86
|
-
|
|
89
|
+
// An item opens where it was made when that has an address (a Studio creation), else in the Library at it.
|
|
90
|
+
...a.files.map((f) => ({ kind: 'file', key: f.id, title: f.name, state: f.kind, text: f.source, href: '', ...(f.origin.startsWith('/') ? { to: f.origin } : { to: '-/artifacts', item: f.id }), updated: f.updated || f.created })),
|
|
87
91
|
];
|
|
88
92
|
return pick(all);
|
|
89
93
|
}
|
|
@@ -155,6 +159,8 @@ export function iconOf(i) {
|
|
|
155
159
|
return 'file';
|
|
156
160
|
}
|
|
157
161
|
}
|
|
162
|
+
/** What a row does, as the model names it. */
|
|
163
|
+
export const ACTIONS = ['open', 'send', 'task', 'schedule'];
|
|
158
164
|
/** The answer's shape, decoded strictly by the model. */
|
|
159
165
|
export const SCHEMA = {
|
|
160
166
|
type: 'object',
|
|
@@ -167,32 +173,35 @@ export const SCHEMA = {
|
|
|
167
173
|
items: {
|
|
168
174
|
type: 'object',
|
|
169
175
|
additionalProperties: false,
|
|
170
|
-
required: ['icon', 'title', '
|
|
176
|
+
required: ['icon', 'title', 'summary', 'action', 'source', 'prompt', 'every'],
|
|
171
177
|
properties: {
|
|
172
178
|
icon: { type: 'string', enum: [...ICONS] },
|
|
173
179
|
title: { type: 'string' },
|
|
174
|
-
href: { type: 'string' },
|
|
175
180
|
summary: { type: 'string' },
|
|
176
|
-
action: { type: 'string' },
|
|
181
|
+
action: { type: 'string', enum: [...ACTIONS] },
|
|
182
|
+
source: { type: 'string' },
|
|
177
183
|
prompt: { type: 'string' },
|
|
184
|
+
every: { type: 'string' },
|
|
178
185
|
},
|
|
179
186
|
},
|
|
180
187
|
},
|
|
181
188
|
},
|
|
182
189
|
};
|
|
183
190
|
const SYSTEM = [
|
|
184
|
-
'You choose
|
|
185
|
-
'Each row
|
|
191
|
+
'You choose three things for one person to do next with Hanzo, from their own recent activity, shown under the box where they type. Pressing a row DOES it at once, so every row is an action, never a draft.',
|
|
192
|
+
'Each row has one action:',
|
|
193
|
+
'- open: open the thing an activity item names (a conversation to pick up, a run to check, a creation or file to review, an issue to read). source is that item\'s key, copied exactly.',
|
|
194
|
+
'- send: start a new chat and ask prompt. Use it for a question or a piece of work a chat answers (a summary, a draft, research).',
|
|
195
|
+
'- task: start a Dev coding run on prompt, in the person\'s current project. Use it for a change to code.',
|
|
196
|
+
'- schedule: make a scheduled task that runs prompt every `every`. every is one of: hourly · daily HH:MM · weekdays HH:MM · weekly mon|tue|wed|thu|fri|sat|sun HH:MM (24-hour).',
|
|
186
197
|
'Fields:',
|
|
187
|
-
'- title:
|
|
188
|
-
'-
|
|
189
|
-
'-
|
|
190
|
-
'- prompt:
|
|
191
|
-
'-
|
|
192
|
-
'- icon: github for a github.com
|
|
193
|
-
'Answer three rows
|
|
194
|
-
'Never repeat an item listed under skip. Never suggest the same item twice.',
|
|
195
|
-
'With neither activity nor anything from signup worth a row, answer fewer; never pad with a weak one.',
|
|
198
|
+
'- title: the action itself, plain words, at most 7, naming the thing: "Review poster.png in Studio", "Continue the physics chat", "Pick up the Oct 10 hello thread". Never a label and an action glued together.',
|
|
199
|
+
'- summary: one or two plain sentences of what the source says, for the hover card. Only facts from the input; never invent numbers, names or dates.',
|
|
200
|
+
'- source: for open, the key of the activity item; else "".',
|
|
201
|
+
'- prompt: for send, task and schedule, the complete message, first person, naming its source; for open, "".',
|
|
202
|
+
'- every: for schedule; else "".',
|
|
203
|
+
'- icon: github for a github.com issue or pull request, agent for a run, a task or a schedule, chat for a conversation or a send, file for a file or creation, task for any other issue.',
|
|
204
|
+
'Answer three rows: the activity first, then from what the person told us at signup (their role, their work). A row you cannot bind to one of the actions is no row. Never repeat an item under skip, and never the same item twice.',
|
|
196
205
|
].join('\n');
|
|
197
206
|
/** The chat completion that asks for rows, or null when there is nothing to make them from. */
|
|
198
207
|
export function request(inputs, seed, skip, now) {
|
|
@@ -200,11 +209,11 @@ export function request(inputs, seed, skip, now) {
|
|
|
200
209
|
if (!inputs.length && !Object.keys(person).length)
|
|
201
210
|
return null;
|
|
202
211
|
const activity = inputs.map((i) => ({
|
|
212
|
+
key: i.key,
|
|
203
213
|
kind: i.kind,
|
|
204
214
|
title: line(i.title, 140),
|
|
205
215
|
...(i.state ? { state: i.state } : {}),
|
|
206
216
|
...(i.text ? { text: line(i.text, 240) } : {}),
|
|
207
|
-
href: i.href,
|
|
208
217
|
...(i.updated ? { updated: i.updated.slice(0, 10) } : {}),
|
|
209
218
|
}));
|
|
210
219
|
return {
|
|
@@ -218,6 +227,19 @@ export function request(inputs, seed, skip, now) {
|
|
|
218
227
|
temperature: 0.3,
|
|
219
228
|
};
|
|
220
229
|
}
|
|
230
|
+
const DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
231
|
+
const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
232
|
+
/** `every` as a schedule, or null for words that are not one. */
|
|
233
|
+
export function every(v) {
|
|
234
|
+
const [kind, a, b] = v.trim().toLowerCase().split(/\s+/);
|
|
235
|
+
if (kind === 'hourly' && !a)
|
|
236
|
+
return { kind: 'hourly', at: '00:00' };
|
|
237
|
+
if ((kind === 'daily' || kind === 'weekdays') && a && HHMM.test(a) && !b)
|
|
238
|
+
return { kind, at: a };
|
|
239
|
+
if (kind === 'weekly' && a && DAYS.includes(a) && b && HHMM.test(b))
|
|
240
|
+
return { kind: 'weekly', day: a, at: b };
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
221
243
|
/** The first JSON object in a model's text: all of it, or from its first `{` to its last `}`. */
|
|
222
244
|
function json(text) {
|
|
223
245
|
try {
|
|
@@ -242,57 +264,89 @@ export function content(raw) {
|
|
|
242
264
|
const first = obj(Array.isArray(choices) ? choices[0] : null);
|
|
243
265
|
return str(obj(first.message).content);
|
|
244
266
|
}
|
|
245
|
-
/** A row's id:
|
|
246
|
-
export const idOf = (
|
|
267
|
+
/** A row's id: what it opens, or its words. */
|
|
268
|
+
export const idOf = (to, title) => hash(to || title.toLowerCase());
|
|
269
|
+
/** A model's row bound to what pressing it does, or null for one that binds to nothing. */
|
|
270
|
+
function bind(o, inputs) {
|
|
271
|
+
const prompt = str(o.prompt).trim().slice(0, 2000);
|
|
272
|
+
switch (str(o.action)) {
|
|
273
|
+
case 'open': {
|
|
274
|
+
const i = inputs.find((x) => x.key === str(o.source).trim());
|
|
275
|
+
if (!i?.to)
|
|
276
|
+
return null;
|
|
277
|
+
return i.item ? { kind: 'open', to: i.to, item: i.item } : { kind: 'open', to: i.to };
|
|
278
|
+
}
|
|
279
|
+
case 'send':
|
|
280
|
+
case 'task':
|
|
281
|
+
return prompt ? { kind: str(o.action), prompt } : null;
|
|
282
|
+
case 'schedule': {
|
|
283
|
+
const when = every(str(o.every));
|
|
284
|
+
return prompt && when ? { kind: 'schedule', name: line(o.title, 80), prompt, schedule: when } : null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
/** What a row is about, for its mark: the input it opens, if it opens one. */
|
|
290
|
+
const about = (act, inputs) => (act.kind === 'open' ? inputs.find((i) => i.to === act.to && i.item === act.item) : undefined);
|
|
247
291
|
/**
|
|
248
|
-
* The rows of a chat completion, validated: each with
|
|
249
|
-
*
|
|
250
|
-
* per
|
|
292
|
+
* The rows of a chat completion, validated: each with words and an action that
|
|
293
|
+
* binds (an open names an input's key; a send, a task or a schedule carries its
|
|
294
|
+
* prompt, and a schedule a time it reads), one row per thing, at most `MOST`.
|
|
295
|
+
* Anything else is dropped, never repaired.
|
|
251
296
|
*/
|
|
252
297
|
export function answer(raw, inputs) {
|
|
253
|
-
const by = new Map(inputs.filter((i) => i.href).map((i) => [i.href, i]));
|
|
254
298
|
const rows = obj(json(content(raw))).rows;
|
|
255
299
|
const out = [];
|
|
256
300
|
for (const r of Array.isArray(rows) ? rows : []) {
|
|
257
301
|
const o = obj(r);
|
|
258
|
-
const title = line(o.title, 80);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (!title || !action || !prompt)
|
|
262
|
-
continue;
|
|
263
|
-
const said = str(o.href).trim();
|
|
264
|
-
const source = said ? by.get(said) : undefined;
|
|
265
|
-
if (said && !source)
|
|
302
|
+
const title = line(o.title, 80).replace(/\.$/, '');
|
|
303
|
+
const act = bind(o, inputs);
|
|
304
|
+
if (!title || !act)
|
|
266
305
|
continue;
|
|
267
|
-
|
|
306
|
+
const source = about(act, inputs);
|
|
307
|
+
// GitHub's mark only beside GitHub's own address: anything else is a task at most.
|
|
268
308
|
const named = ICONS.includes(o.icon) ? o.icon : 'chat';
|
|
269
309
|
const icon = source ? iconOf(source) : named === 'github' ? 'task' : named;
|
|
270
|
-
const id = idOf(
|
|
310
|
+
const id = idOf(act.kind === 'open' ? `${act.to}${act.item ?? ''}` : '', title);
|
|
271
311
|
if (out.some((x) => x.id === id))
|
|
272
312
|
continue;
|
|
273
|
-
out.push({ id, icon, title
|
|
313
|
+
out.push({ id, icon, title, summary: line(o.summary, 600), act });
|
|
274
314
|
if (out.length === MOST)
|
|
275
315
|
break;
|
|
276
316
|
}
|
|
277
317
|
return out;
|
|
278
318
|
}
|
|
279
|
-
/**
|
|
319
|
+
/** A kept action, or null for one that does not read as one. */
|
|
320
|
+
function actOf(raw) {
|
|
321
|
+
const o = obj(raw);
|
|
322
|
+
const prompt = str(o.prompt);
|
|
323
|
+
switch (str(o.kind)) {
|
|
324
|
+
case 'open':
|
|
325
|
+
return str(o.to) ? { kind: 'open', to: str(o.to), ...(str(o.item) ? { item: str(o.item) } : {}) } : null;
|
|
326
|
+
case 'send':
|
|
327
|
+
case 'task':
|
|
328
|
+
return prompt ? { kind: str(o.kind), prompt } : null;
|
|
329
|
+
case 'schedule': {
|
|
330
|
+
const sc = obj(o.schedule);
|
|
331
|
+
const kind = str(sc.kind);
|
|
332
|
+
return prompt && kind ? { kind: 'schedule', name: str(o.name), prompt, schedule: { kind, ...(str(sc.at) ? { at: str(sc.at) } : {}), ...(str(sc.day) ? { day: str(sc.day) } : {}) } } : null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
/** One kept row, or null for one that does not read as a row (a row from before rows were actions, among them). */
|
|
280
338
|
function row(raw) {
|
|
281
339
|
const o = obj(raw);
|
|
282
340
|
const title = str(o.title);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const href = str(o.href);
|
|
286
|
-
if (!title || !action || !prompt || (href && https(href) !== href))
|
|
341
|
+
const act = actOf(o.act);
|
|
342
|
+
if (!title || !act)
|
|
287
343
|
return null;
|
|
288
344
|
return {
|
|
289
|
-
id: str(o.id) || idOf(
|
|
345
|
+
id: str(o.id) || idOf(act.kind === 'open' ? act.to : '', title),
|
|
290
346
|
icon: ICONS.includes(o.icon) ? o.icon : 'chat',
|
|
291
347
|
title,
|
|
292
|
-
href,
|
|
293
348
|
summary: str(o.summary),
|
|
294
|
-
|
|
295
|
-
prompt,
|
|
349
|
+
act,
|
|
296
350
|
};
|
|
297
351
|
}
|
|
298
352
|
/** The `suggest` key of the preference document, or undefined when it holds none. */
|
|
@@ -309,11 +363,11 @@ export function kept(raw) {
|
|
|
309
363
|
}
|
|
310
364
|
/** The rows to draw: those not dismissed. */
|
|
311
365
|
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. */
|
|
366
|
+
/** What Hanzo does, three ways in, each done at once: what fills the rows a person's activity and signup do not. */
|
|
313
367
|
export const STARTERS = [
|
|
314
|
-
{ id: 'start:chat', icon: 'chat', title: '
|
|
315
|
-
{ id: 'start:dev', icon: 'agent', title: 'Build
|
|
316
|
-
{ id: 'start:research', icon: 'file', title: '
|
|
368
|
+
{ id: 'start:chat', icon: 'chat', title: 'Ask what Hanzo can do for you', summary: '', act: { kind: 'send', prompt: 'What can you do for me? Give me three concrete things to try today.' } },
|
|
369
|
+
{ id: 'start:dev', icon: 'agent', title: 'Build a landing page with Dev', summary: '', act: { kind: 'task', prompt: 'Build a one-page landing site for my project and give me the preview link.' } },
|
|
370
|
+
{ id: 'start:research', icon: 'file', title: 'Get a cited brief on AI agents', summary: '', act: { kind: 'send', prompt: 'Research the state of AI coding agents this month and give me a short brief with sources.' } },
|
|
317
371
|
];
|
|
318
372
|
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
319
373
|
export const fill = (k) => {
|
|
@@ -333,10 +387,10 @@ export function skip(k, inputs) {
|
|
|
333
387
|
const gone = new Set(k.dismissed);
|
|
334
388
|
const out = new Set();
|
|
335
389
|
for (const i of inputs)
|
|
336
|
-
if (i.
|
|
337
|
-
out.add(i.
|
|
390
|
+
if (i.to && gone.has(idOf(`${i.to}${i.item ?? ''}`, '')))
|
|
391
|
+
out.add(i.key);
|
|
338
392
|
for (const r of k.rows)
|
|
339
393
|
if (gone.has(r.id))
|
|
340
|
-
out.add(r.
|
|
394
|
+
out.add(r.title);
|
|
341
395
|
return [...out];
|
|
342
396
|
}
|
package/lib/suggestions.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type Automation, type Source } from './api/auto.ts';
|
|
1
2
|
import { type Target } from './api/call.ts';
|
|
2
|
-
import { type Suggestion } from './suggest.ts';
|
|
3
|
+
import { type Act, type Suggestion } from './suggest.ts';
|
|
3
4
|
export interface Suggested {
|
|
4
5
|
/** The rows to draw: three when any are, none dismissed. */
|
|
5
6
|
rows: Suggestion[];
|
|
@@ -10,11 +11,24 @@ export interface Suggested {
|
|
|
10
11
|
}
|
|
11
12
|
/** The person's suggestions. Signed out, or with nothing worth saying, there are none. */
|
|
12
13
|
export declare function useSuggestions(t: Target): Suggested;
|
|
14
|
+
/**
|
|
15
|
+
* Leave `prompt` for the product `kind` names, which does it on arrival, once:
|
|
16
|
+
* Chat sends a `send` as a new chat, Dev starts a `task` as a run. Same-tab
|
|
17
|
+
* session storage, written by the person's own press, so an address can never
|
|
18
|
+
* carry one (a link is not consent to spend).
|
|
19
|
+
*/
|
|
20
|
+
export declare function hand(kind: 'send' | 'task', prompt: string): void;
|
|
21
|
+
/** The prompt left for `kind`, taken so it is done once, or ''. */
|
|
22
|
+
export declare function taken(kind: 'send' | 'task'): string;
|
|
23
|
+
/** Make a `schedule` row's task: on, asking before it acts, and notifying its person. */
|
|
24
|
+
export declare function book(t: Target, act: Extract<Act, {
|
|
25
|
+
kind: 'schedule';
|
|
26
|
+
}>, source: Source): Promise<Automation>;
|
|
13
27
|
export interface SuggestionsProps {
|
|
14
28
|
rows: readonly Suggestion[];
|
|
15
|
-
/** A row was
|
|
16
|
-
|
|
29
|
+
/** A row was pressed: do what it says. */
|
|
30
|
+
onAct: (act: Act, row: Suggestion) => void;
|
|
17
31
|
onDismiss: (id: string) => void;
|
|
18
32
|
}
|
|
19
33
|
/** The rows, or nothing when there are none. */
|
|
20
|
-
export declare function Suggestions({ rows,
|
|
34
|
+
export declare function Suggestions({ rows, onAct, onDismiss }: SuggestionsProps): import("react").JSX.Element | null;
|
package/lib/suggestions.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* The suggestions under the composer (above it on a phone): up to three rows,
|
|
4
|
-
* each the source's mark
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* each the source's mark and one short action, its summary on hover. Pressing
|
|
5
|
+
* a row DOES it (suggest.ts `Act`): it opens what it names, sends its prompt as
|
|
6
|
+
* a new chat, starts its run, or makes its scheduled task. Nothing only fills
|
|
7
|
+
* the composer. Its × dismisses it, and the account remembers.
|
|
7
8
|
*
|
|
8
9
|
* `useSuggestions` reads the kept rows from the account (`suggest` in /v1/pref)
|
|
9
10
|
* and, when they are due (suggest.ts `due`), reads the person's activity and
|
|
@@ -19,12 +20,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
19
20
|
* POST /v1/iam/preferences {} what signup kept (`onboarding`)
|
|
20
21
|
* POST /v1/chat/completions the rows, as `MODEL` writes them
|
|
21
22
|
* PATCH /v1/pref {suggest} the rows kept, and each dismissal
|
|
23
|
+
* POST /v1/auto/automations a `schedule` row's task
|
|
22
24
|
*/
|
|
23
25
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
24
|
-
import {
|
|
26
|
+
import { Bot, CalendarClock, FileText, Github, MessageSquare, X } from '@hanzogui/lucide-icons-2';
|
|
25
27
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@hanzo/ui';
|
|
26
28
|
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
27
|
-
import { automations } from './api/auto.js';
|
|
29
|
+
import { automations, create } from './api/auto.js';
|
|
28
30
|
import { call, Refusal, unwrap } from './api/call.js';
|
|
29
31
|
import { conversations } from './api/conversations.js';
|
|
30
32
|
import { items } from './api/library.js';
|
|
@@ -32,7 +34,6 @@ import { read, save } from './api/pref.js';
|
|
|
32
34
|
import { list } from './api/sessions.js';
|
|
33
35
|
import { everything } from './api/work.js';
|
|
34
36
|
import { claims } from './claims.js';
|
|
35
|
-
import { Out } from './out.js';
|
|
36
37
|
import { answer, dismissed, due, fill, inputs, request, seed, sig, skip, stale, TAKE } from './suggest.js';
|
|
37
38
|
/** 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
39
|
function soft(p, none) {
|
|
@@ -132,6 +133,49 @@ export function useSuggestions(t) {
|
|
|
132
133
|
}, [t]);
|
|
133
134
|
return useMemo(() => ({ rows, dismiss, error }), [rows, dismiss, error]);
|
|
134
135
|
}
|
|
136
|
+
/** Where a row leaves its act for the product that does it. */
|
|
137
|
+
const HAND = 'hanzo.suggest.hand';
|
|
138
|
+
/**
|
|
139
|
+
* Leave `prompt` for the product `kind` names, which does it on arrival, once:
|
|
140
|
+
* Chat sends a `send` as a new chat, Dev starts a `task` as a run. Same-tab
|
|
141
|
+
* session storage, written by the person's own press, so an address can never
|
|
142
|
+
* carry one (a link is not consent to spend).
|
|
143
|
+
*/
|
|
144
|
+
export function hand(kind, prompt) {
|
|
145
|
+
try {
|
|
146
|
+
window.sessionStorage.setItem(HAND, JSON.stringify({ kind, prompt }));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Without storage the product opens without it.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The prompt left for `kind`, taken so it is done once, or ''. */
|
|
153
|
+
export function taken(kind) {
|
|
154
|
+
try {
|
|
155
|
+
const v = JSON.parse(window.sessionStorage.getItem(HAND) ?? 'null');
|
|
156
|
+
if (v?.kind !== kind || typeof v.prompt !== 'string')
|
|
157
|
+
return '';
|
|
158
|
+
window.sessionStorage.removeItem(HAND);
|
|
159
|
+
return v.prompt.trim();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Make a `schedule` row's task: on, asking before it acts, and notifying its person. */
|
|
166
|
+
export function book(t, act, source) {
|
|
167
|
+
return create(t, {
|
|
168
|
+
name: act.name || act.prompt.slice(0, 80),
|
|
169
|
+
instructions: act.prompt,
|
|
170
|
+
model: null,
|
|
171
|
+
schedule: { ...act.schedule, tz: Intl.DateTimeFormat().resolvedOptions().timeZone },
|
|
172
|
+
permissions: 'ask',
|
|
173
|
+
notify: true,
|
|
174
|
+
postTo: [],
|
|
175
|
+
enabled: true,
|
|
176
|
+
source,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
135
179
|
const MARKS = {
|
|
136
180
|
github: Github,
|
|
137
181
|
agent: Bot,
|
|
@@ -142,30 +186,17 @@ const MARKS = {
|
|
|
142
186
|
/** The row's pitch, px: a finger's on a phone, and the same at a desk. */
|
|
143
187
|
const PITCH = 44;
|
|
144
188
|
/** The rows, or nothing when there are none. */
|
|
145
|
-
export function Suggestions({ rows,
|
|
189
|
+
export function Suggestions({ rows, onAct, onDismiss }) {
|
|
146
190
|
// A phone has no hover: its × stays in sight.
|
|
147
191
|
const touch = !useMedia().sm;
|
|
148
192
|
if (!rows.length)
|
|
149
193
|
return null;
|
|
150
|
-
return (_jsx(YStack, { role: "list", "aria-label": "Suggestions", "data-slot": "suggestions", children: rows.map((r) => (_jsx(Row, { row: r, touch: touch,
|
|
194
|
+
return (_jsx(YStack, { role: "list", "aria-label": "Suggestions", "data-slot": "suggestions", children: rows.map((r) => (_jsx(Row, { row: r, touch: touch, onAct: onAct, onDismiss: onDismiss }, r.id))) }));
|
|
151
195
|
}
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
* positioned after it, so it paints above, and lets presses through), and over
|
|
155
|
-
* it the title's link and the × are their own: three stops for the keyboard,
|
|
156
|
-
* none inside another.
|
|
157
|
-
*/
|
|
158
|
-
function Row({ row, touch, onPick, onDismiss }) {
|
|
196
|
+
/** One row: the whole of it one button that does the act, and its × beside it. Two stops for the keyboard, neither inside the other. */
|
|
197
|
+
function Row({ row, touch, onAct, onDismiss }) {
|
|
159
198
|
const [near, setNear] = useState(false);
|
|
160
199
|
const Mark = MARKS[row.icon];
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
/** What happened, linked to its source with a ↗ when it has one, its summary in a card above it. It keeps its words before the step does, up to 70% of the line. */
|
|
164
|
-
function Title({ row }) {
|
|
165
|
-
const words = (_jsx(SizableText, { fontSize: 15, color: "$soft", numberOfLines: 1, textDecorationLine: row.href ? 'underline' : 'none', textDecorationStyle: "dotted", children: row.title }));
|
|
166
|
-
const text = row.href ? (_jsxs(Out, { href: row.href, label: `${row.title}, opens the source`, children: [words, _jsx(ArrowUpRight, { size: 12, color: "$soft" })] })) : (words);
|
|
167
|
-
if (!row.summary) {
|
|
168
|
-
return (_jsx(XStack, { shrink: 0, maxW: "70%", minW: 0, pointerEvents: "auto", children: text }));
|
|
169
|
-
}
|
|
170
|
-
return (_jsxs(HoverCard, { openDelay: 300, closeDelay: 150, children: [_jsx(HoverCardTrigger, { shrink: 0, maxW: "70%", minW: 0, pointerEvents: "auto", children: text }), _jsx(HoverCardContent, { side: "top", align: "start", sideOffset: 8, width: 470, maxW: "calc(100vw - 32px)", rounded: "$6", p: "$4", children: _jsx(SizableText, { fontSize: 16, lineHeight: 26, color: "$ink", children: row.summary }) })] }));
|
|
200
|
+
const button = (_jsxs(XStack, { render: "button", "data-act": row.act.kind, "aria-label": row.title, onPress: () => onAct(row.act, row), flex: 1, minW: 0, self: "stretch", items: "center", gap: "$3", pl: "$3", rounded: "$4", cursor: "pointer", focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(Mark, { size: 18, color: "$soft" }), _jsx(SizableText, { fontSize: 15, color: "$soft", numberOfLines: 1, shrink: 1, minW: 0, children: row.title })] }));
|
|
201
|
+
return (_jsxs(XStack, { role: "listitem", "data-slot": "suggestion", items: "center", gap: "$1", minH: PITCH, pr: "$2", rounded: "$4", hoverStyle: { bg: '$hover' }, onPointerEnter: () => setNear(true), onPointerLeave: () => setNear(false), children: [row.summary ? (_jsxs(HoverCard, { openDelay: 300, closeDelay: 150, children: [_jsx(HoverCardTrigger, { flex: 1, minW: 0, self: "stretch", flexDirection: "row", children: button }), _jsx(HoverCardContent, { side: "top", align: "start", sideOffset: 8, width: 470, maxW: "calc(100vw - 32px)", rounded: "$6", p: "$4", children: _jsx(SizableText, { fontSize: 16, lineHeight: 26, color: "$ink", children: row.summary }) })] })) : (button), _jsx(XStack, { render: "button", "aria-label": `Dismiss ${row.title}`, onPress: () => onDismiss(row.id), p: "$1.5", rounded: "$2", cursor: "pointer", opacity: touch || near ? 1 : 0, hoverStyle: { bg: '$hover' }, focusVisibleStyle: { opacity: 1, outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: _jsx(X, { size: 16, color: "$soft" }) })] }));
|
|
171
202
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.79",
|
|
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.",
|