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,680 @@
|
|
|
1
|
+
// The folder picker, as a desktop's file dialog: places on the left (home, the computer, recent folders), the folders
|
|
2
|
+
// where you are on the right, and Cancel and Open at the bottom. With a mouse, a click selects a folder and a double
|
|
3
|
+
// click goes into it; on a touch screen, a tap goes into it. Open takes the selected folder, or the one you are in.
|
|
4
|
+
// It opens in the folder of the session you are in, so New session here is a tap away without walking the tree;
|
|
5
|
+
// favorites, each person's own and on all their devices, sit first among the places; Ctrl/⌘+1…9 starts in one.
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
7
|
+
import {
|
|
8
|
+
actions,
|
|
9
|
+
api,
|
|
10
|
+
attempt,
|
|
11
|
+
canSteer,
|
|
12
|
+
closeSheet,
|
|
13
|
+
navigate,
|
|
14
|
+
notify,
|
|
15
|
+
openSheet,
|
|
16
|
+
scoped,
|
|
17
|
+
store,
|
|
18
|
+
} from "../store.js";
|
|
19
|
+
import { sessionStarted } from "../extension-ui.js";
|
|
20
|
+
import { APPLE, html, Icon, Sheet, shortPath } from "../ui.js";
|
|
21
|
+
import { piSessionsAvailable } from "./pi-sessions.js";
|
|
22
|
+
|
|
23
|
+
/** Whether hidden folders show: kept for the next time the picker opens. */
|
|
24
|
+
const HIDDEN_KEY = "pocket.folderHidden";
|
|
25
|
+
/** Touch screens go into a folder on a tap; a mouse selects on a click and goes in on a double click. */
|
|
26
|
+
const touch = matchMedia("(pointer: coarse)").matches;
|
|
27
|
+
|
|
28
|
+
/** A folder's path with a name added: `/a` and `b` make `/a/b`, `/` and `b` make `/b`. */
|
|
29
|
+
const joined = (folder, name) => `${folder.replace(/\/+$/, "")}/${name.replace(/^\/+/, "")}`;
|
|
30
|
+
|
|
31
|
+
/** Whether a failure says a folder is not there yet, and so could be made: only a whole path, as the server makes no other. */
|
|
32
|
+
const notThere = (error, target) =>
|
|
33
|
+
error.status === 404 && /isn't there/.test(error.message) && /^\s*[~/]/.test(target);
|
|
34
|
+
|
|
35
|
+
/** A folder's name: the last part of its path, or ~ for home and / for the root. */
|
|
36
|
+
function nameOf(path, home) {
|
|
37
|
+
if (path === home) {
|
|
38
|
+
return "~";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return path.replace(/\/+$/, "").split("/").pop() || "/";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The path as steps to click back up to: ~ or /, then each folder under it. */
|
|
45
|
+
function crumbsOf(path, home) {
|
|
46
|
+
const underHome = Boolean(home) && (path === home || path.startsWith(`${home}/`));
|
|
47
|
+
const start = underHome ? home : "";
|
|
48
|
+
const crumbs = [{ name: underHome ? "~" : "/", path: underHome ? home : "/" }];
|
|
49
|
+
let at = start;
|
|
50
|
+
|
|
51
|
+
for (const part of path.slice(start.length).split("/").filter(Boolean)) {
|
|
52
|
+
at = `${at}/${part}`;
|
|
53
|
+
crumbs.push({ name: part, path: at });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return crumbs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Recent folders' labels: each one's name, with its parent's where two have the same name. */
|
|
60
|
+
export function recentLabels(paths, home) {
|
|
61
|
+
const names = paths.map((path) => nameOf(path, home));
|
|
62
|
+
|
|
63
|
+
return paths.map((path, index) =>
|
|
64
|
+
names.indexOf(names[index]) === names.lastIndexOf(names[index])
|
|
65
|
+
? names[index]
|
|
66
|
+
: shortPath(path, home).split("/").slice(-2).join("/"),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Marks the path's field when its start is scrolled out of sight, so it fades there. */
|
|
71
|
+
const markCut = (crumbs) => crumbs.classList.toggle("cut", crumbs.scrollLeft > 0);
|
|
72
|
+
|
|
73
|
+
/** A line to type a name or a path into: Enter takes it, Esc lets it go. */
|
|
74
|
+
function LineInput({ value, placeholder, label, onSubmit, onCancel, class: className = "" }) {
|
|
75
|
+
const [text, setText] = useState(value ?? "");
|
|
76
|
+
const input = useRef(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
input.current?.focus();
|
|
80
|
+
input.current?.setSelectionRange(text.length, text.length);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
return html`<form
|
|
84
|
+
class=${`fp-line ${className}`}
|
|
85
|
+
onSubmit=${(event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
|
|
88
|
+
if (text.trim() !== "") {
|
|
89
|
+
onSubmit(text.trim());
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<input
|
|
94
|
+
ref=${input}
|
|
95
|
+
class="mono"
|
|
96
|
+
value=${text}
|
|
97
|
+
placeholder=${placeholder}
|
|
98
|
+
aria-label=${label}
|
|
99
|
+
autocapitalize="off"
|
|
100
|
+
autocomplete="off"
|
|
101
|
+
spellcheck="false"
|
|
102
|
+
enterkeyhint="go"
|
|
103
|
+
onInput=${(event) => setText(event.currentTarget.value)}
|
|
104
|
+
onKeyDown=${(event) => {
|
|
105
|
+
// Esc lets go of the line, not the whole picker.
|
|
106
|
+
if (event.key === "Escape") {
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
onCancel();
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
<button class="button small primary" type="submit" disabled=${text.trim() === ""}>
|
|
113
|
+
OK
|
|
114
|
+
</button>
|
|
115
|
+
<button class="icon-button" type="button" aria-label="Cancel" onClick=${onCancel}>
|
|
116
|
+
<${Icon} name="close" size=${16} />
|
|
117
|
+
</button>
|
|
118
|
+
</form>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function CwdSheet({ mode, fromList = false }) {
|
|
122
|
+
const { view, server, folderFavorites: favorites } = store.state;
|
|
123
|
+
const changing = mode === "change";
|
|
124
|
+
// Where it opens: the folder of the session you are in (New session here), else the server's own. Issue #96.
|
|
125
|
+
const initial = view.agent?.cwd ?? server?.defaultCwd ?? "~";
|
|
126
|
+
const [listing, setListing] = useState(null);
|
|
127
|
+
const [failed, setFailed] = useState(null);
|
|
128
|
+
// The folder a click selected (mouse only): Open takes it over the one you are in.
|
|
129
|
+
const [selected, setSelected] = useState(null);
|
|
130
|
+
const [hidden, setHidden] = useState(() => localStorage.getItem(HIDDEN_KEY) === "1");
|
|
131
|
+
const [worktree, setWorktree] = useState(false);
|
|
132
|
+
// Typing a new folder's name, or a path to go to; and a path typed that is not there yet.
|
|
133
|
+
const [naming, setNaming] = useState(false);
|
|
134
|
+
const [editing, setEditing] = useState(null);
|
|
135
|
+
const [missing, setMissing] = useState(null);
|
|
136
|
+
// Why a favorite in `missing` does not open, when it is there but is no folder to open (a link to nothing, a file).
|
|
137
|
+
const [missingWhy, setMissingWhy] = useState(null);
|
|
138
|
+
const [busy, setBusy] = useState(false);
|
|
139
|
+
// Set at once, unlike busy: a second shortcut in the same moment does not start a second session.
|
|
140
|
+
const starting = useRef(false);
|
|
141
|
+
const [saving, setSaving] = useState(false);
|
|
142
|
+
const list = useRef(null);
|
|
143
|
+
const crumbsBox = useRef(null);
|
|
144
|
+
// Only the latest read shows: a slow one answering after a later click is dropped.
|
|
145
|
+
const asked = useRef(0);
|
|
146
|
+
// Letters typed in the list jump to the folder they start; they add up for a moment.
|
|
147
|
+
const typed = useRef({ text: "", at: 0 });
|
|
148
|
+
const home = listing?.home ?? server?.home;
|
|
149
|
+
const folder = listing?.path ?? null;
|
|
150
|
+
const dirs = listing?.dirs ?? [];
|
|
151
|
+
const target = selected ?? folder;
|
|
152
|
+
const here = changing && target !== null && target === view.agent?.cwd;
|
|
153
|
+
|
|
154
|
+
/** Show a folder; `select`, a folder in it to select (the one stepped up from, with a mouse). */
|
|
155
|
+
const load = (path, { showHidden = hidden, select = null } = {}) => {
|
|
156
|
+
const ask = ++asked.current;
|
|
157
|
+
|
|
158
|
+
return api(`fs?path=${encodeURIComponent(path)}${showHidden ? "&hidden=1" : ""}`).then(
|
|
159
|
+
(result) => {
|
|
160
|
+
if (ask !== asked.current) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setListing(result);
|
|
165
|
+
setFailed(null);
|
|
166
|
+
setSelected(touch ? null : select);
|
|
167
|
+
setMissing(null);
|
|
168
|
+
setEditing(null);
|
|
169
|
+
setNaming(false);
|
|
170
|
+
},
|
|
171
|
+
(error) => {
|
|
172
|
+
if (ask !== asked.current) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A favorite that does not open, however, gets the banner, which can remove it.
|
|
177
|
+
if (notThere(error, path) || store.state.folderFavorites.includes(path)) {
|
|
178
|
+
setMissing(path);
|
|
179
|
+
setMissingWhy(notThere(error, path) ? null : error.message);
|
|
180
|
+
setEditing(null);
|
|
181
|
+
} else if (listing) {
|
|
182
|
+
notify("error", error.message);
|
|
183
|
+
} else {
|
|
184
|
+
setFailed(error.message);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
load(initial);
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
// A new folder shows from its top, or with the selected one in view; its path shows to its end. Keys go to the
|
|
195
|
+
// list, with a mouse and keyboard.
|
|
196
|
+
useLayoutEffect(() => {
|
|
197
|
+
if (crumbsBox.current) {
|
|
198
|
+
crumbsBox.current.scrollLeft = crumbsBox.current.scrollWidth;
|
|
199
|
+
markCut(crumbsBox.current);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const row =
|
|
203
|
+
selected && list.current?.querySelector(`.fp-row[data-path="${CSS.escape(selected)}"]`);
|
|
204
|
+
|
|
205
|
+
if (row) {
|
|
206
|
+
row.scrollIntoView({ block: "nearest" });
|
|
207
|
+
} else if (list.current) {
|
|
208
|
+
list.current.scrollTop = 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!touch && !naming && editing === null) {
|
|
212
|
+
list.current?.focus({ preventScroll: true });
|
|
213
|
+
}
|
|
214
|
+
}, [folder]);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (selected) {
|
|
218
|
+
list.current
|
|
219
|
+
?.querySelector(`.fp-row[data-path="${CSS.escape(selected)}"]`)
|
|
220
|
+
?.scrollIntoView({ block: "nearest" });
|
|
221
|
+
}
|
|
222
|
+
}, [selected]);
|
|
223
|
+
|
|
224
|
+
const up = () => listing?.parent && load(listing.parent, { select: listing.path });
|
|
225
|
+
|
|
226
|
+
/** Make a folder and go into it: Open starts there. */
|
|
227
|
+
const make = async (path) => {
|
|
228
|
+
try {
|
|
229
|
+
const made = await api("fs", { path });
|
|
230
|
+
|
|
231
|
+
await load(made.path);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
notify("error", error.message);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Start the session in the folder Open names (or a favorite's), or move this one there. */
|
|
238
|
+
const use = async (path = target) => {
|
|
239
|
+
if (path === null || starting.current || (changing && path === view.agent?.cwd)) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
starting.current = true;
|
|
244
|
+
setBusy(true);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (changing) {
|
|
248
|
+
await actions.configure({ cwd: path });
|
|
249
|
+
closeSheet();
|
|
250
|
+
} else {
|
|
251
|
+
const created = await actions.createSession(path, { worktree });
|
|
252
|
+
|
|
253
|
+
navigate(created.id);
|
|
254
|
+
|
|
255
|
+
// New in the session list: extensions' filters that are on can make it meet them.
|
|
256
|
+
if (fromList) {
|
|
257
|
+
sessionStarted(created.id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
notify("error", error.message);
|
|
262
|
+
} finally {
|
|
263
|
+
starting.current = false;
|
|
264
|
+
setBusy(false);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const mayStar = canSteer() && !scoped();
|
|
269
|
+
const starred = target !== null && favorites.includes(target);
|
|
270
|
+
|
|
271
|
+
/** Star or unstar a folder. The list changes when the server says it did, on every device. */
|
|
272
|
+
const setFavorite = (path, on) =>
|
|
273
|
+
attempt(async () => {
|
|
274
|
+
setSaving(true);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await api("folder-favorites", { path, starred: on });
|
|
278
|
+
|
|
279
|
+
return true;
|
|
280
|
+
} finally {
|
|
281
|
+
setSaving(false);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const toggleFavorite = () => {
|
|
286
|
+
setFavorite(target, !starred);
|
|
287
|
+
// The list keeps the keys: the star, off while it saves, would drop the focus to the page.
|
|
288
|
+
list.current?.focus({ preventScroll: true });
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Ctrl/⌘+1…9 starts a session in a favorite, while this is the New session picker on top. Caught before the
|
|
292
|
+
// app's own keys; Alt+1…9 (sessions) has Alt, so it never gets here.
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (changing) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const shortcut = (event) => {
|
|
299
|
+
const sheet = store.state.sheet;
|
|
300
|
+
const mod = APPLE ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
|
|
301
|
+
const digit = /^Digit([1-9])$/.exec(event.code);
|
|
302
|
+
const favorite = digit && favorites[Number(digit[1]) - 1];
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
!mod ||
|
|
306
|
+
!favorite ||
|
|
307
|
+
event.altKey ||
|
|
308
|
+
event.shiftKey ||
|
|
309
|
+
event.isComposing ||
|
|
310
|
+
sheet?.type !== "cwd" ||
|
|
311
|
+
sheet.mode !== "new" ||
|
|
312
|
+
store.state.launcher
|
|
313
|
+
) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
event.stopImmediatePropagation();
|
|
319
|
+
|
|
320
|
+
if (!event.repeat) {
|
|
321
|
+
use(favorite);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
addEventListener("keydown", shortcut, true);
|
|
326
|
+
|
|
327
|
+
return () => removeEventListener("keydown", shortcut, true);
|
|
328
|
+
}, [changing, favorites, worktree]);
|
|
329
|
+
|
|
330
|
+
const toggleHidden = () => {
|
|
331
|
+
const next = !hidden;
|
|
332
|
+
|
|
333
|
+
localStorage.setItem(HIDDEN_KEY, next ? "1" : "0");
|
|
334
|
+
setHidden(next);
|
|
335
|
+
|
|
336
|
+
if (folder !== null) {
|
|
337
|
+
load(folder, { showHidden: next, select: selected });
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/** A path typed: whole, or from the folder you are in. */
|
|
342
|
+
const goTo = (text) => load(/^[~/]/.test(text) ? text : joined(folder, text));
|
|
343
|
+
|
|
344
|
+
// The list's keys, as a file dialog's: arrows select, Enter goes into the selected folder (or opens the one you are
|
|
345
|
+
// in), Ctrl+Enter opens, Backspace goes up, letters jump to a folder, and / or ~ starts a path.
|
|
346
|
+
const onKeyDown = (event) => {
|
|
347
|
+
if (event.target !== list.current || event.isComposing) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const at = dirs.findIndex((dir) => dir.path === selected);
|
|
352
|
+
|
|
353
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
|
|
356
|
+
if (event.altKey && event.key === "ArrowUp") {
|
|
357
|
+
up();
|
|
358
|
+
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const next =
|
|
363
|
+
at < 0
|
|
364
|
+
? event.key === "ArrowDown"
|
|
365
|
+
? 0
|
|
366
|
+
: dirs.length - 1
|
|
367
|
+
: Math.max(
|
|
368
|
+
0,
|
|
369
|
+
Math.min(dirs.length - 1, at + (event.key === "ArrowDown" ? 1 : -1)),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
setSelected(dirs[next]?.path ?? null);
|
|
373
|
+
} else if (event.key === "Home" || event.key === "End") {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
setSelected(dirs[event.key === "Home" ? 0 : dirs.length - 1]?.path ?? null);
|
|
376
|
+
} else if (event.key === "Enter") {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
|
|
379
|
+
if (event.ctrlKey || event.metaKey || at < 0) {
|
|
380
|
+
use();
|
|
381
|
+
} else {
|
|
382
|
+
load(selected);
|
|
383
|
+
}
|
|
384
|
+
} else if (event.key === "Backspace") {
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
up();
|
|
387
|
+
} else if ((event.ctrlKey || event.metaKey) && event.key === "l") {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
setEditing(folder ?? "");
|
|
390
|
+
} else if (event.key === "/" || event.key === "~") {
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
setEditing(event.key);
|
|
393
|
+
} else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const text =
|
|
396
|
+
(now - typed.current.at < 800 ? typed.current.text : "") + event.key.toLowerCase();
|
|
397
|
+
const found = dirs.find((dir) => dir.name.toLowerCase().startsWith(text));
|
|
398
|
+
|
|
399
|
+
typed.current = { text, at: now };
|
|
400
|
+
|
|
401
|
+
if (found) {
|
|
402
|
+
setSelected(found.path);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const crumbs = folder === null ? [] : crumbsOf(folder, home);
|
|
408
|
+
const recent = (listing?.recent ?? []).filter((path) => !favorites.includes(path));
|
|
409
|
+
const favoriteLabels = recentLabels(favorites, home);
|
|
410
|
+
const labels = recentLabels(recent, home);
|
|
411
|
+
const places = [
|
|
412
|
+
...(home ? [{ path: home, name: "Home", icon: "home" }] : []),
|
|
413
|
+
{ path: "/", name: "Computer", icon: "monitor" },
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const place = (path, name, icon, key = null) => html`<button
|
|
417
|
+
key=${path}
|
|
418
|
+
type="button"
|
|
419
|
+
class=${`fp-place ${path === folder ? "on" : ""}`}
|
|
420
|
+
title=${shortPath(path, home)}
|
|
421
|
+
aria-current=${path === folder ? "location" : undefined}
|
|
422
|
+
onClick=${() => load(path)}
|
|
423
|
+
>
|
|
424
|
+
<${Icon} name=${icon} size=${15} />
|
|
425
|
+
<span>${name}</span>
|
|
426
|
+
${key && html`<span class="fp-key">${key}</span>`}
|
|
427
|
+
</button>`;
|
|
428
|
+
|
|
429
|
+
return html`<${Sheet}
|
|
430
|
+
title=${changing ? "Move this session: pick a folder" : "New session: pick a folder"}
|
|
431
|
+
onClose=${closeSheet}
|
|
432
|
+
wide=${true}
|
|
433
|
+
className="folder-sheet"
|
|
434
|
+
>
|
|
435
|
+
<div class="fp-bar">
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
class="icon-button"
|
|
439
|
+
aria-label="Up one folder"
|
|
440
|
+
title="Up one folder (Backspace)"
|
|
441
|
+
disabled=${!listing?.parent}
|
|
442
|
+
onClick=${up}
|
|
443
|
+
>
|
|
444
|
+
<${Icon} name="up" size=${18} />
|
|
445
|
+
</button>
|
|
446
|
+
${
|
|
447
|
+
editing !== null
|
|
448
|
+
? html`<${LineInput}
|
|
449
|
+
class="fp-path-input"
|
|
450
|
+
value=${editing}
|
|
451
|
+
placeholder="A path: ~/code or /srv"
|
|
452
|
+
label="Go to a path"
|
|
453
|
+
onSubmit=${goTo}
|
|
454
|
+
onCancel=${() => {
|
|
455
|
+
setEditing(null);
|
|
456
|
+
list.current?.focus({ preventScroll: true });
|
|
457
|
+
}}
|
|
458
|
+
/>`
|
|
459
|
+
: html`<nav
|
|
460
|
+
class="fp-crumbs"
|
|
461
|
+
ref=${crumbsBox}
|
|
462
|
+
onScroll=${(event) => markCut(event.currentTarget)}
|
|
463
|
+
aria-label="Folder"
|
|
464
|
+
title="Click the path's end to type one (Ctrl+L)"
|
|
465
|
+
>
|
|
466
|
+
${crumbs.map((crumb, index) =>
|
|
467
|
+
index === crumbs.length - 1
|
|
468
|
+
? html`<button
|
|
469
|
+
key=${crumb.path}
|
|
470
|
+
type="button"
|
|
471
|
+
class="fp-crumb here"
|
|
472
|
+
aria-current="location"
|
|
473
|
+
aria-label=${`${crumb.name}: type a path`}
|
|
474
|
+
onClick=${() => setEditing(folder)}
|
|
475
|
+
>
|
|
476
|
+
${crumb.name}
|
|
477
|
+
</button>`
|
|
478
|
+
: html`<button
|
|
479
|
+
key=${crumb.path}
|
|
480
|
+
type="button"
|
|
481
|
+
class="fp-crumb"
|
|
482
|
+
onClick=${() => load(crumb.path)}
|
|
483
|
+
>
|
|
484
|
+
${crumb.name}
|
|
485
|
+
</button>
|
|
486
|
+
<${Icon}
|
|
487
|
+
key=${`${crumb.path}/`}
|
|
488
|
+
name="chevron"
|
|
489
|
+
size=${11}
|
|
490
|
+
class="fp-sep"
|
|
491
|
+
/>`,
|
|
492
|
+
)}
|
|
493
|
+
</nav>`
|
|
494
|
+
}
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
class="button small fp-new"
|
|
498
|
+
disabled=${folder === null}
|
|
499
|
+
onClick=${() => {
|
|
500
|
+
setEditing(null);
|
|
501
|
+
setNaming(true);
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
<${Icon} name="plus" size=${14} /> New folder
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
${
|
|
508
|
+
missing !== null &&
|
|
509
|
+
html`<div class="folder-missing">
|
|
510
|
+
<span>
|
|
511
|
+
${missingWhy ?? html`<span class="mono">${shortPath(missing, home)}</span> isn't there.`}
|
|
512
|
+
</span>
|
|
513
|
+
<span class="fp-missing-actions">
|
|
514
|
+
${
|
|
515
|
+
missingWhy === null &&
|
|
516
|
+
html`<button class="button small" onClick=${() => make(missing)}>
|
|
517
|
+
Create it
|
|
518
|
+
</button>`
|
|
519
|
+
}
|
|
520
|
+
${
|
|
521
|
+
// A favorite that is gone is unstarred here: the star acts only on a folder that opens.
|
|
522
|
+
mayStar &&
|
|
523
|
+
favorites.includes(missing) &&
|
|
524
|
+
html`<button
|
|
525
|
+
class="button small"
|
|
526
|
+
disabled=${saving}
|
|
527
|
+
onClick=${() =>
|
|
528
|
+
setFavorite(missing, false).then(
|
|
529
|
+
(done) => done && setMissing(null),
|
|
530
|
+
)}
|
|
531
|
+
>
|
|
532
|
+
Remove from favorites
|
|
533
|
+
</button>`
|
|
534
|
+
}
|
|
535
|
+
<button
|
|
536
|
+
class="icon-button"
|
|
537
|
+
aria-label="Never mind"
|
|
538
|
+
onClick=${() => setMissing(null)}
|
|
539
|
+
>
|
|
540
|
+
<${Icon} name="close" size=${15} />
|
|
541
|
+
</button>
|
|
542
|
+
</span>
|
|
543
|
+
</div>`
|
|
544
|
+
}
|
|
545
|
+
<div class="fp-main">
|
|
546
|
+
<nav class="fp-places" aria-label="Places">
|
|
547
|
+
${places.map((each) => place(each.path, each.name, each.icon))}
|
|
548
|
+
${favorites.length > 0 && html`<div class="fp-places-title">Favorites</div>`}
|
|
549
|
+
${favorites.map((path, index) =>
|
|
550
|
+
place(
|
|
551
|
+
path,
|
|
552
|
+
favoriteLabels[index],
|
|
553
|
+
"star",
|
|
554
|
+
!changing && !touch && index < 9
|
|
555
|
+
? `${APPLE ? "⌘" : "Ctrl+"}${index + 1}`
|
|
556
|
+
: null,
|
|
557
|
+
),
|
|
558
|
+
)}
|
|
559
|
+
${recent.length > 0 && html`<div class="fp-places-title">Recent</div>`}
|
|
560
|
+
${recent.map((path, index) => place(path, labels[index], "folder"))}
|
|
561
|
+
${
|
|
562
|
+
!changing &&
|
|
563
|
+
piSessionsAvailable() &&
|
|
564
|
+
html`<button
|
|
565
|
+
type="button"
|
|
566
|
+
class="link small fp-pi"
|
|
567
|
+
onClick=${() => openSheet({ type: "pi-sessions" })}
|
|
568
|
+
>
|
|
569
|
+
Continue a Pi session…
|
|
570
|
+
</button>`
|
|
571
|
+
}
|
|
572
|
+
</nav>
|
|
573
|
+
<div
|
|
574
|
+
class="fp-list"
|
|
575
|
+
ref=${list}
|
|
576
|
+
role="listbox"
|
|
577
|
+
aria-label="Folders"
|
|
578
|
+
tabindex="0"
|
|
579
|
+
onKeyDown=${onKeyDown}
|
|
580
|
+
>
|
|
581
|
+
${
|
|
582
|
+
naming &&
|
|
583
|
+
html`<${LineInput}
|
|
584
|
+
class="fp-naming"
|
|
585
|
+
placeholder="Folder name"
|
|
586
|
+
label="The new folder's name"
|
|
587
|
+
onSubmit=${(name) => make(joined(folder, name))}
|
|
588
|
+
onCancel=${() => {
|
|
589
|
+
setNaming(false);
|
|
590
|
+
list.current?.focus({ preventScroll: true });
|
|
591
|
+
}}
|
|
592
|
+
/>`
|
|
593
|
+
}
|
|
594
|
+
${failed && html`<p class="fp-note">${failed}</p>`}
|
|
595
|
+
${!listing && !failed && html`<p class="fp-note">Opening…</p>`}
|
|
596
|
+
${dirs.map(
|
|
597
|
+
(dir) => html`<button
|
|
598
|
+
key=${dir.path}
|
|
599
|
+
type="button"
|
|
600
|
+
tabindex="-1"
|
|
601
|
+
class=${`fp-row ${dir.path === selected ? "on" : ""}`}
|
|
602
|
+
role="option"
|
|
603
|
+
aria-selected=${dir.path === selected ? "true" : "false"}
|
|
604
|
+
data-path=${dir.path}
|
|
605
|
+
title=${touch ? undefined : "Double-click to open"}
|
|
606
|
+
onClick=${() => {
|
|
607
|
+
if (touch) {
|
|
608
|
+
load(dir.path);
|
|
609
|
+
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// The list keeps the keys: a click gives the row the focus, which the list's keys ignore.
|
|
614
|
+
setSelected(dir.path);
|
|
615
|
+
list.current?.focus({ preventScroll: true });
|
|
616
|
+
}}
|
|
617
|
+
onDblClick=${() => !touch && load(dir.path)}
|
|
618
|
+
>
|
|
619
|
+
<${Icon} name="folder" size=${16} />
|
|
620
|
+
<span class="fp-name">${dir.name}</span>
|
|
621
|
+
${touch && html`<${Icon} name="chevron" size=${14} class="fp-chev" />`}
|
|
622
|
+
</button>`,
|
|
623
|
+
)}
|
|
624
|
+
${listing && dirs.length === 0 && html`<p class="fp-note">No folders in here.</p>`}
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="fp-foot">
|
|
628
|
+
<div class="fp-target" title=${target ? shortPath(target, home) : ""}>
|
|
629
|
+
<span class="muted">Folder:</span>
|
|
630
|
+
<span class="mono">${target ? shortPath(target, home) : "…"}</span>
|
|
631
|
+
${
|
|
632
|
+
mayStar &&
|
|
633
|
+
target !== null &&
|
|
634
|
+
html`<button
|
|
635
|
+
type="button"
|
|
636
|
+
class=${`icon-button fp-star ${starred ? "on" : ""}`}
|
|
637
|
+
aria-pressed=${starred ? "true" : "false"}
|
|
638
|
+
aria-label=${starred ? "Remove from favorites" : "Add to favorites"}
|
|
639
|
+
title=${starred ? "Remove from favorites" : "Add to favorites"}
|
|
640
|
+
disabled=${saving}
|
|
641
|
+
onClick=${toggleFavorite}
|
|
642
|
+
>
|
|
643
|
+
<${Icon} name="star" size=${15} />
|
|
644
|
+
</button>`
|
|
645
|
+
}
|
|
646
|
+
</div>
|
|
647
|
+
<div class="fp-options">
|
|
648
|
+
${
|
|
649
|
+
!changing &&
|
|
650
|
+
html`<label
|
|
651
|
+
class="check"
|
|
652
|
+
title="The session gets a branch of its own, apart from this folder"
|
|
653
|
+
>
|
|
654
|
+
<input
|
|
655
|
+
type="checkbox"
|
|
656
|
+
checked=${worktree}
|
|
657
|
+
onChange=${(event) => setWorktree(event.currentTarget.checked)}
|
|
658
|
+
/>
|
|
659
|
+
Own git worktree
|
|
660
|
+
</label>`
|
|
661
|
+
}
|
|
662
|
+
<label class="check">
|
|
663
|
+
<input type="checkbox" checked=${hidden} onChange=${toggleHidden} /> Show hidden
|
|
664
|
+
</label>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="fp-buttons">
|
|
667
|
+
<button type="button" class="button" onClick=${closeSheet}>Cancel</button>
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
670
|
+
class="button primary"
|
|
671
|
+
title=${touch ? undefined : "Ctrl+Enter"}
|
|
672
|
+
disabled=${target === null || busy || here}
|
|
673
|
+
onClick=${() => use()}
|
|
674
|
+
>
|
|
675
|
+
${here ? "Already here" : changing ? "Move here" : "Open"}
|
|
676
|
+
</button>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
<//>`;
|
|
680
|
+
}
|