opencode-sesh 2.0.1 → 2.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/CHANGELOG.md +7 -0
- package/README.md +6 -5
- package/dist/tui.js +4359 -0
- package/package.json +10 -4
package/dist/tui.js
ADDED
|
@@ -0,0 +1,4359 @@
|
|
|
1
|
+
import { use as _$use } from "@opentui/solid";
|
|
2
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
3
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
4
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
5
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
6
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
8
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
9
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
|
+
/** @jsxImportSource @opentui/solid */
|
|
11
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
12
|
+
import { createEffect, createMemo, createRoot, createSignal, For, getOwner, on, onCleanup, onMount, runWithOwner, Show } from "solid-js";
|
|
13
|
+
import { mkdir, readFile, rename, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
+
import { getTreeSitterClient, RGBA, SyntaxStyle, TextAttributes } from "@opentui/core";
|
|
18
|
+
import { Portal } from "@opentui/solid";
|
|
19
|
+
let markdownStyleCache;
|
|
20
|
+
function buildMarkdownStyle(theme, owner) {
|
|
21
|
+
return SyntaxStyle.fromTheme([{
|
|
22
|
+
scope: ["default"],
|
|
23
|
+
style: {
|
|
24
|
+
foreground: theme.text
|
|
25
|
+
}
|
|
26
|
+
}, {
|
|
27
|
+
scope: ["comment", "comment.documentation"],
|
|
28
|
+
style: {
|
|
29
|
+
foreground: theme.syntaxComment,
|
|
30
|
+
italic: true
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
scope: ["string", "symbol", "character"],
|
|
34
|
+
style: {
|
|
35
|
+
foreground: theme.syntaxString
|
|
36
|
+
}
|
|
37
|
+
}, {
|
|
38
|
+
scope: ["number", "boolean", "constant", "float"],
|
|
39
|
+
style: {
|
|
40
|
+
foreground: theme.syntaxNumber
|
|
41
|
+
}
|
|
42
|
+
}, {
|
|
43
|
+
scope: ["keyword", "keyword.import", "keyword.directive", "keyword.modifier", "keyword.exception"],
|
|
44
|
+
style: {
|
|
45
|
+
foreground: theme.syntaxKeyword,
|
|
46
|
+
italic: true
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
|
|
50
|
+
style: {
|
|
51
|
+
foreground: theme.syntaxKeyword,
|
|
52
|
+
italic: true
|
|
53
|
+
}
|
|
54
|
+
}, {
|
|
55
|
+
scope: ["keyword.type"],
|
|
56
|
+
style: {
|
|
57
|
+
foreground: theme.syntaxType,
|
|
58
|
+
bold: true,
|
|
59
|
+
italic: true
|
|
60
|
+
}
|
|
61
|
+
}, {
|
|
62
|
+
scope: ["keyword.function", "function.method", "function", "constructor", "variable.member"],
|
|
63
|
+
style: {
|
|
64
|
+
foreground: theme.syntaxFunction
|
|
65
|
+
}
|
|
66
|
+
}, {
|
|
67
|
+
scope: ["operator", "keyword.operator", "punctuation.delimiter", "punctuation.special"],
|
|
68
|
+
style: {
|
|
69
|
+
foreground: theme.syntaxOperator
|
|
70
|
+
}
|
|
71
|
+
}, {
|
|
72
|
+
scope: ["variable", "variable.parameter", "function.method.call", "function.call", "property", "parameter"],
|
|
73
|
+
style: {
|
|
74
|
+
foreground: theme.syntaxVariable
|
|
75
|
+
}
|
|
76
|
+
}, {
|
|
77
|
+
scope: ["type", "module", "class"],
|
|
78
|
+
style: {
|
|
79
|
+
foreground: theme.syntaxType
|
|
80
|
+
}
|
|
81
|
+
}, {
|
|
82
|
+
scope: ["punctuation", "punctuation.bracket"],
|
|
83
|
+
style: {
|
|
84
|
+
foreground: theme.syntaxPunctuation
|
|
85
|
+
}
|
|
86
|
+
}, {
|
|
87
|
+
scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin", "variable.super"],
|
|
88
|
+
style: {
|
|
89
|
+
foreground: theme.error
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
// Some opencode themes give Markdown roles the same foreground as body
|
|
93
|
+
// text. Use their semantic accents here so transcript structure remains
|
|
94
|
+
// legible without recoloring the surrounding TUI or ordinary prose.
|
|
95
|
+
{
|
|
96
|
+
scope: ["markup.heading"],
|
|
97
|
+
style: {
|
|
98
|
+
foreground: theme.accent,
|
|
99
|
+
bold: true
|
|
100
|
+
}
|
|
101
|
+
}, {
|
|
102
|
+
scope: ["markup.heading.1"],
|
|
103
|
+
style: {
|
|
104
|
+
foreground: theme.primary,
|
|
105
|
+
bold: true,
|
|
106
|
+
underline: true
|
|
107
|
+
}
|
|
108
|
+
}, {
|
|
109
|
+
scope: ["markup.heading.2", "markup.heading.3", "markup.heading.4", "markup.heading.5", "markup.heading.6"],
|
|
110
|
+
style: {
|
|
111
|
+
foreground: theme.accent,
|
|
112
|
+
bold: true
|
|
113
|
+
}
|
|
114
|
+
}, {
|
|
115
|
+
scope: ["markup.bold", "markup.strong"],
|
|
116
|
+
style: {
|
|
117
|
+
foreground: theme.accent,
|
|
118
|
+
bold: true
|
|
119
|
+
}
|
|
120
|
+
}, {
|
|
121
|
+
scope: ["markup.italic"],
|
|
122
|
+
style: {
|
|
123
|
+
foreground: theme.syntaxString,
|
|
124
|
+
italic: true
|
|
125
|
+
}
|
|
126
|
+
}, {
|
|
127
|
+
scope: ["markup.list"],
|
|
128
|
+
style: {
|
|
129
|
+
foreground: theme.syntaxKeyword
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
scope: ["markup.quote"],
|
|
133
|
+
style: {
|
|
134
|
+
foreground: theme.markdownBlockQuote,
|
|
135
|
+
italic: true
|
|
136
|
+
}
|
|
137
|
+
}, {
|
|
138
|
+
scope: ["markup.raw", "markup.raw.block"],
|
|
139
|
+
style: {
|
|
140
|
+
foreground: theme.syntaxString
|
|
141
|
+
}
|
|
142
|
+
}, {
|
|
143
|
+
scope: ["markup.raw.inline"],
|
|
144
|
+
style: {
|
|
145
|
+
foreground: theme.syntaxString,
|
|
146
|
+
background: theme.backgroundElement
|
|
147
|
+
}
|
|
148
|
+
}, {
|
|
149
|
+
scope: ["markup.link", "markup.link.url", "string.special", "string.special.url"],
|
|
150
|
+
style: {
|
|
151
|
+
foreground: theme.markdownLink,
|
|
152
|
+
underline: true
|
|
153
|
+
}
|
|
154
|
+
}, {
|
|
155
|
+
scope: ["markup.link.label", "label"],
|
|
156
|
+
style: {
|
|
157
|
+
foreground: theme.markdownLinkText,
|
|
158
|
+
underline: true
|
|
159
|
+
}
|
|
160
|
+
}, {
|
|
161
|
+
scope: ["conceal"],
|
|
162
|
+
style: {
|
|
163
|
+
foreground: theme.textMuted
|
|
164
|
+
}
|
|
165
|
+
}], owner);
|
|
166
|
+
}
|
|
167
|
+
function getMarkdownStyle(theme, owner) {
|
|
168
|
+
if (markdownStyleCache?.theme === theme) return markdownStyleCache?.style;
|
|
169
|
+
try {
|
|
170
|
+
markdownStyleCache = {
|
|
171
|
+
theme,
|
|
172
|
+
style: buildMarkdownStyle(theme, owner)
|
|
173
|
+
};
|
|
174
|
+
return markdownStyleCache?.style;
|
|
175
|
+
} catch {
|
|
176
|
+
try {
|
|
177
|
+
return SyntaxStyle.create(owner);
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const emptyPins = () => ({
|
|
184
|
+
sessions: [],
|
|
185
|
+
directories: []
|
|
186
|
+
});
|
|
187
|
+
const pinsPath = () => process.env.SESH_PINS_FILE ?? join(process.env.XDG_DATA_HOME ?? join(process.env.HOME ?? "", ".local/share"), "sesh/pins.json");
|
|
188
|
+
async function readPins() {
|
|
189
|
+
try {
|
|
190
|
+
const data = JSON.parse(await readFile(pinsPath(), "utf8"));
|
|
191
|
+
return {
|
|
192
|
+
sessions: Array.isArray(data.sessions) ? data.sessions.filter(id => typeof id === "string") : [],
|
|
193
|
+
directories: Array.isArray(data.directories) ? data.directories.filter(dir => typeof dir === "string") : []
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return emptyPins();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function togglePin(kind, value) {
|
|
200
|
+
const file = pinsPath();
|
|
201
|
+
await mkdir(dirname(file), {
|
|
202
|
+
recursive: true
|
|
203
|
+
});
|
|
204
|
+
const lock = `${file}.lock`;
|
|
205
|
+
let locked = false;
|
|
206
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
207
|
+
try {
|
|
208
|
+
await mkdir(lock);
|
|
209
|
+
locked = true;
|
|
210
|
+
break;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error.code !== "EEXIST") throw error;
|
|
213
|
+
try {
|
|
214
|
+
const owner = Number((await readFile(join(lock, "pid"), "utf8")).trim());
|
|
215
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
216
|
+
try {
|
|
217
|
+
process.kill(owner, 0);
|
|
218
|
+
} catch (check) {
|
|
219
|
+
if (check.code === "ESRCH") {
|
|
220
|
+
await unlink(join(lock, "pid")).catch(() => {});
|
|
221
|
+
await rmdir(lock).catch(() => {});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {}
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!locked) throw new Error("pins are busy");
|
|
230
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
231
|
+
try {
|
|
232
|
+
await writeFile(join(lock, "pid"), `${process.pid}\n`);
|
|
233
|
+
const next = await readPins();
|
|
234
|
+
next[kind] = next[kind].includes(value) ? next[kind].filter(item => item !== value) : [...next[kind], value];
|
|
235
|
+
await writeFile(tmp, JSON.stringify(next) + "\n", {
|
|
236
|
+
mode: 0o600
|
|
237
|
+
});
|
|
238
|
+
await rename(tmp, file);
|
|
239
|
+
return next;
|
|
240
|
+
} finally {
|
|
241
|
+
await unlink(tmp).catch(() => {});
|
|
242
|
+
await unlink(join(lock, "pid")).catch(() => {});
|
|
243
|
+
await rmdir(lock);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function pinnedFirst(entries, pins) {
|
|
247
|
+
const sessions = new Set(pins.sessions);
|
|
248
|
+
const directories = new Set(pins.directories);
|
|
249
|
+
return [...entries].sort((a, b) => {
|
|
250
|
+
const aRank = sessions.has(a.id) ? 2 : directories.has(a.dir) ? 1 : 0;
|
|
251
|
+
const bRank = sessions.has(b.id) ? 2 : directories.has(b.dir) ? 1 : 0;
|
|
252
|
+
return bRank - aRank || b.updated - a.updated;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Sessions panel: an opencode-native session switcher in two parts.
|
|
257
|
+
//
|
|
258
|
+
// 1. Sidebar (always visible): a compact recent-sessions section appended to
|
|
259
|
+
// the native sidebar via the `sidebar_content` slot. Native session_list
|
|
260
|
+
// (<leader>l) is left untouched; this complements it with a cross-project
|
|
261
|
+
// view. Rows are display-only; option+o opens the full picker.
|
|
262
|
+
// 2. Picker (option+o, `/sesh`, command palette): an xlarge grouped picker over
|
|
263
|
+
// every session across all project directories, newest first.
|
|
264
|
+
//
|
|
265
|
+
// `/sesh` is registered by this plugin (a markdown command cannot open the
|
|
266
|
+
// dialog). Native `/sessions` and session_list (<leader>l) stay untouched.
|
|
267
|
+
const BASE_MODE = "base";
|
|
268
|
+
const POLL_MS = 15_000;
|
|
269
|
+
function shortDir(dir, home) {
|
|
270
|
+
if (!dir || dir === "/") return "other";
|
|
271
|
+
const pretty = home && dir.startsWith(home + "/") ? "~" + dir.slice(home.length) : dir;
|
|
272
|
+
return pretty.replace(/\/+$/, "").split("/").pop() || "other";
|
|
273
|
+
}
|
|
274
|
+
function prettyDir(dir, home) {
|
|
275
|
+
if (!dir) return "other";
|
|
276
|
+
if (home && dir === home) return "~";
|
|
277
|
+
const pretty = home && dir.startsWith(home + "/") ? "~" + dir.slice(home.length) : dir.replace(/\/+$/, "");
|
|
278
|
+
const parts = pretty.split("/").filter(Boolean);
|
|
279
|
+
if (parts.length <= 3) return pretty;
|
|
280
|
+
return "…/" + parts.slice(-2).join("/");
|
|
281
|
+
}
|
|
282
|
+
function ago(ms) {
|
|
283
|
+
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
|
284
|
+
if (s < 60) return "now";
|
|
285
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
286
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
287
|
+
return `${Math.floor(s / 86400)}d`;
|
|
288
|
+
}
|
|
289
|
+
function truncate(text, width) {
|
|
290
|
+
if (width <= 0) return "";
|
|
291
|
+
if (text.length <= width) return text;
|
|
292
|
+
return text.slice(0, width - 1) + "…";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Sidebar and home rows should read at a similar length. The sidebar title
|
|
296
|
+
// column is ~2 chars narrower than the group column (status dot + timestamp),
|
|
297
|
+
// so cap it a bit lower to guarantee separation.
|
|
298
|
+
const SIDEBAR_TITLE_WIDTH = 27;
|
|
299
|
+
const SIDEBAR_GROUP_WIDTH = 30;
|
|
300
|
+
const HOME_TITLE_WIDTH = 30;
|
|
301
|
+
|
|
302
|
+
// The sidebar lists every session (newest first, pins ahead) and fills
|
|
303
|
+
// whatever vertical space it gets: overflow scrolls inside the stretched
|
|
304
|
+
// scrollbox. The picker (option+o) remains the place for transcript search.
|
|
305
|
+
|
|
306
|
+
// Uppercase is included deliberately: a search box that silently drops shifted
|
|
307
|
+
// letters cannot be used for acronyms or paths like README.
|
|
308
|
+
const SEARCH_KEYS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@/+:,?!()[]".split("");
|
|
309
|
+
function searchBindings(append, exit) {
|
|
310
|
+
return [...SEARCH_KEYS.map(ch => ({
|
|
311
|
+
key: ch,
|
|
312
|
+
preventDefault: true,
|
|
313
|
+
cmd: () => append(ch)
|
|
314
|
+
})), {
|
|
315
|
+
key: "space",
|
|
316
|
+
preventDefault: true,
|
|
317
|
+
cmd: () => append(" ")
|
|
318
|
+
}, {
|
|
319
|
+
key: "backspace",
|
|
320
|
+
preventDefault: true,
|
|
321
|
+
cmd: () => append("\b")
|
|
322
|
+
}, {
|
|
323
|
+
key: "escape",
|
|
324
|
+
preventDefault: true,
|
|
325
|
+
cmd: exit
|
|
326
|
+
}];
|
|
327
|
+
}
|
|
328
|
+
const rootMouseHandlers = new Set();
|
|
329
|
+
let rootMouseInstalled = false;
|
|
330
|
+
let removeRootMouse;
|
|
331
|
+
const ROOT_MOUSE = Symbol.for("opencode-sesh.root-mouse");
|
|
332
|
+
function ensureRootMouse(renderer) {
|
|
333
|
+
if (rootMouseInstalled || !renderer?.root) return;
|
|
334
|
+
rootMouseInstalled = true;
|
|
335
|
+
const root = renderer.root;
|
|
336
|
+
if (!root[ROOT_MOUSE]) {
|
|
337
|
+
const handlers = new Set();
|
|
338
|
+
root[ROOT_MOUSE] = handlers;
|
|
339
|
+
const previous = root._mouseListeners?.down;
|
|
340
|
+
root.onMouseDown = event => {
|
|
341
|
+
previous?.(event);
|
|
342
|
+
for (const handler of [...handlers]) handler();
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const dispatch = () => {
|
|
346
|
+
for (const handler of [...rootMouseHandlers]) handler();
|
|
347
|
+
};
|
|
348
|
+
root[ROOT_MOUSE].add(dispatch);
|
|
349
|
+
removeRootMouse = () => {
|
|
350
|
+
root[ROOT_MOUSE]?.delete(dispatch);
|
|
351
|
+
rootMouseHandlers.clear();
|
|
352
|
+
rootMouseInstalled = false;
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const LEFT_BUTTON = 0;
|
|
356
|
+
const RIGHT_BUTTON = 2;
|
|
357
|
+
function openInCmuxWorkspace(api, entry) {
|
|
358
|
+
if (!process.env.CMUX_WORKSPACE_ID) {
|
|
359
|
+
api.ui.toast({
|
|
360
|
+
message: "Right-click opens a new workspace only inside cmux",
|
|
361
|
+
variant: "info"
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const args = ["new-workspace", "--name", entry.title || entry.id, "--command", `opencode --session ${entry.id}`, "--focus", "true"];
|
|
366
|
+
if (entry.dir) args.push("--cwd", entry.dir);
|
|
367
|
+
const child = spawn("cmux", args, {
|
|
368
|
+
stdio: "ignore"
|
|
369
|
+
});
|
|
370
|
+
child.on("error", () => api.ui.toast({
|
|
371
|
+
message: "Could not open a cmux workspace",
|
|
372
|
+
variant: "error"
|
|
373
|
+
}));
|
|
374
|
+
child.on("exit", code => {
|
|
375
|
+
if (code) api.ui.toast({
|
|
376
|
+
message: "Could not open a cmux workspace",
|
|
377
|
+
variant: "error"
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const CONTEXT_MENU_LAYER_PRIORITY = 30;
|
|
382
|
+
function contextMenuItems(pinned, cmux) {
|
|
383
|
+
return [{
|
|
384
|
+
id: "open",
|
|
385
|
+
label: "Open"
|
|
386
|
+
}, {
|
|
387
|
+
id: "preview",
|
|
388
|
+
label: "Preview transcript"
|
|
389
|
+
}, ...(cmux ? [{
|
|
390
|
+
id: "workspace",
|
|
391
|
+
label: "Open in new cmux workspace"
|
|
392
|
+
}] : []), {
|
|
393
|
+
id: "fork",
|
|
394
|
+
label: "Fork"
|
|
395
|
+
}, {
|
|
396
|
+
id: "pin",
|
|
397
|
+
label: pinned ? "Unpin" : "Pin"
|
|
398
|
+
}, {
|
|
399
|
+
id: "delete",
|
|
400
|
+
label: "Delete"
|
|
401
|
+
}];
|
|
402
|
+
}
|
|
403
|
+
function contextMenuSize(items) {
|
|
404
|
+
return {
|
|
405
|
+
width: Math.max(...items.map(item => item.label.length)) + 4,
|
|
406
|
+
height: items.length + 2
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function contextMenuBox(anchor, cols, rows) {
|
|
410
|
+
const width = Math.max(1, Math.min(anchor.width, cols));
|
|
411
|
+
const height = Math.max(1, Math.min(anchor.height, rows));
|
|
412
|
+
const left = Math.max(0, Math.min(anchor.x, cols - width));
|
|
413
|
+
const below = anchor.y + 1;
|
|
414
|
+
const top = below + height <= rows ? below : Math.max(0, anchor.y - height);
|
|
415
|
+
return {
|
|
416
|
+
left,
|
|
417
|
+
top,
|
|
418
|
+
width,
|
|
419
|
+
height
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function createContextMenu(host) {
|
|
423
|
+
let state;
|
|
424
|
+
let dispose;
|
|
425
|
+
const publish = next => {
|
|
426
|
+
state = next;
|
|
427
|
+
host.onChange(next);
|
|
428
|
+
};
|
|
429
|
+
const close = () => {
|
|
430
|
+
dispose?.();
|
|
431
|
+
dispose = undefined;
|
|
432
|
+
if (state) publish(undefined);
|
|
433
|
+
};
|
|
434
|
+
const select = index => {
|
|
435
|
+
if (!state) return;
|
|
436
|
+
const next = Math.max(0, Math.min(state.items.length - 1, index));
|
|
437
|
+
// A still pointer re-fires over events; republishing an identical state
|
|
438
|
+
// would rebuild the keyed menu on every frame, churning renderables until
|
|
439
|
+
// clicks fall through to whatever is beneath.
|
|
440
|
+
if (next === state.index) return;
|
|
441
|
+
publish({
|
|
442
|
+
...state,
|
|
443
|
+
index: next
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
const move = delta => {
|
|
447
|
+
if (!state) return;
|
|
448
|
+
const count = state.items.length;
|
|
449
|
+
const next = (state.index + delta + count) % count;
|
|
450
|
+
if (next === state.index) return;
|
|
451
|
+
publish({
|
|
452
|
+
...state,
|
|
453
|
+
index: next
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
const activate = index => {
|
|
457
|
+
if (!state) return;
|
|
458
|
+
const current = state;
|
|
459
|
+
const item = current.items[index ?? current.index];
|
|
460
|
+
if (!item) return;
|
|
461
|
+
if (item.id === "delete") {
|
|
462
|
+
publish({
|
|
463
|
+
...current,
|
|
464
|
+
confirming: true,
|
|
465
|
+
items: [{
|
|
466
|
+
id: "confirm",
|
|
467
|
+
label: "Confirm delete"
|
|
468
|
+
}, {
|
|
469
|
+
id: "cancel",
|
|
470
|
+
label: "Cancel"
|
|
471
|
+
}],
|
|
472
|
+
index: 1
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
close();
|
|
477
|
+
if (item.id === "confirm") host.actions.delete(current.entry);else if (item.id !== "cancel") host.actions[item.id](current.entry);
|
|
478
|
+
};
|
|
479
|
+
const click = (index, button) => {
|
|
480
|
+
if ((button ?? LEFT_BUTTON) === LEFT_BUTTON) activate(index);
|
|
481
|
+
};
|
|
482
|
+
const guarded = run => () => {
|
|
483
|
+
if (host.blocked()) close();else run();
|
|
484
|
+
};
|
|
485
|
+
const open = (entry, x, y) => {
|
|
486
|
+
if (host.blocked()) return;
|
|
487
|
+
publish({
|
|
488
|
+
entry,
|
|
489
|
+
x,
|
|
490
|
+
y,
|
|
491
|
+
items: contextMenuItems(host.pinned(entry), host.cmux()),
|
|
492
|
+
index: 0,
|
|
493
|
+
confirming: false
|
|
494
|
+
});
|
|
495
|
+
if (dispose) return;
|
|
496
|
+
dispose = host.registerLayer({
|
|
497
|
+
mode: "base",
|
|
498
|
+
priority: CONTEXT_MENU_LAYER_PRIORITY,
|
|
499
|
+
bindings: [{
|
|
500
|
+
key: "up",
|
|
501
|
+
desc: "Previous menu item",
|
|
502
|
+
preventDefault: true,
|
|
503
|
+
cmd: guarded(() => move(-1))
|
|
504
|
+
}, {
|
|
505
|
+
key: "down",
|
|
506
|
+
desc: "Next menu item",
|
|
507
|
+
preventDefault: true,
|
|
508
|
+
cmd: guarded(() => move(1))
|
|
509
|
+
}, {
|
|
510
|
+
key: "enter",
|
|
511
|
+
desc: "Choose menu item",
|
|
512
|
+
preventDefault: true,
|
|
513
|
+
cmd: guarded(() => activate())
|
|
514
|
+
}, {
|
|
515
|
+
key: "escape",
|
|
516
|
+
desc: "Close menu",
|
|
517
|
+
preventDefault: true,
|
|
518
|
+
cmd: close
|
|
519
|
+
}]
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
open,
|
|
524
|
+
close,
|
|
525
|
+
select,
|
|
526
|
+
activate,
|
|
527
|
+
click,
|
|
528
|
+
current: () => state
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Loading-spinner frames for the marker of a session whose agent is working.
|
|
532
|
+
const SIDEBAR_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
533
|
+
function newestFirst(entries) {
|
|
534
|
+
return [...entries].sort((a, b) => b.updated - a.updated || a.id.localeCompare(b.id));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// A working agent is the only state the marker displays: everything that
|
|
538
|
+
// finished goes back to the neutral row, with the open session marked inside.
|
|
539
|
+
function sidebarMarker(current, live) {
|
|
540
|
+
if (live === "running" || live === "busy" || live === "retry") return "running";
|
|
541
|
+
return current ? "current" : "idle";
|
|
542
|
+
}
|
|
543
|
+
const SESSION_PAGE_LIMIT = 200;
|
|
544
|
+
const SESSION_MAX = 5000;
|
|
545
|
+
async function fetchEntries(api) {
|
|
546
|
+
const home = process.env.HOME ?? "";
|
|
547
|
+
const projectResult = await api.client.project.list();
|
|
548
|
+
const projects = projectResult?.data ?? projectResult ?? [];
|
|
549
|
+
const projectName = new Map(projects.map(p => [p.id, p.name?.trim() || shortDir(p.canonical, home)]));
|
|
550
|
+
const sessions = [];
|
|
551
|
+
let cursor;
|
|
552
|
+
let truncated = false;
|
|
553
|
+
for (;;) {
|
|
554
|
+
const result = await api.client.experimental.session.list({
|
|
555
|
+
limit: SESSION_PAGE_LIMIT,
|
|
556
|
+
roots: true,
|
|
557
|
+
...(cursor === undefined ? {} : {
|
|
558
|
+
cursor
|
|
559
|
+
})
|
|
560
|
+
});
|
|
561
|
+
const page = result?.data ?? result ?? [];
|
|
562
|
+
if (!Array.isArray(page) || page.length === 0) break;
|
|
563
|
+
sessions.push(...page);
|
|
564
|
+
if (!result.cursor?.next) break;
|
|
565
|
+
if (sessions.length >= SESSION_MAX) {
|
|
566
|
+
truncated = true;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
const next = result.cursor.next;
|
|
570
|
+
if (next === cursor) {
|
|
571
|
+
truncated = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
cursor = next;
|
|
575
|
+
}
|
|
576
|
+
const entries = sessions.filter(s => !s.time?.archived && !s.parentID).sort((a, b) => b.time.updated - a.time.updated).map(s => {
|
|
577
|
+
const projectLabel = s.projectID ? projectName.get(s.projectID) : undefined;
|
|
578
|
+
const sDir = s.directory ?? s.location?.directory ?? "";
|
|
579
|
+
return {
|
|
580
|
+
id: s.id,
|
|
581
|
+
title: s.title?.trim() || "(untitled)",
|
|
582
|
+
dir: sDir,
|
|
583
|
+
group: (projectLabel && projectLabel !== "other" ? projectLabel : shortDir(sDir, home)) || "other",
|
|
584
|
+
updated: s.time.updated
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
entries,
|
|
589
|
+
truncated
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const SESSION_ID_PATTERN = /^ses_[A-Za-z0-9]+$/;
|
|
593
|
+
const TRANSCRIPT_BATCH = 64;
|
|
594
|
+
const REMOTE_CONCURRENCY = 8;
|
|
595
|
+
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
596
|
+
function resolveDbPath() {
|
|
597
|
+
if (process.env.SESH_DB) return process.env.SESH_DB;
|
|
598
|
+
const home = process.env.HOME ?? "";
|
|
599
|
+
const dataHome = process.env.XDG_DATA_HOME ?? `${home}/.local/share`;
|
|
600
|
+
const xdgPath = `${dataHome}/opencode/opencode.db`;
|
|
601
|
+
if (existsSync(xdgPath)) return xdgPath;
|
|
602
|
+
try {
|
|
603
|
+
const result = spawnSync(process.env.SESH_OPENCODE ?? "opencode", ["debug", "paths"], {
|
|
604
|
+
encoding: "utf8"
|
|
605
|
+
});
|
|
606
|
+
const match = result.stdout?.match(/^db\s+(.+)$/m);
|
|
607
|
+
if (match) return match[1].trimEnd();
|
|
608
|
+
} catch {}
|
|
609
|
+
return xdgPath;
|
|
610
|
+
}
|
|
611
|
+
async function openTranscriptDb() {
|
|
612
|
+
try {
|
|
613
|
+
// @ts-ignore - bun:sqlite ships with the Bun runtime (verified); no type package installed
|
|
614
|
+
const sqlite = await import("bun:sqlite");
|
|
615
|
+
if (typeof sqlite?.Database !== "function") return undefined;
|
|
616
|
+
return new sqlite.Database(resolveDbPath(), {
|
|
617
|
+
readonly: true
|
|
618
|
+
});
|
|
619
|
+
} catch {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function costRowsQuery(cutoff, includeLegacy) {
|
|
624
|
+
const legacy = includeLegacy ? `UNION ALL
|
|
625
|
+
SELECT s.directory, s.id AS session_id, m.time_created, m.data
|
|
626
|
+
FROM message m JOIN session s ON s.id = m.session_id
|
|
627
|
+
WHERE json_extract(m.data, '$.role') = 'assistant'
|
|
628
|
+
AND NOT EXISTS (SELECT 1 FROM session_v2 v WHERE v.id = s.id)` : "";
|
|
629
|
+
return `WITH costs AS (
|
|
630
|
+
SELECT s.directory, s.id AS session_id, m.time_created, m.data
|
|
631
|
+
FROM session_message m JOIN session_v2 s ON s.id = m.session_id
|
|
632
|
+
WHERE m.type = 'assistant'
|
|
633
|
+
${legacy}
|
|
634
|
+
)
|
|
635
|
+
SELECT m.directory AS directory,
|
|
636
|
+
ROUND(SUM(CASE WHEN m.time_created >= ${cutoff} THEN COALESCE(json_extract(m.data, '$.cost'), 0) ELSE 0 END), 4) AS window_cost,
|
|
637
|
+
ROUND(SUM(COALESCE(json_extract(m.data, '$.cost'), 0)), 4) AS lifetime_cost,
|
|
638
|
+
COUNT(DISTINCT m.session_id) AS sessions
|
|
639
|
+
FROM costs m
|
|
640
|
+
WHERE json_extract(m.data, '$.cost') IS NOT NULL
|
|
641
|
+
GROUP BY m.directory
|
|
642
|
+
ORDER BY lifetime_cost DESC`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// NEEDS_INPUT_SQL: sessions waiting on the user (shared heuristic — keep in
|
|
646
|
+
// sync with bin/sesh-waiting.sh). A session waits when it is not archived,
|
|
647
|
+
// is not a child (subagent) session, and either has an unanswered `question`
|
|
648
|
+
// tool part or a `tool` part still marked running older than STUCK_AFTER_MS
|
|
649
|
+
// with no message activity in the session or its direct children since then (a
|
|
650
|
+
// long-running subagent/shell tool with recent activity is not stuck; V2
|
|
651
|
+
// resumes interrupted runs server-side, so `running` with no activity means
|
|
652
|
+
// orphaned or awaiting approval). Server-independent on purpose: the TUI
|
|
653
|
+
// sync layer only sees permission/question state for sessions owned by the
|
|
654
|
+
// current server, which is invisible cross-project.
|
|
655
|
+
const STUCK_AFTER_MS = 600000;
|
|
656
|
+
const LIVE_TOOLS_CTE = sessionFilter => `
|
|
657
|
+
WITH live AS (
|
|
658
|
+
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
659
|
+
json_extract(c.value, '$.name') AS name,
|
|
660
|
+
json_extract(c.value, '$.state.status') AS status,
|
|
661
|
+
COALESCE(json_extract(c.value, '$.time.created'), m.time_created) AS created
|
|
662
|
+
FROM session_message m, json_each(m.data, '$.content') c
|
|
663
|
+
WHERE m.type = 'assistant'
|
|
664
|
+
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
665
|
+
AND json_extract(c.value, '$.type') = 'tool'
|
|
666
|
+
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}
|
|
667
|
+
)`;
|
|
668
|
+
const QUESTION_OPEN = `live.name = 'question'
|
|
669
|
+
AND NOT EXISTS (
|
|
670
|
+
SELECT 1 FROM session_message u
|
|
671
|
+
WHERE u.session_id = live.session_id AND u.type = 'user' AND u.seq > live.seq)`;
|
|
672
|
+
const NO_RECENT_ACTIVITY = (sessionIdExpr, stuckBefore) => `NOT EXISTS (
|
|
673
|
+
SELECT 1 FROM session_message r JOIN session_v2 f ON f.id = r.session_id
|
|
674
|
+
WHERE (f.id = ${sessionIdExpr} OR f.parent_id = ${sessionIdExpr}) AND r.time_updated >= ${stuckBefore})`;
|
|
675
|
+
const NEEDS_INPUT_SQL = stuckBefore => `${LIVE_TOOLS_CTE("")}
|
|
676
|
+
SELECT s.id AS id,
|
|
677
|
+
CASE WHEN EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
678
|
+
THEN 'question' ELSE 'stuck' END AS reason
|
|
679
|
+
FROM session_v2 s
|
|
680
|
+
WHERE COALESCE(s.time_archived, 0) = 0
|
|
681
|
+
AND s.parent_id IS NULL
|
|
682
|
+
AND (EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
683
|
+
OR (EXISTS (
|
|
684
|
+
SELECT 1 FROM live
|
|
685
|
+
WHERE live.session_id = s.id AND live.status = 'running' AND live.created < ${stuckBefore})
|
|
686
|
+
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
687
|
+
ORDER BY s.time_updated DESC`;
|
|
688
|
+
async function queryWaitingIds(db) {
|
|
689
|
+
const rows = db.query(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS)).all() ?? [];
|
|
690
|
+
const out = new Map();
|
|
691
|
+
for (const row of rows) {
|
|
692
|
+
if (typeof row?.id === "string" && SESSION_ID_PATTERN.test(row.id) && (row.reason === "question" || row.reason === "stuck")) {
|
|
693
|
+
out.set(row.id, row.reason);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
// The shared needs-input query decides *which* sessions are waiting. Read the
|
|
699
|
+
// oldest still-unanswered question or stuck tool for each result so triage can
|
|
700
|
+
// show how long it has waited, rather than its last (unrelated) update time.
|
|
701
|
+
async function queryWaitingDetails(db) {
|
|
702
|
+
const reasons = await queryWaitingIds(db);
|
|
703
|
+
const details = new Map([...reasons].map(([id, reason]) => [id, {
|
|
704
|
+
reason: reason
|
|
705
|
+
}]));
|
|
706
|
+
const ids = [...details.keys()];
|
|
707
|
+
const cutoff = Date.now() - STUCK_AFTER_MS;
|
|
708
|
+
for (let i = 0; i < ids.length; i += 200) {
|
|
709
|
+
const batch = ids.slice(i, i + 200);
|
|
710
|
+
const rows = db.query(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
|
|
711
|
+
SELECT live.session_id AS id,
|
|
712
|
+
MIN(CASE WHEN ${QUESTION_OPEN} THEN live.created END) AS question_since,
|
|
713
|
+
MIN(CASE WHEN live.status = 'running' AND live.created < ${cutoff} THEN live.created END) AS stuck_since
|
|
714
|
+
FROM live
|
|
715
|
+
GROUP BY live.session_id`).all(...batch) ?? [];
|
|
716
|
+
for (const row of rows) {
|
|
717
|
+
const detail = details.get(row.id);
|
|
718
|
+
if (!detail) continue;
|
|
719
|
+
const since = detail.reason === "question" ? row.question_since : row.stuck_since;
|
|
720
|
+
if (typeof since === "number") detail.since = since;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return details;
|
|
724
|
+
}
|
|
725
|
+
function addTranscript(index, sid, data) {
|
|
726
|
+
try {
|
|
727
|
+
const message = JSON.parse(data);
|
|
728
|
+
const texts = [];
|
|
729
|
+
if (typeof message?.text === "string") texts.push(message.text);
|
|
730
|
+
if (Array.isArray(message?.content)) {
|
|
731
|
+
for (const part of message.content) {
|
|
732
|
+
if (part?.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const text of texts) {
|
|
736
|
+
const base = index.get(sid) ?? "";
|
|
737
|
+
index.set(sid, `${base} ${text.toLowerCase()}`.trim());
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
// ignore malformed messages
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Index titles/directories immediately, then add transcript text progressively
|
|
745
|
+
// in batches so the render thread can breathe. Covers every entry, not a fixed
|
|
746
|
+
// newest window; `onProgress` lets the picker show coverage.
|
|
747
|
+
async function buildSearchIndex(api, entries, onProgress) {
|
|
748
|
+
const index = new Map();
|
|
749
|
+
for (const entry of entries) {
|
|
750
|
+
index.set(entry.id, `${entry.title.toLowerCase()} ${entry.dir.toLowerCase()}`);
|
|
751
|
+
}
|
|
752
|
+
const ids = entries.map(entry => entry.id).filter(id => SESSION_ID_PATTERN.test(id));
|
|
753
|
+
const remaining = new Set(ids);
|
|
754
|
+
const total = ids.length;
|
|
755
|
+
let indexed = 0;
|
|
756
|
+
const report = complete => onProgress?.({
|
|
757
|
+
indexed,
|
|
758
|
+
total,
|
|
759
|
+
complete
|
|
760
|
+
}, index);
|
|
761
|
+
report(false);
|
|
762
|
+
const db = await openTranscriptDb();
|
|
763
|
+
if (db) {
|
|
764
|
+
try {
|
|
765
|
+
for (let i = 0; i < ids.length; i += TRANSCRIPT_BATCH) {
|
|
766
|
+
const batch = ids.slice(i, i + TRANSCRIPT_BATCH);
|
|
767
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
768
|
+
const rows = db.query(`SELECT session_id AS sid, data FROM session_message WHERE session_id IN (${placeholders}) AND type IN ('user', 'assistant') ORDER BY seq`).all(...batch) ?? [];
|
|
769
|
+
for (const row of rows) addTranscript(index, row.sid, row.data);
|
|
770
|
+
for (const id of batch) remaining.delete(id);
|
|
771
|
+
indexed += batch.length;
|
|
772
|
+
report(false);
|
|
773
|
+
await nextTick();
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
// local store unavailable mid-walk; fall back to the remote path below
|
|
777
|
+
} finally {
|
|
778
|
+
db.close();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (remaining.size > 0) {
|
|
782
|
+
await buildSearchIndexRemote(api, [...remaining], index, () => {
|
|
783
|
+
indexed += 1;
|
|
784
|
+
if (indexed % TRANSCRIPT_BATCH === 0) report(false);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
report(true);
|
|
788
|
+
return index;
|
|
789
|
+
}
|
|
790
|
+
async function buildSearchIndexRemote(api, ids, index, onEach) {
|
|
791
|
+
const home = process.env.HOME ?? "";
|
|
792
|
+
const cacheDir = process.env.SESH_CACHE_DIR ?? `${process.env.XDG_CACHE_HOME ?? `${home}/.cache`}/sesh`;
|
|
793
|
+
let cursor = 0;
|
|
794
|
+
// Bounded concurrency: a fixed worker pool instead of one request per session.
|
|
795
|
+
const worker = async () => {
|
|
796
|
+
for (;;) {
|
|
797
|
+
const i = cursor++;
|
|
798
|
+
if (i >= ids.length) return;
|
|
799
|
+
const id = ids[i];
|
|
800
|
+
const base = index.get(id) ?? "";
|
|
801
|
+
try {
|
|
802
|
+
const raw = await readFile(`${cacheDir}/extractions/${id}.json`, "utf8");
|
|
803
|
+
const record = JSON.parse(raw);
|
|
804
|
+
const fulltext = typeof record?.fulltextLower === "string" ? record.fulltextLower : "";
|
|
805
|
+
// Extraction caches prefix the transcript with the cached title. The
|
|
806
|
+
// live entry already indexes its own title; strip that duplicate so a
|
|
807
|
+
// title-only match cannot appear as a transcript excerpt.
|
|
808
|
+
const title = typeof record?.title === "string" ? `${record.title.toLowerCase()} ` : "";
|
|
809
|
+
const transcript = title && fulltext.startsWith(title) ? fulltext.slice(title.length) : fulltext;
|
|
810
|
+
index.set(id, `${base} ${transcript}`.trim());
|
|
811
|
+
} catch {
|
|
812
|
+
try {
|
|
813
|
+
const result = await api.client.session.messages({
|
|
814
|
+
sessionID: id
|
|
815
|
+
});
|
|
816
|
+
const messages = result.data ?? [];
|
|
817
|
+
const text = messages.flatMap(message => message.parts.filter(part => part.type === "text").map(part => part.text)).join(" ").toLowerCase();
|
|
818
|
+
index.set(id, `${base} ${text}`.trim());
|
|
819
|
+
} catch {}
|
|
820
|
+
}
|
|
821
|
+
onEach();
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
await Promise.all(Array.from({
|
|
825
|
+
length: Math.min(REMOTE_CONCURRENCY, ids.length)
|
|
826
|
+
}, worker));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// The index is title + directory + text parts, all lowercased. Start looking
|
|
830
|
+
// after the metadata so a title match never masquerades as a transcript hit.
|
|
831
|
+
function transcriptMatchExcerpt(indexed, entry, query) {
|
|
832
|
+
const needle = query.trim().toLowerCase();
|
|
833
|
+
if (!indexed || !needle) return undefined;
|
|
834
|
+
const transcriptStart = `${entry.title.toLowerCase()} ${entry.dir.toLowerCase()}`.length + 1;
|
|
835
|
+
const at = indexed.indexOf(needle, transcriptStart);
|
|
836
|
+
if (at < 0) return undefined;
|
|
837
|
+
const start = Math.max(transcriptStart, at - 28);
|
|
838
|
+
const end = Math.min(indexed.length, Math.max(at + needle.length + 28, start + 90));
|
|
839
|
+
const excerpt = indexed.slice(start, end).replace(/\s+/g, " ").trim();
|
|
840
|
+
return `${start > transcriptStart ? "…" : ""}${excerpt}${end < indexed.length ? "…" : ""}`;
|
|
841
|
+
}
|
|
842
|
+
function filterPickerEntries(entries, filters, index, waitingIds, pinnedSessions) {
|
|
843
|
+
const q = filters.query.trim().toLowerCase();
|
|
844
|
+
const pinned = new Set(pinnedSessions);
|
|
845
|
+
return entries.filter(entry => {
|
|
846
|
+
if (filters.scope && entry.group !== filters.scope) return false;
|
|
847
|
+
if (filters.waitingOnly && !waitingIds.has(entry.id)) return false;
|
|
848
|
+
if (filters.pinnedOnly && !pinned.has(entry.id)) return false;
|
|
849
|
+
if (!q) return true;
|
|
850
|
+
return (index.get(entry.id) ?? `${entry.title} ${entry.dir}`.toLowerCase()).includes(q);
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
const TRANSCRIPT_PREVIEW_TURNS = 8;
|
|
854
|
+
const TRANSCRIPT_PREVIEW_CHARS = 2000;
|
|
855
|
+
|
|
856
|
+
// Case-insensitive range of `query` inside `text`, so a matched row can show
|
|
857
|
+
// which part of it the search hit instead of only filtering invisibly. Kept
|
|
858
|
+
// below the data-layer markers that tests/tui.mjs evaluates: JSX must not enter
|
|
859
|
+
// that extraction window.
|
|
860
|
+
function matchRange(text, query) {
|
|
861
|
+
const q = query.trim().toLowerCase();
|
|
862
|
+
if (!q) return undefined;
|
|
863
|
+
const at = text.toLowerCase().indexOf(q);
|
|
864
|
+
return at < 0 ? undefined : [at, at + q.length];
|
|
865
|
+
}
|
|
866
|
+
function Highlighted(props) {
|
|
867
|
+
const parts = createMemo(() => {
|
|
868
|
+
const range = matchRange(props.text, props.query);
|
|
869
|
+
if (!range) return [props.text, "", ""];
|
|
870
|
+
return [props.text.slice(0, range[0]), props.text.slice(range[0], range[1]), props.text.slice(range[1])];
|
|
871
|
+
});
|
|
872
|
+
return (() => {
|
|
873
|
+
var _el$ = _$createElement("text"),
|
|
874
|
+
_el$2 = _$createElement("span");
|
|
875
|
+
_$insertNode(_el$, _el$2);
|
|
876
|
+
_$setProp(_el$, "flexGrow", 1);
|
|
877
|
+
_$setProp(_el$, "flexShrink", 1);
|
|
878
|
+
_$setProp(_el$, "overflow", "hidden");
|
|
879
|
+
_$setProp(_el$, "wrapMode", "none");
|
|
880
|
+
_$insert(_el$, () => parts()[0], _el$2);
|
|
881
|
+
_$insert(_el$2, () => parts()[1]);
|
|
882
|
+
_$insert(_el$, () => parts()[2], null);
|
|
883
|
+
_$effect(_p$ => {
|
|
884
|
+
var _v$ = props.bold ? TextAttributes.BOLD : undefined,
|
|
885
|
+
_v$2 = {
|
|
886
|
+
fg: props.color
|
|
887
|
+
},
|
|
888
|
+
_v$3 = {
|
|
889
|
+
fg: props.matchColor
|
|
890
|
+
};
|
|
891
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "attributes", _v$, _p$.e));
|
|
892
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "style", _v$2, _p$.t));
|
|
893
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "style", _v$3, _p$.a));
|
|
894
|
+
return _p$;
|
|
895
|
+
}, {
|
|
896
|
+
e: undefined,
|
|
897
|
+
t: undefined,
|
|
898
|
+
a: undefined
|
|
899
|
+
});
|
|
900
|
+
return _el$;
|
|
901
|
+
})();
|
|
902
|
+
}
|
|
903
|
+
async function fetchTranscriptText(api, sessionID) {
|
|
904
|
+
try {
|
|
905
|
+
const result = await api.client.session.messages({
|
|
906
|
+
sessionID,
|
|
907
|
+
preview: true
|
|
908
|
+
});
|
|
909
|
+
const messages = result.data ?? [];
|
|
910
|
+
const blocks = messages.map(message => {
|
|
911
|
+
const text = message.parts.filter(part => part.type === "text").map(part => part.text).join("\n").trim();
|
|
912
|
+
if (!text) return "";
|
|
913
|
+
const role = message.info.role;
|
|
914
|
+
return `**${role === "user" ? "You" : "Assistant"}**\n\n${text.slice(0, TRANSCRIPT_PREVIEW_CHARS)}`;
|
|
915
|
+
}).filter(Boolean).slice(-TRANSCRIPT_PREVIEW_TURNS).reverse();
|
|
916
|
+
const text = blocks.join("\n\n---\n\n");
|
|
917
|
+
return text || "(no text in this session)";
|
|
918
|
+
} catch {
|
|
919
|
+
return "(preview unavailable)";
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Shared transcript preview dialog. Call from a component's setup scope so the
|
|
924
|
+
// preview key layer is torn down with it.
|
|
925
|
+
function createTranscriptPreview(api) {
|
|
926
|
+
const [previewID, setPreviewID] = createSignal();
|
|
927
|
+
const [previewText, setPreviewText] = createSignal("");
|
|
928
|
+
let disposePreviewKeys;
|
|
929
|
+
const close = () => {
|
|
930
|
+
setPreviewID(undefined);
|
|
931
|
+
api.ui.dialog.clear();
|
|
932
|
+
disposePreviewKeys?.();
|
|
933
|
+
disposePreviewKeys = undefined;
|
|
934
|
+
};
|
|
935
|
+
const open = entry => {
|
|
936
|
+
if (previewID() === entry.id) {
|
|
937
|
+
close();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (previewID()) close();
|
|
941
|
+
setPreviewID(entry.id);
|
|
942
|
+
setPreviewText("Loading…");
|
|
943
|
+
void fetchTranscriptText(api, entry.id).then(text => {
|
|
944
|
+
if (previewID() === entry.id) setPreviewText(text);
|
|
945
|
+
});
|
|
946
|
+
api.ui.dialog.replace(() => (() => {
|
|
947
|
+
var _el$3 = _$createElement("box"),
|
|
948
|
+
_el$4 = _$createElement("box"),
|
|
949
|
+
_el$5 = _$createElement("text"),
|
|
950
|
+
_el$6 = _$createElement("text"),
|
|
951
|
+
_el$8 = _$createElement("text"),
|
|
952
|
+
_el$9 = _$createTextNode(` · `),
|
|
953
|
+
_el$0 = _$createElement("scrollbox");
|
|
954
|
+
_$insertNode(_el$3, _el$4);
|
|
955
|
+
_$insertNode(_el$3, _el$8);
|
|
956
|
+
_$insertNode(_el$3, _el$0);
|
|
957
|
+
_$setProp(_el$3, "flexDirection", "column");
|
|
958
|
+
_$setProp(_el$3, "paddingLeft", 4);
|
|
959
|
+
_$setProp(_el$3, "paddingRight", 4);
|
|
960
|
+
_$setProp(_el$3, "paddingBottom", 1);
|
|
961
|
+
_$setProp(_el$3, "gap", 1);
|
|
962
|
+
_$setProp(_el$3, "onMouseDown", event => event.stopPropagation());
|
|
963
|
+
_$insertNode(_el$4, _el$5);
|
|
964
|
+
_$insertNode(_el$4, _el$6);
|
|
965
|
+
_$setProp(_el$4, "flexDirection", "row");
|
|
966
|
+
_$setProp(_el$4, "justifyContent", "space-between");
|
|
967
|
+
_$insert(_el$5, () => entry.title);
|
|
968
|
+
_$insertNode(_el$6, _$createTextNode(`esc`));
|
|
969
|
+
_$setProp(_el$6, "onMouseUp", close);
|
|
970
|
+
_$insertNode(_el$8, _el$9);
|
|
971
|
+
_$insert(_el$8, () => prettyDir(entry.dir, process.env.HOME ?? ""), _el$9);
|
|
972
|
+
_$insert(_el$8, () => ago(entry.updated), null);
|
|
973
|
+
_$setProp(_el$0, "paddingLeft", 1);
|
|
974
|
+
_$setProp(_el$0, "paddingRight", 1);
|
|
975
|
+
_$setProp(_el$0, "scrollbarOptions", {
|
|
976
|
+
visible: false
|
|
977
|
+
});
|
|
978
|
+
_$insert(_el$0, (() => {
|
|
979
|
+
var _c$ = _$memo(() => !!getMarkdownStyle(api.theme.current, api.renderer?.nativeSession));
|
|
980
|
+
return () => _c$() ? (() => {
|
|
981
|
+
var _el$1 = _$createElement("markdown");
|
|
982
|
+
_$effect(_p$ => {
|
|
983
|
+
var _v$9 = previewText(),
|
|
984
|
+
_v$0 = getMarkdownStyle(api.theme.current, api.renderer?.nativeSession),
|
|
985
|
+
_v$1 = getTreeSitterClient();
|
|
986
|
+
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$1, "content", _v$9, _p$.e));
|
|
987
|
+
_v$0 !== _p$.t && (_p$.t = _$setProp(_el$1, "syntaxStyle", _v$0, _p$.t));
|
|
988
|
+
_v$1 !== _p$.a && (_p$.a = _$setProp(_el$1, "treeSitterClient", _v$1, _p$.a));
|
|
989
|
+
return _p$;
|
|
990
|
+
}, {
|
|
991
|
+
e: undefined,
|
|
992
|
+
t: undefined,
|
|
993
|
+
a: undefined
|
|
994
|
+
});
|
|
995
|
+
return _el$1;
|
|
996
|
+
})() : (() => {
|
|
997
|
+
var _el$10 = _$createElement("text");
|
|
998
|
+
_$setProp(_el$10, "wrapMode", "word");
|
|
999
|
+
_$insert(_el$10, previewText);
|
|
1000
|
+
return _el$10;
|
|
1001
|
+
})();
|
|
1002
|
+
})());
|
|
1003
|
+
_$effect(_p$ => {
|
|
1004
|
+
var _v$4 = api.theme.current.backgroundPanel,
|
|
1005
|
+
_v$5 = TextAttributes.BOLD,
|
|
1006
|
+
_v$6 = {
|
|
1007
|
+
fg: api.theme.current.textMuted
|
|
1008
|
+
},
|
|
1009
|
+
_v$7 = {
|
|
1010
|
+
fg: api.theme.current.textMuted
|
|
1011
|
+
},
|
|
1012
|
+
_v$8 = Math.max(6, Math.floor(api.renderer.height / 2));
|
|
1013
|
+
_v$4 !== _p$.e && (_p$.e = _$setProp(_el$3, "backgroundColor", _v$4, _p$.e));
|
|
1014
|
+
_v$5 !== _p$.t && (_p$.t = _$setProp(_el$5, "attributes", _v$5, _p$.t));
|
|
1015
|
+
_v$6 !== _p$.a && (_p$.a = _$setProp(_el$6, "style", _v$6, _p$.a));
|
|
1016
|
+
_v$7 !== _p$.o && (_p$.o = _$setProp(_el$8, "style", _v$7, _p$.o));
|
|
1017
|
+
_v$8 !== _p$.i && (_p$.i = _$setProp(_el$0, "maxHeight", _v$8, _p$.i));
|
|
1018
|
+
return _p$;
|
|
1019
|
+
}, {
|
|
1020
|
+
e: undefined,
|
|
1021
|
+
t: undefined,
|
|
1022
|
+
a: undefined,
|
|
1023
|
+
o: undefined,
|
|
1024
|
+
i: undefined
|
|
1025
|
+
});
|
|
1026
|
+
return _el$3;
|
|
1027
|
+
})(), () => {
|
|
1028
|
+
setPreviewID(undefined);
|
|
1029
|
+
disposePreviewKeys?.();
|
|
1030
|
+
disposePreviewKeys = undefined;
|
|
1031
|
+
});
|
|
1032
|
+
api.ui.dialog.setSize("large");
|
|
1033
|
+
disposePreviewKeys?.();
|
|
1034
|
+
disposePreviewKeys = api.keymap.registerLayer({
|
|
1035
|
+
mode: "modal",
|
|
1036
|
+
bindings: [{
|
|
1037
|
+
key: "escape",
|
|
1038
|
+
desc: "Close preview",
|
|
1039
|
+
preventDefault: true,
|
|
1040
|
+
cmd: close
|
|
1041
|
+
}]
|
|
1042
|
+
});
|
|
1043
|
+
};
|
|
1044
|
+
onCleanup(() => disposePreviewKeys?.());
|
|
1045
|
+
|
|
1046
|
+
// Outside presses reach the root handler; close on the first one instead of
|
|
1047
|
+
// relying on the host backdrop. Gated on our own preview so other dialogs
|
|
1048
|
+
// are never touched.
|
|
1049
|
+
const closeIfOpen = () => {
|
|
1050
|
+
if (previewID()) close();
|
|
1051
|
+
};
|
|
1052
|
+
return {
|
|
1053
|
+
open,
|
|
1054
|
+
closeIfOpen
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Window by terminal lines, not session count: group spacing and transcript
|
|
1058
|
+
// excerpts each consume an extra line. Carry the directory heading forward
|
|
1059
|
+
// when a long group starts above the viewport.
|
|
1060
|
+
function pickerWindow(rows, start, height, excerpt) {
|
|
1061
|
+
if (!rows.length) return [];
|
|
1062
|
+
const first = Math.max(0, Math.min(start, rows.length - 1));
|
|
1063
|
+
const shown = [];
|
|
1064
|
+
let remaining = height;
|
|
1065
|
+
if (rows[first].kind === "item") {
|
|
1066
|
+
for (let i = first - 1; i >= 0; i--) {
|
|
1067
|
+
const row = rows[i];
|
|
1068
|
+
if (row.kind !== "group") continue;
|
|
1069
|
+
shown.push({
|
|
1070
|
+
...row,
|
|
1071
|
+
continued: true
|
|
1072
|
+
});
|
|
1073
|
+
remaining--;
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
for (let i = first; i < rows.length; i++) {
|
|
1078
|
+
const row = rows[i];
|
|
1079
|
+
const lines = row.kind === "group" ? 2 : excerpt(row.entry) ? 2 : 1;
|
|
1080
|
+
if (lines > remaining && shown.length) break;
|
|
1081
|
+
shown.push(row);
|
|
1082
|
+
remaining -= lines;
|
|
1083
|
+
}
|
|
1084
|
+
return shown;
|
|
1085
|
+
}
|
|
1086
|
+
function pickerLastStart(rows, height, excerpt) {
|
|
1087
|
+
if (!rows.length) return 0;
|
|
1088
|
+
let start = rows.length - 1;
|
|
1089
|
+
while (start > 0 && pickerWindow(rows, start - 1, height, excerpt).at(-1) === rows.at(-1)) start--;
|
|
1090
|
+
return start;
|
|
1091
|
+
}
|
|
1092
|
+
function SidebarSessions(props) {
|
|
1093
|
+
const theme = () => props.api.theme.current;
|
|
1094
|
+
const home = process.env.HOME ?? "";
|
|
1095
|
+
const [entries, setEntries] = createSignal([]);
|
|
1096
|
+
const [sectionCollapsed, setSectionCollapsed] = createSignal(false);
|
|
1097
|
+
const [collapsed, setCollapsed] = createSignal({});
|
|
1098
|
+
const [hovered, setHovered] = createSignal();
|
|
1099
|
+
const preview = createTranscriptPreview(props.api);
|
|
1100
|
+
const [menuState, setMenuState] = createSignal();
|
|
1101
|
+
let portalContainer;
|
|
1102
|
+
const [query, setQuery] = createSignal("");
|
|
1103
|
+
const [deleting, setDeleting] = createSignal();
|
|
1104
|
+
const [searching, setSearching] = createSignal(false);
|
|
1105
|
+
const [loadFailed, setLoadFailed] = createSignal(false);
|
|
1106
|
+
const [navActive, setNavActive] = createSignal(false);
|
|
1107
|
+
const [cursor, setCursor] = createSignal(0);
|
|
1108
|
+
const [pendingDelete, setPendingDelete] = createSignal();
|
|
1109
|
+
const [waiting, setWaiting] = createSignal(new Map());
|
|
1110
|
+
let confirmTimer;
|
|
1111
|
+
const currentID = createMemo(() => {
|
|
1112
|
+
const route = props.api.route.current;
|
|
1113
|
+
if (route.name !== "session") return undefined;
|
|
1114
|
+
const params = route.params;
|
|
1115
|
+
return params && typeof params.sessionID === "string" ? params.sessionID : undefined;
|
|
1116
|
+
});
|
|
1117
|
+
onMount(() => {
|
|
1118
|
+
let alive = true;
|
|
1119
|
+
const load = async () => {
|
|
1120
|
+
props.refreshPins();
|
|
1121
|
+
try {
|
|
1122
|
+
const {
|
|
1123
|
+
entries: all
|
|
1124
|
+
} = await fetchEntries(props.api);
|
|
1125
|
+
if (alive) {
|
|
1126
|
+
setEntries(all);
|
|
1127
|
+
setLoadFailed(false);
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
// leave the previous list in place rather than flashing to blank, but
|
|
1131
|
+
// say so when there is nothing at all to show
|
|
1132
|
+
if (alive && entries().length === 0) setLoadFailed(true);
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const db = await openTranscriptDb();
|
|
1136
|
+
if (db) {
|
|
1137
|
+
try {
|
|
1138
|
+
const ids = await queryWaitingIds(db);
|
|
1139
|
+
if (alive) setWaiting(ids);
|
|
1140
|
+
} finally {
|
|
1141
|
+
db.close();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
// leave the previous waiting set in place rather than flashing
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
void load();
|
|
1149
|
+
const clock = setInterval(() => void load(), POLL_MS);
|
|
1150
|
+
onCleanup(() => {
|
|
1151
|
+
alive = false;
|
|
1152
|
+
clearInterval(clock);
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
const filteredEntries = createMemo(() => {
|
|
1156
|
+
const q = query().trim().toLowerCase();
|
|
1157
|
+
const ordered = entries();
|
|
1158
|
+
if (!q) return ordered;
|
|
1159
|
+
return ordered.filter(entry => entry.title.toLowerCase().includes(q) || entry.dir.toLowerCase().includes(q));
|
|
1160
|
+
});
|
|
1161
|
+
const shownEntries = createMemo(() => filteredEntries());
|
|
1162
|
+
const remaining = createMemo(() => Math.max(0, filteredEntries().length - shownEntries().length));
|
|
1163
|
+
const NEEDS_INPUT_DIR = "__needs_input__";
|
|
1164
|
+
const waitingEntries = createMemo(() => {
|
|
1165
|
+
const ids = waiting();
|
|
1166
|
+
return newestFirst(shownEntries().filter(entry => ids.has(entry.id)));
|
|
1167
|
+
});
|
|
1168
|
+
const tree = createMemo(() => {
|
|
1169
|
+
const rows = [];
|
|
1170
|
+
const pending = waitingEntries();
|
|
1171
|
+
if (pending.length > 0) {
|
|
1172
|
+
rows.push({
|
|
1173
|
+
kind: "group",
|
|
1174
|
+
dir: NEEDS_INPUT_DIR,
|
|
1175
|
+
label: "Needs input",
|
|
1176
|
+
count: pending.length
|
|
1177
|
+
});
|
|
1178
|
+
if (!collapsed()[NEEDS_INPUT_DIR]) {
|
|
1179
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1180
|
+
rows.push({
|
|
1181
|
+
kind: "item",
|
|
1182
|
+
entry: pending[i],
|
|
1183
|
+
last: i === pending.length - 1
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const groups = new Map();
|
|
1189
|
+
for (const entry of shownEntries()) {
|
|
1190
|
+
if (waiting().has(entry.id)) continue;
|
|
1191
|
+
const list = groups.get(entry.dir) ?? [];
|
|
1192
|
+
list.push(entry);
|
|
1193
|
+
groups.set(entry.dir, list);
|
|
1194
|
+
}
|
|
1195
|
+
const ordered = [...groups.entries()].map(([dir, list]) => ({
|
|
1196
|
+
dir,
|
|
1197
|
+
list,
|
|
1198
|
+
updated: Math.max(...list.map(entry => entry.updated))
|
|
1199
|
+
})).sort((a, b) => {
|
|
1200
|
+
const pins = props.pins();
|
|
1201
|
+
const rank = group => pins.directories.includes(group.dir) ? 2 : group.list.some(entry => pins.sessions.includes(entry.id)) ? 1 : 0;
|
|
1202
|
+
return rank(b) - rank(a) || b.updated - a.updated;
|
|
1203
|
+
});
|
|
1204
|
+
for (const group of ordered) {
|
|
1205
|
+
const sessions = newestFirst(group.list);
|
|
1206
|
+
rows.push({
|
|
1207
|
+
kind: "group",
|
|
1208
|
+
dir: group.dir,
|
|
1209
|
+
label: prettyDir(group.dir, home),
|
|
1210
|
+
count: group.list.length
|
|
1211
|
+
});
|
|
1212
|
+
if (collapsed()[group.dir]) continue;
|
|
1213
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
1214
|
+
rows.push({
|
|
1215
|
+
kind: "item",
|
|
1216
|
+
entry: sessions[i],
|
|
1217
|
+
last: i === sessions.length - 1
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return rows;
|
|
1222
|
+
});
|
|
1223
|
+
const toggleGroup = dir => {
|
|
1224
|
+
setCollapsed(prev => ({
|
|
1225
|
+
...prev,
|
|
1226
|
+
[dir]: !prev[dir]
|
|
1227
|
+
}));
|
|
1228
|
+
};
|
|
1229
|
+
const toggleSection = () => {
|
|
1230
|
+
if (!sectionCollapsed()) {
|
|
1231
|
+
cancelDelete();
|
|
1232
|
+
setNavActive(false);
|
|
1233
|
+
setSearching(false);
|
|
1234
|
+
setHovered(undefined);
|
|
1235
|
+
menu.close();
|
|
1236
|
+
}
|
|
1237
|
+
setSectionCollapsed(value => !value);
|
|
1238
|
+
};
|
|
1239
|
+
const openSession = entry => {
|
|
1240
|
+
// Opening a session hands input back to the main view: release every
|
|
1241
|
+
// sidebar capture first, so its key layers cannot shadow the composer's
|
|
1242
|
+
// once the new session is open.
|
|
1243
|
+
menu.close();
|
|
1244
|
+
cancelDelete();
|
|
1245
|
+
setNavActive(false);
|
|
1246
|
+
setSearching(false);
|
|
1247
|
+
setHovered(undefined);
|
|
1248
|
+
props.api.route.navigate("session", {
|
|
1249
|
+
sessionID: entry.id
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
// Flat order of the rows a cursor can land on, so keyboard navigation and
|
|
1254
|
+
// mouse hover resolve to the same row.
|
|
1255
|
+
const itemRows = createMemo(() => tree().flatMap(row => row.kind === "item" ? [row.entry] : []));
|
|
1256
|
+
const isActive = id => navActive() ? itemRows()[cursor()]?.id === id : hovered() === id;
|
|
1257
|
+
const isPendingDelete = id => pendingDelete() === id;
|
|
1258
|
+
const routeKey = createMemo(() => {
|
|
1259
|
+
const route = props.api.route.current;
|
|
1260
|
+
const id = route.name === "session" && typeof route.params?.sessionID === "string" ? route.params.sessionID : "";
|
|
1261
|
+
return `${route.name}:${id}`;
|
|
1262
|
+
});
|
|
1263
|
+
createEffect(on(routeKey, () => menu.close(), {
|
|
1264
|
+
defer: true
|
|
1265
|
+
}));
|
|
1266
|
+
// Navigating away also releases the sidebar's keyboard captures: a nav,
|
|
1267
|
+
// search, hover or armed delete left over from the previous session must
|
|
1268
|
+
// not shadow the composer's keys in the next one.
|
|
1269
|
+
createEffect(on(routeKey, () => {
|
|
1270
|
+
cancelDelete();
|
|
1271
|
+
setNavActive(false);
|
|
1272
|
+
setSearching(false);
|
|
1273
|
+
setHovered(undefined);
|
|
1274
|
+
}, {
|
|
1275
|
+
defer: true
|
|
1276
|
+
}));
|
|
1277
|
+
createEffect(() => {
|
|
1278
|
+
const state = menuState();
|
|
1279
|
+
if (state && !itemRows().some(entry => entry.id === state.entry.id)) menu.close();
|
|
1280
|
+
});
|
|
1281
|
+
createEffect(() => {
|
|
1282
|
+
if (props.api.ui.dialog.open) menu.close();
|
|
1283
|
+
// A modal dialog owns input while open: yield the sidebar's keyboard
|
|
1284
|
+
// captures too, so its layers cannot double-fire with the dialog's own.
|
|
1285
|
+
if (props.api.ui.dialog.open) {
|
|
1286
|
+
cancelDelete();
|
|
1287
|
+
setNavActive(false);
|
|
1288
|
+
setSearching(false);
|
|
1289
|
+
setHovered(undefined);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
const moveCursor = delta => {
|
|
1293
|
+
const count = itemRows().length;
|
|
1294
|
+
if (count === 0) return;
|
|
1295
|
+
setCursor(value => Math.max(0, Math.min(count - 1, value + delta)));
|
|
1296
|
+
};
|
|
1297
|
+
let previousRows = [];
|
|
1298
|
+
createEffect(() => {
|
|
1299
|
+
const rows = itemRows();
|
|
1300
|
+
const selected = previousRows[cursor()]?.id;
|
|
1301
|
+
const position = rows.findIndex(entry => entry.id === selected);
|
|
1302
|
+
if (position >= 0 && position !== cursor()) setCursor(position);else if (cursor() >= rows.length) setCursor(Math.max(0, rows.length - 1));
|
|
1303
|
+
previousRows = rows;
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Marker only: a spinner for a working agent, the open-session dot, and the
|
|
1307
|
+
// neutral row for idle sessions. Titles stay plain and no status text is
|
|
1308
|
+
// rendered.
|
|
1309
|
+
const markerOf = id => {
|
|
1310
|
+
let live;
|
|
1311
|
+
try {
|
|
1312
|
+
live = props.api.state.session.status(id);
|
|
1313
|
+
} catch {}
|
|
1314
|
+
return sidebarMarker(id === currentID(), live);
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// The spinner animates only while a listed session is actually running, so
|
|
1318
|
+
// an idle list costs no timer and only running rows re-render per frame.
|
|
1319
|
+
const [spinFrame, setSpinFrame] = createSignal(0);
|
|
1320
|
+
let spinTimer;
|
|
1321
|
+
createEffect(() => {
|
|
1322
|
+
const spinning = itemRows().some(entry => markerOf(entry.id) === "running");
|
|
1323
|
+
if (spinning && !spinTimer) {
|
|
1324
|
+
spinTimer = setInterval(() => setSpinFrame(value => (value + 1) % SIDEBAR_SPINNER.length), 120);
|
|
1325
|
+
} else if (!spinning && spinTimer) {
|
|
1326
|
+
clearInterval(spinTimer);
|
|
1327
|
+
spinTimer = undefined;
|
|
1328
|
+
setSpinFrame(0);
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
onCleanup(() => {
|
|
1332
|
+
if (spinTimer) clearInterval(spinTimer);
|
|
1333
|
+
});
|
|
1334
|
+
const cancelDelete = () => {
|
|
1335
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
1336
|
+
confirmTimer = undefined;
|
|
1337
|
+
setPendingDelete(undefined);
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// Deleting is irreversible, so ctrl+x arms the row and a second ctrl+x (or y)
|
|
1341
|
+
// commits it. The arm survives pointer movement — edge-hover flicker must
|
|
1342
|
+
// not eat the confirm — and is cleared by the five-second timeout, n, Esc,
|
|
1343
|
+
// collapsing the section, or the delete itself.
|
|
1344
|
+
const requestDelete = entry => {
|
|
1345
|
+
if (deleting()) return;
|
|
1346
|
+
if (pendingDelete() === entry.id) {
|
|
1347
|
+
void deleteEntry(entry);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
setPendingDelete(entry.id);
|
|
1351
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
1352
|
+
confirmTimer = setTimeout(() => setPendingDelete(undefined), 5000);
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
// The confirm always targets the visibly armed row ("ctrl+x again"),
|
|
1356
|
+
// wherever the pointer or cursor currently is. Returns true when an arm
|
|
1357
|
+
// was pending (and is now committed or discarded as stale).
|
|
1358
|
+
const confirmArmed = () => {
|
|
1359
|
+
const id = pendingDelete();
|
|
1360
|
+
if (!id) return false;
|
|
1361
|
+
const entry = entries().find(e => e.id === id);
|
|
1362
|
+
if (!entry) cancelDelete();else void deleteEntry(entry);
|
|
1363
|
+
return true;
|
|
1364
|
+
};
|
|
1365
|
+
let sidebarScroll;
|
|
1366
|
+
const deleteEntry = async entry => {
|
|
1367
|
+
cancelDelete();
|
|
1368
|
+
if (deleting()) return;
|
|
1369
|
+
setDeleting(entry.id);
|
|
1370
|
+
try {
|
|
1371
|
+
const result = await props.api.client.session.delete({
|
|
1372
|
+
sessionID: entry.id,
|
|
1373
|
+
directory: entry.dir || undefined
|
|
1374
|
+
});
|
|
1375
|
+
if (result && "error" in result && result.error) throw result.error;
|
|
1376
|
+
const {
|
|
1377
|
+
entries: remaining
|
|
1378
|
+
} = await fetchEntries(props.api);
|
|
1379
|
+
if (remaining.some(item => item.id === entry.id)) {
|
|
1380
|
+
setEntries(remaining);
|
|
1381
|
+
props.api.ui.toast({
|
|
1382
|
+
message: "Session was not deleted",
|
|
1383
|
+
variant: "error"
|
|
1384
|
+
});
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const scrollTop = sidebarScroll?.scrollTop ?? 0;
|
|
1388
|
+
setEntries(remaining);
|
|
1389
|
+
setHovered(undefined);
|
|
1390
|
+
const restore = () => {
|
|
1391
|
+
if (sidebarScroll) sidebarScroll.scrollTop = scrollTop;
|
|
1392
|
+
};
|
|
1393
|
+
queueMicrotask(restore);
|
|
1394
|
+
setTimeout(restore, 0);
|
|
1395
|
+
props.api.ui.toast({
|
|
1396
|
+
message: `Deleted "${truncate(entry.title, 40)}"`,
|
|
1397
|
+
variant: "info"
|
|
1398
|
+
});
|
|
1399
|
+
if (currentID() === entry.id) props.api.route.navigate("home");
|
|
1400
|
+
} catch {
|
|
1401
|
+
props.api.ui.toast({
|
|
1402
|
+
message: "Could not delete session",
|
|
1403
|
+
variant: "error"
|
|
1404
|
+
});
|
|
1405
|
+
} finally {
|
|
1406
|
+
setDeleting(undefined);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
const forkEntry = async entry => {
|
|
1410
|
+
try {
|
|
1411
|
+
const result = await props.api.client.session.fork({
|
|
1412
|
+
sessionID: entry.id,
|
|
1413
|
+
directory: entry.dir || undefined
|
|
1414
|
+
});
|
|
1415
|
+
const created = result?.data ?? result;
|
|
1416
|
+
if (!created?.id) throw new Error("fork returned no session");
|
|
1417
|
+
props.api.route.navigate("session", {
|
|
1418
|
+
sessionID: created.id
|
|
1419
|
+
});
|
|
1420
|
+
props.api.ui.toast({
|
|
1421
|
+
message: `Forked "${truncate(entry.title, 40)}"`,
|
|
1422
|
+
variant: "info"
|
|
1423
|
+
});
|
|
1424
|
+
} catch {
|
|
1425
|
+
props.api.ui.toast({
|
|
1426
|
+
message: "Could not fork session",
|
|
1427
|
+
variant: "error"
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
const menu = createContextMenu({
|
|
1432
|
+
registerLayer: layer => props.api.keymap.registerLayer(layer),
|
|
1433
|
+
actions: {
|
|
1434
|
+
open: entry => void openSession(entry),
|
|
1435
|
+
preview: entry => preview.open(entry),
|
|
1436
|
+
workspace: entry => openInCmuxWorkspace(props.api, entry),
|
|
1437
|
+
fork: entry => void forkEntry(entry),
|
|
1438
|
+
pin: entry => props.onTogglePin("sessions", entry.id),
|
|
1439
|
+
delete: entry => void deleteEntry(entry)
|
|
1440
|
+
},
|
|
1441
|
+
pinned: entry => props.pins().sessions.includes(entry.id),
|
|
1442
|
+
cmux: () => Boolean(process.env.CMUX_WORKSPACE_ID),
|
|
1443
|
+
blocked: () => props.api.ui.dialog.open,
|
|
1444
|
+
onChange: setMenuState
|
|
1445
|
+
});
|
|
1446
|
+
const menuView = createMemo(() => {
|
|
1447
|
+
const state = menuState();
|
|
1448
|
+
if (!state) return undefined;
|
|
1449
|
+
const box = contextMenuBox({
|
|
1450
|
+
x: state.x,
|
|
1451
|
+
y: state.y,
|
|
1452
|
+
...contextMenuSize(state.items)
|
|
1453
|
+
}, props.api.renderer.width, props.api.renderer.height);
|
|
1454
|
+
return {
|
|
1455
|
+
state,
|
|
1456
|
+
box
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
let disposeHoverSpace;
|
|
1460
|
+
let disposeSearch;
|
|
1461
|
+
let disposeNav;
|
|
1462
|
+
let disposeConfirm;
|
|
1463
|
+
let insideSearchBox = false;
|
|
1464
|
+
createEffect(() => {
|
|
1465
|
+
disposeConfirm?.();
|
|
1466
|
+
disposeConfirm = undefined;
|
|
1467
|
+
if (!pendingDelete()) return;
|
|
1468
|
+
disposeConfirm = props.api.keymap.registerLayer({
|
|
1469
|
+
mode: "base",
|
|
1470
|
+
priority: 25,
|
|
1471
|
+
bindings: [{
|
|
1472
|
+
key: "y",
|
|
1473
|
+
desc: "Confirm delete",
|
|
1474
|
+
preventDefault: true,
|
|
1475
|
+
cmd: () => {
|
|
1476
|
+
confirmArmed();
|
|
1477
|
+
}
|
|
1478
|
+
}, {
|
|
1479
|
+
key: "alt+d",
|
|
1480
|
+
desc: "Confirm delete",
|
|
1481
|
+
preventDefault: true,
|
|
1482
|
+
cmd: () => {
|
|
1483
|
+
confirmArmed();
|
|
1484
|
+
}
|
|
1485
|
+
}, {
|
|
1486
|
+
key: "enter",
|
|
1487
|
+
desc: "Confirm delete",
|
|
1488
|
+
preventDefault: true,
|
|
1489
|
+
cmd: () => {
|
|
1490
|
+
confirmArmed();
|
|
1491
|
+
}
|
|
1492
|
+
}, {
|
|
1493
|
+
key: "n",
|
|
1494
|
+
desc: "Cancel delete",
|
|
1495
|
+
preventDefault: true,
|
|
1496
|
+
cmd: cancelDelete
|
|
1497
|
+
}, {
|
|
1498
|
+
key: "escape",
|
|
1499
|
+
desc: "Cancel delete",
|
|
1500
|
+
preventDefault: true,
|
|
1501
|
+
cmd: cancelDelete
|
|
1502
|
+
}]
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
const onRootMouseDown = () => {
|
|
1506
|
+
menu.close();
|
|
1507
|
+
preview.closeIfOpen();
|
|
1508
|
+
if (insideSearchBox) {
|
|
1509
|
+
insideSearchBox = false;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (searching()) setSearching(false);
|
|
1513
|
+
// A click anywhere outside the sidebar means input is back in the main
|
|
1514
|
+
// view: release the nav layer, hover shortcuts and any armed delete so
|
|
1515
|
+
// the sidebar's keys and rows stop shadowing the composer.
|
|
1516
|
+
cancelDelete();
|
|
1517
|
+
if (navActive()) setNavActive(false);
|
|
1518
|
+
setHovered(undefined);
|
|
1519
|
+
};
|
|
1520
|
+
onMount(() => {
|
|
1521
|
+
ensureRootMouse(props.api.renderer);
|
|
1522
|
+
rootMouseHandlers.add(onRootMouseDown);
|
|
1523
|
+
const closeMenu = () => menu.close();
|
|
1524
|
+
props.api.renderer.on("resize", closeMenu);
|
|
1525
|
+
onCleanup(() => {
|
|
1526
|
+
rootMouseHandlers.delete(onRootMouseDown);
|
|
1527
|
+
props.api.renderer.off("resize", closeMenu);
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
const appendQuery = input => {
|
|
1531
|
+
if (input === "\b") setQuery(value => value.slice(0, -1));else setQuery(value => value + input);
|
|
1532
|
+
};
|
|
1533
|
+
createEffect(() => {
|
|
1534
|
+
disposeSearch?.();
|
|
1535
|
+
disposeSearch = undefined;
|
|
1536
|
+
if (!searching()) return;
|
|
1537
|
+
disposeSearch = props.api.keymap.registerLayer({
|
|
1538
|
+
mode: "base",
|
|
1539
|
+
priority: 20,
|
|
1540
|
+
bindings: searchBindings(appendQuery, () => setSearching(false))
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
const openSidebarPreview = entry => preview.open(entry);
|
|
1544
|
+
createEffect(() => {
|
|
1545
|
+
disposeHoverSpace?.();
|
|
1546
|
+
disposeHoverSpace = undefined;
|
|
1547
|
+
const id = hovered();
|
|
1548
|
+
if (!id) return;
|
|
1549
|
+
const entry = entries().find(e => e.id === id);
|
|
1550
|
+
if (!entry) return;
|
|
1551
|
+
disposeHoverSpace = props.api.keymap.registerLayer({
|
|
1552
|
+
mode: "base",
|
|
1553
|
+
priority: 20,
|
|
1554
|
+
bindings: [{
|
|
1555
|
+
key: "alt+p",
|
|
1556
|
+
desc: "Preview session transcript",
|
|
1557
|
+
preventDefault: true,
|
|
1558
|
+
cmd: () => openSidebarPreview(entry)
|
|
1559
|
+
}, {
|
|
1560
|
+
key: "alt+d",
|
|
1561
|
+
desc: "Delete session",
|
|
1562
|
+
preventDefault: true,
|
|
1563
|
+
cmd: () => {
|
|
1564
|
+
if (!confirmArmed()) requestDelete(entry);
|
|
1565
|
+
}
|
|
1566
|
+
}, {
|
|
1567
|
+
key: "alt+s",
|
|
1568
|
+
desc: "Pin session",
|
|
1569
|
+
preventDefault: true,
|
|
1570
|
+
cmd: () => props.onTogglePin("sessions", entry.id)
|
|
1571
|
+
}, {
|
|
1572
|
+
key: "alt+r",
|
|
1573
|
+
desc: "Pin directory",
|
|
1574
|
+
preventDefault: true,
|
|
1575
|
+
cmd: () => props.onTogglePin("directories", entry.dir)
|
|
1576
|
+
}, {
|
|
1577
|
+
key: "/",
|
|
1578
|
+
desc: "Search sessions",
|
|
1579
|
+
preventDefault: true,
|
|
1580
|
+
cmd: () => setSearching(true)
|
|
1581
|
+
}]
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// Keyboard navigation is opt-in (click the "Sessions" heading or the search
|
|
1586
|
+
// box) so the sidebar never steals the arrow keys from the main prompt.
|
|
1587
|
+
// Clicking anywhere outside the sidebar, opening a session, or navigating
|
|
1588
|
+
// releases it again, so its layers cannot shadow the composer afterwards.
|
|
1589
|
+
createEffect(() => {
|
|
1590
|
+
disposeNav?.();
|
|
1591
|
+
disposeNav = undefined;
|
|
1592
|
+
if (!navActive()) return;
|
|
1593
|
+
const bindings = [{
|
|
1594
|
+
key: "up",
|
|
1595
|
+
desc: "Previous session",
|
|
1596
|
+
preventDefault: true,
|
|
1597
|
+
cmd: () => moveCursor(-1)
|
|
1598
|
+
}, {
|
|
1599
|
+
key: "down",
|
|
1600
|
+
desc: "Next session",
|
|
1601
|
+
preventDefault: true,
|
|
1602
|
+
cmd: () => moveCursor(1)
|
|
1603
|
+
}, {
|
|
1604
|
+
key: "pageup",
|
|
1605
|
+
desc: "Page up",
|
|
1606
|
+
preventDefault: true,
|
|
1607
|
+
cmd: () => moveCursor(-5)
|
|
1608
|
+
}, {
|
|
1609
|
+
key: "pagedown",
|
|
1610
|
+
desc: "Page down",
|
|
1611
|
+
preventDefault: true,
|
|
1612
|
+
cmd: () => moveCursor(5)
|
|
1613
|
+
}, {
|
|
1614
|
+
key: "home",
|
|
1615
|
+
desc: "First session",
|
|
1616
|
+
preventDefault: true,
|
|
1617
|
+
cmd: () => setCursor(0)
|
|
1618
|
+
}, {
|
|
1619
|
+
key: "end",
|
|
1620
|
+
desc: "Last session",
|
|
1621
|
+
preventDefault: true,
|
|
1622
|
+
cmd: () => setCursor(Math.max(0, itemRows().length - 1))
|
|
1623
|
+
}, {
|
|
1624
|
+
key: "alt+p",
|
|
1625
|
+
desc: "Preview session transcript",
|
|
1626
|
+
preventDefault: true,
|
|
1627
|
+
cmd: () => {
|
|
1628
|
+
const entry = itemRows()[cursor()];
|
|
1629
|
+
if (entry) openSidebarPreview(entry);
|
|
1630
|
+
}
|
|
1631
|
+
}, {
|
|
1632
|
+
key: "alt+d",
|
|
1633
|
+
desc: "Delete session",
|
|
1634
|
+
preventDefault: true,
|
|
1635
|
+
cmd: () => {
|
|
1636
|
+
if (confirmArmed()) return;
|
|
1637
|
+
const entry = itemRows()[cursor()];
|
|
1638
|
+
if (entry) requestDelete(entry);
|
|
1639
|
+
}
|
|
1640
|
+
}, {
|
|
1641
|
+
key: "alt+s",
|
|
1642
|
+
desc: "Pin session",
|
|
1643
|
+
preventDefault: true,
|
|
1644
|
+
cmd: () => {
|
|
1645
|
+
const entry = itemRows()[cursor()];
|
|
1646
|
+
if (entry) props.onTogglePin("sessions", entry.id);
|
|
1647
|
+
}
|
|
1648
|
+
}, {
|
|
1649
|
+
key: "alt+r",
|
|
1650
|
+
desc: "Pin directory",
|
|
1651
|
+
preventDefault: true,
|
|
1652
|
+
cmd: () => {
|
|
1653
|
+
const entry = itemRows()[cursor()];
|
|
1654
|
+
if (entry) props.onTogglePin("directories", entry.dir);
|
|
1655
|
+
}
|
|
1656
|
+
}, {
|
|
1657
|
+
key: "enter",
|
|
1658
|
+
desc: "Open session",
|
|
1659
|
+
preventDefault: true,
|
|
1660
|
+
cmd: () => {
|
|
1661
|
+
if (confirmArmed()) return;
|
|
1662
|
+
const entry = itemRows()[cursor()];
|
|
1663
|
+
if (entry) void openSession(entry);
|
|
1664
|
+
}
|
|
1665
|
+
}, {
|
|
1666
|
+
key: "escape",
|
|
1667
|
+
desc: "Leave the session list",
|
|
1668
|
+
preventDefault: true,
|
|
1669
|
+
cmd: () => {
|
|
1670
|
+
cancelDelete();
|
|
1671
|
+
setNavActive(false);
|
|
1672
|
+
}
|
|
1673
|
+
}, ...(pendingDelete() ? [{
|
|
1674
|
+
key: "y",
|
|
1675
|
+
desc: "Confirm delete",
|
|
1676
|
+
preventDefault: true,
|
|
1677
|
+
cmd: () => {
|
|
1678
|
+
confirmArmed();
|
|
1679
|
+
}
|
|
1680
|
+
}, {
|
|
1681
|
+
key: "n",
|
|
1682
|
+
desc: "Cancel delete",
|
|
1683
|
+
preventDefault: true,
|
|
1684
|
+
cmd: cancelDelete
|
|
1685
|
+
}] : [])];
|
|
1686
|
+
disposeNav = props.api.keymap.registerLayer({
|
|
1687
|
+
mode: "base",
|
|
1688
|
+
priority: 20,
|
|
1689
|
+
bindings
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
onCleanup(() => {
|
|
1693
|
+
disposeHoverSpace?.();
|
|
1694
|
+
disposeSearch?.();
|
|
1695
|
+
disposeNav?.();
|
|
1696
|
+
disposeConfirm?.();
|
|
1697
|
+
menu.close();
|
|
1698
|
+
const container = portalContainer;
|
|
1699
|
+
portalContainer = undefined;
|
|
1700
|
+
process.nextTick(() => container?.destroyRecursively?.());
|
|
1701
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
1702
|
+
});
|
|
1703
|
+
return (() => {
|
|
1704
|
+
var _el$11 = _$createElement("box"),
|
|
1705
|
+
_el$12 = _$createElement("box"),
|
|
1706
|
+
_el$13 = _$createElement("text"),
|
|
1707
|
+
_el$14 = _$createElement("b"),
|
|
1708
|
+
_el$16 = _$createElement("span"),
|
|
1709
|
+
_el$17 = _$createElement("span"),
|
|
1710
|
+
_el$21 = _$createElement("text");
|
|
1711
|
+
_$insertNode(_el$11, _el$12);
|
|
1712
|
+
_$setProp(_el$11, "flexDirection", "column");
|
|
1713
|
+
_$setProp(_el$11, "paddingRight", 1);
|
|
1714
|
+
_$setProp(_el$11, "flexGrow", 1);
|
|
1715
|
+
_$setProp(_el$11, "flexShrink", 1);
|
|
1716
|
+
_$insertNode(_el$12, _el$13);
|
|
1717
|
+
_$insertNode(_el$12, _el$21);
|
|
1718
|
+
_$setProp(_el$12, "flexDirection", "row");
|
|
1719
|
+
_$setProp(_el$12, "justifyContent", "space-between");
|
|
1720
|
+
_$setProp(_el$12, "gap", 1);
|
|
1721
|
+
_$insertNode(_el$13, _el$14);
|
|
1722
|
+
_$insertNode(_el$13, _el$16);
|
|
1723
|
+
_$insertNode(_el$13, _el$17);
|
|
1724
|
+
_$setProp(_el$13, "flexShrink", 1);
|
|
1725
|
+
_$setProp(_el$13, "onMouseDown", event => {
|
|
1726
|
+
// Sidebar-internal: keep this click from reaching the root handler,
|
|
1727
|
+
// which would release the nav layer this toggle just activated.
|
|
1728
|
+
event.stopPropagation();
|
|
1729
|
+
if (!sectionCollapsed()) setNavActive(value => !value);
|
|
1730
|
+
});
|
|
1731
|
+
_$insertNode(_el$14, _$createTextNode(`Sessions`));
|
|
1732
|
+
_$insert(_el$16, (() => {
|
|
1733
|
+
var _c$2 = _$memo(() => !!query().trim());
|
|
1734
|
+
return () => _c$2() ? ` (${filteredEntries().length}/${entries().length})` : ` (${entries().length})`;
|
|
1735
|
+
})());
|
|
1736
|
+
_$insert(_el$17, () => navActive() ? " · nav" : "");
|
|
1737
|
+
_$insert(_el$13, _$createComponent(Show, {
|
|
1738
|
+
get when() {
|
|
1739
|
+
return waitingEntries().length > 0;
|
|
1740
|
+
},
|
|
1741
|
+
get children() {
|
|
1742
|
+
var _el$18 = _$createElement("span"),
|
|
1743
|
+
_el$19 = _$createTextNode(` · `),
|
|
1744
|
+
_el$20 = _$createTextNode(` need input`);
|
|
1745
|
+
_$insertNode(_el$18, _el$19);
|
|
1746
|
+
_$insertNode(_el$18, _el$20);
|
|
1747
|
+
_$insert(_el$18, () => waitingEntries().length, _el$20);
|
|
1748
|
+
_$effect(_$p => _$setProp(_el$18, "style", {
|
|
1749
|
+
fg: theme().warning
|
|
1750
|
+
}, _$p));
|
|
1751
|
+
return _el$18;
|
|
1752
|
+
}
|
|
1753
|
+
}), null);
|
|
1754
|
+
_$setProp(_el$21, "flexShrink", 0);
|
|
1755
|
+
_$setProp(_el$21, "onMouseDown", event => {
|
|
1756
|
+
event.stopPropagation();
|
|
1757
|
+
toggleSection();
|
|
1758
|
+
});
|
|
1759
|
+
_$insert(_el$21, () => sectionCollapsed() ? "▸ show" : "▾ hide");
|
|
1760
|
+
_$insert(_el$11, _$createComponent(Show, {
|
|
1761
|
+
get when() {
|
|
1762
|
+
return !sectionCollapsed();
|
|
1763
|
+
},
|
|
1764
|
+
get children() {
|
|
1765
|
+
return _$createComponent(Show, {
|
|
1766
|
+
get when() {
|
|
1767
|
+
return entries().length > 0;
|
|
1768
|
+
},
|
|
1769
|
+
get fallback() {
|
|
1770
|
+
return (() => {
|
|
1771
|
+
var _el$33 = _$createElement("text");
|
|
1772
|
+
_$insert(_el$33, () => loadFailed() ? "Session list unavailable · will retry" : "No sessions yet · option+o to browse");
|
|
1773
|
+
_$effect(_$p => _$setProp(_el$33, "style", {
|
|
1774
|
+
fg: theme().textMuted
|
|
1775
|
+
}, _$p));
|
|
1776
|
+
return _el$33;
|
|
1777
|
+
})();
|
|
1778
|
+
},
|
|
1779
|
+
get children() {
|
|
1780
|
+
return [(() => {
|
|
1781
|
+
var _el$22 = _$createElement("box"),
|
|
1782
|
+
_el$23 = _$createElement("text");
|
|
1783
|
+
_$insertNode(_el$22, _el$23);
|
|
1784
|
+
_$setProp(_el$22, "flexDirection", "row");
|
|
1785
|
+
_$setProp(_el$22, "gap", 1);
|
|
1786
|
+
_$setProp(_el$22, "paddingRight", 1);
|
|
1787
|
+
_$setProp(_el$22, "onMouseDown", event => {
|
|
1788
|
+
// Sidebar-internal: the root handler must not see this click, or
|
|
1789
|
+
// it would clear the search this box just activated.
|
|
1790
|
+
event.stopPropagation();
|
|
1791
|
+
insideSearchBox = true;
|
|
1792
|
+
setSearching(true);
|
|
1793
|
+
});
|
|
1794
|
+
_$setProp(_el$23, "flexGrow", 1);
|
|
1795
|
+
_$setProp(_el$23, "flexShrink", 1);
|
|
1796
|
+
_$setProp(_el$23, "overflow", "hidden");
|
|
1797
|
+
_$setProp(_el$23, "wrapMode", "none");
|
|
1798
|
+
_$insert(_el$23, () => query() || (searching() ? "" : "search…"), null);
|
|
1799
|
+
_$insert(_el$23, () => searching() ? "▏" : "", null);
|
|
1800
|
+
_$insert(_el$22, _$createComponent(Show, {
|
|
1801
|
+
get when() {
|
|
1802
|
+
return query();
|
|
1803
|
+
},
|
|
1804
|
+
get children() {
|
|
1805
|
+
var _el$24 = _$createElement("text");
|
|
1806
|
+
_$insertNode(_el$24, _$createTextNode(`✕`));
|
|
1807
|
+
_$setProp(_el$24, "flexShrink", 0);
|
|
1808
|
+
_$setProp(_el$24, "onMouseDown", event => {
|
|
1809
|
+
event.stopPropagation();
|
|
1810
|
+
setQuery("");
|
|
1811
|
+
});
|
|
1812
|
+
_$effect(_$p => _$setProp(_el$24, "style", {
|
|
1813
|
+
fg: theme().textMuted
|
|
1814
|
+
}, _$p));
|
|
1815
|
+
return _el$24;
|
|
1816
|
+
}
|
|
1817
|
+
}), null);
|
|
1818
|
+
_$effect(_p$ => {
|
|
1819
|
+
var _v$10 = searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
1820
|
+
_v$11 = {
|
|
1821
|
+
fg: query() ? theme().text : theme().textMuted
|
|
1822
|
+
};
|
|
1823
|
+
_v$10 !== _p$.e && (_p$.e = _$setProp(_el$22, "backgroundColor", _v$10, _p$.e));
|
|
1824
|
+
_v$11 !== _p$.t && (_p$.t = _$setProp(_el$23, "style", _v$11, _p$.t));
|
|
1825
|
+
return _p$;
|
|
1826
|
+
}, {
|
|
1827
|
+
e: undefined,
|
|
1828
|
+
t: undefined
|
|
1829
|
+
});
|
|
1830
|
+
return _el$22;
|
|
1831
|
+
})(), (() => {
|
|
1832
|
+
var _el$26 = _$createElement("scrollbox");
|
|
1833
|
+
_$use(node => sidebarScroll = node, _el$26);
|
|
1834
|
+
_$setProp(_el$26, "flexGrow", 1);
|
|
1835
|
+
_$setProp(_el$26, "flexShrink", 1);
|
|
1836
|
+
_$setProp(_el$26, "verticalScrollbarOptions", {
|
|
1837
|
+
visible: false
|
|
1838
|
+
});
|
|
1839
|
+
_$setProp(_el$26, "horizontalScrollbarOptions", {
|
|
1840
|
+
visible: false
|
|
1841
|
+
});
|
|
1842
|
+
_$setProp(_el$26, "onMouseScroll", () => menu.close());
|
|
1843
|
+
_$insert(_el$26, _$createComponent(For, {
|
|
1844
|
+
get each() {
|
|
1845
|
+
return tree();
|
|
1846
|
+
},
|
|
1847
|
+
children: row => {
|
|
1848
|
+
if (row.kind === "group") return (() => {
|
|
1849
|
+
var _el$34 = _$createElement("box"),
|
|
1850
|
+
_el$35 = _$createElement("text"),
|
|
1851
|
+
_el$36 = _$createElement("span"),
|
|
1852
|
+
_el$37 = _$createElement("b"),
|
|
1853
|
+
_el$38 = _$createElement("text"),
|
|
1854
|
+
_el$39 = _$createTextNode(` (`),
|
|
1855
|
+
_el$41 = _$createTextNode(`)`);
|
|
1856
|
+
_$insertNode(_el$34, _el$35);
|
|
1857
|
+
_$insertNode(_el$34, _el$38);
|
|
1858
|
+
_$setProp(_el$34, "flexDirection", "row");
|
|
1859
|
+
_$setProp(_el$34, "paddingTop", 1);
|
|
1860
|
+
_$setProp(_el$34, "onMouseDown", event => {
|
|
1861
|
+
event.stopPropagation();
|
|
1862
|
+
if (event.button === RIGHT_BUTTON) return;
|
|
1863
|
+
toggleGroup(row.dir);
|
|
1864
|
+
});
|
|
1865
|
+
_$insertNode(_el$35, _el$36);
|
|
1866
|
+
_$insertNode(_el$35, _el$37);
|
|
1867
|
+
_$setProp(_el$35, "flexGrow", 1);
|
|
1868
|
+
_$setProp(_el$35, "flexShrink", 1);
|
|
1869
|
+
_$setProp(_el$35, "wrapMode", "none");
|
|
1870
|
+
_$insert(_el$36, () => collapsed()[row.dir] ? "▸ " : "▾ ");
|
|
1871
|
+
_$insert(_el$37, () => props.pins().directories.includes(row.dir) ? "★ " : "", null);
|
|
1872
|
+
_$insert(_el$37, () => truncate(row.label, SIDEBAR_GROUP_WIDTH - (props.pins().directories.includes(row.dir) ? 2 : 0)), null);
|
|
1873
|
+
_$insertNode(_el$38, _el$39);
|
|
1874
|
+
_$insertNode(_el$38, _el$41);
|
|
1875
|
+
_$setProp(_el$38, "flexShrink", 0);
|
|
1876
|
+
_$insert(_el$38, () => row.count, _el$41);
|
|
1877
|
+
_$effect(_p$ => {
|
|
1878
|
+
var _v$15 = {
|
|
1879
|
+
fg: theme().textMuted
|
|
1880
|
+
},
|
|
1881
|
+
_v$16 = {
|
|
1882
|
+
fg: theme().textMuted
|
|
1883
|
+
};
|
|
1884
|
+
_v$15 !== _p$.e && (_p$.e = _$setProp(_el$36, "style", _v$15, _p$.e));
|
|
1885
|
+
_v$16 !== _p$.t && (_p$.t = _$setProp(_el$38, "style", _v$16, _p$.t));
|
|
1886
|
+
return _p$;
|
|
1887
|
+
}, {
|
|
1888
|
+
e: undefined,
|
|
1889
|
+
t: undefined
|
|
1890
|
+
});
|
|
1891
|
+
return _el$34;
|
|
1892
|
+
})();
|
|
1893
|
+
const marker = createMemo(() => markerOf(row.entry.id));
|
|
1894
|
+
return (() => {
|
|
1895
|
+
var _el$42 = _$createElement("box"),
|
|
1896
|
+
_el$43 = _$createElement("text"),
|
|
1897
|
+
_el$44 = _$createElement("span"),
|
|
1898
|
+
_el$45 = _$createElement("span"),
|
|
1899
|
+
_el$46 = _$createElement("text");
|
|
1900
|
+
_$insertNode(_el$42, _el$43);
|
|
1901
|
+
_$insertNode(_el$42, _el$46);
|
|
1902
|
+
_$setProp(_el$42, "flexDirection", "row");
|
|
1903
|
+
_$setProp(_el$42, "gap", 1);
|
|
1904
|
+
_$setProp(_el$42, "paddingLeft", 1);
|
|
1905
|
+
_$setProp(_el$42, "paddingRight", 1);
|
|
1906
|
+
_$setProp(_el$42, "onMouseOver", () => {
|
|
1907
|
+
setHovered(row.entry.id);
|
|
1908
|
+
// The keyboard layer wins over the hover layer while nav is
|
|
1909
|
+
// active. Keep its cursor on the hovered row so Ctrl-S/Ctrl-D
|
|
1910
|
+
// cannot silently pin a different (often current) session.
|
|
1911
|
+
if (navActive()) {
|
|
1912
|
+
const index = itemRows().findIndex(entry => entry.id === row.entry.id);
|
|
1913
|
+
if (index >= 0) setCursor(index);
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
_$setProp(_el$42, "onMouseOut", () => setHovered(undefined));
|
|
1917
|
+
_$setProp(_el$42, "onMouseDown", event => {
|
|
1918
|
+
if (event.button === RIGHT_BUTTON) {
|
|
1919
|
+
event.stopPropagation();
|
|
1920
|
+
menu.open(row.entry, event.x, event.y);
|
|
1921
|
+
} else {
|
|
1922
|
+
// Sidebar-internal: openSession releases the sidebar's own
|
|
1923
|
+
// captures, so this click must not also reach the root
|
|
1924
|
+
// handler as an outside click.
|
|
1925
|
+
event.stopPropagation();
|
|
1926
|
+
void openSession(row.entry);
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
_$insertNode(_el$43, _el$44);
|
|
1930
|
+
_$insertNode(_el$43, _el$45);
|
|
1931
|
+
_$setProp(_el$43, "flexShrink", 0);
|
|
1932
|
+
_$insert(_el$44, () => row.last ? "└" : "├");
|
|
1933
|
+
_$insert(_el$45, (() => {
|
|
1934
|
+
var _c$3 = _$memo(() => marker() === "current");
|
|
1935
|
+
return () => _c$3() ? "●" : _$memo(() => marker() === "running")() ? SIDEBAR_SPINNER[spinFrame()] : isActive(row.entry.id) ? "▸" : "○";
|
|
1936
|
+
})());
|
|
1937
|
+
_$insert(_el$42, _$createComponent(Highlighted, {
|
|
1938
|
+
get text() {
|
|
1939
|
+
return `${props.pins().sessions.includes(row.entry.id) ? "★ " : ""}${truncate(row.entry.title, SIDEBAR_TITLE_WIDTH - (props.pins().sessions.includes(row.entry.id) ? 2 : 0))}`;
|
|
1940
|
+
},
|
|
1941
|
+
get query() {
|
|
1942
|
+
return query();
|
|
1943
|
+
},
|
|
1944
|
+
get color() {
|
|
1945
|
+
return _$memo(() => !!isPendingDelete(row.entry.id))() ? theme().error : theme().text;
|
|
1946
|
+
},
|
|
1947
|
+
get matchColor() {
|
|
1948
|
+
return theme().warning;
|
|
1949
|
+
}
|
|
1950
|
+
}), _el$46);
|
|
1951
|
+
_$setProp(_el$46, "flexShrink", 0);
|
|
1952
|
+
_$insert(_el$46, (() => {
|
|
1953
|
+
var _c$4 = _$memo(() => !!isPendingDelete(row.entry.id));
|
|
1954
|
+
return () => _c$4() ? "option+d again" : ago(row.entry.updated);
|
|
1955
|
+
})());
|
|
1956
|
+
_$effect(_p$ => {
|
|
1957
|
+
var _v$17 = isActive(row.entry.id) && row.entry.id !== currentID() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
1958
|
+
_v$18 = {
|
|
1959
|
+
fg: theme().textMuted
|
|
1960
|
+
},
|
|
1961
|
+
_v$19 = {
|
|
1962
|
+
fg: isPendingDelete(row.entry.id) ? theme().error : marker() === "current" ? theme().accent : marker() === "running" ? theme().success : theme().textMuted
|
|
1963
|
+
},
|
|
1964
|
+
_v$20 = {
|
|
1965
|
+
fg: isPendingDelete(row.entry.id) ? theme().error : theme().textMuted
|
|
1966
|
+
};
|
|
1967
|
+
_v$17 !== _p$.e && (_p$.e = _$setProp(_el$42, "backgroundColor", _v$17, _p$.e));
|
|
1968
|
+
_v$18 !== _p$.t && (_p$.t = _$setProp(_el$44, "style", _v$18, _p$.t));
|
|
1969
|
+
_v$19 !== _p$.a && (_p$.a = _$setProp(_el$45, "style", _v$19, _p$.a));
|
|
1970
|
+
_v$20 !== _p$.o && (_p$.o = _$setProp(_el$46, "style", _v$20, _p$.o));
|
|
1971
|
+
return _p$;
|
|
1972
|
+
}, {
|
|
1973
|
+
e: undefined,
|
|
1974
|
+
t: undefined,
|
|
1975
|
+
a: undefined,
|
|
1976
|
+
o: undefined
|
|
1977
|
+
});
|
|
1978
|
+
return _el$42;
|
|
1979
|
+
})();
|
|
1980
|
+
}
|
|
1981
|
+
}));
|
|
1982
|
+
return _el$26;
|
|
1983
|
+
})(), _$createComponent(Show, {
|
|
1984
|
+
get when() {
|
|
1985
|
+
return remaining() > 0;
|
|
1986
|
+
},
|
|
1987
|
+
get children() {
|
|
1988
|
+
var _el$27 = _$createElement("text");
|
|
1989
|
+
_$insert(_el$27, () => `… ${remaining()} more · option+o for all`);
|
|
1990
|
+
_$effect(_$p => _$setProp(_el$27, "style", {
|
|
1991
|
+
fg: theme().textMuted
|
|
1992
|
+
}, _$p));
|
|
1993
|
+
return _el$27;
|
|
1994
|
+
}
|
|
1995
|
+
}), _$createComponent(Show, {
|
|
1996
|
+
get when() {
|
|
1997
|
+
return _$memo(() => !!query().trim())() && filteredEntries().length === 0;
|
|
1998
|
+
},
|
|
1999
|
+
get children() {
|
|
2000
|
+
var _el$28 = _$createElement("text");
|
|
2001
|
+
_$insertNode(_el$28, _$createTextNode(`No title or directory matches · option+o to search transcripts`));
|
|
2002
|
+
_$effect(_$p => _$setProp(_el$28, "style", {
|
|
2003
|
+
fg: theme().textMuted
|
|
2004
|
+
}, _$p));
|
|
2005
|
+
return _el$28;
|
|
2006
|
+
}
|
|
2007
|
+
}), _$createComponent(Show, {
|
|
2008
|
+
get when() {
|
|
2009
|
+
return navActive();
|
|
2010
|
+
},
|
|
2011
|
+
get children() {
|
|
2012
|
+
var _el$30 = _$createElement("box"),
|
|
2013
|
+
_el$31 = _$createElement("text");
|
|
2014
|
+
_$insertNode(_el$30, _el$31);
|
|
2015
|
+
_$setProp(_el$30, "paddingTop", 1);
|
|
2016
|
+
_$insertNode(_el$31, _$createTextNode(`↑↓ move · enter open · option+p preview · option+s/r pin · option+d delete · esc done`));
|
|
2017
|
+
_$effect(_$p => _$setProp(_el$31, "style", {
|
|
2018
|
+
fg: theme().textMuted
|
|
2019
|
+
}, _$p));
|
|
2020
|
+
return _el$30;
|
|
2021
|
+
}
|
|
2022
|
+
})];
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}), null);
|
|
2027
|
+
_$insert(_el$11, _$createComponent(Portal, {
|
|
2028
|
+
get mount() {
|
|
2029
|
+
return props.api.renderer.root;
|
|
2030
|
+
},
|
|
2031
|
+
ref: container => {
|
|
2032
|
+
portalContainer = container;
|
|
2033
|
+
container.position = "absolute";
|
|
2034
|
+
container.left = 0;
|
|
2035
|
+
container.top = 0;
|
|
2036
|
+
container.zIndex = 1000;
|
|
2037
|
+
},
|
|
2038
|
+
get children() {
|
|
2039
|
+
return _$createComponent(Show, {
|
|
2040
|
+
get when() {
|
|
2041
|
+
return menuView();
|
|
2042
|
+
},
|
|
2043
|
+
keyed: true,
|
|
2044
|
+
children: view => (() => {
|
|
2045
|
+
var _el$47 = _$createElement("box");
|
|
2046
|
+
_$setProp(_el$47, "position", "absolute");
|
|
2047
|
+
_$setProp(_el$47, "flexDirection", "column");
|
|
2048
|
+
_$setProp(_el$47, "border", true);
|
|
2049
|
+
_$setProp(_el$47, "borderStyle", "rounded");
|
|
2050
|
+
_$setProp(_el$47, "paddingLeft", 1);
|
|
2051
|
+
_$setProp(_el$47, "paddingRight", 1);
|
|
2052
|
+
_$setProp(_el$47, "onMouseDown", event => event.stopPropagation());
|
|
2053
|
+
_$setProp(_el$47, "onMouseUp", event => event.stopPropagation());
|
|
2054
|
+
_$insert(_el$47, _$createComponent(For, {
|
|
2055
|
+
get each() {
|
|
2056
|
+
return view.state.items;
|
|
2057
|
+
},
|
|
2058
|
+
children: (item, index) => (() => {
|
|
2059
|
+
var _el$48 = _$createElement("box"),
|
|
2060
|
+
_el$49 = _$createElement("text");
|
|
2061
|
+
_$insertNode(_el$48, _el$49);
|
|
2062
|
+
_$setProp(_el$48, "height", 1);
|
|
2063
|
+
_$setProp(_el$48, "onMouseOver", () => menu.select(index()));
|
|
2064
|
+
_$setProp(_el$48, "onMouseDown", event => event.stopPropagation());
|
|
2065
|
+
_$setProp(_el$48, "onMouseUp", event => {
|
|
2066
|
+
event.stopPropagation();
|
|
2067
|
+
menu.click(index(), event.button);
|
|
2068
|
+
});
|
|
2069
|
+
_$setProp(_el$49, "wrapMode", "none");
|
|
2070
|
+
_$insert(_el$49, () => item.label);
|
|
2071
|
+
_$effect(_p$ => {
|
|
2072
|
+
var _v$27 = view.state.index === index() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
2073
|
+
_v$28 = {
|
|
2074
|
+
fg: item.id === "delete" || item.id === "confirm" ? theme().error : view.state.index === index() ? theme().text : theme().textMuted
|
|
2075
|
+
};
|
|
2076
|
+
_v$27 !== _p$.e && (_p$.e = _$setProp(_el$48, "backgroundColor", _v$27, _p$.e));
|
|
2077
|
+
_v$28 !== _p$.t && (_p$.t = _$setProp(_el$49, "style", _v$28, _p$.t));
|
|
2078
|
+
return _p$;
|
|
2079
|
+
}, {
|
|
2080
|
+
e: undefined,
|
|
2081
|
+
t: undefined
|
|
2082
|
+
});
|
|
2083
|
+
return _el$48;
|
|
2084
|
+
})()
|
|
2085
|
+
}));
|
|
2086
|
+
_$effect(_p$ => {
|
|
2087
|
+
var _v$21 = view.box.left,
|
|
2088
|
+
_v$22 = view.box.top,
|
|
2089
|
+
_v$23 = view.box.width,
|
|
2090
|
+
_v$24 = view.box.height,
|
|
2091
|
+
_v$25 = theme().borderActive,
|
|
2092
|
+
_v$26 = theme().backgroundPanel;
|
|
2093
|
+
_v$21 !== _p$.e && (_p$.e = _$setProp(_el$47, "left", _v$21, _p$.e));
|
|
2094
|
+
_v$22 !== _p$.t && (_p$.t = _$setProp(_el$47, "top", _v$22, _p$.t));
|
|
2095
|
+
_v$23 !== _p$.a && (_p$.a = _$setProp(_el$47, "width", _v$23, _p$.a));
|
|
2096
|
+
_v$24 !== _p$.o && (_p$.o = _$setProp(_el$47, "height", _v$24, _p$.o));
|
|
2097
|
+
_v$25 !== _p$.i && (_p$.i = _$setProp(_el$47, "borderColor", _v$25, _p$.i));
|
|
2098
|
+
_v$26 !== _p$.n && (_p$.n = _$setProp(_el$47, "backgroundColor", _v$26, _p$.n));
|
|
2099
|
+
return _p$;
|
|
2100
|
+
}, {
|
|
2101
|
+
e: undefined,
|
|
2102
|
+
t: undefined,
|
|
2103
|
+
a: undefined,
|
|
2104
|
+
o: undefined,
|
|
2105
|
+
i: undefined,
|
|
2106
|
+
n: undefined
|
|
2107
|
+
});
|
|
2108
|
+
return _el$47;
|
|
2109
|
+
})()
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
}), null);
|
|
2113
|
+
_$effect(_p$ => {
|
|
2114
|
+
var _v$12 = {
|
|
2115
|
+
fg: theme().textMuted
|
|
2116
|
+
},
|
|
2117
|
+
_v$13 = {
|
|
2118
|
+
fg: theme().accent
|
|
2119
|
+
},
|
|
2120
|
+
_v$14 = {
|
|
2121
|
+
fg: theme().textMuted
|
|
2122
|
+
};
|
|
2123
|
+
_v$12 !== _p$.e && (_p$.e = _$setProp(_el$16, "style", _v$12, _p$.e));
|
|
2124
|
+
_v$13 !== _p$.t && (_p$.t = _$setProp(_el$17, "style", _v$13, _p$.t));
|
|
2125
|
+
_v$14 !== _p$.a && (_p$.a = _$setProp(_el$21, "style", _v$14, _p$.a));
|
|
2126
|
+
return _p$;
|
|
2127
|
+
}, {
|
|
2128
|
+
e: undefined,
|
|
2129
|
+
t: undefined,
|
|
2130
|
+
a: undefined
|
|
2131
|
+
});
|
|
2132
|
+
return _el$11;
|
|
2133
|
+
})();
|
|
2134
|
+
}
|
|
2135
|
+
const HOME_SESSION_LIMIT = 6;
|
|
2136
|
+
function HomeSessions(props) {
|
|
2137
|
+
const theme = () => props.api.theme.current;
|
|
2138
|
+
const home = process.env.HOME ?? "";
|
|
2139
|
+
const [entries, setEntries] = createSignal([]);
|
|
2140
|
+
const [query, setQuery] = createSignal("");
|
|
2141
|
+
const [hovered, setHovered] = createSignal();
|
|
2142
|
+
const [searching, setSearching] = createSignal(false);
|
|
2143
|
+
const [loadFailed, setLoadFailed] = createSignal(false);
|
|
2144
|
+
const [pendingDelete, setPendingDelete] = createSignal();
|
|
2145
|
+
const [deleting, setDeleting] = createSignal();
|
|
2146
|
+
let confirmTimer;
|
|
2147
|
+
let disposeConfirm;
|
|
2148
|
+
const preview = createTranscriptPreview(props.api);
|
|
2149
|
+
let disposeHoverSpace;
|
|
2150
|
+
let disposeSearch;
|
|
2151
|
+
let insideSearchBox = false;
|
|
2152
|
+
const cancelDelete = () => {
|
|
2153
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
2154
|
+
confirmTimer = undefined;
|
|
2155
|
+
disposeConfirm?.();
|
|
2156
|
+
disposeConfirm = undefined;
|
|
2157
|
+
setPendingDelete(undefined);
|
|
2158
|
+
};
|
|
2159
|
+
const confirmArmed = () => {
|
|
2160
|
+
const id = pendingDelete();
|
|
2161
|
+
if (!id) return false;
|
|
2162
|
+
const entry = entries().find(e => e.id === id);
|
|
2163
|
+
if (!entry) cancelDelete();else void deleteEntry(entry);
|
|
2164
|
+
return true;
|
|
2165
|
+
};
|
|
2166
|
+
const requestDelete = entry => {
|
|
2167
|
+
if (deleting()) return;
|
|
2168
|
+
if (pendingDelete() === entry.id) {
|
|
2169
|
+
void deleteEntry(entry);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
setPendingDelete(entry.id);
|
|
2173
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
2174
|
+
confirmTimer = setTimeout(() => setPendingDelete(undefined), 5000);
|
|
2175
|
+
};
|
|
2176
|
+
const deleteEntry = async entry => {
|
|
2177
|
+
cancelDelete();
|
|
2178
|
+
if (deleting()) return;
|
|
2179
|
+
setDeleting(entry.id);
|
|
2180
|
+
try {
|
|
2181
|
+
const result = await props.api.client.session.delete({
|
|
2182
|
+
sessionID: entry.id,
|
|
2183
|
+
directory: entry.dir || undefined
|
|
2184
|
+
});
|
|
2185
|
+
if (result && "error" in result && result.error) throw result.error;
|
|
2186
|
+
const {
|
|
2187
|
+
entries: remaining
|
|
2188
|
+
} = await fetchEntries(props.api);
|
|
2189
|
+
if (remaining.some(item => item.id === entry.id)) {
|
|
2190
|
+
setEntries(remaining);
|
|
2191
|
+
props.api.ui.toast({
|
|
2192
|
+
message: "Session was not deleted",
|
|
2193
|
+
variant: "error"
|
|
2194
|
+
});
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
setEntries(remaining);
|
|
2198
|
+
setHovered(undefined);
|
|
2199
|
+
props.api.ui.toast({
|
|
2200
|
+
message: `Deleted "${truncate(entry.title, 40)}"`,
|
|
2201
|
+
variant: "info"
|
|
2202
|
+
});
|
|
2203
|
+
if ("params" in props.api.route.current && props.api.route.current.params?.sessionID === entry.id) {
|
|
2204
|
+
props.api.route.navigate("home");
|
|
2205
|
+
}
|
|
2206
|
+
} catch {
|
|
2207
|
+
props.api.ui.toast({
|
|
2208
|
+
message: "Could not delete session",
|
|
2209
|
+
variant: "error"
|
|
2210
|
+
});
|
|
2211
|
+
} finally {
|
|
2212
|
+
setDeleting(undefined);
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
createEffect(() => {
|
|
2216
|
+
disposeConfirm?.();
|
|
2217
|
+
disposeConfirm = undefined;
|
|
2218
|
+
if (!pendingDelete()) return;
|
|
2219
|
+
disposeConfirm = props.api.keymap.registerLayer({
|
|
2220
|
+
mode: "base",
|
|
2221
|
+
priority: 25,
|
|
2222
|
+
bindings: [{
|
|
2223
|
+
key: "y",
|
|
2224
|
+
desc: "Confirm delete",
|
|
2225
|
+
preventDefault: true,
|
|
2226
|
+
cmd: () => {
|
|
2227
|
+
confirmArmed();
|
|
2228
|
+
}
|
|
2229
|
+
}, {
|
|
2230
|
+
key: "alt+d",
|
|
2231
|
+
desc: "Confirm delete",
|
|
2232
|
+
preventDefault: true,
|
|
2233
|
+
cmd: () => {
|
|
2234
|
+
confirmArmed();
|
|
2235
|
+
}
|
|
2236
|
+
}, {
|
|
2237
|
+
key: "enter",
|
|
2238
|
+
desc: "Confirm delete",
|
|
2239
|
+
preventDefault: true,
|
|
2240
|
+
cmd: () => {
|
|
2241
|
+
confirmArmed();
|
|
2242
|
+
}
|
|
2243
|
+
}, {
|
|
2244
|
+
key: "n",
|
|
2245
|
+
desc: "Cancel delete",
|
|
2246
|
+
preventDefault: true,
|
|
2247
|
+
cmd: cancelDelete
|
|
2248
|
+
}, {
|
|
2249
|
+
key: "escape",
|
|
2250
|
+
desc: "Cancel delete",
|
|
2251
|
+
preventDefault: true,
|
|
2252
|
+
cmd: cancelDelete
|
|
2253
|
+
}]
|
|
2254
|
+
});
|
|
2255
|
+
});
|
|
2256
|
+
const onRootMouseDown = () => {
|
|
2257
|
+
preview.closeIfOpen();
|
|
2258
|
+
if (insideSearchBox) {
|
|
2259
|
+
insideSearchBox = false;
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (searching()) setSearching(false);
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
// A modal dialog owns input while open: yield this list's keyboard captures
|
|
2266
|
+
// too, so its layers cannot double-fire with the dialog's own.
|
|
2267
|
+
createEffect(() => {
|
|
2268
|
+
if (props.api.ui.dialog.open) {
|
|
2269
|
+
cancelDelete();
|
|
2270
|
+
setSearching(false);
|
|
2271
|
+
setHovered(undefined);
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
onMount(() => {
|
|
2275
|
+
ensureRootMouse(props.api.renderer);
|
|
2276
|
+
rootMouseHandlers.add(onRootMouseDown);
|
|
2277
|
+
onCleanup(() => rootMouseHandlers.delete(onRootMouseDown));
|
|
2278
|
+
});
|
|
2279
|
+
const appendQuery = input => {
|
|
2280
|
+
if (input === "\b") setQuery(value => value.slice(0, -1));else setQuery(value => value + input);
|
|
2281
|
+
};
|
|
2282
|
+
createEffect(() => {
|
|
2283
|
+
disposeSearch?.();
|
|
2284
|
+
disposeSearch = undefined;
|
|
2285
|
+
if (!searching()) return;
|
|
2286
|
+
disposeSearch = props.api.keymap.registerLayer({
|
|
2287
|
+
mode: "base",
|
|
2288
|
+
priority: 20,
|
|
2289
|
+
bindings: searchBindings(appendQuery, () => setSearching(false))
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
createEffect(() => {
|
|
2293
|
+
disposeHoverSpace?.();
|
|
2294
|
+
disposeHoverSpace = undefined;
|
|
2295
|
+
const id = hovered();
|
|
2296
|
+
if (!id) return;
|
|
2297
|
+
const entry = entries().find(e => e.id === id);
|
|
2298
|
+
if (!entry) return;
|
|
2299
|
+
disposeHoverSpace = props.api.keymap.registerLayer({
|
|
2300
|
+
mode: "base",
|
|
2301
|
+
priority: 20,
|
|
2302
|
+
bindings: [{
|
|
2303
|
+
key: "alt+p",
|
|
2304
|
+
desc: "Preview session transcript",
|
|
2305
|
+
preventDefault: true,
|
|
2306
|
+
cmd: () => preview.open(entry)
|
|
2307
|
+
}, {
|
|
2308
|
+
key: "alt+d",
|
|
2309
|
+
desc: "Delete session",
|
|
2310
|
+
preventDefault: true,
|
|
2311
|
+
cmd: () => {
|
|
2312
|
+
if (!confirmArmed()) requestDelete(entry);
|
|
2313
|
+
}
|
|
2314
|
+
}, {
|
|
2315
|
+
key: "alt+s",
|
|
2316
|
+
desc: "Pin session",
|
|
2317
|
+
preventDefault: true,
|
|
2318
|
+
cmd: () => props.onTogglePin("sessions", entry.id)
|
|
2319
|
+
}, {
|
|
2320
|
+
key: "alt+r",
|
|
2321
|
+
desc: "Pin directory",
|
|
2322
|
+
preventDefault: true,
|
|
2323
|
+
cmd: () => props.onTogglePin("directories", entry.dir)
|
|
2324
|
+
}]
|
|
2325
|
+
});
|
|
2326
|
+
});
|
|
2327
|
+
onCleanup(() => {
|
|
2328
|
+
disposeSearch?.();
|
|
2329
|
+
disposeHoverSpace?.();
|
|
2330
|
+
disposeConfirm?.();
|
|
2331
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
2332
|
+
});
|
|
2333
|
+
onMount(() => {
|
|
2334
|
+
let alive = true;
|
|
2335
|
+
const load = async () => {
|
|
2336
|
+
props.refreshPins();
|
|
2337
|
+
try {
|
|
2338
|
+
const {
|
|
2339
|
+
entries: all
|
|
2340
|
+
} = await fetchEntries(props.api);
|
|
2341
|
+
if (alive) {
|
|
2342
|
+
setEntries(all);
|
|
2343
|
+
setLoadFailed(false);
|
|
2344
|
+
}
|
|
2345
|
+
} catch {
|
|
2346
|
+
if (alive && entries().length === 0) setLoadFailed(true);
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2349
|
+
void load();
|
|
2350
|
+
const clock = setInterval(() => void load(), POLL_MS);
|
|
2351
|
+
onCleanup(() => {
|
|
2352
|
+
alive = false;
|
|
2353
|
+
clearInterval(clock);
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
const visible = createMemo(() => {
|
|
2357
|
+
const q = query().trim().toLowerCase();
|
|
2358
|
+
const list = q ? entries().filter(entry => entry.title.toLowerCase().includes(q) || entry.dir.toLowerCase().includes(q)) : entries();
|
|
2359
|
+
return pinnedFirst(list, props.pins()).slice(0, HOME_SESSION_LIMIT);
|
|
2360
|
+
});
|
|
2361
|
+
return (() => {
|
|
2362
|
+
var _el$50 = _$createElement("box"),
|
|
2363
|
+
_el$51 = _$createElement("box");
|
|
2364
|
+
_$insertNode(_el$50, _el$51);
|
|
2365
|
+
_$setProp(_el$50, "width", "100%");
|
|
2366
|
+
_$setProp(_el$50, "alignItems", "center");
|
|
2367
|
+
_$setProp(_el$51, "flexDirection", "column");
|
|
2368
|
+
_$setProp(_el$51, "width", "100%");
|
|
2369
|
+
_$setProp(_el$51, "maxWidth", 72);
|
|
2370
|
+
_$setProp(_el$51, "paddingLeft", 1);
|
|
2371
|
+
_$setProp(_el$51, "paddingRight", 1);
|
|
2372
|
+
_$setProp(_el$51, "paddingTop", 1);
|
|
2373
|
+
_$setProp(_el$51, "gap", 1);
|
|
2374
|
+
_$insert(_el$51, _$createComponent(Show, {
|
|
2375
|
+
get when() {
|
|
2376
|
+
return entries().length > 0;
|
|
2377
|
+
},
|
|
2378
|
+
get fallback() {
|
|
2379
|
+
return (() => {
|
|
2380
|
+
var _el$65 = _$createElement("box"),
|
|
2381
|
+
_el$66 = _$createElement("text"),
|
|
2382
|
+
_el$68 = _$createElement("text");
|
|
2383
|
+
_$insertNode(_el$65, _el$66);
|
|
2384
|
+
_$insertNode(_el$65, _el$68);
|
|
2385
|
+
_$setProp(_el$65, "flexDirection", "row");
|
|
2386
|
+
_$setProp(_el$65, "gap", 1);
|
|
2387
|
+
_$insertNode(_el$66, _$createTextNode(`Recent sessions`));
|
|
2388
|
+
_$insert(_el$68, () => loadFailed() ? "· list unavailable, will retry" : "· none yet, option+o to browse");
|
|
2389
|
+
_$effect(_p$ => {
|
|
2390
|
+
var _v$33 = {
|
|
2391
|
+
fg: theme().textMuted
|
|
2392
|
+
},
|
|
2393
|
+
_v$34 = {
|
|
2394
|
+
fg: theme().textMuted
|
|
2395
|
+
};
|
|
2396
|
+
_v$33 !== _p$.e && (_p$.e = _$setProp(_el$66, "style", _v$33, _p$.e));
|
|
2397
|
+
_v$34 !== _p$.t && (_p$.t = _$setProp(_el$68, "style", _v$34, _p$.t));
|
|
2398
|
+
return _p$;
|
|
2399
|
+
}, {
|
|
2400
|
+
e: undefined,
|
|
2401
|
+
t: undefined
|
|
2402
|
+
});
|
|
2403
|
+
return _el$65;
|
|
2404
|
+
})();
|
|
2405
|
+
},
|
|
2406
|
+
get children() {
|
|
2407
|
+
return [(() => {
|
|
2408
|
+
var _el$52 = _$createElement("box"),
|
|
2409
|
+
_el$53 = _$createElement("box"),
|
|
2410
|
+
_el$54 = _$createElement("text"),
|
|
2411
|
+
_el$56 = _$createElement("text"),
|
|
2412
|
+
_el$58 = _$createElement("box"),
|
|
2413
|
+
_el$59 = _$createElement("box"),
|
|
2414
|
+
_el$60 = _$createElement("text");
|
|
2415
|
+
_$insertNode(_el$52, _el$53);
|
|
2416
|
+
_$insertNode(_el$52, _el$58);
|
|
2417
|
+
_$setProp(_el$52, "flexDirection", "row");
|
|
2418
|
+
_$setProp(_el$52, "justifyContent", "space-between");
|
|
2419
|
+
_$setProp(_el$52, "gap", 2);
|
|
2420
|
+
_$insertNode(_el$53, _el$54);
|
|
2421
|
+
_$insertNode(_el$53, _el$56);
|
|
2422
|
+
_$setProp(_el$53, "flexDirection", "row");
|
|
2423
|
+
_$setProp(_el$53, "gap", 1);
|
|
2424
|
+
_$insertNode(_el$54, _$createTextNode(`Recent sessions`));
|
|
2425
|
+
_$insertNode(_el$56, _$createTextNode(`· option+o for all`));
|
|
2426
|
+
_$insertNode(_el$58, _el$59);
|
|
2427
|
+
_$setProp(_el$58, "flexDirection", "row");
|
|
2428
|
+
_$setProp(_el$58, "gap", 1);
|
|
2429
|
+
_$setProp(_el$58, "paddingLeft", 1);
|
|
2430
|
+
_$setProp(_el$58, "paddingRight", 1);
|
|
2431
|
+
_$setProp(_el$58, "onMouseDown", () => {
|
|
2432
|
+
insideSearchBox = true;
|
|
2433
|
+
setSearching(true);
|
|
2434
|
+
});
|
|
2435
|
+
_$insertNode(_el$59, _el$60);
|
|
2436
|
+
_$setProp(_el$59, "width", 24);
|
|
2437
|
+
_$setProp(_el$60, "flexGrow", 1);
|
|
2438
|
+
_$setProp(_el$60, "flexShrink", 1);
|
|
2439
|
+
_$setProp(_el$60, "overflow", "hidden");
|
|
2440
|
+
_$setProp(_el$60, "wrapMode", "none");
|
|
2441
|
+
_$insert(_el$60, () => query() || (searching() ? "" : "search…"), null);
|
|
2442
|
+
_$insert(_el$60, () => searching() ? "▏" : "", null);
|
|
2443
|
+
_$insert(_el$58, _$createComponent(Show, {
|
|
2444
|
+
get when() {
|
|
2445
|
+
return query();
|
|
2446
|
+
},
|
|
2447
|
+
get children() {
|
|
2448
|
+
var _el$61 = _$createElement("text");
|
|
2449
|
+
_$insertNode(_el$61, _$createTextNode(`✕`));
|
|
2450
|
+
_$setProp(_el$61, "flexShrink", 0);
|
|
2451
|
+
_$setProp(_el$61, "onMouseDown", () => setQuery(""));
|
|
2452
|
+
_$effect(_$p => _$setProp(_el$61, "style", {
|
|
2453
|
+
fg: theme().textMuted
|
|
2454
|
+
}, _$p));
|
|
2455
|
+
return _el$61;
|
|
2456
|
+
}
|
|
2457
|
+
}), null);
|
|
2458
|
+
_$effect(_p$ => {
|
|
2459
|
+
var _v$29 = {
|
|
2460
|
+
fg: theme().textMuted
|
|
2461
|
+
},
|
|
2462
|
+
_v$30 = {
|
|
2463
|
+
fg: theme().textMuted
|
|
2464
|
+
},
|
|
2465
|
+
_v$31 = searching() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
2466
|
+
_v$32 = {
|
|
2467
|
+
fg: query() ? theme().text : theme().textMuted
|
|
2468
|
+
};
|
|
2469
|
+
_v$29 !== _p$.e && (_p$.e = _$setProp(_el$54, "style", _v$29, _p$.e));
|
|
2470
|
+
_v$30 !== _p$.t && (_p$.t = _$setProp(_el$56, "style", _v$30, _p$.t));
|
|
2471
|
+
_v$31 !== _p$.a && (_p$.a = _$setProp(_el$58, "backgroundColor", _v$31, _p$.a));
|
|
2472
|
+
_v$32 !== _p$.o && (_p$.o = _$setProp(_el$60, "style", _v$32, _p$.o));
|
|
2473
|
+
return _p$;
|
|
2474
|
+
}, {
|
|
2475
|
+
e: undefined,
|
|
2476
|
+
t: undefined,
|
|
2477
|
+
a: undefined,
|
|
2478
|
+
o: undefined
|
|
2479
|
+
});
|
|
2480
|
+
return _el$52;
|
|
2481
|
+
})(), _$createComponent(For, {
|
|
2482
|
+
get each() {
|
|
2483
|
+
return visible();
|
|
2484
|
+
},
|
|
2485
|
+
children: entry => (() => {
|
|
2486
|
+
var _el$69 = _$createElement("box"),
|
|
2487
|
+
_el$70 = _$createElement("text"),
|
|
2488
|
+
_el$72 = _$createElement("text");
|
|
2489
|
+
_$insertNode(_el$69, _el$70);
|
|
2490
|
+
_$insertNode(_el$69, _el$72);
|
|
2491
|
+
_$setProp(_el$69, "flexDirection", "row");
|
|
2492
|
+
_$setProp(_el$69, "gap", 1);
|
|
2493
|
+
_$setProp(_el$69, "onMouseOver", () => setHovered(entry.id));
|
|
2494
|
+
_$setProp(_el$69, "onMouseOut", () => setHovered(undefined));
|
|
2495
|
+
_$setProp(_el$69, "onMouseDown", event => event.button === RIGHT_BUTTON ? openInCmuxWorkspace(props.api, entry) : props.api.route.navigate("session", {
|
|
2496
|
+
sessionID: entry.id
|
|
2497
|
+
}));
|
|
2498
|
+
_$insertNode(_el$70, _$createTextNode(`○`));
|
|
2499
|
+
_$setProp(_el$70, "flexShrink", 0);
|
|
2500
|
+
_$insert(_el$69, _$createComponent(Highlighted, {
|
|
2501
|
+
get text() {
|
|
2502
|
+
return `${props.pins().sessions.includes(entry.id) ? "★ " : ""}${truncate(entry.title, HOME_TITLE_WIDTH - (props.pins().sessions.includes(entry.id) ? 2 : 0))}`;
|
|
2503
|
+
},
|
|
2504
|
+
get query() {
|
|
2505
|
+
return query();
|
|
2506
|
+
},
|
|
2507
|
+
get color() {
|
|
2508
|
+
return theme().text;
|
|
2509
|
+
},
|
|
2510
|
+
get matchColor() {
|
|
2511
|
+
return theme().warning;
|
|
2512
|
+
}
|
|
2513
|
+
}), _el$72);
|
|
2514
|
+
_$setProp(_el$72, "flexShrink", 0);
|
|
2515
|
+
_$insert(_el$72, (() => {
|
|
2516
|
+
var _c$5 = _$memo(() => pendingDelete() === entry.id);
|
|
2517
|
+
return () => _c$5() ? "option+d again" : `${props.pins().directories.includes(entry.dir) ? "★ " : ""}${prettyDir(entry.dir, home)} · ${ago(entry.updated)}`;
|
|
2518
|
+
})());
|
|
2519
|
+
_$effect(_p$ => {
|
|
2520
|
+
var _v$35 = entry.id === hovered() ? theme().backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
2521
|
+
_v$36 = {
|
|
2522
|
+
fg: theme().textMuted
|
|
2523
|
+
},
|
|
2524
|
+
_v$37 = {
|
|
2525
|
+
fg: pendingDelete() === entry.id ? theme().error : theme().textMuted
|
|
2526
|
+
};
|
|
2527
|
+
_v$35 !== _p$.e && (_p$.e = _$setProp(_el$69, "backgroundColor", _v$35, _p$.e));
|
|
2528
|
+
_v$36 !== _p$.t && (_p$.t = _$setProp(_el$70, "style", _v$36, _p$.t));
|
|
2529
|
+
_v$37 !== _p$.a && (_p$.a = _$setProp(_el$72, "style", _v$37, _p$.a));
|
|
2530
|
+
return _p$;
|
|
2531
|
+
}, {
|
|
2532
|
+
e: undefined,
|
|
2533
|
+
t: undefined,
|
|
2534
|
+
a: undefined
|
|
2535
|
+
});
|
|
2536
|
+
return _el$69;
|
|
2537
|
+
})()
|
|
2538
|
+
}), _$createComponent(Show, {
|
|
2539
|
+
get when() {
|
|
2540
|
+
return visible().length === 0;
|
|
2541
|
+
},
|
|
2542
|
+
get children() {
|
|
2543
|
+
var _el$63 = _$createElement("text");
|
|
2544
|
+
_$insertNode(_el$63, _$createTextNode(`No title or directory matches · option+o to search transcripts`));
|
|
2545
|
+
_$effect(_$p => _$setProp(_el$63, "style", {
|
|
2546
|
+
fg: theme().textMuted
|
|
2547
|
+
}, _$p));
|
|
2548
|
+
return _el$63;
|
|
2549
|
+
}
|
|
2550
|
+
})];
|
|
2551
|
+
}
|
|
2552
|
+
}));
|
|
2553
|
+
return _el$50;
|
|
2554
|
+
})();
|
|
2555
|
+
}
|
|
2556
|
+
const tui = async api => {
|
|
2557
|
+
const [pins, setPins] = createSignal(emptyPins());
|
|
2558
|
+
// Keep exploratory context while the plugin remains loaded; a full OpenCode
|
|
2559
|
+
// restart starts with a clean picker rather than an old cross-project filter.
|
|
2560
|
+
let pickerState = {
|
|
2561
|
+
query: "",
|
|
2562
|
+
waitingOnly: false,
|
|
2563
|
+
pinnedOnly: false
|
|
2564
|
+
};
|
|
2565
|
+
let pinVersion = 0;
|
|
2566
|
+
const refreshPins = () => {
|
|
2567
|
+
const version = pinVersion;
|
|
2568
|
+
void readPins().then(latest => {
|
|
2569
|
+
if (version === pinVersion) setPins(latest);
|
|
2570
|
+
});
|
|
2571
|
+
};
|
|
2572
|
+
refreshPins();
|
|
2573
|
+
const onTogglePin = (kind, value) => {
|
|
2574
|
+
if (!value) return;
|
|
2575
|
+
pinVersion += 1;
|
|
2576
|
+
void togglePin(kind, value).then(updated => {
|
|
2577
|
+
setPins(updated);
|
|
2578
|
+
api.ui.toast({
|
|
2579
|
+
message: `${updated[kind].includes(value) ? "Pinned" : "Unpinned"} ${kind === "sessions" ? "session" : "directory"}`,
|
|
2580
|
+
variant: "info"
|
|
2581
|
+
});
|
|
2582
|
+
}, () => api.ui.toast({
|
|
2583
|
+
message: "Could not update pins",
|
|
2584
|
+
variant: "error"
|
|
2585
|
+
}));
|
|
2586
|
+
};
|
|
2587
|
+
api.slots.register({
|
|
2588
|
+
order: 100,
|
|
2589
|
+
slots: {
|
|
2590
|
+
sidebar_content() {
|
|
2591
|
+
return _$createComponent(SidebarSessions, {
|
|
2592
|
+
api: api,
|
|
2593
|
+
pins: pins,
|
|
2594
|
+
onTogglePin: onTogglePin,
|
|
2595
|
+
refreshPins: refreshPins
|
|
2596
|
+
});
|
|
2597
|
+
},
|
|
2598
|
+
home_bottom() {
|
|
2599
|
+
return _$createComponent(HomeSessions, {
|
|
2600
|
+
api: api,
|
|
2601
|
+
pins: pins,
|
|
2602
|
+
onTogglePin: onTogglePin,
|
|
2603
|
+
refreshPins: refreshPins
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
const openPicker = async (command = false) => {
|
|
2609
|
+
refreshPins();
|
|
2610
|
+
if (!command && api.mode.current() !== BASE_MODE) return;
|
|
2611
|
+
if (!command && api.renderer.currentFocusedEditor === null) return;
|
|
2612
|
+
let entriesResult;
|
|
2613
|
+
try {
|
|
2614
|
+
entriesResult = await fetchEntries(api);
|
|
2615
|
+
} catch {
|
|
2616
|
+
api.ui.toast({
|
|
2617
|
+
message: "Could not load sessions",
|
|
2618
|
+
variant: "error"
|
|
2619
|
+
});
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const entries = entriesResult.entries;
|
|
2623
|
+
if (entries.length === 0) {
|
|
2624
|
+
api.ui.toast({
|
|
2625
|
+
message: "No sessions found",
|
|
2626
|
+
variant: "warning"
|
|
2627
|
+
});
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
const [allEntries, setAllEntries] = createSignal(entries);
|
|
2631
|
+
const [previewText, setPreviewText] = createSignal("");
|
|
2632
|
+
const [showPreview, setShowPreview] = createSignal(false);
|
|
2633
|
+
const [query, setQuery] = createSignal(pickerState.query);
|
|
2634
|
+
const [cursor, setCursor] = createSignal(0);
|
|
2635
|
+
const [viewportStart, setViewportStart] = createSignal(0);
|
|
2636
|
+
const [collapsed, setCollapsed] = createSignal({});
|
|
2637
|
+
const [scope, setScope] = createSignal(entries.some(entry => entry.group === pickerState.scope) ? pickerState.scope : undefined);
|
|
2638
|
+
const [waitingOnly, setWaitingOnly] = createSignal(pickerState.waitingOnly);
|
|
2639
|
+
const [pinnedOnly, setPinnedOnly] = createSignal(pickerState.pinnedOnly);
|
|
2640
|
+
const [waitingIds, setWaitingIds] = createSignal(new Set());
|
|
2641
|
+
const [waitingCoverage, setWaitingCoverage] = createSignal("loading");
|
|
2642
|
+
let alive = true;
|
|
2643
|
+
void (async () => {
|
|
2644
|
+
const db = await openTranscriptDb();
|
|
2645
|
+
if (!db) {
|
|
2646
|
+
if (alive) setWaitingCoverage("unavailable");
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
const ids = await queryWaitingIds(db);
|
|
2651
|
+
if (alive) {
|
|
2652
|
+
setWaitingIds(new Set(ids.keys()));
|
|
2653
|
+
setWaitingCoverage("ready");
|
|
2654
|
+
}
|
|
2655
|
+
} catch {
|
|
2656
|
+
if (alive) setWaitingCoverage("unavailable");
|
|
2657
|
+
} finally {
|
|
2658
|
+
db.close();
|
|
2659
|
+
}
|
|
2660
|
+
})();
|
|
2661
|
+
const [pendingDelete, setPendingDelete] = createSignal();
|
|
2662
|
+
const [searchIndex, setSearchIndex] = createSignal(new Map());
|
|
2663
|
+
const [indexProgress, setIndexProgress] = createSignal({
|
|
2664
|
+
indexed: 0,
|
|
2665
|
+
total: 0,
|
|
2666
|
+
complete: false
|
|
2667
|
+
});
|
|
2668
|
+
void buildSearchIndex(api, entries, (progress, index) => {
|
|
2669
|
+
if (!alive) return;
|
|
2670
|
+
setIndexProgress(progress);
|
|
2671
|
+
// Publish a fresh snapshot: the indexing workers mutate their own map,
|
|
2672
|
+
// while Solid needs a new reference to update search results mid-scan.
|
|
2673
|
+
setSearchIndex(new Map(index));
|
|
2674
|
+
});
|
|
2675
|
+
const orderedEntries = createMemo(() => pinnedFirst(allEntries(), pins()));
|
|
2676
|
+
const currentProject = () => {
|
|
2677
|
+
const route = api.route.current;
|
|
2678
|
+
const id = route.name === "session" ? route.params?.sessionID : undefined;
|
|
2679
|
+
return allEntries().find(entry => entry.id === id)?.group ?? allEntries().find(entry => entry.dir === api.state.path.directory)?.group;
|
|
2680
|
+
};
|
|
2681
|
+
const matchedGroups = createMemo(() => {
|
|
2682
|
+
const matched = filterPickerEntries(orderedEntries(), {
|
|
2683
|
+
query: query(),
|
|
2684
|
+
scope: scope(),
|
|
2685
|
+
waitingOnly: waitingOnly(),
|
|
2686
|
+
pinnedOnly: pinnedOnly()
|
|
2687
|
+
}, searchIndex(), waitingIds(), pins().sessions);
|
|
2688
|
+
const byGroup = new Map();
|
|
2689
|
+
for (const entry of matched) {
|
|
2690
|
+
const list = byGroup.get(entry.dir) ?? [];
|
|
2691
|
+
list.push(entry);
|
|
2692
|
+
byGroup.set(entry.dir, list);
|
|
2693
|
+
}
|
|
2694
|
+
const groups = [...byGroup.entries()].map(([dir, list]) => ({
|
|
2695
|
+
dir,
|
|
2696
|
+
list: newestFirst(list)
|
|
2697
|
+
}));
|
|
2698
|
+
const currentPins = pins();
|
|
2699
|
+
const rank = group => currentPins.directories.includes(group.dir) ? 2 : group.list.some(entry => currentPins.sessions.includes(entry.id)) ? 1 : 0;
|
|
2700
|
+
return groups.sort((a, b) => rank(b) - rank(a) || Math.max(...b.list.map(entry => entry.updated)) - Math.max(...a.list.map(entry => entry.updated)));
|
|
2701
|
+
});
|
|
2702
|
+
const selectableEntries = createMemo(() => matchedGroups().flatMap(g => collapsed()[g.dir] && !query().trim() ? [] : g.list));
|
|
2703
|
+
|
|
2704
|
+
// Visible bounding: say how much transcript text search actually covers and
|
|
2705
|
+
// when the metadata walk hit its cap, instead of silently under-reporting.
|
|
2706
|
+
const coverageLabel = createMemo(() => {
|
|
2707
|
+
const progress = indexProgress();
|
|
2708
|
+
const bits = [];
|
|
2709
|
+
if (scope()) bits.push(`scope ${scope()}`);
|
|
2710
|
+
if (waitingOnly()) bits.push(waitingCoverage() === "loading" ? "checking needs input" : waitingCoverage() === "unavailable" ? "needs-input unavailable" : "needs input");
|
|
2711
|
+
if (pinnedOnly()) bits.push("pinned sessions");
|
|
2712
|
+
if (progress.total > 0) {
|
|
2713
|
+
bits.push(progress.complete ? `transcripts ${progress.total}` : `indexing transcripts ${progress.indexed}/${progress.total}`);
|
|
2714
|
+
}
|
|
2715
|
+
if (entriesResult.truncated) bits.push(`showing newest ${allEntries().length}`);
|
|
2716
|
+
return bits.join(" · ");
|
|
2717
|
+
});
|
|
2718
|
+
const pickerRows = createMemo(() => {
|
|
2719
|
+
const flat = [];
|
|
2720
|
+
for (const group of matchedGroups()) {
|
|
2721
|
+
flat.push({
|
|
2722
|
+
kind: "group",
|
|
2723
|
+
dir: group.dir,
|
|
2724
|
+
label: prettyDir(group.dir, process.env.HOME ?? ""),
|
|
2725
|
+
count: group.list.length,
|
|
2726
|
+
pinned: pins().directories.includes(group.dir)
|
|
2727
|
+
});
|
|
2728
|
+
if (collapsed()[group.dir] && !query().trim()) continue;
|
|
2729
|
+
for (let i = 0; i < group.list.length; i++) {
|
|
2730
|
+
flat.push({
|
|
2731
|
+
kind: "item",
|
|
2732
|
+
entry: group.list[i],
|
|
2733
|
+
last: i === group.list.length - 1
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
return flat;
|
|
2738
|
+
});
|
|
2739
|
+
const termHeight = api.renderer.height;
|
|
2740
|
+
// The dialog host anchors content a quarter of the way down the screen and
|
|
2741
|
+
// lets it size to content, so a taller picker drifts downward. Pin the
|
|
2742
|
+
// content height and offset it up by the same amount to keep it centred.
|
|
2743
|
+
const dialogWidth = Math.min(116, api.renderer.width - 2);
|
|
2744
|
+
const CHROME_ROWS = 13;
|
|
2745
|
+
const listHeight = () => Math.max(6, Math.floor(termHeight * 0.75) - CHROME_ROWS);
|
|
2746
|
+
const dialogHeight = () => listHeight() + CHROME_ROWS;
|
|
2747
|
+
const dialogOffset = () => Math.min(0, Math.floor(termHeight / 4 - dialogHeight() / 2 - 1));
|
|
2748
|
+
const hasExcerpt = entry => !!transcriptMatchExcerpt(searchIndex().get(entry.id), entry, query());
|
|
2749
|
+
const maxViewportStart = createMemo(() => pickerLastStart(pickerRows(), listHeight(), hasExcerpt));
|
|
2750
|
+
const visiblePickerRows = createMemo(() => pickerWindow(pickerRows(), Math.min(viewportStart(), maxViewportStart()), listHeight(), hasExcerpt));
|
|
2751
|
+
let previousSelection = [];
|
|
2752
|
+
let deletedAt;
|
|
2753
|
+
createEffect(() => {
|
|
2754
|
+
const rows = selectableEntries();
|
|
2755
|
+
const position = rows.findIndex(entry => entry.id === previousSelection[cursor()]?.id);
|
|
2756
|
+
if (rows !== previousSelection && (rows.length !== previousSelection.length || rows.some((entry, index) => entry.id !== previousSelection[index]?.id))) {
|
|
2757
|
+
// A search/filter/collapse changed the tree: reveal the retained row or
|
|
2758
|
+
// start at the first result rather than leaving a stale window open.
|
|
2759
|
+
if (deletedAt !== undefined) {
|
|
2760
|
+
setCursor(Math.max(0, Math.min(deletedAt, rows.length - 1)));
|
|
2761
|
+
deletedAt = undefined;
|
|
2762
|
+
} else {
|
|
2763
|
+
const next = position >= 0 ? position : 0;
|
|
2764
|
+
setCursor(next);
|
|
2765
|
+
const id = rows[next]?.id;
|
|
2766
|
+
setViewportStart(position < 0 || next === 0 ? 0 : Math.max(0, pickerRows().findIndex(row => row.kind === "item" && row.entry.id === id)));
|
|
2767
|
+
}
|
|
2768
|
+
} else if (cursor() >= rows.length) setCursor(Math.max(0, rows.length - 1));
|
|
2769
|
+
previousSelection = rows;
|
|
2770
|
+
});
|
|
2771
|
+
const previewCache = new Map();
|
|
2772
|
+
let previewTimer;
|
|
2773
|
+
const previewID = () => selectableEntries()[cursor()]?.id;
|
|
2774
|
+
const loadPreview = async id => {
|
|
2775
|
+
if (!id) return;
|
|
2776
|
+
const cached = previewCache.get(id);
|
|
2777
|
+
if (cached !== undefined) {
|
|
2778
|
+
setPreviewText(cached);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
try {
|
|
2782
|
+
const text = await fetchTranscriptText(api, id);
|
|
2783
|
+
previewCache.set(id, text);
|
|
2784
|
+
if (showPreview() && previewID() === id) setPreviewText(text);
|
|
2785
|
+
} catch {}
|
|
2786
|
+
};
|
|
2787
|
+
const currentSessionID = () => {
|
|
2788
|
+
const route = api.route.current;
|
|
2789
|
+
if (route.name !== "session") return undefined;
|
|
2790
|
+
const params = route.params;
|
|
2791
|
+
return params && typeof params.sessionID === "string" ? params.sessionID : undefined;
|
|
2792
|
+
};
|
|
2793
|
+
const close = () => {
|
|
2794
|
+
cleanup();
|
|
2795
|
+
api.ui.dialog.clear();
|
|
2796
|
+
};
|
|
2797
|
+
createEffect(() => {
|
|
2798
|
+
const showing = showPreview();
|
|
2799
|
+
const id = previewID();
|
|
2800
|
+
if (previewTimer) clearTimeout(previewTimer);
|
|
2801
|
+
if (!showing || !id) return;
|
|
2802
|
+
const cached = previewCache.get(id);
|
|
2803
|
+
if (cached !== undefined) {
|
|
2804
|
+
setPreviewText(cached);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
// Never show another session's transcript beneath the new selection.
|
|
2808
|
+
setPreviewText("Loading…");
|
|
2809
|
+
previewTimer = setTimeout(() => void loadPreview(id), 120);
|
|
2810
|
+
});
|
|
2811
|
+
const togglePreview = () => setShowPreview(value => !value);
|
|
2812
|
+
const selectCursor = index => {
|
|
2813
|
+
const count = selectableEntries().length;
|
|
2814
|
+
if (count === 0) return;
|
|
2815
|
+
cancelDelete();
|
|
2816
|
+
const next = Math.max(0, Math.min(count - 1, index));
|
|
2817
|
+
const id = selectableEntries()[next].id;
|
|
2818
|
+
const flat = pickerRows();
|
|
2819
|
+
const target = flat.findIndex(row => row.kind === "item" && row.entry.id === id);
|
|
2820
|
+
let start = Math.min(viewportStart(), maxViewportStart());
|
|
2821
|
+
if (target < start) start = target;else while (start < target && !pickerWindow(flat, start, listHeight(), hasExcerpt).some(row => row.kind === "item" && row.entry.id === id)) start++;
|
|
2822
|
+
setViewportStart(Math.max(0, Math.min(start, maxViewportStart())));
|
|
2823
|
+
setCursor(next);
|
|
2824
|
+
};
|
|
2825
|
+
const moveCursor = delta => selectCursor(cursor() + delta);
|
|
2826
|
+
|
|
2827
|
+
// Match OpenTUI's scrollbox: honor every wheel event's delta. The viewport
|
|
2828
|
+
// stays independent of selection, so fast scrolling cannot recenter on hover.
|
|
2829
|
+
const scrollPicker = scroll => {
|
|
2830
|
+
if (showPreview() || !scroll || scroll.direction !== "up" && scroll.direction !== "down") return;
|
|
2831
|
+
const amount = Math.trunc(scroll.delta) * (scroll.direction === "down" ? 1 : -1);
|
|
2832
|
+
setViewportStart(start => Math.max(0, Math.min(maxViewportStart(), start + amount)));
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
// Delete confirmation lives in its own layer, registered only while a row
|
|
2836
|
+
// is armed, so y/n never shadow the search box's typing.
|
|
2837
|
+
let disposeConfirm;
|
|
2838
|
+
let confirmTimer;
|
|
2839
|
+
const cancelDelete = () => {
|
|
2840
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
2841
|
+
confirmTimer = undefined;
|
|
2842
|
+
disposeConfirm?.();
|
|
2843
|
+
disposeConfirm = undefined;
|
|
2844
|
+
setPendingDelete(undefined);
|
|
2845
|
+
};
|
|
2846
|
+
const confirmArmed = () => {
|
|
2847
|
+
const armed = pendingDelete();
|
|
2848
|
+
if (!armed) return false;
|
|
2849
|
+
void deleteEntry(armed);
|
|
2850
|
+
return true;
|
|
2851
|
+
};
|
|
2852
|
+
|
|
2853
|
+
// Escape first closes the transcript overlay, then the picker. One Escape
|
|
2854
|
+
// can reach both the focused search input and the key layer (sometimes in
|
|
2855
|
+
// separate ticks), so ignore the duplicate that trails right behind.
|
|
2856
|
+
let escapeHandledAt = 0;
|
|
2857
|
+
const handleEscape = () => {
|
|
2858
|
+
const now = Date.now();
|
|
2859
|
+
if (escapeHandledAt > now) return;
|
|
2860
|
+
if (showPreview()) {
|
|
2861
|
+
setShowPreview(false);
|
|
2862
|
+
escapeHandledAt = now + 150;
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
if (pendingDelete()) {
|
|
2866
|
+
cancelDelete();
|
|
2867
|
+
escapeHandledAt = now + 150;
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
cancelDelete();
|
|
2871
|
+
close();
|
|
2872
|
+
};
|
|
2873
|
+
const deleteEntry = async entry => {
|
|
2874
|
+
cancelDelete();
|
|
2875
|
+
try {
|
|
2876
|
+
const result = await api.client.session.delete({
|
|
2877
|
+
sessionID: entry.id,
|
|
2878
|
+
directory: entry.dir || undefined
|
|
2879
|
+
});
|
|
2880
|
+
if (result && "error" in result && result.error) throw result.error;
|
|
2881
|
+
const {
|
|
2882
|
+
entries: remaining
|
|
2883
|
+
} = await fetchEntries(api);
|
|
2884
|
+
if (remaining.some(item => item.id === entry.id)) {
|
|
2885
|
+
setAllEntries(remaining);
|
|
2886
|
+
api.ui.toast({
|
|
2887
|
+
message: "Session was not deleted",
|
|
2888
|
+
variant: "error"
|
|
2889
|
+
});
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
deletedAt = selectableEntries().findIndex(item => item.id === entry.id);
|
|
2893
|
+
if (deletedAt < 0) deletedAt = undefined;
|
|
2894
|
+
setAllEntries(remaining);
|
|
2895
|
+
api.ui.toast({
|
|
2896
|
+
message: `Deleted "${truncate(entry.title, 40)}"`,
|
|
2897
|
+
variant: "info"
|
|
2898
|
+
});
|
|
2899
|
+
if (currentSessionID() === entry.id) api.route.navigate("home");
|
|
2900
|
+
} catch {
|
|
2901
|
+
api.ui.toast({
|
|
2902
|
+
message: "Could not delete session",
|
|
2903
|
+
variant: "error"
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
};
|
|
2907
|
+
const armDelete = entry => {
|
|
2908
|
+
setPendingDelete(entry);
|
|
2909
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
2910
|
+
confirmTimer = setTimeout(cancelDelete, 5000);
|
|
2911
|
+
disposeConfirm?.();
|
|
2912
|
+
disposeConfirm = api.keymap.registerLayer({
|
|
2913
|
+
mode: "modal",
|
|
2914
|
+
priority: 20,
|
|
2915
|
+
bindings: [{
|
|
2916
|
+
key: "y",
|
|
2917
|
+
desc: "Confirm delete",
|
|
2918
|
+
preventDefault: true,
|
|
2919
|
+
cmd: () => {
|
|
2920
|
+
confirmArmed();
|
|
2921
|
+
}
|
|
2922
|
+
}, {
|
|
2923
|
+
key: "alt+d",
|
|
2924
|
+
desc: "Confirm delete",
|
|
2925
|
+
preventDefault: true,
|
|
2926
|
+
cmd: () => {
|
|
2927
|
+
confirmArmed();
|
|
2928
|
+
}
|
|
2929
|
+
}, {
|
|
2930
|
+
key: "enter",
|
|
2931
|
+
desc: "Confirm delete",
|
|
2932
|
+
preventDefault: true,
|
|
2933
|
+
cmd: () => {
|
|
2934
|
+
confirmArmed();
|
|
2935
|
+
}
|
|
2936
|
+
}, {
|
|
2937
|
+
key: "n",
|
|
2938
|
+
desc: "Cancel delete",
|
|
2939
|
+
preventDefault: true,
|
|
2940
|
+
cmd: cancelDelete
|
|
2941
|
+
}, {
|
|
2942
|
+
key: "escape",
|
|
2943
|
+
desc: "Cancel delete",
|
|
2944
|
+
preventDefault: true,
|
|
2945
|
+
cmd: cancelDelete
|
|
2946
|
+
}]
|
|
2947
|
+
});
|
|
2948
|
+
};
|
|
2949
|
+
const forkEntry = async entry => {
|
|
2950
|
+
try {
|
|
2951
|
+
const result = await api.client.session.fork({
|
|
2952
|
+
sessionID: entry.id,
|
|
2953
|
+
directory: entry.dir || undefined
|
|
2954
|
+
});
|
|
2955
|
+
const created = result?.data ?? result;
|
|
2956
|
+
if (!created?.id) throw new Error("fork returned no session");
|
|
2957
|
+
cleanup();
|
|
2958
|
+
api.ui.dialog.clear();
|
|
2959
|
+
api.route.navigate("session", {
|
|
2960
|
+
sessionID: created.id
|
|
2961
|
+
});
|
|
2962
|
+
api.ui.toast({
|
|
2963
|
+
message: `Forked "${truncate(entry.title, 40)}"`,
|
|
2964
|
+
variant: "info"
|
|
2965
|
+
});
|
|
2966
|
+
} catch {
|
|
2967
|
+
api.ui.toast({
|
|
2968
|
+
message: "Could not fork session",
|
|
2969
|
+
variant: "error"
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
const choose = entry => {
|
|
2974
|
+
const target = entry ?? selectableEntries()[cursor()];
|
|
2975
|
+
if (!target) return;
|
|
2976
|
+
cleanup();
|
|
2977
|
+
api.ui.dialog.clear();
|
|
2978
|
+
api.route.navigate("session", {
|
|
2979
|
+
sessionID: target.id
|
|
2980
|
+
});
|
|
2981
|
+
};
|
|
2982
|
+
const disposeNav = api.keymap.registerLayer({
|
|
2983
|
+
mode: "modal",
|
|
2984
|
+
priority: 1,
|
|
2985
|
+
bindings: [{
|
|
2986
|
+
key: "up",
|
|
2987
|
+
desc: "Previous session",
|
|
2988
|
+
preventDefault: true,
|
|
2989
|
+
cmd: () => moveCursor(-1)
|
|
2990
|
+
}, {
|
|
2991
|
+
key: "down",
|
|
2992
|
+
desc: "Next session",
|
|
2993
|
+
preventDefault: true,
|
|
2994
|
+
cmd: () => moveCursor(1)
|
|
2995
|
+
}, {
|
|
2996
|
+
key: "pageup",
|
|
2997
|
+
desc: "Page up",
|
|
2998
|
+
preventDefault: true,
|
|
2999
|
+
cmd: () => moveCursor(-10)
|
|
3000
|
+
}, {
|
|
3001
|
+
key: "pagedown",
|
|
3002
|
+
desc: "Page down",
|
|
3003
|
+
preventDefault: true,
|
|
3004
|
+
cmd: () => moveCursor(10)
|
|
3005
|
+
}, {
|
|
3006
|
+
key: "home",
|
|
3007
|
+
desc: "First session",
|
|
3008
|
+
preventDefault: true,
|
|
3009
|
+
cmd: () => selectCursor(0)
|
|
3010
|
+
}, {
|
|
3011
|
+
key: "end",
|
|
3012
|
+
desc: "Last session",
|
|
3013
|
+
preventDefault: true,
|
|
3014
|
+
cmd: () => selectCursor(selectableEntries().length - 1)
|
|
3015
|
+
}, {
|
|
3016
|
+
key: "enter",
|
|
3017
|
+
desc: "Open session",
|
|
3018
|
+
preventDefault: true,
|
|
3019
|
+
cmd: () => {
|
|
3020
|
+
if (confirmArmed()) return;
|
|
3021
|
+
choose();
|
|
3022
|
+
}
|
|
3023
|
+
}, {
|
|
3024
|
+
key: "alt+p",
|
|
3025
|
+
desc: "Toggle transcript preview",
|
|
3026
|
+
preventDefault: true,
|
|
3027
|
+
cmd: togglePreview
|
|
3028
|
+
}, {
|
|
3029
|
+
key: "ctrl+s",
|
|
3030
|
+
desc: "Pin session",
|
|
3031
|
+
preventDefault: true,
|
|
3032
|
+
cmd: () => {
|
|
3033
|
+
const entry = selectableEntries()[cursor()];
|
|
3034
|
+
if (entry) onTogglePin("sessions", entry.id);
|
|
3035
|
+
}
|
|
3036
|
+
}, {
|
|
3037
|
+
key: "ctrl+d",
|
|
3038
|
+
desc: "Pin directory",
|
|
3039
|
+
preventDefault: true,
|
|
3040
|
+
cmd: () => {
|
|
3041
|
+
const entry = selectableEntries()[cursor()];
|
|
3042
|
+
if (entry) onTogglePin("directories", entry.dir);
|
|
3043
|
+
}
|
|
3044
|
+
}, {
|
|
3045
|
+
key: "alt+d",
|
|
3046
|
+
desc: "Delete session",
|
|
3047
|
+
preventDefault: true,
|
|
3048
|
+
cmd: () => {
|
|
3049
|
+
if (confirmArmed()) return;
|
|
3050
|
+
const target = selectableEntries()[cursor()];
|
|
3051
|
+
if (target) armDelete(target);
|
|
3052
|
+
}
|
|
3053
|
+
}, {
|
|
3054
|
+
key: "ctrl+f",
|
|
3055
|
+
desc: "Fork session",
|
|
3056
|
+
preventDefault: true,
|
|
3057
|
+
cmd: () => {
|
|
3058
|
+
const target = selectableEntries()[cursor()];
|
|
3059
|
+
if (target) void forkEntry(target);
|
|
3060
|
+
}
|
|
3061
|
+
}, {
|
|
3062
|
+
key: "ctrl+g",
|
|
3063
|
+
desc: "Toggle project scope",
|
|
3064
|
+
preventDefault: true,
|
|
3065
|
+
cmd: () => {
|
|
3066
|
+
const target = selectableEntries()[cursor()];
|
|
3067
|
+
if (scope()) setScope(undefined);else if (target) setScope(target.group);
|
|
3068
|
+
}
|
|
3069
|
+
}, {
|
|
3070
|
+
key: "alt+w",
|
|
3071
|
+
desc: "Filter needs input",
|
|
3072
|
+
preventDefault: true,
|
|
3073
|
+
cmd: () => setWaitingOnly(value => !value)
|
|
3074
|
+
}, {
|
|
3075
|
+
key: "alt+s",
|
|
3076
|
+
desc: "Filter pinned sessions",
|
|
3077
|
+
preventDefault: true,
|
|
3078
|
+
cmd: () => setPinnedOnly(value => !value)
|
|
3079
|
+
}, {
|
|
3080
|
+
key: "escape",
|
|
3081
|
+
desc: "Close",
|
|
3082
|
+
preventDefault: true,
|
|
3083
|
+
cmd: handleEscape
|
|
3084
|
+
}]
|
|
3085
|
+
});
|
|
3086
|
+
const cleanup = () => {
|
|
3087
|
+
if (!alive) return;
|
|
3088
|
+
alive = false;
|
|
3089
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
3090
|
+
pickerState = {
|
|
3091
|
+
query: query(),
|
|
3092
|
+
scope: scope(),
|
|
3093
|
+
waitingOnly: waitingOnly(),
|
|
3094
|
+
pinnedOnly: pinnedOnly()
|
|
3095
|
+
};
|
|
3096
|
+
disposeNav();
|
|
3097
|
+
disposeConfirm?.();
|
|
3098
|
+
disposeConfirm = undefined;
|
|
3099
|
+
if (previewTimer) clearTimeout(previewTimer);
|
|
3100
|
+
};
|
|
3101
|
+
api.ui.dialog.replace(() => (() => {
|
|
3102
|
+
var _el$73 = _$createElement("box"),
|
|
3103
|
+
_el$74 = _$createElement("box"),
|
|
3104
|
+
_el$75 = _$createElement("box"),
|
|
3105
|
+
_el$76 = _$createElement("text"),
|
|
3106
|
+
_el$77 = _$createTextNode(`Sessions `),
|
|
3107
|
+
_el$78 = _$createElement("span"),
|
|
3108
|
+
_el$79 = _$createTextNode(`(`),
|
|
3109
|
+
_el$80 = _$createTextNode(`)`),
|
|
3110
|
+
_el$81 = _$createElement("text"),
|
|
3111
|
+
_el$83 = _$createElement("box"),
|
|
3112
|
+
_el$84 = _$createElement("input"),
|
|
3113
|
+
_el$87 = _$createElement("box"),
|
|
3114
|
+
_el$88 = _$createElement("text"),
|
|
3115
|
+
_el$89 = _$createElement("text"),
|
|
3116
|
+
_el$90 = _$createElement("text"),
|
|
3117
|
+
_el$91 = _$createElement("box"),
|
|
3118
|
+
_el$94 = _$createElement("box"),
|
|
3119
|
+
_el$95 = _$createElement("text");
|
|
3120
|
+
_$insertNode(_el$73, _el$74);
|
|
3121
|
+
_$insertNode(_el$73, _el$91);
|
|
3122
|
+
_$insertNode(_el$73, _el$94);
|
|
3123
|
+
_$setProp(_el$73, "flexDirection", "column");
|
|
3124
|
+
_$setProp(_el$73, "paddingTop", 1);
|
|
3125
|
+
_$setProp(_el$73, "paddingBottom", 1);
|
|
3126
|
+
_$setProp(_el$73, "gap", 1);
|
|
3127
|
+
_$insertNode(_el$74, _el$75);
|
|
3128
|
+
_$insertNode(_el$74, _el$83);
|
|
3129
|
+
_$insertNode(_el$74, _el$87);
|
|
3130
|
+
_$setProp(_el$74, "paddingLeft", 4);
|
|
3131
|
+
_$setProp(_el$74, "paddingRight", 4);
|
|
3132
|
+
_$insertNode(_el$75, _el$76);
|
|
3133
|
+
_$insertNode(_el$75, _el$81);
|
|
3134
|
+
_$setProp(_el$75, "flexDirection", "row");
|
|
3135
|
+
_$setProp(_el$75, "justifyContent", "space-between");
|
|
3136
|
+
_$insertNode(_el$76, _el$77);
|
|
3137
|
+
_$insertNode(_el$76, _el$78);
|
|
3138
|
+
_$insertNode(_el$78, _el$79);
|
|
3139
|
+
_$insertNode(_el$78, _el$80);
|
|
3140
|
+
_$insert(_el$78, () => selectableEntries().length, _el$80);
|
|
3141
|
+
_$insertNode(_el$81, _$createTextNode(`esc`));
|
|
3142
|
+
_$setProp(_el$81, "onMouseUp", close);
|
|
3143
|
+
_$insertNode(_el$83, _el$84);
|
|
3144
|
+
_$setProp(_el$83, "paddingTop", 1);
|
|
3145
|
+
_$setProp(_el$84, "focused", true);
|
|
3146
|
+
_$setProp(_el$84, "placeholder", "Search title, directory, transcript…");
|
|
3147
|
+
_$setProp(_el$84, "cursorStyle", {
|
|
3148
|
+
style: "line",
|
|
3149
|
+
blinking: false
|
|
3150
|
+
});
|
|
3151
|
+
_$setProp(_el$84, "onInput", value => {
|
|
3152
|
+
if (pendingDelete()) return;
|
|
3153
|
+
setQuery(value);
|
|
3154
|
+
});
|
|
3155
|
+
_$setProp(_el$84, "onSubmit", () => {
|
|
3156
|
+
if (confirmArmed()) return;
|
|
3157
|
+
choose();
|
|
3158
|
+
});
|
|
3159
|
+
_$setProp(_el$84, "onKeyDown", event => {
|
|
3160
|
+
if (event.name === "escape") {
|
|
3161
|
+
event.preventDefault();
|
|
3162
|
+
handleEscape();
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (event.ctrl && event.name === "x") {
|
|
3166
|
+
event.preventDefault();
|
|
3167
|
+
if (confirmArmed()) return;
|
|
3168
|
+
const target = selectableEntries()[cursor()];
|
|
3169
|
+
if (target) armDelete(target);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
if (event.ctrl && event.name === "f") {
|
|
3173
|
+
event.preventDefault();
|
|
3174
|
+
const target = selectableEntries()[cursor()];
|
|
3175
|
+
if (target) void forkEntry(target);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
if (pendingDelete()) {
|
|
3179
|
+
if (event.name === "y" || event.name === "enter" || event.name === "return") {
|
|
3180
|
+
event.preventDefault();
|
|
3181
|
+
confirmArmed();
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
if (event.name === "n") {
|
|
3185
|
+
event.preventDefault();
|
|
3186
|
+
cancelDelete();
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
_$insert(_el$74, _$createComponent(Show, {
|
|
3192
|
+
get when() {
|
|
3193
|
+
return coverageLabel();
|
|
3194
|
+
},
|
|
3195
|
+
get children() {
|
|
3196
|
+
var _el$85 = _$createElement("box"),
|
|
3197
|
+
_el$86 = _$createElement("text");
|
|
3198
|
+
_$insertNode(_el$85, _el$86);
|
|
3199
|
+
_$setProp(_el$85, "paddingTop", 1);
|
|
3200
|
+
_$insert(_el$86, coverageLabel);
|
|
3201
|
+
_$effect(_$p => _$setProp(_el$86, "style", {
|
|
3202
|
+
fg: api.theme.current.textMuted
|
|
3203
|
+
}, _$p));
|
|
3204
|
+
return _el$85;
|
|
3205
|
+
}
|
|
3206
|
+
}), _el$87);
|
|
3207
|
+
_$insertNode(_el$87, _el$88);
|
|
3208
|
+
_$insertNode(_el$87, _el$89);
|
|
3209
|
+
_$insertNode(_el$87, _el$90);
|
|
3210
|
+
_$setProp(_el$87, "flexDirection", "row");
|
|
3211
|
+
_$setProp(_el$87, "gap", 2);
|
|
3212
|
+
_$setProp(_el$87, "paddingTop", 1);
|
|
3213
|
+
_$setProp(_el$88, "onMouseDown", () => {
|
|
3214
|
+
const group = currentProject();
|
|
3215
|
+
if (group) setScope(scope() === group ? undefined : group);else api.ui.toast({
|
|
3216
|
+
message: "No current project to filter",
|
|
3217
|
+
variant: "info"
|
|
3218
|
+
});
|
|
3219
|
+
});
|
|
3220
|
+
_$insert(_el$88, (() => {
|
|
3221
|
+
var _c$6 = _$memo(() => !!scope());
|
|
3222
|
+
return () => _c$6() ? `Project: ${truncate(scope(), 22)}` : "Project: all";
|
|
3223
|
+
})());
|
|
3224
|
+
_$setProp(_el$89, "onMouseDown", () => setWaitingOnly(value => !value));
|
|
3225
|
+
_$insert(_el$89, () => waitingOnly() ? "● Needs input" : "○ Needs input");
|
|
3226
|
+
_$setProp(_el$90, "onMouseDown", () => setPinnedOnly(value => !value));
|
|
3227
|
+
_$insert(_el$90, () => pinnedOnly() ? "★ Pinned" : "☆ Pinned");
|
|
3228
|
+
_$setProp(_el$91, "flexDirection", "column");
|
|
3229
|
+
_$setProp(_el$91, "flexGrow", 1);
|
|
3230
|
+
_$setProp(_el$91, "overflow", "hidden");
|
|
3231
|
+
_$setProp(_el$91, "onMouseScroll", event => {
|
|
3232
|
+
scrollPicker(event.scroll);
|
|
3233
|
+
});
|
|
3234
|
+
_$insert(_el$91, _$createComponent(For, {
|
|
3235
|
+
get each() {
|
|
3236
|
+
return visiblePickerRows();
|
|
3237
|
+
},
|
|
3238
|
+
children: row => {
|
|
3239
|
+
if (row.kind === "group") return (() => {
|
|
3240
|
+
var _el$108 = _$createElement("box"),
|
|
3241
|
+
_el$109 = _$createElement("text"),
|
|
3242
|
+
_el$110 = _$createElement("text"),
|
|
3243
|
+
_el$111 = _$createTextNode(` (`),
|
|
3244
|
+
_el$112 = _$createTextNode(`)`);
|
|
3245
|
+
_$insertNode(_el$108, _el$109);
|
|
3246
|
+
_$insertNode(_el$108, _el$110);
|
|
3247
|
+
_$setProp(_el$108, "flexDirection", "row");
|
|
3248
|
+
_$setProp(_el$108, "paddingLeft", 4);
|
|
3249
|
+
_$setProp(_el$108, "paddingRight", 4);
|
|
3250
|
+
_$setProp(_el$108, "onMouseDown", () => setCollapsed(state => ({
|
|
3251
|
+
...state,
|
|
3252
|
+
[row.dir]: !state[row.dir]
|
|
3253
|
+
})));
|
|
3254
|
+
_$setProp(_el$109, "flexGrow", 1);
|
|
3255
|
+
_$setProp(_el$109, "flexShrink", 1);
|
|
3256
|
+
_$setProp(_el$109, "wrapMode", "none");
|
|
3257
|
+
_$insert(_el$109, () => collapsed()[row.dir] && !query().trim() ? "▸ " : "▾ ", null);
|
|
3258
|
+
_$insert(_el$109, () => row.pinned ? "★ " : "", null);
|
|
3259
|
+
_$insert(_el$109, () => row.label, null);
|
|
3260
|
+
_$insertNode(_el$110, _el$111);
|
|
3261
|
+
_$insertNode(_el$110, _el$112);
|
|
3262
|
+
_$setProp(_el$110, "flexShrink", 0);
|
|
3263
|
+
_$insert(_el$110, () => row.count, _el$112);
|
|
3264
|
+
_$effect(_p$ => {
|
|
3265
|
+
var _v$58 = row.continued ? 0 : 1,
|
|
3266
|
+
_v$59 = {
|
|
3267
|
+
fg: api.theme.current.accent
|
|
3268
|
+
},
|
|
3269
|
+
_v$60 = TextAttributes.BOLD,
|
|
3270
|
+
_v$61 = {
|
|
3271
|
+
fg: api.theme.current.textMuted
|
|
3272
|
+
};
|
|
3273
|
+
_v$58 !== _p$.e && (_p$.e = _$setProp(_el$108, "paddingTop", _v$58, _p$.e));
|
|
3274
|
+
_v$59 !== _p$.t && (_p$.t = _$setProp(_el$109, "style", _v$59, _p$.t));
|
|
3275
|
+
_v$60 !== _p$.a && (_p$.a = _$setProp(_el$109, "attributes", _v$60, _p$.a));
|
|
3276
|
+
_v$61 !== _p$.o && (_p$.o = _$setProp(_el$110, "style", _v$61, _p$.o));
|
|
3277
|
+
return _p$;
|
|
3278
|
+
}, {
|
|
3279
|
+
e: undefined,
|
|
3280
|
+
t: undefined,
|
|
3281
|
+
a: undefined,
|
|
3282
|
+
o: undefined
|
|
3283
|
+
});
|
|
3284
|
+
return _el$108;
|
|
3285
|
+
})();
|
|
3286
|
+
const excerpt = createMemo(() => transcriptMatchExcerpt(searchIndex().get(row.entry.id), row.entry, query()));
|
|
3287
|
+
return (() => {
|
|
3288
|
+
var _el$113 = _$createElement("box"),
|
|
3289
|
+
_el$114 = _$createElement("box"),
|
|
3290
|
+
_el$115 = _$createElement("text"),
|
|
3291
|
+
_el$118 = _$createElement("text");
|
|
3292
|
+
_$insertNode(_el$113, _el$114);
|
|
3293
|
+
_$setProp(_el$113, "flexDirection", "column");
|
|
3294
|
+
_$setProp(_el$113, "paddingLeft", 6);
|
|
3295
|
+
_$setProp(_el$113, "paddingRight", 4);
|
|
3296
|
+
_$setProp(_el$113, "onMouseMove", () => {
|
|
3297
|
+
// Repainting under a stationary pointer must not select
|
|
3298
|
+
// another row while the wheel is moving the viewport.
|
|
3299
|
+
const index = selectableEntries().findIndex(entry => entry.id === row.entry.id);
|
|
3300
|
+
if (index >= 0) setCursor(index);
|
|
3301
|
+
});
|
|
3302
|
+
_$setProp(_el$113, "onMouseDown", event => event.button === RIGHT_BUTTON ? (cleanup(), api.ui.dialog.clear(), openInCmuxWorkspace(api, row.entry)) : choose(row.entry));
|
|
3303
|
+
_$insertNode(_el$114, _el$115);
|
|
3304
|
+
_$insertNode(_el$114, _el$118);
|
|
3305
|
+
_$setProp(_el$114, "flexDirection", "row");
|
|
3306
|
+
_$setProp(_el$114, "gap", 1);
|
|
3307
|
+
_$setProp(_el$115, "flexShrink", 0);
|
|
3308
|
+
_$insert(_el$115, () => row.last ? "└" : "├");
|
|
3309
|
+
_$insert(_el$114, _$createComponent(Show, {
|
|
3310
|
+
get when() {
|
|
3311
|
+
return row.entry.id === currentSessionID();
|
|
3312
|
+
},
|
|
3313
|
+
get children() {
|
|
3314
|
+
var _el$116 = _$createElement("text");
|
|
3315
|
+
_$insertNode(_el$116, _$createTextNode(`●`));
|
|
3316
|
+
_$setProp(_el$116, "flexShrink", 0);
|
|
3317
|
+
_$effect(_$p => _$setProp(_el$116, "style", {
|
|
3318
|
+
fg: row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.text : api.theme.current.primary
|
|
3319
|
+
}, _$p));
|
|
3320
|
+
return _el$116;
|
|
3321
|
+
}
|
|
3322
|
+
}), _el$118);
|
|
3323
|
+
_$insert(_el$114, _$createComponent(Highlighted, {
|
|
3324
|
+
get text() {
|
|
3325
|
+
return `${pins().sessions.includes(row.entry.id) ? "★ " : ""}${truncate(row.entry.title, pins().sessions.includes(row.entry.id) ? 59 : 61)}`;
|
|
3326
|
+
},
|
|
3327
|
+
get query() {
|
|
3328
|
+
return query();
|
|
3329
|
+
},
|
|
3330
|
+
get bold() {
|
|
3331
|
+
return row.entry.id === selectableEntries()[cursor()]?.id;
|
|
3332
|
+
},
|
|
3333
|
+
get color() {
|
|
3334
|
+
return _$memo(() => row.entry.id === selectableEntries()[cursor()]?.id)() ? api.theme.current.text : api.theme.current.text;
|
|
3335
|
+
},
|
|
3336
|
+
get matchColor() {
|
|
3337
|
+
return _$memo(() => row.entry.id === selectableEntries()[cursor()]?.id)() ? api.theme.current.text : api.theme.current.warning;
|
|
3338
|
+
}
|
|
3339
|
+
}), _el$118);
|
|
3340
|
+
_$setProp(_el$118, "flexShrink", 0);
|
|
3341
|
+
_$insert(_el$118, (() => {
|
|
3342
|
+
var _c$0 = _$memo(() => pendingDelete()?.id === row.entry.id);
|
|
3343
|
+
return () => _c$0() ? "press y to delete" : `${ago(row.entry.updated)}${query().trim() && !`${row.entry.title} ${row.entry.dir}`.toLowerCase().includes(query().trim().toLowerCase()) ? " · transcript match" : ""}`;
|
|
3344
|
+
})());
|
|
3345
|
+
_$insert(_el$113, _$createComponent(Show, {
|
|
3346
|
+
get when() {
|
|
3347
|
+
return excerpt();
|
|
3348
|
+
},
|
|
3349
|
+
children: match => (() => {
|
|
3350
|
+
var _el$119 = _$createElement("box"),
|
|
3351
|
+
_el$120 = _$createElement("text");
|
|
3352
|
+
_$insertNode(_el$119, _el$120);
|
|
3353
|
+
_$setProp(_el$119, "flexDirection", "row");
|
|
3354
|
+
_$setProp(_el$119, "gap", 1);
|
|
3355
|
+
_$setProp(_el$119, "paddingLeft", 2);
|
|
3356
|
+
_$insertNode(_el$120, _$createTextNode(`↳`));
|
|
3357
|
+
_$setProp(_el$120, "flexShrink", 0);
|
|
3358
|
+
_$insert(_el$119, _$createComponent(Highlighted, {
|
|
3359
|
+
get text() {
|
|
3360
|
+
return match();
|
|
3361
|
+
},
|
|
3362
|
+
get query() {
|
|
3363
|
+
return query();
|
|
3364
|
+
},
|
|
3365
|
+
get color() {
|
|
3366
|
+
return _$memo(() => row.entry.id === selectableEntries()[cursor()]?.id)() ? api.theme.current.text : api.theme.current.textMuted;
|
|
3367
|
+
},
|
|
3368
|
+
get matchColor() {
|
|
3369
|
+
return api.theme.current.warning;
|
|
3370
|
+
}
|
|
3371
|
+
}), null);
|
|
3372
|
+
_$effect(_$p => _$setProp(_el$120, "style", {
|
|
3373
|
+
fg: api.theme.current.textMuted
|
|
3374
|
+
}, _$p));
|
|
3375
|
+
return _el$119;
|
|
3376
|
+
})()
|
|
3377
|
+
}), null);
|
|
3378
|
+
_$effect(_p$ => {
|
|
3379
|
+
var _v$62 = row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
3380
|
+
_v$63 = {
|
|
3381
|
+
fg: row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.text : api.theme.current.textMuted
|
|
3382
|
+
},
|
|
3383
|
+
_v$64 = {
|
|
3384
|
+
fg: pendingDelete()?.id === row.entry.id ? api.theme.current.error : row.entry.id === selectableEntries()[cursor()]?.id ? api.theme.current.text : api.theme.current.textMuted
|
|
3385
|
+
};
|
|
3386
|
+
_v$62 !== _p$.e && (_p$.e = _$setProp(_el$113, "backgroundColor", _v$62, _p$.e));
|
|
3387
|
+
_v$63 !== _p$.t && (_p$.t = _$setProp(_el$115, "style", _v$63, _p$.t));
|
|
3388
|
+
_v$64 !== _p$.a && (_p$.a = _$setProp(_el$118, "style", _v$64, _p$.a));
|
|
3389
|
+
return _p$;
|
|
3390
|
+
}, {
|
|
3391
|
+
e: undefined,
|
|
3392
|
+
t: undefined,
|
|
3393
|
+
a: undefined
|
|
3394
|
+
});
|
|
3395
|
+
return _el$113;
|
|
3396
|
+
})();
|
|
3397
|
+
}
|
|
3398
|
+
}), null);
|
|
3399
|
+
_$insert(_el$91, _$createComponent(Show, {
|
|
3400
|
+
get when() {
|
|
3401
|
+
return matchedGroups().length === 0;
|
|
3402
|
+
},
|
|
3403
|
+
get children() {
|
|
3404
|
+
var _el$92 = _$createElement("box"),
|
|
3405
|
+
_el$93 = _$createElement("text");
|
|
3406
|
+
_$insertNode(_el$92, _el$93);
|
|
3407
|
+
_$setProp(_el$92, "paddingLeft", 4);
|
|
3408
|
+
_$setProp(_el$92, "paddingRight", 4);
|
|
3409
|
+
_$insert(_el$93, (() => {
|
|
3410
|
+
var _c$7 = _$memo(() => !!(waitingOnly() && waitingCoverage() === "loading"));
|
|
3411
|
+
return () => _c$7() ? "Checking which sessions need input…" : _$memo(() => !!(waitingOnly() && waitingCoverage() === "unavailable"))() ? "Needs-input data unavailable · turn off the filter" : _$memo(() => !!(pinnedOnly() && pins().sessions.length === 0))() ? "No pinned sessions yet · ctrl+s pins a session" : _$memo(() => !!(!indexProgress().complete && query().trim()))() ? "No matches yet · still searching transcripts…" : _$memo(() => !!scope())() ? `No matches in ${scope()} · ctrl+g for all projects` : waitingOnly() ? "No sessions need input with these filters" : "No sessions match · try another search";
|
|
3412
|
+
})());
|
|
3413
|
+
_$effect(_$p => _$setProp(_el$93, "style", {
|
|
3414
|
+
fg: api.theme.current.textMuted
|
|
3415
|
+
}, _$p));
|
|
3416
|
+
return _el$92;
|
|
3417
|
+
}
|
|
3418
|
+
}), null);
|
|
3419
|
+
_$insertNode(_el$94, _el$95);
|
|
3420
|
+
_$setProp(_el$94, "paddingLeft", 4);
|
|
3421
|
+
_$setProp(_el$94, "paddingRight", 4);
|
|
3422
|
+
_$setProp(_el$94, "flexShrink", 0);
|
|
3423
|
+
_$setProp(_el$94, "flexDirection", "column");
|
|
3424
|
+
_$insert(_el$95, (() => {
|
|
3425
|
+
var _c$8 = _$memo(() => !!pendingDelete());
|
|
3426
|
+
return () => _c$8() ? `Delete "${truncate(pendingDelete().title, 40)}"? y confirm · n cancel` : "↑↓ move · enter open · ctrl+g project · option+w input · option+s pinned · option+p preview";
|
|
3427
|
+
})());
|
|
3428
|
+
_$insert(_el$94, _$createComponent(Show, {
|
|
3429
|
+
get when() {
|
|
3430
|
+
return !pendingDelete();
|
|
3431
|
+
},
|
|
3432
|
+
get children() {
|
|
3433
|
+
var _el$96 = _$createElement("text");
|
|
3434
|
+
_$insertNode(_el$96, _$createTextNode(`ctrl+s/d pin · option+d delete · ctrl+f fork · esc close`));
|
|
3435
|
+
_$effect(_$p => _$setProp(_el$96, "style", {
|
|
3436
|
+
fg: api.theme.current.textMuted
|
|
3437
|
+
}, _$p));
|
|
3438
|
+
return _el$96;
|
|
3439
|
+
}
|
|
3440
|
+
}), null);
|
|
3441
|
+
_$insert(_el$73, _$createComponent(Show, {
|
|
3442
|
+
get when() {
|
|
3443
|
+
return showPreview();
|
|
3444
|
+
},
|
|
3445
|
+
get children() {
|
|
3446
|
+
var _el$98 = _$createElement("box"),
|
|
3447
|
+
_el$99 = _$createElement("box"),
|
|
3448
|
+
_el$100 = _$createElement("text"),
|
|
3449
|
+
_el$101 = _$createElement("text"),
|
|
3450
|
+
_el$105 = _$createElement("scrollbox"),
|
|
3451
|
+
_el$106 = _$createElement("text");
|
|
3452
|
+
_$insertNode(_el$98, _el$99);
|
|
3453
|
+
_$insertNode(_el$98, _el$105);
|
|
3454
|
+
_$insertNode(_el$98, _el$106);
|
|
3455
|
+
_$setProp(_el$98, "position", "absolute");
|
|
3456
|
+
_$setProp(_el$98, "left", 0);
|
|
3457
|
+
_$setProp(_el$98, "top", 0);
|
|
3458
|
+
_$setProp(_el$98, "width", dialogWidth);
|
|
3459
|
+
_$setProp(_el$98, "flexDirection", "column");
|
|
3460
|
+
_$setProp(_el$98, "paddingLeft", 4);
|
|
3461
|
+
_$setProp(_el$98, "paddingRight", 4);
|
|
3462
|
+
_$setProp(_el$98, "paddingTop", 1);
|
|
3463
|
+
_$setProp(_el$98, "paddingBottom", 1);
|
|
3464
|
+
_$setProp(_el$98, "gap", 1);
|
|
3465
|
+
_$insertNode(_el$99, _el$100);
|
|
3466
|
+
_$insertNode(_el$99, _el$101);
|
|
3467
|
+
_$setProp(_el$99, "flexDirection", "row");
|
|
3468
|
+
_$setProp(_el$99, "justifyContent", "space-between");
|
|
3469
|
+
_$insert(_el$100, (() => {
|
|
3470
|
+
var _c$9 = _$memo(() => !!previewID());
|
|
3471
|
+
return () => _c$9() ? truncate(selectableEntries()[cursor()].title, dialogWidth - 18) : "Preview";
|
|
3472
|
+
})());
|
|
3473
|
+
_$insertNode(_el$101, _$createTextNode(`esc close`));
|
|
3474
|
+
_$insert(_el$98, _$createComponent(Show, {
|
|
3475
|
+
get when() {
|
|
3476
|
+
return previewID();
|
|
3477
|
+
},
|
|
3478
|
+
get children() {
|
|
3479
|
+
var _el$103 = _$createElement("text"),
|
|
3480
|
+
_el$104 = _$createTextNode(` · `);
|
|
3481
|
+
_$insertNode(_el$103, _el$104);
|
|
3482
|
+
_$insert(_el$103, () => prettyDir(selectableEntries()[cursor()].dir, process.env.HOME ?? ""), _el$104);
|
|
3483
|
+
_$insert(_el$103, () => ago(selectableEntries()[cursor()].updated), null);
|
|
3484
|
+
_$effect(_$p => _$setProp(_el$103, "style", {
|
|
3485
|
+
fg: api.theme.current.textMuted
|
|
3486
|
+
}, _$p));
|
|
3487
|
+
return _el$103;
|
|
3488
|
+
}
|
|
3489
|
+
}), _el$105);
|
|
3490
|
+
_$setProp(_el$105, "flexGrow", 1);
|
|
3491
|
+
_$setProp(_el$105, "scrollbarOptions", {
|
|
3492
|
+
visible: false
|
|
3493
|
+
});
|
|
3494
|
+
_$insert(_el$105, _$createComponent(Show, {
|
|
3495
|
+
get when() {
|
|
3496
|
+
return previewID();
|
|
3497
|
+
},
|
|
3498
|
+
get fallback() {
|
|
3499
|
+
return (() => {
|
|
3500
|
+
var _el$122 = _$createElement("text");
|
|
3501
|
+
_$insertNode(_el$122, _$createTextNode(`Select a session to preview`));
|
|
3502
|
+
_$effect(_$p => _$setProp(_el$122, "style", {
|
|
3503
|
+
fg: api.theme.current.textMuted
|
|
3504
|
+
}, _$p));
|
|
3505
|
+
return _el$122;
|
|
3506
|
+
})();
|
|
3507
|
+
},
|
|
3508
|
+
get children() {
|
|
3509
|
+
return _$createComponent(Show, {
|
|
3510
|
+
get when() {
|
|
3511
|
+
return previewText();
|
|
3512
|
+
},
|
|
3513
|
+
get fallback() {
|
|
3514
|
+
return (() => {
|
|
3515
|
+
var _el$124 = _$createElement("text");
|
|
3516
|
+
_$insertNode(_el$124, _$createTextNode(`Loading…`));
|
|
3517
|
+
_$effect(_$p => _$setProp(_el$124, "style", {
|
|
3518
|
+
fg: api.theme.current.textMuted
|
|
3519
|
+
}, _$p));
|
|
3520
|
+
return _el$124;
|
|
3521
|
+
})();
|
|
3522
|
+
},
|
|
3523
|
+
children: text => getMarkdownStyle(api.theme.current, api.renderer?.nativeSession) ? (() => {
|
|
3524
|
+
var _el$126 = _$createElement("markdown");
|
|
3525
|
+
_$effect(_p$ => {
|
|
3526
|
+
var _v$65 = text(),
|
|
3527
|
+
_v$66 = getMarkdownStyle(api.theme.current, api.renderer?.nativeSession),
|
|
3528
|
+
_v$67 = getTreeSitterClient();
|
|
3529
|
+
_v$65 !== _p$.e && (_p$.e = _$setProp(_el$126, "content", _v$65, _p$.e));
|
|
3530
|
+
_v$66 !== _p$.t && (_p$.t = _$setProp(_el$126, "syntaxStyle", _v$66, _p$.t));
|
|
3531
|
+
_v$67 !== _p$.a && (_p$.a = _$setProp(_el$126, "treeSitterClient", _v$67, _p$.a));
|
|
3532
|
+
return _p$;
|
|
3533
|
+
}, {
|
|
3534
|
+
e: undefined,
|
|
3535
|
+
t: undefined,
|
|
3536
|
+
a: undefined
|
|
3537
|
+
});
|
|
3538
|
+
return _el$126;
|
|
3539
|
+
})() : (() => {
|
|
3540
|
+
var _el$127 = _$createElement("text");
|
|
3541
|
+
_$setProp(_el$127, "wrapMode", "word");
|
|
3542
|
+
_$insert(_el$127, text);
|
|
3543
|
+
return _el$127;
|
|
3544
|
+
})()
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
}));
|
|
3548
|
+
_$insertNode(_el$106, _$createTextNode(`↑↓ switch session · esc back to picker`));
|
|
3549
|
+
_$effect(_p$ => {
|
|
3550
|
+
var _v$38 = dialogHeight(),
|
|
3551
|
+
_v$39 = api.theme.current.backgroundPanel,
|
|
3552
|
+
_v$40 = TextAttributes.BOLD,
|
|
3553
|
+
_v$41 = {
|
|
3554
|
+
fg: api.theme.current.textMuted
|
|
3555
|
+
},
|
|
3556
|
+
_v$42 = {
|
|
3557
|
+
fg: api.theme.current.textMuted
|
|
3558
|
+
};
|
|
3559
|
+
_v$38 !== _p$.e && (_p$.e = _$setProp(_el$98, "height", _v$38, _p$.e));
|
|
3560
|
+
_v$39 !== _p$.t && (_p$.t = _$setProp(_el$98, "backgroundColor", _v$39, _p$.t));
|
|
3561
|
+
_v$40 !== _p$.a && (_p$.a = _$setProp(_el$100, "attributes", _v$40, _p$.a));
|
|
3562
|
+
_v$41 !== _p$.o && (_p$.o = _$setProp(_el$101, "style", _v$41, _p$.o));
|
|
3563
|
+
_v$42 !== _p$.i && (_p$.i = _$setProp(_el$106, "style", _v$42, _p$.i));
|
|
3564
|
+
return _p$;
|
|
3565
|
+
}, {
|
|
3566
|
+
e: undefined,
|
|
3567
|
+
t: undefined,
|
|
3568
|
+
a: undefined,
|
|
3569
|
+
o: undefined,
|
|
3570
|
+
i: undefined
|
|
3571
|
+
});
|
|
3572
|
+
return _el$98;
|
|
3573
|
+
}
|
|
3574
|
+
}), null);
|
|
3575
|
+
_$effect(_p$ => {
|
|
3576
|
+
var _v$43 = dialogHeight(),
|
|
3577
|
+
_v$44 = dialogOffset(),
|
|
3578
|
+
_v$45 = api.theme.current.backgroundPanel,
|
|
3579
|
+
_v$46 = TextAttributes.BOLD,
|
|
3580
|
+
_v$47 = {
|
|
3581
|
+
fg: api.theme.current.textMuted
|
|
3582
|
+
},
|
|
3583
|
+
_v$48 = {
|
|
3584
|
+
fg: api.theme.current.textMuted
|
|
3585
|
+
},
|
|
3586
|
+
_v$49 = query(),
|
|
3587
|
+
_v$50 = api.theme.current.textMuted,
|
|
3588
|
+
_v$51 = api.theme.current.backgroundPanel,
|
|
3589
|
+
_v$52 = api.theme.current.text,
|
|
3590
|
+
_v$53 = api.theme.current.primary,
|
|
3591
|
+
_v$54 = {
|
|
3592
|
+
fg: scope() ? api.theme.current.accent : api.theme.current.textMuted
|
|
3593
|
+
},
|
|
3594
|
+
_v$55 = {
|
|
3595
|
+
fg: waitingOnly() ? api.theme.current.warning : api.theme.current.textMuted
|
|
3596
|
+
},
|
|
3597
|
+
_v$56 = {
|
|
3598
|
+
fg: pinnedOnly() ? api.theme.current.accent : api.theme.current.textMuted
|
|
3599
|
+
},
|
|
3600
|
+
_v$57 = {
|
|
3601
|
+
fg: pendingDelete() ? api.theme.current.warning : api.theme.current.textMuted
|
|
3602
|
+
};
|
|
3603
|
+
_v$43 !== _p$.e && (_p$.e = _$setProp(_el$73, "height", _v$43, _p$.e));
|
|
3604
|
+
_v$44 !== _p$.t && (_p$.t = _$setProp(_el$73, "marginTop", _v$44, _p$.t));
|
|
3605
|
+
_v$45 !== _p$.a && (_p$.a = _$setProp(_el$73, "backgroundColor", _v$45, _p$.a));
|
|
3606
|
+
_v$46 !== _p$.o && (_p$.o = _$setProp(_el$76, "attributes", _v$46, _p$.o));
|
|
3607
|
+
_v$47 !== _p$.i && (_p$.i = _$setProp(_el$78, "style", _v$47, _p$.i));
|
|
3608
|
+
_v$48 !== _p$.n && (_p$.n = _$setProp(_el$81, "style", _v$48, _p$.n));
|
|
3609
|
+
_v$49 !== _p$.s && (_p$.s = _$setProp(_el$84, "value", _v$49, _p$.s));
|
|
3610
|
+
_v$50 !== _p$.h && (_p$.h = _$setProp(_el$84, "placeholderColor", _v$50, _p$.h));
|
|
3611
|
+
_v$51 !== _p$.r && (_p$.r = _$setProp(_el$84, "focusedBackgroundColor", _v$51, _p$.r));
|
|
3612
|
+
_v$52 !== _p$.d && (_p$.d = _$setProp(_el$84, "focusedTextColor", _v$52, _p$.d));
|
|
3613
|
+
_v$53 !== _p$.l && (_p$.l = _$setProp(_el$84, "cursorColor", _v$53, _p$.l));
|
|
3614
|
+
_v$54 !== _p$.u && (_p$.u = _$setProp(_el$88, "style", _v$54, _p$.u));
|
|
3615
|
+
_v$55 !== _p$.c && (_p$.c = _$setProp(_el$89, "style", _v$55, _p$.c));
|
|
3616
|
+
_v$56 !== _p$.w && (_p$.w = _$setProp(_el$90, "style", _v$56, _p$.w));
|
|
3617
|
+
_v$57 !== _p$.m && (_p$.m = _$setProp(_el$95, "style", _v$57, _p$.m));
|
|
3618
|
+
return _p$;
|
|
3619
|
+
}, {
|
|
3620
|
+
e: undefined,
|
|
3621
|
+
t: undefined,
|
|
3622
|
+
a: undefined,
|
|
3623
|
+
o: undefined,
|
|
3624
|
+
i: undefined,
|
|
3625
|
+
n: undefined,
|
|
3626
|
+
s: undefined,
|
|
3627
|
+
h: undefined,
|
|
3628
|
+
r: undefined,
|
|
3629
|
+
d: undefined,
|
|
3630
|
+
l: undefined,
|
|
3631
|
+
u: undefined,
|
|
3632
|
+
c: undefined,
|
|
3633
|
+
w: undefined,
|
|
3634
|
+
m: undefined
|
|
3635
|
+
});
|
|
3636
|
+
return _el$73;
|
|
3637
|
+
})(), () => {
|
|
3638
|
+
cleanup();
|
|
3639
|
+
});
|
|
3640
|
+
api.ui.dialog.setSize("xlarge");
|
|
3641
|
+
};
|
|
3642
|
+
|
|
3643
|
+
// Read-only digest dialog: spend per project, last day beside lifetime.
|
|
3644
|
+
// Same message-level sums as `sesh costs`; the dialog just renders them.
|
|
3645
|
+
const openCosts = async (command = false) => {
|
|
3646
|
+
if (!command && api.mode.current() !== BASE_MODE) return;
|
|
3647
|
+
if (!command && api.renderer.currentFocusedEditor === null) return;
|
|
3648
|
+
let rows = [];
|
|
3649
|
+
let failed = false;
|
|
3650
|
+
try {
|
|
3651
|
+
const db = await openTranscriptDb();
|
|
3652
|
+
if (!db) failed = true;else {
|
|
3653
|
+
try {
|
|
3654
|
+
const cutoff = Date.now() - 86400000;
|
|
3655
|
+
const legacyTables = db.query("SELECT count(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN ('session', 'message')").get();
|
|
3656
|
+
const raw = db.query(costRowsQuery(cutoff, legacyTables?.count === 2)).all() ?? [];
|
|
3657
|
+
rows = raw.filter(row => typeof row?.directory === "string" && typeof row?.window_cost === "number" && typeof row?.lifetime_cost === "number" && typeof row?.sessions === "number" && row.lifetime_cost > 0).map(row => ({
|
|
3658
|
+
directory: row.directory,
|
|
3659
|
+
window: row.window_cost,
|
|
3660
|
+
lifetime: row.lifetime_cost,
|
|
3661
|
+
sessions: row.sessions
|
|
3662
|
+
}));
|
|
3663
|
+
} finally {
|
|
3664
|
+
db.close();
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
} catch {
|
|
3668
|
+
failed = true;
|
|
3669
|
+
}
|
|
3670
|
+
const money = n => n === 0 ? "—" : `$${n.toFixed(2)}`;
|
|
3671
|
+
const home = process.env.HOME ?? "";
|
|
3672
|
+
const disposeKeys = api.keymap.registerLayer({
|
|
3673
|
+
mode: "modal",
|
|
3674
|
+
bindings: [{
|
|
3675
|
+
key: "escape",
|
|
3676
|
+
desc: "Close",
|
|
3677
|
+
preventDefault: true,
|
|
3678
|
+
cmd: () => api.ui.dialog.clear()
|
|
3679
|
+
}]
|
|
3680
|
+
});
|
|
3681
|
+
api.ui.dialog.replace(() => (() => {
|
|
3682
|
+
var _el$128 = _$createElement("box"),
|
|
3683
|
+
_el$129 = _$createElement("text"),
|
|
3684
|
+
_el$130 = _$createTextNode(`Cost digest `),
|
|
3685
|
+
_el$132 = _$createElement("span"),
|
|
3686
|
+
_el$134 = _$createElement("box"),
|
|
3687
|
+
_el$135 = _$createElement("text");
|
|
3688
|
+
_$insertNode(_el$128, _el$129);
|
|
3689
|
+
_$insertNode(_el$128, _el$134);
|
|
3690
|
+
_$setProp(_el$128, "flexDirection", "column");
|
|
3691
|
+
_$setProp(_el$128, "paddingLeft", 4);
|
|
3692
|
+
_$setProp(_el$128, "paddingRight", 4);
|
|
3693
|
+
_$setProp(_el$128, "paddingTop", 1);
|
|
3694
|
+
_$setProp(_el$128, "gap", 1);
|
|
3695
|
+
_$insertNode(_el$129, _el$130);
|
|
3696
|
+
_$insertNode(_el$129, _el$132);
|
|
3697
|
+
_$insertNode(_el$132, _$createTextNode(`(24h · lifetime)`));
|
|
3698
|
+
_$insert(_el$128, _$createComponent(Show, {
|
|
3699
|
+
get when() {
|
|
3700
|
+
return !failed && rows.length > 0;
|
|
3701
|
+
},
|
|
3702
|
+
get fallback() {
|
|
3703
|
+
return (() => {
|
|
3704
|
+
var _el$137 = _$createElement("text");
|
|
3705
|
+
_$insert(_el$137, failed ? "Cost data unavailable." : "No assistant cost recorded.");
|
|
3706
|
+
_$effect(_$p => _$setProp(_el$137, "style", {
|
|
3707
|
+
fg: api.theme.current.textMuted
|
|
3708
|
+
}, _$p));
|
|
3709
|
+
return _el$137;
|
|
3710
|
+
})();
|
|
3711
|
+
},
|
|
3712
|
+
get children() {
|
|
3713
|
+
return _$createComponent(For, {
|
|
3714
|
+
each: rows,
|
|
3715
|
+
children: row => (() => {
|
|
3716
|
+
var _el$138 = _$createElement("box"),
|
|
3717
|
+
_el$139 = _$createElement("text"),
|
|
3718
|
+
_el$140 = _$createElement("text"),
|
|
3719
|
+
_el$141 = _$createElement("text"),
|
|
3720
|
+
_el$142 = _$createTextNode(`(`),
|
|
3721
|
+
_el$143 = _$createTextNode(`)`),
|
|
3722
|
+
_el$144 = _$createElement("text");
|
|
3723
|
+
_$insertNode(_el$138, _el$139);
|
|
3724
|
+
_$insertNode(_el$138, _el$140);
|
|
3725
|
+
_$insertNode(_el$138, _el$141);
|
|
3726
|
+
_$insertNode(_el$138, _el$144);
|
|
3727
|
+
_$setProp(_el$138, "flexDirection", "row");
|
|
3728
|
+
_$setProp(_el$138, "gap", 2);
|
|
3729
|
+
_$setProp(_el$139, "flexShrink", 0);
|
|
3730
|
+
_$insert(_el$139, () => money(row.window));
|
|
3731
|
+
_$setProp(_el$140, "flexShrink", 0);
|
|
3732
|
+
_$insert(_el$140, () => money(row.lifetime));
|
|
3733
|
+
_$insertNode(_el$141, _el$142);
|
|
3734
|
+
_$insertNode(_el$141, _el$143);
|
|
3735
|
+
_$setProp(_el$141, "flexShrink", 0);
|
|
3736
|
+
_$insert(_el$141, () => row.sessions, _el$143);
|
|
3737
|
+
_$setProp(_el$144, "flexGrow", 1);
|
|
3738
|
+
_$setProp(_el$144, "flexShrink", 1);
|
|
3739
|
+
_$setProp(_el$144, "wrapMode", "none");
|
|
3740
|
+
_$insert(_el$144, () => prettyDir(row.directory, home));
|
|
3741
|
+
_$effect(_p$ => {
|
|
3742
|
+
var _v$72 = {
|
|
3743
|
+
fg: api.theme.current.success
|
|
3744
|
+
},
|
|
3745
|
+
_v$73 = {
|
|
3746
|
+
fg: api.theme.current.textMuted
|
|
3747
|
+
},
|
|
3748
|
+
_v$74 = {
|
|
3749
|
+
fg: api.theme.current.textMuted
|
|
3750
|
+
};
|
|
3751
|
+
_v$72 !== _p$.e && (_p$.e = _$setProp(_el$139, "style", _v$72, _p$.e));
|
|
3752
|
+
_v$73 !== _p$.t && (_p$.t = _$setProp(_el$140, "style", _v$73, _p$.t));
|
|
3753
|
+
_v$74 !== _p$.a && (_p$.a = _$setProp(_el$141, "style", _v$74, _p$.a));
|
|
3754
|
+
return _p$;
|
|
3755
|
+
}, {
|
|
3756
|
+
e: undefined,
|
|
3757
|
+
t: undefined,
|
|
3758
|
+
a: undefined
|
|
3759
|
+
});
|
|
3760
|
+
return _el$138;
|
|
3761
|
+
})()
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
}), _el$134);
|
|
3765
|
+
_$insertNode(_el$134, _el$135);
|
|
3766
|
+
_$setProp(_el$134, "flexShrink", 0);
|
|
3767
|
+
_$insertNode(_el$135, _$createTextNode(`esc close`));
|
|
3768
|
+
_$effect(_p$ => {
|
|
3769
|
+
var _v$68 = api.theme.current.backgroundPanel,
|
|
3770
|
+
_v$69 = TextAttributes.BOLD,
|
|
3771
|
+
_v$70 = {
|
|
3772
|
+
fg: api.theme.current.textMuted
|
|
3773
|
+
},
|
|
3774
|
+
_v$71 = {
|
|
3775
|
+
fg: api.theme.current.textMuted
|
|
3776
|
+
};
|
|
3777
|
+
_v$68 !== _p$.e && (_p$.e = _$setProp(_el$128, "backgroundColor", _v$68, _p$.e));
|
|
3778
|
+
_v$69 !== _p$.t && (_p$.t = _$setProp(_el$129, "attributes", _v$69, _p$.t));
|
|
3779
|
+
_v$70 !== _p$.a && (_p$.a = _$setProp(_el$132, "style", _v$70, _p$.a));
|
|
3780
|
+
_v$71 !== _p$.o && (_p$.o = _$setProp(_el$135, "style", _v$71, _p$.o));
|
|
3781
|
+
return _p$;
|
|
3782
|
+
}, {
|
|
3783
|
+
e: undefined,
|
|
3784
|
+
t: undefined,
|
|
3785
|
+
a: undefined,
|
|
3786
|
+
o: undefined
|
|
3787
|
+
});
|
|
3788
|
+
return _el$128;
|
|
3789
|
+
})(), () => {
|
|
3790
|
+
disposeKeys();
|
|
3791
|
+
});
|
|
3792
|
+
api.ui.dialog.setSize("large");
|
|
3793
|
+
};
|
|
3794
|
+
|
|
3795
|
+
// Triage dialog: the waiting set as an openable list. Enter opens the
|
|
3796
|
+
// highlighted session; the sidebar section stays the always-visible view.
|
|
3797
|
+
let lastNeedsID;
|
|
3798
|
+
const openNeeds = async (command = false) => {
|
|
3799
|
+
if (!command && api.mode.current() !== BASE_MODE) return;
|
|
3800
|
+
if (!command && api.renderer.currentFocusedEditor === null) return;
|
|
3801
|
+
let items = [];
|
|
3802
|
+
try {
|
|
3803
|
+
const {
|
|
3804
|
+
entries
|
|
3805
|
+
} = await fetchEntries(api);
|
|
3806
|
+
const db = await openTranscriptDb();
|
|
3807
|
+
if (!db) {
|
|
3808
|
+
api.ui.toast({
|
|
3809
|
+
message: "Needs-input data unavailable",
|
|
3810
|
+
variant: "error"
|
|
3811
|
+
});
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
let waiting;
|
|
3815
|
+
try {
|
|
3816
|
+
waiting = await queryWaitingDetails(db);
|
|
3817
|
+
} finally {
|
|
3818
|
+
db.close();
|
|
3819
|
+
}
|
|
3820
|
+
items = entries.filter(entry => waiting.has(entry.id)).map(entry => ({
|
|
3821
|
+
entry,
|
|
3822
|
+
reason: waiting.get(entry.id)?.reason === "question" ? "awaiting answer" : "run stuck",
|
|
3823
|
+
since: waiting.get(entry.id)?.since
|
|
3824
|
+
})).sort((a, b) => (a.since ?? a.entry.updated) - (b.since ?? b.entry.updated));
|
|
3825
|
+
} catch {
|
|
3826
|
+
api.ui.toast({
|
|
3827
|
+
message: "Could not load sessions",
|
|
3828
|
+
variant: "error"
|
|
3829
|
+
});
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
if (items.length === 0) {
|
|
3833
|
+
api.ui.toast({
|
|
3834
|
+
message: "No sessions are waiting on you.",
|
|
3835
|
+
variant: "info"
|
|
3836
|
+
});
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
const route = api.route.current;
|
|
3840
|
+
const currentID = route.name === "session" ? route.params?.sessionID : undefined;
|
|
3841
|
+
const previous = items.findIndex(item => item.entry.id === (currentID ?? lastNeedsID));
|
|
3842
|
+
const [cursor, setCursor] = createSignal(previous < 0 ? 0 : (previous + 1) % items.length);
|
|
3843
|
+
const moveCursor = delta => setCursor(value => Math.max(0, Math.min(items.length - 1, value + delta)));
|
|
3844
|
+
const choose = (target = items[cursor()]) => {
|
|
3845
|
+
if (!target) return;
|
|
3846
|
+
lastNeedsID = target.entry.id;
|
|
3847
|
+
disposeNav();
|
|
3848
|
+
api.ui.dialog.clear();
|
|
3849
|
+
api.route.navigate("session", {
|
|
3850
|
+
sessionID: target.entry.id
|
|
3851
|
+
});
|
|
3852
|
+
};
|
|
3853
|
+
const disposeNav = api.keymap.registerLayer({
|
|
3854
|
+
mode: "modal",
|
|
3855
|
+
bindings: [{
|
|
3856
|
+
key: "up",
|
|
3857
|
+
desc: "Previous session",
|
|
3858
|
+
preventDefault: true,
|
|
3859
|
+
cmd: () => moveCursor(-1)
|
|
3860
|
+
}, {
|
|
3861
|
+
key: "down",
|
|
3862
|
+
desc: "Next session",
|
|
3863
|
+
preventDefault: true,
|
|
3864
|
+
cmd: () => moveCursor(1)
|
|
3865
|
+
}, {
|
|
3866
|
+
key: "enter",
|
|
3867
|
+
desc: "Open session",
|
|
3868
|
+
preventDefault: true,
|
|
3869
|
+
cmd: () => choose()
|
|
3870
|
+
}, {
|
|
3871
|
+
key: "n",
|
|
3872
|
+
desc: "Open next waiting session",
|
|
3873
|
+
preventDefault: true,
|
|
3874
|
+
cmd: () => {
|
|
3875
|
+
const at = items.findIndex(item => item.entry.id === currentID);
|
|
3876
|
+
choose(items[at < 0 ? cursor() : (at + 1) % items.length]);
|
|
3877
|
+
}
|
|
3878
|
+
}, {
|
|
3879
|
+
key: "escape",
|
|
3880
|
+
desc: "Close",
|
|
3881
|
+
preventDefault: true,
|
|
3882
|
+
cmd: () => api.ui.dialog.clear()
|
|
3883
|
+
}]
|
|
3884
|
+
});
|
|
3885
|
+
const home = process.env.HOME ?? "";
|
|
3886
|
+
api.ui.dialog.replace(() => (() => {
|
|
3887
|
+
var _el$145 = _$createElement("box"),
|
|
3888
|
+
_el$146 = _$createElement("text"),
|
|
3889
|
+
_el$147 = _$createTextNode(`Needs input `),
|
|
3890
|
+
_el$149 = _$createElement("span"),
|
|
3891
|
+
_el$150 = _$createTextNode(`(`),
|
|
3892
|
+
_el$151 = _$createTextNode(` · longest waiting first)`),
|
|
3893
|
+
_el$152 = _$createElement("scrollbox"),
|
|
3894
|
+
_el$153 = _$createElement("box"),
|
|
3895
|
+
_el$154 = _$createElement("text");
|
|
3896
|
+
_$insertNode(_el$145, _el$146);
|
|
3897
|
+
_$insertNode(_el$145, _el$152);
|
|
3898
|
+
_$insertNode(_el$145, _el$153);
|
|
3899
|
+
_$setProp(_el$145, "flexDirection", "column");
|
|
3900
|
+
_$setProp(_el$145, "paddingLeft", 4);
|
|
3901
|
+
_$setProp(_el$145, "paddingRight", 4);
|
|
3902
|
+
_$setProp(_el$145, "paddingTop", 1);
|
|
3903
|
+
_$setProp(_el$145, "gap", 1);
|
|
3904
|
+
_$insertNode(_el$146, _el$147);
|
|
3905
|
+
_$insertNode(_el$146, _el$149);
|
|
3906
|
+
_$insertNode(_el$149, _el$150);
|
|
3907
|
+
_$insertNode(_el$149, _el$151);
|
|
3908
|
+
_$insert(_el$149, () => items.length, _el$151);
|
|
3909
|
+
_$setProp(_el$152, "flexGrow", 1);
|
|
3910
|
+
_$setProp(_el$152, "scrollbarOptions", {
|
|
3911
|
+
visible: false
|
|
3912
|
+
});
|
|
3913
|
+
_$insert(_el$152, _$createComponent(For, {
|
|
3914
|
+
each: items,
|
|
3915
|
+
children: (item, index) => (() => {
|
|
3916
|
+
var _el$156 = _$createElement("box"),
|
|
3917
|
+
_el$157 = _$createElement("text"),
|
|
3918
|
+
_el$158 = _$createElement("text"),
|
|
3919
|
+
_el$159 = _$createElement("text"),
|
|
3920
|
+
_el$160 = _$createTextNode(` · `);
|
|
3921
|
+
_$insertNode(_el$156, _el$157);
|
|
3922
|
+
_$insertNode(_el$156, _el$158);
|
|
3923
|
+
_$insertNode(_el$156, _el$159);
|
|
3924
|
+
_$setProp(_el$156, "flexDirection", "row");
|
|
3925
|
+
_$setProp(_el$156, "gap", 2);
|
|
3926
|
+
_$setProp(_el$156, "paddingLeft", 1);
|
|
3927
|
+
_$setProp(_el$156, "paddingRight", 1);
|
|
3928
|
+
_$setProp(_el$156, "onMouseDown", () => {
|
|
3929
|
+
setCursor(index());
|
|
3930
|
+
choose(item);
|
|
3931
|
+
});
|
|
3932
|
+
_$setProp(_el$157, "flexGrow", 1);
|
|
3933
|
+
_$setProp(_el$157, "flexShrink", 1);
|
|
3934
|
+
_$setProp(_el$157, "wrapMode", "none");
|
|
3935
|
+
_$insert(_el$157, () => truncate(item.entry.title, 48));
|
|
3936
|
+
_$setProp(_el$158, "flexShrink", 0);
|
|
3937
|
+
_$insert(_el$158, () => item.reason);
|
|
3938
|
+
_$insertNode(_el$159, _el$160);
|
|
3939
|
+
_$setProp(_el$159, "flexShrink", 0);
|
|
3940
|
+
_$insert(_el$159, () => prettyDir(item.entry.dir, home), _el$160);
|
|
3941
|
+
_$insert(_el$159, (() => {
|
|
3942
|
+
var _c$1 = _$memo(() => item.since === undefined);
|
|
3943
|
+
return () => _c$1() ? "time unknown" : `waiting ${ago(item.since)}`;
|
|
3944
|
+
})(), null);
|
|
3945
|
+
_$effect(_p$ => {
|
|
3946
|
+
var _v$80 = index() === cursor() ? api.theme.current.backgroundElement : RGBA.fromInts(0, 0, 0, 0),
|
|
3947
|
+
_v$81 = {
|
|
3948
|
+
fg: index() === cursor() ? api.theme.current.text : api.theme.current.text
|
|
3949
|
+
},
|
|
3950
|
+
_v$82 = {
|
|
3951
|
+
fg: index() === cursor() ? api.theme.current.text : api.theme.current.warning
|
|
3952
|
+
},
|
|
3953
|
+
_v$83 = {
|
|
3954
|
+
fg: index() === cursor() ? api.theme.current.text : api.theme.current.textMuted
|
|
3955
|
+
};
|
|
3956
|
+
_v$80 !== _p$.e && (_p$.e = _$setProp(_el$156, "backgroundColor", _v$80, _p$.e));
|
|
3957
|
+
_v$81 !== _p$.t && (_p$.t = _$setProp(_el$157, "style", _v$81, _p$.t));
|
|
3958
|
+
_v$82 !== _p$.a && (_p$.a = _$setProp(_el$158, "style", _v$82, _p$.a));
|
|
3959
|
+
_v$83 !== _p$.o && (_p$.o = _$setProp(_el$159, "style", _v$83, _p$.o));
|
|
3960
|
+
return _p$;
|
|
3961
|
+
}, {
|
|
3962
|
+
e: undefined,
|
|
3963
|
+
t: undefined,
|
|
3964
|
+
a: undefined,
|
|
3965
|
+
o: undefined
|
|
3966
|
+
});
|
|
3967
|
+
return _el$156;
|
|
3968
|
+
})()
|
|
3969
|
+
}));
|
|
3970
|
+
_$insertNode(_el$153, _el$154);
|
|
3971
|
+
_$setProp(_el$153, "flexShrink", 0);
|
|
3972
|
+
_$insertNode(_el$154, _$createTextNode(`↑↓ navigate · enter open · n open next waiting · esc close`));
|
|
3973
|
+
_$effect(_p$ => {
|
|
3974
|
+
var _v$75 = api.theme.current.backgroundPanel,
|
|
3975
|
+
_v$76 = TextAttributes.BOLD,
|
|
3976
|
+
_v$77 = {
|
|
3977
|
+
fg: api.theme.current.textMuted
|
|
3978
|
+
},
|
|
3979
|
+
_v$78 = Math.max(6, Math.floor(api.renderer.height / 2) - 10),
|
|
3980
|
+
_v$79 = {
|
|
3981
|
+
fg: api.theme.current.textMuted
|
|
3982
|
+
};
|
|
3983
|
+
_v$75 !== _p$.e && (_p$.e = _$setProp(_el$145, "backgroundColor", _v$75, _p$.e));
|
|
3984
|
+
_v$76 !== _p$.t && (_p$.t = _$setProp(_el$146, "attributes", _v$76, _p$.t));
|
|
3985
|
+
_v$77 !== _p$.a && (_p$.a = _$setProp(_el$149, "style", _v$77, _p$.a));
|
|
3986
|
+
_v$78 !== _p$.o && (_p$.o = _$setProp(_el$152, "height", _v$78, _p$.o));
|
|
3987
|
+
_v$79 !== _p$.i && (_p$.i = _$setProp(_el$154, "style", _v$79, _p$.i));
|
|
3988
|
+
return _p$;
|
|
3989
|
+
}, {
|
|
3990
|
+
e: undefined,
|
|
3991
|
+
t: undefined,
|
|
3992
|
+
a: undefined,
|
|
3993
|
+
o: undefined,
|
|
3994
|
+
i: undefined
|
|
3995
|
+
});
|
|
3996
|
+
return _el$145;
|
|
3997
|
+
})(), () => {
|
|
3998
|
+
disposeNav();
|
|
3999
|
+
});
|
|
4000
|
+
api.ui.dialog.setSize("large");
|
|
4001
|
+
};
|
|
4002
|
+
api.command?.register(() => [{
|
|
4003
|
+
title: "Pick session (all directories)",
|
|
4004
|
+
value: "sesh.pick",
|
|
4005
|
+
description: "Switch to any session across all project directories",
|
|
4006
|
+
category: "Sessions",
|
|
4007
|
+
slash: {
|
|
4008
|
+
name: "sesh"
|
|
4009
|
+
},
|
|
4010
|
+
onSelect: () => {
|
|
4011
|
+
void openPicker(true);
|
|
4012
|
+
}
|
|
4013
|
+
}, {
|
|
4014
|
+
title: "Cost digest (per project)",
|
|
4015
|
+
value: "sesh.costs",
|
|
4016
|
+
description: "Spend per project, last day and lifetime",
|
|
4017
|
+
category: "Sessions",
|
|
4018
|
+
slash: {
|
|
4019
|
+
name: "sesh-costs"
|
|
4020
|
+
},
|
|
4021
|
+
onSelect: () => {
|
|
4022
|
+
void openCosts(true);
|
|
4023
|
+
}
|
|
4024
|
+
}, {
|
|
4025
|
+
title: "Sessions waiting on you",
|
|
4026
|
+
value: "sesh.needs",
|
|
4027
|
+
description: "Unanswered questions and stuck runs",
|
|
4028
|
+
category: "Sessions",
|
|
4029
|
+
slash: {
|
|
4030
|
+
name: "sesh-needs"
|
|
4031
|
+
},
|
|
4032
|
+
onSelect: () => {
|
|
4033
|
+
void openNeeds(true);
|
|
4034
|
+
}
|
|
4035
|
+
}]);
|
|
4036
|
+
api.keymap.registerLayer({
|
|
4037
|
+
bindings: [{
|
|
4038
|
+
key: "alt+o",
|
|
4039
|
+
desc: "Pick session",
|
|
4040
|
+
preventDefault: true,
|
|
4041
|
+
cmd: () => void openPicker()
|
|
4042
|
+
}]
|
|
4043
|
+
});
|
|
4044
|
+
};
|
|
4045
|
+
let hostOwner = null;
|
|
4046
|
+
function adaptTheme(theme) {
|
|
4047
|
+
const opaque = color => RGBA.fromValues(color.r, color.g, color.b, 1);
|
|
4048
|
+
return {
|
|
4049
|
+
text: theme.text.base,
|
|
4050
|
+
textMuted: theme.text.muted,
|
|
4051
|
+
background: theme.background.base,
|
|
4052
|
+
backgroundPanel: opaque(theme.background.raised.base),
|
|
4053
|
+
backgroundElement: opaque(theme.background.raised.high),
|
|
4054
|
+
border: theme.border.base,
|
|
4055
|
+
borderActive: theme.hue.accent[200],
|
|
4056
|
+
borderSubtle: theme.border.base,
|
|
4057
|
+
accent: theme.hue.accent[200],
|
|
4058
|
+
primary: theme.hue.accent[200],
|
|
4059
|
+
secondary: theme.text.action.secondary.base,
|
|
4060
|
+
error: theme.text.feedback.error.base,
|
|
4061
|
+
warning: theme.text.feedback.warning.base,
|
|
4062
|
+
success: theme.text.feedback.success.base,
|
|
4063
|
+
info: theme.text.feedback.info.base,
|
|
4064
|
+
selectedListItemText: theme.text.base,
|
|
4065
|
+
syntaxComment: theme.syntax.comment,
|
|
4066
|
+
syntaxString: theme.syntax.string,
|
|
4067
|
+
syntaxNumber: theme.syntax.number,
|
|
4068
|
+
syntaxKeyword: theme.syntax.keyword,
|
|
4069
|
+
syntaxType: theme.syntax.type,
|
|
4070
|
+
syntaxFunction: theme.syntax.function,
|
|
4071
|
+
syntaxOperator: theme.syntax.operator,
|
|
4072
|
+
syntaxVariable: theme.syntax.variable,
|
|
4073
|
+
syntaxPunctuation: theme.syntax.punctuation,
|
|
4074
|
+
markdownLink: theme.markdown.link,
|
|
4075
|
+
markdownLinkText: theme.markdown.linkText,
|
|
4076
|
+
markdownBlockQuote: theme.markdown.blockQuote
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
function createApi(ctx) {
|
|
4080
|
+
const [dialogOpen, setDialogOpen] = createSignal(false);
|
|
4081
|
+
let sourceTheme;
|
|
4082
|
+
let currentTheme;
|
|
4083
|
+
return {
|
|
4084
|
+
theme: {
|
|
4085
|
+
get current() {
|
|
4086
|
+
if (sourceTheme !== ctx.theme) {
|
|
4087
|
+
sourceTheme = ctx.theme;
|
|
4088
|
+
currentTheme = adaptTheme(sourceTheme);
|
|
4089
|
+
}
|
|
4090
|
+
return currentTheme;
|
|
4091
|
+
}
|
|
4092
|
+
},
|
|
4093
|
+
ui: {
|
|
4094
|
+
toast(opts) {
|
|
4095
|
+
ctx.ui.toast.show({
|
|
4096
|
+
title: opts.title ?? "Sessions",
|
|
4097
|
+
message: opts.message,
|
|
4098
|
+
variant: opts.variant ?? "info"
|
|
4099
|
+
});
|
|
4100
|
+
},
|
|
4101
|
+
dialog: {
|
|
4102
|
+
clear() {
|
|
4103
|
+
ctx.ui.dialog.clear();
|
|
4104
|
+
setDialogOpen(false);
|
|
4105
|
+
},
|
|
4106
|
+
replace(render, onClose) {
|
|
4107
|
+
let closed = false;
|
|
4108
|
+
ctx.ui.dialog.show(render, () => {
|
|
4109
|
+
if (closed) return;
|
|
4110
|
+
closed = true;
|
|
4111
|
+
setDialogOpen(false);
|
|
4112
|
+
onClose?.();
|
|
4113
|
+
});
|
|
4114
|
+
setDialogOpen(true);
|
|
4115
|
+
},
|
|
4116
|
+
setSize(size) {
|
|
4117
|
+
ctx.ui.dialog.set({
|
|
4118
|
+
size
|
|
4119
|
+
});
|
|
4120
|
+
},
|
|
4121
|
+
// V2's Dialog has no reactive open/isOpen accessor; track it locally
|
|
4122
|
+
// from the replace/clear calls above and the host's onClose callback.
|
|
4123
|
+
get open() {
|
|
4124
|
+
return dialogOpen();
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
},
|
|
4128
|
+
route: {
|
|
4129
|
+
get current() {
|
|
4130
|
+
const r = ctx.ui.router.current();
|
|
4131
|
+
if (r.type === "session") {
|
|
4132
|
+
return {
|
|
4133
|
+
name: "session",
|
|
4134
|
+
params: {
|
|
4135
|
+
sessionID: r.sessionID
|
|
4136
|
+
}
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
return {
|
|
4140
|
+
name: r.type,
|
|
4141
|
+
params: {}
|
|
4142
|
+
};
|
|
4143
|
+
},
|
|
4144
|
+
navigate(name, params) {
|
|
4145
|
+
if (name === "session" && params?.sessionID) {
|
|
4146
|
+
ctx.ui.router.navigate({
|
|
4147
|
+
type: "session",
|
|
4148
|
+
sessionID: params.sessionID
|
|
4149
|
+
});
|
|
4150
|
+
} else if (name === "home") {
|
|
4151
|
+
ctx.ui.router.navigate({
|
|
4152
|
+
type: "home"
|
|
4153
|
+
});
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
},
|
|
4157
|
+
state: {
|
|
4158
|
+
path: {
|
|
4159
|
+
get directory() {
|
|
4160
|
+
return ctx.location?.directory ?? process.cwd();
|
|
4161
|
+
}
|
|
4162
|
+
},
|
|
4163
|
+
session: {
|
|
4164
|
+
status(id) {
|
|
4165
|
+
return ctx.data.session.status(id);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
},
|
|
4169
|
+
mode: {
|
|
4170
|
+
current() {
|
|
4171
|
+
return ctx.keymap.mode.current();
|
|
4172
|
+
}
|
|
4173
|
+
},
|
|
4174
|
+
renderer: {
|
|
4175
|
+
get height() {
|
|
4176
|
+
return ctx.renderer?.height ?? 40;
|
|
4177
|
+
},
|
|
4178
|
+
get width() {
|
|
4179
|
+
return ctx.renderer?.width ?? 120;
|
|
4180
|
+
},
|
|
4181
|
+
get root() {
|
|
4182
|
+
return ctx.renderer.root;
|
|
4183
|
+
},
|
|
4184
|
+
get currentFocusedEditor() {
|
|
4185
|
+
return ctx.renderer.currentFocusedEditor;
|
|
4186
|
+
},
|
|
4187
|
+
get nativeSession() {
|
|
4188
|
+
return ctx.renderer?.nativeSession;
|
|
4189
|
+
},
|
|
4190
|
+
on(event, handler) {
|
|
4191
|
+
ctx.renderer.on(event, handler);
|
|
4192
|
+
},
|
|
4193
|
+
off(event, handler) {
|
|
4194
|
+
ctx.renderer.off(event, handler);
|
|
4195
|
+
}
|
|
4196
|
+
},
|
|
4197
|
+
keymap: {
|
|
4198
|
+
registerLayer(layer) {
|
|
4199
|
+
return runWithOwner(hostOwner, () => createRoot(dispose => {
|
|
4200
|
+
ctx.keymap.layer(() => ({
|
|
4201
|
+
mode: layer.mode ?? "base",
|
|
4202
|
+
priority: layer.priority ?? 20,
|
|
4203
|
+
commands: (layer.bindings ?? []).map(b => ({
|
|
4204
|
+
title: b.desc,
|
|
4205
|
+
bind: b.key,
|
|
4206
|
+
run: () => {
|
|
4207
|
+
b.cmd();
|
|
4208
|
+
}
|
|
4209
|
+
}))
|
|
4210
|
+
}));
|
|
4211
|
+
return dispose;
|
|
4212
|
+
}));
|
|
4213
|
+
}
|
|
4214
|
+
},
|
|
4215
|
+
slots: {
|
|
4216
|
+
register(cfg) {
|
|
4217
|
+
if (cfg.slots.sidebar_content) {
|
|
4218
|
+
ctx.ui.slot({
|
|
4219
|
+
prepend: "sidebar.content",
|
|
4220
|
+
render: cfg.slots.sidebar_content
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
if (cfg.slots.home_bottom) {
|
|
4224
|
+
ctx.ui.slot({
|
|
4225
|
+
prepend: "home.footer",
|
|
4226
|
+
render: cfg.slots.home_bottom
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
},
|
|
4231
|
+
command: {
|
|
4232
|
+
register(factory) {
|
|
4233
|
+
ctx.keymap.layer(() => ({
|
|
4234
|
+
mode: "global",
|
|
4235
|
+
priority: 20,
|
|
4236
|
+
commands: factory().map(cmd => ({
|
|
4237
|
+
id: cmd.value,
|
|
4238
|
+
title: cmd.title,
|
|
4239
|
+
description: cmd.description,
|
|
4240
|
+
group: cmd.category,
|
|
4241
|
+
palette: true,
|
|
4242
|
+
slash: cmd.slash ? {
|
|
4243
|
+
name: cmd.slash.name
|
|
4244
|
+
} : undefined,
|
|
4245
|
+
run: () => {
|
|
4246
|
+
cmd.onSelect();
|
|
4247
|
+
}
|
|
4248
|
+
}))
|
|
4249
|
+
}));
|
|
4250
|
+
}
|
|
4251
|
+
},
|
|
4252
|
+
client: {
|
|
4253
|
+
project: {
|
|
4254
|
+
async list() {
|
|
4255
|
+
const res = await ctx.client.project.list();
|
|
4256
|
+
return {
|
|
4257
|
+
data: Array.isArray(res) ? res : res?.data ?? []
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
},
|
|
4261
|
+
session: {
|
|
4262
|
+
async delete(req) {
|
|
4263
|
+
await ctx.client.session.remove({
|
|
4264
|
+
sessionID: req.sessionID
|
|
4265
|
+
});
|
|
4266
|
+
return {
|
|
4267
|
+
data: undefined
|
|
4268
|
+
};
|
|
4269
|
+
},
|
|
4270
|
+
async fork(req) {
|
|
4271
|
+
const res = await ctx.client.session.fork({
|
|
4272
|
+
sessionID: req.sessionID
|
|
4273
|
+
});
|
|
4274
|
+
return {
|
|
4275
|
+
data: res
|
|
4276
|
+
};
|
|
4277
|
+
},
|
|
4278
|
+
async messages(req) {
|
|
4279
|
+
let cursor;
|
|
4280
|
+
const all = [];
|
|
4281
|
+
do {
|
|
4282
|
+
const res = await ctx.client.message.list({
|
|
4283
|
+
sessionID: req.sessionID,
|
|
4284
|
+
order: req.preview ? "desc" : "asc",
|
|
4285
|
+
limit: 100,
|
|
4286
|
+
cursor
|
|
4287
|
+
});
|
|
4288
|
+
all.push(...res.data);
|
|
4289
|
+
const next = res.cursor.next;
|
|
4290
|
+
if (req.preview || !next || next === cursor) break;
|
|
4291
|
+
cursor = next;
|
|
4292
|
+
} while (true);
|
|
4293
|
+
if (req.preview) all.reverse();
|
|
4294
|
+
const messages = all.map(m => {
|
|
4295
|
+
let text = "";
|
|
4296
|
+
if (m.type === "user") text = m.text ?? "";else if (m.type === "assistant") {
|
|
4297
|
+
text = (m.content ?? []).filter(c => c.type === "text").map(c => c.text).join("\n");
|
|
4298
|
+
}
|
|
4299
|
+
return {
|
|
4300
|
+
id: m.id,
|
|
4301
|
+
info: {
|
|
4302
|
+
role: m.type
|
|
4303
|
+
},
|
|
4304
|
+
parts: [{
|
|
4305
|
+
type: "text",
|
|
4306
|
+
text
|
|
4307
|
+
}]
|
|
4308
|
+
};
|
|
4309
|
+
});
|
|
4310
|
+
return {
|
|
4311
|
+
data: messages
|
|
4312
|
+
};
|
|
4313
|
+
}
|
|
4314
|
+
},
|
|
4315
|
+
experimental: {
|
|
4316
|
+
session: {
|
|
4317
|
+
async list(input) {
|
|
4318
|
+
const res = await ctx.client.session.list({
|
|
4319
|
+
parentID: null,
|
|
4320
|
+
order: "desc",
|
|
4321
|
+
limit: input.limit,
|
|
4322
|
+
cursor: input.cursor
|
|
4323
|
+
});
|
|
4324
|
+
return {
|
|
4325
|
+
cursor: res.cursor,
|
|
4326
|
+
data: res.data.map(s => ({
|
|
4327
|
+
...s,
|
|
4328
|
+
directory: s.location?.directory ?? ""
|
|
4329
|
+
}))
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
export default Plugin.define({
|
|
4338
|
+
id: "sesh-panel",
|
|
4339
|
+
async setup(ctx) {
|
|
4340
|
+
const api = createApi(ctx);
|
|
4341
|
+
let initialized = false;
|
|
4342
|
+
const disposeSlot = ctx.ui.slot({
|
|
4343
|
+
append: "app",
|
|
4344
|
+
render: () => {
|
|
4345
|
+
if (!initialized) {
|
|
4346
|
+
initialized = true;
|
|
4347
|
+
hostOwner = getOwner();
|
|
4348
|
+
void tui(api);
|
|
4349
|
+
}
|
|
4350
|
+
return _$createElement("box");
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
return () => {
|
|
4354
|
+
if (api.ui.dialog.open) api.ui.dialog.clear();
|
|
4355
|
+
disposeSlot();
|
|
4356
|
+
removeRootMouse?.();
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
});
|