Repository navigation
Commit bfb5a7c
feat(tui): toggle YOLO mode from the TUI with ctrl+y (#1078)
* feat(tui): toggle YOLO mode from the TUI with ctrl+y
YOLO mode could only be turned on at startup, via `--yolo` or
`ALTIMATE_CLI_YOLO`. Add a `ctrl+y` toggle so it can be switched on and
off mid-session.
- Enabling asks for confirmation; disabling is immediate, so turning a
dangerous mode off is never harder than leaving it on.
- Scoped to the current session, and to subagents spawned from it. A
subagent runs in its own child session, so its permission requests
carry the child id; lookups normalize to the root of the parent chain.
- In-memory only. YOLO never survives a restart, so resuming an old
session cannot silently put you back into it.
- An explicit toggle beats the `--yolo` default in both directions,
so a session started with `--yolo` can still be switched off.
- The confirmation says what stops asking AND what stays blocked.
Deny rules are refused by `Permission.ask` before any event reaches
the TUI, so DROP DATABASE / DROP SCHEMA / TRUNCATE cannot be
auto-approved by this mode.
The footer indicator previously read `Flag.ALTIMATE_CLI_YOLO` directly.
That is a `process.env` getter, not a signal, so it never re-rendered on
change; it is now reactive and per-session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): address review findings on the YOLO ctrl+y toggle
Security and correctness fixes from review of the initial implementation.
Scoping (critical): a welcome-screen choice no longer acts as the fallback
for every session without an explicit override. `session.new` navigates to
home while the previous session keeps streaming, so pressing ctrl+y there
would start auto-approving for that still-running session. The pending
choice is now display-only until adopted, and adoption moved from a route
effect to the session-creation path — the route effect also fired when
resuming an existing conversation, silently enabling YOLO on it.
Fail closed on an unresolvable session. Root resolution reads the TUI
session store, which can lag a child's first permission request. Resolving
an unhydrated child to itself missed the root's override, so a session the
user had explicitly turned OFF could still auto-approve under `--yolo`.
`resolveRoot` now returns undefined for an unknown session, an unknown
ancestor, or an over-deep chain, and callers treat that as "do not approve".
Auto-approve failures are no longer silent. The handler does not enqueue the
request before replying, so a lost reply left the server-side Deferred
unresolved and the agent hanging with nothing on screen. The generated SDK
returns `{ error }` rather than throwing unless `throwOnError` is set, so
the previous `.catch()` could never fire. Now opts in and falls back to the
normal prompt.
Enabling YOLO while a prompt is already on screen now clears it. Previously
the most natural moment to press ctrl+y — blocked awaiting approval — showed
"YOLO ON" beside a still-blocked agent.
Also: re-check the route before applying a confirmation (quick-switch stays
live during a modal), drop overrides on session.deleted, and say
"this conversation and any subagents it spawns" rather than the contradictory
"this session only" two lines above "applies to subagents too".
Testing: the real-binary journeys are gated on OPENCODE_TEST_CLI, which CI
deliberately does not set, so none of them gated the merge. Adds in-process
sync tests covering auto-approve, reply failure, subagent inheritance,
fail-closed, pending isolation and flush-on-enable.
Fixing the harness to run those uncovered a missing ExitProvider in
sync-fixture: every test in it failed with "Exit context must be used within
a context provider". That one-line fix takes the TUI suite from 9 failures
to 1 by repairing 8 pre-existing reds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* test(tui): pin ctrl+y behaviour when the console overlay has focus
Review flagged that `ctrl+y` is already bound to `copy-selection` in the
opentui console overlay (`consoleOptions` in app.tsx) and that the runtime
behaviour was undefined and untested. A static keybind-config check cannot
answer it — the console binding does not live in TuiKeybind.Definitions —
so this drives the real binary and records what actually happens.
Measured: with the console open, the yolo binding takes precedence and the
confirmation dialog still appears. YOLO is never enabled without it, so the
dangerous case (a bypass switched on by a keystroke aimed at the console)
does not occur. Closing the console leaves the shortcut working normally.
Pinned as assertions rather than logged, so changing the precedence has to
be a deliberate decision. The practical cost is that console copy-selection
is shadowed while this binding is registered — acceptable given the console
ships unbound (`app_console: "none"`) and must be deliberately bound to open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): wrap yolo additions in altimate_change start/end markers
Marker Guard failed in strict mode on four upstream-shared files. Each had a
single-line `// altimate_change - ...` comment, which the guard does not count
— it looks for a `altimate_change start` … `altimate_change end` block so the
enclosed lines are protected when upstream changes are merged in.
No behaviour change; comments only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): pass workspace on yolo auto-approve and stop duplicate replies
Two defects introduced by the previous review round, found by CodeRabbit.
The workspace was never sent on the flush path. `set()` took an optional
workspace argument, but the app.tsx caller did not pass one, so replies from
flushPendingPermissions went out with `workspace: undefined` while the session
permission UI sends `project.workspace.current()` on every reply. Fixed at the
source instead of at the call site — sync already has the project context, and
threading the value through each caller is how it went missing in the first
place.
Replies could be sent twice for one request. flushPendingPermissions does not
remove requests from the store; removal waits for the server's
`permission.replied` event. Toggling yolo off and on again, or two set() calls,
therefore replied to an already-settled id. The server rejects the second reply
and autoApprove treated that rejection as a lost reply, putting a prompt back on
screen for a request that was already answered. Now guarded by an in-flight set
for concurrent duplicates plus optimistic removal on success for sequential
ones — the event handler's removal stays idempotent.
Adds a regression test for the double-enable path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): make the flush test assert its own claim, and stop y/n propagation
Two remaining review points.
The test named "enabling yolo clears a prompt that is already on screen" only
asserted that a reply was sent — never that the prompt left the screen, which is
the behaviour in its name. It now asserts the pending list empties. This works
without emitting `permission.replied` because autoApprove removes the request
optimistically on success.
Adds `stopPropagation()` to the dialog's `y` and `n` branches, matching the
`return` branch. Two reviewers disagreed on whether this is load-bearing: the
dialog blurs the focused renderable on open and refocuses in a later tick, so a
stray character cannot reach the prompt today. It is free consistency that keeps
that true if the ordering ever changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): fail closed on cyclic chains, drop dead code, de-duplicate removal
Follow-up review round.
resolveRoot treated a cyclic parent chain as a valid root, returning whichever
node the walk stopped on. That node could carry an override or inherit `--yolo`,
so a malformed chain could auto-approve — the same fail-open shape the
unknown-session guard already prevents. Cycles now return undefined. The unit
test had encoded the old behaviour as expected; corrected, and extended to
assert a cyclic chain never auto-approves.
Reverts the change to routes/session/footer.tsx. That component is imported
nowhere (its sibling subagent-footer.tsx is), so the indicator there can never
render and the earlier claim about fixing its reactivity was wrong. The visible
indicator users get is the chat-panel hint in component/prompt, which the
journeys assert. Leaving the dead component untouched.
Removes `sync.yolo.forget()` — added but never called; session cleanup already
happens in the session.deleted handler.
`permission.replied` now shares removePermission with the auto-approve path
instead of repeating the search/splice inline.
The double-enable regression test was passing vacuously: with an instant mock
the store is cleared before the second set() runs, so the second flush was a
no-op and the in-flight guard was never exercised. The reply is now held open so
the second toggle lands while the first is genuinely in flight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
* fix(tui): wrap the shared permission removal in altimate_change markers
Marker Guard strict mode again — the permission.replied handler now delegates to
removePermission, and that replacement line needs start/end markers like the
rest of the fork's edits to upstream-shared files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01964Prd1Sz5JwWZmNTrFdiU
---------
Co-authored-by: Haider <haider@altimate.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent 092567a commit bfb5a7c
14 files changed
Lines changed: 1660 additions & 51 deletions
File tree
- packages
- opencode/test/tui-journeys
- tui
- src
- component
- prompt
- config
- context
- util
- test
- cli/cmd/tui
- util
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
0 commit comments