@hanzo/build 0.2.78 → 0.2.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/lib/api/agents.d.ts +37 -17
- package/lib/api/agents.js +52 -23
- package/lib/customize/agents.d.ts +3 -0
- package/lib/customize/agents.js +85 -35
- package/lib/customize/index.js +2 -4
- package/lib/host.d.ts +3 -1
- 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.js +4 -2
- 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/README.md
CHANGED
|
@@ -226,10 +226,15 @@ are any member's.
|
|
|
226
226
|
- Plugins: TypeScript connectors built on the platform, from source or from a
|
|
227
227
|
description of an API; a failed build says why. Browse offers building one and
|
|
228
228
|
lists what the deployment mounts.
|
|
229
|
-
- Agents: Browse is
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
- Agents: Browse is every agent to start from — the host's default agents
|
|
230
|
+
(`cast`) and personas (`personas`), and the platform's presets whose tool
|
|
231
|
+
calls run on the platform — searched by name and filtered by category. Use
|
|
232
|
+
makes one agent of the org's own from one template (a name taken becomes
|
|
233
|
+
`-2`, `-3`), with its face, model and instructions, and opens its profile;
|
|
234
|
+
nothing is added in bulk. Start from scratch, Clone yourself (the person
|
|
235
|
+
signed in, with writing pasted or uploaded) and Create from a person make one
|
|
236
|
+
too. Yours is the org's own — model, instructions, tools, budget — created,
|
|
237
|
+
edited (only what changed is sent) and deleted.
|
|
233
238
|
|
|
234
239
|
**New.** "What's up next?", and at the foot the composer: where the run runs
|
|
235
240
|
(Cloud is the platform's sandbox with the codebase's environment; the org's
|
package/lib/api/agents.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agents: the org's own — a model, instructions, the tools it may call
|
|
3
|
-
* budget — and the
|
|
2
|
+
* Agents: the org's own — a model, instructions, the tools it may call, a
|
|
3
|
+
* budget and a face — and the templates one is made from.
|
|
4
4
|
*
|
|
5
5
|
* GET /v1/agent {agents: [agent]}, without instructions
|
|
6
6
|
* POST /v1/agent create → 201 agent
|
|
@@ -35,6 +35,8 @@ export interface Agent {
|
|
|
35
35
|
task: number;
|
|
36
36
|
spent: number;
|
|
37
37
|
period: Period | '';
|
|
38
|
+
/** Its picture: an https link or a data URL; '' draws its emoji or initial. */
|
|
39
|
+
avatar: string;
|
|
38
40
|
emoji: string;
|
|
39
41
|
}
|
|
40
42
|
export interface Preset {
|
|
@@ -52,6 +54,8 @@ export interface Draft {
|
|
|
52
54
|
cap: string;
|
|
53
55
|
task: string;
|
|
54
56
|
period: Period;
|
|
57
|
+
/** Sent on create only: an https link or a data URL. */
|
|
58
|
+
avatar: string;
|
|
55
59
|
}
|
|
56
60
|
/** An agent's name, as the handler takes it. */
|
|
57
61
|
export declare const NAME: RegExp;
|
|
@@ -86,29 +90,45 @@ export declare function profile(t: Target, ref: string): Promise<{
|
|
|
86
90
|
agent: Agent;
|
|
87
91
|
runs: Run[];
|
|
88
92
|
}>;
|
|
89
|
-
/**
|
|
93
|
+
/** An agent to start from: a default agent, a persona or a preset. */
|
|
90
94
|
export interface Cast {
|
|
95
|
+
/** The name it is made under. */
|
|
91
96
|
name: string;
|
|
97
|
+
/** What Browse calls it. */
|
|
98
|
+
title: string;
|
|
92
99
|
description: string;
|
|
100
|
+
/** What Browse lists it under. */
|
|
101
|
+
category: string;
|
|
93
102
|
model: string;
|
|
94
103
|
instructions: string;
|
|
95
|
-
/**
|
|
104
|
+
/** Reads the instructions, where they are a file the host serves. */
|
|
105
|
+
read?: () => Promise<string>;
|
|
106
|
+
/** Its face, an address of the host. */
|
|
96
107
|
avatar?: string;
|
|
108
|
+
tools?: string[];
|
|
97
109
|
}
|
|
98
|
-
/** What a
|
|
110
|
+
/** What an agent made from a template may spend until its owner says otherwise, in dollars: each month, and one run. */
|
|
99
111
|
export declare const CAST_CAP = "10";
|
|
100
112
|
export declare const CAST_TASK = "1";
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
/**
|
|
114
|
+
* A name the API takes: accents dropped, any other run of characters a dash,
|
|
115
|
+
* from a letter or digit, up to 64. "Spärck Jones" is Sparck-Jones.
|
|
116
|
+
*/
|
|
117
|
+
export declare const handle: (name: string) => string;
|
|
118
|
+
/** `name`, or `name-2`, `name-3`… : the first the org has no agent called, in any case. */
|
|
119
|
+
export declare function free(name: string, have: readonly {
|
|
107
120
|
name: string;
|
|
108
|
-
}[]):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
}[]): string;
|
|
122
|
+
/** An address of the page as an https link the API keeps, or '' (a page served over http has none). */
|
|
123
|
+
export declare const link: (at: string | undefined, base: string) => string;
|
|
124
|
+
/**
|
|
125
|
+
* Make the org's own agent from a template: under the first name the org has
|
|
126
|
+
* free (read now, so it is never one that exists), with its instructions read
|
|
127
|
+
* and its face linked. The template is not touched.
|
|
128
|
+
*/
|
|
129
|
+
export declare function use(t: Target, c: Cast, base: string): Promise<Agent>;
|
|
130
|
+
/** Anyone as a template: their name, who they are, and writing of theirs to take the voice from. */
|
|
131
|
+
export declare function person(name: string, about: string, writing: string, avatar?: string): Cast;
|
|
112
132
|
export declare function create(t: Target, d: Draft): Promise<Agent>;
|
|
113
133
|
/** The fields that differ from `was`, and nothing else: an omitted field keeps its value. */
|
|
114
134
|
export declare function changes(was: Agent, d: Draft): Record<string, unknown>;
|
|
@@ -116,5 +136,5 @@ export declare function update(t: Target, was: Agent, d: Draft): Promise<Agent>;
|
|
|
116
136
|
export declare function remove(t: Target, ref: string): Promise<void>;
|
|
117
137
|
/** The presets that make a working agent. */
|
|
118
138
|
export declare function presets(t: Target): Promise<Preset[]>;
|
|
119
|
-
/** A
|
|
120
|
-
export declare function fromPreset(p: Preset):
|
|
139
|
+
/** A preset as a template. */
|
|
140
|
+
export declare function fromPreset(p: Preset): Cast;
|
package/lib/api/agents.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agents: the org's own — a model, instructions, the tools it may call
|
|
3
|
-
* budget — and the
|
|
2
|
+
* Agents: the org's own — a model, instructions, the tools it may call, a
|
|
3
|
+
* budget and a face — and the templates one is made from.
|
|
4
4
|
*
|
|
5
5
|
* GET /v1/agent {agents: [agent]}, without instructions
|
|
6
6
|
* POST /v1/agent create → 201 agent
|
|
@@ -47,6 +47,7 @@ export function agent(raw) {
|
|
|
47
47
|
task: num(o.max_task_micro_usd),
|
|
48
48
|
spent: num(o.consumed_micro_usd),
|
|
49
49
|
period: PERIODS.includes(period) ? period : '',
|
|
50
|
+
avatar: str(o.avatar),
|
|
50
51
|
emoji: str(o.emoji),
|
|
51
52
|
};
|
|
52
53
|
}
|
|
@@ -61,9 +62,10 @@ export function draft(a) {
|
|
|
61
62
|
cap: dollars(a.cap),
|
|
62
63
|
task: dollars(a.task),
|
|
63
64
|
period: a.period || 'month',
|
|
65
|
+
avatar: a.avatar,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
|
-
export const EMPTY = { name: '', description: '', model: '', instructions: '', tools: [], cap: '', task: '', period: 'month' };
|
|
68
|
+
export const EMPTY = { name: '', description: '', model: '', instructions: '', tools: [], cap: '', task: '', period: 'month', avatar: '' };
|
|
67
69
|
/** Why a draft cannot be saved as it stands, or '' when it can. */
|
|
68
70
|
export function refuse(d) {
|
|
69
71
|
if (!NAME.test(d.name))
|
|
@@ -95,27 +97,52 @@ export async function profile(t, ref) {
|
|
|
95
97
|
const raw = await call(t, 'GET', `/v1/agent/${seg(ref)}`);
|
|
96
98
|
return { agent: agent(raw), runs: rows(obj(raw).recentRuns).map(run).filter((r) => r.id) };
|
|
97
99
|
}
|
|
98
|
-
/** What a
|
|
100
|
+
/** What an agent made from a template may spend until its owner says otherwise, in dollars: each month, and one run. */
|
|
99
101
|
export const CAST_CAP = '10';
|
|
100
102
|
export const CAST_TASK = '1';
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
/**
|
|
104
|
+
* A name the API takes: accents dropped, any other run of characters a dash,
|
|
105
|
+
* from a letter or digit, up to 64. "Spärck Jones" is Sparck-Jones.
|
|
106
|
+
*/
|
|
107
|
+
export const handle = (name) => name
|
|
108
|
+
.normalize('NFD')
|
|
109
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
110
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
111
|
+
.replace(/^[^A-Za-z0-9]+/, '')
|
|
112
|
+
.slice(0, 64)
|
|
113
|
+
.replace(/-+$/, '');
|
|
114
|
+
/** `name`, or `name-2`, `name-3`… : the first the org has no agent called, in any case. */
|
|
115
|
+
export function free(name, have) {
|
|
116
|
+
const taken = new Set(have.map((a) => a.name.toLowerCase()));
|
|
117
|
+
if (!taken.has(name.toLowerCase()))
|
|
118
|
+
return name;
|
|
119
|
+
for (let i = 2;; i++) {
|
|
120
|
+
const next = `${name.slice(0, 63 - String(i).length)}-${i}`;
|
|
121
|
+
if (!taken.has(next.toLowerCase()))
|
|
122
|
+
return next;
|
|
117
123
|
}
|
|
118
|
-
|
|
124
|
+
}
|
|
125
|
+
/** An address of the page as an https link the API keeps, or '' (a page served over http has none). */
|
|
126
|
+
export const link = (at, base) => {
|
|
127
|
+
if (!at)
|
|
128
|
+
return '';
|
|
129
|
+
const url = new URL(at, base).href;
|
|
130
|
+
return url.startsWith('https://') || url.startsWith('data:') ? url : '';
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Make the org's own agent from a template: under the first name the org has
|
|
134
|
+
* free (read now, so it is never one that exists), with its instructions read
|
|
135
|
+
* and its face linked. The template is not touched.
|
|
136
|
+
*/
|
|
137
|
+
export async function use(t, c, base) {
|
|
138
|
+
const [have, instructions] = await Promise.all([agents(t), c.read ? c.read() : c.instructions]);
|
|
139
|
+
return create(t, { ...EMPTY, name: free(handle(c.name), have), description: c.description, model: c.model, instructions, tools: c.tools ?? [], cap: CAST_CAP, task: CAST_TASK, avatar: link(c.avatar, base) });
|
|
140
|
+
}
|
|
141
|
+
/** Anyone as a template: their name, who they are, and writing of theirs to take the voice from. */
|
|
142
|
+
export function person(name, about, writing, avatar = '') {
|
|
143
|
+
const who = name.trim();
|
|
144
|
+
const lines = [`You are ${who}.`, about.trim(), writing.trim() && `Writing by ${who}:\n\n${writing.trim()}`];
|
|
145
|
+
return { name: who, title: who, description: about.trim(), category: 'Person', model: '', instructions: lines.filter(Boolean).join('\n\n'), avatar };
|
|
119
146
|
}
|
|
120
147
|
export async function create(t, d) {
|
|
121
148
|
const why = refuse(d);
|
|
@@ -132,6 +159,8 @@ export async function create(t, d) {
|
|
|
132
159
|
};
|
|
133
160
|
if (d.model)
|
|
134
161
|
body.model = d.model;
|
|
162
|
+
if (d.avatar)
|
|
163
|
+
body.avatar = d.avatar;
|
|
135
164
|
return agent(await call(t, 'POST', '/v1/agent', body));
|
|
136
165
|
}
|
|
137
166
|
/** The fields that differ from `was`, and nothing else: an omitted field keeps its value. */
|
|
@@ -173,7 +202,7 @@ export async function presets(t) {
|
|
|
173
202
|
.map((p) => ({ id: str(p.id), title: str(p.title), prompt: str(p.systemPrompt) }))
|
|
174
203
|
.filter((p) => p.id);
|
|
175
204
|
}
|
|
176
|
-
/** A
|
|
205
|
+
/** A preset as a template. */
|
|
177
206
|
export function fromPreset(p) {
|
|
178
|
-
return {
|
|
207
|
+
return { name: p.id, title: p.title || p.id, description: p.title, category: 'Preset', model: '', instructions: p.prompt };
|
|
179
208
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { type Cast } from '../api/agents.ts';
|
|
1
2
|
import { type Pane } from './ui.tsx';
|
|
3
|
+
/** Categories as Browse offers them: the default agents' and presets' first, as listed, then the rest by how many each holds. */
|
|
4
|
+
export declare function kinds(first: readonly Cast[], rest: readonly Cast[]): string[];
|
|
2
5
|
export declare function Agents({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
|
|
3
6
|
/** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
|
|
4
7
|
export declare function Profile({ name }: {
|
package/lib/customize/agents.js
CHANGED
|
@@ -3,10 +3,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* Agents: the org's own — a model, instructions, the tools it may call, and
|
|
4
4
|
* what it may spend.
|
|
5
5
|
*
|
|
6
|
-
* Browse is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Browse is every agent there is to start from: the host's default agents
|
|
7
|
+
* (`cast`) and personas (`personas`), and the platform's presets whose tool
|
|
8
|
+
* calls run on the platform, by name and category. Use makes the org's own
|
|
9
|
+
* agent from one and opens it; nothing is added any other way, so an org starts
|
|
10
|
+
* with none. An agent is also made from scratch, from the person signed in
|
|
11
|
+
* (Clone yourself) or from anyone named. Yours lists the org's agents. Opening one is its profile page
|
|
10
12
|
* (`-/customize/agents/<name>`): its face, part, model, instructions, tools,
|
|
11
13
|
* budget and runs, edited in place, and only what changed is sent. An
|
|
12
14
|
* agent calls only the tools it names, and only those that are on for the org,
|
|
@@ -15,11 +17,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
15
17
|
* spend each period, and in one run.
|
|
16
18
|
*/
|
|
17
19
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
18
|
-
import { ArrowLeft,
|
|
20
|
+
import { ArrowLeft, Upload } from '@hanzogui/lucide-icons-2';
|
|
19
21
|
import { Button, Input, Switch, Textarea } from '@hanzo/ui';
|
|
20
22
|
import { ModelPicker } from '@hanzo/ui/models';
|
|
21
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
22
|
-
import { agents, ALL, create, draft, EMPTY, fromPreset,
|
|
23
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
24
|
+
import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, person, presets, profile, remove, update, use } from '../api/agents.js';
|
|
23
25
|
import { models } from '../api/models.js';
|
|
24
26
|
import { tools } from '../api/tools.js';
|
|
25
27
|
import { useHost, useTarget } from '../host.js';
|
|
@@ -27,58 +29,106 @@ import { path } from '../route.js';
|
|
|
27
29
|
import { useLoad } from './load.js';
|
|
28
30
|
import { say } from './say.js';
|
|
29
31
|
import { Add, Choice, Confirm, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
|
|
32
|
+
/** How many cards Browse draws at a time. */
|
|
33
|
+
const PAGE = 60;
|
|
34
|
+
const ANY = 'All';
|
|
35
|
+
/** Categories as Browse offers them: the default agents' and presets' first, as listed, then the rest by how many each holds. */
|
|
36
|
+
export function kinds(first, rest) {
|
|
37
|
+
const lead = [...new Set(first.map((c) => c.category))];
|
|
38
|
+
const count = new Map();
|
|
39
|
+
for (const c of rest)
|
|
40
|
+
if (!lead.includes(c.category))
|
|
41
|
+
count.set(c.category, (count.get(c.category) ?? 0) + 1);
|
|
42
|
+
return [ANY, ...lead, ...[...count.keys()].sort((a, b) => count.get(b) - count.get(a) || a.localeCompare(b))];
|
|
43
|
+
}
|
|
30
44
|
const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
|
|
31
45
|
export function Agents({ view, q, adding, onAdding, onView, onCount }) {
|
|
32
46
|
const host = useHost();
|
|
33
47
|
const t = useTarget();
|
|
34
48
|
const signed = Boolean(host.person);
|
|
49
|
+
const browsing = view === 'discover';
|
|
35
50
|
const mine = useLoad(signed ? () => agents(t) : null, [], [t, signed]);
|
|
36
|
-
const offered = useLoad(
|
|
51
|
+
const offered = useLoad(browsing ? () => presets(t) : null, [], [t, view]);
|
|
52
|
+
const figures = useLoad(browsing && host.personas ? host.personas : null, [], [view]);
|
|
37
53
|
useCount(mine.value.length, onCount);
|
|
38
|
-
const [start, setStart] = useState(null);
|
|
39
54
|
const [note, setNote] = useState('');
|
|
40
|
-
const [
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
const [kind, setKind] = useState(ANY);
|
|
56
|
+
const [more, setMore] = useState(PAGE);
|
|
57
|
+
const [using, setUsing] = useState(null);
|
|
58
|
+
const [making, setMaking] = useState(null);
|
|
59
|
+
useEffect(() => setMore(PAGE), [q, kind]);
|
|
60
|
+
const opened = (a) => host.go(path({ kind: 'agent', name: a.name }));
|
|
61
|
+
const take = async (c) => {
|
|
62
|
+
setUsing(c);
|
|
46
63
|
setNote('');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
try {
|
|
65
|
+
opened(await use(t, c, window.location.href));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
setNote(say(e));
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setUsing(null);
|
|
72
|
+
}
|
|
56
73
|
};
|
|
57
|
-
const editor = adding
|
|
58
|
-
|
|
74
|
+
const editor = adding ? (_jsx(Editor, { onClose: () => onAdding(false), onSaved: (a) => {
|
|
75
|
+
onAdding(false);
|
|
59
76
|
onView('yours');
|
|
60
77
|
mine.reload();
|
|
61
78
|
setNote(`${a.name} is saved`);
|
|
62
79
|
} })) : null;
|
|
63
|
-
if (
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
80
|
+
if (browsing) {
|
|
81
|
+
const first = [...(host.cast ?? []), ...offered.value.map(fromPreset)];
|
|
82
|
+
const all = [...first, ...figures.value];
|
|
83
|
+
const shown = all.filter((c) => (kind === ANY || c.category === kind) && matches(q, c.title, c.name, c.description, c.category));
|
|
84
|
+
const failed = figures.error ?? offered.error;
|
|
85
|
+
return (_jsxs(YStack, { gap: "$3", children: [signed ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: "Start from scratch" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('self'), children: "Clone yourself" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('person'), children: "Create from a person" })] })) : null, _jsx(XStack, { overflowX: "auto", overflowY: "hidden", pb: "$1", children: _jsx(Choice, { label: "Category", value: kind, options: kinds(first, figures.value).map((id) => ({ id, label: id })), onChange: setKind }) }), _jsx(Line, { children: note }), shown.length ? (_jsx(Grid, { label: "Agents to start from", children: shown.slice(0, more).map((c) => (_jsx(Tile, { title: c.title, detail: c.description === c.title || c.description === c.category ? undefined : c.description, meta: c.category, mark: _jsx(Mark, { name: c.title, size: 40, logo: c.avatar }), action: signed ? _jsx(Add, { name: c.title, label: "Use", added: false, busy: using !== null, onPress: () => void take(c) }) : undefined }, `${c.category}/${c.name}/${c.title}`))) })) : figures.loading || offered.loading ? null : (_jsx(Soft, { children: `No agent matches “${q.trim()}”.` })), shown.length > more ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setMore(more + PAGE), children: "More" }) })) : null, failed ? _jsx(Failed, { error: failed, onRetry: figures.error ? figures.reload : offered.reload }) : figures.loading || offered.loading ? _jsx(Soft, { children: "Loading\u2026" }) : null, editor, making ? _jsx(Person, { self: making === 'self', onClose: () => setMaking(null), onMade: opened }) : null] }));
|
|
67
86
|
}
|
|
68
87
|
if (!signed) {
|
|
69
88
|
return _jsx(Visitor, { children: "Sign in to see your agents." });
|
|
70
89
|
}
|
|
71
90
|
const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
|
|
72
|
-
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail:
|
|
91
|
+
return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: "", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse agents" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Face, { agent: a, cast: host.cast ?? [] }), onOpen: () => opened(a) }, a.id || a.name))) })), editor] }));
|
|
92
|
+
}
|
|
93
|
+
/** Clone yourself, or anyone named: a name, who they are, and writing of theirs, pasted or uploaded. */
|
|
94
|
+
function Person({ self, onClose, onMade }) {
|
|
95
|
+
const host = useHost();
|
|
96
|
+
const t = useTarget();
|
|
97
|
+
const me = self ? host.person : null;
|
|
98
|
+
const [name, setName] = useState(me?.name ?? '');
|
|
99
|
+
const [about, setAbout] = useState('');
|
|
100
|
+
const [writing, setWriting] = useState('');
|
|
101
|
+
const [working, setWorking] = useState(false);
|
|
102
|
+
const [note, setNote] = useState('');
|
|
103
|
+
const file = useRef(null);
|
|
104
|
+
const read = async (files) => {
|
|
105
|
+
const texts = await Promise.all([...(files ?? [])].map((f) => f.text()));
|
|
106
|
+
setWriting((w) => [w.trim(), ...texts.map((x) => x.trim())].filter(Boolean).join('\n\n'));
|
|
107
|
+
};
|
|
108
|
+
const save = async () => {
|
|
109
|
+
setWorking(true);
|
|
110
|
+
setNote('');
|
|
111
|
+
try {
|
|
112
|
+
onMade(await use(t, person(name, about, writing, me?.avatar), window.location.href));
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
setNote(say(e));
|
|
116
|
+
setWorking(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return (_jsxs(Sheet, { title: self ? 'Clone yourself' : 'Create from a person', open: true, onOpenChange: (o) => !o && onClose(), children: [_jsx(Field, { label: "Name", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name" }) }), _jsx(Field, { label: "Description", children: _jsx(Textarea, { value: about, onChangeText: setAbout, "aria-label": "Description", rows: 3 }) }), _jsx(Field, { label: "Writing", children: _jsx(Textarea, { value: writing, onChangeText: setWriting, "aria-label": "Writing", rows: 6 }) }), _jsxs(XStack, { children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => file.current?.click(), children: [_jsx(Upload, { size: 14 }), " Upload"] }), _jsx("input", { ref: file, type: "file", multiple: true, accept: ".txt,.md,.markdown,.csv,.json,.html,text/*", hidden: true, "aria-label": "Upload writing", onChange: (e) => {
|
|
120
|
+
void read(e.currentTarget.files);
|
|
121
|
+
e.currentTarget.value = '';
|
|
122
|
+
} })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working || !name.trim(), onPress: () => void save(), children: "Create" })] })] }));
|
|
73
123
|
}
|
|
74
124
|
function reach(a) {
|
|
75
125
|
if (a.tools.includes(ALL))
|
|
76
126
|
return 'every tool';
|
|
77
127
|
return a.tools.length === 1 ? '1 tool' : `${a.tools.length} tools`;
|
|
78
128
|
}
|
|
79
|
-
/** An agent's face: the
|
|
129
|
+
/** An agent's face: its picture, the default agent's of its name, its emoji, or its initial. */
|
|
80
130
|
function Face({ agent: a, cast, size = 32 }) {
|
|
81
|
-
const avatar = cast.find((c) => c.name.toLowerCase() === a.name.toLowerCase())?.avatar;
|
|
131
|
+
const avatar = a.avatar || cast.find((c) => c.name.toLowerCase() === a.name.toLowerCase())?.avatar;
|
|
82
132
|
return _jsx(Mark, { name: a.name, size: size, logo: avatar, icon: a.emoji ? _jsx(SizableText, { size: size > 40 ? '$8' : '$4', children: a.emoji }) : undefined });
|
|
83
133
|
}
|
|
84
134
|
/** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
|
|
@@ -95,12 +145,12 @@ export function Profile({ name }) {
|
|
|
95
145
|
}, onDeleted: back })) : null] }));
|
|
96
146
|
}
|
|
97
147
|
/** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
|
|
98
|
-
function Editor({ agent,
|
|
148
|
+
function Editor({ agent, onClose, onSaved, onDeleted, }) {
|
|
99
149
|
const t = useTarget();
|
|
100
150
|
const full = useLoad(agent ? () => one(t, agent.id || agent.name) : null, null, [t, agent?.id]);
|
|
101
151
|
const catalog = useLoad(() => models(t), [], [t]);
|
|
102
152
|
const on = useLoad(() => tools(t, { activated: true }), [], [t]);
|
|
103
|
-
const [d, setD] = useState(
|
|
153
|
+
const [d, setD] = useState(EMPTY);
|
|
104
154
|
const [ready, setReady] = useState(!agent);
|
|
105
155
|
const [working, setWorking] = useState(false);
|
|
106
156
|
const [note, setNote] = useState('');
|
package/lib/customize/index.js
CHANGED
|
@@ -29,11 +29,9 @@ const LABEL = {
|
|
|
29
29
|
plugins: { title: 'Plugins', says: 'Connectors you build in TypeScript.', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
|
|
30
30
|
agents: { title: 'Agents', says: 'Agents with their own model, instructions and budget.', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
|
|
31
31
|
};
|
|
32
|
-
/** Where a tab opens: an org's own agents (and the cast to add) for Agents; Browse for the rest, what there is to add being the page. */
|
|
33
|
-
const opens = (tab) => (tab === 'agents' ? 'yours' : 'discover');
|
|
34
32
|
export function Customize({ tab, view: first }) {
|
|
35
33
|
const host = useHost();
|
|
36
|
-
const [view, setView] = useState(first ??
|
|
34
|
+
const [view, setView] = useState(first ?? 'discover');
|
|
37
35
|
const [q, setQ] = useState('');
|
|
38
36
|
const [adding, setAdding] = useState(false);
|
|
39
37
|
const [count, setCount] = useState(0);
|
|
@@ -41,7 +39,7 @@ export function Customize({ tab, view: first }) {
|
|
|
41
39
|
const pick = (next) => {
|
|
42
40
|
setQ('');
|
|
43
41
|
setAdding(false);
|
|
44
|
-
setView(
|
|
42
|
+
setView('discover');
|
|
45
43
|
setCount(0);
|
|
46
44
|
host.go(path({ kind: 'customize', tab: next }));
|
|
47
45
|
};
|
package/lib/host.d.ts
CHANGED
|
@@ -107,8 +107,10 @@ export interface Host {
|
|
|
107
107
|
browser?: Browser;
|
|
108
108
|
/** The host's sidebar lists Settings' sections and Customize's tabs, so the pages draw neither. */
|
|
109
109
|
sections?: boolean;
|
|
110
|
-
/** The default agents
|
|
110
|
+
/** The default agents Customize › Agents offers to start from. Absent, none. */
|
|
111
111
|
cast?: readonly Cast[];
|
|
112
|
+
/** Every persona Customize › Agents offers to start from, read when Browse opens. Absent, none. */
|
|
113
|
+
personas?: () => Promise<readonly Cast[]>;
|
|
112
114
|
}
|
|
113
115
|
export declare function HostProvider({ host, children }: {
|
|
114
116
|
host: Host;
|
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.js
CHANGED
|
@@ -168,14 +168,16 @@ export function prompt(o, state) {
|
|
|
168
168
|
disabled,
|
|
169
169
|
placeholder: o.placeholder ?? ASK,
|
|
170
170
|
...(o.label ? { label: o.label } : {}),
|
|
171
|
-
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,
|
|
172
173
|
field: {
|
|
173
174
|
...o.field,
|
|
174
175
|
fontSize: 16,
|
|
175
176
|
lineHeight: 24,
|
|
176
177
|
px: 8,
|
|
177
178
|
pt: 2,
|
|
178
|
-
|
|
179
|
+
pb: 0,
|
|
180
|
+
minH: 26,
|
|
179
181
|
bg: 'transparent',
|
|
180
182
|
},
|
|
181
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.80",
|
|
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.",
|