@namzu/cli 26.3.0 → 28.0.0
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/CHANGELOG.md +271 -0
- package/README.md +67 -46
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +17 -4
- package/dist/checkpoints/store.d.ts.map +1 -1
- package/dist/checkpoints/store.js +20 -5
- package/dist/checkpoints/store.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +124 -14
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts +23 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +41 -2
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts +69 -2
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +216 -136
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/{run-flags.d.ts → exec-flags.d.ts} +36 -28
- package/dist/commands/exec-flags.d.ts.map +1 -0
- package/dist/commands/{run-flags.js → exec-flags.js} +35 -27
- package/dist/commands/exec-flags.js.map +1 -0
- package/dist/commands/exec-json.d.ts +75 -0
- package/dist/commands/exec-json.d.ts.map +1 -0
- package/dist/commands/exec-json.js +473 -0
- package/dist/commands/exec-json.js.map +1 -0
- package/dist/commands/exec.d.ts +42 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +683 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/host-queries.d.ts +12 -0
- package/dist/commands/host-queries.d.ts.map +1 -0
- package/dist/commands/host-queries.js +149 -0
- package/dist/commands/host-queries.js.map +1 -0
- package/dist/commands/prior-messages.js +1 -1
- package/dist/commands/prior-messages.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +5 -5
- package/dist/commands/provider-wait.d.ts.map +1 -1
- package/dist/commands/provider-wait.js +2 -2
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/registry.js.map +1 -1
- package/dist/commands/resident-flags.d.ts +2 -2
- package/dist/commands/resident-flags.d.ts.map +1 -1
- package/dist/commands/resident-flags.js +4 -2
- package/dist/commands/resident-flags.js.map +1 -1
- package/dist/commands/resident-learning.js +1 -1
- package/dist/commands/resident-learning.js.map +1 -1
- package/dist/commands/resident.d.ts.map +1 -1
- package/dist/commands/resident.js +3 -2
- package/dist/commands/resident.js.map +1 -1
- package/dist/commands/serve.js +2 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/state.js +8 -8
- package/dist/commands/state.js.map +1 -1
- package/dist/commands/stdin.d.ts +5 -5
- package/dist/commands/stdin.js +5 -5
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +36 -5
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +59 -20
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tool-result-screens.js +1 -1
- package/dist/config/turn-guards.d.ts +23 -0
- package/dist/config/turn-guards.d.ts.map +1 -0
- package/dist/config/turn-guards.js +46 -0
- package/dist/config/turn-guards.js.map +1 -0
- package/dist/context/directories.d.ts +37 -0
- package/dist/context/directories.d.ts.map +1 -0
- package/dist/context/directories.js +69 -0
- package/dist/context/directories.js.map +1 -0
- package/dist/context/environment.d.ts +55 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +96 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/context/project-tracker.d.ts +3 -3
- package/dist/context/project-tracker.d.ts.map +1 -1
- package/dist/context/project-tracker.js +3 -3
- package/dist/context/project-tracker.js.map +1 -1
- package/dist/context/project.d.ts +2 -2
- package/dist/context/project.js +2 -2
- package/dist/context/sandbox.d.ts +22 -3
- package/dist/context/sandbox.d.ts.map +1 -1
- package/dist/context/sandbox.js +42 -4
- package/dist/context/sandbox.js.map +1 -1
- package/dist/doctor/checks/chain.d.ts +1 -1
- package/dist/doctor/checks/chain.js +7 -7
- package/dist/doctor/checks/chain.js.map +1 -1
- package/dist/doctor/checks/invariants.d.ts +2 -2
- package/dist/doctor/checks/invariants.js +5 -5
- package/dist/doctor/checks/invariants.js.map +1 -1
- package/dist/doctor/checks/logging.js +1 -1
- package/dist/doctor/checks/logging.js.map +1 -1
- package/dist/doctor/checks/sandbox.js +1 -1
- package/dist/doctor/checks/sandbox.js.map +1 -1
- package/dist/doctor/checks/session-export.d.ts +1 -1
- package/dist/doctor/checks/session-export.js +2 -2
- package/dist/doctor/checks/session-export.js.map +1 -1
- package/dist/doctor/checks/telemetry.d.ts +1 -1
- package/dist/doctor/checks/telemetry.js +1 -1
- package/dist/doctor/registry.js +2 -2
- package/dist/doctor/registry.js.map +1 -1
- package/dist/exit-codes.d.ts +2 -2
- package/dist/exit-codes.js +2 -2
- package/dist/integrations/mcp/servers.d.ts +1 -1
- package/dist/integrations/providers/access.d.ts +4 -1
- package/dist/integrations/providers/access.d.ts.map +1 -1
- package/dist/integrations/providers/access.js +6 -3
- package/dist/integrations/providers/access.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.d.ts +6 -6
- package/dist/integrations/providers/chain-capabilities.js +8 -8
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/register.d.ts +1 -1
- package/dist/integrations/providers/register.js +1 -1
- package/dist/integrations/providers/registry.d.ts +1 -1
- package/dist/integrations/providers/zen-catalogue.d.ts +100 -0
- package/dist/integrations/providers/zen-catalogue.d.ts.map +1 -0
- package/dist/integrations/providers/zen-catalogue.js +196 -0
- package/dist/integrations/providers/zen-catalogue.js.map +1 -0
- package/dist/integrations/resident/inspection.d.ts +4 -1
- package/dist/integrations/resident/inspection.d.ts.map +1 -1
- package/dist/integrations/resident/inspection.js +53 -34
- package/dist/integrations/resident/inspection.js.map +1 -1
- package/dist/integrations/resident/learning-storage.d.ts +7 -2
- package/dist/integrations/resident/learning-storage.d.ts.map +1 -1
- package/dist/integrations/resident/learning-storage.js +12 -8
- package/dist/integrations/resident/learning-storage.js.map +1 -1
- package/dist/integrations/resident/runner-launch.d.ts +3 -3
- package/dist/integrations/resident/runner-launch.d.ts.map +1 -1
- package/dist/integrations/resident/runner-store.d.ts +1 -1
- package/dist/integrations/resident/runner-store.d.ts.map +1 -1
- package/dist/integrations/resident/runner-store.js +5 -2
- package/dist/integrations/resident/runner-store.js.map +1 -1
- package/dist/integrations/resident/runner-worker.js +23 -0
- package/dist/integrations/resident/runner-worker.js.map +1 -1
- package/dist/integrations/resident/session-log-reads.d.ts +37 -0
- package/dist/integrations/resident/session-log-reads.d.ts.map +1 -0
- package/dist/integrations/resident/session-log-reads.js +125 -0
- package/dist/integrations/resident/session-log-reads.js.map +1 -0
- package/dist/integrations/resident/session-step.d.ts +4 -2
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +16 -14
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/integrations/resident/storage.d.ts +23 -1
- package/dist/integrations/resident/storage.d.ts.map +1 -1
- package/dist/integrations/resident/storage.js +60 -17
- package/dist/integrations/resident/storage.js.map +1 -1
- package/dist/integrations/resident/tool-evidence.d.ts +4 -2
- package/dist/integrations/resident/tool-evidence.d.ts.map +1 -1
- package/dist/integrations/resident/tool-evidence.js +36 -30
- package/dist/integrations/resident/tool-evidence.js.map +1 -1
- package/dist/integrations/resident/verification.d.ts +2 -2
- package/dist/integrations/resident/verification.d.ts.map +1 -1
- package/dist/integrations/resident/verification.js +4 -4
- package/dist/integrations/resident/verification.js.map +1 -1
- package/dist/integrations/sessions/checkpoint-view.d.ts +13 -0
- package/dist/integrations/sessions/checkpoint-view.d.ts.map +1 -0
- package/dist/integrations/sessions/checkpoint-view.js +48 -0
- package/dist/integrations/sessions/checkpoint-view.js.map +1 -0
- package/dist/integrations/sessions/context-inventory.js +1 -1
- package/dist/integrations/sessions/context-inventory.js.map +1 -1
- package/dist/integrations/sessions/conversation-search.d.ts +16 -20
- package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
- package/dist/integrations/sessions/conversation-search.js +305 -731
- package/dist/integrations/sessions/conversation-search.js.map +1 -1
- package/dist/integrations/sessions/evidence-page-validation.d.ts +4 -4
- package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -1
- package/dist/integrations/sessions/evidence-page-validation.js +1 -1
- package/dist/integrations/sessions/evidence-page-validation.js.map +1 -1
- package/dist/integrations/sessions/evidence-recall.d.ts +40 -2
- package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -1
- package/dist/integrations/sessions/evidence-recall.js +41 -97
- package/dist/integrations/sessions/evidence-recall.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +153 -85
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +414 -484
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/task-context.d.ts +15 -2
- package/dist/integrations/sessions/task-context.d.ts.map +1 -1
- package/dist/integrations/sessions/task-context.js +34 -11
- package/dist/integrations/sessions/task-context.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts +13 -6
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +148 -263
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/home.d.ts +4 -16
- package/dist/integrations/state/home.d.ts.map +1 -1
- package/dist/integrations/state/home.js +6 -42
- package/dist/integrations/state/home.js.map +1 -1
- package/dist/integrations/state/project.d.ts +6 -7
- package/dist/integrations/state/project.d.ts.map +1 -1
- package/dist/integrations/state/project.js +7 -9
- package/dist/integrations/state/project.js.map +1 -1
- package/dist/integrations/state/report.d.ts +27 -41
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +172 -464
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/state/retention.d.ts +18 -0
- package/dist/integrations/state/retention.d.ts.map +1 -0
- package/dist/integrations/state/retention.js +18 -0
- package/dist/integrations/state/retention.js.map +1 -0
- package/dist/integrations/subagents/activity.d.ts +36 -31
- package/dist/integrations/subagents/activity.d.ts.map +1 -1
- package/dist/integrations/subagents/activity.js +27 -23
- package/dist/integrations/subagents/activity.js.map +1 -1
- package/dist/integrations/subagents/batches.d.ts +91 -0
- package/dist/integrations/subagents/batches.d.ts.map +1 -0
- package/dist/integrations/subagents/batches.js +172 -0
- package/dist/integrations/subagents/batches.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +26 -11
- package/dist/integrations/subagents/parent.d.ts.map +1 -1
- package/dist/integrations/subagents/parent.js +13 -28
- package/dist/integrations/subagents/parent.js.map +1 -1
- package/dist/integrations/subagents/policy.d.ts +2 -2
- package/dist/integrations/subagents/policy.d.ts.map +1 -1
- package/dist/integrations/subagents/policy.js +2 -2
- package/dist/integrations/subagents/policy.js.map +1 -1
- package/dist/integrations/subagents/replay.d.ts +91 -26
- package/dist/integrations/subagents/replay.d.ts.map +1 -1
- package/dist/integrations/subagents/replay.js +216 -114
- package/dist/integrations/subagents/replay.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +50 -26
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +132 -126
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/subagents/saved-agents.d.ts +57 -0
- package/dist/integrations/subagents/saved-agents.d.ts.map +1 -0
- package/dist/integrations/subagents/saved-agents.js +102 -0
- package/dist/integrations/subagents/saved-agents.js.map +1 -0
- package/dist/integrations/subagents/slash.d.ts +55 -0
- package/dist/integrations/subagents/slash.d.ts.map +1 -0
- package/dist/integrations/subagents/slash.js +45 -0
- package/dist/integrations/subagents/slash.js.map +1 -0
- package/dist/integrations/telemetry/session-export.d.ts +6 -6
- package/dist/integrations/telemetry/session-export.d.ts.map +1 -1
- package/dist/integrations/telemetry/session-export.js +2 -2
- package/dist/integrations/web/search.d.ts +1 -1
- package/dist/logging.d.ts +5 -5
- package/dist/logging.js +6 -6
- package/dist/memory/presentation.d.ts +11 -0
- package/dist/memory/presentation.d.ts.map +1 -1
- package/dist/memory/presentation.js +25 -0
- package/dist/memory/presentation.js.map +1 -1
- package/dist/memory/store.d.ts +13 -7
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +37 -11
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/typed.d.ts +116 -0
- package/dist/memory/typed.d.ts.map +1 -0
- package/dist/memory/typed.js +336 -0
- package/dist/memory/typed.js.map +1 -0
- package/dist/permissions/checks.d.ts +1 -1
- package/dist/permissions/checks.js +1 -1
- package/dist/permissions/headless-trust.d.ts +4 -4
- package/dist/permissions/headless-trust.d.ts.map +1 -1
- package/dist/permissions/headless-trust.js +6 -6
- package/dist/permissions/headless-trust.js.map +1 -1
- package/dist/permissions/mode.d.ts +4 -4
- package/dist/permissions/mode.js +1 -1
- package/dist/permissions/rules.d.ts +1 -1
- package/dist/permissions/rules.js +1 -1
- package/dist/termination.d.ts +75 -0
- package/dist/termination.d.ts.map +1 -0
- package/dist/termination.js +143 -0
- package/dist/termination.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +2 -2
- package/dist/tui/AgentExplorer.js +4 -4
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts +9 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +456 -307
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +1 -1
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +8 -0
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +25 -2
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.js +1 -1
- package/dist/tui/agent.d.ts +211 -135
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +535 -316
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/conversation-evidence-view.js +3 -3
- package/dist/tui/conversation-evidence-view.js.map +1 -1
- package/dist/tui/conversation-history.d.ts +4 -21
- package/dist/tui/conversation-history.d.ts.map +1 -1
- package/dist/tui/conversation-history.js +4 -28
- package/dist/tui/conversation-history.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +27 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/log-pane.js +1 -1
- package/dist/tui/markdownParser.d.ts +1 -1
- package/dist/tui/markdownParser.js +1 -1
- package/dist/tui/permission-review.d.ts +16 -0
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +25 -2
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +30 -41
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +75 -66
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/status-panel-layout.js +1 -1
- package/dist/tui/status-panel-layout.js.map +1 -1
- package/dist/tui/{run-interruption.d.ts → turn-interruption.d.ts} +4 -4
- package/dist/tui/turn-interruption.d.ts.map +1 -0
- package/dist/tui/{run-interruption.js → turn-interruption.js} +20 -20
- package/dist/tui/turn-interruption.js.map +1 -0
- package/dist/tui/turn-limits-settings.d.ts +39 -0
- package/dist/tui/turn-limits-settings.d.ts.map +1 -0
- package/dist/tui/{run-limits-settings.js → turn-limits-settings.js} +18 -18
- package/dist/tui/turn-limits-settings.js.map +1 -0
- package/dist/tui/types.d.ts +3 -3
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/user-commands/store.d.ts +4 -4
- package/dist/user-commands/store.js +5 -5
- package/dist/user-commands/store.js.map +1 -1
- package/package.json +6 -6
- package/dist/commands/run-flags.d.ts.map +0 -1
- package/dist/commands/run-flags.js.map +0 -1
- package/dist/commands/run-stream.d.ts +0 -65
- package/dist/commands/run-stream.d.ts.map +0 -1
- package/dist/commands/run-stream.js +0 -603
- package/dist/commands/run-stream.js.map +0 -1
- package/dist/commands/run.d.ts +0 -39
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -533
- package/dist/commands/run.js.map +0 -1
- package/dist/config/run-limits.d.ts +0 -11
- package/dist/config/run-limits.d.ts.map +0 -1
- package/dist/config/run-limits.js +0 -64
- package/dist/config/run-limits.js.map +0 -1
- package/dist/integrations/sessions/compaction-evidence.d.ts +0 -5
- package/dist/integrations/sessions/compaction-evidence.d.ts.map +0 -1
- package/dist/integrations/sessions/compaction-evidence.js +0 -59
- package/dist/integrations/sessions/compaction-evidence.js.map +0 -1
- package/dist/integrations/sessions/database.d.ts +0 -5
- package/dist/integrations/sessions/database.d.ts.map +0 -1
- package/dist/integrations/sessions/database.js +0 -34
- package/dist/integrations/sessions/database.js.map +0 -1
- package/dist/integrations/sessions/paths.d.ts +0 -7
- package/dist/integrations/sessions/paths.d.ts.map +0 -1
- package/dist/integrations/sessions/paths.js +0 -14
- package/dist/integrations/sessions/paths.js.map +0 -1
- package/dist/integrations/sessions/run-discovery.d.ts +0 -24
- package/dist/integrations/sessions/run-discovery.d.ts.map +0 -1
- package/dist/integrations/sessions/run-discovery.js +0 -142
- package/dist/integrations/sessions/run-discovery.js.map +0 -1
- package/dist/integrations/sessions/turn-evidence.d.ts +0 -129
- package/dist/integrations/sessions/turn-evidence.d.ts.map +0 -1
- package/dist/integrations/sessions/turn-evidence.js +0 -517
- package/dist/integrations/sessions/turn-evidence.js.map +0 -1
- package/dist/integrations/subagents/history.d.ts +0 -26
- package/dist/integrations/subagents/history.d.ts.map +0 -1
- package/dist/integrations/subagents/history.js +0 -76
- package/dist/integrations/subagents/history.js.map +0 -1
- package/dist/integrations/subagents/runs.d.ts +0 -72
- package/dist/integrations/subagents/runs.d.ts.map +0 -1
- package/dist/integrations/subagents/runs.js +0 -155
- package/dist/integrations/subagents/runs.js.map +0 -1
- package/dist/tui/run-interruption.d.ts.map +0 -1
- package/dist/tui/run-interruption.js.map +0 -1
- package/dist/tui/run-limits-settings.d.ts +0 -39
- package/dist/tui/run-limits-settings.d.ts.map +0 -1
- package/dist/tui/run-limits-settings.js.map +0 -1
package/dist/tui/App.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { CliPathBuilder } from '../integrations/sessions/paths.js';
|
|
2
|
+
import { resolveTurnGuards } from '../config/turn-guards.js';
|
|
3
|
+
import { TURN_LIMIT_FIELDS, turnLimitCommands } from './turn-limits-settings.js';
|
|
5
4
|
/**
|
|
6
5
|
* TUI root. Composes the banner, transcript, composer, status bar, and
|
|
7
6
|
* the first-run provider picker overlay.
|
|
@@ -17,8 +16,8 @@ import { CliPathBuilder } from '../integrations/sessions/paths.js';
|
|
|
17
16
|
import { discoverProviders } from '../integrations/providers/discover.js';
|
|
18
17
|
import { ProviderSetup } from './ProviderSetup.js';
|
|
19
18
|
import { ToolOutputViewer } from './ToolOutputViewer.js';
|
|
20
|
-
import {
|
|
21
|
-
import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry,
|
|
19
|
+
import { relative } from 'node:path';
|
|
20
|
+
import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
22
21
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
23
22
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
24
23
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
@@ -27,13 +26,13 @@ import { runtimeContextLabel } from '../context/runtime-message.js';
|
|
|
27
26
|
import { writeClipboardText } from '../integrations/clipboard/text.js';
|
|
28
27
|
import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
|
|
29
28
|
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
30
|
-
import {
|
|
29
|
+
import { activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
31
30
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
32
|
-
import { liveOrchestrationRuns } from '../integrations/subagents/runs.js';
|
|
33
31
|
import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
|
|
32
|
+
import { agentsSlashCommand } from '../integrations/subagents/slash.js';
|
|
34
33
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
35
34
|
import { checkUpdates } from '../integrations/updates.js';
|
|
36
|
-
import { renderMemoryReport, renderMemorySaveResult } from '../memory/presentation.js';
|
|
35
|
+
import { renderMemoryReport, renderMemorySaveResult, renderStoredMemorySection, renderTypedNoteResult, } from '../memory/presentation.js';
|
|
37
36
|
import { appendMemoryWithStatus, readMemory } from '../memory/store.js';
|
|
38
37
|
import { effectivePermissionMode, permissionModeDescription, permissionModeLabel, } from '../permissions/mode.js';
|
|
39
38
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
@@ -61,7 +60,7 @@ import { createAgentSession, describeProviderModels, probeAgentSession, } from '
|
|
|
61
60
|
import { choicePickerWindowSize, filterChoiceOptions, moveChoiceSelection, } from './choice-selection.js';
|
|
62
61
|
import { keepRecentRows } from './compact-transcript.js';
|
|
63
62
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
64
|
-
import { assistantTranscriptTexts
|
|
63
|
+
import { assistantTranscriptTexts } from './conversation-history.js';
|
|
65
64
|
import { copyTargetsForResponse } from './copy-targets.js';
|
|
66
65
|
import { editablePrompts } from './edit-prompts.js';
|
|
67
66
|
import { editDraftInExternalEditor } from './external-editor.js';
|
|
@@ -74,11 +73,11 @@ import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
|
74
73
|
import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
75
74
|
import { expandFileMentions, listMentionableFiles } from './mentions.js';
|
|
76
75
|
import { openInBrowser } from './open-browser.js';
|
|
77
|
-
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, } from './permission-review.js';
|
|
78
|
-
import {
|
|
76
|
+
import { buildPermissionReview, buildPermissionSummary, permissionReviewPageRows, permissionReviewRefusal, permissionReviewRows, releasedByApproveAll, } from './permission-review.js';
|
|
77
|
+
import { describeTurnInterruption, describeTurnStop } from './turn-interruption.js';
|
|
79
78
|
import { moveSelection } from './selection-window.js';
|
|
80
79
|
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
81
|
-
import { baseBranchReviewPrompt,
|
|
80
|
+
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
82
81
|
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
83
82
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
84
83
|
import { theme } from './theme.js';
|
|
@@ -246,7 +245,7 @@ function goalRoundPrompt(authority) {
|
|
|
246
245
|
*/
|
|
247
246
|
const LIVE_FURNITURE_ROWS = 10;
|
|
248
247
|
const defaultExternalEditor = ({ seed, cwd, signal }) => editDraftInExternalEditor(seed, { cwd, signal });
|
|
249
|
-
export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultExternalEditor, }) {
|
|
248
|
+
export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultExternalEditor, terminationExit, }) {
|
|
250
249
|
// Bind approval and every operation it admits to one real directory. A
|
|
251
250
|
// lexical cwd may be a writable symlink; resolving it again after the gate
|
|
252
251
|
// would let it be repointed from the approved project to another.
|
|
@@ -264,7 +263,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
264
263
|
const terminal = useWindowSize();
|
|
265
264
|
const hyperlinks = terminalSupportsHyperlinks(process.env, stdout.isTTY === true);
|
|
266
265
|
/**
|
|
267
|
-
* The last assistant message id the
|
|
266
|
+
* The last assistant message id the turn reported, for `/feedback`.
|
|
268
267
|
*
|
|
269
268
|
* A ref rather than state: nothing renders it, and making it state would
|
|
270
269
|
* re-render the transcript on every delta — the exact cost the `pending`
|
|
@@ -276,7 +275,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
276
275
|
*
|
|
277
276
|
* Separate from `lastAssistantMessage`: feedback needs ids as soon as a
|
|
278
277
|
* streamed message exists, while copying must keep the previous finished
|
|
279
|
-
* answer until the current
|
|
278
|
+
* answer until the current turn reaches a normal `done`. A budget stop,
|
|
280
279
|
* cancellation, guardrail or thrown error may leave partial text and must not
|
|
281
280
|
* silently promote it to a completed answer. A resumed session is different:
|
|
282
281
|
* the conversation store predates stop-reason persistence, so its last saved
|
|
@@ -384,7 +383,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
384
383
|
const permissionModeSourceRef = useRef(ctx.skipPermissions === true ? 'launch-bypass' : 'default');
|
|
385
384
|
const [reasoningEffort, setReasoningEffortState] = useState();
|
|
386
385
|
const reasoningEffortRef = useRef(undefined);
|
|
387
|
-
const
|
|
386
|
+
const turnLimitsOverrideRef = useRef({});
|
|
388
387
|
const setReasoningEffort = useCallback((next) => {
|
|
389
388
|
reasoningEffortRef.current = next;
|
|
390
389
|
setReasoningEffortState(next);
|
|
@@ -594,7 +593,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
594
593
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
595
594
|
const [composerHasDraft, setComposerHasDraft] = useState(false);
|
|
596
595
|
const composerDraftTokenRef = useRef(0);
|
|
597
|
-
/** Bounded child
|
|
596
|
+
/** Bounded child session projection published by the current AgentSession. */
|
|
598
597
|
const [subagents, setSubagentsState] = useState([]);
|
|
599
598
|
const subagentsRef = useRef([]);
|
|
600
599
|
/**
|
|
@@ -623,8 +622,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
623
622
|
agentSurfaceCommittedRef.current = agentSurface;
|
|
624
623
|
}, [agentSurface]);
|
|
625
624
|
/**
|
|
626
|
-
* Children of this conversation's earlier
|
|
627
|
-
*
|
|
625
|
+
* Children of this conversation's earlier turns, rebuilt from their own
|
|
626
|
+
* session logs, kept apart from the live monitor's own snapshot.
|
|
628
627
|
*
|
|
629
628
|
* A ref and not state because it is an input to `replaceSubagents`, which
|
|
630
629
|
* runs from the monitor's subscription: holding it as state would make
|
|
@@ -638,11 +637,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
638
637
|
if (saved.length === 0)
|
|
639
638
|
return live;
|
|
640
639
|
// A child the live monitor still holds wins over its own saved copy:
|
|
641
|
-
// both describe one
|
|
642
|
-
// anything. Saved rows land after the live ones, so a cohort that is
|
|
640
|
+
// both describe one child session, and only one of the two is attached
|
|
641
|
+
// to anything. Saved rows land after the live ones, so a cohort that is
|
|
643
642
|
// still running keeps the top of the list.
|
|
644
|
-
const
|
|
645
|
-
return [
|
|
643
|
+
const liveSessionIds = new Set(live.map((agent) => agent.sessionId).filter((id) => id !== undefined));
|
|
644
|
+
return [
|
|
645
|
+
...live,
|
|
646
|
+
...saved.filter((agent) => !agent.sessionId || !liveSessionIds.has(agent.sessionId)),
|
|
647
|
+
];
|
|
646
648
|
}, []);
|
|
647
649
|
const replaceSubagents = useCallback((incoming) => {
|
|
648
650
|
const next = withSavedSubagents(incoming);
|
|
@@ -753,6 +755,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
753
755
|
const reviewChoiceInFlightRef = useRef(null);
|
|
754
756
|
const exitArmedRef = useRef(false);
|
|
755
757
|
const abortRef = useRef(null);
|
|
758
|
+
/**
|
|
759
|
+
* A prompt that holds the turn slot while it waits at the turn boundary,
|
|
760
|
+
* before it has an abort controller. An interrupt or a conversation switch
|
|
761
|
+
* takes the slot back by clearing this; the prompt then drops itself.
|
|
762
|
+
*/
|
|
763
|
+
const admissionRef = useRef(null);
|
|
756
764
|
/** Identity of the turn allowed to notify when it settles. */
|
|
757
765
|
const activeTurnTokenRef = useRef(null);
|
|
758
766
|
/** A tool can reserve a switch; only the owning turn's finalizer may publish it. */
|
|
@@ -1118,81 +1126,87 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1118
1126
|
return id;
|
|
1119
1127
|
}, [nextId]);
|
|
1120
1128
|
/**
|
|
1121
|
-
* `/agents
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
* the
|
|
1126
|
-
* `choicePickerRef.current` against the exact loading-picker
|
|
1127
|
-
* call put there
|
|
1128
|
-
* rather than a counter: Escape and every other picker-replacing path
|
|
1129
|
-
* already route through `setChoicePicker`, so they retire this object as a
|
|
1130
|
-
* side effect and need no separate token bump of their own. That covers
|
|
1131
|
-
* both "the operator left this picker" (any other object is current now,
|
|
1132
|
-
* `null` included) and "opened it again" (a second call's own object is
|
|
1133
|
-
* current instead).
|
|
1129
|
+
* `/agents [running|available|batches]`, answered by the delegation UI's
|
|
1130
|
+
* `agentsSlashCommand` over this session's live monitor and the batches the
|
|
1131
|
+
* session index holds. A `batches` answer opens a picker immediately with a
|
|
1132
|
+
* disabled loading row, then replaces it once the listing resolves; the
|
|
1133
|
+
* composer stays reachable the whole time. A stale read is dropped by
|
|
1134
|
+
* comparing `choicePickerRef.current` against the exact loading-picker
|
|
1135
|
+
* object this call put there.
|
|
1134
1136
|
*/
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
const pending =
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
finished = (await session?.listOrchestrationRuns?.()) ?? [];
|
|
1151
|
-
}
|
|
1152
|
-
catch {
|
|
1153
|
-
// Disk evidence is optional here: a live-only listing is still an
|
|
1154
|
-
// honest answer, and the empty-history message below still applies
|
|
1155
|
-
// if that live half turns out empty too.
|
|
1137
|
+
const runAgentsCommand = useCallback((args, openCockpit, listAvailable) => {
|
|
1138
|
+
const wantsBatches = args.length === 1 && args[0] === 'batches';
|
|
1139
|
+
const pending = wantsBatches
|
|
1140
|
+
? {
|
|
1141
|
+
kind: 'agent-batches',
|
|
1142
|
+
title: 'Batches',
|
|
1143
|
+
values: ['loading'],
|
|
1144
|
+
options: [
|
|
1145
|
+
{
|
|
1146
|
+
label: 'Loading…',
|
|
1147
|
+
description: 'Reading saved batches…',
|
|
1148
|
+
// A disabled row shows its reason in place of its description.
|
|
1149
|
+
disabledReason: 'Reading saved batches…',
|
|
1150
|
+
},
|
|
1151
|
+
],
|
|
1156
1152
|
}
|
|
1157
|
-
|
|
1153
|
+
: undefined;
|
|
1154
|
+
if (pending) {
|
|
1155
|
+
setSelectedChoice(0);
|
|
1156
|
+
setChoicePicker(pending);
|
|
1157
|
+
}
|
|
1158
|
+
void (async () => {
|
|
1159
|
+
const result = await agentsSlashCommand(args, {
|
|
1160
|
+
liveAgents: () => subagentsRef.current,
|
|
1161
|
+
...(session?.listSavedBatches ? { savedBatches: session.listSavedBatches } : {}),
|
|
1162
|
+
});
|
|
1163
|
+
if (appLifetime.signal.aborted)
|
|
1158
1164
|
return;
|
|
1159
|
-
|
|
1160
|
-
const empty = renderAgentRuns(listing);
|
|
1161
|
-
if (empty) {
|
|
1162
|
-
setChoicePicker(null);
|
|
1163
|
-
pushMessage('system', empty);
|
|
1165
|
+
if (pending && choicePickerRef.current !== pending)
|
|
1164
1166
|
return;
|
|
1167
|
+
switch (result.kind) {
|
|
1168
|
+
case 'agent-cockpit':
|
|
1169
|
+
openCockpit();
|
|
1170
|
+
return;
|
|
1171
|
+
case 'available-agents':
|
|
1172
|
+
listAvailable();
|
|
1173
|
+
return;
|
|
1174
|
+
case 'message':
|
|
1175
|
+
if (pending)
|
|
1176
|
+
setChoicePicker(null);
|
|
1177
|
+
pushMessage('system', result.content);
|
|
1178
|
+
return;
|
|
1179
|
+
case 'batches':
|
|
1180
|
+
setChoicePicker({
|
|
1181
|
+
kind: 'agent-batches',
|
|
1182
|
+
title: 'Batches',
|
|
1183
|
+
notice: result.listing.omitted > 0
|
|
1184
|
+
? `${result.listing.omitted} older batch${result.listing.omitted === 1 ? '' : 'es'} not shown.`
|
|
1185
|
+
: undefined,
|
|
1186
|
+
values: result.listing.batches,
|
|
1187
|
+
options: result.listing.batches.map(batchOption),
|
|
1188
|
+
});
|
|
1189
|
+
return;
|
|
1165
1190
|
}
|
|
1166
|
-
setChoicePicker({
|
|
1167
|
-
kind: 'agent-runs',
|
|
1168
|
-
title: 'Orchestration runs',
|
|
1169
|
-
notice: listing.omitted > 0
|
|
1170
|
-
? `${listing.omitted} older run${listing.omitted === 1 ? '' : 's'} not shown.`
|
|
1171
|
-
: undefined,
|
|
1172
|
-
values: listing.runs,
|
|
1173
|
-
options: listing.runs.map(agentRunOption),
|
|
1174
|
-
});
|
|
1175
1191
|
})();
|
|
1176
1192
|
}, [appLifetime, pushMessage, session, setChoicePicker, setSelectedChoice]);
|
|
1177
1193
|
/**
|
|
1178
|
-
* Opens an `/agents
|
|
1194
|
+
* Opens an `/agents batches` selection straight into its first agent's
|
|
1179
1195
|
* transcript — where the replayed banner actually lives, for a finished
|
|
1180
|
-
*
|
|
1196
|
+
* batch — rather than the phase/agent list Ctrl+T lands on.
|
|
1181
1197
|
*
|
|
1182
|
-
* A finished
|
|
1183
|
-
* has not opened the cockpit for yet may not be in
|
|
1184
|
-
* A live
|
|
1185
|
-
* run this process is still writing would risk the very half-written
|
|
1186
|
-
* evidence `listSavedOrchestrationRuns` exists to stay off of.
|
|
1198
|
+
* A finished batch's children are hydrated from their logs first, since a
|
|
1199
|
+
* batch this operator has not opened the cockpit for yet may not be in
|
|
1200
|
+
* `subagents` at all. A live batch skips that: it is already there.
|
|
1187
1201
|
*/
|
|
1188
|
-
const
|
|
1189
|
-
if (!
|
|
1202
|
+
const openBatch = useCallback(async (batch) => {
|
|
1203
|
+
if (!batch.live)
|
|
1190
1204
|
await hydrateSavedChildren();
|
|
1191
|
-
const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId ===
|
|
1205
|
+
const workflow = agentWorkflows(subagentsRef.current).find((candidate) => candidate.agents.some((agent) => agent.workflowId === batch.id));
|
|
1192
1206
|
const firstPhase = workflow?.phases[0];
|
|
1193
1207
|
const firstAgent = firstPhase?.agents[0];
|
|
1194
1208
|
if (!firstPhase || !firstAgent) {
|
|
1195
|
-
pushMessage('system', `Could not open "${
|
|
1209
|
+
pushMessage('system', `Could not open "${batch.name}" — its saved children are no longer available.`);
|
|
1196
1210
|
return;
|
|
1197
1211
|
}
|
|
1198
1212
|
setAgentSurface({
|
|
@@ -1379,31 +1393,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1379
1393
|
}
|
|
1380
1394
|
applyReasoningEffort(next);
|
|
1381
1395
|
}, [applyReasoningEffort, pushMessage, session]);
|
|
1382
|
-
const recordFeedback = useCallback((sessionId,
|
|
1396
|
+
const recordFeedback = useCallback((sessionId, messageIdValue, rating, note) => {
|
|
1383
1397
|
const sessions = sessionsRef.current;
|
|
1384
1398
|
if (!sessions) {
|
|
1385
1399
|
pushMessage('system', 'Could not record feedback: conversation persistence is unavailable.');
|
|
1386
1400
|
return;
|
|
1387
1401
|
}
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
const sessionDir = new CliPathBuilder(sessions.root).sessionDir(sessions.projectId, sessionId);
|
|
1394
|
-
const store = new DiskMessageFeedbackStore({
|
|
1395
|
-
rootDir: join(sessionDir, 'feedback'),
|
|
1396
|
-
runsDir: join(sessionDir, 'runs'),
|
|
1397
|
-
});
|
|
1398
|
-
const runId = runIdValue;
|
|
1402
|
+
// Feedback lives beside the conversation's log (`<session-id>/feedback/`)
|
|
1403
|
+
// and is checked against it: a rating names a message that log holds.
|
|
1404
|
+
// Bound to the exact conversation captured with the streamed message;
|
|
1405
|
+
// `/resume` may change the active scope before a delayed click.
|
|
1406
|
+
const store = new DiskMessageFeedbackStore({ paths: sessions.paths });
|
|
1399
1407
|
const messageId = messageIdValue;
|
|
1400
1408
|
void (async () => {
|
|
1401
1409
|
try {
|
|
1402
1410
|
// A later rating replaces the first. Read-then-write preserves the
|
|
1403
1411
|
// store's owner-version collision check instead of blind overwrite.
|
|
1404
|
-
const current = (await store.listMessageFeedback({
|
|
1412
|
+
const current = (await store.listMessageFeedback({ sessionId })).find((record) => record.messageId === messageId);
|
|
1405
1413
|
const record = await store.putMessageFeedback({
|
|
1406
|
-
|
|
1414
|
+
sessionId,
|
|
1407
1415
|
messageId,
|
|
1408
1416
|
rating,
|
|
1409
1417
|
...(note ? { note } : {}),
|
|
@@ -1857,10 +1865,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1857
1865
|
setChoicePicker({ ...child, back: picker });
|
|
1858
1866
|
return;
|
|
1859
1867
|
}
|
|
1860
|
-
if (picker.kind === 'agent-
|
|
1868
|
+
if (picker.kind === 'agent-batches') {
|
|
1861
1869
|
if (value === 'loading')
|
|
1862
1870
|
return;
|
|
1863
|
-
void
|
|
1871
|
+
void openBatch(value);
|
|
1864
1872
|
return;
|
|
1865
1873
|
}
|
|
1866
1874
|
if (picker.kind === 'archive-conversation') {
|
|
@@ -1905,7 +1913,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1905
1913
|
return;
|
|
1906
1914
|
}
|
|
1907
1915
|
if (picker.kind === 'feedback-rating') {
|
|
1908
|
-
recordFeedback(picker.sessionId, picker.
|
|
1916
|
+
recordFeedback(picker.sessionId, picker.messageId, value);
|
|
1909
1917
|
return;
|
|
1910
1918
|
}
|
|
1911
1919
|
if (picker.kind === 'skill') {
|
|
@@ -1931,7 +1939,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1931
1939
|
archiveCurrentConversation,
|
|
1932
1940
|
ctx.cwd,
|
|
1933
1941
|
enqueueQueued,
|
|
1934
|
-
|
|
1942
|
+
openBatch,
|
|
1935
1943
|
pushMessage,
|
|
1936
1944
|
recordFeedback,
|
|
1937
1945
|
removeStoredCredential,
|
|
@@ -2655,7 +2663,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2655
2663
|
// same reason, as `neverPrompted` below.
|
|
2656
2664
|
availableTools: () => session?.toolNames() ?? [],
|
|
2657
2665
|
// From the session, not re-resolved: resolving builds a provider, and a
|
|
2658
|
-
// second one would describe a different sandbox than the
|
|
2666
|
+
// second one would describe a different sandbox than the turn is using.
|
|
2659
2667
|
sandbox: session?.sandbox ?? null,
|
|
2660
2668
|
mcp: () => session
|
|
2661
2669
|
? (session.mcpStatus?.() ?? {
|
|
@@ -2700,12 +2708,104 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2700
2708
|
sessionId: scopeRef.current?.sessionId,
|
|
2701
2709
|
};
|
|
2702
2710
|
// `/resume`: open the picker with this folder's recent conversations.
|
|
2711
|
+
/**
|
|
2712
|
+
* `/abandon`: close this conversation's paused or interrupted turn without
|
|
2713
|
+
* resuming it (`turn_failed{ abandoned }`), so the next prompt can begin a
|
|
2714
|
+
* turn. A turn running in this process is stopped with esc, not abandoned.
|
|
2715
|
+
*/
|
|
2716
|
+
const abandonActiveTurn = useCallback(async (reason) => {
|
|
2717
|
+
const sessions = sessionsRef.current;
|
|
2718
|
+
const scope = scopeRef.current;
|
|
2719
|
+
if (!sessions || !scope || !conversationMaterializedRef.current) {
|
|
2720
|
+
pushMessage('system', 'Nothing to abandon: this conversation has no recorded turn yet.');
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (abortRef.current || state !== 'idle') {
|
|
2724
|
+
pushMessage('system', 'A turn is running here. Press esc to stop it; /abandon closes a paused or interrupted one.');
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
try {
|
|
2728
|
+
const active = await activeConversationTurn(sessions, scope.sessionId);
|
|
2729
|
+
if (!active) {
|
|
2730
|
+
pushMessage('system', 'This conversation has no paused or interrupted turn to abandon.');
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (!session?.abandonTurn) {
|
|
2734
|
+
pushMessage('system', 'This session cannot abandon turns.');
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
await session.abandonTurn(active.turnId, reason);
|
|
2738
|
+
pushMessage('system', `Abandoned turn ${active.turnId}. The next prompt starts a new turn in this conversation.`);
|
|
2739
|
+
}
|
|
2740
|
+
catch (err) {
|
|
2741
|
+
pushMessage('system', `Could not abandon the turn: ${err instanceof Error ? err.message : String(err)}`);
|
|
2742
|
+
}
|
|
2743
|
+
}, [pushMessage, session, state]);
|
|
2744
|
+
/**
|
|
2745
|
+
* `/resume` in a conversation whose turn is parked: continue that turn —
|
|
2746
|
+
* the same turn id, from its checkpoint — rendering it like any turn.
|
|
2747
|
+
* Returns false when there is no parked turn to continue.
|
|
2748
|
+
*/
|
|
2749
|
+
// `applyEvent` is declared further down; the resume path reads it at call time.
|
|
2750
|
+
const applyEventRef = useRef(null);
|
|
2751
|
+
const resumeActiveTurn = useCallback(async () => {
|
|
2752
|
+
const sessions = sessionsRef.current;
|
|
2753
|
+
const scope = scopeRef.current;
|
|
2754
|
+
if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
|
|
2755
|
+
return false;
|
|
2756
|
+
const active = await activeConversationTurn(sessions, scope.sessionId).catch(() => undefined);
|
|
2757
|
+
if (!active)
|
|
2758
|
+
return false;
|
|
2759
|
+
if (abortRef.current || state !== 'idle') {
|
|
2760
|
+
pushMessage('system', 'Wait for the running turn to finish before resuming the parked one.');
|
|
2761
|
+
return true;
|
|
2762
|
+
}
|
|
2763
|
+
const ac = new AbortController();
|
|
2764
|
+
abortRef.current = ac;
|
|
2765
|
+
setState('thinking');
|
|
2766
|
+
const st = {
|
|
2767
|
+
assistantId: null,
|
|
2768
|
+
text: '',
|
|
2769
|
+
conversationMessages: undefined,
|
|
2770
|
+
pending: '',
|
|
2771
|
+
completed: false,
|
|
2772
|
+
outcome: null,
|
|
2773
|
+
sessionId: scope.sessionId,
|
|
2774
|
+
notification: null,
|
|
2775
|
+
};
|
|
2776
|
+
pushMessage('system', `Resuming turn ${active.turnId} from its checkpoint.`, false, '▶');
|
|
2777
|
+
try {
|
|
2778
|
+
for await (const event of session.resumePaused({
|
|
2779
|
+
turnId: active.turnId,
|
|
2780
|
+
signal: ac.signal,
|
|
2781
|
+
})) {
|
|
2782
|
+
applyEventRef.current?.(event, st);
|
|
2783
|
+
}
|
|
2784
|
+
flushStream(st);
|
|
2785
|
+
if (st.assistantId)
|
|
2786
|
+
finalizeMessage(st.assistantId);
|
|
2787
|
+
// The resumed segment was appended to the log; the fold is the context
|
|
2788
|
+
// the next prompt continues from.
|
|
2789
|
+
modelHistoryRef.current = await loadConversation(sessions, scope.sessionId);
|
|
2790
|
+
}
|
|
2791
|
+
catch (err) {
|
|
2792
|
+
pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
|
|
2793
|
+
}
|
|
2794
|
+
finally {
|
|
2795
|
+
if (abortRef.current === ac)
|
|
2796
|
+
abortRef.current = null;
|
|
2797
|
+
setState('idle');
|
|
2798
|
+
}
|
|
2799
|
+
return true;
|
|
2800
|
+
}, [finalizeMessage, flushStream, pushMessage, session, state]);
|
|
2703
2801
|
const doResume = useCallback(async () => {
|
|
2704
2802
|
const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
|
|
2705
2803
|
if (!sessions) {
|
|
2706
2804
|
pushMessage('system', 'Conversation history is unavailable in this folder.');
|
|
2707
2805
|
return;
|
|
2708
2806
|
}
|
|
2807
|
+
if (await resumeActiveTurn())
|
|
2808
|
+
return;
|
|
2709
2809
|
try {
|
|
2710
2810
|
const recent = await listRecent(sessions);
|
|
2711
2811
|
// Don't offer the active (empty/just-started) conversation.
|
|
@@ -2721,7 +2821,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2721
2821
|
catch (err) {
|
|
2722
2822
|
pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
|
|
2723
2823
|
}
|
|
2724
|
-
}, [ensureSessions, pushMessage]);
|
|
2824
|
+
}, [ensureSessions, pushMessage, resumeActiveTurn]);
|
|
2725
2825
|
// Resolve a pending permission prompt with the user's decision and tear
|
|
2726
2826
|
// down the overlay. No-op if nothing is pending.
|
|
2727
2827
|
const resolvePermission = useCallback((decision) => {
|
|
@@ -2743,8 +2843,17 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2743
2843
|
resolve(decision);
|
|
2744
2844
|
}
|
|
2745
2845
|
if (decision.kind === 'approve-all') {
|
|
2746
|
-
|
|
2747
|
-
|
|
2846
|
+
// A queued sandbox escape or outside path stays queued: "allow
|
|
2847
|
+
// all" was answered on another prompt, and a crossing is approved
|
|
2848
|
+
// only on its own.
|
|
2849
|
+
const kept = [];
|
|
2850
|
+
for (const pending of permissionQueueRef.current.splice(0)) {
|
|
2851
|
+
if (releasedByApproveAll(pending.permission.toolCalls))
|
|
2852
|
+
pending.resolve(decision);
|
|
2853
|
+
else
|
|
2854
|
+
kept.push(pending);
|
|
2855
|
+
}
|
|
2856
|
+
permissionQueueRef.current.push(...kept);
|
|
2748
2857
|
}
|
|
2749
2858
|
const next = permissionQueueRef.current.shift();
|
|
2750
2859
|
setQueuedPermissionCount(permissionQueueRef.current.length);
|
|
@@ -2784,11 +2893,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2784
2893
|
resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
|
|
2785
2894
|
const ac = abortRef.current;
|
|
2786
2895
|
if (!ac) {
|
|
2896
|
+
// A prompt still at the turn boundary: nothing has begun, so taking
|
|
2897
|
+
// the slot back is the whole interrupt. It drops itself when its
|
|
2898
|
+
// await returns.
|
|
2899
|
+
if (admissionRef.current) {
|
|
2900
|
+
admissionRef.current = null;
|
|
2901
|
+
const activeSessionId = scopeRef.current?.sessionId;
|
|
2902
|
+
if (activeSessionId)
|
|
2903
|
+
goalActivation.disarm(activeSessionId);
|
|
2904
|
+
wakeGoalDriver();
|
|
2905
|
+
discardQueued();
|
|
2906
|
+
setState('idle');
|
|
2907
|
+
return true;
|
|
2908
|
+
}
|
|
2787
2909
|
if (cancelledSwitch)
|
|
2788
2910
|
discardQueued();
|
|
2789
2911
|
return cancelledSwitch;
|
|
2790
2912
|
}
|
|
2791
|
-
ac.abort(new
|
|
2913
|
+
ac.abort(new TurnCancelled('user'));
|
|
2792
2914
|
const activeSessionId = scopeRef.current?.sessionId;
|
|
2793
2915
|
if (activeSessionId)
|
|
2794
2916
|
goalActivation.disarm(activeSessionId);
|
|
@@ -2811,6 +2933,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2811
2933
|
resolvePermission,
|
|
2812
2934
|
wakeGoalDriver,
|
|
2813
2935
|
]);
|
|
2936
|
+
// A termination signal leaves like `/exit`, after stopping the turn: the
|
|
2937
|
+
// session still owns tool servers, jobs and the `session_end` hook, and
|
|
2938
|
+
// Ink's unmount is what gives the terminal back.
|
|
2939
|
+
useEffect(() => {
|
|
2940
|
+
if (!terminationExit)
|
|
2941
|
+
return;
|
|
2942
|
+
terminationExit.current = () => {
|
|
2943
|
+
interruptTurn();
|
|
2944
|
+
exitWithSummary();
|
|
2945
|
+
};
|
|
2946
|
+
return () => {
|
|
2947
|
+
terminationExit.current = null;
|
|
2948
|
+
};
|
|
2949
|
+
}, [terminationExit, interruptTurn, exitWithSummary]);
|
|
2814
2950
|
/**
|
|
2815
2951
|
* Load the chosen conversation into the transcript and continue in it.
|
|
2816
2952
|
*
|
|
@@ -2821,12 +2957,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2821
2957
|
* would have run against a conversation nobody asked them of. And, the one
|
|
2822
2958
|
* that outlived the process, its `appendMessages` wrote into the RESUMED
|
|
2823
2959
|
* conversation's durable record, because `sessionId` was mutated on a
|
|
2824
|
-
* `
|
|
2960
|
+
* `SessionScope` the running loop held the very same object of.
|
|
2825
2961
|
*
|
|
2826
2962
|
* The mutation below stays, and it is the sharing that makes it necessary:
|
|
2827
2963
|
* `createAgentSession` closed over this exact object and spreads it into
|
|
2828
2964
|
* every `query()`, so replacing it here would leave the agent attributing
|
|
2829
|
-
* every future turn to the conversation the operator has left. `
|
|
2965
|
+
* every future turn to the conversation the operator has left. `SessionScope`
|
|
2830
2966
|
* says as much — `sessionId` is its one non-`readonly` field. What changed is
|
|
2831
2967
|
* on the other side: a turn now fixes its own destination when it starts
|
|
2832
2968
|
* (see `runTurn`), so a shared cursor moving under it can no longer decide
|
|
@@ -2916,7 +3052,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2916
3052
|
// it runs when the abandoned turn finishes unwinding, which is after
|
|
2917
3053
|
// this line — and a surface that reports a result it has not read is
|
|
2918
3054
|
// the defect class this whole change is about. If that write fails,
|
|
2919
|
-
// the turn says so itself, in the same words `
|
|
3055
|
+
// the turn says so itself, in the same words `exec --json` uses.
|
|
2920
3056
|
pushMessage('system', 'The turn that was running was interrupted. Its reply so far is being saved to the conversation it started in, so it is not in the transcript above. A tool call already dispatched was not undone.');
|
|
2921
3057
|
}
|
|
2922
3058
|
}, [
|
|
@@ -2940,7 +3076,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2940
3076
|
* because its empty successor could not be published. Once it exists, moving
|
|
2941
3077
|
* the shared scope and advancing the generation are one synchronous boundary:
|
|
2942
3078
|
* old events can still unwind and persist to their captured destination, but
|
|
2943
|
-
* they cannot render into or start an SDK
|
|
3079
|
+
* they cannot render into or start an SDK turn under the new conversation.
|
|
2944
3080
|
*
|
|
2945
3081
|
* A process whose initial persistence setup failed has no shared scope to
|
|
2946
3082
|
* move. It still gets an honest in-memory context reset; the notice names that
|
|
@@ -2978,7 +3114,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2978
3114
|
if (sourceScope && targetSessionId) {
|
|
2979
3115
|
sourceScope.sessionId = targetSessionId;
|
|
2980
3116
|
conversationMaterializedRef.current = true;
|
|
2981
|
-
// A fresh conversation owns no
|
|
3117
|
+
// A fresh conversation owns no turns yet, so this normally reads
|
|
2982
3118
|
// nothing. It runs anyway because the rule is "the scope changed,
|
|
2983
3119
|
// re-read", and an exception here is how the resume path lost
|
|
2984
3120
|
// its own re-read.
|
|
@@ -3038,7 +3174,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3038
3174
|
}
|
|
3039
3175
|
try {
|
|
3040
3176
|
if (!clear && title === '') {
|
|
3041
|
-
const current = titleOf(sessions, scope.sessionId);
|
|
3177
|
+
const current = await titleOf(sessions, scope.sessionId);
|
|
3042
3178
|
textPromptTokenRef.current += 1;
|
|
3043
3179
|
setTextPrompt({
|
|
3044
3180
|
token: textPromptTokenRef.current,
|
|
@@ -3051,7 +3187,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3051
3187
|
});
|
|
3052
3188
|
return;
|
|
3053
3189
|
}
|
|
3054
|
-
setTitle(sessions, scope.sessionId, clear ? '' : title);
|
|
3190
|
+
await setTitle(sessions, scope.sessionId, clear ? '' : title);
|
|
3055
3191
|
pushMessage('system', clear
|
|
3056
3192
|
? 'Name removed. /resume will list this conversation by its opening message again.'
|
|
3057
3193
|
: `Named "${title}".`);
|
|
@@ -3083,8 +3219,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3083
3219
|
pushMessage('system', 'The conversation changed while this editor was open. Reopen the command in the current conversation.');
|
|
3084
3220
|
return;
|
|
3085
3221
|
}
|
|
3086
|
-
if (prompt.kind === '
|
|
3087
|
-
const field =
|
|
3222
|
+
if (prompt.kind === 'turn-limit') {
|
|
3223
|
+
const field = TURN_LIMIT_FIELDS.find((field) => field.key === prompt.limitKey);
|
|
3088
3224
|
if (field)
|
|
3089
3225
|
commandPickerSubmitRef.current(`/config limits ${field.name} ${value.trim()}`);
|
|
3090
3226
|
return;
|
|
@@ -3103,13 +3239,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3103
3239
|
pushMessage('system', 'Conversation history is unavailable in this folder.');
|
|
3104
3240
|
return;
|
|
3105
3241
|
}
|
|
3106
|
-
|
|
3107
|
-
setTitle(sessions, prompt.sessionId, value);
|
|
3108
|
-
pushMessage('system', `Named "${value.trim()}".`);
|
|
3109
|
-
}
|
|
3110
|
-
catch (error) {
|
|
3111
|
-
pushMessage('system', `Could not save the name: ${error instanceof Error ? error.message : String(error)}`);
|
|
3112
|
-
}
|
|
3242
|
+
void setTitle(sessions, prompt.sessionId, value).then(() => pushMessage('system', `Named "${value.trim()}".`), (error) => pushMessage('system', `Could not save the name: ${error instanceof Error ? error.message : String(error)}`));
|
|
3113
3243
|
}, [pushMessage, runConversationExport, setTextPrompt]);
|
|
3114
3244
|
const cancelTextPrompt = useCallback(() => {
|
|
3115
3245
|
const prompt = textPromptRef.current;
|
|
@@ -3431,9 +3561,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3431
3561
|
st.sourceTextPartId = event.textPart?.id;
|
|
3432
3562
|
}
|
|
3433
3563
|
setState('thinking');
|
|
3434
|
-
if (event.messageId && event.
|
|
3564
|
+
if (event.messageId && event.turnId && st.sessionId) {
|
|
3435
3565
|
lastAssistantMessage.current = {
|
|
3436
|
-
runId: event.runId,
|
|
3437
3566
|
messageId: event.messageId,
|
|
3438
3567
|
sessionId: st.sessionId,
|
|
3439
3568
|
};
|
|
@@ -3476,12 +3605,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3476
3605
|
setState('tool');
|
|
3477
3606
|
const waitingAgent = event.toolName === 'wait_for_task' && event.taskId
|
|
3478
3607
|
? subagentsRef.current.find((agent) => agent.taskId === event.taskId &&
|
|
3479
|
-
(event.
|
|
3608
|
+
(event.turnId === undefined || agent.workflowId === event.turnId))
|
|
3480
3609
|
: undefined;
|
|
3481
3610
|
const tool = {
|
|
3482
3611
|
...(waitingAgent ? { taskId: waitingAgent.taskId } : {}),
|
|
3483
3612
|
id: event.toolUseId,
|
|
3484
|
-
...(event.
|
|
3613
|
+
...(event.turnId ? { turnId: event.turnId } : {}),
|
|
3485
3614
|
toolName: event.toolName,
|
|
3486
3615
|
activity: event.activity,
|
|
3487
3616
|
label: waitingAgent
|
|
@@ -3497,7 +3626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3497
3626
|
case 'tool-progress': {
|
|
3498
3627
|
const running = activeToolsRef.current;
|
|
3499
3628
|
const index = running.findIndex((tool) => tool.id === event.toolUseId &&
|
|
3500
|
-
(event.
|
|
3629
|
+
(event.turnId === undefined || tool.turnId === event.turnId));
|
|
3501
3630
|
// A terminal event may already have removed this call. Never recreate
|
|
3502
3631
|
// a live row from late diagnostic state.
|
|
3503
3632
|
if (index < 0)
|
|
@@ -3524,7 +3653,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3524
3653
|
// result to the wrong call. If no id matches, render the
|
|
3525
3654
|
// completion on its own (label from the end event itself).
|
|
3526
3655
|
const i = running.findIndex((tool) => tool.id === event.toolUseId &&
|
|
3527
|
-
(event.
|
|
3656
|
+
(event.turnId === undefined || tool.turnId === event.turnId));
|
|
3528
3657
|
const done = i >= 0 ? running[i] : undefined;
|
|
3529
3658
|
if (i >= 0) {
|
|
3530
3659
|
activeToolsRef.current = [...running.slice(0, i), ...running.slice(i + 1)];
|
|
@@ -3533,7 +3662,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3533
3662
|
// Lifecycle updates own completion rows, even when the model never waits.
|
|
3534
3663
|
// Keep unknown waits and errors visible; never hide an unrepresented task.
|
|
3535
3664
|
const inspectableWait = done?.taskId && subagentsRef.current.some((agent) => agent.taskId === done.taskId &&
|
|
3536
|
-
(event.
|
|
3665
|
+
(event.turnId === undefined || agent.workflowId === event.turnId) &&
|
|
3537
3666
|
agent.transcript.some((row) => row.kind === 'assistant' && row.text.trim().length > 0));
|
|
3538
3667
|
if (!event.isError && inspectableWait && event.toolName === 'wait_for_task') {
|
|
3539
3668
|
setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
|
|
@@ -3542,7 +3671,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3542
3671
|
const representedSuccessfulAgent = !event.isError &&
|
|
3543
3672
|
event.toolName.toLowerCase() === 'agent' &&
|
|
3544
3673
|
subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
|
|
3545
|
-
(event.
|
|
3674
|
+
(event.turnId === undefined || agent.workflowId === event.turnId));
|
|
3546
3675
|
// The band above the rail already carries a narrated line, in the
|
|
3547
3676
|
// parent's own words, from the moment it is written. Printing the
|
|
3548
3677
|
// call that wrote it would show the same sentence twice, the
|
|
@@ -3697,7 +3826,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3697
3826
|
// commentary deltas. The transcript keeps each visible part above.
|
|
3698
3827
|
if (event.text !== undefined)
|
|
3699
3828
|
st.text = event.text;
|
|
3700
|
-
// `
|
|
3829
|
+
// `turn_completed` is not synonymous with success: budgets,
|
|
3701
3830
|
// cancellation and output guardrails arrive through this event too.
|
|
3702
3831
|
// Missing remains a normal end for older producers, matching the
|
|
3703
3832
|
// headless command's compatibility rule.
|
|
@@ -3709,7 +3838,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3709
3838
|
outcome: st.completed ? 'completed' : 'stopped',
|
|
3710
3839
|
};
|
|
3711
3840
|
closeAssistant();
|
|
3712
|
-
const stopNotice =
|
|
3841
|
+
const stopNotice = describeTurnStop(event.stopReason, event.budget);
|
|
3713
3842
|
if (stopNotice)
|
|
3714
3843
|
pushMessage('system', stopNotice, false, '■');
|
|
3715
3844
|
break;
|
|
@@ -3723,20 +3852,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3723
3852
|
st.outcome = 'stopped';
|
|
3724
3853
|
st.queuePauseOutcome = 'paused';
|
|
3725
3854
|
st.notification = { kind: 'turn-settled', outcome: 'stopped' };
|
|
3726
|
-
pushMessage('system',
|
|
3855
|
+
pushMessage('system', describeTurnInterruption(event), false, '⏸');
|
|
3727
3856
|
break;
|
|
3728
3857
|
case 'error':
|
|
3729
3858
|
closeAssistant();
|
|
3730
3859
|
st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
|
|
3731
3860
|
if (event.message !== 'aborted') {
|
|
3732
3861
|
st.notification = { kind: 'turn-settled', outcome: 'failed' };
|
|
3733
|
-
pushMessage('system',
|
|
3862
|
+
pushMessage('system', describeTurnInterruption(event), false, '!');
|
|
3734
3863
|
}
|
|
3735
3864
|
else
|
|
3736
3865
|
st.notification = null;
|
|
3737
3866
|
break;
|
|
3738
3867
|
}
|
|
3739
3868
|
}, [appendToMessage, finalizeMessage, flushStream, pushMessage]);
|
|
3869
|
+
applyEventRef.current = applyEvent;
|
|
3740
3870
|
const runTurn = useCallback(async (prompt) => {
|
|
3741
3871
|
// A passive queue effect can still hold the previous render's session
|
|
3742
3872
|
// while a replacement is being committed. Keep FIFO ownership until
|
|
@@ -3764,6 +3894,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3764
3894
|
// state idle while the admission read was pending let a second submit
|
|
3765
3895
|
// start beside it and broke the queue's FIFO ownership.
|
|
3766
3896
|
setState('thinking');
|
|
3897
|
+
// Both awaits below can span an interrupt, a `/clear` or a `/resume`,
|
|
3898
|
+
// and neither has an abort controller yet for them to cancel. The
|
|
3899
|
+
// token is what they take back instead.
|
|
3900
|
+
const admission = {};
|
|
3901
|
+
admissionRef.current = admission;
|
|
3767
3902
|
let durableScope;
|
|
3768
3903
|
try {
|
|
3769
3904
|
durableScope = await materializeConversation();
|
|
@@ -3786,6 +3921,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3786
3921
|
await requireWritableConversation(turnSessions, destination, 'start conversation turn');
|
|
3787
3922
|
}
|
|
3788
3923
|
catch (err) {
|
|
3924
|
+
// Taken back meanwhile: the slot is no longer this prompt's.
|
|
3925
|
+
if (admissionRef.current !== admission)
|
|
3926
|
+
return;
|
|
3927
|
+
admissionRef.current = null;
|
|
3789
3928
|
if (prompt.kind === 'goal' && prompt.goalRound) {
|
|
3790
3929
|
goalActivation.disarm(prompt.goalRound.sessionId, prompt.goalRound);
|
|
3791
3930
|
wakeGoalDriver();
|
|
@@ -3795,6 +3934,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3795
3934
|
return;
|
|
3796
3935
|
}
|
|
3797
3936
|
}
|
|
3937
|
+
// Interrupted, or the conversation changed, while this prompt waited at
|
|
3938
|
+
// the boundary. Whoever took the slot back already freed it and said
|
|
3939
|
+
// so, and a turn of the new conversation may hold it now; this prompt
|
|
3940
|
+
// is dropped rather than begun under that conversation.
|
|
3941
|
+
if (admissionRef.current !== admission)
|
|
3942
|
+
return;
|
|
3943
|
+
admissionRef.current = null;
|
|
3798
3944
|
// `@path` mentions: the visible human message keeps the readable token,
|
|
3799
3945
|
// but the model receives the file contents inlined. An automatic goal
|
|
3800
3946
|
// prompt is already host-authored context and is never reinterpreted as
|
|
@@ -3815,7 +3961,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3815
3961
|
}
|
|
3816
3962
|
: undefined);
|
|
3817
3963
|
const priorForSdk = [...historyBeforeTurn, userMessage];
|
|
3818
|
-
|
|
3964
|
+
// Reserved before the turn begins, so everything that refers to this
|
|
3965
|
+
// turn — its goal-round authority above all — names it from the start.
|
|
3966
|
+
const turnId = generateTurnId();
|
|
3819
3967
|
if (goalRound) {
|
|
3820
3968
|
pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
|
|
3821
3969
|
}
|
|
@@ -4010,7 +4158,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4010
4158
|
const turnPermissionMode = permissionModeRef.current;
|
|
4011
4159
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
4012
4160
|
const turnOrchestrateMode = orchestrateModeRef.current;
|
|
4013
|
-
const
|
|
4161
|
+
const turnLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
|
|
4014
4162
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
4015
4163
|
// calling it in makeResumeHandler; retaining it is what lets a session
|
|
4016
4164
|
// launched with --yolo later return to prompt mode truthfully.
|
|
@@ -4023,41 +4171,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4023
4171
|
}
|
|
4024
4172
|
return onPermission(req);
|
|
4025
4173
|
};
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
sessionId: destination,
|
|
4032
|
-
runId,
|
|
4033
|
-
displayText: text,
|
|
4034
|
-
user: userMessage,
|
|
4035
|
-
});
|
|
4036
|
-
evidenceReady = true;
|
|
4037
|
-
}
|
|
4038
|
-
catch (err) {
|
|
4039
|
-
if (goalRound) {
|
|
4040
|
-
goalActivation.disarm(goalRound.sessionId, goalRound);
|
|
4041
|
-
wakeGoalDriver();
|
|
4042
|
-
pushMessage('system', `Goal round ${goalRound.round} was not started because its durable evidence could not be recorded: ${err instanceof Error ? err.message : String(err)}. Automatic continuation is disarmed; /goal resume retries explicitly.`);
|
|
4043
|
-
}
|
|
4044
|
-
else {
|
|
4045
|
-
pushMessage('system', `Could not record durable evidence for this turn before run ${runId}: ${err instanceof Error ? err.message : String(err)}. The turn will continue, but a complete export of conversation ${destination} will refuse rather than omit it.`);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
else if (goalRound) {
|
|
4174
|
+
// A goal round is a turn of a durable conversation: its round is recorded
|
|
4175
|
+
// on the turn (`origin.kind: 'goal-round'`), and a conversation that is
|
|
4176
|
+
// not in a log has nowhere to record it.
|
|
4177
|
+
const admitted = goalRound === undefined || Boolean(turnSessions && destination);
|
|
4178
|
+
if (!admitted && goalRound) {
|
|
4050
4179
|
goalActivation.disarm(goalRound.sessionId, goalRound);
|
|
4051
4180
|
wakeGoalDriver();
|
|
4052
|
-
pushMessage('system', `Goal round ${goalRound.round} was not started because
|
|
4181
|
+
pushMessage('system', `Goal round ${goalRound.round} was not started because this conversation is not durable. Automatic continuation is disarmed; /goal resume retries explicitly.`);
|
|
4053
4182
|
}
|
|
4054
4183
|
try {
|
|
4055
|
-
//
|
|
4056
|
-
//
|
|
4057
|
-
//
|
|
4058
|
-
//
|
|
4059
|
-
//
|
|
4060
|
-
if (
|
|
4184
|
+
// Setup above awaited. A conversation switch can happen meanwhile and
|
|
4185
|
+
// move the mutable SessionScope captured by `createAgentSession`. Re-admit
|
|
4186
|
+
// the turn here, immediately before the generator exists, or an
|
|
4187
|
+
// abandoned prompt can begin its reserved turn under the new
|
|
4188
|
+
// conversation.
|
|
4189
|
+
if (admitted &&
|
|
4061
4190
|
!ac.signal.aborted &&
|
|
4062
4191
|
stillHere() &&
|
|
4063
4192
|
activeTurnTokenRef.current === turnToken &&
|
|
@@ -4066,9 +4195,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4066
4195
|
goalActivation.isArmed(goalRound.sessionId, goalRound)))) {
|
|
4067
4196
|
for await (const event of session.send(priorForSdk, {
|
|
4068
4197
|
signal: ac.signal,
|
|
4069
|
-
|
|
4198
|
+
turnId,
|
|
4199
|
+
origin: { protocol: 'cli', kind: goalRound ? 'goal-round' : 'prompt' },
|
|
4200
|
+
// The TUI never resumes a turn whose process is gone; the next
|
|
4201
|
+
// prompt closes it. A paused turn is never closed this way.
|
|
4202
|
+
abandonInterrupted: true,
|
|
4070
4203
|
permissionMode: turnPermissionMode,
|
|
4071
|
-
limits:
|
|
4204
|
+
limits: turnLimits,
|
|
4072
4205
|
...(turnReasoningEffort !== undefined ? { effort: turnReasoningEffort } : {}),
|
|
4073
4206
|
...(turnOrchestrateMode ? { orchestrate: true } : {}),
|
|
4074
4207
|
...(goalRound ? { goalRound } : {}),
|
|
@@ -4143,16 +4276,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4143
4276
|
// Prefer the kernel's settled conversation projection: it retains opaque
|
|
4144
4277
|
// reasoning, citations and complete tool turns that the visible delta
|
|
4145
4278
|
// stream cannot reconstruct. A fake/legacy session that does not publish
|
|
4146
|
-
// one keeps the old text-only shape. The
|
|
4147
|
-
// the
|
|
4148
|
-
//
|
|
4149
|
-
const fallbackTurn =
|
|
4279
|
+
// one keeps the old text-only shape. The durable record is the session
|
|
4280
|
+
// log, which the kernel appended to as the turn ran; this is only the
|
|
4281
|
+
// in-memory context the next send starts from.
|
|
4282
|
+
const fallbackTurn = admitted ? [userMessage] : [];
|
|
4150
4283
|
if (st.text.trim().length > 0)
|
|
4151
4284
|
fallbackTurn.push(createAssistantMessage(st.text));
|
|
4152
|
-
const conversationMessages =
|
|
4153
|
-
const publication = conversationMessages
|
|
4154
|
-
? planTurnPublication(historyBeforeTurn, userMessage, conversationMessages)
|
|
4155
|
-
: { kind: 'append', messages: fallbackTurn };
|
|
4285
|
+
const conversationMessages = admitted ? st.conversationMessages : undefined;
|
|
4156
4286
|
const nextModelHistory = conversationMessages ?? [...historyBeforeTurn, ...fallbackTurn];
|
|
4157
4287
|
if (goalRound && (ac.signal.aborted || st.outcome !== 'completed')) {
|
|
4158
4288
|
goalActivation.disarm(goalRound.sessionId, goalRound);
|
|
@@ -4207,61 +4337,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4207
4337
|
sendTerminalNotification(st.notification);
|
|
4208
4338
|
}
|
|
4209
4339
|
}
|
|
4210
|
-
//
|
|
4211
|
-
//
|
|
4212
|
-
//
|
|
4213
|
-
// Best-effort is about not FAILING, not about staying quiet. The
|
|
4214
|
-
// rejection used to be swallowed whole, and it is the one failure here
|
|
4215
|
-
// that makes a LATER surface wrong: `/resume` comes back missing a
|
|
4216
|
-
// turn that was on screen, and the next turn in that conversation
|
|
4217
|
-
// silently lacks it as context, with nothing connecting either to a
|
|
4218
|
-
// write that failed minutes ago. `run-stream` already says this, in
|
|
4219
|
-
// these words and for this reason.
|
|
4220
|
-
//
|
|
4221
|
-
// Said wherever the operator is now, even when that is a different
|
|
4222
|
-
// conversation, because there is no other channel and it is news they
|
|
4223
|
-
// need. Naming the conversation is what keeps it from reading as a
|
|
4224
|
-
// fault of the one in front of them.
|
|
4225
|
-
if (turnSessions &&
|
|
4226
|
-
destination &&
|
|
4227
|
-
(publication.kind === 'replace' || publication.messages.length > 0 || evidenceTurn)) {
|
|
4228
|
-
persistenceTailRef.current = persistenceTailRef.current.then(async () => {
|
|
4229
|
-
if (evidenceTurn && turnSessions.turnEvidence) {
|
|
4230
|
-
try {
|
|
4231
|
-
await turnSessions.turnEvidence.recordTurnSettled({
|
|
4232
|
-
sessionId: destination,
|
|
4233
|
-
turnId: evidenceTurn.turnId,
|
|
4234
|
-
runId,
|
|
4235
|
-
outcome: ac.signal.aborted
|
|
4236
|
-
? 'cancelled'
|
|
4237
|
-
: (st.outcome ?? (st.completed ? 'completed' : 'stopped')),
|
|
4238
|
-
assistantText: st.text,
|
|
4239
|
-
});
|
|
4240
|
-
}
|
|
4241
|
-
catch (err) {
|
|
4242
|
-
pushMessage('system', `Could not finish the durable evidence for turn ${evidenceTurn.turnId}: ${err instanceof Error ? err.message : String(err)}. A complete export of conversation ${destination} will refuse if the SDK run record cannot prove the missing text.`);
|
|
4243
|
-
}
|
|
4244
|
-
}
|
|
4245
|
-
try {
|
|
4246
|
-
if (publication.kind === 'replace') {
|
|
4247
|
-
await replaceConversation(turnSessions, destination, publication.messages);
|
|
4248
|
-
}
|
|
4249
|
-
else if (publication.messages.length > 0) {
|
|
4250
|
-
await appendMessages(turnSessions, destination, publication.messages);
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
catch (err) {
|
|
4254
|
-
if (goalRound) {
|
|
4255
|
-
goalActivation.disarm(goalRound.sessionId, goalRound);
|
|
4256
|
-
wakeGoalDriver();
|
|
4257
|
-
}
|
|
4258
|
-
pushMessage('system', `A turn was not saved to conversation ${destination}: ${err instanceof Error ? err.message : String(err)}. Its durable history will not include it. It remains only in this process's in-memory context if that conversation is still open; resuming or restarting will lose it.`);
|
|
4259
|
-
}
|
|
4260
|
-
});
|
|
4261
|
-
}
|
|
4262
|
-
// Removed only AFTER the write has been attached to the tail. A history
|
|
4263
|
-
// operation that sees no current-generation entries can now await that
|
|
4264
|
-
// tail without a turn appearing behind its read later.
|
|
4340
|
+
// The kernel recorded the turn in the session log as it ran; nothing is
|
|
4341
|
+
// left to publish once the loop has unwound.
|
|
4265
4342
|
unsettledTurnGenerationsRef.current.delete(turnToken);
|
|
4266
4343
|
const pending = pendingModelSwitchRef.current;
|
|
4267
4344
|
if (pending?.turnToken === turnToken && pending.selection) {
|
|
@@ -4445,6 +4522,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4445
4522
|
hydrateSession,
|
|
4446
4523
|
wakeGoalDriver,
|
|
4447
4524
|
]);
|
|
4525
|
+
// `#note` and `/memory add`: a typed memory file in the session's stored
|
|
4526
|
+
// memory, default type `project`. A session without a store — no provider
|
|
4527
|
+
// yet — appends to the curated project file the way every note used to.
|
|
4528
|
+
const rememberProjectNote = useCallback(async (note, type) => {
|
|
4529
|
+
try {
|
|
4530
|
+
if (session?.rememberNote) {
|
|
4531
|
+
pushMessage('system', renderTypedNoteResult(await session.rememberNote(note, type), note));
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
|
|
4535
|
+
pushMessage('system', renderMemorySaveResult(saved, note));
|
|
4536
|
+
}
|
|
4537
|
+
catch (err) {
|
|
4538
|
+
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
4539
|
+
}
|
|
4540
|
+
}, [ctx.cwd, pushMessage, session]);
|
|
4448
4541
|
const handleSubmit = useCallback((value, attachments, mode = 'submit') => {
|
|
4449
4542
|
if (goalCommandInFlightRef.current) {
|
|
4450
4543
|
pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
|
|
@@ -4478,13 +4571,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4478
4571
|
// and both leave a row the model reads on its next turn.
|
|
4479
4572
|
if (value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
4480
4573
|
const note = value.slice(1).trim();
|
|
4481
|
-
|
|
4482
|
-
const saved = appendMemoryWithStatus(note, { scope: 'project', cwd: ctx.cwd });
|
|
4483
|
-
pushMessage('system', renderMemorySaveResult(saved, note));
|
|
4484
|
-
}
|
|
4485
|
-
catch (err) {
|
|
4486
|
-
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
4487
|
-
}
|
|
4574
|
+
void rememberProjectNote(note);
|
|
4488
4575
|
return;
|
|
4489
4576
|
}
|
|
4490
4577
|
const escaped = shellEscapeCommand(value);
|
|
@@ -4550,21 +4637,23 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4550
4637
|
});
|
|
4551
4638
|
return;
|
|
4552
4639
|
}
|
|
4553
|
-
case '
|
|
4554
|
-
// Reported after the read, not before it: the answer to "are
|
|
4555
|
-
// there any" may be on disk, and a conversation whose evidence
|
|
4556
|
-
// is sitting in its own directory must never be told it
|
|
4557
|
-
// delegated nothing.
|
|
4640
|
+
case 'agents': {
|
|
4558
4641
|
const agentIds = session?.agentIds ?? [];
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4642
|
+
runAgentsCommand(slash.args, () => {
|
|
4643
|
+
// Reported after the read, not before it: the answer to "are
|
|
4644
|
+
// there any" may be in the session's logs, and a conversation
|
|
4645
|
+
// whose children are recorded there must never be told it
|
|
4646
|
+
// delegated nothing.
|
|
4647
|
+
void openAgentCockpit().then((opened) => {
|
|
4648
|
+
if (opened)
|
|
4649
|
+
return;
|
|
4650
|
+
pushMessage('system', `No delegated agents in this conversation.${agentIds.length ? `\nAvailable: ${agentIds.join(', ')}` : '\nNo agents are configured.'}`);
|
|
4651
|
+
});
|
|
4652
|
+
}, () => pushMessage('system', renderAgents(agentIds)));
|
|
4564
4653
|
return;
|
|
4565
4654
|
}
|
|
4566
|
-
case '
|
|
4567
|
-
|
|
4655
|
+
case 'abandon': {
|
|
4656
|
+
void abandonActiveTurn(slash.reason);
|
|
4568
4657
|
return;
|
|
4569
4658
|
}
|
|
4570
4659
|
case 'provider-setup':
|
|
@@ -4574,21 +4663,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4574
4663
|
}
|
|
4575
4664
|
setProviderSetup(true);
|
|
4576
4665
|
return;
|
|
4577
|
-
case '
|
|
4578
|
-
case '
|
|
4579
|
-
case '
|
|
4580
|
-
const currentLimits =
|
|
4581
|
-
if (slash.kind === '
|
|
4666
|
+
case 'turn-limits-set':
|
|
4667
|
+
case 'turn-limit-editor':
|
|
4668
|
+
case 'turn-limits-picker': {
|
|
4669
|
+
const currentLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
|
|
4670
|
+
if (slash.kind === 'turn-limit-editor') {
|
|
4582
4671
|
const scope = scopeRef.current;
|
|
4583
4672
|
if (!scope) {
|
|
4584
|
-
pushMessage('system', '
|
|
4673
|
+
pushMessage('system', 'Turn limits are available after the session starts.');
|
|
4585
4674
|
return;
|
|
4586
4675
|
}
|
|
4587
|
-
const field =
|
|
4676
|
+
const field = TURN_LIMIT_FIELDS.find((field) => field.key === slash.key);
|
|
4588
4677
|
textPromptTokenRef.current += 1;
|
|
4589
4678
|
setTextPrompt({
|
|
4590
4679
|
token: textPromptTokenRef.current,
|
|
4591
|
-
kind: '
|
|
4680
|
+
kind: 'turn-limit',
|
|
4592
4681
|
limitKey: slash.key,
|
|
4593
4682
|
title: `${field.label} (${slash.key === 'timeoutMs' ? 'ms; ' : ''}0 = unlimited)`,
|
|
4594
4683
|
placeholder: field.hint,
|
|
@@ -4598,15 +4687,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4598
4687
|
});
|
|
4599
4688
|
return;
|
|
4600
4689
|
}
|
|
4601
|
-
if (slash.kind === '
|
|
4602
|
-
|
|
4690
|
+
if (slash.kind === 'turn-limits-set') {
|
|
4691
|
+
turnLimitsOverrideRef.current = { ...turnLimitsOverrideRef.current, ...slash.limits };
|
|
4603
4692
|
}
|
|
4604
|
-
const commands =
|
|
4693
|
+
const commands = turnLimitCommands(resolveTurnGuards(currentLimits, turnLimitsOverrideRef.current));
|
|
4605
4694
|
setSelectedChoice(0);
|
|
4606
4695
|
setChoicePicker({
|
|
4607
4696
|
kind: 'command',
|
|
4608
|
-
title: '
|
|
4609
|
-
notice: `${slash.kind === '
|
|
4697
|
+
title: 'Turn limits',
|
|
4698
|
+
notice: `${slash.kind === 'turn-limits-set' ? 'Updated. ' : ''}This session · new turns and their agents. Existing turns keep their limits.`,
|
|
4610
4699
|
values: commands,
|
|
4611
4700
|
options: commands.map((command) => ({
|
|
4612
4701
|
label: command.label,
|
|
@@ -4638,8 +4727,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4638
4727
|
},
|
|
4639
4728
|
{
|
|
4640
4729
|
name: 'config limits',
|
|
4641
|
-
label: '
|
|
4642
|
-
description:
|
|
4730
|
+
label: 'Turn limits',
|
|
4731
|
+
description: turnLimitCommands(resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current))
|
|
4643
4732
|
.slice(0, 3)
|
|
4644
4733
|
.map((field) => `${field.label}: ${field.description}`)
|
|
4645
4734
|
.join(' · '),
|
|
@@ -4660,7 +4749,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4660
4749
|
setChoicePicker({
|
|
4661
4750
|
kind: 'command',
|
|
4662
4751
|
title: 'Configuration',
|
|
4663
|
-
notice: 'Model, reasoning, permission and
|
|
4752
|
+
notice: 'Model, reasoning, permission and turn-limit changes apply to this session.',
|
|
4664
4753
|
values: commands,
|
|
4665
4754
|
options: commands.map((command) => ({
|
|
4666
4755
|
label: command.label,
|
|
@@ -4940,6 +5029,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4940
5029
|
return;
|
|
4941
5030
|
}
|
|
4942
5031
|
case 'remember':
|
|
5032
|
+
if (slash.scope === 'project') {
|
|
5033
|
+
void rememberProjectNote(slash.text, slash.memoryType);
|
|
5034
|
+
return;
|
|
5035
|
+
}
|
|
4943
5036
|
try {
|
|
4944
5037
|
const saved = appendMemoryWithStatus(slash.text, { scope: slash.scope, cwd: ctx.cwd });
|
|
4945
5038
|
pushMessage('system', renderMemorySaveResult(saved, slash.text));
|
|
@@ -4948,14 +5041,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4948
5041
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
4949
5042
|
}
|
|
4950
5043
|
return;
|
|
5044
|
+
case 'import-notes': {
|
|
5045
|
+
if (!session?.importCuratedNotes) {
|
|
5046
|
+
pushMessage('system', 'Stored memory is not open yet; /memory import-notes works once a session has started.');
|
|
5047
|
+
return;
|
|
5048
|
+
}
|
|
5049
|
+
void session.importCuratedNotes().then((report) => pushMessage('system', report), (err) => pushMessage('system', `Notes were not copied: ${err instanceof Error ? err.message : String(err)}`));
|
|
5050
|
+
return;
|
|
5051
|
+
}
|
|
4951
5052
|
case 'show-memory': {
|
|
4952
|
-
|
|
5053
|
+
const curated = renderMemoryReport(readMemory(undefined, ctx.cwd), { cwd: ctx.cwd });
|
|
5054
|
+
const stored = session?.storedMemoryIndex;
|
|
5055
|
+
if (!stored) {
|
|
5056
|
+
pushMessage('system', curated);
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
void stored().then(({ directory, index, derived }) => {
|
|
5060
|
+
const section = renderStoredMemorySection(directory, index, derived);
|
|
5061
|
+
pushMessage('system', section
|
|
5062
|
+
? curated.startsWith('No saved memory.')
|
|
5063
|
+
? section
|
|
5064
|
+
: `${section}\n\n${curated}`
|
|
5065
|
+
: curated);
|
|
5066
|
+
}, (err) => pushMessage('system', `${curated}\n\nStored memories not loaded: ${err instanceof Error ? err.message : String(err)}`));
|
|
4953
5067
|
return;
|
|
4954
5068
|
}
|
|
4955
5069
|
case 'list-skills': {
|
|
4956
5070
|
// The session's directory, not the process's — the same
|
|
4957
5071
|
// distinction the headless commands get from `--cwd`. They are
|
|
4958
|
-
// the same value today, and were the same value in `
|
|
5072
|
+
// the same value today, and were the same value in `exec --json`
|
|
4959
5073
|
// too until they were not.
|
|
4960
5074
|
const skills = discoverSkills({ cwd: ctx.cwd });
|
|
4961
5075
|
if (skills.length === 0) {
|
|
@@ -5042,9 +5156,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5042
5156
|
pushMessage('system', 'No session yet; /add-dir works once one has started.');
|
|
5043
5157
|
return;
|
|
5044
5158
|
}
|
|
5045
|
-
void dirs
|
|
5159
|
+
void dirs
|
|
5160
|
+
.add(slash.path, {
|
|
5161
|
+
// Outside the working directory, adding it lets every file
|
|
5162
|
+
// tool reach it unasked, so it is asked about once here.
|
|
5163
|
+
approve: (absolute) => {
|
|
5164
|
+
if (questionRef.current) {
|
|
5165
|
+
pushMessage('system', 'Answer the open question first; /add-dir asks one of its own.');
|
|
5166
|
+
return Promise.resolve(false);
|
|
5167
|
+
}
|
|
5168
|
+
return askQuestion({
|
|
5169
|
+
questionId: `add-dir:${absolute}`,
|
|
5170
|
+
question: `Let the file tools read and write ${absolute} for this session?`,
|
|
5171
|
+
header: 'Outside the working directory',
|
|
5172
|
+
options: [
|
|
5173
|
+
{
|
|
5174
|
+
id: 'yes',
|
|
5175
|
+
label: 'Yes, add it',
|
|
5176
|
+
description: 'File tools reach it without asking again; a sandboxed turn binds it read-write.',
|
|
5177
|
+
},
|
|
5178
|
+
{ id: 'no', label: 'No', description: 'Leave it out' },
|
|
5179
|
+
],
|
|
5180
|
+
multiSelect: false,
|
|
5181
|
+
allowFreeText: false,
|
|
5182
|
+
}).then((answer) => answer.kind === 'answer' && answer.selectedOptionIds.includes('yes'));
|
|
5183
|
+
},
|
|
5184
|
+
})
|
|
5185
|
+
.then((result) => {
|
|
5046
5186
|
pushMessage('system', result.added
|
|
5047
|
-
? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed
|
|
5187
|
+
? `Added ${result.path}. The file tools reach it by absolute path from the next turn; a sandboxed turn binds it read-write.`
|
|
5048
5188
|
: `Not added: ${result.path} — ${result.reason ?? 'refused'}`);
|
|
5049
5189
|
});
|
|
5050
5190
|
return;
|
|
@@ -5079,7 +5219,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5079
5219
|
if (goalCommand)
|
|
5080
5220
|
await materializeConversation();
|
|
5081
5221
|
const durableSessions = sessionsRef.current;
|
|
5082
|
-
const
|
|
5222
|
+
const sessionScope = scopeRef.current;
|
|
5083
5223
|
const taskStore = slash.name === 'tasks' ? session?.currentTaskStore?.() : undefined;
|
|
5084
5224
|
if (slash.name === 'tasks' && !taskStore) {
|
|
5085
5225
|
pushMessage('system', session?.currentTaskStore
|
|
@@ -5091,11 +5231,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5091
5231
|
registry.register(kernelHostCommands({
|
|
5092
5232
|
allowedAgentIds: session?.agentIds ?? [],
|
|
5093
5233
|
...(taskStore ? { taskStore } : {}),
|
|
5094
|
-
...(durableSessions &&
|
|
5234
|
+
...(durableSessions && sessionScope && conversationMaterializedRef.current
|
|
5095
5235
|
? {
|
|
5096
5236
|
goal: {
|
|
5097
5237
|
store: durableSessions.goals,
|
|
5098
|
-
sessionId:
|
|
5238
|
+
sessionId: sessionScope.sessionId,
|
|
5099
5239
|
tenantId: durableSessions.tenantId,
|
|
5100
5240
|
activation: goalActivation,
|
|
5101
5241
|
},
|
|
@@ -5140,7 +5280,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5140
5280
|
setChoicePicker({
|
|
5141
5281
|
kind: 'feedback-rating',
|
|
5142
5282
|
title: 'Rate the latest answer',
|
|
5143
|
-
runId: target.runId,
|
|
5144
5283
|
messageId: target.messageId,
|
|
5145
5284
|
sessionId: target.sessionId,
|
|
5146
5285
|
values,
|
|
@@ -5160,7 +5299,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5160
5299
|
pushMessage('system', 'Nothing to rate yet.');
|
|
5161
5300
|
return;
|
|
5162
5301
|
}
|
|
5163
|
-
recordFeedback(target.sessionId,
|
|
5302
|
+
recordFeedback(target.sessionId, slash.messageId, slash.rating, slash.note);
|
|
5164
5303
|
return;
|
|
5165
5304
|
}
|
|
5166
5305
|
case 'review': {
|
|
@@ -5249,16 +5388,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5249
5388
|
pushMessage('system', 'Nothing to compact yet — this conversation is short enough that adding a summary would save no messages.');
|
|
5250
5389
|
return;
|
|
5251
5390
|
}
|
|
5252
|
-
// Durable first, screen second
|
|
5253
|
-
//
|
|
5254
|
-
//
|
|
5255
|
-
const
|
|
5256
|
-
|
|
5257
|
-
if (sessions && destination) {
|
|
5258
|
-
await persistenceTailRef.current;
|
|
5259
|
-
await replaceConversation(sessions, destination, result.messages);
|
|
5260
|
-
}
|
|
5261
|
-
modelHistoryRef.current = result.messages;
|
|
5391
|
+
// Durable first, screen second: for a conversation in a log,
|
|
5392
|
+
// `session.compact` has already appended the compaction record
|
|
5393
|
+
// the next `/resume` folds, before it returned.
|
|
5394
|
+
const durable = Boolean(sessionsRef.current && scopeRef.current && conversationMaterializedRef.current);
|
|
5395
|
+
modelHistoryRef.current = [...result.messages];
|
|
5262
5396
|
// How many user/assistant turns survived the pass.
|
|
5263
5397
|
// `keepRecentRows` explains why the transcript is trimmed
|
|
5264
5398
|
// to match rather than rebuilt from `result.messages`.
|
|
@@ -5274,7 +5408,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5274
5408
|
role: 'system',
|
|
5275
5409
|
content: `Compacted ${result.shed} earlier message(s) into a summary.${result.usage.totalTokens > 0
|
|
5276
5410
|
? ` Verifier used ${result.usage.totalTokens.toLocaleString('en-US')} tokens.`
|
|
5277
|
-
: ''}${
|
|
5411
|
+
: ''}${durable
|
|
5278
5412
|
? ''
|
|
5279
5413
|
: ' Conversation persistence is unavailable, so this compacted history lasts only for this process.'}`,
|
|
5280
5414
|
detail: summaryDetail(result.summary),
|
|
@@ -5406,9 +5540,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5406
5540
|
hostCommands,
|
|
5407
5541
|
nextId,
|
|
5408
5542
|
openAgentCockpit,
|
|
5409
|
-
|
|
5543
|
+
runAgentsCommand,
|
|
5544
|
+
abandonActiveTurn,
|
|
5410
5545
|
pushMessage,
|
|
5411
5546
|
rawOutput,
|
|
5547
|
+
rememberProjectNote,
|
|
5412
5548
|
removeStoredCredential,
|
|
5413
5549
|
resetTranscript,
|
|
5414
5550
|
runConversationExport,
|
|
@@ -5502,7 +5638,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5502
5638
|
}
|
|
5503
5639
|
const durableSessions = sessionsRef.current;
|
|
5504
5640
|
const scope = scopeRef.current;
|
|
5505
|
-
if (!durableSessions
|
|
5641
|
+
if (!durableSessions || !scope || !conversationMaterializedRef.current)
|
|
5506
5642
|
return;
|
|
5507
5643
|
const generation = conversationGenRef.current;
|
|
5508
5644
|
const sessionId = scope.sessionId;
|
|
@@ -5858,7 +5994,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5858
5994
|
? permission.review
|
|
5859
5995
|
: (permission.summary.compactText ?? permission.summary.text);
|
|
5860
5996
|
const count = permissionReviewRows(source, terminal.columns).length;
|
|
5861
|
-
const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (permission.
|
|
5997
|
+
const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (reviewSource(permission, scopeRef.current?.sessionId) ? 1 : 0));
|
|
5862
5998
|
const maxOffset = Math.max(0, count - pageRows);
|
|
5863
5999
|
const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
|
|
5864
6000
|
const next = key.home
|
|
@@ -6263,8 +6399,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6263
6399
|
// while the provider event that removes the generic tool row arrives on the
|
|
6264
6400
|
// next tick; filtering only live children made that raw row flash back.
|
|
6265
6401
|
const visibleActiveTools = activeTools.filter((tool) => tool.toolName.toLowerCase() !== 'agent' ||
|
|
6266
|
-
!(tool.
|
|
6267
|
-
? representedSubagentTools.has(JSON.stringify([tool.
|
|
6402
|
+
!(tool.turnId
|
|
6403
|
+
? representedSubagentTools.has(JSON.stringify([tool.turnId, tool.id]))
|
|
6268
6404
|
: representedUnscopedSubagentToolUseIds.has(tool.id)));
|
|
6269
6405
|
const outputBlocks = outputViewer
|
|
6270
6406
|
? messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0)
|
|
@@ -6321,11 +6457,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6321
6457
|
: goalStatus && phase === 'ready' && state === 'idle'
|
|
6322
6458
|
? undefined
|
|
6323
6459
|
: hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
|
|
6324
|
-
const
|
|
6325
|
-
|
|
6460
|
+
const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
|
|
6461
|
+
const permissionOwner = permissionSource
|
|
6462
|
+
? subagents.find((agent) => agent.sessionId === permissionSource)
|
|
6326
6463
|
: undefined;
|
|
6327
|
-
const permissionSourceLabel =
|
|
6328
|
-
? permissionOwner
|
|
6464
|
+
const permissionSourceLabel = permissionSource
|
|
6465
|
+
? permissionOwner
|
|
6466
|
+
? `Agent: ${permissionOwner.description}`
|
|
6467
|
+
: `Session: ${permissionSource}`
|
|
6329
6468
|
: undefined;
|
|
6330
6469
|
const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
|
|
6331
6470
|
// True exactly when the phase ternary below renders its final branch — the
|
|
@@ -6375,7 +6514,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6375
6514
|
copyPicker === null &&
|
|
6376
6515
|
agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
6377
6516
|
? queuePause.outcome === 'paused'
|
|
6378
|
-
? 'held after a resumable
|
|
6517
|
+
? 'held after a resumable turn paused; wait for recovery, change model, or send a message to release it'
|
|
6379
6518
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
6380
6519
|
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
6381
6520
|
phase !== 'ready' ||
|
|
@@ -6483,27 +6622,37 @@ function goalStatusLabel(goal, armed) {
|
|
|
6483
6622
|
return 'Goal achieved';
|
|
6484
6623
|
}
|
|
6485
6624
|
}
|
|
6625
|
+
/**
|
|
6626
|
+
* The child session a tool review came from, when it is not this
|
|
6627
|
+
* conversation's own turn: what names the agent asking in the prompt. Read by
|
|
6628
|
+
* field, because the review request names its session only when the kernel
|
|
6629
|
+
* attributes it.
|
|
6630
|
+
*/
|
|
6631
|
+
function reviewSource(request, currentSessionId) {
|
|
6632
|
+
const sessionId = request?.sessionId;
|
|
6633
|
+
return typeof sessionId === 'string' && sessionId !== currentSessionId ? sessionId : undefined;
|
|
6634
|
+
}
|
|
6486
6635
|
/** The label the operator saw for the option id they chose. */
|
|
6487
6636
|
function labelOfOption(question, optionId) {
|
|
6488
6637
|
return question.options.find((option) => option.id === optionId)?.label ?? optionId;
|
|
6489
6638
|
}
|
|
6490
|
-
/** `/agents
|
|
6491
|
-
function
|
|
6492
|
-
const agents = `${
|
|
6493
|
-
const tokens = `${
|
|
6639
|
+
/** `/agents batches`' one-line-per-batch text, mirroring `/jobs`' own column shape. */
|
|
6640
|
+
function batchOption(batch) {
|
|
6641
|
+
const agents = `${batch.agentsDone}/${batch.agentsTotal} agent${batch.agentsTotal === 1 ? '' : 's'}`;
|
|
6642
|
+
const tokens = `${batch.tokensTotal.toLocaleString('en-US')} tokens`;
|
|
6494
6643
|
const columns = [
|
|
6495
|
-
|
|
6496
|
-
|
|
6644
|
+
relativeStart(batch.startedAt),
|
|
6645
|
+
batch.phases.join(', '),
|
|
6497
6646
|
agents,
|
|
6498
6647
|
tokens,
|
|
6499
|
-
formatElapsed(
|
|
6648
|
+
formatElapsed(batch.elapsedMs),
|
|
6500
6649
|
];
|
|
6501
|
-
if (
|
|
6650
|
+
if (batch.live)
|
|
6502
6651
|
columns.push('live');
|
|
6503
|
-
return { label:
|
|
6652
|
+
return { label: batch.name, description: columns.join(' · ') };
|
|
6504
6653
|
}
|
|
6505
6654
|
/** `just now` / `12m ago` / `3h ago` / `2d ago` — the same buckets a conversation's own resume picker shows. */
|
|
6506
|
-
function
|
|
6655
|
+
function relativeStart(startedAt) {
|
|
6507
6656
|
const minutes = Math.round((Date.now() - startedAt) / 60_000);
|
|
6508
6657
|
if (minutes < 1)
|
|
6509
6658
|
return 'just now';
|