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,580 @@
|
|
|
1
|
+
// The menu: what can be done in this session and in the app.
|
|
2
|
+
import { useLayoutEffect, useRef } from "preact/hooks";
|
|
3
|
+
import { browserAvailable, displayUrl, setBrowserOpen } from "../browser.js";
|
|
4
|
+
import { chatUnread } from "../chat.js";
|
|
5
|
+
import { schedulesAvailable, triggersOn, watchAvailable, watchPr } from "../commands.js";
|
|
6
|
+
import { extensionMenuItems, safeMode } from "../extension-ui.js";
|
|
7
|
+
import { useChanges } from "../diff.js";
|
|
8
|
+
import { filesAvailable, setFilesOpen } from "../files-panel.js";
|
|
9
|
+
import { peeksOn, togglePeeks } from "../peeks.js";
|
|
10
|
+
import { modelRefreshAvailable, refreshModels } from "../model-refresh.js";
|
|
11
|
+
import { openPip, pipAvailable, pipUnread } from "../pip.js";
|
|
12
|
+
import { setArchived } from "../sessions.js";
|
|
13
|
+
import { openSubagent } from "../subagents.js";
|
|
14
|
+
import {
|
|
15
|
+
actions,
|
|
16
|
+
api,
|
|
17
|
+
attempt,
|
|
18
|
+
canSteer,
|
|
19
|
+
canToggleTurns,
|
|
20
|
+
closeSheet,
|
|
21
|
+
collab,
|
|
22
|
+
navigate,
|
|
23
|
+
notify,
|
|
24
|
+
openSheet,
|
|
25
|
+
scoped,
|
|
26
|
+
setUnread,
|
|
27
|
+
signOut,
|
|
28
|
+
store,
|
|
29
|
+
} from "../store.js";
|
|
30
|
+
import { defineItem, slotItems } from "../slots.js";
|
|
31
|
+
import { isPinned, lookLabel, togglePin } from "../theme.js";
|
|
32
|
+
import { currentTrust, trustAvailable } from "../trust.js";
|
|
33
|
+
import { branchAvailable } from "./branch.js";
|
|
34
|
+
import { memoryAvailable, memoryHint, useMemory } from "./memory.js";
|
|
35
|
+
import { defaultLabel, defaultsKept } from "./model.js";
|
|
36
|
+
import { copyText, html, Icon, item, PlaceTile, Sheet, shortPath } from "../ui.js";
|
|
37
|
+
|
|
38
|
+
// Key hints are for people with a keyboard: a touch screen has none.
|
|
39
|
+
const touch = matchMedia("(pointer: coarse)").matches;
|
|
40
|
+
|
|
41
|
+
/** The Extensions row's hint: that this tab loads none of their browser parts (safe mode), and Lancet Guard. */
|
|
42
|
+
function extensionsHint() {
|
|
43
|
+
const { guard } = store.state;
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
safeMode() && "browser parts off here",
|
|
47
|
+
guard?.enabled ? "Lancet Guard on" : guard?.available ? "Lancet Guard off" : "",
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" · ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The session's places as big tiles at the top of the menu: Files, Changes, the browser, artifacts, the chat, find,
|
|
55
|
+
* the branch, and peek tiles. The top bar keeps only the buttons with something to say; these are always here, a
|
|
56
|
+
* thumb's width each.
|
|
57
|
+
*/
|
|
58
|
+
function Places() {
|
|
59
|
+
if (!store.state.view.conversation) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return filesAvailable() ? html`<${PlacesWithChanges} />` : html`<${PlacesGrid} />`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Only people who can open Files request its shared changes data, just as the top bar does. */
|
|
67
|
+
function PlacesWithChanges() {
|
|
68
|
+
const { changes } = useChanges();
|
|
69
|
+
|
|
70
|
+
return html`<${PlacesGrid} changes=${changes} />`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The app's own places, items of the `places` slot (slots.js), which a person can hide and reorder in Appearance, with
|
|
75
|
+
* extensions' panels after them. Each gets `{ changes }`: the folder's changes, for the count on Changes.
|
|
76
|
+
*/
|
|
77
|
+
const PLACES = [
|
|
78
|
+
{
|
|
79
|
+
id: "core:files",
|
|
80
|
+
label: "Files",
|
|
81
|
+
place: () =>
|
|
82
|
+
filesAvailable() && {
|
|
83
|
+
icon: "folder",
|
|
84
|
+
on: store.state.filesOpen && store.state.filesTab === "files",
|
|
85
|
+
run: () => setFilesOpen(true, "files"),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "core:changes",
|
|
90
|
+
label: "Changes",
|
|
91
|
+
place({ changes }) {
|
|
92
|
+
// Match the top bar: uncommitted files in a repository, Pi's edits elsewhere, plus any unlisted files.
|
|
93
|
+
const count = changes
|
|
94
|
+
? changes.files.length +
|
|
95
|
+
(changes.repo ? 0 : changes.piOnly.length) +
|
|
96
|
+
(changes.more ?? 0)
|
|
97
|
+
: 0;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
filesAvailable() && {
|
|
101
|
+
icon: "diff",
|
|
102
|
+
count,
|
|
103
|
+
counted: count === 1 ? "changed file" : "changed files",
|
|
104
|
+
on: store.state.filesOpen && store.state.filesTab === "changes",
|
|
105
|
+
run: () => setFilesOpen(true, "changes"),
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "core:browser",
|
|
112
|
+
label: "Browser",
|
|
113
|
+
place() {
|
|
114
|
+
const { browser, browserOpen } = store.state;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
browserAvailable() && {
|
|
118
|
+
icon: "globe",
|
|
119
|
+
on: browserOpen,
|
|
120
|
+
dot: browser?.open && browser.url !== "" && browser.url !== "about:blank",
|
|
121
|
+
run: () => {
|
|
122
|
+
setBrowserOpen(true);
|
|
123
|
+
closeSheet();
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "core:artifacts",
|
|
131
|
+
label: "Artifacts",
|
|
132
|
+
place: () => ({
|
|
133
|
+
icon: "artifact",
|
|
134
|
+
count: store.state.view.artifacts.length,
|
|
135
|
+
run: () => openSheet({ type: "artifacts" }),
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "core:chat",
|
|
140
|
+
label: "Chat",
|
|
141
|
+
place: () =>
|
|
142
|
+
collab() && {
|
|
143
|
+
icon: "chat",
|
|
144
|
+
count: chatUnread(),
|
|
145
|
+
counted: "unread",
|
|
146
|
+
run: () => openSheet({ type: "chat" }),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "core:find",
|
|
151
|
+
label: "Find",
|
|
152
|
+
place: () => ({ icon: "search", run: () => openSheet({ type: "find" }) }),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
// Ctrl K's launcher, for touch screens that have no keyboard to open it.
|
|
156
|
+
id: "core:launcher",
|
|
157
|
+
label: "Launcher",
|
|
158
|
+
place: () =>
|
|
159
|
+
touch && {
|
|
160
|
+
icon: "command",
|
|
161
|
+
run: () => {
|
|
162
|
+
closeSheet();
|
|
163
|
+
store.set({ launcher: true });
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "core:branch",
|
|
169
|
+
label: "Branch",
|
|
170
|
+
place: () =>
|
|
171
|
+
branchAvailable() && { icon: "fork", run: () => openSheet({ type: "branch" }) },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "core:pip",
|
|
175
|
+
label: "PiP",
|
|
176
|
+
place: () =>
|
|
177
|
+
pipAvailable() && {
|
|
178
|
+
icon: "pip",
|
|
179
|
+
dot: pipUnread(),
|
|
180
|
+
dotLabel: "something new",
|
|
181
|
+
run: () => {
|
|
182
|
+
openPip();
|
|
183
|
+
closeSheet();
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "core:peeks",
|
|
189
|
+
label: "Peeks",
|
|
190
|
+
place: () =>
|
|
191
|
+
store.state.server?.peeks === true && {
|
|
192
|
+
icon: "tiles",
|
|
193
|
+
on: peeksOn(),
|
|
194
|
+
run: () => {
|
|
195
|
+
togglePeeks();
|
|
196
|
+
closeSheet();
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
PLACES.forEach(({ id, label, place }, index) =>
|
|
203
|
+
defineItem("places", {
|
|
204
|
+
id,
|
|
205
|
+
label,
|
|
206
|
+
order: (index + 1) * 100,
|
|
207
|
+
render(context) {
|
|
208
|
+
const shown = place(context);
|
|
209
|
+
|
|
210
|
+
// As before slots: `data-place="files"`, which tests and themes may select.
|
|
211
|
+
return (
|
|
212
|
+
shown &&
|
|
213
|
+
html`<${PlaceTile} place=${{ id: id.replace(/^core:/, ""), label, ...shown }} />`
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
function PlacesGrid({ changes = null }) {
|
|
220
|
+
return html`<nav class="places" aria-label="Places">
|
|
221
|
+
${slotItems("places", { changes }).map(({ node }) => node)}
|
|
222
|
+
</nav>`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The places alone, in a short sheet that stays near the bottom edge: what the button beside the message box and a
|
|
227
|
+
* swipe up from it open on a phone, a thumb's reach away. The rest of the menu is a row below.
|
|
228
|
+
*/
|
|
229
|
+
export function PlacesSheet() {
|
|
230
|
+
const conversation = store.state.view.conversation;
|
|
231
|
+
|
|
232
|
+
return html`<${Sheet}
|
|
233
|
+
title=${conversation?.title ?? "Open"}
|
|
234
|
+
onClose=${closeSheet}
|
|
235
|
+
className="places-sheet"
|
|
236
|
+
>
|
|
237
|
+
<${Places} />
|
|
238
|
+
${item("Session menu", () => openSheet({ type: "menu", fromPlaces: true }), "rename, context, settings")}
|
|
239
|
+
<//>`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Keep Sheet's Esc, drag, and focus handling; the sheet trail already owns its back-stack layer. */
|
|
243
|
+
function MenuFrame({ title, children }) {
|
|
244
|
+
const host = useRef(null);
|
|
245
|
+
|
|
246
|
+
useLayoutEffect(() => {
|
|
247
|
+
const element = host.current;
|
|
248
|
+
const button = document.querySelector('.topbar [aria-label="Menu"]');
|
|
249
|
+
const active = document.activeElement;
|
|
250
|
+
const opener = active === document.body ? button : active;
|
|
251
|
+
|
|
252
|
+
const place = () => {
|
|
253
|
+
const rect = button?.getBoundingClientRect();
|
|
254
|
+
const top = (rect?.bottom ?? 58) + 6;
|
|
255
|
+
const bottom =
|
|
256
|
+
(visualViewport?.offsetTop ?? 0) + (visualViewport?.height ?? innerHeight);
|
|
257
|
+
const left = Math.max(
|
|
258
|
+
8,
|
|
259
|
+
Math.min((rect?.right ?? innerWidth - 8) - 360, innerWidth - 368),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
element.style.setProperty("--menu-left", `${left}px`);
|
|
263
|
+
element.style.setProperty("--menu-top", `${top}px`);
|
|
264
|
+
element.style.setProperty("--menu-height", `${Math.max(100, bottom - top - 8)}px`);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const body = element.querySelector(".sheet-body");
|
|
268
|
+
|
|
269
|
+
if (menuScroll !== null) {
|
|
270
|
+
body.scrollTop = menuScroll;
|
|
271
|
+
menuScroll = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
place();
|
|
275
|
+
addEventListener("resize", place);
|
|
276
|
+
visualViewport?.addEventListener("resize", place);
|
|
277
|
+
|
|
278
|
+
return () => {
|
|
279
|
+
removeEventListener("resize", place);
|
|
280
|
+
visualViewport?.removeEventListener("resize", place);
|
|
281
|
+
|
|
282
|
+
// Another sheet open now: the menu is going back under it, not closing.
|
|
283
|
+
if (store.state.sheet) {
|
|
284
|
+
menuScroll = body.scrollTop;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const focused = document.activeElement;
|
|
288
|
+
const back = opener?.isConnected ? opener : button;
|
|
289
|
+
// As sheets.js does: not behind a sheet the menu opened, and not into a text field on a touch screen,
|
|
290
|
+
// where its keyboard would pop up.
|
|
291
|
+
const field =
|
|
292
|
+
matchMedia("(pointer: coarse)").matches &&
|
|
293
|
+
back?.matches("input, textarea, [contenteditable]");
|
|
294
|
+
|
|
295
|
+
if (
|
|
296
|
+
!store.state.sheet &&
|
|
297
|
+
!field &&
|
|
298
|
+
(focused === document.body || element.contains(focused)) &&
|
|
299
|
+
back?.isConnected
|
|
300
|
+
) {
|
|
301
|
+
back.focus({ preventScroll: true });
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
const onKey = (event) => {
|
|
307
|
+
if (
|
|
308
|
+
event.defaultPrevented ||
|
|
309
|
+
event.isComposing ||
|
|
310
|
+
event.keyCode === 229 ||
|
|
311
|
+
event.altKey ||
|
|
312
|
+
event.ctrlKey ||
|
|
313
|
+
event.metaKey
|
|
314
|
+
) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const items = [...host.current.querySelectorAll(".place, .list-item")];
|
|
319
|
+
const active = document.activeElement;
|
|
320
|
+
const index = items.indexOf(active);
|
|
321
|
+
let target;
|
|
322
|
+
|
|
323
|
+
if (event.key === "Tab") {
|
|
324
|
+
// Close stays reachable with Tab, but is not part of the menu's item navigation.
|
|
325
|
+
const controls = [...items, ...host.current.querySelectorAll(".sheet-head button")];
|
|
326
|
+
const at = controls.indexOf(active);
|
|
327
|
+
const next =
|
|
328
|
+
at < 0
|
|
329
|
+
? event.shiftKey
|
|
330
|
+
? controls.length - 1
|
|
331
|
+
: 0
|
|
332
|
+
: (at + (event.shiftKey ? -1 : 1) + controls.length) % controls.length;
|
|
333
|
+
|
|
334
|
+
target = controls[next];
|
|
335
|
+
} else if (event.key === "Home") {
|
|
336
|
+
target = items[0];
|
|
337
|
+
} else if (event.key === "End") {
|
|
338
|
+
target = items.at(-1);
|
|
339
|
+
} else if (["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(event.key)) {
|
|
340
|
+
const down = event.key === "ArrowDown" || event.key === "ArrowRight";
|
|
341
|
+
const vertical = event.key === "ArrowDown" || event.key === "ArrowUp";
|
|
342
|
+
const tiles = host.current.querySelectorAll(".place").length;
|
|
343
|
+
const step = vertical && index >= 0 && index < tiles ? 3 : 1;
|
|
344
|
+
let next = index < 0 ? (down ? 0 : items.length - 1) : index + (down ? step : -step);
|
|
345
|
+
|
|
346
|
+
// Down from the grid's last row enters the first text row; Up from its first row wraps to the last item.
|
|
347
|
+
if (vertical && index >= 0 && index < tiles) {
|
|
348
|
+
next = next < 0 ? items.length - 1 : Math.min(next, tiles);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
target = items[(next + items.length) % items.length];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (target) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
target.focus();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return html`<div class="nav-menu-host" data-part="menu" ref=${host} onKeyDown=${onKey}>
|
|
361
|
+
<${Sheet} title=${title} onClose=${closeSheet} className="nav-menu">
|
|
362
|
+
${children}
|
|
363
|
+
<//>
|
|
364
|
+
</div>`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The menu's scroll, kept while a sheet it opened shows, so back finds the menu where it was left. */
|
|
368
|
+
let menuScroll = null;
|
|
369
|
+
|
|
370
|
+
store.subscribe(({ sheet }) => {
|
|
371
|
+
if (!sheet) {
|
|
372
|
+
menuScroll = null;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/** The project's trust in a word, once the server said. */
|
|
377
|
+
function trustHint() {
|
|
378
|
+
const info = currentTrust();
|
|
379
|
+
|
|
380
|
+
if (!info) {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (info.ask) {
|
|
385
|
+
return "not decided";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// No decision, and nothing waiting for one: there is nothing to say.
|
|
389
|
+
if (info.saved === null && info.defaultProjectTrust === "ask") {
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return info.trusted ? "trusted" : "not trusted";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The menu; `places` is false when it opens from the places sheet, which just showed the tiles. */
|
|
397
|
+
export function MenuSheet({ places = true }) {
|
|
398
|
+
const { view, me, server } = store.state;
|
|
399
|
+
const conversation = view.conversation;
|
|
400
|
+
const steer = canSteer();
|
|
401
|
+
const session = conversation?.kind === "session";
|
|
402
|
+
const turns = view.turns;
|
|
403
|
+
// While take turns is on, settings belong to the driver.
|
|
404
|
+
const driving = !turns?.on || turns.driver === me?.id;
|
|
405
|
+
const instructions = view.agent?.instructions;
|
|
406
|
+
const memory = useMemory();
|
|
407
|
+
|
|
408
|
+
return html`<${MenuFrame} title=${conversation?.title ?? "Menu"}>
|
|
409
|
+
${places && html`<${Places} />`}
|
|
410
|
+
<div class="menu-session">
|
|
411
|
+
${
|
|
412
|
+
conversation &&
|
|
413
|
+
collab() &&
|
|
414
|
+
canToggleTurns() &&
|
|
415
|
+
item(
|
|
416
|
+
turns?.on ? "Turn off take turns" : "Take turns",
|
|
417
|
+
() =>
|
|
418
|
+
attempt(async () => {
|
|
419
|
+
await actions.turns(turns?.on ? "off" : "on");
|
|
420
|
+
closeSheet();
|
|
421
|
+
}),
|
|
422
|
+
turns?.on
|
|
423
|
+
? "anyone here can send to Pi again"
|
|
424
|
+
: "one person drives Pi at a time",
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
${session && steer && item("Rename", () => openSheet({ type: "rename" }))}
|
|
428
|
+
${
|
|
429
|
+
session &&
|
|
430
|
+
item(
|
|
431
|
+
isPinned(conversation.id) ? "Unpin from the top" : "Pin to the top",
|
|
432
|
+
() => {
|
|
433
|
+
togglePin(conversation.id);
|
|
434
|
+
closeSheet();
|
|
435
|
+
},
|
|
436
|
+
"this browser",
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
${
|
|
440
|
+
session &&
|
|
441
|
+
item(
|
|
442
|
+
"Mark unread",
|
|
443
|
+
() => {
|
|
444
|
+
setUnread(conversation.id, true);
|
|
445
|
+
closeSheet();
|
|
446
|
+
navigate(null);
|
|
447
|
+
},
|
|
448
|
+
"this browser; opening it again clears it",
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
${
|
|
452
|
+
conversation?.worktree && steer
|
|
453
|
+
? item(
|
|
454
|
+
"Worktree",
|
|
455
|
+
() => openSheet({ type: "worktree" }),
|
|
456
|
+
conversation.worktree.branch,
|
|
457
|
+
)
|
|
458
|
+
: conversation &&
|
|
459
|
+
steer &&
|
|
460
|
+
driving &&
|
|
461
|
+
!scoped() &&
|
|
462
|
+
item(
|
|
463
|
+
"Working directory",
|
|
464
|
+
() => openSheet({ type: "cwd", mode: "change" }),
|
|
465
|
+
shortPath(view.agent?.cwd, server?.home),
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
${
|
|
469
|
+
conversation &&
|
|
470
|
+
browserAvailable() &&
|
|
471
|
+
store.state.browserOpen &&
|
|
472
|
+
item(
|
|
473
|
+
"Close the browser",
|
|
474
|
+
() => {
|
|
475
|
+
setBrowserOpen(false);
|
|
476
|
+
closeSheet();
|
|
477
|
+
},
|
|
478
|
+
store.state.browser?.open && displayUrl(store.state.browser.url),
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
${conversation && trustAvailable() && item("Project trust", () => openSheet({ type: "trust" }), trustHint())}
|
|
482
|
+
${session && extensionMenuItems("session").map((each) => item(each.label, each.run, each.hint))}
|
|
483
|
+
${session && steer && driving && item("Instructions for Pi", () => openSheet({ type: "instructions" }), instructions ? "on" : "none")}
|
|
484
|
+
${conversation && steer && driving && item("Compact context", () => openSheet({ type: "compact" }), "summarize older messages")}
|
|
485
|
+
${conversation && steer && driving && item("New context", () => openSheet({ type: "reset" }), "Pi starts fresh; history stays")}
|
|
486
|
+
${conversation && memoryAvailable() && item("Memory", () => openSheet({ type: "memory" }), memoryHint(memory))}
|
|
487
|
+
${schedulesAvailable() && item("Scheduled messages", () => openSheet({ type: "schedules" }), view.schedules.length === 0 ? "none" : `${view.schedules.length} coming`)}
|
|
488
|
+
${
|
|
489
|
+
watchAvailable() &&
|
|
490
|
+
steer &&
|
|
491
|
+
view.watch?.status !== "watching" &&
|
|
492
|
+
item(
|
|
493
|
+
"Watch pull request",
|
|
494
|
+
() => {
|
|
495
|
+
closeSheet();
|
|
496
|
+
attempt(() => watchPr());
|
|
497
|
+
},
|
|
498
|
+
"fix CI, answer reviews",
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
${
|
|
502
|
+
conversation &&
|
|
503
|
+
item("Copy link", () =>
|
|
504
|
+
copyText(location.href).then(
|
|
505
|
+
() => notify("info", "Link copied. Other signed-in devices can open it."),
|
|
506
|
+
() => notify("error", "Could not copy."),
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
${
|
|
511
|
+
conversation &&
|
|
512
|
+
html`<a class="list-item" href=${`/api/c/${conversation.id}/export`} download>
|
|
513
|
+
<span>Export as Markdown</span>
|
|
514
|
+
<span class="muted small">the whole history</span>
|
|
515
|
+
</a>`
|
|
516
|
+
}
|
|
517
|
+
${
|
|
518
|
+
session &&
|
|
519
|
+
steer &&
|
|
520
|
+
item(conversation.archived ? "Unarchive" : "Archive", () => {
|
|
521
|
+
closeSheet();
|
|
522
|
+
setArchived([conversation.id], !conversation.archived);
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
</div>
|
|
526
|
+
${
|
|
527
|
+
view.subagents.length > 0 &&
|
|
528
|
+
html`<div class="group">
|
|
529
|
+
<div class="group-title">Subagents</div>
|
|
530
|
+
${view.subagents.map((agent) =>
|
|
531
|
+
item(
|
|
532
|
+
html`${agent.busy ? html`<span class="pulse"></span> ` : ""}${agent.name}`,
|
|
533
|
+
() => openSubagent(agent.conversationId),
|
|
534
|
+
agent.busy ? "working" : "idle",
|
|
535
|
+
),
|
|
536
|
+
)}
|
|
537
|
+
</div>`
|
|
538
|
+
}
|
|
539
|
+
<div class="group">
|
|
540
|
+
<div class="group-title">App</div>
|
|
541
|
+
${item("Appearance", () => openSheet({ type: "appearance" }), lookLabel())}
|
|
542
|
+
${!touch && item("Keyboard shortcuts", () => openSheet({ type: "shortcuts" }), "?")}
|
|
543
|
+
${
|
|
544
|
+
me?.role === "owner" &&
|
|
545
|
+
defaultsKept() &&
|
|
546
|
+
item(
|
|
547
|
+
"Default model",
|
|
548
|
+
() => openSheet({ type: "model", id: "default", forDefault: true }),
|
|
549
|
+
defaultLabel(server?.defaultModel, store.state.models) ||
|
|
550
|
+
"project, last picked, then Pi global",
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
${item("Your name", () => openSheet({ type: "name" }), me?.name)}
|
|
554
|
+
${collab() ? item("People", () => openSheet({ type: "people" }), me?.role === "viewer" ? "you can view" : "") : item("Sign in another device", () => openSheet({ type: "invite" }))}
|
|
555
|
+
${collab() && item("Notifications", () => openSheet({ type: "notifications" }), "Pi finished, approvals, chat")}
|
|
556
|
+
${item("Running now", () => openSheet({ type: "running" }), "everything Pi is doing")}
|
|
557
|
+
${item("Spend", () => openSheet({ type: "spend" }), me?.role === "owner" ? "by person and session, limits" : "yours")}
|
|
558
|
+
${triggersOn() && me?.role !== "viewer" && item("Webhooks and watches", () => openSheet({ type: "hooks" }), "GitHub, other services")}
|
|
559
|
+
${me?.role === "owner" && item("Providers", () => openSheet({ type: "providers" }))}
|
|
560
|
+
${modelRefreshAvailable() && item("Refresh models", refreshModels, store.state.modelRefresh.loading ? "refreshing…" : "models.json and providers")}
|
|
561
|
+
${extensionMenuItems().map((each) => item(each.label, each.run, each.hint))}
|
|
562
|
+
${item("Extensions", () => openSheet({ type: "extensions" }), extensionsHint())}
|
|
563
|
+
${
|
|
564
|
+
me?.role === "owner" &&
|
|
565
|
+
server?.supervised &&
|
|
566
|
+
item(
|
|
567
|
+
"Restart server",
|
|
568
|
+
() =>
|
|
569
|
+
attempt(async () => {
|
|
570
|
+
await api("restart", {});
|
|
571
|
+
closeSheet();
|
|
572
|
+
notify("info", "Restarting. Running work continues after the restart.");
|
|
573
|
+
}),
|
|
574
|
+
"running work resumes",
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
${item(html`<span class="menu-signout">Sign out</span>`, signOut)}
|
|
578
|
+
</div>
|
|
579
|
+
<//>`;
|
|
580
|
+
}
|