tmux-ide 2.9.0-beta.16 → 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 +170 -26
- package/package.json +2 -2
- package/packages/contracts/src/fleet-lifecycle.ts +5 -1
- package/packages/contracts/src/workspace-multiplexer.ts +11 -1
- package/packages/daemon/dist/command-center/actions/handlers/fleet-lifecycle.js +5 -0
- package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +102 -17
- package/packages/daemon/dist/lib/fleet-preview-model.js +1 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +35 -4
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +20 -2
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +12 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +5 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +80 -13
- package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +192 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +9 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +2 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +1 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +106 -69
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +66 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +10 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +71 -1
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +47 -1
- package/packages/daemon/dist/tui/mirror/runtime/fleet-presentation.js +20 -0
- package/packages/daemon/dist/tui/mirror/theme.js +9 -0
- package/packages/daemon/dist/tui/mirror/ui/badge.jsx +3 -1
- package/packages/daemon/dist/tui/mirror/ui/keyboard-router.jsx +22 -0
- package/packages/daemon/dist/tui/mirror/ui/navigation-row.jsx +3 -1
- package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +25 -2
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +46 -11
- package/packages/daemon/dist/tui/team/fuzzy.js +36 -15
- package/packages/daemon/src/command-center/actions/handlers/fleet-lifecycle.ts +5 -0
- package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +141 -35
- package/packages/daemon/src/lib/fleet-preview-model.ts +13 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +41 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +33 -2
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +14 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +6 -1
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +4 -1
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +84 -14
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +218 -9
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +253 -0
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +87 -113
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +17 -1
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +111 -75
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +8 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +84 -8
- package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +16 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +235 -0
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +89 -1
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +34 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +33 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +204 -56
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +27 -1
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +66 -1
- package/packages/daemon/src/tui/mirror/runtime/fleet-presentation.ts +33 -0
- package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +2 -0
- package/packages/daemon/src/tui/mirror/theme.ts +10 -0
- package/packages/daemon/src/tui/mirror/ui/badge.tsx +10 -1
- package/packages/daemon/src/tui/mirror/ui/keyboard-router.tsx +22 -0
- package/packages/daemon/src/tui/mirror/ui/navigation-row.tsx +10 -1
- package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +30 -2
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +71 -12
- package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +1 -0
- 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
|
@@ -3730,7 +3730,14 @@ var init_workspace_multiplexer = __esm({
|
|
|
3730
3730
|
WorkspacePaneKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.extend({
|
|
3731
3731
|
semanticPaneId: TerminalAttachmentSemanticPaneIdSchemaZ
|
|
3732
3732
|
}).strict())();
|
|
3733
|
-
WorkspaceSessionKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.
|
|
3733
|
+
WorkspaceSessionKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.extend({
|
|
3734
|
+
/** Passive fleet actions fence the daemon and exact live session incarnation. */
|
|
3735
|
+
fleetTarget: z31.object({
|
|
3736
|
+
daemonInstanceId: z31.uuid(),
|
|
3737
|
+
liveSessionId: z31.string().regex(/^live-session\.[a-f0-9]{20}$/),
|
|
3738
|
+
sessionName: WorkspaceMultiplexerNameSchemaZ
|
|
3739
|
+
}).strict().optional()
|
|
3740
|
+
}).strict())();
|
|
3734
3741
|
WorkspaceRenameArgumentsSchemaZ = /* @__PURE__ */ (() => z31.discriminatedUnion("scope", [
|
|
3735
3742
|
WorkspaceScopedSchemaZ.extend({
|
|
3736
3743
|
scope: z31.literal("session"),
|
|
@@ -3938,7 +3945,11 @@ var init_fleet_lifecycle = __esm({
|
|
|
3938
3945
|
"display name contains control bytes"
|
|
3939
3946
|
).refine((value) => !value.startsWith("-"), "display name cannot be parsed as an option"))();
|
|
3940
3947
|
OwnerPathSchemaZ = /* @__PURE__ */ (() => z32.string().min(1).max(4096).refine((value) => !value.includes("\0")))();
|
|
3941
|
-
WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z32.object({
|
|
3948
|
+
WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z32.object({
|
|
3949
|
+
displayName: SafeDisplayNameSchemaZ,
|
|
3950
|
+
cwd: OwnerPathSchemaZ.optional(),
|
|
3951
|
+
expectedDaemonInstanceId: z32.uuid().optional()
|
|
3952
|
+
}).strict())();
|
|
3942
3953
|
WorkspaceSessionCreateResultSchemaZ = /* @__PURE__ */ (() => z32.object({
|
|
3943
3954
|
operationId: z32.uuid(),
|
|
3944
3955
|
daemonInstanceId: z32.uuid(),
|
|
@@ -10261,7 +10272,7 @@ var require_package = __commonJS({
|
|
|
10261
10272
|
"package.json"(exports, module) {
|
|
10262
10273
|
module.exports = {
|
|
10263
10274
|
name: "tmux-ide",
|
|
10264
|
-
version: "2.9.0-beta.
|
|
10275
|
+
version: "2.9.0-beta.18",
|
|
10265
10276
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
10266
10277
|
type: "module",
|
|
10267
10278
|
bin: {
|
|
@@ -10323,7 +10334,7 @@ var require_package = __commonJS({
|
|
|
10323
10334
|
postinstall: "node scripts/postinstall.js",
|
|
10324
10335
|
docs: "turbo run dev --filter=@tmux-ide/docs",
|
|
10325
10336
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
10326
|
-
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx",
|
|
10337
|
+
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
10327
10338
|
"test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
|
|
10328
10339
|
"test:tui-live": "node scripts/tui-testdrive.mjs smoke",
|
|
10329
10340
|
"test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
|
|
@@ -18414,7 +18425,7 @@ import { z as z71 } from "zod";
|
|
|
18414
18425
|
import { bodyLimit } from "hono/body-limit";
|
|
18415
18426
|
import { stripVTControlCharacters } from "node:util";
|
|
18416
18427
|
function createFleetPreviewCapture(run) {
|
|
18417
|
-
|
|
18428
|
+
const snapshot = async (liveSessionId2, signal, windowId) => {
|
|
18418
18429
|
const readSessions = async () => {
|
|
18419
18430
|
const raw = await run(
|
|
18420
18431
|
["list-panes", "-a", "-F", "#{pid} #{session_id} #{session_created} #{session_name}"],
|
|
@@ -18424,24 +18435,83 @@ function createFleetPreviewCapture(run) {
|
|
|
18424
18435
|
};
|
|
18425
18436
|
const session = (await readSessions()).find((s) => s.liveSessionId === liveSessionId2);
|
|
18426
18437
|
if (!session) return null;
|
|
18427
|
-
const
|
|
18438
|
+
const readPanes = async () => (await run(
|
|
18428
18439
|
[
|
|
18429
18440
|
"list-panes",
|
|
18430
18441
|
"-s",
|
|
18431
18442
|
"-t",
|
|
18432
18443
|
`=${session.sessionName}`,
|
|
18433
18444
|
"-F",
|
|
18434
|
-
"#{pane_id} #{window_active} #{pane_active}"
|
|
18445
|
+
"#{pane_id} #{window_active} #{pane_active} #{window_id} #{window_index}"
|
|
18435
18446
|
],
|
|
18436
18447
|
signal
|
|
18437
|
-
);
|
|
18438
|
-
const
|
|
18439
|
-
const
|
|
18448
|
+
)).trim().split("\n").map((line) => line.split(" "));
|
|
18449
|
+
const names = /* @__PURE__ */ new Map();
|
|
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
|
+
]);
|
|
18457
|
+
for (const line of windowNames.split("\n")) {
|
|
18458
|
+
const tab = line.indexOf(" ");
|
|
18459
|
+
if (tab > 0)
|
|
18460
|
+
names.set(
|
|
18461
|
+
line.slice(0, tab),
|
|
18462
|
+
cleanText(line.slice(tab + 1)).replace(/[\n\t]/gu, " ").slice(0, 80)
|
|
18463
|
+
);
|
|
18464
|
+
}
|
|
18465
|
+
const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
|
|
18466
|
+
const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
|
|
18467
|
+
if (windowId && !windowRows.some((row) => row[3] === windowId)) return null;
|
|
18468
|
+
const toWindow = (row) => ({
|
|
18469
|
+
id: row[3],
|
|
18470
|
+
index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
|
|
18471
|
+
name: names.get(row[3]) ?? `Window ${row[4]}`,
|
|
18472
|
+
active: row[1] === "1"
|
|
18473
|
+
});
|
|
18474
|
+
const windows = [];
|
|
18475
|
+
for (const row of windowRows) {
|
|
18476
|
+
if (windows.some((w) => w.id === row[3])) continue;
|
|
18477
|
+
if (windows.length >= 64) break;
|
|
18478
|
+
windows.push(toWindow(row));
|
|
18479
|
+
}
|
|
18480
|
+
if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
|
|
18481
|
+
const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
|
|
18482
|
+
if (selectedRow) windows.splice(63, 1, toWindow(selectedRow));
|
|
18483
|
+
}
|
|
18484
|
+
let paneBudget = 256;
|
|
18485
|
+
for (const window2 of [...windows].sort(
|
|
18486
|
+
(a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId)
|
|
18487
|
+
)) {
|
|
18488
|
+
const ids = [
|
|
18489
|
+
...new Set(
|
|
18490
|
+
windowRows.filter((row) => row[3] === window2.id && /^%\d+$/u.test(row[0] ?? "")).map((row) => row[0])
|
|
18491
|
+
)
|
|
18492
|
+
];
|
|
18493
|
+
if (ids.length <= paneBudget) {
|
|
18494
|
+
window2.paneIds = ids;
|
|
18495
|
+
paneBudget -= ids.length;
|
|
18496
|
+
}
|
|
18497
|
+
}
|
|
18498
|
+
windows.sort((a, b) => a.index - b.index);
|
|
18499
|
+
const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
|
|
18500
|
+
const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
|
|
18440
18501
|
if (!pane || !/^%\d+$/u.test(pane)) return null;
|
|
18441
18502
|
const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
|
|
18442
|
-
|
|
18443
|
-
|
|
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))
|
|
18506
|
+
return null;
|
|
18507
|
+
return {
|
|
18508
|
+
windows,
|
|
18509
|
+
selectedWindowId,
|
|
18510
|
+
text: cleanText(captured).split("\n").slice(-24).map((line) => line.slice(0, 180)).join("\n").slice(0, 8192)
|
|
18511
|
+
};
|
|
18444
18512
|
};
|
|
18513
|
+
const capture = async (liveSessionId2, signal) => (await snapshot(liveSessionId2, signal))?.text ?? null;
|
|
18514
|
+
return Object.assign(capture, { snapshot });
|
|
18445
18515
|
}
|
|
18446
18516
|
function mountFleetPreviewRoute(app, options) {
|
|
18447
18517
|
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
@@ -18476,11 +18546,18 @@ function mountFleetPreviewRoute(app, options) {
|
|
|
18476
18546
|
next = now + 250;
|
|
18477
18547
|
pending = true;
|
|
18478
18548
|
try {
|
|
18479
|
-
const
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
);
|
|
18483
|
-
|
|
18549
|
+
const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
|
|
18550
|
+
if (input.data.windowId && !options.capture.snapshot)
|
|
18551
|
+
return c.json({ error: "Window preview unavailable" }, 503);
|
|
18552
|
+
const snapshot = options.capture.snapshot ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId) : null;
|
|
18553
|
+
const legacy = !options.capture.snapshot ? await options.capture(input.data.liveSessionId, signal) : null;
|
|
18554
|
+
const text = snapshot?.text ?? legacy;
|
|
18555
|
+
return text === null ? c.json({ error: "Session changed" }, 409) : c.json({
|
|
18556
|
+
daemon: options.daemon,
|
|
18557
|
+
liveSessionId: input.data.liveSessionId,
|
|
18558
|
+
text,
|
|
18559
|
+
...snapshot ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId } : {}
|
|
18560
|
+
});
|
|
18484
18561
|
} catch {
|
|
18485
18562
|
return c.json({ error: "Preview unavailable" }, 503);
|
|
18486
18563
|
} finally {
|
|
@@ -18488,14 +18565,16 @@ function mountFleetPreviewRoute(app, options) {
|
|
|
18488
18565
|
}
|
|
18489
18566
|
});
|
|
18490
18567
|
}
|
|
18491
|
-
var requestSchema;
|
|
18568
|
+
var cleanText, requestSchema;
|
|
18492
18569
|
var init_fleet_preview_route = __esm({
|
|
18493
18570
|
"packages/daemon/src/command-center/resources/fleet-preview-route.ts"() {
|
|
18494
18571
|
"use strict";
|
|
18495
18572
|
init_owner_authority();
|
|
18496
18573
|
init_discovery();
|
|
18574
|
+
cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
18497
18575
|
requestSchema = z71.strictObject({
|
|
18498
18576
|
expectedInstanceId: z71.uuid(),
|
|
18577
|
+
windowId: z71.string().regex(/^@\d+$/u).optional(),
|
|
18499
18578
|
liveSessionId: z71.string().regex(/^live-session\.[a-f0-9]{20}$/u)
|
|
18500
18579
|
});
|
|
18501
18580
|
}
|
|
@@ -33670,6 +33749,7 @@ var CREATION_OPTION2, SEMANTIC_PANE_OPTION4, SEMANTIC_WINDOW_OPTION3, DISPLAY_TI
|
|
|
33670
33749
|
var init_workspace_multiplexer_verbs = __esm({
|
|
33671
33750
|
"packages/daemon/src/lib/workspace-multiplexer-verbs.ts"() {
|
|
33672
33751
|
"use strict";
|
|
33752
|
+
init_discovery();
|
|
33673
33753
|
init_src();
|
|
33674
33754
|
init_src2();
|
|
33675
33755
|
init_workspace_pane_creation2();
|
|
@@ -33888,6 +33968,37 @@ var init_workspace_multiplexer_verbs = __esm({
|
|
|
33888
33968
|
operationId: request.operationId
|
|
33889
33969
|
});
|
|
33890
33970
|
}
|
|
33971
|
+
if (request.intent.verb === "workspace.session.kill" && request.intent.fleetTarget) {
|
|
33972
|
+
const target = request.intent.fleetTarget;
|
|
33973
|
+
if (target.daemonInstanceId !== this.#daemonInstanceId)
|
|
33974
|
+
throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
|
|
33975
|
+
operationId: request.operationId
|
|
33976
|
+
});
|
|
33977
|
+
const raw2 = this.#io.runTmux([
|
|
33978
|
+
"list-panes",
|
|
33979
|
+
"-a",
|
|
33980
|
+
"-F",
|
|
33981
|
+
"#{pid} #{session_id} #{session_created} #{session_name}"
|
|
33982
|
+
]);
|
|
33983
|
+
const match = raw2.split("\n").find((line) => {
|
|
33984
|
+
const session = discoverLiveSessionSummaries(() => line)[0];
|
|
33985
|
+
return session?.liveSessionId === target.liveSessionId && session.sessionName === target.sessionName;
|
|
33986
|
+
});
|
|
33987
|
+
if (!match)
|
|
33988
|
+
throw new WorkspaceMultiplexerError("workspace_not_found", {
|
|
33989
|
+
reason: "session_incarnation_changed"
|
|
33990
|
+
});
|
|
33991
|
+
const runtimeId = match.split(" ")[1];
|
|
33992
|
+
return this.#killSession(
|
|
33993
|
+
target.sessionName,
|
|
33994
|
+
{
|
|
33995
|
+
operationId: request.operationId,
|
|
33996
|
+
daemonInstanceId: this.#daemonInstanceId,
|
|
33997
|
+
workspaceName: request.intent.workspaceName
|
|
33998
|
+
},
|
|
33999
|
+
runtimeId
|
|
34000
|
+
);
|
|
34001
|
+
}
|
|
33891
34002
|
const workspace = this.#registry.get(request.intent.workspaceName);
|
|
33892
34003
|
if (!workspace) {
|
|
33893
34004
|
throw new WorkspaceMultiplexerError("workspace_not_found", {
|
|
@@ -34128,19 +34239,19 @@ var init_workspace_multiplexer_verbs = __esm({
|
|
|
34128
34239
|
remainingWindowCount: windowIdsOf(after).length
|
|
34129
34240
|
};
|
|
34130
34241
|
}
|
|
34131
|
-
#killSession(sessionName, envelope) {
|
|
34242
|
+
#killSession(sessionName, envelope, exactTarget = `=${sessionName}`) {
|
|
34132
34243
|
let existed = true;
|
|
34133
34244
|
try {
|
|
34134
|
-
this.#io.runTmux(["has-session", "-t",
|
|
34245
|
+
this.#io.runTmux(["has-session", "-t", exactTarget]);
|
|
34135
34246
|
} catch (error) {
|
|
34136
34247
|
if (!this.#io.isMissingTmuxTarget(error)) throw error;
|
|
34137
34248
|
existed = false;
|
|
34138
34249
|
}
|
|
34139
34250
|
if (existed) {
|
|
34140
|
-
this.#io.runTmux(["kill-session", "-t",
|
|
34251
|
+
this.#io.runTmux(["kill-session", "-t", exactTarget]);
|
|
34141
34252
|
let stillPresent = true;
|
|
34142
34253
|
try {
|
|
34143
|
-
this.#io.runTmux(["has-session", "-t",
|
|
34254
|
+
this.#io.runTmux(["has-session", "-t", exactTarget]);
|
|
34144
34255
|
} catch {
|
|
34145
34256
|
stillPresent = false;
|
|
34146
34257
|
}
|
|
@@ -39533,7 +39644,7 @@ var init_semantic_mutation_executor = __esm({
|
|
|
39533
39644
|
intent = { ...intent, origin: authority.origin };
|
|
39534
39645
|
}
|
|
39535
39646
|
const authenticatedSourceSemanticPaneId = authority.authenticatedSourceSemanticPaneId ?? null;
|
|
39536
|
-
const session = this.#options.resolveSession(intent.workspaceName);
|
|
39647
|
+
const session = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.sessionName : this.#options.resolveSession(intent.workspaceName);
|
|
39537
39648
|
const ledger2 = this.#ledger(session ?? MISSING_SESSION_LEDGER);
|
|
39538
39649
|
const origin = authority.origin;
|
|
39539
39650
|
const fingerprint2 = JSON.stringify([intent, authenticatedSourceSemanticPaneId, origin]);
|
|
@@ -56807,7 +56918,7 @@ var init_registry2 = __esm({
|
|
|
56807
56918
|
if (this.#disposed) return Promise.reject(new Error("SessionRuntimeRegistry is disposed"));
|
|
56808
56919
|
runtime.assertController(lease);
|
|
56809
56920
|
let intent = SessionRuntimeSemanticIntentSchemaZ.parse(rawIntent);
|
|
56810
|
-
const resolvedSession = this.#resolveSession?.(intent.workspaceName) ?? null;
|
|
56921
|
+
const resolvedSession = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.daemonInstanceId === this.generation ? intent.fleetTarget.sessionName : null : this.#resolveSession?.(intent.workspaceName) ?? null;
|
|
56811
56922
|
if (resolvedSession !== runtime.session) {
|
|
56812
56923
|
return Promise.reject(
|
|
56813
56924
|
new SessionRuntimeControllerLeaseError(
|
|
@@ -64732,6 +64843,14 @@ var init_pane_stream_websocket = __esm({
|
|
|
64732
64843
|
this.close(1011, "stream-unavailable");
|
|
64733
64844
|
return;
|
|
64734
64845
|
}
|
|
64846
|
+
const visibleAtOpen = new Set(
|
|
64847
|
+
stagedAuthority.layouts.flatMap(
|
|
64848
|
+
(layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
|
|
64849
|
+
)
|
|
64850
|
+
);
|
|
64851
|
+
channels.sort(
|
|
64852
|
+
(left, right) => Number(visibleAtOpen.has(right.semanticPaneId)) - Number(visibleAtOpen.has(left.semanticPaneId))
|
|
64853
|
+
);
|
|
64735
64854
|
const openings = channels.map(async (channel) => {
|
|
64736
64855
|
const pending = [];
|
|
64737
64856
|
let ready = false;
|
|
@@ -64802,7 +64921,15 @@ var init_pane_stream_websocket = __esm({
|
|
|
64802
64921
|
});
|
|
64803
64922
|
layoutActivated = true;
|
|
64804
64923
|
this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
|
|
64805
|
-
|
|
64924
|
+
const visibleAtDelivery = new Set(
|
|
64925
|
+
authority.layouts.flatMap(
|
|
64926
|
+
(layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
|
|
64927
|
+
)
|
|
64928
|
+
);
|
|
64929
|
+
opened.sort(
|
|
64930
|
+
(left, right) => Number(visibleAtDelivery.has(right.channel.semanticPaneId)) - Number(visibleAtDelivery.has(left.channel.semanticPaneId))
|
|
64931
|
+
);
|
|
64932
|
+
for (const { channel, delivery } of opened) {
|
|
64806
64933
|
if (this.#closed || channel.closed) {
|
|
64807
64934
|
await delivery.close();
|
|
64808
64935
|
continue;
|
|
@@ -64824,6 +64951,9 @@ var init_pane_stream_websocket = __esm({
|
|
|
64824
64951
|
pane: channel.semanticPaneId,
|
|
64825
64952
|
negotiation: delivery.negotiation
|
|
64826
64953
|
});
|
|
64954
|
+
}
|
|
64955
|
+
for (const { channel, pending, markReady } of opened) {
|
|
64956
|
+
if (this.#closed || channel.closed) continue;
|
|
64827
64957
|
markReady();
|
|
64828
64958
|
for (const message of pending)
|
|
64829
64959
|
await this.#sendTerminalDelivery(channel.semanticPaneId, message);
|
|
@@ -65895,7 +66025,16 @@ function createSessionRuntimeMultiplexerBackend(options) {
|
|
|
65895
66025
|
};
|
|
65896
66026
|
return {
|
|
65897
66027
|
mutate: async (request, authenticatedHostClientId, sourcePaneCredential, ownerAuthorized = false) => {
|
|
65898
|
-
const
|
|
66028
|
+
const fleetTarget = request.intent.verb === "workspace.session.kill" ? request.intent.fleetTarget : void 0;
|
|
66029
|
+
if (fleetTarget && (!ownerAuthorized || authenticatedHostClientId || sourcePaneCredential))
|
|
66030
|
+
throw new WorkspaceMultiplexerError("operation_conflict", {
|
|
66031
|
+
reason: "fleet_close_requires_explicit_owner"
|
|
66032
|
+
});
|
|
66033
|
+
if (fleetTarget && fleetTarget.daemonInstanceId !== options.registry.generation)
|
|
66034
|
+
throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
|
|
66035
|
+
operationId: request.operationId
|
|
66036
|
+
});
|
|
66037
|
+
const session = fleetTarget && ownerAuthorized && !authenticatedHostClientId && !sourcePaneCredential ? fleetTarget.sessionName : options.resolveSession(request.intent.workspaceName);
|
|
65899
66038
|
if (!session) {
|
|
65900
66039
|
throw new Error(`Workspace ${request.intent.workspaceName} has no live tmux session`);
|
|
65901
66040
|
}
|
|
@@ -68774,6 +68913,11 @@ async function workspaceSessionCreateHandler(input, context) {
|
|
|
68774
68913
|
message: "Fleet lifecycle is unavailable."
|
|
68775
68914
|
});
|
|
68776
68915
|
const [operation, generation] = authorityContext(context);
|
|
68916
|
+
if (input.expectedDaemonInstanceId && input.expectedDaemonInstanceId !== generation)
|
|
68917
|
+
throw new ActionError({
|
|
68918
|
+
code: "bad_request",
|
|
68919
|
+
message: "The selected daemon has been replaced."
|
|
68920
|
+
});
|
|
68777
68921
|
return await mapAuthority(
|
|
68778
68922
|
() => context.fleetLifecycleBackend.createSession(operation, generation, input)
|
|
68779
68923
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.9.0-beta.
|
|
3
|
+
"version": "2.9.0-beta.18",
|
|
4
4
|
"description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"postinstall": "node scripts/postinstall.js",
|
|
63
63
|
"docs": "turbo run dev --filter=@tmux-ide/docs",
|
|
64
64
|
"demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
|
|
65
|
-
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx",
|
|
65
|
+
"test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
|
|
66
66
|
"test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
|
|
67
67
|
"test:tui-live": "node scripts/tui-testdrive.mjs smoke",
|
|
68
68
|
"test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
|
|
@@ -27,7 +27,11 @@ const OwnerPathSchemaZ = z
|
|
|
27
27
|
.max(4_096)
|
|
28
28
|
.refine((value) => !value.includes("\0"));
|
|
29
29
|
export const WorkspaceSessionCreateArgumentsSchemaZ = z
|
|
30
|
-
.object({
|
|
30
|
+
.object({
|
|
31
|
+
displayName: SafeDisplayNameSchemaZ,
|
|
32
|
+
cwd: OwnerPathSchemaZ.optional(),
|
|
33
|
+
expectedDaemonInstanceId: z.uuid().optional(),
|
|
34
|
+
})
|
|
31
35
|
.strict();
|
|
32
36
|
export type WorkspaceSessionCreateArguments = z.infer<
|
|
33
37
|
typeof WorkspaceSessionCreateArgumentsSchemaZ
|
|
@@ -84,7 +84,17 @@ export const WorkspacePaneKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
|
|
|
84
84
|
}).strict();
|
|
85
85
|
export type WorkspacePaneKillArguments = z.infer<typeof WorkspacePaneKillArgumentsSchemaZ>;
|
|
86
86
|
|
|
87
|
-
export const WorkspaceSessionKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.
|
|
87
|
+
export const WorkspaceSessionKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
|
|
88
|
+
/** Passive fleet actions fence the daemon and exact live session incarnation. */
|
|
89
|
+
fleetTarget: z
|
|
90
|
+
.object({
|
|
91
|
+
daemonInstanceId: z.uuid(),
|
|
92
|
+
liveSessionId: z.string().regex(/^live-session\.[a-f0-9]{20}$/),
|
|
93
|
+
sessionName: WorkspaceMultiplexerNameSchemaZ,
|
|
94
|
+
})
|
|
95
|
+
.strict()
|
|
96
|
+
.optional(),
|
|
97
|
+
}).strict();
|
|
88
98
|
export type WorkspaceSessionKillArguments = z.infer<typeof WorkspaceSessionKillArgumentsSchemaZ>;
|
|
89
99
|
|
|
90
100
|
export const WorkspaceRenameArgumentsSchemaZ = z.discriminatedUnion("scope", [
|
|
@@ -34,6 +34,11 @@ export async function workspaceSessionCreateHandler(input, context) {
|
|
|
34
34
|
message: "Fleet lifecycle is unavailable.",
|
|
35
35
|
});
|
|
36
36
|
const [operation, generation] = authorityContext(context);
|
|
37
|
+
if (input.expectedDaemonInstanceId && input.expectedDaemonInstanceId !== generation)
|
|
38
|
+
throw new ActionError({
|
|
39
|
+
code: "bad_request",
|
|
40
|
+
message: "The selected daemon has been replaced.",
|
|
41
|
+
});
|
|
37
42
|
return await mapAuthority(() => context.fleetLifecycleBackend.createSession(operation, generation, input));
|
|
38
43
|
}
|
|
39
44
|
export async function fleetAgentMutateHandler(input, context) {
|
|
@@ -3,8 +3,9 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
3
3
|
import { stripVTControlCharacters } from "node:util";
|
|
4
4
|
import { ownerAuthorityGate } from "../owner-authority.js";
|
|
5
5
|
import { discoverLiveSessionSummaries } from "../discovery.js";
|
|
6
|
+
const cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
6
7
|
export function createFleetPreviewCapture(run) {
|
|
7
|
-
|
|
8
|
+
const snapshot = async (liveSessionId, signal, windowId) => {
|
|
8
9
|
const readSessions = async () => {
|
|
9
10
|
const raw = await run(["list-panes", "-a", "-F", "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}"], signal);
|
|
10
11
|
return discoverLiveSessionSummaries(() => raw);
|
|
@@ -12,36 +13,103 @@ export function createFleetPreviewCapture(run) {
|
|
|
12
13
|
const session = (await readSessions()).find((s) => s.liveSessionId === liveSessionId);
|
|
13
14
|
if (!session)
|
|
14
15
|
return null;
|
|
15
|
-
const
|
|
16
|
+
const readPanes = async () => (await run([
|
|
16
17
|
"list-panes",
|
|
17
18
|
"-s",
|
|
18
19
|
"-t",
|
|
19
20
|
`=${session.sessionName}`,
|
|
20
21
|
"-F",
|
|
21
|
-
"#{pane_id}\t#{window_active}\t#{pane_active}",
|
|
22
|
-
], signal)
|
|
23
|
-
const rows = panes
|
|
22
|
+
"#{pane_id}\t#{window_active}\t#{pane_active}\t#{window_id}\t#{window_index}",
|
|
23
|
+
], signal))
|
|
24
24
|
.trim()
|
|
25
25
|
.split("\n")
|
|
26
26
|
.map((line) => line.split("\t"));
|
|
27
|
-
|
|
27
|
+
// Names are untrusted display text. Keep them out of the pane membership
|
|
28
|
+
// format so embedded newlines cannot manufacture a capture target.
|
|
29
|
+
const names = new Map();
|
|
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
|
+
]);
|
|
36
|
+
for (const line of windowNames.split("\n")) {
|
|
37
|
+
const tab = line.indexOf("\t");
|
|
38
|
+
if (tab > 0)
|
|
39
|
+
names.set(line.slice(0, tab), cleanText(line.slice(tab + 1))
|
|
40
|
+
.replace(/[\n\t]/gu, " ")
|
|
41
|
+
.slice(0, 80));
|
|
42
|
+
}
|
|
43
|
+
const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
|
|
44
|
+
const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
|
|
45
|
+
if (windowId && !windowRows.some((row) => row[3] === windowId))
|
|
46
|
+
return null;
|
|
47
|
+
const toWindow = (row) => ({
|
|
48
|
+
id: row[3],
|
|
49
|
+
index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
|
|
50
|
+
name: names.get(row[3]) ?? `Window ${row[4]}`,
|
|
51
|
+
active: row[1] === "1",
|
|
52
|
+
});
|
|
53
|
+
const windows = [];
|
|
54
|
+
for (const row of windowRows) {
|
|
55
|
+
if (windows.some((w) => w.id === row[3]))
|
|
56
|
+
continue;
|
|
57
|
+
if (windows.length >= 64)
|
|
58
|
+
break;
|
|
59
|
+
windows.push(toWindow(row));
|
|
60
|
+
}
|
|
61
|
+
// Preserve the actual active/requested target even on unusually large sessions.
|
|
62
|
+
if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
|
|
63
|
+
const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
|
|
64
|
+
if (selectedRow)
|
|
65
|
+
windows.splice(63, 1, toWindow(selectedRow));
|
|
66
|
+
}
|
|
67
|
+
let paneBudget = 256;
|
|
68
|
+
// The selected window gets the membership budget first; omitted membership
|
|
69
|
+
// remains explicitly unknown instead of reporting incomplete activity counts.
|
|
70
|
+
for (const window of [...windows].sort((a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId))) {
|
|
71
|
+
const ids = [
|
|
72
|
+
...new Set(windowRows
|
|
73
|
+
.filter((row) => row[3] === window.id && /^%\d+$/u.test(row[0] ?? ""))
|
|
74
|
+
.map((row) => row[0])),
|
|
75
|
+
];
|
|
76
|
+
if (ids.length <= paneBudget) {
|
|
77
|
+
window.paneIds = ids;
|
|
78
|
+
paneBudget -= ids.length;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
windows.sort((a, b) => a.index - b.index);
|
|
82
|
+
const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
|
|
83
|
+
const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
|
|
28
84
|
if (!pane || !/^%\d+$/u.test(pane))
|
|
29
85
|
return null;
|
|
30
|
-
//
|
|
86
|
+
// Passive capture never changes active windows, size, input or terminal ownership.
|
|
31
87
|
const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
|
|
32
|
-
|
|
88
|
+
const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
|
|
89
|
+
if (!currentSessions.some((s) => s.liveSessionId === liveSessionId))
|
|
33
90
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
91
|
+
if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
windows,
|
|
95
|
+
selectedWindowId,
|
|
96
|
+
text: cleanText(captured)
|
|
97
|
+
.split("\n")
|
|
98
|
+
.slice(-24)
|
|
99
|
+
.map((line) => line.slice(0, 180))
|
|
100
|
+
.join("\n")
|
|
101
|
+
.slice(0, 8192),
|
|
102
|
+
};
|
|
41
103
|
};
|
|
104
|
+
const capture = async (liveSessionId, signal) => (await snapshot(liveSessionId, signal))?.text ?? null;
|
|
105
|
+
return Object.assign(capture, { snapshot });
|
|
42
106
|
}
|
|
43
107
|
const requestSchema = z.strictObject({
|
|
44
108
|
expectedInstanceId: z.uuid(),
|
|
109
|
+
windowId: z
|
|
110
|
+
.string()
|
|
111
|
+
.regex(/^@\d+$/u)
|
|
112
|
+
.optional(),
|
|
45
113
|
liveSessionId: z.string().regex(/^live-session\.[a-f0-9]{20}$/u),
|
|
46
114
|
});
|
|
47
115
|
export function mountFleetPreviewRoute(app, options) {
|
|
@@ -83,10 +151,27 @@ export function mountFleetPreviewRoute(app, options) {
|
|
|
83
151
|
next = now + 250;
|
|
84
152
|
pending = true;
|
|
85
153
|
try {
|
|
86
|
-
const
|
|
154
|
+
const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
|
|
155
|
+
if (input.data.windowId && !options.capture.snapshot)
|
|
156
|
+
return c.json({ error: "Window preview unavailable" }, 503);
|
|
157
|
+
const snapshot = options.capture.snapshot
|
|
158
|
+
? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId)
|
|
159
|
+
: null;
|
|
160
|
+
// Older capture providers remain valid for the original text-only contract.
|
|
161
|
+
const legacy = !options.capture.snapshot
|
|
162
|
+
? await options.capture(input.data.liveSessionId, signal)
|
|
163
|
+
: null;
|
|
164
|
+
const text = snapshot?.text ?? legacy;
|
|
87
165
|
return text === null
|
|
88
166
|
? c.json({ error: "Session changed" }, 409)
|
|
89
|
-
: c.json({
|
|
167
|
+
: c.json({
|
|
168
|
+
daemon: options.daemon,
|
|
169
|
+
liveSessionId: input.data.liveSessionId,
|
|
170
|
+
text,
|
|
171
|
+
...(snapshot
|
|
172
|
+
? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId }
|
|
173
|
+
: {}),
|
|
174
|
+
});
|
|
90
175
|
}
|
|
91
176
|
catch {
|
|
92
177
|
return c.json({ error: "Preview unavailable" }, 503);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|