diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b2df7dd66..8df584c207 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ file is the human-readable summary of what changed and why.
## Unreleased
+### TUI
+
+- **Permission mode is always shown, and `shift+tab` toggles it** — the prompt
+ footer now labels the mode next to the agent name: gray `manual` in normal
+ mode, red `auto` when permissions are auto-approved (`--auto`, previously a
+ gray `auto` shown only in that mode). `shift+tab` cycles between the two,
+ configurable as the new `permission_mode_cycle` keybind, and the idle footer
+ advertises it with a `shift+tab modes` hint next to `tab agents`. Its former default,
+ previous-agent (`agent_cycle_reverse`), is now unbound by default — `tab`
+ still cycles agents forward.
+
## 1.18.4-1
### CodeGraph
diff --git a/DIVERGENCES.md b/DIVERGENCES.md
index bfb7bcaddf..801100d274 100644
--- a/DIVERGENCES.md
+++ b/DIVERGENCES.md
@@ -161,6 +161,7 @@ from the squash message first, or use a plain merge.
| `packages/app/src/context/language.tsx`, `packages/app/src/entry.tsx`, `packages/desktop/src/renderer/i18n/index.ts`, `packages/console/app/src/i18n/index.ts`, `packages/console/app/src/lib/language.ts`, `packages/console/app/src/component/footer.tsx`, `packages/stats/app/src/i18n.ts`, `packages/enterprise/src/app.tsx`, `packages/app/e2e/regression/session-timeline-locale-projection.spec.ts` | Locale registries trimmed to `en` + `es`: `Locale` types, LOCALES/LABEL/TAG/DOCS maps, dict loaders, accept-language matchers; enterprise/entry `zh` special cases now use `es`; console footer always links Discord (Feishu was zh-only); e2e locale-projection spec tests `es` ("Explorado") instead of de/ar | Fork ships English + Spanish only. | Keep our trimmed lists; take upstream's structural changes and re-trim any new locale entries. |
| `packages/tui/src/component/prompt/autocomplete.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/opencode/src/cli/cmd/run/footer.prompt.tsx`, `packages/core/src/filesystem/search.ts`, `packages/tui/test/fixture/tui-sdk.ts`, `packages/opencode/test/cli/run/footer.view.test.tsx`, `packages/core/test/filesystem/search.test.ts` | `@` mention autocomplete lists file/directory results before references/agents/MCP resources; TUI and run-CLI menus cap the leading files so up to 3 non-file matches stay in the first visible page; web app group rank reordered to recent → file → reference → agent → resource (extracted into fork-only `prompt-input/at-options.ts`). TUI and run-CLI file resources track menu visibility so opening on an empty query fetches (previously nothing refetched until the query changed); core's ripgrep-fallback `find` returns its first entries for an empty query to mirror fff. Tests: tui-sdk fixture's `FetchHandler` gains an optional `method` arg, `renderFooter` accepts `findFiles`/`agents` overrides + a mention-menu ordering test, core search tests cover the fallback's empty query (fork-only test files: `packages/tui/test/prompt-autocomplete.test.tsx`, `packages/app/test-browser/prompt-at-options.test.ts`, `packages/app/src/components/prompt-input/at-options{,.test}.ts`) | Files are the dominant `@` use case; they should rank first and appear on a bare `@`. | Keep our ordering/reserve logic, resource source tracking, empty-query branch, fixture/test additions; take upstream's surrounding changes. |
| `packages/slack/.gitignore`, `sdks/vscode/.gitignore` | Added the missing trailing newline (content unchanged) | Upstream's copies end without a newline; POSIX text files should end with one, and tools that concatenate or append to these files misbehave without it. | Whitespace-only: keep the trailing newline; take upstream's entries. |
+| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the idle footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same no-usage `Match` arm. app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. |
## Known rebrand gaps
diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx
index 0d919b93b3..9a32730b72 100644
--- a/packages/tui/src/app.tsx
+++ b/packages/tui/src/app.tsx
@@ -142,6 +142,7 @@ const appBindingCommands = [
"app.toggle.diffwrap",
"app.toggle.paste_summary",
"app.toggle.session_directory_filter",
+ "permission.mode",
] as const
export type TuiInput = {
diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
index c97538af97..89bb5f9a74 100644
--- a/packages/tui/src/component/prompt/index.tsx
+++ b/packages/tui/src/component/prompt/index.tsx
@@ -166,6 +166,7 @@ export function Prompt(props: PromptProps) {
const stash = usePromptStash()
const keymap = useOpencodeKeymap()
const agentShortcut = useCommandShortcut("agent.cycle")
+ const modeShortcut = useCommandShortcut("permission.mode")
const paletteShortcut = useCommandShortcut("command.palette.show")
const interruptSequence = useCommandShortcut("session.interrupt")
const interruptNowSequence = useCommandShortcut("session.interrupt.now")
@@ -1435,8 +1436,15 @@ export function Prompt(props: PromptProps) {
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}
-
- auto
+
+
+ {local.permission.mode === "auto" ? "auto" : "manual"}
+
@@ -1644,6 +1652,9 @@ export function Prompt(props: PromptProps) {
)}
+
+ {modeShortcut()} modes
+
{agentShortcut()} agents
diff --git a/packages/tui/src/config/keybind.ts b/packages/tui/src/config/keybind.ts
index e7e886b855..147274e6c7 100644
--- a/packages/tui/src/config/keybind.ts
+++ b/packages/tui/src/config/keybind.ts
@@ -130,7 +130,8 @@ export const Definitions = {
console_org_switch: keybind("none", "Switch console organization"),
agent_list: keybind("a", "List agents"),
agent_cycle: keybind("tab", "Next agent"),
- agent_cycle_reverse: keybind("shift+tab", "Previous agent"),
+ agent_cycle_reverse: keybind("none", "Previous agent"),
+ permission_mode_cycle: keybind("shift+tab", "Cycle permission mode (manual/auto)"),
variant_cycle: keybind("ctrl+t", "Cycle model variants"),
variant_list: keybind("none", "List model variants"),
@@ -339,6 +340,7 @@ export const CommandMap = {
agent_list: "agent.list",
agent_cycle: "agent.cycle",
agent_cycle_reverse: "agent.cycle.reverse",
+ permission_mode_cycle: "permission.mode",
variant_cycle: "variant.cycle",
variant_list: "variant.list",
messages_page_up: "session.page.up",