tmux-ide 2.9.0-beta.17 → 2.9.0-beta.18
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/bin/cli.js +11 -8
- package/package.json +1 -1
- package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +9 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +30 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +35 -78
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +22 -9
- package/packages/daemon/dist/tui/mirror/ui/keyboard-router.jsx +22 -0
- package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +25 -2
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +23 -5
- package/packages/daemon/dist/tui/team/fuzzy.js +36 -15
- package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +12 -10
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +35 -2
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +87 -113
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -92
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +8 -1
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +32 -5
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +34 -7
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +10 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +16 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +178 -82
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -0
- package/packages/daemon/src/tui/mirror/ui/keyboard-router.tsx +22 -0
- package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +30 -2
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +29 -6
- package/packages/daemon/src/tui/team/fuzzy.ts +38 -14
- package/packages/daemon/dist/tui/mirror/runtime/application-fleet-switcher.jsx +0 -88
package/bin/cli.js
CHANGED
|
@@ -10272,7 +10272,7 @@ var require_package = __commonJS({
|
|
|
10272
10272
|
"package.json"(exports, module) {
|
|
10273
10273
|
module.exports = {
|
|
10274
10274
|
name: "tmux-ide",
|
|
10275
|
-
version: "2.9.0-beta.
|
|
10275
|
+
version: "2.9.0-beta.18",
|
|
10276
10276
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
10277
10277
|
type: "module",
|
|
10278
10278
|
bin: {
|
|
@@ -18446,12 +18446,14 @@ function createFleetPreviewCapture(run) {
|
|
|
18446
18446
|
],
|
|
18447
18447
|
signal
|
|
18448
18448
|
)).trim().split("\n").map((line) => line.split(" "));
|
|
18449
|
-
const rows = await readPanes();
|
|
18450
18449
|
const names = /* @__PURE__ */ new Map();
|
|
18451
|
-
const windowNames = await
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18450
|
+
const [rows, windowNames] = await Promise.all([
|
|
18451
|
+
readPanes(),
|
|
18452
|
+
run(
|
|
18453
|
+
["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id} #{window_name}"],
|
|
18454
|
+
signal
|
|
18455
|
+
)
|
|
18456
|
+
]);
|
|
18455
18457
|
for (const line of windowNames.split("\n")) {
|
|
18456
18458
|
const tab = line.indexOf(" ");
|
|
18457
18459
|
if (tab > 0)
|
|
@@ -18498,8 +18500,9 @@ function createFleetPreviewCapture(run) {
|
|
|
18498
18500
|
const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
|
|
18499
18501
|
if (!pane || !/^%\d+$/u.test(pane)) return null;
|
|
18500
18502
|
const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
|
|
18501
|
-
|
|
18502
|
-
if (
|
|
18503
|
+
const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
|
|
18504
|
+
if (!currentSessions.some((s) => s.liveSessionId === liveSessionId2)) return null;
|
|
18505
|
+
if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
|
|
18503
18506
|
return null;
|
|
18504
18507
|
return {
|
|
18505
18508
|
windows,
|
package/package.json
CHANGED
|
@@ -24,11 +24,15 @@ export function createFleetPreviewCapture(run) {
|
|
|
24
24
|
.trim()
|
|
25
25
|
.split("\n")
|
|
26
26
|
.map((line) => line.split("\t"));
|
|
27
|
-
const rows = await readPanes();
|
|
28
27
|
// Names are untrusted display text. Keep them out of the pane membership
|
|
29
28
|
// format so embedded newlines cannot manufacture a capture target.
|
|
30
29
|
const names = new Map();
|
|
31
|
-
|
|
30
|
+
// Independent metadata reads share one capture's deadline and run in two
|
|
31
|
+
// bounded lanes. Incarnation checks still surround the actual capture.
|
|
32
|
+
const [rows, windowNames] = await Promise.all([
|
|
33
|
+
readPanes(),
|
|
34
|
+
run(["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id}\t#{window_name}"], signal),
|
|
35
|
+
]);
|
|
32
36
|
for (const line of windowNames.split("\n")) {
|
|
33
37
|
const tab = line.indexOf("\t");
|
|
34
38
|
if (tab > 0)
|
|
@@ -81,10 +85,10 @@ export function createFleetPreviewCapture(run) {
|
|
|
81
85
|
return null;
|
|
82
86
|
// Passive capture never changes active windows, size, input or terminal ownership.
|
|
83
87
|
const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
|
|
84
|
-
|
|
88
|
+
const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
|
|
89
|
+
if (!currentSessions.some((s) => s.liveSessionId === liveSessionId))
|
|
85
90
|
return null;
|
|
86
|
-
if (selectedWindowId &&
|
|
87
|
-
!(await readPanes()).some((r) => r[3] === selectedWindowId && r[0] === pane))
|
|
91
|
+
if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
|
|
88
92
|
return null;
|
|
89
93
|
return {
|
|
90
94
|
windows,
|
|
@@ -145,6 +145,32 @@ export function createFleetPreviewOwner(publish, delay = 180) {
|
|
|
145
145
|
dispose: clear,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
/** Short-lived, bounded memory only. Keys must include route, incarnation and transport epoch. */
|
|
149
|
+
export function createFleetPreviewCache(now = Date.now) {
|
|
150
|
+
const entries = new Map();
|
|
151
|
+
return {
|
|
152
|
+
get(key) {
|
|
153
|
+
const value = entries.get(key);
|
|
154
|
+
if (!value || now() - value.at > 30_000) {
|
|
155
|
+
entries.delete(key);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
entries.delete(key);
|
|
159
|
+
entries.set(key, value);
|
|
160
|
+
return value.snapshot;
|
|
161
|
+
},
|
|
162
|
+
set(key, snapshot) {
|
|
163
|
+
entries.delete(key);
|
|
164
|
+
entries.set(key, { snapshot, at: now() });
|
|
165
|
+
while (entries.size > 24)
|
|
166
|
+
entries.delete(entries.keys().next().value);
|
|
167
|
+
},
|
|
168
|
+
clear() {
|
|
169
|
+
entries.clear();
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export const fleetPreviewMemory = createFleetPreviewCache();
|
|
148
174
|
/** One selected request, scheduled after settlement; no hidden polling or terminal streams. */
|
|
149
175
|
export function createAdaptiveFleetPreviewOwner(publish, options = {}) {
|
|
150
176
|
let key;
|
|
@@ -197,6 +223,8 @@ export function createAdaptiveFleetPreviewOwner(publish, options = {}) {
|
|
|
197
223
|
return;
|
|
198
224
|
if (result.status === "ready") {
|
|
199
225
|
snapshot = result.snapshot;
|
|
226
|
+
if (key)
|
|
227
|
+
options.cache?.set(key, snapshot);
|
|
200
228
|
failures = 0;
|
|
201
229
|
emit({ status: "ready", snapshot, stale: false });
|
|
202
230
|
}
|
|
@@ -215,7 +243,8 @@ export function createAdaptiveFleetPreviewOwner(publish, options = {}) {
|
|
|
215
243
|
key = nextRead ? nextKey : undefined;
|
|
216
244
|
snapshot = null;
|
|
217
245
|
failures = 0;
|
|
218
|
-
|
|
246
|
+
snapshot = read && key ? (options.cache?.get(key) ?? null) : null;
|
|
247
|
+
emit({ status: snapshot ? "loading" : "idle", snapshot, stale: snapshot !== null });
|
|
219
248
|
if (!read)
|
|
220
249
|
return;
|
|
221
250
|
controller = new AbortController();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { fleetHostColor } from "./fleet-presentation.js";
|
|
2
2
|
import { createFleetTabs } from "./application-fleet-tabs.js";
|
|
3
|
-
import { readFleetPreview } from "./application-fleet-preview.js";
|
|
4
3
|
import { saveMachineProfiles } from "../../../lib/local-fleet-request.js";
|
|
5
4
|
import { createApplicationFleetPreferences } from "./application-fleet-preferences.js";
|
|
6
5
|
import { createApplicationMachineAgents, } from "./application-machine-agents.js";
|
|
@@ -244,28 +243,42 @@ export function createApplicationMachineNavigation(options) {
|
|
|
244
243
|
},
|
|
245
244
|
closeSwitcher: () => setSwitching(false),
|
|
246
245
|
paletteCommands() {
|
|
246
|
+
const groupsById = new Map(agentGroups().map((group) => [group.machineId, group]));
|
|
247
247
|
return snapshot().groups.flatMap((group) => {
|
|
248
|
+
const agentGroup = groupsById.get(group.id);
|
|
249
|
+
const agentsBySession = new Map();
|
|
250
|
+
for (const agent of agentGroup?.agents ?? []) {
|
|
251
|
+
if (!agent.liveSessionId || !agent.paneId)
|
|
252
|
+
continue;
|
|
253
|
+
const list = agentsBySession.get(agent.liveSessionId) ?? [];
|
|
254
|
+
list.push(agent);
|
|
255
|
+
agentsBySession.set(agent.liveSessionId, list);
|
|
256
|
+
}
|
|
248
257
|
const daemon = manager.getMachine(group.id)?.read();
|
|
249
258
|
const sessions = group.sessions.flatMap((session) => {
|
|
250
259
|
if (!session.liveSessionId)
|
|
251
260
|
return [];
|
|
252
261
|
const fleet = {
|
|
253
262
|
machineId: group.id,
|
|
263
|
+
favorite: saved().favorites.includes(session.id),
|
|
264
|
+
recentRank: saved().recent.includes(session.id)
|
|
265
|
+
? saved().recent.indexOf(session.id)
|
|
266
|
+
: 1000,
|
|
254
267
|
liveSessionId: session.liveSessionId,
|
|
255
268
|
hostLabel: group.label,
|
|
256
|
-
agentActivities:
|
|
257
|
-
? (
|
|
258
|
-
|
|
259
|
-
|
|
269
|
+
agentActivities: agentGroup?.available
|
|
270
|
+
? (agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
|
|
271
|
+
paneId: a.paneId,
|
|
272
|
+
attention: a.attention,
|
|
273
|
+
activity: a.activity,
|
|
274
|
+
}))
|
|
260
275
|
: undefined,
|
|
261
276
|
daemonInstanceId: daemon?.instanceId ?? "",
|
|
262
277
|
disabled: session.disabled || group.state !== "ready",
|
|
263
278
|
};
|
|
264
279
|
return [
|
|
265
280
|
{ kind: "open-session", sessionName: session.name, label: session.name, fleet },
|
|
266
|
-
...(
|
|
267
|
-
.filter((a) => a.liveSessionId === session.liveSessionId && a.paneId)
|
|
268
|
-
.map((a) => ({
|
|
281
|
+
...(agentsBySession.get(session.liveSessionId) ?? []).map((a) => ({
|
|
269
282
|
kind: "jump-agent",
|
|
270
283
|
sessionName: session.name,
|
|
271
284
|
paneId: a.paneId,
|
|
@@ -291,6 +304,20 @@ export function createApplicationMachineNavigation(options) {
|
|
|
291
304
|
];
|
|
292
305
|
});
|
|
293
306
|
},
|
|
307
|
+
togglePaletteFavorite(command) {
|
|
308
|
+
if (typeof command !== "object" || command.kind !== "open-session" || !command.fleet)
|
|
309
|
+
return;
|
|
310
|
+
const target = command.fleet;
|
|
311
|
+
const session = snapshot()
|
|
312
|
+
.groups.find((g) => g.id === target.machineId)
|
|
313
|
+
?.sessions.find((s) => s.liveSessionId === target.liveSessionId);
|
|
314
|
+
if (session)
|
|
315
|
+
preferences.change({
|
|
316
|
+
type: "favorite",
|
|
317
|
+
key: session.id,
|
|
318
|
+
enabled: !saved().favorites.includes(session.id),
|
|
319
|
+
});
|
|
320
|
+
},
|
|
294
321
|
async openPalette(command, source) {
|
|
295
322
|
const target = command.fleet;
|
|
296
323
|
if (!target)
|
|
@@ -317,76 +344,6 @@ export function createApplicationMachineNavigation(options) {
|
|
|
317
344
|
else
|
|
318
345
|
await open(target.machineId, session.name, source, true, target.liveSessionId);
|
|
319
346
|
},
|
|
320
|
-
switcherRows: () => {
|
|
321
|
-
const favorite = new Set(saved().favorites);
|
|
322
|
-
const recent = saved().recent;
|
|
323
|
-
const rows = snapshot().groups.flatMap((group) => [
|
|
324
|
-
{
|
|
325
|
-
key: `machine:${group.id}`,
|
|
326
|
-
label: group.label,
|
|
327
|
-
detail: `Machine · ${group.state}`,
|
|
328
|
-
favorite: false,
|
|
329
|
-
attention: false,
|
|
330
|
-
disabled: false,
|
|
331
|
-
canFavorite: false,
|
|
332
|
-
open: () => sidebar.onSelectMachine(group.id, "keyboard"),
|
|
333
|
-
toggleFavorite: () => { },
|
|
334
|
-
},
|
|
335
|
-
...group.sessions.map((session) => ({
|
|
336
|
-
key: session.id,
|
|
337
|
-
previewKey: JSON.stringify([
|
|
338
|
-
session.id,
|
|
339
|
-
manager.getMachine(group.id)?.endpoint().epoch,
|
|
340
|
-
]),
|
|
341
|
-
preview: session.liveSessionId
|
|
342
|
-
? (signal) => {
|
|
343
|
-
const handle = manager.getMachine(group.id);
|
|
344
|
-
return handle
|
|
345
|
-
? readFleetPreview(handle, session.liveSessionId, signal)
|
|
346
|
-
: Promise.resolve("Preview unavailable");
|
|
347
|
-
}
|
|
348
|
-
: undefined,
|
|
349
|
-
label: session.name,
|
|
350
|
-
detail: group.label,
|
|
351
|
-
favorite: favorite.has(session.id),
|
|
352
|
-
attention: false,
|
|
353
|
-
disabled: session.disabled,
|
|
354
|
-
canFavorite: true,
|
|
355
|
-
open: () => {
|
|
356
|
-
const current = catalog
|
|
357
|
-
.getSnapshot()
|
|
358
|
-
.groups.find((g) => g.id === group.id)
|
|
359
|
-
?.sessions.find((s) => s.id === session.id && !s.disabled);
|
|
360
|
-
if (current)
|
|
361
|
-
void open(group.id, current.name, "keyboard");
|
|
362
|
-
},
|
|
363
|
-
toggleFavorite: () => preferences.change({
|
|
364
|
-
type: "favorite",
|
|
365
|
-
key: session.id,
|
|
366
|
-
enabled: !favorite.has(session.id),
|
|
367
|
-
}),
|
|
368
|
-
})),
|
|
369
|
-
...(agentGroups().find((g) => g.machineId === group.id)?.agents ?? []).map((agent) => ({
|
|
370
|
-
key: agent.id,
|
|
371
|
-
label: agent.name,
|
|
372
|
-
detail: `${group.label} / ${agent.sessionName}`,
|
|
373
|
-
favorite: false,
|
|
374
|
-
attention: agent.attention && !agent.disabled,
|
|
375
|
-
disabled: agent.disabled || group.state !== "ready",
|
|
376
|
-
canFavorite: false,
|
|
377
|
-
open: () => {
|
|
378
|
-
if (agent.paneId)
|
|
379
|
-
sidebar.onOpenAgent?.(group.id, agent.sessionName, agent.paneId, "keyboard");
|
|
380
|
-
},
|
|
381
|
-
toggleFavorite: () => { },
|
|
382
|
-
})),
|
|
383
|
-
]);
|
|
384
|
-
return rows.sort((a, b) => Number(b.favorite) - Number(a.favorite) ||
|
|
385
|
-
Number(b.attention) - Number(a.attention) ||
|
|
386
|
-
(recent.includes(a.key) ? recent.indexOf(a.key) : 1000) -
|
|
387
|
-
(recent.includes(b.key) ? recent.indexOf(b.key) : 1000) ||
|
|
388
|
-
a.label.localeCompare(b.label));
|
|
389
|
-
},
|
|
390
347
|
adding,
|
|
391
348
|
alias,
|
|
392
349
|
error,
|
|
@@ -4,11 +4,16 @@ import { applicationPaletteKeyboardDisposition, } from "./application-palette-in
|
|
|
4
4
|
import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
|
|
5
5
|
/** Query/selection/input only. No physical listeners or terminal subscriptions. */
|
|
6
6
|
export function createApplicationPaletteSearchOwner(options) {
|
|
7
|
+
const [localOnly, setLocalOnly] = createSignal(false);
|
|
8
|
+
const [viewport, setViewport] = createSignal(10);
|
|
9
|
+
const pageSize = () => options.pageSize?.() ?? viewport();
|
|
7
10
|
const [normal, setNormal] = createSignal(false);
|
|
8
11
|
const [help, setHelp] = createSignal(false);
|
|
9
12
|
const [query, setQuery] = createSignal("");
|
|
10
13
|
const [selectedId, setSelectedId] = createSignal(null);
|
|
11
|
-
const commands = createMemo(() => filterApplicationCommands(options
|
|
14
|
+
const commands = createMemo(() => filterApplicationCommands(options
|
|
15
|
+
.commands()
|
|
16
|
+
.filter((c) => !localOnly() || typeof c === "string" || !c.fleet || c.fleet.machineId === "local"), query()));
|
|
12
17
|
const selection = () => Math.max(0, commands().findIndex((command) => applicationCommandDescription(command).id === selectedId()));
|
|
13
18
|
const select = (index) => {
|
|
14
19
|
const command = commands()[index];
|
|
@@ -28,11 +33,14 @@ export function createApplicationPaletteSearchOwner(options) {
|
|
|
28
33
|
};
|
|
29
34
|
return {
|
|
30
35
|
query,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
setViewport,
|
|
37
|
+
setQuery: updateQuery,
|
|
38
|
+
keyboardHint: () => (localOnly() ? "LOCAL ONLY · ^H all hosts · " : "") +
|
|
39
|
+
(help()
|
|
40
|
+
? "j/k g/G ^U/D · i search · ^N new · ^X close · ^F favorite · ^←/→ windows · ^P hide · ^E expand"
|
|
41
|
+
: normal()
|
|
42
|
+
? "NORMAL · i search · ? help · Ctrl-Space mode"
|
|
43
|
+
: "SEARCH · ^Space navigation · ^H local/all hosts"),
|
|
36
44
|
commands,
|
|
37
45
|
selection,
|
|
38
46
|
select,
|
|
@@ -49,6 +57,10 @@ export function createApplicationPaletteSearchOwner(options) {
|
|
|
49
57
|
if (event.eventType === "release")
|
|
50
58
|
return true;
|
|
51
59
|
const name = event.name.toLowerCase();
|
|
60
|
+
if (event.ctrl && name === "h") {
|
|
61
|
+
setLocalOnly(!localOnly());
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
52
64
|
if (event.ctrl && name === "space") {
|
|
53
65
|
setNormal(!normal());
|
|
54
66
|
return true;
|
|
@@ -67,14 +79,15 @@ export function createApplicationPaletteSearchOwner(options) {
|
|
|
67
79
|
return true;
|
|
68
80
|
}
|
|
69
81
|
const last = Math.max(0, commands().length - 1);
|
|
82
|
+
const halfPage = Math.max(1, Math.floor(pageSize() / 2));
|
|
70
83
|
const delta = name === "j"
|
|
71
84
|
? 1
|
|
72
85
|
: name === "k"
|
|
73
86
|
? -1
|
|
74
87
|
: event.ctrl && name === "d"
|
|
75
|
-
?
|
|
88
|
+
? halfPage
|
|
76
89
|
: event.ctrl && name === "u"
|
|
77
|
-
? -
|
|
90
|
+
? -halfPage
|
|
78
91
|
: 0;
|
|
79
92
|
if (name === "g") {
|
|
80
93
|
select(event.shift ? last : 0);
|
|
@@ -93,7 +106,7 @@ export function createApplicationPaletteSearchOwner(options) {
|
|
|
93
106
|
? 0
|
|
94
107
|
: name === "end"
|
|
95
108
|
? last
|
|
96
|
-
: Math.max(0, Math.min(last, selection() + (name === "pageup" ? -
|
|
109
|
+
: Math.max(0, Math.min(last, selection() + (name === "pageup" ? -1 : 1) * Math.max(1, pageSize()))));
|
|
97
110
|
return true;
|
|
98
111
|
}
|
|
99
112
|
const action = !event.ctrl && !event.meta
|
|
@@ -2,7 +2,22 @@ import { createContext, onCleanup, useContext } from "solid-js";
|
|
|
2
2
|
/** One application keyboard ingress with component-local semantic routes. */
|
|
3
3
|
export function createKeyboardRouteOwner() {
|
|
4
4
|
const routes = [];
|
|
5
|
+
const pasteRoutes = [];
|
|
5
6
|
return {
|
|
7
|
+
registerPaste(route) {
|
|
8
|
+
pasteRoutes.push(route);
|
|
9
|
+
return () => {
|
|
10
|
+
const index = pasteRoutes.lastIndexOf(route);
|
|
11
|
+
if (index >= 0)
|
|
12
|
+
pasteRoutes.splice(index, 1);
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
routePaste(bytes) {
|
|
16
|
+
for (let index = pasteRoutes.length - 1; index >= 0; index--)
|
|
17
|
+
if (pasteRoutes[index](bytes))
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
6
21
|
register(route) {
|
|
7
22
|
routes.push(route);
|
|
8
23
|
let active = true;
|
|
@@ -24,6 +39,7 @@ export function createKeyboardRouteOwner() {
|
|
|
24
39
|
},
|
|
25
40
|
dispose() {
|
|
26
41
|
routes.length = 0;
|
|
42
|
+
pasteRoutes.length = 0;
|
|
27
43
|
},
|
|
28
44
|
get size() {
|
|
29
45
|
return routes.length;
|
|
@@ -44,3 +60,9 @@ export function useKeyboardRoute(route) {
|
|
|
44
60
|
const unregister = owner.register(route);
|
|
45
61
|
onCleanup(unregister);
|
|
46
62
|
}
|
|
63
|
+
/** Component-local paste ownership; physical ingress remains at the application root. */
|
|
64
|
+
export function usePasteRoute(route) {
|
|
65
|
+
const owner = useContext(KeyboardRouteContext);
|
|
66
|
+
if (owner)
|
|
67
|
+
onCleanup(owner.registerPaste(route));
|
|
68
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { For, createMemo } from "solid-js";
|
|
3
|
+
import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
|
|
1
4
|
import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
|
|
2
5
|
import { componentPalette } from "./state.js";
|
|
3
6
|
export function OverlayListRow(props) {
|
|
@@ -13,7 +16,21 @@ export function OverlayListRow(props) {
|
|
|
13
16
|
terminalDisplayWidth(shortcut));
|
|
14
17
|
return clipTerminal(`${prefix}${label}${" ".repeat(gap)}${shortcut}`, props.width);
|
|
15
18
|
};
|
|
16
|
-
|
|
19
|
+
const segments = createMemo(() => {
|
|
20
|
+
const value = content();
|
|
21
|
+
const matches = new Set(props.query ? (commandSearchMatch(value, props.query)?.indices ?? []) : []);
|
|
22
|
+
const runs = [];
|
|
23
|
+
for (const [index, character] of Array.from(value).entries()) {
|
|
24
|
+
const match = matches.has(index);
|
|
25
|
+
const previous = runs.at(-1);
|
|
26
|
+
if (previous?.match === match)
|
|
27
|
+
previous.text += character;
|
|
28
|
+
else
|
|
29
|
+
runs.push({ text: character, match });
|
|
30
|
+
}
|
|
31
|
+
return runs;
|
|
32
|
+
});
|
|
33
|
+
return (<text id={`ui-overlay-row:${props.id}`} width={props.width} height={1} overflow="hidden" fg={palette().foreground} bg={palette().background} onMouseOver={() => {
|
|
17
34
|
if (!props.disabled)
|
|
18
35
|
props.onHighlight?.();
|
|
19
36
|
}} onMouseMove={() => {
|
|
@@ -25,5 +42,11 @@ export function OverlayListRow(props) {
|
|
|
25
42
|
event.preventDefault();
|
|
26
43
|
event.stopPropagation();
|
|
27
44
|
props.onPress();
|
|
28
|
-
}}
|
|
45
|
+
}}>
|
|
46
|
+
<For each={segments()}>
|
|
47
|
+
{(segment) => segment.match ? (<strong>
|
|
48
|
+
<u>{segment.text}</u>
|
|
49
|
+
</strong>) : (segment.text)}
|
|
50
|
+
</For>
|
|
51
|
+
</text>);
|
|
29
52
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
|
|
2
|
+
export { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
|
|
1
3
|
import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
|
|
2
4
|
/** Presentation only: execution remains in the existing application owners. */
|
|
3
5
|
export function applicationCommandDescription(command) {
|
|
@@ -52,10 +54,26 @@ export function applicationCommandDescription(command) {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
export function filterApplicationCommands(commands, query) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
return commands
|
|
58
|
+
.map((command, index) => {
|
|
57
59
|
const { label, detail } = applicationCommandDescription(command);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const score = commandSearchMatch(`${label} ${detail}`, query)?.score;
|
|
61
|
+
const fleet = typeof command === "object" ? command.fleet : undefined;
|
|
62
|
+
return {
|
|
63
|
+
command,
|
|
64
|
+
index,
|
|
65
|
+
score,
|
|
66
|
+
favorite: Boolean(fleet?.favorite),
|
|
67
|
+
recent: fleet?.recentRank ?? 1000,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter((row) => row.score !== undefined)
|
|
71
|
+
.sort((a, b) => (!query.trim()
|
|
72
|
+
? Number(typeof a.command !== "string") - Number(typeof b.command !== "string")
|
|
73
|
+
: 0) ||
|
|
74
|
+
b.score - a.score ||
|
|
75
|
+
Number(b.favorite) - Number(a.favorite) ||
|
|
76
|
+
a.recent - b.recent ||
|
|
77
|
+
a.index - b.index)
|
|
78
|
+
.map((row) => row.command);
|
|
61
79
|
}
|
|
@@ -71,25 +71,25 @@ export function fuzzyMatch(query, target) {
|
|
|
71
71
|
const fi = f[i];
|
|
72
72
|
const fnext = f[i + 1];
|
|
73
73
|
const ni = nextPos[i];
|
|
74
|
-
|
|
74
|
+
// Scan backwards with the best suffix. This preserves the exact scoring
|
|
75
|
+
// and leftmost tie-break while reducing O(query * target²) to O(query * target).
|
|
76
|
+
let suffixScore = NEG;
|
|
77
|
+
let suffixIndex = -1;
|
|
78
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
79
|
+
const candidate = j + 2;
|
|
80
|
+
if (candidate < n && fnext[candidate] !== NEG && fnext[candidate] >= suffixScore) {
|
|
81
|
+
suffixScore = fnext[candidate];
|
|
82
|
+
suffixIndex = candidate;
|
|
83
|
+
}
|
|
75
84
|
if (t[j] !== ch)
|
|
76
85
|
continue;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const sub = fnext[j2];
|
|
81
|
-
if (sub === NEG)
|
|
82
|
-
continue;
|
|
83
|
-
const val = (j2 === j + 1 ? CONTIGUOUS_BONUS : 0) + sub;
|
|
84
|
-
if (val > best) {
|
|
85
|
-
best = val;
|
|
86
|
-
bestNext = j2;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
86
|
+
const adjacent = j + 1 < n ? fnext[j + 1] : NEG;
|
|
87
|
+
const contiguous = adjacent === NEG ? NEG : adjacent + CONTIGUOUS_BONUS;
|
|
88
|
+
const best = Math.max(contiguous, suffixScore);
|
|
89
89
|
if (best === NEG)
|
|
90
|
-
continue;
|
|
90
|
+
continue;
|
|
91
91
|
fi[j] = BASE + positionBonus(j) + best;
|
|
92
|
-
ni[j] =
|
|
92
|
+
ni[j] = contiguous >= suffixScore ? j + 1 : suffixIndex;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
// Best starting placement for q[0] (leftmost on ties, keeping it deterministic).
|
|
@@ -139,3 +139,24 @@ export function fuzzyFilter(query, items, key) {
|
|
|
139
139
|
matches.sort((a, b) => b.score - a.score);
|
|
140
140
|
return matches;
|
|
141
141
|
}
|
|
142
|
+
/** Multiple search terms can match in any order. Indices address displayed code points. */
|
|
143
|
+
export function fuzzyTermsMatch(text, query) {
|
|
144
|
+
let score = 0;
|
|
145
|
+
const positions = new Set();
|
|
146
|
+
for (const term of query.trim().split(/\s+/u).filter(Boolean)) {
|
|
147
|
+
const match = fuzzyMatch(term, text);
|
|
148
|
+
if (!match)
|
|
149
|
+
return null;
|
|
150
|
+
score += match.score;
|
|
151
|
+
for (const position of match.positions)
|
|
152
|
+
positions.add(position);
|
|
153
|
+
}
|
|
154
|
+
const indices = [];
|
|
155
|
+
let offset = 0;
|
|
156
|
+
for (const [index, character] of Array.from(text).entries()) {
|
|
157
|
+
if ([...Array(character.length).keys()].some((i) => positions.has(offset + i)))
|
|
158
|
+
indices.push(index);
|
|
159
|
+
offset += character.length;
|
|
160
|
+
}
|
|
161
|
+
return { score, indices };
|
|
162
|
+
}
|
|
@@ -53,14 +53,18 @@ export function createFleetPreviewCapture(
|
|
|
53
53
|
.trim()
|
|
54
54
|
.split("\n")
|
|
55
55
|
.map((line) => line.split("\t"));
|
|
56
|
-
const rows = await readPanes();
|
|
57
56
|
// Names are untrusted display text. Keep them out of the pane membership
|
|
58
57
|
// format so embedded newlines cannot manufacture a capture target.
|
|
59
58
|
const names = new Map<string, string>();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// Independent metadata reads share one capture's deadline and run in two
|
|
60
|
+
// bounded lanes. Incarnation checks still surround the actual capture.
|
|
61
|
+
const [rows, windowNames] = await Promise.all([
|
|
62
|
+
readPanes(),
|
|
63
|
+
run(
|
|
64
|
+
["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id}\t#{window_name}"],
|
|
65
|
+
signal,
|
|
66
|
+
),
|
|
67
|
+
]);
|
|
64
68
|
for (const line of windowNames.split("\n")) {
|
|
65
69
|
const tab = line.indexOf("\t");
|
|
66
70
|
if (tab > 0)
|
|
@@ -116,11 +120,9 @@ export function createFleetPreviewCapture(
|
|
|
116
120
|
if (!pane || !/^%\d+$/u.test(pane)) return null;
|
|
117
121
|
// Passive capture never changes active windows, size, input or terminal ownership.
|
|
118
122
|
const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
!(await readPanes()).some((r) => r[3] === selectedWindowId && r[0] === pane)
|
|
123
|
-
)
|
|
123
|
+
const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
|
|
124
|
+
if (!currentSessions.some((s) => s.liveSessionId === liveSessionId)) return null;
|
|
125
|
+
if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
|
|
124
126
|
return null;
|
|
125
127
|
return {
|
|
126
128
|
windows,
|
|
@@ -170,10 +170,41 @@ export interface AdaptiveFleetPreviewState {
|
|
|
170
170
|
snapshot: FleetPreviewSnapshot | null;
|
|
171
171
|
stale: boolean;
|
|
172
172
|
}
|
|
173
|
+
/** Short-lived, bounded memory only. Keys must include route, incarnation and transport epoch. */
|
|
174
|
+
export function createFleetPreviewCache(now = Date.now) {
|
|
175
|
+
const entries = new Map<string, { snapshot: FleetPreviewSnapshot; at: number }>();
|
|
176
|
+
return {
|
|
177
|
+
get(key: string) {
|
|
178
|
+
const value = entries.get(key);
|
|
179
|
+
if (!value || now() - value.at > 30_000) {
|
|
180
|
+
entries.delete(key);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
entries.delete(key);
|
|
184
|
+
entries.set(key, value);
|
|
185
|
+
return value.snapshot;
|
|
186
|
+
},
|
|
187
|
+
set(key: string, snapshot: FleetPreviewSnapshot) {
|
|
188
|
+
entries.delete(key);
|
|
189
|
+
entries.set(key, { snapshot, at: now() });
|
|
190
|
+
while (entries.size > 24) entries.delete(entries.keys().next().value!);
|
|
191
|
+
},
|
|
192
|
+
clear() {
|
|
193
|
+
entries.clear();
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export const fleetPreviewMemory = createFleetPreviewCache();
|
|
198
|
+
|
|
173
199
|
/** One selected request, scheduled after settlement; no hidden polling or terminal streams. */
|
|
174
200
|
export function createAdaptiveFleetPreviewOwner(
|
|
175
201
|
publish: (state: AdaptiveFleetPreviewState) => void,
|
|
176
|
-
options: {
|
|
202
|
+
options: {
|
|
203
|
+
debounceMs?: number;
|
|
204
|
+
refreshMs?: number;
|
|
205
|
+
maxBackoffMs?: number;
|
|
206
|
+
cache?: ReturnType<typeof createFleetPreviewCache>;
|
|
207
|
+
} = {},
|
|
177
208
|
) {
|
|
178
209
|
let key: string | undefined;
|
|
179
210
|
let read: ((signal: AbortSignal) => Promise<FleetPreviewResult>) | undefined;
|
|
@@ -217,6 +248,7 @@ export function createAdaptiveFleetPreviewOwner(
|
|
|
217
248
|
if (owned.signal.aborted || controller !== owned) return;
|
|
218
249
|
if (result.status === "ready") {
|
|
219
250
|
snapshot = result.snapshot;
|
|
251
|
+
if (key) options.cache?.set(key, snapshot);
|
|
220
252
|
failures = 0;
|
|
221
253
|
emit({ status: "ready", snapshot, stale: false });
|
|
222
254
|
} else {
|
|
@@ -236,7 +268,8 @@ export function createAdaptiveFleetPreviewOwner(
|
|
|
236
268
|
key = nextRead ? nextKey : undefined;
|
|
237
269
|
snapshot = null;
|
|
238
270
|
failures = 0;
|
|
239
|
-
|
|
271
|
+
snapshot = read && key ? (options.cache?.get(key) ?? null) : null;
|
|
272
|
+
emit({ status: snapshot ? "loading" : "idle", snapshot, stale: snapshot !== null });
|
|
240
273
|
if (!read) return;
|
|
241
274
|
controller = new AbortController();
|
|
242
275
|
schedule(Math.max(0, options.debounceMs ?? 180), controller);
|