tmux-ide 2.6.1 → 2.8.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.
Files changed (154) hide show
  1. package/README.md +36 -14
  2. package/bin/cli.js +4174 -1235
  3. package/bin/cli.ts +426 -72
  4. package/package.json +10 -6
  5. package/packages/contracts/src/__tests__/control.test.ts +154 -0
  6. package/packages/contracts/src/control.ts +217 -0
  7. package/packages/contracts/src/index.ts +1 -0
  8. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  9. package/packages/daemon/dist/agent-explain.js +19 -3
  10. package/packages/daemon/dist/control/client.d.ts +23 -0
  11. package/packages/daemon/dist/control/client.js +105 -0
  12. package/packages/daemon/dist/control/dispatch.d.ts +34 -0
  13. package/packages/daemon/dist/control/dispatch.js +83 -0
  14. package/packages/daemon/dist/control/fanout.d.ts +19 -0
  15. package/packages/daemon/dist/control/fanout.js +37 -0
  16. package/packages/daemon/dist/control/frames.d.ts +23 -0
  17. package/packages/daemon/dist/control/frames.js +37 -0
  18. package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
  19. package/packages/daemon/dist/control/lifecycle.js +114 -0
  20. package/packages/daemon/dist/control/server.d.ts +16 -0
  21. package/packages/daemon/dist/control/server.js +214 -0
  22. package/packages/daemon/dist/control/verbs.d.ts +11 -0
  23. package/packages/daemon/dist/control/verbs.js +91 -0
  24. package/packages/daemon/dist/doctor.d.ts +18 -0
  25. package/packages/daemon/dist/doctor.js +105 -15
  26. package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
  27. package/packages/daemon/dist/lib/agent-discovery.js +29 -14
  28. package/packages/daemon/dist/lib/app-config.d.ts +106 -0
  29. package/packages/daemon/dist/lib/app-config.js +104 -5
  30. package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
  31. package/packages/daemon/dist/lib/manifest-pack.js +232 -0
  32. package/packages/daemon/dist/lib/state-home.d.ts +2 -0
  33. package/packages/daemon/dist/lib/state-home.js +12 -0
  34. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  35. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  36. package/packages/daemon/dist/lib/update-check.js +5 -0
  37. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
  38. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
  43. package/packages/daemon/dist/restore.d.ts +35 -8
  44. package/packages/daemon/dist/restore.js +52 -15
  45. package/packages/daemon/dist/send.d.ts +33 -1
  46. package/packages/daemon/dist/send.js +32 -19
  47. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  48. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  49. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  50. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  51. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  52. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  53. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  54. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  55. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  56. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  57. package/packages/daemon/src/agent-explain.ts +34 -6
  58. package/packages/daemon/src/control/client.ts +128 -0
  59. package/packages/daemon/src/control/dispatch.ts +107 -0
  60. package/packages/daemon/src/control/fanout.ts +44 -0
  61. package/packages/daemon/src/control/frames.ts +40 -0
  62. package/packages/daemon/src/control/lifecycle.ts +151 -0
  63. package/packages/daemon/src/control/server.ts +237 -0
  64. package/packages/daemon/src/control/verbs.ts +118 -0
  65. package/packages/daemon/src/doctor.ts +113 -28
  66. package/packages/daemon/src/lib/agent-discovery.ts +53 -13
  67. package/packages/daemon/src/lib/app-config.ts +194 -5
  68. package/packages/daemon/src/lib/manifest-pack.ts +255 -0
  69. package/packages/daemon/src/lib/state-home.ts +13 -0
  70. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  71. package/packages/daemon/src/lib/update-check.ts +5 -0
  72. package/packages/daemon/src/restore.ts +53 -15
  73. package/packages/daemon/src/send.ts +55 -21
  74. package/packages/daemon/src/tui/chrome/events.ts +4 -4
  75. package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
  76. package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
  77. package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
  78. package/packages/daemon/src/tui/chrome/notify.ts +737 -52
  79. package/packages/daemon/src/tui/chrome/updater.ts +318 -30
  80. package/packages/daemon/src/tui/compiled.ts +11 -3
  81. package/packages/daemon/src/tui/detect/classify.ts +49 -0
  82. package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
  83. package/packages/daemon/src/tui/detect/manifest.ts +39 -3
  84. package/packages/daemon/src/tui/detect/manifests.ts +395 -33
  85. package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
  86. package/packages/daemon/src/tui/detect/session-id.ts +503 -0
  87. package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
  88. package/packages/daemon/src/tui/main.ts +13 -1
  89. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  90. package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
  91. package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
  92. package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
  93. package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
  94. package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
  95. package/packages/daemon/src/tui/mirror/attention.ts +110 -0
  96. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  97. package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
  98. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  99. package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
  100. package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
  101. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  102. package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
  103. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  104. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  105. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  106. package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
  107. package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
  108. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  109. package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
  110. package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
  111. package/packages/daemon/src/tui/mirror/palette.ts +564 -0
  112. package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
  113. package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
  114. package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
  115. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  116. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  117. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  118. package/packages/daemon/src/tui/mirror/selection.ts +376 -0
  119. package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
  120. package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
  121. package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
  122. package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
  123. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  124. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  125. package/packages/daemon/src/tui/mirror/theme.ts +45 -0
  126. package/packages/daemon/src/tui/team/entry.ts +34 -7
  127. package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
  128. package/packages/daemon/src/tui/team/report.ts +11 -1
  129. package/packages/daemon/src/tui/team/sessions.ts +184 -17
  130. package/packages/daemon/src/tui/team/wait.ts +144 -0
  131. package/scripts/build-macos-notifier.mjs +160 -0
  132. package/scripts/build-tui.mjs +11 -4
  133. package/scripts/perf-mirror.mjs +313 -0
  134. package/scripts/postinstall.js +8 -1
  135. package/scripts/prepublish-check.mjs +37 -1
  136. package/scripts/publish-tap.sh +55 -0
  137. package/skill/SKILL.md +110 -2
  138. package/templates/AGENTS.md +14 -7
  139. package/templates/agent-team-monorepo.yml +8 -0
  140. package/templates/agent-team-nextjs.yml +8 -0
  141. package/templates/agent-team.yml +10 -0
  142. package/templates/convex.yml +2 -0
  143. package/templates/default.yml +11 -5
  144. package/templates/go.yml +4 -0
  145. package/templates/missions.yml +6 -0
  146. package/templates/nextjs.yml +4 -0
  147. package/templates/python.yml +4 -0
  148. package/templates/skills/backend.md +5 -12
  149. package/templates/skills/frontend.md +5 -12
  150. package/templates/skills/general-worker.md +5 -12
  151. package/templates/skills/researcher.md +7 -12
  152. package/templates/skills/reviewer.md +7 -16
  153. package/templates/vite.yml +4 -0
  154. package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The v1 verb handlers — each one a THIN adapter from validated params to
3
+ * the SAME data-layer functions the CLI cases call (report/sessions/wait/
4
+ * send/agent-explain/lifecycle). No fleet logic lives here; if a verb needs
5
+ * logic a CLI case has inline, the logic moves to the data layer and both
6
+ * point at it (that's how `wait` and `send` got their shared cores).
7
+ */
8
+ import { agentsParamsSchema, explainParamsSchema, restartAgentParamsSchema, sendParamsSchema, spawnParamsSchema, stopAgentParamsSchema, waitParamsSchema, } from "@tmux-ide/contracts";
9
+ import { buildReport } from "../agent-explain.js";
10
+ import { deliverMessage } from "../send.js";
11
+ import { toFleetJson } from "../tui/team/report.js";
12
+ import { listTeamProjects } from "../tui/team/projects.js";
13
+ import { listTeamSessions } from "../tui/team/sessions.js";
14
+ import { waitForAgentStatus, waitForOutputMatch } from "../tui/team/wait.js";
15
+ import { ControlVerbError } from "./dispatch.js";
16
+ import { resolveLaunchCommand, restartAgent, spawnAgent, stopAgent } from "./lifecycle.js";
17
+ /** Validate `params` against a verb's schema, or answer `bad-request`. */
18
+ function parse(schema, params) {
19
+ const result = schema.safeParse(params);
20
+ if (!result.success) {
21
+ const issue = result.error.issues[0];
22
+ const at = issue?.path?.length ? ` at ${issue.path.join(".")}` : "";
23
+ throw new ControlVerbError("bad-request", `invalid params${at}: ${issue?.message ?? "?"}`);
24
+ }
25
+ return result.data;
26
+ }
27
+ /**
28
+ * Build the handler map. `tracker` is the server's ONE persistent status
29
+ * tracker (shared with the event tick) so `fleet`/`agents` see the
30
+ * cross-tick `done` transition exactly like the chrome updater does —
31
+ * a fresh tracker per call could never observe working→idle.
32
+ */
33
+ export function createVerbHandlers(ctx) {
34
+ return {
35
+ fleet: () => toFleetJson(listTeamProjects(ctx.tracker)),
36
+ agents: (params) => {
37
+ const p = parse(agentsParamsSchema, params);
38
+ const sessions = listTeamSessions(ctx.tracker);
39
+ const scoped = p.session ? sessions.filter((s) => s.name === p.session) : sessions;
40
+ if (p.session && scoped.length === 0) {
41
+ throw new ControlVerbError("not-found", `no session "${p.session}"`);
42
+ }
43
+ return { agents: scoped.flatMap((s) => s.agents ?? []) };
44
+ },
45
+ send: (params) => {
46
+ const p = parse(sendParamsSchema, params);
47
+ return deliverMessage(p);
48
+ },
49
+ wait: async (params) => {
50
+ const p = parse(waitParamsSchema, params);
51
+ if (p.kind === "output") {
52
+ try {
53
+ new RegExp(p.match);
54
+ }
55
+ catch (err) {
56
+ throw new ControlVerbError("bad-request", `invalid match regex: ${err.message}`);
57
+ }
58
+ }
59
+ const result = p.kind === "agent-status"
60
+ ? await waitForAgentStatus(p.session, p.status, { timeoutMs: p.timeoutMs })
61
+ : await waitForOutputMatch(p.target, p.match, { timeoutMs: p.timeoutMs });
62
+ if (!result.ok) {
63
+ const what = p.kind === "agent-status"
64
+ ? `"${p.session}" to reach status "${p.status}"`
65
+ : `${p.target} output to match /${p.match}/`;
66
+ throw new ControlVerbError("timeout", `timed out after ${result.timedOutAfterMs}ms waiting for ${what}`);
67
+ }
68
+ return result;
69
+ },
70
+ spawn: (params) => {
71
+ const p = parse(spawnParamsSchema, params);
72
+ return spawnAgent({ ...p, command: resolveLaunchCommand(p) });
73
+ },
74
+ "restart-agent": (params) => {
75
+ const p = parse(restartAgentParamsSchema, params);
76
+ return restartAgent(p.paneId, resolveLaunchCommand(p));
77
+ },
78
+ "stop-agent": (params) => {
79
+ const p = parse(stopAgentParamsSchema, params);
80
+ return stopAgent(p.paneId);
81
+ },
82
+ explain: (params) => {
83
+ const p = parse(explainParamsSchema, params);
84
+ return buildReport(p.target);
85
+ },
86
+ subscribe: (_params, verbCtx) => {
87
+ verbCtx.subscribe();
88
+ return { subscribed: true, events: ["agent-status"] };
89
+ },
90
+ };
91
+ }
@@ -15,6 +15,24 @@ interface CheckResult {
15
15
  * active but there's no lifecycle integration yet.
16
16
  */
17
17
  export declare function agentIntegrationRows(agents: DiscoveredAgent[]): CheckResult[];
18
+ /**
19
+ * PURE — the "Claude hooks target writable" row from observed facts. Both
20
+ * `integration install claude` and the npm postinstall write the settings
21
+ * file; surface a permissions problem BEFORE an install fails halfway.
22
+ * Optional: a machine without Claude Code shouldn't fail doctor over this.
23
+ */
24
+ export declare function hooksTargetRow(facts: {
25
+ settingsPath: string;
26
+ fileExists: boolean;
27
+ writable: boolean;
28
+ }): CheckResult;
29
+ /**
30
+ * PURE — native notifier health, shown only when macOS notifications are ON.
31
+ * Release packages always include the helper; absence means an incomplete or
32
+ * old installation, which can still fall back to the unbranded AppleScript path.
33
+ * Optional — informational, never fails doctor.
34
+ */
35
+ export declare function notifierRow(present: boolean): CheckResult;
18
36
  export declare function doctor({ json, }?: {
19
37
  json?: boolean;
20
38
  }): Promise<void>;
@@ -1,11 +1,13 @@
1
1
  import { execSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { accessSync, constants, existsSync } from "node:fs";
3
3
  import { resolve, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getCurrentVersion, getUpdateStatus } from "./lib/update-check.js";
6
6
  import { installedSkillVersion } from "./lib/skill-sync.js";
7
7
  import { discoverAgents, presentAgents } from "./lib/agent-discovery.js";
8
8
  import { findCompiledTui, isBunAvailable } from "./tui/compiled.js";
9
+ import { claudeSettingsPath } from "./tui/integrations/claude.js";
10
+ import { readNotificationPrefs, resolveNativeMacosNotifierPath } from "./tui/chrome/notify.js";
9
11
  /**
10
12
  * PURE — the "agent integrations" doctor rows, one per DISCOVERED agent (absent
11
13
  * agents produce nothing — no noise). All rows are optional (informational, they
@@ -19,12 +21,18 @@ export function agentIntegrationRows(agents) {
19
21
  return presentAgents(agents).map((agent) => {
20
22
  const label = `agent: ${agent.id}`;
21
23
  if (agent.integration) {
24
+ // What installing actually buys, honestly per mechanism: claude's hooks
25
+ // give ground-truth status (and record resume ids); opencode's plugin
26
+ // records resume ids only.
27
+ const benefit = agent.capture === "hooks"
28
+ ? "for ground-truth status"
29
+ : "to record session ids for restore --resume-agents";
22
30
  return agent.installed
23
31
  ? { label, pass: true, detail: "integration installed ✓", optional: true }
24
32
  : {
25
33
  label,
26
34
  pass: false,
27
- detail: `found on PATH — run \`tmux-ide integration install ${agent.id}\` for ground-truth status`,
35
+ detail: `found on PATH — run \`tmux-ide integration install ${agent.id}\` ${benefit}`,
28
36
  optional: true,
29
37
  };
30
38
  }
@@ -36,6 +44,52 @@ export function agentIntegrationRows(agents) {
36
44
  };
37
45
  });
38
46
  }
47
+ /**
48
+ * PURE — the "Claude hooks target writable" row from observed facts. Both
49
+ * `integration install claude` and the npm postinstall write the settings
50
+ * file; surface a permissions problem BEFORE an install fails halfway.
51
+ * Optional: a machine without Claude Code shouldn't fail doctor over this.
52
+ */
53
+ export function hooksTargetRow(facts) {
54
+ const label = "Claude hooks target writable";
55
+ if (facts.writable) {
56
+ return {
57
+ label,
58
+ pass: true,
59
+ detail: facts.fileExists ? facts.settingsPath : `${facts.settingsPath} (will be created)`,
60
+ optional: true,
61
+ };
62
+ }
63
+ return {
64
+ label,
65
+ pass: false,
66
+ detail: `cannot write ${facts.settingsPath} — fix its permissions (chown/chmod), or point TMUX_IDE_CLAUDE_SETTINGS at a writable path`,
67
+ optional: true,
68
+ };
69
+ }
70
+ /**
71
+ * PURE — native notifier health, shown only when macOS notifications are ON.
72
+ * Release packages always include the helper; absence means an incomplete or
73
+ * old installation, which can still fall back to the unbranded AppleScript path.
74
+ * Optional — informational, never fails doctor.
75
+ */
76
+ export function notifierRow(present) {
77
+ const label = "native macOS notifications";
78
+ if (present) {
79
+ return {
80
+ label,
81
+ pass: true,
82
+ detail: "bundled — branded banners and click-to-jump are ready",
83
+ optional: true,
84
+ };
85
+ }
86
+ return {
87
+ label,
88
+ pass: false,
89
+ detail: "native helper missing — reinstall tmux-ide; unbranded AppleScript banners remain available",
90
+ optional: true,
91
+ };
92
+ }
39
93
  function check(label, fn, { optional = false } = {}) {
40
94
  try {
41
95
  const result = fn();
@@ -48,7 +102,12 @@ function check(label, fn, { optional = false } = {}) {
48
102
  export async function doctor({ json, } = {}) {
49
103
  const checks = [];
50
104
  checks.push(check("tmux installed", () => {
51
- execSync("which tmux", { stdio: "ignore" });
105
+ try {
106
+ execSync("which tmux", { stdio: "ignore" });
107
+ }
108
+ catch {
109
+ throw new Error("not found on PATH — install it (macOS: `brew install tmux`; Debian/Ubuntu: `sudo apt install tmux`)");
110
+ }
52
111
  return "found";
53
112
  }));
54
113
  checks.push(check("tmux version ≥ 3.0", () => {
@@ -103,18 +162,45 @@ export async function doctor({ json, } = {}) {
103
162
  }
104
163
  return "enabled";
105
164
  }, { optional: true }));
106
- checks.push(check("tailscale CLI", () => {
107
- const version = execSync("tailscale version", { encoding: "utf-8" }).trim().split("\n")[0];
108
- return version;
109
- }, { optional: true }));
110
- checks.push(check("ngrok CLI", () => {
111
- const version = execSync("ngrok version", { encoding: "utf-8" }).trim();
112
- return version;
113
- }, { optional: true }));
114
- checks.push(check("cloudflared CLI", () => {
115
- const version = execSync("cloudflared --version", { encoding: "utf-8" }).trim();
116
- return version;
117
- }, { optional: true }));
165
+ // Optional tunnel CLIs: probe quietly (a missing binary must not leak
166
+ // "command not found" stderr into doctor's own output) and fail with a
167
+ // plain hint rather than execSync's raw error.
168
+ const tunnelCli = (label, cmd) => check(label, () => {
169
+ try {
170
+ return execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] })
171
+ .trim()
172
+ .split("\n")[0];
173
+ }
174
+ catch {
175
+ throw new Error("not found (optional — used for remote access tunnels)");
176
+ }
177
+ }, { optional: true });
178
+ checks.push(tunnelCli("tailscale CLI", "tailscale version"));
179
+ checks.push(tunnelCli("ngrok CLI", "ngrok version"));
180
+ checks.push(tunnelCli("cloudflared CLI", "cloudflared --version"));
181
+ checks.push((() => {
182
+ // Claude hooks target: the settings file `integration install claude`
183
+ // (and the npm postinstall) will write. Writability of the file itself
184
+ // when it exists, else of its nearest existing ancestor directory.
185
+ const settingsPath = claudeSettingsPath();
186
+ const fileExists = existsSync(settingsPath);
187
+ let probe = fileExists ? settingsPath : dirname(settingsPath);
188
+ while (!existsSync(probe)) {
189
+ const parent = dirname(probe);
190
+ if (parent === probe)
191
+ break;
192
+ probe = parent;
193
+ }
194
+ let writable = false;
195
+ try {
196
+ accessSync(probe, constants.W_OK);
197
+ writable = true;
198
+ }
199
+ catch {
200
+ // leave writable=false — the row explains the fix
201
+ }
202
+ return hooksTargetRow({ settingsPath, fileExists, writable });
203
+ })());
118
204
  checks.push(check("tmux-ide up to date", () => {
119
205
  const current = getCurrentVersion();
120
206
  const { latest, updateAvailable } = getUpdateStatus({ currentVersion: current });
@@ -137,6 +223,10 @@ export async function doctor({ json, } = {}) {
137
223
  }
138
224
  return `in sync (v${installed})`;
139
225
  }, { optional: true }));
226
+ // Native branded sender: only when the macOS channel is actually on.
227
+ if (process.platform === "darwin" && readNotificationPrefs().macos) {
228
+ checks.push(notifierRow(resolveNativeMacosNotifierPath() !== null));
229
+ }
140
230
  // Agent integrations: one row per agent discovered on PATH (absent → no row).
141
231
  checks.push(...agentIntegrationRows(discoverAgents()));
142
232
  const allPass = checks.every((c) => c.pass || c.optional);
@@ -1,3 +1,16 @@
1
+ /**
2
+ * How a kind's `@agent_session_id` (the `restore --resume-agents` key) gets
3
+ * captured:
4
+ * - `"hooks"` — the agent's own lifecycle hooks stamp it (claude; needs
5
+ * `integration install`).
6
+ * - `"plugin"` — an in-process plugin stamps it (opencode; needs
7
+ * `integration install`).
8
+ * - `"probe"` — the chrome updater discovers it from the agent's own on-disk
9
+ * session state (codex, cursor; automatic, nothing to install).
10
+ * - `null` — no defensible capture surface (see
11
+ * {@link ../tui/detect/session-id.ts} for the per-kind evidence).
12
+ */
13
+ export type CaptureMechanism = "hooks" | "plugin" | "probe" | null;
1
14
  /** A coding agent tmux-ide knows how to detect. */
2
15
  export interface KnownAgent {
3
16
  /** Stable id — also the `tmux-ide integration <install|status> <id>` selector. */
@@ -6,11 +19,14 @@ export interface KnownAgent {
6
19
  bin: string;
7
20
  /** True → tmux-ide ships a lifecycle-integration installer for this agent. */
8
21
  integration: boolean;
22
+ /** How this kind's session id is captured for `restore --resume-agents`. */
23
+ capture: CaptureMechanism;
9
24
  }
10
25
  /**
11
26
  * The agents tmux-ide recognizes. `integration: true` means we HAVE an installer
12
- * (a real lifecycle hook → ground-truth pane state); the rest are detected via
13
- * screen-manifest scraping only, with no lifecycle hook yet.
27
+ * (claude's lifecycle hooks, opencode's plugin); the rest are detected via
28
+ * screen-manifest scraping only. `capture` records each kind's session-id
29
+ * story ({@link CaptureMechanism}).
14
30
  */
15
31
  export declare const KNOWN_AGENTS: readonly KnownAgent[];
16
32
  /** One probed agent: its registry facts plus what the PATH/integration probe found. */
@@ -27,6 +43,15 @@ export interface DiscoveredAgent {
27
43
  * integrate (there's nothing to install) and for any agent absent from PATH.
28
44
  */
29
45
  installed: boolean;
46
+ /** How this kind's session id is captured (copied from the registry). */
47
+ capture: CaptureMechanism;
48
+ /**
49
+ * Whether session-id capture is LIVE for this kind on this machine:
50
+ * `"probe"` capture is automatic whenever the binary is present; hook/plugin
51
+ * capture requires the integration to be installed; `null` capture is never
52
+ * active.
53
+ */
54
+ captureActive: boolean;
30
55
  }
31
56
  /** Resolve a binary to its absolute path, or null. Must never throw. */
32
57
  export type WhichRunner = (bin: string) => string | null;
@@ -17,17 +17,21 @@
17
17
  */
18
18
  import { execFileSync } from "node:child_process";
19
19
  import { claudeIntegrationStatus } from "../tui/integrations/claude.js";
20
+ import { opencodeIntegrationStatus } from "../tui/integrations/opencode.js";
20
21
  /**
21
22
  * The agents tmux-ide recognizes. `integration: true` means we HAVE an installer
22
- * (a real lifecycle hook → ground-truth pane state); the rest are detected via
23
- * screen-manifest scraping only, with no lifecycle hook yet.
23
+ * (claude's lifecycle hooks, opencode's plugin); the rest are detected via
24
+ * screen-manifest scraping only. `capture` records each kind's session-id
25
+ * story ({@link CaptureMechanism}).
24
26
  */
25
27
  export const KNOWN_AGENTS = [
26
- { id: "claude", bin: "claude", integration: true },
27
- { id: "codex", bin: "codex", integration: false },
28
- { id: "opencode", bin: "opencode", integration: false },
29
- { id: "gemini", bin: "gemini", integration: false },
30
- { id: "aider", bin: "aider", integration: false },
28
+ { id: "claude", bin: "claude", integration: true, capture: "hooks" },
29
+ { id: "codex", bin: "codex", integration: false, capture: "probe" },
30
+ { id: "opencode", bin: "opencode", integration: true, capture: "plugin" },
31
+ { id: "gemini", bin: "gemini", integration: false, capture: null },
32
+ { id: "aider", bin: "aider", integration: false, capture: null },
33
+ { id: "cursor", bin: "cursor-agent", integration: false, capture: "probe" },
34
+ { id: "copilot", bin: "copilot", integration: false, capture: null },
31
35
  ];
32
36
  /**
33
37
  * Default which-runner: `which <bin>`, hard-capped at 2s, swallowing every
@@ -51,15 +55,17 @@ const defaultWhich = (bin) => {
51
55
  }
52
56
  };
53
57
  /**
54
- * Default integration probe: only `claude` has an installer, so only it can be
55
- * "installed". Reads the real Claude settings; any failure degrades to false so
56
- * discovery never throws.
58
+ * Default integration probe: claude's hooks and opencode's plugin are the two
59
+ * shipped installers. Reads the real settings/plugin file; any failure degrades
60
+ * to false so discovery never throws.
57
61
  */
58
62
  const defaultIntegrationProbe = (agentId) => {
59
- if (agentId !== "claude")
60
- return false;
61
63
  try {
62
- return claudeIntegrationStatus().installed;
64
+ if (agentId === "claude")
65
+ return claudeIntegrationStatus().installed;
66
+ if (agentId === "opencode")
67
+ return opencodeIntegrationStatus().installed;
68
+ return false;
63
69
  }
64
70
  catch {
65
71
  return false;
@@ -76,7 +82,16 @@ export function discoverAgents(which = defaultWhich, isInstalled = defaultIntegr
76
82
  const path = which(agent.bin);
77
83
  const present = path !== null;
78
84
  const installed = present && agent.integration ? isInstalled(agent.id) : false;
79
- return { id: agent.id, bin: agent.bin, integration: agent.integration, path, installed };
85
+ const captureActive = agent.capture === "probe" ? present : agent.capture !== null ? installed : false;
86
+ return {
87
+ id: agent.id,
88
+ bin: agent.bin,
89
+ integration: agent.integration,
90
+ path,
91
+ installed,
92
+ capture: agent.capture,
93
+ captureActive,
94
+ };
80
95
  });
81
96
  }
82
97
  /** The subset of discovered agents actually present on PATH (path resolved). */
@@ -60,10 +60,29 @@ export interface AppUpdater {
60
60
  /** Write a disaster-recovery snapshot every N ticks (default 15). */
61
61
  snapshotEvery: number;
62
62
  }
63
+ /** The sound channel's states: ping sound on blocked only, on every ping, or never. */
64
+ export type NotificationSound = "blocked" | "all" | "none";
63
65
  /** Notification channel toggles. */
64
66
  export interface AppNotifications {
65
67
  toast: boolean;
66
68
  macos: boolean;
69
+ /**
70
+ * Terminal-native banners (M25.2): an OSC 9 (kitty: OSC 99) escape written to
71
+ * each eligible attached client's tty, so terminals that surface them (iTerm2,
72
+ * Ghostty, WezTerm, kitty) show a native notification even over ssh. Default
73
+ * true — terminals without support ignore the sequence entirely.
74
+ */
75
+ terminal: boolean;
76
+ /**
77
+ * Seconds the OS-LEVEL channels (banner / terminal escape / sound / BEL) wait
78
+ * before firing, re-verifying the pane is STILL in the notified state — a
79
+ * blocked→working flap inside the window fires nothing. 0 fires immediately.
80
+ * The in-app toast never waits. Tick-native: the delay rounds up to the
81
+ * updater's next tick at or after `delaySeconds`.
82
+ */
83
+ delaySeconds: number;
84
+ /** Sound channel (M25.2) — `afplay`/`paplay` + a BEL to eligible client ttys. */
85
+ sound: NotificationSound;
67
86
  }
68
87
  /** Restore behaviour toggles. */
69
88
  export interface AppRestore {
@@ -72,6 +91,14 @@ export interface AppRestore {
72
91
  /** Update-check toggles (consumed later by the update-flow card). */
73
92
  export interface AppUpdates {
74
93
  check: boolean;
94
+ /**
95
+ * Whether the daily update check ALSO refreshes the remote agent-detection
96
+ * manifest pack into `~/.tmux-ide/agent-detection/pack/` (M25.4). Default
97
+ * FALSE — auto-installing detection rules is opt-in; `tmux-ide update
98
+ * --manifests` always works explicitly. Requires `check: true` (the refresh
99
+ * rides the same daily throttle — no timer of its own).
100
+ */
101
+ manifests: boolean;
75
102
  }
76
103
  /** First-run welcome toggles. */
77
104
  export interface AppWelcome {
@@ -92,6 +119,55 @@ export interface AppIntegrations {
92
119
  */
93
120
  offer: boolean;
94
121
  }
122
+ /** The unified-app front-door decision (M22.6). */
123
+ export interface AppApp {
124
+ /**
125
+ * Whether bare `tmux-ide` (no ide.yml here, no `--team`) launches the unified
126
+ * app (`tmux-ide app`) instead of the classic team cockpit. Default false —
127
+ * the flip is opt-in until the default-entry decision is made. `tmux-ide team`
128
+ * (the explicit cockpit) and a project's `ide.yml` auto-launch are unaffected.
129
+ */
130
+ frontDoor: boolean;
131
+ /**
132
+ * Whether `tmux-ide app` runs HOSTED by default (M23.2): the app lives in an
133
+ * internal `_tmux-ide-app` tmux session and the invoking terminal attaches to
134
+ * it, so the cockpit survives the terminal and reattaches from anywhere (^q
135
+ * detaches instead of quitting). Default false — the same behavior as the
136
+ * explicit `--detachable` flag; `--detachable`/`--hosted` still force it on a
137
+ * single run.
138
+ */
139
+ detachable: boolean;
140
+ /**
141
+ * When a plain left drag on an app-mouse pane SELECTS locally instead of
142
+ * forwarding to the pane's app (M24.2). "agents" (default): panes our own
143
+ * detection matches to a fleet agent entry select; other app-mouse panes
144
+ * (vim/htop) forward. "always": every app-mouse pane selects on drag.
145
+ * "never": every app-mouse pane forwards (the pre-M24.2 behavior). A genuine
146
+ * click (press+release in one cell) is still forwarded on select-default
147
+ * panes; shift inverts a pane's default; the right-click pane menu overrides
148
+ * per pane for the session.
149
+ */
150
+ dragSelect: "agents" | "always" | "never";
151
+ /**
152
+ * Where a Terminal-surface "New agent" spawn starts (M24.1): `"pane"`
153
+ * (default) inherits the FOCUSED pane's current working directory —
154
+ * `#{pane_current_path}`, so the agent lands where you are — while
155
+ * `"session"` keeps the session/project directory. Home/sidebar spawns
156
+ * always use the session/project dir (no focused pane exists there).
157
+ */
158
+ newAgentCwd: NewAgentCwd;
159
+ /**
160
+ * Whether the unified app enables the kitty keyboard protocol on its host
161
+ * terminal (M24.4). On (default), kitty-capable terminals deliver ⌘-modified
162
+ * keys — ⌘K opens the command palette — and disambiguated escapes; the app
163
+ * re-encodes every key for the mirrored panes either way, and terminals
164
+ * without the protocol ignore the request entirely. Off restores the legacy
165
+ * key encoding for hosts where the protocol misbehaves.
166
+ */
167
+ kittyKeys: boolean;
168
+ }
169
+ /** The two Terminal-spawn cwd policies (see {@link AppApp.newAgentCwd}). */
170
+ export type NewAgentCwd = "pane" | "session";
95
171
  /** Worktree flow config (`tmux-ide worktree`). */
96
172
  export interface AppWorktrees {
97
173
  /**
@@ -112,6 +188,7 @@ export interface AppConfig {
112
188
  welcome: AppWelcome;
113
189
  integrations: AppIntegrations;
114
190
  worktrees: AppWorktrees;
191
+ app: AppApp;
115
192
  }
116
193
  /** The built-in config — every field's fallback. */
117
194
  export declare const DEFAULT_APP_CONFIG: AppConfig;
@@ -145,3 +222,32 @@ export declare function loadAppConfig(): AppConfig;
145
222
  export declare function getAppConfig(): AppConfig;
146
223
  /** Drop the cache so the next {@link getAppConfig} re-reads. Test-only. */
147
224
  export declare function _resetForTests(): void;
225
+ /**
226
+ * io — the RAW parsed config file as the user wrote it (`{}` when missing or
227
+ * malformed). Writers merge over THIS, not over {@link parseAppConfig}'s output:
228
+ * round-tripping through the parser would materialize every default into the
229
+ * hand-editable file and silently DROP fields the typed shape doesn't model yet
230
+ * (e.g. the notification polish fields `notifications.enabled` /
231
+ * `notifications.quietHours` that {@link ../tui/chrome/notify.ts} reads raw).
232
+ */
233
+ export declare function loadRawAppConfig(): Record<string, unknown>;
234
+ /** A partial config write: plain objects merge recursively, scalars/arrays
235
+ * replace, and an explicit `undefined` DELETES the key (how "reset to
236
+ * defaults" removes a block so the parser's defaults take over). */
237
+ export type AppConfigPatch = {
238
+ [key: string]: unknown;
239
+ };
240
+ /**
241
+ * PURE — merge `patch` into `raw` without touching either input. Objects merge
242
+ * key-by-key (recursively), anything else replaces, and a key whose patch value
243
+ * is `undefined` is REMOVED from the result. Unknown user fields survive — the
244
+ * merge only visits keys the patch names.
245
+ */
246
+ export declare function mergeConfigPatch(raw: Record<string, unknown>, patch: AppConfigPatch): Record<string, unknown>;
247
+ /**
248
+ * io — apply a patch to the config file ATOMICALLY (temp + rename, the same
249
+ * discipline as the editor save): read raw → {@link mergeConfigPatch} → write.
250
+ * Busts the {@link getAppConfig} process cache and returns the new parsed
251
+ * config. Honors `TMUX_IDE_CONFIG`, so tests never touch the real file.
252
+ */
253
+ export declare function updateAppConfig(patch: AppConfigPatch): AppConfig;