tmux-ide 2.9.14 → 2.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.js +2015 -1428
- package/package.json +2 -1
- package/packages/contracts/src/index.ts +1 -0
- package/packages/contracts/src/native-browser.ts +43 -0
- package/packages/contracts/src/tmux-server-scope.ts +54 -0
- package/packages/core/src/terminal-replica.ts +32 -10
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +8 -1
- package/packages/daemon/dist/command-center/tmux-servers.js +52 -10
- package/packages/daemon/dist/lib/daemon-embed.js +15 -0
- package/packages/daemon/dist/lib/native-browser-rpc.js +110 -0
- package/packages/daemon/dist/lib/session-favicon.js +152 -0
- package/packages/daemon/dist/lib/skill-sync.js +3 -3
- package/packages/daemon/dist/lib/tmux-native-browser.js +80 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +25 -3
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/open-browser.js +64 -8
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +16 -0
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +9 -1
- package/packages/daemon/src/command-center/tmux-servers.ts +66 -11
- package/packages/daemon/src/lib/daemon-embed.ts +17 -0
- package/packages/daemon/src/lib/native-browser-rpc.ts +118 -0
- package/packages/daemon/src/lib/session-favicon.ts +185 -0
- package/packages/daemon/src/lib/skill-sync.ts +3 -3
- package/packages/daemon/src/lib/tmux-native-browser.ts +100 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +44 -6
- package/packages/daemon/src/open-browser.ts +120 -8
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +25 -0
- package/skill/SKILL.md +6 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { IdeError } from "./lib/errors.ts";
|
|
4
5
|
import { resolveRuntimeNamespace, runtimeNamespaceEnvironment } from "./lib/runtime-namespace.ts";
|
|
5
6
|
import {
|
|
@@ -16,14 +17,40 @@ export interface OpenBrowserOptions {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/** Resolve once before creating a pane; ignore relative PATH entries. */
|
|
19
|
-
export function resolveTerminalBrowser(
|
|
20
|
+
export function resolveTerminalBrowser(
|
|
21
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
22
|
+
platform: NodeJS.Platform = process.platform,
|
|
23
|
+
): string {
|
|
20
24
|
const configured = environment.TMUX_IDE_TERMINAL_BROWSER_BIN;
|
|
25
|
+
const home = environment.HOME ?? homedir();
|
|
26
|
+
// Resolve the managed alias on every invocation so an app update selects its
|
|
27
|
+
// matching browser. Native assets remain outside the universal npm package.
|
|
28
|
+
const bundled =
|
|
29
|
+
platform === "darwin" && isAbsolute(home)
|
|
30
|
+
? [
|
|
31
|
+
join(
|
|
32
|
+
home,
|
|
33
|
+
"Library",
|
|
34
|
+
"Application Support",
|
|
35
|
+
"tmux-ide",
|
|
36
|
+
"native-alpha",
|
|
37
|
+
"TmuxIDE.app",
|
|
38
|
+
"Contents",
|
|
39
|
+
"Resources",
|
|
40
|
+
"browser",
|
|
41
|
+
"terminal-browser",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
: [];
|
|
21
45
|
const candidates = configured
|
|
22
46
|
? [configured]
|
|
23
|
-
:
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
47
|
+
: [
|
|
48
|
+
...bundled,
|
|
49
|
+
...(environment.PATH ?? "")
|
|
50
|
+
.split(delimiter)
|
|
51
|
+
.filter(isAbsolute)
|
|
52
|
+
.map((dir) => join(dir, "terminal-browser")),
|
|
53
|
+
];
|
|
27
54
|
for (const candidate of candidates) {
|
|
28
55
|
try {
|
|
29
56
|
if (!isAbsolute(candidate)) continue;
|
|
@@ -88,6 +115,54 @@ export function validateBrowserOpen(url: string | undefined, options: OpenBrowse
|
|
|
88
115
|
}
|
|
89
116
|
|
|
90
117
|
type Run = (args: readonly string[]) => string;
|
|
118
|
+
|
|
119
|
+
/** Printed when the opened browser never publishes tmux-ide's native controls. */
|
|
120
|
+
export const NO_NATIVE_CONTROLS_NOTICE =
|
|
121
|
+
"This browser has no tmux-ide controls; install the native app for back/forward/reload.";
|
|
122
|
+
|
|
123
|
+
/** A browser bundled in a native app publishes its controls; a standalone one may not. */
|
|
124
|
+
function isBundledBrowser(executable: string): boolean {
|
|
125
|
+
return /\/[^/]+\.app\/Contents\/Resources\/browser\//u.test(executable);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ControlsWait {
|
|
129
|
+
sleep?: (ms: number) => Promise<void>;
|
|
130
|
+
/** Milliseconds; Date.now by default. */
|
|
131
|
+
now?: () => number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** How long a standalone browser has to publish its controls. */
|
|
135
|
+
const STANDALONE_WAIT_MS = 2000;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Watches the new pane for @tmux_ide_browser_control, which only a browser with
|
|
139
|
+
* native controls sets. A browser bundled in an app is never reported: if it is
|
|
140
|
+
* slow, its controls appear when it is ready. A standalone browser gets 2 s of
|
|
141
|
+
* wall-clock time, however slow tmux answers, and then the notice. Returns
|
|
142
|
+
* null once the option appears or when the pane is already gone.
|
|
143
|
+
*/
|
|
144
|
+
export async function nativeControlsNotice(
|
|
145
|
+
run: Run,
|
|
146
|
+
pane: string,
|
|
147
|
+
executable: string,
|
|
148
|
+
wait: ControlsWait = {},
|
|
149
|
+
): Promise<string | null> {
|
|
150
|
+
if (isBundledBrowser(executable)) return null;
|
|
151
|
+
const sleep = wait.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
152
|
+
const now = wait.now ?? Date.now;
|
|
153
|
+
const deadline = now() + STANDALONE_WAIT_MS;
|
|
154
|
+
for (;;) {
|
|
155
|
+
let control: string;
|
|
156
|
+
try {
|
|
157
|
+
control = run(["display-message", "-p", "-t", pane, "#{@tmux_ide_browser_control}"]).trim();
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (control) return null;
|
|
162
|
+
if (now() >= deadline) return NO_NATIVE_CONTROLS_NOTICE;
|
|
163
|
+
await sleep(250);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
91
166
|
export function createBrowserPane(
|
|
92
167
|
run: Run,
|
|
93
168
|
executable: string,
|
|
@@ -193,9 +268,37 @@ export function createBrowserPane(
|
|
|
193
268
|
return { pane: pane!, session: createdSession!, window: createdWindow!, status: "created" };
|
|
194
269
|
}
|
|
195
270
|
|
|
196
|
-
|
|
271
|
+
/** Seams for tests; the CLI uses the defaults. */
|
|
272
|
+
export interface OpenBrowserDependencies {
|
|
273
|
+
notice?: (line: string) => void;
|
|
274
|
+
executable?: () => string;
|
|
275
|
+
create?: typeof createPinnedBrowserPane;
|
|
276
|
+
wait?: ControlsWait;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function openBrowser(
|
|
280
|
+
url: string | undefined,
|
|
281
|
+
options: OpenBrowserOptions = {},
|
|
282
|
+
dependencies: OpenBrowserDependencies = {},
|
|
283
|
+
) {
|
|
197
284
|
validateBrowserOpen(url, options);
|
|
198
|
-
const executable = resolveTerminalBrowser();
|
|
285
|
+
const executable = (dependencies.executable ?? resolveTerminalBrowser)();
|
|
286
|
+
const { result, run } = (dependencies.create ?? createPinnedBrowserPane)(
|
|
287
|
+
executable,
|
|
288
|
+
url,
|
|
289
|
+
options,
|
|
290
|
+
);
|
|
291
|
+
const message = await nativeControlsNotice(run, result.pane, executable, dependencies.wait);
|
|
292
|
+
if (message)
|
|
293
|
+
(dependencies.notice ?? ((line: string) => process.stderr.write(`${line}\n`)))(message);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createPinnedBrowserPane(
|
|
298
|
+
executable: string,
|
|
299
|
+
url: string | undefined,
|
|
300
|
+
options: OpenBrowserOptions,
|
|
301
|
+
): { result: ReturnType<typeof createBrowserPane>; run: Run } {
|
|
199
302
|
try {
|
|
200
303
|
const namespace = { ...runtimeNamespaceEnvironment(resolveRuntimeNamespace()) };
|
|
201
304
|
const authority = resolveWorkspacePaneTmuxAuthority();
|
|
@@ -203,7 +306,16 @@ export function openBrowser(url: string | undefined, options: OpenBrowserOptions
|
|
|
203
306
|
const run = createPinnedWorkspaceTmuxRunner(authority, {
|
|
204
307
|
timeoutMs: 10_000,
|
|
205
308
|
});
|
|
206
|
-
|
|
309
|
+
const result = createBrowserPane(
|
|
310
|
+
run,
|
|
311
|
+
executable,
|
|
312
|
+
url,
|
|
313
|
+
options,
|
|
314
|
+
process.env,
|
|
315
|
+
process.cwd(),
|
|
316
|
+
namespace,
|
|
317
|
+
);
|
|
318
|
+
return { result, run };
|
|
207
319
|
} catch (error) {
|
|
208
320
|
if (error instanceof IdeError) throw error;
|
|
209
321
|
throw new IdeError(
|
|
@@ -28,6 +28,7 @@ import { inflateSync } from "node:zlib";
|
|
|
28
28
|
import { PaneStreamLayoutSnapshotFrameSchemaZ, type WindowLinkTopology } from "@tmux-ide/contracts";
|
|
29
29
|
import type { TerminalDeliveryGraphics } from "../session-runtime/terminal-delivery-hub.ts";
|
|
30
30
|
import { z } from "zod";
|
|
31
|
+
import { logger } from "../../lib/log.ts";
|
|
31
32
|
import {
|
|
32
33
|
PANE_STREAM_MAX_HELD_DELTAS,
|
|
33
34
|
PANE_STREAM_MAX_OUTPUT_BYTES,
|
|
@@ -988,6 +989,11 @@ class PreAuthAdmission implements PaneStreamPreAuthAdmission {
|
|
|
988
989
|
this.#open = false;
|
|
989
990
|
this.#cancelDeadline();
|
|
990
991
|
const socket = this.#socket;
|
|
992
|
+
logger.info("pane-stream", "admission closed", {
|
|
993
|
+
code,
|
|
994
|
+
reason,
|
|
995
|
+
hostClientId: this.hostClientId,
|
|
996
|
+
});
|
|
991
997
|
this.#detach();
|
|
992
998
|
this.#onRelease(this);
|
|
993
999
|
if (socket) safeCloseSocket(socket, code, reason);
|
|
@@ -1386,6 +1392,12 @@ export class PaneStreamLiveConnection {
|
|
|
1386
1392
|
close(code = 1000, reason = "stream-closed"): void {
|
|
1387
1393
|
if (this.#closed) return;
|
|
1388
1394
|
this.#recordDiagnosticLifecycle("pane-stream-terminal", code, reason);
|
|
1395
|
+
logger.info("pane-stream", "stream closed", {
|
|
1396
|
+
code,
|
|
1397
|
+
reason,
|
|
1398
|
+
session: this.#descriptor.sessionName,
|
|
1399
|
+
clientId: this.#clientId,
|
|
1400
|
+
});
|
|
1389
1401
|
this.#closed = true;
|
|
1390
1402
|
for (const demand of this.#surfaceDemands.values()) demand.close();
|
|
1391
1403
|
this.#surfaceDemands.clear();
|
|
@@ -12,6 +12,7 @@ import { encodeTerminalProjectionCooperatively } from "@tmux-ide/core";
|
|
|
12
12
|
type DeliveryEnvelope = TerminalDeliveryEnvelope | TerminalProjectionEnvelope;
|
|
13
13
|
import type { KittyImage } from "../graphics/kitty-graphics.ts";
|
|
14
14
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
15
|
+
import { logger } from "../../lib/log.ts";
|
|
15
16
|
import {
|
|
16
17
|
MAX_RETAINED_NATIVE_BACKING_BYTES,
|
|
17
18
|
takeNativeSeedBacking,
|
|
@@ -512,6 +513,11 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
512
513
|
} catch (error) {
|
|
513
514
|
if (!(error instanceof StockCaptureTabUnavailableError)) throw error;
|
|
514
515
|
// Negotiation still settles this pane; no invented canonical seed is published.
|
|
516
|
+
logDeliveryFault(
|
|
517
|
+
{ paneId: semanticPaneId, deliveryWorkspaceName: address },
|
|
518
|
+
"source-closed",
|
|
519
|
+
"capture-unavailable",
|
|
520
|
+
);
|
|
515
521
|
await accept({
|
|
516
522
|
type: "terminal.delivery.fault",
|
|
517
523
|
reason: "source-closed",
|
|
@@ -697,6 +703,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
697
703
|
this.#cancelEncoding(client);
|
|
698
704
|
client.outgoing.length = 0;
|
|
699
705
|
client.inFlight = null;
|
|
706
|
+
logDeliveryFault(client, "source-closed", "source-restarted");
|
|
700
707
|
this.#enqueue(client, {
|
|
701
708
|
type: "terminal.delivery.fault",
|
|
702
709
|
reason: "source-closed",
|
|
@@ -2008,6 +2015,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
2008
2015
|
message: string,
|
|
2009
2016
|
selectionObservation: SemanticSelectionObservation | null = null,
|
|
2010
2017
|
): void {
|
|
2018
|
+
logDeliveryFault(client, reason);
|
|
2011
2019
|
if (this.#observability.enabled)
|
|
2012
2020
|
try {
|
|
2013
2021
|
const atMicros = this.#observability.nowMicros();
|
|
@@ -2694,3 +2702,20 @@ function joinBytes(chunks: readonly Uint8Array[]): Uint8Array {
|
|
|
2694
2702
|
}
|
|
2695
2703
|
return joined;
|
|
2696
2704
|
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* One info line per fault a client is sent, so a client's drop has a daemon-side
|
|
2708
|
+
* record. Ids and fixed reasons only: the fault message can carry source text.
|
|
2709
|
+
*/
|
|
2710
|
+
function logDeliveryFault(
|
|
2711
|
+
client: Pick<ClientState, "paneId" | "deliveryWorkspaceName">,
|
|
2712
|
+
reason: "state-too-large" | "source-closed" | "protocol-violation",
|
|
2713
|
+
cause?: "source-restarted" | "capture-unavailable",
|
|
2714
|
+
): void {
|
|
2715
|
+
logger.info("terminal-delivery", "fault sent to client", {
|
|
2716
|
+
reason,
|
|
2717
|
+
...(cause ? { cause } : {}),
|
|
2718
|
+
pane: client.paneId,
|
|
2719
|
+
workspace: client.deliveryWorkspaceName,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
package/skill/SKILL.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tmux-ide
|
|
3
|
+
description: "Use tmux-ide from a coding agent to report your own working/blocked/done status, read the agent fleet, message other panes, restore sessions after a crash, and set up worktrees and workspace layouts."
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# tmux-ide — Claude Code Skill
|
|
2
7
|
|
|
3
8
|
<!-- tmux-ide-skill-version: 2.6.0 -->
|
|
4
9
|
|
|
5
|
-
tmux-ide is **the open-source workspace for coding agents
|
|
10
|
+
tmux-ide is **the open-source tmux workspace for coding agents**. The
|
|
6
11
|
app (`tmux-ide app`) shows every agent across local and SSH machines with
|
|
7
12
|
ground-truth working/blocked/done status, and drives the live tmux session.
|
|
8
13
|
`tmux-ide adopt` adds the same status to plain tmux clients as tmux chrome: a
|