tmux-ide 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- 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/src/agent-explain.ts +270 -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 +336 -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/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 +195 -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 +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -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 +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -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 +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -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/postinstall.js +26 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPIKE viewer — renders ONE live tmux pane inside an OpenTUI app.
|
|
3
|
+
*
|
|
4
|
+
* The proof for the unified-TUI architecture: a control-mode client streams
|
|
5
|
+
* the pane's bytes into a headless terminal (PaneMirror), this app draws the
|
|
6
|
+
* grid, and keystrokes are forwarded back through `send-keys`. tmux stays the
|
|
7
|
+
* multiplexer underneath; tmux-ide owns the screen.
|
|
8
|
+
*
|
|
9
|
+
* Run (needs the repo-root bunfig preload):
|
|
10
|
+
* bun packages/daemon/src/tui/mirror/viewer.tsx --target <session>
|
|
11
|
+
* Exit with ctrl+q (everything else is forwarded to the pane).
|
|
12
|
+
*/
|
|
13
|
+
import { parseArgs } from "node:util";
|
|
14
|
+
import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
|
|
15
|
+
import { RGBA } from "@opentui/core";
|
|
16
|
+
import { createSignal, onMount, onCleanup, For } from "solid-js";
|
|
17
|
+
import { ControlModeClient } from "./control-client.ts";
|
|
18
|
+
import { PaneMirror, type MirrorSnapshot } from "./pane-mirror.ts";
|
|
19
|
+
|
|
20
|
+
const { values } = parseArgs({ options: { target: { type: "string" } } });
|
|
21
|
+
const target = values.target ?? "_spike-target";
|
|
22
|
+
|
|
23
|
+
// The 16 base ANSI palette colors, mapped for OpenTUI. Higher palette
|
|
24
|
+
// entries fall back to the default foreground — fine for a spike.
|
|
25
|
+
const PALETTE: Array<[number, number, number]> = [
|
|
26
|
+
[40, 40, 48],
|
|
27
|
+
[220, 90, 90],
|
|
28
|
+
[110, 200, 130],
|
|
29
|
+
[220, 200, 100],
|
|
30
|
+
[110, 150, 240],
|
|
31
|
+
[190, 120, 220],
|
|
32
|
+
[100, 200, 210],
|
|
33
|
+
[200, 200, 210],
|
|
34
|
+
[120, 120, 140],
|
|
35
|
+
[250, 120, 120],
|
|
36
|
+
[140, 230, 160],
|
|
37
|
+
[240, 220, 130],
|
|
38
|
+
[140, 180, 250],
|
|
39
|
+
[220, 150, 250],
|
|
40
|
+
[130, 230, 240],
|
|
41
|
+
[240, 240, 250],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Map named OpenTUI key events to tmux send-keys names. */
|
|
45
|
+
const KEYMAP: Record<string, string> = {
|
|
46
|
+
return: "Enter",
|
|
47
|
+
backspace: "BSpace",
|
|
48
|
+
tab: "Tab",
|
|
49
|
+
escape: "Escape",
|
|
50
|
+
up: "Up",
|
|
51
|
+
down: "Down",
|
|
52
|
+
left: "Left",
|
|
53
|
+
right: "Right",
|
|
54
|
+
pageup: "PgUp",
|
|
55
|
+
pagedown: "PgDn",
|
|
56
|
+
home: "Home",
|
|
57
|
+
end: "End",
|
|
58
|
+
delete: "DC",
|
|
59
|
+
space: "Space",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
render(() => {
|
|
63
|
+
const dims = useTerminalDimensions();
|
|
64
|
+
const cols = Math.max(20, dims().width - 2);
|
|
65
|
+
const rows = Math.max(5, dims().height - 3);
|
|
66
|
+
|
|
67
|
+
const mirror = new PaneMirror(cols, rows);
|
|
68
|
+
const [snap, setSnap] = createSignal<MirrorSnapshot>(mirror.snapshot());
|
|
69
|
+
const [status, setStatus] = createSignal(`attaching to ${target}…`);
|
|
70
|
+
let pane = "";
|
|
71
|
+
let dirty = false;
|
|
72
|
+
|
|
73
|
+
const client = new ControlModeClient({
|
|
74
|
+
attachTarget: target,
|
|
75
|
+
onOutput: (p, data) => {
|
|
76
|
+
if (p === pane || pane === "") {
|
|
77
|
+
mirror.write(data);
|
|
78
|
+
dirty = true;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onExit: () => {
|
|
82
|
+
setStatus("control client exited");
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
onMount(() => {
|
|
87
|
+
(async () => {
|
|
88
|
+
try {
|
|
89
|
+
await client.start();
|
|
90
|
+
// Pin our control client's size so tmux lays the window out to match
|
|
91
|
+
// the mirror grid, then seed with the pane's current content (-e keeps
|
|
92
|
+
// colors) so we don't start from a blank screen.
|
|
93
|
+
await client.command(`refresh-client -C ${cols}x${rows}`);
|
|
94
|
+
const panes = await client.command(`list-panes -t ${target} -F "#{pane_id}"`);
|
|
95
|
+
pane = panes[0] ?? "";
|
|
96
|
+
const seed = await client.command(`capture-pane -p -e -t ${pane}`);
|
|
97
|
+
mirror.write(seed.join("\r\n") + "\r\n");
|
|
98
|
+
dirty = true;
|
|
99
|
+
setStatus(`live: ${target} ${pane} (ctrl+q to quit)`);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
setStatus(`error: ${(e as Error).message}`);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
// Coalesce bursts: redraw at most ~30fps instead of per %output event.
|
|
106
|
+
const timer = setInterval(() => {
|
|
107
|
+
if (!dirty) return;
|
|
108
|
+
dirty = false;
|
|
109
|
+
setSnap(mirror.snapshot());
|
|
110
|
+
}, 33);
|
|
111
|
+
onCleanup(() => {
|
|
112
|
+
clearInterval(timer);
|
|
113
|
+
client.dispose();
|
|
114
|
+
mirror.dispose();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
useKeyboard((evt) => {
|
|
119
|
+
if (evt.ctrl && evt.name === "q") {
|
|
120
|
+
client.dispose();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
if (!pane) return;
|
|
124
|
+
if (evt.ctrl && evt.name.length === 1) {
|
|
125
|
+
void client.sendKey(pane, `C-${evt.name}`).catch(() => {});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const named = KEYMAP[evt.name];
|
|
129
|
+
if (named) {
|
|
130
|
+
void client.sendKey(pane, named).catch(() => {});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (evt.name.length === 1 && !evt.meta && !evt.alt) {
|
|
134
|
+
void client.sendText(pane, evt.name).catch(() => {});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const fgFor = (idx: number | null): RGBA => {
|
|
139
|
+
if (idx === null || idx < 0 || idx > 15) return RGBA.fromInts(200, 200, 210, 255);
|
|
140
|
+
const [r, g, b] = PALETTE[idx]!;
|
|
141
|
+
return RGBA.fromInts(r, g, b, 255);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<box flexDirection="column" flexGrow={1} backgroundColor={RGBA.fromInts(18, 18, 24, 255)}>
|
|
146
|
+
<box paddingLeft={1} flexDirection="row" gap={1}>
|
|
147
|
+
<text fg={RGBA.fromInts(130, 170, 255, 255)}>tmux-ide mirror</text>
|
|
148
|
+
<text fg={RGBA.fromInts(120, 120, 140, 255)}>{status()}</text>
|
|
149
|
+
</box>
|
|
150
|
+
<box flexDirection="column" flexGrow={1} paddingLeft={1}>
|
|
151
|
+
<For each={snap().rows}>
|
|
152
|
+
{(runs) => (
|
|
153
|
+
<box flexDirection="row">
|
|
154
|
+
<For each={runs}>{(run) => <text fg={fgFor(run.fg)}>{run.text}</text>}</For>
|
|
155
|
+
</box>
|
|
156
|
+
)}
|
|
157
|
+
</For>
|
|
158
|
+
</box>
|
|
159
|
+
<box paddingLeft={1}>
|
|
160
|
+
<text fg={RGBA.fromInts(120, 120, 140, 255)}>
|
|
161
|
+
{`cursor ${snap().cursorX},${snap().cursorY} · keys forwarded · ctrl+q quits`}
|
|
162
|
+
</text>
|
|
163
|
+
</box>
|
|
164
|
+
</box>
|
|
165
|
+
);
|
|
166
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# tmux-ide control surface
|
|
2
|
+
|
|
3
|
+
A small, scriptable API over the tmux-ide fleet. Meant for agents and scripts
|
|
4
|
+
that need to read cockpit state or block on a session's status.
|
|
5
|
+
|
|
6
|
+
## `tmux-ide team --json`
|
|
7
|
+
|
|
8
|
+
Prints the whole fleet as JSON (no TUI). Shape:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"projects": [
|
|
13
|
+
{
|
|
14
|
+
"name": "web",
|
|
15
|
+
"dir": "/workspace/web",
|
|
16
|
+
"registered": true,
|
|
17
|
+
"running": true,
|
|
18
|
+
"status": "working",
|
|
19
|
+
"sessions": [{ "name": "web", "status": "working", "panes": 3, "attached": true }]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`status` is one of `blocked | working | done | idle | unknown`. `dir` may be
|
|
26
|
+
`null` for ad-hoc sessions. Projects with no live session have `running:false`
|
|
27
|
+
and an empty `sessions` array.
|
|
28
|
+
|
|
29
|
+
## `tmux-ide wait agent-status <session> --status <status> [--timeout <ms>]`
|
|
30
|
+
|
|
31
|
+
Blocks until `<session>` reaches `<status>`, polling every ~750ms.
|
|
32
|
+
|
|
33
|
+
- `--status` — required, one of `blocked | working | done | idle | unknown`.
|
|
34
|
+
- `--timeout` — milliseconds, default `60000`.
|
|
35
|
+
- Exit `0` on match (prints a success line, or JSON with `--json`).
|
|
36
|
+
- Exit `1` on timeout (message to stderr).
|
|
37
|
+
|
|
38
|
+
A session that does not exist yet is not an error — `wait` keeps polling until
|
|
39
|
+
it appears or the timeout elapses. `done` is a working→idle transition, so the
|
|
40
|
+
poller keeps one status tracker alive across ticks to observe it.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
tmux-ide wait agent-status build --status done --timeout 120000 && echo "build finished"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `TMUX_IDE=1` env marker
|
|
47
|
+
|
|
48
|
+
Sessions created by the cockpit have `TMUX_IDE=1` set in the tmux session
|
|
49
|
+
environment. Agents inside a pane can check `$TMUX_IDE` to detect that they are
|
|
50
|
+
running under tmux-ide.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Front-door decision for bare `tmux-ide` / `tmux-ide start`: open the team
|
|
3
|
+
* cockpit (the multi-project TUI) instead of launching a single project.
|
|
4
|
+
*
|
|
5
|
+
* The cockpit is the default entry when there's no single-project `ide.yml` to
|
|
6
|
+
* launch here, or when `--team` forces it. When an `ide.yml` is present, bare
|
|
7
|
+
* invocation still launches that project (backward compatible).
|
|
8
|
+
*/
|
|
9
|
+
export function shouldOpenCockpit(hasIdeYml: boolean, teamFlag: boolean): boolean {
|
|
10
|
+
return teamFlag || !hasIdeYml;
|
|
11
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free fuzzy subsequence matcher for the team TUI's quick-jump
|
|
3
|
+
* filter. Pure so it can be unit-tested in isolation from `index.tsx` (which
|
|
4
|
+
* runs `render(...)` on import).
|
|
5
|
+
*
|
|
6
|
+
* The match is a case-insensitive SUBSEQUENCE test: every char of the query
|
|
7
|
+
* must appear in the target in order (not necessarily contiguously). Scoring
|
|
8
|
+
* rewards matches that feel "meaningful" to a human — contiguous runs, matches
|
|
9
|
+
* anchored at the start of the string, and matches right after a separator
|
|
10
|
+
* (so typing "web" ranks the "web" in `wavyr-website` well).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A char that starts a new "word" — a match right after one scores a bonus. */
|
|
14
|
+
const SEPARATORS = new Set(["/", "-", "_", " ", "."]);
|
|
15
|
+
|
|
16
|
+
export interface FuzzyMatch<T> {
|
|
17
|
+
item: T;
|
|
18
|
+
score: number;
|
|
19
|
+
positions: number[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Per-char position bonuses. */
|
|
23
|
+
const START_BONUS = 10; // matched char is the first char of the target
|
|
24
|
+
const SEPARATOR_BONUS = 8; // matched char follows a separator (word start)
|
|
25
|
+
const CONTIGUOUS_BONUS = 5; // matched char is adjacent to the previous match
|
|
26
|
+
const BASE = 1; // every matched char is worth at least this
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Case-insensitive subsequence match of `query` against `target`.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` when `query` is not a subsequence of `target`. An empty query
|
|
32
|
+
* matches everything with score 0 and no positions. Otherwise returns the
|
|
33
|
+
* matched indices in `target` plus a heuristic score (higher is better).
|
|
34
|
+
*
|
|
35
|
+
* Uses a small dynamic program to find the highest-scoring alignment among all
|
|
36
|
+
* valid subsequences — a greedy leftmost scan would anchor on the first
|
|
37
|
+
* occurrence of each char and miss word-boundary matches (e.g. "web" should
|
|
38
|
+
* favour the "web" after the "-" in `wavyr-website`, not the leading "w").
|
|
39
|
+
*/
|
|
40
|
+
export function fuzzyMatch(
|
|
41
|
+
query: string,
|
|
42
|
+
target: string,
|
|
43
|
+
): { score: number; positions: number[] } | null {
|
|
44
|
+
if (query.length === 0) return { score: 0, positions: [] };
|
|
45
|
+
|
|
46
|
+
const q = query.toLowerCase();
|
|
47
|
+
const t = target.toLowerCase();
|
|
48
|
+
const m = q.length;
|
|
49
|
+
const n = t.length;
|
|
50
|
+
|
|
51
|
+
/** Position-context bonus for placing a matched char at target index j. */
|
|
52
|
+
const positionBonus = (j: number): number => {
|
|
53
|
+
if (j === 0) return START_BONUS;
|
|
54
|
+
if (SEPARATORS.has(t[j - 1]!)) return SEPARATOR_BONUS;
|
|
55
|
+
return 0;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const NEG = Number.NEGATIVE_INFINITY;
|
|
59
|
+
// f[i][j] = best score for matching q[i..] with q[i] placed at target index j
|
|
60
|
+
// (only meaningful when t[j] === q[i]); nextPos[i][j] = chosen index for q[i+1].
|
|
61
|
+
const f: number[][] = [];
|
|
62
|
+
const nextPos: number[][] = [];
|
|
63
|
+
for (let i = 0; i < m; i++) {
|
|
64
|
+
f.push(new Array<number>(n).fill(NEG));
|
|
65
|
+
nextPos.push(new Array<number>(n).fill(-1));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const last = m - 1;
|
|
69
|
+
for (let j = 0; j < n; j++) {
|
|
70
|
+
if (t[j] === q[last]) f[last]![j] = BASE + positionBonus(j);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let i = m - 2; i >= 0; i--) {
|
|
74
|
+
const ch = q[i]!;
|
|
75
|
+
const fi = f[i]!;
|
|
76
|
+
const fnext = f[i + 1]!;
|
|
77
|
+
const ni = nextPos[i]!;
|
|
78
|
+
for (let j = 0; j < n; j++) {
|
|
79
|
+
if (t[j] !== ch) continue;
|
|
80
|
+
let best = NEG;
|
|
81
|
+
let bestNext = -1;
|
|
82
|
+
for (let j2 = j + 1; j2 < n; j2++) {
|
|
83
|
+
const sub = fnext[j2]!;
|
|
84
|
+
if (sub === NEG) continue;
|
|
85
|
+
const val = (j2 === j + 1 ? CONTIGUOUS_BONUS : 0) + sub;
|
|
86
|
+
if (val > best) {
|
|
87
|
+
best = val;
|
|
88
|
+
bestNext = j2;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (best === NEG) continue; // q[i+1..] cannot be placed after j
|
|
92
|
+
fi[j] = BASE + positionBonus(j) + best;
|
|
93
|
+
ni[j] = bestNext;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Best starting placement for q[0] (leftmost on ties, keeping it deterministic).
|
|
98
|
+
let bestScore = NEG;
|
|
99
|
+
let start = -1;
|
|
100
|
+
const f0 = f[0]!;
|
|
101
|
+
for (let j = 0; j < n; j++) {
|
|
102
|
+
if (f0[j]! > bestScore) {
|
|
103
|
+
bestScore = f0[j]!;
|
|
104
|
+
start = j;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (start === -1) return null;
|
|
108
|
+
|
|
109
|
+
const positions: number[] = [];
|
|
110
|
+
let j = start;
|
|
111
|
+
for (let i = 0; i < m && j !== -1; i++) {
|
|
112
|
+
positions.push(j);
|
|
113
|
+
j = nextPos[i]![j]!;
|
|
114
|
+
}
|
|
115
|
+
return { score: bestScore, positions };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filter `items` by `query`, keeping only fuzzy matches, sorted by score
|
|
120
|
+
* descending. The sort is STABLE — items with equal scores keep their input
|
|
121
|
+
* order. An empty query returns every item (score 0) in original order.
|
|
122
|
+
*/
|
|
123
|
+
export function fuzzyFilter<T>(query: string, items: T[], key: (t: T) => string): FuzzyMatch<T>[] {
|
|
124
|
+
const matches: FuzzyMatch<T>[] = [];
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
const m = fuzzyMatch(query, key(item));
|
|
127
|
+
if (m) matches.push({ item, score: m.score, positions: m.positions });
|
|
128
|
+
}
|
|
129
|
+
// Stable sort by score desc: Array.prototype.sort is stable in modern JS, so
|
|
130
|
+
// equal scores preserve the push (input) order.
|
|
131
|
+
matches.sort((a, b) => b.score - a.score);
|
|
132
|
+
return matches;
|
|
133
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model helpers for the cockpit's HOME screen — the surface bare
|
|
3
|
+
* `tmux-ide` opens when there's no single-project `ide.yml` to launch.
|
|
4
|
+
*
|
|
5
|
+
* The `.tsx` renders these plain-data shapes: the header's fleet rollup counts,
|
|
6
|
+
* the empty-fleet hero actions, the in-app panel keys (e/g/,), and the footer
|
|
7
|
+
* hint line. Keeping them here (io-free, opentui-free) means the header/footer/
|
|
8
|
+
* empty-state logic unit-tests as tables, and the panel keys + footer stay
|
|
9
|
+
* sourced from the grammar and the panel registry so they can't drift from the
|
|
10
|
+
* chrome binds or the widget set.
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentStatus } from "../detect/classify.ts";
|
|
13
|
+
import type { TeamProject } from "./projects.ts";
|
|
14
|
+
import { GRAMMAR_KEYS } from "../../widgets/lib/grammar.ts";
|
|
15
|
+
import { PANEL_POPUPS, type PanelPopup } from "../chrome/panels.ts";
|
|
16
|
+
|
|
17
|
+
/** Live-session status tallies across the whole fleet, for the home header. */
|
|
18
|
+
export interface FleetRollup {
|
|
19
|
+
blocked: number;
|
|
20
|
+
working: number;
|
|
21
|
+
done: number;
|
|
22
|
+
idle: number;
|
|
23
|
+
unknown: number;
|
|
24
|
+
/** Total live sessions counted. */
|
|
25
|
+
sessions: number;
|
|
26
|
+
/** Total projects (registered + ad-hoc) in the fleet. */
|
|
27
|
+
projects: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PURE — tally every live session's status across the fleet. Counts SESSIONS
|
|
32
|
+
* (the unit a person acts on), not panes: a project with three sessions
|
|
33
|
+
* contributes three to the rollup.
|
|
34
|
+
*/
|
|
35
|
+
export function fleetRollup(projects: TeamProject[]): FleetRollup {
|
|
36
|
+
const r: FleetRollup = {
|
|
37
|
+
blocked: 0,
|
|
38
|
+
working: 0,
|
|
39
|
+
done: 0,
|
|
40
|
+
idle: 0,
|
|
41
|
+
unknown: 0,
|
|
42
|
+
sessions: 0,
|
|
43
|
+
projects: projects.length,
|
|
44
|
+
};
|
|
45
|
+
for (const p of projects) {
|
|
46
|
+
for (const s of p.sessions) {
|
|
47
|
+
r[s.status] += 1;
|
|
48
|
+
r.sessions += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A header rollup chip: a status and its count, colored by the status token. */
|
|
55
|
+
export interface RollupChip {
|
|
56
|
+
status: AgentStatus;
|
|
57
|
+
count: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The rollup chips shown in the header, in severity order — the meaningful
|
|
61
|
+
* agent states (blocked/working/done) plus idle to round out the total. */
|
|
62
|
+
export const ROLLUP_ORDER: AgentStatus[] = ["blocked", "working", "done", "idle"];
|
|
63
|
+
|
|
64
|
+
/** PURE — the ordered header chips for a rollup. */
|
|
65
|
+
export function rollupChips(r: FleetRollup): RollupChip[] {
|
|
66
|
+
return ROLLUP_ORDER.map((status) => ({ status, count: r[status] }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* PURE — true when the fleet has nothing to show: no registered projects and no
|
|
71
|
+
* live sessions (ad-hoc projects only exist for live sessions, and registered
|
|
72
|
+
* projects always appear, so an empty list means a truly empty fleet). The home
|
|
73
|
+
* screen swaps to the hero box in this state.
|
|
74
|
+
*/
|
|
75
|
+
export function isFleetEmpty(projects: TeamProject[]): boolean {
|
|
76
|
+
return projects.length === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** An empty-fleet hero call-to-action: the key and what it does. */
|
|
80
|
+
export interface HeroAction {
|
|
81
|
+
key: string;
|
|
82
|
+
label: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** PURE — the empty-fleet hero's action rows. `l` opens the add-a-project-dir
|
|
86
|
+
* prompt (the entry point to get a launchable project into the fleet). */
|
|
87
|
+
export function emptyFleetActions(): HeroAction[] {
|
|
88
|
+
return [
|
|
89
|
+
{ key: "n", label: "new session" },
|
|
90
|
+
{ key: "l", label: "launch a project dir" },
|
|
91
|
+
{ key: "q", label: "quit" },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The cockpit's IN-APP panel keys — plain letters matched against a bare
|
|
97
|
+
* keypress in the home handler, distinct from the tmux root-table `M-e`/`M-g`/
|
|
98
|
+
* `M-,` chrome binds. `,` opens config to echo its `M-,` bind.
|
|
99
|
+
*/
|
|
100
|
+
export const HOME_PANEL_KEYS: Record<string, PanelPopup["widget"]> = {
|
|
101
|
+
e: "explorer",
|
|
102
|
+
g: "changes",
|
|
103
|
+
",": "config",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** PURE — the panel a home keypress opens, or null when it isn't a panel key. */
|
|
107
|
+
export function panelForKey(name: string): PanelPopup["widget"] | null {
|
|
108
|
+
return HOME_PANEL_KEYS[name] ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** PURE — the in-app key that opens `widget`, or "" when unbound. */
|
|
112
|
+
export function keyForPanel(widget: PanelPopup["widget"]): string {
|
|
113
|
+
const found = Object.entries(HOME_PANEL_KEYS).find(([, w]) => w === widget);
|
|
114
|
+
return found ? found[0] : "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One footer / help hint: the key glyph(s) and the verb. */
|
|
118
|
+
export interface FooterHint {
|
|
119
|
+
keys: string;
|
|
120
|
+
label: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* PURE — the panel hints, DERIVED from the panel registry + the in-app keys so
|
|
125
|
+
* adding a panel or re-keying it updates the footer and help automatically.
|
|
126
|
+
* `use` picks the label source: the terse `widget` name for the footer, the
|
|
127
|
+
* registry `label` (e.g. "⊞ Files") for the roomier help overlay.
|
|
128
|
+
*/
|
|
129
|
+
export function panelHints(use: "widget" | "label" = "widget"): FooterHint[] {
|
|
130
|
+
return PANEL_POPUPS.map((p) => ({
|
|
131
|
+
keys: keyForPanel(p.widget),
|
|
132
|
+
label: use === "label" ? p.label : p.widget,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* PURE — the home footer hint line. The universal verbs' KEY glyphs come from
|
|
138
|
+
* {@link GRAMMAR_KEYS} (so the footer can't advertise a key the grammar doesn't
|
|
139
|
+
* bind), interleaved with the cockpit's own session + panel keys. Rendered as a
|
|
140
|
+
* single flex row by the `.tsx`.
|
|
141
|
+
*/
|
|
142
|
+
export function homeFooterHints(): FooterHint[] {
|
|
143
|
+
return [
|
|
144
|
+
{ keys: "↑↓", label: "nav" },
|
|
145
|
+
{ keys: "↵", label: "attach/launch" },
|
|
146
|
+
{ keys: "n", label: "new" },
|
|
147
|
+
{ keys: "l", label: "launch" },
|
|
148
|
+
...panelHints("widget"),
|
|
149
|
+
{ keys: GRAMMAR_KEYS.filter[0]!, label: "filter" },
|
|
150
|
+
{ keys: GRAMMAR_KEYS.help[0]!, label: "help" },
|
|
151
|
+
{ keys: GRAMMAR_KEYS.quit[0]!, label: "quit" },
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* PURE — the compact PICKER popup's footer hints. The picker ends in a
|
|
157
|
+
* switch-client + close, so it advertises that. The discoverable universal verbs
|
|
158
|
+
* (filter / help / dismiss) come from {@link GRAMMAR_KEYS} so the footer can't
|
|
159
|
+
* advertise a key the grammar doesn't bind — in particular `? help`, which
|
|
160
|
+
* surfaces the full keybindings overlay from inside the popup.
|
|
161
|
+
*/
|
|
162
|
+
export function pickerFooterHints(): FooterHint[] {
|
|
163
|
+
return [
|
|
164
|
+
{ keys: "↵", label: "switch" },
|
|
165
|
+
{ keys: "l", label: "launch" },
|
|
166
|
+
{ keys: GRAMMAR_KEYS.filter[0]!, label: "find" },
|
|
167
|
+
{ keys: GRAMMAR_KEYS.help[0]!, label: "help" },
|
|
168
|
+
{ keys: "esc", label: "close" },
|
|
169
|
+
];
|
|
170
|
+
}
|