tmux-ide 1.3.1 → 2.0.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/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/package.json +27 -14
- package/src/__tests__/support.ts +186 -0
- package/src/attach.ts +20 -0
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +818 -0
- package/src/command-center/server.ts +1426 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/detect.ts +248 -0
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/inspect.ts +212 -0
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +222 -0
- package/src/launch.ts +494 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +417 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1792 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/session-options.ts +100 -0
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/sizes.ts +36 -0
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/yaml-io.ts +27 -0
- package/src/ls.test.ts +76 -0
- package/src/ls.ts +40 -0
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +176 -0
- package/src/stop.ts +47 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +192 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/dist/attach.d.ts +0 -3
- package/dist/attach.js +0 -14
- package/dist/config.d.ts +0 -5
- package/dist/config.js +0 -269
- package/dist/detect.d.ts +0 -15
- package/dist/detect.js +0 -228
- package/dist/doctor.d.ts +0 -3
- package/dist/doctor.js +0 -66
- package/dist/init.d.ts +0 -4
- package/dist/init.js +0 -67
- package/dist/inspect.d.ts +0 -54
- package/dist/inspect.js +0 -118
- package/dist/launch.d.ts +0 -22
- package/dist/launch.js +0 -174
- package/dist/lib/dot-path.d.ts +0 -2
- package/dist/lib/dot-path.js +0 -17
- package/dist/lib/errors.d.ts +0 -29
- package/dist/lib/errors.js +0 -37
- package/dist/lib/launch-plan.d.ts +0 -6
- package/dist/lib/launch-plan.js +0 -40
- package/dist/lib/output.d.ts +0 -9
- package/dist/lib/output.js +0 -65
- package/dist/lib/session-monitor.d.ts +0 -12
- package/dist/lib/session-monitor.js +0 -160
- package/dist/lib/session-options.d.ts +0 -13
- package/dist/lib/session-options.js +0 -85
- package/dist/lib/sizes.d.ts +0 -16
- package/dist/lib/sizes.js +0 -36
- package/dist/lib/tmux.d.ts +0 -42
- package/dist/lib/tmux.js +0 -225
- package/dist/lib/yaml-io.d.ts +0 -10
- package/dist/lib/yaml-io.js +0 -24
- package/dist/ls.d.ts +0 -3
- package/dist/ls.js +0 -35
- package/dist/restart.d.ts +0 -4
- package/dist/restart.js +0 -13
- package/dist/status.d.ts +0 -3
- package/dist/status.js +0 -29
- package/dist/stop.d.ts +0 -3
- package/dist/stop.js +0 -21
- package/dist/types.d.ts +0 -41
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +0 -4
- package/dist/validate.js +0 -172
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import ignore, { type Ignore } from "ignore";
|
|
4
|
+
|
|
5
|
+
const ALWAYS_IGNORE = new Set([
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
".svn",
|
|
9
|
+
".hg",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
"out",
|
|
13
|
+
".next",
|
|
14
|
+
".turbo",
|
|
15
|
+
".cache",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
"coverage",
|
|
18
|
+
".nyc_output",
|
|
19
|
+
"target",
|
|
20
|
+
"vendor",
|
|
21
|
+
"bower_components",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export { type Ignore };
|
|
25
|
+
|
|
26
|
+
export interface FileEntry {
|
|
27
|
+
name: string;
|
|
28
|
+
path: string; // relative to root
|
|
29
|
+
absolutePath: string;
|
|
30
|
+
isDir: boolean;
|
|
31
|
+
ignored: boolean; // gitignored
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createIgnoreFilter(rootDir: string): Ignore {
|
|
35
|
+
const ig = ignore();
|
|
36
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
37
|
+
if (existsSync(gitignorePath)) {
|
|
38
|
+
ig.add(readFileSync(gitignorePath, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
return ig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readDirectory(
|
|
44
|
+
dir: string,
|
|
45
|
+
rootDir: string,
|
|
46
|
+
ig: Ignore,
|
|
47
|
+
showHidden: boolean,
|
|
48
|
+
showIgnored: boolean = false,
|
|
49
|
+
): FileEntry[] {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return entries
|
|
57
|
+
.filter((e) => {
|
|
58
|
+
if (ALWAYS_IGNORE.has(e.name)) return false;
|
|
59
|
+
if (!showHidden && e.name.startsWith(".")) return false;
|
|
60
|
+
if (showIgnored) return true;
|
|
61
|
+
const rel = relative(rootDir, join(dir, e.name));
|
|
62
|
+
try {
|
|
63
|
+
return !ig.ignores(e.isDirectory() ? rel + "/" : rel);
|
|
64
|
+
} catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.map((e) => {
|
|
69
|
+
const rel = relative(rootDir, join(dir, e.name));
|
|
70
|
+
let isIgnored = false;
|
|
71
|
+
try {
|
|
72
|
+
isIgnored = ig.ignores(e.isDirectory() ? rel + "/" : rel);
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore malformed paths for ignore rules
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name: e.name,
|
|
78
|
+
path: rel,
|
|
79
|
+
absolutePath: join(dir, e.name),
|
|
80
|
+
isDir: e.isDirectory(),
|
|
81
|
+
ignored: isIgnored,
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.sort((a, b) => {
|
|
85
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
86
|
+
return a.name.localeCompare(b.name);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, unlinkSync, rmSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
getGitBranch,
|
|
8
|
+
getGitFileStatuses,
|
|
9
|
+
getGitStatusMap,
|
|
10
|
+
getFileDiff,
|
|
11
|
+
isGitRepo,
|
|
12
|
+
} from "./git.ts";
|
|
13
|
+
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
|
|
16
|
+
function git(...args: string[]) {
|
|
17
|
+
execFileSync("git", args, { cwd: tmpDir, stdio: "ignore" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-git-test-"));
|
|
22
|
+
git("init", "-b", "main");
|
|
23
|
+
git("config", "user.email", "test@test.com");
|
|
24
|
+
git("config", "user.name", "Test");
|
|
25
|
+
writeFileSync(join(tmpDir, "initial.txt"), "hello\n");
|
|
26
|
+
git("add", ".");
|
|
27
|
+
git("commit", "-m", "initial");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("getGitBranch", () => {
|
|
35
|
+
it("returns the current branch name", () => {
|
|
36
|
+
expect(getGitBranch(tmpDir)).toBe("main");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns null for a non-git directory", () => {
|
|
40
|
+
const nonGit = mkdtempSync(join(tmpdir(), "tmux-ide-nongit-"));
|
|
41
|
+
try {
|
|
42
|
+
expect(getGitBranch(nonGit)).toBe(null);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(nonGit, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("isGitRepo", () => {
|
|
50
|
+
it("returns true for a git directory", () => {
|
|
51
|
+
expect(isGitRepo(tmpDir)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns false for a non-git directory", () => {
|
|
55
|
+
const nonGit = mkdtempSync(join(tmpdir(), "tmux-ide-nongit-"));
|
|
56
|
+
try {
|
|
57
|
+
expect(isGitRepo(nonGit)).toBe(false);
|
|
58
|
+
} finally {
|
|
59
|
+
rmSync(nonGit, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("getGitFileStatuses", () => {
|
|
65
|
+
it("returns empty array for a clean working tree", () => {
|
|
66
|
+
const statuses = getGitFileStatuses(tmpDir);
|
|
67
|
+
expect(statuses).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("detects modified files with line counts", () => {
|
|
71
|
+
writeFileSync(join(tmpDir, "initial.txt"), "hello\nworld\n");
|
|
72
|
+
const statuses = getGitFileStatuses(tmpDir);
|
|
73
|
+
const modified = statuses.find((s) => s.path === "initial.txt");
|
|
74
|
+
expect(modified).toBeTruthy();
|
|
75
|
+
expect(modified.status).toBe("M");
|
|
76
|
+
expect(modified.additions).toBe(1);
|
|
77
|
+
expect(modified.deletions).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("detects untracked files", () => {
|
|
81
|
+
writeFileSync(join(tmpDir, "new-file.txt"), "content\n");
|
|
82
|
+
const statuses = getGitFileStatuses(tmpDir);
|
|
83
|
+
const untracked = statuses.find((s) => s.path === "new-file.txt");
|
|
84
|
+
expect(untracked).toBeTruthy();
|
|
85
|
+
expect(untracked.status).toBe("?");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("detects deleted files", () => {
|
|
89
|
+
unlinkSync(join(tmpDir, "initial.txt"));
|
|
90
|
+
const statuses = getGitFileStatuses(tmpDir);
|
|
91
|
+
const deleted = statuses.find((s) => s.path === "initial.txt");
|
|
92
|
+
expect(deleted).toBeTruthy();
|
|
93
|
+
expect(deleted.status).toBe("D");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("detects files in subdirectories", () => {
|
|
97
|
+
mkdirSync(join(tmpDir, "subdir"));
|
|
98
|
+
writeFileSync(join(tmpDir, "subdir", "nested.txt"), "nested\n");
|
|
99
|
+
const statuses = getGitFileStatuses(tmpDir);
|
|
100
|
+
const nested = statuses.find((s) => s.path === "subdir/nested.txt");
|
|
101
|
+
expect(nested).toBeTruthy();
|
|
102
|
+
expect(nested.status).toBe("?");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns empty array for a non-git directory", () => {
|
|
106
|
+
const nonGit = mkdtempSync(join(tmpdir(), "tmux-ide-nongit-"));
|
|
107
|
+
try {
|
|
108
|
+
expect(getGitFileStatuses(nonGit)).toEqual([]);
|
|
109
|
+
} finally {
|
|
110
|
+
rmSync(nonGit, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("getGitStatusMap", () => {
|
|
116
|
+
it("propagates status to parent directories", () => {
|
|
117
|
+
mkdirSync(join(tmpDir, "src"));
|
|
118
|
+
mkdirSync(join(tmpDir, "src", "lib"));
|
|
119
|
+
writeFileSync(join(tmpDir, "src", "lib", "deep.txt"), "deep\n");
|
|
120
|
+
const map = getGitStatusMap(tmpDir);
|
|
121
|
+
expect(map.get("src/lib/deep.txt")).toBe("?");
|
|
122
|
+
expect(map.get("src/lib")).toBe("?");
|
|
123
|
+
expect(map.get("src")).toBe("?");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns empty map for clean working tree", () => {
|
|
127
|
+
const map = getGitStatusMap(tmpDir);
|
|
128
|
+
expect(map.size).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("includes both modified and untracked files", () => {
|
|
132
|
+
writeFileSync(join(tmpDir, "initial.txt"), "changed\n");
|
|
133
|
+
writeFileSync(join(tmpDir, "new.txt"), "new\n");
|
|
134
|
+
const map = getGitStatusMap(tmpDir);
|
|
135
|
+
expect(map.get("initial.txt")).toBe("M");
|
|
136
|
+
expect(map.get("new.txt")).toBe("?");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("getFileDiff", () => {
|
|
141
|
+
it("returns a diff for modified files", () => {
|
|
142
|
+
writeFileSync(join(tmpDir, "initial.txt"), "hello\nworld\n");
|
|
143
|
+
const diff = getFileDiff(tmpDir, "initial.txt", false);
|
|
144
|
+
expect(diff.includes("+world")).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns empty string for unmodified files", () => {
|
|
148
|
+
const diff = getFileDiff(tmpDir, "initial.txt", false);
|
|
149
|
+
expect(diff).toBe("");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface GitFileStatus {
|
|
4
|
+
path: string;
|
|
5
|
+
status: "M" | "A" | "D" | "R" | "?";
|
|
6
|
+
additions: number;
|
|
7
|
+
deletions: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function execGit(dir: string, args: string[]): string {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync(
|
|
13
|
+
"git",
|
|
14
|
+
["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", ...args],
|
|
15
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
|
|
16
|
+
);
|
|
17
|
+
} catch {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getGitBranch(dir: string): string | null {
|
|
23
|
+
const result = execGit(dir, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
24
|
+
return result || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getGitFileStatuses(dir: string): GitFileStatus[] {
|
|
28
|
+
const results: GitFileStatus[] = [];
|
|
29
|
+
|
|
30
|
+
// 1. Identify deleted files first so we can tag them correctly in numstat
|
|
31
|
+
const deleted = execGit(dir, ["diff", "--name-only", "--diff-filter=D", "HEAD"]);
|
|
32
|
+
const deletedPaths = new Set(deleted.split("\n").filter(Boolean));
|
|
33
|
+
|
|
34
|
+
// 2. Modified/deleted files with line counts
|
|
35
|
+
const numstat = execGit(dir, ["diff", "--numstat", "HEAD"]);
|
|
36
|
+
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
37
|
+
const parts = line.split("\t");
|
|
38
|
+
if (parts.length < 3) continue;
|
|
39
|
+
const [added, removed, ...pathParts] = parts;
|
|
40
|
+
const filepath = pathParts.join("\t");
|
|
41
|
+
if (added === "-") continue; // binary file
|
|
42
|
+
results.push({
|
|
43
|
+
path: filepath,
|
|
44
|
+
status: deletedPaths.has(filepath) ? "D" : "M",
|
|
45
|
+
additions: parseInt(added!, 10) || 0,
|
|
46
|
+
deletions: parseInt(removed!, 10) || 0,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Deleted files not in numstat (shouldn't happen, but be safe)
|
|
51
|
+
const numstatPaths = new Set(results.map((r) => r.path));
|
|
52
|
+
for (const filepath of deletedPaths) {
|
|
53
|
+
if (!numstatPaths.has(filepath)) {
|
|
54
|
+
results.push({ path: filepath, status: "D", additions: 0, deletions: 0 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. Untracked files
|
|
59
|
+
const untracked = execGit(dir, ["ls-files", "--others", "--exclude-standard"]);
|
|
60
|
+
for (const filepath of untracked.split("\n").filter(Boolean)) {
|
|
61
|
+
results.push({ path: filepath, status: "?", additions: 0, deletions: 0 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getGitStatusMap(dir: string): Map<string, string> {
|
|
68
|
+
const map = new Map<string, string>();
|
|
69
|
+
for (const file of getGitFileStatuses(dir)) {
|
|
70
|
+
map.set(file.path, file.status);
|
|
71
|
+
// Propagate to parent directories
|
|
72
|
+
let parent = file.path;
|
|
73
|
+
while (parent.includes("/")) {
|
|
74
|
+
parent = parent.substring(0, parent.lastIndexOf("/"));
|
|
75
|
+
if (!map.has(parent)) map.set(parent, file.status);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getFileDiff(dir: string, path: string, staged: boolean): string {
|
|
82
|
+
const args = staged ? ["diff", "--cached", "--", path] : ["diff", "--", path];
|
|
83
|
+
return execGit(dir, args);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isGitRepo(dir: string): boolean {
|
|
87
|
+
return execGit(dir, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
88
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
_setExecutor,
|
|
4
|
+
listSessionPanes,
|
|
5
|
+
findPaneByTitle,
|
|
6
|
+
findPaneByPattern,
|
|
7
|
+
findAdjacentPane,
|
|
8
|
+
isPaneBusy,
|
|
9
|
+
getPaneBusyStatus,
|
|
10
|
+
resolveTarget,
|
|
11
|
+
} from "./pane-comms.ts";
|
|
12
|
+
|
|
13
|
+
let restoreExec: () => void;
|
|
14
|
+
let mockOutput: string;
|
|
15
|
+
let tmuxCalls: string[][];
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockOutput = "";
|
|
19
|
+
tmuxCalls = [];
|
|
20
|
+
restoreExec = _setExecutor((_cmd: string, args: string[]) => {
|
|
21
|
+
tmuxCalls.push(args);
|
|
22
|
+
return mockOutput;
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
restoreExec();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function setMockPanes(output: string) {
|
|
31
|
+
mockOutput = output;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TWO_PANES = ["%0\t0\tFiles\tzsh\t80\t24\t0", "%1\t1\tClaude\tclaude\t80\t24\t1"].join("\n");
|
|
35
|
+
|
|
36
|
+
const THREE_PANES = [
|
|
37
|
+
"%0\t0\tFiles\tzsh\t80\t24\t0",
|
|
38
|
+
"%1\t1\tClaude\tclaude\t80\t24\t1",
|
|
39
|
+
"%2\t2\tShell\tzsh\t80\t24\t0",
|
|
40
|
+
].join("\n");
|
|
41
|
+
|
|
42
|
+
describe("listSessionPanes", () => {
|
|
43
|
+
it("parses tmux list-panes output correctly", () => {
|
|
44
|
+
setMockPanes(TWO_PANES);
|
|
45
|
+
const panes = listSessionPanes("test-session");
|
|
46
|
+
expect(panes.length).toBe(2);
|
|
47
|
+
expect(panes[0]!.id).toBe("%0");
|
|
48
|
+
expect(panes[0]!.title).toBe("Files");
|
|
49
|
+
expect(panes[0]!.currentCommand).toBe("zsh");
|
|
50
|
+
expect(panes[0]!.active).toBe(false);
|
|
51
|
+
expect(panes[1]!.id).toBe("%1");
|
|
52
|
+
expect(panes[1]!.title).toBe("Claude");
|
|
53
|
+
expect(panes[1]!.active).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns empty array when no output", () => {
|
|
57
|
+
setMockPanes("");
|
|
58
|
+
const panes = listSessionPanes("test-session");
|
|
59
|
+
expect(panes).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("findPaneByTitle", () => {
|
|
64
|
+
it("finds pane by exact title match", () => {
|
|
65
|
+
setMockPanes(TWO_PANES);
|
|
66
|
+
expect(findPaneByTitle("s", "Claude")).toBe("%1");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns null when title not found", () => {
|
|
70
|
+
setMockPanes(TWO_PANES);
|
|
71
|
+
expect(findPaneByTitle("s", "Shell")).toBe(null);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("findPaneByPattern", () => {
|
|
76
|
+
it("finds pane by case-insensitive substring", () => {
|
|
77
|
+
setMockPanes(TWO_PANES);
|
|
78
|
+
expect(findPaneByPattern("s", "claude")).toBe("%1");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns null when pattern not found", () => {
|
|
82
|
+
setMockPanes(TWO_PANES);
|
|
83
|
+
expect(findPaneByPattern("s", "editor")).toBe(null);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("findAdjacentPane", () => {
|
|
88
|
+
it("returns the next pane", () => {
|
|
89
|
+
setMockPanes(TWO_PANES);
|
|
90
|
+
expect(findAdjacentPane("s", "%0")).toBe("%1");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("wraps around to the first pane", () => {
|
|
94
|
+
setMockPanes(TWO_PANES);
|
|
95
|
+
expect(findAdjacentPane("s", "%1")).toBe("%0");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns null when only one pane", () => {
|
|
99
|
+
setMockPanes("%0\t0\tFiles\tzsh\t80\t24\t1");
|
|
100
|
+
expect(findAdjacentPane("s", "%0")).toBe(null);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns null when pane not found", () => {
|
|
104
|
+
setMockPanes(TWO_PANES);
|
|
105
|
+
expect(findAdjacentPane("s", "%99")).toBe(null);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("isPaneBusy", () => {
|
|
110
|
+
it("returns false for shell panes", () => {
|
|
111
|
+
setMockPanes("%0\t0\tShell\tzsh\t80\t24\t0");
|
|
112
|
+
expect(isPaneBusy("s", "%0")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns true for vim", () => {
|
|
116
|
+
setMockPanes("%0\t0\tEditor\tvim\t80\t24\t0");
|
|
117
|
+
expect(isPaneBusy("s", "%0")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns true for unknown pane", () => {
|
|
121
|
+
setMockPanes(TWO_PANES);
|
|
122
|
+
expect(isPaneBusy("s", "%99")).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("getPaneBusyStatus", () => {
|
|
127
|
+
it("returns agent for claude panes", () => {
|
|
128
|
+
setMockPanes("%0\t0\tClaude\tclaude\t80\t24\t1");
|
|
129
|
+
expect(getPaneBusyStatus("s", "%0")).toBe("agent");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns idle for shell panes", () => {
|
|
133
|
+
setMockPanes("%0\t0\tShell\tbash\t80\t24\t0");
|
|
134
|
+
expect(getPaneBusyStatus("s", "%0")).toBe("idle");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns busy for vim", () => {
|
|
138
|
+
setMockPanes("%0\t0\tEditor\tvim\t80\t24\t0");
|
|
139
|
+
expect(getPaneBusyStatus("s", "%0")).toBe("busy");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns busy for unknown pane", () => {
|
|
143
|
+
setMockPanes(TWO_PANES);
|
|
144
|
+
expect(getPaneBusyStatus("s", "%99")).toBe("busy");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("resolveTarget", () => {
|
|
149
|
+
it("returns explicit paneId first", () => {
|
|
150
|
+
setMockPanes(THREE_PANES);
|
|
151
|
+
expect(resolveTarget("s", { paneId: "%2" })).toBe("%2");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("finds by title", () => {
|
|
155
|
+
setMockPanes(THREE_PANES);
|
|
156
|
+
expect(resolveTarget("s", { title: "Shell" })).toBe("%2");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("finds by title pattern", () => {
|
|
160
|
+
setMockPanes(THREE_PANES);
|
|
161
|
+
expect(resolveTarget("s", { titlePattern: "claude" })).toBe("%1");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("falls back to adjacent pane", () => {
|
|
165
|
+
setMockPanes(THREE_PANES);
|
|
166
|
+
expect(resolveTarget("s", { selfPaneId: "%0" })).toBe("%1");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("prefers title over adjacency", () => {
|
|
170
|
+
setMockPanes(THREE_PANES);
|
|
171
|
+
expect(resolveTarget("s", { title: "Shell", selfPaneId: "%0" })).toBe("%2");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns null when nothing matches", () => {
|
|
175
|
+
setMockPanes(THREE_PANES);
|
|
176
|
+
expect(resolveTarget("s", {})).toBe(null);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface PaneInfo {
|
|
4
|
+
id: string;
|
|
5
|
+
index: number;
|
|
6
|
+
title: string;
|
|
7
|
+
currentCommand: string;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
active: boolean;
|
|
11
|
+
role: "lead" | "teammate" | "planner" | "validator" | "researcher" | "widget" | "shell" | null;
|
|
12
|
+
name: string | null;
|
|
13
|
+
type: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TmuxExecutor = (cmd: string, args: string[], options?: object) => string;
|
|
17
|
+
|
|
18
|
+
let _executor: TmuxExecutor = (cmd, args, options) =>
|
|
19
|
+
execFileSync(cmd, args, { encoding: "utf-8", ...options }).toString();
|
|
20
|
+
|
|
21
|
+
export function _setExecutor(fn: TmuxExecutor): () => void {
|
|
22
|
+
const prev = _executor;
|
|
23
|
+
_executor = fn;
|
|
24
|
+
return () => {
|
|
25
|
+
_executor = prev;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tmux(...args: string[]): string {
|
|
30
|
+
try {
|
|
31
|
+
return _executor("tmux", args, {
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
}).trim();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const stderr = (error as { stderr?: Buffer | string })?.stderr?.toString() ?? "";
|
|
36
|
+
if (stderr.includes("no server running") || stderr.includes("can't find session")) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function listSessionPanes(session: string): PaneInfo[] {
|
|
44
|
+
const format = [
|
|
45
|
+
"#{pane_id}",
|
|
46
|
+
"#{pane_index}",
|
|
47
|
+
"#{pane_title}",
|
|
48
|
+
"#{pane_current_command}",
|
|
49
|
+
"#{pane_width}",
|
|
50
|
+
"#{pane_height}",
|
|
51
|
+
"#{pane_active}",
|
|
52
|
+
"#{@ide_role}",
|
|
53
|
+
"#{@ide_name}",
|
|
54
|
+
"#{@ide_type}",
|
|
55
|
+
].join("\t");
|
|
56
|
+
|
|
57
|
+
const output = tmux("list-panes", "-t", session, "-F", format);
|
|
58
|
+
if (!output) return [];
|
|
59
|
+
|
|
60
|
+
return output
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((line) => {
|
|
64
|
+
const [id, index, title, cmd, width, height, active, role, name, type] = line.split("\t");
|
|
65
|
+
return {
|
|
66
|
+
id: id!,
|
|
67
|
+
index: parseInt(index!, 10),
|
|
68
|
+
title: title!,
|
|
69
|
+
currentCommand: cmd!,
|
|
70
|
+
width: parseInt(width!, 10),
|
|
71
|
+
height: parseInt(height!, 10),
|
|
72
|
+
active: active === "1",
|
|
73
|
+
role: (role || null) as PaneInfo["role"],
|
|
74
|
+
name: name || null,
|
|
75
|
+
type: type || null,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function findPaneByTitle(session: string, title: string): string | null {
|
|
81
|
+
const panes = listSessionPanes(session);
|
|
82
|
+
const match = panes.find((p) => p.title === title);
|
|
83
|
+
return match?.id ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function findPaneByPattern(session: string, pattern: string): string | null {
|
|
87
|
+
const panes = listSessionPanes(session);
|
|
88
|
+
const lower = pattern.toLowerCase();
|
|
89
|
+
const match = panes.find((p) => p.title.toLowerCase().includes(lower));
|
|
90
|
+
return match?.id ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function findAdjacentPane(session: string, currentPaneId: string): string | null {
|
|
94
|
+
const panes = listSessionPanes(session);
|
|
95
|
+
const currentIdx = panes.findIndex((p) => p.id === currentPaneId);
|
|
96
|
+
if (currentIdx === -1 || panes.length < 2) return null;
|
|
97
|
+
const nextIdx = (currentIdx + 1) % panes.length;
|
|
98
|
+
return panes[nextIdx]!.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const BUSY_COMMANDS = new Set([
|
|
102
|
+
"claude",
|
|
103
|
+
"codex",
|
|
104
|
+
"vim",
|
|
105
|
+
"nvim",
|
|
106
|
+
"vi",
|
|
107
|
+
"nano",
|
|
108
|
+
"emacs",
|
|
109
|
+
"less",
|
|
110
|
+
"more",
|
|
111
|
+
"top",
|
|
112
|
+
"htop",
|
|
113
|
+
"man",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const SHELL_COMMANDS = new Set(["zsh", "bash", "sh", "fish"]);
|
|
117
|
+
|
|
118
|
+
export type PaneBusyStatus = "idle" | "busy" | "agent";
|
|
119
|
+
|
|
120
|
+
export function isPaneBusy(session: string, paneId: string): boolean {
|
|
121
|
+
const panes = listSessionPanes(session);
|
|
122
|
+
const pane = panes.find((p) => p.id === paneId);
|
|
123
|
+
if (!pane) return true;
|
|
124
|
+
const cmd = pane.currentCommand.toLowerCase();
|
|
125
|
+
if (SHELL_COMMANDS.has(cmd)) return false;
|
|
126
|
+
if (BUSY_COMMANDS.has(cmd)) return true;
|
|
127
|
+
return true; // unknown command — assume busy
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getPaneBusyStatus(session: string, paneId: string): PaneBusyStatus {
|
|
131
|
+
const panes = listSessionPanes(session);
|
|
132
|
+
const pane = panes.find((p) => p.id === paneId);
|
|
133
|
+
if (!pane) return "busy";
|
|
134
|
+
const cmd = pane.currentCommand.toLowerCase();
|
|
135
|
+
if (cmd.startsWith("claude") || cmd.startsWith("codex")) return "agent";
|
|
136
|
+
if (SHELL_COMMANDS.has(cmd)) return "idle";
|
|
137
|
+
return "busy";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function sendText(session: string, paneId: string, text: string): void {
|
|
141
|
+
tmux("send-keys", "-t", paneId, "-l", "--", text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sleepMs(ms: number): void {
|
|
145
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function sendCommand(session: string, paneId: string, command: string): boolean {
|
|
149
|
+
const status = getPaneBusyStatus(session, paneId);
|
|
150
|
+
try {
|
|
151
|
+
tmux("send-keys", "-t", paneId, "-l", "--", command);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (status === "agent") {
|
|
156
|
+
// Claude Code shows a [Pasted text] preview for long input (roughly >200 chars).
|
|
157
|
+
// Short commands land directly in the prompt — just need a brief delay for the
|
|
158
|
+
// TUI to register the input. Long commands trigger the paste preview which needs
|
|
159
|
+
// two Enters: one to confirm the preview, one to submit the prompt.
|
|
160
|
+
if (command.length < 200) {
|
|
161
|
+
sleepMs(150);
|
|
162
|
+
} else {
|
|
163
|
+
sleepMs(5000);
|
|
164
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
165
|
+
sleepMs(2000);
|
|
166
|
+
}
|
|
167
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function openFileInEditor(session: string, paneId: string, filePath: string): void {
|
|
175
|
+
const editor = process.env.EDITOR ?? "vim";
|
|
176
|
+
sendCommand(session, paneId, `${editor} ${filePath}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface TargetOptions {
|
|
180
|
+
title?: string;
|
|
181
|
+
titlePattern?: string;
|
|
182
|
+
paneId?: string;
|
|
183
|
+
selfPaneId?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resolveTarget(session: string, opts: TargetOptions): string | null {
|
|
187
|
+
if (opts.paneId) return opts.paneId;
|
|
188
|
+
if (opts.title) return findPaneByTitle(session, opts.title);
|
|
189
|
+
if (opts.titlePattern) return findPaneByPattern(session, opts.titlePattern);
|
|
190
|
+
if (opts.selfPaneId) return findAdjacentPane(session, opts.selfPaneId);
|
|
191
|
+
return null;
|
|
192
|
+
}
|