tmux-ide 2.6.0 → 2.7.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 +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- 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/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 +298 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +427 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +382 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -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/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -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 +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -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/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- 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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { PaneInfo } from "@tmux-ide/contracts";
|
|
3
|
+
|
|
4
|
+
export type { PaneInfo };
|
|
5
|
+
|
|
6
|
+
type TmuxExecutor = (cmd: string, args: string[], options?: object) => string;
|
|
7
|
+
|
|
8
|
+
let _executor: TmuxExecutor = (cmd, args, options) =>
|
|
9
|
+
execFileSync(cmd, args, { encoding: "utf-8", ...options }).toString();
|
|
10
|
+
|
|
11
|
+
export function _setExecutor(fn: TmuxExecutor): () => void {
|
|
12
|
+
const prev = _executor;
|
|
13
|
+
_executor = fn;
|
|
14
|
+
return () => {
|
|
15
|
+
_executor = prev;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tmux(...args: string[]): string {
|
|
20
|
+
try {
|
|
21
|
+
return _executor("tmux", args, {
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
}).trim();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const stderr = (error as { stderr?: Buffer | string })?.stderr?.toString() ?? "";
|
|
26
|
+
if (stderr.includes("no server running") || stderr.includes("can't find session")) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function listSessionPanes(session: string): PaneInfo[] {
|
|
34
|
+
const format = [
|
|
35
|
+
"#{pane_id}",
|
|
36
|
+
"#{pane_index}",
|
|
37
|
+
"#{pane_title}",
|
|
38
|
+
"#{pane_current_command}",
|
|
39
|
+
"#{pane_width}",
|
|
40
|
+
"#{pane_height}",
|
|
41
|
+
"#{pane_active}",
|
|
42
|
+
"#{@ide_role}",
|
|
43
|
+
"#{@ide_name}",
|
|
44
|
+
"#{@ide_type}",
|
|
45
|
+
].join("\t");
|
|
46
|
+
|
|
47
|
+
const output = tmux("list-panes", "-t", session, "-F", format);
|
|
48
|
+
if (!output) return [];
|
|
49
|
+
|
|
50
|
+
return output
|
|
51
|
+
.split("\n")
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.map((line) => {
|
|
54
|
+
const [id, index, title, cmd, width, height, active, role, name, type] = line.split("\t");
|
|
55
|
+
return {
|
|
56
|
+
id: id!,
|
|
57
|
+
index: parseInt(index!, 10),
|
|
58
|
+
title: title!,
|
|
59
|
+
currentCommand: cmd!,
|
|
60
|
+
width: parseInt(width!, 10),
|
|
61
|
+
height: parseInt(height!, 10),
|
|
62
|
+
active: active === "1",
|
|
63
|
+
role: (role || null) as PaneInfo["role"],
|
|
64
|
+
name: name || null,
|
|
65
|
+
type: type || null,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function findPaneByTitle(session: string, title: string): string | null {
|
|
71
|
+
const panes = listSessionPanes(session);
|
|
72
|
+
const match = panes.find((p) => p.title === title);
|
|
73
|
+
return match?.id ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findPaneByPattern(session: string, pattern: string): string | null {
|
|
77
|
+
const panes = listSessionPanes(session);
|
|
78
|
+
const lower = pattern.toLowerCase();
|
|
79
|
+
const match = panes.find((p) => p.title.toLowerCase().includes(lower));
|
|
80
|
+
return match?.id ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function findAdjacentPane(session: string, currentPaneId: string): string | null {
|
|
84
|
+
const panes = listSessionPanes(session);
|
|
85
|
+
const currentIdx = panes.findIndex((p) => p.id === currentPaneId);
|
|
86
|
+
if (currentIdx === -1 || panes.length < 2) return null;
|
|
87
|
+
const nextIdx = (currentIdx + 1) % panes.length;
|
|
88
|
+
return panes[nextIdx]!.id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const BUSY_COMMANDS = new Set([
|
|
92
|
+
"claude",
|
|
93
|
+
"codex",
|
|
94
|
+
"vim",
|
|
95
|
+
"nvim",
|
|
96
|
+
"vi",
|
|
97
|
+
"nano",
|
|
98
|
+
"emacs",
|
|
99
|
+
"less",
|
|
100
|
+
"more",
|
|
101
|
+
"top",
|
|
102
|
+
"htop",
|
|
103
|
+
"man",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const SHELL_COMMANDS = new Set(["zsh", "bash", "sh", "fish"]);
|
|
107
|
+
|
|
108
|
+
export type PaneBusyStatus = "idle" | "busy" | "agent";
|
|
109
|
+
|
|
110
|
+
export function isPaneBusy(session: string, paneId: string): boolean {
|
|
111
|
+
const panes = listSessionPanes(session);
|
|
112
|
+
const pane = panes.find((p) => p.id === paneId);
|
|
113
|
+
if (!pane) return true;
|
|
114
|
+
const cmd = pane.currentCommand.toLowerCase();
|
|
115
|
+
if (SHELL_COMMANDS.has(cmd)) return false;
|
|
116
|
+
if (BUSY_COMMANDS.has(cmd)) return true;
|
|
117
|
+
return true; // unknown command — assume busy
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getPaneBusyStatus(session: string, paneId: string): PaneBusyStatus {
|
|
121
|
+
const panes = listSessionPanes(session);
|
|
122
|
+
const pane = panes.find((p) => p.id === paneId);
|
|
123
|
+
if (!pane) return "busy";
|
|
124
|
+
const cmd = pane.currentCommand.toLowerCase();
|
|
125
|
+
if (cmd.startsWith("claude") || cmd.startsWith("codex")) return "agent";
|
|
126
|
+
if (SHELL_COMMANDS.has(cmd)) return "idle";
|
|
127
|
+
return "busy";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function sendText(session: string, paneId: string, text: string): void {
|
|
131
|
+
tmux("send-keys", "-t", paneId, "-l", "--", text);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// tmux pane ids (e.g. %10) are GLOBAL — they don't take a session prefix.
|
|
135
|
+
// `tmux send-keys -t <session>:<%paneId>` makes tmux interpret the
|
|
136
|
+
// session prefix and then look up `%paneId` as a window inside that
|
|
137
|
+
// session, which fails ("can't find window: %10"). Pass the bare pane id.
|
|
138
|
+
export function sendLiteralToPane(_session: string, paneId: string, text: string): void {
|
|
139
|
+
tmux("send-keys", "-t", paneId, "-l", "--", text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function sendEnterToPane(_session: string, paneId: string): void {
|
|
143
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sleepMs(ms: number): void {
|
|
147
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function sendCommand(session: string, paneId: string, command: string): boolean {
|
|
151
|
+
const status = getPaneBusyStatus(session, paneId);
|
|
152
|
+
try {
|
|
153
|
+
tmux("send-keys", "-t", paneId, "-l", "--", command);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (status === "agent") {
|
|
158
|
+
// Claude Code shows a [Pasted text] preview for long input (roughly >200 chars).
|
|
159
|
+
// Short commands land directly in the prompt — just need a brief delay for the
|
|
160
|
+
// TUI to register the input. Long commands trigger the paste preview which needs
|
|
161
|
+
// two Enters: one to confirm the preview, one to submit the prompt.
|
|
162
|
+
if (command.length < 200) {
|
|
163
|
+
sleepMs(150);
|
|
164
|
+
} else {
|
|
165
|
+
sleepMs(5000);
|
|
166
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
167
|
+
sleepMs(2000);
|
|
168
|
+
}
|
|
169
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
tmux("send-keys", "-t", paneId, "Enter");
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function openFileInEditor(session: string, paneId: string, filePath: string): void {
|
|
177
|
+
const editor = process.env.EDITOR ?? "vim";
|
|
178
|
+
sendCommand(session, paneId, `${editor} ${filePath}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface TargetOptions {
|
|
182
|
+
title?: string;
|
|
183
|
+
titlePattern?: string;
|
|
184
|
+
paneId?: string;
|
|
185
|
+
selfPaneId?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveTarget(session: string, opts: TargetOptions): string | null {
|
|
189
|
+
if (opts.paneId) return opts.paneId;
|
|
190
|
+
if (opts.title) return findPaneByTitle(session, opts.title);
|
|
191
|
+
if (opts.titlePattern) return findPaneByPattern(session, opts.titlePattern);
|
|
192
|
+
if (opts.selfPaneId) return findAdjacentPane(session, opts.selfPaneId);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Capture the last line of a tmux pane. Uses the mockable executor so tests
|
|
198
|
+
* can intercept the call instead of hitting a real tmux server.
|
|
199
|
+
*/
|
|
200
|
+
export function captureLastLine(paneId: string): string {
|
|
201
|
+
try {
|
|
202
|
+
return _executor("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-1"], {
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
205
|
+
}).trim();
|
|
206
|
+
} catch {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { DEFAULT_THEME, type AppTheme } from "../../lib/app-config.ts";
|
|
2
|
+
|
|
3
|
+
export type RGBA = { r: number; g: number; b: number; a: number };
|
|
4
|
+
|
|
5
|
+
export interface WidgetTheme {
|
|
6
|
+
bg: RGBA;
|
|
7
|
+
fg: RGBA;
|
|
8
|
+
fgMuted: RGBA;
|
|
9
|
+
accent: RGBA;
|
|
10
|
+
border: RGBA;
|
|
11
|
+
/** Per-agent-status colors, shared with the tmux chrome via the app theme. */
|
|
12
|
+
statusBlocked: RGBA;
|
|
13
|
+
statusWorking: RGBA;
|
|
14
|
+
statusDone: RGBA;
|
|
15
|
+
statusIdle: RGBA;
|
|
16
|
+
statusUnknown: RGBA;
|
|
17
|
+
selected: RGBA;
|
|
18
|
+
selectedText: RGBA;
|
|
19
|
+
dirName: RGBA;
|
|
20
|
+
rowAlt: RGBA;
|
|
21
|
+
indentGuide: RGBA;
|
|
22
|
+
ignored: RGBA;
|
|
23
|
+
gitModified: RGBA;
|
|
24
|
+
gitAdded: RGBA;
|
|
25
|
+
gitDeleted: RGBA;
|
|
26
|
+
gitUntracked: RGBA;
|
|
27
|
+
diffAdded: RGBA;
|
|
28
|
+
diffAddedBg: RGBA;
|
|
29
|
+
diffRemoved: RGBA;
|
|
30
|
+
diffRemovedBg: RGBA;
|
|
31
|
+
diffContext: RGBA;
|
|
32
|
+
diffContextBg: RGBA;
|
|
33
|
+
diffHunk: RGBA;
|
|
34
|
+
diffLineNumber: RGBA;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rgba(r: number, g: number, b: number, a = 255) {
|
|
38
|
+
return { r, g, b, a };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULTS: WidgetTheme = {
|
|
42
|
+
bg: rgba(30, 30, 40),
|
|
43
|
+
fg: rgba(200, 200, 210),
|
|
44
|
+
fgMuted: rgba(120, 120, 140),
|
|
45
|
+
accent: rgba(130, 170, 255),
|
|
46
|
+
border: rgba(60, 60, 80),
|
|
47
|
+
statusBlocked: rgba(255, 95, 95),
|
|
48
|
+
statusWorking: rgba(255, 215, 95),
|
|
49
|
+
statusDone: rgba(135, 175, 255),
|
|
50
|
+
statusIdle: rgba(135, 215, 135),
|
|
51
|
+
statusUnknown: rgba(128, 128, 128),
|
|
52
|
+
selected: rgba(50, 50, 70),
|
|
53
|
+
selectedText: rgba(255, 255, 255),
|
|
54
|
+
dirName: rgba(100, 160, 255),
|
|
55
|
+
rowAlt: rgba(255, 255, 255, 5),
|
|
56
|
+
indentGuide: rgba(60, 60, 80),
|
|
57
|
+
ignored: rgba(80, 80, 100),
|
|
58
|
+
gitModified: rgba(230, 180, 80),
|
|
59
|
+
gitAdded: rgba(80, 200, 120),
|
|
60
|
+
gitDeleted: rgba(230, 80, 80),
|
|
61
|
+
gitUntracked: rgba(120, 120, 140),
|
|
62
|
+
diffAdded: rgba(80, 200, 120),
|
|
63
|
+
diffAddedBg: rgba(20, 60, 30),
|
|
64
|
+
diffRemoved: rgba(230, 80, 80),
|
|
65
|
+
diffRemovedBg: rgba(60, 20, 20),
|
|
66
|
+
diffContext: rgba(160, 160, 170),
|
|
67
|
+
diffContextBg: rgba(30, 30, 40),
|
|
68
|
+
diffHunk: rgba(100, 180, 220),
|
|
69
|
+
diffLineNumber: rgba(80, 80, 100),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a tmux/CSS color string into RGBA.
|
|
74
|
+
* Supports: colourN / colorN (0-255), #rgb, #rrggbb.
|
|
75
|
+
*/
|
|
76
|
+
function parseColor(color: string): RGBA | null {
|
|
77
|
+
// tmux colour0–colour255
|
|
78
|
+
const tmuxMatch = color.match(/^colou?r(\d+)$/);
|
|
79
|
+
if (tmuxMatch) {
|
|
80
|
+
const rawIndex = tmuxMatch[1];
|
|
81
|
+
if (!rawIndex) return null;
|
|
82
|
+
const n = parseInt(rawIndex, 10);
|
|
83
|
+
if (n < 0 || n > 255) return null;
|
|
84
|
+
|
|
85
|
+
// Standard colors 0–15 (approximate to common terminal defaults)
|
|
86
|
+
const STANDARD: [number, number, number][] = [
|
|
87
|
+
[0, 0, 0],
|
|
88
|
+
[128, 0, 0],
|
|
89
|
+
[0, 128, 0],
|
|
90
|
+
[128, 128, 0],
|
|
91
|
+
[0, 0, 128],
|
|
92
|
+
[128, 0, 128],
|
|
93
|
+
[0, 128, 128],
|
|
94
|
+
[192, 192, 192],
|
|
95
|
+
[128, 128, 128],
|
|
96
|
+
[255, 0, 0],
|
|
97
|
+
[0, 255, 0],
|
|
98
|
+
[255, 255, 0],
|
|
99
|
+
[0, 0, 255],
|
|
100
|
+
[255, 0, 255],
|
|
101
|
+
[0, 255, 255],
|
|
102
|
+
[255, 255, 255],
|
|
103
|
+
];
|
|
104
|
+
if (n < 16) {
|
|
105
|
+
const standard = STANDARD[n];
|
|
106
|
+
if (!standard) return null;
|
|
107
|
+
return rgba(standard[0], standard[1], standard[2]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 6×6×6 color cube (16–231)
|
|
111
|
+
if (n < 232) {
|
|
112
|
+
const idx = n - 16;
|
|
113
|
+
const LEVELS: number[] = [0, 95, 135, 175, 215, 255];
|
|
114
|
+
const r = LEVELS[Math.floor(idx / 36)];
|
|
115
|
+
const g = LEVELS[Math.floor((idx % 36) / 6)];
|
|
116
|
+
const b = LEVELS[idx % 6];
|
|
117
|
+
if (r == null || g == null || b == null) return null;
|
|
118
|
+
return rgba(r, g, b);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Grayscale ramp (232–255)
|
|
122
|
+
const level = 8 + 10 * (n - 232);
|
|
123
|
+
return rgba(level, level, level);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Hex: #rgb or #rrggbb
|
|
127
|
+
const hexMatch = color.match(/^#([0-9a-fA-F]{3,6})$/);
|
|
128
|
+
if (hexMatch) {
|
|
129
|
+
let hex = hexMatch[1];
|
|
130
|
+
if (!hex) return null;
|
|
131
|
+
if (hex.length === 3) {
|
|
132
|
+
const [r, g, b] = hex.split("");
|
|
133
|
+
if (!r || !g || !b) return null;
|
|
134
|
+
hex = r + r + g + g + b + b;
|
|
135
|
+
}
|
|
136
|
+
if (hex.length !== 6) return null;
|
|
137
|
+
return rgba(
|
|
138
|
+
parseInt(hex.slice(0, 2), 16),
|
|
139
|
+
parseInt(hex.slice(2, 4), 16),
|
|
140
|
+
parseInt(hex.slice(4, 6), 16),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Map the shared app-theme tokens ({@link AppTheme}) onto a {@link WidgetTheme}
|
|
149
|
+
* base: accent, muted→fgMuted, fg, and the per-status colors. This is the layer
|
|
150
|
+
* that makes the OpenTUI widgets share the tmux chrome's palette. Unmappable
|
|
151
|
+
* tokens (e.g. a hex the parser rejects) keep their default.
|
|
152
|
+
*/
|
|
153
|
+
function applyAppTheme(base: WidgetTheme, appTheme: AppTheme): void {
|
|
154
|
+
const assign = (prop: keyof WidgetTheme, token: string) => {
|
|
155
|
+
const parsed = parseColor(token);
|
|
156
|
+
if (parsed) base[prop] = parsed;
|
|
157
|
+
};
|
|
158
|
+
assign("accent", appTheme.accent);
|
|
159
|
+
assign("fgMuted", appTheme.muted);
|
|
160
|
+
assign("fg", appTheme.fg);
|
|
161
|
+
assign("statusBlocked", appTheme.status.blocked);
|
|
162
|
+
assign("statusWorking", appTheme.status.working);
|
|
163
|
+
assign("statusDone", appTheme.status.done);
|
|
164
|
+
assign("statusIdle", appTheme.status.idle);
|
|
165
|
+
assign("statusUnknown", appTheme.status.unknown);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build a widget theme. The shared app-theme tokens (`appTheme`, defaulting to
|
|
170
|
+
* {@link DEFAULT_THEME}) form the base palette so widgets and chrome match; the
|
|
171
|
+
* per-project ide.yml `theme` (`config`: accent/border/bg/fg) overrides on top.
|
|
172
|
+
*/
|
|
173
|
+
export function createTheme(
|
|
174
|
+
config?: Record<string, string>,
|
|
175
|
+
appTheme: AppTheme = DEFAULT_THEME,
|
|
176
|
+
): WidgetTheme {
|
|
177
|
+
const theme = { ...DEFAULTS };
|
|
178
|
+
applyAppTheme(theme, appTheme);
|
|
179
|
+
if (!config) return theme;
|
|
180
|
+
const MAP: Record<string, keyof WidgetTheme> = {
|
|
181
|
+
accent: "accent",
|
|
182
|
+
border: "border",
|
|
183
|
+
bg: "bg",
|
|
184
|
+
fg: "fg",
|
|
185
|
+
};
|
|
186
|
+
for (const [key, prop] of Object.entries(MAP)) {
|
|
187
|
+
const val = config[key];
|
|
188
|
+
if (val) {
|
|
189
|
+
const parsed = parseColor(val);
|
|
190
|
+
if (parsed) theme[prop] = parsed;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return theme;
|
|
194
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { watch as fsWatch, type FSWatcher } from "node:fs";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface WatchEvent {
|
|
5
|
+
type: "create" | "update" | "delete";
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* File watching that survives BOTH runtimes. In a dev checkout / node install
|
|
11
|
+
* we use `@parcel/watcher` (fast, native, recursive with real ignore globs).
|
|
12
|
+
* In the compiled `tmux-ide-tui` binary that native `.node` addon can't be
|
|
13
|
+
* embedded by `bun build --compile`, so importing it throws — we catch that
|
|
14
|
+
* once and fall back to node's builtin `fs.watch`. The fallback is coarser
|
|
15
|
+
* (segment-level ignores, synthesized "update" events) but keeps the explorer
|
|
16
|
+
* and changes surfaces live instead of crashing on boot.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface ParcelSubscription {
|
|
20
|
+
unsubscribe(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface ParcelWatcher {
|
|
23
|
+
subscribe(
|
|
24
|
+
dir: string,
|
|
25
|
+
cb: (err: Error | null, events: { type: string; path: string }[]) => void,
|
|
26
|
+
opts?: { ignore?: string[] },
|
|
27
|
+
): Promise<ParcelSubscription>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// undefined = not attempted yet, null = confirmed unavailable (compiled binary).
|
|
31
|
+
let parcel: ParcelWatcher | null | undefined;
|
|
32
|
+
|
|
33
|
+
async function loadParcel(): Promise<ParcelWatcher | null> {
|
|
34
|
+
if (parcel !== undefined) return parcel;
|
|
35
|
+
try {
|
|
36
|
+
const mod = (await import("@parcel/watcher")) as unknown as ParcelWatcher;
|
|
37
|
+
if (typeof mod.subscribe !== "function") throw new Error("no subscribe");
|
|
38
|
+
parcel = mod;
|
|
39
|
+
} catch {
|
|
40
|
+
parcel = null;
|
|
41
|
+
}
|
|
42
|
+
return parcel;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fsWatchDirectory(
|
|
46
|
+
dir: string,
|
|
47
|
+
onChange: (events: WatchEvent[]) => void,
|
|
48
|
+
ignore: string[],
|
|
49
|
+
debounceMs: number,
|
|
50
|
+
): () => Promise<void> {
|
|
51
|
+
const ignoreSet = new Set(ignore);
|
|
52
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
let handle: FSWatcher | null = null;
|
|
54
|
+
try {
|
|
55
|
+
handle = fsWatch(dir, { recursive: true }, (_event, filename) => {
|
|
56
|
+
if (!filename) return;
|
|
57
|
+
const rel = filename.toString();
|
|
58
|
+
if (rel.split(sep).some((part) => ignoreSet.has(part))) return;
|
|
59
|
+
if (timeout) clearTimeout(timeout);
|
|
60
|
+
timeout = setTimeout(() => onChange([{ type: "update", path: join(dir, rel) }]), debounceMs);
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// Directory may be unreadable/vanished — degrade to a no-op watcher.
|
|
64
|
+
}
|
|
65
|
+
return async () => {
|
|
66
|
+
if (timeout) clearTimeout(timeout);
|
|
67
|
+
handle?.close();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function watchDirectory(
|
|
72
|
+
dir: string,
|
|
73
|
+
onChange: (events: WatchEvent[]) => void,
|
|
74
|
+
options?: { debounceMs?: number; ignore?: string[] },
|
|
75
|
+
): Promise<() => Promise<void>> {
|
|
76
|
+
const debounceMs = options?.debounceMs ?? 300;
|
|
77
|
+
const ignore = options?.ignore ?? ["node_modules", ".git", "dist", "build", ".next"];
|
|
78
|
+
|
|
79
|
+
const native = await loadParcel();
|
|
80
|
+
if (!native) return fsWatchDirectory(dir, onChange, ignore, debounceMs);
|
|
81
|
+
|
|
82
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
const subscription = await native.subscribe(
|
|
84
|
+
dir,
|
|
85
|
+
(err, events) => {
|
|
86
|
+
if (err) return;
|
|
87
|
+
if (timeout) clearTimeout(timeout);
|
|
88
|
+
timeout = setTimeout(() => onChange(events as unknown as WatchEvent[]), debounceMs);
|
|
89
|
+
},
|
|
90
|
+
{ ignore },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return async () => {
|
|
94
|
+
if (timeout) clearTimeout(timeout);
|
|
95
|
+
await subscription.unsubscribe();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function watchGitHead(
|
|
100
|
+
dir: string,
|
|
101
|
+
onBranchChange: () => void,
|
|
102
|
+
): Promise<(() => Promise<void>) | null> {
|
|
103
|
+
const gitDir = join(dir, ".git");
|
|
104
|
+
|
|
105
|
+
const native = await loadParcel();
|
|
106
|
+
if (!native) {
|
|
107
|
+
try {
|
|
108
|
+
const handle = fsWatch(gitDir, (_event, filename) => {
|
|
109
|
+
if (filename && filename.toString().endsWith("HEAD")) onBranchChange();
|
|
110
|
+
});
|
|
111
|
+
return async () => handle.close();
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const subscription = await native.subscribe(
|
|
119
|
+
gitDir,
|
|
120
|
+
(err, events) => {
|
|
121
|
+
if (err) return;
|
|
122
|
+
if (events.some((e) => e.path.endsWith("HEAD"))) {
|
|
123
|
+
onBranchChange();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{ ignore: ["objects", "pack", "refs", "logs"] },
|
|
127
|
+
);
|
|
128
|
+
return () => subscription.unsubscribe();
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|