@zergai/ztc 0.2.6-dev.36101352252 → 0.2.7-dev.36236183737
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 +43 -17
- 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 +19 -5
- package/dist/agent/agent.d.ts +5 -1
- package/dist/agent/agent.js +51 -8
- package/dist/agent/backends/account_pool.d.ts +5 -6
- package/dist/agent/backends/account_pool.js +39 -14
- package/dist/agent/backends/account_rotation.d.ts +3 -0
- package/dist/agent/backends/account_rotation.js +14 -3
- package/dist/agent/backends/codex_account_status.d.ts +42 -0
- package/dist/agent/backends/codex_account_status.js +196 -0
- package/dist/agent/backends/codex_app_server.d.ts +12 -2
- package/dist/agent/backends/codex_app_server.js +66 -6
- package/dist/agent/backends/index.d.ts +2 -0
- package/dist/agent/backends/index.js +13 -5
- package/dist/agent/backends/openai.d.ts +2 -2
- package/dist/agent/backends/openai.js +4 -3
- package/dist/agent/backends/openai_compatible.js +10 -10
- package/dist/agent/backends/openai_responses.d.ts +17 -5
- package/dist/agent/backends/openai_responses.js +2 -4
- package/dist/agent/backends/token_usage.js +1 -1
- package/dist/agent/backends/types.d.ts +3 -0
- package/dist/agent/commands/accounts.d.ts +7 -1
- package/dist/agent/commands/accounts.js +299 -101
- package/dist/agent/commands/config.js +11 -3
- package/dist/agent/commands/config_profile_switch.d.ts +2 -0
- package/dist/agent/commands/config_profile_switch.js +22 -0
- package/dist/agent/commands/context.js +4 -3
- package/dist/agent/commands/index.js +1 -2
- package/dist/agent/commands/input_mode.js +1 -1
- package/dist/agent/commands/model.js +7 -2
- package/dist/agent/commands/plan.d.ts +12 -0
- package/dist/agent/commands/plan.js +39 -28
- package/dist/agent/commands/profile.d.ts +1 -1
- package/dist/agent/commands/profile.js +34 -257
- package/dist/agent/commands/provider_choices.d.ts +12 -1
- package/dist/agent/commands/provider_choices.js +29 -2
- package/dist/agent/commands/provider_selection.d.ts +3 -0
- package/dist/agent/commands/provider_selection.js +63 -0
- package/dist/agent/commands/settings.d.ts +2 -0
- package/dist/agent/commands/settings.js +655 -50
- package/dist/agent/commands/types.d.ts +19 -2
- package/dist/agent/tools/ask_user.js +2 -2
- package/dist/app/agent_runner.d.ts +1 -1
- package/dist/app/agent_runner.js +2 -2
- package/dist/app/ask_user_response.d.ts +3 -0
- package/dist/app/ask_user_response.js +16 -0
- package/dist/app/command_context.js +29 -13
- package/dist/app/controller.d.ts +17 -2
- package/dist/app/controller.js +24 -1
- package/dist/app/selection_completion.d.ts +9 -3
- package/dist/app/selection_completion.js +54 -22
- package/dist/cli_args.js +10 -1
- package/dist/cli_main.js +85 -2
- package/dist/cli_ratatui.d.ts +22 -4
- package/dist/cli_ratatui.js +421 -146
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +20 -0
- package/dist/config/profiles.d.ts +9 -0
- package/dist/config/profiles.js +87 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +283 -40
- package/dist/headless.js +14 -0
- package/dist/panes/model_catalog.d.ts +1 -1
- package/dist/panes/model_catalog.js +24 -2
- package/dist/panes/ztc_launch.js +6 -0
- package/dist/router_auth.js +3 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +3 -0
- package/dist/types.d.ts +23 -2
- package/dist/ui/views/app.d.ts +3 -1
- package/dist/ui/views/app.js +3 -2
- package/dist/ui/views/completion_menu.js +6 -6
- package/dist/ui/views/input_area.d.ts +2 -1
- package/dist/ui/views/input_area.js +9 -8
- package/dist/ui/views/status_bar.d.ts +2 -0
- package/dist/ui/views/status_bar.js +15 -4
- package/dist/ui/widgets/color_panel.d.ts +9 -0
- package/dist/ui/widgets/color_panel.js +41 -26
- package/dist/ui/widgets/navigator_panel.d.ts +9 -0
- package/dist/ui/widgets/navigator_panel.js +17 -16
- package/dist/ui/widgets/panel_frame.d.ts +83 -3
- package/dist/ui/widgets/panel_frame.js +107 -9
- package/dist/ui/widgets/questionnaire.d.ts +9 -0
- package/dist/ui/widgets/questionnaire.js +214 -16
- package/dist/ui/widgets/resume_panel.d.ts +9 -0
- package/dist/ui/widgets/resume_panel.js +18 -17
- package/dist/ui/widgets/rewind_panel.d.ts +9 -0
- package/dist/ui/widgets/rewind_panel.js +15 -16
- package/dist/ui/widgets/select.d.ts +6 -0
- package/dist/ui/widgets/select.js +47 -12
- package/dist/ui/widgets/settings_panel.d.ts +75 -0
- package/dist/ui/widgets/settings_panel.js +197 -39
- package/dist/ui/widgets/status_line_panel.d.ts +9 -0
- package/dist/ui/widgets/status_line_panel.js +18 -19
- package/dist/ui/widgets/transcript_preview.d.ts +8 -0
- package/dist/ui/widgets/transcript_preview.js +10 -0
- package/dist/utils/context.js +2 -0
- package/dist/utils/fireworks.d.ts +20 -0
- package/dist/utils/fireworks.js +23 -0
- package/dist/utils/models.d.ts +2 -1
- package/dist/utils/models.js +19 -0
- package/dist/web/accounts_mirror.d.ts +7 -0
- package/dist/web/accounts_mirror.js +59 -7
- package/dist/web/mirror_stream.js +19 -5
- package/dist/zlm-embed.mjs +5734 -5362
- package/package.json +2 -2
- package/dist/agent/commands/provider.d.ts +0 -2
- package/dist/agent/commands/provider.js +0 -75
package/dist/cli_ratatui.js
CHANGED
|
@@ -14,6 +14,7 @@ import { join } from 'path';
|
|
|
14
14
|
import { execFile, spawn as spawnChild } from 'child_process';
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import { configStore } from './config.js';
|
|
17
|
+
import { withActiveProfileArg } from './config/profiles.js';
|
|
17
18
|
import { CODEX_REASONING_EFFORTS } from './config/types.js';
|
|
18
19
|
import { getSupportedCodexReasoningEfforts, isCodexReasoningEffortSupported, unsupportedEffortMessage, } from './utils/models.js';
|
|
19
20
|
import { getVersion } from './utils/version.js';
|
|
@@ -36,7 +37,7 @@ import { runtimePluginRegistry } from './plugin/runtime_registry.js';
|
|
|
36
37
|
import { getMirrorCommandUsages, isRichSlashUsage } from './agent/commands/mirror_usages.js';
|
|
37
38
|
import { createModeContext } from './agent/commands/mode_commands.js';
|
|
38
39
|
import { goalManager, goalPinTask, detectGoalCompletion, detectGoalBlocked, goalBlockedReason, goalFinalNote, turnProducedText, turnToolOutcomes, shouldAccountFinalTurn, snapshotGoalForTurn } from './agent/commands/goal.js';
|
|
39
|
-
import { allocatePlanArtifact, findPlanContent, generatePlanPath, commitPlan, PlanFileCreationError, } from './agent/commands/plan.js';
|
|
40
|
+
import { allocatePlanArtifact, askPlanApproval, findPlanContent, generatePlanPath, commitPlan, PlanFileCreationError, } from './agent/commands/plan.js';
|
|
40
41
|
import { setTodoUpdateCallback, clearTodoUpdateCallback, setPaneToolsTuiProcess, clearPaneToolsTuiProcess, setPaneToolsOwnId, setPaneToolsExitCallback, setPaneToolsMirrorCallback, setPaneToolsZdeLiteCallback, setPaneToolsPendingLaunchCallback, clearPaneToolsPendingLaunchCallback, setSupervisionManager, clearSupervisionManager, setBusTransport, clearBusTransport, getBusTransport, setEvolutionTuiProcess, clearEvolutionTuiProcess, resetAskUserTurnFlag } from './agent/tools/index.js';
|
|
41
42
|
import { shutdownLSPManager } from './lsp/index.js';
|
|
42
43
|
import { zmuxKeymapPayload } from './config/keybindings.js';
|
|
@@ -56,6 +57,7 @@ import { completePath } from './utils/path_complete.js';
|
|
|
56
57
|
import { togglePlanMode } from './app/modes.js';
|
|
57
58
|
import { isMeaningfulUserInput } from './app/input_validation.js';
|
|
58
59
|
import { headlessInteractionPolicy } from './app/headless_interaction.js';
|
|
60
|
+
import { formatAskUserResponse } from './app/ask_user_response.js';
|
|
59
61
|
import { connectStartupZerg } from './zerg/startup_connect.js';
|
|
60
62
|
import { showSettingsPage } from './app/settings_page.js';
|
|
61
63
|
import { readRepoConfig, writeRepoConfig } from './config/repo_config.js';
|
|
@@ -180,6 +182,21 @@ const RESUME_INTERRUPT_CONFIRM_MS = 5000;
|
|
|
180
182
|
* match `paddedWidgetView` in `ui/views/app.ts`.
|
|
181
183
|
*/
|
|
182
184
|
const WIDGET_SURFACE_PADDING_X = 1;
|
|
185
|
+
/**
|
|
186
|
+
* Spaces a full-page surface painted before its first frame cell.
|
|
187
|
+
*
|
|
188
|
+
* A panel that caps its frame centres it, so its margin is data the panel
|
|
189
|
+
* produced, not something the host can recompute without duplicating the cap
|
|
190
|
+
* and the split. Reading it off the painted line keeps the two in step: whatever
|
|
191
|
+
* the panel decided is what the wheel undoes.
|
|
192
|
+
*/
|
|
193
|
+
function leadingSurfaceMargin(content) {
|
|
194
|
+
if (!content)
|
|
195
|
+
return 0;
|
|
196
|
+
const first = content.split('\n', 1)[0];
|
|
197
|
+
const frameAt = first.search(/[╭╰│]/);
|
|
198
|
+
return frameAt > 0 ? frameAt : 0;
|
|
199
|
+
}
|
|
183
200
|
/**
|
|
184
201
|
* Notice that appears in the status line when the user cancels a run. It reports
|
|
185
202
|
* a run that is already over, so it is feedback rather than state; the
|
|
@@ -393,7 +410,10 @@ export class RatatuiApp {
|
|
|
393
410
|
nativeDictationModule = null;
|
|
394
411
|
legacyDictationModule = null;
|
|
395
412
|
dictationFilterModule = null;
|
|
396
|
-
|
|
413
|
+
/** Busy Enter prompts waiting for the next completed tool batch. */
|
|
414
|
+
pendingSteering = [];
|
|
415
|
+
nativeSteeringInFlight = [];
|
|
416
|
+
nativeSteeringTail = Promise.resolve();
|
|
397
417
|
pendingBusTrigger = false; // Bus message arrived while agent was busy — trigger runAgent after current run
|
|
398
418
|
// Queue state lives in the controller; this latch guards single-flight flush.
|
|
399
419
|
flushingQueue = false;
|
|
@@ -482,6 +502,15 @@ export class RatatuiApp {
|
|
|
482
502
|
* only the render path knows which layout the frame actually used.
|
|
483
503
|
*/
|
|
484
504
|
widgetSurfaceOriginY = null;
|
|
505
|
+
/**
|
|
506
|
+
* Columns between the ZTC pane's left edge and the surface's first column.
|
|
507
|
+
*
|
|
508
|
+
* A full-page panel caps its frame and centres it, so the frame no longer
|
|
509
|
+
* starts at the pane's own gutter: the margin is whatever the panel did not
|
|
510
|
+
* spend. Recorded from the surface it just painted, because only the panel
|
|
511
|
+
* knows how wide its frame ended up.
|
|
512
|
+
*/
|
|
513
|
+
widgetSurfaceOriginX = 0;
|
|
485
514
|
/** Last value pushed to zmux's wheel capture, to send only on change. */
|
|
486
515
|
wheelCaptureActive = false;
|
|
487
516
|
promptViewer;
|
|
@@ -530,6 +559,7 @@ export class RatatuiApp {
|
|
|
530
559
|
// Pending widget callbacks from mirror browser (keyed by widget ID)
|
|
531
560
|
pendingWidgetCallbacks = new Map();
|
|
532
561
|
widgetIdCounter = 0;
|
|
562
|
+
activeSettingsWidgetId = null;
|
|
533
563
|
// zmux multiplexer mode
|
|
534
564
|
muxMode = false;
|
|
535
565
|
/**
|
|
@@ -967,8 +997,7 @@ export class RatatuiApp {
|
|
|
967
997
|
if (this.muxMode) {
|
|
968
998
|
process.env.ZTC_PANE_RELAY = generateRelaySocketPath();
|
|
969
999
|
}
|
|
970
|
-
// Don't set ZTC_TUI_VIEWPORT_HEIGHT — let Rust compute it from terminal rows
|
|
971
|
-
// Both sides use min(rows-1, 20) independently.
|
|
1000
|
+
// Don't set ZTC_TUI_VIEWPORT_HEIGHT — let Rust compute it from terminal rows.
|
|
972
1001
|
// Colony children: use real zmux in headless mode so sub-panes get real
|
|
973
1002
|
// PTYs (shells run interactively, ZTC sub-panes work properly).
|
|
974
1003
|
// Parent --headless-mirror keeps HeadlessPaneManager (pure-Node, no PTY)
|
|
@@ -1548,6 +1577,101 @@ export class RatatuiApp {
|
|
|
1548
1577
|
// Sync mirror superuser state after auth changes (login/logout/status).
|
|
1549
1578
|
this.mirrorServer?.setSuperuser(configStore.isSuperuser());
|
|
1550
1579
|
}
|
|
1580
|
+
/** Change the active config while retaining this renderer's session and transcript. */
|
|
1581
|
+
async switchConfigProfileInPlace(name) {
|
|
1582
|
+
if (this.isHostedRuntime()) {
|
|
1583
|
+
throw new Error('Hosted thread settings cannot be replaced by a local config profile.');
|
|
1584
|
+
}
|
|
1585
|
+
const status = this.controller.getState().agentState.status;
|
|
1586
|
+
if (status !== 'idle' && status !== 'error') {
|
|
1587
|
+
throw new Error('Wait for the current turn to finish before switching profiles.');
|
|
1588
|
+
}
|
|
1589
|
+
const previousOverseerOverride = this.overseerModelOverride;
|
|
1590
|
+
const previousConfig = configStore.get();
|
|
1591
|
+
const previousMcp = JSON.stringify(previousConfig.mcpServers ?? {});
|
|
1592
|
+
const previousColonyMcp = process.env.ZTC_COLONY_MCP;
|
|
1593
|
+
const previousColonyOwned = process.env.ZTC_COLONY_MCP_CONFIG_OWNED;
|
|
1594
|
+
let mcpChanged = false;
|
|
1595
|
+
let touchedRuntime = false;
|
|
1596
|
+
const applyColonyOptIn = () => {
|
|
1597
|
+
if (process.env.ZTC_COLONY_MCP_CONFIG_OWNED === '1') {
|
|
1598
|
+
delete process.env.ZTC_COLONY_MCP;
|
|
1599
|
+
delete process.env.ZTC_COLONY_MCP_CONFIG_OWNED;
|
|
1600
|
+
}
|
|
1601
|
+
if (configStore.get().mcp?.colonyAutoAttach && !process.env.ZTC_COLONY_MCP) {
|
|
1602
|
+
process.env.ZTC_COLONY_MCP = '1';
|
|
1603
|
+
process.env.ZTC_COLONY_MCP_CONFIG_OWNED = '1';
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
const reloadMcp = async () => {
|
|
1607
|
+
const { closeMcpServers, colonyOptIn } = await import('./mcp/registry.js');
|
|
1608
|
+
await closeMcpServers();
|
|
1609
|
+
const servers = configStore.get().mcpServers;
|
|
1610
|
+
if ((servers && Object.keys(servers).length > 0) || colonyOptIn()) {
|
|
1611
|
+
const { initMcpServers } = await import('./mcp/client.js');
|
|
1612
|
+
await initMcpServers(servers);
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
const reloadSkills = async () => {
|
|
1616
|
+
const { invalidateSkillsCache, preloadSkillsManifest } = await import('./skills/registry.js');
|
|
1617
|
+
invalidateSkillsCache();
|
|
1618
|
+
await preloadSkillsManifest();
|
|
1619
|
+
};
|
|
1620
|
+
try {
|
|
1621
|
+
await configStore.switchProfile(name, async () => {
|
|
1622
|
+
touchedRuntime = true;
|
|
1623
|
+
applyColonyOptIn();
|
|
1624
|
+
mcpChanged = previousMcp !== JSON.stringify(configStore.get().mcpServers ?? {})
|
|
1625
|
+
|| previousColonyMcp !== process.env.ZTC_COLONY_MCP;
|
|
1626
|
+
if (mcpChanged)
|
|
1627
|
+
await reloadMcp();
|
|
1628
|
+
await reloadSkills();
|
|
1629
|
+
this.overseerModelOverride = null;
|
|
1630
|
+
const nextAgent = this.createAgent();
|
|
1631
|
+
this.replaceAgent(nextAgent);
|
|
1632
|
+
if (nextAgent)
|
|
1633
|
+
await this.awaitAgentBind();
|
|
1634
|
+
this.hookLifecycle.ensureWatcher();
|
|
1635
|
+
if (this.shouldPersistSession())
|
|
1636
|
+
await this.saveCurrentSession(true);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
catch (error) {
|
|
1640
|
+
this.overseerModelOverride = previousOverseerOverride;
|
|
1641
|
+
if (previousColonyMcp === undefined)
|
|
1642
|
+
delete process.env.ZTC_COLONY_MCP;
|
|
1643
|
+
else
|
|
1644
|
+
process.env.ZTC_COLONY_MCP = previousColonyMcp;
|
|
1645
|
+
if (previousColonyOwned === undefined)
|
|
1646
|
+
delete process.env.ZTC_COLONY_MCP_CONFIG_OWNED;
|
|
1647
|
+
else
|
|
1648
|
+
process.env.ZTC_COLONY_MCP_CONFIG_OWNED = previousColonyOwned;
|
|
1649
|
+
try {
|
|
1650
|
+
if (touchedRuntime) {
|
|
1651
|
+
if (mcpChanged)
|
|
1652
|
+
await reloadMcp();
|
|
1653
|
+
await reloadSkills();
|
|
1654
|
+
const restoredAgent = this.createAgent();
|
|
1655
|
+
this.replaceAgent(restoredAgent);
|
|
1656
|
+
if (restoredAgent)
|
|
1657
|
+
await this.awaitAgentBind();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
catch (restoreError) {
|
|
1661
|
+
throw new Error(`Profile switch failed: ${error.message}. Restoring the previous runtime also failed: ${restoreError.message}`);
|
|
1662
|
+
}
|
|
1663
|
+
throw error;
|
|
1664
|
+
}
|
|
1665
|
+
if (this.muxMode)
|
|
1666
|
+
this.tui.send({ type: 'set_keybindings', keymap: zmuxKeymapPayload(configStore.get().keybindings) });
|
|
1667
|
+
this.mirrorServer?.setSfxConfig(configStore.get().sfx ?? { enabled: false });
|
|
1668
|
+
this.mirrorServer?.setSuperuser(configStore.isSuperuser());
|
|
1669
|
+
const { notifyAccountsChanged } = await import('./agent/backends/account_events.js');
|
|
1670
|
+
notifyAccountsChanged();
|
|
1671
|
+
if (!this.agent)
|
|
1672
|
+
this.addSystemMessage('This profile has no usable provider yet. Configure one in /settings.');
|
|
1673
|
+
this.render();
|
|
1674
|
+
}
|
|
1551
1675
|
/**
|
|
1552
1676
|
* Environment handed to the renderer so zmux boots with the user's rebinds.
|
|
1553
1677
|
* Live changes are pushed over the socket afterwards (`set_keybindings`).
|
|
@@ -1587,6 +1711,7 @@ export class RatatuiApp {
|
|
|
1587
1711
|
// and auto-compact onto the char/4 estimate while the bar stayed real.
|
|
1588
1712
|
getContextTokens: () => this.controller.getState().contextLength ?? 0,
|
|
1589
1713
|
getCachedInputTokens: () => this.controller.getState().agentState.cachedInputTokens,
|
|
1714
|
+
getCacheInputTokens: () => this.controller.getState().agentState.cacheInputTokens,
|
|
1590
1715
|
// The window the bar last used (status.total) — provider-reported (Codex's
|
|
1591
1716
|
// modelContextWindow) when available, else the static guess. Keeps /context
|
|
1592
1717
|
// + auto-compact on the SAME denominator the bar shows.
|
|
@@ -1594,6 +1719,7 @@ export class RatatuiApp {
|
|
|
1594
1719
|
setContextUsage: (used, estimated) => this.publishContextUsage(used, estimated),
|
|
1595
1720
|
setAgentState: (state) => this.controller.setAgentState(state),
|
|
1596
1721
|
reloadAgent: (options) => this.reloadAgentInPlace(options?.defer),
|
|
1722
|
+
switchConfigProfile: (name) => this.switchConfigProfileInPlace(name),
|
|
1597
1723
|
setRuntimeModelOverride: (provider, model) => {
|
|
1598
1724
|
configStore.setCliOverride('provider', provider);
|
|
1599
1725
|
configStore.setCliOverride('model', model);
|
|
@@ -2044,14 +2170,36 @@ export class RatatuiApp {
|
|
|
2044
2170
|
},
|
|
2045
2171
|
} : {}),
|
|
2046
2172
|
settingsPanel: {
|
|
2173
|
+
closeSettingsPanel: () => {
|
|
2174
|
+
if (this.settingsPanelWidget.isActive()) {
|
|
2175
|
+
this.settingsPanelWidget.cancel();
|
|
2176
|
+
}
|
|
2177
|
+
else if (this.activeSettingsWidgetId) {
|
|
2178
|
+
this.pendingWidgetCallbacks.delete(this.activeSettingsWidgetId);
|
|
2179
|
+
if (this.mirrorServer)
|
|
2180
|
+
this.mirrorServer.publishWidgetDismiss(this.activeSettingsWidgetId);
|
|
2181
|
+
else if (this.mirrorClient)
|
|
2182
|
+
this.mirrorClient.publishWidgetDismiss(this.activeSettingsWidgetId);
|
|
2183
|
+
this.activeSettingsWidgetId = null;
|
|
2184
|
+
}
|
|
2185
|
+
this.render();
|
|
2186
|
+
},
|
|
2047
2187
|
showSettingsPanel: (options) => {
|
|
2188
|
+
if (this.activeSettingsWidgetId) {
|
|
2189
|
+
this.pendingWidgetCallbacks.delete(this.activeSettingsWidgetId);
|
|
2190
|
+
if (this.mirrorServer)
|
|
2191
|
+
this.mirrorServer.publishWidgetDismiss(this.activeSettingsWidgetId);
|
|
2192
|
+
else if (this.mirrorClient)
|
|
2193
|
+
this.mirrorClient.publishWidgetDismiss(this.activeSettingsWidgetId);
|
|
2194
|
+
}
|
|
2048
2195
|
const widgetId = `settings_${++this.widgetIdCounter}`;
|
|
2196
|
+
this.activeSettingsWidgetId = widgetId;
|
|
2049
2197
|
// The full settings page replaced the widget_settings modal in the
|
|
2050
2198
|
// web mirror: navigate watching clients to it. A headless child
|
|
2051
2199
|
// (mux child or colony cell) posts to the PARENT mirror — the
|
|
2052
2200
|
// colony singleton's page is the colony-wide config surface, its
|
|
2053
2201
|
// edits landing in the shared config every cell reads.
|
|
2054
|
-
showSettingsPage({ title: options.title, initialFilter: options.initialFilter }, {
|
|
2202
|
+
showSettingsPage({ title: options.title, initialFilter: options.initialFilter ?? options.initialCategory?.toLowerCase() }, {
|
|
2055
2203
|
mirrorServer: this.mirrorServer,
|
|
2056
2204
|
mirrorClient: this.headlessMirror ? this.mirrorClient : null,
|
|
2057
2205
|
});
|
|
@@ -2059,12 +2207,19 @@ export class RatatuiApp {
|
|
|
2059
2207
|
this.pendingWidgetCallbacks.set(widgetId, (answer) => {
|
|
2060
2208
|
if (answer._closed) {
|
|
2061
2209
|
this.pendingWidgetCallbacks.delete(widgetId);
|
|
2062
|
-
this.
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
this.
|
|
2067
|
-
|
|
2210
|
+
if (this.activeSettingsWidgetId === widgetId)
|
|
2211
|
+
this.activeSettingsWidgetId = null;
|
|
2212
|
+
if (this.settingsPanelWidget.isActive()) {
|
|
2213
|
+
// Its onClose owns dismissal and the user callback.
|
|
2214
|
+
this.settingsPanelWidget.cancel();
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
if (this.mirrorServer)
|
|
2218
|
+
this.mirrorServer.publishWidgetDismiss(widgetId);
|
|
2219
|
+
else if (this.mirrorClient)
|
|
2220
|
+
this.mirrorClient.publishWidgetDismiss(widgetId);
|
|
2221
|
+
options.onClose?.();
|
|
2222
|
+
}
|
|
2068
2223
|
return;
|
|
2069
2224
|
}
|
|
2070
2225
|
if (typeof answer.setting === 'string' && typeof answer.value === 'string') {
|
|
@@ -2080,10 +2235,16 @@ export class RatatuiApp {
|
|
|
2080
2235
|
this.settingsPanelWidget.show({
|
|
2081
2236
|
title: options.title,
|
|
2082
2237
|
categories: options.categories,
|
|
2238
|
+
layout: options.layout,
|
|
2239
|
+
notice: options.notice,
|
|
2240
|
+
initialSetting: options.initialSetting,
|
|
2241
|
+
initialCategory: options.initialCategory,
|
|
2083
2242
|
initialFilter: options.initialFilter,
|
|
2084
2243
|
onSettingChange: (setting, value) => options.onSettingChange(setting, value),
|
|
2085
2244
|
onClose: () => {
|
|
2086
2245
|
this.pendingWidgetCallbacks.delete(widgetId);
|
|
2246
|
+
if (this.activeSettingsWidgetId === widgetId)
|
|
2247
|
+
this.activeSettingsWidgetId = null;
|
|
2087
2248
|
if (this.mirrorServer)
|
|
2088
2249
|
this.mirrorServer.publishWidgetDismiss(widgetId);
|
|
2089
2250
|
options.onClose?.();
|
|
@@ -3055,7 +3216,7 @@ export class RatatuiApp {
|
|
|
3055
3216
|
debugLog('[start] Calling tui.start()');
|
|
3056
3217
|
// Start the TUI process
|
|
3057
3218
|
const desktopUpdateRelaunch = this.desktopMode
|
|
3058
|
-
? buildDesktopUpdateRelaunchContext(process.argv.slice(1), this.sessionId, process.execPath, process.cwd(), process.pid)
|
|
3219
|
+
? buildDesktopUpdateRelaunchContext(withActiveProfileArg(process.argv.slice(1)), this.sessionId, process.execPath, process.cwd(), process.pid)
|
|
3059
3220
|
: undefined;
|
|
3060
3221
|
const size = await this.tui.start({
|
|
3061
3222
|
mux: this.muxMode,
|
|
@@ -3070,6 +3231,7 @@ export class RatatuiApp {
|
|
|
3070
3231
|
// rather than zmux scrolling history under a panel it thinks is closed.
|
|
3071
3232
|
this.wheelCaptureActive = false;
|
|
3072
3233
|
this.widgetSurfaceOriginY = null;
|
|
3234
|
+
this.widgetSurfaceOriginX = 0;
|
|
3073
3235
|
const normalized = normalizeTerminalSize(size.cols, size.rows);
|
|
3074
3236
|
this.cols = normalized.cols;
|
|
3075
3237
|
this.rows = normalized.rows;
|
|
@@ -4736,7 +4898,10 @@ export class RatatuiApp {
|
|
|
4736
4898
|
const status = contextStatusFromUsed(used, model, windowOverride);
|
|
4737
4899
|
this.controller.dispatch({ type: 'SET_CONTEXT', length: used, estimated, status });
|
|
4738
4900
|
const agentState = this.controller.getState().agentState;
|
|
4739
|
-
this.controller.setAgentState({
|
|
4901
|
+
this.controller.setAgentState({
|
|
4902
|
+
...agentState, contextTokens: used, tokensEstimated: estimated,
|
|
4903
|
+
cacheInputTokens: undefined, cachedInputTokens: undefined,
|
|
4904
|
+
});
|
|
4740
4905
|
this.mirror?.publishTokenUsage(used, 0, status.total, status.percentage, { reset: true });
|
|
4741
4906
|
}
|
|
4742
4907
|
clearTranscript() {
|
|
@@ -5237,8 +5402,9 @@ export class RatatuiApp {
|
|
|
5237
5402
|
* used to give up a row here anyway, which left one blank line at the bottom
|
|
5238
5403
|
* of every spawned ZTC pane.
|
|
5239
5404
|
*
|
|
5240
|
-
* Inline
|
|
5241
|
-
*
|
|
5405
|
+
* Inline fills the terminal except for one shell row. The transcript remains
|
|
5406
|
+
* in terminal scrollback when it no longer fits the live view. Desktop is
|
|
5407
|
+
* left reserving one because ZTerm's own reservation is unverified.
|
|
5242
5408
|
*/
|
|
5243
5409
|
viewportHeightFor(rows, size) {
|
|
5244
5410
|
if (this.muxMode)
|
|
@@ -5247,7 +5413,7 @@ export class RatatuiApp {
|
|
|
5247
5413
|
return rows;
|
|
5248
5414
|
if (this.desktopMode)
|
|
5249
5415
|
return rows - 1;
|
|
5250
|
-
return
|
|
5416
|
+
return rows - 1;
|
|
5251
5417
|
}
|
|
5252
5418
|
handleTuiEvent(event) {
|
|
5253
5419
|
debugLog(`[handleTuiEvent] type=${event.type}`);
|
|
@@ -5447,7 +5613,7 @@ export class RatatuiApp {
|
|
|
5447
5613
|
? this.inputState.segments[0].text : '';
|
|
5448
5614
|
if (textCursorIndex(this.inputState.segments, this.inputState.cursor) !== text.length)
|
|
5449
5615
|
text = '';
|
|
5450
|
-
this.selectionCompletion.update(text, configStore.getProvider(), signal => buildModelsController(configStore).list(
|
|
5616
|
+
this.selectionCompletion.update(text, configStore.getProvider(), (signal, provider, authMode) => buildModelsController(configStore).list(provider, { signal, authMode }), commands, configStore.getModel());
|
|
5451
5617
|
return this.selectionCompletion.view();
|
|
5452
5618
|
}
|
|
5453
5619
|
handleInput(input, key) {
|
|
@@ -5543,8 +5709,7 @@ export class RatatuiApp {
|
|
|
5543
5709
|
this.setInputToText(selection.text, selection.text.length);
|
|
5544
5710
|
else
|
|
5545
5711
|
this.render();
|
|
5546
|
-
|
|
5547
|
-
return;
|
|
5712
|
+
return;
|
|
5548
5713
|
}
|
|
5549
5714
|
// Handle Ctrl+C — priority order:
|
|
5550
5715
|
// 1. If a generation is running, cancel it.
|
|
@@ -5951,7 +6116,7 @@ export class RatatuiApp {
|
|
|
5951
6116
|
}
|
|
5952
6117
|
return;
|
|
5953
6118
|
}
|
|
5954
|
-
// Slash
|
|
6119
|
+
// Slash and model completion is handled above; Tab here completes paths.
|
|
5955
6120
|
if (key.tab && !key.shift) {
|
|
5956
6121
|
const currentText = getPlainText(this.inputState.segments);
|
|
5957
6122
|
// @-mention file completion (ZTC-12): repeated Tab cycles candidates.
|
|
@@ -6973,27 +7138,35 @@ export class RatatuiApp {
|
|
|
6973
7138
|
this.addErrorMessage(buildCredentialSetupMessage());
|
|
6974
7139
|
return;
|
|
6975
7140
|
}
|
|
6976
|
-
//
|
|
7141
|
+
// Enter during a run steers the current turn at its next tool boundary.
|
|
7142
|
+
// Tab remains the explicit after-turn queue.
|
|
6977
7143
|
const state = this.controller.getState();
|
|
6978
7144
|
const busy = state.agentState.status !== 'idle' && state.agentState.status !== 'error';
|
|
6979
7145
|
const inputMode = state.inputMode || 'queue';
|
|
6980
7146
|
if (busy) {
|
|
6981
7147
|
if (inputMode === 'queue') {
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
7148
|
+
this.controller.addMessage({ role: 'user', content: text, attachments, metadata: { steering: true } });
|
|
7149
|
+
const message = this.controller.getState().messages.at(-1);
|
|
7150
|
+
if (message) {
|
|
7151
|
+
this.pendingSteering.push(message);
|
|
7152
|
+
if (this.agent) {
|
|
7153
|
+
const activeAgent = this.agent;
|
|
7154
|
+
// Serial requests preserve the submitted order on a provider-owned
|
|
7155
|
+
// thread and in the thread-reuse prefix recorded by that backend.
|
|
7156
|
+
const attempt = this.nativeSteeringTail.then(() => activeAgent.steerUserMessage(message)).then(accepted => {
|
|
7157
|
+
if (accepted) {
|
|
7158
|
+
this.pendingSteering = this.pendingSteering.filter(pending => pending.id !== message.id);
|
|
7159
|
+
}
|
|
7160
|
+
}).catch(() => undefined);
|
|
7161
|
+
this.nativeSteeringTail = attempt;
|
|
7162
|
+
this.nativeSteeringInFlight.push(attempt);
|
|
7163
|
+
}
|
|
6996
7164
|
}
|
|
7165
|
+
this.nextTurnAutomaticGoal = false;
|
|
7166
|
+
this.mirror?.publishMessage('user', text, attachments.length > 0 ? { attachments } : undefined);
|
|
7167
|
+
this.controller.setToast(this.pendingSteering.length > 1
|
|
7168
|
+
? `Sending ${this.pendingSteering.length} messages after the current tool finishes`
|
|
7169
|
+
: 'Sending after the current tool finishes');
|
|
6997
7170
|
return;
|
|
6998
7171
|
}
|
|
6999
7172
|
// In interrupt mode, show warning but continue
|
|
@@ -7082,6 +7255,11 @@ export class RatatuiApp {
|
|
|
7082
7255
|
}
|
|
7083
7256
|
// Snapshot message count before the run so we can roll back on cancel
|
|
7084
7257
|
const preRunMessageCount = this.controller.getState().messages.length;
|
|
7258
|
+
// Any steering retained across a prior interrupted or widget-ended turn
|
|
7259
|
+
// is already present in this snapshot. It must not be injected twice.
|
|
7260
|
+
this.pendingSteering = [];
|
|
7261
|
+
this.nativeSteeringInFlight = [];
|
|
7262
|
+
this.nativeSteeringTail = Promise.resolve();
|
|
7085
7263
|
// Auto-continue and queue flushes re-enter runAgent without going through
|
|
7086
7264
|
// handleSubmit, so cancel any stale recap here too.
|
|
7087
7265
|
if (!isRetry) {
|
|
@@ -7232,7 +7410,8 @@ export class RatatuiApp {
|
|
|
7232
7410
|
// Unreported clears to undefined rather than holding the previous
|
|
7233
7411
|
// turn's value, which would pair a stale cache count with this
|
|
7234
7412
|
// turn's input size.
|
|
7235
|
-
|
|
7413
|
+
cacheInputTokens: event.cacheInputTokens,
|
|
7414
|
+
cachedInputTokens: event.contextCachedInputTokens,
|
|
7236
7415
|
});
|
|
7237
7416
|
this.controller.dispatch({
|
|
7238
7417
|
type: 'SET_CONTEXT',
|
|
@@ -7243,7 +7422,7 @@ export class RatatuiApp {
|
|
|
7243
7422
|
// Publish token usage to observer
|
|
7244
7423
|
this.mirror?.publishTokenUsage(used, event.usage.outputTokens || 0, total, percentage, {
|
|
7245
7424
|
reset: contextReset,
|
|
7246
|
-
cached: event.
|
|
7425
|
+
cached: event.contextCachedInputTokens,
|
|
7247
7426
|
});
|
|
7248
7427
|
break;
|
|
7249
7428
|
}
|
|
@@ -7259,7 +7438,15 @@ export class RatatuiApp {
|
|
|
7259
7438
|
break;
|
|
7260
7439
|
}
|
|
7261
7440
|
}
|
|
7262
|
-
}));
|
|
7441
|
+
}, () => this.pendingSteering.splice(0)));
|
|
7442
|
+
if (!cancelled) {
|
|
7443
|
+
while (this.nativeSteeringInFlight.length > 0) {
|
|
7444
|
+
await Promise.allSettled(this.nativeSteeringInFlight.splice(0));
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
else {
|
|
7448
|
+
this.nativeSteeringInFlight = [];
|
|
7449
|
+
}
|
|
7263
7450
|
}
|
|
7264
7451
|
catch (err) {
|
|
7265
7452
|
this.flushLocalStreamDelta();
|
|
@@ -7283,6 +7470,7 @@ export class RatatuiApp {
|
|
|
7283
7470
|
}
|
|
7284
7471
|
}
|
|
7285
7472
|
this.addErrorMessage(`Error: ${errorMsg}`);
|
|
7473
|
+
this.movePendingSteeringToEnd();
|
|
7286
7474
|
this.controller.setAgentState({ status: 'error', error: errorMsg });
|
|
7287
7475
|
this.stopSpinner();
|
|
7288
7476
|
this.mirror?.publishStreamEnd();
|
|
@@ -7296,6 +7484,8 @@ export class RatatuiApp {
|
|
|
7296
7484
|
contextTokens: this.controller.getState().agentState.contextTokens,
|
|
7297
7485
|
tokensUsed: this.controller.getState().agentState.tokensUsed,
|
|
7298
7486
|
tokensEstimated: this.controller.getState().agentState.tokensEstimated,
|
|
7487
|
+
cacheInputTokens: this.controller.getState().agentState.cacheInputTokens,
|
|
7488
|
+
cachedInputTokens: this.controller.getState().agentState.cachedInputTokens,
|
|
7299
7489
|
});
|
|
7300
7490
|
this.mirror?.publishStatus('idle');
|
|
7301
7491
|
}, 3000);
|
|
@@ -7314,6 +7504,7 @@ export class RatatuiApp {
|
|
|
7314
7504
|
// with a correction. Use Esc while idle to rewind to an earlier prompt
|
|
7315
7505
|
// when the whole exchange should come out of context.
|
|
7316
7506
|
this.addInterruptedNotice();
|
|
7507
|
+
this.movePendingSteeringToEnd();
|
|
7317
7508
|
this.controller.setAgentState({
|
|
7318
7509
|
status: 'idle',
|
|
7319
7510
|
// Preserve the token accounting across the idle flip — a wholesale
|
|
@@ -7322,6 +7513,8 @@ export class RatatuiApp {
|
|
|
7322
7513
|
contextTokens: this.controller.getState().agentState.contextTokens,
|
|
7323
7514
|
tokensUsed: this.controller.getState().agentState.tokensUsed,
|
|
7324
7515
|
tokensEstimated: this.controller.getState().agentState.tokensEstimated,
|
|
7516
|
+
cacheInputTokens: this.controller.getState().agentState.cacheInputTokens,
|
|
7517
|
+
cachedInputTokens: this.controller.getState().agentState.cachedInputTokens,
|
|
7325
7518
|
});
|
|
7326
7519
|
this.stopSpinner();
|
|
7327
7520
|
this.mirror?.publishStreamEnd();
|
|
@@ -7336,70 +7529,113 @@ export class RatatuiApp {
|
|
|
7336
7529
|
return;
|
|
7337
7530
|
}
|
|
7338
7531
|
if (result) {
|
|
7339
|
-
//
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
//
|
|
7343
|
-
const
|
|
7344
|
-
const
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
else if (hasStreamedResponse) {
|
|
7368
|
-
const lastAssistantIdx = state.messages.reduce((last, m, i) => isPersistableAssistantMessage(m) ? i : last, -1);
|
|
7369
|
-
// A streamed turn already has its assistant message in state, so the
|
|
7370
|
-
// reasoning trace has to be attached here too — otherwise it is kept only
|
|
7371
|
-
// for buffered responses and streamed turns replay a stripped history.
|
|
7372
|
-
if (lastAssistantIdx >= 0 && (lightToolCalls.length > 0 || result.reasoning)) {
|
|
7373
|
-
const updatedMessages = state.messages.map((m, i) => i === lastAssistantIdx
|
|
7374
|
-
? {
|
|
7375
|
-
...m,
|
|
7376
|
-
...(lightToolCalls.length > 0 ? { toolCalls: lightToolCalls } : {}),
|
|
7377
|
-
metadata: { ...m.metadata, ...finalMeta,
|
|
7378
|
-
...(result.reasoning ? { reasoningContent: result.reasoning } : {}),
|
|
7379
|
-
},
|
|
7380
|
-
}
|
|
7381
|
-
: m);
|
|
7382
|
-
this.controller.setMessages(updatedMessages);
|
|
7383
|
-
}
|
|
7384
|
-
}
|
|
7385
|
-
// Persist tool_result messages for all completed/errored tool calls.
|
|
7386
|
-
// The agent's internal loop pushes these to its local messages array,
|
|
7387
|
-
// but that array is detached from the controller state (immutable reducer).
|
|
7388
|
-
// Without this, repairConversationHistory injects phantom "interrupted"
|
|
7389
|
-
// errors for every tool call, poisoning the context on the next turn.
|
|
7390
|
-
if (result.toolCalls.length > 0) {
|
|
7391
|
-
const toolResultContent = result.toolCalls
|
|
7392
|
-
.filter(tc => tc.status === 'complete' || tc.status === 'error')
|
|
7393
|
-
.map(tc => ({
|
|
7394
|
-
tool_use_id: tc.id,
|
|
7395
|
-
content: tc.error ? `Error: ${tc.error}` : (tc.result || ''),
|
|
7532
|
+
// The agent's ordered tool-use/result/steering messages are the source of
|
|
7533
|
+
// truth for a steered turn. The normal display path collapses all tool
|
|
7534
|
+
// calls onto the final assistant message, which would replay a steering
|
|
7535
|
+
// prompt before the tool result it followed.
|
|
7536
|
+
const turnMessages = result.turnMessages ?? [];
|
|
7537
|
+
const steeredTurn = turnMessages.some(message => message.metadata?.steering === true);
|
|
7538
|
+
if (steeredTurn) {
|
|
7539
|
+
const live = this.controller.getState().messages;
|
|
7540
|
+
// A steering message the turn actually consumed is replayed from
|
|
7541
|
+
// `turnMessages`, so keeping the live copy would duplicate it. One still
|
|
7542
|
+
// in `pendingSteering` never met a tool boundary: dropping it here loses
|
|
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
|
+
} : {}),
|
|
7396
7560
|
}));
|
|
7397
|
-
|
|
7561
|
+
const finalAssistant = result.content.trim()
|
|
7562
|
+
? {
|
|
7563
|
+
id: generateId(), role: 'assistant', content: result.content,
|
|
7564
|
+
timestamp: new Date(),
|
|
7565
|
+
...(result.reasoning ? { metadata: { reasoningContent: result.reasoning } } : {}),
|
|
7566
|
+
}
|
|
7567
|
+
: null;
|
|
7568
|
+
this.controller.setMessages([
|
|
7569
|
+
...live.slice(0, preRunMessageCount), ...orderedMessages,
|
|
7570
|
+
...(finalAssistant ? [finalAssistant] : []), ...retainedMessages,
|
|
7571
|
+
]);
|
|
7572
|
+
}
|
|
7573
|
+
else {
|
|
7574
|
+
// Add final response if not already streamed
|
|
7575
|
+
const state = this.controller.getState();
|
|
7576
|
+
const hasStreamedResponse = this.currentRunStreamed;
|
|
7577
|
+
// Hoist finalMeta so all branches can use it
|
|
7578
|
+
const finalMode = this.controller.getMode();
|
|
7579
|
+
const finalMeta = {
|
|
7580
|
+
...(finalMode === 'swarm' ? { conductorMode: true } : {}),
|
|
7581
|
+
...(result.toolCalls.length > 0 ? { toolCallsInTranscript: true } : {}),
|
|
7582
|
+
};
|
|
7583
|
+
// Strip .result/.error from toolCalls before persisting on the assistant —
|
|
7584
|
+
// the explicit tool_result user message (below) carries that data. Keeping
|
|
7585
|
+
// both doubles the context size and the token estimate.
|
|
7586
|
+
const lightToolCalls = result.toolCalls.map(tc => {
|
|
7587
|
+
const { result: _r, error: _e, ...rest } = tc;
|
|
7588
|
+
return rest;
|
|
7589
|
+
});
|
|
7590
|
+
if (!hasStreamedResponse && (result.content || result.toolCalls.length > 0)) {
|
|
7398
7591
|
this.controller.addMessage({
|
|
7399
|
-
role: '
|
|
7400
|
-
content:
|
|
7401
|
-
|
|
7592
|
+
role: 'assistant',
|
|
7593
|
+
content: result.content || '[Using tools...]',
|
|
7594
|
+
toolCalls: lightToolCalls.length > 0 ? lightToolCalls : undefined,
|
|
7595
|
+
// Carry the provider's reasoning trace so the next turn replays this
|
|
7596
|
+
// assistant message intact; reasoning models expect it back as-is.
|
|
7597
|
+
metadata: result.reasoning ? { ...finalMeta, reasoningContent: result.reasoning } : finalMeta,
|
|
7402
7598
|
});
|
|
7599
|
+
if (result.content)
|
|
7600
|
+
this.maybeSpeakText(result.content);
|
|
7601
|
+
}
|
|
7602
|
+
else if (hasStreamedResponse) {
|
|
7603
|
+
const lastAssistantIdx = state.messages.reduce((last, m, i) => isPersistableAssistantMessage(m) ? i : last, -1);
|
|
7604
|
+
// A streamed turn already has its assistant message in state, so the
|
|
7605
|
+
// reasoning trace has to be attached here too — otherwise it is kept only
|
|
7606
|
+
// for buffered responses and streamed turns replay a stripped history.
|
|
7607
|
+
if (lastAssistantIdx >= 0 && (lightToolCalls.length > 0 || result.reasoning)) {
|
|
7608
|
+
const updatedMessages = state.messages.map((m, i) => i === lastAssistantIdx
|
|
7609
|
+
? {
|
|
7610
|
+
...m,
|
|
7611
|
+
...(lightToolCalls.length > 0 ? { toolCalls: lightToolCalls } : {}),
|
|
7612
|
+
metadata: { ...m.metadata, ...finalMeta,
|
|
7613
|
+
...(result.reasoning ? { reasoningContent: result.reasoning } : {}),
|
|
7614
|
+
},
|
|
7615
|
+
}
|
|
7616
|
+
: m);
|
|
7617
|
+
this.controller.setMessages(updatedMessages);
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
// Persist tool_result messages for all completed/errored tool calls.
|
|
7621
|
+
// The agent's internal loop pushes these to its local messages array,
|
|
7622
|
+
// but that array is detached from the controller state (immutable reducer).
|
|
7623
|
+
// Without this, repairConversationHistory injects phantom "interrupted"
|
|
7624
|
+
// errors for every tool call, poisoning the context on the next turn.
|
|
7625
|
+
if (result.toolCalls.length > 0) {
|
|
7626
|
+
const toolResultContent = result.toolCalls
|
|
7627
|
+
.filter(tc => tc.status === 'complete' || tc.status === 'error')
|
|
7628
|
+
.map(tc => ({
|
|
7629
|
+
tool_use_id: tc.id,
|
|
7630
|
+
content: tc.error ? `Error: ${tc.error}` : (tc.result || ''),
|
|
7631
|
+
}));
|
|
7632
|
+
if (toolResultContent.length > 0) {
|
|
7633
|
+
this.controller.addMessage({
|
|
7634
|
+
role: 'user',
|
|
7635
|
+
content: JSON.stringify(toolResultContent),
|
|
7636
|
+
metadata: { toolResult: true },
|
|
7637
|
+
});
|
|
7638
|
+
}
|
|
7403
7639
|
}
|
|
7404
7640
|
}
|
|
7405
7641
|
// Progress text is not completion. Explicit caps must remain visible even
|
|
@@ -7441,8 +7677,13 @@ export class RatatuiApp {
|
|
|
7441
7677
|
contextTokens: this.controller.getState().agentState.contextTokens,
|
|
7442
7678
|
tokensUsed: this.controller.getState().agentState.tokensUsed,
|
|
7443
7679
|
tokensEstimated: this.controller.getState().agentState.tokensEstimated,
|
|
7680
|
+
cacheInputTokens: this.controller.getState().agentState.cacheInputTokens,
|
|
7681
|
+
cachedInputTokens: this.controller.getState().agentState.cachedInputTokens,
|
|
7444
7682
|
});
|
|
7445
7683
|
this.stopSpinner();
|
|
7684
|
+
// The idle-edge subscriber sees pending steering and leaves the Tab queue
|
|
7685
|
+
// alone. Move the prompt after the answer before starting its fallback turn.
|
|
7686
|
+
const needsSteeringTurn = this.movePendingSteeringToEnd();
|
|
7446
7687
|
// Fold in the final turn when the goal went terminal during this turn (the
|
|
7447
7688
|
// agent ran `/goal complete` / `/goal pause` via ztc_command, which carries no
|
|
7448
7689
|
// turn facts). This sits above every early return below — ask_user, plan
|
|
@@ -7512,9 +7753,24 @@ export class RatatuiApp {
|
|
|
7512
7753
|
setTimeout(() => void this.runAgent(), 0);
|
|
7513
7754
|
return;
|
|
7514
7755
|
}
|
|
7756
|
+
// A prompt sent during inference had no later tool boundary. Its user
|
|
7757
|
+
// message is already in the transcript; start a fresh turn with it now.
|
|
7758
|
+
if (needsSteeringTurn) {
|
|
7759
|
+
setTimeout(() => void this.runAgent(), 0);
|
|
7760
|
+
return;
|
|
7761
|
+
}
|
|
7762
|
+
// A `subturn` entry that never met a tool boundary (the model answered
|
|
7763
|
+
// without calling one, or the turn ended right after its last call) must not
|
|
7764
|
+
// be stranded behind Tab entries. Promote it now and take the next turn, in
|
|
7765
|
+
// queue order, so Enter's "send it soon" promise still resolves.
|
|
7766
|
+
const strandedSubturn = this.controller.dequeueSubturnMessage();
|
|
7515
7767
|
this.maybeTriggerRecap(endOfTurnTokens);
|
|
7516
7768
|
// Flushing is also subscribed to idle transitions in start().
|
|
7517
7769
|
const processedQueue = this.maybeFlushQueue();
|
|
7770
|
+
if (strandedSubturn) {
|
|
7771
|
+
void this.handleSubmit({ text: strandedSubturn.text, attachments: strandedSubturn.attachments || [] });
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7518
7774
|
// If nothing the user queued is taking the next turn, let an active /goal
|
|
7519
7775
|
// drive the next step. Queued user input always wins, so the goal only
|
|
7520
7776
|
// continues when the queue is empty.
|
|
@@ -7678,25 +7934,49 @@ export class RatatuiApp {
|
|
|
7678
7934
|
maybeFlushQueue() {
|
|
7679
7935
|
if (this.flushingQueue)
|
|
7680
7936
|
return false;
|
|
7681
|
-
|
|
7682
|
-
if (state.inputMode !== 'queue')
|
|
7937
|
+
if (this.pendingSteering.length > 0)
|
|
7683
7938
|
return false;
|
|
7939
|
+
const state = this.controller.getState();
|
|
7940
|
+
// Deliberately NOT gated on `inputMode`. `/inputmode interrupt` selects what
|
|
7941
|
+
// Enter does while busy; it must never strand a prompt the user queued with
|
|
7942
|
+
// Tab, which promised delivery at the end of the turn regardless.
|
|
7684
7943
|
if (state.queue.length === 0)
|
|
7685
7944
|
return false;
|
|
7686
7945
|
if (state.agentState.status !== 'idle')
|
|
7687
7946
|
return false;
|
|
7688
7947
|
this.flushingQueue = true;
|
|
7689
|
-
|
|
7690
|
-
|
|
7948
|
+
// End-of-turn entries only. A `subturn` entry still waiting here means no
|
|
7949
|
+
// tool boundary arrived before the turn ended; runAgent's idle path starts a
|
|
7950
|
+
// fresh turn for it, so picking it up here too would double-send it.
|
|
7951
|
+
const index = state.queue.findIndex(entry => entry.delivery !== 'subturn');
|
|
7952
|
+
if (index < 0) {
|
|
7691
7953
|
this.flushingQueue = false;
|
|
7692
7954
|
return false;
|
|
7693
7955
|
}
|
|
7956
|
+
const next = this.controller.getQueue()[index];
|
|
7957
|
+
this.controller.removeQueueEntry(next.id);
|
|
7694
7958
|
void this.handleSubmit({ text: next.text, attachments: next.attachments || [] }).finally(() => {
|
|
7695
7959
|
this.flushingQueue = false;
|
|
7696
7960
|
this.maybeFlushQueue();
|
|
7697
7961
|
});
|
|
7698
7962
|
return true;
|
|
7699
7963
|
}
|
|
7964
|
+
/** Put steering that missed a tool boundary after the finished answer. */
|
|
7965
|
+
movePendingSteeringToEnd() {
|
|
7966
|
+
if (this.pendingSteering.length === 0)
|
|
7967
|
+
return false;
|
|
7968
|
+
const ids = new Set(this.pendingSteering.map(message => message.id));
|
|
7969
|
+
const messages = this.controller.getState().messages;
|
|
7970
|
+
const pending = messages.filter(message => ids.has(message.id));
|
|
7971
|
+
this.pendingSteering = [];
|
|
7972
|
+
if (pending.length === 0)
|
|
7973
|
+
return false;
|
|
7974
|
+
this.controller.setMessages([
|
|
7975
|
+
...messages.filter(message => !ids.has(message.id)),
|
|
7976
|
+
...pending,
|
|
7977
|
+
]);
|
|
7978
|
+
return true;
|
|
7979
|
+
}
|
|
7700
7980
|
/** True while an agent turn is active (same definition as handleSubmit's busy check). */
|
|
7701
7981
|
isAgentBusy() {
|
|
7702
7982
|
const status = this.controller.getState().agentState.status;
|
|
@@ -7775,11 +8055,15 @@ export class RatatuiApp {
|
|
|
7775
8055
|
const serializedText = serializeSegments(this.inputState.segments);
|
|
7776
8056
|
if (!isMeaningfulUserInput(serializedText))
|
|
7777
8057
|
return;
|
|
7778
|
-
|
|
8058
|
+
// `end-of-turn`: a Tab-queued prompt waits for the WHOLE turn to finish.
|
|
8059
|
+
// Marking the intent on the entry (instead of relying on the busy-state
|
|
8060
|
+
// `inputMode` at flush time) keeps the promise even if the user later runs
|
|
8061
|
+
// /inputmode interrupt, which only governs what Enter does.
|
|
8062
|
+
if (this.controller.enqueueMessage(serializedText, 'end-of-turn')) {
|
|
7779
8063
|
this.clearComposerAfterSubmit(plainText);
|
|
7780
8064
|
// Durable audit alongside the live queue panel; mirror publish is
|
|
7781
8065
|
// explicit because controller.addMessage is terminal-only.
|
|
7782
|
-
const breadcrumb = `Queued (${this.controller.getQueue().length})`;
|
|
8066
|
+
const breadcrumb = `Queued (${this.controller.getQueue().length}) — sends when the turn finishes`;
|
|
7783
8067
|
this.controller.addMessage({ role: 'system', content: breadcrumb });
|
|
7784
8068
|
this.mirror?.publishMessage('system', breadcrumb);
|
|
7785
8069
|
}
|
|
@@ -8674,39 +8958,28 @@ export class RatatuiApp {
|
|
|
8674
8958
|
// the next rebuild and break the streaming-line accounting).
|
|
8675
8959
|
this.flushTranscriptState();
|
|
8676
8960
|
const ctx = this.buildCommandContext((msg) => this.controller.addMessage(msg));
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
title: 'Plan Approval',
|
|
8680
|
-
question: 'Ready to begin implementation?',
|
|
8681
|
-
type: 'select',
|
|
8682
|
-
options: [
|
|
8683
|
-
{ value: 'approve', label: 'Yes, begin implementation (Recommended)', description: 'Approve the plan, create tasks, and start coding' },
|
|
8684
|
-
{ value: 'edit', label: 'Edit plan first', description: 'Return to plan mode and continue editing' },
|
|
8685
|
-
],
|
|
8686
|
-
allowOther: true,
|
|
8687
|
-
}, (value) => {
|
|
8688
|
-
if (value === null) {
|
|
8961
|
+
askPlanApproval(ctx, decision => {
|
|
8962
|
+
if (decision.kind === 'cancel') {
|
|
8689
8963
|
this.controller.addMessage({ role: 'system', content: 'Plan execution cancelled. Still in plan mode.' });
|
|
8690
8964
|
return;
|
|
8691
8965
|
}
|
|
8692
|
-
|
|
8693
|
-
if (selection === 'approve') {
|
|
8966
|
+
if (decision.kind === 'approve') {
|
|
8694
8967
|
this.planApprovedRecently = true;
|
|
8695
8968
|
commitPlan(planFile, planContent, tasks, ctx, false);
|
|
8696
8969
|
ctx.triggerAgentTurn?.();
|
|
8697
8970
|
}
|
|
8698
|
-
else if (
|
|
8971
|
+
else if (decision.kind === 'edit') {
|
|
8699
8972
|
this.controller.addMessage({ role: 'system', content: 'Returning to plan mode. Continue editing your plan, then run /execute when ready.' });
|
|
8700
8973
|
}
|
|
8701
8974
|
else {
|
|
8702
|
-
this.controller.addMessage({ role: 'system', content: `Feedback noted. Still in plan mode.\n\nFeedback: ${
|
|
8703
|
-
this.controller.addMessage({ role: 'user', content: `Please update the plan based on this feedback: ${
|
|
8975
|
+
this.controller.addMessage({ role: 'system', content: `Feedback noted. Still in plan mode.\n\nFeedback: ${decision.text}` });
|
|
8976
|
+
this.controller.addMessage({ role: 'user', content: `Please update the plan based on this feedback: ${decision.text}` });
|
|
8704
8977
|
}
|
|
8705
8978
|
});
|
|
8706
8979
|
}
|
|
8707
8980
|
/**
|
|
8708
8981
|
* Show ask_user widget after ask_user tool is called.
|
|
8709
|
-
* Presents
|
|
8982
|
+
* Presents options with an optional note for additional context.
|
|
8710
8983
|
*/
|
|
8711
8984
|
showAskUserWidget() {
|
|
8712
8985
|
const data = this.askUserData;
|
|
@@ -8769,6 +9042,7 @@ export class RatatuiApp {
|
|
|
8769
9042
|
})),
|
|
8770
9043
|
})),
|
|
8771
9044
|
allowOther,
|
|
9045
|
+
allowOptionNotes: true,
|
|
8772
9046
|
callback: (answers) => {
|
|
8773
9047
|
debugLog(`[askUser multi] callback fired, answers=${JSON.stringify(answers)?.slice(0, 200)}, mutex=${this.askUserAnswerPending}`);
|
|
8774
9048
|
if (this.askUserAnswerPending)
|
|
@@ -8780,20 +9054,15 @@ export class RatatuiApp {
|
|
|
8780
9054
|
return;
|
|
8781
9055
|
}
|
|
8782
9056
|
if (data.questions.length === 1) {
|
|
8783
|
-
const
|
|
8784
|
-
const answerText = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
9057
|
+
const answerText = formatAskUserResponse(data.questions, answers);
|
|
8785
9058
|
this.controller.addMessage({ role: 'user', content: answerText });
|
|
8786
9059
|
this.mirror?.publishMessage('user', answerText, { isAskUserAnswer: true });
|
|
8787
9060
|
setTimeout(() => void this.runAgent(), 0);
|
|
8788
9061
|
return;
|
|
8789
9062
|
}
|
|
8790
|
-
// Preserve the existing multi-question wire format
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
const answerStr = Array.isArray(val) ? val.join(', ') : (val || '(no answer)');
|
|
8794
|
-
return { question: q.question, answer: answerStr };
|
|
8795
|
-
});
|
|
8796
|
-
const answerText = JSON.stringify(jsonAnswers);
|
|
9063
|
+
// Preserve the existing multi-question wire format, adding note only
|
|
9064
|
+
// when the user supplied one.
|
|
9065
|
+
const answerText = formatAskUserResponse(data.questions, answers);
|
|
8797
9066
|
debugLog(`[askUser multi] sending answer: ${answerText.slice(0, 200)}`);
|
|
8798
9067
|
this.controller.addMessage({ role: 'user', content: answerText });
|
|
8799
9068
|
this.mirror?.publishMessage('user', answerText, { isAskUserAnswer: true });
|
|
@@ -9052,11 +9321,9 @@ export class RatatuiApp {
|
|
|
9052
9321
|
keepPrintedInPane: this.muxMode || this.paneMode,
|
|
9053
9322
|
});
|
|
9054
9323
|
}
|
|
9055
|
-
//
|
|
9056
|
-
//
|
|
9057
|
-
//
|
|
9058
|
-
// exactly once. Mux/pane modes retain their existing fallback because
|
|
9059
|
-
// their printed scrollback may not be visible without user navigation.
|
|
9324
|
+
// Once the first-run banner has reached scrollback, remove only that
|
|
9325
|
+
// explicitly-marked message from the live frame so it appears once when
|
|
9326
|
+
// the user scrolls. Mux/pane modes retain their existing fallback.
|
|
9060
9327
|
if (!externalTranscriptActive && !this.muxMode && !this.paneMode) {
|
|
9061
9328
|
liveMessages = excludePrintedFirstRunWelcome(liveMessages, this.printedMessageIds);
|
|
9062
9329
|
}
|
|
@@ -9076,15 +9343,15 @@ export class RatatuiApp {
|
|
|
9076
9343
|
|| this.formWidget.isActive()
|
|
9077
9344
|
|| this.selectWidget.isActive()
|
|
9078
9345
|
|| this.textInputWidget.isActive();
|
|
9079
|
-
// The question form is a transient overlay, not a work surface: it sizes to
|
|
9080
|
-
// its own content and keeps the recent transcript visible above it.
|
|
9081
|
-
const compactWidget = questionnaireActive;
|
|
9082
|
-
// Use liveViewportHeight (not effectiveViewportHeight which is
|
|
9083
|
-
// computed later + expanded for widgets). scroll_region must match
|
|
9084
|
-
// the BASE viewport so it doesn't overflow into the activity_line.
|
|
9085
9346
|
const agentActive = state.agentState.status === 'thinking'
|
|
9086
9347
|
|| state.agentState.status === 'streaming'
|
|
9087
9348
|
|| state.agentState.status === 'tool_use';
|
|
9349
|
+
const completionView = widgetActive ? null : this.updateSelectionCompletion();
|
|
9350
|
+
// The question form is a transient overlay, not a work surface: it sizes to
|
|
9351
|
+
// its own content and keeps the recent transcript visible above it.
|
|
9352
|
+
const compactWidget = questionnaireActive;
|
|
9353
|
+
// The transcript region must stay above the activity line as the
|
|
9354
|
+
// completion menu expands or contracts.
|
|
9088
9355
|
// Settings is a full work surface. Unlike transient questions and pickers,
|
|
9089
9356
|
// it replaces the transcript while open so small terminals still provide
|
|
9090
9357
|
// enough rows for browsing and editing.
|
|
@@ -9094,7 +9361,7 @@ export class RatatuiApp {
|
|
|
9094
9361
|
cols: this.cols,
|
|
9095
9362
|
inputState: this.inputState,
|
|
9096
9363
|
commands,
|
|
9097
|
-
completion:
|
|
9364
|
+
completion: completionView,
|
|
9098
9365
|
showHeader: false,
|
|
9099
9366
|
showTodos: state.showTodos,
|
|
9100
9367
|
todoCount: state.todos.length,
|
|
@@ -9105,6 +9372,8 @@ export class RatatuiApp {
|
|
|
9105
9372
|
agentActive,
|
|
9106
9373
|
statusLine: configStore.get().statusLine,
|
|
9107
9374
|
});
|
|
9375
|
+
if (completionView)
|
|
9376
|
+
this.selectionCompletion.setVisibleRows(Math.max(1, widgetLayout.completionHeight - 1));
|
|
9108
9377
|
// Determine if a widget is active and get its content
|
|
9109
9378
|
let widgetContent = null;
|
|
9110
9379
|
let widgetView = null;
|
|
@@ -9224,6 +9493,7 @@ export class RatatuiApp {
|
|
|
9224
9493
|
model: configStore.get().model,
|
|
9225
9494
|
emulationId: configStore.getEmulationId(),
|
|
9226
9495
|
effort: configStore.getCodexEffort(),
|
|
9496
|
+
unsupportedEffort: configStore.getUnsupportedCodexEffort(),
|
|
9227
9497
|
inputMode: state.inputMode,
|
|
9228
9498
|
toast: state.toast,
|
|
9229
9499
|
spinnerLabel: state.spinnerLabel,
|
|
@@ -9326,6 +9596,10 @@ export class RatatuiApp {
|
|
|
9326
9596
|
this.widgetSurfaceOriginY = widgetContent && !compactWidget
|
|
9327
9597
|
? frameLayout.contentAreaY
|
|
9328
9598
|
: null;
|
|
9599
|
+
// The panel decides its own left edge, so read it off the line it painted
|
|
9600
|
+
// rather than re-deriving it: a panel that capped its frame starts further
|
|
9601
|
+
// right, and the wheel has to land in the same columns the frame occupies.
|
|
9602
|
+
this.widgetSurfaceOriginX = WIDGET_SURFACE_PADDING_X + leadingSurfaceMargin(widgetContent);
|
|
9329
9603
|
// Tell zmux whether this process is claiming the wheel. While a panel owns
|
|
9330
9604
|
// it, zmux forwards notches instead of scrolling the transcript history
|
|
9331
9605
|
// underneath the panel — otherwise closing the panel would reveal a
|
|
@@ -9410,7 +9684,7 @@ export class RatatuiApp {
|
|
|
9410
9684
|
if (!owner || originY === null)
|
|
9411
9685
|
return;
|
|
9412
9686
|
const y = row - originY;
|
|
9413
|
-
const x = col -
|
|
9687
|
+
const x = col - this.widgetSurfaceOriginX;
|
|
9414
9688
|
if (x < 0 || y < 0)
|
|
9415
9689
|
return;
|
|
9416
9690
|
owner.handleMouse({ wheel: direction, x, y });
|
|
@@ -10112,7 +10386,7 @@ export class RatatuiApp {
|
|
|
10112
10386
|
await this.tui.stop();
|
|
10113
10387
|
// Build relaunch args: preserve runtime flags but force the target session
|
|
10114
10388
|
// as the only resume target.
|
|
10115
|
-
const { command, args, shell } = buildHotRestartLaunch(process.argv.slice(1), options.sessionId, process.execPath);
|
|
10389
|
+
const { command, args, shell } = buildHotRestartLaunch(withActiveProfileArg(process.argv.slice(1)), options.sessionId, process.execPath);
|
|
10116
10390
|
const spawnCwd = existsSync(options.cwd) ? options.cwd : process.cwd();
|
|
10117
10391
|
debugLog(`[${options.label}] Spawning new process: ${command} ${args.join(' ')} (cwd=${spawnCwd})`);
|
|
10118
10392
|
const child = spawnChild(command, args, {
|
|
@@ -10209,6 +10483,7 @@ export class RatatuiApp {
|
|
|
10209
10483
|
model: configStore.get().model,
|
|
10210
10484
|
emulationId: configStore.getEmulationId(),
|
|
10211
10485
|
effort: configStore.getCodexEffort(),
|
|
10486
|
+
unsupportedEffort: configStore.getUnsupportedCodexEffort(),
|
|
10212
10487
|
inputMode: state.inputMode,
|
|
10213
10488
|
toast: state.toast,
|
|
10214
10489
|
mode: state.mode,
|