opencode-mux 0.1.0

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.
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ export const VERSION = "0.1.0";
2
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { OpenCodeClient, OpenCodeEvent } from "@opencode/client";
2
+ import type { Layout } from "./config.js";
3
+ import type { PaneTracker } from "./tracker.js";
4
+ import type { BorderStyles, Tmux } from "./tmux.js";
5
+ export interface WatchDeps {
6
+ client: OpenCodeClient;
7
+ tmux: Tmux;
8
+ tracker: PaneTracker;
9
+ log: (line: string) => void;
10
+ targetPane?: string;
11
+ layout: Layout;
12
+ mainPaneSize?: number;
13
+ /**
14
+ * Resolves the active theme's border styles. Applied at start and re-applied
15
+ * on tick whenever the resolved styles change; null clears the styles.
16
+ */
17
+ themeStyles?: {
18
+ current(): BorderStyles | null;
19
+ };
20
+ parentID: string | null;
21
+ paneArgv?: (sessionID: string) => string[];
22
+ tickEveryMs: number;
23
+ now?: () => number;
24
+ }
25
+ export interface Watcher {
26
+ adopt(): Promise<void>;
27
+ dispatch(event: OpenCodeEvent): Promise<void>;
28
+ tick(now: number): Promise<void>;
29
+ start(signal: AbortSignal): Promise<void>;
30
+ }
31
+ /** Pane command: attach the child session in the new pane via the minimal mini UI (no tab bar, no agent switcher). */
32
+ export declare function defaultPaneArgv(sessionID: string): string[];
33
+ /** Wires server events → tracker state machine → tmux actions. */
34
+ export declare function createWatcher(deps: WatchDeps): Watcher;
package/dist/watch.js ADDED
@@ -0,0 +1,166 @@
1
+ import { childTitle, classifyEvent } from "./session.js";
2
+ import { childSessions, events } from "./server.js";
3
+ import { currentPane } from "./tmux.js";
4
+ /** Pane command: attach the child session in the new pane via the minimal mini UI (no tab bar, no agent switcher). */
5
+ export function defaultPaneArgv(sessionID) {
6
+ return ["opencode", "mini", "-s", sessionID];
7
+ }
8
+ /** Wires server events → tracker state machine → tmux actions. */
9
+ export function createWatcher(deps) {
10
+ const targetPane = deps.targetPane ?? currentPane(process.env);
11
+ if (targetPane === null) {
12
+ throw new Error("opencode-mux: watcher requires TMUX_PANE (run inside tmux)");
13
+ }
14
+ const paneArgv = deps.paneArgv ?? defaultPaneArgv;
15
+ const now = deps.now ?? Date.now;
16
+ const log = deps.log;
17
+ const openPane = async (sessionID, title) => {
18
+ const argv = paneArgv(sessionID);
19
+ const result = await deps.tmux.splitPane({ targetPane, layout: deps.layout, mainPaneSize: deps.mainPaneSize, argv });
20
+ if (!result.ok || result.paneId === null) {
21
+ log(`action failed: split-window ${argv.join(" ")} — ${result.error ?? "unknown error"}`);
22
+ return null;
23
+ }
24
+ log(`opened pane ${result.paneId} for session ${sessionID} (${title})`);
25
+ return result.paneId;
26
+ };
27
+ /** Reserve and split a new pane for the session, then record the open phase. */
28
+ const openTracked = async (sessionID, title) => {
29
+ if (!deps.tracker.reserve(sessionID))
30
+ return;
31
+ const paneId = await openPane(sessionID, title);
32
+ if (paneId === null) {
33
+ deps.tracker.remove(sessionID);
34
+ return;
35
+ }
36
+ deps.tracker.open(sessionID, paneId, now());
37
+ };
38
+ const applyAction = async (action) => {
39
+ if (action.kind !== "close")
40
+ return;
41
+ const result = await deps.tmux.killPane(action.paneId);
42
+ if (!result.ok) {
43
+ log(`action failed: kill-pane ${action.paneId} — ${result.error ?? "unknown error"}`);
44
+ }
45
+ };
46
+ const applyTick = async (nowMs) => {
47
+ for (const action of deps.tracker.tick(nowMs)) {
48
+ await applyAction(action);
49
+ }
50
+ };
51
+ /** JSON of the styles last applied to the window (null when none/cleared). */
52
+ let appliedThemeSig = null;
53
+ /**
54
+ * Applies the resolved theme's border styles, or clears them when the theme
55
+ * became null. No tmux calls when the provider is absent or unchanged;
56
+ * failures are logged at most once per change and never fail the watcher.
57
+ */
58
+ const syncTheme = async () => {
59
+ if (deps.themeStyles === undefined)
60
+ return;
61
+ let styles;
62
+ let sig;
63
+ try {
64
+ styles = deps.themeStyles.current();
65
+ sig = JSON.stringify(styles);
66
+ }
67
+ catch (error) {
68
+ log(`styling failed: ${String(error)}`);
69
+ appliedThemeSig = "error";
70
+ return;
71
+ }
72
+ if (sig === appliedThemeSig)
73
+ return;
74
+ appliedThemeSig = sig;
75
+ if (styles === null) {
76
+ const result = await deps.tmux.clearBorderStyles(targetPane);
77
+ if (!result.ok)
78
+ log(`styling failed: clear — ${result.error ?? "unknown error"}`);
79
+ return;
80
+ }
81
+ const result = await deps.tmux.applyBorderStyles(targetPane, styles);
82
+ if (!result.ok)
83
+ log(`styling failed: ${result.error ?? "unknown error"}`);
84
+ };
85
+ const watcher = {
86
+ async adopt() {
87
+ try {
88
+ const active = await deps.client.session.active();
89
+ const children = await childSessions(deps.client, deps.parentID);
90
+ for (const info of children) {
91
+ // Only currently-executing children need a pane. Finished children
92
+ // from previous runs are skipped outright: opening one just to close
93
+ // it again flashed panes at startup (their panes were already closed
94
+ // when they completed).
95
+ if (!(info.id in active))
96
+ continue;
97
+ await openTracked(info.id, childTitle(info.title, info.id));
98
+ }
99
+ await applyTick(now());
100
+ }
101
+ catch (error) {
102
+ log(`adopt failed: ${String(error)}`);
103
+ }
104
+ },
105
+ async dispatch(event) {
106
+ const cls = classifyEvent(event);
107
+ if (cls.kind === "created") {
108
+ if (cls.parentID === undefined)
109
+ return;
110
+ if (deps.parentID !== null && cls.parentID !== deps.parentID)
111
+ return;
112
+ await openTracked(cls.sessionID, childTitle(cls.title, cls.slug));
113
+ return;
114
+ }
115
+ if (cls.kind === "terminal") {
116
+ deps.tracker.markTerminal(cls.sessionID, now());
117
+ return;
118
+ }
119
+ if (cls.kind === "busy") {
120
+ deps.tracker.markBusy(cls.sessionID);
121
+ }
122
+ },
123
+ async tick(nowMs) {
124
+ await applyTick(nowMs);
125
+ await syncTheme();
126
+ },
127
+ async start(signal) {
128
+ if (deps.themeStyles !== undefined) {
129
+ // Apply the initial value once, before the polling loop starts.
130
+ try {
131
+ const initial = deps.themeStyles.current();
132
+ appliedThemeSig = JSON.stringify(initial);
133
+ if (initial !== null) {
134
+ const result = await deps.tmux.applyBorderStyles(targetPane, initial);
135
+ if (!result.ok)
136
+ log(`styling failed: ${result.error ?? "unknown error"}`);
137
+ }
138
+ }
139
+ catch (error) {
140
+ log(`styling failed: ${String(error)}`);
141
+ appliedThemeSig = "error";
142
+ }
143
+ }
144
+ const timer = setInterval(() => void watcher.tick(now()), deps.tickEveryMs);
145
+ timer.unref();
146
+ signal.addEventListener("abort", () => clearInterval(timer), { once: true });
147
+ try {
148
+ await watcher.adopt();
149
+ const stream = await events(deps.client, signal);
150
+ for await (const event of stream) {
151
+ await watcher.dispatch(event);
152
+ await watcher.tick(now());
153
+ }
154
+ }
155
+ catch (error) {
156
+ if (!signal.aborted)
157
+ log(`watcher error: ${String(error)}`);
158
+ }
159
+ finally {
160
+ clearInterval(timer);
161
+ }
162
+ },
163
+ };
164
+ return watcher;
165
+ }
166
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "opencode-mux",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in opencode wrapper that opens tmux panes for subagent sessions automatically",
5
+ "type": "module",
6
+ "bin": { "opencode-mux": "./dist/bin.js" },
7
+ "files": ["dist"],
8
+ "engines": { "node": ">=20" },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "check": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "smoke": "tsx scripts/e2e-smoke.ts",
14
+ "prepublishOnly": "npm run check && npm test && npm run build"
15
+ },
16
+ "dependencies": {
17
+ "@opencode/client": "^2.0.6",
18
+ "@opencode/client-legacy": "npm:@opencode/client@2.0.5"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.10.0",
22
+ "tsx": "^4.19.0",
23
+ "typescript": "^5.6.0",
24
+ "vitest": "^4.1.11"
25
+ },
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/DemonGPT/opencode-mux"
30
+ },
31
+ "publishConfig": { "access": "public" },
32
+ "keywords": ["opencode", "tmux", "subagent", "multiplexer", "wrapper", "tui"]
33
+ }