@zergai/ztc 0.9.0-beta.1 → 0.10.0-beta.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/README.md CHANGED
@@ -1322,6 +1322,49 @@ npm run web # Web UI at http://localhost:3000
1322
1322
  npm start # Production entry point
1323
1323
  ```
1324
1324
 
1325
+ ### Claude Code and Codex in panes
1326
+
1327
+ ZTC hosts your own Claude Code and Codex next to its agents. Each pane runs
1328
+ your installed, unmodified `claude` or `codex`, signed in with your own account
1329
+ through that CLI's own sign-in; ZTC never reads, stores or forwards those
1330
+ credentials. Panes need zmux (plain `ztc`) or the desktop app.
1331
+
1332
+ - `/claude [prompt | flags]` and `/codex [prompt | flags]` open one in this
1333
+ pane's directory. Plain text is the opening prompt (`/claude fix the failing
1334
+ test`); text that starts with a flag goes to the CLI as its arguments
1335
+ (`/claude --continue`, `/codex -m gpt-5 "review the diff"`).
1336
+ - Ctrl+B c offers Claude Code and Codex next to bash and ZTC when they are
1337
+ installed. In the desktop app, the tab strip's + opens a menu: New ZTC Pane,
1338
+ New Claude Code Pane, New Codex Pane, New Shell Pane. Cmd+T still opens a ZTC
1339
+ tab directly.
1340
+
1341
+ Each of these panes starts with ZTC's workspace MCP server (`ztc
1342
+ workspace-mcp`) attached for that session only, through Claude Code's
1343
+ `--mcp-config` or Codex's `-c mcp_servers.ztc.*`, so your own CLI configuration
1344
+ is never edited. Its tools give the agent the workspace:
1345
+
1346
+ | Tool | What it does |
1347
+ |------|--------------|
1348
+ | `list_panes` | Each pane's id, name and kind, and which pane is the agent's own |
1349
+ | `open_pane` | Open a ZTC agent, Claude Code, Codex or a shell, optionally with a first task |
1350
+ | `read_pane` | Read a pane's scrollback, or what is on screen |
1351
+ | `type_into_pane` | Type into a pane, then press Enter |
1352
+ | `send_message` | Message another agent over ZTC's bus |
1353
+ | `close_pane` | Close another pane |
1354
+
1355
+ A ZTC agent reaches these panes by name as well: a message for a Claude Code or
1356
+ Codex pane is typed into it as `[Message from ben (pane 2)] …` (a question also
1357
+ says how to answer), and the answer comes back as a message from that pane.
1358
+ Broadcasts to `all` reach ZTC agents only.
1359
+
1360
+ ZTC's own agent loop uses an Anthropic API key for Claude models (`/config key`
1361
+ or `ANTHROPIC_API_KEY`). Anthropic's [legal and compliance
1362
+ terms](https://code.claude.com/docs/en/legal-and-compliance) reserve Claude
1363
+ subscription sign-in for Claude Code and Anthropic's own apps: third-party tools
1364
+ may not route requests through Free, Pro, Max or Ultra plan credentials or
1365
+ handle Claude.ai credentials. Running your own Claude Code in a pane is how to
1366
+ use a Claude subscription alongside ZTC.
1367
+
1325
1368
  ### Claude Code CLI OAuth
1326
1369
 
1327
1370
  ZTC can use Claude Code subscription/OAuth auth through the official Claude Code CLI instead of direct Anthropic API-key billing:
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `/claude` and `/codex` — open the user's own Claude Code or Codex in a new
3
+ * pane, in this pane's directory.
4
+ *
5
+ * The CLI signs in with the user's own account through its own flow; ZTC only
6
+ * runs it, unmodified, and attaches its workspace MCP server so the agent can
7
+ * open, read, type into and message other panes. Plain text after the command
8
+ * is the opening prompt; text starting with a flag is the CLI's arguments.
9
+ */
10
+ import { type AgentCliId, type PaneProgram } from '../../panes/agent_cli.js';
11
+ import type { PaneCommandTransport } from '../../ui/ratatui/pane_relay.js';
12
+ import type { Command } from './types.js';
13
+ export interface AgentCliCommandDeps {
14
+ transport?: () => PaneCommandTransport | null;
15
+ findBinary?: (id: AgentCliId) => string | null;
16
+ server?: () => PaneProgram | Error;
17
+ env?: () => NodeJS.ProcessEnv;
18
+ }
19
+ export declare function createAgentCliCommand(id: AgentCliId, deps?: AgentCliCommandDeps): Command;
20
+ export declare const claudeCommand: Command;
21
+ export declare const codexCommand: Command;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `/claude` and `/codex` — open the user's own Claude Code or Codex in a new
3
+ * pane, in this pane's directory.
4
+ *
5
+ * The CLI signs in with the user's own account through its own flow; ZTC only
6
+ * runs it, unmodified, and attaches its workspace MCP server so the agent can
7
+ * open, read, type into and message other panes. Plain text after the command
8
+ * is the opening prompt; text starting with a flag is the CLI's arguments.
9
+ */
10
+ import { getMuxTransport } from '../tools/pane.js';
11
+ import { AGENT_CLIS, findAgentCli, formatCliArguments, parseAgentCommandBody, workspaceMcpProgram, } from '../../panes/agent_cli.js';
12
+ import { openAgentPane } from '../../panes/agent_panes.js';
13
+ function shownCommand(binary, request) {
14
+ const words = [...request.words, ...(request.prompt ? ['--', request.prompt] : [])];
15
+ return words.length > 0 ? `${binary} ${formatCliArguments(words)}` : binary;
16
+ }
17
+ export function createAgentCliCommand(id, deps = {}) {
18
+ const spec = AGENT_CLIS[id];
19
+ const account = id === 'claude' ? 'Claude' : 'ChatGPT';
20
+ return {
21
+ name: id,
22
+ description: `Open ${spec.label} in a new pane, signed in with your own ${account} account`,
23
+ usage: '[prompt | flags]',
24
+ agentRunnable: true,
25
+ category: 'panes',
26
+ handler: async (args, ctx) => {
27
+ let request;
28
+ try {
29
+ request = parseAgentCommandBody(args.join(' '));
30
+ }
31
+ catch (error) {
32
+ ctx.addMessage({ role: 'system', content: `/${id}: ${error.message}` });
33
+ return;
34
+ }
35
+ const transport = (deps.transport ?? getMuxTransport)();
36
+ if (!transport) {
37
+ ctx.addMessage({
38
+ role: 'system',
39
+ content: `/${id} opens ${spec.label} in a pane, which needs ZTC's panes: start ZTC with zmux (plain \`ztc\`) or use the desktop app. You can also run \`${spec.binary}\` in another terminal.`,
40
+ });
41
+ return;
42
+ }
43
+ const cwd = ctx.shell.getCwd();
44
+ const result = await openAgentPane(transport, {
45
+ cli: id,
46
+ request,
47
+ cwd,
48
+ findBinary: deps.findBinary ?? findAgentCli,
49
+ server: (deps.server ?? workspaceMcpProgram)(),
50
+ env: (deps.env ?? (() => process.env))(),
51
+ });
52
+ if (result.error) {
53
+ ctx.addMessage({ role: 'system', content: result.error });
54
+ return;
55
+ }
56
+ ctx.addMessage({
57
+ role: 'system',
58
+ content: `Opened ${result.title} in pane ${result.paneId} (${cwd}): ${shownCommand(spec.binary, request)}`,
59
+ });
60
+ if (result.notice)
61
+ ctx.addMessage({ role: 'system', content: result.notice });
62
+ },
63
+ };
64
+ }
65
+ export const claudeCommand = createAgentCliCommand('claude');
66
+ export const codexCommand = createAgentCliCommand('codex');
@@ -30,6 +30,7 @@ import { connectCommand } from './connect.js';
30
30
  import { reloadCommand, restartCommand } from './restart.js';
31
31
  import { initCommand } from './init.js';
32
32
  import { forkCommand, sideCommand } from './fork.js';
33
+ import { claudeCommand, codexCommand } from './agent_cli.js';
33
34
  import { diffCommand } from './diff.js';
34
35
  import { rawCommand } from './raw.js';
35
36
  import { themeCommand } from './theme.js';
@@ -82,7 +83,7 @@ import { effortCommand, thinkingCommand, reasoningCommand, fastCommand } from '.
82
83
  import { runtimePluginRegistry } from '../../plugin/runtime_registry.js';
83
84
  const commandList = [];
84
85
  const helpCommand = makeHelpCommand(() => commandList);
85
- commandList.push(helpCommand, authCommand, loginCommand, logoutCommand, accountsCommand, zergCommand, zergsCommand, configCommand, debugCommand, clearCommand, handoffCommand, statusCommand, runtimeCommand, environmentCommand, jobCommand, shareCommand, hooksCommand, emulationCommand, shellCommand, cdCommand, clipboardCommand, modelsCommand, modelCommand, permissionsCommand, resourcesCommand, initCommand, forkCommand, sideCommand, diffCommand, rawCommand, themeCommand, doctorCommand, intentCommand, profileCommand, skillsCommand, runbooksCommand, keybindingsCommand, keymapCommand, shortcutsCommand, updateCommand, desktopCommand, layoutCommand, setupCommand, connectCommand, keysCommand, restartCommand, reloadCommand, inputModeCommand, retryCommand, dictationStatusCommand, embeddedCommand, benchCommand, voiceCommand, sfxCommand, compactCommand, automodeCommand, sessionsCommand, resumeCommand, rewindCommand, renameCommand, labelCommand, inboxCommand, attentionCommand, contextCommand, evidenceCommand, boardCommand, usageCommand, shannonRunnerCommand, planCommand, executeCommand, modeCommand, modeListCommand, exitModeCommand, validateCommand, previewCommand, saveCommand, dcaigCommand, manifestCommand, swarmCommand, swarmStopCommand, swarmStatusCommand, superviseCommand, superviseStopCommand, superviseStatusCommand, zlickyCommand, zlickyStopCommand, zlickyStatusCommand, zlickyActCommand, zlickyPresenceCommand, zlickyPttCommand, simCommand, simStopCommand, simStatusCommand, simRunCommand, simCheckpointCommand, simResumeCommand, evolveCommand, evolveStopCommand, evolveStatusCommand, evolveBestCommand, optimizeCommand, batchCommand, batchStatusCommand, batchStopCommand, workflowsCommand, mirrorCommand, settingsCommand, mcpCommand, loopCommand, goalCommand, effortCommand, thinkingCommand, reasoningCommand, fastCommand, exitCommand);
86
+ commandList.push(helpCommand, authCommand, loginCommand, logoutCommand, accountsCommand, zergCommand, zergsCommand, configCommand, debugCommand, clearCommand, handoffCommand, statusCommand, runtimeCommand, environmentCommand, jobCommand, shareCommand, hooksCommand, emulationCommand, shellCommand, cdCommand, clipboardCommand, modelsCommand, modelCommand, permissionsCommand, resourcesCommand, initCommand, forkCommand, sideCommand, claudeCommand, codexCommand, diffCommand, rawCommand, themeCommand, doctorCommand, intentCommand, profileCommand, skillsCommand, runbooksCommand, keybindingsCommand, keymapCommand, shortcutsCommand, updateCommand, desktopCommand, layoutCommand, setupCommand, connectCommand, keysCommand, restartCommand, reloadCommand, inputModeCommand, retryCommand, dictationStatusCommand, embeddedCommand, benchCommand, voiceCommand, sfxCommand, compactCommand, automodeCommand, sessionsCommand, resumeCommand, rewindCommand, renameCommand, labelCommand, inboxCommand, attentionCommand, contextCommand, evidenceCommand, boardCommand, usageCommand, shannonRunnerCommand, planCommand, executeCommand, modeCommand, modeListCommand, exitModeCommand, validateCommand, previewCommand, saveCommand, dcaigCommand, manifestCommand, swarmCommand, swarmStopCommand, swarmStatusCommand, superviseCommand, superviseStopCommand, superviseStatusCommand, zlickyCommand, zlickyStopCommand, zlickyStatusCommand, zlickyActCommand, zlickyPresenceCommand, zlickyPttCommand, simCommand, simStopCommand, simStatusCommand, simRunCommand, simCheckpointCommand, simResumeCommand, evolveCommand, evolveStopCommand, evolveStatusCommand, evolveBestCommand, optimizeCommand, batchCommand, batchStatusCommand, batchStopCommand, workflowsCommand, mirrorCommand, settingsCommand, mcpCommand, loopCommand, goalCommand, effortCommand, thinkingCommand, reasoningCommand, fastCommand, exitCommand);
86
87
  export const commands = commandList;
87
88
  export function refreshPluginCommands() {
88
89
  const existing = new Set(commandList.map(command => command.name));
@@ -5,7 +5,7 @@
5
5
  * from cli_ratatui.ts at startup.
6
6
  */
7
7
  import { readPaneResponseSnapshot } from '../../utils/pane_response.js';
8
- import { resolvePaneIdByName } from './pane.js';
8
+ import { listWorkspacePanes, resolvePaneIdByName } from './pane.js';
9
9
  // --- Setter-based transport access ---
10
10
  let busTransportRef = null;
11
11
  export function setBusTransport(transport) {
@@ -34,6 +34,18 @@ async function resolveBusRecipient(value) {
34
34
  return parsed;
35
35
  return resolvePaneIdByName(String(value).trim());
36
36
  }
37
+ /**
38
+ * The name of a pane that is not a ZTC agent (Claude Code, Codex, a shell), or
39
+ * null. Such a pane writes no ZTC response to wait for: its agent answers later
40
+ * with a message of its own.
41
+ */
42
+ async function nonZtcPaneName(paneId) {
43
+ const panes = await listWorkspacePanes(undefined, 3000);
44
+ if (panes instanceof Error)
45
+ return null;
46
+ const pane = panes.find(candidate => candidate.id === paneId);
47
+ return pane && pane.pane_type !== 'ztc' ? (pane.labels?.manual_title || pane.title) : null;
48
+ }
37
49
  async function waitForNewPaneResponse(paneId, baseline, timeoutMs) {
38
50
  const deadline = Date.now() + timeoutMs;
39
51
  while (Date.now() < deadline) {
@@ -182,10 +194,17 @@ export const sendMessageTool = {
182
194
  }
183
195
  const rawKind = String(args.kind || 'statement').toLowerCase();
184
196
  const kind = rawKind === 'question' || rawKind === 'request' || rawKind === 'reply' ? rawKind : 'statement';
185
- const waitForResponse = typeof to === 'number' && (kind === 'question' || kind === 'request');
186
- const baseline = waitForResponse ? readPaneResponseSnapshot(to) : null;
187
- const outgoingText = waitForResponse ? buildZtcRequestPayload(text, kind) : text;
197
+ const asks = typeof to === 'number' && (kind === 'question' || kind === 'request');
198
+ // The baseline is read and the message sent before anything awaits, so a
199
+ // reply written right after delivery is never mistaken for the baseline.
200
+ const baseline = asks ? readPaneResponseSnapshot(to) : null;
201
+ const outgoingText = asks ? buildZtcRequestPayload(text, kind) : text;
188
202
  busTransportRef.sendBusMessage(to, outgoingText, 'ztc_message');
203
+ const external = asks ? await nonZtcPaneName(to) : null;
204
+ if (external !== null) {
205
+ return `${kind === 'question' ? 'Question' : 'Request'} sent to ${external} (pane ${to}). It is not a ZTC agent, so its answer arrives later as a message from that pane.`;
206
+ }
207
+ const waitForResponse = asks;
189
208
  if (waitForResponse && baseline) {
190
209
  try {
191
210
  const response = await waitForNewPaneResponse(to, baseline, 90_000);
@@ -311,6 +330,10 @@ export const askZtcPaneTool = {
311
330
  : 90;
312
331
  const baseline = readPaneResponseSnapshot(paneId);
313
332
  busTransportRef.sendBusMessage(paneId, buildZtcRequestPayload(text, 'request'), 'ztc_message');
333
+ const external = await nonZtcPaneName(paneId);
334
+ if (external !== null) {
335
+ return `Sent to ${external} (pane ${paneId}). It is not a ZTC agent, so its answer arrives later as a message from that pane.`;
336
+ }
314
337
  try {
315
338
  const response = await waitForNewPaneResponse(paneId, baseline, timeoutSeconds * 1000);
316
339
  return `Response from ZTC ${paneId}:\n${response}`;
@@ -1,6 +1,6 @@
1
1
  import { Tool } from './types.js';
2
2
  import type { PaneCommandTransport } from '../../ui/ratatui/pane_relay.js';
3
- import type { RustEvent } from '../../ui/ratatui/protocol.js';
3
+ import type { PaneInfo, RustEvent } from '../../ui/ratatui/protocol.js';
4
4
  import { resolveModelAndProvider } from '../../panes/model_catalog.js';
5
5
  import type { PaneLaunchSpec } from '../../panes/types.js';
6
6
  export { resolveModelAndProvider };
@@ -36,6 +36,8 @@ export declare function getPaneToolsTransport(): PaneCommandTransport | null;
36
36
  * Resolves "ben" to the one other pane named ben. People address panes by the
37
37
  * names they gave them with /rename, not by pane number.
38
38
  */
39
+ /** The renderer's current pane list, through the given transport (default: the pane tools'). */
40
+ export declare function listWorkspacePanes(transport?: PaneCommandTransport | null, timeoutMs?: number): Promise<PaneInfo[] | Error>;
39
41
  export declare function resolvePaneIdByName(name: string): Promise<number | Error>;
40
42
  export declare function waitForPaneCreatedOrError(tui: PaneCommandTransport, expected: {
41
43
  paneType?: string;
@@ -43,6 +45,8 @@ export declare function waitForPaneCreatedOrError(tui: PaneCommandTransport, exp
43
45
  requestId?: string;
44
46
  }, timeoutMs?: number): Promise<RustEvent>;
45
47
  export declare function paneLimitReached(): boolean;
48
+ /** Environment a ZTC child pane needs to join this workspace (relay, mirror bus, PATH). */
49
+ export declare function childZtcPaneEnv(): Record<string, string>;
46
50
  export declare function buildChildZtcPaneLaunch(extraArgs?: string[], childSessionId?: string, options?: {
47
51
  mirrorPort?: number;
48
52
  entrypoint?: string;
@@ -277,23 +277,33 @@ function paneName(title) {
277
277
  * Resolves "ben" to the one other pane named ben. People address panes by the
278
278
  * names they gave them with /rename, not by pane number.
279
279
  */
280
- export async function resolvePaneIdByName(name) {
281
- if (!tuiProcessRef || !tuiProcessRef.isMuxMode) {
282
- return new Error('Error: Pane names are only available with panes (zmux or the desktop app).');
280
+ /** The renderer's current pane list, through the given transport (default: the pane tools'). */
281
+ export async function listWorkspacePanes(transport = tuiProcessRef, timeoutMs = 5000) {
282
+ if (!transport || !transport.isMuxMode) {
283
+ return new Error('Error: Panes are only available with zmux or the desktop app.');
283
284
  }
284
- const transport = tuiProcessRef;
285
- const listed = waitForEvent(transport, (event) => event.type === 'pane_list', 5000);
285
+ const listed = waitForEvent(transport, (event) => event.type === 'pane_list', timeoutMs);
286
286
  if (!transport.send({ type: 'list_panes' })) {
287
287
  listed.catch(() => { });
288
- return new Error('Error: Unable to list panes to find that name.');
288
+ return new Error('Error: Unable to list panes.');
289
289
  }
290
290
  const event = await listed.catch((error) => error);
291
291
  if (event instanceof Error)
292
292
  return new Error(`Error: ${event.message}`);
293
293
  if (event.type !== 'pane_list')
294
294
  return new Error('Error: Unexpected response while listing panes.');
295
+ return event.panes;
296
+ }
297
+ export async function resolvePaneIdByName(name) {
298
+ if (!tuiProcessRef || !tuiProcessRef.isMuxMode) {
299
+ return new Error('Error: Pane names are only available with panes (zmux or the desktop app).');
300
+ }
301
+ const panes = await listWorkspacePanes(tuiProcessRef);
302
+ if (panes instanceof Error) {
303
+ return new Error(panes.message === 'Error: Unable to list panes.' ? 'Error: Unable to list panes to find that name.' : panes.message);
304
+ }
295
305
  const wanted = paneName(name);
296
- const others = event.panes.filter((pane) => pane.id !== ownPaneId);
306
+ const others = panes.filter((pane) => pane.id !== ownPaneId);
297
307
  const matches = others.filter((pane) => paneName(pane.title) === wanted);
298
308
  if (matches.length === 1)
299
309
  return matches[0].id;
@@ -417,7 +427,8 @@ function filterChildZtcArgs(args) {
417
427
  }
418
428
  return filtered;
419
429
  }
420
- function childZtcPaneEnv() {
430
+ /** Environment a ZTC child pane needs to join this workspace (relay, mirror bus, PATH). */
431
+ export function childZtcPaneEnv() {
421
432
  const env = {
422
433
  ZTC_PARENT_PID: String(process.pid),
423
434
  };
@@ -76,9 +76,11 @@ export function routeUserInput(text) {
76
76
  tokens.push(token);
77
77
  return { type: 'command', cmd, args: tokens.slice(1) };
78
78
  }
79
- if (cmd.toLowerCase() === 'fork' || cmd.toLowerCase() === 'side') {
80
- // A fork's body is authored task data, not tokenized command options.
81
- // Remove exactly the command separator and retain the remaining bytes.
79
+ if (['fork', 'side', 'claude', 'codex'].includes(cmd.toLowerCase())) {
80
+ // A fork's body is authored task data, not tokenized command options,
81
+ // and /claude and /codex read theirs as a prompt or a shell-quoted
82
+ // argument list. Remove exactly the command separator and retain the
83
+ // remaining bytes.
82
84
  const task = line.slice(cmd.length + 2);
83
85
  return { type: 'command', cmd, args: task.length ? [task] : [] };
84
86
  }
package/dist/cli_main.js CHANGED
@@ -76,6 +76,19 @@ if (commandArgs[0] === 'attach') {
76
76
  const { runTerminalAttachCli } = await import('./terminal_attach.js');
77
77
  process.exit(await runTerminalAttachCli(commandArgs.slice(1)));
78
78
  }
79
+ // The MCP server Claude Code and Codex panes start: stdio only, no agent, no TTY,
80
+ // and nothing on stdout but the protocol.
81
+ if (commandArgs[0] === 'workspace-mcp') {
82
+ try {
83
+ const { runWorkspaceMcpServer } = await import('./mcp/workspace_server.js');
84
+ await runWorkspaceMcpServer();
85
+ }
86
+ catch (error) {
87
+ console.error(`[ztc workspace-mcp] ${error.message}`);
88
+ process.exit(1);
89
+ }
90
+ process.exit(0);
91
+ }
79
92
  // Adapters must be able to deliver receipts without starting an agent or a TTY.
80
93
  if (commandArgs[0] === 'inbox') {
81
94
  try {
@@ -420,6 +420,8 @@ export declare class RatatuiApp {
420
420
  private desktopMode;
421
421
  private desktopRuntime;
422
422
  private childPaneIds;
423
+ /** Panes running the user's own Claude Code or Codex, by pane id. */
424
+ private readonly agentCliPanes;
423
425
  private colonyPaneIds;
424
426
  /** Cache of latest pane list from pane_state events (including colony panes, filtered in switcher) */
425
427
  private knownPanes;
@@ -1290,6 +1292,14 @@ export declare class RatatuiApp {
1290
1292
  * working. Ctrl+B c does not come through here — that raises the chooser.
1291
1293
  */
1292
1294
  private handleCreatePaneRequest;
1295
+ /**
1296
+ * Open the user's own Claude Code or Codex in a pane: the chooser's and the
1297
+ * desktop menu's entry point. Returns what to tell the user (an error or a
1298
+ * notice), or null.
1299
+ */
1300
+ private spawnAgentCliPane;
1301
+ /** Where the user's own `claude` and `codex` are installed, if they are. */
1302
+ private installedAgentClis;
1293
1303
  /**
1294
1304
  * Create a ZTC agent pane on the user's behalf: no agent turn, no tool call.
1295
1305
  *
@@ -5,6 +5,9 @@ import { reconcileOrderedTranscript } from './app/ordered_transcript.js';
5
5
  import { listCommandJobs, shutdownCommandJobs, subscribeCommandJobs } from './agent/runtime/command_jobs.js';
6
6
  import { CommandPaneManager, commandPaneText } from './agent/runtime/command_panes.js';
7
7
  import { waitForPaneCreatedOrError } from './agent/tools/pane.js';
8
+ import { findAgentCli } from './panes/agent_cli.js';
9
+ import { agentCliFromRequestId, formatAgentPaneMessage, openAgentPane, typeLineIntoPane } from './panes/agent_panes.js';
10
+ import { agentCliFromChoice, newPaneChoices } from './panes/new_pane_choices.js';
8
11
  import { IntakeNotifications } from './intake/notifications.js';
9
12
  import { projectGoal } from './goal_contract.js';
10
13
  import { isToolResultMessage } from './utils/tool_result_message.js';
@@ -772,6 +775,8 @@ export class RatatuiApp {
772
775
  desktopMode = false;
773
776
  desktopRuntime;
774
777
  childPaneIds = new Set();
778
+ /** Panes running the user's own Claude Code or Codex, by pane id. */
779
+ agentCliPanes = new Map();
775
780
  colonyPaneIds = new Set();
776
781
  /** Cache of latest pane list from pane_state events (including colony panes, filtered in switcher) */
777
782
  knownPanes = [];
@@ -1468,6 +1473,13 @@ export class RatatuiApp {
1468
1473
  debugLog(`[Mirror] Pane control via stdin (bare): pane=${paneId} cmd=${controlLine.slice(0, 60)}`);
1469
1474
  this.tui.send({ type: 'write_pane', pane_id: paneId, input: controlLine, control: true });
1470
1475
  });
1476
+ // An agent's message for a Claude Code or Codex pane is typed in, one
1477
+ // line saying who sent it, with Enter as its own keystroke.
1478
+ this.mirrorServer.onAgentPaneMessage((paneId, msg) => {
1479
+ if (!this.agentCliPanes.has(paneId))
1480
+ return false;
1481
+ return typeLineIntoPane(this.tui, paneId, formatAgentPaneMessage(msg));
1482
+ });
1471
1483
  }
1472
1484
  // Handle input from browser mirror (single-pane mode fallback)
1473
1485
  this.mirrorServer.onInput((input) => {
@@ -6793,6 +6805,9 @@ export class RatatuiApp {
6793
6805
  case 'pane_created': {
6794
6806
  this.childPaneIds.add(event.pane_id);
6795
6807
  this.notePaneCreated(event.pane_id, event.request_id);
6808
+ const agentCli = agentCliFromRequestId(event.request_id);
6809
+ if (agentCli)
6810
+ this.agentCliPanes.set(event.pane_id, agentCli);
6796
6811
  const launchSpec = this.paneRegistry.get(event.pane_id)?.launchSpec
6797
6812
  || this.paneRegistry.consumePendingCreate(event.request_id)
6798
6813
  || this.resumedPaneSpecs.find((pane) => pane.paneId === event.pane_id)?.launchSpec;
@@ -6910,6 +6925,7 @@ export class RatatuiApp {
6910
6925
  this.childPaneModels.delete(event.pane_id);
6911
6926
  this.childPaneSessionIds.delete(event.pane_id);
6912
6927
  this.colonyPaneIds.delete(event.pane_id);
6928
+ this.agentCliPanes.delete(event.pane_id);
6913
6929
  this.paneRegistry.remove(event.pane_id);
6914
6930
  if (this.mirrorServer) {
6915
6931
  this.mirrorServer.unregisterPane(event.pane_id);
@@ -12988,18 +13004,48 @@ export class RatatuiApp {
12988
13004
  * working. Ctrl+B c does not come through here — that raises the chooser.
12989
13005
  */
12990
13006
  handleCreatePaneRequest(event) {
12991
- void this.spawnZtcPane({
12992
- cwd: event.cwd,
12993
- direction: event.direction,
12994
- ratio: event.ratio,
12995
- target: event.target,
12996
- newWindow: event.new_window,
12997
- }).then((result) => {
13007
+ const placement = { direction: event.direction, ratio: event.ratio, target: event.target, newWindow: event.new_window };
13008
+ // The desktop app's new-pane menu names what it wants; no kind is a ZTC pane.
13009
+ if (event.kind === 'shell') {
13010
+ const error = this.spawnShellPane({ cwd: event.cwd, ...placement });
13011
+ if (error)
13012
+ this.addSystemMessage(`Could not open a shell pane: ${error}`);
13013
+ return;
13014
+ }
13015
+ if (event.kind === 'claude_code' || event.kind === 'codex') {
13016
+ void this.spawnAgentCliPane(event.kind === 'codex' ? 'codex' : 'claude', { cwd: event.cwd, ...placement })
13017
+ .then((message) => { if (message)
13018
+ this.addSystemMessage(message); });
13019
+ return;
13020
+ }
13021
+ void this.spawnZtcPane({ cwd: event.cwd, ...placement }).then((result) => {
12998
13022
  if (result.error) {
12999
13023
  debugLog(`[handleCreatePaneRequest] ${result.error}`);
13000
13024
  }
13001
13025
  });
13002
13026
  }
13027
+ /**
13028
+ * Open the user's own Claude Code or Codex in a pane: the chooser's and the
13029
+ * desktop menu's entry point. Returns what to tell the user (an error or a
13030
+ * notice), or null.
13031
+ */
13032
+ async spawnAgentCliPane(cli, options) {
13033
+ if (!this.muxMode || this.paneMode)
13034
+ return 'Opening Claude Code or Codex needs a zmux session or the desktop app.';
13035
+ const result = await openAgentPane(this.tui, {
13036
+ cli,
13037
+ ...options,
13038
+ cwd: options.cwd || this.shellCwd || process.cwd(),
13039
+ findBinary: id => this.installedAgentClis()[id] ?? null,
13040
+ });
13041
+ if (result.notice)
13042
+ this.addSystemMessage(result.notice);
13043
+ return result.error ?? null;
13044
+ }
13045
+ /** Where the user's own `claude` and `codex` are installed, if they are. */
13046
+ installedAgentClis() {
13047
+ return { claude: findAgentCli('claude'), codex: findAgentCli('codex') };
13048
+ }
13003
13049
  /**
13004
13050
  * Create a ZTC agent pane on the user's behalf: no agent turn, no tool call.
13005
13051
  *
@@ -13108,20 +13154,12 @@ export class RatatuiApp {
13108
13154
  return;
13109
13155
  }
13110
13156
  const ctx = this.buildCommandContext((msg) => this.controller.addMessage(msg));
13111
- const custom = customPaneOptions(configStore.get());
13157
+ const choices = newPaneChoices(customPaneOptions(configStore.get()), this.installedAgentClis());
13112
13158
  ctx.select.showSelect({
13113
13159
  title: 'New pane',
13114
13160
  question: 'What should the new pane be?',
13115
13161
  type: 'select',
13116
- options: [
13117
- { value: 'bash', label: 'bash', description: 'A shell pane' },
13118
- { value: 'ztc', label: 'ZTC', description: 'A ZTC agent pane' },
13119
- ...custom.map((option, index) => ({
13120
- value: `custom:${index}`,
13121
- label: option.label,
13122
- description: option.command,
13123
- })),
13124
- ],
13162
+ options: choices,
13125
13163
  }, async (value) => {
13126
13164
  if (value === null || value === undefined)
13127
13165
  return;
@@ -13145,6 +13183,9 @@ export class RatatuiApp {
13145
13183
  const result = await this.spawnZtcPane({ cwd });
13146
13184
  return result.error ?? null;
13147
13185
  }
13186
+ const agentCli = agentCliFromChoice(choice);
13187
+ if (agentCli)
13188
+ return this.spawnAgentCliPane(agentCli, { cwd });
13148
13189
  if (choice.startsWith('custom:')) {
13149
13190
  const option = customPaneOptions(configStore.get())[Number(choice.slice('custom:'.length))];
13150
13191
  if (!option)
@@ -13179,6 +13220,10 @@ export class RatatuiApp {
13179
13220
  cwd,
13180
13221
  title,
13181
13222
  pane_type: 'process',
13223
+ ...(options?.direction ? { direction: options.direction } : {}),
13224
+ ...(options?.ratio !== undefined ? { ratio: options.ratio } : {}),
13225
+ ...(options?.target !== undefined ? { target: options.target } : {}),
13226
+ ...(options?.newWindow ? { new_window: true } : {}),
13182
13227
  })
13183
13228
  ? null
13184
13229
  : 'failed to create shell pane';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `ztc workspace-mcp` — the MCP server attached to Claude Code and Codex panes.
3
+ *
4
+ * The CLI starts it from its session-scoped MCP config (see panes/agent_cli.ts).
5
+ * It reaches this workspace through the pane relay ($ZTC_PANE_RELAY) as pane
6
+ * $ZTC_PANE_ID, and gives the agent the workspace: list panes; open a ZTC
7
+ * agent, Claude Code, Codex or a shell; read a pane; type into one; message
8
+ * another agent; close a pane. Messages travel over ZTC's bus when the mirror
9
+ * is reachable, so ZTC panes receive them as messages from this pane;
10
+ * otherwise they are typed in with the sender's name.
11
+ */
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { type AgentCliId, type PaneProgram } from '../panes/agent_cli.js';
14
+ import { type PaneCommandTransport } from '../ui/ratatui/pane_relay.js';
15
+ export interface WorkspaceBusMessage {
16
+ from: {
17
+ id: number;
18
+ name: string;
19
+ };
20
+ to: number | 'all';
21
+ type: 'ztc_message';
22
+ text: string;
23
+ }
24
+ export interface WorkspaceBusSender {
25
+ send(message: WorkspaceBusMessage): Promise<void>;
26
+ }
27
+ export interface WorkspaceMcpOptions {
28
+ /** The pane relay; null outside a ZTC workspace (every tool then says so). */
29
+ transport: PaneCommandTransport | null;
30
+ ownPaneId: number | null;
31
+ bus?: WorkspaceBusSender | null;
32
+ findBinary?: (id: AgentCliId) => string | null;
33
+ /** The server to attach to agent panes this one opens. */
34
+ server?: PaneProgram | Error | null;
35
+ env?: NodeJS.ProcessEnv;
36
+ /** Working directory for panes opened without one. */
37
+ cwd?: string;
38
+ }
39
+ /** The MCP server, connected to a workspace through `transport`. */
40
+ export declare function createWorkspaceMcpServer(options: WorkspaceMcpOptions): Server;
41
+ /** Posts bus messages to the overseer's mirror, as MirrorClient does, without joining the bus. */
42
+ export declare function mirrorBusSender(env?: NodeJS.ProcessEnv): WorkspaceBusSender | null;
43
+ /** Entry point for `ztc workspace-mcp`: serve on stdio until the CLI closes it. */
44
+ export declare function runWorkspaceMcpServer(env?: NodeJS.ProcessEnv): Promise<void>;