Merge upstream main into feat/subagent-cost-rollup

This commit is contained in:
Igor Velho
2026-08-27 11:08:57 +01:00
144 changed files with 6230 additions and 2902 deletions
@@ -55,6 +55,7 @@ Use `gh pr view <number> --json number,title,body,author,mergedAt` for PR eviden
## Highlights and Ordering ## Highlights and Ordering
- Sort bullets by user impact, not commit order. Breaking changes first, then significant new capabilities or broad user-visible improvements, then smaller features, fixes, and visual polish. - Sort bullets by user impact, not commit order. Breaking changes first, then significant new capabilities or broad user-visible improvements, then smaller features, fixes, and visual polish.
- Keep the opening highlight block contiguous. Place every bold highlight before the first regular bullet; a regular bullet marks the end of the highlight block.
- Mark only the strongest highlights with a bold area prefix, such as `- **Chat attachments:** ...`. Usually the first 13 bullets; fewer when the release lacks substantial changes, more only when clearly justified. - Mark only the strongest highlights with a bold area prefix, such as `- **Chat attachments:** ...`. Usually the first 13 bullets; fewer when the release lacks substantial changes, more only when clearly justified.
- Treat a change as a highlight only when it introduces a substantial user-facing capability, materially changes a common workflow, or fixes a severe/widespread problem. Do not bold merely because a bullet is first, has a large diff, or was hard to implement. - Treat a change as a highlight only when it introduces a substantial user-facing capability, materially changes a common workflow, or fixes a severe/widespread problem. Do not bold merely because a bullet is first, has a large diff, or was hard to implement.
- Keep related platform bullets together only when that does not push a more important change too far down. - Keep related platform bullets together only when that does not push a more important change too far down.
+45 -32
View File
@@ -4,42 +4,55 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
- **Chat scrolling rebuilt around your message.** Sending a message parks it near the top of the view and the reply streams into the space below it, so you read from where you asked instead of chasing the bottom. Streamed text arrives a paragraph at a time (code blocks line by line) with a soft fade, and the view glides after it in one continuous motion instead of snapping per line. Scrolling up during a stream immediately hands you the wheel — nothing yanks the view back — and the scroll-to-bottom pill appears on the left, carrying the model's working status while you're away from the live edge. Sending from anywhere mid-conversation jumps you straight to your new message, and opening a session goes straight to the newest message with no scroll animation.
- Work status: the session cost now counts what its subagents spent, with a line under the context meter splitting the session's own cost from the subagents' share, and each subagent's cost shown next to it in the Subagents list. Previously a session that delegated most of its work looked far cheaper than it was. - Work status: the session cost now counts what its subagents spent, with a line under the context meter splitting the session's own cost from the subagents' share, and each subagent's cost shown next to it in the Subagents list. Previously a session that delegated most of its work looked far cheaper than it was.
- **Chat context attachments:** everything you attach to a message — diff/file/plan comments, terminal selections, browser annotations, PR comments and failed checks, linked issues and PRs — now shows up in the conversation as a compact context card: a header naming the source, the captured content behind an expander, and your comment below it. Previously most of these arrived as a wall of raw text inside your message.
- **Faster session switching in large workspaces** (thanks @c-w-xiaohei): switching sessions no longer rebuilds the whole sidebar, returning to a recently viewed session restores its rendered messages instead of re-rendering them (file links included), and scrolling long conversations costs less. In a workspace with thousands of loaded sessions, end-to-end switch time dropped by roughly half. ## [1.21.0] - 2026-08-26
- **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many and carry the sidebar's running/unread dot. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab.
- Chat: comment on a reply — select text in a chat message (or in a rendered markdown preview in Files) and choose Comment to attach exactly that quote, with a source line range when it can be located, plus your note to the next message. The selection stays highlighted while you type, and the selection menu was restyled — Add to chat is now Add to input. - **Chat scrolling rebuilt around your message.** Sending parks your message near the top and the reply streams in below it, gliding smoothly a paragraph at a time. Scrolling up immediately hands you the wheel; the scroll-to-bottom pill carries the model's working status while you're away.
- Diff: comment like a review — hovering a line shows a + button in the gutter; clicking it, clicking a line, or dragging across lines opens the comment editor for that line or range. The comment editor and saved-comment cards match the chat's comment style. - **Keyboard shortcuts redesigned:** single chords for everyday actions, a Cmd/Ctrl+K leader for two-step open/go actions, held Cmd/Ctrl+digit for session tabs and Cmd/Ctrl+Option+digit for panel surfaces. Shortcuts work on non-English keyboard layouts now, tooltips show the binding you actually have set, and old custom bindings reset once. The full map lives in Settings → Shortcuts (registry contributed by @ChangeHow — thanks!).
- Composer: hovering or tapping a context chip above the input opens a stacked preview of everything attached, where a comment can be edited in place or an item removed before sending. - **Chat context attachments:** diff comments, terminal selections, browser annotations, linked issues/PRs and the rest now appear in the conversation as compact context cards instead of walls of raw text.
- **Session tabs (opt-in):** the web/desktop header can show open sessions as browser-style tabs (Settings → General → Navigation). A tab switches the whole workspace; closing one never touches the session itself.
- Sessions: switching is much faster in large workspaces — the sidebar no longer rebuilds on switch and recently viewed sessions restore their rendered messages; end-to-end switch time roughly halved with thousands of loaded sessions (thanks to @c-w-xiaohei).
- Permission: cards answer to the keyboard Alt+Enter allows once, Alt+Shift+Enter allows always, Alt+Backspace denies — the keys are printed on the buttons. The auto-accept toggle got Cmd/Ctrl+K, A.
- Sessions: Cmd/Ctrl+Alt+Left/Right steps back and forward through the sessions you opened in this window, browser-history style; with session tabs enabled it moves between neighbouring tabs instead.
- Git: Cmd/Ctrl+Enter in the commit message box commits. Diff review moves between changed files with Alt+Down/Up, expanding a collapsed file on arrival.
- Chat: Cmd/Ctrl+Shift+T now cycles through every thinking level offered by the selected model instead of skipping levels after reaching the end (thanks to @nimobeeren).
- Panels: the context rail got a configure button — a dialog chooses which panels the rail shows. Hidden panels keep their data, stay reachable from the command palette, and leave the digit switcher, so digits always match the icons you see.
- Chat: comment on a reply — select text in a chat message (or a rendered markdown preview in Files) and choose Comment to attach exactly that quote, with a source line range when it can be located, plus your note. The selection stays highlighted while you type.
- Diff: comment like a review — hovering a line shows a + in the gutter; clicking or dragging across lines opens the comment editor for that range, styled like the chat's comments.
- Composer: hovering or tapping a context chip opens a stacked preview of everything attached, where a comment can be edited in place or an item removed before sending.
- Mobile: the chat comment input overlays the composer exactly and rides the keyboard; Enter makes a new line there, with attach on the button. - Mobile: the chat comment input overlays the composer exactly and rides the keyboard; Enter makes a new line there, with attach on the button.
- Terminal: terminals no longer vanish or die behind your back. Opening the app in another browser tab, on another device, or after a reload shows the terminals already running on the server instead of an empty list, and terminals sitting in background tabs are no longer closed by the server's idle cleanup while the app is open. - Terminal: terminals no longer vanish behind your back — every tab and device shows the ones already running on the server, and background tabs survive the idle cleanup.
- Search: every searchable picker — branches, projects, agents, models, providers, stashes, SSH hosts, skills, archived sessions — now uses one matcher: best matches come first, multi-word queries match in any order, and punctuation doesn't matter (so "gpt4o" finds "gpt-4o"). Ctrl/Cmd+P also matches the whole file path, not just the file name, and the git branch and gitmoji pickers stopped silently dropping rows a second built-in filter didn't like. Sidebar session search and the Todos/Memory/Plans/Notes filters match the same way. - Search: every searchable picker uses one matcher now — best matches first, multi-word queries in any order, punctuation ignored ("gpt4o" finds "gpt-4o"). Ctrl/Cmd+P matches whole file paths.
- Chat: @ file mentions rank files and directories together by how well they match, so the file you typed is at the top instead of below unrelated directories. Multi-word queries match in any order, and long paths keep the folder next to the file name visible so identical-looking index.md rows are distinguishable. - Chat: @ file mentions rank files and directories together by match quality, and long paths keep the folder next to the file name visible.
- Chat: a new "Follow new content while streaming" checkbox (Settings → Chat → Streaming, on by default) turns the automatic following off entirely — your message still parks at the top on send, but the view never moves on its own afterwards. - Chat: a "Follow new content while streaming" checkbox (Settings → Chat → Streaming, on by default) turns automatic following off entirely; with it off, the scroll-to-bottom pill now appears as soon as the reply grows past the visible area.
- Mobile: narrowing a browser window past phone size switches into the mobile app layout (and back when widened) instead of squeezing the desktop layout. The old/new mobile layout setting is gone — phones always get the mobile layout. - Command palette: rarely used commands (pin session, copy session ID, multi-run launcher, archived sessions, notes, todos, status, theme) are found by typing but stay off the first screen.
- Chat: streamed code blocks are syntax-highlighted while they stream, and finished messages no longer jump when a code block's line numbers fill in at the end of a reply. - Mobile: narrowing a browser window past phone size switches into the mobile layout (and back when widened); the old/new mobile layout setting is gone.
- Chat: finished replies no longer flicker — tool cards stopped re-rendering (and replaying their reveal animation) when they completed, and resizing the window no longer throws the conversation up and down while you're at the bottom. - Browser: an agent opening a page with the browser tool no longer pops the browser panel open (or switches the surface you're on) — the page loads in the background and the rail is where you peek at it.
- Chat: clicking the last item in the prompt rail now always lands on it, and rail jumps teleport instead of a long smooth scroll that could stop halfway. - Usage: the Command Code tile is gone — their official API exposes no usage data, so the tile could only fail.
- Mobile: scrolling during a streaming reply works again — a drag immediately takes over, the scroll-to-bottom pill shows up, and the load-older button no longer throws you to the bottom of the chat. - Desktop: a relay-paired default host no longer greets every restart with the "Remote Server Unreachable" screen — the stored direct address (often the pairing machine's own loopback) failing its probe now boots the app normally and connects over the relay, picking the direct route back up automatically when it answers again.
- Fixed file links in messages being checked twice against the filesystem, and against the wrong project directory on the first pass. - Mobile: on Android browsers the composer now stays above the keyboard in the chat too — the keyboard could cover it with no way to scroll it into view; the draft screen's viewport pinning now covers the chat screen on Android.
- Auth: an expired OpenChamber login is announced within seconds by a banner with a Log in button, instead of being discovered through failing actions. Sending pauses until login, and a conversation that failed to load reloads itself afterwards.
- Chat: a failed send returns your typed prompt to the input — whatever the reason — instead of losing it to an error toast; a mid-send session switch lands it in that session's draft.
- Chat: opening a session or resizing panels could strand the view in a large empty space below the last message; the list now returns to the real end, and a width resize keeps a reader who was at the bottom at the bottom.
- Chat: prompt-rail and message jumps land exactly on the target once the layout finishes measuring, and clicking the last rail item always works.
- Desktop: two windows on different projects no longer hijack each other — one window's session switch could make the other adopt its project mid-typing. Notification clicks and openchamber:// links now open in one window instead of all of them.
- Git: the branch's PR badge no longer picks up a stranger's pull request — with contributor forks added as remotes, a fork's closed PR sharing only the branch name could show up on the local branch.
- Chat: streamed code blocks are syntax-highlighted while streaming, and finished messages no longer jump when line numbers fill in.
- Chat: finished replies no longer flicker — tool cards stopped replaying their reveal animation on completion, and window resizing no longer throws the conversation around at the bottom.
- Mobile: scrolling during a streaming reply works again — a drag immediately takes over, the pill shows up, and load-older no longer throws you to the bottom.
- Fixed file links in messages being checked twice, and against the wrong project directory on the first pass.
- Fixed the selected project or session briefly jumping back to a previous choice when settings responses arrived out of order. - Fixed the selected project or session briefly jumping back to a previous choice when settings responses arrived out of order.
- Fixed sessions staying on "loading sessions" forever after the connection to OpenCode went half-open — stalled reads now time out and retry instead of holding bootstrap hostage (thanks @herjarsa). - Fixed sessions staying on "loading sessions" forever after a half-open connection to OpenCode — stalled reads now time out and retry (thanks to @herjarsa).
- Files: previews of files above the editable size cap now show the whole file instead of the first 200k characters, virtualized so opening and scrolling a huge file no longer freezes the app (thanks @gaojunran). - Files: previews above the editable size cap show the whole file, virtualized so huge files no longer freeze the app (thanks to @gaojunran).
- VSCode: the chat view no longer stays stuck on its loading screen on slow or remote connections (for example code-server behind a reverse proxy) — the connection status is re-sent until the webview is ready to hear it (thanks @VinciYan). - VSCode: the chat view no longer sticks on its loading screen on slow or remote connections (thanks to @VinciYan).
- Terminal: mobile keyboards no longer capitalize the first letter of every command on iOS and Android. - Terminal: mobile keyboards no longer capitalize the first letter of every command.
- Desktop: a freshly installed or updated build no longer keeps loading the previous version's interface from cache. - Desktop: a freshly installed or updated build no longer loads the previous version's interface from cache.
- Devices: re-pairing a phone (or logging in again) keeps the device's existing name in Connected Devices instead of resetting it to "OpenChamber Mobile". - Devices: re-pairing a phone keeps the device's existing name instead of resetting it to "OpenChamber Mobile".
- Relay: paired devices no longer get logged out when the app restarts (for example during an update) while another local OpenChamber process is running — the restarted app keeps serving them instead of a bystander process taking over. - Relay: paired devices no longer get logged out when the app restarts while another local OpenChamber process is running.
- Sessions: headers now find archived sessions too, so an archived session's title no longer goes missing. - Sessions: headers now find archived sessions too, so an archived session's title no longer goes missing.
- Files: the editor toolbar is now always docked under the file tabs; the floating hover toolbar and its setting were removed. - Files: the editor toolbar is always docked under the file tabs; the floating hover toolbar and its setting were removed.
- UI: the chat's top and bottom scroll fades are back, and the first uncached open of a session fades the conversation in instead of popping. - UI: the chat's scroll fades are back, the first uncached session open fades in, the timeline dialog fits small screens (thanks to @gaojunran), OpenCode notices share one style, draft target menus stay inside the chat area, Linear and Cloudflare tools show their own icons, sidebar tooltips no longer appear on passing hover, and the btw panel's shadow matches the composer.
- UI: the timeline dialog now fits small screens instead of squeezing the message list to a couple of rows (thanks to @gaojunran).
- Chat: OpenCode notices now share one style.
- UI: draft target menus stay inside the chat area instead of overlapping the header.
- UI: Linear and Cloudflare tools now show their own icons.
- UI: sidebar item tooltips no longer appear instantly on passing hover.
- UI: the btw panel's shadow is lighter, matching the composer.
## [1.20.0] - 2026-08-23 ## [1.20.0] - 2026-08-23
+1 -1
View File
@@ -3,7 +3,7 @@
[![GitHub stars](https://img.shields.io/github/stars/openchamber/openchamber?style=flat&labelColor=100F0F&color=66800B)](https://github.com/openchamber/openchamber/stargazers) [![GitHub stars](https://img.shields.io/github/stars/openchamber/openchamber?style=flat&labelColor=100F0F&color=66800B)](https://github.com/openchamber/openchamber/stargazers)
[![GitHub release](https://img.shields.io/github/v/release/openchamber/openchamber?style=flat&labelColor=100F0F&color=205EA6)](https://github.com/openchamber/openchamber/releases/latest) [![GitHub release](https://img.shields.io/github/v/release/openchamber/openchamber?style=flat&labelColor=100F0F&color=205EA6)](https://github.com/openchamber/openchamber/releases/latest)
[![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA) [![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA)
[![Support the project](https://img.shields.io/badge/Support-Project-black?style=flat&labelColor=100F0F&color=EC8B49&logo=ko-fi&logoColor=FFFCF0)](https://ko-fi.com/G2G41SAWNS) [![Support the project](https://img.shields.io/badge/Support-Project-black?style=flat&labelColor=100F0F&color=EC8B49&logo=patreon&logoColor=FFFCF0)](https://www.patreon.com/openchamber)
## Run agent work. Keep control. Ship from anywhere. ## Run agent work. Keep control. Ship from anywhere.
+5 -5
View File
@@ -30,7 +30,7 @@
"@heroui/theme": "^2.4.23", "@heroui/theme": "^2.4.23",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -169,7 +169,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@legendapp/list": "3.3.8", "@legendapp/list": "3.3.8",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"@pierre/diffs": "1.3.0-beta.6", "@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.4.0", "@replit/codemirror-vim": "^6.4.0",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
@@ -243,7 +243,7 @@
"version": "1.20.0", "version": "1.20.0",
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"adm-zip": "^0.6.0", "adm-zip": "^0.6.0",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"react": "^19.1.1", "react": "^19.1.1",
@@ -270,7 +270,7 @@
"dependencies": { "dependencies": {
"@clack/prompts": "^1.1.0", "@clack/prompts": "^1.1.0",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "13.3.1",
"bun-pty": "^0.4.5", "bun-pty": "^0.4.5",
"compression": "^1.8.1", "compression": "^1.8.1",
@@ -1007,7 +1007,7 @@
"@openchamber/web": ["@openchamber/web@workspace:packages/web"], "@openchamber/web": ["@openchamber/web@workspace:packages/web"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.21", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-k6iHQ5C8wOPglk+LgFyYnst168cGMQYumgpbVoeXJ+iC1AtvwD5zmjuF8CxMze/y9G1K2bOeO6p9yRvA7eHZLA=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.23", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-VouYbL8O2ynLq0atr5fjzCv8YLtFi/zKLQ/uYkCvTJ4CmUdA01XwPn8om+u3TvxnGEfQsBfmThGU141vt3sg5w=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="], "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="],
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "openchamber-monorepo", "name": "openchamber-monorepo",
"version": "1.20.0", "version": "1.21.0",
"description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes", "description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -115,7 +115,7 @@
"@heroui/theme": "^2.4.23", "@heroui/theme": "^2.4.23",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -21,6 +21,64 @@ Einige Prompts haben einen sichtbaren Teil (die Nachricht, die du sehen würdest
Anders entschieden? Jeder Prompt hat **Auf Standard zurücksetzen**, und es gibt **Alle zurücksetzen**, wenn du überall neu anfangen möchtest. Anders entschieden? Jeder Prompt hat **Auf Standard zurücksetzen**, und es gibt **Alle zurücksetzen**, wenn du überall neu anfangen möchtest.
## Wo jeder Prompt verwendet wird
Für jeden Prompt steht unten, wo er läuft und was ihn auslöst. Prüfe den Auslöser vor der Bearbeitung, dann weißt du, welchen Ablauf du änderst.
### Git
| Prompt | Wo er läuft | Wann er ausgelöst wird |
| --- | --- | --- |
| Commit-Erstellung | Die Generieren-Schaltfläche im Commit-Feld der Git-Ansicht und im mobilen Changes-Bildschirm | Du erzeugst eine Commit-Nachricht. Ausgewählte Dateien und die letzten Commit-Betreffs des Branchs werden eingesetzt, sodass die Nachricht zum Stil deines Repos passt. |
| PR-Erstellung | Das Pull-Request-Anlegen-Formular im PR-Tab der Git-Ansicht | Du erzeugst Titel und Beschreibung eines PRs. Eingebaut werden Base- und Head-Branch, die Commits und geänderten Dateien dazwischen, dein zusätzlicher Kontext und die PR-Vorlage des Repos, falls vorhanden. |
| Merge/Rebase-Konfliktlösung | Der Konflikt-Dialog der Git-Ansicht, wenn ein Merge oder Rebase auf Konflikten stoppt | Du wählst "Resolve in current session" oder "Resolve in new session". Der Agent liest die konfliktbehafteten Dateien, schlägt eine Lösungsstrategie pro Datei vor und wartet auf deine Bestätigung, bevor er etwas ändert, staged oder fortfährt. |
| Cherry-pick-Konfliktlösung | Der Bereich "Re-integrate commits" einer Worktree-Sitzung | Beim Übertragen der Sitzungs-Commits auf den Zielbranch entsteht ein Konflikt und du übergibst ihn dem Agenten. Der Agent löst im temporären Worktree, staged die Dateien und setzt den Cherry-pick fort. |
### GitHub
| Prompt | Wo er läuft | Wann er ausgelöst wird |
| --- | --- | --- |
| PR-Review | Der "Link GitHub PR"-Picker im Anhänge-Menü des Composers und der neue Worktree-Dialog | Zwei Auslöser. Hängst du einen PR als Kontext an, werden die Anweisungen erzeugt und mit deiner nächsten Nachricht mitgesendet. Startest du eine Worktree-Sitzung aus einem PR, bildet der Prompt die erste Nachricht dieser Sitzung, mit dem vollständigen PR-Kontext. |
| Issue-Review | Der neue Worktree-Dialog, wenn der Worktree aus einem Issue startet | Die erste Nachricht der neuen Sitzung reviewed das Issue, mit Titel, Text und Kommentaren als Kontext. |
| Fehlgeschlagene PR-Checks / PR-Kommentare / einzelner PR-Kommentar | — | Wird heute von keinem Ablauf gesendet. Die PR-Ansicht löste sie früher über Ein-Klick-Review-Aktionen aus; fehlgeschlagene Checks und Kommentare werden jetzt als Chat-Kontext-Entwürfe angeheftet. Sie bleiben editierbar, damit bestehende Overrides weiter funktionieren. |
### Planung
| Prompt | Wo er läuft | Wann er ausgelöst wird |
| --- | --- | --- |
| Todo-Planung | Das Todos-Panel in der Projekt-Seitenleiste | Du schickst ein Todo an eine Sitzung oder eine neue Worktree-Sitzung. Der Todo-Text wird zur sichtbaren Nachricht; die Anweisungen machen daraus einen fragegesteuerten Planungsdialog statt sofort loszulegen. |
| Plan verbessern | Die Aktion "Improve" für einen gespeicherten Plan in der Plans-Ansicht | Du schickst einen gespeicherten Plan in den Verbesserungsfluss. Der Agent liest zuerst die Plandatei, schlägt dann Änderungen auf Basis des aktuellen Repo-Zustands vor und bietet an, dieselbe Datei zu bearbeiten. |
| Plan umsetzen | Die Aktion "Implement" für einen gespeicherten Plan | Du schickst einen gespeicherten Plan in den Umsetzungsfluss. Der Agent liest die Plandatei und setzt sie komplett um, ohne den Rahmen zu sprengen; nötige Plananpassungen schreibt er in dieselbe Datei zurück. |
### Sitzung
Die meisten davon treiben Slash-Befehle an, die du im Composer eingibst. Die meisten erscheinen auch als Starter-Chips im Entwurf einer neuen Sitzung.
| Prompt | Wo er läuft | Wann er ausgelöst wird |
| --- | --- | --- |
| Codebase-Tour | `/explore` | Du möchtest einen Überblick über die Codebase. |
| Sitzungszusammenfassung | `/summary`, optional `/summary <Thema>` | Du fasst die bisherige Konversation zusammen — nützlich zur Übergabe an eine neue Sitzung. Benötigt eine bestehende Sitzung. |
| Workspace-Review | `/workspace-review` | Du lässt den Agenten den aktuellen Workspace-Diff auf Absicht, Korrektheit und Sicherheit prüfen. |
| Feature-Planung | `/plan-feature` | Du machst aus einer groben Feature-Idee über einen geführten Frage-Antwort-Dialog einen Umsetzungsplan. |
| Goal formulieren | `/craft-goal`, optional `/craft-goal <Idee>` | Du machst aus einer Idee ein überprüfbares Goal-Ziel für den Goal-Dialog. |
| Catch-up | `/catch-up` | Du kehrst zu einem Projekt zurück und fragst, wo es steht und wie es weitergeht. |
| Debugging | `/debug` | Du untersuchst einen Bug: Der Agent bildet Hypothesen, bestätigt die Ursache aus dem Code und schlägt erst dann eine Lösung vor. |
| Optionen abwägen | `/weigh` | Du weißt, was du bauen willst, aber nicht wie. Der Agent vergleicht zwei oder drei Ansätze und empfiehlt einen. |
| Fusion | Die Aktion "Run fusion" auf einer Multi-run-Gruppe | Du vereinigst die Ausgaben mehrerer Läufe zu einer Antwort. Die Lauf-Ausgaben werden hinter die Anweisungen angehängt. |
### Prompts ohne Settings-Seite
Einige Prompts laufen automatisch und haben keine editierbare Seite in den Einstellungen:
| Prompt | Wann er ausgelöst wird |
| --- | --- |
| Geplante Aufgabe | `/schedule-task`, optional mit einer ersten Idee. Führt durch den Dialog, der eine geplante Aufgabe definiert. |
| Review-Übergabe | `/handoff-review` oder die Review-Schaltfläche in der Diff-Ansicht mit aktivierter Übergabe. Erzeugt die Übergabe in der Arbeitssitzung. |
| Startnachricht der Review-Sitzung | Die erste Nachricht der erzeugten Review-Sitzung — mit Übergabe, wenn eine erzeugt wurde, sonst ohne. |
| Review-Feedback / Umsetzungsantwort | Bringen Nachrichten zwischen den beiden Sitzungen hin und her: Review-Feedback geht zurück an die umsetzende Sitzung, die Antwort des Umsetzers zurück an die Review-Sitzung. |
## Weiterführend ## Weiterführend
- [Git- & GitHub-Workflows](/git/) — viele dieser Prompts treiben die Git-Abläufe an - [Git- & GitHub-Workflows](/git/) — viele dieser Prompts treiben die Git-Abläufe an
- [Notizen, Todos & Pläne](/notes-todos-plans/) — die Todos und Pläne hinter den Planungs-Prompts
- [Multi-run](/multi-run/) — Laufgruppen und Fusion
@@ -21,6 +21,64 @@ Algunos prompts tienen una parte visible (el mensaje que verías) y una parte de
¿Cambiaste de opinión? Cada prompt tiene **reset to default**, y hay un **reset all** si quieres empezar de cero en todas partes. ¿Cambiaste de opinión? Cada prompt tiene **reset to default**, y hay un **reset all** si quieres empezar de cero en todas partes.
## Dónde se usa cada prompt
Cada prompt de las tablas indica dónde se ejecuta y qué lo dispara. Revisa el disparador antes de editar, para saber qué flujo estás cambiando.
### Git
| Prompt | Dónde se ejecuta | Cuándo se dispara |
| --- | --- | --- |
| Generación de commit | El botón de generar en el cuadro de commit de la vista git, y la pantalla Changes en móvil | Generas un mensaje de commit. Se rellenan los archivos seleccionados y los asuntos de los commits recientes de la rama, para que el mensaje siga el estilo de tu repositorio. |
| Generación de PR | El formulario de creación de pull request en la pestaña PR de la vista git | Generas el título y el cuerpo de un PR. Se rellenan las ramas base y head, los commits y archivos cambiados entre ambas, tu contexto adicional y la plantilla de PR del repositorio si existe. |
| Resolución de conflicto de merge/rebase | El diálogo de conflictos en la vista git, cuando un merge o rebase se detiene por conflictos | Eliges "Resolve in current session" o "Resolve in new session". El agente lee los archivos en conflicto, propone una estrategia por archivo y espera tu confirmación antes de editar, hacer stage o continuar la operación. |
| Resolución de conflicto de cherry-pick | La sección "Re-integrate commits" de una sesión en worktree | Mover los commits de la sesión a la rama destino produce un conflicto y se lo pasas al agente. El agente resuelve dentro del worktree temporal, hace stage de los archivos y continúa el cherry-pick. |
### GitHub
| Prompt | Dónde se ejecuta | Cuándo se dispara |
| --- | --- | --- |
| Revisión de PR | El selector "Link GitHub PR" en el menú de adjuntos del composer, y el diálogo de nuevo worktree | Dos disparadores. Adjuntar un PR como contexto prepara las instrucciones, que se envían con tu siguiente mensaje. Crear una sesión de worktree desde un PR usa el prompt como primer mensaje de esa sesión, con el contexto completo del PR adjunto. |
| Revisión de issue | El diálogo de nuevo worktree, cuando el worktree parte de una issue | El primer mensaje de la nueva sesión revisa la issue, con su cuerpo y comentarios adjuntos como contexto. |
| Revisión de checks fallidos / comentarios de PR / comentario único de PR | — | Hoy no los envía ningún flujo. La vista de PR antes los disparaba con acciones de revisión de un clic; ahora los checks fallidos y los comentarios se fijan como borradores de contexto del chat. Siguen siendo editables para que las anulaciones existentes sigan funcionando. |
### Planning
| Prompt | Dónde se ejecuta | Cuándo se dispara |
| --- | --- | --- |
| Planificación desde todo | El panel Todos en la barra lateral del proyecto | Envías un todo a una sesión o a una nueva sesión en worktree. El texto del todo se convierte en el mensaje visible; las instrucciones lo convierten en un diálogo de planificación con preguntas en vez de saltar a implementar. |
| Mejorar plan | La acción "Improve" sobre un plan guardado en la vista Plans | Envías un plan guardado al flujo de mejora. El agente lee primero el archivo del plan, luego propone cambios basados en el estado actual del repositorio y se ofrece a editar ese mismo archivo. |
| Implementar plan | La acción "Implement" sobre un plan guardado | Envías un plan guardado al flujo de implementación. El agente lee el archivo del plan y lo implementa de principio a fin sin ampliar el alcance, y guarda ajustes del plan en el archivo cuando el propio plan resulta estar mal. |
### Session
La mayoría alimentan comandos de barra que se escriben en el composer. La mayoría también aparecen como chips de inicio en el borrador de una sesión nueva.
| Prompt | Dónde se ejecuta | Cuándo se dispara |
| --- | --- | --- |
| Tour del código | `/explore` | Pides una orientación general del código. |
| Resumen de sesión | `/summary`, opcionalmente `/summary <tema>` | Resumes la conversación hasta ahora, útil para pasar a una sesión nueva. Requiere una sesión existente. |
| Revisión del workspace | `/workspace-review` | Pides al agente revisar el diff actual del workspace en cuanto a intención, corrección y seguridad. |
| Planificación de feature | `/plan-feature` | Conviertes una idea rough de feature en un plan de implementación mediante un diálogo guiado de preguntas y respuestas. |
| Definir Goal | `/craft-goal`, opcionalmente `/craft-goal <idea>` | Conviertes una idea en un objetivo Goal verificable para el diálogo de Goal. |
| Ponerse al día | `/catch-up` | Vuelves a un proyecto y preguntas en qué quedó y qué seguir. |
| Depuración | `/debug` | Investigas un bug: el agente forma hipótesis, confirma la causa raíz desde el código y solo entonces propone un arreglo. |
| Sopesar opciones | `/weigh` | Sabes qué construir pero no cómo. El agente compara dos o tres enfoques y recomienda uno. |
| Fusion | La acción "Run fusion" sobre un grupo de multi-run | Combinas los resultados de varias ejecuciones en una respuesta. Los resultados se añaden después de las instrucciones. |
### Prompts sin página en Settings
Algunos prompts se disparan automáticamente y no tienen página editable en Settings:
| Prompt | Cuándo se dispara |
| --- | --- |
| Tarea programada | `/schedule-task`, opcionalmente con una idea inicial. Guía el diálogo que define una tarea programada. |
| Handoff de revisión | `/handoff-review`, o el botón Review en la vista de diff con el handoff activado. Genera el handoff en la sesión de trabajo. |
| Mensaje inicial de la sesión de revisión | El primer mensaje de la sesión de revisión generada, con el handoff cuando se produjo, o sin él. |
| Feedback de revisión / respuesta de implementación | Llevan mensajes entre las dos sesiones: el feedback del revisor vuelve a la sesión que implementa, y la respuesta del implementador regresa a la sesión de revisión. |
## Relacionado ## Relacionado
- [Flujos de trabajo de Git y GitHub](/es/git/) — muchos de estos prompts impulsan los flujos de git - [Flujos de trabajo de Git y GitHub](/es/git/) — muchos de estos prompts impulsan los flujos de git
- [Notas, todos y planes](/es/notes-todos-plans/) — los todos y planes detrás de los prompts de Planning
- [Multi-run](/es/multi-run/) — grupos de ejecución y fusion
@@ -21,6 +21,64 @@ Certains prompts ont une partie visible (le message que vous verriez) et une par
Vous avez changé davis ? Chaque prompt possède **reset to default**, et il existe aussi **reset all** si vous voulez tout reprendre depuis le début. Vous avez changé davis ? Chaque prompt possède **reset to default**, et il existe aussi **reset all** si vous voulez tout reprendre depuis le début.
## Où chaque prompt est utilisé
Chaque prompt ci-dessous indique où il sexécute et ce qui le déclenche. Vérifiez le déclencheur avant de modifier, pour savoir quel flux vous changez.
### Git
| Prompt | Où il sexécute | Quand il se déclenche |
| --- | --- | --- |
| Génération de commit | Le bouton de génération dans la zone de commit de la vue git, et l’écran Changes sur mobile | Vous générez un message de commit. Les fichiers sélectionnés et les sujets des commits récents de la branche sont insérés, pour que le message respecte le style du dépôt. |
| Génération de PR | Le formulaire de création de pull request dans longlet PR de la vue git | Vous générez le titre et le corps dune PR. Sont insérés les branches base et head, les commits et fichiers modifiés entre elles, votre contexte additionnel et le modèle de PR du dépôt sil existe. |
| Résolution de conflit merge/rebase | Le dialogue de conflits dans la vue git, quand un merge ou un rebase sarrête sur des conflits | Vous choisissez « Resolve in current session » ou « Resolve in new session ». Lagent lit les fichiers en conflit, propose une stratégie par fichier et attend votre confirmation avant de modifier, staging ou poursuivre lopération. |
| Résolution de conflit cherry-pick | La section « Re-integrate commits » dune session en worktree | Le déplacement des commits de la session vers la branche cible rencontre un conflit et vous le confiez à lagent. Lagent résout dans le worktree temporaire, stage les fichiers et poursuit le cherry-pick. |
### GitHub
| Prompt | Où il sexécute | Quand il se déclenche |
| --- | --- | --- |
| Relecture de PR | Le sélecteur « Link GitHub PR » dans le menu de pièces jointes du composer, et le dialogue de nouveau worktree | Deux déclencheurs. Attacher une PR comme contexte prépare les instructions, envoyées avec votre prochain message. Créer une session de worktree depuis une PR utilise le prompt comme premier message de la session, avec le contexte complet de la PR. |
| Relecture dissue | Le dialogue de nouveau worktree, quand le worktree part dune issue | Le premier message de la nouvelle session relit lissue, avec son corps et ses commentaires attachés comme contexte. |
| Relecture de checks échoués / commentaires de PR / commentaire unique de PR | — | Aucun flux ne les envoie aujourdhui. La vue PR les déclenchait avant via des actions de relecture en un clic ; désormais les checks échoués et les commentaires s’épinglent comme brouillons de contexte de chat. Ils restent modifiables pour que les overrides existants continuent de fonctionner. |
### Planning
| Prompt | Où il sexécute | Quand il se déclenche |
| --- | --- | --- |
| Planification depuis un todo | Le panneau Todos dans la barre latérale du projet | Vous envoyez un todo vers une session ou une nouvelle session en worktree. Le texte du todo devient le message visible ; les instructions en font un dialogue de planification guidé par des questions plutôt quun passage direct à limplémentation. |
| Améliorer un plan | Laction « Improve » sur un plan enregistré dans la vue Plans | Vous envoyez un plan enregistré dans le flux damélioration. Lagent lit dabord le fichier du plan, propose ensuite des changements ancrés dans l’état actuel du dépôt et propose de modifier le même fichier. |
| Implémenter un plan | Laction « Implement » sur un plan enregistré | Vous envoyez un plan enregistré dans le flux dimplémentation. Lagent lit le fichier du plan et limplémente de bout en bout sans élargir le périmètre, enregistrant les ajustements dans le fichier quand le plan lui-même savère erroné. |
### Session
La plupart alimentent des commandes slash saisies dans le composer. La plupart apparaissent aussi comme chips de départ sur le brouillon dune nouvelle session.
| Prompt | Où il sexécute | Quand il se déclenche |
| --- | --- | --- |
| Tour du code | `/explore` | Vous demandez une vue densemble du code. |
| Résumé de session | `/summary`, éventuellement `/summary <sujet>` | Vous résumez la conversation en cours — utile pour passer à une nouvelle session. Nécessite une session existante. |
| Relecture du workspace | `/workspace-review` | Vous demandez à lagent de relire le diff actuel du workspace sous langle intention, correction et sécurité. |
| Planification de fonctionnalité | `/plan-feature` | Vous transformez une idée grossière de fonctionnalité en plan dimplémentation via un dialogue guidé de questions-réponses. |
| Formuler un Goal | `/craft-goal`, éventuellement `/craft-goal <idée>` | Vous transformez une idée en objectif Goal vérifiable pour le dialogue Goal. |
| Se remettre dans le bain | `/catch-up` | Vous revenez sur un projet et demandez où en sont les choses et quoi reprendre. |
| Débogage | `/debug` | Vous investiguez un bug : lagent forme des hypothèses, confirme la cause racine dans le code et seulement ensuite propose un correctif. |
| Peser les options | `/weigh` | Vous savez quoi construire mais pas comment. Lagent compare deux ou trois approches et en recommande une. |
| Fusion | Laction « Run fusion » sur un groupe multi-run | Vous combinez les sorties de plusieurs exécutions en une réponse. Les sorties des exécutions sont ajoutées après les instructions. |
### Prompts sans page dans les Paramètres
Quelques prompts se déclenchent automatiquement et nont pas de page modifiable dans les Paramètres :
| Prompt | Quand il se déclenche |
| --- | --- |
| Tâche planifiée | `/schedule-task`, éventuellement avec une idée initiale. Guide le dialogue qui définit une tâche planifiée. |
| Handoff de relecture | `/handoff-review`, ou le bouton Review dans la vue diff avec handoff activé. Génère le handoff dans la session de travail. |
| Premier message de la session de relecture | Le message douverture de la session de relecture générée — avec le handoff quand il a été produit, sans sinon. |
| Retour de relecture / réponse dimplémentation | Font circuler les messages entre les deux sessions : le retour du relecteur revient vers la session qui implémente, et la réponse de limplémenteur repart vers la session de relecture. |
## Pages liées ## Pages liées
- [Workflows Git et GitHub](/git/) — beaucoup de ces prompts alimentent les flux git - [Workflows Git et GitHub](/git/) — beaucoup de ces prompts alimentent les flux git
- [Notes, todos et plans](/notes-todos-plans/) — les todos et plans derrière les prompts Planning
- [Multi-run](/multi-run/) — groupes dexécution et fusion
@@ -21,6 +21,64 @@ OpenChamber は、コミットメッセージの作成、PR の下書き、Issue
気が変わりましたか?各プロンプトには **reset to default** があり、すべてを最初からやり直したい場合は **reset all** もあります。 気が変わりましたか?各プロンプトには **reset to default** があり、すべてを最初からやり直したい場合は **reset all** もあります。
## 各プロンプトが使われる場所
以下の表は、各プロンプトがどこで実行され、何がきっかけで動くかを示します。編集前にトリガーを確認し、どのフローを変えるのかを把握してください。
### Git
| プロンプト | 実行される場所 | 動くタイミング |
| --- | --- | --- |
| コミット生成 | git ビューのコミット欄にある生成ボタン、およびモバイルの Changes 画面 | コミットメッセージを生成するとき。選択したファイルとブランチの直近コミットの件名が差し込まれ、メッセージがリポジトリの既存スタイルに合います。 |
| PR 生成 | git ビュー PR タブの pull request 作成フォーム | PR のタイトルと本文を生成するとき。base と head ブランチ、その間のコミットと変更ファイル、追加コンテキスト、リポジトリに PR テンプレートがあればそれも差し込まれます。 |
| merge/rebase コンフリクト解決 | merge や rebase がコンフリクトで止まったときの git ビューのコンフリクトダイアログ | "Resolve in current session" または "Resolve in new session" を選んだとき。エージェントはコンフリクトファイルを読み、ファイルごとの解決戦略を提案し、編集・stage・操作の再開の前に確認を待ちます。 |
| cherry-pick コンフリクト解決 | worktree セッションの "Re-integrate commits" セクション | セッションのコミットを対象ブランチへ移す途中でコンフリクトが起き、エージェントに任せたとき。エージェントは一時 worktree の中で解決し、ファイルを stage して cherry-pick を続けます。 |
### GitHub
| プロンプト | 実行される場所 | 動くタイミング |
| --- | --- | --- |
| PR レビュー | composer の添付メニューにある "Link GitHub PR" ピッカー、および新規 worktree ダイアログ | 2 つのトリガー。PR をコンテキストとして添付すると instructions が用意され、次のメッセージと一緒に送られます。PR から worktree セッションを始めると、このプロンプトがそのセッションの最初のメッセージになり、PR の完全なコンテキストが添付されます。 |
| Issue レビュー | Issue から worktree を作るときの新規 worktree ダイアログ | 新しいセッションの最初のメッセージが Issue をレビューし、本文とコメントがコンテキストとして添付されます。 |
| PR の失敗チェック / PR コメント / 個別 PR コメント | — | 現在はどのフローからも送信されません。以前は PR ビューのワンクリックレビューアクションから起動されましたが、今は失敗チェックとコメントがチャットコンテキストの下書きとしてピン留めされます。既存のオーバーライドが機能し続けるよう、編集可能なまま残っています。 |
### Planning
| プロンプト | 実行される場所 | 動くタイミング |
| --- | --- | --- |
| todo からの計画 | プロジェクトサイドバーの Todos パネル | todo をセッションまたは新しい worktree セッションへ送るとき。todo のテキストが見えるメッセージになり、instructions は実装へ飛ばず、質問主体の計画対話に変えます。 |
| 計画の改善 | Plans ビューの保存済み計画に対する "Improve" アクション | 保存済み計画を改善フローへ送るとき。エージェントはまず計画ファイルを読み、リポジトリの現在の状態に即した変更を提案し、同じファイルの編集を申し出ます。 |
| 計画の実装 | 保存済み計画に対する "Implement" アクション | 保存済み計画を実装フローへ送るとき。エージェントは計画ファイルを読み、スコープを広げずに最後まで実装し、計画自体に誤りが見つかった場合は調整を同じファイルへ保存します。 |
### Session
これらの多くは、composer に入力するスラッシュコマンドとして動きます。多くは新しいセッションの下書き画面でスターターチップとしても表示されます。
| プロンプト | 実行される場所 | 動くタイミング |
| --- | --- | --- |
| コードベースツアー | `/explore` | コードベースの概要を把握したいとき。 |
| セッション要約 | `/summary`、オプションで `/summary <トピック>` | ここまでの会話を要約します。新しいセッションへの引き継ぎに便利です。既存のセッションが必要です。 |
| ワークスペースレビュー | `/workspace-review` | 現在のワークスペース差分を意図・正確性・セキュリティの観点でレビューしてほしいとき。 |
| 機能計画 | `/plan-feature` | 大まかな機能アイデアを、質疑応答の対話を通じて実装計画に変えたいとき。 |
| Goal 作成 | `/craft-goal`、オプションで `/craft-goal <アイデア>` | アイデアを、Goal ダイアログで使える検証可能な Goal 目標に変えたいとき。 |
| キャッチアップ | `/catch-up` | プロジェクトに戻って、どこまで進んでいて次に何をするか知りたいとき。 |
| デバッグ | `/debug` | バグを調査するとき。エージェントは仮説を立て、コードから根本原因を確認してから修正を提案します。 |
| 選択肢の比較 | `/weigh` | 何を作るかは分かっているが作り方が分からないとき。エージェントが 2〜3 のアプローチを比較し、1 つを推奨します。 |
| Fusion | multi-run グループの "Run fusion" アクション | 複数ランの出力を 1 つの回答にまとめるとき。ランの出力は instructions の後に続けて添付されます。 |
### Settings にページのないプロンプト
一部のプロンプトは自動的に動き、Settings には編集ページがありません:
| プロンプト | 動くタイミング |
| --- | --- |
| スケジュールタスク | `/schedule-task`、オプションで初期アイデアと一緒に。スケジュールタスクを定義する対話を進めます。 |
| レビュー用ハンドオフ | `/handoff-review`、またはハンドオフを有効にした diff ビューの Review ボタン。作業セッション内でハンドオフを生成します。 |
| レビューセッションの開始メッセージ | 生成されたレビューセッションの最初のメッセージ。ハンドオフが作られた場合はそれを含み、なければ含みません。 |
| レビューフィードバック / 実装応答 | 2 つのセッションの間でメッセージを運びます。レビュアーのフィードバックは実装セッションへ、実装者の応答はレビューセッションへ戻ります。 |
## 関連 ## 関連
- [Git と GitHub ワークフロー](/git/) — これらのプロンプトの多くが Git フローを支えています - [Git と GitHub ワークフロー](/git/) — これらのプロンプトの多くが Git フローを支えています
- [ノート、todo と計画](/notes-todos-plans/) — Planning プロンプトの背後にある todo と計画
- [Multi-run](/multi-run/) — ラングループと fusion
@@ -21,6 +21,64 @@ OpenChamber는 커밋 메시지 작성, PR 초안 작성, 이슈 검토, 충돌
마음이 바뀌었나요? 각 프롬프트에는 **reset to default**가 있고, 모든 곳에서 처음부터 다시 시작하려면 **reset all**이 있습니다. 마음이 바뀌었나요? 각 프롬프트에는 **reset to default**가 있고, 모든 곳에서 처음부터 다시 시작하려면 **reset all**이 있습니다.
## 각 프롬프트가 사용되는 곳
아래 표의 각 프롬프트는 실행되는 위치와 실행을 일으키는 트리거를 나타냅니다. 편집하기 전에 트리거를 확인해 어떤 흐름을 바꾸는지 알아두세요.
### Git
| 프롬프트 | 실행 위치 | 트리거 시점 |
| --- | --- | --- |
| 커밋 생성 | git 뷰 커밋 상자의 생성 버튼, 모바일 Changes 화면 | 커밋 메시지를 생성할 때. 선택한 파일과 브랜치의 최근 커밋 제목이 채워져 메시지가 저장소 스타일을 따르게 됩니다. |
| PR 생성 | git 뷰 PR 탭의 pull request 생성 폼 | PR 제목과 본문을 생성할 때. base와 head 브랜치, 그 사이의 커밋과 변경 파일, 추가 컨텍스트, 저장소에 PR 템플릿이 있으면 그것까지 채워집니다. |
| merge/rebase 충돌 해결 | merge나 rebase가 충돌로 멈췄을 때 git 뷰의 충돌 대화상자 | "Resolve in current session" 또는 "Resolve in new session"을 선택할 때. 에이전트가 충돌 파일을 읽고 파일별 해결 전략을 제안하며, 편집·stage·계속 진행 전에 확인을 기다립니다. |
| cherry-pick 충돌 해결 | worktree 세션의 "Re-integrate commits" 섹션 | 세션의 커밋을 대상 브랜치로 옮기다 충돌이 나서 에이전트에 맡길 때. 에이전트가 임시 worktree에서 해결하고 파일을 stage한 뒤 cherry-pick을 계속합니다. |
### GitHub
| 프롬프트 | 실행 위치 | 트리거 시점 |
| --- | --- | --- |
| PR 검토 | 작성기 첨부 메뉴의 "Link GitHub PR" 선택기, 새 worktree 대화상자 | 두 가지 트리거. PR을 컨텍스트로 첨부하면 지침이 준비되어 다음 메시지와 함께 전송됩니다. PR에서 worktree 세션을 시작하면 이 프롬프트가 그 세션의 첫 메시지가 되고 전체 PR 컨텍스트가 첨부됩니다. |
| 이슈 검토 | worktree를 이슈에서 시작할 때의 새 worktree 대화상자 | 새 세션의 첫 메시지가 이슈를 검토하며, 본문과 댓글이 컨텍스트로 첨부됩니다. |
| PR 실패 검사 / PR 댓글 / 단일 PR 댓글 검토 | — | 현재 어떤 흐름도 이것들을 보내지 않습니다. PR 뷰가 이전에는 원클릭 검토 액션으로 실행했지만, 이제 실패한 검사와 댓글은 채팅 컨텍스트 초안으로 고정됩니다. 기존 재정의가 계속 동작하도록 편집 가능한 상태로 남습니다. |
### Planning
| 프롬프트 | 실행 위치 | 트리거 시점 |
| --- | --- | --- |
| todo 계획 | 프로젝트 사이드바의 Todos 패널 | todo를 세션 또는 새 worktree 세션으로 보낼 때. todo 텍스트가 보이는 메시지가 되고, 지침은 바로 구현으로 넘어가지 않고 질문 중심의 계획 대화로 만듭니다. |
| 계획 개선 | Plans 뷰에서 저장된 계획의 "Improve" 액션 | 저장된 계획을 개선 흐름으로 보낼 때. 에이전트가 먼저 계획 파일을 읽고, 저장소 현재 상태에 근거한 변경을 제안하며 같은 파일을 편집하겠다고 제안합니다. |
| 계획 구현 | 저장된 계획의 "Implement" 액션 | 저장된 계획을 구현 흐름으로 보낼 때. 에이전트가 계획 파일을 읽고 범위를 늘리지 않고 끝까지 구현하며, 계획 자체가 잘못된 것으로 밝혀지면 조정을 같은 파일에 저장합니다. |
### Session
대부분 작성기에 입력하는 슬래시 명령으로 동작합니다. 대부분 새 세션 초안 화면의 시작 칩으로도 나타납니다.
| 프롬프트 | 실행 위치 | 트리거 시점 |
| --- | --- | --- |
| 코드베이스 투어 | `/explore` | 코드베이스의 전체 개요를 요청할 때. |
| 세션 요약 | `/summary`, 선택적으로 `/summary <주제>` | 지금까지의 대화를 요약할 때 — 새 세션으로 넘길 때 유용합니다. 기존 세션이 필요합니다. |
| 작업 공간 검토 | `/workspace-review` | 현재 작업 공간 diff를 의도, 정확성, 보안 관점에서 검토해 달라고 요청할 때. |
| 기능 계획 | `/plan-feature` | 거친 기능 아이디어를 안내된 질문-답변 대화를 통해 구현 계획으로 만들 때. |
| Goal 만들기 | `/craft-goal`, 선택적으로 `/craft-goal <아이디어>` | 아이디어를 Goal 대화상자에 쓸 수 있는 검증 가능한 Goal 목표로 바꿀 때. |
| 따라잡기 | `/catch-up` | 프로젝트로 돌아와 어디까지 진행됐고 다음에 무엇을 할지 물을 때. |
| 디버깅 | `/debug` | 버그를 조사할 때: 에이전트가 가설을 세우고 코드에서 근본 원인을 확인한 뒤에야 수정을 제안합니다. |
| 옵션 저울질 | `/weigh` | 무엇을 만들지는 알지만 어떻게 할지 모를 때. 에이전트가 두세 가지 접근을 비교하고 하나를 추천합니다. |
| Fusion | multi-run 그룹의 "Run fusion" 액션 | 여러 실행의 출력을 하나의 답변으로 합칠 때. 실행 출력은 지침 뒤에 추가됩니다. |
### Settings에 페이지가 없는 프롬프트
일부 프롬프트는 자동으로 실행되며 Settings에 편집 가능한 페이지가 없습니다:
| 프롬프트 | 트리거 시점 |
| --- | --- |
| 예약 작업 | `/schedule-task`, 선택적으로 초기 아이디어와 함께. 예약 작업을 정의하는 대화를 이끕니다. |
| 검토 핸드오프 | `/handoff-review`, 또는 핸드오프를 켜고 diff 뷰의 Review 버튼. 작업 세션에서 핸드오프를 생성합니다. |
| 검토 세션 시작 메시지 | 생성된 검토 세션의 첫 메시지 — 핸드오프가 만들어졌으면 포함, 아니면 제외. |
| 검토 피드백 / 구현 응답 | 두 세션 사이에서 메시지를 전달합니다: 검토자 피드백은 구현 세션으로, 구현자 응답은 검토 세션으로 돌아갑니다. |
## 관련 항목 ## 관련 항목
- [Git & GitHub Workflows](/ko/git/) — 이러한 프롬프트 중 다수가 git 흐름을 구동합니다 - [Git & GitHub Workflows](/ko/git/) — 이러한 프롬프트 중 다수가 git 흐름을 구동합니다
- [노트, todo와 계획](/ko/notes-todos-plans/) — Planning 프롬프트 뒤에 있는 todo와 계획
- [Multi-run](/ko/multi-run/) — 실행 그룹과 fusion
@@ -21,6 +21,64 @@ Some prompts have a visible part (the message you'd see) and an instructions par
Changed your mind? Each prompt has **reset to default**, and there's a **reset all** if you want to start over everywhere. Changed your mind? Each prompt has **reset to default**, and there's a **reset all** if you want to start over everywhere.
## Where each prompt is used
Every prompt below lists where it runs and the trigger that fires it. Check the trigger before editing, so you know which flow you're changing.
### Git
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Commit generation | The generate button in the git view's commit box, and the mobile Changes screen | You generate a commit message. The selected files and the branch's recent commit subjects are filled in, so the subject matches your repo's existing style. |
| PR generation | The create-pull-request form in the git view's PR tab | You generate a PR title and body. Filled with the base and head branches, the commits and changed files between them, your additional context, and the repo's PR template when one exists. |
| Merge/rebase conflict resolution | The conflicts dialog in the git view, when a merge or rebase stops on conflicts | You pick "Resolve in current session" or "Resolve in new session". The agent reads the conflicted files, proposes a per-file resolution strategy, and waits for your confirmation before editing, staging, or continuing the operation. |
| Cherry-pick conflict resolution | The "Re-integrate commits" section for a worktree session | Moving the session's commits onto the target branch hits a conflict and you hand it to the agent. The agent resolves inside the temporary worktree, stages the resolved files, and continues the cherry-pick. |
### GitHub
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| PR review | The "Link GitHub PR" picker in the composer's attach menu, and the new worktree dialog | Two triggers. Attaching a PR as context renders the instructions, which go out with your next message. Starting a worktree session from a PR uses the prompt as that session's opening message, with the full PR context attached. |
| Issue review | The new worktree dialog, when you start the worktree from an issue | The new session's opening message reviews the issue, with its body and comments attached as context. |
| PR failed checks / PR comments / single PR comment | — | Not sent by any flow today. The PR view used to fire these from one-click review actions; failed checks and comments now pin as chat-context drafts instead. They stay editable so existing overrides keep working. |
### Planning
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Todo planning | The Todos panel in the project sidebar | You send a todo to a session or a new worktree session. The todo text becomes the visible message; the instructions turn it into a question-first planning dialogue instead of jumping straight to implementation. |
| Improve plan | The "Improve" action on a saved plan in the Plans view | You send a saved plan into an improve flow. The agent reads the plan file first, then proposes changes grounded in the current repo state and offers to edit the same file. |
| Implement plan | The "Implement" action on a saved plan | You send a saved plan into an implement flow. The agent reads the plan file and implements it end to end without expanding scope, saving plan adjustments back to the file when the plan itself turns out to be wrong. |
### Session
Most of these power slash commands typed in the composer. Most also appear as starter chips on a new-session draft.
| Prompt | Where it runs | When it fires |
| --- | --- | --- |
| Codebase tour | `/explore` | You ask for a high-level orientation of the codebase. |
| Session summary | `/summary`, optionally `/summary <topic>` | You summarize the conversation so far, useful for handing off to a new session. Needs an existing session. |
| Workspace review | `/workspace-review` | You ask the agent to review the current workspace diff for intent, correctness, and security. |
| Feature planning | `/plan-feature` | You turn a rough feature idea into an implementation plan through a guided question-and-answer dialogue. |
| Goal crafting | `/craft-goal`, optionally `/craft-goal <idea>` | You turn an idea into a verifiable Goal objective for the Goal dialog. |
| Catch up | `/catch-up` | You return to a project and ask where things stand and what to pick up next. |
| Debugging | `/debug` | You investigate a bug: the agent forms hypotheses, confirms the root cause from the code, and only then proposes a fix. |
| Weigh options | `/weigh` | You know what you want to build but not how. The agent compares two or three approaches and recommends one. |
| Fusion | The "Run fusion" action on a multi-run group | You combine the outputs of several runs into one answer. The run outputs are appended after the instructions. |
### Prompts without a Settings entry
A few prompts fire automatically and have no editable page in Settings:
| Prompt | When it fires |
| --- | --- |
| Scheduled task | `/schedule-task`, optionally with an initial idea. Guides the dialogue that defines a scheduled task. |
| Review handoff | `/handoff-review`, or the Review button in the diff view with handoff enabled. Generates the handoff in the working session. |
| Review session starter | The opening message of the generated review session, with the handoff when one was produced and without it otherwise. |
| Review feedback / implementation response | Shuttle messages between the two sessions: reviewer feedback goes back to the implementing session, and the implementer's response returns to the review session. |
## Related ## Related
- [Git & GitHub Workflows](/git/) — many of these prompts power the git flows - [Git & GitHub Workflows](/git/) — many of these prompts power the git flows
- [Notes, Todos & Plans](/notes-todos-plans/) — the todos and plans behind the Planning prompts
- [Multi-run](/multi-run/) — run groups and fusion
@@ -21,6 +21,64 @@ Niektóre prompty mają część widoczną (wiadomość, którą zobaczysz) i cz
Zmieniłeś zdanie? Każdy prompt ma **reset to default**, a jest też **reset all**, jeśli chcesz zacząć wszystko od nowa. Zmieniłeś zdanie? Każdy prompt ma **reset to default**, a jest też **reset all**, jeśli chcesz zacząć wszystko od nowa.
## Gdzie używany jest każdy prompt
Dla każdego prompta poniżej podano, gdzie się wykonuje i co go uruchamia. Sprawdź wyzwalacz przed edycją, żeby wiedzieć, który przepływ zmieniasz.
### Git
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
| --- | --- | --- |
| Generowanie commita | Przycisk generowania w polu commita w widoku git oraz ekran Changes na mobile | Generujesz komunikat commita. Wstawiane są wybrane pliki i tematy ostatnich commitów gałęzi, dzięki czemu komunikat trzyma styl twojego repozytorium. |
| Generowanie PR | Formularz tworzenia pull requesta w zakładce PR widoku git | Generujesz tytuł i treść PR. Wstawiane są gałęzie base i head, commity i zmienione pliki między nimi, twój dodatkowy kontekst oraz szablon PR repozytorium, jeśli istnieje. |
| Rozwiązywanie konfliktu merge/rebase | Okno konfliktów w widoku git, gdy merge lub rebase zatrzyma się na konfliktach | Wybierasz "Resolve in current session" albo "Resolve in new session". Agent czyta pliki z konfliktem, proponuje strategię dla każdego pliku i czeka na twoje potwierdzenie przed edycją, stage'owaniem lub kontynuowaniem operacji. |
| Rozwiązywanie konfliktu cherry-pick | Sekcja "Re-integrate commits" sesji w worktree | Przenoszenie commitów sesji na gałąź docelową trafia na konflikt i przekazujesz go agentowi. Agent rozwiązuje konflikty w tymczasowym worktree, robi stage plików i kontynuuje cherry-pick. |
### GitHub
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
| --- | --- | --- |
| Review PR | Selektor "Link GitHub PR" w menu załączników kompozytora oraz okno nowego worktree | Dwa wyzwalacze. Przypięcie PR jako kontekstu przygotowuje instrukcje, które wychodzą z twoją następną wiadomością. Utworzenie sesji worktree z PR używa prompta jako pierwszej wiadomości tej sesji, z pełnym kontekstem PR w załączeniu. |
| Review issue | Okno nowego worktree, gdy worktree startuje z issue | Pierwsza wiadomość nowej sesji przegląda issue, z jej treścią i komentarzami jako kontekstem. |
| Review nieudanych checków PR / komentarzy PR / pojedynczego komentarza PR | — | Dziś żaden przepływ ich nie wysyła. Widok PR uruchamiał je kiedyś akcjami review jednym kliknięciem; teraz nieudane checki i komentarze są przypinane jako szkice kontekstu czatu. Zostają edytowalne, aby istniejące nadpisania dalej działały. |
### Planning
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
| --- | --- | --- |
| Planowanie z todo | Panel Todos w pasku bocznym projektu | Wysyłasz todo do sesji albo nowej sesji w worktree. Tekst todo staje się widoczną wiadomością; instrukcje zamieniają go w planistyczny dialog oparty na pytaniach, zamiast skakać od razu do implementacji. |
| Ulepsz plan | Akcja "Improve" na zapisanym planie w widoku Plans | Wysyłasz zapisany plan do przepływu ulepszania. Agent najpierw czyta plik planu, potem proponuje zmiany zakorzenione w aktualnym stanie repozytorium i proponuje edycję tego samego pliku. |
| Zaimplementuj plan | Akcja "Implement" na zapisanym planie | Wysyłasz zapisany plan do przepływu implementacji. Agent czyta plik planu i implementuje go od początku do końca bez rozszerzania zakresu, zapisując korekty planu z powrotem do pliku, gdy sam plan okaże się błędny. |
### Session
Większość z nich zasila komendy z ukośnikiem wpisywane w kompozytorze. Większość pojawia się też jako startowe chipy na szkicu nowej sesji.
| Prompt | Gdzie się wykonuje | Kiedy się uruchamia |
| --- | --- | --- |
| Tour po kodzie | `/explore` | Prosisz o ogólną orientację w bazie kodu. |
| Podsumowanie sesji | `/summary`, opcjonalnie `/summary <temat>` | Podsumowujesz dotychczasową rozmowę — przydatne do przekazania do nowej sesji. Wymaga istniejącej sesji. |
| Review workspace | `/workspace-review` | Prosisz agenta o przegląd aktualnego diffu workspace pod kątem intencji, poprawności i bezpieczeństwa. |
| Planowanie funkcji | `/plan-feature` | Zamieniasz surowy pomysł na funkcję w plan implementacji przez prowadzony dialog pytań i odpowiedzi. |
| Formułowanie Goal | `/craft-goal`, opcjonalnie `/craft-goal <pomysł>` | Zamieniasz pomysł w weryfikowalny cel Goal do okna Goal. |
| Nadrobienie bieżące | `/catch-up` | Wracasz do projektu i pytasz, na czym stanęło i co dalej. |
| Debugowanie | `/debug` | Badasz buga: agent stawia hipotezy, potwierdza przyczynę źródłową w kodzie i dopiero wtedy proponuje poprawkę. |
| Ważenie opcji | `/weigh` | Wiesz, co zbudować, ale nie jak. Agent porównuje dwa-trzy podejścia i poleca jedno. |
| Fusion | Akcja "Run fusion" na grupie multi-run | Łączysz wyniki kilku uruchomień w jedną odpowiedź. Wyniki uruchomień są doklejane po instrukcjach. |
### Prompty bez strony w Settings
Kilka promptów uruchamia się automatycznie i nie ma edytowalnej strony w Settings:
| Prompt | Kiedy się uruchamia |
| --- | --- |
| Zaplanowane zadanie | `/schedule-task`, opcjonalnie z początkowym pomysłem. Prowadzi dialog, który definiuje zaplanowane zadanie. |
| Handoff do review | `/handoff-review` albo przycisk Review w widoku diff z włączonym handoffem. Generuje handoff w sesji roboczej. |
| Wiadomość startowa sesji review | Pierwsza wiadomość wygenerowanej sesji review — z handoffem, gdy powstał, bez niego w przeciwnym razie. |
| Feedback z review / odpowiedź implementacji | Przenoszą wiadomości między dwiema sesjami: feedback recenzenta wraca do sesji implementującej, a odpowiedź implementatora wraca do sesji review. |
## Powiązane ## Powiązane
- [Przepływy Git i GitHub](/pl/git/) — wiele z tych promptów napędza przepływy git - [Przepływy Git i GitHub](/pl/git/) — wiele z tych promptów napędza przepływy git
- [Notatki, todo i plany](/pl/notes-todos-plans/) — todo i plany stojące za promptami Planning
- [Multi-run](/pl/multi-run/) — grupy uruchomień i fusion
@@ -21,6 +21,64 @@ Alguns prompts têm uma parte visível (a mensagem que você veria) e uma parte
Mudou de ideia? Cada prompt tem **reset to default**, e há um **reset all** se você quiser recomeçar em tudo. Mudou de ideia? Cada prompt tem **reset to default**, e há um **reset all** se você quiser recomeçar em tudo.
## Onde cada prompt é usado
Cada prompt nas tabelas abaixo indica onde ele roda e o que o dispara. Confira o gatilho antes de editar, para saber qual fluxo você está mudando.
### Git
| Prompt | Onde roda | Quando dispara |
| --- | --- | --- |
| Geração de commit | O botão de gerar na caixa de commit da vista git, e a tela Changes no mobile | Você gera uma mensagem de commit. São preenchidos os arquivos selecionados e os assuntos dos commits recentes do branch, para a mensagem seguir o estilo do seu repositório. |
| Geração de PR | O formulário de criação de pull request na aba PR da vista git | Você gera título e corpo de um PR. São preenchidos os branches base e head, os commits e arquivos alterados entre eles, o contexto adicional que você escreveu e o template de PR do repositório, quando existe. |
| Resolução de conflito de merge/rebase | O diálogo de conflitos na vista git, quando um merge ou rebase para em conflitos | Você escolhe "Resolve in current session" ou "Resolve in new session". O agente lê os arquivos em conflito, propõe uma estratégia por arquivo e aguarda sua confirmação antes de editar, fazer stage ou continuar a operação. |
| Resolução de conflito de cherry-pick | A seção "Re-integrate commits" de uma sessão em worktree | Mover os commits da sessão para o branch de destino esbarra em um conflito e você passa para o agente. O agente resolve dentro do worktree temporário, faz stage dos arquivos e continua o cherry-pick. |
### GitHub
| Prompt | Onde roda | Quando dispara |
| --- | --- | --- |
| Revisão de PR | O seletor "Link GitHub PR" no menu de anexos do composer, e o diálogo de novo worktree | Dois gatilhos. Anexar um PR como contexto renderiza as instruções, que saem com a sua próxima mensagem. Criar uma sessão de worktree a partir de um PR usa o prompt como primeira mensagem dessa sessão, com o contexto completo do PR anexado. |
| Revisão de issue | O diálogo de novo worktree, quando o worktree parte de uma issue | A primeira mensagem da nova sessão revisa a issue, com o corpo e os comentários anexados como contexto. |
| Revisão de checks falhos / comentários de PR / comentário único de PR | — | Hoje nenhum fluxo os envia. A vista de PR antes os disparava com ações de revisão de um clique; agora checks falhos e comentários são fixados como rascunhos de contexto do chat. Continuam editáveis para que overrides existentes mantenham efeito. |
### Planning
| Prompt | Onde roda | Quando dispara |
| --- | --- | --- |
| Planejamento a partir de todo | O painel Todos na barra lateral do projeto | Você envia um todo para uma sessão ou uma nova sessão em worktree. O texto do todo vira a mensagem visível; as instruções transformam isso em um diálogo de planejamento guiado por perguntas, em vez de pular direto para a implementação. |
| Melhorar plano | A ação "Improve" sobre um plano salvo na vista Plans | Você envia um plano salvo para o fluxo de melhoria. O agente lê primeiro o arquivo do plano, propõe mudanças ancoradas no estado atual do repositório e se oferece para editar o mesmo arquivo. |
| Implementar plano | A ação "Implement" sobre um plano salvo | Você envia um plano salvo para o fluxo de implementação. O agente lê o arquivo do plano e o implementa do início ao fim sem ampliar o escopo, salvando ajustes do plano no arquivo quando o próprio plano se mostra errado. |
### Session
A maioria alimenta comandos de barra digitados no composer. A maioria também aparece como chips de partida no rascunho de nova sessão.
| Prompt | Onde roda | Quando dispara |
| --- | --- | --- |
| Tour pelo código | `/explore` | Você pede uma orientação geral do código. |
| Resumo de sessão | `/summary`, opcionalmente `/summary <tópico>` | Você resume a conversa até aqui — útil para passar para uma nova sessão. Precisa de uma sessão existente. |
| Revisão do workspace | `/workspace-review` | Você pede ao agente para revisar o diff atual do workspace quanto a intenção, correção e segurança. |
| Planejamento de feature | `/plan-feature` | Você transforma uma ideia grosseira de feature em um plano de implementação por um diálogo guiado de perguntas e respostas. |
| Construir Goal | `/craft-goal`, opcionalmente `/craft-goal <ideia>` | Você transforma uma ideia em um objetivo Goal verificável para o diálogo de Goal. |
| Retomar o fio | `/catch-up` | Você volta a um projeto e pergunta onde as coisas pararam e o que fazer a seguir. |
| Depuração | `/debug` | Você investiga um bug: o agente levanta hipóteses, confirma a causa raiz no código e só então propõe uma correção. |
| Pesar opções | `/weigh` | Você sabe o que construir, mas não como. O agente compara duas ou três abordagens e recomenda uma. |
| Fusion | A ação "Run fusion" em um grupo de multi-run | Você combina as saídas de várias execuções em uma resposta. As saídas das execuções são anexadas depois das instruções. |
### Prompts sem página no Settings
Alguns prompts disparam automaticamente e não têm página editável no Settings:
| Prompt | Quando dispara |
| --- | --- |
| Tarefa agendada | `/schedule-task`, opcionalmente com uma ideia inicial. Conduz o diálogo que define uma tarefa agendada. |
| Handoff de revisão | `/handoff-review`, ou o botão Review na vista de diff com handoff ativado. Gera o handoff na sessão de trabalho. |
| Mensagem inicial da sessão de revisão | A primeira mensagem da sessão de revisão gerada — com o handoff quando um foi produzido, sem ele caso contrário. |
| Feedback de revisão / resposta de implementação | Levam mensagens entre as duas sessões: o feedback do revisor volta para a sessão que implementa, e a resposta do implementador retorna à sessão de revisão. |
## Relacionado ## Relacionado
- [Fluxos de Git e GitHub](/pt-br/git/) — muitos desses prompts alimentam os fluxos de git - [Fluxos de Git e GitHub](/pt-br/git/) — muitos desses prompts alimentam os fluxos de git
- [Notas, todos e planos](/pt-br/notes-todos-plans/) — os todos e planos por trás dos prompts de Planning
- [Multi-run](/pt-br/multi-run/) — grupos de execução e fusion
@@ -21,6 +21,64 @@ OpenChamber використовує вбудовані промпти за ла
Передумали? Кожен промпт має **reset to default**, а ще є **reset all**, якщо хочете почати спочатку всюди. Передумали? Кожен промпт має **reset to default**, а ще є **reset all**, якщо хочете почати спочатку всюди.
## Де використовується кожен промпт
Для кожного промпту нижче вказано, де він виконується та який тригер його запускає. Перевірте тригер перед редагуванням, щоб розуміти, який процес ви змінюєте.
### Git
| Промпт | Де виконується | Коли спрацьовує |
| --- | --- | --- |
| Генерація коміту | Кнопка генерації в полі коміту у git-поданні та на мобільному екрані Changes | Ви генеруєте повідомлення коміту. Підставляються вибрані файли та теми нещодавніх комітів гілки, щоб стиль відповідав вашому репозиторію. |
| Генерація PR | Форма створення pull request у вкладці PR git-подання | Ви генеруєте заголовок і опис PR. Підставляються базова та головна гілки, коміти та змінені файли між ними, ваш додатковий контекст і PR-шаблон репозиторію, якщо він є. |
| Розв'язання конфліктів merge/rebase | Діалог конфліктів у git-поданні, коли merge або rebase зупинився на конфліктах | Ви обираєте "Resolve in current session" або "Resolve in new session". Агент читає конфліктні файли, пропонує стратегію розв'язання для кожного файлу та чекає на ваше підтвердження перед редагуванням, індексацією чи продовженням операції. |
| Розв'язання конфліктів cherry-pick | Розділ "Re-integrate commits" для сесії у worktree | Перенесення комітів сесії на цільову гілку впирається в конфлікт, і ви передаєте його агентові. Агент розв'язує конфлікти в тимчасовому worktree, індексує файли та продовжує cherry-pick. |
### GitHub
| Промпт | Де виконується | Коли спрацьовує |
| --- | --- | --- |
| Рев'ю PR | Пікер "Link GitHub PR" у меню вкладень композера та діалог нового worktree | Два тригери. Прикріплення PR як контексту готує інструкції, які надсилаються разом із вашим наступним повідомленням. Створення сесії у worktree з PR використовує промпт як перше повідомлення сесії з повним контекстом PR. |
| Рев'ю issue | Діалог нового worktree, коли worktree створюється з issue | Перше повідомлення нової сесії рев'ю issue, з тілом і коментарями як контекстом. |
| Рев'ю провалених перевірок PR / коментарів PR / окремого коментаря PR | — | Нині не надсилаються жодним процесом. Подання PR раніше запускало їх кнопками швидкого рев'ю; тепер провалені перевірки й коментарі прикріплюються як чернетки контексту чату. Вони лишаються редагованими, щоб наявні перевизначення продовжували працювати. |
### Planning
| Промпт | Де виконується | Коли спрацьовує |
| --- | --- | --- |
| Планування з todo | Панель Todos у проєктній бічній панелі | Ви надсилаєте todo в сесію або нову сесію у worktree. Текст todo стає видимим повідомленням; інструкції перетворюють його на планувальний діалог із питаннями, а не стрибок одразу в імплементацію. |
| Покращення плану | Дія "Improve" для збереженого плану у поданні Plans | Ви надсилаєте збережений план у потік покращення. Агент спершу читає файл плану, потім пропонує зміни на основі поточного стану репозиторію та пропонує відредагувати той самий файл. |
| Імплементація плану | Дія "Implement" для збереженого плану | Ви надсилаєте збережений план у потік імплементації. Агент читає файл плану та імплементує його від початку до кінця без розширення обсягу, зберігаючи корективи плану назад у файл, якщо сам план виявився хибним. |
### Session
Більшість із них живлять слеш-команди, які вводяться в композері. Більшість також доступні як стартові чіпи на чернетці нової сесії.
| Промпт | Де виконується | Коли спрацьовує |
| --- | --- | --- |
| Тур кодовою базою | `/explore` | Ви просите загальний огляд кодової бази. |
| Підсумок сесії | `/summary`, необов'язково `/summary <тема>` | Ви підсумовуєте поточну розмову — зручно для передачі в нову сесію. Потрібна наявна сесія. |
| Рев'ю робочої області | `/workspace-review` | Ви просите агента переглянути поточний diff робочої області на намір, коректність і безпеку. |
| Планування фічі | `/plan-feature` | Ви перетворюєте грубу ідею фічі на план імплементації через керований діалог питань і відповідей. |
| Формулювання Goal | `/craft-goal`, необов'язково `/craft-goal <ідея>` | Ви перетворюєте ідею на перевірювану ціль Goal для діалогу Goal. |
| Catch up | `/catch-up` | Ви повертаєтеся до проєкту й питаєте, на чому зупинилися і що робити далі. |
| Дебаг | `/debug` | Ви досліджуєте баг: агент формує гіпотези, підтверджує кореневу причину з коду і лише тоді пропонує виправлення. |
| Зважування варіантів | `/weigh` | Ви знаєте, що будувати, але не як. Агент порівнює два-три підходи та рекомендує один. |
| Fusion | Дія "Run fusion" на групі multi-run | Ви об'єднуєте виводи кількох запусків в одну відповідь. Виводи запусків додаються після інструкцій. |
### Промпти без сторінки в Settings
Кілька промптів спрацьовують автоматично й не мають редагованих сторінок у Settings:
| Промпт | Коли спрацьовує |
| --- | --- |
| Заплановане завдання | `/schedule-task`, необов'язково з початковою ідеєю. Веде діалог, який визначає заплановане завдання. |
| Handoff для рев'ю | `/handoff-review` або кнопка Review у поданні diff із увімкненим handoff. Генерує handoff у робочій сесії. |
| Стартове повідомлення сесії рев'ю | Перше повідомлення згенерованої сесії рев'ю — з handoff, якщо він створений, або без нього. |
| Відгук рев'ю / відповідь імплементатора | Переносять повідомлення між двома сесіями: відгук рев'юера повертається в сесію імплементації, а відповідь імплементатора — назад у сесію рев'ю. |
## Пов'язане ## Пов'язане
- [Робочі процеси Git і GitHub](/uk/git/) — багато з цих промптів живлять git-процеси - [Робочі процеси Git і GitHub](/uk/git/) — багато з цих промптів живлять git-процеси
- [Нотатки, todo та плани](/uk/notes-todos-plans/) — todo і плани за промптами групи Planning
- [Multi-run](/uk/multi-run/) — групи запусків і fusion
@@ -21,6 +21,64 @@ description: 自定义 OpenChamber 自动化流程背后的内置提示词。
改主意了?每个提示词都有 **reset to default**,如果你想在所有地方重新开始,还有一个 **reset all**。 改主意了?每个提示词都有 **reset to default**,如果你想在所有地方重新开始,还有一个 **reset all**。
## 每个提示词在哪里使用
下表列出每个提示词的运行位置和触发时机。编辑前先看清触发条件,你就知道自己在改哪个流程。
### Git
| 提示词 | 运行位置 | 触发时机 |
| --- | --- | --- |
| 提交信息生成 | git 视图提交框中的生成按钮,以及移动端 Changes 页面 | 你生成提交信息时。会填入选中的文件和分支最近的提交主题,让提交信息符合仓库的现有风格。 |
| PR 生成 | git 视图 PR 标签页中的创建 pull request 表单 | 你生成 PR 标题和正文时。会填入 base 和 head 分支、两者之间的提交和变更文件、你补充的附加上下文,以及仓库的 PR 模板(如果存在)。 |
| merge/rebase 冲突解决 | git 视图中的冲突对话框,当 merge 或 rebase 因冲突停止时 | 你选择 "Resolve in current session" 或 "Resolve in new session"。智能体会阅读冲突文件,为每个文件提出解决策略,并等你确认后才编辑、暂存或继续操作。 |
| cherry-pick 冲突解决 | worktree 会话的 "Re-integrate commits" 区域 | 把会话的提交迁移到目标分支时遇到冲突并交给智能体。智能体在临时 worktree 中解决冲突、暂存文件并继续 cherry-pick。 |
### GitHub
| 提示词 | 运行位置 | 触发时机 |
| --- | --- | --- |
| PR 审阅 | 输入框附件菜单中的 "Link GitHub PR" 选择器,以及新 worktree 对话框 | 两个触发点。把 PR 附为上下文时会渲染指令,并随你的下一条消息发出。从 PR 创建 worktree 会话时,该提示词成为会话的开场消息,并附上完整 PR 上下文。 |
| issue 审阅 | 新 worktree 对话框,当你从 issue 创建 worktree 时 | 新会话的开场消息会审阅该 issue,并将其正文和评论附为上下文。 |
| PR 失败检查 / PR 评论 / 单条 PR 评论 | — | 目前没有任何流程发送它们。PR 视图过去通过一键审阅操作触发这些提示词;现在失败的检查和评论会改为固定为聊天上下文草稿。保留它们是为了让已有的覆盖配置继续生效。 |
### Planning
| 提示词 | 运行位置 | 触发时机 |
| --- | --- | --- |
| todo 规划 | 项目侧边栏中的 Todos 面板 | 你把一个 todo 发送到会话或新的 worktree 会话。todo 文本成为可见消息;指令会把它变成先提问的规划对话,而不是直接开始实现。 |
| 改进计划 | Plans 视图中已保存计划上的 "Improve" 操作 | 你把已保存的计划送入改进流程。智能体先读取计划文件,再基于仓库当前状态提出修改,并主动提出编辑同一个文件。 |
| 实现计划 | 已保存计划上的 "Implement" 操作 | 你把已保存的计划送入实现流程。智能体读取计划文件并端到端地实现它,不扩大范围;当计划本身有问题时,会把计划调整保存回该文件。 |
### Session
其中大多数由在输入框中输入的斜杠命令驱动。大多数也会以启动芯片的形式出现在新会话草稿页上。
| 提示词 | 运行位置 | 触发时机 |
| --- | --- | --- |
| 代码库导览 | `/explore` | 你想要一份代码库的高层次概览。 |
| 会话总结 | `/summary`,可选 `/summary <主题>` | 你总结目前的对话 — 适合移交给新会话。需要已存在的会话。 |
| 工作区审阅 | `/workspace-review` | 你让智能体从意图、正确性和安全性角度审阅当前的工作区 diff。 |
| 功能规划 | `/plan-feature` | 你通过引导式问答对话,把粗略的功能想法变成实现计划。 |
| Goal 制定 | `/craft-goal`,可选 `/craft-goal <想法>` | 你把一个想法变成可用于 Goal 对话框的可验证 Goal 目标。 |
| 快速追平 | `/catch-up` | 你回到一个项目,想知道进展如何、接下来做什么。 |
| 调试 | `/debug` | 你调查一个 bug:智能体提出假设、从代码确认根因,然后才提出修复方案。 |
| 权衡选项 | `/weigh` | 你知道要做什么,但不知道怎么做。智能体会比较两三种方案并推荐其一。 |
| Fusion | multi-run 组上的 "Run fusion" 操作 | 你把多次运行的输出合并成一个答案。运行输出会附加在指令之后。 |
### 没有 Settings 页面的提示词
少数提示词会自动触发,在 Settings 中没有可编辑的页面:
| 提示词 | 触发时机 |
| --- | --- |
| 计划任务 | `/schedule-task`,可选附带初始想法。引导完成定义计划任务的对话。 |
| 审阅交接 | `/handoff-review`,或 diff 视图中启用交接时的 Review 按钮。在当前工作会话中生成交接内容。 |
| 审阅会话开场消息 | 生成的审阅会话的开场消息 — 生成了交接内容时包含它,否则不包含。 |
| 审阅反馈 / 实现响应 | 在两个会话之间传递消息:审阅者的反馈回到实现会话,实现者的响应返回审阅会话。 |
## 相关内容 ## 相关内容
- [Git 与 GitHub 工作流](/zh-cn/git/) — 其中许多提示词为 git 流程提供动力 - [Git 与 GitHub 工作流](/zh-cn/git/) — 其中许多提示词为 git 流程提供动力
- [笔记、todo 与计划](/zh-cn/notes-todos-plans/) — Planning 提示词背后的 todo 和计划
- [Multi-run](/zh-cn/multi-run/) — 运行组与 fusion
+37 -4
View File
@@ -1369,7 +1369,7 @@ const maybeShowNativeNotification = (rawInput) => {
notification.on('click', () => { notification.on('click', () => {
focusForegroundWindow(); focusForegroundWindow();
if (sessionId) { if (sessionId) {
emitToAllWindows('openchamber:open-session', { sessionId, directory }); emitToPrimaryWindow('openchamber:open-session', { sessionId, directory });
} }
release(); release();
}); });
@@ -1770,6 +1770,15 @@ const computeBootOutcome = ({ envTargetUrl, probe, config, localAvailable }) =>
: probe?.status === 'wrong-service' : probe?.status === 'wrong-service'
? 'wrong-service' ? 'wrong-service'
: 'ok'; : 'ok';
// A relay-capable host is not a recovery case just because its stored
// direct URL failed the http probe — that URL is often the pairing
// creator's own loopback (unreachable here, or worse, someone else's
// service). The relay leg is activated in the renderer's relay restore,
// which cannot run from a recovery screen: boot to main on the local
// substrate and let it pick direct-or-relay.
if (status !== 'ok' && sanitizeHostRelayForStorage(host.relay)) {
return { target: 'remote', status: 'ok', hostId: host.id, url: host.apiUrl || host.url, ...availability };
}
return { target: 'remote', status, hostId: host.id, url: host.apiUrl || host.url, ...availability }; return { target: 'remote', status, hostId: host.id, url: host.apiUrl || host.url, ...availability };
}; };
@@ -1997,6 +2006,18 @@ const emitToAllWindows = (event, detail) => {
} }
}; };
// Session navigation must land in ONE window. Broadcasting it makes every
// open window adopt the same session, hijacking whatever the other windows
// were doing.
const emitToPrimaryWindow = (event, detail) => {
const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed());
if (windows.length === 0) return;
const target = (state.mainWindow && !state.mainWindow.isDestroyed())
? state.mainWindow
: windows.find((window) => window.isFocused()) || windows.find((window) => window.isVisible()) || windows[0];
emitToWindow(target, event, detail);
};
const setTaskbarProgress = (value) => { const setTaskbarProgress = (value) => {
if (process.platform !== 'win32') return; if (process.platform !== 'win32') return;
for (const browserWindow of BrowserWindow.getAllWindows()) { for (const browserWindow of BrowserWindow.getAllWindows()) {
@@ -2278,7 +2299,7 @@ const dispatchDeepLink = (link) => {
} }
if (link.type === 'session' && link.value) { if (link.type === 'session' && link.value) {
emitToAllWindows('openchamber:open-session', { sessionId: link.value }); emitToPrimaryWindow('openchamber:open-session', { sessionId: link.value });
return; return;
} }
if (link.type === 'host' && link.value) { if (link.type === 'host' && link.value) {
@@ -3013,12 +3034,24 @@ const resolveInitialUrl = async () => {
} }
} }
const defaultHostRelayCapable = Boolean(
config.defaultHostId
&& config.defaultHostId !== LOCAL_HOST_ID
&& sanitizeHostRelayForStorage(config.hosts.find((entry) => entry.id === config.defaultHostId)?.relay),
);
if (apiBaseUrl && apiBaseUrl !== localUrl) { if (apiBaseUrl && apiBaseUrl !== localUrl) {
remoteProbe = await probeHostWithTimeout(apiBaseUrl, 2_000, clientToken, requestHeaders); remoteProbe = await probeHostWithTimeout(apiBaseUrl, 2_000, clientToken, requestHeaders);
if (remoteProbe.status === 'unreachable') { if (remoteProbe.status === 'unreachable' && !defaultHostRelayCapable) {
remoteProbe = await probeHostWithTimeout(apiBaseUrl, 10_000, clientToken, requestHeaders); remoteProbe = await probeHostWithTimeout(apiBaseUrl, 10_000, clientToken, requestHeaders);
} }
if (remoteProbe.status === 'unreachable') { // The renderer's relay restore owns transport selection for relay-capable
// hosts; any failed direct probe falls back to the local substrate.
if (remoteProbe.status !== 'ok' && defaultHostRelayCapable) {
apiBaseUrl = localUrl || '';
clientToken = localUrl ? readDesktopLocalClientToken() : '';
requestHeaders = {};
initialUrl = localUiUrl;
} else if (remoteProbe.status === 'unreachable') {
state.unreachableHosts.add(apiBaseUrl); state.unreachableHosts.add(apiBaseUrl);
apiBaseUrl = localUrl || ''; apiBaseUrl = localUrl || '';
clientToken = localUrl ? readDesktopLocalClientToken() : ''; clientToken = localUrl ? readDesktopLocalClientToken() : '';
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@openchamber/electron", "name": "@openchamber/electron",
"version": "1.20.0", "version": "1.21.0",
"private": true, "private": true,
"description": "Electron desktop runtime for OpenChamber", "description": "Electron desktop runtime for OpenChamber",
"author": "OpenChamber", "author": "OpenChamber",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@openchamber/ui", "name": "@openchamber/ui",
"version": "1.20.0", "version": "1.21.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/main.tsx", "main": "src/main.tsx",
@@ -45,7 +45,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@legendapp/list": "3.3.8", "@legendapp/list": "3.3.8",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.21", "@opencode-ai/sdk": "1.18.23",
"@pierre/diffs": "1.3.0-beta.6", "@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.4.0", "@replit/codemirror-vim": "^6.4.0",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
+5 -19
View File
@@ -20,7 +20,6 @@ import { useAgentMemorySync } from '@/hooks/useAgentMemorySync';
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
import { useWindowTitle } from '@/hooks/useWindowTitle'; import { useWindowTitle } from '@/hooks/useWindowTitle';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { hasModifier } from '@/lib/utils';
import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, invokeDesktop } from '@/lib/desktop'; import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, invokeDesktop } from '@/lib/desktop';
import { import {
getInjectedBootOutcome, getInjectedBootOutcome,
@@ -723,25 +722,12 @@ function App({ apis }: AppProps) {
useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled }); useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled });
// Palette-only action: the memory debug panel has no keyboard shortcut.
React.useEffect(() => { React.useEffect(() => {
if (embeddedSessionChat) { if (embeddedSessionChat) return;
return; const handleToggle = () => setShowMemoryDebug((previous) => !previous);
} window.addEventListener('openchamber:memory-debug-toggle', handleToggle);
return () => window.removeEventListener('openchamber:memory-debug-toggle', handleToggle);
const handleKeyDown = (e: KeyboardEvent) => {
const isDebugShortcut = hasModifier(e)
&& e.shiftKey
&& !e.altKey
&& (e.code === 'KeyD' || e.key.toLowerCase() === 'd');
if (isDebugShortcut) {
e.preventDefault();
setShowMemoryDebug(prev => !prev);
}
};
window.addEventListener('keydown', handleKeyDown, true);
return () => window.removeEventListener('keydown', handleKeyDown, true);
}, [embeddedSessionChat]); }, [embeddedSessionChat]);
React.useEffect(() => { React.useEffect(() => {
+18
View File
@@ -12,6 +12,7 @@ import { SettingsView } from '@/components/views/SettingsView';
import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog'; import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
@@ -772,6 +773,23 @@ export function MobileApp({ apis }: MobileAppProps) {
}; };
}, [isNativeMobileApp, handleNativeResume]); }, [isNativeMobileApp, handleNativeResume]);
// A confirmed mid-session auth expiry (classified centrally from live 401
// traffic) runs the same seq-guarded re-probe the resume path uses: it ends
// in needs-login → the native welcome screen with the auth-expired notice.
// The shared web banner never renders on native (the session gate is not
// mounted here), so this is the only surface reacting to the signal.
React.useEffect(() => {
if (!isNativeMobileApp) return;
return useAuthSessionStore.subscribe((store, previous) => {
if (store.state === 'expired' && previous.state !== 'expired') {
handleNativeResume();
// The probe ladder owns the outcome from here; the shared store goes
// back to 'ok' so a later expiry can signal again.
useAuthSessionStore.getState().markAuthenticated();
}
});
}, [isNativeMobileApp, handleNativeResume]);
React.useEffect(() => { React.useEffect(() => {
registerRuntimeAPIs(apis); registerRuntimeAPIs(apis);
return () => registerRuntimeAPIs(null); return () => registerRuntimeAPIs(null);
+1 -1
View File
@@ -15,7 +15,7 @@ import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
import { useTerminalStore } from '@/stores/useTerminalStore'; import { useTerminalStore } from '@/stores/useTerminalStore';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { resetStreamingState } from '@/sync/streaming'; import { resetStreamingState } from '@/sync/streaming';
import { useGlobalSessionStatusStore, replaceGlobalSessionStatusById } from '@/sync/global-session-status'; import { replaceGlobalSessionStatusById } from '@/sync/global-session-status';
import { resetSessionOrdering } from '@/sync/session-ordering'; import { resetSessionOrdering } from '@/sync/session-ordering';
import { resetSessionActivityTiming } from '@/sync/session-activity-timing'; import { resetSessionActivityTiming } from '@/sync/session-activity-timing';
import { syncDesktopSettings } from '@/lib/persistence'; import { syncDesktopSettings } from '@/lib/persistence';
@@ -0,0 +1,41 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { useI18n } from '@/lib/i18n';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
/**
* Non-blocking notice that the OpenChamber session expired mid-work. It never
* takes the screen on its own: work stays visible and interactive, and only
* the explicit "Log in" click hands control to the session gate's full login
* flow (password, passkey, desktop shell all already there).
*/
export const AuthExpiredBanner: React.FC = () => {
const { t } = useI18n();
const authState = useAuthSessionStore((store) => store.state);
const markReauthenticating = useAuthSessionStore((store) => store.markReauthenticating);
if (authState !== 'expired') {
return null;
}
return (
// Below the header on purpose: the header row can be a window-drag region
// on desktop, where nothing under the cursor is clickable.
<div
className="pointer-events-none fixed inset-x-0 z-[200] flex justify-center px-4"
style={{ top: 'calc(var(--oc-header-height, 56px) + 8px)' }}
>
<div
role="alert"
className="oc-glass-popover oc-glass-floating pointer-events-auto flex items-center gap-3 rounded-lg px-3 py-2"
>
<Icon name="lock" className="size-4 flex-shrink-0" style={{ color: 'var(--status-error)' }} />
<span className="typography-ui-label text-foreground">{t('sessionAuth.expired.banner')}</span>
<Button size="xs" variant="outline" onClick={markReauthenticating} className="normal-case">
{t('sessionAuth.expired.loginAction')}
</Button>
</div>
</div>
);
};
@@ -12,6 +12,8 @@ import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch'; import { runtimeFetch } from '@/lib/runtime-fetch';
import { installAuthSessionFocusWatch, useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { AuthExpiredBanner } from './AuthExpiredBanner';
import { getRuntimeExtraHeadersSync } from '@/lib/runtime-auth'; import { getRuntimeExtraHeadersSync } from '@/lib/runtime-auth';
import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch'; import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch';
import { desktopHostsGet, desktopHostsSet, getDesktopHostApiUrl, normalizeHostUrl } from '@/lib/desktopHosts'; import { desktopHostsGet, desktopHostsSet, getDesktopHostApiUrl, normalizeHostUrl } from '@/lib/desktopHosts';
@@ -351,6 +353,7 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null); const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null);
const passwordInputRef = React.useRef<HTMLInputElement | null>(null); const passwordInputRef = React.useRef<HTMLInputElement | null>(null);
const hasResyncedRef = React.useRef(skipAuth); const hasResyncedRef = React.useRef(skipAuth);
const hasBootstrapResyncedRef = React.useRef(skipAuth);
React.useEffect(() => { React.useEffect(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
@@ -557,6 +560,27 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
} }
}, [skipAuth, state]); }, [skipAuth, state]);
// Mid-session expiry: the banner asks for a re-login by flipping the shared
// auth store to 'reauthenticating'; the gate answers with its own status
// check, which lands in the full 'locked' flow on a genuine 401. A
// successful login resolves the store back to 'ok'.
const authSessionState = useAuthSessionStore((store) => store.state);
React.useEffect(() => {
if (!skipAuth) installAuthSessionFocusWatch();
}, [skipAuth]);
React.useEffect(() => {
if (skipAuth) return;
if (authSessionState === 'reauthenticating') {
void checkStatusRef.current?.();
}
}, [authSessionState, skipAuth]);
React.useEffect(() => {
if (skipAuth) return;
if (state === 'authenticated' && useAuthSessionStore.getState().state !== 'ok') {
useAuthSessionStore.getState().markAuthenticated();
}
}, [skipAuth, state]);
React.useEffect(() => { React.useEffect(() => {
if (state === 'locked' && passwordInputRef.current) { if (state === 'locked' && passwordInputRef.current) {
passwordInputRef.current.focus(); passwordInputRef.current.focus();
@@ -570,10 +594,18 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
} }
if (state === 'authenticated' && !hasResyncedRef.current) { if (state === 'authenticated' && !hasResyncedRef.current) {
hasResyncedRef.current = true; hasResyncedRef.current = true;
// First authentication of this page load is bootstrap: adopt the
// persisted workspace pointers. A re-login after mid-session expiry is
// not — this window already has its own workspace, and the shared
// settings document may carry another window's pointers.
const isBootstrapResync = !hasBootstrapResyncedRef.current;
hasBootstrapResyncedRef.current = true;
void (async () => { void (async () => {
await initializeAppearancePreferences(); await initializeAppearancePreferences();
await syncDesktopSettings(); await syncDesktopSettings({ adoptWorkspace: isBootstrapResync });
await applyPersistedDirectoryPreferences(); if (isBootstrapResync) {
await applyPersistedDirectoryPreferences();
}
})(); })();
} }
}, [skipAuth, state]); }, [skipAuth, state]);
@@ -983,5 +1015,10 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({
); );
} }
return <>{children}</>; return (
<>
{skipAuth ? null : <AuthExpiredBanner />}
{children}
</>
);
}; };
@@ -18,6 +18,7 @@ import { StatusRowContainer } from './StatusRowContainer';
import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer'; import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer';
import ScrollToBottomButton from './components/ScrollToBottomButton'; import ScrollToBottomButton from './components/ScrollToBottomButton';
import { PromptNavigatorRail } from './components/PromptNavigatorRail'; import { PromptNavigatorRail } from './components/PromptNavigatorRail';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { useScrollShadow } from '@/components/ui/useScrollShadow'; import { useScrollShadow } from '@/components/ui/useScrollShadow';
import { useChatTimelineScroll, type TimelineListHandle } from '@/hooks/useChatTimelineScroll'; import { useChatTimelineScroll, type TimelineListHandle } from '@/hooks/useChatTimelineScroll';
import { useChatTimelineController } from './hooks/useChatTimelineController'; import { useChatTimelineController } from './hooks/useChatTimelineController';
@@ -645,6 +646,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
suspendPartUpdatesForMessageId: streamingMessageId, suspendPartUpdatesForMessageId: streamingMessageId,
}); });
const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES; const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES;
const authSessionExpired = useAuthSessionStore((store) => store.state !== 'ok');
const wasAuthExpiredRef = React.useRef(false);
const sessionMessageLoadState = useSessionMessageLoadState( const sessionMessageLoadState = useSessionMessageLoadState(
currentSessionId ?? '', currentSessionId ?? '',
effectiveSessionDirectory, effectiveSessionDirectory,
@@ -1170,6 +1173,23 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
void sync.ensureSessionRenderable(currentSessionId, true, effectiveSessionDirectory); void sync.ensureSessionRenderable(currentSessionId, true, effectiveSessionDirectory);
}, [currentSessionId, effectiveSessionDirectory, messagesEnabled, sync]); }, [currentSessionId, effectiveSessionDirectory, messagesEnabled, sync]);
// A load that failed while the session was expired retries itself the
// moment the re-login lands — the error screen should never outlive its
// cause.
React.useEffect(() => {
if (authSessionExpired) {
wasAuthExpiredRef.current = true;
return;
}
if (wasAuthExpiredRef.current) {
wasAuthExpiredRef.current = false;
if (sessionMessageLoadState.status === 'error') {
retrySessionLoad();
}
}
}, [authSessionExpired, retrySessionLoad, sessionMessageLoadState.status]);
React.useEffect(() => { React.useEffect(() => {
if (!active || !currentSessionId) return; if (!active || !currentSessionId) return;
if (lastScrolledSessionKeyRef.current === currentSessionKey) return; if (lastScrolledSessionKeyRef.current === currentSessionKey) return;
@@ -1298,10 +1318,20 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
<Icon name="error-warning" className="size-4" /> <Icon name="error-warning" className="size-4" />
</div> </div>
<p className="typography-ui-label font-medium text-foreground">{t('chat.container.sessionLoadError.title')}</p> <p className="typography-ui-label font-medium text-foreground">{t('chat.container.sessionLoadError.title')}</p>
<p className="typography-meta mt-1 text-muted-foreground">{t('chat.container.sessionLoadError.description')}</p> <p className="typography-meta mt-1 text-muted-foreground">
<Button variant="outline" size="sm" className="mt-4" onClick={retrySessionLoad}> {authSessionExpired
{t('chat.container.sessionLoadError.retry')} ? t('chat.container.sessionLoadError.authDescription')
</Button> : t('chat.container.sessionLoadError.description')}
</p>
{authSessionExpired ? (
<Button variant="outline" size="sm" className="mt-4" onClick={() => useAuthSessionStore.getState().markReauthenticating()}>
{t('sessionAuth.expired.loginAction')}
</Button>
) : (
<Button variant="outline" size="sm" className="mt-4" onClick={retrySessionLoad}>
{t('chat.container.sessionLoadError.retry')}
</Button>
)}
</div> </div>
</div> </div>
); );
+42 -45
View File
@@ -77,6 +77,8 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { usePermissionStore } from '@/stores/permissionStore'; import { usePermissionStore } from '@/stores/permissionStore';
import { togglePermissionAutoAccept } from './permissionAutoAccept'; import { togglePermissionAutoAccept } from './permissionAutoAccept';
import { useKeybind } from '@/hooks/useKeybind';
import { useAuthSessionStore } from '@/lib/runtime-auth-expiry';
import { extractGitChangedFiles } from './changedFiles'; import { extractGitChangedFiles } from './changedFiles';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
@@ -417,7 +419,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const ensureGitStatus = useGitStore((state) => state.ensureStatus); const ensureGitStatus = useGitStore((state) => state.ensureStatus);
const fetchGitStatus = useGitStore((state) => state.fetchStatus); const fetchGitStatus = useGitStore((state) => state.fetchStatus);
const clearGitDiffCache = useGitStore((state) => state.clearDiffCache); const clearGitDiffCache = useGitStore((state) => state.clearDiffCache);
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept); const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept);
const [isNarrowComposer, setIsNarrowComposer] = React.useState(false); const [isNarrowComposer, setIsNarrowComposer] = React.useState(false);
const [attachmentPreview, setAttachmentPreview] = React.useState<ToolPopupContent>({ const [attachmentPreview, setAttachmentPreview] = React.useState<ToolPopupContent>({
@@ -695,7 +696,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
attachments, attachments,
}; };
}, [resolveInlineFileMention]); }, [resolveInlineFileMention]);
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const prevWasAbortedRef = React.useRef(false); const prevWasAbortedRef = React.useRef(false);
// Issue linking state // Issue linking state
@@ -964,6 +964,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
const queuedMessageId = options?.queuedMessageId; const queuedMessageId = options?.queuedMessageId;
const delivery = options?.delivery === 'steer' && sessionPhase !== 'idle' ? 'steer' : undefined; const delivery = options?.delivery === 'steer' && sessionPhase !== 'idle' ? 'steer' : undefined;
const capturedTarget = messageQueueTarget; const capturedTarget = messageQueueTarget;
// An expired session cannot deliver anything: keep the prompt in the
// composer and point at the login banner instead of burning the send
// on a guaranteed 401.
if (useAuthSessionStore.getState().state !== 'ok') {
toast.error(t('sessionAuth.expired.sendBlocked'));
return;
}
// Snapshot the draft and current-session identity before the first // Snapshot the draft and current-session identity before the first
// async gap so a later sidebar selection cannot reroute the send. // async gap so a later sidebar selection cannot reroute the send.
const capturedDraftSnapshot = newSessionDraftOpen ? { ...newSessionDraft } : null; const capturedDraftSnapshot = newSessionDraftOpen ? { ...newSessionDraft } : null;
@@ -1382,10 +1390,25 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
console.error('Message send failed:', rawMessage || error); console.error('Message send failed:', rawMessage || error);
restoreConsumedDrafts(); restoreConsumedDrafts();
const currentInput = composerRef.current?.getValue() ?? messageRef.current; // A failed send returns the typed prompt no matter WHY it failed —
if (newSessionDraftOpen && inputSnapshot.message && (!currentInput || currentInput === inputSnapshot.message)) { // auth, network, server, anything. Losing a long prompt to a toast
setMessage(inputSnapshot.message); // is the one outcome this handler must never produce.
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current); if (inputSnapshot.message) {
if (currentChatDraftIdentityRef.current !== chatDraftIdentity) {
// The user switched sessions mid-send: restore into that
// session's persisted draft, not the visible composer.
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current);
} else {
const currentInput = composerRef.current?.getValue() ?? messageRef.current;
if (!currentInput || currentInput === inputSnapshot.message) {
setMessage(inputSnapshot.message);
writeChatDraft(chatDraftIdentity, inputSnapshot.message, confirmedMentionsRef.current);
} else {
// New typing already lives in the composer; the failed
// prompt joins it instead of clobbering either text.
useInputStore.getState().setPendingInputText(inputSnapshot.message, 'append');
}
}
} }
const isSoftNetworkError = const isSoftNetworkError =
@@ -1696,29 +1719,15 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
containerRef: dropZoneRef, containerRef: dropZoneRef,
}); });
const startAbortIndicator = React.useCallback(() => {
if (abortTimeoutRef.current) {
clearTimeout(abortTimeoutRef.current);
abortTimeoutRef.current = null;
}
setShowAbortStatus(true);
abortTimeoutRef.current = setTimeout(() => {
setShowAbortStatus(false);
abortTimeoutRef.current = null;
}, 1800);
}, []);
const handleAbort = React.useCallback(() => { const handleAbort = React.useCallback(() => {
clearAbortPrompt(); clearAbortPrompt();
startAbortIndicator();
// btw mode: the stop button stops the fork's turn, not the main // btw mode: the stop button stops the fork's turn, not the main
// session's. // session's.
const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId; const abortTarget = isBtwActive && btwSessionId ? btwSessionId : currentSessionId;
void abortCurrentOperation(abortTarget || undefined); void abortCurrentOperation(abortTarget || undefined);
}, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive, startAbortIndicator]); }, [abortCurrentOperation, btwSessionId, clearAbortPrompt, currentSessionId, isBtwActive]);
const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => { const handleCycleAgent = React.useCallback((direction: 1 | -1 = 1) => {
const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction); const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName, direction);
@@ -2562,31 +2571,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
t, t,
]); ]);
React.useEffect(() => { useKeybind('toggle_permission_auto_accept', () => {
const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; if (!isPermissionAutoAcceptInteractive) return false;
if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { handlePermissionAutoAcceptToggle();
startAbortIndicator(); });
if (currentSessionId) {
acknowledgeSessionAbort(currentSessionId);
}
}
prevWasAbortedRef.current = pendingAbortBanner;
}, [
abortPromptSessionId,
acknowledgeSessionAbort,
currentSessionId,
showAbortStatus,
startAbortIndicator,
]);
// Acknowledging the abort record is what lets the working chip resume for
// the next run; the old "Aborted" banner that used to accompany it is gone.
React.useEffect(() => { React.useEffect(() => {
return () => { const pendingAbort = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId;
if (abortTimeoutRef.current) { if (!prevWasAbortedRef.current && pendingAbort && currentSessionId) {
clearTimeout(abortTimeoutRef.current); acknowledgeSessionAbort(currentSessionId);
abortTimeoutRef.current = null; }
} prevWasAbortedRef.current = pendingAbort;
}; }, [abortPromptSessionId, acknowledgeSessionAbort, currentSessionId]);
}, []);
return ( return (
<> <>
@@ -2657,7 +2655,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
directory={currentSessionDirectoryForSync ?? currentDirectory} directory={currentSessionDirectoryForSync ?? currentDirectory}
/> />
<MemoComposerStatusBar <MemoComposerStatusBar
showAbortStatus={showAbortStatus}
showTodos={composerStatusExtrasEnabled} showTodos={composerStatusExtrasEnabled}
leftAccessory={!composerStatusExtrasEnabled || newSessionDraftOpen || !hasPendingChanges leftAccessory={!composerStatusExtrasEnabled || newSessionDraftOpen || !hasPendingChanges
? null ? null
@@ -457,13 +457,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
}, [chatRenderMode, isMessageCompleted, isUser, visibleParts]); }, [chatRenderMode, isMessageCompleted, isUser, visibleParts]);
const assistantTextParts = React.useMemo(() => {
if (isUser) {
return [];
}
return visibleParts.filter((part) => part.type === 'text');
}, [isUser, visibleParts]);
const toolParts = React.useMemo(() => { const toolParts = React.useMemo(() => {
if (isUser) { if (isUser) {
return []; return [];
@@ -545,19 +538,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const shouldHideUserMessage = isUser && displayParts.length === 0; const shouldHideUserMessage = isUser && displayParts.length === 0;
// Message is considered to have an "open step" if info.finish is not yet present
const hasOpenStep = typeof messageFinish !== 'string';
const shouldCoordinateRendering = React.useMemo(() => {
if (isUser) {
return false;
}
if (assistantTextParts.length === 0 || toolParts.length === 0) {
return hasOpenStep;
}
return true;
}, [assistantTextParts.length, toolParts.length, hasOpenStep, isUser]);
const themeVariant = currentTheme?.metadata.variant; const themeVariant = currentTheme?.metadata.variant;
const isDarkTheme = React.useMemo(() => { const isDarkTheme = React.useMemo(() => {
if (themeVariant) { if (themeVariant) {
@@ -116,13 +116,11 @@ const TodoItemRow: React.FC<{ todo: TodoItem }> = ({ todo }) => {
const EMPTY_TODOS: TodoItem[] = []; const EMPTY_TODOS: TodoItem[] = [];
interface ComposerStatusBarProps { interface ComposerStatusBarProps {
showAbortStatus?: boolean;
showTodos?: boolean; showTodos?: boolean;
leftAccessory?: React.ReactNode; leftAccessory?: React.ReactNode;
} }
export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
showAbortStatus,
showTodos = true, showTodos = true,
leftAccessory, leftAccessory,
}) => { }) => {
@@ -186,7 +184,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
const hasTodoContent = showTodos && statusSummary.left > 0; const hasTodoContent = showTodos && statusSummary.left > 0;
const hasLeftAccessory = Boolean(leftAccessory); const hasLeftAccessory = Boolean(leftAccessory);
const hasContent = Boolean(showAbortStatus) || hasTodoContent || hasLeftAccessory; const hasContent = hasTodoContent || hasLeftAccessory;
const popoverRef = React.useRef<HTMLDivElement>(null); const popoverRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => { React.useEffect(() => {
@@ -252,16 +250,7 @@ export const ComposerStatusBar: React.FC<ComposerStatusBarProps> = ({
<div className={cn("flex items-center justify-between gap-2 h-8", hasLeftAccessory && "px-0.5")}> <div className={cn("flex items-center justify-between gap-2 h-8", hasLeftAccessory && "px-0.5")}>
{/* Left: abort status | pending-changes accessory */} {/* Left: abort status | pending-changes accessory */}
<div className={cn("flex-1 flex items-center min-w-0 gap-2", hasLeftAccessory ? "pl-1.5" : "overflow-x-hidden")}> <div className={cn("flex-1 flex items-center min-w-0 gap-2", hasLeftAccessory ? "pl-1.5" : "overflow-x-hidden")}>
{showAbortStatus ? ( {leftAccessory ?? null}
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true" />
{t('chat.statusRow.aborted')}
</span>
</div>
) : leftAccessory ? (
leftAccessory
) : null}
</div> </div>
{/* Right: todos dropdown */} {/* Right: todos dropdown */}
@@ -1534,6 +1534,54 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return true; return true;
}, [allEntries.length]); }, [allEntries.length]);
// A navigation scroll lands on estimates: an unmounted target teleports
// to its estimated offset, and even a mounted one drifts when neighbours
// finish measuring a frame later. This settle loop re-aligns the target to
// the requested viewport position until the layout stops moving, and backs
// off the moment the user touches the scroll.
const settleNavigationTarget = React.useCallback((
findElement: () => HTMLElement | null,
desiredOffsetTop: number,
) => {
const container = resolveScrollContainer();
if (!container || typeof window === 'undefined') {
return;
}
let frames = 0;
let stable = 0;
let cancelled = false;
const cancelOnUserInput = () => {
cancelled = true;
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
};
container.addEventListener('touchstart', cancelOnUserInput, { passive: true });
container.addEventListener('wheel', cancelOnUserInput, { passive: true });
const step = () => {
if (cancelled) return;
const element = findElement();
if (element) {
const delta = element.getBoundingClientRect().top
- container.getBoundingClientRect().top
- desiredOffsetTop;
if (Math.abs(delta) > 0.5) {
container.scrollTop += delta;
stable = 0;
} else {
stable += 1;
}
}
frames += 1;
if (stable >= ANCHOR_HOLD_STABLE_FRAMES || frames >= ANCHOR_HOLD_MAX_FRAMES) {
container.removeEventListener('touchstart', cancelOnUserInput);
container.removeEventListener('wheel', cancelOnUserInput);
return;
}
window.requestAnimationFrame(step);
};
window.requestAnimationFrame(step);
}, [resolveScrollContainer]);
const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => { const scrollMessageElementIntoView = React.useCallback((messageId: string, behavior: ScrollBehavior = 'auto') => {
const container = resolveScrollContainer(); const container = resolveScrollContainer();
if (!container) { if (!container) {
@@ -1569,14 +1617,19 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
if (!container) { if (!container) {
return false; return false;
} }
const turnElement = container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`); const findTurnElement = () => container.querySelector<HTMLElement>(`[data-turn-id="${turnId}"]`);
const turnElement = findTurnElement();
if (turnElement) { if (turnElement) {
turnElement.scrollIntoView({ behavior, block: 'start' }); turnElement.scrollIntoView({ behavior, block: 'start' });
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true; return true;
} }
if (!scrollHistoryIndexIntoView(index)) {
return scrollHistoryIndexIntoView(index); return false;
}
if (behavior !== 'smooth') settleNavigationTarget(findTurnElement, 0);
return true;
}, },
scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => { scrollToMessageId: (messageId: string, options?: { behavior?: ScrollBehavior }) => {
@@ -1586,8 +1639,12 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return false; return false;
} }
return scrollMessageElementIntoView(messageId, behavior) const didScroll = scrollMessageElementIntoView(messageId, behavior)
|| scrollHistoryIndexIntoView(index); || scrollHistoryIndexIntoView(index);
if (didScroll && behavior !== 'smooth') {
settleNavigationTarget(() => findMessageElement(messageId), 50);
}
return didScroll;
}, },
holdViewportAnchor: (anchor) => { holdViewportAnchor: (anchor) => {
@@ -1730,7 +1787,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
return () => { return () => {
objectRef.current = null; objectRef.current = null;
}; };
}, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, turnIndexMap, ref]); }, [findMessageElement, historyEntries.length, messageIndexMap, resolveScrollContainer, scrollHistoryIndexIntoView, scrollMessageElementIntoView, settleNavigationTarget, turnIndexMap, ref]);
const anchoredEndSpace = React.useMemo<TimelineAnchoredEndSpace | undefined>(() => { const anchoredEndSpace = React.useMemo<TimelineAnchoredEndSpace | undefined>(() => {
const resolved = resolveChatListAnchoredEndSpace( const resolved = resolveChatListAnchoredEndSpace(
@@ -324,7 +324,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const providers = useConfigStore((state) => state.providers); const providers = useConfigStore((state) => state.providers);
const currentProviderId = useConfigStore((state) => state.currentProviderId); const currentProviderId = useConfigStore((state) => state.currentProviderId);
const currentModelId = useConfigStore((state) => state.currentModelId); const currentModelId = useConfigStore((state) => state.currentModelId);
const currentVariant = useConfigStore((state) => state.currentVariant); const effectiveCurrentVariant = useConfigStore((state) => state.currentVariant);
const currentVariantSelection = useConfigStore((state) => state.currentVariantSelection);
const currentVariant = currentVariantSelection.override ?? undefined;
const currentAgentName = useConfigStore((state) => state.currentAgentName); const currentAgentName = useConfigStore((state) => state.currentAgentName);
const settingsDefaultVariant = useConfigStore((state) => state.settingsDefaultVariant); const settingsDefaultVariant = useConfigStore((state) => state.settingsDefaultVariant);
const settingsDefaultAgent = useConfigStore((state) => state.settingsDefaultAgent); const settingsDefaultAgent = useConfigStore((state) => state.settingsDefaultAgent);
@@ -332,6 +334,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider); const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
const setModel = useConfigStore((state) => state.setModel); const setModel = useConfigStore((state) => state.setModel);
const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant); const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant);
const setCurrentVariantOverride = useConfigStore((state) => state.setCurrentVariantOverride);
const getCurrentModelVariants = useConfigStore((state) => state.getCurrentModelVariants); const getCurrentModelVariants = useConfigStore((state) => state.getCurrentModelVariants);
const setAgent = useConfigStore((state) => state.setAgent); const setAgent = useConfigStore((state) => state.setAgent);
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider); const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
@@ -693,6 +696,30 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return variants ? Object.keys(variants) : []; return variants ? Object.keys(variants) : [];
}, [providers]); }, [providers]);
const resolveInheritedVariantForModel = React.useCallback((providerId: string, modelId: string, agentName?: string | null) => {
const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) return undefined;
let currentInherited: string | undefined;
if (currentProviderId === providerId && currentModelId === modelId) {
currentInherited = currentVariantSelection.inherited
?? (currentVariantSelection.override === null || currentVariantSelection.override === undefined
? effectiveCurrentVariant
: undefined);
}
const effectiveAgentName = agentName ?? uiAgentName ?? currentAgentName;
const agent = effectiveAgentName ? agents.find((candidate) => candidate.name === effectiveAgentName) : undefined;
const agentVariant = (
agent?.model?.providerID === providerId
&& agent.model.modelID === modelId
) ? agent.variant : undefined;
const candidates = currentSessionId
? [agentVariant, settingsDefaultVariant, currentInherited]
: [currentInherited, agentVariant, settingsDefaultVariant];
return candidates.find((candidate) => candidate !== undefined && variantOptions.includes(candidate));
}, [agents, currentAgentName, currentModelId, currentProviderId, currentSessionId, currentVariantSelection, effectiveCurrentVariant, getModelVariantOptions, settingsDefaultVariant, uiAgentName]);
const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string) => { const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string) => {
const variantOptions = getModelVariantOptions(providerId, modelId); const variantOptions = getModelVariantOptions(providerId, modelId);
if (variantOptions.length === 0) { if (variantOptions.length === 0) {
@@ -711,10 +738,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return currentVariant; return currentVariant;
} }
if (!currentSessionId && settingsDefaultVariant && variantOptions.includes(settingsDefaultVariant)) {
return settingsDefaultVariant;
}
return undefined; return undefined;
}, [ }, [
currentAgentName, currentAgentName,
@@ -724,7 +747,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentVariant, currentVariant,
getAgentModelVariantForSession, getAgentModelVariantForSession,
getModelVariantOptions, getModelVariantOptions,
settingsDefaultVariant,
uiAgentName, uiAgentName,
]); ]);
@@ -748,7 +770,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
} }
manualVariantSelectionRef.current = true; manualVariantSelectionRef.current = true;
setCurrentVariant(variant); setCurrentVariantOverride(
variant ?? null,
resolveInheritedVariantForModel(providerId, modelId, agentNameOverride),
);
addRecentEffort(providerId, modelId, variant); addRecentEffort(providerId, modelId, variant);
const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName(); const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName();
@@ -759,9 +784,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
addRecentEffort, addRecentEffort,
currentSessionId, currentSessionId,
getModelVariantOptions, getModelVariantOptions,
resolveInheritedVariantForModel,
resolveLiveAgentName, resolveLiveAgentName,
saveAgentModelVariantForSession, saveAgentModelVariantForSession,
setCurrentVariant, setCurrentVariant,
setCurrentVariantOverride,
]); ]);
const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => { const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => {
@@ -1121,18 +1148,21 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
} }
if (currentVariant && !availableVariants.includes(currentVariant)) { if (currentVariant && !availableVariants.includes(currentVariant)) {
setCurrentVariant(undefined); setCurrentVariantOverride(
null,
resolveInheritedVariantForModel(currentProviderId, currentModelId),
);
return; return;
} }
// Draft state (no session yet): seed from settings default, but don't override // Draft state (no session yet): seed from settings default, but don't override
// user selection while drafting. // user selection while drafting.
if (!currentSessionId) { if (!currentSessionId) {
if (!currentVariant && !manualVariantSelectionRef.current) { if (currentVariantSelection.override === undefined && !manualVariantSelectionRef.current) {
const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant) const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
? settingsDefaultVariant ? settingsDefaultVariant
: undefined; : undefined;
setCurrentVariant(desired); setCurrentVariantOverride(desired ?? null, desired);
} }
return; return;
} }
@@ -1144,13 +1174,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentModelId, currentModelId,
); );
const resolvedSaved = savedVariant && availableVariants.includes(savedVariant) const inheritedVariant = resolveInheritedVariantForModel(currentProviderId, currentModelId);
? savedVariant if (savedVariant && availableVariants.includes(savedVariant)) {
: settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant) setCurrentVariantOverride(savedVariant, inheritedVariant);
? settingsDefaultVariant } else if (currentVariantSelection.override === null) {
: undefined; setCurrentVariantOverride(null, inheritedVariant);
} else {
setCurrentVariant(resolvedSaved); setCurrentVariant(inheritedVariant);
}
manualVariantSelectionRef.current = false; manualVariantSelectionRef.current = false;
}, [ }, [
availableVariants, availableVariants,
@@ -1160,8 +1191,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
currentProviderId, currentProviderId,
currentModelId, currentModelId,
currentVariant, currentVariant,
currentVariantSelection.override,
effectiveCurrentVariant,
getAgentModelVariantForSession, getAgentModelVariantForSession,
resolveInheritedVariantForModel,
setCurrentVariant, setCurrentVariant,
setCurrentVariantOverride,
settingsDefaultVariant, settingsDefaultVariant,
]); ]);
@@ -10,6 +10,10 @@ import { Icon } from "@/components/icon/Icon";
import { DiffPreview, WritePreview } from './DiffPreview'; import { DiffPreview, WritePreview } from './DiffPreview';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { getVisiblePermissionPatterns } from './permissionCardPatterns'; import { getVisiblePermissionPatterns } from './permissionCardPatterns';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
// Newest pending card owns the keyboard; older cards wait their turn.
const activePermissionCardIds: string[] = [];
const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = { const PERMISSION_BASH_CUSTOM_STYLE: React.CSSProperties = {
margin: 0, margin: 0,
@@ -126,6 +130,33 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
} }
}; };
const handleResponseRef = React.useRef(handleResponse);
handleResponseRef.current = handleResponse;
React.useEffect(() => {
if (hasResponded) return;
activePermissionCardIds.push(permission.id);
const handleKeyDown = (event: KeyboardEvent) => {
if (activePermissionCardIds.at(-1) !== permission.id) return;
if (!event.altKey || event.metaKey || event.ctrlKey) return;
const response = event.key === 'Enter'
? (event.shiftKey ? 'always' as const : 'once' as const)
: event.key === 'Backspace' && !event.shiftKey
? 'reject' as const
: null;
if (!response) return;
event.preventDefault();
event.stopPropagation();
void handleResponseRef.current(response);
};
window.addEventListener('keydown', handleKeyDown, true);
return () => {
window.removeEventListener('keydown', handleKeyDown, true);
const index = activePermissionCardIds.lastIndexOf(permission.id);
if (index !== -1) activePermissionCardIds.splice(index, 1);
};
}, [hasResponded, permission.id]);
if (hasResponded) { if (hasResponded) {
return null; return null;
} }
@@ -380,6 +411,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="check" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Allow Once Allow Once
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+enter')}</kbd>
</button> </button>
{permission.always.length > 0 ? ( {permission.always.length > 0 ? (
@@ -436,6 +468,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="time" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Always Allow Always Allow
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+shift+enter')}</kbd>
</button> </button>
)} )}
@@ -459,6 +492,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
> >
<Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" /> <Icon name="close" className="h-3.5 w-3.5 sm:h-3 sm:w-3 flex-shrink-0" />
Deny Deny
<kbd className="ml-1 hidden sm:inline typography-micro opacity-60">{formatShortcutForDisplay('alt+backspace')}</kbd>
</button> </button>
{isResponding && ( {isResponding && (
+4 -18
View File
@@ -1,11 +1,9 @@
import React from "react"; import React from "react";
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder"; import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder";
import { Icon } from "@/components/icon/Icon";
import { useI18n } from "@/lib/i18n";
// The floating assistant-status chip that hovers above the composer while the // The floating assistant-status chip that hovers above the composer while the
// agent works ("Claude is working…", abort notice). ONLY that. The composer's // agent works ("Claude is working…"). ONLY that. The composer's
// own bar — pending changes, todos dropdown — is ComposerStatusBar: they used // own bar — pending changes, todos dropdown — is ComposerStatusBar: they used
// to share this component, and every restyle of this chip (glass, placement) // to share this component, and every restyle of this chip (glass, placement)
// silently dragged the composer bar and its dropdown along with it. // silently dragged the composer bar and its dropdown along with it.
@@ -17,10 +15,8 @@ interface StatusRowProps {
statusText?: string | null; statusText?: string | null;
isGenericStatus?: boolean; isGenericStatus?: boolean;
isWaitingForPermission?: boolean; isWaitingForPermission?: boolean;
wasAborted?: boolean;
abortActive?: boolean; abortActive?: boolean;
retryInfo?: { attempt?: number; next?: number } | null; retryInfo?: { attempt?: number; next?: number } | null;
showAbortStatus?: boolean;
agentName?: string; agentName?: string;
modelName?: string | null; modelName?: string | null;
providerId?: string | null; providerId?: string | null;
@@ -31,19 +27,16 @@ export const StatusRow: React.FC<StatusRowProps> = ({
statusText = null, statusText = null,
isGenericStatus, isGenericStatus,
isWaitingForPermission, isWaitingForPermission,
wasAborted,
abortActive, abortActive,
retryInfo, retryInfo,
showAbortStatus,
agentName, agentName,
modelName, modelName,
providerId, providerId,
}) => { }) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive); const shouldRenderPlaceholder = !abortActive;
const hasContent = isWorking || Boolean(wasAborted) || Boolean(showAbortStatus); const hasContent = isWorking;
if (!hasContent) { if (!hasContent) {
return null; return null;
@@ -63,14 +56,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
a shrink-to-fit wrapper around it always collapsed to zero. */} a shrink-to-fit wrapper around it always collapsed to zero. */}
<div className="oc-glass-popover inline-flex w-max max-w-full items-center gap-2 h-8 whitespace-nowrap rounded-full [corner-shape:round] px-3"> <div className="oc-glass-popover inline-flex w-max max-w-full items-center gap-2 h-8 whitespace-nowrap rounded-full [corner-shape:round] px-3">
<div className="flex items-center min-w-0 gap-2 overflow-x-hidden"> <div className="flex items-center min-w-0 gap-2 overflow-x-hidden">
{showAbortStatus ? ( {shouldRenderPlaceholder ? (
<div className="flex h-full items-center text-[var(--status-error)] pl-0.5">
<span className="flex items-center gap-1.5 typography-ui-label">
<Icon name="close-circle" aria-hidden="true"/>
{t('chat.statusRow.aborted')}
</span>
</div>
) : shouldRenderPlaceholder ? (
<WorkingPlaceholder <WorkingPlaceholder
key={currentSessionId ?? "no-session"} key={currentSessionId ?? "no-session"}
isWorking={isWorking} isWorking={isWorking}
@@ -2,7 +2,6 @@ import React from 'react';
import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { useAssistantStatus } from '@/hooks/useAssistantStatus';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { getProviderModelDisplayName } from '@/lib/modelDisplay'; import { getProviderModelDisplayName } from '@/lib/modelDisplay';
import { StatusRow } from './StatusRow'; import { StatusRow } from './StatusRow';
@@ -12,15 +11,6 @@ import { StatusRow } from './StatusRow';
* labels while still limiting subscriptions to the active assistant message. * labels while still limiting subscriptions to the active assistant message.
*/ */
export const StatusRowContainer: React.FC = React.memo(() => { export const StatusRowContainer: React.FC = React.memo(() => {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const abortRecord = useSessionUIStore(
React.useCallback((state) => {
if (!currentSessionId) {
return null;
}
return state.sessionAbortFlags?.get(currentSessionId) ?? null;
}, [currentSessionId]),
);
const { activeModel, working } = useAssistantStatus(); const { activeModel, working } = useAssistantStatus();
const currentAgentName = useConfigStore((state) => state.currentAgentName); const currentAgentName = useConfigStore((state) => state.currentAgentName);
const providers = useConfigStore((state) => state.providers); const providers = useConfigStore((state) => state.providers);
@@ -35,16 +25,13 @@ export const StatusRowContainer: React.FC = React.memo(() => {
return getProviderModelDisplayName(provider, activeModel.modelId) || null; return getProviderModelDisplayName(provider, activeModel.modelId) || null;
}, [activeModel, providers]); }, [activeModel, providers]);
const wasAborted = Boolean(abortRecord && !abortRecord.acknowledged);
return ( return (
<StatusRow <StatusRow
isWorking={working.isWorking} isWorking={working.isWorking}
statusText={working.statusText} statusText={working.statusText}
isGenericStatus={working.isGenericStatus} isGenericStatus={working.isGenericStatus}
isWaitingForPermission={working.isWaitingForPermission} isWaitingForPermission={working.isWaitingForPermission}
wasAborted={wasAborted || working.wasAborted} abortActive={working.abortActive}
abortActive={wasAborted || working.abortActive}
retryInfo={working.retryInfo} retryInfo={working.retryInfo}
agentName={currentAgentName} agentName={currentAgentName}
modelName={modelDisplayName} modelName={modelDisplayName}
@@ -141,6 +141,9 @@ and the send path reading the same grammar.
- `state/useDraftTarget.ts` — the draft can target a directory that does not - `state/useDraftTarget.ts` — the draft can target a directory that does not
exist yet (a worktree being created). It must survive not appearing in the exist yet (a worktree being created). It must survive not appearing in the
branch list, or the selector snaps back to the project root mid-creation. branch list, or the selector snaps back to the project root mid-creation.
- `ui/DraftTargetSelectors.tsx` owns the controlled project/worktree picker
state and registers its application shortcuts locally. The selectors only
consume their shared prefix while the draft target UI is mounted.
## Mobile ## Mobile
@@ -17,6 +17,15 @@ import React from 'react';
import { isCapacitorApp } from '@/lib/platform'; import { isCapacitorApp } from '@/lib/platform';
import type { ComposerEditorHandle } from '../editor/ComposerEditor'; import type { ComposerEditorHandle } from '../editor/ComposerEditor';
// Android mobile browsers are the pan-mode holdouts this pin exists for on
// the CHAT screen too: interactive-widget=resizes-content is ignored by a
// fair share of Android WebView/Chrome builds, and unlike iOS Safari they do
// not reliably reveal the focused field either — the composer just stays
// behind the keyboard. iOS keeps its browser-native reveal on the chat
// screen, so this stays Android-only there.
// Callers are browser-only React effects, so navigator always exists here.
const isAndroidBrowser = (): boolean => /Android/i.test(navigator.userAgent);
export interface MobileViewportPinOptions { export interface MobileViewportPinOptions {
isMobile: boolean; isMobile: boolean;
/** Composer expanded to fullscreen on mobile. */ /** Composer expanded to fullscreen on mobile. */
@@ -96,12 +105,14 @@ export function useMobileViewportPin(options: MobileViewportPinOptions): void {
}; };
}, [editorRef, formRef, isFullscreen, isMobile]); }, [editorRef, formRef, isFullscreen, isMobile]);
// Draft screen with the keyboard up: anchor the normal-height composer to // Keyboard up: anchor the normal-height composer to the visible bottom.
// the visible bottom. The chat screen does not need this — its own // Draft screen on every mobile browser; chat screen only on Android,
// focused-field reveal works there. // where neither viewport resizing nor the focused-field reveal can be
// relied on (iOS chat keeps the browser's own reveal).
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
if (!isMobile || isCapacitorApp()) return; if (!isMobile || isCapacitorApp()) return;
if (!isDraftScreen || isFullscreen || !isFocused) return; if (isFullscreen || !isFocused) return;
if (!isDraftScreen && !isAndroidBrowser()) return;
const vv = window.visualViewport; const vv = window.visualViewport;
const form = formRef.current; const form = formRef.current;
if (!vv || !form) return; if (!vv || !form) return;
@@ -12,6 +12,7 @@ import React from 'react';
import { Icon } from '@/components/icon/Icon'; import { Icon } from '@/components/icon/Icon';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { shouldDismissDropdown } from '@/components/ui/dropdown-navigation';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -26,6 +27,7 @@ import { useI18n } from '@/lib/i18n';
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch'; import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
import { createWorktreeDraft } from '@/lib/worktreeSessionCreator'; import { createWorktreeDraft } from '@/lib/worktreeSessionCreator';
import { useKeybind } from '@/hooks/useKeybind';
import type { Theme } from '@/types/theme'; import type { Theme } from '@/types/theme';
import { normalizePath } from '../attachments/filePaths'; import { normalizePath } from '../attachments/filePaths';
import { getProjectDisplayLabel, type DraftTargetProject } from '../state/useDraftTarget'; import { getProjectDisplayLabel, type DraftTargetProject } from '../state/useDraftTarget';
@@ -106,14 +108,48 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
onDirectoryChange, onDirectoryChange,
theme, theme,
} = props; } = props;
const [openPicker, setOpenPicker] = React.useState<'project' | 'worktree' | null>(null);
const projectTriggerRef = React.useRef<HTMLButtonElement>(null);
const worktreeTriggerRef = React.useRef<HTMLButtonElement>(null);
const handlePickerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
if (openPicker === null || !shouldDismissDropdown(event)) return;
event.preventDefault();
event.stopPropagation();
setOpenPicker(null);
};
useKeybind('open_draft_project_picker', () => {
projectTriggerRef.current?.focus();
setOpenPicker('project');
});
useKeybind('open_draft_worktree_picker', () => {
if (!showBranchSelector) return false;
worktreeTriggerRef.current?.focus();
setOpenPicker('worktree');
});
const handleProjectChange = (projectId: string) => {
onProjectChange(projectId);
setOpenPicker(null);
};
const handleDirectoryChange = (directory: string) => {
onDirectoryChange(directory);
setOpenPicker(null);
};
return ( return (
<div className="mb-1.5 flex min-w-0 items-center gap-1.5 px-0.5"> <div className="mb-1.5 flex min-w-0 items-center gap-1.5 px-0.5">
<Select <Select
value={selectedProject.id} value={selectedProject.id}
onValueChange={onProjectChange} open={openPicker === 'project'}
onOpenChange={(open) => setOpenPicker(open ? 'project' : null)}
onValueChange={handleProjectChange}
disableGlobalShortcuts
> >
<SelectTrigger <SelectTrigger
ref={projectTriggerRef}
onKeyDown={handlePickerKeyDown}
size="sm" size="sm"
className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent" className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent"
> >
@@ -123,9 +159,9 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
: <ProjectLabel project={selectedProject} theme={theme} />} : <ProjectLabel project={selectedProject} theme={theme} />}
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain fitContent> <SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain fitContent onKeyDown={handlePickerKeyDown}>
{projects.map((project) => ( {projects.map((project) => (
<SelectItem key={project.id} value={project.id} className="max-w-[24rem] truncate"> <SelectItem key={project.id} value={project.id} showSelectedBackground={false} className="max-w-[24rem] truncate">
<ProjectLabel project={project} theme={theme} /> <ProjectLabel project={project} theme={theme} />
</SelectItem> </SelectItem>
))} ))}
@@ -135,9 +171,14 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
{showBranchSelector ? ( {showBranchSelector ? (
<Select <Select
value={selectedDirectory ?? branchItems[0]?.value ?? normalizePath(selectedProject.path) ?? ''} value={selectedDirectory ?? branchItems[0]?.value ?? normalizePath(selectedProject.path) ?? ''}
onValueChange={onDirectoryChange} open={openPicker === 'worktree'}
onOpenChange={(open) => setOpenPicker(open ? 'worktree' : null)}
onValueChange={handleDirectoryChange}
disableGlobalShortcuts
> >
<SelectTrigger <SelectTrigger
ref={worktreeTriggerRef}
onKeyDown={handlePickerKeyDown}
size="sm" size="sm"
className="h-7 min-w-0 w-fit max-w-[48vw] sm:max-w-[20rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent" className="h-7 min-w-0 w-fit max-w-[48vw] sm:max-w-[20rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent"
> >
@@ -145,11 +186,11 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
{selectedBranchLabel ?? t('chat.chatInput.branch')} {selectedBranchLabel ?? t('chat.chatInput.branch')}
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain className="w-max min-w-48"> <SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain className="w-max min-w-48" onKeyDown={handlePickerKeyDown}>
{projectRootBranchOption ? ( {projectRootBranchOption ? (
<SelectGroup> <SelectGroup>
<SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel> <SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel>
<SelectItem key={projectRootBranchOption.value} value={projectRootBranchOption.value} className="max-w-[24rem] truncate"> <SelectItem key={projectRootBranchOption.value} value={projectRootBranchOption.value} showSelectedBackground={false} className="max-w-[24rem] truncate">
{projectRootBranchOption.label} {projectRootBranchOption.label}
</SelectItem> </SelectItem>
</SelectGroup> </SelectGroup>
@@ -168,13 +209,13 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
</button> </button>
</div> </div>
{worktreeBranchOptions.map((option) => ( {worktreeBranchOptions.map((option) => (
<SelectItem key={option.value} value={option.value} className="max-w-[24rem] truncate"> <SelectItem key={option.value} value={option.value} showSelectedBackground={false} className="max-w-[24rem] truncate">
{option.pending ? '⏳ ' : ''}{option.label} {option.pending ? '⏳ ' : ''}{option.label}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
{selectedDirectory && !selectedBranchIsKnown ? ( {selectedDirectory && !selectedBranchIsKnown ? (
<SelectItem value={selectedDirectory} className="max-w-[24rem] truncate"> <SelectItem value={selectedDirectory} showSelectedBackground={false} className="max-w-[24rem] truncate">
{selectedBranchLabel} {selectedBranchLabel}
</SelectItem> </SelectItem>
) : null} ) : null}
@@ -5,7 +5,12 @@ import React from 'react';
import { Icon } from '@/components/icon/Icon'; import { Icon } from '@/components/icon/Icon';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { cn, isMacOS } from '@/lib/utils'; import {
formatShortcutForDisplay,
getEffectiveShortcutCombo,
} from '@/lib/shortcuts';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/useUIStore';
type FocusModeButtonProps = { type FocusModeButtonProps = {
footerIconButtonClass: string; footerIconButtonClass: string;
@@ -17,6 +22,12 @@ type FocusModeButtonProps = {
export const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButtonProps) { export const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButtonProps) {
const { footerIconButtonClass, iconSizeClass, isExpandedInput, onToggle } = props; const { footerIconButtonClass, iconSizeClass, isExpandedInput, onToggle } = props;
const { t } = useI18n(); const { t } = useI18n();
const expandInputShortcutOverride = useUIStore((state) => state.shortcutOverrides.expand_input);
const expandInputCombo = getEffectiveShortcutCombo(
'expand_input',
expandInputShortcutOverride === undefined ? undefined : { expand_input: expandInputShortcutOverride },
);
const shortcut = expandInputCombo ? formatShortcutForDisplay(expandInputCombo) : null;
return ( return (
<Tooltip> <Tooltip>
@@ -43,9 +54,7 @@ export const FocusModeButton = React.memo(function FocusModeButton(props: FocusM
<TooltipContent side="top" sideOffset={8}> <TooltipContent side="top" sideOffset={8}>
<div className="flex flex-col gap-0.5 text-center"> <div className="flex flex-col gap-0.5 text-center">
<span>{t('chat.chatInput.focusMode.label')}</span> <span>{t('chat.chatInput.focusMode.label')}</span>
<span className="font-mono opacity-60"> {shortcut ? <span className="font-mono opacity-60">{shortcut}</span> : null}
{isMacOS() ? '⌘⇧E' : 'Ctrl+Shift+E'}
</span>
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -1343,16 +1343,6 @@ const AssistantMessageBody = React.memo(({
return resolved ? { id: resolved.id, path: resolved.path } : null; return resolved ? { id: resolved.id, path: resolved.path } : null;
}, [availableWorktreesByProject, canUseProjectPlanActions, currentSessionId, effectiveDirectory, getDirectoryForSession, projects]); }, [availableWorktreesByProject, canUseProjectPlanActions, currentSessionId, effectiveDirectory, getDirectoryForSession, projects]);
const hasTools = toolParts.length > 0;
const hasPendingTools = React.useMemo(() => {
return toolParts.some((toolPart) => {
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
const status = state?.status;
return status === 'pending' || status === 'running' || status === 'started';
});
}, [toolParts]);
const isActiveTool = React.useCallback((toolPart: ToolPartType): boolean => { const isActiveTool = React.useCallback((toolPart: ToolPartType): boolean => {
const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {}; const state = (toolPart as Record<string, unknown>).state as Record<string, unknown> | undefined ?? {};
const status = state?.status; const status = state?.status;
@@ -1381,42 +1371,6 @@ const AssistantMessageBody = React.memo(({
return isActiveTool(toolPart) || isToolFinalized(toolPart); return isActiveTool(toolPart) || isToolFinalized(toolPart);
}, [isActiveTool, isToolFinalized]); }, [isActiveTool, isToolFinalized]);
const allToolsFinalized = React.useMemo(() => {
if (toolParts.length === 0) {
return true;
}
if (hasPendingTools) {
return false;
}
return toolParts.every((toolPart) => isToolFinalized(toolPart));
}, [toolParts, hasPendingTools, isToolFinalized]);
const reasoningParts = React.useMemo(() => {
return visibleParts.filter((part) => part.type === 'reasoning');
}, [visibleParts]);
const reasoningComplete = React.useMemo(() => {
if (reasoningParts.length === 0) {
return true;
}
return reasoningParts.every((part) => {
const time = (part as Record<string, unknown>).time as { end?: number } | undefined;
return typeof time?.end === 'number';
});
}, [reasoningParts]);
// Message is considered to have an "open step" if info.finish is not yet present
const hasOpenStep = typeof messageFinish !== 'string';
const shouldHoldForReasoning =
reasoningParts.length > 0 &&
hasTools &&
(hasPendingTools || hasOpenStep || !allToolsFinalized);
const shouldHoldTools = awaitingMessageCompletion
|| (hasTools && (hasPendingTools || hasOpenStep || !allToolsFinalized));
const shouldHoldReasoning = awaitingMessageCompletion || shouldHoldForReasoning;
const hasCopyableText = Boolean(hasTextContent) && !awaitingMessageCompletion; const hasCopyableText = Boolean(hasTextContent) && !awaitingMessageCompletion;
const handleForkClick = React.useCallback( const handleForkClick = React.useCallback(
@@ -18,6 +18,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { rangeToMarkdown, trimSelectionValue, wrapMarkdownSelectionForChat } from './selectionMarkdown'; import { rangeToMarkdown, trimSelectionValue, wrapMarkdownSelectionForChat } from './selectionMarkdown';
import { focusChatInput } from '@/components/chat/composer/editor/dom'; import { focusChatInput } from '@/components/chat/composer/editor/dom';
import { registerActiveSelectionToolbar } from '@/lib/addSelectionToChat';
import { collectSelectionOverlayRects } from '@/lib/selectionOverlayRects'; import { collectSelectionOverlayRects } from '@/lib/selectionOverlayRects';
interface TextSelectionMenuProps { interface TextSelectionMenuProps {
@@ -106,7 +107,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
const openRafRef = React.useRef<number | null>(null); const openRafRef = React.useRef<number | null>(null);
const mouseUpTimeoutRef = React.useRef<number | null>(null); const mouseUpTimeoutRef = React.useRef<number | null>(null);
const isMenuVisibleRef = React.useRef(false); const isMenuVisibleRef = React.useRef(false);
const createSession = useSessionUIStore((state) => state.createSession); const activeAddToChatCleanupRef = React.useRef<(() => void) | null>(null);
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionUIStore((state) => state.newSessionDraft?.open); const newSessionDraftOpen = useSessionUIStore((state) => state.newSessionDraft?.open);
const addContextDraft = useInlineCommentDraftStore((state) => state.addDraft); const addContextDraft = useInlineCommentDraftStore((state) => state.addDraft);
@@ -156,6 +157,8 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
activeAddToChatCleanupRef.current?.();
activeAddToChatCleanupRef.current = null;
if (openRafRef.current !== null) { if (openRafRef.current !== null) {
window.cancelAnimationFrame(openRafRef.current); window.cancelAnimationFrame(openRafRef.current);
openRafRef.current = null; openRafRef.current = null;
@@ -169,6 +172,8 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
const hideMenu = React.useCallback(() => { const hideMenu = React.useCallback(() => {
pendingSelectionRef.current = null; pendingSelectionRef.current = null;
activeAddToChatCleanupRef.current?.();
activeAddToChatCleanupRef.current = null;
setCommentRects(null); setCommentRects(null);
if (!isMenuVisibleRef.current) { if (!isMenuVisibleRef.current) {
@@ -209,12 +214,30 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
return Math.min(Math.max(anchorX, minX), maxX); return Math.min(Math.max(anchorX, minX), maxX);
}, []); }, []);
const addMarkdownToChat = React.useCallback((markdownText: string) => {
const markdownBlock = wrapMarkdownSelectionForChat(markdownText);
setPendingInputText(markdownBlock, 'append');
hideMenu();
window.getSelection()?.removeAllRanges();
queueMicrotask(() => {
focusChatInput();
});
}, [hideMenu, setPendingInputText]);
const showMenu = React.useCallback(() => { const showMenu = React.useCallback(() => {
if (!pendingSelectionRef.current) return; if (!pendingSelectionRef.current) return;
const { plainText, markdownText, rect, messageId } = pendingSelectionRef.current; const { plainText, markdownText, rect, messageId } = pendingSelectionRef.current;
const shouldAnimateIn = !position.show; const shouldAnimateIn = !position.show;
activeAddToChatCleanupRef.current?.();
activeAddToChatCleanupRef.current = registerActiveSelectionToolbar({
addToChat: () => addMarkdownToChat(markdownText),
dismiss: hideMenu,
});
// Position menu above the selection // Position menu above the selection
const menuX = isMobile const menuX = isMobile
? rect.left + rect.width / 2 ? rect.left + rect.width / 2
@@ -241,7 +264,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
openRafRef.current = null; openRafRef.current = null;
}); });
} }
}, [getDesktopClampedX, isMobile, position.show]); }, [addMarkdownToChat, getDesktopClampedX, hideMenu, isMobile, position.show]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
if (!position.show || isMobile || !menuRef.current) { if (!position.show || isMobile || !menuRef.current) {
@@ -428,18 +451,8 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
const handleAddToChat = React.useCallback(() => { const handleAddToChat = React.useCallback(() => {
if (!selectedTextMarkdown) return; if (!selectedTextMarkdown) return;
addMarkdownToChat(selectedTextMarkdown);
const markdownBlock = wrapMarkdownSelectionForChat(selectedTextMarkdown); }, [addMarkdownToChat, selectedTextMarkdown]);
setPendingInputText(markdownBlock, 'append');
hideMenu();
// Clear selection
window.getSelection()?.removeAllRanges();
queueMicrotask(() => {
focusChatInput();
});
}, [selectedTextMarkdown, setPendingInputText, hideMenu]);
const handleOpenComment = React.useCallback(() => { const handleOpenComment = React.useCallback(() => {
if (!selectedTextMarkdown) return; if (!selectedTextMarkdown) return;
@@ -473,18 +486,6 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
}); });
}, [addContextDraft, commentText, currentSessionId, effectiveDirectory, hideMenu, newSessionDraftOpen, selectedMessageId, selectedTextMarkdown]); }, [addContextDraft, commentText, currentSessionId, effectiveDirectory, hideMenu, newSessionDraftOpen, selectedMessageId, selectedTextMarkdown]);
const handleCreateNewSession = React.useCallback(async () => {
if (!selectedText) return;
const session = await createSession(undefined, null, null);
if (session) {
setPendingInputText(selectedText, 'replace');
}
hideMenu();
window.getSelection()?.removeAllRanges();
}, [selectedText, createSession, setPendingInputText, hideMenu]);
const currentSession = React.useMemo(() => { const currentSession = React.useMemo(() => {
if (!currentSessionId) { if (!currentSessionId) {
return null; return null;
@@ -686,22 +687,6 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.addToInput')}</span> <span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.addToInput')}</span>
</button> </button>
<button
onClick={handleCreateNewSession}
className={cn(
'flex min-w-0 items-center gap-2 rounded-xl px-3 py-2.5 text-left',
'text-sm font-medium leading-tight',
'bg-[var(--interactive-selection)] text-[var(--interactive-selection-foreground)]',
'active:opacity-80',
'transition-opacity duration-150'
)}
title={t('chat.textSelection.title.newSessionWithSelection')}
type="button"
>
<Icon name="chat-new" className="h-5 w-5 flex-shrink-0" />
<span className="min-w-0 whitespace-normal">{t('chat.textSelection.actions.newSession')}</span>
</button>
{!isVSCodeRuntime() ? ( {!isVSCodeRuntime() ? (
<button <button
onClick={handleAddToNotes} onClick={handleAddToNotes}
@@ -763,39 +748,6 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
{t('chat.textSelection.actions.comment')} {t('chat.textSelection.actions.comment')}
</button> </button>
<div className="mx-0.5 h-5 w-px shrink-0 bg-[var(--interactive-border)]" />
<button
onClick={handleAddToChat}
className={cn(
'px-3.5 py-1.5 rounded-full',
'text-sm font-medium',
'text-[var(--surface-foreground)]',
'hover:bg-[var(--interactive-hover)]',
'transition-colors duration-150'
)}
title={t('chat.textSelection.title.addToCurrentChat')}
type="button"
>
{t('chat.textSelection.actions.addToInput')}
</button>
<div className="mx-0.5 h-5 w-px shrink-0 bg-[var(--interactive-border)]" />
<button
onClick={handleCreateNewSession}
className={cn(
'px-3.5 py-1.5 rounded-full',
'text-sm font-medium',
'text-[var(--surface-foreground)]',
'hover:bg-[var(--interactive-hover)]',
'transition-colors duration-150'
)}
title={t('chat.textSelection.title.newSessionWithSelection')}
type="button"
>
{t('chat.textSelection.actions.newSession')}
</button>
{!isVSCodeRuntime() ? ( {!isVSCodeRuntime() ? (
<> <>
@@ -3,6 +3,7 @@ import { cn } from '@/lib/utils';
import { Icon } from '@/components/icon/Icon'; import { Icon } from '@/components/icon/Icon';
import { useDeviceInfo } from '@/lib/device'; import { useDeviceInfo } from '@/lib/device';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
export interface InlineCommentInputProps { export interface InlineCommentInputProps {
initialText?: string; initialText?: string;
@@ -37,6 +38,7 @@ export function InlineCommentInput({
const { isMobile } = useDeviceInfo(); const { isMobile } = useDeviceInfo();
const [text, setText] = React.useState(initialText); const [text, setText] = React.useState(initialText);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const saveShortcut = formatShortcutForDisplay('mod+enter');
void isEditing; void isEditing;
const handleTextChange = (value: string) => { const handleTextChange = (value: string) => {
@@ -166,7 +168,9 @@ export function InlineCommentInput({
value={text} value={text}
onChange={(e) => handleTextChange(e.target.value)} onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={isMobile ? t('inlineComment.input.placeholderShort') : t('inlineComment.input.placeholder')} placeholder={isMobile
? t('inlineComment.input.placeholderShort')
: t('inlineComment.input.placeholder', { shortcut: saveShortcut })}
className={cn( className={cn(
'min-w-0 flex-1 resize-none bg-transparent text-sm leading-5 text-[var(--surface-foreground)] outline-none placeholder:text-[var(--surface-mutedForeground)] placeholder:opacity-60', 'min-w-0 flex-1 resize-none bg-transparent text-sm leading-5 text-[var(--surface-foreground)] outline-none placeholder:text-[var(--surface-mutedForeground)] placeholder:opacity-60',
isMobile ? 'py-1.5 text-base leading-6' : 'py-1.5' isMobile ? 'py-1.5 text-base leading-6' : 'py-1.5'
@@ -452,10 +452,13 @@ export const ContextPanel: React.FC = () => {
// Lets an agent's browser.open create the tab it needs when none is open yet. // Lets an agent's browser.open create the tab it needs when none is open yet.
// Registered from the panel because opening a tab is panel state, not // Registered from the panel because opening a tab is panel state, not
// something the browser view itself can do before it exists. // something the browser view itself can do before it exists. Background on
// purpose: an agent working a page must not pop the panel open (or steal
// the active surface) under the user — the tab mounts invisibly, and the
// rail is where the user opens it when curious.
React.useEffect(() => { React.useEffect(() => {
if (!effectiveDirectory) return; if (!effectiveDirectory) return;
return registerBrowserOpener((url) => openContextBrowser(effectiveDirectory, url)); return registerBrowserOpener((url) => openContextBrowser(effectiveDirectory, url, { reveal: false }));
}, [effectiveDirectory, openContextBrowser]); }, [effectiveDirectory, openContextBrowser]);
const reorderContextPanelTabs = useUIStore((state) => state.reorderContextPanelTabs); const reorderContextPanelTabs = useUIStore((state) => state.reorderContextPanelTabs);
const setSelectedFilePath = useFilesViewTabsStore((state) => state.setSelectedPath); const setSelectedFilePath = useFilesViewTabsStore((state) => state.setSelectedPath);
@@ -36,6 +36,7 @@ import { cn } from '@/lib/utils';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitStatus } from '@/stores/useGitStore'; import { useGitStatus } from '@/stores/useGitStore';
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore'; import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
const RAIL_TOOLTIP_DELAY_MS = 150; const RAIL_TOOLTIP_DELAY_MS = 150;
// Hold the surface-switch modifier for this long before revealing the order // Hold the surface-switch modifier for this long before revealing the order
@@ -161,6 +162,7 @@ export const ContextPanelRail: React.FC = () => {
const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined)); const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined));
const workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible); const workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible);
const contextRailOrder = useUIStore((state) => state.contextRailOrder); const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const contextRailHiddenSurfaces = useUIStore((state) => state.contextRailHiddenSurfaces);
const setContextRailOrder = useUIStore((state) => state.setContextRailOrder); const setContextRailOrder = useUIStore((state) => state.setContextRailOrder);
const openContextSurface = useUIStore((state) => state.openContextSurface); const openContextSurface = useUIStore((state) => state.openContextSurface);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
@@ -256,12 +258,15 @@ export const ContextPanelRail: React.FC = () => {
const surfaces = React.useMemo(() => { const surfaces = React.useMemo(() => {
return getVisibleContextRailSurfaces({ return getVisibleContextRailSurfaces({
railOrder: contextRailOrder, railOrder: contextRailOrder,
hiddenSurfaces: contextRailHiddenSurfaces,
planModeEnabled, planModeEnabled,
isVSCode: isVSCodeRuntime(), isVSCode: isVSCodeRuntime(),
screenWidth, screenWidth,
tabs, tabs,
}); });
}, [contextRailOrder, planModeEnabled, screenWidth, tabs]); }, [contextRailHiddenSurfaces, contextRailOrder, planModeEnabled, screenWidth, tabs]);
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
const handleDragEnd = React.useCallback((event: DragEndEvent) => { const handleDragEnd = React.useCallback((event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
@@ -331,6 +336,24 @@ export const ContextPanelRail: React.FC = () => {
})} })}
</SortableContext> </SortableContext>
</DndContext> </DndContext>
{/* Outside the sortable list on purpose: this button takes no digit,
cannot be dragged, and configures the rail rather than living on it. */}
<Tooltip delayDuration={RAIL_TOOLTIP_DELAY_MS}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={t('contextRail.configure.open')}
onClick={() => setIsSurfacesDialogOpen(true)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:text-foreground"
>
<Icon name="equalizer-2" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
{t('contextRail.configure.open')}
</TooltipContent>
</Tooltip>
<ContextRailSurfacesDialog open={isSurfacesDialogOpen} onOpenChange={setIsSurfacesDialogOpen} />
</nav> </nav>
); );
}; };
@@ -0,0 +1,78 @@
import React from 'react';
import { useI18n } from '@/lib/i18n';
import { useUIStore } from '@/stores/useUIStore';
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { sortContextSurfaces } from '@/lib/surfaces/registry';
/**
* Which surfaces the context rail shows. Everything is on by default and the
* choice is stored as the *hidden* set, so a surface added in a later release
* appears for everyone rather than staying invisible to whoever had saved
* settings before it existed. Hidden surfaces also leave the digit shortcuts
* (the rail and the shortcut share one visibility filter).
*/
export const ContextRailSurfacesDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const { t } = useI18n();
const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const hidden = useUIStore((state) => state.contextRailHiddenSurfaces);
const setSurfaceVisible = useUIStore((state) => state.setContextRailSurfaceVisible);
const setHiddenSurfaces = useUIStore((state) => state.setContextRailHiddenSurfaces);
// The full registry in the user's rail order — including surfaces a runtime
// filter currently drops, so a choice made on desktop is editable anywhere.
const surfaces = React.useMemo(() => sortContextSurfaces(contextRailOrder), [contextRailOrder]);
const allVisible = hidden.length === 0;
const noneVisible = surfaces.every((surface) => hidden.includes(surface.id));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('contextRail.configure.dialogTitle')}</DialogTitle>
<DialogDescription>{t('contextRail.configure.dialogDescription')}</DialogDescription>
</DialogHeader>
<div className="flex flex-col">
{surfaces.map((surface) => (
<SettingsCheckboxRow
key={surface.id}
settingsItem={`layout.context-rail.surface.${surface.id}`}
checked={!hidden.includes(surface.id)}
onChange={(checked) => setSurfaceVisible(surface.id, checked)}
label={t(surface.labelKey)}
ariaLabel={t(surface.labelKey)}
/>
))}
</div>
{!allVisible ? (
<div className="flex items-center justify-between border-t pt-3">
{noneVisible ? (
<span className="text-xs text-destructive">{t('contextRail.configure.noneWarning')}</span>
) : <span />}
<Button
variant="link"
size="xs"
onClick={() => setHiddenSurfaces([])}
className="normal-case text-muted-foreground hover:text-foreground"
>
{t('contextRail.configure.showAll')}
</Button>
</div>
) : null}
</DialogContent>
</Dialog>
);
};
+16 -74
View File
@@ -38,7 +38,8 @@ import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControl
import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device'; import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { formatShortcutForDisplay, getEffectiveShortcutCombo, type ShortcutActionId } from '@/lib/shortcuts';
import { useKeybinds } from '@/hooks/useKeybind';
import { import {
} from '@/lib/quota/model-families'; } from '@/lib/quota/model-families';
@@ -256,7 +257,7 @@ type DesktopServicesMenuProps = {
isDesktopServicesOpen: boolean; isDesktopServicesOpen: boolean;
setIsDesktopServicesOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsDesktopServicesOpen: React.Dispatch<React.SetStateAction<boolean>>;
refreshCurrentInstanceLabel: () => Promise<void>; refreshCurrentInstanceLabel: () => Promise<void>;
shortcutLabel: (actionId: string) => string; shortcutLabel: (actionId: ShortcutActionId) => string;
remoteUpdateInfo: UpdateInfo | null; remoteUpdateInfo: UpdateInfo | null;
remoteUpdateChecking: boolean; remoteUpdateChecking: boolean;
remoteUpdateError: string | null; remoteUpdateError: string | null;
@@ -433,7 +434,6 @@ export const Header: React.FC = () => {
const { t } = useI18n(); const { t } = useI18n();
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
const openContextOverview = useUIStore((state) => state.openContextOverview); const openContextOverview = useUIStore((state) => state.openContextOverview);
const openContextPlan = useUIStore((state) => state.openContextPlan);
const closeContextPanel = useUIStore((state) => state.closeContextPanel); const closeContextPanel = useUIStore((state) => state.closeContextPanel);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const sessionTabsEnabled = useUIStore((state) => state.sessionTabsEnabled); const sessionTabsEnabled = useUIStore((state) => state.sessionTabsEnabled);
@@ -485,8 +485,6 @@ export const Header: React.FC = () => {
const pathSegments = activeProject.path.split(/[\\/]/).filter(Boolean); const pathSegments = activeProject.path.split(/[\\/]/).filter(Boolean);
return pathSegments[pathSegments.length - 1] ?? null; return pathSegments[pathSegments.length - 1] ?? null;
}, [activeProject]); }, [activeProject]);
const quotaResults = useQuotaStore((state) => state.results);
const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas);
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings); const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
const { isMobile } = useDeviceInfo(); const { isMobile } = useDeviceInfo();
@@ -1264,21 +1262,6 @@ export const Header: React.FC = () => {
const isContextPanelActive = activeContextMode === 'context'; const isContextPanelActive = activeContextMode === 'context';
const handleOpenContextPlan = React.useCallback(() => {
const directory = normalize(openDirectory || '');
if (!directory) {
return;
}
const panelState = useUIStore.getState().contextPanelByDirectory[directory];
if (getActiveContextMode(panelState) === 'plan') {
closeContextPanel(directory);
return;
}
openContextPlan(directory);
}, [closeContextPanel, openContextPlan, openDirectory]);
const desktopHeaderIconButtonClass = DESKTOP_HEADER_ICON_BUTTON_CLASS; const desktopHeaderIconButtonClass = DESKTOP_HEADER_ICON_BUTTON_CLASS;
// Left padding the header needs to clear the OS window controls (macOS // Left padding the header needs to clear the OS window controls (macOS
@@ -1445,67 +1428,26 @@ export const Header: React.FC = () => {
} }
}, [isDesktopApp]); }, [isDesktopApp]);
const shortcutLabel = React.useCallback((actionId: string) => { const shortcutLabel = React.useCallback((actionId: ShortcutActionId) => {
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides)); return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
}, [shortcutOverrides]); }, [shortcutOverrides]);
// Desktop keeps instances only: quota and MCP now live in the work-status
// panel, which reports them per session rather than per window. The mobile
// menu below is untouched — it has no panel to defer to.
const servicesTabs = React.useMemo(() => {
const base: Array<{ value: 'instance' | 'usage' | 'mcp'; label: string; icon: React.ReactNode }> = [];
if (isDesktopApp) {
base.push({ value: 'instance', label: t('layout.services.instance'), icon: <Icon name="server" className="h-3.5 w-3.5" /> });
}
return base;
}, [isDesktopApp, t]);
useEffect(() => { useKeybinds({
const handleKeyDown = (e: KeyboardEvent) => { rename_current_session: () => {
const toggleServicesCombo = getEffectiveShortcutCombo('toggle_services_menu', shortcutOverrides); if (!currentSessionId || isMobile) return false;
if (eventMatchesShortcut(e, toggleServicesCombo)) { beginHeaderSessionRename();
e.preventDefault(); },
toggle_services_menu: () => {
if (isDesktopServicesOpen) { if (isDesktopServicesOpen) {
setIsDesktopServicesOpen(false); setIsDesktopServicesOpen(false);
} else {
setIsDesktopServicesOpen(true);
void refreshCurrentInstanceLabel();
}
return; return;
} }
setIsDesktopServicesOpen(true);
// The desktop menu holds one destination now, so this shortcut opens it void refreshCurrentInstanceLabel();
// rather than cycling. The binding is kept: it is user-configurable and },
// silently dropping it would break existing setups. });
const cycleServicesCombo = getEffectiveShortcutCombo('cycle_services_tab', shortcutOverrides);
if (eventMatchesShortcut(e, cycleServicesCombo)) {
e.preventDefault();
if (servicesTabs.length === 0) return;
setIsDesktopServicesOpen(true);
void refreshCurrentInstanceLabel();
return;
}
const toggleContextPlanCombo = getEffectiveShortcutCombo('toggle_context_plan', shortcutOverrides);
if (eventMatchesShortcut(e, toggleContextPlanCombo)) {
e.preventDefault();
handleOpenContextPlan();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
shortcutOverrides,
isDesktopServicesOpen,
servicesTabs,
quotaResults.length,
fetchAllQuotas,
refreshCurrentInstanceLabel,
handleOpenContextPlan,
]);
const desktopSidebarActions = ( const desktopSidebarActions = (
<> <>
@@ -42,6 +42,7 @@ export const DefaultsSettings: React.FC = () => {
const setModel = useConfigStore((state) => state.setModel); const setModel = useConfigStore((state) => state.setModel);
const setAgent = useConfigStore((state) => state.setAgent); const setAgent = useConfigStore((state) => state.setAgent);
const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant); const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant);
const setCurrentVariantOverride = useConfigStore((state) => state.setCurrentVariantOverride);
const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel); const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel);
const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant); const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant);
const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent); const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent);
@@ -210,7 +211,7 @@ export const DefaultsSettings: React.FC = () => {
setDefaultVariant(newValue); setDefaultVariant(newValue);
setSettingsDefaultVariant(newValue); setSettingsDefaultVariant(newValue);
if (!chatHasOwnModel) { if (!chatHasOwnModel) {
setCurrentVariant(newValue); setCurrentVariantOverride(newValue ?? null, newValue);
} }
try { try {
@@ -219,7 +220,7 @@ export const DefaultsSettings: React.FC = () => {
console.warn('Failed to save default variant:', error); console.warn('Failed to save default variant:', error);
} }
}, },
[chatHasOwnModel, setCurrentVariant, setSettingsDefaultVariant] [chatHasOwnModel, setCurrentVariantOverride, setSettingsDefaultVariant]
); );
const handleAgentChange = React.useCallback( const handleAgentChange = React.useCallback(
@@ -1,12 +1,7 @@
import React from 'react'; import React from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { SettingsFieldRow, SettingsSection } from '@/components/sections/shared/SettingsSection';
import {
SettingsSection,
SettingsFieldRow,
} from '@/components/sections/shared/SettingsSection';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { cn } from '@/lib/utils';
import { updateDesktopSettings } from '@/lib/persistence'; import { updateDesktopSettings } from '@/lib/persistence';
import { isVSCodeRuntime } from '@/lib/desktop'; import { isVSCodeRuntime } from '@/lib/desktop';
import { import {
@@ -14,314 +9,128 @@ import {
getCustomizableShortcutActions, getCustomizableShortcutActions,
getEffectiveShortcutCombo, getEffectiveShortcutCombo,
getEffectiveShortcutPrefix, getEffectiveShortcutPrefix,
isRiskyBrowserShortcut,
keyToShortcutToken,
normalizeCombo,
UNASSIGNED_SHORTCUT, UNASSIGNED_SHORTCUT,
type ShortcutActionId,
type ShortcutCategory,
type ShortcutCombo, type ShortcutCombo,
type CustomizableShortcutAction,
} from '@/lib/shortcuts'; } from '@/lib/shortcuts';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { ShortcutRecordingDialog } from './ShortcutRecordingDialog';
const MODIFIER_KEYS = new Set(['shift', 'control', 'alt', 'meta']); const CATEGORIES: ShortcutCategory[] = ['session', 'models', 'panels', 'navigation', 'application'];
const keyboardEventToCombo = (event: React.KeyboardEvent<HTMLInputElement>): ShortcutCombo | null => {
if (MODIFIER_KEYS.has(event.key.toLowerCase())) {
return null;
}
const parts: string[] = [];
if (event.metaKey || event.ctrlKey) {
parts.push('mod');
}
if (event.shiftKey) {
parts.push('shift');
}
if (event.altKey) {
parts.push('alt');
}
const keyToken = keyToShortcutToken(event.key);
if (!keyToken) {
return null;
}
parts.push(keyToken);
return normalizeCombo(parts.join('+'));
};
// Prefix capture for chord-style shortcuts (e.g. "switch context panel
// surface"): a bare modifier press is accepted so the prefix can be just the
// primary modifier (default) or a modifier + key chord like `mod+p`.
const keyboardEventToPrefixCombo = (event: React.KeyboardEvent<HTMLInputElement>): ShortcutCombo | null => {
const parts: string[] = [];
if (event.metaKey || event.ctrlKey) {
parts.push('mod');
}
if (event.shiftKey) {
parts.push('shift');
}
if (event.altKey) {
parts.push('alt');
}
if (MODIFIER_KEYS.has(event.key.toLowerCase())) {
return parts.length > 0 ? normalizeCombo(parts.join('+')) : null;
}
const keyToken = keyToShortcutToken(event.key);
if (!keyToken) {
return null;
}
parts.push(keyToken);
return parts.length > 0 ? normalizeCombo(parts.join('+')) : null;
};
export const KeyboardShortcutsSettings: React.FC = () => { export const KeyboardShortcutsSettings: React.FC = () => {
const { t } = useI18n(); const { t } = useI18n();
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const setShortcutOverride = useUIStore((state) => state.setShortcutOverride); const setShortcutOverride = useUIStore((state) => state.setShortcutOverride);
const clearShortcutOverride = useUIStore((state) => state.clearShortcutOverride); const clearShortcutOverride = useUIStore((state) => state.clearShortcutOverride);
const resetAllShortcutOverrides = useUIStore((state) => state.resetAllShortcutOverrides); const resetAllShortcutOverrides = useUIStore((state) => state.resetAllShortcutOverrides);
const [editingAction, setEditingAction] = React.useState<CustomizableShortcutAction | null>(null);
const actions = React.useMemo(() => { const actions = React.useMemo(() => {
const all = getCustomizableShortcutActions(); const all = getCustomizableShortcutActions();
if (!isVSCodeRuntime()) { return isVSCodeRuntime() ? all.filter((action) => action.id !== 'toggle_prompt_navigator') : all;
return all;
}
return all.filter((action) => action.id !== 'toggle_prompt_navigator');
}, []); }, []);
const actionLabel = React.useCallback((id: string, fallbackLabel: string): string => { const persist = (nextOverrides: Record<string, ShortcutCombo>) => {
const key = `settings.openchamber.keyboardShortcuts.action.${id}.label`;
const translated = tUnsafe(key);
return translated === key ? fallbackLabel : translated;
}, [tUnsafe]);
const [capturingActionId, setCapturingActionId] = React.useState<string | null>(null);
const [draftByAction, setDraftByAction] = React.useState<Record<string, ShortcutCombo>>({});
const [errorText, setErrorText] = React.useState<string>('');
const [warningText, setWarningText] = React.useState<string>('');
const [pendingOverwrite, setPendingOverwrite] = React.useState<{
actionId: string;
combo: ShortcutCombo;
conflictActionId: string;
} | null>(null);
const persistShortcutOverrides = React.useCallback((nextOverrides: Record<string, ShortcutCombo>) => {
void updateDesktopSettings({ shortcutOverrides: nextOverrides }); void updateDesktopSettings({ shortcutOverrides: nextOverrides });
}, []); };
const save = (
const findConflict = React.useCallback((actionId: string, combo: ShortcutCombo): string | null => { actionId: ShortcutActionId,
const normalized = normalizeCombo(combo); combo: ShortcutCombo,
for (const action of actions) { replaceActionId?: ShortcutActionId,
if (action.id === actionId) { ) => {
continue; const nextOverrides = { ...shortcutOverrides, [actionId]: combo };
} if (replaceActionId) nextOverrides[replaceActionId] = UNASSIGNED_SHORTCUT;
const existing = getEffectiveShortcutCombo(action.id, shortcutOverrides); setShortcutOverride(actionId, combo);
if (normalizeCombo(existing) === normalized) { if (replaceActionId) setShortcutOverride(replaceActionId, UNASSIGNED_SHORTCUT);
return action.id; persist(nextOverrides);
} };
} const resetOne = (actionId: ShortcutActionId) => {
return null;
}, [actions, shortcutOverrides]);
const saveCombo = React.useCallback((actionId: string, combo: ShortcutCombo) => {
const normalized = normalizeCombo(combo);
const conflictActionId = findConflict(actionId, normalized);
if (conflictActionId) {
setPendingOverwrite({ actionId, combo: normalized, conflictActionId });
setErrorText('');
return;
}
const nextOverrides = { ...shortcutOverrides, [actionId]: normalized };
setShortcutOverride(actionId, normalized);
persistShortcutOverrides(nextOverrides);
setPendingOverwrite(null);
setErrorText('');
setWarningText(isRiskyBrowserShortcut(normalized) ? t('settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut') : '');
setDraftByAction((current) => {
const rest = { ...current };
delete rest[actionId];
return rest;
});
}, [findConflict, persistShortcutOverrides, setShortcutOverride, shortcutOverrides, t]);
const confirmOverwrite = React.useCallback(() => {
if (!pendingOverwrite) {
return;
}
const nextOverrides = {
...shortcutOverrides,
[pendingOverwrite.conflictActionId]: UNASSIGNED_SHORTCUT,
[pendingOverwrite.actionId]: pendingOverwrite.combo,
};
setShortcutOverride(pendingOverwrite.conflictActionId, UNASSIGNED_SHORTCUT);
setShortcutOverride(pendingOverwrite.actionId, pendingOverwrite.combo);
persistShortcutOverrides(nextOverrides);
setPendingOverwrite(null);
setErrorText('');
setWarningText(isRiskyBrowserShortcut(pendingOverwrite.combo) ? t('settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut') : '');
setDraftByAction((current) => {
const rest = { ...current };
delete rest[pendingOverwrite.actionId];
return rest;
});
}, [pendingOverwrite, persistShortcutOverrides, setShortcutOverride, shortcutOverrides, t]);
const resetOne = React.useCallback((actionId: string) => {
const nextOverrides = { ...shortcutOverrides }; const nextOverrides = { ...shortcutOverrides };
delete nextOverrides[actionId]; delete nextOverrides[actionId];
clearShortcutOverride(actionId); clearShortcutOverride(actionId);
persistShortcutOverrides(nextOverrides); persist(nextOverrides);
setDraftByAction((current) => { };
const rest = { ...current }; const shortcutDisplay = (action: CustomizableShortcutAction): string => {
delete rest[actionId]; const isPrefixStyle = 'prefixStyle' in action && action.prefixStyle;
return rest; const combo = isPrefixStyle
}); ? getEffectiveShortcutPrefix(action.id, shortcutOverrides)
setPendingOverwrite(null); : getEffectiveShortcutCombo(action.id, shortcutOverrides);
setErrorText(''); const formatted = formatShortcutForDisplay(
setWarningText(''); combo,
}, [clearShortcutOverride, persistShortcutOverrides, shortcutOverrides]); t('settings.openchamber.keyboardShortcuts.unassigned'),
);
if (!isPrefixStyle || !combo || combo === UNASSIGNED_SHORTCUT) return formatted;
const suffix = action.id === 'switch_session_tab'
? t('settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix')
: t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix');
return `${formatted}${suffix}`;
};
return ( return (
<SettingsSection <>
settingsItem="shortcuts.keyboard-shortcuts" {CATEGORIES.map((category, categoryIndex) => {
title={t('settings.openchamber.keyboardShortcuts.title')} const categoryActions = actions.filter((action) => action.category === category);
divider={false} if (categoryActions.length === 0) return null;
info={t('settings.openchamber.keyboardShortcuts.tooltip')} return (
headerAction={( <SettingsSection
<Button key={category}
type="button" settingsItem={categoryIndex === 0 ? 'shortcuts.keyboard-shortcuts' : undefined}
variant="outline" title={t(`settings.openchamber.keyboardShortcuts.category.${category}`)}
size="xs" divider={categoryIndex !== 0}
className="!font-normal" info={categoryIndex === 0 ? t('settings.openchamber.keyboardShortcuts.tooltip') : undefined}
onClick={() => { headerAction={categoryIndex === 0 ? (
resetAllShortcutOverrides(); <Button type="button" variant="outline" size="xs" className="!font-normal" onClick={() => {
persistShortcutOverrides({}); resetAllShortcutOverrides();
setDraftByAction({}); persist({});
setPendingOverwrite(null); }}>
setErrorText(''); {t('settings.openchamber.keyboardShortcuts.actions.resetAll')}
setWarningText(''); </Button>
}} ) : undefined}
> >
{t('settings.openchamber.keyboardShortcuts.actions.resetAll')} <div className="space-y-2">
</Button> {categoryActions.map((action) => (
)} <SettingsFieldRow key={action.id} label={t(action.settingsLabelKey)}>
> <kbd
{(errorText || warningText || pendingOverwrite) && ( className="min-w-32 rounded-md border border-border bg-muted px-2 py-1 text-center typography-meta font-mono text-foreground"
<div className="mb-2 space-y-2"> >
{pendingOverwrite && ( {shortcutDisplay(action)}
<div className="rounded-lg border border-[var(--status-warning-border)] bg-[var(--status-warning-background)] p-3 flex flex-col @xl:flex-row @xl:items-center justify-between gap-3"> </kbd>
<span className="typography-meta text-foreground"> <Button
{t('settings.openchamber.keyboardShortcuts.overwritePrompt')} type="button"
</span> variant="secondary"
<div className="flex gap-2 shrink-0"> size="xs"
<Button type="button" size="xs" className="!font-normal" onClick={confirmOverwrite}>{t('settings.openchamber.keyboardShortcuts.actions.overwrite')}</Button> className="!font-normal"
<Button type="button" size="xs" className="!font-normal" variant="ghost" onClick={() => setPendingOverwrite(null)}>{t('settings.common.actions.cancel')}</Button> onClick={() => setEditingAction(action)}
</div> >
{t('settings.openchamber.keyboardShortcuts.actions.edit')}
</Button>
{action.id in shortcutOverrides ? (
<Button
type="button"
variant="ghost"
size="xs"
className="!font-normal"
onClick={() => resetOne(action.id)}
>
{t('settings.common.actions.reset')}
</Button>
) : null}
</SettingsFieldRow>
))}
</div> </div>
)} </SettingsSection>
{errorText && ( );
<div className="rounded-lg border border-[var(--status-error-border)] bg-[var(--status-error-background)] p-3 typography-meta text-foreground"> })}
{errorText} <ShortcutRecordingDialog
</div> action={editingAction}
)} overrides={shortcutOverrides}
{warningText && ( onSave={save}
<div className="rounded-lg border border-[var(--status-warning-border)] bg-[var(--status-warning-background)] p-3 typography-meta text-foreground"> onOpenChange={(open) => {
{warningText} if (!open) setEditingAction(null);
</div> }}
)} />
</div> </>
)}
<div>
{actions.map((action, index) => {
const isSurfaceSwitch = action.id === 'switch_context_surface';
const effective = isSurfaceSwitch
? getEffectiveShortcutPrefix(action.id, shortcutOverrides)
: getEffectiveShortcutCombo(action.id, shortcutOverrides);
const draft = draftByAction[action.id];
const displayCombo = draft ?? effective;
const hasDraft = typeof draft === 'string' && normalizeCombo(draft) !== normalizeCombo(effective);
const isUnassignedDisplay = displayCombo === '' || normalizeCombo(displayCombo) === UNASSIGNED_SHORTCUT;
const displayValue = capturingActionId === action.id
? t('settings.openchamber.keyboardShortcuts.field.pressKeys')
: isSurfaceSwitch && !isUnassignedDisplay
? `${formatShortcutForDisplay(displayCombo)}${t('settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix')}`
: formatShortcutForDisplay(displayCombo);
return (
<div key={action.id} className={cn("py-1.5", index > 0 && "border-t border-border/40")}>
<SettingsFieldRow
label={actionLabel(action.id, action.label)}
alignEnd={false}
>
<Input
readOnly
value={displayValue}
onFocus={() => {
setCapturingActionId(action.id);
setErrorText('');
}}
onBlur={() => {
if (capturingActionId === action.id) {
setCapturingActionId(null);
}
}}
onKeyDown={(event) => {
event.preventDefault();
event.stopPropagation();
if (event.key === 'Escape') {
setCapturingActionId(null);
return;
}
const combo = isSurfaceSwitch ? keyboardEventToPrefixCombo(event) : keyboardEventToCombo(event);
if (!combo) {
return;
}
setDraftByAction((current) => ({
...current,
[action.id]: combo,
}));
setCapturingActionId(null);
setPendingOverwrite(null);
setErrorText('');
}}
className="h-7 w-40 min-w-0 typography-ui-label text-center"
/>
<Button
type="button"
variant="secondary"
size="xs"
className="!font-normal"
onClick={() => {
const next = draftByAction[action.id];
if (!next) {
setErrorText(t('settings.openchamber.keyboardShortcuts.error.captureFirst'));
return;
}
saveCombo(action.id, next);
}}
disabled={!hasDraft}
>
{t('settings.common.actions.saveChanges')}
</Button>
<Button type="button" size="xs" className="!font-normal" variant="ghost" onClick={() => resetOne(action.id)}>
{t('settings.common.actions.reset')}
</Button>
</SettingsFieldRow>
</div>
);
})}
</div>
</SettingsSection>
); );
}; };
@@ -62,6 +62,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import type { TerminalShellOption } from '@/lib/api/types'; import type { TerminalShellOption } from '@/lib/api/types';
import { isTerminalShell } from '@/lib/terminalShell'; import { isTerminalShell } from '@/lib/terminalShell';
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch'; import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
interface Option<T extends string> { interface Option<T extends string> {
id: T; id: T;
@@ -1480,7 +1481,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
label={t('settings.openchamber.visual.field.terminalQuickKeys')} label={t('settings.openchamber.visual.field.terminalQuickKeys')}
ariaLabel={t('settings.openchamber.visual.field.terminalQuickKeysAria')} ariaLabel={t('settings.openchamber.visual.field.terminalQuickKeysAria')}
settingsItem="appearance.terminal-quick-keys" settingsItem="appearance.terminal-quick-keys"
info={t('settings.openchamber.visual.field.terminalQuickKeysTooltip')} info={t('settings.openchamber.visual.field.terminalQuickKeysTooltip', {
control: formatShortcutForDisplay('ctrl'),
alt: formatShortcutForDisplay('alt'),
})}
/> />
)} )}
</div> </div>
@@ -0,0 +1,65 @@
import { describe, expect, test } from 'bun:test';
import { settleShortcutRecordingState, updateShortcutRecordingState } from './ShortcutRecordingDialog';
const emptyState = { chords: [], livePreview: null, settled: false };
function keyEvent(key: string, modifiers: Partial<Record<'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey', boolean>> = {}) {
const code = /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` : /^[0-9]$/.test(key) ? `Digit${key}` : key;
return { key, code, repeat: false, isComposing: false, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...modifiers };
}
describe('ShortcutRecordingDialog recording state', () => {
test('previews modifiers and clears the preview when they are released', () => {
const pressed = updateShortcutRecordingState(emptyState, keyEvent('Control', { ctrlKey: true, shiftKey: true }), 'keydown');
expect(pressed.livePreview).toBe('mod+shift');
expect(updateShortcutRecordingState(pressed, keyEvent('Control'), 'keyup').livePreview).toBeNull();
});
test('waits after the first chord and settles when a second chord is recorded', () => {
const first = updateShortcutRecordingState(emptyState, keyEvent('s', { ctrlKey: true }), 'keydown');
const second = updateShortcutRecordingState(first, keyEvent('p'), 'keydown');
const third = updateShortcutRecordingState(second, keyEvent('x'), 'keydown');
expect(first.chords).toEqual(['mod+s']);
expect(first.settled).toBe(false);
expect(second.chords).toEqual(['mod+s', 'p']);
expect(second.settled).toBe(true);
expect(third.chords).toEqual(['x']);
expect(third.settled).toBe(false);
});
test('settles a single chord for timeout and Confirm validation', () => {
const waiting = updateShortcutRecordingState(emptyState, keyEvent('s', { ctrlKey: true }), 'keydown');
expect(settleShortcutRecordingState(waiting)).toEqual({ chords: ['mod+s'], livePreview: null, settled: true });
});
test('records at most three simultaneous keys', () => {
const previous = { chords: ['mod+k'], livePreview: null, settled: false };
const threeKeys = updateShortcutRecordingState(
previous,
keyEvent('s', { ctrlKey: true, shiftKey: true }),
'keydown',
);
const fourKeys = updateShortcutRecordingState(
previous,
keyEvent('s', { ctrlKey: true, metaKey: true, shiftKey: true }),
'keydown',
);
expect(threeKeys.chords).toEqual(['mod+k', 'mod+shift+s']);
expect(fourKeys.chords).toEqual(['mod+k']);
});
test('ignores repeat and IME events', () => {
expect(updateShortcutRecordingState(emptyState, { ...keyEvent('k', { ctrlKey: true }), repeat: true }, 'keydown')).toEqual(emptyState);
expect(updateShortcutRecordingState(emptyState, { ...keyEvent('k', { ctrlKey: true }), isComposing: true }, 'keydown')).toEqual(emptyState);
});
test('records Enter and Escape while Backspace removes the final chord', () => {
const state = { chords: ['mod+k', 'mod+p'], livePreview: null, settled: true };
expect(updateShortcutRecordingState(emptyState, keyEvent('Enter'), 'keydown').chords).toEqual(['enter']);
expect(updateShortcutRecordingState(emptyState, keyEvent('Escape'), 'keydown').chords).toEqual(['escape']);
expect(updateShortcutRecordingState(state, keyEvent('Backspace'), 'keydown').chords).toEqual(['mod+k']);
expect(updateShortcutRecordingState(state, keyEvent('Backspace'), 'keydown').settled).toBe(false);
expect(updateShortcutRecordingState({ chords: ['mod+k'], livePreview: null, settled: false }, keyEvent('Backspace'), 'keydown')).toEqual(emptyState);
});
});
@@ -0,0 +1,315 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
formatShortcutForDisplay,
getShortcutBindingConflicts,
isRiskyBrowserShortcut,
keyToShortcutToken,
resolveShortcutEventKey,
normalizeCombo,
type ShortcutActionId,
type ShortcutBindingConflict,
type ShortcutCombo,
type CustomizableShortcutAction,
} from '@/lib/shortcuts';
import { useI18n } from '@/lib/i18n';
const MODIFIER_KEYS = new Set(['shift', 'control', 'alt', 'meta']);
const MAX_SHORTCUT_KEY_COUNT = 3;
const SECOND_CHORD_TIMEOUT_MS = 3000;
interface RecordingKeyboardEvent {
altKey: boolean;
code: string;
ctrlKey: boolean;
isComposing: boolean;
key: string;
metaKey: boolean;
repeat: boolean;
shiftKey: boolean;
}
interface ShortcutRecordingState {
chords: ShortcutCombo[];
livePreview: ShortcutCombo | null;
settled: boolean;
}
interface ShortcutRecordingDialogProps {
action: CustomizableShortcutAction | null;
overrides: Record<string, string>;
onSave: (
actionId: ShortcutActionId,
combo: ShortcutCombo,
replaceActionId?: ShortcutActionId,
) => void;
onOpenChange: (open: boolean) => void;
}
function getPhysicalKeyCount(
event: Pick<RecordingKeyboardEvent, 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'>,
includeEventKey = false,
): number {
const keys = new Set<string>();
if (event.altKey) keys.add('alt');
if (event.ctrlKey) keys.add('control');
if (event.metaKey) keys.add('meta');
if (event.shiftKey) keys.add('shift');
if (includeEventKey) keys.add(event.key.toLowerCase());
return keys.size;
}
function isCustomizableConflict(
conflict: ShortcutBindingConflict,
): conflict is ShortcutBindingConflict & { action: CustomizableShortcutAction } {
return conflict.action.customizable;
}
function getModifierPreview(event: RecordingKeyboardEvent): ShortcutCombo | null {
if (getPhysicalKeyCount(event) > MAX_SHORTCUT_KEY_COUNT) return null;
const parts: string[] = [];
if (event.metaKey || event.ctrlKey) parts.push('mod');
if (event.shiftKey) parts.push('shift');
if (event.altKey) parts.push('alt');
return parts.length > 0 ? normalizeCombo(parts.join('+')) : null;
}
function keyboardEventToCombo(event: RecordingKeyboardEvent): ShortcutCombo | null {
if (MODIFIER_KEYS.has(event.key.toLowerCase())) return null;
if (getPhysicalKeyCount(event, true) > MAX_SHORTCUT_KEY_COUNT) return null;
const key = keyToShortcutToken(resolveShortcutEventKey(event));
if (!key) return null;
const parts: string[] = [];
if (event.metaKey || event.ctrlKey) parts.push('mod');
if (event.shiftKey) parts.push('shift');
if (event.altKey) parts.push('alt');
parts.push(key);
return normalizeCombo(parts.join('+'));
}
function modifierKeyUpToCombo(event: React.KeyboardEvent<HTMLDivElement>): ShortcutCombo | null {
const key = event.key.toLowerCase();
if (!MODIFIER_KEYS.has(key)) return null;
if (getPhysicalKeyCount(event, true) > MAX_SHORTCUT_KEY_COUNT) return null;
const parts: string[] = [];
if (event.metaKey || event.ctrlKey || key === 'meta' || key === 'control') parts.push('mod');
if (event.shiftKey || key === 'shift') parts.push('shift');
if (event.altKey || key === 'alt') parts.push('alt');
return parts.length > 0 ? normalizeCombo(parts.join('+')) : null;
}
// eslint-disable-next-line react-refresh/only-export-components -- tested pure recording state transition
export function settleShortcutRecordingState(state: ShortcutRecordingState): ShortcutRecordingState {
return state.chords.length > 0 ? { ...state, livePreview: null, settled: true } : state;
}
// eslint-disable-next-line react-refresh/only-export-components -- tested pure recording state transition
export function updateShortcutRecordingState(
state: ShortcutRecordingState,
event: RecordingKeyboardEvent,
phase: 'keydown' | 'keyup',
): ShortcutRecordingState {
if (event.repeat || event.isComposing) return state;
if (phase === 'keyup') {
return { ...state, livePreview: getModifierPreview(event) };
}
if (event.key === 'Backspace') {
return { chords: state.chords.slice(0, -1), livePreview: null, settled: false };
}
const chord = keyboardEventToCombo(event);
if (chord) {
if (state.settled) {
return { chords: [chord], livePreview: null, settled: false };
}
const chords = state.chords.length < 2 ? [...state.chords, chord] : state.chords;
return {
chords,
livePreview: null,
settled: chords.length === 2,
};
}
return { ...state, livePreview: getModifierPreview(event) };
}
export const ShortcutRecordingDialog: React.FC<ShortcutRecordingDialogProps> = ({
action,
overrides,
onSave,
onOpenChange,
}) => {
const { t } = useI18n();
const actionLabel = (shortcut: CustomizableShortcutAction) => t(shortcut.settingsLabelKey);
const conflictActionLabel = (conflict: ShortcutBindingConflict) => (
conflict.action.customizable
? actionLabel(conflict.action)
: formatShortcutForDisplay(conflict.action.defaultBinding)
);
const [recording, setRecording] = React.useState<ShortcutRecordingState>({ chords: [], livePreview: null, settled: false });
const recordingRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!action) return;
setRecording({ chords: [], livePreview: null, settled: false });
recordingRef.current?.focus();
}, [action]);
const waitingForSecondChord = recording.chords.length === 1 && !recording.settled;
React.useEffect(() => {
if (!waitingForSecondChord) return;
const timeout = window.setTimeout(
() => setRecording(settleShortcutRecordingState),
SECOND_CHORD_TIMEOUT_MS,
);
return () => window.clearTimeout(timeout);
}, [waitingForSecondChord]);
const combo = normalizeCombo(recording.chords.join(' '));
const conflicts = React.useMemo(
() => action && combo ? getShortcutBindingConflicts(action.id, combo, overrides) : [],
[action, combo, overrides],
);
const protectedConflict = conflicts.find((conflict) => (
!conflict.action.customizable && conflict.kind !== 'contextual-prefix'
));
const customizableConflicts = conflicts.filter(isCustomizableConflict);
const prefixConflict = customizableConflicts.find((conflict) => conflict.kind === 'prefix');
const exactConflict = customizableConflicts.find((conflict) => conflict.kind === 'exact');
const contextualPrefixConflict = conflicts.find((conflict) => conflict.kind === 'contextual-prefix');
const close = () => onOpenChange(false);
const confirm = () => {
if (!recording.settled) setRecording(settleShortcutRecordingState);
if (!action || !combo || protectedConflict || prefixConflict) return;
onSave(action.id, combo, exactConflict?.action.id);
close();
};
const handleRecordingEvent = (event: React.KeyboardEvent<HTMLDivElement>, phase: 'keydown' | 'keyup') => {
event.preventDefault();
event.stopPropagation();
const isPrefixStyleAction = Boolean(action && 'prefixStyle' in action && action.prefixStyle);
if (phase === 'keyup' && isPrefixStyleAction && recording.chords.length === 0) {
const modifierCombo = modifierKeyUpToCombo(event);
if (modifierCombo) {
setRecording({ chords: [modifierCombo], livePreview: null, settled: true });
return;
}
}
const nextRecording = updateShortcutRecordingState(recording, {
altKey: event.altKey,
code: event.nativeEvent.code,
ctrlKey: event.ctrlKey,
isComposing: event.nativeEvent.isComposing,
key: event.key,
metaKey: event.metaKey,
repeat: event.repeat,
shiftKey: event.shiftKey,
}, phase);
setRecording(isPrefixStyleAction && nextRecording.chords.length > 1
? recording
: nextRecording);
};
return (
<Dialog
open={action !== null}
onOpenChange={(open, eventDetails) => {
if (!open) {
eventDetails.cancel();
}
}}
>
<DialogContent className="max-w-md" initialFocus={recordingRef} showCloseButton={false}>
<DialogHeader>
<DialogTitle>
{action ? t('settings.openchamber.keyboardShortcuts.dialog.title', { action: actionLabel(action) }) : ''}
</DialogTitle>
<DialogDescription>{t('settings.openchamber.keyboardShortcuts.dialog.instructions')}</DialogDescription>
</DialogHeader>
<div
className="flex min-h-28 items-center justify-center rounded-lg border border-border bg-[var(--surface-elevated)] px-4 py-5 text-center outline-none focus-visible:ring-2 focus-visible:ring-ring"
tabIndex={0}
ref={recordingRef}
onKeyDown={(event) => handleRecordingEvent(event, 'keydown')}
onKeyUp={(event) => handleRecordingEvent(event, 'keyup')}
onBlur={() => setRecording((current) => ({ ...current, livePreview: null }))}
>
<div className="flex flex-wrap items-center justify-center gap-2">
{recording.chords.map((chord, index) => (
<kbd key={`${chord}-${index}`} className="rounded-md border border-border bg-muted px-3 py-2 typography-ui-label font-mono text-foreground">
{formatShortcutForDisplay(chord)}
</kbd>
))}
{recording.livePreview ? (
<kbd className="rounded-md border border-dashed border-border bg-muted px-3 py-2 typography-ui-label font-mono text-muted-foreground">
{formatShortcutForDisplay(recording.livePreview)}
</kbd>
) : null}
{recording.chords.length === 0 && !recording.livePreview ? (
<span className="typography-ui-label text-muted-foreground">
{t('settings.openchamber.keyboardShortcuts.dialog.recording')}
</span>
) : null}
</div>
</div>
{recording.settled && protectedConflict ? (
<p className="typography-meta text-[var(--status-error)]">
{t('settings.openchamber.keyboardShortcuts.error.internalConflict')}
</p>
) : recording.settled && prefixConflict ? (
<p className="typography-meta text-[var(--status-error)]">
{t('settings.openchamber.keyboardShortcuts.error.prefixConflict', { action: actionLabel(prefixConflict.action) })}
</p>
) : null}
{recording.settled && exactConflict && !protectedConflict && !prefixConflict ? (
<p className="typography-meta text-[var(--status-warning)]">
{t('settings.openchamber.keyboardShortcuts.error.exactConflict', { action: actionLabel(exactConflict.action) })}
</p>
) : null}
{recording.settled && contextualPrefixConflict && !protectedConflict && !prefixConflict ? (
<p className="typography-meta text-[var(--status-warning)]">
{t('settings.openchamber.keyboardShortcuts.warning.contextualPrefix', {
action: conflictActionLabel(contextualPrefixConflict),
})}
</p>
) : null}
{recording.settled && combo && isRiskyBrowserShortcut(combo) ? (
<p className="typography-meta text-[var(--status-warning)]">
{t('settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut')}
</p>
) : null}
<DialogFooter>
<Button type="button" variant="ghost" size="sm" onClick={close}>
{t('settings.common.actions.cancel')}
</Button>
<Button
type="button"
size="sm"
disabled={!combo || (recording.settled && (Boolean(protectedConflict) || Boolean(prefixConflict)))}
onClick={confirm}
>
{t('settings.openchamber.keyboardShortcuts.actions.confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -24,6 +24,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { opencodeClient } from '@/lib/opencode/client'; import { opencodeClient } from '@/lib/opencode/client';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
import { import {
isFilesystemError, isFilesystemError,
type FilesystemErrorReason, type FilesystemErrorReason,
@@ -360,9 +361,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
const hasHighlightedBrowseItem = Boolean( const hasHighlightedBrowseItem = Boolean(
highlightedRow && (highlightedRow.type === 'up' || (highlightedRow.type === 'directory' && !highlightedRow.disabled)) highlightedRow && (highlightedRow.type === 'up' || (highlightedRow.type === 'directory' && !highlightedRow.disabled))
); );
const submitModifierLabel = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) const submitModifierLabel = formatShortcutForDisplay('mod');
? '⌘'
: 'Ctrl';
const submitActionLabel = isAlreadyAdded const submitActionLabel = isAlreadyAdded
? t('directoryExplorerDialog.actions.alreadyAdded') ? t('directoryExplorerDialog.actions.alreadyAdded')
: isCloneMode : isCloneMode
@@ -11,7 +11,11 @@ import { Icon } from '@/components/icon/Icon';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useGlobalSessionStatus } from '@/sync/sync-context'; import { useGlobalSessionStatus } from '@/sync/sync-context';
import { useSessionUnseenCount } from '@/sync/notification-store'; import { useSessionUnseenCount } from '@/sync/notification-store';
import { useSwitcherItems, type SwitcherItem } from '@/components/session/sidebar/shell/useSwitcherItems'; import {
findSwitcherItemAncestorIds,
useSwitcherItems,
type SwitcherItem,
} from '@/components/session/sidebar/shell/useSwitcherItems';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore'; import { resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
import { formatSessionCompactDateLabel } from './sidebar/utils'; import { formatSessionCompactDateLabel } from './sidebar/utils';
@@ -22,6 +26,7 @@ import { cn } from '@/lib/utils';
type SecondaryMeta = SwitcherItem['secondaryMeta']; type SecondaryMeta = SwitcherItem['secondaryMeta'];
type SwitcherVariant = 'default' | 'compact'; type SwitcherVariant = 'default' | 'compact';
const NEW_SESSION_SWITCHER_TARGET = 'new-session';
type SessionSwitcherDropdownProps = { type SessionSwitcherDropdownProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -40,7 +45,7 @@ export function SessionSwitcherDropdown({
const setOpen = useUIStore((state) => state.setSessionDropdownOpen); const setOpen = useUIStore((state) => state.setSessionDropdownOpen);
return ( return (
<DropdownMenu open={isOpen} onOpenChange={setOpen} modal={false}> <DropdownMenu open={isOpen} onOpenChange={setOpen} modal={false} disableGlobalShortcuts>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align={align} align={align}
@@ -69,7 +74,9 @@ type SwitcherContentProps = {
}; };
function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentProps): React.ReactElement { function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentProps): React.ReactElement {
const items = useSwitcherItems(true, { scopeProjectId }); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const isNewSessionDraftOpen = useSessionUIStore((state) => state.newSessionDraft.open === true);
const items = useSwitcherItems(true, { scopeProjectId, currentSessionId });
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
const { t } = useI18n(); const { t } = useI18n();
@@ -79,6 +86,9 @@ function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentP
}, [onSelect, openNewSessionDraft]); }, [onSelect, openNewSessionDraft]);
const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set()); const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set());
const contentRef = React.useRef<HTMLDivElement>(null);
const initialFocusCompleteRef = React.useRef(false);
const initialTarget = isNewSessionDraftOpen ? NEW_SESSION_SWITCHER_TARGET : currentSessionId;
const toggleParent = React.useCallback((sessionId: string) => { const toggleParent = React.useCallback((sessionId: string) => {
setExpandedParents((prev) => { setExpandedParents((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -91,10 +101,36 @@ function SwitcherContent({ onSelect, variant, scopeProjectId }: SwitcherContentP
}); });
}, []); }, []);
React.useLayoutEffect(() => {
if (initialFocusCompleteRef.current || !initialTarget) return;
const ancestorIds = initialTarget === NEW_SESSION_SWITCHER_TARGET
? []
: findSwitcherItemAncestorIds(items, initialTarget);
if (!ancestorIds) return;
if (ancestorIds.some((id) => !expandedParents.has(id))) {
setExpandedParents((previous) => new Set([...previous, ...ancestorIds]));
return;
}
const animationFrame = requestAnimationFrame(() => {
const item = Array.from(
contentRef.current?.querySelectorAll<HTMLElement>('[data-switcher-item-id]') ?? [],
).find((element) => element.dataset.switcherItemId === initialTarget);
if (!item) return;
item.focus();
item.scrollIntoView({ block: 'nearest' });
initialFocusCompleteRef.current = true;
});
return () => cancelAnimationFrame(animationFrame);
}, [expandedParents, initialTarget, items]);
return ( return (
<div className="max-h-[60vh] overflow-y-auto"> <div ref={contentRef} className="max-h-[60vh] overflow-y-auto">
<div className="space-y-0.5"> <div className="space-y-0.5">
<BaseMenu.Item <BaseMenu.Item
data-switcher-item-id={NEW_SESSION_SWITCHER_TARGET}
onClick={handleNewSession} onClick={handleNewSession}
className={cn( className={cn(
'group relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden select-none', 'group relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden select-none',
@@ -227,6 +263,7 @@ function SwitcherRow({ session, depth, variant, secondaryMeta, hasChildren, isEx
handleSelect(); handleSelect();
}} }}
data-slot="session-switcher-item" data-slot="session-switcher-item"
data-switcher-item-id={session.id}
className={cn( className={cn(
'group relative flex w-full cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 outline-hidden select-none', 'group relative flex w-full cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 outline-hidden select-none',
'data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover', 'data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover',
@@ -4,7 +4,7 @@ import type { Event } from '@opencode-ai/sdk/v2/client';
import React, { act } from 'react'; import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client'; import { createRoot, type Root } from 'react-dom/client';
import { deriveRecentSessions } from '../recent/activitySections'; import { deriveRecentSessions } from '../recent/activitySections';
import { applyGlobalSessionStatusEvent, useGlobalSessionStatusStore , replaceGlobalSessionStatusById} from '@/sync/global-session-status'; import { applyGlobalSessionStatusEvent, replaceGlobalSessionStatusById } from '@/sync/global-session-status';
import { import {
buildSidebarSessionProjection, buildSidebarSessionProjection,
getDescendantIds, getDescendantIds,
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
import React, { act } from 'react'; import React, { act } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import type { Session } from '@opencode-ai/sdk/v2'; import type { Session } from '@opencode-ai/sdk/v2';
import { useGlobalSessionStatusStore , replaceGlobalSessionStatusById} from '@/sync/global-session-status'; import { replaceGlobalSessionStatusById } from '@/sync/global-session-status';
import { useNotificationStore } from '@/sync/notification-store'; import { useNotificationStore } from '@/sync/notification-store';
import { useCollapsedSessionActivityState } from './collapsedActivityState'; import { useCollapsedSessionActivityState } from './collapsedActivityState';
import type { SessionNode } from '../types'; import type { SessionNode } from '../types';
@@ -26,6 +26,21 @@ export const useSessionSearchEffects = ({
return () => window.cancelAnimationFrame(raf); return () => window.cancelAnimationFrame(raf);
}, [enabled, isSessionSearchOpen, sessionSearchInputRef]); }, [enabled, isSessionSearchOpen, sessionSearchInputRef]);
// The open_session_list shortcut lands here when the sidebar is visible:
// the session list is already on screen, so the shortcut opens its search.
React.useEffect(() => {
if (!enabled || typeof window === 'undefined') {
return;
}
const handleOpenRequest = () => {
setIsSessionSearchOpen(true);
sessionSearchInputRef.current?.focus();
sessionSearchInputRef.current?.select();
};
window.addEventListener('openchamber:sidebar-session-search', handleOpenRequest);
return () => window.removeEventListener('openchamber:sidebar-session-search', handleOpenRequest);
}, [enabled, setIsSessionSearchOpen, sessionSearchInputRef]);
React.useEffect(() => { React.useEffect(() => {
if (!enabled || !isSessionSearchOpen || typeof document === 'undefined') { if (!enabled || !isSessionSearchOpen || typeof document === 'undefined') {
return; return;
@@ -0,0 +1,44 @@
import { describe, expect, test } from 'bun:test';
import type { Session } from '@opencode-ai/sdk/v2';
import {
findSwitcherItemAncestorIds,
selectSwitcherParents,
type SwitcherItem,
} from './useSwitcherItems';
const session = (id: string, options: { parentID?: string; archived?: boolean; projectId?: string } = {}): Session => ({
id,
parentID: options.parentID,
time: options.archived ? { archived: Date.now() } : undefined,
projectId: options.projectId ?? 'project-a',
} as unknown as Session);
const selectParents = (sessions: Session[], currentSessionId: string | null, scopeProjectId: string | null = null): Session[] => (
selectSwitcherParents(sessions, new Set(), new Map(), scopeProjectId, currentSessionId, (item) => (item as Session & { projectId: string }).projectId)
);
describe('session switcher initial selection', () => {
test('finds all local ancestors for a current child session', () => {
const items: SwitcherItem[] = [{
node: { session: session('root'), worktree: null, children: [{ session: session('parent', { parentID: 'root' }), worktree: null, children: [{ session: session('child', { parentID: 'parent' }), worktree: null, children: [] }] }] },
projectId: 'project-a', groupDirectory: null, secondaryMeta: null,
}];
expect(findSwitcherItemAncestorIds(items, 'child')).toEqual(['root', 'parent']);
expect(findSwitcherItemAncestorIds(items, 'missing')).toBeNull();
});
test('replaces the final recent slot with the current root and excludes invalid current sessions', () => {
const roots = Array.from({ length: 8 }, (_, index) => session(`root-${index}`));
const child = session('child', { parentID: 'root-7' });
expect(selectParents([...roots, child], 'child').map((item) => item.id)).toEqual([
'root-0', 'root-1', 'root-2', 'root-3', 'root-4', 'root-5', 'root-7',
]);
expect(selectParents([...roots, child], 'missing').map((item) => item.id)).toEqual(roots.slice(0, 7).map((item) => item.id));
expect(selectParents([...roots, child], 'child', 'project-b').map((item) => item.id)).toEqual([]);
expect(selectParents([...roots.slice(0, 7), session('archived', { archived: true })], 'archived').map((item) => item.id)).toEqual(roots.slice(0, 7).map((item) => item.id));
expect(selectParents([...roots, session('archived-child', { archived: true, parentID: 'root-7' })], 'archived-child').map((item) => item.id)).toEqual(roots.slice(0, 7).map((item) => item.id));
});
});
@@ -27,6 +27,7 @@ const MAX_PARENT_SESSIONS = 7;
type SwitcherItemsOptions = { type SwitcherItemsOptions = {
scopeProjectId?: string | null; scopeProjectId?: string | null;
currentSessionId?: string | null;
/** How many parent sessions to return (default 7 — the desktop dropdown). */ /** How many parent sessions to return (default 7 — the desktop dropdown). */
maxParents?: number; maxParents?: number;
}; };
@@ -46,8 +47,69 @@ const formatProjectLabel = (project: { label?: string | null; path: string } | n
return segments[segments.length - 1] ?? null; return segments[segments.length - 1] ?? null;
}; };
export const findSwitcherItemAncestorIds = (items: SwitcherItem[], sessionId: string): string[] | null => {
const visit = (node: SessionNode, ancestors: string[]): string[] | null => {
if (node.session.id === sessionId) return ancestors;
for (const child of node.children) {
const result = visit(child, [...ancestors, node.session.id]);
if (result) return result;
}
return null;
};
for (const item of items) {
const result = visit(item.node, []);
if (result) return result;
}
return null;
};
export const selectSwitcherParents = (
activeSessions: Session[],
pinnedSessionIds: Set<string>,
sessionOrderRanks: Map<string, number>,
scopeProjectId: string | null,
currentSessionId: string | null,
getProjectId: (session: Session) => string | null,
maxParents = MAX_PARENT_SESSIONS,
isExcluded?: (session: Session) => boolean,
): Session[] => {
const sessionsById = new Map(activeSessions.map((session) => [session.id, session]));
const isEligibleParent = (session: Session): boolean => {
if (session.time?.archived) return false;
if (isExcluded?.(session)) return false;
// SAFETY: the SDK Session type omits parentID, but the server includes it on child sessions.
if ((session as Session & { parentID?: string | null }).parentID) return false;
return !scopeProjectId || getProjectId(session) === scopeProjectId;
};
const parents = activeSessions
.filter(isEligibleParent)
.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks));
const currentSession = currentSessionId ? sessionsById.get(currentSessionId) ?? null : null;
let currentRoot: Session | null = currentSession?.time?.archived ? null : currentSession;
const visited = new Set<string>();
while (currentRoot) {
// SAFETY: the SDK Session type omits parentID, but the server includes it on child sessions.
const parentId = (currentRoot as Session & { parentID?: string | null }).parentID;
if (!parentId) break;
if (visited.has(parentId)) {
currentRoot = null;
break;
}
visited.add(parentId);
currentRoot = sessionsById.get(parentId) ?? null;
}
const currentRootIndex = currentRoot && isEligibleParent(currentRoot) ? parents.indexOf(currentRoot) : -1;
if (currentRootIndex >= maxParents) {
return [...parents.slice(0, Math.max(0, maxParents - 1)), currentRoot!];
}
return parents.slice(0, maxParents);
};
export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions = {}): SwitcherItem[] => { export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions = {}): SwitcherItem[] => {
const { scopeProjectId = null, maxParents = MAX_PARENT_SESSIONS } = options; const { scopeProjectId = null, currentSessionId = null, maxParents = MAX_PARENT_SESSIONS } = options;
const activeSessions = useGlobalSessionsStore((state) => state.activeSessions); const activeSessions = useGlobalSessionsStore((state) => state.activeSessions);
const projects = useProjectsStore((state) => state.projects); const projects = useProjectsStore((state) => state.projects);
const pinnedSessionIds = useSessionPinnedStore((state) => state.ids); const pinnedSessionIds = useSessionPinnedStore((state) => state.ids);
@@ -116,19 +178,17 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
list.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks)); list.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks));
}); });
const parents = activeSessions const parents = selectSwitcherParents(
.filter((session) => !session.time?.archived) activeSessions,
pinnedSessionIds,
sessionOrderRanks,
scopeProjectId,
currentSessionId,
(session) => findProjectForDirectory(resolveGlobalSessionDirectory(session))?.id ?? null,
maxParents,
// btw forks stay hidden until promoted to a full session // btw forks stay hidden until promoted to a full session
.filter((session) => !isBtwSession(session)) (session) => isBtwSession(session) || (isVSCode && isChatDirectoryPath(resolveGlobalSessionDirectory(session))),
.filter((session) => !isVSCode || !isChatDirectoryPath(resolveGlobalSessionDirectory(session))) );
.filter((session) => !(session as Session & { parentID?: string | null }).parentID)
.filter((session) => {
if (!scopeProjectId) return true;
const directory = resolveGlobalSessionDirectory(session);
return findProjectForDirectory(directory)?.id === scopeProjectId;
})
.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks))
.slice(0, maxParents);
const buildNode = (session: Session): SessionNode => { const buildNode = (session: Session): SessionNode => {
const childSessions = childrenByParent.get(session.id) ?? []; const childSessions = childrenByParent.get(session.id) ?? [];
@@ -158,7 +218,7 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
}, },
}; };
}); });
}, [activeSessions, branchesByDirectory, enabled, findProjectForDirectory, isVSCode, maxParents, pinnedSessionIds, scopeProjectId, sessionOrderRanks, worktreeInfoByPath]); }, [activeSessions, branchesByDirectory, currentSessionId, enabled, findProjectForDirectory, isVSCode, maxParents, pinnedSessionIds, scopeProjectId, sessionOrderRanks, worktreeInfoByPath]);
return items; return items;
}; };
@@ -37,7 +37,8 @@ import { toast } from '@/components/ui';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import type { Session } from '@opencode-ai/sdk/v2'; import type { Session } from '@opencode-ai/sdk/v2';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { formatShortcutForDisplay, getEffectiveShortcutCombo, shortcutRegistry } from '@/lib/shortcuts';
import { showOpenCodeStatus } from '@/lib/openCodeStatus';
import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata'; import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata';
@@ -49,6 +50,7 @@ import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
import { truncatePathMiddle } from '@/lib/utils'; import { truncatePathMiddle } from '@/lib/utils';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
import { copyTextToClipboard } from '@/lib/clipboard';
import { useProjectsStore } from '@/stores/useProjectsStore'; import { useProjectsStore } from '@/stores/useProjectsStore';
import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState'; import { buildCommandPaletteFileSearchKey, scoreCommandPaletteFiles } from './commandPaletteFilesState';
@@ -58,6 +60,9 @@ type CommandEntry = {
icon: React.ReactNode; icon: React.ReactNode;
shortcutId?: string; shortcutId?: string;
searchText: string; searchText: string;
/** Search-only command: reachable by typing, hidden from the initial list
so the first screen stays scroll-free. */
secondary?: boolean;
onSelect: () => void; onSelect: () => void;
}; };
@@ -89,9 +94,14 @@ export const CommandPalette: React.FC = () => {
const openContextSurface = useUIStore((s) => s.openContextSurface); const openContextSurface = useUIStore((s) => s.openContextSurface);
const openContextFile = useUIStore((s) => s.openContextFile); const openContextFile = useUIStore((s) => s.openContextFile);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides); const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
const openMultiRunLauncher = useUIStore((s) => s.openMultiRunLauncher);
const setArchivePageOpen = useUIStore((s) => s.setArchivePageOpen);
const setProjectContextTab = useUIStore((s) => s.setProjectContextTab);
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const togglePinnedSession = useSessionPinnedStore((s) => s.toggle);
const activeSessions = useGlobalSessionsStore(React.useCallback( const activeSessions = useGlobalSessionsStore(React.useCallback(
(state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS, (state) => isCommandPaletteOpen ? state.activeSessions : EMPTY_SESSIONS,
@@ -230,6 +240,27 @@ export const CommandPalette: React.FC = () => {
if (currentDirectory) openContextOverview(currentDirectory); if (currentDirectory) openContextOverview(currentDirectory);
}), }),
}, },
{
id: 'cycle-theme',
secondary: true,
title: t('commandPalette.item.cycleTheme'),
icon: <Icon name="palette" className="mr-2 h-4 w-4" />,
shortcutId: 'cycle_theme',
searchText: t('commandPalette.item.cycleTheme'),
onSelect: run(() => {
shortcutRegistry.invoke('cycle_theme');
}),
},
{
id: 'open-status',
secondary: true,
title: t('commandPalette.item.showOpenCodeStatus'),
icon: <Icon name="pulse" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.showOpenCodeStatus'),
onSelect: run(() => {
void showOpenCodeStatus();
}),
},
{ {
id: 'open-settings', id: 'open-settings',
title: t('commandPalette.item.openSettings'), title: t('commandPalette.item.openSettings'),
@@ -239,6 +270,97 @@ export const CommandPalette: React.FC = () => {
onSelect: run(() => setSettingsDialogOpen(true)), onSelect: run(() => setSettingsDialogOpen(true)),
}, },
]; ];
list.push(
{
id: 'pin-session',
secondary: true,
title: t('commandPalette.item.pinSession'),
icon: <Icon name="pushpin" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.pinSession'),
onSelect: run(() => {
if (currentSessionId && currentDirectory) {
togglePinnedSession({ directory: currentDirectory, sessionId: currentSessionId });
}
}),
},
{
id: 'copy-session-id',
secondary: true,
title: t('commandPalette.item.copySessionId'),
icon: <Icon name="file-copy" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.copySessionId'),
onSelect: run(() => {
if (!currentSessionId) return;
void copyTextToClipboard(currentSessionId)
.then((result) => {
if (result.ok) {
toast.success(t('sessions.sidebar.session.copyId.success'));
return;
}
toast.error(t('sessions.sidebar.session.copyId.error'));
})
.catch(() => toast.error(t('sessions.sidebar.session.copyId.error')));
}),
},
{
id: 'open-multi-run',
secondary: true,
title: t('commandPalette.item.openMultiRun'),
icon: <Icon name="checkbox-multiple" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openMultiRun'),
onSelect: run(() => {
setSessionSwitcherOpen(false);
openMultiRunLauncher();
}),
},
{
id: 'open-archive',
secondary: true,
title: t('commandPalette.item.openArchive'),
icon: <Icon name="archive" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openArchive'),
onSelect: run(() => {
setSessionSwitcherOpen(false);
setArchivePageOpen(true);
}),
},
{
id: 'open-notes',
secondary: true,
title: t('commandPalette.item.openNotes'),
icon: <Icon name="sticky-note" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openNotes'),
onSelect: run(() => {
if (currentDirectory) {
setProjectContextTab('notes');
openContextSurface(currentDirectory, 'notes');
}
}),
},
{
id: 'open-todos',
secondary: true,
title: t('commandPalette.item.openTodos'),
icon: <Icon name="checkbox-circle" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.openTodos'),
onSelect: run(() => {
if (currentDirectory) {
setProjectContextTab('todos');
openContextSurface(currentDirectory, 'notes');
}
}),
},
);
list.push({
id: 'toggle-memory-debug',
secondary: true,
title: t('commandPalette.item.toggleMemoryDebug'),
icon: <Icon name="bug" className="mr-2 h-4 w-4" />,
searchText: t('commandPalette.item.toggleMemoryDebug'),
onSelect: run(() => {
window.dispatchEvent(new CustomEvent('openchamber:memory-debug-toggle'));
}),
});
if (canUseElectronDesktopIPC()) { if (canUseElectronDesktopIPC()) {
list.splice(1, 0, { list.splice(1, 0, {
id: 'new-mini-chat', id: 'new-mini-chat',
@@ -270,6 +392,11 @@ export const CommandPalette: React.FC = () => {
setSettingsDialogOpen, setSettingsDialogOpen,
activeProject?.id, activeProject?.id,
activeProject?.path, activeProject?.path,
currentSessionId,
togglePinnedSession,
openMultiRunLauncher,
setArchivePageOpen,
setProjectContextTab,
]); ]);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -378,7 +505,9 @@ export const CommandPalette: React.FC = () => {
const hasQuery = liveTrimmed.length > 0; const hasQuery = liveTrimmed.length > 0;
const scoredCommands = React.useMemo(() => { const scoredCommands = React.useMemo(() => {
if (!hasQuery) return commands.map((item) => ({ item, score: 0 })); if (!hasQuery) {
return commands.filter((item) => !item.secondary).map((item) => ({ item, score: 0 }));
}
return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, { return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, {
limit: 7, limit: 7,
noFuzzy: true, noFuzzy: true,
+84 -73
View File
@@ -10,18 +10,19 @@ import { Icon } from "@/components/icon/Icon";
import { useUIStore } from "@/stores/useUIStore"; import { useUIStore } from "@/stores/useUIStore";
import { import {
getEffectiveShortcutCombo, getEffectiveShortcutCombo,
getEffectiveShortcutPrefix,
getShortcutAction, getShortcutAction,
getModifierLabel,
formatShortcutForDisplay, formatShortcutForDisplay,
type ShortcutActionId,
} from "@/lib/shortcuts"; } from "@/lib/shortcuts";
import { useI18n, type I18nKey } from "@/lib/i18n"; import { useI18n, type I18nKey } from "@/lib/i18n";
import { isVSCodeRuntime } from "@/lib/desktop"; import { isVSCodeRuntime } from "@/lib/desktop";
import type { IconName } from "@/components/icon/icons"; import type { IconName } from "@/components/icon/icons";
type ShortcutItem = { type ShortcutItem = {
id?: string; id?: ShortcutActionId;
keys: string | string[]; keys: string | string[];
descriptionKey: I18nKey; descriptionKey?: I18nKey;
icon: IconName | null; icon: IconName | null;
}; };
@@ -30,9 +31,12 @@ type ShortcutSection = {
items: ShortcutItem[]; items: ShortcutItem[];
}; };
const renderShortcut = (id: string, fallbackCombo: string, overrides: Record<string, string>) => { const renderShortcut = (
const action = getShortcutAction(id); id: ShortcutActionId,
return action ? formatShortcutForDisplay(getEffectiveShortcutCombo(id, overrides)) : fallbackCombo; overrides: Record<string, string>,
unassignedLabel: string,
) => {
return formatShortcutForDisplay(getEffectiveShortcutCombo(id, overrides), unassignedLabel);
}; };
export const HelpDialog: React.FC = () => { export const HelpDialog: React.FC = () => {
@@ -40,7 +44,6 @@ export const HelpDialog: React.FC = () => {
const isHelpDialogOpen = useUIStore((state) => state.isHelpDialogOpen); const isHelpDialogOpen = useUIStore((state) => state.isHelpDialogOpen);
const setHelpDialogOpen = useUIStore((state) => state.setHelpDialogOpen); const setHelpDialogOpen = useUIStore((state) => state.setHelpDialogOpen);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides); const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const mod = getModifierLabel();
const isVSCode = isVSCodeRuntime(); const isVSCode = isVSCodeRuntime();
const shortcuts: ShortcutSection[] = [ const shortcuts: ShortcutSection[] = [
@@ -100,7 +103,7 @@ export const HelpDialog: React.FC = () => {
keys: '', keys: '',
}, },
{ {
keys: [`Shift + Alt + ${mod} + N`], keys: [formatShortcutForDisplay('mod+shift+alt+n')],
descriptionKey: "helpDialog.item.newWindow", descriptionKey: "helpDialog.item.newWindow",
icon: "window", icon: "window",
}, },
@@ -121,6 +124,21 @@ export const HelpDialog: React.FC = () => {
icon: "git-branch", icon: "git-branch",
keys: '', keys: '',
}, },
{
id: 'open_draft_project_picker',
icon: 'folder',
keys: '',
},
{
id: 'open_draft_worktree_picker',
icon: 'git-branch',
keys: '',
},
{
id: 'open_session_list',
icon: 'list-unordered',
keys: '',
},
{ id: 'focus_input', descriptionKey: "helpDialog.item.focusChatInput", icon: "text", keys: '' }, { id: 'focus_input', descriptionKey: "helpDialog.item.focusChatInput", icon: "text", keys: '' },
{ {
id: 'toggle_prompt_navigator', id: 'toggle_prompt_navigator',
@@ -139,24 +157,6 @@ export const HelpDialog: React.FC = () => {
{ {
categoryKey: "helpDialog.section.panels", categoryKey: "helpDialog.section.panels",
items: [ items: [
{
id: 'toggle_right_sidebar',
descriptionKey: 'helpDialog.item.toggleRightSidebar',
icon: "layout-right",
keys: '',
},
{
id: 'open_right_sidebar_git',
descriptionKey: 'helpDialog.item.openRightSidebarGitTab',
icon: "git-branch",
keys: '',
},
{
id: 'open_right_sidebar_files',
descriptionKey: 'helpDialog.item.openRightSidebarFilesTab',
icon: "layout-right",
keys: '',
},
{ {
id: 'toggle_terminal', id: 'toggle_terminal',
descriptionKey: 'helpDialog.item.toggleTerminalDock', descriptionKey: 'helpDialog.item.toggleTerminalDock',
@@ -170,14 +170,13 @@ export const HelpDialog: React.FC = () => {
keys: '', keys: '',
}, },
{ {
id: 'toggle_context_plan', keys: [`${formatShortcutForDisplay(getEffectiveShortcutPrefix('switch_context_surface', shortcutOverrides))} + 1...0`],
descriptionKey: 'helpDialog.item.togglePlanContextPanel', descriptionKey: "helpDialog.item.switchContextSurface",
icon: "time", icon: "layout-right",
keys: '',
}, },
{ {
keys: [`${mod} + 1...0`], keys: [`${formatShortcutForDisplay(getEffectiveShortcutPrefix('switch_session_tab', shortcutOverrides))} + 1...9`],
descriptionKey: "helpDialog.item.switchContextSurface", descriptionKey: "helpDialog.item.switchSessionTab",
icon: "layout-right", icon: "layout-right",
}, },
], ],
@@ -197,12 +196,6 @@ export const HelpDialog: React.FC = () => {
icon: "stack", icon: "stack",
keys: '', keys: '',
}, },
{
id: 'cycle_services_tab',
descriptionKey: 'helpDialog.item.cycleServicesTab',
icon: "stack",
keys: '',
},
{ {
id: 'open_settings', id: 'open_settings',
descriptionKey: "helpDialog.item.openSettings", descriptionKey: "helpDialog.item.openSettings",
@@ -214,7 +207,7 @@ export const HelpDialog: React.FC = () => {
]; ];
return ( return (
<Dialog open={isHelpDialogOpen} onOpenChange={setHelpDialogOpen}> <Dialog open={isHelpDialogOpen} onOpenChange={setHelpDialogOpen}>
<DialogContent className="max-w-2xl w-[min(42rem,calc(100vw-1.5rem))] max-h-[calc(100dvh-2rem)] flex flex-col overflow-hidden"> <DialogContent className="max-w-2xl w-[min(42rem,calc(100vw-1.5rem))] max-h-[calc(100dvh-2rem)] flex flex-col overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@@ -237,40 +230,54 @@ export const HelpDialog: React.FC = () => {
{section.items {section.items
.filter((shortcut) => !(isVSCode && shortcut.id === 'toggle_prompt_navigator')) .filter((shortcut) => !(isVSCode && shortcut.id === 'toggle_prompt_navigator'))
.map((shortcut) => { .map((shortcut) => {
const displayKeys = shortcut.id const action = shortcut.id ? getShortcutAction(shortcut.id) : undefined;
? renderShortcut(shortcut.id, Array.isArray(shortcut.keys) ? shortcut.keys[0] : shortcut.keys, shortcutOverrides) const descriptionKey = shortcut.descriptionKey
: (Array.isArray(shortcut.keys) ? shortcut.keys : shortcut.keys.split(" / ")); ?? (action?.customizable ? action.settingsLabelKey : undefined);
if (!descriptionKey) return null;
// This dialog lists what the keyboard can do right now;
// an action without a binding belongs to the command
// palette and Settings, not here.
if (shortcut.id && !getEffectiveShortcutCombo(shortcut.id, shortcutOverrides)) {
return null;
}
const displayKeys = shortcut.id
? renderShortcut(
shortcut.id,
shortcutOverrides,
t('settings.openchamber.keyboardShortcuts.unassigned'),
)
: (Array.isArray(shortcut.keys) ? shortcut.keys : shortcut.keys.split(" / "));
return ( return (
<div <div
key={shortcut.id || shortcut.descriptionKey} key={shortcut.id || descriptionKey}
className="flex items-center justify-between py-1 px-2" className="flex items-center justify-between py-1 px-2"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{shortcut.icon && ( {shortcut.icon && (
<Icon name={shortcut.icon} className="h-3.5 w-3.5 text-muted-foreground" /> <Icon name={shortcut.icon} className="h-3.5 w-3.5 text-muted-foreground" />
)} )}
<span className="typography-meta"> <span className="typography-meta">
{t(shortcut.descriptionKey)} {t(descriptionKey)}
</span> </span>
</div>
<div className="flex items-center gap-1">
{(Array.isArray(displayKeys) ? displayKeys : [displayKeys]).map((keyCombo: string, i: number) => (
<React.Fragment key={`${keyCombo}-${i}`}>
{i > 0 && (
<span className="typography-meta text-muted-foreground mx-1">
{t('helpDialog.keyCombiner.or')}
</span>
)}
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
{keyCombo}
</kbd>
</React.Fragment>
))}
</div>
</div> </div>
<div className="flex items-center gap-1"> );
{(Array.isArray(displayKeys) ? displayKeys : [displayKeys]).map((keyCombo: string, i: number) => ( })}
<React.Fragment key={`${keyCombo}-${i}`}>
{i > 0 && (
<span className="typography-meta text-muted-foreground mx-1">
{t('helpDialog.keyCombiner.or')}
</span>
)}
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
{keyCombo}
</kbd>
</React.Fragment>
))}
</div>
</div>
);
})}
</div> </div>
</div> </div>
))} ))}
@@ -284,14 +291,18 @@ export const HelpDialog: React.FC = () => {
<ul className="space-y-0.5 typography-meta"> <ul className="space-y-0.5 typography-meta">
<li> <li>
{t('helpDialog.proTips.commandPalette', { {t('helpDialog.proTips.commandPalette', {
shortcut: renderShortcut('open_command_palette', `${mod} P`, shortcutOverrides), shortcut: renderShortcut(
'open_command_palette',
shortcutOverrides,
t('settings.openchamber.keyboardShortcuts.unassigned'),
),
})} })}
</li> </li>
<li> <li>
{t('helpDialog.proTips.recentSessions')} {t('helpDialog.proTips.recentSessions')}
</li> </li>
<li> <li>
{t('helpDialog.proTips.themeCycling')} {t('helpDialog.proTips.leaderSequences')}
</li> </li>
</ul> </ul>
</div> </div>
@@ -3,6 +3,8 @@ import { Menu as BaseMenu } from "@base-ui/react/menu"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { shortcutRegistry } from "@/lib/shortcuts";
import { handleDropdownNavigationKey } from "./dropdown-navigation";
import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from "./dropdown-menu.styles"; import { dropdownMenuItemClass, dropdownMenuPopupClass, dropdownMenuSeparatorClass, dropdownMenuSubTriggerClass } from "./dropdown-menu.styles";
type AsChildProps = { asChild?: boolean }; type AsChildProps = { asChild?: boolean };
@@ -34,11 +36,21 @@ function renderFromAsChild(asChild: boolean | undefined, children: React.ReactNo
return { children }; return { children };
} }
type DropdownMenuProps = React.ComponentProps<typeof BaseMenu.Root> & {
disableGlobalShortcuts?: boolean;
};
function DropdownMenu({ function DropdownMenu({
disableGlobalShortcuts = false,
open,
defaultOpen,
onOpenChange,
...props ...props
}: React.ComponentProps<typeof BaseMenu.Root>) { }: DropdownMenuProps) {
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null); const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null); const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null);
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);
const isOpen = open ?? uncontrolledOpen;
const portalContextValue = React.useMemo<DropdownPortalContextValue>(() => ({ const portalContextValue = React.useMemo<DropdownPortalContextValue>(() => ({
portalContainer, portalContainer,
collisionBoundary, collisionBoundary,
@@ -46,9 +58,24 @@ function DropdownMenu({
setCollisionBoundary, setCollisionBoundary,
}), [collisionBoundary, portalContainer]); }), [collisionBoundary, portalContainer]);
React.useLayoutEffect(() => {
if (!disableGlobalShortcuts || !isOpen) return;
return shortcutRegistry.suspend();
}, [disableGlobalShortcuts, isOpen]);
const handleOpenChange: NonNullable<React.ComponentProps<typeof BaseMenu.Root>['onOpenChange']> = (nextOpen, eventDetails) => {
if (open === undefined) setUncontrolledOpen(nextOpen);
onOpenChange?.(nextOpen, eventDetails);
};
return ( return (
<DropdownPortalContext.Provider value={portalContextValue}> <DropdownPortalContext.Provider value={portalContextValue}>
<BaseMenu.Root {...props} /> <BaseMenu.Root
{...props}
defaultOpen={defaultOpen}
open={open}
onOpenChange={handleOpenChange}
/>
</DropdownPortalContext.Provider> </DropdownPortalContext.Provider>
) )
} }
@@ -116,11 +143,23 @@ function DropdownMenuContent({
style, style,
children, children,
onCloseAutoFocus, onCloseAutoFocus,
onKeyDown,
...props ...props
}: ContentProps) { }: ContentProps) {
const portalContext = React.useContext(DropdownPortalContext); const portalContext = React.useContext(DropdownPortalContext);
void onCloseAutoFocus void onCloseAutoFocus
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseMenu.Popup>['onKeyDown']> = (event) => {
onKeyDown?.(event);
handleDropdownNavigationKey(event, (navigationKey) => {
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
key: navigationKey,
bubbles: true,
cancelable: true,
}));
});
};
return ( return (
<BaseMenu.Portal container={portalToBody ? undefined : portalContext?.portalContainer || undefined}> <BaseMenu.Portal container={portalToBody ? undefined : portalContext?.portalContainer || undefined}>
<BaseMenu.Positioner <BaseMenu.Positioner
@@ -143,6 +182,7 @@ function DropdownMenuContent({
className className
)} )}
{...props} {...props}
onKeyDown={handleKeyDown}
> >
{children} {children}
</BaseMenu.Popup> </BaseMenu.Popup>
@@ -0,0 +1,47 @@
import type React from 'react';
import { isIMECompositionEvent } from '@/lib/ime';
function getDropdownNavigationKey(event: Pick<KeyboardEvent, 'key' | 'code' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): 'ArrowDown' | 'ArrowUp' | null {
if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return null;
// `code` covers non-Latin layouts, where `key` is the layout's own letter.
if (event.key.toLowerCase() === 'n' || event.code === 'KeyN') return 'ArrowDown';
if (event.key.toLowerCase() === 'p' || event.code === 'KeyP') return 'ArrowUp';
return null;
}
type DropdownNavigationEvent = Pick<
React.KeyboardEvent<HTMLElement>,
| 'altKey'
| 'code'
| 'ctrlKey'
| 'defaultPrevented'
| 'isPropagationStopped'
| 'key'
| 'metaKey'
| 'preventDefault'
| 'shiftKey'
| 'stopPropagation'
>;
export function handleDropdownNavigationKey(
event: DropdownNavigationEvent,
navigate: (key: 'ArrowDown' | 'ArrowUp') => void,
): boolean {
if (event.defaultPrevented || event.isPropagationStopped()) return false;
const navigationKey = getDropdownNavigationKey(event);
if (!navigationKey) return false;
// Do not add an IME guard: exact Ctrl+N/P remain intentional commands, while
// every other composing key falls through without being handled.
navigate(navigationKey);
event.preventDefault();
event.stopPropagation();
return true;
}
export function shouldDismissDropdown(
event: KeyboardEvent | React.KeyboardEvent,
): boolean {
return event.key === 'Escape' && !isIMECompositionEvent(event);
}
+46 -3
View File
@@ -8,6 +8,8 @@ import { cn } from "@/lib/utils"
import { dropdownTriggerVariants } from "@/components/ui/dropdown-trigger" import { dropdownTriggerVariants } from "@/components/ui/dropdown-trigger"
import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay"; import { ScrollableOverlay } from "@/components/ui/ScrollableOverlay";
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { shortcutRegistry } from "@/lib/shortcuts";
import { handleDropdownNavigationKey } from "./dropdown-navigation";
type AsChildProps = { asChild?: boolean }; type AsChildProps = { asChild?: boolean };
type AsChildRenderProps = { type AsChildRenderProps = {
@@ -38,15 +40,22 @@ type SelectRootProps<Value extends string = string> = Omit<
value?: Value; value?: Value;
defaultValue?: Value; defaultValue?: Value;
onValueChange?: (value: Value, eventDetails: SelectRootChangeEventDetails) => void; onValueChange?: (value: Value, eventDetails: SelectRootChangeEventDetails) => void;
disableGlobalShortcuts?: boolean;
}; };
function Select<Value extends string = string>({ function Select<Value extends string = string>({
onValueChange, onValueChange,
modal = false, modal = false,
disableGlobalShortcuts = false,
open,
defaultOpen,
onOpenChange,
...props ...props
}: SelectRootProps<Value>) { }: SelectRootProps<Value>) {
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null); const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null); const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null);
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);
const isOpen = open ?? uncontrolledOpen;
const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({ const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({
portalContainer, portalContainer,
collisionBoundary, collisionBoundary,
@@ -63,9 +72,26 @@ function Select<Value extends string = string>({
[onValueChange] [onValueChange]
); );
React.useLayoutEffect(() => {
if (!disableGlobalShortcuts || !isOpen) return;
return shortcutRegistry.suspend();
}, [disableGlobalShortcuts, isOpen]);
const handleOpenChange: NonNullable<React.ComponentProps<typeof BaseSelect.Root>['onOpenChange']> = (nextOpen, eventDetails) => {
if (open === undefined) setUncontrolledOpen(nextOpen);
onOpenChange?.(nextOpen, eventDetails);
};
return ( return (
<SelectPortalContext.Provider value={portalContextValue}> <SelectPortalContext.Provider value={portalContextValue}>
<BaseSelect.Root {...props} modal={modal} onValueChange={handleValueChange} /> <BaseSelect.Root
{...props}
modal={modal}
open={open}
defaultOpen={defaultOpen}
onOpenChange={handleOpenChange}
onValueChange={handleValueChange}
/>
</SelectPortalContext.Provider> </SelectPortalContext.Provider>
) )
} }
@@ -184,12 +210,24 @@ function SelectContent({
align, align,
collisionAvoidance, collisionAvoidance,
constrainToMain = false, constrainToMain = false,
onKeyDown,
...props ...props
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) { }: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
const portalContext = React.useContext(SelectPortalContext); const portalContext = React.useContext(SelectPortalContext);
const alignItemWithTrigger = position === "item-aligned"; const alignItemWithTrigger = position === "item-aligned";
const portalContainer = portalContext?.portalContainer ?? null; const portalContainer = portalContext?.portalContainer ?? null;
const handleKeyDown: NonNullable<React.ComponentProps<typeof BaseSelect.Popup>['onKeyDown']> = (event) => {
onKeyDown?.(event);
handleDropdownNavigationKey(event, (navigationKey) => {
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
key: navigationKey,
bubbles: true,
cancelable: true,
}));
});
};
return ( return (
<BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}> <BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}>
<BaseSelect.Positioner <BaseSelect.Positioner
@@ -214,6 +252,7 @@ function SelectContent({
className className
)} )}
{...props} {...props}
onKeyDown={handleKeyDown}
> >
<ScrollableOverlay <ScrollableOverlay
outerClassName={cn( outerClassName={cn(
@@ -253,13 +292,17 @@ function SelectLabel({
function SelectItem({ function SelectItem({
className, className,
children, children,
showSelectedBackground = true,
...props ...props
}: React.ComponentProps<typeof BaseSelect.Item>) { }: React.ComponentProps<typeof BaseSelect.Item> & {
showSelectedBackground?: boolean;
}) {
return ( return (
<BaseSelect.Item <BaseSelect.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[selected]:bg-interactive-selection data-[selected]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
showSelectedBackground && "data-[selected]:bg-interactive-selection data-[selected]:text-interactive-selection-foreground",
className className
)} )}
{...props} {...props}
@@ -1768,6 +1768,37 @@ export const DiffView: React.FC<DiffViewProps> = ({
scrollToFile(value); scrollToFile(value);
}, [cancelPendingScrollAlignment, expandStackedFile, scrollToFile]); }, [cancelPendingScrollAlignment, expandStackedFile, scrollToFile]);
// Step review to the adjacent changed file (alt+arrow): selects, expands
// a collapsed section, and scrolls to it. Window-level because the diff
// surface has no persistent focus target; guarded off editable fields.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!event.altKey || event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
const target = event.target;
if (target instanceof HTMLElement && (
target.isContentEditable
|| target.tagName === 'INPUT'
|| target.tagName === 'TEXTAREA'
|| target.closest('[role="dialog"]')
)) {
return;
}
if (changedFiles.length === 0) return;
const delta = event.key === 'ArrowDown' ? 1 : -1;
const index = displayFile ? changedFiles.findIndex((file) => file.path === displayFile) : -1;
const nextIndex = index === -1
? (delta > 0 ? 0 : changedFiles.length - 1)
: index + delta;
const next = changedFiles[nextIndex];
if (!next) return;
event.preventDefault();
handleSelectFileAndScroll(next.path);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [changedFiles, displayFile, handleSelectFileAndScroll]);
const handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => { const handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => {
const nextLayout: 'inline' | 'side-by-side' = const nextLayout: 'inline' | 'side-by-side' =
mode === 'side-by-side' ? 'side-by-side' : 'inline'; mode === 'side-by-side' ? 'side-by-side' : 'inline';
+37 -64
View File
@@ -45,7 +45,7 @@ import {
import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { useFileSearchStore } from '@/stores/useFileSearchStore';
import { useDeviceInfo } from '@/lib/device'; import { useDeviceInfo } from '@/lib/device';
import { cn, getModifierLabel, getRevealLabelKey, hasModifier } from '@/lib/utils'; import { cn, getRevealLabelKey } from '@/lib/utils';
import { getLanguageFromExtension, getImageMimeType, isBinaryFile, isDrawioFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers'; import { getLanguageFromExtension, getImageMimeType, isBinaryFile, isDrawioFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/fileEditorAutosave'; import { shouldAllowFileDraftSave, shouldScheduleFileAutosave } from '@/lib/fileEditorAutosave';
import { getRuntimeUrlResolver } from '@/lib/runtime-url'; import { getRuntimeUrlResolver } from '@/lib/runtime-url';
@@ -75,7 +75,8 @@ import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes'; import { getDefaultTheme } from '@/lib/theme/themes';
import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop'; import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop';
import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore'; import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore';
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { useKeybind, useKeybinds } from '@/hooks/useKeybind';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents'; import { sessionEvents } from '@/lib/sessionEvents';
import { syncScheduledTaskLoops } from '@/lib/scheduledTasksApi'; import { syncScheduledTaskLoops } from '@/lib/scheduledTasksApi';
@@ -1032,7 +1033,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation); const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath); const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath); const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap); const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview); const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons); const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
@@ -1759,35 +1759,28 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
setAutoSaveStatus('idle'); setAutoSaveStatus('idle');
}, [selectedFile?.path]); }, [selectedFile?.path]);
React.useEffect(() => { useKeybinds({
const handleKeyDown = (e: KeyboardEvent) => { save_file: (event) => {
if (!hasModifier(e)) { if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
return;
}
if (e.key.toLowerCase() === 's') { // Cancel pending auto-save because the explicit save should run immediately.
e.preventDefault(); if (autoSaveTimerRef.current) {
// Cancel pending auto-save; user wants immediate save clearTimeout(autoSaveTimerRef.current);
if (autoSaveTimerRef.current) { autoSaveTimerRef.current = null;
clearTimeout(autoSaveTimerRef.current);
autoSaveTimerRef.current = null;
}
if (!isSaving) {
void saveDraft().then((saved) => {
if (!saved) return;
setAutoSaveStatus('saved');
setTimeout(() => setAutoSaveStatus('idle'), 2000);
});
}
} else if (e.key.toLowerCase() === 'f') {
e.preventDefault();
setIsSearchOpen(true);
} }
}; if (!isSaving) {
void saveDraft().then((saved) => {
window.addEventListener('keydown', handleKeyDown); if (!saved) return;
return () => window.removeEventListener('keydown', handleKeyDown); setAutoSaveStatus('saved');
}, [isSaving, saveDraft]); setTimeout(() => setAutoSaveStatus('idle'), 2000);
});
}
},
find_in_file: (event) => {
if (!(event.target instanceof Node) || !editorWrapperRef.current?.contains(event.target)) return false;
setIsSearchOpen(true);
},
});
const loadSelectedFile = React.useCallback(async (node: FileNode) => { const loadSelectedFile = React.useCallback(async (node: FileNode) => {
const loadId = activeFileLoadIdRef.current + 1; const loadId = activeFileLoadIdRef.current + 1;
@@ -2906,42 +2899,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}; };
}, [isMobile, nudgeEditorSelectionAboveKeyboard]); }, [isMobile, nudgeEditorSelectionAboveKeyboard]);
React.useEffect(() => { useKeybind('open_go_to_line', (event) => {
if (!canEdit || textViewMode !== 'edit' || isMobile) { if (!canEdit || textViewMode !== 'edit' || isMobile) {
return; return false;
} }
const goToLineCombo = getEffectiveShortcutCombo('open_go_to_line', shortcutOverrides); const target = event.target as Element | null;
if (target?.closest('[role="dialog"]')) return false;
if (!(target instanceof Node) || !editorWrapperRef.current?.contains(target)) return false;
const handleKeyDown = (event: KeyboardEvent) => { const isEditorTarget = Boolean(target?.closest('.cm-editor'));
const target = event.target as Element | null; const isTypingTarget = Boolean(target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]'));
if (target?.closest('[role="dialog"]')) { if (isTypingTarget && !isEditorTarget) return false;
return;
}
const isEditorTarget = Boolean(target?.closest('.cm-editor')); setIsGoToLineOpen(true);
const isTypingTarget = Boolean( });
target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]')
);
if (isTypingTarget && !isEditorTarget) {
return;
}
const activeElement = document.activeElement as Element | null;
const editorHasFocus = Boolean(activeElement?.closest('.cm-editor'));
if (!editorHasFocus) {
return;
}
if (eventMatchesShortcut(event, goToLineCombo)) {
event.preventDefault();
setIsGoToLineOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [canEdit, isMobile, shortcutOverrides, textViewMode]);
const editorFontSize = useUIStore((state) => state.editorFontSize); const editorFontSize = useUIStore((state) => state.editorFontSize);
@@ -3196,6 +3168,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
} }
const docked = layout === 'docked'; const docked = layout === 'docked';
const saveShortcut = formatShortcutForDisplay(getEffectiveShortcutCombo('save_file'));
const wrapperCls = docked const wrapperCls = docked
? 'pointer-events-auto flex flex-wrap items-center gap-1' ? 'pointer-events-auto flex flex-wrap items-center gap-1'
: 'pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm'; : 'pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm';
@@ -3225,14 +3198,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Icon name="check" className="size-3.5" /> <Icon name="check" className="size-3.5" />
{t('filesView.editor.saved')} {t('filesView.editor.saved')}
</span> </span>
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` }), ) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut }),
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => void saveDraft()} onClick={() => void saveDraft()}
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent" className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })} title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: saveShortcut })}
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })} aria-label={t('filesView.editor.saveAria', { shortcut: saveShortcut })}
> >
<Icon name="save-3" className="size-4" /> <Icon name="save-3" className="size-4" />
</Button> </Button>
@@ -1,5 +1,9 @@
import React from 'react'; import React from 'react';
import { cn, getModifierLabel } from '@/lib/utils'; import { cn } from '@/lib/utils';
import {
formatShortcutForDisplay,
getEffectiveShortcutCombo,
} from '@/lib/shortcuts';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useSettingsDirectory } from '@/hooks/useSettingsDirectory'; import { useSettingsDirectory } from '@/hooks/useSettingsDirectory';
import { useProjectsStore } from '@/stores/useProjectsStore'; import { useProjectsStore } from '@/stores/useProjectsStore';
@@ -187,6 +191,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const settingsPageRaw = useUIStore((state) => state.settingsPage); const settingsPageRaw = useUIStore((state) => state.settingsPage);
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen); const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage); const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const openSettingsShortcutOverride = useUIStore((state) => state.shortcutOverrides.open_settings);
const settingsSlug = resolveSettingsSlug(settingsPageRaw); const settingsSlug = resolveSettingsSlug(settingsPageRaw);
const [mobileStage, setMobileStage] = React.useState<MobileStage>(initialMobileStage); const [mobileStage, setMobileStage] = React.useState<MobileStage>(initialMobileStage);
@@ -728,7 +733,15 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
: showBackButton : showBackButton
? t('settings.view.actions.backToSettings') ? t('settings.view.actions.backToSettings')
: t('settings.view.actions.closeSettings'); : t('settings.view.actions.closeSettings');
const shortcutKey = getModifierLabel(); const openSettingsCombo = getEffectiveShortcutCombo(
'open_settings',
openSettingsShortcutOverride === undefined ? undefined : { open_settings: openSettingsShortcutOverride },
);
const closeSettingsTitle = openSettingsCombo
? t('settings.view.actions.closeSettingsWithShortcut', {
shortcut: formatShortcutForDisplay(openSettingsCombo),
})
: t('settings.view.actions.closeSettings');
const pushMobileSplitDetailHistory = React.useCallback((slug: SettingsPageSlug) => { const pushMobileSplitDetailHistory = React.useCallback((slug: SettingsPageSlug) => {
if (typeof window === 'undefined' || runtimeCtx.isVSCode) { if (typeof window === 'undefined' || runtimeCtx.isVSCode) {
@@ -1077,7 +1090,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
type="button" type="button"
onClick={onClose} onClick={onClose}
aria-label={t('settings.view.actions.closeSettings')} aria-label={t('settings.view.actions.closeSettings')}
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })} title={closeSettingsTitle}
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
> >
<Icon name="close" className="h-5 w-5" /> <Icon name="close" className="h-5 w-5" />
@@ -1105,7 +1118,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
type="button" type="button"
onClick={onClose} onClick={onClose}
aria-label={t('settings.view.actions.closeSettings')} aria-label={t('settings.view.actions.closeSettings')}
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })} title={closeSettingsTitle}
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
> >
<Icon name="close" className="h-5 w-5" /> <Icon name="close" className="h-5 w-5" />
@@ -21,6 +21,7 @@ import { useI18n } from '@/lib/i18n';
import { PROJECT_ACTION_ICON_MAP, type ProjectActionIconKey } from '@/lib/projectActions'; import { PROJECT_ACTION_ICON_MAP, type ProjectActionIconKey } from '@/lib/projectActions';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore'; import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { applyTerminalModifier, terminalControlCharacter, terminalSequenceForKey, type TerminalModifier as Modifier, type TerminalQuickKey as MobileKey } from '@/lib/terminalInput'; import { applyTerminalModifier, terminalControlCharacter, terminalSequenceForKey, type TerminalModifier as Modifier, type TerminalQuickKey as MobileKey } from '@/lib/terminalInput';
import { formatShortcutForDisplay } from '@/lib/shortcuts';
type TerminalViewProps = { type TerminalViewProps = {
visible?: boolean; visible?: boolean;
@@ -968,7 +969,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
onClick={() => handleModifierToggle('ctrl')} onClick={() => handleModifierToggle('ctrl')}
disabled={quickKeysDisabled} disabled={quickKeysDisabled}
> >
<span className="text-xs font-medium">{t('terminalView.quickKeys.controlLabel')}</span> <span className="text-xs font-medium">{formatShortcutForDisplay('ctrl')}</span>
<span className="sr-only">{t('terminalView.quickKeys.controlModifierAria')}</span> <span className="sr-only">{t('terminalView.quickKeys.controlModifierAria')}</span>
</Button> </Button>
<Button <Button
@@ -981,7 +982,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible }) => {
onClick={() => handleModifierToggle('alt')} onClick={() => handleModifierToggle('alt')}
disabled={quickKeysDisabled} disabled={quickKeysDisabled}
> >
<span className="text-xs font-medium">{t('terminalView.quickKeys.altLabel')}</span> <span className="text-xs font-medium">{formatShortcutForDisplay('alt')}</span>
<span className="sr-only">{t('terminalView.quickKeys.altModifierAria')}</span> <span className="sr-only">{t('terminalView.quickKeys.altModifierAria')}</span>
</Button> </Button>
<Button <Button
@@ -6,6 +6,7 @@ import { useI18n } from '@/lib/i18n';
interface CommitInputProps { interface CommitInputProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit?: () => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
hasTouchInput?: boolean; hasTouchInput?: boolean;
@@ -18,6 +19,7 @@ const MAX_HEIGHT = 200;
export const CommitInput: React.FC<CommitInputProps> = ({ export const CommitInput: React.FC<CommitInputProps> = ({
value, value,
onChange, onChange,
onSubmit,
placeholder, placeholder,
disabled = false, disabled = false,
hasTouchInput = false, hasTouchInput = false,
@@ -58,6 +60,12 @@ export const CommitInput: React.FC<CommitInputProps> = ({
ref={textareaRef} ref={textareaRef}
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
e.preventDefault();
onSubmit?.();
}
}}
placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')} placeholder={placeholder ?? t('gitView.commit.messagePlaceholder')}
rows={1} rows={1}
disabled={disabled} disabled={disabled}
@@ -68,6 +68,9 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
<CommitInput <CommitInput
value={commitMessage} value={commitMessage}
onChange={onCommitMessageChange} onChange={onCommitMessageChange}
onSubmit={() => {
if (canCommit && !isGeneratingMessage) onCommit();
}}
placeholder={t('gitView.commit.messagePlaceholder')} placeholder={t('gitView.commit.messagePlaceholder')}
disabled={commitAction !== null} disabled={commitAction !== null}
hasTouchInput={hasTouchInput} hasTouchInput={hasTouchInput}
@@ -7,11 +7,10 @@ import React, {
} from 'react'; } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import type { Theme, ThemeMode } from '@/types/theme'; import type { Theme, ThemeMode } from '@/types/theme';
import type { DesktopSettings } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell as detectDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { isDesktopLocalOriginActive, isDesktopShell as detectDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { setDesktopWindowTheme } from '@/lib/desktopNative'; import { setDesktopWindowTheme } from '@/lib/desktopNative';
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator'; import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
import { updateDesktopSettings } from '@/lib/persistence'; import { type SettingsSyncedDetail, updateDesktopSettings } from '@/lib/persistence';
import { import {
themes, themes,
getThemeById, getThemeById,
@@ -622,7 +621,7 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
return; return;
} }
const handleSettingsSynced = (event: Event) => { const handleSettingsSynced = (event: Event) => {
const detail = (event as CustomEvent<DesktopSettings>).detail; const detail = (event as CustomEvent<SettingsSyncedDetail>).detail?.settings;
if (!detail) { if (!detail) {
return; return;
} }
@@ -1,6 +1,6 @@
import { expect, test } from 'bun:test'; import { expect, test } from 'bun:test';
import { hasOpenDropdown } from './keyboard-shortcut-dom'; import { hasOpenDropdown, shouldStopDropdownImeEscape } from './keyboard-shortcut-dom';
test('does not treat an unrelated visible listbox as an open dropdown', () => { test('does not treat an unrelated visible listbox as an open dropdown', () => {
const promptNavigator = {} as Element; const promptNavigator = {} as Element;
@@ -28,3 +28,10 @@ test('detects an open select popup', () => {
expect(hasOpenDropdown(root)).toBe(true); expect(hasOpenDropdown(root)).toBe(true);
}); });
test('stops IME Escape before an open dropdown dismiss listener', () => {
expect(shouldStopDropdownImeEscape({ key: 'Escape', isComposing: true, keyCode: 0 }, true)).toBe(true);
expect(shouldStopDropdownImeEscape({ key: 'Escape', isComposing: false, keyCode: 229 }, true)).toBe(true);
expect(shouldStopDropdownImeEscape({ key: 'Escape', isComposing: false, keyCode: 27 }, true)).toBe(false);
expect(shouldStopDropdownImeEscape({ key: 'Escape', isComposing: true, keyCode: 0 }, false)).toBe(false);
});
@@ -6,3 +6,19 @@ const OPEN_DROPDOWN_SELECTOR = [
export function hasOpenDropdown(root: ParentNode = document): boolean { export function hasOpenDropdown(root: ParentNode = document): boolean {
return Boolean(root.querySelector(OPEN_DROPDOWN_SELECTOR)); return Boolean(root.querySelector(OPEN_DROPDOWN_SELECTOR));
} }
export function shouldStopDropdownImeEscape(
event: Pick<KeyboardEvent, 'isComposing' | 'key' | 'keyCode'>,
dropdownOpen: boolean,
): boolean {
return dropdownOpen
&& event.key === 'Escape'
&& (event.isComposing || event.keyCode === 229);
}
export function isEditableEventTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
if (target.isContentEditable) return true;
const tagName = target.tagName;
return tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
}
+65 -5
View File
@@ -7,7 +7,9 @@ import { useUIStore } from '@/stores/useUIStore';
import { import {
CHAT_LIST_ANCHOR_OFFSET, CHAT_LIST_ANCHOR_OFFSET,
getAnchoredTurnMetrics, getAnchoredTurnMetrics,
getRowBottom,
resolveTimelineIsAtEnd, resolveTimelineIsAtEnd,
TIMELINE_FOLLOW_REARM_THRESHOLD_PX,
type TimelineListMeasurementState, type TimelineListMeasurementState,
type TimelineScrollMode, type TimelineScrollMode,
} from '@/components/chat/lib/scroll/timelineScrollAnchoring'; } from '@/components/chat/lib/scroll/timelineScrollAnchoring';
@@ -129,6 +131,8 @@ export const useChatTimelineScroll = ({
// True after a real gesture until an explicit opt back in; drives the // True after a real gesture until an explicit opt back in; drives the
// overlay scrollbar suppression instead of the anchor's mere existence. // overlay scrollbar suppression instead of the anchor's mere existence.
const [userOwnsScroll, setUserOwnsScroll] = React.useState(false); const [userOwnsScroll, setUserOwnsScroll] = React.useState(false);
const userOwnsScrollRef = React.useRef(userOwnsScroll);
userOwnsScrollRef.current = userOwnsScroll;
const modeRef = React.useRef<TimelineScrollMode>('following-end'); const modeRef = React.useRef<TimelineScrollMode>('following-end');
const isAtEndRef = React.useRef(true); const isAtEndRef = React.useRef(true);
@@ -547,9 +551,12 @@ export const useChatTimelineScroll = ({
// per-frame row re-measure and the pinned viewport shakes. Corrections // per-frame row re-measure and the pinned viewport shakes. Corrections
// stand down for the whole resize and the visible content is held by the // stand down for the whole resize and the visible content is held by the
// list's size compensation instead. Deliberately NO snap back to the end // list's size compensation instead. Deliberately NO snap back to the end
// afterwards: a slow drag settles repeatedly, and each snap reads as the // afterwards for a mid-conversation reader: a slow drag settles
// very jump this suspension removes — geometry changed, staying where the // repeatedly, and each snap reads as the very jump this suspension
// reader is beats re-asserting the edge. // removes. A reader who WAS at the end is the exception — after rows
// re-wrap, stale cached sizes can leave a large phantom gap below the
// last row, so re-asserting the end once on settle is what "staying
// where the reader is" means for them.
const widthResizingRef = React.useRef(false); const widthResizingRef = React.useRef(false);
React.useEffect(() => { React.useEffect(() => {
if (!scrollNode || typeof ResizeObserver === 'undefined') return; if (!scrollNode || typeof ResizeObserver === 'undefined') return;
@@ -569,6 +576,9 @@ export const useChatTimelineScroll = ({
quietTimer = setTimeout(() => { quietTimer = setTimeout(() => {
quietTimer = null; quietTimer = null;
widthResizingRef.current = false; widthResizingRef.current = false;
if (isAtEndRef.current && pendingAnchorRef.current === null) {
void listRef.current?.scrollToEnd({ animated: false });
}
}, 350); }, 350);
}); });
observer.observe(scrollNode); observer.observe(scrollNode);
@@ -580,7 +590,57 @@ export const useChatTimelineScroll = ({
const onTimelineDataChange = React.useCallback(() => { const onTimelineDataChange = React.useCallback(() => {
if (widthResizingRef.current) return; if (widthResizingRef.current) return;
if (!streamingAutoFollowEnabledRef.current) return;
// Stranded-viewport rescue, independent of any follow mode or
// preference: when off-screen size estimates settle smaller than
// estimated, the measured content can end ABOVE the viewport while
// the scroll offset stays at the stale end — the reader faces a blank
// phantom tail with every row out of reach above. That state is never
// intentional, so it is corrected even when auto-follow is off. Only
// a fully blank viewport qualifies; partial visibility is left alone.
if (!userOwnsScrollRef.current) {
const list = listRef.current;
if (list) {
const state = list.getState();
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null && state.scroll > lastBottom) {
const visibleLength = Math.max(
0,
state.scrollLength - composerOverlayHeightRef.current - CHAT_LIST_ANCHOR_OFFSET,
);
void list.scrollToOffset({
offset: Math.max(0, lastBottom - visibleLength),
animated: false,
});
return;
}
}
}
if (!streamingAutoFollowEnabledRef.current) {
// With auto-follow off nothing moves the viewport, so a growing
// reply slides below the visible area without a single scroll
// event — and the at-end transition that offers the pill never
// fires. Content growth is the signal here: once the real last
// row extends past what the composer leaves visible, the reader
// is factually behind and the pill must say so.
const list = listRef.current;
if (list && isAtEndRef.current) {
const state = list.getState();
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null) {
const visibleBottom = state.scroll + state.scrollLength - composerOverlayHeightRef.current;
if (lastBottom - visibleBottom > TIMELINE_FOLLOW_REARM_THRESHOLD_PX) {
isAtEndRef.current = false;
setIsPinned(false);
scheduleShowScrollButton();
}
}
}
return;
}
if (!isLiveFollowActive()) return; if (!isLiveFollowActive()) return;
// Since @legendapp/list 3.3.x, maintainScrollAtEnd follows content // Since @legendapp/list 3.3.x, maintainScrollAtEnd follows content
@@ -637,7 +697,7 @@ export const useChatTimelineScroll = ({
}); });
}); });
}, [isLiveFollowActive]); }, [isLiveFollowActive, scheduleShowScrollButton]);
// The streaming tail grows inside one row without changing the entries // The streaming tail grows inside one row without changing the entries
// array, so data-change callbacks are silent for the entire stream. The // array, so data-change callbacks are silent for the entire stream. The
+21
View File
@@ -0,0 +1,21 @@
import { expect, test } from 'bun:test';
import type { ShortcutHandler } from '@/lib/shortcuts';
import type { ShortcutBindings } from './useKeybind';
const handler: ShortcutHandler = () => {};
const validBindings = {
open_session_list: handler,
};
const mixedBindingsWithTypo = {
open_session_list: handler,
open_session_lsit: handler,
};
const acceptedBindings: ShortcutBindings<typeof validBindings> = validBindings;
// @ts-expect-error A misspelled key must fail even when the object also contains a valid ID.
const rejectedBindings: ShortcutBindings<typeof mixedBindingsWithTypo> = mixedBindingsWithTypo;
void rejectedBindings;
test('accepts bindings whose IDs are declared in the shortcut schema', () => {
expect(Object.keys(acceptedBindings)).toEqual(['open_session_list']);
});
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { shortcutRegistry, type ShortcutActionId, type ShortcutHandler } from '@/lib/shortcuts';
export function useKeybind(actionId: ShortcutActionId, handler: ShortcutHandler): void {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => shortcutRegistry.register(actionId, (event) => handlerRef.current(event)), [actionId]);
}
export type ShortcutBindings<
Bindings extends Partial<Record<ShortcutActionId, ShortcutHandler>>,
> = Bindings & Record<Exclude<keyof Bindings, ShortcutActionId>, never>;
export function useKeybinds<
const Bindings extends Partial<Record<ShortcutActionId, ShortcutHandler>>,
>(bindings: ShortcutBindings<Bindings>): void {
const handlersRef = React.useRef(bindings);
handlersRef.current = bindings;
const actionIdsKey = Object.keys(bindings).sort().join('\0');
React.useEffect(() => {
const actionIds = (actionIdsKey ? actionIdsKey.split('\0') : []) as ShortcutActionId[];
const unregister = actionIds.map((actionId) => shortcutRegistry.register(actionId, (event) => {
const handler = handlersRef.current[actionId];
return handler ? handler(event) : false;
}));
return () => unregister.forEach((remove) => remove());
}, [actionIdsKey]);
}
File diff suppressed because it is too large Load Diff
@@ -1,97 +1,129 @@
import React from 'react'; import React from 'react';
import { focusChatInput } from '@/components/chat/composer/editor/dom'; import { focusChatInput } from '@/components/chat/composer/editor/dom';
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop'; import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { ShortcutDispatcher, getEffectiveShortcutCombo, shortcutRegistry } from '@/lib/shortcuts';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useSelectionStore } from '@/sync/selection-store'; import { useSelectionStore } from '@/sync/selection-store';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useKeybinds } from './useKeybind';
import { isEditableEventTarget } from './keyboard-shortcut-dom';
export const useMiniChatKeyboardShortcuts = () => { export const useMiniChatKeyboardShortcuts = () => {
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
const dispatcherRef = React.useRef<ShortcutDispatcher | null>(null);
if (!dispatcherRef.current) {
dispatcherRef.current = new ShortcutDispatcher({
registry: shortcutRegistry,
getBinding: (actionId) => getEffectiveShortcutCombo(
actionId,
useUIStore.getState().shortcutOverrides,
),
});
}
const dispatcher = dispatcherRef.current;
const cycleFavoriteModel = (delta: number): boolean | void => {
const { favoriteModels, addRecentModel } = useUIStore.getState();
if (favoriteModels.length === 0) return false;
const {
currentProviderId,
currentModelId,
setProvider,
setModel,
} = useConfigStore.getState();
const currentIndex = favoriteModels.findIndex(
(favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId,
);
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
setProvider(next.providerID);
setModel(next.modelID);
addRecentModel(next.providerID, next.modelID);
};
useKeybinds({
focus_input: () => {
focusChatInput();
},
new_mini_chat: () => {
if (!canUseElectronDesktopIPC()) return false;
void invokeDesktop('desktop_open_draft_mini_chat_window', {
directory: '',
projectId: null,
})?.catch((error) => {
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
});
},
new_chat: () => {
const sessionState = useSessionUIStore.getState();
openNewSessionDraft(sessionState.currentSessionId && sessionState.currentSessionDirectory
? { directoryOverride: sessionState.currentSessionDirectory }
: undefined);
focusChatInput();
},
open_model_selector: () => {
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
setModelSelectorOpen(!isModelSelectorOpen);
},
cycle_thinking_variant: () => {
const configState = useConfigStore.getState();
if (configState.getCurrentModelVariants().length === 0) return false;
const nextVariantOverride = configState.cycleCurrentVariant();
const sessionId = useSessionUIStore.getState().currentSessionId;
const {
currentAgentName,
currentProviderId,
currentModelId,
} = useConfigStore.getState();
if (sessionId && currentAgentName && currentProviderId && currentModelId) {
useSelectionStore.getState().saveAgentModelVariantForSession(
sessionId,
currentAgentName,
currentProviderId,
currentModelId,
nextVariantOverride,
);
}
},
cycle_favorite_model_forward: () => cycleFavoriteModel(1),
cycle_favorite_model_backward: () => cycleFavoriteModel(-1),
});
React.useEffect(() => { React.useEffect(() => {
const combo = (actionId: string) => getEffectiveShortcutCombo(actionId, shortcutOverrides); const handleActivePrefixKeyDownCapture = (event: KeyboardEvent) => {
if (!dispatcher.hasActivePrefix()) return;
const handleKeyDown = (event: KeyboardEvent) => { // An unmodified completion key typed into an editable target is only a
if (eventMatchesShortcut(event, combo('focus_input'))) { // deliberate sequence when the prefix was armed from that same target;
event.preventDefault(); // otherwise it is regular typing and must not be swallowed.
focusChatInput(); if (
!event.ctrlKey && !event.metaKey && !event.altKey
&& isEditableEventTarget(event.target)
&& dispatcher.getActivePrefixTarget() !== event.target
) {
dispatcher.clear();
return; return;
} }
if (dispatcher.dispatchActivePrefix(event)) {
if (canUseElectronDesktopIPC() && eventMatchesShortcut(event, combo('new_mini_chat'))) {
event.preventDefault(); event.preventDefault();
void invokeDesktop('desktop_open_draft_mini_chat_window', { event.stopPropagation();
directory: '',
projectId: null,
})?.catch((error) => {
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
});
return;
}
if (eventMatchesShortcut(event, combo('new_chat'))) {
event.preventDefault();
const sessionState = useSessionUIStore.getState();
openNewSessionDraft(sessionState.currentSessionId && sessionState.currentSessionDirectory
? { directoryOverride: sessionState.currentSessionDirectory }
: undefined);
focusChatInput();
return;
}
if (eventMatchesShortcut(event, combo('open_model_selector'))) {
event.preventDefault();
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
setModelSelectorOpen(!isModelSelectorOpen);
return;
}
if (eventMatchesShortcut(event, combo('cycle_thinking_variant'))) {
const configState = useConfigStore.getState();
const variants = configState.getCurrentModelVariants();
if (variants.length === 0) {
return;
}
event.preventDefault();
configState.cycleCurrentVariant();
const nextVariant = useConfigStore.getState().currentVariant;
const sessionId = useSessionUIStore.getState().currentSessionId;
const agentName = useConfigStore.getState().currentAgentName;
const providerId = useConfigStore.getState().currentProviderId;
const modelId = useConfigStore.getState().currentModelId;
if (sessionId && agentName && providerId && modelId) {
useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, nextVariant);
}
return;
}
const cyclesForward = eventMatchesShortcut(event, combo('cycle_favorite_model_forward'));
const cyclesBackward = eventMatchesShortcut(event, combo('cycle_favorite_model_backward'));
if (cyclesForward || cyclesBackward) {
const { favoriteModels, addRecentModel } = useUIStore.getState();
if (favoriteModels.length === 0) {
return;
}
event.preventDefault();
const { currentProviderId, currentModelId, setProvider, setModel } = useConfigStore.getState();
const currentIndex = favoriteModels.findIndex((favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId);
const delta = cyclesForward ? 1 : -1;
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
setProvider(next.providerID);
setModel(next.modelID);
addRecentModel(next.providerID, next.modelID);
} }
}; };
const handleKeyDown = (event: KeyboardEvent) => {
if (dispatcher.consumeCapturedPrefixEvent(event)) return;
if (dispatcher.dispatch(event)) event.preventDefault();
};
const handleBlur = () => dispatcher.handleBlur();
window.addEventListener('keydown', handleActivePrefixKeyDownCapture, true);
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); window.addEventListener('blur', handleBlur);
}, [openNewSessionDraft, shortcutOverrides]); return () => {
window.removeEventListener('keydown', handleActivePrefixKeyDownCapture, true);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('blur', handleBlur);
};
}, [dispatcher]);
}; };
+57
View File
@@ -8,9 +8,66 @@ import {
} from '@/components/chat/message/selectionMarkdown'; } from '@/components/chat/message/selectionMarkdown';
import { useInputStore } from '@/sync/input-store'; import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { shortcutRegistry } from '@/lib/shortcuts';
const CHAT_INPUT_HOST_SELECTOR = '[data-chat-input="true"]'; const CHAT_INPUT_HOST_SELECTOR = '[data-chat-input="true"]';
interface ActiveSelectionToolbarActions {
addToChat: () => void;
dismiss: () => void;
}
interface ActiveSelectionToolbarRegistration extends ActiveSelectionToolbarActions {
resumeGlobalShortcuts: () => void;
}
const activeSelectionToolbarRegistrations: ActiveSelectionToolbarRegistration[] = [];
let activeSelectionToolbarVersion = 0;
const releaseSelectionToolbar = (registration: ActiveSelectionToolbarRegistration): void => {
const index = activeSelectionToolbarRegistrations.indexOf(registration);
if (index === -1) return;
activeSelectionToolbarRegistrations.splice(index, 1);
registration.resumeGlobalShortcuts();
activeSelectionToolbarVersion += 1;
};
export const registerActiveSelectionToolbar = (
actions: ActiveSelectionToolbarActions,
): (() => void) => {
const registration: ActiveSelectionToolbarRegistration = {
...actions,
resumeGlobalShortcuts: shortcutRegistry.suspend(),
};
activeSelectionToolbarRegistrations.push(registration);
activeSelectionToolbarVersion += 1;
return () => releaseSelectionToolbar(registration);
};
export const hasActiveSelectionToolbar = (): boolean => activeSelectionToolbarRegistrations.length > 0;
export const getActiveSelectionToolbarVersion = (): number => activeSelectionToolbarVersion;
export const invokeActiveSelectionAddToChat = (): boolean => {
const registration = activeSelectionToolbarRegistrations.at(-1);
if (!registration) return false;
releaseSelectionToolbar(registration);
registration.addToChat();
return true;
};
export const dismissActiveSelectionToolbar = (): boolean => {
const registration = activeSelectionToolbarRegistrations.at(-1);
if (!registration) return false;
releaseSelectionToolbar(registration);
registration.dismiss();
return true;
};
const isInsideChatComposer = (node: Node | null): boolean => { const isInsideChatComposer = (node: Node | null): boolean => {
if (!node) { if (!node) {
return false; return false;
@@ -1080,18 +1080,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal erweitert umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal erweitert umschalten',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Auswahl zum Chat hinzufügen', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Auswahl zum Chat hinzufügen',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Seitenleiste umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Seitenleiste umschalten',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Rechte Seitenleiste umschalten', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Sitzungs-Tab wechseln',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git-Tab der rechten Seitenleiste öffnen', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Datei-Tab der rechten Seitenleiste öffnen',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Kontextpanel-Oberfläche wechseln', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Kontextpanel-Oberfläche wechseln',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Vorherige Sitzung',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Nächste Sitzung',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Aktuelle Sitzung umbenennen',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Auto-Genehmigung umschalten',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Sitzungs-Tab schließen',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Tastenkürzel öffnen', 'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Tastenkürzel öffnen',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Plan-Kontextpanel umschalten',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Dienstemenü umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Dienstemenü umschalten',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Dienste-Tab durchschalten',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thema wechseln', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thema wechseln',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent wechseln', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent wechseln',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Favorites Modell vorwärts durchschalten', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Favorites Modell vorwärts durchschalten',
@@ -1100,6 +1102,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Eingabe erweitern', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Eingabe erweitern',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Konversations-Zeitleiste öffnen', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Konversations-Zeitleiste öffnen',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Prompt-Navigator umschalten', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Prompt-Navigator umschalten',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': 'Diese Sequenz teilt ein kontextabhängiges Präfix mit {action}. Wenn dessen Kontext aktiv ist, hat diese Aktion Vorrang.',
'settings.openchamber.keyboardShortcuts.category.session': 'Sitzungssteuerung',
'settings.openchamber.keyboardShortcuts.category.models': 'Modelle und Agenten',
'settings.openchamber.keyboardShortcuts.category.panels': 'Panels und Werkzeuge',
'settings.openchamber.keyboardShortcuts.category.navigation': 'Navigation',
'settings.openchamber.keyboardShortcuts.category.application': 'Anwendung',
'settings.openchamber.keyboardShortcuts.actions.edit': 'Bearbeiten',
'settings.openchamber.keyboardShortcuts.actions.confirm': 'Bestätigen',
'settings.openchamber.keyboardShortcuts.dialog.title': '{action} bearbeiten',
'settings.openchamber.keyboardShortcuts.dialog.instructions': 'Drücken Sie bis zu zwei Tastenkombinationen mit jeweils höchstens drei Tasten. Warten Sie nach der ersten bis zu 3 Sekunden auf eine zweite Kombination. Wählen Sie Bestätigen zum Anwenden oder Abbrechen zum Verwerfen. Mit der Rücktaste entfernen Sie die letzte.',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': 'Erste Kombination',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': 'Zweite Kombination',
'settings.openchamber.keyboardShortcuts.dialog.recording': 'Tasten drücken…',
'settings.openchamber.keyboardShortcuts.unassigned': 'Nicht zugewiesen',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': 'Dies kollidiert mit der von {action} verwendeten Sequenz. Wählen Sie eine andere Kombination.',
'settings.openchamber.keyboardShortcuts.error.exactConflict': 'Diese Kombination wird bereits von {action} verwendet.',
'settings.openchamber.keyboardShortcuts.error.internalConflict': 'Diese Kombination kollidiert mit einem integrierten Tastenkürzel, das nicht ersetzt werden kann.',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': 'Projektauswahl für Entwurf öffnen',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': 'Worktree-Auswahl für Entwurf öffnen',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': 'Letzte Sitzungen öffnen',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': 'Spracheingabe',
'settings.projects.sidebar.total': 'Gesamt {count}', 'settings.projects.sidebar.total': 'Gesamt {count}',
'settings.projects.sidebar.actions.addProject': 'Projekt hinzufügen', 'settings.projects.sidebar.actions.addProject': 'Projekt hinzufügen',
'settings.projects.page.empty.noProjects': 'Keine Projekte verfügbar.', 'settings.projects.page.empty.noProjects': 'Keine Projekte verfügbar.',
+22 -11
View File
@@ -1498,7 +1498,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Plan importiert', 'rightSidebar.contextNotesTodo.toast.planImported': 'Plan importiert',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Fehler beim Lesen der Plan-Datei', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Fehler beim Lesen der Plan-Datei',
'inlineComment.range.lines': 'Zeilen {start}-{end}', 'inlineComment.range.lines': 'Zeilen {start}-{end}',
'inlineComment.input.placeholder': 'Kommentar hinzufügen... (Cmd+Enter zum Speichern)', 'inlineComment.input.placeholder': 'Kommentar hinzufügen... ({shortcut} zum Speichern)',
'inlineComment.input.placeholderShort': 'Kommentar hinzufügen...', 'inlineComment.input.placeholderShort': 'Kommentar hinzufügen...',
'inlineComment.actions.cancel': 'Abbrechen', 'inlineComment.actions.cancel': 'Abbrechen',
'inlineComment.actions.save': 'Speichern', 'inlineComment.actions.save': 'Speichern',
@@ -1684,22 +1684,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Chat-Eingabe fokussieren', 'helpDialog.item.focusChatInput': 'Chat-Eingabe fokussieren',
'helpDialog.item.togglePromptNavigator': 'Aufforderungs-Navigator umschalten', 'helpDialog.item.togglePromptNavigator': 'Aufforderungs-Navigator umschalten',
'helpDialog.item.abortActiveRun': 'Aktuelle Ausführung abbrechen (Doppeltaste)', 'helpDialog.item.abortActiveRun': 'Aktuelle Ausführung abbrechen (Doppeltaste)',
'helpDialog.item.toggleRightSidebar': 'Rechte Seitenleiste umschalten',
'helpDialog.item.openRightSidebarGitTab': 'Git-Registerkarte der rechten Seitenleiste öffnen',
'helpDialog.item.openRightSidebarFilesTab': 'Datei-Registerkarte der rechten Seitenleiste öffnen',
'helpDialog.item.toggleTerminalDock': 'Terminal-Dock umschalten', 'helpDialog.item.toggleTerminalDock': 'Terminal-Dock umschalten',
'helpDialog.item.toggleTerminalExpanded': 'Terminal erweitert umschalten', 'helpDialog.item.toggleTerminalExpanded': 'Terminal erweitert umschalten',
'helpDialog.item.togglePlanContextPanel': 'Plan-Kontext-Panel umschalten',
'helpDialog.item.cycleTheme': 'Thema wechseln (Hell → Dunkel → System)', 'helpDialog.item.cycleTheme': 'Thema wechseln (Hell → Dunkel → System)',
'helpDialog.item.switchSessionTab': 'Sitzungs-Tab wechseln',
'helpDialog.item.switchContextSurface': 'Kontextpanel-Oberfläche wechseln (Zahlentaste)', 'helpDialog.item.switchContextSurface': 'Kontextpanel-Oberfläche wechseln (Zahlentaste)',
'helpDialog.item.toggleServicesMenu': 'Dienstemenü umschalten', 'helpDialog.item.toggleServicesMenu': 'Dienstemenü umschalten',
'helpDialog.item.cycleServicesTab': 'Dienste-Registerkarte durchgehen',
'helpDialog.item.openSettings': 'Einstellungen öffnen', 'helpDialog.item.openSettings': 'Einstellungen öffnen',
'helpDialog.keyCombiner.or': 'oder', 'helpDialog.keyCombiner.or': 'oder',
'helpDialog.proTips.title': 'Pro-Tipps:', 'helpDialog.proTips.title': 'Pro-Tipps:',
'helpDialog.proTips.commandPalette': 'Verwenden Sie die Befehlspalette ({shortcut}), um schnell auf alle Aktionen zuzugreifen', 'helpDialog.proTips.commandPalette': 'Verwenden Sie die Befehlspalette ({shortcut}), um schnell auf alle Aktionen zuzugreifen',
'helpDialog.proTips.recentSessions': 'Die 5 zuletzt verwendeten Sitzungen erscheinen in der Befehlspalette', 'helpDialog.proTips.recentSessions': 'Die 5 zuletzt verwendeten Sitzungen erscheinen in der Befehlspalette',
'helpDialog.proTips.themeCycling': 'Themenwechsel merken sich Ihre Einstellung über Sitzungen hinweg', 'helpDialog.proTips.leaderSequences': 'Zweistufige Kürzel: erst die Kombination, dann die zweite Taste — Esc bricht ab',
'header.actions.rightSidebarWithShortcut': 'Rechte Seitenleiste ({shortcut})', 'header.actions.rightSidebarWithShortcut': 'Rechte Seitenleiste ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Rechte Seitenleiste umschalten', 'header.actions.toggleRightSidebarAria': 'Rechte Seitenleiste umschalten',
'header.actions.openAppMenu': 'OpenChamber-Menü', 'header.actions.openAppMenu': 'OpenChamber-Menü',
@@ -1907,7 +1903,6 @@ export const dict = {
'chat.statusRow.actions.stopGeneratingAria': 'Generierung stoppen', 'chat.statusRow.actions.stopGeneratingAria': 'Generierung stoppen',
'chat.statusRow.tasksTitle': 'Aufgaben', 'chat.statusRow.tasksTitle': 'Aufgaben',
'chat.statusRow.summary.activeLeft': '{active} aktiv · {left} übrig', 'chat.statusRow.summary.activeLeft': '{active} aktiv · {left} übrig',
'chat.statusRow.aborted': 'Abgebrochen',
'chat.revertIndicator.redo': 'Wiederholen', 'chat.revertIndicator.redo': 'Wiederholen',
'chat.revertIndicator.redoAria': 'Wiederholen — wiederhergestellte Nachrichten', 'chat.revertIndicator.redoAria': 'Wiederholen — wiederhergestellte Nachrichten',
'chat.revertPopover.title': 'Zurückgesetzt', 'chat.revertPopover.title': 'Zurückgesetzt',
@@ -2018,10 +2013,8 @@ export const dict = {
'chat.textSelection.title.commentOnSelection': 'Auswahl kommentieren', 'chat.textSelection.title.commentOnSelection': 'Auswahl kommentieren',
'chat.textSelection.comment.placeholder': 'Optionalen Kommentar hinzufügen...', 'chat.textSelection.comment.placeholder': 'Optionalen Kommentar hinzufügen...',
'chat.textSelection.comment.attach': 'Anhängen', 'chat.textSelection.comment.attach': 'Anhängen',
'chat.textSelection.actions.newSession': 'Neue Sitzung',
'chat.textSelection.actions.addToNotes': 'Zu Notizen hinzufügen', 'chat.textSelection.actions.addToNotes': 'Zu Notizen hinzufügen',
'chat.textSelection.title.addToCurrentChat': 'Zum aktuellen Chat hinzufügen', 'chat.textSelection.title.addToCurrentChat': 'Zum aktuellen Chat hinzufügen',
'chat.textSelection.title.newSessionWithSelection': 'Neue Sitzung mit Auswahl erstellen',
'chat.textSelection.title.saveInsightToNotes': 'Ausgewählten Text zu Notizen speichern', 'chat.textSelection.title.saveInsightToNotes': 'Ausgewählten Text zu Notizen speichern',
'chat.messageBody.actions.revertAria': 'Zu dieser Nachricht zurückkehren', 'chat.messageBody.actions.revertAria': 'Zu dieser Nachricht zurückkehren',
'chat.messageBody.actions.revert': 'Von hier zurückkehren', 'chat.messageBody.actions.revert': 'Von hier zurückkehren',
@@ -2293,6 +2286,15 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Seitenleiste umschalten', 'commandPalette.item.toggleSidebar': 'Seitenleiste umschalten',
'commandPalette.item.showContextUsage': 'Kontextnutzung anzeigen', 'commandPalette.item.showContextUsage': 'Kontextnutzung anzeigen',
'commandPalette.item.toggleTerminal': 'Terminal umschalten', 'commandPalette.item.toggleTerminal': 'Terminal umschalten',
'commandPalette.item.cycleTheme': 'Thema wechseln',
'commandPalette.item.showOpenCodeStatus': 'OpenCode-Status anzeigen',
'commandPalette.item.toggleMemoryDebug': 'Memory-Debug-Panel umschalten',
'commandPalette.item.pinSession': 'Sitzung anheften oder lösen',
'commandPalette.item.copySessionId': 'Sitzungs-ID kopieren',
'commandPalette.item.openMultiRun': 'Multi-Run-Launcher öffnen',
'commandPalette.item.openArchive': 'Archivierte Sitzungen öffnen',
'commandPalette.item.openNotes': 'Notizbereich öffnen',
'commandPalette.item.openTodos': 'To-do-Bereich öffnen',
'commandPalette.item.openSettings': 'Einstellungen öffnen...', 'commandPalette.item.openSettings': 'Einstellungen öffnen...',
'commandPalette.session.untitled': 'Unbenannte Sitzung', 'commandPalette.session.untitled': 'Unbenannte Sitzung',
'openCodeStatusDialog.title': 'OpenCode-Status', 'openCodeStatusDialog.title': 'OpenCode-Status',
@@ -2506,6 +2508,9 @@ export const dict = {
'sessionAuth.error.passkeySignInCanceled': 'Passkey-Anmeldung wurde abgebrochen.', 'sessionAuth.error.passkeySignInCanceled': 'Passkey-Anmeldung wurde abgebrochen.',
'sessionAuth.error.enterPasswordForPasskey': 'Geben Sie Ihr Passwort ein, um einen Passkey hinzuzufügen.', 'sessionAuth.error.enterPasswordForPasskey': 'Geben Sie Ihr Passwort ein, um einen Passkey hinzuzufügen.',
'sessionAuth.locked.tunnelTitle': 'Tunnel-Zugriff erforderlich', 'sessionAuth.locked.tunnelTitle': 'Tunnel-Zugriff erforderlich',
'sessionAuth.expired.banner': 'Deine Sitzung ist abgelaufen — melde dich an, um fortzufahren.',
'sessionAuth.expired.loginAction': 'Anmelden',
'sessionAuth.expired.sendBlocked': 'Sitzung abgelaufen — melde dich an, um Nachrichten zu senden.',
'sessionAuth.locked.unlockTitle': 'OpenChamber entsperren', 'sessionAuth.locked.unlockTitle': 'OpenChamber entsperren',
'sessionAuth.locked.tunnelDescription': 'Öffnen Sie diesen Tunnel über den Einmal-Verbindungslink aus der Desktop-Anwendung.', 'sessionAuth.locked.tunnelDescription': 'Öffnen Sie diesen Tunnel über den Einmal-Verbindungslink aus der Desktop-Anwendung.',
'sessionAuth.locked.passwordDescription': 'Diese Sitzung ist passwortgeschützt.', 'sessionAuth.locked.passwordDescription': 'Diese Sitzung ist passwortgeschützt.',
@@ -2990,6 +2995,11 @@ export const dict = {
'gitView.pr.segment.comments': 'Kommentare', 'gitView.pr.segment.comments': 'Kommentare',
'gitView.pr.comments.addAll': 'Alle hinzufügen', 'gitView.pr.comments.addAll': 'Alle hinzufügen',
'contextPanel.mode.pr': 'PR', 'contextPanel.mode.pr': 'PR',
'contextRail.configure.open': 'Panels konfigurieren',
'contextRail.configure.dialogTitle': 'Leisten-Panels',
'contextRail.configure.dialogDescription': 'Wähle, welche Panels die Leiste zeigt. Ausgeblendete Panels behalten ihre Daten und bleiben über die Befehlspalette erreichbar.',
'contextRail.configure.showAll': 'Alle anzeigen',
'contextRail.configure.noneWarning': 'Alle Panels sind ausgeblendet.',
'contextRail.aria.rail': 'Kontextleiste', 'contextRail.aria.rail': 'Kontextleiste',
'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt', 'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt',
'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.', 'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.',
@@ -3081,7 +3091,8 @@ export const dict = {
'chat.commandAutocomplete.command.scheduleTaskDescription': 'Eine geplante Aufgabe erstellen', 'chat.commandAutocomplete.command.scheduleTaskDescription': 'Eine geplante Aufgabe erstellen',
'chat.chatInput.toast.scheduleTaskFailed': 'Aufgabe konnte nicht geplant werden', 'chat.chatInput.toast.scheduleTaskFailed': 'Aufgabe konnte nicht geplant werden',
'chat.container.sessionLoadError.title': 'Sitzung konnte nicht geladen werden', 'chat.container.sessionLoadError.title': 'Sitzung konnte nicht geladen werden',
'chat.container.sessionLoadError.description': 'Die Sitzung konnte nicht geladen werden.', 'chat.container.sessionLoadError.description': 'Die Unterhaltung konnte nicht geladen werden — der Server ist womöglich offline oder nicht erreichbar. Nichts ist verloren; versuche es erneut, sobald er wieder da ist.',
'chat.container.sessionLoadError.authDescription': 'Deine Sitzung ist abgelaufen, daher hat der Server die Anfrage abgelehnt. Melde dich an, dann wird die Unterhaltung geladen.',
'chat.container.sessionLoadError.retry': 'Erneut versuchen', 'chat.container.sessionLoadError.retry': 'Erneut versuchen',
'sessions.sidebar.group.empty.loadingSessions': 'Sitzungen werden geladen...', 'sessions.sidebar.group.empty.loadingSessions': 'Sitzungen werden geladen...',
'sessions.sidebar.group.empty.loadFailed': 'Sitzungen konnten nicht geladen werden', 'sessions.sidebar.group.empty.loadFailed': 'Sitzungen konnten nicht geladen werden',
@@ -1133,7 +1133,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': 'This combo is already used by another shortcut. Overwrite and clear that other mapping?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': 'This combo is already used by another shortcut. Overwrite and clear that other mapping?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': 'Press keys...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': 'Press keys...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': 'Capture a shortcut first.', 'settings.openchamber.keyboardShortcuts.error.captureFirst': 'Capture a shortcut first.',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'This shortcut can conflict with browser defaults. It is still saved.', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'This shortcut can conflict with browser defaults. You can still save it.',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Go to line (files editor)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Go to line (files editor)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Open command palette', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Open command palette',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Focus input', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Focus input',
@@ -1142,18 +1142,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Toggle terminal expanded', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Toggle terminal expanded',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Add selection to chat', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Add selection to chat',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Toggle sidebar', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Toggle sidebar',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Toggle context panel', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Switch session tab',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Open Git surface', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Open Files surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Switch context panel surface', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Switch context panel surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Previous session',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Next session',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Rename current session',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Toggle permission auto-accept',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Close session tab',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Open keyboard shortcuts', 'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Open keyboard shortcuts',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Toggle plan context panel',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Toggle services menu', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Toggle services menu',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Cycle services tab',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Cycle theme', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Cycle theme',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Cycle agent', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Cycle agent',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Cycle favorite model forward', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Cycle favorite model forward',
@@ -1162,6 +1164,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Expand input', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Expand input',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Open conversation timeline', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Open conversation timeline',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Toggle prompt navigator', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Toggle prompt navigator',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': 'This sequence shares a contextual prefix with {action}. That action takes priority while its context is active.',
'settings.openchamber.keyboardShortcuts.category.session': 'Session Controls',
'settings.openchamber.keyboardShortcuts.category.models': 'Models & Agents',
'settings.openchamber.keyboardShortcuts.category.panels': 'Panels & Tools',
'settings.openchamber.keyboardShortcuts.category.navigation': 'Navigation',
'settings.openchamber.keyboardShortcuts.category.application': 'Application',
'settings.openchamber.keyboardShortcuts.actions.edit': 'Edit',
'settings.openchamber.keyboardShortcuts.actions.confirm': 'Confirm',
'settings.openchamber.keyboardShortcuts.dialog.title': 'Edit {action}',
'settings.openchamber.keyboardShortcuts.dialog.instructions': 'Press up to two key combinations, with at most three keys each. After the first, wait up to 3 seconds for a second combination. Use Confirm to apply or Cancel to discard. Backspace removes the last one.',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': 'First combination',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': 'Second combination',
'settings.openchamber.keyboardShortcuts.dialog.recording': 'Press keys…',
'settings.openchamber.keyboardShortcuts.unassigned': 'Unassigned',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': 'This conflicts with the sequence used by {action}. Choose a different combination.',
'settings.openchamber.keyboardShortcuts.error.exactConflict': 'This combination is already used by {action}.',
'settings.openchamber.keyboardShortcuts.error.internalConflict': 'This combination conflicts with a built-in shortcut, which cannot be replaced.',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': 'Open draft project picker',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': 'Open draft worktree picker',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': 'Open recent sessions',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': 'Voice input',
'settings.projects.sidebar.total': 'Total {count}', 'settings.projects.sidebar.total': 'Total {count}',
'settings.projects.sidebar.actions.addProject': 'Add project', 'settings.projects.sidebar.actions.addProject': 'Add project',
'settings.projects.page.empty.noProjects': 'No projects available.', 'settings.projects.page.empty.noProjects': 'No projects available.',
+22 -11
View File
@@ -1139,6 +1139,11 @@ export const dict = {
'contextPanel.mode.context': 'Context', 'contextPanel.mode.context': 'Context',
'contextPanel.mode.preview': 'Preview', 'contextPanel.mode.preview': 'Preview',
'contextPanel.mode.browser': 'Browser', 'contextPanel.mode.browser': 'Browser',
'contextRail.configure.open': 'Configure panels',
'contextRail.configure.dialogTitle': 'Rail panels',
'contextRail.configure.dialogDescription': 'Choose which panels the rail shows. Hidden panels keep their data and stay reachable from the command palette.',
'contextRail.configure.showAll': 'Show all',
'contextRail.configure.noneWarning': 'All panels are hidden.',
'contextRail.aria.rail': 'Panel surfaces', 'contextRail.aria.rail': 'Panel surfaces',
'contextPanel.editorEmpty.title': 'No file open', 'contextPanel.editorEmpty.title': 'No file open',
'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.', 'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.',
@@ -1668,7 +1673,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Plan imported', 'rightSidebar.contextNotesTodo.toast.planImported': 'Plan imported',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Failed to read plan file', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Failed to read plan file',
'inlineComment.range.lines': 'Lines {start}-{end}', 'inlineComment.range.lines': 'Lines {start}-{end}',
'inlineComment.input.placeholder': 'Add a comment... (Cmd+Enter to save)', 'inlineComment.input.placeholder': 'Add a comment... ({shortcut} to save)',
'inlineComment.input.placeholderShort': 'Add a comment...', 'inlineComment.input.placeholderShort': 'Add a comment...',
'inlineComment.actions.cancel': 'Cancel', 'inlineComment.actions.cancel': 'Cancel',
'inlineComment.actions.save': 'Save', 'inlineComment.actions.save': 'Save',
@@ -1858,22 +1863,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Focus Chat Input', 'helpDialog.item.focusChatInput': 'Focus Chat Input',
'helpDialog.item.togglePromptNavigator': 'Toggle Prompt Navigator', 'helpDialog.item.togglePromptNavigator': 'Toggle Prompt Navigator',
'helpDialog.item.abortActiveRun': 'Abort active run (double press)', 'helpDialog.item.abortActiveRun': 'Abort active run (double press)',
'helpDialog.item.toggleRightSidebar': 'Toggle context panel',
'helpDialog.item.openRightSidebarGitTab': 'Open Git surface',
'helpDialog.item.openRightSidebarFilesTab': 'Open Files surface',
'helpDialog.item.toggleTerminalDock': 'Toggle Terminal Dock', 'helpDialog.item.toggleTerminalDock': 'Toggle Terminal Dock',
'helpDialog.item.toggleTerminalExpanded': 'Toggle Terminal Expanded', 'helpDialog.item.toggleTerminalExpanded': 'Toggle Terminal Expanded',
'helpDialog.item.togglePlanContextPanel': 'Toggle Plan Context Panel', 'helpDialog.item.switchSessionTab': 'Switch Session Tab',
'helpDialog.item.switchContextSurface': 'Switch Context Panel Surface (number key)', 'helpDialog.item.switchContextSurface': 'Switch Context Panel Surface (number key)',
'helpDialog.item.cycleTheme': 'Cycle Theme (Light → Dark → System)', 'helpDialog.item.cycleTheme': 'Cycle Theme (Light → Dark → System)',
'helpDialog.item.toggleServicesMenu': 'Toggle Services Menu', 'helpDialog.item.toggleServicesMenu': 'Toggle Services Menu',
'helpDialog.item.cycleServicesTab': 'Cycle Services Tab',
'helpDialog.item.openSettings': 'Open Settings', 'helpDialog.item.openSettings': 'Open Settings',
'helpDialog.keyCombiner.or': 'or', 'helpDialog.keyCombiner.or': 'or',
'helpDialog.proTips.title': 'Pro Tips:', 'helpDialog.proTips.title': 'Pro Tips:',
'helpDialog.proTips.commandPalette': 'Use Command Palette ({shortcut}) to quickly access all actions', 'helpDialog.proTips.commandPalette': 'Use Command Palette ({shortcut}) to quickly access all actions',
'helpDialog.proTips.recentSessions': 'The 5 most recent sessions appear in the Command Palette', 'helpDialog.proTips.recentSessions': 'The 5 most recent sessions appear in the Command Palette',
'helpDialog.proTips.themeCycling': 'Theme cycling remembers your preference across sessions', 'helpDialog.proTips.leaderSequences': 'Two-step shortcuts: press the first combo, then the second key — Esc cancels',
'header.actions.rightSidebarWithShortcut': 'Right sidebar ({shortcut})', 'header.actions.rightSidebarWithShortcut': 'Right sidebar ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Toggle right sidebar', 'header.actions.toggleRightSidebarAria': 'Toggle right sidebar',
'header.actions.openAppMenu': 'OpenChamber menu', 'header.actions.openAppMenu': 'OpenChamber menu',
@@ -2083,7 +2084,6 @@ export const dict = {
'chat.statusRow.tasksTitle': 'Tasks', 'chat.statusRow.tasksTitle': 'Tasks',
'chat.statusRow.modelStatus': '{model} is {status}', 'chat.statusRow.modelStatus': '{model} is {status}',
'chat.statusRow.summary.activeLeft': '{active} active · {left} left', 'chat.statusRow.summary.activeLeft': '{active} active · {left} left',
'chat.statusRow.aborted': 'Aborted',
'chat.revertIndicator.redo': 'Redo', 'chat.revertIndicator.redo': 'Redo',
'chat.revertIndicator.redoAria': 'Redo — restore reverted messages', 'chat.revertIndicator.redoAria': 'Redo — restore reverted messages',
'chat.revertPopover.title': 'Reverted', 'chat.revertPopover.title': 'Reverted',
@@ -2161,7 +2161,8 @@ export const dict = {
'chat.btw.promoteAria': 'Keep as a separate session', 'chat.btw.promoteAria': 'Keep as a separate session',
'chat.btw.toast.promoteFailed': 'Failed to keep the btw session', 'chat.btw.toast.promoteFailed': 'Failed to keep the btw session',
'chat.container.sessionLoadError.title': 'Session could not be loaded', 'chat.container.sessionLoadError.title': 'Session could not be loaded',
'chat.container.sessionLoadError.description': 'Check the connection and try loading this session again.', 'chat.container.sessionLoadError.description': 'The conversation could not be fetched — the server may be offline or unreachable. Nothing is lost; retry once it is back.',
'chat.container.sessionLoadError.authDescription': 'Your session expired, so the server refused the request. Log in and the conversation will load.',
'chat.container.sessionLoadError.retry': 'Try again', 'chat.container.sessionLoadError.retry': 'Try again',
'sessions.sidebar.group.empty.loadingSessions': 'Loading sessions…', 'sessions.sidebar.group.empty.loadingSessions': 'Loading sessions…',
'sessions.sidebar.group.empty.loadFailed': 'Could not refresh sessions.', 'sessions.sidebar.group.empty.loadFailed': 'Could not refresh sessions.',
@@ -2204,10 +2205,8 @@ export const dict = {
'chat.textSelection.title.commentOnSelection': 'Comment on selection', 'chat.textSelection.title.commentOnSelection': 'Comment on selection',
'chat.textSelection.comment.placeholder': 'Add an optional comment...', 'chat.textSelection.comment.placeholder': 'Add an optional comment...',
'chat.textSelection.comment.attach': 'Attach', 'chat.textSelection.comment.attach': 'Attach',
'chat.textSelection.actions.newSession': 'New session',
'chat.textSelection.actions.addToNotes': 'Add to notes', 'chat.textSelection.actions.addToNotes': 'Add to notes',
'chat.textSelection.title.addToCurrentChat': 'Add to current chat', 'chat.textSelection.title.addToCurrentChat': 'Add to current chat',
'chat.textSelection.title.newSessionWithSelection': 'Create new session with selection',
'chat.textSelection.title.saveInsightToNotes': 'Save selected text to notes', 'chat.textSelection.title.saveInsightToNotes': 'Save selected text to notes',
'chat.messageBody.actions.revertAria': 'Revert to this message', 'chat.messageBody.actions.revertAria': 'Revert to this message',
'chat.messageBody.actions.revert': 'Revert from here', 'chat.messageBody.actions.revert': 'Revert from here',
@@ -2483,6 +2482,15 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Toggle Sidebar', 'commandPalette.item.toggleSidebar': 'Toggle Sidebar',
'commandPalette.item.showContextUsage': 'Show Context Usage', 'commandPalette.item.showContextUsage': 'Show Context Usage',
'commandPalette.item.toggleTerminal': 'Toggle Terminal', 'commandPalette.item.toggleTerminal': 'Toggle Terminal',
'commandPalette.item.cycleTheme': 'Cycle theme',
'commandPalette.item.showOpenCodeStatus': 'Show OpenCode status',
'commandPalette.item.toggleMemoryDebug': 'Toggle memory debug panel',
'commandPalette.item.pinSession': 'Pin or unpin session',
'commandPalette.item.copySessionId': 'Copy session ID',
'commandPalette.item.openMultiRun': 'Open multi-run launcher',
'commandPalette.item.openArchive': 'Open archived sessions',
'commandPalette.item.openNotes': 'Open notes surface',
'commandPalette.item.openTodos': 'Open todos surface',
'commandPalette.item.openSettings': 'Open Settings...', 'commandPalette.item.openSettings': 'Open Settings...',
'commandPalette.session.untitled': 'Untitled Session', 'commandPalette.session.untitled': 'Untitled Session',
'openCodeStatusDialog.title': 'OpenCode Status', 'openCodeStatusDialog.title': 'OpenCode Status',
@@ -2697,6 +2705,9 @@ export const dict = {
'sessionAuth.error.passkeySignInCanceled': 'Passkey sign-in was canceled.', 'sessionAuth.error.passkeySignInCanceled': 'Passkey sign-in was canceled.',
'sessionAuth.error.enterPasswordForPasskey': 'Enter your password to add a passkey.', 'sessionAuth.error.enterPasswordForPasskey': 'Enter your password to add a passkey.',
'sessionAuth.locked.tunnelTitle': 'Tunnel access required', 'sessionAuth.locked.tunnelTitle': 'Tunnel access required',
'sessionAuth.expired.banner': 'Your session expired — log in to continue.',
'sessionAuth.expired.loginAction': 'Log in',
'sessionAuth.expired.sendBlocked': 'Session expired — log in to send messages.',
'sessionAuth.locked.unlockTitle': 'Unlock OpenChamber', 'sessionAuth.locked.unlockTitle': 'Unlock OpenChamber',
'sessionAuth.locked.tunnelDescription': 'Open this tunnel using the one-time connect link from the desktop app.', 'sessionAuth.locked.tunnelDescription': 'Open this tunnel using the one-time connect link from the desktop app.',
'sessionAuth.locked.passwordDescription': 'This session is password-protected.', 'sessionAuth.locked.passwordDescription': 'This session is password-protected.',
@@ -1101,7 +1101,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.overwritePrompt": "Esta combinación ya está usada por otro atajo. ¿Sobrescribir y limpiar esa otra asignación?", "settings.openchamber.keyboardShortcuts.overwritePrompt": "Esta combinación ya está usada por otro atajo. ¿Sobrescribir y limpiar esa otra asignación?",
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Pulsa las teclas...", "settings.openchamber.keyboardShortcuts.field.pressKeys": "Pulsa las teclas...",
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura un atajo primero.", "settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura un atajo primero.",
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atajo puede entrar en conflicto con los predeterminados del navegador. Todavía se guarda.", "settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atajo puede entrar en conflicto con los predeterminados del navegador. Aun así, puedes guardarlo.",
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir a línea (editor de archivos)", "settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir a línea (editor de archivos)",
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos", "settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos",
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Enfocar entrada", "settings.openchamber.keyboardShortcuts.action.focus_input.label": "Enfocar entrada",
@@ -1110,18 +1110,20 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir o contraer terminal", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir o contraer terminal",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Agregar selección al chat", "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Agregar selección al chat",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar u ocultar barra lateral", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar u ocultar barra lateral",
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar panel de contexto', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Cambiar pestaña de sesión",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superficie de Git', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superficie de archivos',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión",
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sesión anterior",
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Sesión siguiente",
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renombrar sesión actual",
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprobación automática",
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Cerrar pestaña de sesión",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atajos de teclado", "settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atajos de teclado",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar panel de plan de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar u ocultar menú de servicios", "settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar u ocultar menú de servicios",
"settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Cambiar pestaña de servicios",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Cambiar tema", "settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Cambiar tema",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Cambiar agente", "settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Cambiar agente",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Siguiente modelo favorito", "settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Siguiente modelo favorito",
@@ -1130,6 +1132,27 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada", "settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir línea de tiempo de conversación", "settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir línea de tiempo de conversación",
"settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Mostrar u ocultar navegador de prompts", "settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Mostrar u ocultar navegador de prompts",
"settings.openchamber.keyboardShortcuts.warning.contextualPrefix": "Esta secuencia comparte un prefijo contextual con {action}. Cuando su contexto está activo, esa acción tiene prioridad.",
"settings.openchamber.keyboardShortcuts.category.session": "Controles de sesión",
"settings.openchamber.keyboardShortcuts.category.models": "Modelos y agentes",
"settings.openchamber.keyboardShortcuts.category.panels": "Paneles y herramientas",
"settings.openchamber.keyboardShortcuts.category.navigation": "Navegación",
"settings.openchamber.keyboardShortcuts.category.application": "Aplicación",
"settings.openchamber.keyboardShortcuts.actions.edit": "Editar",
"settings.openchamber.keyboardShortcuts.actions.confirm": "Confirmar",
"settings.openchamber.keyboardShortcuts.dialog.title": "Editar {action}",
"settings.openchamber.keyboardShortcuts.dialog.instructions": "Pulse hasta dos combinaciones de teclas, con un máximo de tres teclas cada una. Tras la primera, espere hasta 3 segundos por una segunda combinación. Use Confirmar para aplicar o Cancelar para descartar. Retroceso elimina la última.",
"settings.openchamber.keyboardShortcuts.dialog.firstChord": "Primera combinación",
"settings.openchamber.keyboardShortcuts.dialog.secondChord": "Segunda combinación",
"settings.openchamber.keyboardShortcuts.dialog.recording": "Pulse las teclas…",
"settings.openchamber.keyboardShortcuts.unassigned": "Sin asignar",
"settings.openchamber.keyboardShortcuts.error.prefixConflict": "Esto entra en conflicto con la secuencia usada por {action}. Elija otra combinación.",
"settings.openchamber.keyboardShortcuts.error.exactConflict": "Esta combinación ya la usa {action}.",
"settings.openchamber.keyboardShortcuts.error.internalConflict": "Esta combinación entra en conflicto con un atajo integrado, que no se puede reemplazar.",
"settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label": "Abrir selector de proyecto de borrador",
"settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label": "Abrir selector de árbol de trabajo de borrador",
"settings.openchamber.keyboardShortcuts.action.open_session_list.label": "Abrir sesiones recientes",
"settings.openchamber.keyboardShortcuts.action.toggle_dictation.label": "Entrada de voz",
"settings.projects.sidebar.total": "Total {count}", "settings.projects.sidebar.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Añadir proyecto", "settings.projects.sidebar.actions.addProject": "Añadir proyecto",
"settings.projects.page.empty.noProjects": "No hay proyectos disponibles.", "settings.projects.page.empty.noProjects": "No hay proyectos disponibles.",
+22 -11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Contexto", "contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Vista previa", "contextPanel.mode.preview": "Vista previa",
"contextPanel.mode.browser": "Navegador", "contextPanel.mode.browser": "Navegador",
"contextRail.configure.open": "Configurar paneles",
"contextRail.configure.dialogTitle": "Paneles de la barra",
"contextRail.configure.dialogDescription": "Elige qué paneles muestra la barra. Los paneles ocultos conservan sus datos y siguen accesibles desde la paleta de comandos.",
"contextRail.configure.showAll": "Mostrar todos",
"contextRail.configure.noneWarning": "Todos los paneles están ocultos.",
"contextRail.aria.rail": "Superficies del panel", "contextRail.aria.rail": "Superficies del panel",
"contextPanel.editorEmpty.title": "Ningún archivo abierto", "contextPanel.editorEmpty.title": "Ningún archivo abierto",
"contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.", "contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.",
@@ -1646,7 +1651,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "Plan importado", "rightSidebar.contextNotesTodo.toast.planImported": "Plan importado",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "No se pudo leer el archivo del plan", "rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "No se pudo leer el archivo del plan",
"inlineComment.range.lines": "Líneas {start}-{end}", "inlineComment.range.lines": "Líneas {start}-{end}",
"inlineComment.input.placeholder": "Añadir un comentario... (Cmd+Enter para guardar)", "inlineComment.input.placeholder": "Añadir un comentario... ({shortcut} para guardar)",
"inlineComment.input.placeholderShort": "Añadir un comentario...", "inlineComment.input.placeholderShort": "Añadir un comentario...",
"inlineComment.actions.cancel": "Cancelar", "inlineComment.actions.cancel": "Cancelar",
"inlineComment.actions.save": "Guardar", "inlineComment.actions.save": "Guardar",
@@ -1836,22 +1841,18 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.focusChatInput": "Enfocar entrada de chat", "helpDialog.item.focusChatInput": "Enfocar entrada de chat",
"helpDialog.item.togglePromptNavigator": "Mostrar u ocultar navegador de prompts", "helpDialog.item.togglePromptNavigator": "Mostrar u ocultar navegador de prompts",
"helpDialog.item.abortActiveRun": "Detener ejecución activa (doble presionar)", "helpDialog.item.abortActiveRun": "Detener ejecución activa (doble presionar)",
"helpDialog.item.toggleRightSidebar": 'Alternar panel de contexto',
"helpDialog.item.openRightSidebarGitTab": 'Abrir superficie de Git',
"helpDialog.item.openRightSidebarFilesTab": 'Abrir superficie de archivos',
"helpDialog.item.toggleTerminalDock": "Mostrar u ocultar dock de terminal", "helpDialog.item.toggleTerminalDock": "Mostrar u ocultar dock de terminal",
"helpDialog.item.toggleTerminalExpanded": "Expandir o contraer terminal", "helpDialog.item.toggleTerminalExpanded": "Expandir o contraer terminal",
"helpDialog.item.togglePlanContextPanel": "Alternar panel de contexto del plan",
"helpDialog.item.cycleTheme": "Cambiar tema (Claro → Oscuro → Sistema)", "helpDialog.item.cycleTheme": "Cambiar tema (Claro → Oscuro → Sistema)",
"helpDialog.item.switchSessionTab": "Cambiar pestaña de sesión",
"helpDialog.item.switchContextSurface": "Cambiar superficie del panel de contexto (tecla numérica)", "helpDialog.item.switchContextSurface": "Cambiar superficie del panel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar u ocultar menú de servicios", "helpDialog.item.toggleServicesMenu": "Mostrar u ocultar menú de servicios",
"helpDialog.item.cycleServicesTab": "Cambiar pestaña de servicios",
"helpDialog.item.openSettings": "Abrir configuración", "helpDialog.item.openSettings": "Abrir configuración",
"helpDialog.keyCombiner.or": "o", "helpDialog.keyCombiner.or": "o",
"helpDialog.proTips.title": "Consejos:", "helpDialog.proTips.title": "Consejos:",
"helpDialog.proTips.commandPalette": "Usa la paleta de comandos ({shortcut}) para acceder rápidamente a todas las acciones", "helpDialog.proTips.commandPalette": "Usa la paleta de comandos ({shortcut}) para acceder rápidamente a todas las acciones",
"helpDialog.proTips.recentSessions": "Las cinco sesiones más recientes aparecen en la paleta de comandos", "helpDialog.proTips.recentSessions": "Las cinco sesiones más recientes aparecen en la paleta de comandos",
"helpDialog.proTips.themeCycling": "El ciclo de tema recuerda tu preferencia entre sesiones", "helpDialog.proTips.leaderSequences": "Atajos en dos pasos: pulsa la combinación y luego la segunda tecla; Esc cancela",
"header.actions.rightSidebarWithShortcut": "Barra lateral derecha ({shortcut})", "header.actions.rightSidebarWithShortcut": "Barra lateral derecha ({shortcut})",
"header.actions.toggleRightSidebarAria": "Mostrar u ocultar barra lateral derecha", "header.actions.toggleRightSidebarAria": "Mostrar u ocultar barra lateral derecha",
"header.actions.openAppMenu": "Menú de OpenChamber", "header.actions.openAppMenu": "Menú de OpenChamber",
@@ -2061,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "Tareas", "chat.statusRow.tasksTitle": "Tareas",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "{active} activas · {left} restantes", "chat.statusRow.summary.activeLeft": "{active} activas · {left} restantes",
"chat.statusRow.aborted": "Interrumpido",
"chat.revertIndicator.redo": "Rehacer", "chat.revertIndicator.redo": "Rehacer",
"chat.revertIndicator.redoAria": "Rehacer — restaurar mensajes revertidos", "chat.revertIndicator.redoAria": "Rehacer — restaurar mensajes revertidos",
"chat.revertPopover.title": "Revertidos", "chat.revertPopover.title": "Revertidos",
@@ -2139,7 +2139,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'No se pudo conservar la sesión btw', 'chat.btw.toast.promoteFailed': 'No se pudo conservar la sesión btw',
"chat.container.readOnlySubagentPromptBanner": "Las sesiones de subagentes no pueden recibir prompts.", "chat.container.readOnlySubagentPromptBanner": "Las sesiones de subagentes no pueden recibir prompts.",
"chat.container.sessionLoadError.title": "No se pudo cargar la sesión", "chat.container.sessionLoadError.title": "No se pudo cargar la sesión",
"chat.container.sessionLoadError.description": "Comprueba la conexión e intenta cargar esta sesión de nuevo.", "chat.container.sessionLoadError.description": "No se pudo obtener la conversación: puede que el servidor esté apagado o inaccesible. No se perdió nada; reintenta cuando vuelva.",
"chat.container.sessionLoadError.authDescription": "Tu sesión expiró, por lo que el servidor rechazó la solicitud. Inicia sesión y la conversación se cargará.",
"chat.container.sessionLoadError.retry": "Reintentar", "chat.container.sessionLoadError.retry": "Reintentar",
"sessions.sidebar.group.empty.loadingSessions": "Cargando sesiones…", "sessions.sidebar.group.empty.loadingSessions": "Cargando sesiones…",
"sessions.sidebar.group.empty.loadFailed": "No se pudieron actualizar las sesiones.", "sessions.sidebar.group.empty.loadFailed": "No se pudieron actualizar las sesiones.",
@@ -2182,10 +2183,8 @@ export const dict: Record<I18nKey, string> = {
"chat.textSelection.title.commentOnSelection": "Comentar la selección", "chat.textSelection.title.commentOnSelection": "Comentar la selección",
"chat.textSelection.comment.placeholder": "Añade un comentario opcional...", "chat.textSelection.comment.placeholder": "Añade un comentario opcional...",
"chat.textSelection.comment.attach": "Adjuntar", "chat.textSelection.comment.attach": "Adjuntar",
"chat.textSelection.actions.newSession": "Nueva sesión",
"chat.textSelection.actions.addToNotes": "Añadir a las notas", "chat.textSelection.actions.addToNotes": "Añadir a las notas",
"chat.textSelection.title.addToCurrentChat": "Añadir al chat actual", "chat.textSelection.title.addToCurrentChat": "Añadir al chat actual",
"chat.textSelection.title.newSessionWithSelection": "Crear nueva sesión con selección",
"chat.textSelection.title.saveInsightToNotes": "Guardar texto seleccionado en notas", "chat.textSelection.title.saveInsightToNotes": "Guardar texto seleccionado en notas",
"chat.messageBody.actions.revertAria": "Volver a este mensaje", "chat.messageBody.actions.revertAria": "Volver a este mensaje",
"chat.messageBody.actions.revert": "Volver desde aquí", "chat.messageBody.actions.revert": "Volver desde aquí",
@@ -2449,6 +2448,15 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.toggleSidebar": "Mostrar u ocultar barra lateral", "commandPalette.item.toggleSidebar": "Mostrar u ocultar barra lateral",
"commandPalette.item.showContextUsage": "Mostrar uso del contexto", "commandPalette.item.showContextUsage": "Mostrar uso del contexto",
"commandPalette.item.toggleTerminal": "Mostrar u ocultar terminal", "commandPalette.item.toggleTerminal": "Mostrar u ocultar terminal",
"commandPalette.item.cycleTheme": "Cambiar tema",
"commandPalette.item.showOpenCodeStatus": "Mostrar estado de OpenCode",
"commandPalette.item.toggleMemoryDebug": "Alternar panel de depuración de memoria",
"commandPalette.item.pinSession": "Anclar o desanclar sesión",
"commandPalette.item.copySessionId": "Copiar ID de sesión",
"commandPalette.item.openMultiRun": "Abrir lanzador multi-run",
"commandPalette.item.openArchive": "Abrir sesiones archivadas",
"commandPalette.item.openNotes": "Abrir panel de notas",
"commandPalette.item.openTodos": "Abrir panel de tareas",
"commandPalette.item.openSettings": "Abrir configuración...", "commandPalette.item.openSettings": "Abrir configuración...",
"commandPalette.session.untitled": "Sesión sin título", "commandPalette.session.untitled": "Sesión sin título",
"openCodeStatusDialog.title": "Estado de OpenCode", "openCodeStatusDialog.title": "Estado de OpenCode",
@@ -2663,6 +2671,9 @@ export const dict: Record<I18nKey, string> = {
"sessionAuth.error.passkeySignInCanceled": "El inicio de sesión con clave de paso se canceló.", "sessionAuth.error.passkeySignInCanceled": "El inicio de sesión con clave de paso se canceló.",
"sessionAuth.error.enterPasswordForPasskey": "Introduce tu contraseña para añadir una clave de paso.", "sessionAuth.error.enterPasswordForPasskey": "Introduce tu contraseña para añadir una clave de paso.",
"sessionAuth.locked.tunnelTitle": "Se requiere acceso por túnel", "sessionAuth.locked.tunnelTitle": "Se requiere acceso por túnel",
"sessionAuth.expired.banner": "Tu sesión expiró: inicia sesión para continuar.",
"sessionAuth.expired.loginAction": "Iniciar sesión",
"sessionAuth.expired.sendBlocked": "Sesión expirada: inicia sesión para enviar mensajes.",
"sessionAuth.locked.unlockTitle": "Desbloquear OpenChamber", "sessionAuth.locked.unlockTitle": "Desbloquear OpenChamber",
"sessionAuth.locked.tunnelDescription": "Abre este túnel usando el enlace de conexión única desde la aplicación de escritorio.", "sessionAuth.locked.tunnelDescription": "Abre este túnel usando el enlace de conexión única desde la aplicación de escritorio.",
"sessionAuth.locked.passwordDescription": "Esta sesión está protegida con contraseña.", "sessionAuth.locked.passwordDescription": "Esta sesión está protegida con contraseña.",
@@ -1019,7 +1019,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': 'Ce combo est déjà utilisé par un autre raccourci. Écraser et effacer cet autre mappage ?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': 'Ce combo est déjà utilisé par un autre raccourci. Écraser et effacer cet autre mappage ?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': 'Appuyez sur les touches...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': 'Appuyez sur les touches...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': 'Capturez d\'abord un raccourci.', 'settings.openchamber.keyboardShortcuts.error.captureFirst': 'Capturez d\'abord un raccourci.',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'Ce raccourci peut entrer en conflit avec les paramètres par défaut du navigateur. Il est toujours sauvegardé.', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'Ce raccourci peut entrer en conflit avec les paramètres par défaut du navigateur. Vous pouvez tout de même lenregistrer.',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Aller à la ligne (éditeur de fichiers)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Aller à la ligne (éditeur de fichiers)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Ouvrir la palette de commandes', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Ouvrir la palette de commandes',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Entrée de mise au point', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Entrée de mise au point',
@@ -1028,18 +1028,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal à bascule étendu', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal à bascule étendu',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Ajouter la sélection au chat', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Ajouter la sélection au chat',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Basculer la barre latérale', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Basculer la barre latérale',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Afficher/masquer le panneau de contexte', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Basculer longlet de session',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Ouvrir la surface Git', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Ouvrir la surface Fichiers',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Session précédente',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Session suivante',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Renommer la session actuelle',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Basculer lapprobation automatique',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Fermer longlet de session',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Ouvrir les raccourcis clavier', 'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Ouvrir les raccourcis clavier',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Basculer le panneau contextuel du plan',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Basculer le menu des services', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Basculer le menu des services',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Onglet Services vélo',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thème du cycle', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Thème du cycle',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent de cycle', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent de cycle',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Faire avancer le modèle favori', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Faire avancer le modèle favori',
@@ -1048,6 +1050,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Développer l\'entrée', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Développer l\'entrée',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Chronologie de la conversation ouverte', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Chronologie de la conversation ouverte',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Afficher ou masquer le navigateur de prompts', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Afficher ou masquer le navigateur de prompts',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': 'Cette séquence partage un préfixe contextuel avec {action}. Lorsque son contexte est actif, cette action est prioritaire.',
'settings.openchamber.keyboardShortcuts.category.session': 'Commandes de session',
'settings.openchamber.keyboardShortcuts.category.models': 'Modèles et agents',
'settings.openchamber.keyboardShortcuts.category.panels': 'Panneaux et outils',
'settings.openchamber.keyboardShortcuts.category.navigation': 'Navigation',
'settings.openchamber.keyboardShortcuts.category.application': 'Application',
'settings.openchamber.keyboardShortcuts.actions.edit': 'Modifier',
'settings.openchamber.keyboardShortcuts.actions.confirm': 'Confirmer',
'settings.openchamber.keyboardShortcuts.dialog.title': 'Modifier {action}',
'settings.openchamber.keyboardShortcuts.dialog.instructions': 'Appuyez sur deux combinaisons de touches au maximum, avec trois touches au plus chacune. Après la première, attendez jusqu’à 3 secondes une seconde combinaison. Utilisez Confirmer pour appliquer ou Annuler pour abandonner. Retour arrière supprime la dernière.',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': 'Première combinaison',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': 'Deuxième combinaison',
'settings.openchamber.keyboardShortcuts.dialog.recording': 'Appuyez sur les touches…',
'settings.openchamber.keyboardShortcuts.unassigned': 'Non attribué',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': 'Cela entre en conflit avec la séquence utilisée par {action}. Choisissez une autre combinaison.',
'settings.openchamber.keyboardShortcuts.error.exactConflict': 'Cette combinaison est déjà utilisée par {action}.',
'settings.openchamber.keyboardShortcuts.error.internalConflict': 'Cette combinaison entre en conflit avec un raccourci intégré qui ne peut pas être remplacé.',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': 'Ouvrir le sélecteur de projet de brouillon',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': 'Ouvrir le sélecteur de worktree de brouillon',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': 'Ouvrir les sessions récentes',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': 'Saisie vocale',
'settings.projects.sidebar.total': 'Total {count}', 'settings.projects.sidebar.total': 'Total {count}',
'settings.projects.sidebar.actions.addProject': 'Ajouter un projet', 'settings.projects.sidebar.actions.addProject': 'Ajouter un projet',
'settings.projects.page.empty.noProjects': 'Aucun projet disponible.', 'settings.projects.page.empty.noProjects': 'Aucun projet disponible.',
+22 -11
View File
@@ -959,6 +959,11 @@ export const dict = {
'contextPanel.mode.context': 'Contexte', 'contextPanel.mode.context': 'Contexte',
'contextPanel.mode.preview': 'Aperçu', 'contextPanel.mode.preview': 'Aperçu',
'contextPanel.mode.browser': 'Navigateur', 'contextPanel.mode.browser': 'Navigateur',
'contextRail.configure.open': 'Configurer les panneaux',
'contextRail.configure.dialogTitle': 'Panneaux de la barre',
'contextRail.configure.dialogDescription': 'Choisissez les panneaux affichés par la barre. Les panneaux masqués conservent leurs données et restent accessibles via la palette de commandes.',
'contextRail.configure.showAll': 'Tout afficher',
'contextRail.configure.noneWarning': 'Tous les panneaux sont masqués.',
'contextRail.aria.rail': 'Surfaces du panneau', 'contextRail.aria.rail': 'Surfaces du panneau',
'contextPanel.editorEmpty.title': 'Aucun fichier ouvert', 'contextPanel.editorEmpty.title': 'Aucun fichier ouvert',
'contextPanel.editorEmpty.description': 'Choisissez un fichier dans larborescence pour commencer.', 'contextPanel.editorEmpty.description': 'Choisissez un fichier dans larborescence pour commencer.',
@@ -1433,7 +1438,7 @@ export const dict = {
'rightSidebar.contextNotesTodo.toast.planImported': 'Forfait importé', 'rightSidebar.contextNotesTodo.toast.planImported': 'Forfait importé',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Échec de la lecture du fichier de plan', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': 'Échec de la lecture du fichier de plan',
'inlineComment.range.lines': 'Lignes {start}-{end}', 'inlineComment.range.lines': 'Lignes {start}-{end}',
'inlineComment.input.placeholder': 'Ajouter un commentaire... (Cmd+Entrée pour enregistrer)', 'inlineComment.input.placeholder': 'Ajouter un commentaire... ({shortcut} pour enregistrer)',
'inlineComment.actions.cancel': 'Annuler', 'inlineComment.actions.cancel': 'Annuler',
'inlineComment.actions.save': 'Sauvegarder', 'inlineComment.actions.save': 'Sauvegarder',
'inlineComment.actions.comment': 'Commentaire', 'inlineComment.actions.comment': 'Commentaire',
@@ -1616,22 +1621,18 @@ export const dict = {
'helpDialog.item.focusChatInput': 'Concentration sur la saisie du chat', 'helpDialog.item.focusChatInput': 'Concentration sur la saisie du chat',
'helpDialog.item.togglePromptNavigator': 'Afficher ou masquer le navigateur de prompts', 'helpDialog.item.togglePromptNavigator': 'Afficher ou masquer le navigateur de prompts',
'helpDialog.item.abortActiveRun': 'Abandonner lexécution active (double pression)', 'helpDialog.item.abortActiveRun': 'Abandonner lexécution active (double pression)',
'helpDialog.item.toggleRightSidebar': 'Afficher/masquer le panneau de contexte',
'helpDialog.item.openRightSidebarGitTab': 'Ouvrir la surface Git',
'helpDialog.item.openRightSidebarFilesTab': 'Ouvrir la surface Fichiers',
'helpDialog.item.toggleTerminalDock': 'Basculer la station d\'accueil du terminal', 'helpDialog.item.toggleTerminalDock': 'Basculer la station d\'accueil du terminal',
'helpDialog.item.toggleTerminalExpanded': 'Terminal à bascule étendu', 'helpDialog.item.toggleTerminalExpanded': 'Terminal à bascule étendu',
'helpDialog.item.togglePlanContextPanel': 'Toggle Panneau contextuel du plan',
'helpDialog.item.cycleTheme': 'Basculer le thème (clair → sombre → système)', 'helpDialog.item.cycleTheme': 'Basculer le thème (clair → sombre → système)',
'helpDialog.item.switchSessionTab': 'Basculer longlet de session',
'helpDialog.item.switchContextSurface': 'Basculer la surface du panneau contextuel (touche numérique)', 'helpDialog.item.switchContextSurface': 'Basculer la surface du panneau contextuel (touche numérique)',
'helpDialog.item.toggleServicesMenu': 'Basculer le menu des services', 'helpDialog.item.toggleServicesMenu': 'Basculer le menu des services',
'helpDialog.item.cycleServicesTab': 'Onglet Services de vélo',
'helpDialog.item.openSettings': 'Ouvrir les paramètres', 'helpDialog.item.openSettings': 'Ouvrir les paramètres',
'helpDialog.keyCombiner.or': 'ou', 'helpDialog.keyCombiner.or': 'ou',
'helpDialog.proTips.title': 'Conseils de pro :', 'helpDialog.proTips.title': 'Conseils de pro :',
'helpDialog.proTips.commandPalette': 'Utilisez la palette de commandes ({shortcut}) pour accéder rapidement à toutes les actions', 'helpDialog.proTips.commandPalette': 'Utilisez la palette de commandes ({shortcut}) pour accéder rapidement à toutes les actions',
'helpDialog.proTips.recentSessions': 'Les 5 sessions les plus récentes apparaissent dans la palette de commandes', 'helpDialog.proTips.recentSessions': 'Les 5 sessions les plus récentes apparaissent dans la palette de commandes',
'helpDialog.proTips.themeCycling': 'Le cyclisme thématique mémorise vos préférences au fil des sessions', 'helpDialog.proTips.leaderSequences': 'Raccourcis en deux temps : appuyez sur la combinaison, puis sur la seconde touche — Échap annule',
'header.actions.rightSidebarWithShortcut': 'Barre latérale droite ({shortcut})', 'header.actions.rightSidebarWithShortcut': 'Barre latérale droite ({shortcut})',
'header.actions.toggleRightSidebarAria': 'Basculer la barre latérale droite', 'header.actions.toggleRightSidebarAria': 'Basculer la barre latérale droite',
'header.actions.openAppMenu': 'Menu de OpenChamber', 'header.actions.openAppMenu': 'Menu de OpenChamber',
@@ -1825,7 +1826,6 @@ export const dict = {
'chat.statusRow.tasksTitle': 'Tâches', 'chat.statusRow.tasksTitle': 'Tâches',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} actif · {left} gauche', 'chat.statusRow.summary.activeLeft': '{active} actif · {left} gauche',
'chat.statusRow.aborted': 'Avorté',
'chat.revertIndicator.redo': 'Refaire', 'chat.revertIndicator.redo': 'Refaire',
'chat.revertIndicator.redoAria': 'Rétablir : restaurer les messages annulés', 'chat.revertIndicator.redoAria': 'Rétablir : restaurer les messages annulés',
'chat.revertPopover.title': 'Rétabli', 'chat.revertPopover.title': 'Rétabli',
@@ -1892,7 +1892,8 @@ export const dict = {
'chat.btw.toast.promoteFailed': 'Échec de la conservation de la session btw', 'chat.btw.toast.promoteFailed': 'Échec de la conservation de la session btw',
'chat.container.readOnlySubagentPromptBanner': 'Les sessions de sous-agent ne peuvent pas être invitées.', 'chat.container.readOnlySubagentPromptBanner': 'Les sessions de sous-agent ne peuvent pas être invitées.',
'chat.container.sessionLoadError.title': 'Impossible de charger la session', 'chat.container.sessionLoadError.title': 'Impossible de charger la session',
'chat.container.sessionLoadError.description': 'Vérifiez la connexion et essayez de charger à nouveau cette session.', 'chat.container.sessionLoadError.description': 'Impossible de récupérer la conversation — le serveur est peut-être hors ligne ou injoignable. Rien n\'est perdu ; réessayez quand il sera de retour.',
'chat.container.sessionLoadError.authDescription': 'Votre session a expiré, le serveur a donc refusé la requête. Connectez-vous et la conversation se chargera.',
'chat.container.sessionLoadError.retry': 'Réessayer', 'chat.container.sessionLoadError.retry': 'Réessayer',
'sessions.sidebar.group.empty.loadingSessions': 'Chargement des sessions…', 'sessions.sidebar.group.empty.loadingSessions': 'Chargement des sessions…',
'sessions.sidebar.group.empty.loadFailed': 'Impossible dactualiser les sessions.', 'sessions.sidebar.group.empty.loadFailed': 'Impossible dactualiser les sessions.',
@@ -1931,10 +1932,8 @@ export const dict = {
'chat.textSelection.title.commentOnSelection': 'Commenter la sélection', 'chat.textSelection.title.commentOnSelection': 'Commenter la sélection',
'chat.textSelection.comment.placeholder': 'Ajouter un commentaire facultatif...', 'chat.textSelection.comment.placeholder': 'Ajouter un commentaire facultatif...',
'chat.textSelection.comment.attach': 'Joindre', 'chat.textSelection.comment.attach': 'Joindre',
'chat.textSelection.actions.newSession': 'Nouvelle session',
'chat.textSelection.actions.addToNotes': 'Ajouter aux notes', 'chat.textSelection.actions.addToNotes': 'Ajouter aux notes',
'chat.textSelection.title.addToCurrentChat': 'Ajouter au chat actuel', 'chat.textSelection.title.addToCurrentChat': 'Ajouter au chat actuel',
'chat.textSelection.title.newSessionWithSelection': 'Créer une nouvelle session avec sélection',
'chat.textSelection.title.saveInsightToNotes': 'Enregistrer le texte sélectionné dans les notes', 'chat.textSelection.title.saveInsightToNotes': 'Enregistrer le texte sélectionné dans les notes',
'chat.messageBody.actions.revertAria': 'Revenir à ce message', 'chat.messageBody.actions.revertAria': 'Revenir à ce message',
'chat.messageBody.actions.revert': 'Revenir à partir d\'ici', 'chat.messageBody.actions.revert': 'Revenir à partir d\'ici',
@@ -2187,6 +2186,15 @@ export const dict = {
'commandPalette.item.toggleSidebar': 'Basculer la barre latérale', 'commandPalette.item.toggleSidebar': 'Basculer la barre latérale',
'commandPalette.item.showContextUsage': 'Afficher l\'utilisation du contexte', 'commandPalette.item.showContextUsage': 'Afficher l\'utilisation du contexte',
'commandPalette.item.toggleTerminal': 'Basculer le terminal', 'commandPalette.item.toggleTerminal': 'Basculer le terminal',
'commandPalette.item.cycleTheme': 'Changer de thème',
'commandPalette.item.showOpenCodeStatus': 'Afficher le statut OpenCode',
'commandPalette.item.toggleMemoryDebug': 'Basculer le panneau de débogage mémoire',
'commandPalette.item.pinSession': 'Épingler ou désépingler la session',
'commandPalette.item.copySessionId': 'Copier l\'ID de session',
'commandPalette.item.openMultiRun': 'Ouvrir le lanceur multi-run',
'commandPalette.item.openArchive': 'Ouvrir les sessions archivées',
'commandPalette.item.openNotes': 'Ouvrir le panneau de notes',
'commandPalette.item.openTodos': 'Ouvrir le panneau de tâches',
'commandPalette.item.openSettings': 'Ouvrez les paramètres...', 'commandPalette.item.openSettings': 'Ouvrez les paramètres...',
'commandPalette.session.untitled': 'Session sans titre', 'commandPalette.session.untitled': 'Session sans titre',
'openCodeStatusDialog.title': 'Statut OpenCode', 'openCodeStatusDialog.title': 'Statut OpenCode',
@@ -2401,6 +2409,9 @@ export const dict = {
'sessionAuth.error.passkeySignInCanceled': 'La connexion par mot de passe a été annulée.', 'sessionAuth.error.passkeySignInCanceled': 'La connexion par mot de passe a été annulée.',
'sessionAuth.error.enterPasswordForPasskey': 'Entrez votre mot de passe pour ajouter un mot de passe.', 'sessionAuth.error.enterPasswordForPasskey': 'Entrez votre mot de passe pour ajouter un mot de passe.',
'sessionAuth.locked.tunnelTitle': 'Accès au tunnel requis', 'sessionAuth.locked.tunnelTitle': 'Accès au tunnel requis',
'sessionAuth.expired.banner': 'Votre session a expiré — connectez-vous pour continuer.',
'sessionAuth.expired.loginAction': 'Se connecter',
'sessionAuth.expired.sendBlocked': 'Session expirée — connectez-vous pour envoyer des messages.',
'sessionAuth.locked.unlockTitle': 'Débloquez OpenChamber', 'sessionAuth.locked.unlockTitle': 'Débloquez OpenChamber',
'sessionAuth.locked.tunnelDescription': 'Ouvrez ce tunnel à l\'aide du lien de connexion unique depuis l\'application de bureau.', 'sessionAuth.locked.tunnelDescription': 'Ouvrez ce tunnel à l\'aide du lien de connexion unique depuis l\'application de bureau.',
'sessionAuth.locked.passwordDescription': 'Cette session est protégée par mot de passe.', 'sessionAuth.locked.passwordDescription': 'Cette session est protégée par mot de passe.',
@@ -1134,7 +1134,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': 'このキーコンボは別のショートカットで既に使用されています。上書きしてそのマッピングをクリアしますか?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': 'このキーコンボは別のショートカットで既に使用されています。上書きしてそのマッピングをクリアしますか?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': 'キーを押してください...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': 'キーを押してください...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': '最初にショートカットを設定してください。', 'settings.openchamber.keyboardShortcuts.error.captureFirst': '最初にショートカットを設定してください。',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'このショートカットはブラウザのデフォルトと競合する可能性があります。それでも保存されます。', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'このショートカットはブラウザのデフォルトと競合する可能性がありますが、そのまま保存できます。',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '指定行に移動(ファイルエディター)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '指定行に移動(ファイルエディター)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'コマンドパレットを開く', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'コマンドパレットを開く',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '入力をフォーカス', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': '入力をフォーカス',
@@ -1143,18 +1143,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'ターミナル拡大の切替', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'ターミナル拡大の切替',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '選択範囲をチャットに追加', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '選択範囲をチャットに追加',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'サイドバーの切替', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'サイドバーの切替',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'コンテキストパネルの表示切替', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'セッションタブを切り替え',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git サーフェスを開く', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'ファイルサーフェスを開く',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '前のセッション',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '次のセッション',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '現在のセッション名を変更',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '権限の自動承認を切り替え',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'セッションタブを閉じる',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'キーボードショートカットを開く', 'settings.openchamber.keyboardShortcuts.action.open_help.label': 'キーボードショートカットを開く',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '計画コンテキストパネルの切替',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'サービスメニューの切替', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'サービスメニューの切替',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'サービスタブを順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'テーマを順に切替', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'テーマを順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent を順に切替', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Agent を順に切替',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'お気に入りモデルを次へ', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'お気に入りモデルを次へ',
@@ -1163,6 +1165,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.open_model_selector.label': 'モデルセレクターを開く', 'settings.openchamber.keyboardShortcuts.action.open_model_selector.label': 'モデルセレクターを開く',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '会話タイムラインを開く', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '会話タイムラインを開く',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'プロンプトナビゲーターの表示切替', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'プロンプトナビゲーターの表示切替',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': 'このシーケンスは {action} とコンテキスト依存のプレフィックスを共有しています。そのコンテキストが有効な間は、この操作が優先されます。',
'settings.openchamber.keyboardShortcuts.category.session': 'セッション操作',
'settings.openchamber.keyboardShortcuts.category.models': 'モデルとエージェント',
'settings.openchamber.keyboardShortcuts.category.panels': 'パネルとツール',
'settings.openchamber.keyboardShortcuts.category.navigation': 'ナビゲーション',
'settings.openchamber.keyboardShortcuts.category.application': 'アプリケーション',
'settings.openchamber.keyboardShortcuts.actions.edit': '編集',
'settings.openchamber.keyboardShortcuts.actions.confirm': '確認',
'settings.openchamber.keyboardShortcuts.dialog.title': '{action} を編集',
'settings.openchamber.keyboardShortcuts.dialog.instructions': 'キーの組み合わせを最大2つ入力でき、各組み合わせは最大3キーです。最初の組み合わせの後、2つ目の組み合わせを最大3秒待ちます。適用するには確認、破棄するにはキャンセルを選択してください。Backspace で最後の組み合わせを削除します。',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': '最初の組み合わせ',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': '2番目の組み合わせ',
'settings.openchamber.keyboardShortcuts.dialog.recording': 'キーを押してください…',
'settings.openchamber.keyboardShortcuts.unassigned': '未割り当て',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': '{action} のシーケンスと競合しています。別の組み合わせを選択してください。',
'settings.openchamber.keyboardShortcuts.error.exactConflict': 'この組み合わせは {action} で使用されています。',
'settings.openchamber.keyboardShortcuts.error.internalConflict': 'この組み合わせは組み込みショートカットと競合しています。組み込みショートカットは置き換えられません。',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': '下書きプロジェクト選択を開く',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': '下書きワークツリー選択を開く',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': '最近のセッションを開く',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': '音声入力',
'settings.projects.sidebar.total': '合計 {count}', 'settings.projects.sidebar.total': '合計 {count}',
'settings.projects.sidebar.actions.addProject': 'プロジェクトを追加', 'settings.projects.sidebar.actions.addProject': 'プロジェクトを追加',
'settings.projects.page.empty.noProjects': '利用可能なプロジェクトがありません。', 'settings.projects.page.empty.noProjects': '利用可能なプロジェクトがありません。',
+22 -11
View File
@@ -1136,6 +1136,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': 'コンテキスト', 'contextPanel.mode.context': 'コンテキスト',
'contextPanel.mode.preview': 'プレビュー', 'contextPanel.mode.preview': 'プレビュー',
'contextPanel.mode.browser': 'ブラウザ', 'contextPanel.mode.browser': 'ブラウザ',
'contextRail.configure.open': 'パネルを設定',
'contextRail.configure.dialogTitle': 'レールのパネル',
'contextRail.configure.dialogDescription': 'レールに表示するパネルを選択します。非表示のパネルもデータは保持され、コマンドパレットから引き続き開けます。',
'contextRail.configure.showAll': 'すべて表示',
'contextRail.configure.noneWarning': 'すべてのパネルが非表示です。',
'contextRail.aria.rail': 'パネルサーフェス', 'contextRail.aria.rail': 'パネルサーフェス',
'contextPanel.editorEmpty.title': 'ファイルが開かれていません', 'contextPanel.editorEmpty.title': 'ファイルが開かれていません',
'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。', 'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。',
@@ -1664,7 +1669,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '計画をインポートしました', 'rightSidebar.contextNotesTodo.toast.planImported': '計画をインポートしました',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '計画ファイルの読み込みに失敗しました', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '計画ファイルの読み込みに失敗しました',
'inlineComment.range.lines': '{start}行目~{end}行目', 'inlineComment.range.lines': '{start}行目~{end}行目',
'inlineComment.input.placeholder': 'コメントを追加...Cmd+Enterで保存)', 'inlineComment.input.placeholder': 'コメントを追加...{shortcut}で保存)',
'inlineComment.input.placeholderShort': 'コメントを追加...', 'inlineComment.input.placeholderShort': 'コメントを追加...',
'inlineComment.actions.cancel': 'キャンセル', 'inlineComment.actions.cancel': 'キャンセル',
'inlineComment.actions.save': '保存', 'inlineComment.actions.save': '保存',
@@ -1854,22 +1859,18 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.focusChatInput': 'チャット入力にフォーカス', 'helpDialog.item.focusChatInput': 'チャット入力にフォーカス',
'helpDialog.item.togglePromptNavigator': 'プロンプトナビゲーターの表示切替', 'helpDialog.item.togglePromptNavigator': 'プロンプトナビゲーターの表示切替',
'helpDialog.item.abortActiveRun': 'アクティブな実行を中止(ダブルプレス)', 'helpDialog.item.abortActiveRun': 'アクティブな実行を中止(ダブルプレス)',
'helpDialog.item.toggleRightSidebar': 'コンテキストパネルの表示切替',
'helpDialog.item.openRightSidebarGitTab': 'Git サーフェスを開く',
'helpDialog.item.openRightSidebarFilesTab': 'ファイルサーフェスを開く',
'helpDialog.item.toggleTerminalDock': 'ターミナルドックの切り替え', 'helpDialog.item.toggleTerminalDock': 'ターミナルドックの切り替え',
'helpDialog.item.toggleTerminalExpanded': 'ターミナル展開の切り替え', 'helpDialog.item.toggleTerminalExpanded': 'ターミナル展開の切り替え',
'helpDialog.item.togglePlanContextPanel': '計画コンテキストパネルの切り替え',
'helpDialog.item.cycleTheme': 'テーマ切り替え(ライト→ダーク→システム)', 'helpDialog.item.cycleTheme': 'テーマ切り替え(ライト→ダーク→システム)',
'helpDialog.item.switchSessionTab': 'セッションタブを切り替え',
'helpDialog.item.switchContextSurface': 'コンテキストパネルのサーフェスを切り替え(数字キー)', 'helpDialog.item.switchContextSurface': 'コンテキストパネルのサーフェスを切り替え(数字キー)',
'helpDialog.item.toggleServicesMenu': 'サービスの切り替え', 'helpDialog.item.toggleServicesMenu': 'サービスの切り替え',
'helpDialog.item.cycleServicesTab': 'サービス変数の切り替え',
'helpDialog.item.openSettings': '設定を開く', 'helpDialog.item.openSettings': '設定を開く',
'helpDialog.keyCombiner.or': 'または', 'helpDialog.keyCombiner.or': 'または',
'helpDialog.proTips.title': 'プロのヒント:', 'helpDialog.proTips.title': 'プロのヒント:',
'helpDialog.proTips.commandPalette': 'コマンドパレット({shortcut})を使うとすべての操作にすばやくアクセスできます', 'helpDialog.proTips.commandPalette': 'コマンドパレット({shortcut})を使うとすべての操作にすばやくアクセスできます',
'helpDialog.proTips.recentSessions': '最近の5つのセッションがコマンドパレットに表示されます', 'helpDialog.proTips.recentSessions': '最近の5つのセッションがコマンドパレットに表示されます',
'helpDialog.proTips.themeCycling': 'テーマの切り替えはセッション間で設定が記憶されます', 'helpDialog.proTips.leaderSequences': '2段階ショートカット:組み合わせを押してから2つ目のキーを押します(Escで取消)',
'header.actions.rightSidebarWithShortcut': '右サイドバー({shortcut}', 'header.actions.rightSidebarWithShortcut': '右サイドバー({shortcut}',
'header.actions.toggleRightSidebarAria': '右サイドバーの切り替え', 'header.actions.toggleRightSidebarAria': '右サイドバーの切り替え',
'header.actions.openAppMenu': 'OpenChamberメニュー', 'header.actions.openAppMenu': 'OpenChamberメニュー',
@@ -2079,7 +2080,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': 'タスク', 'chat.statusRow.tasksTitle': 'タスク',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active}アクティブ · {left}残り', 'chat.statusRow.summary.activeLeft': '{active}アクティブ · {left}残り',
'chat.statusRow.aborted': '中止されました',
'chat.revertIndicator.redo': 'やり直し', 'chat.revertIndicator.redo': 'やり直し',
'chat.revertIndicator.redoAria': 'やり直し — 元に戻したメッセージを復元', 'chat.revertIndicator.redoAria': 'やり直し — 元に戻したメッセージを復元',
'chat.revertPopover.title': '元に戻しました', 'chat.revertPopover.title': '元に戻しました',
@@ -2157,7 +2157,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'btwセッションを保持できませんでした', 'chat.btw.toast.promoteFailed': 'btwセッションを保持できませんでした',
'chat.container.readOnlySubagentPromptBanner': 'サブエージェントセッションはプロンプトを受け付けません。', 'chat.container.readOnlySubagentPromptBanner': 'サブエージェントセッションはプロンプトを受け付けません。',
'chat.container.sessionLoadError.title': 'セッションを読み込めませんでした', 'chat.container.sessionLoadError.title': 'セッションを読み込めませんでした',
'chat.container.sessionLoadError.description': '接続を確認して、このセッションをもう一度読み込んでください。', 'chat.container.sessionLoadError.description': '会話を取得できませんでした。サーバーが停止中か到達できない可能性があります。データは失われていません。復旧後に再試行してください。',
'chat.container.sessionLoadError.authDescription': 'セッションの有効期限が切れたため、サーバーがリクエストを拒否しました。ログインすると会話が読み込まれます。',
'chat.container.sessionLoadError.retry': '再試行', 'chat.container.sessionLoadError.retry': '再試行',
'sessions.sidebar.group.empty.loadingSessions': 'セッションを読み込んでいます…', 'sessions.sidebar.group.empty.loadingSessions': 'セッションを読み込んでいます…',
'sessions.sidebar.group.empty.loadFailed': 'セッションを更新できませんでした。', 'sessions.sidebar.group.empty.loadFailed': 'セッションを更新できませんでした。',
@@ -2200,10 +2201,8 @@ export const dict: Record<I18nKey, string> = {
'chat.textSelection.title.commentOnSelection': '選択範囲にコメント', 'chat.textSelection.title.commentOnSelection': '選択範囲にコメント',
'chat.textSelection.comment.placeholder': '任意のコメントを追加...', 'chat.textSelection.comment.placeholder': '任意のコメントを追加...',
'chat.textSelection.comment.attach': '添付', 'chat.textSelection.comment.attach': '添付',
'chat.textSelection.actions.newSession': '新しいセッション',
'chat.textSelection.actions.addToNotes': 'メモに追加', 'chat.textSelection.actions.addToNotes': 'メモに追加',
'chat.textSelection.title.addToCurrentChat': '現在のチャットに追加', 'chat.textSelection.title.addToCurrentChat': '現在のチャットに追加',
'chat.textSelection.title.newSessionWithSelection': '選択範囲で新しいセッションを作成',
'chat.textSelection.title.saveInsightToNotes': '選択テキストをメモに保存', 'chat.textSelection.title.saveInsightToNotes': '選択テキストをメモに保存',
'chat.messageBody.actions.revertAria': 'このメッセージに戻す', 'chat.messageBody.actions.revertAria': 'このメッセージに戻す',
'chat.messageBody.actions.revert': 'ここから元に戻す', 'chat.messageBody.actions.revert': 'ここから元に戻す',
@@ -2482,6 +2481,15 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.toggleSidebar': 'サイドバーの切り替え', 'commandPalette.item.toggleSidebar': 'サイドバーの切り替え',
'commandPalette.item.showContextUsage': 'コンテキスト使用量を表示', 'commandPalette.item.showContextUsage': 'コンテキスト使用量を表示',
'commandPalette.item.toggleTerminal': 'ターミナルの切り替え', 'commandPalette.item.toggleTerminal': 'ターミナルの切り替え',
'commandPalette.item.cycleTheme': 'テーマを順に切替',
'commandPalette.item.showOpenCodeStatus': 'OpenCode のステータスを表示',
'commandPalette.item.toggleMemoryDebug': 'メモリデバッグパネルの切替',
'commandPalette.item.pinSession': 'セッションをピン留め/解除',
'commandPalette.item.copySessionId': 'セッションIDをコピー',
'commandPalette.item.openMultiRun': 'マルチラン起動画面を開く',
'commandPalette.item.openArchive': 'アーカイブ済みセッションを開く',
'commandPalette.item.openNotes': 'ノートパネルを開く',
'commandPalette.item.openTodos': 'ToDoパネルを開く',
'commandPalette.item.openSettings': '設定を開く...', 'commandPalette.item.openSettings': '設定を開く...',
'commandPalette.session.untitled': '無題のセッション', 'commandPalette.session.untitled': '無題のセッション',
'openCodeStatusDialog.title': 'OpenCodeステータス', 'openCodeStatusDialog.title': 'OpenCodeステータス',
@@ -2696,6 +2704,9 @@ export const dict: Record<I18nKey, string> = {
'sessionAuth.error.passkeySignInCanceled': 'パスキーサインインがキャンセルされました。', 'sessionAuth.error.passkeySignInCanceled': 'パスキーサインインがキャンセルされました。',
'sessionAuth.error.enterPasswordForPasskey': 'パスキーを追加するためにパスワードを入力してください。', 'sessionAuth.error.enterPasswordForPasskey': 'パスキーを追加するためにパスワードを入力してください。',
'sessionAuth.locked.tunnelTitle': 'トンネルアクセスが必要', 'sessionAuth.locked.tunnelTitle': 'トンネルアクセスが必要',
'sessionAuth.expired.banner': 'セッションの有効期限が切れました。続行するにはログインしてください。',
'sessionAuth.expired.loginAction': 'ログイン',
'sessionAuth.expired.sendBlocked': 'セッションが切れています。メッセージを送るにはログインしてください。',
'sessionAuth.locked.unlockTitle': 'OpenChamberのロックを解除', 'sessionAuth.locked.unlockTitle': 'OpenChamberのロックを解除',
'sessionAuth.locked.tunnelDescription': 'デスクトップアプリのワンタイム接続リンクを使用してこのトンネルを開きます。', 'sessionAuth.locked.tunnelDescription': 'デスクトップアプリのワンタイム接続リンクを使用してこのトンネルを開きます。',
'sessionAuth.locked.passwordDescription': 'このセッションはパスワードで保護されています。', 'sessionAuth.locked.passwordDescription': 'このセッションはパスワードで保護されています。',
@@ -1101,7 +1101,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': '이 조합은 이미 다른 단축키에서 사용 중입니다. 덮어쓰고 기존 매핑을 지울까요?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': '이 조합은 이미 다른 단축키에서 사용 중입니다. 덮어쓰고 기존 매핑을 지울까요?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': '키를 누르세요...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': '키를 누르세요...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': '먼저 단축키를 입력하세요.', 'settings.openchamber.keyboardShortcuts.error.captureFirst': '먼저 단축키를 입력하세요.',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '이 단축키는 브라우저 기본값과 충돌할 수 있습니다. 그래도 저장니다.', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '이 단축키는 브라우저 기본값과 충돌할 수 있지만 그래도 저장할 수 있습니다.',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '줄로 이동(파일 편집기)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '줄로 이동(파일 편집기)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '명령 팔레트 열기', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '명령 팔레트 열기',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '입력에 포커스', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': '입력에 포커스',
@@ -1110,18 +1110,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '터미널 확장 토글', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '터미널 확장 토글',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '선택 내용을 채팅에 추가', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '선택 내용을 채팅에 추가',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '사이드바 토글', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '사이드바 토글',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '컨텍스트 패널 표시 전환', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '세션 탭 전환',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git 서피스 열기', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '파일 서피스 열기',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '이전 세션',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '다음 세션',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '현재 세션 이름 바꾸기',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '권한 자동 승인 전환',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '세션 탭 닫기',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '키보드 단축키 열기', 'settings.openchamber.keyboardShortcuts.action.open_help.label': '키보드 단축키 열기',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '계획 컨텍스트 패널 토글',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '서비스 메뉴 토글', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '서비스 메뉴 토글',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '서비스 탭 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '테마 순환', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '테마 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '에이전트 순환', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '에이전트 순환',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '즐겨찾기 모델 앞으로 순환', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '즐겨찾기 모델 앞으로 순환',
@@ -1130,6 +1132,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '입력 확장', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': '입력 확장',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '대화 타임라인 열기', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '대화 타임라인 열기',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '프롬프트 탐색기 표시/숨기기', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '프롬프트 탐색기 표시/숨기기',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': '이 시퀀스는 {action}과 컨텍스트 접두사를 공유합니다. 해당 컨텍스트가 활성화된 동안에는 그 동작이 우선합니다.',
'settings.openchamber.keyboardShortcuts.category.session': '세션 제어',
'settings.openchamber.keyboardShortcuts.category.models': '모델 및 에이전트',
'settings.openchamber.keyboardShortcuts.category.panels': '패널 및 도구',
'settings.openchamber.keyboardShortcuts.category.navigation': '탐색',
'settings.openchamber.keyboardShortcuts.category.application': '애플리케이션',
'settings.openchamber.keyboardShortcuts.actions.edit': '편집',
'settings.openchamber.keyboardShortcuts.actions.confirm': '확인',
'settings.openchamber.keyboardShortcuts.dialog.title': '{action} 편집',
'settings.openchamber.keyboardShortcuts.dialog.instructions': '키 조합을 최대 두 개까지 누르세요. 각 조합에는 최대 세 개의 키를 사용할 수 있습니다. 첫 번째 조합 뒤에는 두 번째 조합을 위해 최대 3초 동안 기다립니다. 적용하려면 확인을, 취소하려면 취소를 선택하세요. Backspace로 마지막 조합을 삭제합니다.',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': '첫 번째 조합',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': '두 번째 조합',
'settings.openchamber.keyboardShortcuts.dialog.recording': '키를 누르세요…',
'settings.openchamber.keyboardShortcuts.unassigned': '할당되지 않음',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': '{action}에서 사용하는 시퀀스와 충돌합니다. 다른 조합을 선택하세요.',
'settings.openchamber.keyboardShortcuts.error.exactConflict': '이 조합은 이미 {action}에서 사용합니다.',
'settings.openchamber.keyboardShortcuts.error.internalConflict': '이 조합은 바꿀 수 없는 기본 제공 단축키와 충돌합니다.',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': '초안 프로젝트 선택기 열기',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': '초안 워크트리 선택기 열기',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': '최근 세션 열기',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': '음성 입력',
'settings.projects.sidebar.total': '총 {count}개', 'settings.projects.sidebar.total': '총 {count}개',
'settings.projects.sidebar.actions.addProject': '프로젝트 추가', 'settings.projects.sidebar.actions.addProject': '프로젝트 추가',
'settings.projects.page.empty.noProjects': '사용 가능한 프로젝트가 없습니다.', 'settings.projects.page.empty.noProjects': '사용 가능한 프로젝트가 없습니다.',
+22 -11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '컨텍스트', 'contextPanel.mode.context': '컨텍스트',
'contextPanel.mode.preview': '미리보기', 'contextPanel.mode.preview': '미리보기',
'contextPanel.mode.browser': '브라우저', 'contextPanel.mode.browser': '브라우저',
'contextRail.configure.open': '패널 구성',
'contextRail.configure.dialogTitle': '레일 패널',
'contextRail.configure.dialogDescription': '레일에 표시할 패널을 선택하세요. 숨긴 패널의 데이터는 유지되며 명령 팔레트에서 계속 열 수 있습니다.',
'contextRail.configure.showAll': '모두 표시',
'contextRail.configure.noneWarning': '모든 패널이 숨겨져 있습니다.',
'contextRail.aria.rail': '패널 서피스', 'contextRail.aria.rail': '패널 서피스',
'contextPanel.editorEmpty.title': '열린 파일 없음', 'contextPanel.editorEmpty.title': '열린 파일 없음',
'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.', 'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.',
@@ -1670,7 +1675,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '플랜 가져옴', 'rightSidebar.contextNotesTodo.toast.planImported': '플랜 가져옴',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '플랜 파일 읽기 실패', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '플랜 파일 읽기 실패',
'inlineComment.range.lines': '줄 {start}-{end}', 'inlineComment.range.lines': '줄 {start}-{end}',
'inlineComment.input.placeholder': '댓글 추가… (Cmd+Enter로 저장)', 'inlineComment.input.placeholder': '댓글 추가… ({shortcut}로 저장)',
'inlineComment.input.placeholderShort': '댓글 추가…', 'inlineComment.input.placeholderShort': '댓글 추가…',
'inlineComment.actions.cancel': '취소', 'inlineComment.actions.cancel': '취소',
'inlineComment.actions.save': '저장', 'inlineComment.actions.save': '저장',
@@ -1860,22 +1865,18 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.focusChatInput': '채팅 입력창으로 포커스 이동', 'helpDialog.item.focusChatInput': '채팅 입력창으로 포커스 이동',
'helpDialog.item.togglePromptNavigator': '프롬프트 탐색기 표시/숨기기', 'helpDialog.item.togglePromptNavigator': '프롬프트 탐색기 표시/숨기기',
'helpDialog.item.abortActiveRun': '활성 실행 중단(두 번 누르기)', 'helpDialog.item.abortActiveRun': '활성 실행 중단(두 번 누르기)',
'helpDialog.item.toggleRightSidebar': '컨텍스트 패널 표시 전환',
'helpDialog.item.openRightSidebarGitTab': 'Git 서피스 열기',
'helpDialog.item.openRightSidebarFilesTab': '파일 서피스 열기',
'helpDialog.item.toggleTerminalDock': '터미널 독 전환', 'helpDialog.item.toggleTerminalDock': '터미널 독 전환',
'helpDialog.item.toggleTerminalExpanded': '터미널 펼치기/접기', 'helpDialog.item.toggleTerminalExpanded': '터미널 펼치기/접기',
'helpDialog.item.togglePlanContextPanel': '플랜 컨텍스트 패널 전환',
'helpDialog.item.cycleTheme': '테마 순환(라이트 → 다크 → 시스템)', 'helpDialog.item.cycleTheme': '테마 순환(라이트 → 다크 → 시스템)',
'helpDialog.item.switchSessionTab': '세션 탭 전환',
'helpDialog.item.switchContextSurface': '컨텍스트 패널 서피스 전환(숫자 키)', 'helpDialog.item.switchContextSurface': '컨텍스트 패널 서피스 전환(숫자 키)',
'helpDialog.item.toggleServicesMenu': '서비스 메뉴 전환', 'helpDialog.item.toggleServicesMenu': '서비스 메뉴 전환',
'helpDialog.item.cycleServicesTab': '서비스 탭 순환',
'helpDialog.item.openSettings': '설정 열기', 'helpDialog.item.openSettings': '설정 열기',
'helpDialog.keyCombiner.or': '또는', 'helpDialog.keyCombiner.or': '또는',
'helpDialog.proTips.title': '팁:', 'helpDialog.proTips.title': '팁:',
'helpDialog.proTips.commandPalette': '명령 팔레트({shortcut})로 모든 작업에 빠르게 접근하세요', 'helpDialog.proTips.commandPalette': '명령 팔레트({shortcut})로 모든 작업에 빠르게 접근하세요',
'helpDialog.proTips.recentSessions': '최근 세션 5개가 명령 팔레트에 표시됩니다', 'helpDialog.proTips.recentSessions': '최근 세션 5개가 명령 팔레트에 표시됩니다',
'helpDialog.proTips.themeCycling': '테마 순환은 세션 간에도 선호 설정을 기억합니다', 'helpDialog.proTips.leaderSequences': '2단계 단축키: 조합을 누른 뒤 두 번째 키를 누르세요 (Esc로 취소)',
'header.actions.rightSidebarWithShortcut': '오른쪽 사이드바 ({shortcut})', 'header.actions.rightSidebarWithShortcut': '오른쪽 사이드바 ({shortcut})',
'header.actions.toggleRightSidebarAria': '오른쪽 사이드바 토글', 'header.actions.toggleRightSidebarAria': '오른쪽 사이드바 토글',
'header.actions.openAppMenu': 'OpenChamber 메뉴', 'header.actions.openAppMenu': 'OpenChamber 메뉴',
@@ -2085,7 +2086,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '작업', 'chat.statusRow.tasksTitle': '작업',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active}개 활성 · {left}개 남음', 'chat.statusRow.summary.activeLeft': '{active}개 활성 · {left}개 남음',
'chat.statusRow.aborted': '중단됨',
'chat.revertIndicator.redo': '다시 실행', 'chat.revertIndicator.redo': '다시 실행',
'chat.revertIndicator.redoAria': '다시 실행 — 되돌린 메시지 복원', 'chat.revertIndicator.redoAria': '다시 실행 — 되돌린 메시지 복원',
'chat.revertPopover.title': '되돌림', 'chat.revertPopover.title': '되돌림',
@@ -2163,7 +2163,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'btw 세션을 유지하지 못했습니다', 'chat.btw.toast.promoteFailed': 'btw 세션을 유지하지 못했습니다',
'chat.container.readOnlySubagentPromptBanner': '하위 에이전트 세션에는 프롬프트를 보낼 수 없습니다.', 'chat.container.readOnlySubagentPromptBanner': '하위 에이전트 세션에는 프롬프트를 보낼 수 없습니다.',
'chat.container.sessionLoadError.title': '세션을 불러올 수 없습니다', 'chat.container.sessionLoadError.title': '세션을 불러올 수 없습니다',
'chat.container.sessionLoadError.description': '연결을 확인한 후 이 세션을 다시 불러오세요.', 'chat.container.sessionLoadError.description': '대화를 가져오지 못했습니다. 서버가 꺼져 있거나 연결할 수 없는 상태일 수 있습니다. 데이터는 사라지지 않았으니 복구되면 다시 시도하세요.',
'chat.container.sessionLoadError.authDescription': '세션이 만료되어 서버가 요청을 거부했습니다. 로그인하면 대화가 로드됩니다.',
'chat.container.sessionLoadError.retry': '다시 시도', 'chat.container.sessionLoadError.retry': '다시 시도',
'sessions.sidebar.group.empty.loadingSessions': '세션을 불러오는 중…', 'sessions.sidebar.group.empty.loadingSessions': '세션을 불러오는 중…',
'sessions.sidebar.group.empty.loadFailed': '세션을 새로 고칠 수 없습니다.', 'sessions.sidebar.group.empty.loadFailed': '세션을 새로 고칠 수 없습니다.',
@@ -2206,10 +2207,8 @@ export const dict: Record<I18nKey, string> = {
'chat.textSelection.title.commentOnSelection': '선택 영역에 댓글 달기', 'chat.textSelection.title.commentOnSelection': '선택 영역에 댓글 달기',
'chat.textSelection.comment.placeholder': '선택적 댓글 추가...', 'chat.textSelection.comment.placeholder': '선택적 댓글 추가...',
'chat.textSelection.comment.attach': '첨부', 'chat.textSelection.comment.attach': '첨부',
'chat.textSelection.actions.newSession': '새 세션',
'chat.textSelection.actions.addToNotes': '메모에 추가', 'chat.textSelection.actions.addToNotes': '메모에 추가',
'chat.textSelection.title.addToCurrentChat': '현재 채팅에 추가', 'chat.textSelection.title.addToCurrentChat': '현재 채팅에 추가',
'chat.textSelection.title.newSessionWithSelection': '선택한 내용으로 새 세션 생성',
'chat.textSelection.title.saveInsightToNotes': '선택한 텍스트를 메모에 저장', 'chat.textSelection.title.saveInsightToNotes': '선택한 텍스트를 메모에 저장',
'chat.messageBody.actions.revertAria': '이 메시지로 되돌리기', 'chat.messageBody.actions.revertAria': '이 메시지로 되돌리기',
'chat.messageBody.actions.revert': '여기부터 되돌리기', 'chat.messageBody.actions.revert': '여기부터 되돌리기',
@@ -2483,6 +2482,15 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.toggleSidebar': '토글 사이드바', 'commandPalette.item.toggleSidebar': '토글 사이드바',
'commandPalette.item.showContextUsage': '컨텍스트 사용량 표시', 'commandPalette.item.showContextUsage': '컨텍스트 사용량 표시',
'commandPalette.item.toggleTerminal': '토글 터미널', 'commandPalette.item.toggleTerminal': '토글 터미널',
'commandPalette.item.cycleTheme': '테마 순환',
'commandPalette.item.showOpenCodeStatus': 'OpenCode 상태 표시',
'commandPalette.item.toggleMemoryDebug': '메모리 디버그 패널 토글',
'commandPalette.item.pinSession': '세션 고정 또는 고정 해제',
'commandPalette.item.copySessionId': '세션 ID 복사',
'commandPalette.item.openMultiRun': '멀티 런 런처 열기',
'commandPalette.item.openArchive': '보관된 세션 열기',
'commandPalette.item.openNotes': '노트 패널 열기',
'commandPalette.item.openTodos': '할 일 패널 열기',
'commandPalette.item.openSettings': '설정... 열기', 'commandPalette.item.openSettings': '설정... 열기',
'commandPalette.session.untitled': '제목 없는 세션', 'commandPalette.session.untitled': '제목 없는 세션',
'openCodeStatusDialog.title': 'OpenCode 상태', 'openCodeStatusDialog.title': 'OpenCode 상태',
@@ -2697,6 +2705,9 @@ export const dict: Record<I18nKey, string> = {
'sessionAuth.error.passkeySignInCanceled': '패스키 로그인이 취소되었습니다.', 'sessionAuth.error.passkeySignInCanceled': '패스키 로그인이 취소되었습니다.',
'sessionAuth.error.enterPasswordForPasskey': '패스키를 추가하려면 비밀번호를 입력하세요.', 'sessionAuth.error.enterPasswordForPasskey': '패스키를 추가하려면 비밀번호를 입력하세요.',
'sessionAuth.locked.tunnelTitle': '터널 접근 필요', 'sessionAuth.locked.tunnelTitle': '터널 접근 필요',
'sessionAuth.expired.banner': '세션이 만료되었습니다. 계속하려면 로그인하세요.',
'sessionAuth.expired.loginAction': '로그인',
'sessionAuth.expired.sendBlocked': '세션이 만료되었습니다. 메시지를 보내려면 로그인하세요.',
'sessionAuth.locked.unlockTitle': 'OpenChamber 잠금 해제', 'sessionAuth.locked.unlockTitle': 'OpenChamber 잠금 해제',
'sessionAuth.locked.tunnelDescription': '데스크톱 앱의 일회용 연결 링크로 이 터널을 여세요.', 'sessionAuth.locked.tunnelDescription': '데스크톱 앱의 일회용 연결 링크로 이 터널을 여세요.',
'sessionAuth.locked.passwordDescription': '이 세션은 비밀번호로 보호됩니다.', 'sessionAuth.locked.passwordDescription': '이 세션은 비밀번호로 보호됩니다.',
@@ -818,25 +818,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': 'Przełącz ulubiony model wstecz', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_backward.label': 'Przełącz ulubiony model wstecz',
'settings.openchamber.keyboardShortcuts.action.open_model_selector.label': 'Otwórz wybór modelu', 'settings.openchamber.keyboardShortcuts.action.open_model_selector.label': 'Otwórz wybór modelu',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Przełącz ulubiony model w przód', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': 'Przełącz ulubiony model w przód',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': 'Przełącz zakładkę usług',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Przełącz motyw', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': 'Przełącz motyw',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Przełącz agenta', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': 'Przełącz agenta',
'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Rozwiń pole wprowadzania', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': 'Rozwiń pole wprowadzania',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Przełącz nawigator promptów', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': 'Przełącz nawigator promptów',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Skup pole wprowadzania', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Skup pole wprowadzania',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': 'Poprzednia sesja',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': 'Następna sesja',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': 'Zmień nazwę bieżącej sesji',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': 'Przełącz automatyczne zatwierdzanie',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': 'Zamknij kartę sesji',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Otwórz paletę poleceń', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Otwórz paletę poleceń',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Przejdź do linii (edytor plików)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Przejdź do linii (edytor plików)',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Otwórz skróty klawiszowe', 'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Otwórz skróty klawiszowe',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Otwórz powierzchnię plików', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': 'Przełącz kartę sesji',
'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Przełącz powierzchnię panelu kontekstu', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Przełącz powierzchnię panelu kontekstu',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Otwórz powierzchnię Git',
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Otwórz ustawienia', 'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Otwórz ustawienia',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Przełącz panel kontekstu planu',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Przełącz panel kontekstu',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Przełącz menu usług', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Przełącz menu usług',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Dodaj zaznaczenie do czatu', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Dodaj zaznaczenie do czatu',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Przełącz pasek boczny', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Przełącz pasek boczny',
@@ -849,7 +851,29 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': 'Ta kombinacja jest już używana przez inny skrót. Nadpisać i wyczyścić to inne przypisanie?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': 'Ta kombinacja jest już używana przez inny skrót. Nadpisać i wyczyścić to inne przypisanie?',
'settings.openchamber.keyboardShortcuts.title': 'Skróty klawiszowe', 'settings.openchamber.keyboardShortcuts.title': 'Skróty klawiszowe',
'settings.openchamber.keyboardShortcuts.tooltip': 'Przechwyć nową kombinację klawiszy, zapisz ją, a przypisania zostaną natychmiast zaktualizowane.', 'settings.openchamber.keyboardShortcuts.tooltip': 'Przechwyć nową kombinację klawiszy, zapisz ją, a przypisania zostaną natychmiast zaktualizowane.',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'Ten skrót może kolidować z domyślnymi skrótami przeglądarki. Został jednak zapisany.', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'Ten skrót może kolidować z domyślnymi skrótami przeglądarki. Nadal możesz go zapisać.',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': 'Ta sekwencja współdzieli prefiks kontekstowy z działaniem {action}. Gdy jego kontekst jest aktywny, to działanie ma pierwszeństwo.',
'settings.openchamber.keyboardShortcuts.category.session': 'Sterowanie sesją',
'settings.openchamber.keyboardShortcuts.category.models': 'Modele i agenci',
'settings.openchamber.keyboardShortcuts.category.panels': 'Panele i narzędzia',
'settings.openchamber.keyboardShortcuts.category.navigation': 'Nawigacja',
'settings.openchamber.keyboardShortcuts.category.application': 'Aplikacja',
'settings.openchamber.keyboardShortcuts.actions.edit': 'Edytuj',
'settings.openchamber.keyboardShortcuts.actions.confirm': 'Potwierdź',
'settings.openchamber.keyboardShortcuts.dialog.title': 'Edytuj: {action}',
'settings.openchamber.keyboardShortcuts.dialog.instructions': 'Naciśnij maksymalnie dwie kombinacje klawiszy, po najwyżej trzy klawisze każda. Po pierwszej odczekaj do 3 sekund na drugą kombinację. Wybierz Potwierdź, aby zastosować, lub Anuluj, aby odrzucić. Backspace usuwa ostatnią.',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': 'Pierwsza kombinacja',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': 'Druga kombinacja',
'settings.openchamber.keyboardShortcuts.dialog.recording': 'Naciśnij klawisze…',
'settings.openchamber.keyboardShortcuts.unassigned': 'Nieprzypisany',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': 'To koliduje z sekwencją używaną przez {action}. Wybierz inną kombinację.',
'settings.openchamber.keyboardShortcuts.error.exactConflict': 'Ta kombinacja jest już używana przez {action}.',
'settings.openchamber.keyboardShortcuts.error.internalConflict': 'Ta kombinacja koliduje z wbudowanym skrótem, którego nie można zastąpić.',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': 'Otwórz wybór projektu szkicu',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': 'Otwórz wybór worktree szkicu',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': 'Otwórz ostatnie sesje',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': 'Otwórz oś czasu rozmowy',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': 'Wprowadzanie głosowe',
'settings.openchamber.opencodeCli.actions.browse': 'Przeglądaj', 'settings.openchamber.opencodeCli.actions.browse': 'Przeglądaj',
'settings.openchamber.opencodeCli.actions.browseAria': 'Przeglądaj ścieżkę do pliku binarnego OpenCode', 'settings.openchamber.opencodeCli.actions.browseAria': 'Przeglądaj ścieżkę do pliku binarnego OpenCode',
'settings.openchamber.opencodeCli.actions.restartingOpenCode': 'Restartowanie OpenCode...', 'settings.openchamber.opencodeCli.actions.restartingOpenCode': 'Restartowanie OpenCode...',
+22 -11
View File
@@ -776,7 +776,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': 'Zadania', 'chat.statusRow.tasksTitle': 'Zadania',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} aktywne · {left} pozostało', 'chat.statusRow.summary.activeLeft': '{active} aktywne · {left} pozostało',
'chat.statusRow.aborted': 'Przerwane',
'chat.revertIndicator.redo': 'Ponów', 'chat.revertIndicator.redo': 'Ponów',
'chat.revertIndicator.redoAria': 'Ponów — przywróć cofnięte wiadomości', 'chat.revertIndicator.redoAria': 'Ponów — przywróć cofnięte wiadomości',
'chat.revertPopover.title': 'Cofnięte', 'chat.revertPopover.title': 'Cofnięte',
@@ -853,7 +852,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'Nie udało się zachować sesji btw', 'chat.btw.toast.promoteFailed': 'Nie udało się zachować sesji btw',
'chat.container.readOnlySubagentPromptBanner': 'Sesje podagentów nie mogą otrzymywać promptów.', 'chat.container.readOnlySubagentPromptBanner': 'Sesje podagentów nie mogą otrzymywać promptów.',
'chat.container.sessionLoadError.title': 'Nie udało się wczytać sesji', 'chat.container.sessionLoadError.title': 'Nie udało się wczytać sesji',
'chat.container.sessionLoadError.description': 'Sprawdź połączenie i spróbuj ponownie wczytać tę sesję.', 'chat.container.sessionLoadError.description': 'Nie udało się pobrać rozmowy — serwer może być wyłączony lub nieosiągalny. Nic nie przepadło; spróbuj ponownie, gdy wróci.',
'chat.container.sessionLoadError.authDescription': 'Sesja wygasła, więc serwer odrzucił żądanie. Zaloguj się, a rozmowa się wczyta.',
'chat.container.sessionLoadError.retry': 'Spróbuj ponownie', 'chat.container.sessionLoadError.retry': 'Spróbuj ponownie',
'sessions.sidebar.group.empty.loadingSessions': 'Wczytywanie sesji…', 'sessions.sidebar.group.empty.loadingSessions': 'Wczytywanie sesji…',
'sessions.sidebar.group.empty.loadFailed': 'Nie udało się odświeżyć sesji.', 'sessions.sidebar.group.empty.loadFailed': 'Nie udało się odświeżyć sesji.',
@@ -896,10 +896,8 @@ export const dict: Record<I18nKey, string> = {
'chat.textSelection.title.commentOnSelection': 'Skomentuj zaznaczenie', 'chat.textSelection.title.commentOnSelection': 'Skomentuj zaznaczenie',
'chat.textSelection.comment.placeholder': 'Dodaj opcjonalny komentarz...', 'chat.textSelection.comment.placeholder': 'Dodaj opcjonalny komentarz...',
'chat.textSelection.comment.attach': 'Załącz', 'chat.textSelection.comment.attach': 'Załącz',
'chat.textSelection.actions.newSession': 'Nowa sesja',
'chat.textSelection.actions.addToNotes': 'Dodaj do notatek', 'chat.textSelection.actions.addToNotes': 'Dodaj do notatek',
'chat.textSelection.title.addToCurrentChat': 'Dodaj do obecnego czatu', 'chat.textSelection.title.addToCurrentChat': 'Dodaj do obecnego czatu',
'chat.textSelection.title.newSessionWithSelection': 'Utwórz nową sesję z zaznaczeniem',
'chat.textSelection.title.saveInsightToNotes': 'Zapisz zaznaczony tekst do notatek', 'chat.textSelection.title.saveInsightToNotes': 'Zapisz zaznaczony tekst do notatek',
'chat.messageBody.actions.revertAria': 'Cofnij do tej wiadomości', 'chat.messageBody.actions.revertAria': 'Cofnij do tej wiadomości',
'chat.messageBody.actions.revert': 'Cofnij od tego miejsca', 'chat.messageBody.actions.revert': 'Cofnij od tego miejsca',
@@ -1452,6 +1450,15 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.showSessionSwitcher': 'Pokaż przełącznik sesji', 'commandPalette.item.showSessionSwitcher': 'Pokaż przełącznik sesji',
'commandPalette.item.toggleSidebar': 'Przełącz panel boczny', 'commandPalette.item.toggleSidebar': 'Przełącz panel boczny',
'commandPalette.item.toggleTerminal': 'Przełącz terminal', 'commandPalette.item.toggleTerminal': 'Przełącz terminal',
'commandPalette.item.cycleTheme': 'Przełącz motyw',
'commandPalette.item.showOpenCodeStatus': 'Pokaż status OpenCode',
'commandPalette.item.toggleMemoryDebug': 'Przełącz panel debugowania pamięci',
'commandPalette.item.pinSession': 'Przypnij lub odepnij sesję',
'commandPalette.item.copySessionId': 'Kopiuj ID sesji',
'commandPalette.item.openMultiRun': 'Otwórz panel multi-run',
'commandPalette.item.openArchive': 'Otwórz zarchiwizowane sesje',
'commandPalette.item.openNotes': 'Otwórz panel notatek',
'commandPalette.item.openTodos': 'Otwórz panel zadań',
'commandPalette.session.untitled': 'Nienazwana sesja', 'commandPalette.session.untitled': 'Nienazwana sesja',
'commandPalette.title': 'Paleta poleceń', 'commandPalette.title': 'Paleta poleceń',
'contextPanel.actions.closePanel': 'Zamknij panel', 'contextPanel.actions.closePanel': 'Zamknij panel',
@@ -1469,6 +1476,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.pr': 'Pull Request', 'contextPanel.mode.pr': 'Pull Request',
'contextPanel.mode.preview': 'Podgląd', 'contextPanel.mode.preview': 'Podgląd',
'contextPanel.mode.browser': 'Przeglądarka', 'contextPanel.mode.browser': 'Przeglądarka',
'contextRail.configure.open': 'Konfiguruj panele',
'contextRail.configure.dialogTitle': 'Panele paska',
'contextRail.configure.dialogDescription': 'Wybierz, które panele pokazuje pasek. Ukryte panele zachowują dane i pozostają dostępne z palety poleceń.',
'contextRail.configure.showAll': 'Pokaż wszystkie',
'contextRail.configure.noneWarning': 'Wszystkie panele są ukryte.',
'contextRail.aria.rail': 'Powierzchnie panelu', 'contextRail.aria.rail': 'Powierzchnie panelu',
'contextPanel.editorEmpty.title': 'Brak otwartego pliku', 'contextPanel.editorEmpty.title': 'Brak otwartego pliku',
'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.', 'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.',
@@ -2452,7 +2464,6 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.createNewSession': 'Utwórz nową sesję', 'helpDialog.item.createNewSession': 'Utwórz nową sesję',
'helpDialog.item.createNewWorktreeDraft': 'Utwórz nowy szkic drzewa pracy', 'helpDialog.item.createNewWorktreeDraft': 'Utwórz nowy szkic drzewa pracy',
'helpDialog.item.cycleAgent': 'Przełącz agenta (w polu czatu)', 'helpDialog.item.cycleAgent': 'Przełącz agenta (w polu czatu)',
'helpDialog.item.cycleServicesTab': 'Przełącz kartę usług',
'helpDialog.item.cycleTheme': 'Przełącz motyw (Jasny → Ciemny → Systemowy)', 'helpDialog.item.cycleTheme': 'Przełącz motyw (Jasny → Ciemny → Systemowy)',
'helpDialog.item.cycleThinkingVariant': 'Przełącz wariant myślenia (skrót globalny)', 'helpDialog.item.cycleThinkingVariant': 'Przełącz wariant myślenia (skrót globalny)',
'helpDialog.item.focusChatInput': 'Ustaw fokus na polu czatu', 'helpDialog.item.focusChatInput': 'Ustaw fokus na polu czatu',
@@ -2461,13 +2472,10 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.newWindow': 'Nowe okno (tylko desktop)', 'helpDialog.item.newWindow': 'Nowe okno (tylko desktop)',
'helpDialog.item.openCommandPalette': 'Otwórz paletę poleceń', 'helpDialog.item.openCommandPalette': 'Otwórz paletę poleceń',
'helpDialog.item.openModelSelector': 'Otwórz selektor modeli', 'helpDialog.item.openModelSelector': 'Otwórz selektor modeli',
'helpDialog.item.openRightSidebarFilesTab': 'Otwórz powierzchnię plików',
'helpDialog.item.openRightSidebarGitTab': 'Otwórz powierzchnię Git',
'helpDialog.item.openSettings': 'Otwórz ustawienia', 'helpDialog.item.openSettings': 'Otwórz ustawienia',
'helpDialog.item.showKeyboardShortcuts': 'Pokaż skróty klawiaturowe (to okno)', 'helpDialog.item.showKeyboardShortcuts': 'Pokaż skróty klawiaturowe (to okno)',
'helpDialog.item.switchSessionTab': 'Przełącz kartę sesji',
'helpDialog.item.switchContextSurface': 'Przełącz powierzchnię panelu kontekstu (klawisz liczbowy)', 'helpDialog.item.switchContextSurface': 'Przełącz powierzchnię panelu kontekstu (klawisz liczbowy)',
'helpDialog.item.togglePlanContextPanel': 'Przełącz panel kontekstu planu',
'helpDialog.item.toggleRightSidebar': 'Przełącz panel kontekstu',
'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług', 'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług',
'helpDialog.item.toggleSessionSidebar': 'Przełącz panel sesji', 'helpDialog.item.toggleSessionSidebar': 'Przełącz panel sesji',
'helpDialog.item.addSelectionToChat': 'Dodaj zaznaczenie do czatu', 'helpDialog.item.addSelectionToChat': 'Dodaj zaznaczenie do czatu',
@@ -2476,7 +2484,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.keyCombiner.or': 'lub', 'helpDialog.keyCombiner.or': 'lub',
'helpDialog.proTips.commandPalette': 'Użyj Palety poleceń ({shortcut}), aby szybko uzyskać dostęp do wszystkich akcji', 'helpDialog.proTips.commandPalette': 'Użyj Palety poleceń ({shortcut}), aby szybko uzyskać dostęp do wszystkich akcji',
'helpDialog.proTips.recentSessions': '5 ostatnich sesji pojawia się w Palecie poleceń', 'helpDialog.proTips.recentSessions': '5 ostatnich sesji pojawia się w Palecie poleceń',
'helpDialog.proTips.themeCycling': 'Przełączanie motywów zapamiętuje twoje preferencje między sesjami', 'helpDialog.proTips.leaderSequences': 'Skróty dwustopniowe: naciśnij kombinację, potem drugi klawisz — Esc anuluje',
'helpDialog.proTips.title': 'Wskazówki:', 'helpDialog.proTips.title': 'Wskazówki:',
'helpDialog.section.interface': 'Interfejs', 'helpDialog.section.interface': 'Interfejs',
'helpDialog.section.navigationCommands': 'Nawigacja i polecenia', 'helpDialog.section.navigationCommands': 'Nawigacja i polecenia',
@@ -2490,7 +2498,7 @@ export const dict: Record<I18nKey, string> = {
'inlineComment.actions.save': 'Zapisz', 'inlineComment.actions.save': 'Zapisz',
'inlineComment.actions.showLess': 'Show less', 'inlineComment.actions.showLess': 'Show less',
'inlineComment.actions.showMore': 'Show more', 'inlineComment.actions.showMore': 'Show more',
'inlineComment.input.placeholder': 'Add a comment... (Cmd+Enter to save)', 'inlineComment.input.placeholder': 'Dodaj komentarz... ({shortcut}, aby zapisać)',
'inlineComment.input.placeholderShort': 'Dodaj komentarz...', 'inlineComment.input.placeholderShort': 'Dodaj komentarz...',
'inlineComment.range.lines': 'Lines {start}-{end}', 'inlineComment.range.lines': 'Lines {start}-{end}',
'inlineComment.toast.selectSessionToSave': 'Select a session to save comment', 'inlineComment.toast.selectSessionToSave': 'Select a session to save comment',
@@ -2855,6 +2863,9 @@ export const dict: Record<I18nKey, string> = {
'sessionAuth.locked.passwordDescription': 'Ta sesja jest chroniona hasłem.', 'sessionAuth.locked.passwordDescription': 'Ta sesja jest chroniona hasłem.',
'sessionAuth.locked.tunnelDescription': 'Otwórz ten tunel za pomocą jednorazowego linku połączenia z aplikacji desktopowej.', 'sessionAuth.locked.tunnelDescription': 'Otwórz ten tunel za pomocą jednorazowego linku połączenia z aplikacji desktopowej.',
'sessionAuth.locked.tunnelTitle': 'Wymagany dostęp przez tunel', 'sessionAuth.locked.tunnelTitle': 'Wymagany dostęp przez tunel',
'sessionAuth.expired.banner': 'Sesja wygasła — zaloguj się, aby kontynuować.',
'sessionAuth.expired.loginAction': 'Zaloguj się',
'sessionAuth.expired.sendBlocked': 'Sesja wygasła — zaloguj się, aby wysyłać wiadomości.',
'sessionAuth.locked.unlockTitle': 'Odblokuj OpenChamber', 'sessionAuth.locked.unlockTitle': 'Odblokuj OpenChamber',
'sessionAuth.password.placeholder': 'Wpisz hasło', 'sessionAuth.password.placeholder': 'Wpisz hasło',
'sessionAuth.toast.passkeyAdded': 'Dodano klucz dostępu', 'sessionAuth.toast.passkeyAdded': 'Dodano klucz dostępu',
@@ -1101,7 +1101,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.overwritePrompt": "Esta combinação já está sendo usada por outro atalho. Sobrescrever e limpar essa outra atribuição?", "settings.openchamber.keyboardShortcuts.overwritePrompt": "Esta combinação já está sendo usada por outro atalho. Sobrescrever e limpar essa outra atribuição?",
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Pressione as teclas...", "settings.openchamber.keyboardShortcuts.field.pressKeys": "Pressione as teclas...",
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura um atalho primeiro.", "settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura um atalho primeiro.",
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atalho pode entrar em conflito com os padrões do navegador. Ainda assim, ele será salvo.", "settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atalho pode entrar em conflito com os padrões do navegador. Ainda assim, você pode salvá-lo.",
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir para linha (editor de arquivos)", "settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir para linha (editor de arquivos)",
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos", "settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos",
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Focar entrada", "settings.openchamber.keyboardShortcuts.action.focus_input.label": "Focar entrada",
@@ -1110,18 +1110,20 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir ou recolher terminal", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir ou recolher terminal",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Adicionar seleção ao chat", "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Adicionar seleção ao chat",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar ou ocultar barra lateral", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar ou ocultar barra lateral",
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar painel de contexto', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Alternar aba de sessão",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superfície do Git', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superfície de arquivos',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Alternar superfície do painel de contexto", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Alternar superfície do painel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão",
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Sessão anterior",
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Próxima sessão",
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Renomear sessão atual",
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Alternar aprovação automática",
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Fechar aba da sessão",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atalhos de teclado", "settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atalhos de teclado",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar painel de plano de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar ou ocultar menu de serviços", "settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar ou ocultar menu de serviços",
"settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Alternar aba de serviços",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Alternar tema", "settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Alternar tema",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Alternar agente", "settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Alternar agente",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Próximo modelo favorito", "settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Próximo modelo favorito",
@@ -1130,6 +1132,27 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada", "settings.openchamber.keyboardShortcuts.action.expand_input.label": "Expandir entrada",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir linha do tempo da conversa", "settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Abrir linha do tempo da conversa",
"settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Mostrar ou ocultar navegador de prompts", "settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Mostrar ou ocultar navegador de prompts",
"settings.openchamber.keyboardShortcuts.warning.contextualPrefix": "Esta sequência compartilha um prefixo contextual com {action}. Quando esse contexto está ativo, essa ação tem prioridade.",
"settings.openchamber.keyboardShortcuts.category.session": "Controles de sessão",
"settings.openchamber.keyboardShortcuts.category.models": "Modelos e agentes",
"settings.openchamber.keyboardShortcuts.category.panels": "Painéis e ferramentas",
"settings.openchamber.keyboardShortcuts.category.navigation": "Navegação",
"settings.openchamber.keyboardShortcuts.category.application": "Aplicação",
"settings.openchamber.keyboardShortcuts.actions.edit": "Editar",
"settings.openchamber.keyboardShortcuts.actions.confirm": "Confirmar",
"settings.openchamber.keyboardShortcuts.dialog.title": "Editar {action}",
"settings.openchamber.keyboardShortcuts.dialog.instructions": "Pressione até duas combinações de teclas, com no máximo três teclas em cada uma. Após a primeira, aguarde até 3 segundos por uma segunda combinação. Use Confirmar para aplicar ou Cancelar para descartar. Backspace remove a última.",
"settings.openchamber.keyboardShortcuts.dialog.firstChord": "Primeira combinação",
"settings.openchamber.keyboardShortcuts.dialog.secondChord": "Segunda combinação",
"settings.openchamber.keyboardShortcuts.dialog.recording": "Pressione as teclas…",
"settings.openchamber.keyboardShortcuts.unassigned": "Não atribuído",
"settings.openchamber.keyboardShortcuts.error.prefixConflict": "Isto entra em conflito com a sequência usada por {action}. Escolha outra combinação.",
"settings.openchamber.keyboardShortcuts.error.exactConflict": "Esta combinação já é usada por {action}.",
"settings.openchamber.keyboardShortcuts.error.internalConflict": "Esta combinação entra em conflito com um atalho integrado, que não pode ser substituído.",
"settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label": "Abrir seletor de projeto do rascunho",
"settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label": "Abrir seletor de worktree do rascunho",
"settings.openchamber.keyboardShortcuts.action.open_session_list.label": "Abrir sessões recentes",
"settings.openchamber.keyboardShortcuts.action.toggle_dictation.label": "Entrada por voz",
"settings.projects.sidebar.total": "Total {count}", "settings.projects.sidebar.total": "Total {count}",
"settings.projects.sidebar.actions.addProject": "Adicionar projeto", "settings.projects.sidebar.actions.addProject": "Adicionar projeto",
"settings.projects.page.empty.noProjects": "Não há projetos disponíveis.", "settings.projects.page.empty.noProjects": "Não há projetos disponíveis.",
+22 -11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Contexto", "contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Prévia", "contextPanel.mode.preview": "Prévia",
"contextPanel.mode.browser": "Navegador", "contextPanel.mode.browser": "Navegador",
"contextRail.configure.open": "Configurar painéis",
"contextRail.configure.dialogTitle": "Painéis da barra",
"contextRail.configure.dialogDescription": "Escolha quais painéis a barra mostra. Painéis ocultos mantêm seus dados e continuam acessíveis pela paleta de comandos.",
"contextRail.configure.showAll": "Mostrar todos",
"contextRail.configure.noneWarning": "Todos os painéis estão ocultos.",
"contextRail.aria.rail": "Superfícies do painel", "contextRail.aria.rail": "Superfícies do painel",
"contextPanel.editorEmpty.title": "Nenhum arquivo aberto", "contextPanel.editorEmpty.title": "Nenhum arquivo aberto",
"contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.", "contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.",
@@ -1646,7 +1651,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "Plano importado", "rightSidebar.contextNotesTodo.toast.planImported": "Plano importado",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Não foi possível ler o arquivo do plano", "rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Não foi possível ler o arquivo do plano",
"inlineComment.range.lines": "Linhas {start}-{end}", "inlineComment.range.lines": "Linhas {start}-{end}",
"inlineComment.input.placeholder": "Adicionar um comentário... (Cmd+Enter para salvar)", "inlineComment.input.placeholder": "Adicionar um comentário... ({shortcut} para salvar)",
"inlineComment.input.placeholderShort": "Adicionar um comentário...", "inlineComment.input.placeholderShort": "Adicionar um comentário...",
"inlineComment.actions.cancel": "Cancelar", "inlineComment.actions.cancel": "Cancelar",
"inlineComment.actions.save": "Salvar", "inlineComment.actions.save": "Salvar",
@@ -1836,22 +1841,18 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.focusChatInput": "Focar entrada do chat", "helpDialog.item.focusChatInput": "Focar entrada do chat",
"helpDialog.item.togglePromptNavigator": "Mostrar ou ocultar navegador de prompts", "helpDialog.item.togglePromptNavigator": "Mostrar ou ocultar navegador de prompts",
"helpDialog.item.abortActiveRun": "Interromper execução ativa (duplo clique)", "helpDialog.item.abortActiveRun": "Interromper execução ativa (duplo clique)",
"helpDialog.item.toggleRightSidebar": 'Alternar painel de contexto',
"helpDialog.item.openRightSidebarGitTab": 'Abrir superfície do Git',
"helpDialog.item.openRightSidebarFilesTab": 'Abrir superfície de arquivos',
"helpDialog.item.toggleTerminalDock": "Mostrar ou ocultar dock de terminal", "helpDialog.item.toggleTerminalDock": "Mostrar ou ocultar dock de terminal",
"helpDialog.item.toggleTerminalExpanded": "Expandir ou recolher o terminal", "helpDialog.item.toggleTerminalExpanded": "Expandir ou recolher o terminal",
"helpDialog.item.togglePlanContextPanel": "Alternar painel de contexto do plano",
"helpDialog.item.cycleTheme": "Alternar tema (Claro → Escuro → Sistema)", "helpDialog.item.cycleTheme": "Alternar tema (Claro → Escuro → Sistema)",
"helpDialog.item.switchSessionTab": "Alternar aba de sessão",
"helpDialog.item.switchContextSurface": "Alternar superfície do painel de contexto (tecla numérica)", "helpDialog.item.switchContextSurface": "Alternar superfície do painel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar ou ocultar menu de serviços", "helpDialog.item.toggleServicesMenu": "Mostrar ou ocultar menu de serviços",
"helpDialog.item.cycleServicesTab": "Alternar aba de serviços",
"helpDialog.item.openSettings": "Abrir configurações", "helpDialog.item.openSettings": "Abrir configurações",
"helpDialog.keyCombiner.or": "ou", "helpDialog.keyCombiner.or": "ou",
"helpDialog.proTips.title": "Dicas:", "helpDialog.proTips.title": "Dicas:",
"helpDialog.proTips.commandPalette": "Use a paleta de comandos ({shortcut}) para acessar rapidamente todas as ações", "helpDialog.proTips.commandPalette": "Use a paleta de comandos ({shortcut}) para acessar rapidamente todas as ações",
"helpDialog.proTips.recentSessions": "As cinco sessões mais recentes aparecem na paleta de comandos", "helpDialog.proTips.recentSessions": "As cinco sessões mais recentes aparecem na paleta de comandos",
"helpDialog.proTips.themeCycling": "A alternância de tema lembra sua preferência entre sessões", "helpDialog.proTips.leaderSequences": "Atalhos em duas etapas: pressione a combinação e depois a segunda tecla — Esc cancela",
"header.actions.rightSidebarWithShortcut": "Barra lateral direita ({shortcut})", "header.actions.rightSidebarWithShortcut": "Barra lateral direita ({shortcut})",
"header.actions.toggleRightSidebarAria": "Mostrar ou ocultar barra lateral direita", "header.actions.toggleRightSidebarAria": "Mostrar ou ocultar barra lateral direita",
"header.actions.openAppMenu": "Menu do OpenChamber", "header.actions.openAppMenu": "Menu do OpenChamber",
@@ -2061,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "Tarefas", "chat.statusRow.tasksTitle": "Tarefas",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "{active} ativas · {left} restantes", "chat.statusRow.summary.activeLeft": "{active} ativas · {left} restantes",
"chat.statusRow.aborted": "Interrompido",
"chat.revertIndicator.redo": "Refazer", "chat.revertIndicator.redo": "Refazer",
"chat.revertIndicator.redoAria": "Refazer — restaurar mensagens revertidas", "chat.revertIndicator.redoAria": "Refazer — restaurar mensagens revertidas",
"chat.revertPopover.title": "Revertidas", "chat.revertPopover.title": "Revertidas",
@@ -2139,7 +2139,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'Falha ao manter a sessão btw', 'chat.btw.toast.promoteFailed': 'Falha ao manter a sessão btw',
"chat.container.readOnlySubagentPromptBanner": "Sessões de subagente não podem receber prompts.", "chat.container.readOnlySubagentPromptBanner": "Sessões de subagente não podem receber prompts.",
"chat.container.sessionLoadError.title": "Não foi possível carregar a sessão", "chat.container.sessionLoadError.title": "Não foi possível carregar a sessão",
"chat.container.sessionLoadError.description": "Verifique a conexão e tente carregar esta sessão novamente.", "chat.container.sessionLoadError.description": "Não foi possível buscar a conversa — o servidor pode estar desligado ou inacessível. Nada foi perdido; tente novamente quando ele voltar.",
"chat.container.sessionLoadError.authDescription": "Sua sessão expirou, então o servidor recusou a solicitação. Entre e a conversa será carregada.",
"chat.container.sessionLoadError.retry": "Tentar novamente", "chat.container.sessionLoadError.retry": "Tentar novamente",
"sessions.sidebar.group.empty.loadingSessions": "Carregando sessões…", "sessions.sidebar.group.empty.loadingSessions": "Carregando sessões…",
"sessions.sidebar.group.empty.loadFailed": "Não foi possível atualizar as sessões.", "sessions.sidebar.group.empty.loadFailed": "Não foi possível atualizar as sessões.",
@@ -2182,10 +2183,8 @@ export const dict: Record<I18nKey, string> = {
"chat.textSelection.title.commentOnSelection": "Comentar a seleção", "chat.textSelection.title.commentOnSelection": "Comentar a seleção",
"chat.textSelection.comment.placeholder": "Adicione um comentário opcional...", "chat.textSelection.comment.placeholder": "Adicione um comentário opcional...",
"chat.textSelection.comment.attach": "Anexar", "chat.textSelection.comment.attach": "Anexar",
"chat.textSelection.actions.newSession": "Nova sessão",
"chat.textSelection.actions.addToNotes": "Adicionar às notas", "chat.textSelection.actions.addToNotes": "Adicionar às notas",
"chat.textSelection.title.addToCurrentChat": "Adicionar ao chat atual", "chat.textSelection.title.addToCurrentChat": "Adicionar ao chat atual",
"chat.textSelection.title.newSessionWithSelection": "Criar nova sessão com seleção",
"chat.textSelection.title.saveInsightToNotes": "Salvar texto selecionado em notas", "chat.textSelection.title.saveInsightToNotes": "Salvar texto selecionado em notas",
"chat.messageBody.actions.revertAria": "Voltar para esta mensagem", "chat.messageBody.actions.revertAria": "Voltar para esta mensagem",
"chat.messageBody.actions.revert": "Voltar daqui", "chat.messageBody.actions.revert": "Voltar daqui",
@@ -2449,6 +2448,15 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.toggleSidebar": "Mostrar ou ocultar barra lateral", "commandPalette.item.toggleSidebar": "Mostrar ou ocultar barra lateral",
"commandPalette.item.showContextUsage": "Mostrar uso do contexto", "commandPalette.item.showContextUsage": "Mostrar uso do contexto",
"commandPalette.item.toggleTerminal": "Mostrar ou ocultar terminal", "commandPalette.item.toggleTerminal": "Mostrar ou ocultar terminal",
"commandPalette.item.cycleTheme": "Alternar tema",
"commandPalette.item.showOpenCodeStatus": "Mostrar status do OpenCode",
"commandPalette.item.toggleMemoryDebug": "Alternar painel de depuração de memória",
"commandPalette.item.pinSession": "Fixar ou desafixar sessão",
"commandPalette.item.copySessionId": "Copiar ID da sessão",
"commandPalette.item.openMultiRun": "Abrir lançador multi-run",
"commandPalette.item.openArchive": "Abrir sessões arquivadas",
"commandPalette.item.openNotes": "Abrir painel de notas",
"commandPalette.item.openTodos": "Abrir painel de tarefas",
"commandPalette.item.openSettings": "Abrir configurações...", "commandPalette.item.openSettings": "Abrir configurações...",
"commandPalette.session.untitled": "Sessão sem título", "commandPalette.session.untitled": "Sessão sem título",
"openCodeStatusDialog.title": "Status do OpenCode", "openCodeStatusDialog.title": "Status do OpenCode",
@@ -2663,6 +2671,9 @@ export const dict: Record<I18nKey, string> = {
"sessionAuth.error.passkeySignInCanceled": "O início de sessão com chave de acesso foi cancelado.", "sessionAuth.error.passkeySignInCanceled": "O início de sessão com chave de acesso foi cancelado.",
"sessionAuth.error.enterPasswordForPasskey": "Digite sua senha para adicionar uma chave de acesso.", "sessionAuth.error.enterPasswordForPasskey": "Digite sua senha para adicionar uma chave de acesso.",
"sessionAuth.locked.tunnelTitle": "É necessário acesso por túnel", "sessionAuth.locked.tunnelTitle": "É necessário acesso por túnel",
"sessionAuth.expired.banner": "Sua sessão expirou — entre para continuar.",
"sessionAuth.expired.loginAction": "Entrar",
"sessionAuth.expired.sendBlocked": "Sessão expirada — entre para enviar mensagens.",
"sessionAuth.locked.unlockTitle": "Desbloquear OpenChamber", "sessionAuth.locked.unlockTitle": "Desbloquear OpenChamber",
"sessionAuth.locked.tunnelDescription": "Abra este túnel usando o link de conexão única do aplicativo desktop.", "sessionAuth.locked.tunnelDescription": "Abra este túnel usando o link de conexão única do aplicativo desktop.",
"sessionAuth.locked.passwordDescription": "Esta sessão está protegida com senha.", "sessionAuth.locked.passwordDescription": "Esta sessão está protegida com senha.",
@@ -206,9 +206,9 @@ export const settingsDict = {
"settings.openchamber.tunnel.toast.addManagedRemoteTokenBeforeStarting": "Перед початком додайте токен керованого віддаленого тунелю", "settings.openchamber.tunnel.toast.addManagedRemoteTokenBeforeStarting": "Перед початком додайте токен керованого віддаленого тунелю",
"settings.openchamber.tunnel.toast.startFailed": "Не вдалося запустити тунель", "settings.openchamber.tunnel.toast.startFailed": "Не вдалося запустити тунель",
"settings.openchamber.tunnel.toast.startedButNoPublicUrl": "Тунель запущено, але публічний URL не повернувся", "settings.openchamber.tunnel.toast.startedButNoPublicUrl": "Тунель запущено, але публічний URL не повернувся",
"settings.openchamber.tunnel.toast.replacedTunnelSingleSingle": "Попередній тунель замінено: відкликано 1 посилання, анульовано 1 сесія.", "settings.openchamber.tunnel.toast.replacedTunnelSingleSingle": "Попередній тунель замінено: відкликано 1 посилання, анульовано 1 сесію.",
"settings.openchamber.tunnel.toast.replacedTunnelSingleManySessions": "Попередній тунель замінено: відкликано 1 посилання, анульовано сесій: {invalidatedSessionCount}.", "settings.openchamber.tunnel.toast.replacedTunnelSingleManySessions": "Попередній тунель замінено: відкликано 1 посилання, анульовано сесій: {invalidatedSessionCount}.",
"settings.openchamber.tunnel.toast.replacedTunnelManyLinksSingleSession": "Попередній тунель замінено: відкликано посилань: {revokedBootstrapCount}, анульовано 1 сесія.", "settings.openchamber.tunnel.toast.replacedTunnelManyLinksSingleSession": "Попередній тунель замінено: відкликано посилань: {revokedBootstrapCount}, анульовано 1 сесію.",
"settings.openchamber.tunnel.toast.replacedTunnelManyMany": "Попередній тунель замінено: відкликано посилань: {revokedBootstrapCount}, анульовано сесій: {invalidatedSessionCount}.", "settings.openchamber.tunnel.toast.replacedTunnelManyMany": "Попередній тунель замінено: відкликано посилань: {revokedBootstrapCount}, анульовано сесій: {invalidatedSessionCount}.",
"settings.openchamber.tunnel.toast.linkReady": "Тунель готовий", "settings.openchamber.tunnel.toast.linkReady": "Тунель готовий",
"settings.openchamber.tunnel.toast.stopped": "Тунель зупинено", "settings.openchamber.tunnel.toast.stopped": "Тунель зупинено",
@@ -1101,7 +1101,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.overwritePrompt": "Ця комбінація вже використовується іншою комбінацією клавіш. Перезаписати та очистити інше зіставлення?", "settings.openchamber.keyboardShortcuts.overwritePrompt": "Ця комбінація вже використовується іншою комбінацією клавіш. Перезаписати та очистити інше зіставлення?",
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Натисніть клавіші...", "settings.openchamber.keyboardShortcuts.field.pressKeys": "Натисніть клавіші...",
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Спочатку запишіть комбінацію клавіш.", "settings.openchamber.keyboardShortcuts.error.captureFirst": "Спочатку запишіть комбінацію клавіш.",
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Ця комбінація клавіш може конфліктувати зі стандартними скороченнями браузера. Її все одно збережено.", "settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Ця комбінація клавіш може конфліктувати зі стандартними скороченнями браузера. Ви все одно можете її зберегти.",
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Перейти до рядка (редактор файлів)", "settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Перейти до рядка (редактор файлів)",
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Відкрити палітру команд", "settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Відкрити палітру команд",
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Фокус на полі вводу", "settings.openchamber.keyboardShortcuts.action.focus_input.label": "Фокус на полі вводу",
@@ -1110,18 +1110,20 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Розгорнути або згорнути термінал", "settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Розгорнути або згорнути термінал",
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Додати виділення в чат", "settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Додати виділення в чат",
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Перемкнути бічну панель", "settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Перемкнути бічну панель",
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Перемкнути контекстну панель', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.label": "Перемкнути вкладку сесії",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Відкрити поверхню Git', "settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix": " + 1…9",
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Відкрити поверхню файлів',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0", "settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія", "settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія",
"settings.openchamber.keyboardShortcuts.action.switch_session_previous.label": "Попередня сесія",
"settings.openchamber.keyboardShortcuts.action.switch_session_next.label": "Наступна сесія",
"settings.openchamber.keyboardShortcuts.action.rename_current_session.label": "Перейменувати поточну сесію",
"settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label": "Перемкнути авто-дозволи",
"settings.openchamber.keyboardShortcuts.action.close_session_tab.label": "Закрити вкладку сесії",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree", "settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat", "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Відкрити комбінації клавіш", "settings.openchamber.keyboardShortcuts.action.open_help.label": "Відкрити комбінації клавіш",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Перемкнути контекстну панель плану",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Перемкнути меню сервісів", "settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Перемкнути меню сервісів",
"settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label": "Перемкнути вкладку сервісів",
"settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Перемкнути тему", "settings.openchamber.keyboardShortcuts.action.cycle_theme.label": "Перемкнути тему",
"settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Перемкнути агента", "settings.openchamber.keyboardShortcuts.action.cycle_agent.label": "Перемкнути агента",
"settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Перемкнути улюблену модель вперед", "settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label": "Перемкнути улюблену модель вперед",
@@ -1130,6 +1132,27 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.expand_input.label": "Розгорнути введення", "settings.openchamber.keyboardShortcuts.action.expand_input.label": "Розгорнути введення",
"settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Відкрити хронологію розмови", "settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label": "Відкрити хронологію розмови",
"settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Показати або приховати навігатор промптів", "settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label": "Показати або приховати навігатор промптів",
"settings.openchamber.keyboardShortcuts.warning.contextualPrefix": "Ця послідовність має спільний контекстний префікс із дією {action}. Коли її контекст активний, ця дія має пріоритет.",
"settings.openchamber.keyboardShortcuts.category.session": "Керування сесією",
"settings.openchamber.keyboardShortcuts.category.models": "Моделі й агенти",
"settings.openchamber.keyboardShortcuts.category.panels": "Панелі та інструменти",
"settings.openchamber.keyboardShortcuts.category.navigation": "Навігація",
"settings.openchamber.keyboardShortcuts.category.application": "Застосунок",
"settings.openchamber.keyboardShortcuts.actions.edit": "Редагувати",
"settings.openchamber.keyboardShortcuts.actions.confirm": "Підтвердити",
"settings.openchamber.keyboardShortcuts.dialog.title": "Редагувати {action}",
"settings.openchamber.keyboardShortcuts.dialog.instructions": "Натисніть до двох комбінацій клавіш, не більше трьох клавіш у кожній. Після першої зачекайте до 3 секунд на другу комбінацію. Виберіть Підтвердити, щоб застосувати, або Скасувати, щоб відхилити. Backspace видаляє останню.",
"settings.openchamber.keyboardShortcuts.dialog.firstChord": "Перша комбінація",
"settings.openchamber.keyboardShortcuts.dialog.secondChord": "Друга комбінація",
"settings.openchamber.keyboardShortcuts.dialog.recording": "Натисніть клавіші…",
"settings.openchamber.keyboardShortcuts.unassigned": "Не призначено",
"settings.openchamber.keyboardShortcuts.error.prefixConflict": "Це конфліктує з послідовністю, яку використовує {action}. Виберіть іншу комбінацію.",
"settings.openchamber.keyboardShortcuts.error.exactConflict": "Цю комбінацію вже використовує {action}.",
"settings.openchamber.keyboardShortcuts.error.internalConflict": "Ця комбінація конфліктує з вбудованим скороченням, яке не можна замінити.",
"settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label": "Відкрити вибір проєкту чернетки",
"settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label": "Відкрити вибір worktree чернетки",
"settings.openchamber.keyboardShortcuts.action.open_session_list.label": "Відкрити останні сесії",
"settings.openchamber.keyboardShortcuts.action.toggle_dictation.label": "Голосове введення",
"settings.projects.sidebar.total": "Усього {count}", "settings.projects.sidebar.total": "Усього {count}",
"settings.projects.sidebar.actions.addProject": "Додати проєкт", "settings.projects.sidebar.actions.addProject": "Додати проєкт",
"settings.projects.page.empty.noProjects": "Немає доступних проєктів.", "settings.projects.page.empty.noProjects": "Немає доступних проєктів.",
@@ -2122,7 +2145,7 @@ export const settingsDict = {
"settings.magicPrompts.page.group.planImprove.title": "Поліпшити план", "settings.magicPrompts.page.group.planImprove.title": "Поліпшити план",
"settings.magicPrompts.page.group.planImprove.description": "Прихований промпт, який використовується під час надсилання збереженого плану в потік покращення.", "settings.magicPrompts.page.group.planImprove.description": "Прихований промпт, який використовується під час надсилання збереженого плану в потік покращення.",
"settings.magicPrompts.page.group.planTodo.title": "Планування Todo", "settings.magicPrompts.page.group.planTodo.title": "Планування Todo",
"settings.magicPrompts.page.group.planTodo.description": "Прихований промпт, який використовується під час надсилання завдання до нового сесії планування.", "settings.magicPrompts.page.group.planTodo.description": "Прихований промпт, який використовується під час надсилання завдання до нової сесії планування.",
"settings.magicPrompts.page.group.planImplement.title": "Реалізувати план", "settings.magicPrompts.page.group.planImplement.title": "Реалізувати план",
"settings.magicPrompts.page.group.planImplement.description": "Прихований промпт, який використовується під час надсилання збереженого плану в потік реалізації.", "settings.magicPrompts.page.group.planImplement.description": "Прихований промпт, який використовується під час надсилання збереженого плану в потік реалізації.",
"settings.magicPrompts.page.group.sessionSummary.title": "Підсумок сесії", "settings.magicPrompts.page.group.sessionSummary.title": "Підсумок сесії",
+31 -20
View File
@@ -504,12 +504,12 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.bulkActions.failedDeletePlural": "Не вдалося видалити сесії {count}", "sessions.sidebar.bulkActions.failedDeletePlural": "Не вдалося видалити сесії {count}",
"sessions.sidebar.bulkActions.archivedSingle": "Заархівовано сесію: {count}", "sessions.sidebar.bulkActions.archivedSingle": "Заархівовано сесію: {count}",
"sessions.sidebar.bulkActions.archivedPlural": "Заархівовано сесій: {count}", "sessions.sidebar.bulkActions.archivedPlural": "Заархівовано сесій: {count}",
"sessions.sidebar.bulkActions.failedArchiveSingle": "Не вдалося архівувати сесія {count}", "sessions.sidebar.bulkActions.failedArchiveSingle": "Не вдалося архівувати сесію {count}",
"sessions.sidebar.bulkActions.failedArchivePlural": "Не вдалося архівувати сесії {count}", "sessions.sidebar.bulkActions.failedArchivePlural": "Не вдалося архівувати сесії {count}",
"sessions.sidebar.bulkActions.restore": "Відновити", "sessions.sidebar.bulkActions.restore": "Відновити",
"sessions.sidebar.bulkActions.restoredSingle": "Відновлено сесію: {count}", "sessions.sidebar.bulkActions.restoredSingle": "Відновлено сесію: {count}",
"sessions.sidebar.bulkActions.restoredPlural": "Відновлено сесій: {count}", "sessions.sidebar.bulkActions.restoredPlural": "Відновлено сесій: {count}",
"sessions.sidebar.bulkActions.failedRestoreSingle": "Не вдалося відновити сесія {count}", "sessions.sidebar.bulkActions.failedRestoreSingle": "Не вдалося відновити сесію {count}",
"sessions.sidebar.bulkActions.failedRestorePlural": "Не вдалося відновити сесії {count}", "sessions.sidebar.bulkActions.failedRestorePlural": "Не вдалося відновити сесії {count}",
"sessions.sidebar.folders.none": "Папок ще немає", "sessions.sidebar.folders.none": "Папок ще немає",
"sessions.sidebar.folders.newFolderEllipsis": "Нова папка...", "sessions.sidebar.folders.newFolderEllipsis": "Нова папка...",
@@ -560,9 +560,9 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.export.dialog.descriptionMany": "Ця сесія має {count} завдань під-агентів. Додати їх до експорту?", "sessions.sidebar.session.export.dialog.descriptionMany": "Ця сесія має {count} завдань під-агентів. Додати їх до експорту?",
"sessions.sidebar.session.export.dialog.includeSubtasks": "Додати завдання під-агентів", "sessions.sidebar.session.export.dialog.includeSubtasks": "Додати завдання під-агентів",
"sessions.sidebar.session.export.dialog.confirm": "Експортувати", "sessions.sidebar.session.export.dialog.confirm": "Експортувати",
"sessions.sidebar.session.status.active": "Сесія активний", "sessions.sidebar.session.status.active": "Сесія активна",
"sessions.sidebar.session.status.unread": "Непрочитані оновлення", "sessions.sidebar.session.status.unread": "Непрочитані оновлення",
"sessions.sidebar.session.status.pinned": "Закріплений сесія", "sessions.sidebar.session.status.pinned": "Закріплена сесія",
"sessions.sidebar.session.status.movingToWorktree": "Перенесення сесії в новий worktree", "sessions.sidebar.session.status.movingToWorktree": "Перенесення сесії в новий worktree",
"sessions.sidebar.session.status.permissionRequired": "Потрібен дозвіл", "sessions.sidebar.session.status.permissionRequired": "Потрібен дозвіл",
"sessions.sidebar.session.status.questionPendingSingle": "1 запитання очікує відповіді", "sessions.sidebar.session.status.questionPendingSingle": "1 запитання очікує відповіді",
@@ -571,8 +571,8 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.status.lastTurnDuration": "Останній хід тривав {duration}", "sessions.sidebar.session.status.lastTurnDuration": "Останній хід тривав {duration}",
"sessions.sidebar.session.subsessions.collapse": "Згорнути підсесії", "sessions.sidebar.session.subsessions.collapse": "Згорнути підсесії",
"sessions.sidebar.session.subsessions.expand": "Розгорнути підсесії", "sessions.sidebar.session.subsessions.expand": "Розгорнути підсесії",
"sessions.sidebar.dialogs.deleteSession.title": "Видалити сесія?", "sessions.sidebar.dialogs.deleteSession.title": "Видалити сесію?",
"sessions.sidebar.dialogs.archiveSession.title": "Архівувати сесія?", "sessions.sidebar.dialogs.archiveSession.title": "Архівувати сесію?",
"sessions.sidebar.dialogs.deleteSession.withOneSubtask": "\"{sessionTitle}\" і його підзавдання {count} буде остаточно видалено.", "sessions.sidebar.dialogs.deleteSession.withOneSubtask": "\"{sessionTitle}\" і його підзавдання {count} буде остаточно видалено.",
"sessions.sidebar.dialogs.deleteSession.withManySubtasks": "\"{sessionTitle}\" і його підзавдання {count} буде остаточно видалено.", "sessions.sidebar.dialogs.deleteSession.withManySubtasks": "\"{sessionTitle}\" і його підзавдання {count} буде остаточно видалено.",
"sessions.sidebar.dialogs.archiveSession.withOneSubtask": "\"{sessionTitle}\" і його підзавдання {count} буде заархівовано.", "sessions.sidebar.dialogs.archiveSession.withOneSubtask": "\"{sessionTitle}\" і його підзавдання {count} буде заархівовано.",
@@ -581,7 +581,7 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.dialogs.archiveSession.single": "\"{sessionTitle}\" буде заархівовано.", "sessions.sidebar.dialogs.archiveSession.single": "\"{sessionTitle}\" буде заархівовано.",
"sessions.sidebar.dialogs.neverAsk": "Більше не питати", "sessions.sidebar.dialogs.neverAsk": "Більше не питати",
"sessions.sidebar.dialogs.cancel": "Скасувати", "sessions.sidebar.dialogs.cancel": "Скасувати",
"sessions.sidebar.dialogs.deleteSession.titleAction": "Видалити сесія", "sessions.sidebar.dialogs.deleteSession.titleAction": "Видалити сесію",
"sessions.sidebar.dialogs.deleteSessions.titleAction": "Видалити сесії", "sessions.sidebar.dialogs.deleteSessions.titleAction": "Видалити сесії",
"sessions.sidebar.dialogs.deleteSessions.title": "Видалити сесії?", "sessions.sidebar.dialogs.deleteSessions.title": "Видалити сесії?",
"sessions.sidebar.dialogs.archiveSessions.title": "Архівувати сесії?", "sessions.sidebar.dialogs.archiveSessions.title": "Архівувати сесії?",
@@ -661,7 +661,7 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.folderItem.deleteFolderAria": "Видалити папку {folderName}", "sessions.sidebar.folderItem.deleteFolderAria": "Видалити папку {folderName}",
"sessions.sidebar.folderItem.emptyFolder": "Порожня папка", "sessions.sidebar.folderItem.emptyFolder": "Порожня папка",
"sessions.sidebar.sessionDialogs.ok": "OK", "sessions.sidebar.sessionDialogs.ok": "OK",
"sessions.sidebar.sessionDialogs.linkedSessionSingle": "Пов’язаний сесія", "sessions.sidebar.sessionDialogs.linkedSessionSingle": "Пов’язана сесія",
"sessions.sidebar.sessionDialogs.linkedSessionPlural": "Пов’язані сесії", "sessions.sidebar.sessionDialogs.linkedSessionPlural": "Пов’язані сесії",
"sessions.sidebar.sessionDialogs.delete.note": "Каталоги worktree залишаються недоторканими. Підсесії, пов’язані з вибраними сесіями, також буде видалено.", "sessions.sidebar.sessionDialogs.delete.note": "Каталоги worktree залишаються недоторканими. Підсесії, пов’язані з вибраними сесіями, також буде видалено.",
"sessions.sidebar.sessionDialogs.directory.errorSelectTitle": "Не вдалося вибрати каталог", "sessions.sidebar.sessionDialogs.directory.errorSelectTitle": "Не вдалося вибрати каталог",
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.context": "Контекст", "contextPanel.mode.context": "Контекст",
"contextPanel.mode.preview": "Перегляд", "contextPanel.mode.preview": "Перегляд",
"contextPanel.mode.browser": "Браузер", "contextPanel.mode.browser": "Браузер",
"contextRail.configure.open": "Налаштувати панелі",
"contextRail.configure.dialogTitle": "Панелі рейки",
"contextRail.configure.dialogDescription": "Обери, які панелі показує рейка. Приховані панелі зберігають дані й доступні з палітри команд.",
"contextRail.configure.showAll": "Показати всі",
"contextRail.configure.noneWarning": "Усі панелі приховано.",
"contextRail.aria.rail": "Поверхні панелі", "contextRail.aria.rail": "Поверхні панелі",
"contextPanel.editorEmpty.title": "Файл не відкрито", "contextPanel.editorEmpty.title": "Файл не відкрито",
"contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.", "contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.",
@@ -1646,7 +1651,7 @@ export const dict: Record<I18nKey, string> = {
"rightSidebar.contextNotesTodo.toast.planImported": "План імпортовано", "rightSidebar.contextNotesTodo.toast.planImported": "План імпортовано",
"rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Не вдалося прочитати файл плану", "rightSidebar.contextNotesTodo.toast.readPlanFileFailed": "Не вдалося прочитати файл плану",
"inlineComment.range.lines": "Рядки {start}-{end}", "inlineComment.range.lines": "Рядки {start}-{end}",
"inlineComment.input.placeholder": "Додайте коментар... (Cmd+Enter, щоб зберегти)", "inlineComment.input.placeholder": "Додайте коментар... ({shortcut}, щоб зберегти)",
"inlineComment.input.placeholderShort": "Додайте коментар...", "inlineComment.input.placeholderShort": "Додайте коментар...",
"inlineComment.actions.cancel": "Скасувати", "inlineComment.actions.cancel": "Скасувати",
"inlineComment.actions.save": "Зберегти", "inlineComment.actions.save": "Зберегти",
@@ -1836,22 +1841,18 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.focusChatInput": "Фокус на полі вводу чату", "helpDialog.item.focusChatInput": "Фокус на полі вводу чату",
"helpDialog.item.togglePromptNavigator": "Показати або приховати навігатор промптів", "helpDialog.item.togglePromptNavigator": "Показати або приховати навігатор промптів",
"helpDialog.item.abortActiveRun": "Перервати активний запуск (подвійне натискання)", "helpDialog.item.abortActiveRun": "Перервати активний запуск (подвійне натискання)",
"helpDialog.item.toggleRightSidebar": 'Перемкнути контекстну панель',
"helpDialog.item.openRightSidebarGitTab": 'Відкрити поверхню Git',
"helpDialog.item.openRightSidebarFilesTab": 'Відкрити поверхню файлів',
"helpDialog.item.toggleTerminalDock": "Перемкнути панель терміналу", "helpDialog.item.toggleTerminalDock": "Перемкнути панель терміналу",
"helpDialog.item.toggleTerminalExpanded": "Розгорнути або згорнути термінал", "helpDialog.item.toggleTerminalExpanded": "Розгорнути або згорнути термінал",
"helpDialog.item.togglePlanContextPanel": "Перемкнути панель контексту плану",
"helpDialog.item.cycleTheme": "Перемкнути тему (Світла → Темна → Системна)", "helpDialog.item.cycleTheme": "Перемкнути тему (Світла → Темна → Системна)",
"helpDialog.item.switchSessionTab": "Перемкнути вкладку сесії",
"helpDialog.item.switchContextSurface": "Перемкнути поверхню панелі контексту (цифрова клавіша)", "helpDialog.item.switchContextSurface": "Перемкнути поверхню панелі контексту (цифрова клавіша)",
"helpDialog.item.toggleServicesMenu": "Перемкнути меню сервісів", "helpDialog.item.toggleServicesMenu": "Перемкнути меню сервісів",
"helpDialog.item.cycleServicesTab": "Перемкнути вкладку сервісів",
"helpDialog.item.openSettings": "Відкрити налаштування", "helpDialog.item.openSettings": "Відкрити налаштування",
"helpDialog.keyCombiner.or": "або", "helpDialog.keyCombiner.or": "або",
"helpDialog.proTips.title": "Поради:", "helpDialog.proTips.title": "Поради:",
"helpDialog.proTips.commandPalette": "Використовуйте палітру команд ({shortcut}), щоб швидко перейти до будь-якої дії", "helpDialog.proTips.commandPalette": "Використовуйте палітру команд ({shortcut}), щоб швидко перейти до будь-якої дії",
"helpDialog.proTips.recentSessions": "5 останніх сесій відображаються на панелі команд", "helpDialog.proTips.recentSessions": "5 останніх сесій відображаються на панелі команд",
"helpDialog.proTips.themeCycling": "Перемикання теми запам’ятовує ваші переваги протягом сесій", "helpDialog.proTips.leaderSequences": "Двокрокові шорткати: натисни комбінацію, потім другу клавішу — Esc скасовує",
"header.actions.rightSidebarWithShortcut": "Права бічна панель ({shortcut})", "header.actions.rightSidebarWithShortcut": "Права бічна панель ({shortcut})",
"header.actions.toggleRightSidebarAria": "Перемкнути праву бічну панель", "header.actions.toggleRightSidebarAria": "Перемкнути праву бічну панель",
"header.actions.openAppMenu": "Меню OpenChamber", "header.actions.openAppMenu": "Меню OpenChamber",
@@ -2061,7 +2062,6 @@ export const dict: Record<I18nKey, string> = {
"chat.statusRow.tasksTitle": "завдання", "chat.statusRow.tasksTitle": "завдання",
"chat.statusRow.modelStatus": "{model} · {status}", "chat.statusRow.modelStatus": "{model} · {status}",
"chat.statusRow.summary.activeLeft": "Активних: {active} · залишилось: {left}", "chat.statusRow.summary.activeLeft": "Активних: {active} · залишилось: {left}",
"chat.statusRow.aborted": "Перервано",
"chat.revertIndicator.redo": "Повторити", "chat.revertIndicator.redo": "Повторити",
"chat.revertIndicator.redoAria": "Повторити — відновити відкочені повідомлення", "chat.revertIndicator.redoAria": "Повторити — відновити відкочені повідомлення",
"chat.revertPopover.title": "Відкочено", "chat.revertPopover.title": "Відкочено",
@@ -2139,7 +2139,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': 'Не вдалося залишити сесію btw', 'chat.btw.toast.promoteFailed': 'Не вдалося залишити сесію btw',
"chat.container.readOnlySubagentPromptBanner": "Сесії субагентів не можна запитувати.", "chat.container.readOnlySubagentPromptBanner": "Сесії субагентів не можна запитувати.",
"chat.container.sessionLoadError.title": "Не вдалося завантажити сесію", "chat.container.sessionLoadError.title": "Не вдалося завантажити сесію",
"chat.container.sessionLoadError.description": "Перевірте з’єднання та спробуйте завантажити цю сесію ще раз.", "chat.container.sessionLoadError.description": "Не вдалося отримати розмову — сервер може бути вимкнений або недосяжний. Нічого не втрачено; спробуй знову, коли він повернеться.",
"chat.container.sessionLoadError.authDescription": "Сесія завершилась, тож сервер відхилив запит. Увійди — і розмова завантажиться.",
"chat.container.sessionLoadError.retry": "Спробувати знову", "chat.container.sessionLoadError.retry": "Спробувати знову",
"sessions.sidebar.group.empty.loadingSessions": "Завантаження сесій…", "sessions.sidebar.group.empty.loadingSessions": "Завантаження сесій…",
"sessions.sidebar.group.empty.loadFailed": "Не вдалося оновити сесії.", "sessions.sidebar.group.empty.loadFailed": "Не вдалося оновити сесії.",
@@ -2182,10 +2183,8 @@ export const dict: Record<I18nKey, string> = {
"chat.textSelection.title.commentOnSelection": "Коментувати виділене", "chat.textSelection.title.commentOnSelection": "Коментувати виділене",
"chat.textSelection.comment.placeholder": "Додайте коментар за бажанням...", "chat.textSelection.comment.placeholder": "Додайте коментар за бажанням...",
"chat.textSelection.comment.attach": "Прикріпити", "chat.textSelection.comment.attach": "Прикріпити",
"chat.textSelection.actions.newSession": "Нова сесія",
"chat.textSelection.actions.addToNotes": "Додати до нотаток", "chat.textSelection.actions.addToNotes": "Додати до нотаток",
"chat.textSelection.title.addToCurrentChat": "Додати до поточного чату", "chat.textSelection.title.addToCurrentChat": "Додати до поточного чату",
"chat.textSelection.title.newSessionWithSelection": "Створити нову сесію із виділенням",
"chat.textSelection.title.saveInsightToNotes": "Зберегти вибраний текст у нотатках", "chat.textSelection.title.saveInsightToNotes": "Зберегти вибраний текст у нотатках",
"chat.messageBody.actions.revertAria": "Повернутися до цього повідомлення", "chat.messageBody.actions.revertAria": "Повернутися до цього повідомлення",
"chat.messageBody.actions.revert": "Повернутися звідси", "chat.messageBody.actions.revert": "Повернутися звідси",
@@ -2430,7 +2429,7 @@ export const dict: Record<I18nKey, string> = {
"chat.messageBody.subtask.title": "Делеговане завдання", "chat.messageBody.subtask.title": "Делеговане завдання",
"chat.messageBody.subtask.hidePrompt": "Приховати промпт", "chat.messageBody.subtask.hidePrompt": "Приховати промпт",
"chat.messageBody.subtask.showPrompt": "Показати промпт", "chat.messageBody.subtask.showPrompt": "Показати промпт",
"chat.messageBody.subtask.openSession": "Відкрити сесія підзавдання", "chat.messageBody.subtask.openSession": "Відкрити сесію підзавдання",
"chat.messageBody.shellCommand.title": "Команда оболонки", "chat.messageBody.shellCommand.title": "Команда оболонки",
"chat.messageBody.shellCommand.hideOutput": "Приховати вивід", "chat.messageBody.shellCommand.hideOutput": "Приховати вивід",
"chat.messageBody.shellCommand.showOutput": "Показати результат", "chat.messageBody.shellCommand.showOutput": "Показати результат",
@@ -2449,6 +2448,15 @@ export const dict: Record<I18nKey, string> = {
"commandPalette.item.toggleSidebar": "Перемкнути бічну панель", "commandPalette.item.toggleSidebar": "Перемкнути бічну панель",
"commandPalette.item.showContextUsage": "Показати використання контексту", "commandPalette.item.showContextUsage": "Показати використання контексту",
"commandPalette.item.toggleTerminal": "Перемкнути термінал", "commandPalette.item.toggleTerminal": "Перемкнути термінал",
"commandPalette.item.cycleTheme": "Перемкнути тему",
"commandPalette.item.showOpenCodeStatus": "Показати статус OpenCode",
"commandPalette.item.toggleMemoryDebug": "Показати/сховати панель memory debug",
"commandPalette.item.pinSession": "Прикріпити або відкріпити сесію",
"commandPalette.item.copySessionId": "Скопіювати ID сесії",
"commandPalette.item.openMultiRun": "Відкрити лаунчер multi-run",
"commandPalette.item.openArchive": "Відкрити архівовані сесії",
"commandPalette.item.openNotes": "Відкрити панель нотаток",
"commandPalette.item.openTodos": "Відкрити панель завдань",
"commandPalette.item.openSettings": "Відкрити налаштування...", "commandPalette.item.openSettings": "Відкрити налаштування...",
"commandPalette.session.untitled": "Сесія без назви", "commandPalette.session.untitled": "Сесія без назви",
"openCodeStatusDialog.title": "Статус OpenCode", "openCodeStatusDialog.title": "Статус OpenCode",
@@ -2663,6 +2671,9 @@ export const dict: Record<I18nKey, string> = {
"sessionAuth.error.passkeySignInCanceled": "Вхід за ключем доступу скасовано.", "sessionAuth.error.passkeySignInCanceled": "Вхід за ключем доступу скасовано.",
"sessionAuth.error.enterPasswordForPasskey": "Введіть пароль, щоб додати ключ доступу.", "sessionAuth.error.enterPasswordForPasskey": "Введіть пароль, щоб додати ключ доступу.",
"sessionAuth.locked.tunnelTitle": "Потрібен доступ до тунелю", "sessionAuth.locked.tunnelTitle": "Потрібен доступ до тунелю",
"sessionAuth.expired.banner": "Сесія завершилась — увійди, щоб продовжити.",
"sessionAuth.expired.loginAction": "Увійти",
"sessionAuth.expired.sendBlocked": "Сесія завершилась — увійди, щоб надсилати повідомлення.",
"sessionAuth.locked.unlockTitle": "Розблокувати OpenChamber", "sessionAuth.locked.unlockTitle": "Розблокувати OpenChamber",
"sessionAuth.locked.tunnelDescription": "Відкрийте цей тунель за допомогою одноразового посилання для з’єднання з настільної програми.", "sessionAuth.locked.tunnelDescription": "Відкрийте цей тунель за допомогою одноразового посилання для з’єднання з настільної програми.",
"sessionAuth.locked.passwordDescription": "Ця сесія захищена паролем.", "sessionAuth.locked.passwordDescription": "Ця сесія захищена паролем.",
@@ -1101,7 +1101,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': '该组合已被其他快捷键使用。是否覆盖并清除原映射?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': '该组合已被其他快捷键使用。是否覆盖并清除原映射?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': '按下按键...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': '按下按键...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': '请先录入一个快捷键。', 'settings.openchamber.keyboardShortcuts.error.captureFirst': '请先录入一个快捷键。',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '该快捷键可能与浏览器默认快捷键冲突,但仍保存。', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '该快捷键可能与浏览器默认快捷键冲突,但仍保存。',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '跳转到行(文件编辑器)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '跳转到行(文件编辑器)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '打开命令面板', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '打开命令面板',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '聚焦输入框', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': '聚焦输入框',
@@ -1110,18 +1110,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切换终端展开', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切换终端展开',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '将选中内容添加到聊天', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '将选中内容添加到聊天',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切换侧边栏', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切换侧边栏',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切换上下文面板', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '切换会话标签页',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '打开 Git 界面', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '打开文件界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一个会话',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一个会话',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重命名当前会话',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切换权限自动批准',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '关闭会话标签页',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '打开键盘快捷键', 'settings.openchamber.keyboardShortcuts.action.open_help.label': '打开键盘快捷键',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '切换上下文面板中的计划',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切换服务菜单', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切换服务菜单',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '轮换服务菜单标签',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '轮换主题', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '轮换主题',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '轮换智能体', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '轮换智能体',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前轮换收藏模型', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前轮换收藏模型',
@@ -1130,6 +1132,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展开输入框', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展开输入框',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '打开对话时间线', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '打开对话时间线',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '显示或隐藏提示词导航', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '显示或隐藏提示词导航',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': '此序列与“{action}”共享上下文前缀。对应上下文生效时,该操作会优先执行。',
'settings.openchamber.keyboardShortcuts.category.session': '会话控制',
'settings.openchamber.keyboardShortcuts.category.models': '模型和智能体',
'settings.openchamber.keyboardShortcuts.category.panels': '面板和工具',
'settings.openchamber.keyboardShortcuts.category.navigation': '导航',
'settings.openchamber.keyboardShortcuts.category.application': '应用程序',
'settings.openchamber.keyboardShortcuts.actions.edit': '编辑',
'settings.openchamber.keyboardShortcuts.actions.confirm': '确认',
'settings.openchamber.keyboardShortcuts.dialog.title': '编辑{action}',
'settings.openchamber.keyboardShortcuts.dialog.instructions': '最多输入两个按键组合,每个组合最多同时按下三个按键。输入第一个组合后,最多等待 3 秒以输入第二个组合。点击确认应用,或点击取消放弃;按 Backspace 删除最后一个组合。',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': '第一个组合',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': '第二个组合',
'settings.openchamber.keyboardShortcuts.dialog.recording': '按下按键…',
'settings.openchamber.keyboardShortcuts.unassigned': '未分配',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': '这与 {action} 使用的序列冲突。请选择其他组合。',
'settings.openchamber.keyboardShortcuts.error.exactConflict': '此组合已被 {action} 使用。',
'settings.openchamber.keyboardShortcuts.error.internalConflict': '此组合与内置快捷键冲突,内置快捷键不能被替换。',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': '打开草稿项目选择器',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': '打开草稿工作树选择器',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': '打开最近会话',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': '语音输入',
'settings.projects.sidebar.total': '总计 {count}', 'settings.projects.sidebar.total': '总计 {count}',
'settings.projects.sidebar.actions.addProject': '添加项目', 'settings.projects.sidebar.actions.addProject': '添加项目',
'settings.projects.page.empty.noProjects': '暂无项目。', 'settings.projects.page.empty.noProjects': '暂无项目。',
+22 -11
View File
@@ -1140,6 +1140,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '上下文', 'contextPanel.mode.context': '上下文',
'contextPanel.mode.preview': '预览', 'contextPanel.mode.preview': '预览',
'contextPanel.mode.browser': '浏览器', 'contextPanel.mode.browser': '浏览器',
'contextRail.configure.open': '配置面板',
'contextRail.configure.dialogTitle': '侧栏面板',
'contextRail.configure.dialogDescription': '选择侧栏显示哪些面板。隐藏的面板会保留数据,仍可通过命令面板打开。',
'contextRail.configure.showAll': '全部显示',
'contextRail.configure.noneWarning': '所有面板均已隐藏。',
'contextRail.aria.rail': '面板界面', 'contextRail.aria.rail': '面板界面',
'contextPanel.editorEmpty.title': '未打开文件', 'contextPanel.editorEmpty.title': '未打开文件',
'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。', 'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。',
@@ -1634,7 +1639,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '计划已导入', 'rightSidebar.contextNotesTodo.toast.planImported': '计划已导入',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '读取计划文件失败', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '读取计划文件失败',
'inlineComment.range.lines': '行 {start}-{end}', 'inlineComment.range.lines': '行 {start}-{end}',
'inlineComment.input.placeholder': '添加评论...Cmd+Enter 保存)', 'inlineComment.input.placeholder': '添加评论...{shortcut} 保存)',
'inlineComment.input.placeholderShort': '添加评论…', 'inlineComment.input.placeholderShort': '添加评论…',
'inlineComment.actions.cancel': '取消', 'inlineComment.actions.cancel': '取消',
'inlineComment.actions.save': '保存', 'inlineComment.actions.save': '保存',
@@ -1824,22 +1829,18 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.focusChatInput': '聚焦聊天输入框', 'helpDialog.item.focusChatInput': '聚焦聊天输入框',
'helpDialog.item.togglePromptNavigator': '显示或隐藏提示词导航', 'helpDialog.item.togglePromptNavigator': '显示或隐藏提示词导航',
'helpDialog.item.abortActiveRun': '中止当前运行(双击)', 'helpDialog.item.abortActiveRun': '中止当前运行(双击)',
'helpDialog.item.toggleRightSidebar': '切换上下文面板',
'helpDialog.item.openRightSidebarGitTab': '打开 Git 界面',
'helpDialog.item.openRightSidebarFilesTab': '打开文件界面',
'helpDialog.item.toggleTerminalDock': '切换终端停靠栏', 'helpDialog.item.toggleTerminalDock': '切换终端停靠栏',
'helpDialog.item.toggleTerminalExpanded': '切换终端展开状态', 'helpDialog.item.toggleTerminalExpanded': '切换终端展开状态',
'helpDialog.item.togglePlanContextPanel': '切换计划上下文面板',
'helpDialog.item.cycleTheme': '循环切换主题(浅色 → 深色 → 跟随系统)', 'helpDialog.item.cycleTheme': '循环切换主题(浅色 → 深色 → 跟随系统)',
'helpDialog.item.switchSessionTab': '切换会话标签页',
'helpDialog.item.switchContextSurface': '切换上下文面板界面(数字键)', 'helpDialog.item.switchContextSurface': '切换上下文面板界面(数字键)',
'helpDialog.item.toggleServicesMenu': '切换服务菜单', 'helpDialog.item.toggleServicesMenu': '切换服务菜单',
'helpDialog.item.cycleServicesTab': '循环服务标签',
'helpDialog.item.openSettings': '打开设置', 'helpDialog.item.openSettings': '打开设置',
'helpDialog.keyCombiner.or': '或', 'helpDialog.keyCombiner.or': '或',
'helpDialog.proTips.title': '使用提示:', 'helpDialog.proTips.title': '使用提示:',
'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速访问所有操作', 'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速访问所有操作',
'helpDialog.proTips.recentSessions': '最近 5 个会话会显示在命令面板中', 'helpDialog.proTips.recentSessions': '最近 5 个会话会显示在命令面板中',
'helpDialog.proTips.themeCycling': '主题循环会记住你在各会话中的偏好', 'helpDialog.proTips.leaderSequences': '两段式快捷键:先按组合键,再按第二个键(Esc 取消)',
'header.actions.rightSidebarWithShortcut': '右侧边栏({shortcut}', 'header.actions.rightSidebarWithShortcut': '右侧边栏({shortcut}',
'header.actions.toggleRightSidebarAria': '切换右侧边栏', 'header.actions.toggleRightSidebarAria': '切换右侧边栏',
'header.actions.openAppMenu': 'OpenChamber 菜单', 'header.actions.openAppMenu': 'OpenChamber 菜单',
@@ -2049,7 +2050,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '任务', 'chat.statusRow.tasksTitle': '任务',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} 个活跃 · 剩余 {left} 个', 'chat.statusRow.summary.activeLeft': '{active} 个活跃 · 剩余 {left} 个',
'chat.statusRow.aborted': '已中止',
'chat.revertIndicator.redo': '重做', 'chat.revertIndicator.redo': '重做',
'chat.revertIndicator.redoAria': '重做 — 恢复已撤回的消息', 'chat.revertIndicator.redoAria': '重做 — 恢复已撤回的消息',
'chat.revertPopover.title': '已撤回', 'chat.revertPopover.title': '已撤回',
@@ -2127,7 +2127,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': '保留 btw 会话失败', 'chat.btw.toast.promoteFailed': '保留 btw 会话失败',
'chat.container.readOnlySubagentPromptBanner': '无法向子智能体会话发送提示。', 'chat.container.readOnlySubagentPromptBanner': '无法向子智能体会话发送提示。',
'chat.container.sessionLoadError.title': '无法加载会话', 'chat.container.sessionLoadError.title': '无法加载会话',
'chat.container.sessionLoadError.description': '请检查连接,然后重新加载此会话。', 'chat.container.sessionLoadError.description': '无法获取对话——服务器可能已关闭或无法访问。内容没有丢失;等它恢复后重试即可。',
'chat.container.sessionLoadError.authDescription': '会话已过期,服务器拒绝了请求。登录后对话即会加载。',
'chat.container.sessionLoadError.retry': '重试', 'chat.container.sessionLoadError.retry': '重试',
'sessions.sidebar.group.empty.loadingSessions': '正在加载会话…', 'sessions.sidebar.group.empty.loadingSessions': '正在加载会话…',
'sessions.sidebar.group.empty.loadFailed': '无法刷新会话。', 'sessions.sidebar.group.empty.loadFailed': '无法刷新会话。',
@@ -2170,10 +2171,8 @@ export const dict: Record<I18nKey, string> = {
'chat.textSelection.title.commentOnSelection': '评论所选内容', 'chat.textSelection.title.commentOnSelection': '评论所选内容',
'chat.textSelection.comment.placeholder': '添加可选评论...', 'chat.textSelection.comment.placeholder': '添加可选评论...',
'chat.textSelection.comment.attach': '附加', 'chat.textSelection.comment.attach': '附加',
'chat.textSelection.actions.newSession': '新建会话',
'chat.textSelection.actions.addToNotes': '添加到笔记', 'chat.textSelection.actions.addToNotes': '添加到笔记',
'chat.textSelection.title.addToCurrentChat': '添加到当前聊天', 'chat.textSelection.title.addToCurrentChat': '添加到当前聊天',
'chat.textSelection.title.newSessionWithSelection': '使用选中内容创建新会话',
'chat.textSelection.title.saveInsightToNotes': '将选中文本保存到笔记', 'chat.textSelection.title.saveInsightToNotes': '将选中文本保存到笔记',
'chat.messageBody.actions.revertAria': '回退到这条消息', 'chat.messageBody.actions.revertAria': '回退到这条消息',
'chat.messageBody.actions.revert': '从此处回退', 'chat.messageBody.actions.revert': '从此处回退',
@@ -2449,6 +2448,15 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.toggleSidebar': '切换侧边栏', 'commandPalette.item.toggleSidebar': '切换侧边栏',
'commandPalette.item.showContextUsage': '显示上下文用量', 'commandPalette.item.showContextUsage': '显示上下文用量',
'commandPalette.item.toggleTerminal': '切换终端', 'commandPalette.item.toggleTerminal': '切换终端',
'commandPalette.item.cycleTheme': '轮换主题',
'commandPalette.item.showOpenCodeStatus': '显示 OpenCode 状态',
'commandPalette.item.toggleMemoryDebug': '切换内存调试面板',
'commandPalette.item.pinSession': '固定或取消固定会话',
'commandPalette.item.copySessionId': '复制会话 ID',
'commandPalette.item.openMultiRun': '打开多任务启动器',
'commandPalette.item.openArchive': '打开已归档会话',
'commandPalette.item.openNotes': '打开笔记面板',
'commandPalette.item.openTodos': '打开待办面板',
'commandPalette.item.openSettings': '打开设置...', 'commandPalette.item.openSettings': '打开设置...',
'commandPalette.session.untitled': '未命名会话', 'commandPalette.session.untitled': '未命名会话',
'openCodeStatusDialog.title': 'OpenCode 状态', 'openCodeStatusDialog.title': 'OpenCode 状态',
@@ -2663,6 +2671,9 @@ export const dict: Record<I18nKey, string> = {
'sessionAuth.error.passkeySignInCanceled': 'Passkey 登录已取消。', 'sessionAuth.error.passkeySignInCanceled': 'Passkey 登录已取消。',
'sessionAuth.error.enterPasswordForPasskey': '请输入密码以添加 passkey。', 'sessionAuth.error.enterPasswordForPasskey': '请输入密码以添加 passkey。',
'sessionAuth.locked.tunnelTitle': '需要隧道访问', 'sessionAuth.locked.tunnelTitle': '需要隧道访问',
'sessionAuth.expired.banner': '会话已过期——请登录以继续。',
'sessionAuth.expired.loginAction': '登录',
'sessionAuth.expired.sendBlocked': '会话已过期——请登录后再发送消息。',
'sessionAuth.locked.unlockTitle': '解锁 OpenChamber', 'sessionAuth.locked.unlockTitle': '解锁 OpenChamber',
'sessionAuth.locked.tunnelDescription': '请使用桌面应用提供的一次性连接链接打开该隧道。', 'sessionAuth.locked.tunnelDescription': '请使用桌面应用提供的一次性连接链接打开该隧道。',
'sessionAuth.locked.passwordDescription': '此会话受密码保护。', 'sessionAuth.locked.passwordDescription': '此会话受密码保护。',
@@ -1008,7 +1008,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.overwritePrompt': '該組合已被其他快速鍵使用。是否覆寫並清除原對應?', 'settings.openchamber.keyboardShortcuts.overwritePrompt': '該組合已被其他快速鍵使用。是否覆寫並清除原對應?',
'settings.openchamber.keyboardShortcuts.field.pressKeys': '按下按鍵...', 'settings.openchamber.keyboardShortcuts.field.pressKeys': '按下按鍵...',
'settings.openchamber.keyboardShortcuts.error.captureFirst': '請先錄入一個快速鍵。', 'settings.openchamber.keyboardShortcuts.error.captureFirst': '請先錄入一個快速鍵。',
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '該快速鍵可能與瀏覽器預設快速鍵衝突,但仍儲存。', 'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '該快速鍵可能與瀏覽器預設快速鍵衝突,但仍儲存。',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '跳轉到行(檔案編輯器)', 'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '跳轉到行(檔案編輯器)',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '開啟命令面板', 'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '開啟命令面板',
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '聚焦輸入方塊', 'settings.openchamber.keyboardShortcuts.action.focus_input.label': '聚焦輸入方塊',
@@ -1017,18 +1017,20 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切換終端機展開', 'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切換終端機展開',
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '將選取內容加入聊天', 'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '將選取內容加入聊天',
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切換側邊欄', 'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切換側邊欄',
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切換上下文面板', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.label': '切換工作階段分頁',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '開啟 Git 介面', 'settings.openchamber.keyboardShortcuts.action.switch_session_tab.suffix': ' + 1…9',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '開啟檔案介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0', 'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段', 'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段',
'settings.openchamber.keyboardShortcuts.action.switch_session_previous.label': '上一個工作階段',
'settings.openchamber.keyboardShortcuts.action.switch_session_next.label': '下一個工作階段',
'settings.openchamber.keyboardShortcuts.action.rename_current_session.label': '重新命名目前的工作階段',
'settings.openchamber.keyboardShortcuts.action.toggle_permission_auto_accept.label': '切換權限自動核准',
'settings.openchamber.keyboardShortcuts.action.close_session_tab.label': '關閉工作階段分頁',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿', 'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗', 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '開啟鍵盤快速鍵', 'settings.openchamber.keyboardShortcuts.action.open_help.label': '開啟鍵盤快速鍵',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '切換上下文面板中的計畫',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切換服務選單', 'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切換服務選單',
'settings.openchamber.keyboardShortcuts.action.cycle_services_tab.label': '輪換服務選單分頁',
'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '輪換主題', 'settings.openchamber.keyboardShortcuts.action.cycle_theme.label': '輪換主題',
'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '輪換 agent', 'settings.openchamber.keyboardShortcuts.action.cycle_agent.label': '輪換 agent',
'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前輪換收藏模型', 'settings.openchamber.keyboardShortcuts.action.cycle_favorite_model_forward.label': '向前輪換收藏模型',
@@ -1037,6 +1039,27 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展開輸入方塊', 'settings.openchamber.keyboardShortcuts.action.expand_input.label': '展開輸入方塊',
'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '開啟對話時間軸', 'settings.openchamber.keyboardShortcuts.action.open_timeline_dialog.label': '開啟對話時間軸',
'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '顯示或隱藏提示詞導覽', 'settings.openchamber.keyboardShortcuts.action.toggle_prompt_navigator.label': '顯示或隱藏提示詞導覽',
'settings.openchamber.keyboardShortcuts.warning.contextualPrefix': '此序列與「{action}」共用情境前綴。對應情境生效時,該操作會優先執行。',
'settings.openchamber.keyboardShortcuts.category.session': '工作階段控制',
'settings.openchamber.keyboardShortcuts.category.models': '模型與代理',
'settings.openchamber.keyboardShortcuts.category.panels': '面板與工具',
'settings.openchamber.keyboardShortcuts.category.navigation': '導覽',
'settings.openchamber.keyboardShortcuts.category.application': '應用程式',
'settings.openchamber.keyboardShortcuts.actions.edit': '編輯',
'settings.openchamber.keyboardShortcuts.actions.confirm': '確認',
'settings.openchamber.keyboardShortcuts.dialog.title': '編輯 {action}',
'settings.openchamber.keyboardShortcuts.dialog.instructions': '最多輸入兩個按鍵組合,每個組合最多同時按下三個按鍵。輸入第一個組合後,最多等待 3 秒以輸入第二個組合。點擊確認套用,或點擊取消放棄;按 Backspace 刪除最後一個組合。',
'settings.openchamber.keyboardShortcuts.dialog.firstChord': '第一個組合',
'settings.openchamber.keyboardShortcuts.dialog.secondChord': '第二個組合',
'settings.openchamber.keyboardShortcuts.dialog.recording': '按下按鍵…',
'settings.openchamber.keyboardShortcuts.unassigned': '未指派',
'settings.openchamber.keyboardShortcuts.error.prefixConflict': '這與 {action} 使用的序列衝突。請選擇其他組合。',
'settings.openchamber.keyboardShortcuts.error.exactConflict': '此組合已由 {action} 使用。',
'settings.openchamber.keyboardShortcuts.error.internalConflict': '此組合與內建快捷鍵衝突,內建快捷鍵不能被取代。',
'settings.openchamber.keyboardShortcuts.action.open_draft_project_picker.label': '開啟草稿專案選擇器',
'settings.openchamber.keyboardShortcuts.action.open_draft_worktree_picker.label': '開啟草稿 worktree 選擇器',
'settings.openchamber.keyboardShortcuts.action.open_session_list.label': '開啟最近工作階段',
'settings.openchamber.keyboardShortcuts.action.toggle_dictation.label': '語音輸入',
'settings.projects.sidebar.total': '總計 {count}', 'settings.projects.sidebar.total': '總計 {count}',
'settings.projects.sidebar.actions.addProject': '新增專案', 'settings.projects.sidebar.actions.addProject': '新增專案',
'settings.projects.page.empty.noProjects': '暫無專案。', 'settings.projects.page.empty.noProjects': '暫無專案。',
+22 -11
View File
@@ -1152,6 +1152,11 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.context': '上下文', 'contextPanel.mode.context': '上下文',
'contextPanel.mode.preview': '預覽', 'contextPanel.mode.preview': '預覽',
'contextPanel.mode.browser': '瀏覽器', 'contextPanel.mode.browser': '瀏覽器',
'contextRail.configure.open': '設定面板',
'contextRail.configure.dialogTitle': '側欄面板',
'contextRail.configure.dialogDescription': '選擇側欄顯示哪些面板。隱藏的面板會保留資料,仍可透過命令面板開啟。',
'contextRail.configure.showAll': '全部顯示',
'contextRail.configure.noneWarning': '所有面板皆已隱藏。',
'contextRail.aria.rail': '面板介面', 'contextRail.aria.rail': '面板介面',
'contextPanel.editorEmpty.title': '未開啟檔案', 'contextPanel.editorEmpty.title': '未開啟檔案',
'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。', 'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。',
@@ -1644,7 +1649,7 @@ export const dict: Record<I18nKey, string> = {
'rightSidebar.contextNotesTodo.toast.planImported': '計畫已匯入', 'rightSidebar.contextNotesTodo.toast.planImported': '計畫已匯入',
'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '讀取計畫檔案失敗', 'rightSidebar.contextNotesTodo.toast.readPlanFileFailed': '讀取計畫檔案失敗',
'inlineComment.range.lines': '行 {start}-{end}', 'inlineComment.range.lines': '行 {start}-{end}',
'inlineComment.input.placeholder': '新增留言...Cmd+Enter 儲存)', 'inlineComment.input.placeholder': '新增留言...{shortcut} 儲存)',
'inlineComment.input.placeholderShort': '新增留言…', 'inlineComment.input.placeholderShort': '新增留言…',
'inlineComment.actions.cancel': '取消', 'inlineComment.actions.cancel': '取消',
'inlineComment.actions.save': '儲存', 'inlineComment.actions.save': '儲存',
@@ -1828,22 +1833,18 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.focusChatInput': '聚焦聊天輸入框', 'helpDialog.item.focusChatInput': '聚焦聊天輸入框',
'helpDialog.item.togglePromptNavigator': '顯示或隱藏提示詞導覽', 'helpDialog.item.togglePromptNavigator': '顯示或隱藏提示詞導覽',
'helpDialog.item.abortActiveRun': '中止目前執行(連按兩下)', 'helpDialog.item.abortActiveRun': '中止目前執行(連按兩下)',
'helpDialog.item.toggleRightSidebar': '切換上下文面板',
'helpDialog.item.openRightSidebarGitTab': '開啟 Git 介面',
'helpDialog.item.openRightSidebarFilesTab': '開啟檔案介面',
'helpDialog.item.toggleTerminalDock': '切換終端機停靠欄', 'helpDialog.item.toggleTerminalDock': '切換終端機停靠欄',
'helpDialog.item.toggleTerminalExpanded': '切換終端機展開狀態', 'helpDialog.item.toggleTerminalExpanded': '切換終端機展開狀態',
'helpDialog.item.togglePlanContextPanel': '切換計畫上下文面板',
'helpDialog.item.cycleTheme': '循環切換主題(淺色 → 深色 → 跟隨系統)', 'helpDialog.item.cycleTheme': '循環切換主題(淺色 → 深色 → 跟隨系統)',
'helpDialog.item.switchSessionTab': '切換工作階段分頁',
'helpDialog.item.switchContextSurface': '切換上下文面板介面(數字鍵)', 'helpDialog.item.switchContextSurface': '切換上下文面板介面(數字鍵)',
'helpDialog.item.toggleServicesMenu': '切換服務選單', 'helpDialog.item.toggleServicesMenu': '切換服務選單',
'helpDialog.item.cycleServicesTab': '循環服務標籤',
'helpDialog.item.openSettings': '開啟設定', 'helpDialog.item.openSettings': '開啟設定',
'helpDialog.keyCombiner.or': '或', 'helpDialog.keyCombiner.or': '或',
'helpDialog.proTips.title': '使用提示:', 'helpDialog.proTips.title': '使用提示:',
'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速存取所有操作', 'helpDialog.proTips.commandPalette': '使用命令面板({shortcut})可快速存取所有操作',
'helpDialog.proTips.recentSessions': '最近 5 個會话會顯示在命令面板中', 'helpDialog.proTips.recentSessions': '最近 5 個會话會顯示在命令面板中',
'helpDialog.proTips.themeCycling': '主題循環會記住你在各會話中的偏好', 'helpDialog.proTips.leaderSequences': '兩段式快捷鍵:先按組合鍵,再按第二個鍵(Esc 取消)',
'header.actions.rightSidebarWithShortcut': '右側邊欄({shortcut}', 'header.actions.rightSidebarWithShortcut': '右側邊欄({shortcut}',
'header.actions.toggleRightSidebarAria': '切換右側邊欄', 'header.actions.toggleRightSidebarAria': '切換右側邊欄',
'header.actions.openAppMenu': 'OpenChamber 選單', 'header.actions.openAppMenu': 'OpenChamber 選單',
@@ -2053,7 +2054,6 @@ export const dict: Record<I18nKey, string> = {
'chat.statusRow.tasksTitle': '任務', 'chat.statusRow.tasksTitle': '任務',
'chat.statusRow.modelStatus': '{model} · {status}', 'chat.statusRow.modelStatus': '{model} · {status}',
'chat.statusRow.summary.activeLeft': '{active} 個活躍 · 剩餘 {left} 個', 'chat.statusRow.summary.activeLeft': '{active} 個活躍 · 剩餘 {left} 個',
'chat.statusRow.aborted': '已中止',
'chat.revertIndicator.redo': '重做', 'chat.revertIndicator.redo': '重做',
'chat.revertIndicator.redoAria': '重做 — 恢復已收回的訊息', 'chat.revertIndicator.redoAria': '重做 — 恢復已收回的訊息',
'chat.revertPopover.title': '已收回', 'chat.revertPopover.title': '已收回',
@@ -2131,7 +2131,8 @@ export const dict: Record<I18nKey, string> = {
'chat.btw.toast.promoteFailed': '保留 btw 工作階段失敗', 'chat.btw.toast.promoteFailed': '保留 btw 工作階段失敗',
'chat.container.readOnlySubagentPromptBanner': '無法向子 Agent 會話傳送提示。', 'chat.container.readOnlySubagentPromptBanner': '無法向子 Agent 會話傳送提示。',
'chat.container.sessionLoadError.title': '無法載入工作階段', 'chat.container.sessionLoadError.title': '無法載入工作階段',
'chat.container.sessionLoadError.description': '請檢查連線,然後重新載入此工作階段。', 'chat.container.sessionLoadError.description': '無法取得對話——伺服器可能已關閉或無法連線。內容沒有遺失;待其恢復後再試即可。',
'chat.container.sessionLoadError.authDescription': '工作階段已過期,伺服器拒絕了請求。登入後對話即會載入。',
'chat.container.sessionLoadError.retry': '再試一次', 'chat.container.sessionLoadError.retry': '再試一次',
'sessions.sidebar.group.empty.loadingSessions': '正在載入工作階段…', 'sessions.sidebar.group.empty.loadingSessions': '正在載入工作階段…',
'sessions.sidebar.group.empty.loadFailed': '無法重新整理工作階段。', 'sessions.sidebar.group.empty.loadFailed': '無法重新整理工作階段。',
@@ -2174,10 +2175,8 @@ export const dict: Record<I18nKey, string> = {
'chat.textSelection.title.commentOnSelection': '對所選內容留言', 'chat.textSelection.title.commentOnSelection': '對所選內容留言',
'chat.textSelection.comment.placeholder': '新增選填留言...', 'chat.textSelection.comment.placeholder': '新增選填留言...',
'chat.textSelection.comment.attach': '附加', 'chat.textSelection.comment.attach': '附加',
'chat.textSelection.actions.newSession': '新增會話',
'chat.textSelection.actions.addToNotes': '加入筆記', 'chat.textSelection.actions.addToNotes': '加入筆記',
'chat.textSelection.title.addToCurrentChat': '加入目前聊天', 'chat.textSelection.title.addToCurrentChat': '加入目前聊天',
'chat.textSelection.title.newSessionWithSelection': '使用選取內容建立新會話',
'chat.textSelection.title.saveInsightToNotes': '將選取文字儲存到筆記', 'chat.textSelection.title.saveInsightToNotes': '將選取文字儲存到筆記',
'chat.messageBody.actions.revertAria': '收回到這條訊息', 'chat.messageBody.actions.revertAria': '收回到這條訊息',
'chat.messageBody.actions.revert': '從此處收回', 'chat.messageBody.actions.revert': '從此處收回',
@@ -2453,6 +2452,15 @@ export const dict: Record<I18nKey, string> = {
'commandPalette.item.toggleSidebar': '切換側邊欄', 'commandPalette.item.toggleSidebar': '切換側邊欄',
'commandPalette.item.showContextUsage': '顯示上下文用量', 'commandPalette.item.showContextUsage': '顯示上下文用量',
'commandPalette.item.toggleTerminal': '切換終端機', 'commandPalette.item.toggleTerminal': '切換終端機',
'commandPalette.item.cycleTheme': '輪換主題',
'commandPalette.item.showOpenCodeStatus': '顯示 OpenCode 狀態',
'commandPalette.item.toggleMemoryDebug': '切換記憶體偵錯面板',
'commandPalette.item.pinSession': '釘選或取消釘選會話',
'commandPalette.item.copySessionId': '複製會話 ID',
'commandPalette.item.openMultiRun': '開啟多任務啟動器',
'commandPalette.item.openArchive': '開啟已封存會話',
'commandPalette.item.openNotes': '開啟筆記面板',
'commandPalette.item.openTodos': '開啟待辦面板',
'commandPalette.item.openSettings': '開啟設定...', 'commandPalette.item.openSettings': '開啟設定...',
'commandPalette.session.untitled': '未命名會話', 'commandPalette.session.untitled': '未命名會話',
'openCodeStatusDialog.title': 'OpenCode 狀態', 'openCodeStatusDialog.title': 'OpenCode 狀態',
@@ -2667,6 +2675,9 @@ export const dict: Record<I18nKey, string> = {
'sessionAuth.error.passkeySignInCanceled': 'Passkey 登入已取消。', 'sessionAuth.error.passkeySignInCanceled': 'Passkey 登入已取消。',
'sessionAuth.error.enterPasswordForPasskey': '請輸入密碼以新增 passkey。', 'sessionAuth.error.enterPasswordForPasskey': '請輸入密碼以新增 passkey。',
'sessionAuth.locked.tunnelTitle': '需要 Tunnel 存取', 'sessionAuth.locked.tunnelTitle': '需要 Tunnel 存取',
'sessionAuth.expired.banner': '工作階段已過期——請登入以繼續。',
'sessionAuth.expired.loginAction': '登入',
'sessionAuth.expired.sendBlocked': '工作階段已過期——請登入後再傳送訊息。',
'sessionAuth.locked.unlockTitle': '解鎖 OpenChamber', 'sessionAuth.locked.unlockTitle': '解鎖 OpenChamber',
'sessionAuth.locked.tunnelDescription': '請使用桌面應用程式提供的一次性連結開啟該 Tunnel。', 'sessionAuth.locked.tunnelDescription': '請使用桌面應用程式提供的一次性連結開啟該 Tunnel。',
'sessionAuth.locked.passwordDescription': '此會話受密碼保護。', 'sessionAuth.locked.passwordDescription': '此會話受密碼保護。',
+4 -4
View File
@@ -558,7 +558,7 @@ describe('updateDesktopSettings', () => {
}); });
const syncedSettings: SettingsPayload[] = []; const syncedSettings: SettingsPayload[] = [];
const handleSettingsSynced = (event: Event) => { const handleSettingsSynced = (event: Event) => {
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail); syncedSettings.push((event as CustomEvent<{ settings: SettingsPayload }>).detail.settings);
}; };
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced); getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
@@ -584,7 +584,7 @@ describe('updateDesktopSettings', () => {
invalidateSettingsCache(); invalidateSettingsCache();
const syncedSettings: SettingsPayload[] = []; const syncedSettings: SettingsPayload[] = [];
const handleSettingsSynced = (event: Event) => { const handleSettingsSynced = (event: Event) => {
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail); syncedSettings.push((event as CustomEvent<{ settings: SettingsPayload }>).detail.settings);
}; };
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced); getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
@@ -616,7 +616,7 @@ describe('updateDesktopSettings', () => {
invalidateSettingsCache(); invalidateSettingsCache();
const syncedSettings: SettingsPayload[] = []; const syncedSettings: SettingsPayload[] = [];
const handleSettingsSynced = (event: Event) => { const handleSettingsSynced = (event: Event) => {
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail); syncedSettings.push((event as CustomEvent<{ settings: SettingsPayload }>).detail.settings);
}; };
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced); getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
@@ -647,7 +647,7 @@ describe('updateDesktopSettings', () => {
invalidateSettingsCache(); invalidateSettingsCache();
const syncedSettings: SettingsPayload[] = []; const syncedSettings: SettingsPayload[] = [];
const handleSettingsSynced = (event: Event) => { const handleSettingsSynced = (event: Event) => {
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail); syncedSettings.push((event as CustomEvent<{ settings: SettingsPayload }>).detail.settings);
}; };
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced); getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
+19 -6
View File
@@ -199,11 +199,23 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
setOrRemoveLocalStorage('sttLanguage', typeof settings.sttLanguage === 'string' ? settings.sttLanguage : null); setOrRemoveLocalStorage('sttLanguage', typeof settings.sttLanguage === 'string' ? settings.sttLanguage : null);
}; };
const dispatchSettingsSynced = (settings: DesktopSettings): void => { export interface SettingsSyncedDetail {
settings: DesktopSettings;
/** Whether listeners may adopt cross-window workspace pointers
(activeProjectId / lastDirectory). True only for a bootstrap-grade sync:
the settings document is shared by every window of this server, so a
mid-session reconciliation adopting them would hijack this window's
workspace with another window's choice. */
adoptWorkspace: boolean;
}
const dispatchSettingsSynced = (settings: DesktopSettings, adoptWorkspace: boolean): void => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
window.dispatchEvent(new CustomEvent<DesktopSettings>('openchamber:settings-synced', { detail: settings })); window.dispatchEvent(new CustomEvent<SettingsSyncedDetail>('openchamber:settings-synced', {
detail: { settings, adoptWorkspace },
}));
}; };
type SettingsSaveState = 'idle' | 'saving' | 'error'; type SettingsSaveState = 'idle' | 'saving' | 'error';
@@ -1841,7 +1853,8 @@ export const invalidateSettingsCache = (): void => {
_settingsCache = null; _settingsCache = null;
}; };
export const syncDesktopSettings = async (): Promise<void> => { export const syncDesktopSettings = async (options?: { adoptWorkspace?: boolean }): Promise<void> => {
const adoptWorkspace = options?.adoptWorkspace !== false;
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
@@ -1970,7 +1983,7 @@ export const syncDesktopSettings = async (): Promise<void> => {
if (!isSettingsRuntimeContextCurrent(context)) return; if (!isSettingsRuntimeContextCurrent(context)) return;
} }
dispatchSettingsSynced(authoritativeSettings); dispatchSettingsSynced(authoritativeSettings, adoptWorkspace);
}; };
try { try {
@@ -2013,7 +2026,7 @@ async function _flushSettingsUpdate(): Promise<void> {
if (updated) { if (updated) {
const reconciled = _settingsMutationTracker.reconcile(updated, operation); const reconciled = _settingsMutationTracker.reconcile(updated, operation);
applyDesktopUiPreferences(reconciled); applyDesktopUiPreferences(reconciled);
dispatchSettingsSynced(reconciled); dispatchSettingsSynced(reconciled, false);
_settingsCache = null; _settingsCache = null;
} }
dispatchSettingsSaveState(updated ? 'saved' : 'error'); dispatchSettingsSaveState(updated ? 'saved' : 'error');
@@ -2047,7 +2060,7 @@ async function _flushSettingsUpdate(): Promise<void> {
if (updated) { if (updated) {
const reconciled = _settingsMutationTracker.reconcile(updated, operation); const reconciled = _settingsMutationTracker.reconcile(updated, operation);
applyDesktopUiPreferences(reconciled); applyDesktopUiPreferences(reconciled);
dispatchSettingsSynced(reconciled); dispatchSettingsSynced(reconciled, false);
dispatchSettingsSaveState('saved'); dispatchSettingsSaveState('saved');
// Invalidate GET cache so next read sees the fresh data // Invalidate GET cache so next read sees the fresh data
_settingsCache = null; _settingsCache = null;
@@ -8,7 +8,6 @@ export interface QuotaProviderMeta {
export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
{ id: 'claude', name: 'Claude' }, { id: 'claude', name: 'Claude' },
{ id: 'codex', name: 'Codex' }, { id: 'codex', name: 'Codex' },
{ id: 'command-code', name: 'Command Code' },
{ id: 'cursor', name: 'Cursor' }, { id: 'cursor', name: 'Cursor' },
{ id: 'github-copilot', name: 'GitHub Copilot' }, { id: 'github-copilot', name: 'GitHub Copilot' },
{ id: 'google', name: 'Google' }, { id: 'google', name: 'Google' },

Some files were not shown because too many files have changed in this diff Show More