tmux-ide 2.6.0 → 2.6.1
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 +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/src/agent-explain.ts +270 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +336 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +195 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/postinstall.js +26 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified push channel for clients — single WebSocket carrying session,
|
|
3
|
+
* workspace, project-registry, terminal, and config change signals.
|
|
4
|
+
*
|
|
5
|
+
* Endpoint: `/ws/events` (mounted by the daemon's HTTP server).
|
|
6
|
+
*
|
|
7
|
+
* Wire protocol: see `src/schemas/ws-events.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The orchestrator/task/chat event feed moved out of tmux-ide (agent
|
|
10
|
+
* coordination now lives in sfora.ai), so this channel only carries
|
|
11
|
+
* session-control signals.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RawData, WebSocket } from "ws";
|
|
15
|
+
import { discoverSessions, buildOverviews, buildProjectDetail } from "./discovery.ts";
|
|
16
|
+
import { projectRegistryEmitter } from "../lib/project-registry.ts";
|
|
17
|
+
import { getDefaultWorkspaceRegistry } from "../lib/workspace-registry.ts";
|
|
18
|
+
import type { ServerFrame, ClientFrame } from "../schemas/ws-events.ts";
|
|
19
|
+
import type { Workspace } from "@tmux-ide/contracts";
|
|
20
|
+
|
|
21
|
+
const WS_OPEN = 1;
|
|
22
|
+
const KEEPALIVE_INTERVAL_MS = 25_000;
|
|
23
|
+
const SESSIONS_POLL_MS = 2_000;
|
|
24
|
+
|
|
25
|
+
interface WsLike {
|
|
26
|
+
readyState: number;
|
|
27
|
+
send(data: string): void;
|
|
28
|
+
close(code?: number, reason?: string): void;
|
|
29
|
+
on(event: "message", listener: (data: RawData | string, isBinary: boolean) => void): this;
|
|
30
|
+
on(event: "close", listener: () => void): this;
|
|
31
|
+
on(event: "error", listener: (err: Error) => void): this;
|
|
32
|
+
off?(event: string, listener: (...args: unknown[]) => void): this;
|
|
33
|
+
removeListener?(event: string, listener: (...args: unknown[]) => void): this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Module-level globals for cross-connection broadcasts (sessions.changed,
|
|
37
|
+
// projects.changed, init.* job updates). All clients receive these.
|
|
38
|
+
interface ClientHandle {
|
|
39
|
+
broadcastSessionsChanged(): void;
|
|
40
|
+
broadcastProjectsChanged(): void;
|
|
41
|
+
broadcastInitOutput(jobId: string, chunk: string, done?: boolean): void;
|
|
42
|
+
broadcastInitError(jobId: string, message: string): void;
|
|
43
|
+
broadcastActionComplete(name: string, result: unknown): void;
|
|
44
|
+
broadcastConfigChanged(sessionName: string): void;
|
|
45
|
+
broadcastTerminalsChanged(sessionName: string): void;
|
|
46
|
+
}
|
|
47
|
+
const allClients = new Set<ClientHandle>();
|
|
48
|
+
|
|
49
|
+
let sessionsPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
50
|
+
let lastSessionsHash = "";
|
|
51
|
+
let projectRegistryListener: (() => void) | null = null;
|
|
52
|
+
|
|
53
|
+
function snapshotSessionsHash(): string {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(
|
|
56
|
+
discoverSessions()
|
|
57
|
+
.map((s) => s.name)
|
|
58
|
+
.sort(),
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ensureSessionsPoller(): void {
|
|
66
|
+
if (sessionsPollTimer) return;
|
|
67
|
+
lastSessionsHash = snapshotSessionsHash();
|
|
68
|
+
sessionsPollTimer = setInterval(() => {
|
|
69
|
+
const hash = snapshotSessionsHash();
|
|
70
|
+
if (hash === lastSessionsHash) return;
|
|
71
|
+
lastSessionsHash = hash;
|
|
72
|
+
for (const client of allClients) client.broadcastSessionsChanged();
|
|
73
|
+
}, SESSIONS_POLL_MS);
|
|
74
|
+
sessionsPollTimer.unref?.();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function maybeStopSessionsPoller(): void {
|
|
78
|
+
if (allClients.size > 0 || !sessionsPollTimer) return;
|
|
79
|
+
clearInterval(sessionsPollTimer);
|
|
80
|
+
sessionsPollTimer = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe (lazily) to the project-registry emitter and fan changes out to
|
|
85
|
+
* every connected ws client. The listener is registered on the first client
|
|
86
|
+
* and removed when the last one disconnects so we never leak.
|
|
87
|
+
*/
|
|
88
|
+
function ensureProjectRegistryListener(): void {
|
|
89
|
+
if (projectRegistryListener) return;
|
|
90
|
+
const listener = (): void => {
|
|
91
|
+
for (const client of allClients) client.broadcastProjectsChanged();
|
|
92
|
+
};
|
|
93
|
+
projectRegistryListener = listener;
|
|
94
|
+
projectRegistryEmitter.on("change", listener);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function maybeStopProjectRegistryListener(): void {
|
|
98
|
+
if (allClients.size > 0 || !projectRegistryListener) return;
|
|
99
|
+
projectRegistryEmitter.off("change", projectRegistryListener);
|
|
100
|
+
projectRegistryListener = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Push an `init.output` chunk to every connected client. Called by the
|
|
105
|
+
* REST handler that runs `tmux-ide init`; clients filter by `jobId`.
|
|
106
|
+
*/
|
|
107
|
+
export function broadcastInitOutput(jobId: string, chunk: string, done?: boolean): void {
|
|
108
|
+
for (const client of allClients) client.broadcastInitOutput(jobId, chunk, done);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Push an `init.error` frame to every connected client.
|
|
113
|
+
*/
|
|
114
|
+
export function broadcastInitError(jobId: string, message: string): void {
|
|
115
|
+
for (const client of allClients) client.broadcastInitError(jobId, message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Push an `action.complete` frame to every connected client. Called by the
|
|
120
|
+
* v2 action dispatcher after a handler succeeds — clients use it to
|
|
121
|
+
* invalidate caches without polling.
|
|
122
|
+
*/
|
|
123
|
+
export function broadcastActionComplete(name: string, result: unknown): void {
|
|
124
|
+
for (const client of allClients) client.broadcastActionComplete(name, result);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function broadcastConfigChanged(sessionName: string): void {
|
|
128
|
+
for (const client of allClients) client.broadcastConfigChanged(sessionName);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function broadcastTerminalsChanged(sessionName: string): void {
|
|
132
|
+
for (const client of allClients) client.broadcastTerminalsChanged(sessionName);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function rawDataToText(data: RawData | string): string {
|
|
136
|
+
if (typeof data === "string") return data;
|
|
137
|
+
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
138
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
139
|
+
if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
|
|
140
|
+
return Buffer.from(data as Uint8Array).toString("utf8");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build the snapshot payload pushed to a client when they subscribe to a
|
|
145
|
+
* session — the live project + pane state.
|
|
146
|
+
*/
|
|
147
|
+
export function buildSessionSnapshot(sessionName: string): unknown | null {
|
|
148
|
+
const session = discoverSessions().find((s) => s.name === sessionName);
|
|
149
|
+
if (!session) return null;
|
|
150
|
+
return { project: buildProjectDetail(session) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Wire a single WebSocket connection. Tracks per-session subscriptions and
|
|
155
|
+
* tears all listeners down on close — no leaks.
|
|
156
|
+
*/
|
|
157
|
+
export function handleWsEventsConnection(socket: WebSocket | WsLike): void {
|
|
158
|
+
const ws = socket as WsLike;
|
|
159
|
+
const subscriptions = new Set<string>();
|
|
160
|
+
let closed = false;
|
|
161
|
+
|
|
162
|
+
const send = (frame: ServerFrame): void => {
|
|
163
|
+
if (closed || ws.readyState !== WS_OPEN) return;
|
|
164
|
+
try {
|
|
165
|
+
ws.send(JSON.stringify(frame));
|
|
166
|
+
} catch {
|
|
167
|
+
// peer went away mid-send; close path will clean up
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const broadcastSessionsChanged = (): void => {
|
|
172
|
+
send({ type: "sessions.changed" });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const broadcastProjectsChanged = (): void => {
|
|
176
|
+
send({ type: "projects.changed" });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const broadcastInitOutputForClient = (jobId: string, chunk: string, done?: boolean): void => {
|
|
180
|
+
const frame: ServerFrame =
|
|
181
|
+
done === undefined
|
|
182
|
+
? { type: "init.output", jobId, chunk }
|
|
183
|
+
: { type: "init.output", jobId, chunk, done };
|
|
184
|
+
send(frame);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const broadcastInitErrorForClient = (jobId: string, message: string): void => {
|
|
188
|
+
send({ type: "init.error", jobId, message });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const broadcastActionCompleteForClient = (name: string, result: unknown): void => {
|
|
192
|
+
send({ type: "action.complete", name, result });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const broadcastConfigChangedForClient = (sessionName: string): void => {
|
|
196
|
+
send({ type: "config.changed", sessionName });
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const broadcastTerminalsChangedForClient = (sessionName: string): void => {
|
|
200
|
+
send({ type: "terminals.changed", sessionName } as ServerFrame);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Per-connection subscription to the current workspace-registry singleton.
|
|
204
|
+
const workspaceRegistry = getDefaultWorkspaceRegistry();
|
|
205
|
+
const unsubWorkspaceAdded = workspaceRegistry.on("workspace.added", (workspace) =>
|
|
206
|
+
send({ type: "workspace.added", workspace: workspace as Workspace }),
|
|
207
|
+
);
|
|
208
|
+
const unsubWorkspaceRemoved = workspaceRegistry.on("workspace.removed", (name) =>
|
|
209
|
+
send({ type: "workspace.removed", name: name as string }),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const clientHandle: ClientHandle = {
|
|
213
|
+
broadcastSessionsChanged,
|
|
214
|
+
broadcastProjectsChanged,
|
|
215
|
+
broadcastInitOutput: broadcastInitOutputForClient,
|
|
216
|
+
broadcastInitError: broadcastInitErrorForClient,
|
|
217
|
+
broadcastActionComplete: broadcastActionCompleteForClient,
|
|
218
|
+
broadcastConfigChanged: broadcastConfigChangedForClient,
|
|
219
|
+
broadcastTerminalsChanged: broadcastTerminalsChangedForClient,
|
|
220
|
+
};
|
|
221
|
+
allClients.add(clientHandle);
|
|
222
|
+
ensureSessionsPoller();
|
|
223
|
+
ensureProjectRegistryListener();
|
|
224
|
+
|
|
225
|
+
// Server-initiated keepalive — mirrors the SSE behavior so middle-boxes
|
|
226
|
+
// don't reap the connection.
|
|
227
|
+
const keepalive = setInterval(() => {
|
|
228
|
+
send({ type: "pong" });
|
|
229
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
230
|
+
keepalive.unref?.();
|
|
231
|
+
|
|
232
|
+
const subscribe = (sessionName: string): void => {
|
|
233
|
+
if (subscriptions.has(sessionName)) return;
|
|
234
|
+
const session = discoverSessions().find((s) => s.name === sessionName);
|
|
235
|
+
subscriptions.add(sessionName);
|
|
236
|
+
if (session) {
|
|
237
|
+
const data = buildSessionSnapshot(sessionName);
|
|
238
|
+
if (data) {
|
|
239
|
+
send({ type: "snapshot", sessionName, data: data as Record<string, unknown> });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const unsubscribe = (sessionName: string): void => {
|
|
245
|
+
subscriptions.delete(sessionName);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const cleanup = (): void => {
|
|
249
|
+
if (closed) return;
|
|
250
|
+
closed = true;
|
|
251
|
+
clearInterval(keepalive);
|
|
252
|
+
allClients.delete(clientHandle);
|
|
253
|
+
subscriptions.clear();
|
|
254
|
+
unsubWorkspaceAdded();
|
|
255
|
+
unsubWorkspaceRemoved();
|
|
256
|
+
maybeStopSessionsPoller();
|
|
257
|
+
maybeStopProjectRegistryListener();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
ws.on("message", (data) => {
|
|
261
|
+
if (closed) return;
|
|
262
|
+
let parsed: ClientFrame | null = null;
|
|
263
|
+
try {
|
|
264
|
+
const obj = JSON.parse(rawDataToText(data));
|
|
265
|
+
if (obj && typeof obj === "object" && typeof (obj as { type?: unknown }).type === "string") {
|
|
266
|
+
parsed = obj as ClientFrame;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
return; // ignore non-JSON / malformed frames
|
|
270
|
+
}
|
|
271
|
+
if (!parsed) return;
|
|
272
|
+
|
|
273
|
+
if (parsed.type === "subscribe") {
|
|
274
|
+
for (const name of parsed.sessions) subscribe(name);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (parsed.type === "unsubscribe") {
|
|
278
|
+
for (const name of parsed.sessions) unsubscribe(name);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (parsed.type === "ping") {
|
|
282
|
+
send({ type: "pong" });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
ws.on("close", cleanup);
|
|
288
|
+
ws.on("error", cleanup);
|
|
289
|
+
|
|
290
|
+
// Send the initial hello — caller knows which sessions exist without
|
|
291
|
+
// a separate REST round-trip.
|
|
292
|
+
try {
|
|
293
|
+
const sessions = discoverSessions();
|
|
294
|
+
send({ type: "hello", sessions: buildOverviews(sessions) });
|
|
295
|
+
} catch {
|
|
296
|
+
send({ type: "hello", sessions: [] });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Test-only hook to shut down the global sessions poller.
|
|
302
|
+
*/
|
|
303
|
+
export function _stopSessionsPollerForTests(): void {
|
|
304
|
+
if (!sessionsPollTimer) return;
|
|
305
|
+
clearInterval(sessionsPollTimer);
|
|
306
|
+
sessionsPollTimer = null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Test-only hook to detach the registry listener.
|
|
311
|
+
*/
|
|
312
|
+
export function _detachProjectRegistryListenerForTests(): void {
|
|
313
|
+
if (!projectRegistryListener) return;
|
|
314
|
+
projectRegistryEmitter.off("change", projectRegistryListener);
|
|
315
|
+
projectRegistryListener = null;
|
|
316
|
+
}
|