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,71 @@
|
|
|
1
|
+
// Scheduled messages of this session.
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
import { actions, attempt, canSteer, closeSheet, store } from "../store.js";
|
|
4
|
+
import { formatWhen, html, Icon, Sheet } from "../ui.js";
|
|
5
|
+
|
|
6
|
+
/** Messages that go to Pi later or on repeat: set one up, see what is coming, cancel one. */
|
|
7
|
+
export function SchedulesSheet() {
|
|
8
|
+
const { view, users, me } = store.state;
|
|
9
|
+
const [when, setWhen] = useState("");
|
|
10
|
+
const steer = canSteer();
|
|
11
|
+
const setBy = (id) =>
|
|
12
|
+
id === undefined
|
|
13
|
+
? "Pi"
|
|
14
|
+
: id === me?.id
|
|
15
|
+
? "you"
|
|
16
|
+
: (users.find((user) => user.id === id)?.name ?? "someone");
|
|
17
|
+
const add = () =>
|
|
18
|
+
attempt(async () => {
|
|
19
|
+
await actions.schedule(when);
|
|
20
|
+
setWhen("");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return html`<${Sheet} title="Scheduled messages" onClose=${closeSheet}>
|
|
24
|
+
<p class="muted small">
|
|
25
|
+
Pi gets these at their time, also when nobody is here, and the people in this session get a notification when it is done.
|
|
26
|
+
</p>
|
|
27
|
+
${
|
|
28
|
+
steer &&
|
|
29
|
+
html`<div class="row">
|
|
30
|
+
<input
|
|
31
|
+
placeholder="in 2h check the deploy"
|
|
32
|
+
value=${when}
|
|
33
|
+
onInput=${(event) => setWhen(event.currentTarget.value)}
|
|
34
|
+
onKeyDown=${(event) => event.key === "Enter" && add()}
|
|
35
|
+
/>
|
|
36
|
+
<button class="button primary" disabled=${when.trim() === ""} onClick=${add}>
|
|
37
|
+
Add
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<p class="muted small">
|
|
41
|
+
Start with when: in 30m, 7:00, tomorrow 9am, fri 17:30, every 2h, every weekday 8:00. Then what Pi gets.
|
|
42
|
+
</p>`
|
|
43
|
+
}
|
|
44
|
+
${view.schedules.length === 0 && html`<p class="muted">Nothing is scheduled.</p>`}
|
|
45
|
+
${
|
|
46
|
+
view.schedules.length > 0 &&
|
|
47
|
+
html`<div class="sheet-list">
|
|
48
|
+
${view.schedules.map(
|
|
49
|
+
(schedule) => html`<div class="schedule" key=${schedule.id}>
|
|
50
|
+
<${Icon} name="clock" size=${15} />
|
|
51
|
+
<div class="grow">
|
|
52
|
+
<div>${schedule.text}</div>
|
|
53
|
+
<div class="muted small">
|
|
54
|
+
${schedule.repeat ? `${schedule.repeat} · next ${formatWhen(schedule.next)}` : formatWhen(schedule.next)} · set by ${setBy(schedule.by)}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
${
|
|
58
|
+
steer &&
|
|
59
|
+
html`<button
|
|
60
|
+
class="button small ghost"
|
|
61
|
+
onClick=${() => attempt(() => actions.cancelSchedule(schedule.id))}
|
|
62
|
+
>
|
|
63
|
+
Cancel
|
|
64
|
+
</button>`
|
|
65
|
+
}
|
|
66
|
+
</div>`,
|
|
67
|
+
)}
|
|
68
|
+
</div>`
|
|
69
|
+
}
|
|
70
|
+
<//>`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
|
2
|
+
import { sessionActions } from "../extension-ui.js";
|
|
3
|
+
import { deselectInList, selectedInList, selectFromList, setArchived } from "../sessions.js";
|
|
4
|
+
import {
|
|
5
|
+
actions,
|
|
6
|
+
attempt,
|
|
7
|
+
canSteer,
|
|
8
|
+
closeSheet,
|
|
9
|
+
collab,
|
|
10
|
+
isUnread,
|
|
11
|
+
navigate,
|
|
12
|
+
notify,
|
|
13
|
+
scoped,
|
|
14
|
+
setUnread,
|
|
15
|
+
store,
|
|
16
|
+
} from "../store.js";
|
|
17
|
+
import { isPinned, movePin, togglePin } from "../theme.js";
|
|
18
|
+
import { copyText, html, item, scrimPress, Sheet } from "../ui.js";
|
|
19
|
+
|
|
20
|
+
const linkOf = (id) => `${location.origin}/s/${id}`;
|
|
21
|
+
|
|
22
|
+
const copy = (text, what) =>
|
|
23
|
+
copyText(text).then(
|
|
24
|
+
() => notify("info", `Copied the ${what}.`),
|
|
25
|
+
() => notify("error", `Could not copy the ${what}.`),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
let forking = false;
|
|
29
|
+
|
|
30
|
+
function fork(id) {
|
|
31
|
+
if (forking) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
forking = true;
|
|
36
|
+
attempt(async () => {
|
|
37
|
+
const created = await actions.forkSession(id);
|
|
38
|
+
|
|
39
|
+
navigate(created.id);
|
|
40
|
+
notify("info", "Opened the fork. The original is unchanged.");
|
|
41
|
+
}).finally(() => {
|
|
42
|
+
forking = false;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const newIn = (cwd) =>
|
|
47
|
+
attempt(async () => {
|
|
48
|
+
const created = await actions.createSession(cwd);
|
|
49
|
+
|
|
50
|
+
navigate(created.id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function sessionItems(session) {
|
|
54
|
+
const { id } = session;
|
|
55
|
+
const steer = canSteer();
|
|
56
|
+
// People invited to one session start none, and invite nobody.
|
|
57
|
+
const starts = steer && !scoped();
|
|
58
|
+
const pinned = !session.archived && isPinned(id);
|
|
59
|
+
const pins = store.state.pinned.filter((each) =>
|
|
60
|
+
store.state.sessions.some((one) => one.id === each && !one.archived),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
[
|
|
65
|
+
{ label: "Open in new tab", run: () => window.open(linkOf(id), "_blank", "noopener") },
|
|
66
|
+
{ label: "Copy link", run: () => copy(linkOf(id), "link") },
|
|
67
|
+
{ label: "Copy folder path", run: () => copy(session.cwd, "folder path") },
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
steer && {
|
|
71
|
+
label: "Rename",
|
|
72
|
+
opens: true,
|
|
73
|
+
run: () => store.set({ sheet: { type: "rename", id } }),
|
|
74
|
+
},
|
|
75
|
+
starts && { label: "Fork", hint: "up to the last reply", run: () => fork(id) },
|
|
76
|
+
starts && { label: "New session in this folder", run: () => newIn(session.cwd) },
|
|
77
|
+
starts &&
|
|
78
|
+
collab() && {
|
|
79
|
+
label: "Share",
|
|
80
|
+
hint: "invite someone",
|
|
81
|
+
opens: true,
|
|
82
|
+
run: () => store.set({ sheet: { type: "invite", session: id } }),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
!session.archived && {
|
|
87
|
+
label: pinned ? "Unpin from the top" : "Pin to the top",
|
|
88
|
+
hint: "this browser",
|
|
89
|
+
run: () => togglePin(id),
|
|
90
|
+
},
|
|
91
|
+
pinned &&
|
|
92
|
+
pins[0] !== id && {
|
|
93
|
+
label: "Move to top",
|
|
94
|
+
hint: "of the pinned",
|
|
95
|
+
run: () => movePin(id, false),
|
|
96
|
+
},
|
|
97
|
+
pinned &&
|
|
98
|
+
pins.at(-1) !== id && {
|
|
99
|
+
label: "Move to bottom",
|
|
100
|
+
hint: "of the pinned",
|
|
101
|
+
run: () => movePin(id, true),
|
|
102
|
+
},
|
|
103
|
+
!session.archived && {
|
|
104
|
+
label: isUnread(id) ? "Mark read" : "Mark unread",
|
|
105
|
+
hint: "this browser",
|
|
106
|
+
run: () => setUnread(id, !isUnread(id)),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
sessionActions().map((action) => ({
|
|
110
|
+
label: action.label,
|
|
111
|
+
hint: action.module,
|
|
112
|
+
run: () => action.run([id]),
|
|
113
|
+
})),
|
|
114
|
+
[
|
|
115
|
+
selectedInList() > 0
|
|
116
|
+
? {
|
|
117
|
+
label: "Deselect all",
|
|
118
|
+
hint: `${selectedInList()} selected`,
|
|
119
|
+
run: deselectInList,
|
|
120
|
+
}
|
|
121
|
+
: { label: "Select", hint: "then add others", run: () => selectFromList(id) },
|
|
122
|
+
steer && {
|
|
123
|
+
label: session.archived ? "Unarchive" : "Archive",
|
|
124
|
+
run: () => setArchived([id], !session.archived),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
]
|
|
128
|
+
.map((group) => group.filter(Boolean))
|
|
129
|
+
.filter((group) => group.length > 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const runItem = (each) => () => {
|
|
133
|
+
if (!each.opens) {
|
|
134
|
+
closeSheet();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
each.run();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function SessionSheet({ id, at = null }) {
|
|
141
|
+
const session = store.state.sessions.find((each) => each.id === id);
|
|
142
|
+
const groups = sessionItems(session);
|
|
143
|
+
|
|
144
|
+
if (at) {
|
|
145
|
+
return html`<${SessionMenu} title=${session.title ?? "New session"} groups=${groups} at=${at} />`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return html`<${Sheet} title=${session.title ?? "New session"} onClose=${closeSheet}>
|
|
149
|
+
<div class="sheet-list">
|
|
150
|
+
${groups.flat().map((each) => item(each.label, runItem(each), each.hint))}
|
|
151
|
+
</div>
|
|
152
|
+
<//>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function SessionMenu({ title, groups, at }) {
|
|
156
|
+
const box = useRef(null);
|
|
157
|
+
|
|
158
|
+
useLayoutEffect(() => {
|
|
159
|
+
const menu = box.current;
|
|
160
|
+
// Unscaled: the opening animation scales the menu, and a scaled size is a little short.
|
|
161
|
+
const width = menu.offsetWidth;
|
|
162
|
+
const height = menu.offsetHeight;
|
|
163
|
+
const left = at.x + width + 8 > innerWidth ? Math.max(8, at.x - width) : at.x;
|
|
164
|
+
const top = at.y + height + 8 > innerHeight ? Math.max(8, at.y - height) : at.y;
|
|
165
|
+
|
|
166
|
+
menu.style.left = `${left}px`;
|
|
167
|
+
menu.style.top = `${top}px`;
|
|
168
|
+
menu.focus({ preventScroll: true });
|
|
169
|
+
}, [at.x, at.y]);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const onKey = (event) =>
|
|
173
|
+
event.key === "Escape" && !event.isComposing && event.keyCode !== 229 && closeSheet();
|
|
174
|
+
|
|
175
|
+
addEventListener("keydown", onKey);
|
|
176
|
+
|
|
177
|
+
return () => removeEventListener("keydown", onKey);
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
const onKeyDown = (event) => {
|
|
181
|
+
// Here as well as on the window: the menu has the focus from its first frame, before effects run.
|
|
182
|
+
if (event.key === "Escape" && !event.isComposing && event.keyCode !== 229) {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
closeSheet();
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const items = [...box.current.querySelectorAll("button")];
|
|
190
|
+
const index = items.indexOf(document.activeElement);
|
|
191
|
+
let target;
|
|
192
|
+
|
|
193
|
+
if (event.key === "ArrowDown") {
|
|
194
|
+
target = items[(index + 1) % items.length];
|
|
195
|
+
} else if (event.key === "ArrowUp") {
|
|
196
|
+
target = items[(index - 1 + items.length) % items.length];
|
|
197
|
+
} else if (event.key === "Home") {
|
|
198
|
+
target = items[0];
|
|
199
|
+
} else if (event.key === "End") {
|
|
200
|
+
target = items.at(-1);
|
|
201
|
+
} else if (event.key === "Tab") {
|
|
202
|
+
target = items[(index + (event.shiftKey ? -1 : 1) + items.length) % items.length];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (target) {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
target.focus();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const onScrimMenu = (event) => {
|
|
212
|
+
if (event.target === event.currentTarget) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
closeSheet();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return html`<div
|
|
219
|
+
class="overlay pop-overlay"
|
|
220
|
+
onMouseDown=${scrimPress(closeSheet)}
|
|
221
|
+
onContextMenu=${onScrimMenu}
|
|
222
|
+
>
|
|
223
|
+
<section
|
|
224
|
+
class="pop-menu session-menu"
|
|
225
|
+
data-part="session-menu"
|
|
226
|
+
ref=${box}
|
|
227
|
+
role="menu"
|
|
228
|
+
aria-label=${title}
|
|
229
|
+
tabindex="-1"
|
|
230
|
+
onKeyDown=${onKeyDown}
|
|
231
|
+
onContextMenu=${(event) => event.preventDefault()}
|
|
232
|
+
>
|
|
233
|
+
${groups.map(
|
|
234
|
+
(group) => html`<div class="session-menu-group" role="group">
|
|
235
|
+
${group.map(
|
|
236
|
+
(each) => html`<button
|
|
237
|
+
class="session-menu-item"
|
|
238
|
+
role="menuitem"
|
|
239
|
+
onClick=${runItem(each)}
|
|
240
|
+
>
|
|
241
|
+
<span>${each.label}</span>
|
|
242
|
+
${each.hint && html`<span class="session-menu-hint">${each.hint}</span>`}
|
|
243
|
+
</button>`,
|
|
244
|
+
)}
|
|
245
|
+
</div>`,
|
|
246
|
+
)}
|
|
247
|
+
</section>
|
|
248
|
+
</div>`;
|
|
249
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The keyboard shortcuts, as a list. The keys themselves are handled in app.js.
|
|
2
|
+
import { boxKeyRows } from "../box-keys.js";
|
|
3
|
+
import { closeSheet } from "../store.js";
|
|
4
|
+
import { html, Keys, Sheet } from "../ui.js";
|
|
5
|
+
|
|
6
|
+
// Each row: its keys, and what they do. An "or" between keys means either one, and a "…" a run from one to the other.
|
|
7
|
+
const SHORTCUTS = [
|
|
8
|
+
[
|
|
9
|
+
"Sessions",
|
|
10
|
+
[
|
|
11
|
+
["Mod K", "Launcher: sessions, actions, themes"],
|
|
12
|
+
["Alt 1 … Alt 9", "Jump to a session, like a workspace"],
|
|
13
|
+
["Alt ↑ or Alt ↓", "Previous or next session"],
|
|
14
|
+
["Alt N", "New session"],
|
|
15
|
+
["Mod 1 … Mod 9", "In New session: start in a favorite folder"],
|
|
16
|
+
["Alt", "Hold to see session numbers"],
|
|
17
|
+
["Mod B", "Fold or unfold the sidebar"],
|
|
18
|
+
["Mod Click", "In the sidebar: select a session; drag to select every one you pass"],
|
|
19
|
+
["Shift Click", "In the sidebar: select a run of sessions"],
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
"Panels",
|
|
24
|
+
[
|
|
25
|
+
["Alt B", "Open or close the browser"],
|
|
26
|
+
["Alt E", "Open or close the Files tile: the folder's files, and what changed"],
|
|
27
|
+
["Alt A", "Open or close PiP, your assistant"],
|
|
28
|
+
["Alt P", "Show or hide peek tiles"],
|
|
29
|
+
["Alt T", "Show thinking: turn the live disclosure on or off"],
|
|
30
|
+
["Mod F", "Find in this session"],
|
|
31
|
+
],
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
"Message box",
|
|
35
|
+
[
|
|
36
|
+
["/", "Commands"],
|
|
37
|
+
["@", "Mention a file or folder"],
|
|
38
|
+
["!", "Run a command (!! keeps it from Pi)"],
|
|
39
|
+
["↑", "In an empty box: what you sent before"],
|
|
40
|
+
["Ctrl R", "Search what you sent before"],
|
|
41
|
+
["Alt V", "Dictate into the message box, or stop (where the browser can)"],
|
|
42
|
+
["Esc Esc", "Stop Pi while it works"],
|
|
43
|
+
],
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"Changes",
|
|
47
|
+
[
|
|
48
|
+
["j or k", "The next or previous file"],
|
|
49
|
+
["n or p", "The next or previous change"],
|
|
50
|
+
["v", "Mark the file viewed, or not"],
|
|
51
|
+
["s", "The layout: auto, split, or unified"],
|
|
52
|
+
["w", "Wrap long lines, or not"],
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"Anywhere",
|
|
57
|
+
[
|
|
58
|
+
["?", "This list"],
|
|
59
|
+
["Esc", "Close what is open"],
|
|
60
|
+
["Mod Click", "On a file path: show it in the server machine's file manager (owner)"],
|
|
61
|
+
],
|
|
62
|
+
],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Keys as chips, each key of a chord in its own, with the "or" and "…" between chords left plain. */
|
|
66
|
+
function Chords({ keys }) {
|
|
67
|
+
return keys
|
|
68
|
+
.split(" ")
|
|
69
|
+
.map((key) =>
|
|
70
|
+
key === "or" || key === "…"
|
|
71
|
+
? html`<span class="muted">${key}</span>`
|
|
72
|
+
: html`<${Keys} keys=${key} />`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function ShortcutsSheet() {
|
|
77
|
+
// Extensions' keys in the message boxes (`pocket.composerKey`), right after the box's own.
|
|
78
|
+
const keys = boxKeyRows();
|
|
79
|
+
const groups = SHORTCUTS.flatMap((group) =>
|
|
80
|
+
group[0] === "Message box" && keys.length > 0
|
|
81
|
+
? [group, ["Message box: from extensions", keys]]
|
|
82
|
+
: [group],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return html`<${Sheet} title="Keyboard shortcuts" onClose=${closeSheet}>
|
|
86
|
+
${groups.map(
|
|
87
|
+
([title, rows]) => html`<div class="group" key=${title}>
|
|
88
|
+
<div class="group-title">${title}</div>
|
|
89
|
+
<dl class="shortcuts">
|
|
90
|
+
${rows.map(
|
|
91
|
+
([keys, text]) => html`<dt><${Chords} keys=${keys} /></dt>
|
|
92
|
+
<dd>${text}</dd>`,
|
|
93
|
+
)}
|
|
94
|
+
</dl>
|
|
95
|
+
</div>`,
|
|
96
|
+
)}
|
|
97
|
+
<p class="muted small">
|
|
98
|
+
In the launcher, start with <kbd>></kbd> for actions, <kbd>@</kbd> for sessions, or <kbd>#</kbd> for themes. Arrowing onto colors shows them; Enter keeps them.
|
|
99
|
+
</p>
|
|
100
|
+
<//>`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Spend: what each session and person cost, and their limits.
|
|
2
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
3
|
+
import { api, attempt, closeSheet, store } from "../store.js";
|
|
4
|
+
import { html, Icon, Loader, Sheet } from "../ui.js";
|
|
5
|
+
|
|
6
|
+
const money = (amount) => `$${amount.toFixed(2)}`;
|
|
7
|
+
const largest = (rows) => rows.reduce((max, row) => Math.max(max, row.spent), 1);
|
|
8
|
+
|
|
9
|
+
/** A spend limit, and (for the owner) a way to change it. */
|
|
10
|
+
function SpendLimit({ budget, editable, save }) {
|
|
11
|
+
const [draft, setDraft] = useState(null);
|
|
12
|
+
const field = useRef(null);
|
|
13
|
+
const change = useRef(null);
|
|
14
|
+
const edited = useRef(false);
|
|
15
|
+
const editing = draft !== null;
|
|
16
|
+
|
|
17
|
+
// Into the field as it opens; back to Change as it closes, so the keyboard keeps its place.
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (editing) {
|
|
20
|
+
field.current?.focus();
|
|
21
|
+
} else if (edited.current) {
|
|
22
|
+
change.current?.focus();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
edited.current = editing;
|
|
26
|
+
}, [editing]);
|
|
27
|
+
|
|
28
|
+
if (draft === null) {
|
|
29
|
+
return html`<span class="muted small">
|
|
30
|
+
${budget === undefined ? "no limit" : `limit ${money(budget)}`}
|
|
31
|
+
</span>
|
|
32
|
+
${
|
|
33
|
+
editable
|
|
34
|
+
? html`<button
|
|
35
|
+
class="link small spend-change"
|
|
36
|
+
ref=${change}
|
|
37
|
+
onClick=${() => setDraft(budget === undefined ? "" : String(budget))}
|
|
38
|
+
>
|
|
39
|
+
Change
|
|
40
|
+
</button>`
|
|
41
|
+
: html`<span class="spend-change" aria-hidden="true"></span>`
|
|
42
|
+
}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const done = (value) =>
|
|
46
|
+
attempt(async () => {
|
|
47
|
+
await save(value);
|
|
48
|
+
setDraft(null);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return html`<span class="limit-edit">
|
|
52
|
+
$<input type="number" inputmode="decimal" min="0.01" step="0.01" aria-label="Limit in dollars" ref=${field} value=${draft} onInput=${(event) => setDraft(event.currentTarget.value)} onKeyDown=${(
|
|
53
|
+
event,
|
|
54
|
+
) => {
|
|
55
|
+
// Esc lets go of the limit, not the whole sheet.
|
|
56
|
+
if (event.key === "Escape") {
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
setDraft(null);
|
|
59
|
+
}
|
|
60
|
+
}} />
|
|
61
|
+
<button
|
|
62
|
+
class="button small primary"
|
|
63
|
+
disabled=${!(Number(draft) > 0)}
|
|
64
|
+
onClick=${() => done(Number(draft))}
|
|
65
|
+
>
|
|
66
|
+
Set
|
|
67
|
+
</button>
|
|
68
|
+
${
|
|
69
|
+
budget !== undefined &&
|
|
70
|
+
html`<button class="button small ghost" onClick=${() => done(null)}>No limit</button>`
|
|
71
|
+
}
|
|
72
|
+
<button class="icon-button" aria-label="Cancel" onClick=${() => setDraft(null)}>
|
|
73
|
+
<${Icon} name="close" size=${16} />
|
|
74
|
+
</button>
|
|
75
|
+
</span>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Usage against a limit, or the largest spend in the group when there is no limit. */
|
|
79
|
+
function Usage({ row, max }) {
|
|
80
|
+
const percent = Math.min(100, Math.max(0, (row.spent / (row.budget ?? max)) * 100));
|
|
81
|
+
|
|
82
|
+
return html`<span
|
|
83
|
+
class=${`spend-usage ${row.budget !== undefined && percent >= 80 ? "warn" : ""}`}
|
|
84
|
+
style=${`--usage:${percent}%`}
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
><span></span></span>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What Pi spent, by person and by session, with the owner's limits. */
|
|
90
|
+
export function SpendSheet() {
|
|
91
|
+
const owner = store.state.me?.role === "owner";
|
|
92
|
+
const [data, setData] = useState(null);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
attempt(async () => setData(await api("spend")));
|
|
96
|
+
}, []);
|
|
97
|
+
const save = (target) => async (budget) => setData(await api("spend", { ...target, budget }));
|
|
98
|
+
const peopleMax = largest(data?.people ?? []);
|
|
99
|
+
const sessionsMax = largest(data?.sessions ?? []);
|
|
100
|
+
|
|
101
|
+
return html`<${Sheet} title="Spend" onClose=${closeSheet} wide=${true}>
|
|
102
|
+
${data === null && html`<${Loader} label="Adding it up" />`}
|
|
103
|
+
${
|
|
104
|
+
data?.total !== undefined &&
|
|
105
|
+
html`<p class="spend-total">
|
|
106
|
+
<span>Pi spent</span> <strong>${money(data.total)}</strong> <span>on this server so far.</span>
|
|
107
|
+
</p>`
|
|
108
|
+
}
|
|
109
|
+
<p class="muted small">
|
|
110
|
+
Spend goes to whoever asked for the work. Past a limit, Pi takes no new messages there, a run that crosses it stops, and subagents' reports wait until it is raised.
|
|
111
|
+
</p>
|
|
112
|
+
${
|
|
113
|
+
data &&
|
|
114
|
+
html`<div class="group">
|
|
115
|
+
<div class="group-title">People</div>
|
|
116
|
+
<div class="sheet-list">
|
|
117
|
+
${data.people.map(
|
|
118
|
+
(person) => html`<div class="spend-row" key=${person.id}>
|
|
119
|
+
<span class="grow">${person.name}</span>
|
|
120
|
+
<${Usage} row=${person} max=${peopleMax} />
|
|
121
|
+
<span class="mono">${money(person.spent)}</span>
|
|
122
|
+
<${SpendLimit}
|
|
123
|
+
budget=${person.budget}
|
|
124
|
+
editable=${owner && person.id !== store.state.me?.id}
|
|
125
|
+
save=${save({ person: person.id })}
|
|
126
|
+
/>
|
|
127
|
+
</div>`,
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="group">
|
|
132
|
+
<div class="group-title">Sessions</div>
|
|
133
|
+
<div class="sheet-list">
|
|
134
|
+
${data.sessions.map(
|
|
135
|
+
(session) => html`<div class="spend-row" key=${session.id}>
|
|
136
|
+
<span class="grow">${session.title}</span>
|
|
137
|
+
<${Usage} row=${session} max=${sessionsMax} />
|
|
138
|
+
<span class="mono">${money(session.spent)}</span>
|
|
139
|
+
<${SpendLimit}
|
|
140
|
+
budget=${session.budget}
|
|
141
|
+
editable=${owner}
|
|
142
|
+
save=${save({ session: session.id })}
|
|
143
|
+
/>
|
|
144
|
+
</div>`,
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>`
|
|
148
|
+
}
|
|
149
|
+
<//>`;
|
|
150
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// One text field and a button: renaming, resetting, instructions, compacting, and your name.
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
import { attempt, closeSheet } from "../store.js";
|
|
4
|
+
import { html, Sheet } from "../ui.js";
|
|
5
|
+
|
|
6
|
+
export function TextSheet({
|
|
7
|
+
title,
|
|
8
|
+
label,
|
|
9
|
+
hint = "",
|
|
10
|
+
initial = "",
|
|
11
|
+
placeholder = "",
|
|
12
|
+
submit,
|
|
13
|
+
multiline = false,
|
|
14
|
+
button = "Save",
|
|
15
|
+
footer = null,
|
|
16
|
+
}) {
|
|
17
|
+
const [value, setValue] = useState(initial);
|
|
18
|
+
const save = () =>
|
|
19
|
+
attempt(async () => {
|
|
20
|
+
await submit(value);
|
|
21
|
+
closeSheet();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return html`<${Sheet} title=${title} onClose=${closeSheet}>
|
|
25
|
+
${hint && html`<p class="muted small">${hint}</p>`}
|
|
26
|
+
<div class="field">
|
|
27
|
+
<div class="label">${label}</div>
|
|
28
|
+
${
|
|
29
|
+
multiline
|
|
30
|
+
? html`<textarea
|
|
31
|
+
rows="4"
|
|
32
|
+
aria-label=${label}
|
|
33
|
+
value=${value}
|
|
34
|
+
placeholder=${placeholder}
|
|
35
|
+
onInput=${(event) => setValue(event.currentTarget.value)}
|
|
36
|
+
></textarea>`
|
|
37
|
+
: html`<input
|
|
38
|
+
autofocus
|
|
39
|
+
aria-label=${label}
|
|
40
|
+
value=${value}
|
|
41
|
+
placeholder=${placeholder}
|
|
42
|
+
onInput=${(event) => setValue(event.currentTarget.value)}
|
|
43
|
+
onKeyDown=${(event) => event.key === "Enter" && save()}
|
|
44
|
+
/>`
|
|
45
|
+
}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="sheet-actions">
|
|
48
|
+
<button class="button" onClick=${closeSheet}>Cancel</button>
|
|
49
|
+
<button class="button primary" onClick=${save}>${button}</button>
|
|
50
|
+
</div>
|
|
51
|
+
${footer}
|
|
52
|
+
<//>`;
|
|
53
|
+
}
|