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,820 @@
|
|
|
1
|
+
// Appearance: the theme and the colors, and how the app looks and moves.
|
|
2
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
3
|
+
import {
|
|
4
|
+
actions,
|
|
5
|
+
api,
|
|
6
|
+
attempt,
|
|
7
|
+
closeSheet,
|
|
8
|
+
drafts,
|
|
9
|
+
navigate,
|
|
10
|
+
notify,
|
|
11
|
+
openSheet,
|
|
12
|
+
store,
|
|
13
|
+
} from "../store.js";
|
|
14
|
+
import {
|
|
15
|
+
BASE_LOOK,
|
|
16
|
+
chooseLook,
|
|
17
|
+
chooseTheme,
|
|
18
|
+
colorsOnly,
|
|
19
|
+
folderPaletteIds,
|
|
20
|
+
lookIds,
|
|
21
|
+
paletteOf,
|
|
22
|
+
prefs,
|
|
23
|
+
preview,
|
|
24
|
+
setPrefs,
|
|
25
|
+
themeDefault,
|
|
26
|
+
themeIds,
|
|
27
|
+
textSizeOf,
|
|
28
|
+
themeVars,
|
|
29
|
+
varsStyle,
|
|
30
|
+
MAX_TEXT,
|
|
31
|
+
MIN_TEXT,
|
|
32
|
+
STEP_TEXT,
|
|
33
|
+
} from "../theme.js";
|
|
34
|
+
import { arranged, move, resetSlot, setShown, SLOTS } from "../slots.js";
|
|
35
|
+
import { html, Icon, Sheet, Switch } from "../ui.js";
|
|
36
|
+
import { galleryAvailable } from "./gallery.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
|
+
/** A theme as a card: a tiny Hyprland desktop drawn in its own colors. */
|
|
42
|
+
function ThemeCard({ id, desktop = false }) {
|
|
43
|
+
const palette = paletteOf(id);
|
|
44
|
+
const vars = varsStyle(themeVars(palette));
|
|
45
|
+
// "Desktop" on a server without an Omarchy desktop shows Tokyo Night.
|
|
46
|
+
const on =
|
|
47
|
+
prefs().theme === id ||
|
|
48
|
+
(id === "tokyo-night" && prefs().theme === "desktop" && !store.state.desktopTheme);
|
|
49
|
+
const dots = ["--o-red", "--o-yellow", "--o-green", "--o-cyan", "--o-blue", "--o-magenta"];
|
|
50
|
+
const desk = html`<div class="tc-desk">
|
|
51
|
+
<div class="tc-win"><i></i><i></i><i class="lit"></i></div>
|
|
52
|
+
<div class="tc-win"><i></i><i></i><i class="lit"></i></div>
|
|
53
|
+
</div>`;
|
|
54
|
+
const name = html`<div class="tc-name">
|
|
55
|
+
${desktop ? `Desktop · ${palette.name}` : palette.name}
|
|
56
|
+
${palette.pair ? html`<small>light and dark</small>` : palette.colors.mode === "light" && html`<small>light</small>`}
|
|
57
|
+
</div>`;
|
|
58
|
+
|
|
59
|
+
return html`<button
|
|
60
|
+
class=${`theme-card ${on ? "on" : ""} ${desktop ? "theme-desktop" : ""}`}
|
|
61
|
+
aria-pressed=${on}
|
|
62
|
+
style=${vars}
|
|
63
|
+
title=${desktop ? "Follow the Omarchy theme of the machine Pi Pocket runs on" : palette.name}
|
|
64
|
+
onMouseEnter=${() => matchMedia("(hover: hover)").matches && preview(id)}
|
|
65
|
+
onMouseLeave=${() => preview(null)}
|
|
66
|
+
onFocus=${() => preview(id)}
|
|
67
|
+
onBlur=${() => preview(null)}
|
|
68
|
+
onClick=${(event) => chooseTheme(id, event)}
|
|
69
|
+
>
|
|
70
|
+
${desk}
|
|
71
|
+
${
|
|
72
|
+
desktop
|
|
73
|
+
? html`<div class="tc-text">
|
|
74
|
+
${name}
|
|
75
|
+
<div class="tc-note">
|
|
76
|
+
Follows your desktop: change the Omarchy theme and Pi Pocket changes with it.
|
|
77
|
+
</div>
|
|
78
|
+
<div class="tc-dots">
|
|
79
|
+
${dots.map((dot) => html`<i style=${`background:var(${dot})`}></i>`)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>`
|
|
82
|
+
: html`${name}
|
|
83
|
+
<div class="tc-dots">
|
|
84
|
+
${dots.map((dot) => html`<i style=${`background:var(${dot})`}></i>`)}
|
|
85
|
+
</div>`
|
|
86
|
+
}
|
|
87
|
+
</button>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A theme folder as the server lists it, or the app's own look. */
|
|
91
|
+
const lookInfo = (id) =>
|
|
92
|
+
store.state.themeList?.find((each) => each.id === id) ??
|
|
93
|
+
(id === BASE_LOOK ? { id, name: "Pocket", builtIn: true, usable: true, css: [] } : undefined);
|
|
94
|
+
|
|
95
|
+
/** A theme as a card: its name, what it says it is, and why it cannot be used, if it cannot. */
|
|
96
|
+
function LookCard({ id }) {
|
|
97
|
+
const look = lookInfo(id);
|
|
98
|
+
const on = (prefs().look ?? BASE_LOOK) === id;
|
|
99
|
+
|
|
100
|
+
if (!look) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return html`<button
|
|
105
|
+
class=${`look-card ${on ? "on" : ""} ${look.usable ? "" : "broken"}`}
|
|
106
|
+
aria-pressed=${on}
|
|
107
|
+
disabled=${!look.usable}
|
|
108
|
+
onClick=${(event) => chooseLook(id, event)}
|
|
109
|
+
>
|
|
110
|
+
<span class="look-name">
|
|
111
|
+
${look.name}
|
|
112
|
+
${look.builtIn && html`<small>built in</small>`}
|
|
113
|
+
${look.colors && html`<small>own colors</small>`}
|
|
114
|
+
</span>
|
|
115
|
+
${look.description && html`<span class="look-detail">${look.description}</span>`}
|
|
116
|
+
${look.problem && html`<span class="look-problem">${look.problem}</span>`}
|
|
117
|
+
</button>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Settles once the server's list of themes has `id` in it, or after two seconds. */
|
|
121
|
+
function listed(id) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const done = () => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
stop();
|
|
126
|
+
resolve();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const timer = setTimeout(done, 2000);
|
|
130
|
+
const stop = store.subscribe((state) => {
|
|
131
|
+
if (state.themeList?.some((each) => each.id === id)) {
|
|
132
|
+
done();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (store.state.themeList?.some((each) => each.id === id)) {
|
|
137
|
+
done();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Ask for a new theme's name, and make it from `from`: its id, or a palette's. */
|
|
143
|
+
function Duplicate({ from, label, colors = false, onDone }) {
|
|
144
|
+
const [name, setName] = useState(`My ${label}`);
|
|
145
|
+
const [busy, setBusy] = useState(false);
|
|
146
|
+
const field = useRef(null);
|
|
147
|
+
|
|
148
|
+
// Ready to type a name each time (`autofocus` works once a page load), where a keyboard is at hand: on a touch
|
|
149
|
+
// screen, one would pop up over the sheet.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!matchMedia("(pointer: coarse)").matches) {
|
|
152
|
+
field.current?.focus();
|
|
153
|
+
field.current?.select();
|
|
154
|
+
}
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
const make = async (event) => {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
setBusy(true);
|
|
160
|
+
const made = await attempt(() => api("themes/duplicate", { from, name, colors }));
|
|
161
|
+
|
|
162
|
+
setBusy(false);
|
|
163
|
+
|
|
164
|
+
if (made) {
|
|
165
|
+
// The server lists it a moment after it answers: choose it once this tab has heard.
|
|
166
|
+
await listed(made.id);
|
|
167
|
+
notify(
|
|
168
|
+
"info",
|
|
169
|
+
`Made “${name}”: its folder is in the data folder's themes/. Edit with Pi changes it.`,
|
|
170
|
+
);
|
|
171
|
+
onDone(made.id);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return html`<form class="look-duplicate" onSubmit=${make}>
|
|
176
|
+
<input
|
|
177
|
+
ref=${field}
|
|
178
|
+
value=${name}
|
|
179
|
+
aria-label="The new theme's name"
|
|
180
|
+
maxlength="60"
|
|
181
|
+
onInput=${(event) => setName(event.currentTarget.value)}
|
|
182
|
+
/>
|
|
183
|
+
<button class="button primary" disabled=${busy || name.trim() === ""}>Make</button>
|
|
184
|
+
<button class="button ghost" type="button" onClick=${() => onDone(null)}>Cancel</button>
|
|
185
|
+
</form>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Start a session in one of the owner's themes, with a message that says where its guide is. */
|
|
189
|
+
async function editWithPi(id) {
|
|
190
|
+
const { cwd, draft } = await api(`themes/${encodeURIComponent(id)}/edit`);
|
|
191
|
+
const { id: conversationId } = await actions.createSession(cwd);
|
|
192
|
+
|
|
193
|
+
drafts.set(conversationId, draft);
|
|
194
|
+
closeSheet();
|
|
195
|
+
navigate(conversationId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The owner's tools for the theme in use: copy it, change it with Pi, or remove it. */
|
|
199
|
+
function LookTools() {
|
|
200
|
+
const [naming, setNaming] = useState(false);
|
|
201
|
+
const id = prefs().look ?? BASE_LOOK;
|
|
202
|
+
const look = lookInfo(id);
|
|
203
|
+
|
|
204
|
+
if (naming) {
|
|
205
|
+
return html`<${Duplicate}
|
|
206
|
+
from=${id}
|
|
207
|
+
label=${look?.name ?? "theme"}
|
|
208
|
+
onDone=${(made) => {
|
|
209
|
+
setNaming(false);
|
|
210
|
+
|
|
211
|
+
if (made) {
|
|
212
|
+
chooseLook(made);
|
|
213
|
+
}
|
|
214
|
+
}}
|
|
215
|
+
/>`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const remove = () =>
|
|
219
|
+
confirm(
|
|
220
|
+
`Remove the theme “${look.name}”?\n\nIts folder moves to themes/.removed/ in the data folder, so you can get it back from there.`,
|
|
221
|
+
) &&
|
|
222
|
+
attempt(async () => {
|
|
223
|
+
await api(`themes/${encodeURIComponent(id)}`, undefined, { method: "DELETE" });
|
|
224
|
+
setPrefs({ look: BASE_LOOK });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return html`<div class="look-tools">
|
|
228
|
+
<button class="button" disabled=${!look?.usable} onClick=${() => setNaming(true)}>
|
|
229
|
+
<${Icon} name="copy" size=${14} /> Duplicate
|
|
230
|
+
</button>
|
|
231
|
+
${
|
|
232
|
+
look && !look.builtIn
|
|
233
|
+
? html`<button class="button" onClick=${() => attempt(() => editWithPi(id))}>
|
|
234
|
+
<${Icon} name="sparkle" size=${14} /> Edit with Pi
|
|
235
|
+
</button>
|
|
236
|
+
${
|
|
237
|
+
galleryAvailable() &&
|
|
238
|
+
html`<button
|
|
239
|
+
class="button"
|
|
240
|
+
onClick=${() => openSheet({ type: "gallery-share", id, theme: true })}
|
|
241
|
+
>
|
|
242
|
+
Share…
|
|
243
|
+
</button>`
|
|
244
|
+
}
|
|
245
|
+
<button class="button ghost" onClick=${remove}>Remove</button>`
|
|
246
|
+
: html`<span class="muted small">Duplicate it to make a theme of your own.</span>`
|
|
247
|
+
}
|
|
248
|
+
</div>`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The conversation's text size: a few usual sizes, and a field for any other, MIN_TEXT to MAX_TEXT in half pixels. The
|
|
253
|
+
* field takes a size once it is one (14 on the way to 145 shows at once); Enter or leaving it keeps it in range.
|
|
254
|
+
*/
|
|
255
|
+
function TextSize({ size }) {
|
|
256
|
+
const [draft, setDraft] = useState(null);
|
|
257
|
+
|
|
258
|
+
// A size set another way (a button, another tab) shows at once, over what was being typed; what is being typed
|
|
259
|
+
// stays while it means this size.
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (draft !== null && textSizeOf(draft) !== size) {
|
|
262
|
+
setDraft(null);
|
|
263
|
+
}
|
|
264
|
+
}, [size]);
|
|
265
|
+
|
|
266
|
+
const commit = (value) => {
|
|
267
|
+
setDraft(null);
|
|
268
|
+
setPrefs({ text: textSizeOf(value) });
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return html`<div class="text-size" role="group" aria-label="Text size">
|
|
272
|
+
<div class="segmented">
|
|
273
|
+
${[13, 15, 17, 20].map(
|
|
274
|
+
(each) => html`<button
|
|
275
|
+
class=${size === each ? "on" : ""}
|
|
276
|
+
aria-pressed=${size === each}
|
|
277
|
+
onClick=${() => commit(each)}
|
|
278
|
+
>
|
|
279
|
+
${each}
|
|
280
|
+
</button>`,
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
<label class="text-size-field">
|
|
284
|
+
<input
|
|
285
|
+
type="number"
|
|
286
|
+
inputmode="decimal"
|
|
287
|
+
min=${MIN_TEXT}
|
|
288
|
+
max=${MAX_TEXT}
|
|
289
|
+
step=${STEP_TEXT}
|
|
290
|
+
aria-label="Text size in pixels"
|
|
291
|
+
value=${draft ?? size}
|
|
292
|
+
onInput=${(event) => {
|
|
293
|
+
const value = event.currentTarget.value;
|
|
294
|
+
const number = Number(value);
|
|
295
|
+
|
|
296
|
+
setDraft(value);
|
|
297
|
+
|
|
298
|
+
if (value !== "" && number >= MIN_TEXT && number <= MAX_TEXT) {
|
|
299
|
+
setPrefs({ text: textSizeOf(number) });
|
|
300
|
+
}
|
|
301
|
+
}}
|
|
302
|
+
onChange=${(event) =>
|
|
303
|
+
commit(event.currentTarget.value === "" ? size : event.currentTarget.value)}
|
|
304
|
+
onBlur=${() => setDraft(null)}
|
|
305
|
+
onKeyDown=${(event) => event.key === "Enter" && event.currentTarget.blur()}
|
|
306
|
+
/>
|
|
307
|
+
<span>px</span>
|
|
308
|
+
</label>
|
|
309
|
+
</div>`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The fonts Appearance sets in a theme (themes.ts THEME_FONTS): the family, and its name here. */
|
|
313
|
+
const TEXT_FONTS = [
|
|
314
|
+
["Pocket Sans", "Geist"],
|
|
315
|
+
["Pocket Inter", "Inter"],
|
|
316
|
+
["system-ui", "System Sans"],
|
|
317
|
+
["Pocket Mono", "JetBrains Mono"],
|
|
318
|
+
["Pocket Fira Code", "Fira Code"],
|
|
319
|
+
["Pocket IBM Plex Mono", "IBM Plex Mono"],
|
|
320
|
+
["Pocket Iosevka", "Iosevka"],
|
|
321
|
+
["ui-monospace", "System Mono"],
|
|
322
|
+
];
|
|
323
|
+
const CODE_FONTS = TEXT_FONTS.slice(3);
|
|
324
|
+
|
|
325
|
+
/** A font's name here, by the family a stylesheet names first; the app's own for none. */
|
|
326
|
+
const fontName = (fonts, family) =>
|
|
327
|
+
(fonts.find(([each]) => each === family) ?? (family ? [family, family] : fonts[0]))[1];
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The fonts of the theme in use: for words, for code, and ligatures, with a preview. The owner sets them on a theme of
|
|
331
|
+
* their own, saved into it (its `fonts.css`), so everyone who uses it sees them; a theme that comes with the app shows
|
|
332
|
+
* its own, with a way to make a copy that can change.
|
|
333
|
+
*/
|
|
334
|
+
function FontTools() {
|
|
335
|
+
const [naming, setNaming] = useState(false);
|
|
336
|
+
const id = prefs().look ?? BASE_LOOK;
|
|
337
|
+
const look = lookInfo(id);
|
|
338
|
+
|
|
339
|
+
if (!look?.usable) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const set = look.typography ?? { ligatures: false };
|
|
344
|
+
const own = set.own ?? {};
|
|
345
|
+
// Only what changed: the server keeps the rest as it was saved, and null goes back to the theme's own.
|
|
346
|
+
const save = (patch) =>
|
|
347
|
+
attempt(() => api(`themes/${encodeURIComponent(id)}/fonts`, patch, { method: "PUT" }));
|
|
348
|
+
// A family of the theme's own, not one of these, shows as itself, as "": choosing it goes back to it.
|
|
349
|
+
const custom = (fonts, key) =>
|
|
350
|
+
[set[key], own[key]].find((family) => family && !fonts.some(([each]) => each === family));
|
|
351
|
+
|
|
352
|
+
const menu = (label, fonts, key) => {
|
|
353
|
+
const theirs = custom(fonts, key);
|
|
354
|
+
const value = theirs !== undefined && set[key] === theirs ? "" : (set[key] ?? fonts[0][0]);
|
|
355
|
+
|
|
356
|
+
return look.builtIn
|
|
357
|
+
? html`<div class="font-menu">
|
|
358
|
+
<span>${label}</span>
|
|
359
|
+
<strong>${fontName(fonts, set[key])}</strong>
|
|
360
|
+
</div>`
|
|
361
|
+
: html`<label class="font-menu">
|
|
362
|
+
<span>${label}</span>
|
|
363
|
+
<select
|
|
364
|
+
value=${value}
|
|
365
|
+
onChange=${(event) => save({ [key]: event.currentTarget.value || null })}
|
|
366
|
+
>
|
|
367
|
+
${fonts.map(
|
|
368
|
+
([family, name]) => html`<option value=${family}>${name}</option>`,
|
|
369
|
+
)}
|
|
370
|
+
${theirs && html`<option value="">${theirs}</option>`}
|
|
371
|
+
</select>
|
|
372
|
+
</label>`;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return html`<section class="font-tools" aria-label=${`Fonts for ${look.name}`}>
|
|
376
|
+
<h4 class="font-title">Fonts for <em>${look.name}</em></h4>
|
|
377
|
+
${menu("Text font", TEXT_FONTS, "sans")}
|
|
378
|
+
${menu("Code font", CODE_FONTS, "mono")}
|
|
379
|
+
<div class="font-menu">
|
|
380
|
+
<span>Ligatures</span>
|
|
381
|
+
${
|
|
382
|
+
look.builtIn
|
|
383
|
+
? html`<strong>${set.ligatures ? "On" : "Off"}</strong>`
|
|
384
|
+
: html`<div class="font-ligatures">
|
|
385
|
+
<${Switch}
|
|
386
|
+
on=${set.ligatures}
|
|
387
|
+
label="Ligatures"
|
|
388
|
+
onChange=${() => save({ ligatures: !set.ligatures })}
|
|
389
|
+
/>
|
|
390
|
+
<button
|
|
391
|
+
class="button small ghost"
|
|
392
|
+
onClick=${() => save({ ligatures: null })}
|
|
393
|
+
>
|
|
394
|
+
Theme's own
|
|
395
|
+
</button>
|
|
396
|
+
</div>`
|
|
397
|
+
}
|
|
398
|
+
</div>
|
|
399
|
+
<div class="font-preview" role="group" aria-label="Font preview">
|
|
400
|
+
The quick brown fox jumps over the lazy dog. <em>Emphasis.</em>
|
|
401
|
+
<code>const ok = (x) => x !== 42; <span class="hl-com">// a comment</span></code>
|
|
402
|
+
</div>
|
|
403
|
+
${
|
|
404
|
+
look.builtIn &&
|
|
405
|
+
(naming
|
|
406
|
+
? html`<div class="font-duplicate">
|
|
407
|
+
<${Duplicate}
|
|
408
|
+
from=${id}
|
|
409
|
+
label=${look.name}
|
|
410
|
+
onDone=${(made) => {
|
|
411
|
+
setNaming(false);
|
|
412
|
+
|
|
413
|
+
if (made) {
|
|
414
|
+
chooseLook(made);
|
|
415
|
+
}
|
|
416
|
+
}}
|
|
417
|
+
/>
|
|
418
|
+
</div>`
|
|
419
|
+
: html`<div class="font-duplicate">
|
|
420
|
+
<button class="button" onClick=${() => setNaming(true)}>
|
|
421
|
+
<${Icon} name="copy" size=${14} /> Duplicate to change
|
|
422
|
+
</button>
|
|
423
|
+
<span class="muted small">
|
|
424
|
+
${look.name} comes with the app: its fonts stay as they are.
|
|
425
|
+
</span>
|
|
426
|
+
</div>`)
|
|
427
|
+
}
|
|
428
|
+
</section>`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The owner's tools for the colors in use: a copy of them as a theme folder of colors, and for colors of their own, Edit
|
|
433
|
+
* with Pi and Remove.
|
|
434
|
+
*/
|
|
435
|
+
function ColorsTools() {
|
|
436
|
+
const [naming, setNaming] = useState(false);
|
|
437
|
+
const id = prefs().theme;
|
|
438
|
+
const palette = paletteOf(id);
|
|
439
|
+
// The owner's own folder of colors alone (a theme with colors of its own has these in the Theme row).
|
|
440
|
+
const own = palette.folder
|
|
441
|
+
? store.state.themeList?.find(
|
|
442
|
+
(each) => each.id === palette.id.split("@")[0] && colorsOnly(each) && !each.builtIn,
|
|
443
|
+
)
|
|
444
|
+
: undefined;
|
|
445
|
+
|
|
446
|
+
if (naming) {
|
|
447
|
+
return html`<${Duplicate}
|
|
448
|
+
from=${palette.id}
|
|
449
|
+
label=${palette.name}
|
|
450
|
+
colors=${true}
|
|
451
|
+
onDone=${(made) => {
|
|
452
|
+
setNaming(false);
|
|
453
|
+
|
|
454
|
+
if (made) {
|
|
455
|
+
chooseTheme(made);
|
|
456
|
+
}
|
|
457
|
+
}}
|
|
458
|
+
/>`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const remove = () =>
|
|
462
|
+
confirm(
|
|
463
|
+
`Remove the colors “${own.name}”?\n\nTheir folder moves to themes/.removed/ in the data folder, so you can get them back from there.`,
|
|
464
|
+
) &&
|
|
465
|
+
attempt(async () => {
|
|
466
|
+
await api(`themes/${encodeURIComponent(own.id)}`, undefined, { method: "DELETE" });
|
|
467
|
+
setPrefs({ theme: "desktop" });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return html`<div class="look-tools">
|
|
471
|
+
<button class="button" onClick=${() => setNaming(true)}>
|
|
472
|
+
<${Icon} name="copy" size=${14} /> Duplicate colors
|
|
473
|
+
</button>
|
|
474
|
+
${
|
|
475
|
+
own &&
|
|
476
|
+
html`<button class="button" onClick=${() => attempt(() => editWithPi(own.id))}>
|
|
477
|
+
<${Icon} name="sparkle" size=${14} /> Edit with Pi
|
|
478
|
+
</button>
|
|
479
|
+
<button class="button ghost" onClick=${remove}>Remove</button>`
|
|
480
|
+
}
|
|
481
|
+
</div>`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The owner's default: the theme and colors of every browser whose person has not chosen their own, guests' included.
|
|
486
|
+
* It takes the ones in use here.
|
|
487
|
+
*/
|
|
488
|
+
function DefaultTools() {
|
|
489
|
+
const current = themeDefault();
|
|
490
|
+
const p = prefs();
|
|
491
|
+
// The theme and colors in use as the server knows them, as Appearance shows them: a theme that is gone or broken
|
|
492
|
+
// shows Pocket, and "desktop" colors only where there is a desktop's.
|
|
493
|
+
const look = store.state.themeList?.some((each) => each.id === p.look && each.usable)
|
|
494
|
+
? p.look
|
|
495
|
+
: BASE_LOOK;
|
|
496
|
+
const colors = paletteOf(p.theme).id;
|
|
497
|
+
// What new browsers get already: the default, or with none, Pocket and the desktop's colors.
|
|
498
|
+
const same = current
|
|
499
|
+
? current.look === look && current.colors === colors
|
|
500
|
+
: look === BASE_LOOK && p.theme === "desktop";
|
|
501
|
+
// A theme with its own colors is named once: "My Lantern", not "My Lantern with My Lantern". The desktop's colors
|
|
502
|
+
// are Tokyo Night where the server has no desktop.
|
|
503
|
+
const name = (look, colors) =>
|
|
504
|
+
colors.split("@")[0] === look
|
|
505
|
+
? (lookInfo(look)?.name ?? look)
|
|
506
|
+
: `${lookInfo(look)?.name ?? look} with ${paletteOf(colors).desktop ? "the desktop's colors" : paletteOf(colors).name}`;
|
|
507
|
+
const set = (value) =>
|
|
508
|
+
attempt(async () => {
|
|
509
|
+
await api("themes/default", { default: value }, { method: "PUT" });
|
|
510
|
+
notify(
|
|
511
|
+
"info",
|
|
512
|
+
value
|
|
513
|
+
? `Browsers that have not chosen start with ${name(value.look, value.colors)}.`
|
|
514
|
+
: "Browsers that have not chosen start with Pocket.",
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return html`<div class="look-tools">
|
|
519
|
+
<span class="muted small grow">
|
|
520
|
+
${current ? `For new browsers: ${name(current.look, current.colors)}.` : `New browsers start with ${name(BASE_LOOK, "desktop")}.`}
|
|
521
|
+
</span>
|
|
522
|
+
${
|
|
523
|
+
!same &&
|
|
524
|
+
html`<button class="button" onClick=${() => set({ look, colors })}>
|
|
525
|
+
Use these for new browsers
|
|
526
|
+
</button>`
|
|
527
|
+
}
|
|
528
|
+
${current && html`<button class="button ghost" onClick=${() => set(null)}>Reset</button>`}
|
|
529
|
+
</div>`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** A new density, with the choice tapped kept where it was: the rows above it grow or shrink with the change. */
|
|
533
|
+
function chooseDensity(density, event) {
|
|
534
|
+
const button = event.currentTarget;
|
|
535
|
+
const before = button.getBoundingClientRect().top;
|
|
536
|
+
|
|
537
|
+
setPrefs({ density });
|
|
538
|
+
requestAnimationFrame(() => {
|
|
539
|
+
const body = button.closest(".sheet-body");
|
|
540
|
+
|
|
541
|
+
if (body) {
|
|
542
|
+
body.scrollTop += button.getBoundingClientRect().top - before;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function SettingRow({ title, detail, children }) {
|
|
548
|
+
return html`<div class="setting-row">
|
|
549
|
+
<div class="setting-text">
|
|
550
|
+
<span>${title}</span>
|
|
551
|
+
${detail && html`<span>${detail}</span>`}
|
|
552
|
+
</div>
|
|
553
|
+
${children}
|
|
554
|
+
</div>`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* One slot's items, as Layout lists them: each with a switch to show or hide it and arrows to move it, the app's own and
|
|
559
|
+
* extensions' alike. Fixed ones (the menu, Lancet Guard) always show, in their place. Saved in this browser.
|
|
560
|
+
*/
|
|
561
|
+
function SlotLayout({ slot }) {
|
|
562
|
+
const list = arranged(slot);
|
|
563
|
+
const movable = list.filter(({ item }) => !item.fixed).map(({ item }) => item.id);
|
|
564
|
+
const changed = Object.keys(prefs().layout?.[slot] ?? {}).length > 0;
|
|
565
|
+
|
|
566
|
+
return html`<div class="label slot-label">
|
|
567
|
+
<span>${SLOTS[slot].label}</span>
|
|
568
|
+
${
|
|
569
|
+
changed &&
|
|
570
|
+
html`<button class="button ghost small" onClick=${() => resetSlot(slot)}>
|
|
571
|
+
Reset
|
|
572
|
+
</button>`
|
|
573
|
+
}
|
|
574
|
+
</div>
|
|
575
|
+
<div class="sheet-list slot-layout" data-slot=${slot}>
|
|
576
|
+
${list.map(({ item, hidden, hiddenBy }) => {
|
|
577
|
+
const at = movable.indexOf(item.id);
|
|
578
|
+
const detail = [
|
|
579
|
+
item.module && `From ${item.module}`,
|
|
580
|
+
hiddenBy && `Hidden by ${hiddenBy}`,
|
|
581
|
+
item.fixed && "Always shown",
|
|
582
|
+
]
|
|
583
|
+
.filter(Boolean)
|
|
584
|
+
.join(" · ");
|
|
585
|
+
|
|
586
|
+
return html`<${SettingRow} key=${item.id} title=${item.label} detail=${detail}>
|
|
587
|
+
${
|
|
588
|
+
!item.fixed &&
|
|
589
|
+
html`<div class="slot-moves">
|
|
590
|
+
<button
|
|
591
|
+
class="icon-button"
|
|
592
|
+
aria-label=${`Move ${item.label} earlier`}
|
|
593
|
+
disabled=${at <= 0}
|
|
594
|
+
onClick=${() => move(slot, item.id, -1)}
|
|
595
|
+
>
|
|
596
|
+
<${Icon} name="chevron" size=${16} class="slot-earlier" />
|
|
597
|
+
</button>
|
|
598
|
+
<button
|
|
599
|
+
class="icon-button"
|
|
600
|
+
aria-label=${`Move ${item.label} later`}
|
|
601
|
+
disabled=${at === movable.length - 1}
|
|
602
|
+
onClick=${() => move(slot, item.id, 1)}
|
|
603
|
+
>
|
|
604
|
+
<${Icon} name="chevron" size=${16} />
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
<${Switch}
|
|
608
|
+
on=${!hidden}
|
|
609
|
+
label=${`Show ${item.label}`}
|
|
610
|
+
onChange=${() => setShown(slot, item.id, hidden)}
|
|
611
|
+
/>`
|
|
612
|
+
}
|
|
613
|
+
<//>`;
|
|
614
|
+
})}
|
|
615
|
+
</div>`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function AppearanceSheet() {
|
|
619
|
+
const p = prefs();
|
|
620
|
+
const desktop = store.state.desktopTheme;
|
|
621
|
+
|
|
622
|
+
useEffect(() => () => preview(null), []);
|
|
623
|
+
|
|
624
|
+
return html`<${Sheet} title="Appearance" onClose=${closeSheet} wide=${true}>
|
|
625
|
+
<div class="label">Theme</div>
|
|
626
|
+
<div class="look-grid">
|
|
627
|
+
${lookIds().map((id) => html`<${LookCard} key=${id} id=${id} />`)}
|
|
628
|
+
</div>
|
|
629
|
+
${store.state.me?.role === "owner" && html`<${LookTools} />`}
|
|
630
|
+
${store.state.me?.role === "owner" && html`<${FontTools} />`}
|
|
631
|
+
<div class="label">Colors</div>
|
|
632
|
+
<div class="theme-grid">
|
|
633
|
+
${desktop && html`<${ThemeCard} id="desktop" desktop=${true} />`}
|
|
634
|
+
${[...themeIds(), ...folderPaletteIds()].map(
|
|
635
|
+
(id) => html`<${ThemeCard} key=${id} id=${id} />`,
|
|
636
|
+
)}
|
|
637
|
+
</div>
|
|
638
|
+
${store.state.me?.role === "owner" && html`<${ColorsTools} />`}
|
|
639
|
+
${store.state.me?.role === "owner" && html`<${DefaultTools} />`}
|
|
640
|
+
${
|
|
641
|
+
!desktop &&
|
|
642
|
+
html`<p class="muted small">
|
|
643
|
+
Running Pi Pocket on an Omarchy desktop adds “Desktop”: the app follows the theme the desktop uses.
|
|
644
|
+
</p>`
|
|
645
|
+
}
|
|
646
|
+
<div class="label">Layout and motion</div>
|
|
647
|
+
<div class="sheet-list">
|
|
648
|
+
<${SettingRow}
|
|
649
|
+
title="Tiled windows"
|
|
650
|
+
detail="Wide screens: the sidebar and the session as Hyprland windows, with gaps and the active border"
|
|
651
|
+
>
|
|
652
|
+
<${Switch}
|
|
653
|
+
on=${p.tiling}
|
|
654
|
+
label="Tiled windows"
|
|
655
|
+
onChange=${() => setPrefs({ tiling: !p.tiling })}
|
|
656
|
+
/>
|
|
657
|
+
<//>
|
|
658
|
+
<${SettingRow}
|
|
659
|
+
title="Peek tiles"
|
|
660
|
+
detail=${`Other sessions beside this one, live while on screen: working, waiting for you, done since you looked, and pinned. Also the tiles button in the top bar${touch ? "" : ", or Alt+P"}`}
|
|
661
|
+
>
|
|
662
|
+
<${Switch}
|
|
663
|
+
on=${p.peeks}
|
|
664
|
+
label="Peek tiles"
|
|
665
|
+
onChange=${() => setPrefs({ peeks: !p.peeks })}
|
|
666
|
+
/>
|
|
667
|
+
<//>
|
|
668
|
+
${
|
|
669
|
+
desktop?.wallpaper &&
|
|
670
|
+
html`<${SettingRow}
|
|
671
|
+
title="Desktop wallpaper"
|
|
672
|
+
detail="Your wallpaper in the gaps between windows, while following the desktop"
|
|
673
|
+
>
|
|
674
|
+
<${Switch}
|
|
675
|
+
on=${p.wallpaper}
|
|
676
|
+
disabled=${!p.tiling || p.theme !== "desktop"}
|
|
677
|
+
label="Desktop wallpaper"
|
|
678
|
+
onChange=${() => setPrefs({ wallpaper: !p.wallpaper })}
|
|
679
|
+
/>
|
|
680
|
+
<//>`
|
|
681
|
+
}
|
|
682
|
+
<${SettingRow}
|
|
683
|
+
title="Sidebar"
|
|
684
|
+
detail="Folded, it shows numbered sessions like workspaces"
|
|
685
|
+
>
|
|
686
|
+
<div class="segmented">
|
|
687
|
+
${[
|
|
688
|
+
["open", "Open"],
|
|
689
|
+
["rail", "Folded"],
|
|
690
|
+
].map(
|
|
691
|
+
([value, label]) =>
|
|
692
|
+
html`<button
|
|
693
|
+
class=${p.sidebar === value ? "on" : ""}
|
|
694
|
+
aria-pressed=${p.sidebar === value}
|
|
695
|
+
onClick=${() => setPrefs({ sidebar: value })}
|
|
696
|
+
>
|
|
697
|
+
${label}
|
|
698
|
+
</button>`,
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
<//>
|
|
702
|
+
<${SettingRow}
|
|
703
|
+
title="Motion"
|
|
704
|
+
detail="Auto follows your system's reduced-motion setting. Fluid adds springs, staggered entrances, and layout changes that morph"
|
|
705
|
+
>
|
|
706
|
+
<div class="segmented">
|
|
707
|
+
${[
|
|
708
|
+
["auto", "Auto"],
|
|
709
|
+
["full", "Full"],
|
|
710
|
+
["fluid", "Fluid"],
|
|
711
|
+
["reduced", "Reduced"],
|
|
712
|
+
].map(
|
|
713
|
+
([value, label]) =>
|
|
714
|
+
html`<button
|
|
715
|
+
class=${p.motion === value ? "on" : ""}
|
|
716
|
+
aria-pressed=${p.motion === value}
|
|
717
|
+
onClick=${() => setPrefs({ motion: value })}
|
|
718
|
+
>
|
|
719
|
+
${label}
|
|
720
|
+
</button>`,
|
|
721
|
+
)}
|
|
722
|
+
</div>
|
|
723
|
+
<//>
|
|
724
|
+
<${SettingRow}
|
|
725
|
+
title="Text size"
|
|
726
|
+
detail=${`The conversation's, in pixels: any from ${MIN_TEXT} to ${MAX_TEXT}`}
|
|
727
|
+
>
|
|
728
|
+
<${TextSize} size=${textSizeOf(p.text)} />
|
|
729
|
+
<//>
|
|
730
|
+
<${SettingRow}
|
|
731
|
+
title="Width"
|
|
732
|
+
detail="Wide screens: how much of the window the conversation and the message box take. Comfortable grows with the window"
|
|
733
|
+
>
|
|
734
|
+
<div class="segmented">
|
|
735
|
+
${[
|
|
736
|
+
["comfortable", "Comfortable"],
|
|
737
|
+
["wide", "Wide"],
|
|
738
|
+
["full", "Full"],
|
|
739
|
+
].map(
|
|
740
|
+
([value, label]) =>
|
|
741
|
+
html`<button
|
|
742
|
+
class=${(p.width ?? "comfortable") === value ? "on" : ""}
|
|
743
|
+
aria-pressed=${(p.width ?? "comfortable") === value}
|
|
744
|
+
onClick=${() => setPrefs({ width: value })}
|
|
745
|
+
>
|
|
746
|
+
${label}
|
|
747
|
+
</button>`,
|
|
748
|
+
)}
|
|
749
|
+
</div>
|
|
750
|
+
<//>
|
|
751
|
+
<${SettingRow}
|
|
752
|
+
title="Density"
|
|
753
|
+
detail="Less room around messages, rows, and the message box, to fit more on a screen"
|
|
754
|
+
>
|
|
755
|
+
<div class="segmented">
|
|
756
|
+
${[
|
|
757
|
+
["comfortable", "Comfortable"],
|
|
758
|
+
["compact", "Compact"],
|
|
759
|
+
["tight", "Tight"],
|
|
760
|
+
].map(
|
|
761
|
+
([value, label]) =>
|
|
762
|
+
html`<button
|
|
763
|
+
class=${p.density === value ? "on" : ""}
|
|
764
|
+
aria-pressed=${p.density === value}
|
|
765
|
+
onClick=${(event) => chooseDensity(value, event)}
|
|
766
|
+
>
|
|
767
|
+
${label}
|
|
768
|
+
</button>`,
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
<//>
|
|
772
|
+
</div>
|
|
773
|
+
<div class="label">Conversation</div>
|
|
774
|
+
<div class="sheet-list">
|
|
775
|
+
<${SettingRow}
|
|
776
|
+
title="Show thinking"
|
|
777
|
+
detail=${`Shows Pi's current thought above the live line as it comes, and its finished thoughts. The chevron before the line folds the live one${touch ? "" : ". Also Alt+T"}`}
|
|
778
|
+
>
|
|
779
|
+
<${Switch}
|
|
780
|
+
on=${p.showThinking}
|
|
781
|
+
label="Show thinking"
|
|
782
|
+
onChange=${() => setPrefs({ showThinking: !p.showThinking })}
|
|
783
|
+
/>
|
|
784
|
+
<//>
|
|
785
|
+
<${SettingRow}
|
|
786
|
+
title="Keep activity folded"
|
|
787
|
+
detail="Pi's thinking and tool calls stay on their one line until you open them, also while it works and in the newest turn"
|
|
788
|
+
>
|
|
789
|
+
<${Switch}
|
|
790
|
+
on=${p.foldActivity}
|
|
791
|
+
label="Keep activity folded"
|
|
792
|
+
onChange=${() => setPrefs({ foldActivity: !p.foldActivity })}
|
|
793
|
+
/>
|
|
794
|
+
<//>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="label">Status line</div>
|
|
797
|
+
<div class="sheet-list">
|
|
798
|
+
<${SettingRow}
|
|
799
|
+
title="Reply speed"
|
|
800
|
+
detail="Under the message box: how fast the last answer came, in tokens a second, and how long its first token took"
|
|
801
|
+
>
|
|
802
|
+
<${Switch}
|
|
803
|
+
on=${p.speed}
|
|
804
|
+
label="Reply speed"
|
|
805
|
+
onChange=${() => setPrefs({ speed: !p.speed })}
|
|
806
|
+
/>
|
|
807
|
+
<//>
|
|
808
|
+
</div>
|
|
809
|
+
${Object.keys(SLOTS).map((slot) => html`<${SlotLayout} key=${slot} slot=${slot} />`)}
|
|
810
|
+
${
|
|
811
|
+
!touch &&
|
|
812
|
+
html`<button
|
|
813
|
+
class="button ghost wide sheet-footer"
|
|
814
|
+
onClick=${() => openSheet({ type: "shortcuts" })}
|
|
815
|
+
>
|
|
816
|
+
<${Icon} name="keyboard" size=${16} /> Keyboard shortcuts
|
|
817
|
+
</button>`
|
|
818
|
+
}
|
|
819
|
+
<//>`;
|
|
820
|
+
}
|