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.
Files changed (209) hide show
  1. package/bin/cli.js +13 -5
  2. package/bin/cli.ts +1 -1
  3. package/bunfig.toml +4 -0
  4. package/package.json +11 -3
  5. package/packages/contracts/package.json +22 -0
  6. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  7. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  8. package/packages/contracts/src/actions-contract.ts +310 -0
  9. package/packages/contracts/src/actions-errors.ts +41 -0
  10. package/packages/contracts/src/domain.ts +36 -0
  11. package/packages/contracts/src/ide-config.ts +170 -0
  12. package/packages/contracts/src/index.ts +24 -0
  13. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  14. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  15. package/packages/contracts/src/terminals.ts +116 -0
  16. package/packages/contracts/src/tmux.ts +60 -0
  17. package/packages/contracts/src/workspace.ts +67 -0
  18. package/packages/daemon/src/agent-explain.ts +270 -0
  19. package/packages/daemon/src/attach.ts +20 -0
  20. package/packages/daemon/src/bin.ts +4 -0
  21. package/packages/daemon/src/canonical.ts +7 -0
  22. package/packages/daemon/src/cli.ts +499 -0
  23. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  24. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  25. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  26. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  27. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  28. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  29. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  30. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  31. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  32. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  33. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  34. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  35. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  36. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  37. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  38. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  39. package/packages/daemon/src/command-center/discovery.ts +96 -0
  40. package/packages/daemon/src/command-center/index.ts +31 -0
  41. package/packages/daemon/src/command-center/schemas.ts +85 -0
  42. package/packages/daemon/src/command-center/server.ts +1260 -0
  43. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  44. package/packages/daemon/src/config.ts +549 -0
  45. package/packages/daemon/src/detect.ts +248 -0
  46. package/packages/daemon/src/doctor.ts +242 -0
  47. package/packages/daemon/src/embed.ts +5 -0
  48. package/packages/daemon/src/index.ts +12 -0
  49. package/packages/daemon/src/init.ts +211 -0
  50. package/packages/daemon/src/inspect.ts +178 -0
  51. package/packages/daemon/src/js-yaml.d.ts +10 -0
  52. package/packages/daemon/src/launch.ts +349 -0
  53. package/packages/daemon/src/lib/active-projects.ts +49 -0
  54. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  55. package/packages/daemon/src/lib/app-config.ts +336 -0
  56. package/packages/daemon/src/lib/app-settings.ts +53 -0
  57. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  58. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  59. package/packages/daemon/src/lib/auth/types.ts +2 -0
  60. package/packages/daemon/src/lib/auth-token.ts +5 -0
  61. package/packages/daemon/src/lib/authorship.ts +280 -0
  62. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  63. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  64. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  65. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  66. package/packages/daemon/src/lib/daemon.ts +79 -0
  67. package/packages/daemon/src/lib/dot-path.ts +17 -0
  68. package/packages/daemon/src/lib/errors.ts +67 -0
  69. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  70. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  71. package/packages/daemon/src/lib/log.ts +134 -0
  72. package/packages/daemon/src/lib/output.ts +76 -0
  73. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  74. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  75. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  76. package/packages/daemon/src/lib/project-probe.ts +92 -0
  77. package/packages/daemon/src/lib/project-registry.ts +296 -0
  78. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  79. package/packages/daemon/src/lib/session-options.ts +100 -0
  80. package/packages/daemon/src/lib/shell.ts +8 -0
  81. package/packages/daemon/src/lib/sizes.ts +36 -0
  82. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  83. package/packages/daemon/src/lib/slugify.ts +10 -0
  84. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  85. package/packages/daemon/src/lib/update-check.ts +298 -0
  86. package/packages/daemon/src/lib/update.ts +158 -0
  87. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  88. package/packages/daemon/src/lib/worktree.ts +289 -0
  89. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  90. package/packages/daemon/src/ls.ts +40 -0
  91. package/packages/daemon/src/restart.ts +24 -0
  92. package/packages/daemon/src/restore.ts +514 -0
  93. package/packages/daemon/src/schemas/domain.ts +2 -0
  94. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  95. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  96. package/packages/daemon/src/schemas/index.ts +59 -0
  97. package/packages/daemon/src/schemas/inspect.ts +67 -0
  98. package/packages/daemon/src/schemas/registry.ts +55 -0
  99. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  100. package/packages/daemon/src/send.ts +171 -0
  101. package/packages/daemon/src/server/README.md +15 -0
  102. package/packages/daemon/src/server/index.ts +74 -0
  103. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  104. package/packages/daemon/src/server/standalone.ts +18 -0
  105. package/packages/daemon/src/server/ws-route.ts +483 -0
  106. package/packages/daemon/src/status.ts +59 -0
  107. package/packages/daemon/src/stop.ts +28 -0
  108. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  109. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  110. package/packages/daemon/src/terminal/README.md +92 -0
  111. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  112. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  113. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  114. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  115. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  116. package/packages/daemon/src/tui/chrome/notify.ts +195 -0
  117. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  118. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  119. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  120. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  121. package/packages/daemon/src/tui/chrome/updater.ts +428 -0
  122. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  123. package/packages/daemon/src/tui/compiled.ts +113 -0
  124. package/packages/daemon/src/tui/detect/classify.ts +193 -0
  125. package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
  126. package/packages/daemon/src/tui/detect/manifest.ts +184 -0
  127. package/packages/daemon/src/tui/detect/manifests.ts +226 -0
  128. package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
  129. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  130. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  131. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  132. package/packages/daemon/src/tui/main.ts +70 -0
  133. package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
  134. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  135. package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
  136. package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
  137. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  138. package/packages/daemon/src/tui/team/entry.ts +11 -0
  139. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  140. package/packages/daemon/src/tui/team/home.ts +170 -0
  141. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  142. package/packages/daemon/src/tui/team/input.ts +34 -0
  143. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  144. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  145. package/packages/daemon/src/tui/team/nav.ts +31 -0
  146. package/packages/daemon/src/tui/team/preview.ts +34 -0
  147. package/packages/daemon/src/tui/team/projects.ts +191 -0
  148. package/packages/daemon/src/tui/team/report.ts +73 -0
  149. package/packages/daemon/src/tui/team/sessions.ts +344 -0
  150. package/packages/daemon/src/tui/team/tree.ts +62 -0
  151. package/packages/daemon/src/types.ts +13 -0
  152. package/packages/daemon/src/ui/index.ts +32 -0
  153. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  154. package/packages/daemon/src/ui/types.ts +91 -0
  155. package/packages/daemon/src/ui/web/base.css +80 -0
  156. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  157. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  158. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  159. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  160. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  161. package/packages/daemon/src/ui/web/index.ts +27 -0
  162. package/packages/daemon/src/ui/web/render.ts +77 -0
  163. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  164. package/packages/daemon/src/validate.ts +217 -0
  165. package/packages/daemon/src/widgets/changes/README.md +3 -0
  166. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  167. package/packages/daemon/src/widgets/config/README.md +3 -0
  168. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  169. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  170. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  171. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  172. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  173. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  174. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  175. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  176. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  177. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  178. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  179. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  180. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  181. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  182. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  183. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  184. package/packages/daemon/src/widgets/preview/README.md +3 -0
  185. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  186. package/packages/daemon/src/widgets/resolve.ts +121 -0
  187. package/packages/daemon/src/widgets/setup/README.md +3 -0
  188. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  189. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  190. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  191. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  192. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  193. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  194. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  195. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  196. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  197. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  198. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  199. package/packages/tmux-bridge/package.json +22 -0
  200. package/packages/tmux-bridge/src/errors.ts +28 -0
  201. package/packages/tmux-bridge/src/index.ts +31 -0
  202. package/packages/tmux-bridge/src/monitor.ts +77 -0
  203. package/packages/tmux-bridge/src/panes.ts +136 -0
  204. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  205. package/packages/tmux-bridge/src/runner.ts +91 -0
  206. package/packages/tmux-bridge/src/sessions.ts +126 -0
  207. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  208. package/packages/tmux-bridge/src/targeting.ts +90 -0
  209. package/scripts/postinstall.js +26 -2
@@ -0,0 +1,59 @@
1
+ export {
2
+ PaneSchema,
3
+ RowSchema,
4
+ ThemeConfigSchema,
5
+ OrchestratorYamlConfigSchema,
6
+ IdeConfigSchema,
7
+ PaneActionSchema,
8
+ SessionStateSchema,
9
+ } from "./ide-config.ts";
10
+
11
+ export type {
12
+ Pane,
13
+ Row,
14
+ ThemeConfig,
15
+ OrchestratorYamlConfig,
16
+ IdeConfig,
17
+ PaneAction,
18
+ SessionState,
19
+ } from "./ide-config.ts";
20
+
21
+ export { SessionOverviewSchemaZ, PaneInfoSchemaZ } from "./domain.ts";
22
+
23
+ export type { SessionOverview, PaneInfo } from "./domain.ts";
24
+
25
+ export { ClientFrameSchemaZ, ServerFrameSchemaZ, SessionSnapshotSchemaZ } from "./ws-events.ts";
26
+
27
+ export type { ClientFrame, ServerFrame, SessionSnapshot } from "./ws-events.ts";
28
+
29
+ export {
30
+ RegisteredProjectSchemaZ,
31
+ RegisterProjectRequestSchemaZ,
32
+ InitProjectRequestSchemaZ,
33
+ ProjectTemplateSchemaZ,
34
+ } from "./registry.ts";
35
+
36
+ export type {
37
+ RegisteredProject,
38
+ RegisterProjectRequest,
39
+ InitProjectRequest,
40
+ ProjectTemplate,
41
+ } from "./registry.ts";
42
+
43
+ export { FilesystemEntrySchemaZ, FilesystemBrowseResultSchemaZ } from "./filesystem.ts";
44
+
45
+ export type { FilesystemEntry, FilesystemBrowseResult } from "./filesystem.ts";
46
+
47
+ export {
48
+ ProjectInspectDetectedSchemaZ,
49
+ ProjectInspectSchemaZ,
50
+ InspectFilesystemRequestSchemaZ,
51
+ OnboardProjectRequestSchemaZ,
52
+ } from "./inspect.ts";
53
+
54
+ export type {
55
+ ProjectInspectDetected,
56
+ ProjectInspect,
57
+ InspectFilesystemRequest,
58
+ OnboardProjectRequest,
59
+ } from "./inspect.ts";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Project inspect + onboard contracts. Used by the dashboard's "Add project"
3
+ * dialog to inspect an arbitrary directory (without registering it) and to
4
+ * onboard a directory that has no `ide.yml` yet (compose, write, register).
5
+ *
6
+ * The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
7
+ */
8
+
9
+ import { z } from "zod";
10
+
11
+ export const ProjectInspectDetectedSchemaZ = z.object({
12
+ /** Detected package manager from lockfile, or `null`. */
13
+ packageManager: z.enum(["pnpm", "npm", "yarn", "bun"]).nullable(),
14
+ /** Detected frameworks (e.g. `["next", "convex"]`). Empty array when none. */
15
+ frameworks: z.array(z.string()),
16
+ /** Suggested dev command (e.g. `pnpm dev`). `null` if no dev script found. */
17
+ devCommand: z.string().nullable(),
18
+ /** Suggested test command (e.g. `pnpm test`). `null` if no test script found. */
19
+ testCommand: z.string().nullable(),
20
+ });
21
+ export type ProjectInspectDetected = z.infer<typeof ProjectInspectDetectedSchemaZ>;
22
+
23
+ export const ProjectInspectSchemaZ = z.object({
24
+ /** Sanitized basename of the directory — safe to use as a tmux session name. */
25
+ name: z.string(),
26
+ /** Absolute, canonical path to the directory. */
27
+ dir: z.string(),
28
+ /** Whether `<dir>/ide.yml` exists. */
29
+ hasIdeYml: z.boolean(),
30
+ /** Git remote origin URL, or `null` if not a git repo / no origin / probe failed. */
31
+ gitOrigin: z.string().nullable(),
32
+ /** Current git branch, or `null` if not a git repo / detached HEAD / probe failed. */
33
+ gitBranch: z.string().nullable(),
34
+ /** Detected stack signals (reuses `tmux-ide detect` logic). */
35
+ detected: ProjectInspectDetectedSchemaZ,
36
+ });
37
+ export type ProjectInspect = z.infer<typeof ProjectInspectSchemaZ>;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // REST request bodies
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const InspectFilesystemRequestSchemaZ = z.object({
44
+ dir: z.string().min(1),
45
+ });
46
+ export type InspectFilesystemRequest = z.infer<typeof InspectFilesystemRequestSchemaZ>;
47
+
48
+ export const OnboardProjectRequestSchemaZ = z.object({
49
+ dir: z.string().min(1),
50
+ /** Optional override for the project name — defaults to inspect.name. */
51
+ name: z.string().min(1).optional(),
52
+ /** 1, 2, or 3 — how many Claude panes to scaffold in the top row. */
53
+ agents: z.number().int().min(1).max(3),
54
+ /**
55
+ * Optional per-agent pane titles. When provided, length must equal
56
+ * `agents`; the server uses these as `title:` for the Claude panes
57
+ * instead of the canonical `Lead`/`Teammate N`/`Claude N` defaults.
58
+ */
59
+ agentNames: z.array(z.string().min(1)).optional(),
60
+ /** Dev server command (e.g. `pnpm dev`). Omit / null to skip the dev pane. */
61
+ devCommand: z.string().min(1).nullable().optional(),
62
+ /** Test command (e.g. `pnpm test`). Currently informational; stored for later. */
63
+ testCommand: z.string().min(1).nullable().optional(),
64
+ /** Lint command (e.g. `pnpm lint`). Currently informational; stored for later. */
65
+ lintCommand: z.string().min(1).nullable().optional(),
66
+ });
67
+ export type OnboardProjectRequest = z.infer<typeof OnboardProjectRequestSchemaZ>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Project registry contracts. The dashboard manages a list of projects the
3
+ * user has registered with tmux-ide; this schema is the wire shape exchanged
4
+ * over `/api/projects` and the `/ws/events` WebSocket channel.
5
+ *
6
+ * The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
7
+ * Add new fields by appending optional properties; do not rename existing
8
+ * fields without bumping a major.
9
+ */
10
+
11
+ import { z } from "zod";
12
+
13
+ export const RegisteredProjectSchemaZ = z.object({
14
+ /** Unique registry key. Defaults to `basename(dir)`; collisions resolved by appending `-2`, `-3`, … */
15
+ name: z.string(),
16
+ /** Absolute path to the project directory. */
17
+ dir: z.string(),
18
+ /** Whether `<dir>/ide.yml` exists; refreshed on register and on `probe()`. */
19
+ hasIdeYml: z.boolean(),
20
+ /** Git remote origin URL, or `null` if not a git repo / no origin / probe failed. */
21
+ gitOrigin: z.string().nullable(),
22
+ /** Current git branch, or `null` if not a git repo / detached HEAD / probe failed. */
23
+ gitBranch: z.string().nullable(),
24
+ /** ISO-8601 timestamp the project was first registered. */
25
+ registeredAt: z.string(),
26
+ });
27
+
28
+ export type RegisteredProject = z.infer<typeof RegisteredProjectSchemaZ>;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // REST request bodies
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export const RegisterProjectRequestSchemaZ = z.object({
35
+ dir: z.string().min(1),
36
+ name: z.string().min(1).optional(),
37
+ });
38
+ export type RegisterProjectRequest = z.infer<typeof RegisterProjectRequestSchemaZ>;
39
+
40
+ export const InitProjectRequestSchemaZ = z.object({
41
+ dir: z.string().min(1),
42
+ template: z.string().min(1).optional(),
43
+ });
44
+ export type InitProjectRequest = z.infer<typeof InitProjectRequestSchemaZ>;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Template metadata (returned by GET /api/projects/templates)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export const ProjectTemplateSchemaZ = z.object({
51
+ id: z.string(),
52
+ label: z.string(),
53
+ description: z.string(),
54
+ });
55
+ export type ProjectTemplate = z.infer<typeof ProjectTemplateSchemaZ>;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Protocol contracts for the unified `/ws/events` WebSocket channel.
3
+ *
4
+ * Single push channel clients subscribe to for session / pane / workspace
5
+ * changes. One socket replaces a fan of individual SSE streams that hit
6
+ * Chrome's 6-per-origin HTTP/1.1 limit.
7
+ *
8
+ * Add new frame variants by appending to the union; do not rename existing
9
+ * fields.
10
+ */
11
+
12
+ import { z } from "zod";
13
+ import { SessionOverviewSchemaZ } from "./domain.ts";
14
+ import { WorkspaceAddedFrameSchemaZ, WorkspaceRemovedFrameSchemaZ } from "@tmux-ide/contracts";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Snapshot payload — mirrors what `/api/project/<name>/stream` already pushes
18
+ // as its `snapshot` SSE event. Kept loose (passthrough) so that adding fields
19
+ // on the producer side does not require shipping schema updates lock-step
20
+ // with consumers. Consumers should validate fields they actually read.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const SessionSnapshotSchemaZ = z.record(z.string(), z.unknown());
24
+ export type SessionSnapshot = z.infer<typeof SessionSnapshotSchemaZ>;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Client → Server frames
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const SubscribeFrameZ = z.object({
31
+ type: z.literal("subscribe"),
32
+ sessions: z.array(z.string()),
33
+ });
34
+
35
+ const UnsubscribeFrameZ = z.object({
36
+ type: z.literal("unsubscribe"),
37
+ sessions: z.array(z.string()),
38
+ });
39
+
40
+ const PingFrameZ = z.object({
41
+ type: z.literal("ping"),
42
+ });
43
+
44
+ export const ClientFrameSchemaZ = z.discriminatedUnion("type", [
45
+ SubscribeFrameZ,
46
+ UnsubscribeFrameZ,
47
+ PingFrameZ,
48
+ ]);
49
+
50
+ export type ClientFrame = z.infer<typeof ClientFrameSchemaZ>;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Server → Client frames
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const HelloFrameZ = z.object({
57
+ type: z.literal("hello"),
58
+ sessions: z.array(SessionOverviewSchemaZ),
59
+ });
60
+
61
+ const SnapshotFrameZ = z.object({
62
+ type: z.literal("snapshot"),
63
+ sessionName: z.string(),
64
+ data: SessionSnapshotSchemaZ,
65
+ });
66
+
67
+ const SessionsChangedFrameZ = z.object({
68
+ type: z.literal("sessions.changed"),
69
+ });
70
+
71
+ // Project-registry has changed (register / unregister / re-probe / init success).
72
+ // Clients re-fetch GET /api/projects.
73
+ const ProjectsChangedFrameZ = z.object({
74
+ type: z.literal("projects.changed"),
75
+ });
76
+
77
+ // Streaming output from `tmux-ide init` invoked via POST /api/projects/init.
78
+ // `chunk` is a single line of stdout/stderr (newline stripped). `done: true`
79
+ // is set on the final frame for the job; no more `init.output` frames will
80
+ // follow for this jobId.
81
+ const InitOutputFrameZ = z.object({
82
+ type: z.literal("init.output"),
83
+ jobId: z.string(),
84
+ chunk: z.string(),
85
+ done: z.boolean().optional(),
86
+ });
87
+
88
+ // Init job failed. Terminal — no `init.output` follow-ups.
89
+ const InitErrorFrameZ = z.object({
90
+ type: z.literal("init.error"),
91
+ jobId: z.string(),
92
+ message: z.string(),
93
+ });
94
+
95
+ const PongFrameZ = z.object({
96
+ type: z.literal("pong"),
97
+ });
98
+
99
+ // Broadcast after a successful v2 action dispatch. Clients use these frames to
100
+ // invalidate caches without ad-hoc refetches (e.g. invalidate the project list
101
+ // after `project.launch` succeeds). `name` matches the action name in
102
+ // `command-center/actions/contract.ts`. `result` is loose at the schema layer
103
+ // because each action has its own result shape.
104
+ const ActionCompleteFrameZ = z.object({
105
+ type: z.literal("action.complete"),
106
+ name: z.string(),
107
+ result: z.unknown(),
108
+ });
109
+
110
+ const ConfigChangedFrameZ = z.object({
111
+ type: z.literal("config.changed"),
112
+ sessionName: z.string(),
113
+ });
114
+
115
+ const TerminalsChangedFrameZ = z.object({
116
+ type: z.literal("terminals.changed"),
117
+ sessionName: z.string(),
118
+ });
119
+
120
+ export const ServerFrameSchemaZ = z.discriminatedUnion("type", [
121
+ HelloFrameZ,
122
+ SnapshotFrameZ,
123
+ SessionsChangedFrameZ,
124
+ ProjectsChangedFrameZ,
125
+ InitOutputFrameZ,
126
+ InitErrorFrameZ,
127
+ PongFrameZ,
128
+ ActionCompleteFrameZ,
129
+ ConfigChangedFrameZ,
130
+ TerminalsChangedFrameZ,
131
+ WorkspaceAddedFrameSchemaZ,
132
+ WorkspaceRemovedFrameSchemaZ,
133
+ ]);
134
+
135
+ export type ServerFrame = z.infer<typeof ServerFrameSchemaZ>;
@@ -0,0 +1,171 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolve, join } from "node:path";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { getSessionName } from "./lib/yaml-io.ts";
5
+ import { getSessionState } from "@tmux-ide/tmux-bridge";
6
+ import {
7
+ listSessionPanes,
8
+ sendCommand,
9
+ sendText,
10
+ getPaneBusyStatus,
11
+ type PaneInfo,
12
+ type PaneBusyStatus,
13
+ } from "./widgets/lib/pane-comms.ts";
14
+ import { IdeError } from "./lib/errors.ts";
15
+
16
+ export const LONG_MESSAGE_THRESHOLD = 150;
17
+
18
+ /**
19
+ * Write a long message to a dispatch file and return the short trigger command.
20
+ * Returns null if message is short enough to send directly.
21
+ */
22
+ export function writeDispatchFile(
23
+ dir: string,
24
+ paneId: string,
25
+ message: string,
26
+ ): { filePath: string; triggerCmd: string } | null {
27
+ if (message.length <= LONG_MESSAGE_THRESHOLD) return null;
28
+ const dispatchDir = join(dir, ".tasks", "dispatch");
29
+ if (!existsSync(dispatchDir)) mkdirSync(dispatchDir, { recursive: true });
30
+ const paneSlug = paneId.replace("%", "");
31
+ const filename = `send-${paneSlug}-${Date.now()}-${randomUUID().slice(0, 8)}.md`;
32
+ const filePath = join(dispatchDir, filename);
33
+ writeFileSync(filePath, message);
34
+ return { filePath, triggerCmd: `Read and execute: .tasks/dispatch/${filename}` };
35
+ }
36
+
37
+ interface SendOptions {
38
+ json?: boolean;
39
+ to?: string;
40
+ message?: string;
41
+ noEnter?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Resolve a target string to a pane. Priority:
46
+ * 1. Exact pane ID (%N)
47
+ * 2. @ide_name match
48
+ * 3. Exact title match
49
+ * 4. Role match (lead, teammate, planner)
50
+ * 5. Case-insensitive partial title match
51
+ */
52
+ export function resolvePane(panes: PaneInfo[], target: string): PaneInfo | null {
53
+ // 1. Exact pane ID
54
+ if (target.startsWith("%")) {
55
+ return panes.find((p) => p.id === target) ?? null;
56
+ }
57
+
58
+ // 2. @ide_name match
59
+ const byName = panes.find((p) => p.name === target);
60
+ if (byName) return byName;
61
+
62
+ // 3. Exact title match
63
+ const byTitle = panes.find((p) => p.title === target);
64
+ if (byTitle) return byTitle;
65
+
66
+ // 4. Role match
67
+ const lower = target.toLowerCase();
68
+ if (["lead", "teammate", "planner"].includes(lower)) {
69
+ const byRole = panes.find((p) => p.role === lower);
70
+ if (byRole) return byRole;
71
+ }
72
+
73
+ // 5. Case-insensitive partial title match
74
+ const byPattern = panes.find((p) => p.title.toLowerCase().includes(lower));
75
+ if (byPattern) return byPattern;
76
+
77
+ return null;
78
+ }
79
+
80
+ function prepareMessage(message: string, busyStatus: PaneBusyStatus): string {
81
+ if (busyStatus === "agent") {
82
+ // Collapse multiline to single line for Claude Code TUI
83
+ // Prevents paste preview that requires manual Enter
84
+ return message.replace(/\n+/g, " ").trim();
85
+ }
86
+ return message;
87
+ }
88
+
89
+ export async function send(targetDir: string | undefined, opts: SendOptions): Promise<void> {
90
+ const dir = resolve(targetDir ?? ".");
91
+ const { name: session } = getSessionName(dir);
92
+ const { json, to: target, message: rawMessage, noEnter } = opts;
93
+
94
+ if (!target) {
95
+ throw new IdeError("Missing target. Usage: tmux-ide send <target> <message>", {
96
+ code: "USAGE",
97
+ });
98
+ }
99
+
100
+ if (!rawMessage) {
101
+ throw new IdeError("Missing message. Usage: tmux-ide send <target> <message>", {
102
+ code: "USAGE",
103
+ });
104
+ }
105
+
106
+ // Verify session is running
107
+ const state = getSessionState(session);
108
+ if (!state.running) {
109
+ throw new IdeError(`Session "${session}" is not running`, {
110
+ code: "SESSION_NOT_FOUND",
111
+ });
112
+ }
113
+
114
+ const panes = listSessionPanes(session);
115
+ const pane = resolvePane(panes, target);
116
+ if (!pane) {
117
+ const available = panes
118
+ .map((p) => {
119
+ const label = p.name ?? p.title;
120
+ return ` ${p.id} ${label}${p.role ? ` (${p.role})` : ""}`;
121
+ })
122
+ .join("\n");
123
+ throw new IdeError(`Pane "${target}" not found.\n\nAvailable panes:\n${available}`, {
124
+ code: "PANE_NOT_FOUND",
125
+ });
126
+ }
127
+
128
+ const busyStatus = getPaneBusyStatus(session, pane.id);
129
+ const message = prepareMessage(rawMessage, busyStatus);
130
+
131
+ let sentViaFile = false;
132
+ if (noEnter) {
133
+ sendText(session, pane.id, message);
134
+ } else {
135
+ const dispatch = writeDispatchFile(dir, pane.id, message);
136
+ if (dispatch) {
137
+ sendCommand(session, pane.id, dispatch.triggerCmd);
138
+ sentViaFile = true;
139
+ } else {
140
+ sendCommand(session, pane.id, message);
141
+ }
142
+ }
143
+
144
+ const result = {
145
+ ok: true,
146
+ session,
147
+ target: {
148
+ paneId: pane.id,
149
+ name: pane.name,
150
+ title: pane.title,
151
+ role: pane.role,
152
+ },
153
+ message,
154
+ busyStatus,
155
+ sentViaFile,
156
+ ...(busyStatus === "agent" ? { warning: "agent_busy" } : {}),
157
+ };
158
+
159
+ if (json) {
160
+ console.log(JSON.stringify(result, null, 2));
161
+ return;
162
+ }
163
+
164
+ const label = pane.name ?? pane.title;
165
+ const preview = message.length > 60 ? message.slice(0, 60) + "..." : message;
166
+ console.log(`Sent to "${label}" (${pane.id}): ${preview}`);
167
+
168
+ if (busyStatus === "agent") {
169
+ console.log("Warning: agent appears busy. Message sent anyway.");
170
+ }
171
+ }
@@ -0,0 +1,15 @@
1
+ # src/server/
2
+
3
+ v2.5.0 unified server. Single-binary HTTP + WS surface.
4
+
5
+ Slice 1 scope: WebSocket PTY bridge endpoint at `/ws/pty/:id`. Spawns a shell via node-pty, bridges to wterm in the browser.
6
+
7
+ See `plans/v2.5.0-protocol.md` for the wire protocol.
8
+ See `plans/v2.5.0-architecture.md` for the v2.5.0 design.
9
+
10
+ ## Files (slice 1)
11
+
12
+ - `index.ts` — Hono app, server bootstrap (`tmux-ide server` entry point)
13
+ - `pty-bridge.ts` — node-pty bridge: spawn, write, resize, lifecycle, cleanup
14
+ - `ws-route.ts` — WebSocket route handler implementing the protocol
15
+ - `*.test.ts` — Vitest unit tests
@@ -0,0 +1,74 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import { parse } from "node:url";
3
+ import { Hono } from "hono";
4
+ import { getRequestListener } from "@hono/node-server";
5
+ import { WebSocketServer } from "ws";
6
+ import { handlePtyWebSocket, shutdownPtyBridges } from "./ws-route.ts";
7
+
8
+ const DEFAULT_PORT = 6070;
9
+
10
+ export interface StartedTmuxIdeServer {
11
+ port: number;
12
+ server: Server;
13
+ close(): Promise<void>;
14
+ }
15
+
16
+ export function resolvePort(port?: number): number {
17
+ const raw = port ?? Number.parseInt(process.env.TMUX_IDE_PORT ?? String(DEFAULT_PORT), 10);
18
+ if (!Number.isInteger(raw) || raw <= 0) {
19
+ throw new Error(`Invalid server port: ${String(port ?? process.env.TMUX_IDE_PORT)}`);
20
+ }
21
+ return raw;
22
+ }
23
+
24
+ export function createApp(): Hono {
25
+ const app = new Hono();
26
+
27
+ app.get("/", (c) => c.text("tmux-ide server"));
28
+ app.get("/health", (c) => c.json({ ok: true }));
29
+
30
+ return app;
31
+ }
32
+
33
+ export async function start(port?: number): Promise<StartedTmuxIdeServer> {
34
+ const resolvedPort = resolvePort(port);
35
+ const app = createApp();
36
+ const server = createServer(getRequestListener(app.fetch));
37
+ const ptyWss = new WebSocketServer({ noServer: true });
38
+
39
+ server.on("upgrade", (req, socket, head) => {
40
+ const { pathname } = parse(req.url ?? "/", true);
41
+ const match = pathname?.match(/^\/ws\/pty\/([^/]+)$/);
42
+
43
+ if (!match) {
44
+ socket.destroy();
45
+ return;
46
+ }
47
+
48
+ const id = decodeURIComponent(match[1] ?? "");
49
+ ptyWss.handleUpgrade(req, socket, head, (ws) => {
50
+ handlePtyWebSocket(ws, id);
51
+ });
52
+ });
53
+
54
+ await new Promise<void>((resolve, reject) => {
55
+ server.once("error", reject);
56
+ server.listen(resolvedPort, "0.0.0.0", () => {
57
+ server.off("error", reject);
58
+ resolve();
59
+ });
60
+ });
61
+
62
+ console.log(`tmux-ide server listening on http://0.0.0.0:${resolvedPort}`);
63
+
64
+ return {
65
+ port: resolvedPort,
66
+ server,
67
+ close: () =>
68
+ new Promise<void>((resolve, reject) => {
69
+ shutdownPtyBridges();
70
+ ptyWss.close();
71
+ server.close((err) => (err ? reject(err) : resolve()));
72
+ }),
73
+ };
74
+ }