tmux-ide 2.9.3 → 2.9.5
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/LICENSE +1 -1
- package/README.md +120 -139
- package/bin/cli.js +4847 -3585
- package/package.json +16 -9
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
- package/skill/SKILL.md +30 -30
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code agent teams — opt-out enablement in the user's Claude settings.
|
|
3
|
+
*
|
|
4
|
+
* Agent teams let one Claude Code session spawn teammates; with
|
|
5
|
+
* `teammateMode: "auto"` Claude puts them in split panes when it runs inside
|
|
6
|
+
* tmux (in-process elsewhere), which is exactly what tmux-ide shows and
|
|
7
|
+
* routes. The installer enables this by default and `tmux-ide integration
|
|
8
|
+
* agent-teams enable|disable|status` manages it explicitly.
|
|
9
|
+
*
|
|
10
|
+
* Rules (pure, in {@link mergeAgentTeams}):
|
|
11
|
+
* - `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set to "1" only when absent.
|
|
12
|
+
* An explicit off value ("0", "false", false, 0…) is the user's opt-out: the
|
|
13
|
+
* installer never overrides it and touches nothing else. Only an explicit
|
|
14
|
+
* `tmux-ide integration agent-teams enable` (overrideOptOut) turns it back on.
|
|
15
|
+
* - `teammateMode: "auto"` is set only when `teammateMode` is absent.
|
|
16
|
+
* - Every other key is preserved. Nothing changed → nothing written.
|
|
17
|
+
*
|
|
18
|
+
* Writes (io) reuse the hooks integration's settings path resolution, including
|
|
19
|
+
* the `TMUX_IDE_CLAUDE_SETTINGS` override: one-time timestamped backup before
|
|
20
|
+
* the first modification, then an atomic temp-file + rename. Invalid JSON is
|
|
21
|
+
* reported and never rewritten.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
chmodSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
renameSync,
|
|
30
|
+
statSync,
|
|
31
|
+
copyFileSync,
|
|
32
|
+
unlinkSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
accessSync,
|
|
35
|
+
constants,
|
|
36
|
+
} from "node:fs";
|
|
37
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
38
|
+
import { resolveRuntimeNamespace, runtimeOwnedPath } from "../../lib/runtime-namespace.ts";
|
|
39
|
+
|
|
40
|
+
export const AGENT_TEAMS_ENV = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS";
|
|
41
|
+
export const DEFAULT_TEAMMATE_MODE = "auto";
|
|
42
|
+
const BACKUP_INFIX = ".tmux-ide-backup-";
|
|
43
|
+
|
|
44
|
+
type Settings = Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
export type AgentTeamsSetting = "enabled" | "disabled-by-user" | "not-configured";
|
|
47
|
+
|
|
48
|
+
/** PURE — an explicit off value is the user's opt-out. */
|
|
49
|
+
export function isOptOut(value: unknown): boolean {
|
|
50
|
+
if (value === false || value === 0) return true;
|
|
51
|
+
return typeof value === "string" && /^\s*(0|false|off|no)\s*$/iu.test(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function envOf(settings: Settings): Record<string, unknown> | undefined {
|
|
55
|
+
const env = settings.env;
|
|
56
|
+
if (env === undefined) return undefined;
|
|
57
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
58
|
+
throw new Error("settings env must be an object");
|
|
59
|
+
return env as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** PURE — how the settings currently stand for agent teams. */
|
|
63
|
+
export function agentTeamsSetting(settings: Settings): AgentTeamsSetting {
|
|
64
|
+
const value = envOf(settings)?.[AGENT_TEAMS_ENV];
|
|
65
|
+
if (value === undefined) return "not-configured";
|
|
66
|
+
return isOptOut(value) ? "disabled-by-user" : "enabled";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AgentTeamsMerge {
|
|
70
|
+
readonly settings: Settings;
|
|
71
|
+
/** Human-readable `key=value` list of what was set; empty when unchanged. */
|
|
72
|
+
readonly changes: readonly string[];
|
|
73
|
+
readonly setting: AgentTeamsSetting;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* PURE — enable agent teams without overriding anything the user chose, unless
|
|
78
|
+
* `overrideOptOut` (the user explicitly asked to enable) replaces their "0".
|
|
79
|
+
*/
|
|
80
|
+
export function mergeAgentTeams(
|
|
81
|
+
settings: Settings,
|
|
82
|
+
{ overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
|
|
83
|
+
): AgentTeamsMerge {
|
|
84
|
+
const setting = agentTeamsSetting(settings);
|
|
85
|
+
if (setting === "disabled-by-user" && !overrideOptOut) return { settings, changes: [], setting };
|
|
86
|
+
const changes: string[] = [];
|
|
87
|
+
let next: Settings = settings;
|
|
88
|
+
if (setting !== "enabled") {
|
|
89
|
+
next = { ...next, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "1" } };
|
|
90
|
+
changes.push(`env.${AGENT_TEAMS_ENV}=1`);
|
|
91
|
+
}
|
|
92
|
+
if (!("teammateMode" in settings)) {
|
|
93
|
+
next = { ...next, teammateMode: DEFAULT_TEAMMATE_MODE };
|
|
94
|
+
changes.push(`teammateMode=${DEFAULT_TEAMMATE_MODE}`);
|
|
95
|
+
}
|
|
96
|
+
return { settings: next, changes, setting: "enabled" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** PURE — record an explicit opt-out ("0"), which later enables respect. */
|
|
100
|
+
export function disableAgentTeams(settings: Settings): AgentTeamsMerge {
|
|
101
|
+
if (agentTeamsSetting(settings) === "disabled-by-user")
|
|
102
|
+
return { settings, changes: [], setting: "disabled-by-user" };
|
|
103
|
+
return {
|
|
104
|
+
settings: { ...settings, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "0" } },
|
|
105
|
+
changes: [`env.${AGENT_TEAMS_ENV}=0`],
|
|
106
|
+
setting: "disabled-by-user",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface AgentTeamsPaths {
|
|
111
|
+
/** Claude Code's user settings file. */
|
|
112
|
+
readonly settingsPath: string;
|
|
113
|
+
/** Claude Code's home (~/.claude); its presence means Claude is installed. */
|
|
114
|
+
readonly claudeDir: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function agentTeamsPaths(): AgentTeamsPaths {
|
|
118
|
+
const namespace = resolveRuntimeNamespace();
|
|
119
|
+
return { settingsPath: namespace.claudeSettingsPath, claudeDir: namespace.claudeDir };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `claude` on PATH, without spawning anything. */
|
|
123
|
+
export function claudeOnPath(path = process.env.PATH ?? ""): boolean {
|
|
124
|
+
return path
|
|
125
|
+
.split(delimiter)
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.some((dir) => {
|
|
128
|
+
try {
|
|
129
|
+
accessSync(join(dir, "claude"), constants.X_OK);
|
|
130
|
+
return statSync(join(dir, "claude")).isFile();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class InvalidClaudeSettingsError extends Error {}
|
|
138
|
+
|
|
139
|
+
function readSettings(path: string): { settings: Settings; exists: boolean } {
|
|
140
|
+
if (!existsSync(path)) return { settings: {}, exists: false };
|
|
141
|
+
try {
|
|
142
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
143
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
144
|
+
throw new Error("not an object");
|
|
145
|
+
envOf(parsed as Settings);
|
|
146
|
+
return { settings: parsed as Settings, exists: true };
|
|
147
|
+
} catch {
|
|
148
|
+
throw new InvalidClaudeSettingsError(
|
|
149
|
+
`${path} is not valid settings JSON — fix or move it, then retry`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Existing one-time backup, if tmux-ide already made one for this file. */
|
|
155
|
+
function existingBackup(settingsPath: string): string | null {
|
|
156
|
+
const dir = dirname(settingsPath);
|
|
157
|
+
const prefix = `${basename(settingsPath)}${BACKUP_INFIX}`;
|
|
158
|
+
try {
|
|
159
|
+
const found = readdirSync(dir).find((name) => name.startsWith(prefix));
|
|
160
|
+
return found ? join(dir, found) : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeSettings(path: string, settings: Settings, exists: boolean): string | null {
|
|
167
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
168
|
+
let backup: string | null = null;
|
|
169
|
+
if (exists) {
|
|
170
|
+
backup = existingBackup(path);
|
|
171
|
+
if (!backup) {
|
|
172
|
+
const stamp = new Date()
|
|
173
|
+
.toISOString()
|
|
174
|
+
.replace(/[-:]/gu, "")
|
|
175
|
+
.replace(/\.\d+Z$/u, "Z");
|
|
176
|
+
backup = `${path}${BACKUP_INFIX}${stamp}`;
|
|
177
|
+
copyFileSync(path, backup);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const temporary = join(dirname(path), `.${basename(path)}.tmux-ide-${process.pid}.tmp`);
|
|
181
|
+
try {
|
|
182
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
183
|
+
if (exists) chmodSync(temporary, statSync(path).mode & 0o777);
|
|
184
|
+
renameSync(temporary, path);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
try {
|
|
187
|
+
unlinkSync(temporary);
|
|
188
|
+
} catch {
|
|
189
|
+
/* already renamed or never written */
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
return backup;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface AgentTeamsResult {
|
|
197
|
+
readonly action:
|
|
198
|
+
| "enabled"
|
|
199
|
+
| "disabled"
|
|
200
|
+
| "unchanged"
|
|
201
|
+
| "disabled-by-user"
|
|
202
|
+
| "skipped"
|
|
203
|
+
| "invalid";
|
|
204
|
+
readonly settingsPath: string;
|
|
205
|
+
readonly changes: readonly string[];
|
|
206
|
+
readonly backup: string | null;
|
|
207
|
+
readonly claudeDetected: boolean;
|
|
208
|
+
readonly message: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function detected(paths: AgentTeamsPaths, onPath: () => boolean): boolean {
|
|
212
|
+
return existsSync(paths.claudeDir) || onPath();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function guarded(paths: AgentTeamsPaths | undefined): AgentTeamsPaths {
|
|
216
|
+
if (!paths && resolveRuntimeNamespace().development)
|
|
217
|
+
throw new Error("Development agent-teams changes require explicit fixture paths");
|
|
218
|
+
const resolved = paths ?? agentTeamsPaths();
|
|
219
|
+
runtimeOwnedPath(resolved.settingsPath);
|
|
220
|
+
return resolved;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Enable agent teams when Claude Code is installed and the user hasn't opted out. */
|
|
224
|
+
export function enableClaudeAgentTeams(
|
|
225
|
+
paths?: AgentTeamsPaths,
|
|
226
|
+
onPath: () => boolean = () => claudeOnPath(),
|
|
227
|
+
{ overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
|
|
228
|
+
): AgentTeamsResult {
|
|
229
|
+
const resolved = guarded(paths);
|
|
230
|
+
const { settingsPath } = resolved;
|
|
231
|
+
const base = { settingsPath, changes: [], backup: null } as const;
|
|
232
|
+
if (!detected(resolved, onPath))
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
action: "skipped",
|
|
236
|
+
claudeDetected: false,
|
|
237
|
+
message: `Claude Code not found (no ${resolved.claudeDir}, no claude on PATH); agent teams not configured.`,
|
|
238
|
+
};
|
|
239
|
+
let read: ReturnType<typeof readSettings>;
|
|
240
|
+
try {
|
|
241
|
+
read = readSettings(settingsPath);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return { ...base, action: "invalid", claudeDetected: true, message: (error as Error).message };
|
|
244
|
+
}
|
|
245
|
+
const merged = mergeAgentTeams(read.settings, { overrideOptOut });
|
|
246
|
+
if (merged.setting === "disabled-by-user")
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
action: "disabled-by-user",
|
|
250
|
+
claudeDetected: true,
|
|
251
|
+
message:
|
|
252
|
+
`Claude Code agent teams are disabled by user in ${settingsPath} (${AGENT_TEAMS_ENV}); ` +
|
|
253
|
+
"left unchanged. Enable explicitly: tmux-ide integration agent-teams enable",
|
|
254
|
+
};
|
|
255
|
+
if (merged.changes.length === 0)
|
|
256
|
+
return {
|
|
257
|
+
...base,
|
|
258
|
+
action: "unchanged",
|
|
259
|
+
claudeDetected: true,
|
|
260
|
+
message: `Claude Code agent teams already enabled in ${settingsPath}.`,
|
|
261
|
+
};
|
|
262
|
+
const backup = writeSettings(settingsPath, merged.settings, read.exists);
|
|
263
|
+
return {
|
|
264
|
+
settingsPath,
|
|
265
|
+
changes: merged.changes,
|
|
266
|
+
backup,
|
|
267
|
+
action: "enabled",
|
|
268
|
+
claudeDetected: true,
|
|
269
|
+
message:
|
|
270
|
+
`Enabled Claude Code agent teams in ${settingsPath} (${merged.changes.join(", ")}` +
|
|
271
|
+
`${backup ? `; backup: ${backup}` : ""}). Disable: tmux-ide integration agent-teams disable`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Record the user's opt-out so neither the installer nor `enable` turns it back on silently. */
|
|
276
|
+
export function disableClaudeAgentTeams(paths?: AgentTeamsPaths): AgentTeamsResult {
|
|
277
|
+
const resolved = guarded(paths);
|
|
278
|
+
const { settingsPath } = resolved;
|
|
279
|
+
const base = { settingsPath, changes: [], backup: null, claudeDetected: true } as const;
|
|
280
|
+
let read: ReturnType<typeof readSettings>;
|
|
281
|
+
try {
|
|
282
|
+
read = readSettings(settingsPath);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return { ...base, action: "invalid", message: (error as Error).message };
|
|
285
|
+
}
|
|
286
|
+
const next = disableAgentTeams(read.settings);
|
|
287
|
+
if (next.changes.length === 0)
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
action: "unchanged",
|
|
291
|
+
message: `Claude Code agent teams already disabled in ${settingsPath}.`,
|
|
292
|
+
};
|
|
293
|
+
const backup = writeSettings(settingsPath, next.settings, read.exists);
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
changes: next.changes,
|
|
297
|
+
backup,
|
|
298
|
+
action: "disabled",
|
|
299
|
+
message:
|
|
300
|
+
`Disabled Claude Code agent teams in ${settingsPath} (${next.changes.join(", ")}` +
|
|
301
|
+
`${backup ? `; backup: ${backup}` : ""}). Re-enable: tmux-ide integration agent-teams enable`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface AgentTeamsStatus {
|
|
306
|
+
readonly settingsPath: string;
|
|
307
|
+
readonly claudeDetected: boolean;
|
|
308
|
+
readonly setting: AgentTeamsSetting | "invalid-settings";
|
|
309
|
+
readonly teammateMode: string | null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function claudeAgentTeamsStatus(
|
|
313
|
+
paths: AgentTeamsPaths = agentTeamsPaths(),
|
|
314
|
+
onPath: () => boolean = () => claudeOnPath(),
|
|
315
|
+
): AgentTeamsStatus {
|
|
316
|
+
const claudeDetected = detected(paths, onPath);
|
|
317
|
+
try {
|
|
318
|
+
const { settings } = readSettings(paths.settingsPath);
|
|
319
|
+
const mode = settings.teammateMode;
|
|
320
|
+
return {
|
|
321
|
+
settingsPath: paths.settingsPath,
|
|
322
|
+
claudeDetected,
|
|
323
|
+
setting: agentTeamsSetting(settings),
|
|
324
|
+
teammateMode: typeof mode === "string" ? mode : null,
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
return {
|
|
328
|
+
settingsPath: paths.settingsPath,
|
|
329
|
+
claudeDetected,
|
|
330
|
+
setting: "invalid-settings",
|
|
331
|
+
teammateMode: null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STOCK_CAPTURE_TAB_UNAVAILABLE } from "@tmux-ide/contracts";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import type {
|
|
3
4
|
WindowLinkTopology,
|
|
@@ -357,7 +358,12 @@ function updateFromDelivery(
|
|
|
357
358
|
stateHash: envelope.canonicalStateHash,
|
|
358
359
|
hashAlgorithm: "fnv1a64-v1" as const,
|
|
359
360
|
};
|
|
360
|
-
|
|
361
|
+
// Wire patches may span skipped revisions; ordinary canonical patches may not.
|
|
362
|
+
// Publish the fully verified resulting state at its actual revision.
|
|
363
|
+
if (
|
|
364
|
+
payload.frame === "seed" ||
|
|
365
|
+
(payload.frame === "patch" && payload.revision > payload.baseRevision + 1)
|
|
366
|
+
) {
|
|
361
367
|
return Object.freeze({
|
|
362
368
|
...common,
|
|
363
369
|
type: "terminal.seed" as const,
|
|
@@ -410,6 +416,7 @@ class WireTerminalEndpoint {
|
|
|
410
416
|
#resolveReady!: (ready: boolean) => void;
|
|
411
417
|
#readySettled = false;
|
|
412
418
|
#hasCanonicalSeed = false;
|
|
419
|
+
#unavailable: string | null = null;
|
|
413
420
|
|
|
414
421
|
constructor(options: {
|
|
415
422
|
workspaceName: string;
|
|
@@ -450,8 +457,14 @@ class WireTerminalEndpoint {
|
|
|
450
457
|
this.#resolveReady(true);
|
|
451
458
|
}
|
|
452
459
|
|
|
460
|
+
get settled(): boolean {
|
|
461
|
+
return this.#unavailable !== null || this.inputReady;
|
|
462
|
+
}
|
|
463
|
+
|
|
453
464
|
get inputReady(): boolean {
|
|
454
|
-
return
|
|
465
|
+
return (
|
|
466
|
+
!this.#closed && this.#unavailable === null && this.#hasCanonicalSeed && !this.#reseedRequired
|
|
467
|
+
);
|
|
455
468
|
}
|
|
456
469
|
|
|
457
470
|
async subscription(): Promise<
|
|
@@ -477,6 +490,7 @@ class WireTerminalEndpoint {
|
|
|
477
490
|
},
|
|
478
491
|
);
|
|
479
492
|
this.#subscription = subscription;
|
|
493
|
+
if (this.#unavailable) subscription.unavailable(this.#unavailable);
|
|
480
494
|
return subscription;
|
|
481
495
|
}
|
|
482
496
|
|
|
@@ -489,6 +503,24 @@ class WireTerminalEndpoint {
|
|
|
489
503
|
return;
|
|
490
504
|
}
|
|
491
505
|
if (message.type === "terminal.delivery.fault") {
|
|
506
|
+
if (message.deliveryNonce !== this.#negotiated.deliveryNonce) {
|
|
507
|
+
this.#failConnection(new Error("Terminal fault nonce does not match negotiation"));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// The exact shared diagnostic is currently the compatibility-preserving discriminator.
|
|
511
|
+
if (message.reason === "source-closed" && message.message === STOCK_CAPTURE_TAB_UNAVAILABLE) {
|
|
512
|
+
this.#unavailable = message.message;
|
|
513
|
+
this.#decodeToken = null;
|
|
514
|
+
this.#assembler = null;
|
|
515
|
+
this.#assemblyStorage = null;
|
|
516
|
+
this.#envelope = null;
|
|
517
|
+
this.#pending = null;
|
|
518
|
+
this.#canonicalSnapshot = null;
|
|
519
|
+
this.#hasCanonicalSeed = false;
|
|
520
|
+
this.#subscription?.unavailable(message.message);
|
|
521
|
+
this.#canonicalSeedReady();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
492
524
|
const detail = message.message.trim();
|
|
493
525
|
this.#failConnection(
|
|
494
526
|
new Error(
|
|
@@ -497,6 +529,7 @@ class WireTerminalEndpoint {
|
|
|
497
529
|
);
|
|
498
530
|
return;
|
|
499
531
|
}
|
|
532
|
+
if (this.#unavailable) return;
|
|
500
533
|
if (message.type === "terminal.delivery") {
|
|
501
534
|
this.#acceptEnvelope(message);
|
|
502
535
|
return;
|
|
@@ -859,6 +892,31 @@ class WireTerminalSubscription implements SessionRuntimeTerminalSubscription<
|
|
|
859
892
|
readonly #didClose: () => void;
|
|
860
893
|
frozen = false;
|
|
861
894
|
closed = false;
|
|
895
|
+
#unavailable: string | null = null;
|
|
896
|
+
readonly #unavailableListeners = new Set<(message: string) => void>();
|
|
897
|
+
onUnavailable(listener: (message: string) => void): () => void {
|
|
898
|
+
if (this.closed) return () => {};
|
|
899
|
+
this.#unavailableListeners.add(listener);
|
|
900
|
+
if (this.#unavailable) {
|
|
901
|
+
try {
|
|
902
|
+
listener(this.#unavailable);
|
|
903
|
+
} catch {
|
|
904
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return () => this.#unavailableListeners.delete(listener);
|
|
908
|
+
}
|
|
909
|
+
unavailable(message: string): void {
|
|
910
|
+
if (this.closed) return;
|
|
911
|
+
this.#unavailable = message;
|
|
912
|
+
for (const listener of [...this.#unavailableListeners]) {
|
|
913
|
+
try {
|
|
914
|
+
listener(message);
|
|
915
|
+
} catch {
|
|
916
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
862
920
|
|
|
863
921
|
constructor(generation: string, flush: () => void, didClose: () => void) {
|
|
864
922
|
this.generation = generation;
|
|
@@ -901,6 +959,7 @@ class WireTerminalSubscription implements SessionRuntimeTerminalSubscription<
|
|
|
901
959
|
if (this.closed) return;
|
|
902
960
|
this.closed = true;
|
|
903
961
|
this.#listeners.clear();
|
|
962
|
+
this.#unavailableListeners.clear();
|
|
904
963
|
this.#didClose();
|
|
905
964
|
}
|
|
906
965
|
}
|
|
@@ -1472,7 +1531,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1472
1531
|
});
|
|
1473
1532
|
return request.result;
|
|
1474
1533
|
}
|
|
1475
|
-
if ([...endpoints.values()].some((endpoint) => !endpoint.
|
|
1534
|
+
if ([...endpoints.values()].some((endpoint) => !endpoint.settled))
|
|
1476
1535
|
return Promise.resolve("geometry-authority-conflict");
|
|
1477
1536
|
return (
|
|
1478
1537
|
semanticWindowId === undefined
|
|
@@ -1525,7 +1584,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1525
1584
|
if (
|
|
1526
1585
|
closed ||
|
|
1527
1586
|
!latestLayoutSnapshot.windowLinks ||
|
|
1528
|
-
[...endpoints.values()].some((endpoint) => !endpoint.
|
|
1587
|
+
[...endpoints.values()].some((endpoint) => !endpoint.settled)
|
|
1529
1588
|
)
|
|
1530
1589
|
throw new Error("Terminal inventory is still receiving canonical state");
|
|
1531
1590
|
return (await opened.submitIntent(operationId, intent)) ?? undefined;
|
|
@@ -709,7 +709,15 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
709
709
|
}
|
|
710
710
|
if (this._needsWalk) {
|
|
711
711
|
this._needsWalk = false;
|
|
712
|
-
|
|
712
|
+
try {
|
|
713
|
+
this.walk();
|
|
714
|
+
} catch (error) {
|
|
715
|
+
// The source may have consumed its dirty rows before a grapheme or
|
|
716
|
+
// presentation pass fails. Rebuild the retained surface on retry.
|
|
717
|
+
this._needsWalk = true;
|
|
718
|
+
this._forceFull = true;
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
713
721
|
} else if (this._needsCursorPresentation) {
|
|
714
722
|
this.applyCursorOnlyPresentation();
|
|
715
723
|
}
|