bom-pi 0.0.0-stage → 0.0.2
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 +33 -2
- package/apps/README.md +7 -0
- package/apps/web/README.md +21 -0
- package/apps/web/pocket-adapter.mjs +323 -0
- package/apps/web/public/LICENSE.pocket +21 -0
- package/apps/web/public/activity-fold.js +28 -0
- package/apps/web/public/agent-now.js +37 -0
- package/apps/web/public/agent-tile.js +79 -0
- package/apps/web/public/app.js +975 -0
- package/apps/web/public/apple-touch-icon.png +0 -0
- package/apps/web/public/artifact-bridge.js +687 -0
- package/apps/web/public/avatar.js +87 -0
- package/apps/web/public/back.js +554 -0
- package/apps/web/public/box-keys.js +216 -0
- package/apps/web/public/browser-panel.js +1097 -0
- package/apps/web/public/browser.js +68 -0
- package/apps/web/public/calls.js +139 -0
- package/apps/web/public/chat.js +919 -0
- package/apps/web/public/clock.js +34 -0
- package/apps/web/public/commands.js +578 -0
- package/apps/web/public/composer.js +2068 -0
- package/apps/web/public/diff-parse.js +672 -0
- package/apps/web/public/diff.js +1646 -0
- package/apps/web/public/dock.js +299 -0
- package/apps/web/public/extension-ui.js +2734 -0
- package/apps/web/public/files-panel.js +775 -0
- package/apps/web/public/files.js +368 -0
- package/apps/web/public/fonts/FiraCode-VF.woff2 +0 -0
- package/apps/web/public/fonts/Geist.woff2 +0 -0
- package/apps/web/public/fonts/IBMPlexMono-Variable-Italic.woff2 +0 -0
- package/apps/web/public/fonts/IBMPlexMono-Variable.woff2 +0 -0
- package/apps/web/public/fonts/InterVariable-Italic.woff2 +0 -0
- package/apps/web/public/fonts/InterVariable.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Bold.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-BoldItalic.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Italic.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-Regular.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-SemiBold.woff2 +0 -0
- package/apps/web/public/fonts/Iosevka-SemiBoldItalic.woff2 +0 -0
- package/apps/web/public/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/apps/web/public/fonts/JetBrainsMono.woff2 +0 -0
- package/apps/web/public/fonts/NerdFontsSymbolsMono.woff2 +0 -0
- package/apps/web/public/fonts/SOURCES.md +69 -0
- package/apps/web/public/gestures.js +610 -0
- package/apps/web/public/highlight.js +480 -0
- package/apps/web/public/history.js +70 -0
- package/apps/web/public/home.js +169 -0
- package/apps/web/public/icon-192.png +0 -0
- package/apps/web/public/icon-512.png +0 -0
- package/apps/web/public/icon-maskable-512.png +0 -0
- package/apps/web/public/icon.svg +16 -0
- package/apps/web/public/index.html +71 -0
- package/apps/web/public/launcher.js +1293 -0
- package/apps/web/public/linked-sessions.js +55 -0
- package/apps/web/public/live.js +39 -0
- package/apps/web/public/manifest.webmanifest +32 -0
- package/apps/web/public/model-refresh.js +73 -0
- package/apps/web/public/notify.js +251 -0
- package/apps/web/public/peeks.js +920 -0
- package/apps/web/public/pip-attachments.js +182 -0
- package/apps/web/public/pip-window.js +141 -0
- package/apps/web/public/pip.js +1217 -0
- package/apps/web/public/quote-reply.js +109 -0
- package/apps/web/public/quote-text.js +31 -0
- package/apps/web/public/rich.js +403 -0
- package/apps/web/public/run-frame.js +106 -0
- package/apps/web/public/safe-mode.js +71 -0
- package/apps/web/public/seen.js +169 -0
- package/apps/web/public/session-tree.js +117 -0
- package/apps/web/public/sessions.js +2695 -0
- package/apps/web/public/share.js +126 -0
- package/apps/web/public/sheets/appearance.js +820 -0
- package/apps/web/public/sheets/branch.js +353 -0
- package/apps/web/public/sheets/extensions.js +818 -0
- package/apps/web/public/sheets/file.js +318 -0
- package/apps/web/public/sheets/find.js +250 -0
- package/apps/web/public/sheets/folder.js +680 -0
- package/apps/web/public/sheets/gallery.js +2340 -0
- package/apps/web/public/sheets/hooks.js +350 -0
- package/apps/web/public/sheets/memory.js +531 -0
- package/apps/web/public/sheets/menu.js +580 -0
- package/apps/web/public/sheets/message.js +220 -0
- package/apps/web/public/sheets/model.js +412 -0
- package/apps/web/public/sheets/people.js +414 -0
- package/apps/web/public/sheets/pi-sessions.js +264 -0
- package/apps/web/public/sheets/providers.js +525 -0
- package/apps/web/public/sheets/running.js +130 -0
- package/apps/web/public/sheets/schedules.js +71 -0
- package/apps/web/public/sheets/session.js +249 -0
- package/apps/web/public/sheets/shortcuts.js +101 -0
- package/apps/web/public/sheets/spend.js +150 -0
- package/apps/web/public/sheets/text.js +53 -0
- package/apps/web/public/sheets/viewers.js +363 -0
- package/apps/web/public/sheets/worktree.js +50 -0
- package/apps/web/public/sheets.js +410 -0
- package/apps/web/public/signin.js +57 -0
- package/apps/web/public/slots.js +208 -0
- package/apps/web/public/store.js +1760 -0
- package/apps/web/public/style.css +18625 -0
- package/apps/web/public/subagents-board.js +879 -0
- package/apps/web/public/subagents.js +273 -0
- package/apps/web/public/sw.js +169 -0
- package/apps/web/public/taps.js +26 -0
- package/apps/web/public/theme-surface.d.ts +4 -0
- package/apps/web/public/theme-surface.js +266 -0
- package/apps/web/public/theme.js +845 -0
- package/apps/web/public/themes/lantern/colors.toml +25 -0
- package/apps/web/public/themes/lantern/direction.css +60 -0
- package/apps/web/public/themes/lantern/frame.css +254 -0
- package/apps/web/public/themes/lantern/theme.css +10 -0
- package/apps/web/public/themes/lantern/theme.json +7 -0
- package/apps/web/public/themes/pocket/theme.css +189 -0
- package/apps/web/public/themes/pocket/theme.json +6 -0
- package/apps/web/public/themes.js +556 -0
- package/apps/web/public/transcript.js +3348 -0
- package/apps/web/public/trust.js +295 -0
- package/apps/web/public/turns.js +507 -0
- package/apps/web/public/ui.js +1910 -0
- package/apps/web/public/voice.js +345 -0
- package/apps/web/server.mjs +774 -0
- package/package.json +43 -4
- package/packages/cli/src/bom-pi.mjs +452 -0
- package/packages/cli/templates/blank.factory.json +7 -0
- package/packages/cli/templates/dev.factory.json +11 -0
- package/packages/cli/templates/docs.factory.json +12 -0
- package/packages/cli/templates/ops.factory.json +14 -0
- package/packages/cli/templates/triage.factory.json +10 -0
- package/packages/core/src/daemon.mjs +2047 -0
- package/packages/core/src/limit-gate.mjs +192 -0
- package/packages/core/src/rpc-client.mjs +66 -0
- package/systemd/README.md +11 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// Model providers: signing in and out, the models subagents may use, and the dialog a
|
|
2
|
+
// sign-in uses to ask for a code or a key.
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
4
|
+
import { api, attempt, closeSheet, closeSignIn, signInAnswered, store } from "../store.js";
|
|
5
|
+
import { html, Icon, Loader, Sheet } from "../ui.js";
|
|
6
|
+
import { RefreshModels } from "../model-refresh.js";
|
|
7
|
+
|
|
8
|
+
/** Models past this many get a box to find them by. */
|
|
9
|
+
const MODEL_SEARCH_AT = 8;
|
|
10
|
+
|
|
11
|
+
/** This tab's changes to the models for subagents, one after another: their answers come back in the order sent. */
|
|
12
|
+
let saving = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The owner's choice of the models Pi may start subagents and linked sessions on: every model, by provider, each
|
|
16
|
+
* checked or not on its own. None, the default: they run on their session's model, and Pi is offered no model to pick.
|
|
17
|
+
* Some: only those, their session's model too only when it is checked. A tap sends that one model's change, not the
|
|
18
|
+
* whole list, so quick taps and other tabs keep theirs.
|
|
19
|
+
* `names`: providers' names by id, once the sheet has them.
|
|
20
|
+
*/
|
|
21
|
+
function SubagentModels({ names }) {
|
|
22
|
+
const { models, server } = store.state;
|
|
23
|
+
const allowed = server?.subagentModels;
|
|
24
|
+
// What this tab asked for and has no answer to yet, by model: shown at once.
|
|
25
|
+
const [asked, setAsked] = useState({});
|
|
26
|
+
const [query, setQuery] = useState("");
|
|
27
|
+
|
|
28
|
+
// A server that does not keep the choice sends none.
|
|
29
|
+
if (!Array.isArray(allowed)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const specOf = (model) => `${model.provider}/${model.id}`;
|
|
34
|
+
// Allowed models that are not signed in now are listed too, so they can be seen and taken off.
|
|
35
|
+
const rows = [
|
|
36
|
+
...models.map((model) => ({ spec: specOf(model), model })),
|
|
37
|
+
...allowed
|
|
38
|
+
.filter((spec) => !models.some((model) => specOf(model) === spec))
|
|
39
|
+
.map((spec) => ({ spec, model: null })),
|
|
40
|
+
];
|
|
41
|
+
const isOn = (spec) => asked[spec] ?? allowed.includes(spec);
|
|
42
|
+
const chosen = rows.filter((row) => isOn(row.spec)).map((row) => row.model?.name ?? row.spec);
|
|
43
|
+
const summary =
|
|
44
|
+
chosen.length === 0
|
|
45
|
+
? "None allowed: Pi never picks a model."
|
|
46
|
+
: `${chosen.length} allowed: ${chosen.join(", ")}`;
|
|
47
|
+
const needle = query.trim().toLowerCase();
|
|
48
|
+
const groups = new Map();
|
|
49
|
+
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
const provider = row.spec.slice(0, row.spec.indexOf("/"));
|
|
52
|
+
const words = `${row.spec} ${row.model?.name ?? ""} ${names[provider] ?? ""}`;
|
|
53
|
+
|
|
54
|
+
if (needle === "" || words.toLowerCase().includes(needle)) {
|
|
55
|
+
groups.set(provider, [...(groups.get(provider) ?? []), row]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const toggle = (spec, on) => {
|
|
60
|
+
setAsked((was) => ({ ...was, [spec]: on }));
|
|
61
|
+
saving = saving.then(async () => {
|
|
62
|
+
const answer = await attempt(() =>
|
|
63
|
+
api("subagent-models", { model: spec, allowed: on }),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(answer?.subagentModels)) {
|
|
67
|
+
store.set((state) => ({
|
|
68
|
+
server: { ...state.server, subagentModels: answer.subagentModels },
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Unless a later tap asked otherwise, what the server says shows now.
|
|
73
|
+
setAsked((was) => {
|
|
74
|
+
if (was[spec] !== on) {
|
|
75
|
+
return was;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rest = { ...was };
|
|
79
|
+
|
|
80
|
+
delete rest[spec];
|
|
81
|
+
|
|
82
|
+
return rest;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return html`<div class="subagent-models">
|
|
88
|
+
<div class="label">Models for subagents</div>
|
|
89
|
+
<p class="muted small">
|
|
90
|
+
With none checked, subagents and linked sessions run on their session's model, and Pi cannot choose another. Check some, and they run only on those: on the session's model when it is checked, otherwise on a checked model that Pi chooses to suit the work. Pi cannot use any other.
|
|
91
|
+
</p>
|
|
92
|
+
<p class="small subagent-models-chosen" title=${summary}>${summary}</p>
|
|
93
|
+
${
|
|
94
|
+
(rows.length > MODEL_SEARCH_AT || query !== "") &&
|
|
95
|
+
html`<label class="search">
|
|
96
|
+
<${Icon} name="search" size=${16} />
|
|
97
|
+
<input
|
|
98
|
+
placeholder="Find a model"
|
|
99
|
+
aria-label="Find a model for subagents"
|
|
100
|
+
value=${query}
|
|
101
|
+
onInput=${(event) => setQuery(event.currentTarget.value)}
|
|
102
|
+
/>
|
|
103
|
+
</label>`
|
|
104
|
+
}
|
|
105
|
+
<div class="subagent-models-list" role="group" aria-label="Models for subagents">
|
|
106
|
+
${
|
|
107
|
+
groups.size === 0 &&
|
|
108
|
+
html`<p class="muted small">
|
|
109
|
+
${rows.length === 0 ? "No models: sign in to a provider below." : "No model matches."}
|
|
110
|
+
</p>`
|
|
111
|
+
}
|
|
112
|
+
${[...groups].map(
|
|
113
|
+
([provider, list]) => html`<div key=${provider}>
|
|
114
|
+
<div class="subagent-models-head">${names[provider] ?? provider}</div>
|
|
115
|
+
${list.map(
|
|
116
|
+
({ spec, model }) => html`<label class="subagent-model" key=${spec}>
|
|
117
|
+
<input
|
|
118
|
+
type="checkbox"
|
|
119
|
+
value=${spec}
|
|
120
|
+
checked=${isOn(spec)}
|
|
121
|
+
onChange=${(event) => toggle(spec, event.currentTarget.checked)}
|
|
122
|
+
/>
|
|
123
|
+
<span>
|
|
124
|
+
${model?.name ?? spec.slice(spec.indexOf("/") + 1)}
|
|
125
|
+
<span class="muted small">
|
|
126
|
+
${spec}
|
|
127
|
+
${
|
|
128
|
+
!model
|
|
129
|
+
? html` · <span class="warn">not signed in</span>`
|
|
130
|
+
: model.signIn &&
|
|
131
|
+
html` · <span class="warn">needs sign-in</span>`
|
|
132
|
+
}
|
|
133
|
+
</span>
|
|
134
|
+
</span>
|
|
135
|
+
</label>`,
|
|
136
|
+
)}
|
|
137
|
+
</div>`,
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** How long a run waits for a provider's limit to clear (`limit-gate.ts`), at most. */
|
|
144
|
+
const LIMIT_WAIT = [
|
|
145
|
+
[0, "Off"],
|
|
146
|
+
[1, "1 hour"],
|
|
147
|
+
[6, "6 hours"],
|
|
148
|
+
[12, "12 hours"],
|
|
149
|
+
[24, "24 hours"],
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/** The owner's choice of how long a run waits for a limit, rather than failing. Shorter limits wait less. */
|
|
153
|
+
function LimitWait() {
|
|
154
|
+
const hours = store.state.server?.limitWaitHours;
|
|
155
|
+
|
|
156
|
+
// A server that does not wait for limits sends none.
|
|
157
|
+
if (typeof hours !== "number") {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const change = (value) => attempt(() => api("settings", { limitWaitHours: value }));
|
|
162
|
+
|
|
163
|
+
return html`<div class="limit-wait">
|
|
164
|
+
<div class="small">Wait for a limit</div>
|
|
165
|
+
<p class="muted small">
|
|
166
|
+
When a provider's limit stops a run, it waits for the limit to clear, up to this long, and goes on by
|
|
167
|
+
itself. A limit that clears later makes it fail with the time.
|
|
168
|
+
</p>
|
|
169
|
+
<div class="segmented" role="group" aria-label="How long to wait for a limit">
|
|
170
|
+
${LIMIT_WAIT.map(
|
|
171
|
+
([value, label]) => html`<button
|
|
172
|
+
class=${hours === value ? "on" : ""}
|
|
173
|
+
aria-pressed=${hours === value}
|
|
174
|
+
onClick=${() => change(value)}
|
|
175
|
+
>
|
|
176
|
+
${label}
|
|
177
|
+
</button>`,
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function ProvidersSheet() {
|
|
184
|
+
const { me, auth } = store.state;
|
|
185
|
+
const [providers, setProviders] = useState(null);
|
|
186
|
+
const [query, setQuery] = useState("");
|
|
187
|
+
const loginButton = useRef(null);
|
|
188
|
+
const load = () => attempt(async () => setProviders(await api("providers")));
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
load();
|
|
192
|
+
}, [store.state.models]);
|
|
193
|
+
useLayoutEffect(() => {
|
|
194
|
+
if (!auth) {
|
|
195
|
+
loginButton.current?.focus();
|
|
196
|
+
loginButton.current = null;
|
|
197
|
+
}
|
|
198
|
+
}, [auth]);
|
|
199
|
+
const owner = me?.role === "owner";
|
|
200
|
+
|
|
201
|
+
const login = (provider, type, button) => {
|
|
202
|
+
loginButton.current = button;
|
|
203
|
+
|
|
204
|
+
return attempt(() => api(`providers/${encodeURIComponent(provider.id)}/login`, { type }));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const logout = (provider) =>
|
|
208
|
+
confirm(
|
|
209
|
+
`Log out of ${provider.name}? Pi uses the same sign-in, so it is signed out too.`,
|
|
210
|
+
) &&
|
|
211
|
+
attempt(async () => {
|
|
212
|
+
await api(`providers/${encodeURIComponent(provider.id)}/logout`, {});
|
|
213
|
+
load();
|
|
214
|
+
});
|
|
215
|
+
const needle = query.trim().toLowerCase();
|
|
216
|
+
const list = (providers ?? [])
|
|
217
|
+
.filter(
|
|
218
|
+
(provider) =>
|
|
219
|
+
needle === "" || `${provider.id} ${provider.name}`.toLowerCase().includes(needle),
|
|
220
|
+
)
|
|
221
|
+
.sort(
|
|
222
|
+
(a, b) =>
|
|
223
|
+
Number(Boolean(b.needsSignIn)) - Number(Boolean(a.needsSignIn)) ||
|
|
224
|
+
Number(b.configured) - Number(a.configured) ||
|
|
225
|
+
a.name.localeCompare(b.name),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Signed-out providers wait at the end, and an expired sign-in comes first: that is the one to act on.
|
|
229
|
+
const groups = [
|
|
230
|
+
["Needs sign-in", list.filter((provider) => provider.needsSignIn)],
|
|
231
|
+
["Signed in", list.filter((provider) => !provider.needsSignIn && provider.configured)],
|
|
232
|
+
[
|
|
233
|
+
"Not configured",
|
|
234
|
+
list.filter((provider) => !provider.needsSignIn && !provider.configured),
|
|
235
|
+
],
|
|
236
|
+
].filter(([, each]) => each.length > 0);
|
|
237
|
+
|
|
238
|
+
const row = (provider) => html`<div class="provider" key=${provider.id}>
|
|
239
|
+
<div class="provider-info">
|
|
240
|
+
<div class="provider-name">
|
|
241
|
+
${provider.name}
|
|
242
|
+
${
|
|
243
|
+
provider.needsSignIn
|
|
244
|
+
? html` <span class="warn provider-needs">needs sign-in</span>`
|
|
245
|
+
: provider.configured && html` <span class="ok">✓</span>`
|
|
246
|
+
}
|
|
247
|
+
</div>
|
|
248
|
+
<div class="muted small">
|
|
249
|
+
${provider.needsSignIn ? "the sign-in expired and could not be renewed: sign in again. Until then, sessions on its models fail, and PiP and subagents on other models do not use them" : provider.configured ? `signed in${provider.label ? ` · ${provider.label}` : provider.source ? ` · ${provider.source}` : ""}` : "not configured"} · ${provider.models} models
|
|
250
|
+
</div>
|
|
251
|
+
${
|
|
252
|
+
provider.needsSignIn &&
|
|
253
|
+
html`<div class="muted small mono provider-why">${provider.needsSignIn}</div>`
|
|
254
|
+
}
|
|
255
|
+
</div>
|
|
256
|
+
${
|
|
257
|
+
owner &&
|
|
258
|
+
html`<div class="provider-actions">
|
|
259
|
+
${
|
|
260
|
+
provider.oauth &&
|
|
261
|
+
html`<button
|
|
262
|
+
class="button small"
|
|
263
|
+
onClick=${(event) => login(provider, "oauth", event.currentTarget)}
|
|
264
|
+
>
|
|
265
|
+
${provider.oauth}
|
|
266
|
+
</button>`
|
|
267
|
+
}
|
|
268
|
+
${
|
|
269
|
+
provider.apiKey &&
|
|
270
|
+
html`<button
|
|
271
|
+
class="button small"
|
|
272
|
+
onClick=${(event) => login(provider, "api_key", event.currentTarget)}
|
|
273
|
+
>
|
|
274
|
+
API key
|
|
275
|
+
</button>`
|
|
276
|
+
}
|
|
277
|
+
${
|
|
278
|
+
provider.configured &&
|
|
279
|
+
provider.source === "stored" &&
|
|
280
|
+
html`<button class="button small ghost" onClick=${() => logout(provider)}>
|
|
281
|
+
Log out
|
|
282
|
+
</button>`
|
|
283
|
+
}
|
|
284
|
+
</div>`
|
|
285
|
+
}
|
|
286
|
+
</div>`;
|
|
287
|
+
|
|
288
|
+
return html`<${Sheet}
|
|
289
|
+
title="Providers"
|
|
290
|
+
onClose=${closeSheet}
|
|
291
|
+
className=${`providers-sheet ${owner ? "with-side" : ""}`}
|
|
292
|
+
>
|
|
293
|
+
<p class="muted small">
|
|
294
|
+
Pi Pocket shares Pi's sign-ins (<span class="mono">~/.pi/agent/auth.json</span>). ${owner ? "" : "Only the owner can change them."}
|
|
295
|
+
</p>
|
|
296
|
+
<div class="providers-layout">
|
|
297
|
+
${
|
|
298
|
+
owner &&
|
|
299
|
+
html`<aside class="providers-side" aria-label="Models">
|
|
300
|
+
<${RefreshModels} />
|
|
301
|
+
<${LimitWait} />
|
|
302
|
+
<${SubagentModels}
|
|
303
|
+
names=${Object.fromEntries(
|
|
304
|
+
(providers ?? []).map((each) => [each.id, each.name]),
|
|
305
|
+
)}
|
|
306
|
+
/>
|
|
307
|
+
</aside>`
|
|
308
|
+
}
|
|
309
|
+
<div class="providers-main">
|
|
310
|
+
${store.state.auth && html`<${AuthDialog} inline=${true} />`}
|
|
311
|
+
<label class="search">
|
|
312
|
+
<${Icon} name="search" size=${16} />
|
|
313
|
+
<input
|
|
314
|
+
placeholder="Search providers"
|
|
315
|
+
value=${query}
|
|
316
|
+
onInput=${(event) => setQuery(event.currentTarget.value)}
|
|
317
|
+
/>
|
|
318
|
+
</label>
|
|
319
|
+
${providers === null && html`<${Loader} label="Loading providers" />`}
|
|
320
|
+
${
|
|
321
|
+
providers !== null &&
|
|
322
|
+
list.length === 0 &&
|
|
323
|
+
needle !== "" &&
|
|
324
|
+
html`<div class="launcher-empty" role="status">
|
|
325
|
+
<strong>No providers match “${query.trim()}”.</strong>
|
|
326
|
+
<button class="button" onClick=${() => setQuery("")}>Clear search</button>
|
|
327
|
+
</div>`
|
|
328
|
+
}
|
|
329
|
+
${groups.map(
|
|
330
|
+
([title, each]) => html`<section class="provider-group" key=${title}>
|
|
331
|
+
<h3 class="provider-group-head">
|
|
332
|
+
${title} <span class="muted">${each.length}</span>
|
|
333
|
+
</h3>
|
|
334
|
+
<div class="sheet-list">${each.map(row)}</div>
|
|
335
|
+
</section>`,
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<//>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** A provider signs in in place; sign-ins requested elsewhere still get their own dialog. */
|
|
343
|
+
function AuthFrame({ title, onClose, inline, focusKey, children }) {
|
|
344
|
+
const ref = useRef(null);
|
|
345
|
+
const close = useRef(onClose);
|
|
346
|
+
|
|
347
|
+
close.current = onClose;
|
|
348
|
+
|
|
349
|
+
// The first question may arrive after the card mounts. Focus it then, not on every redraw while typing.
|
|
350
|
+
useLayoutEffect(() => {
|
|
351
|
+
if (inline) {
|
|
352
|
+
const card = ref.current?.querySelector(".auth-card");
|
|
353
|
+
const control =
|
|
354
|
+
card?.querySelector(".field input:not(:disabled), .field button:not(:disabled)") ??
|
|
355
|
+
card?.querySelector("a[href], button:not(:disabled)");
|
|
356
|
+
|
|
357
|
+
(control ?? ref.current)?.focus({ preventScroll: true });
|
|
358
|
+
ref.current?.scrollIntoView({ block: "nearest" });
|
|
359
|
+
}
|
|
360
|
+
}, [inline, focusKey]);
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (!inline) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const escape = (event) => {
|
|
367
|
+
if (event.key === "Escape" && !event.isComposing && event.keyCode !== 229) {
|
|
368
|
+
event.stopImmediatePropagation();
|
|
369
|
+
close.current();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
addEventListener("keydown", escape, true);
|
|
374
|
+
|
|
375
|
+
return () => removeEventListener("keydown", escape, true);
|
|
376
|
+
}, [inline]);
|
|
377
|
+
|
|
378
|
+
const content = html`<div class="auth-card">${children}</div>`;
|
|
379
|
+
|
|
380
|
+
return inline
|
|
381
|
+
? html`<section class="auth-inline" aria-label=${title} tabindex="-1" ref=${ref}>
|
|
382
|
+
<div class="auth-inline-head">
|
|
383
|
+
<strong>${title}</strong>
|
|
384
|
+
<button class="icon-button" aria-label="Close" onClick=${onClose}>
|
|
385
|
+
<${Icon} name="close" size=${16} />
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
${content}
|
|
389
|
+
</section>`
|
|
390
|
+
: html`<${Sheet} title=${title} onClose=${onClose}>${content}<//>`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function AuthDialog({ inline = false }) {
|
|
394
|
+
const { auth } = store.state;
|
|
395
|
+
const [value, setValue] = useState("");
|
|
396
|
+
|
|
397
|
+
// Each question starts empty, also one that came before the reply to the answer before it.
|
|
398
|
+
useEffect(() => setValue(""), [auth?.flowId, auth?.prompt?.id]);
|
|
399
|
+
|
|
400
|
+
if (!auth) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const answer = (payload) => {
|
|
405
|
+
const { flowId, prompt: asked } = auth;
|
|
406
|
+
|
|
407
|
+
if (asked.type === "secret" && payload.value?.trim() === "") {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return attempt(async () => {
|
|
412
|
+
await api(`auth/${flowId}/${asked.id}`, payload);
|
|
413
|
+
signInAnswered(flowId, asked.id);
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const close = () => {
|
|
418
|
+
// Stop the sign-in on the server too, also while it waits without a question (an OAuth callback holds a port).
|
|
419
|
+
if (!auth.done) {
|
|
420
|
+
api(`auth/${encodeURIComponent(auth.flowId)}/cancel`, {}).catch(() => {});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
closeSignIn(auth.flowId);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const prompt = auth.prompt;
|
|
427
|
+
|
|
428
|
+
return html`<${AuthFrame}
|
|
429
|
+
title=${`Sign in: ${auth.providerName}`}
|
|
430
|
+
onClose=${close}
|
|
431
|
+
inline=${inline}
|
|
432
|
+
focusKey=${`${auth.flowId}:${prompt?.id ?? auth.events.length}`}
|
|
433
|
+
>
|
|
434
|
+
${(auth.done ? [] : auth.events).map((event) => {
|
|
435
|
+
if (event.type === "auth_url") {
|
|
436
|
+
return html`<div class="auth-event">
|
|
437
|
+
<a class="button primary wide" href=${event.url} target="_blank" rel="noopener">
|
|
438
|
+
Open the sign-in page
|
|
439
|
+
</a>
|
|
440
|
+
${event.instructions && html`<p class="muted small">${event.instructions}</p>`}
|
|
441
|
+
</div>`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (event.type === "device_code") {
|
|
445
|
+
return html`<div class="auth-event">
|
|
446
|
+
<p>
|
|
447
|
+
Enter this code at <a href=${event.verificationUri} target="_blank" rel="noopener">${event.verificationUri}</a>
|
|
448
|
+
</p>
|
|
449
|
+
<div class="code-big">${event.userCode}</div>
|
|
450
|
+
</div>`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return html`<p class="muted small">
|
|
454
|
+
${event.message}
|
|
455
|
+
${event.links?.map(
|
|
456
|
+
(link) => html` <a href=${link.url} target="_blank" rel="noopener">
|
|
457
|
+
${link.label ?? link.url}
|
|
458
|
+
</a>`,
|
|
459
|
+
)}
|
|
460
|
+
</p>`;
|
|
461
|
+
})}
|
|
462
|
+
${
|
|
463
|
+
prompt &&
|
|
464
|
+
(prompt.type === "select"
|
|
465
|
+
? html`<div class="field">
|
|
466
|
+
<div class="label">${prompt.message}</div>
|
|
467
|
+
${prompt.options.map(
|
|
468
|
+
(option) =>
|
|
469
|
+
html`<button
|
|
470
|
+
class="list-item"
|
|
471
|
+
onClick=${() => answer({ value: option.id })}
|
|
472
|
+
>
|
|
473
|
+
<span>${option.label}</span>
|
|
474
|
+
<span class="muted small">${option.description ?? ""}</span>
|
|
475
|
+
</button>`,
|
|
476
|
+
)}
|
|
477
|
+
</div>`
|
|
478
|
+
: html`<div class="field">
|
|
479
|
+
<div class="label">${prompt.message}</div>
|
|
480
|
+
<div class="row">
|
|
481
|
+
<input
|
|
482
|
+
type=${prompt.type === "secret" ? "password" : "text"}
|
|
483
|
+
aria-label=${prompt.message}
|
|
484
|
+
autocomplete="off"
|
|
485
|
+
placeholder=${prompt.placeholder ?? ""}
|
|
486
|
+
value=${value}
|
|
487
|
+
onInput=${(event) => setValue(event.currentTarget.value)}
|
|
488
|
+
onKeyDown=${(event) => event.key === "Enter" && answer({ value })}
|
|
489
|
+
/>
|
|
490
|
+
<button
|
|
491
|
+
class="button primary"
|
|
492
|
+
disabled=${prompt.type === "secret" && value.trim() === ""}
|
|
493
|
+
onClick=${() => answer({ value })}
|
|
494
|
+
>
|
|
495
|
+
Continue
|
|
496
|
+
</button>
|
|
497
|
+
</div>
|
|
498
|
+
</div>`)
|
|
499
|
+
}
|
|
500
|
+
${!prompt && !auth.done && html`<${Loader} label="Waiting for the provider" />`}
|
|
501
|
+
${
|
|
502
|
+
auth.done &&
|
|
503
|
+
!auth.done.ok &&
|
|
504
|
+
html`<div class="error-box">
|
|
505
|
+
<p>${auth.done.details ? auth.done.error : "Sign-in did not finish. Try again."}</p>
|
|
506
|
+
<details>
|
|
507
|
+
<summary>Details</summary>
|
|
508
|
+
<p>${auth.done.details ?? auth.done.error}</p>
|
|
509
|
+
</details>
|
|
510
|
+
<button
|
|
511
|
+
class="button"
|
|
512
|
+
onClick=${() =>
|
|
513
|
+
attempt(async () => {
|
|
514
|
+
await api(`providers/${encodeURIComponent(auth.providerId)}/login`, {
|
|
515
|
+
type: auth.done.loginType ?? "oauth",
|
|
516
|
+
});
|
|
517
|
+
closeSignIn(auth.flowId);
|
|
518
|
+
})}
|
|
519
|
+
>
|
|
520
|
+
Start again
|
|
521
|
+
</button>
|
|
522
|
+
</div>`
|
|
523
|
+
}
|
|
524
|
+
<//>`;
|
|
525
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Running now: everything Pi is doing, across sessions.
|
|
2
|
+
import { useEffect, useState } from "preact/hooks";
|
|
3
|
+
import { folderColor } from "../avatar.js";
|
|
4
|
+
import { api, attempt, canSteer, closeSheet, navigate, notify, store } from "../store.js";
|
|
5
|
+
import { html, Loader, Sheet, Spinner } from "../ui.js";
|
|
6
|
+
|
|
7
|
+
/** How often the Running now sheet asks again while it is open. */
|
|
8
|
+
const RUNNING_EVERY_MS = 2000;
|
|
9
|
+
|
|
10
|
+
/** Everything at work in the sessions you can see: open one, stop a run, or cancel a scheduled message. */
|
|
11
|
+
export function RunningSheet() {
|
|
12
|
+
const [sessions, setSessions] = useState(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let open = true;
|
|
16
|
+
const load = () =>
|
|
17
|
+
api("running").then(
|
|
18
|
+
(list) => open && setSessions(list),
|
|
19
|
+
(error) => open && notify("error", error.message),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
load();
|
|
23
|
+
const timer = setInterval(load, RUNNING_EVERY_MS);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
open = false;
|
|
27
|
+
clearInterval(timer);
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
const steer = canSteer();
|
|
31
|
+
|
|
32
|
+
const go = (id) => {
|
|
33
|
+
closeSheet();
|
|
34
|
+
navigate(id);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const stopCommand = (task) => {
|
|
38
|
+
const path = {
|
|
39
|
+
"pocket.shell": "shell",
|
|
40
|
+
"pocket.background": "background",
|
|
41
|
+
"pocket.extension-command": "extension-commands",
|
|
42
|
+
}[task.kind];
|
|
43
|
+
|
|
44
|
+
return attempt(() => api(`c/${task.conversationId}/${path}/${task.id}/stop`, {}));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return html`<${Sheet} title="Running now" onClose=${closeSheet}>
|
|
48
|
+
${sessions === null && html`<${Loader} label="Looking" />`}
|
|
49
|
+
${sessions?.length === 0 && html`<p class="muted">Nothing is running.</p>`}
|
|
50
|
+
${sessions?.map(
|
|
51
|
+
(session) => html`<div class="running" key=${session.id}>
|
|
52
|
+
<div class="running-head">
|
|
53
|
+
<span
|
|
54
|
+
class="folder-mark"
|
|
55
|
+
style=${`--folder:${folderColor(store.state.sessions.find((entry) => entry.id === session.id)?.cwd)}`}
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
></span>
|
|
58
|
+
<button class="link grow" onClick=${() => go(session.id)}>
|
|
59
|
+
${session.title}
|
|
60
|
+
</button>
|
|
61
|
+
${
|
|
62
|
+
session.approvals > 0 &&
|
|
63
|
+
html`<span class="warn small">
|
|
64
|
+
${session.approvals} waiting for approval
|
|
65
|
+
</span>`
|
|
66
|
+
}
|
|
67
|
+
${
|
|
68
|
+
steer &&
|
|
69
|
+
session.busy &&
|
|
70
|
+
html`<button
|
|
71
|
+
class="button small"
|
|
72
|
+
onClick=${() => attempt(() => api(`c/${session.id}/abort`, {}))}
|
|
73
|
+
>
|
|
74
|
+
Stop
|
|
75
|
+
</button>`
|
|
76
|
+
}
|
|
77
|
+
</div>
|
|
78
|
+
${session.tasks.map(
|
|
79
|
+
(task) => html`<div class="running-task" key=${task.id}>
|
|
80
|
+
${
|
|
81
|
+
task.status === "running"
|
|
82
|
+
? html`<${Spinner} />`
|
|
83
|
+
: html`<span class="muted">·</span>`
|
|
84
|
+
}
|
|
85
|
+
<span class="grow">
|
|
86
|
+
${task.subagent && html`<span class="mono">${task.subagent}</span>: `}
|
|
87
|
+
${task.label}
|
|
88
|
+
</span>
|
|
89
|
+
<span class="muted small mono">${task.status}</span>
|
|
90
|
+
${
|
|
91
|
+
steer &&
|
|
92
|
+
task.scheduleId &&
|
|
93
|
+
html`<button
|
|
94
|
+
class="link small"
|
|
95
|
+
onClick=${() => attempt(() => api(`c/${task.conversationId}/schedules/${encodeURIComponent(task.scheduleId)}/cancel`, {}))}
|
|
96
|
+
>
|
|
97
|
+
Cancel
|
|
98
|
+
</button>`
|
|
99
|
+
}
|
|
100
|
+
${
|
|
101
|
+
steer &&
|
|
102
|
+
task.subagent &&
|
|
103
|
+
task.kind === "pi.generation" &&
|
|
104
|
+
html`<button
|
|
105
|
+
class="link small"
|
|
106
|
+
onClick=${() => attempt(() => api(`c/${task.conversationId}/abort`, {}))}
|
|
107
|
+
>
|
|
108
|
+
Stop
|
|
109
|
+
</button>`
|
|
110
|
+
}
|
|
111
|
+
${
|
|
112
|
+
steer &&
|
|
113
|
+
[
|
|
114
|
+
"pocket.shell",
|
|
115
|
+
"pocket.background",
|
|
116
|
+
"pocket.extension-command",
|
|
117
|
+
].includes(task.kind) &&
|
|
118
|
+
html`<button
|
|
119
|
+
class="link small"
|
|
120
|
+
onClick=${() => stopCommand(task)}
|
|
121
|
+
>
|
|
122
|
+
Stop
|
|
123
|
+
</button>`
|
|
124
|
+
}
|
|
125
|
+
</div>`,
|
|
126
|
+
)}
|
|
127
|
+
</div>`,
|
|
128
|
+
)}
|
|
129
|
+
<//>`;
|
|
130
|
+
}
|