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.
- package/README.md +36 -14
- package/bin/cli.js +4174 -1235
- package/bin/cli.ts +426 -72
- package/package.json +10 -6
- package/packages/contracts/src/__tests__/control.test.ts +154 -0
- package/packages/contracts/src/control.ts +217 -0
- package/packages/contracts/src/index.ts +1 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/control/client.d.ts +23 -0
- package/packages/daemon/dist/control/client.js +105 -0
- package/packages/daemon/dist/control/dispatch.d.ts +34 -0
- package/packages/daemon/dist/control/dispatch.js +83 -0
- package/packages/daemon/dist/control/fanout.d.ts +19 -0
- package/packages/daemon/dist/control/fanout.js +37 -0
- package/packages/daemon/dist/control/frames.d.ts +23 -0
- package/packages/daemon/dist/control/frames.js +37 -0
- package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
- package/packages/daemon/dist/control/lifecycle.js +114 -0
- package/packages/daemon/dist/control/server.d.ts +16 -0
- package/packages/daemon/dist/control/server.js +214 -0
- package/packages/daemon/dist/control/verbs.d.ts +11 -0
- package/packages/daemon/dist/control/verbs.js +91 -0
- package/packages/daemon/dist/doctor.d.ts +18 -0
- package/packages/daemon/dist/doctor.js +105 -15
- package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
- package/packages/daemon/dist/lib/agent-discovery.js +29 -14
- package/packages/daemon/dist/lib/app-config.d.ts +106 -0
- package/packages/daemon/dist/lib/app-config.js +104 -5
- package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
- package/packages/daemon/dist/lib/manifest-pack.js +232 -0
- package/packages/daemon/dist/lib/state-home.d.ts +2 -0
- package/packages/daemon/dist/lib/state-home.js +12 -0
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/lib/update-check.js +5 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
- package/packages/daemon/dist/restore.d.ts +35 -8
- package/packages/daemon/dist/restore.js +52 -15
- package/packages/daemon/dist/send.d.ts +33 -1
- package/packages/daemon/dist/send.js +32 -19
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/control/client.ts +128 -0
- package/packages/daemon/src/control/dispatch.ts +107 -0
- package/packages/daemon/src/control/fanout.ts +44 -0
- package/packages/daemon/src/control/frames.ts +40 -0
- package/packages/daemon/src/control/lifecycle.ts +151 -0
- package/packages/daemon/src/control/server.ts +237 -0
- package/packages/daemon/src/control/verbs.ts +118 -0
- package/packages/daemon/src/doctor.ts +113 -28
- package/packages/daemon/src/lib/agent-discovery.ts +53 -13
- package/packages/daemon/src/lib/app-config.ts +194 -5
- package/packages/daemon/src/lib/manifest-pack.ts +255 -0
- package/packages/daemon/src/lib/state-home.ts +13 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +5 -0
- package/packages/daemon/src/restore.ts +53 -15
- package/packages/daemon/src/send.ts +55 -21
- package/packages/daemon/src/tui/chrome/events.ts +4 -4
- package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
- package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
- package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
- package/packages/daemon/src/tui/chrome/notify.ts +737 -52
- package/packages/daemon/src/tui/chrome/updater.ts +318 -30
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +49 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
- package/packages/daemon/src/tui/detect/manifest.ts +39 -3
- package/packages/daemon/src/tui/detect/manifests.ts +395 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
- package/packages/daemon/src/tui/detect/session-id.ts +503 -0
- package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
- package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
- package/packages/daemon/src/tui/mirror/palette.ts +564 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +376 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +184 -17
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +8 -1
- package/scripts/prepublish-check.mjs +37 -1
- package/scripts/publish-tap.sh +55 -0
- package/skill/SKILL.md +110 -2
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- 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}\`
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
* (
|
|
13
|
-
* screen-manifest scraping only
|
|
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
|
-
* (
|
|
23
|
-
* screen-manifest scraping only
|
|
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:
|
|
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:
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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;
|