@zergai/ztc 0.3.0-beta.1 → 0.4.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/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +16 -2
- package/dist/agent/agent.d.ts +20 -8
- package/dist/agent/agent.js +92 -40
- package/dist/agent/backends/gemini.js +1 -1
- package/dist/agent/backends/openai_format.js +2 -2
- package/dist/agent/backends/openai_responses.js +1 -1
- package/dist/agent/backends/types.d.ts +1 -0
- package/dist/agent/commands/config.js +15 -1
- package/dist/agent/commands/settings.js +23 -0
- package/dist/agent/commands/status.js +3 -0
- package/dist/agent/commands/types.d.ts +2 -0
- package/dist/agent/completion_state.js +2 -2
- package/dist/agent/factory.js +7 -1
- package/dist/agent/request_encoder.js +3 -0
- package/dist/agent/runtime/command_jobs.d.ts +47 -0
- package/dist/agent/runtime/command_jobs.js +268 -0
- package/dist/agent/runtime/execution_context.d.ts +16 -0
- package/dist/agent/runtime/execution_context.js +20 -0
- package/dist/agent/runtime/recovery.d.ts +9 -0
- package/dist/agent/runtime/recovery.js +71 -0
- package/dist/agent/runtime/tool_runtime.js +3 -1
- package/dist/agent/runtime/tool_scheduler.d.ts +6 -0
- package/dist/agent/runtime/tool_scheduler.js +57 -0
- package/dist/agent/subagent.js +30 -12
- package/dist/agent/tool_response_store.d.ts +2 -0
- package/dist/agent/tool_response_store.js +1 -0
- package/dist/agent/tools/deferred.js +2 -0
- package/dist/agent/tools/file.js +2 -0
- package/dist/agent/tools/pane.js +139 -42
- package/dist/agent/tools/recall_tool_response.js +1 -0
- package/dist/agent/tools/registry.js +3 -1
- package/dist/agent/tools/search.js +1 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +57 -2
- package/dist/agent/tools/types.d.ts +4 -0
- package/dist/app/agent_runner.js +1 -1
- package/dist/app/ordered_transcript.d.ts +13 -0
- package/dist/app/ordered_transcript.js +55 -0
- package/dist/cli_ratatui.d.ts +2 -7
- package/dist/cli_ratatui.js +53 -34
- package/dist/config/execution.d.ts +12 -0
- package/dist/config/execution.js +13 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/emulation/tool_adapters.js +5 -2
- package/dist/headless.d.ts +1 -18
- package/dist/headless.js +15 -0
- package/dist/plugin/runtime.d.ts +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/ui/ratatui/headless_pane_manager.d.ts +2 -19
- package/dist/ui/ratatui/headless_pane_manager.js +41 -34
- package/dist/ui/ratatui/pane_input.d.ts +4 -0
- package/dist/ui/ratatui/pane_input.js +15 -0
- package/dist/ui/ratatui/pane_relay.d.ts +3 -0
- package/dist/ui/ratatui/pane_relay.js +20 -2
- package/dist/ui/ratatui/protocol.d.ts +17 -0
- package/dist/ui/ratatui/protocol.js +1 -0
- package/dist/utils/shell.d.ts +2 -0
- package/dist/utils/shell.js +54 -11
- package/dist/web/mirror_stream.d.ts +2 -1
- package/dist/web/mirror_stream.js +7 -0
- package/dist/web/pane_registry.js +8 -2
- package/dist/web/view_plugin.d.ts +1 -0
- package/dist/zlm-embed.mjs +384 -118
- package/package.json +2 -2
package/dist/cli_ratatui.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reconcileOrderedTranscript } from './app/ordered_transcript.js';
|
|
2
|
+
import { shutdownCommandJobs, subscribeCommandJobs } from './agent/runtime/command_jobs.js';
|
|
1
3
|
/**
|
|
2
4
|
* RatatuiApp — the interactive TUI application class.
|
|
3
5
|
*
|
|
@@ -1732,6 +1734,7 @@ export class RatatuiApp {
|
|
|
1732
1734
|
retry: () => { },
|
|
1733
1735
|
exit: () => this.exit(),
|
|
1734
1736
|
getSessionId: () => this.sessionId,
|
|
1737
|
+
getExecutionContext: () => this.agent?.getExecutionContext(),
|
|
1735
1738
|
getSessionName: () => this.sessionName,
|
|
1736
1739
|
setSessionName: (name) => {
|
|
1737
1740
|
this.sessionName = name && name.length > 0 ? name : undefined;
|
|
@@ -7255,6 +7258,17 @@ export class RatatuiApp {
|
|
|
7255
7258
|
}
|
|
7256
7259
|
// Snapshot message count before the run so we can roll back on cancel
|
|
7257
7260
|
const preRunMessageCount = this.controller.getState().messages.length;
|
|
7261
|
+
let historyCheckpoint = [];
|
|
7262
|
+
const transcriptIterations = [];
|
|
7263
|
+
const toolIterations = new Map();
|
|
7264
|
+
const retainCheckpoint = () => {
|
|
7265
|
+
if (!historyCheckpoint.length)
|
|
7266
|
+
return;
|
|
7267
|
+
this.controller.setMessages(reconcileOrderedTranscript({
|
|
7268
|
+
live: this.controller.getState().messages, ordered: historyCheckpoint,
|
|
7269
|
+
iterations: transcriptIterations, toolIterations,
|
|
7270
|
+
}));
|
|
7271
|
+
};
|
|
7258
7272
|
// Any steering retained across a prior interrupted or widget-ended turn
|
|
7259
7273
|
// is already present in this snapshot. It must not be injected twice.
|
|
7260
7274
|
this.pendingSteering = [];
|
|
@@ -7287,9 +7301,13 @@ export class RatatuiApp {
|
|
|
7287
7301
|
// Liveness clock: any agent event proves the model is still producing.
|
|
7288
7302
|
this.lastAgentEventAt = Date.now();
|
|
7289
7303
|
switch (event.type) {
|
|
7304
|
+
case 'history_checkpoint':
|
|
7305
|
+
historyCheckpoint = event.messages;
|
|
7306
|
+
break;
|
|
7290
7307
|
case 'thinking_start': {
|
|
7291
7308
|
this.flushLocalStreamDelta();
|
|
7292
7309
|
this.finishLocalStreamingMessages();
|
|
7310
|
+
transcriptIterations.push({ start: this.controller.getState().messages.length });
|
|
7293
7311
|
this.controller.setAgentState(applyAgentEventToState(this.controller.getState().agentState, event));
|
|
7294
7312
|
break;
|
|
7295
7313
|
}
|
|
@@ -7297,6 +7315,11 @@ export class RatatuiApp {
|
|
|
7297
7315
|
this.flushLocalStreamDelta();
|
|
7298
7316
|
this.finishLocalStreamingMessages();
|
|
7299
7317
|
this.controller.setAgentState(applyAgentEventToState(this.controller.getState().agentState, event));
|
|
7318
|
+
const frameIndex = Math.max(0, transcriptIterations.length - 1);
|
|
7319
|
+
if (!transcriptIterations.length)
|
|
7320
|
+
transcriptIterations.push({ start: preRunMessageCount });
|
|
7321
|
+
transcriptIterations[frameIndex].toolStart ??= this.controller.getState().messages.length;
|
|
7322
|
+
toolIterations.set(event.toolCallId, frameIndex);
|
|
7300
7323
|
this.turnToolCallCount++;
|
|
7301
7324
|
const toolCallId = event.toolCallId;
|
|
7302
7325
|
this.mirror?.publishToolCall(event.tool, event.args || {}, toolCallId);
|
|
@@ -7449,6 +7472,7 @@ export class RatatuiApp {
|
|
|
7449
7472
|
}
|
|
7450
7473
|
}
|
|
7451
7474
|
catch (err) {
|
|
7475
|
+
retainCheckpoint();
|
|
7452
7476
|
this.flushLocalStreamDelta();
|
|
7453
7477
|
this.finishLocalStreamingMessages();
|
|
7454
7478
|
this.finishToolGroups(err.message);
|
|
@@ -7496,6 +7520,7 @@ export class RatatuiApp {
|
|
|
7496
7520
|
this.finishLocalStreamingMessages();
|
|
7497
7521
|
this.finishToolGroups(cancelled ? 'Cancelled' : 'No result received');
|
|
7498
7522
|
if (cancelled) {
|
|
7523
|
+
retainCheckpoint();
|
|
7499
7524
|
// Clean up partial state from cancelled run
|
|
7500
7525
|
this.clearLocalStreamDelta();
|
|
7501
7526
|
// Codex parity: an interrupt stops the run but keeps everything the run
|
|
@@ -7534,41 +7559,15 @@ export class RatatuiApp {
|
|
|
7534
7559
|
// calls onto the final assistant message, which would replay a steering
|
|
7535
7560
|
// prompt before the tool result it followed.
|
|
7536
7561
|
const turnMessages = result.turnMessages ?? [];
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
// the prompt from both the transcript and every request. Keep it, and
|
|
7544
|
-
// `movePendingSteeringToEnd` will place it after the answer and take the
|
|
7545
|
-
// follow-up turn.
|
|
7546
|
-
const unconsumedSteering = new Set(this.pendingSteering.map(message => message.id));
|
|
7547
|
-
const retainedMessages = live.slice(preRunMessageCount)
|
|
7548
|
-
.filter(message => message.role === 'system' || message.role === 'tool'
|
|
7549
|
-
|| (message.role === 'user'
|
|
7550
|
-
&& (message.metadata?.steering !== true || unconsumedSteering.has(message.id))));
|
|
7551
|
-
const orderedMessages = turnMessages.map(message => ({
|
|
7552
|
-
...message,
|
|
7553
|
-
...(message.toolCalls ? {
|
|
7554
|
-
toolCalls: message.toolCalls.map(toolCall => {
|
|
7555
|
-
const { result: _result, error: _error, ...lightCall } = toolCall;
|
|
7556
|
-
return lightCall;
|
|
7557
|
-
}),
|
|
7558
|
-
metadata: { ...message.metadata, toolCallsInTranscript: true },
|
|
7559
|
-
} : {}),
|
|
7560
|
-
}));
|
|
7561
|
-
const finalAssistant = result.content.trim()
|
|
7562
|
-
? {
|
|
7563
|
-
id: generateId(), role: 'assistant', content: result.content,
|
|
7564
|
-
timestamp: new Date(),
|
|
7562
|
+
if (turnMessages.length > 0) {
|
|
7563
|
+
this.controller.setMessages(reconcileOrderedTranscript({
|
|
7564
|
+
live: this.controller.getState().messages, ordered: turnMessages,
|
|
7565
|
+
iterations: transcriptIterations, toolIterations,
|
|
7566
|
+
finalMessage: result.content.trim() ? {
|
|
7567
|
+
id: generateId(), role: 'assistant', content: result.content, timestamp: new Date(),
|
|
7565
7568
|
...(result.reasoning ? { metadata: { reasoningContent: result.reasoning } } : {}),
|
|
7566
|
-
}
|
|
7567
|
-
|
|
7568
|
-
this.controller.setMessages([
|
|
7569
|
-
...live.slice(0, preRunMessageCount), ...orderedMessages,
|
|
7570
|
-
...(finalAssistant ? [finalAssistant] : []), ...retainedMessages,
|
|
7571
|
-
]);
|
|
7569
|
+
} : undefined,
|
|
7570
|
+
}));
|
|
7572
7571
|
}
|
|
7573
7572
|
else {
|
|
7574
7573
|
// Add final response if not already streamed
|
|
@@ -7628,6 +7627,7 @@ export class RatatuiApp {
|
|
|
7628
7627
|
.map(tc => ({
|
|
7629
7628
|
tool_use_id: tc.id,
|
|
7630
7629
|
content: tc.error ? `Error: ${tc.error}` : (tc.result || ''),
|
|
7630
|
+
is_error: tc.status === 'error',
|
|
7631
7631
|
}));
|
|
7632
7632
|
if (toolResultContent.length > 0) {
|
|
7633
7633
|
this.controller.addMessage({
|
|
@@ -8683,7 +8683,23 @@ export class RatatuiApp {
|
|
|
8683
8683
|
}
|
|
8684
8684
|
return paneMode === 'sim-pane' ? 'sim-pane' : controllerMode;
|
|
8685
8685
|
}
|
|
8686
|
+
commandJobsUnsubscribe;
|
|
8687
|
+
commandJobStates = new Map();
|
|
8686
8688
|
createAgent() {
|
|
8689
|
+
this.commandJobsUnsubscribe?.();
|
|
8690
|
+
this.commandJobsUnsubscribe = subscribeCommandJobs(this.sessionId, jobs => {
|
|
8691
|
+
const snapshot = { type: 'command_jobs', data: { jobs } };
|
|
8692
|
+
if (this.mirrorServer)
|
|
8693
|
+
this.mirrorServer.publish(snapshot);
|
|
8694
|
+
else
|
|
8695
|
+
this.mirrorClient?.publish(snapshot);
|
|
8696
|
+
for (const job of jobs) {
|
|
8697
|
+
if (this.commandJobStates.get(job.job_id) !== job.status) {
|
|
8698
|
+
this.commandJobStates.set(job.job_id, job.status);
|
|
8699
|
+
this.addSystemMessage(`Command ${job.job_id}: ${job.status}${job.exit_code === undefined ? '' : ` (exit ${job.exit_code})`}. ${job.command.slice(0, 120)}`);
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
});
|
|
8687
8703
|
const currentMode = this.getEffectiveAgentMode();
|
|
8688
8704
|
if (currentMode === 'plan' && !this.controller.getPlanFile()) {
|
|
8689
8705
|
throw new PlanFileCreationError('<unset>', new Error('refusing to construct a plan agent without an artifact path'));
|
|
@@ -10270,6 +10286,9 @@ export class RatatuiApp {
|
|
|
10270
10286
|
await batch.shutdown(5000);
|
|
10271
10287
|
clearCurrentBatch();
|
|
10272
10288
|
}
|
|
10289
|
+
await shutdownCommandJobs(this.sessionId);
|
|
10290
|
+
this.commandJobsUnsubscribe?.();
|
|
10291
|
+
this.commandJobsUnsubscribe = undefined;
|
|
10273
10292
|
// Shut down background agents before clearing tool callbacks
|
|
10274
10293
|
const bgRegistry = getBackgroundRegistry();
|
|
10275
10294
|
if (bgRegistry) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Execution controls shared by the agent, command jobs, and settings surfaces. */
|
|
2
|
+
export interface ExecutionSettings {
|
|
3
|
+
shellPanePolicy?: 'on_request' | 'auto';
|
|
4
|
+
readConcurrency?: number;
|
|
5
|
+
maxCallsPerResponse?: number;
|
|
6
|
+
recovery?: 'automatic' | 'pause';
|
|
7
|
+
repeatedFailureLimit?: number;
|
|
8
|
+
recoveryAttempts?: number;
|
|
9
|
+
interruptBackgroundJobs?: boolean;
|
|
10
|
+
maxBackgroundJobs?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveExecutionSettings(settings?: ExecutionSettings): Required<ExecutionSettings>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function resolveExecutionSettings(settings = {}) {
|
|
2
|
+
const count = (value, fallback, max, min = 1) => Number.isSafeInteger(value) && value >= min && value <= max ? value : fallback;
|
|
3
|
+
return {
|
|
4
|
+
shellPanePolicy: settings.shellPanePolicy === 'auto' ? 'auto' : 'on_request',
|
|
5
|
+
readConcurrency: count(settings.readConcurrency, 4, 32),
|
|
6
|
+
maxCallsPerResponse: count(settings.maxCallsPerResponse, 16, 256),
|
|
7
|
+
recovery: settings.recovery === 'pause' ? 'pause' : 'automatic',
|
|
8
|
+
repeatedFailureLimit: count(settings.repeatedFailureLimit, 3, 20),
|
|
9
|
+
recoveryAttempts: count(settings.recoveryAttempts, 1, 5, 0),
|
|
10
|
+
interruptBackgroundJobs: settings.interruptBackgroundJobs === true,
|
|
11
|
+
maxBackgroundJobs: count(settings.maxBackgroundJobs, 4, 32),
|
|
12
|
+
};
|
|
13
|
+
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -326,6 +326,7 @@ export interface ZTCConfig {
|
|
|
326
326
|
contextProgressBar?: ContextProgressBarSettings;
|
|
327
327
|
/** Retry and timeout policy for LLM requests. */
|
|
328
328
|
apiRetry?: ApiRetrySettings;
|
|
329
|
+
execution?: import('./execution.js').ExecutionSettings;
|
|
329
330
|
/** Terminal transcript density and reasoning visibility. */
|
|
330
331
|
messageDisplay?: MessageDisplaySettings;
|
|
331
332
|
/** Context compaction strategy (summarize, or Jev delete-only). */
|
package/dist/config.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export declare class ConfigStore {
|
|
|
49
49
|
getConfigWithoutEffortOverride(): ZTCConfig;
|
|
50
50
|
private composeConfig;
|
|
51
51
|
private cliOverridesWithoutEffort;
|
|
52
|
+
getMaxTokensSource(): string;
|
|
52
53
|
getConfigScope(): ZTCConfigScope;
|
|
53
54
|
setConfigScope(scope: ZTCConfigScope): void;
|
|
54
55
|
clearSessionOverrides(): void;
|
package/dist/config.js
CHANGED
|
@@ -734,6 +734,11 @@ export class ConfigStore {
|
|
|
734
734
|
...(this.cliOverrides.model !== undefined ? { model: this.cliOverrides.model } : {}),
|
|
735
735
|
};
|
|
736
736
|
}
|
|
737
|
+
getMaxTokensSource() {
|
|
738
|
+
if (Object.prototype.hasOwnProperty.call(this.sessionOverrides, 'maxTokens'))
|
|
739
|
+
return 'session override';
|
|
740
|
+
return this.config.maxTokens === undefined ? 'provider default' : `saved profile (${this.getConfigPath()})`;
|
|
741
|
+
}
|
|
737
742
|
getConfigScope() {
|
|
738
743
|
return this.configScope;
|
|
739
744
|
}
|
|
@@ -14,7 +14,7 @@ export function buildEmulationCompatibilityTools(emulationId, tools) {
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
existing.add(tool.definition.name);
|
|
17
|
-
adapters.push(tool);
|
|
17
|
+
adapters.push({ ...tool, parallelSafe: ['Read', 'Grep', 'LS'].includes(tool.definition.name) });
|
|
18
18
|
};
|
|
19
19
|
const factories = [];
|
|
20
20
|
if (CLAUDE_PROFILE_IDS.has(emulationId)) {
|
|
@@ -286,7 +286,10 @@ async function callTool(tools, name, args, context) {
|
|
|
286
286
|
if (!tool) {
|
|
287
287
|
throw new Error(`Compatibility adapter target is unavailable: ${name}`);
|
|
288
288
|
}
|
|
289
|
-
|
|
289
|
+
const output = await tool.execute(args, context);
|
|
290
|
+
if (tool.isErrorResult?.(output))
|
|
291
|
+
throw new Error(output);
|
|
292
|
+
return output;
|
|
290
293
|
}
|
|
291
294
|
async function executeCodexShell(tools, args, context) {
|
|
292
295
|
const command = requiredString(args, ['cmd', 'command']);
|
package/dist/headless.d.ts
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Headless Mode for ZTC
|
|
3
|
-
*
|
|
4
|
-
* Enables programmatic control of ZTC via stdin/stdout.
|
|
5
|
-
* Useful for:
|
|
6
|
-
* - Automated testing of modes (e.g., DCAIG)
|
|
7
|
-
* - CI/CD pipelines
|
|
8
|
-
* - Agent orchestration (outer agent driving ZTC)
|
|
9
|
-
*
|
|
10
|
-
* Headless shell mode:
|
|
11
|
-
* - `ZTC_HEADLESS_SHELL_MODE=real` allows slash-command shell execution on the host
|
|
12
|
-
* - `ZTC_HEADLESS_SHELL_MODE=stub` keeps shell calls inert and returns empty results
|
|
13
|
-
* - default: `real`
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* echo "build a JIRA connector" | ztc --headless --mode ~/dcaig/ztc/modes/dcaig.yaml
|
|
17
|
-
* echo '{"prompt": "...", "session_id": "..."}' | ztc --headless --json
|
|
18
|
-
*/
|
|
19
1
|
import type { MessageAttachment } from './types.js';
|
|
20
2
|
import type { CodexReasoningEffort } from './config/types.js';
|
|
21
3
|
interface HeadlessOptions {
|
|
@@ -217,6 +199,7 @@ export declare class HeadlessApp {
|
|
|
217
199
|
/**
|
|
218
200
|
* Create the agent
|
|
219
201
|
*/
|
|
202
|
+
private commandJobsUnsubscribe;
|
|
220
203
|
private createAgent;
|
|
221
204
|
private bindAgentStore;
|
|
222
205
|
private awaitAgentBind;
|
package/dist/headless.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shutdownCommandJobs, subscribeCommandJobs } from './agent/runtime/command_jobs.js';
|
|
1
2
|
/**
|
|
2
3
|
* Headless Mode for ZTC
|
|
3
4
|
*
|
|
@@ -743,6 +744,10 @@ export class HeadlessApp {
|
|
|
743
744
|
// Always show errors
|
|
744
745
|
console.error(`${ts}[error] ${data.data.message}`);
|
|
745
746
|
break;
|
|
747
|
+
case 'command_jobs':
|
|
748
|
+
if (!this.options.quiet || this.options.verbose)
|
|
749
|
+
console.log(`${ts}[commands] ${JSON.stringify(data.data.jobs)}`);
|
|
750
|
+
break;
|
|
746
751
|
case 'status':
|
|
747
752
|
// In quiet mode, skip status unless verbose
|
|
748
753
|
if (this.options.quiet && !this.options.verbose)
|
|
@@ -1412,7 +1417,13 @@ export class HeadlessApp {
|
|
|
1412
1417
|
/**
|
|
1413
1418
|
* Create the agent
|
|
1414
1419
|
*/
|
|
1420
|
+
commandJobsUnsubscribe;
|
|
1415
1421
|
createAgent() {
|
|
1422
|
+
this.commandJobsUnsubscribe?.();
|
|
1423
|
+
this.commandJobsUnsubscribe = subscribeCommandJobs(this.sessionId, jobs => {
|
|
1424
|
+
this.output({ type: 'command_jobs', timestamp: new Date().toISOString(), data: { jobs } });
|
|
1425
|
+
this.autoMirror?.publish({ type: 'command_jobs', data: { jobs } });
|
|
1426
|
+
});
|
|
1416
1427
|
const isEvolveMode = this.currentMode === 'evolved-swarm';
|
|
1417
1428
|
const hookDispatcher = this.hookLifecycle.refresh();
|
|
1418
1429
|
const agent = createAgentWithConfig({
|
|
@@ -1620,6 +1631,7 @@ export class HeadlessApp {
|
|
|
1620
1631
|
},
|
|
1621
1632
|
setDebug: () => { },
|
|
1622
1633
|
getSessionId: () => this.sessionId,
|
|
1634
|
+
getExecutionContext: () => this.agent?.getExecutionContext(),
|
|
1623
1635
|
getSessionName: () => this.sessionName,
|
|
1624
1636
|
setSessionName: (name) => {
|
|
1625
1637
|
this.sessionName = name && name.length > 0 ? name : undefined;
|
|
@@ -2335,6 +2347,9 @@ export class HeadlessApp {
|
|
|
2335
2347
|
* Shut down background agents and batch orchestrator gracefully before exiting.
|
|
2336
2348
|
*/
|
|
2337
2349
|
async shutdownBackgroundAgents() {
|
|
2350
|
+
await shutdownCommandJobs(this.sessionId);
|
|
2351
|
+
this.commandJobsUnsubscribe?.();
|
|
2352
|
+
this.commandJobsUnsubscribe = undefined;
|
|
2338
2353
|
if (this.batchEventUnsub) {
|
|
2339
2354
|
this.batchEventUnsub();
|
|
2340
2355
|
this.batchEventUnsub = null;
|
package/dist/plugin/runtime.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { EmbeddedRuntimeFactory, ZtcPluginContext, ZtcMuxStartContext, ZtcM
|
|
|
3
3
|
export type { EmbeddedRuntimeContext, EmbeddedRuntimeSession, } from '../embedded/runtime.js';
|
|
4
4
|
export type { EmbeddedBackendType, EmbeddedDocPaneDefinition, EmbeddedEmulatorDefinition, EmbeddedFeedPaneDefinition, EmbeddedPaneDefinition, EmbeddedProcessPaneDefinition, EmbeddedRuntimeDefinition, EmbeddedSystemDefinition, EmbeddedSystemSummary, EmbeddedWorkflowDefinition, LoadedEmbeddedSystem, } from '../embedded/types.js';
|
|
5
5
|
export type { Command, CommandContext, ShellResult } from '../agent/commands/types.js';
|
|
6
|
-
export type { MirrorViewState, ViewPlugin, ViewPluginContext } from '../web/view_plugin.js';
|
|
6
|
+
export type { CommandJobView, MirrorViewState, ViewPlugin, ViewPluginContext } from '../web/view_plugin.js';
|
|
7
7
|
export type { MirrorEvent, MirrorPaneRecord } from '../web/mirror_stream.js';
|
|
8
8
|
export { COLONY_WORKTREE_MARKER, generateSessionId, getMuxStatePathForCwd, getSessionDirForCwd, getSessionPathForCwd, loadSessionById, saveSession, } from '../session.js';
|
|
9
9
|
export type { PaneCommandTransport } from '../ui/ratatui/pane_relay.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HeadlessPaneManager — manages child processes for headless mux mode.
|
|
3
|
-
*
|
|
4
|
-
* Implements PaneCommandTransport so pane tools work identically to zmux mode,
|
|
5
|
-
* but spawns/manages child processes directly via Node.js instead of going
|
|
6
|
-
* through the zmux Rust binary.
|
|
7
|
-
*
|
|
8
|
-
* Interactive terminal panes (shell, run-command) get a REAL PTY via node-pty
|
|
9
|
-
* when it's installed (prompt/echo/line-editing, vim/less/top, colors). ZTC and
|
|
10
|
-
* colony panes use piped child_process stdio (they speak the JSON-line pane
|
|
11
|
-
* protocol, not a terminal). If node-pty isn't available (e.g. a CI box with no
|
|
12
|
-
* native build), terminal panes fall back to the old piped-stdio behavior.
|
|
13
|
-
*/
|
|
14
1
|
import type { RustCommand } from './protocol.js';
|
|
15
2
|
import type { PaneCommandTransport, PaneEventHandler } from './pane_relay.js';
|
|
16
3
|
export declare function isZtcPaneType(paneType: string): boolean;
|
|
@@ -59,12 +46,8 @@ export declare class HeadlessPaneManager implements PaneCommandTransport {
|
|
|
59
46
|
on(handler: PaneEventHandler): () => void;
|
|
60
47
|
private emit;
|
|
61
48
|
send(cmd: RustCommand): boolean;
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
* advertises no capabilities. Callers gating on `hasCapability(...)`
|
|
65
|
-
* correctly fall back to the legacy path.
|
|
66
|
-
*/
|
|
67
|
-
hasCapability(_name: string): boolean;
|
|
49
|
+
/** Headless implements correlated pane reads and delivery receipts itself. */
|
|
50
|
+
hasCapability(name: string): boolean;
|
|
68
51
|
/**
|
|
69
52
|
* Headless mode has no native renderer to read the clipboard, so image
|
|
70
53
|
* pastes are unavailable (mirror clients attach images over HTTP instead).
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripPasteMarkers } from './pane_input.js';
|
|
2
|
+
import { CAPABILITY_PANE_DELIVERY_ACK } from './protocol.js';
|
|
1
3
|
/**
|
|
2
4
|
* HeadlessPaneManager — manages child processes for headless mux mode.
|
|
3
5
|
*
|
|
@@ -245,13 +247,9 @@ export class HeadlessPaneManager {
|
|
|
245
247
|
return true;
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
* correctly fall back to the legacy path.
|
|
252
|
-
*/
|
|
253
|
-
hasCapability(_name) {
|
|
254
|
-
return false;
|
|
250
|
+
/** Headless implements correlated pane reads and delivery receipts itself. */
|
|
251
|
+
hasCapability(name) {
|
|
252
|
+
return name === CAPABILITY_PANE_DELIVERY_ACK;
|
|
255
253
|
}
|
|
256
254
|
/**
|
|
257
255
|
* Headless mode has no native renderer to read the clipboard, so image
|
|
@@ -355,6 +353,16 @@ export class HeadlessPaneManager {
|
|
|
355
353
|
launch: { command: cmd.command, args, cwd, requestId: cmd.request_id },
|
|
356
354
|
};
|
|
357
355
|
const appendOutput = (text) => {
|
|
356
|
+
const modeText = (pane.terminalModeTail ?? '') + text;
|
|
357
|
+
for (const match of modeText.matchAll(/\x1b(?:\[\?([0-9;]+)([hl])|\[!p|c)/g)) {
|
|
358
|
+
if (!match[1])
|
|
359
|
+
pane.bracketedPaste = false;
|
|
360
|
+
else if (match[1].split(';').includes('2004'))
|
|
361
|
+
pane.bracketedPaste = match[2] === 'h';
|
|
362
|
+
}
|
|
363
|
+
// Retain only an unfinished escape sequence, never replay a completed mode.
|
|
364
|
+
const tail = modeText.slice(modeText.lastIndexOf('\x1b'));
|
|
365
|
+
pane.terminalModeTail = tail.length <= 256 && /^\x1b(?:\[(?:\?[0-9;]*|!)?)?$/.test(tail) ? tail : '';
|
|
358
366
|
const lines = text.split('\n');
|
|
359
367
|
for (const line of lines) {
|
|
360
368
|
pane.outputBuffer.push(line);
|
|
@@ -598,13 +606,14 @@ export class HeadlessPaneManager {
|
|
|
598
606
|
const pane = this.panes.get(cmd.pane_id) ?? this.closedPanes.get(cmd.pane_id);
|
|
599
607
|
if (!pane) {
|
|
600
608
|
// Return a deterministic read event so callers don't hang on timeout.
|
|
601
|
-
this.emit({ type: 'pane_read', pane_id: cmd.pane_id, content: '', viewport: cmd.viewport ? true : undefined });
|
|
609
|
+
this.emit({ type: 'pane_read', request_id: cmd.request_id, error: `Pane ${cmd.pane_id} not found`, pane_id: cmd.pane_id, content: '', viewport: cmd.viewport ? true : undefined });
|
|
602
610
|
return true;
|
|
603
611
|
}
|
|
604
|
-
const lines = cmd.lines ??
|
|
612
|
+
const lines = Math.min(cmd.lines ?? 200, 2000);
|
|
605
613
|
const content = pane.outputBuffer.slice(-lines).join('\n');
|
|
606
614
|
this.emit({
|
|
607
615
|
type: 'pane_read',
|
|
616
|
+
request_id: cmd.request_id,
|
|
608
617
|
pane_id: cmd.pane_id,
|
|
609
618
|
content,
|
|
610
619
|
viewport: cmd.viewport ? true : undefined,
|
|
@@ -628,37 +637,34 @@ export class HeadlessPaneManager {
|
|
|
628
637
|
}
|
|
629
638
|
handleWritePane(cmd) {
|
|
630
639
|
const pane = this.panes.get(cmd.pane_id);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
640
|
+
const acknowledge = (error, queued = false, bytes) => this.emit({ type: 'pane_write_result',
|
|
641
|
+
request_id: cmd.request_id, pane_id: cmd.pane_id, delivered: !error && !queued, queued,
|
|
642
|
+
...(bytes === undefined ? {} : { total_bytes: bytes, ...(queued ? {} : { delivered_bytes: error ? 0 : bytes, partial: false }) }),
|
|
643
|
+
...(error ? { error } : {}) });
|
|
644
|
+
if (!pane || !pane.isAlive || pane.closing || (!pane.process && !pane.pty)) {
|
|
645
|
+
acknowledge(`Pane ${cmd.pane_id} not found or not alive`);
|
|
636
646
|
return true;
|
|
637
647
|
}
|
|
638
648
|
try {
|
|
639
|
-
// PTY panes: write raw keystrokes (xterm sends \r and control codes itself);
|
|
640
|
-
// the PTY handles echo/line-discipline. Do NOT append \n.
|
|
641
649
|
if (pane.pty) {
|
|
642
|
-
|
|
650
|
+
const text = cmd.input.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n+$/, '');
|
|
651
|
+
const payload = cmd.control ? cmd.input.replace(/[\r\n]+$/, '') + '\r'
|
|
652
|
+
: cmd.submit ? (pane.bracketedPaste ? `\x1b[200~${stripPasteMarkers(text)}\x1b[201~\r` : text.replace(/\n/g, '\r') + '\r') : cmd.input;
|
|
653
|
+
pane.pty.write(payload);
|
|
654
|
+
acknowledge(undefined, payload.length > 0, Buffer.byteLength(payload));
|
|
643
655
|
return true;
|
|
644
656
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
? encodeZtcPaneInput(cmd.input)
|
|
657
|
-
: (cmd.input.endsWith('\n') ? cmd.input : cmd.input + '\n');
|
|
658
|
-
pane.process?.stdin?.write(payload);
|
|
659
|
-
}
|
|
660
|
-
catch {
|
|
661
|
-
// Process stdin may be closed
|
|
657
|
+
const stdin = pane.process?.stdin;
|
|
658
|
+
if (!stdin || stdin.destroyed || !stdin.writable)
|
|
659
|
+
throw new Error('Pane input stream is closed');
|
|
660
|
+
// ZTC child prompts and control lines retain their separate JSON protocol.
|
|
661
|
+
const payload = cmd.control ? cmd.input.replace(/\n+$/, '') + '\n'
|
|
662
|
+
: isZtcPaneType(pane.paneType) ? encodeZtcPaneInput(cmd.input)
|
|
663
|
+
: cmd.submit ? cmd.input.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n+$/, '') + '\n' : cmd.input;
|
|
664
|
+
stdin.write(payload, error => acknowledge(error?.message, false, Buffer.byteLength(payload)));
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
acknowledge(error.message);
|
|
662
668
|
}
|
|
663
669
|
return true;
|
|
664
670
|
}
|
|
@@ -737,6 +743,7 @@ export class HeadlessPaneManager {
|
|
|
737
743
|
}
|
|
738
744
|
if (pane.isAlive && (pane.process || pane.pty)) {
|
|
739
745
|
try {
|
|
746
|
+
pane.closing = true;
|
|
740
747
|
this.killPaneProc(pane, 'SIGTERM');
|
|
741
748
|
}
|
|
742
749
|
catch {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Remove paste delimiters, including ones revealed by removing an inner marker.
|
|
2
|
+
* A stack bounds work to O(n) even for deeply nested input.
|
|
3
|
+
*/
|
|
4
|
+
export function stripPasteMarkers(text) {
|
|
5
|
+
const body = [];
|
|
6
|
+
for (const character of text) {
|
|
7
|
+
body.push(character);
|
|
8
|
+
if (character === '~' && body.length >= 6) {
|
|
9
|
+
const suffix = body.slice(-6).join('');
|
|
10
|
+
if (suffix === '\x1b[200~' || suffix === '\x1b[201~')
|
|
11
|
+
body.length -= 6;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return body.join('');
|
|
15
|
+
}
|
|
@@ -15,6 +15,7 @@ export interface PaneCommandTransport {
|
|
|
15
15
|
send(cmd: RustCommand): boolean;
|
|
16
16
|
on(handler: PaneEventHandler): () => void;
|
|
17
17
|
readonly isMuxMode: boolean;
|
|
18
|
+
hasCapability?(name: string): boolean;
|
|
18
19
|
}
|
|
19
20
|
export declare class PaneRelayServer {
|
|
20
21
|
private transport;
|
|
@@ -35,6 +36,7 @@ export declare class PaneRelayClient implements PaneCommandTransport {
|
|
|
35
36
|
private readline;
|
|
36
37
|
private handlers;
|
|
37
38
|
private ready;
|
|
39
|
+
private capabilities;
|
|
38
40
|
private socketPath;
|
|
39
41
|
constructor(socketPath: string);
|
|
40
42
|
/** Connect to the parent's relay socket */
|
|
@@ -44,6 +46,7 @@ export declare class PaneRelayClient implements PaneCommandTransport {
|
|
|
44
46
|
send(cmd: RustCommand): boolean;
|
|
45
47
|
/** Subscribe to pane events from the parent */
|
|
46
48
|
on(handler: PaneEventHandler): () => void;
|
|
49
|
+
hasCapability(name: string): boolean;
|
|
47
50
|
/** Child has pane capabilities via relay */
|
|
48
51
|
get isMuxMode(): boolean;
|
|
49
52
|
/** Disconnect from the relay */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { createServer, createConnection } from 'net';
|
|
13
13
|
import { createInterface } from 'readline';
|
|
14
14
|
import { unlinkSync } from 'fs';
|
|
15
|
-
import { serializeCommand, parseEvent } from './protocol.js';
|
|
15
|
+
import { serializeCommand, parseEvent, CAPABILITY_PANE_DELIVERY_ACK } from './protocol.js';
|
|
16
16
|
import { generateIpcPath, isNamedPipePath } from './ipc_path.js';
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// PaneRelayServer — runs in parent ZTC (--mux mode)
|
|
@@ -22,6 +22,7 @@ const PANE_EVENT_TYPES = new Set([
|
|
|
22
22
|
'pane_created',
|
|
23
23
|
'pane_closed',
|
|
24
24
|
'pane_read',
|
|
25
|
+
'pane_write_result',
|
|
25
26
|
'pane_list',
|
|
26
27
|
'pane_state',
|
|
27
28
|
'pane_output',
|
|
@@ -47,6 +48,9 @@ export class PaneRelayServer {
|
|
|
47
48
|
});
|
|
48
49
|
this.server = createServer((client) => {
|
|
49
50
|
this.clients.add(client);
|
|
51
|
+
client.write(JSON.stringify({ type: 'ready', version: 'pane-relay', cols: 0, rows: 0,
|
|
52
|
+
capabilities: this.transport.hasCapability?.(CAPABILITY_PANE_DELIVERY_ACK) ? [CAPABILITY_PANE_DELIVERY_ACK] : [],
|
|
53
|
+
}) + '\n');
|
|
50
54
|
const rl = createInterface({ input: client, crlfDelay: Infinity });
|
|
51
55
|
rl.on('line', (line) => {
|
|
52
56
|
const parsed = parseEvent(line);
|
|
@@ -125,6 +129,7 @@ export class PaneRelayClient {
|
|
|
125
129
|
readline = null;
|
|
126
130
|
handlers = new Set();
|
|
127
131
|
ready = false;
|
|
132
|
+
capabilities = new Set();
|
|
128
133
|
socketPath;
|
|
129
134
|
constructor(socketPath) {
|
|
130
135
|
this.socketPath = socketPath;
|
|
@@ -140,7 +145,16 @@ export class PaneRelayClient {
|
|
|
140
145
|
this.socket = createConnection(this.socketPath, () => {
|
|
141
146
|
this.ready = true;
|
|
142
147
|
this.setupHandlers();
|
|
143
|
-
|
|
148
|
+
// New parents advertise transport capabilities. Old parents have no
|
|
149
|
+
// handshake, so retain their unconfirmed-delivery path after a short grace.
|
|
150
|
+
const timer = setTimeout(() => { unsubscribe(); resolve(); }, 100);
|
|
151
|
+
const unsubscribe = this.on(event => {
|
|
152
|
+
if (event.type === 'ready') {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
unsubscribe();
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
144
158
|
});
|
|
145
159
|
this.socket.on('error', (err) => {
|
|
146
160
|
if (attempts < maxAttempts && err.code === 'ENOENT') {
|
|
@@ -167,6 +181,8 @@ export class PaneRelayClient {
|
|
|
167
181
|
const event = parseEvent(line);
|
|
168
182
|
if (!event)
|
|
169
183
|
return;
|
|
184
|
+
if (event.type === 'ready')
|
|
185
|
+
this.capabilities = new Set(event.capabilities ?? []);
|
|
170
186
|
for (const handler of this.handlers) {
|
|
171
187
|
try {
|
|
172
188
|
handler(event);
|
|
@@ -201,6 +217,7 @@ export class PaneRelayClient {
|
|
|
201
217
|
this.handlers.add(handler);
|
|
202
218
|
return () => { this.handlers.delete(handler); };
|
|
203
219
|
}
|
|
220
|
+
hasCapability(name) { return this.capabilities.has(name); }
|
|
204
221
|
/** Child has pane capabilities via relay */
|
|
205
222
|
get isMuxMode() {
|
|
206
223
|
return this.ready;
|
|
@@ -211,6 +228,7 @@ export class PaneRelayClient {
|
|
|
211
228
|
this.cleanup();
|
|
212
229
|
}
|
|
213
230
|
cleanup() {
|
|
231
|
+
this.capabilities.clear();
|
|
214
232
|
if (this.readline) {
|
|
215
233
|
this.readline.close();
|
|
216
234
|
this.readline = null;
|