Commit Graph
726 Commits
Author SHA1 Message Date
l0ng-ai 7d443b2149 fix(settings): build the command-line setting out of the same pieces as the rest
It was the one hand-rolled row on the page: a quieter heading than every
other section, a full-width description, and the only switch in Settings
standing to the left of its own label. It was also the one row the
settings search could neither highlight nor dim, so searching "cli"
badged Agents with (2) and showed one match. Run it through
section_header and settings_row, and point the index entry at the label
the row actually renders.
2026-08-08 08:42:08 +07:00
l0ng-ai e17bebb7e2 fix(chrome): give the last icon-only buttons the tooltips their twins have
Every icon-only control in the chrome names itself on hover — except the
settings close, the two SSH ellipsis menus, the switcher row menu, and
the tab close in both the strip and the sidebar. Same glyphs, same jobs,
same words as the buttons beside them.
2026-08-08 08:35:50 +07:00
l0ng-ai a4f6fdc655 fix(settings): colour a filled slider like an on switch
slider_bar falls back to tokens.primary, the near-black we give primary
buttons, so on one settings page a set slider and an on switch disagreed
about what a set value looks like. Both are the same statement; give
them the same colour.
2026-08-08 08:35:50 +07:00
l0ng-ai a8577fbe5c fix(palette): line section headers up with the rows they head
A section header carried px(11) while a row is a ListItem inset by mx(5)
whose own padding is px_3, so every label under a header started 6px to
its right. Give the header the sum, from named constants so the two
cannot drift apart again.
2026-08-08 08:35:50 +07:00
l0ng-ai 4275bb1aef Revert the settings row wrap that broke every long description
5423301 gave `settings_row` `flex_wrap()` and swapped the label column's
`min_w_0` for `flex_1` + a 200px floor, so the SSH host form would stack
instead of overflowing in a narrow pane. On every other settings page that
stopped long descriptions wrapping at all: the text ran under its own
control and out past the row it belongs to, which is plainly worse than
the case it was meant to fix.

The row goes back to what it was. `#ssh-detail` keeps its `min_w_0` — that
half of the commit is what actually fixed the 1200px overflow, and it is
independent of the row.

The SSH form still degrades below roughly 1000px, where the label column
squeezes to a word per line. That is the real remaining bug and it needs a
different answer than wrapping this row.
2026-08-08 09:00:47 +08:00
l0ng-ai 56075221b5 fix(panel): size the Session label column to the labels it holds
The key column was a hardcoded 46px, which fits "cwd" and "shell" and
little else. In English "changes" folded mid-word into "change / s". In
Chinese and Japanese almost every label wrapped — ja "作業ディレクトリ" is
eight glyphs in a column sized for three ASCII characters.

It now measures the labels actually on screen through the text system and
takes the widest, clamped to 46–108px so one long Japanese label cannot
eat the panel. The labels are `whitespace_nowrap`, so anything past the
clamp runs into the gap rather than folding.

Checked on screen in English and Chinese: "changes" and "工作目录" each sit
on one line, and the values still line up.
2026-08-08 08:43:30 +08:00
l0ng-ai 8d57e07422 fix(search): make a match visible on a light theme
Search matches were painted as the terminal's selection tint at 0.32
alpha. That tint is itself only `mix(bg, fg, 0.24)`, so the blend landed
about 1.17:1 against the background — a 7% shift, in every theme. On the
built-in Light theme the matches were a barely-there grey smudge on white;
on dark ones only the glyph colour gave them away. "3/3" told you there
were three, and you still had to hunt for them.

The wash is now opaque and solved for a contrast ratio instead of a fixed
alpha, so it keeps the same weight whatever the theme's two colours are:

| | Before | After |
|---|---|---|
| Other matches | fixed 0.32 alpha ≈ 1.17:1 | `MATCH_WASH` = 1.45:1 |
| Current match | fixed 0.85 alpha | `CURRENT_MATCH_WASH` = 2.1:1, plus the caret-coloured border it already had |

`presets::wash(surface, tint, target)` bisects the blend the same way
`bisect_contrast` walks a colour, and falls back to `legible_ink` for a
theme whose selection colour is so close to its background that no opacity
reaches the target.

These are non-text ratios on purpose — enough to spot the block, not so
much that the glyph on top of it stops reading.

Verified on screen in both built-in themes: all three matches are now
findable at a glance, and the current one still stands out from the other
two.
2026-08-08 08:29:59 +08:00
l0ng-ai 5423301b82 fix(settings): stop the SSH host form running off the right edge
At 1200x800 — an ordinary window size — the SSH host form was cut off by
the window edge: the Connect button was sliced in half, the Name, Host and
User fields had no visible right edge, and the Auth control lost its last
option entirely. "2FA" was off screen, so at that size an authentication
method could not be selected at all. Nothing scrolled horizontally, so
there was no way to reach any of it short of resizing the window.

Two causes, both in the layout:

- `#ssh-detail` is `flex_1` but had no `min_w_0`. Flex items default to
  `min-width: auto`, so the column sized itself to its content instead of
  to the space left over after the 280px host list, and the overflow went
  past the window.
- `settings_row` then had no way to cope: the label column could shrink to
  nothing while the control could not shrink at all. Adding `min_w_0`
  alone made it worse — at 900px the labels collapsed to one word, then
  one *letter*, per line.

The row now wraps and the label column has a 200px floor, which is the
usual settings-pane behaviour: side by side while there is room, control
stacked beneath the label when there is not.

| Window | Before | After |
|---|---|---|
| 1200px | Connect halved, three fields and "2FA" off screen | everything fits side by side |
| 900px | same, worse | rows stack; all six auth methods reachable |
| wide | fine | unchanged — the 720px cap still holds |

Checked on screen at 1200 and 900, and on Appearance and Terminal at 1200
to confirm the wrap does not fire where there is room: those two pages are
pixel-identical to before.
2026-08-08 08:09:45 +08:00
l0ng-ai 1e7284db2b fix(palette): give the "edit" badge a chord that can actually fire
Every row with an edit variant — quick-connect and every saved SSH profile
— advertised "→ edit", and neither of the two gestures behind it worked:

- `→` never reaches the palette. gpui-component's `Input` binds bare
  `right` to MoveRight inside its own key context, and the query field has
  focus the whole time the palette is open, so the keystroke is consumed
  before the scrim's handler sees it.
- `⌘↵` never reaches it either. The app binds `secondary-enter` to
  ToggleFullscreen, which matches first — pressing it in the palette put
  the window into fullscreen.

So a saved profile could not be opened for editing from the palette at
all, which is the only row-scoped way in; Settings → SSH lists them, but
loses which row you were on.

The chord is now `secondary-e`, claimed by neither the input nor the app.
It lives in one `EDIT_GESTURE` constant next to the matcher that reads it,
and the badge renders through `key_tokens`, so it says ⌘E on macOS and
Ctrl E elsewhere rather than a hardcoded arrow baked into three locale
tables. `EditHint` is now just the word ("edit" / "编辑" / "編集").

Verified on screen: the badge reads "edit ⌘E", and ⌘E on a quick-connect
row opens the SSH profile form with the host, user and port already filled
in from the query.
2026-08-08 07:57:37 +08:00
l0ng-ai 5d1584e4c4 fix(palette): arm the first row when the palette opens
`ListState::new` starts with no selected row, and it only picks one when a
query *changes*. So opening the palette and pressing Return — the shortest
path through the feature — did nothing at all, and until the user typed or
pressed Down there was no row showing what Return was aimed at.

The delegate did think row 0 was selected (`selected: Some(IndexPath::
default())`), which is why this never showed up as a missing-state bug:
`selected_command()` answered correctly, but the highlight and the Confirm
event both come from `ListState`, which disagreed.

`build_list_with_delegate` now hands the list the delegate's own
`first_row()` — the one that skips empty sections — right after
construction, so all three palettes (root, plain, ssh-connect) open armed.

Verified on screen: ⌘P now highlights "New Tab" immediately, and Return
opens a tab instead of doing nothing.
2026-08-08 07:44:10 +08:00
l0ng-ai 91d3731f07 Merge branch 'fix/search-bar-hides-the-match' into integration/polish
# Conflicts:
#	src/terminal/search.rs
2026-08-08 07:33:54 +08:00
l0ng-ai 474ed4d420 fix(search): stop parking the current match under the search bar
The find bar floats over the top of the grid — `.absolute().top_2()`, 34px
tall — but `scroll_match_into_view` compared the match's line against the
whole viewport. A match in the first two or three rows was "on screen", so
nothing scrolled, and ⌘G would say 1/3 with match 1 hidden behind the
search field. Wrapping past the last match landed there every time.

Those rows now count as off screen. When a match is above the readable
band the view walks back just far enough to put it on the first clear row,
rather than calling `scroll_to_point`, which parks it on row 0 — the row
most likely to be covered.

The row count comes from the bar's own geometry, so it follows the font
size: 3 rows at the default 17px line height, 1 at 42px. `BAR_TOP` and
`BAR_HEIGHT` now live next to the `.top()` and `.h()` that render it, so
the two cannot drift apart.

Verified on screen: with three matches and the viewport at the live end,
⌘F then Return used to leave 1/3 behind the bar; it now scrolls so the
outlined match sits directly below it.
2026-08-08 07:32:41 +08:00
l0ng-ai fa12d84b9b fix(reveal): stop offering Finder a path that is not on this machine
The file tree and the Session panel both list whatever host the workspace
spawns on, and every other action they offer goes through that host. Only
"Reveal in Finder" does not: `cx.reveal_path` talks to the local file
manager, so in a remote workspace it was handed a path that lives on the
other machine. Finder either does nothing at all — a menu item that
silently no-ops — or, if a local path happens to collide, opens the wrong
directory.

Both are now hidden unless the paths really are local:

| | Gate |
|---|---|
| File tree context menu | `spawn_host(cx).is_local()`, the same predicate `guard_local_spawn` uses |
| Session panel button | `view.local_cwd().is_some()` — a helper that already existed for exactly this (`remote_context().is_none() && host_id.is_local()`) and that this call site was not using |

Copy Path stays in both places: a remote path is still worth copying.

Checked on screen that the local file tree keeps the item and its
separators. The remote branch is reasoned from the predicate, not
exercised — it needs a real SSH host.
2026-08-08 07:22:41 +08:00
l0ng-ai b77f70310d fix(prompts): put the action on the right and give Escape a home
Every confirmation dialog in the app was built as `&[Cancel, Delete]`, and
gpui hands answer 0 to the platform first — which NSAlert draws on the
*right* and gives Return. So the app shipped 14 dialogs with the buttons
mirrored: Delete sat on the left, exactly where a decade of macOS has
trained people to expect Cancel, and Cancel sat on the right holding the
default.

Escape was worse: it did nothing at all, anywhere. gpui only sets the
Escape key equivalent on a `PromptButton::cancel`, and every call site
passed plain strings, which become `PromptButton::Other`. There was no way
to dismiss any of these dialogs from the keyboard except by pressing
Return.

A shared `ui::confirm_answers(action, keep)` now builds the pair, so the
arrangement is decided in one place:

| | Before | After |
|---|---|---|
| Right button (Return) | Cancel | the action |
| Left button | the action | Cancel |
| Escape | nothing | Cancel |
| Space / initial focus | Cancel | Cancel |
| `Ok(1)` means | act | — |
| `Ok(0)` means | cancel | act |

Verified on the file-tree delete, end to end: Escape leaves both files in
place, Return removes only the one that was right-clicked. Also checked on
the ⌘W busy-pane guard, where Escape and Space keep the tab and Return
closes it.

Two prompts keep their own shape and say why in a comment:

- The daemon-mismatch prompt at launch offers Quit and Restart Server and
  nothing else. With no answer that leaves things alone there is nothing
  safe to give Escape, so Quit stays on Return — it loses no sessions,
  while restarting the server ends every one of them.
- The unsaved-editor prompt has three answers. Save keeps answer 0, Cancel
  is marked so it takes Escape, and Discard sits on the far left where
  nothing lands by reflex. gpui puts the initial keyboard focus on Discard
  so it stays reachable without a mouse.
2026-08-08 07:16:02 +08:00
l0ng-ai 2a0b910cd4 fix(themes): reuse the key that was already written for this failure
f8e2a59 added `OpenInFileManagerFailed` in all three locales for exactly
this case and then only wired its sibling, so the key sat dead — clippy
was reporting it as never constructed. The follow-up fix invented a second
key rather than using it.

Now the notification reads "Could not open /…/themes", naming the path
instead of a category, and `ThemeFolderFailed` is gone. Clippy's
never-constructed list is back to what it is on main.
2026-08-08 06:55:36 +08:00
l0ng-ai b608fcaae9 fix(themes): say so when the themes folder cannot be opened
`open_themes_folder` created the directory with `let _ =` and then handed
the path to the system file manager regardless. On the one click that can
actually fail — the first one on a fresh install, where the folder does
not exist yet — a read-only or permission-denied config directory left the
button doing nothing at all, with no message.

It now reports the failure through the same `notify_err` path as the rest
of the theme editor, and stops instead of opening a path that is not there.

The `themes_dir() == None` branch stays a log line: without a config
directory there is no theme editor to have clicked from.
2026-08-08 06:51:56 +08:00
l0ng-ai 698db815e2 Merge branch 'fix/completion-after-pipe' into integration/polish 2026-08-08 06:49:24 +08:00
l0ng-ai 10abd37134 fix(completion): complete a command name after a pipe, not a filename
`complete()` decided "is this a command position?" with
`chars[..word_start].all(is_whitespace)` — true only at the very start of
the line. Two lines below, `current_command()` already knew better: it
splits the prefix at the last `| & ; \n (` and looks only at the segment
holding the cursor.

So `ls | gre<Tab>` asked for files named `gre*` in the cwd instead of
offering `grep`, and the same for `a && b`, `a; b`. Every shell treats the
first word of each pipeline segment as a command.

`remote_path_request()` carried the identical check, so over SSH the same
keystroke also spent a round trip listing the remote cwd for a word that
was never a path.

Both now go through one `at_command_position()` helper, and the segment
split that was copy-pasted in two places is a `segment_start()` helper.

`ls |gre` — no space — is unchanged: `shell_word_start` only breaks words
on whitespace, so the word is `|gre`. Teaching it to break on
metacharacters has to respect quoting first, or `grep "a|b"` starts
completing `b"`.
2026-08-08 06:49:20 +08:00
l0ng-ai 3e03a1cbb2 refactor(terminal): drop the duration PaneBusy carried and never showed
The close question names what is running, not how long it has been —
carrying the elapsed seconds through the type invited someone to believe
it reached the screen. Command is now a plain name.
2026-08-08 06:33:03 +08:00
l0ng-ai 846ce1defc fix(terminal): invert the half of a wide character that holds the glyph
A TUI can park the cursor on the trailing spacer of a wide character.
Inverting that cell fills the right half with the caret while the glyph,
which lives on the lead cell, keeps its own colour across both halves —
an opaque block over half a character.

The reverse-video pass now steps back to the cell that owns the glyph,
which also lets paint_backgrounds absorb the spacer into its run and
cover both columns. A spacer in column 0 has no lead to step back to and
is left alone rather than wrapping to the previous row.

Found reviewing my own diff; the translucent block this replaced had the
same half-coverage but stayed readable through it.
2026-08-08 06:30:34 +08:00
l0ng-ai befb689a1a fix(editor): keep Discard away from the button Return presses
The unsaved-changes sheet listed [Save, Discard, Cancel]. The platform
draws the first entry as the default and lays the rest out beside it, so
that rendered as "Cancel | Discard | Save" — with Discard directly
adjacent to the key Return lands on. Apple puts Cancel between them for
exactly this reason.

Reordered to [Save, Cancel, Discard], which renders "Discard | Cancel |
Save", and moved the discard arm to index 2 to match.

Tested: the index mapping reads 0 = Save, 1 = Cancel (falls through to
no-op), 2 = Discard. The right-to-left rendering was confirmed on the
two-button prompts, where array index 0 is drawn rightmost and takes
Return; the three-button sheet itself was not driven on screen.
2026-08-08 06:26:05 +08:00
l0ng-ai 5b65267d7f Merge branch 'fix/tree-search-cap' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 06:03:19 +08:00
l0ng-ai 2ec01b0264 Merge branch 'fix/search-match-cap' into integration/polish 2026-08-08 06:01:43 +08:00
l0ng-ai 5e70a125ea Merge branch 'fix/silent-theme-writes' into integration/polish
# Conflicts:
#	src/ui/i18n/en.rs
#	src/ui/i18n/ja.rs
#	src/ui/i18n/mod.rs
#	src/ui/i18n/zh.rs
2026-08-08 06:01:43 +08:00
l0ng-ai ecaab93c8f fix(file-tree): say when the search list is only the first 200 matches
The tree search stops at SEARCH_LIMIT and returned the prefix with no
mark, so a query with more matches than that looked like it had found
all of them — and the ones you were looking for were simply not there.

A trailing row now reads "First 200 matches", using the same placeholder
rows that already explain an empty or unreadable directory.

Verified against a 260-file directory: the row appears; on this repo,
where no query reaches the ceiling, it does not.
2026-08-08 06:01:19 +08:00
l0ng-ai 4fc2dfd541 fix(search): mark a match count that hit the ceiling
The scrollback scan stops at 10,000 matches. The count rendered that as
"1/10000", which reads as the whole truth — a search for a common
letter looked like it had found every occurrence and settled on a round
number.

It now reads "1/10000+" once the ceiling is reached, the same way every
search that caps says so.
2026-08-08 05:50:50 +08:00
l0ng-ai f8e2a59456 fix(themes): stop the theme editor from failing without saying so
"Duplicate to edit" logged its failure and did nothing else — a button
that does nothing is indistinguishable from having clicked the wrong
thing. Worse, every colour edit runs through mutate_active_theme, which
returned early on a write failure: the picker moved, the theme did not,
and nothing said why.

Both now report, through the same notify_err the rest of the app uses.
On a themes folder the user cannot write, that reads "Could not
duplicate the theme: You do not have permission." rather than
"Permission denied (os error 13)", or nothing at all.
2026-08-08 05:46:18 +08:00
l0ng-ai b2c2ecbc45 Repair the merged plural table and test module 2026-08-08 05:38:15 +08:00
l0ng-ai 2ef162da36 Merge branch 'fix/restart-server-failure' into integration/polish
# Conflicts:
#	src/ui/app.rs
#	src/ui/i18n/en.rs
#	src/ui/i18n/ja.rs
#	src/ui/i18n/zh.rs
2026-08-08 05:34:25 +08:00
l0ng-ai f49281d53c Merge branch 'fix/theme-load-errors' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 05:34:24 +08:00
l0ng-ai 08c47e37fb Merge branch 'fix/modal-scrims' into integration/polish 2026-08-08 05:33:03 +08:00
l0ng-ai 14156f51a4 fix(app): say when a server restart failed instead of showing an empty home page
Restarting the background server is something the user explicitly asks
for, and a failure logged the reason and left them on the home page with
nothing on it — which looks exactly like the restart working and taking
every tab with it.

The reason now lands on the home page, in the same slot a failed first
terminal uses.
2026-08-08 05:32:37 +08:00
l0ng-ai 6a1b843797 fix(session): say when the layout came back short
A restored tab whose panes could not be started was dropped with a log
line and nothing else. The layout coming back is the product's headline
claim, so a tab quietly not coming back is exactly the thing that needs
saying — a broken shell or an unreachable remote silently thinned the
window and left no trace on screen.

The restore now counts what it dropped. On launch the count lands on the
home screen next to whatever else went wrong; switching workspaces
raises it as a notification, since there is already a window to read.
2026-08-08 05:29:00 +08:00
l0ng-ai 66d4d03d33 fix(themes): say which files in the themes folder did not load, and why
A theme file that fails to parse logged a warning and then simply was
not in the list. The themes folder is one the app tells you to open and
drop files into — "it isn't there" needs a reason attached to it, and a
log file is not where anyone looks for one.

The loader now carries its rejections out with the themes, and the theme
picker lists each file name with the parser's reason above the presets:
broken.yaml — missing field `accent`.
2026-08-08 05:20:59 +08:00
l0ng-ai 4406340974 fix(ui): make the two card overlays look as modal as they behave
The worktree prompt and the SSH auth prompt each cover the window with a
full-bleed layer that swallows every click — and drew nothing, so the
app simply stopped responding with no sign of why. The palette and the
switcher already answer this with presets::scrim_fill; these two just
never used it.

Both now dim what is behind them. The worktree prompt also cancels on a
click outside, the same gesture the palette and switcher take. The auth
prompt deliberately does not: a mis-aimed click would abandon a
handshake mid-flight, so Escape stays its only way out.
2026-08-08 05:15:07 +08:00
l0ng-ai 90cf1c0d7a Drop the two SSH close helpers the general close guard superseded 2026-08-08 05:06:16 +08:00
l0ng-ai e86cb44923 Merge branch 'fix/ssh-in-menu-bar' into integration/polish 2026-08-08 04:50:37 +08:00
l0ng-ai 12566d9b7f Merge branch 'fix/sidebar-search-branch' into integration/polish 2026-08-08 04:50:37 +08:00
l0ng-ai 6cab19ce01 fix(menu): put SSH in the menu bar, and stop offering Full Screen twice
SSH is one of the reasons to pick tty7 and it had no entry in the menu
bar at all — the only routes were ⌘P and Settings, both of which you
have to already know about. Manage Profiles and Reconnect now sit in
File beside the other things that open a pane; Remote Files and Port
Forwarding sit in View beside the other panel toggles. Same labels as
the command palette, so there is still one name per thing.

AppKit adds its own "Enter Full Screen" to the bottom of any menu named
View, so ours sat directly above it: the same command listed twice under
two different shortcuts. Ours is gone; ⌘↵ still works and is listed on
the Keybindings page. Drops the AppMenuEnterFullscreen key with it.
2026-08-08 04:50:14 +08:00
l0ng-ai 7f3f69dfd6 fix(sidebar): search what the row shows, not just the part that fits
Each sidebar row shows a truncated title and a branch. The filter read
only the truncated title, so typing the branch you can see — or the part
of the path the row elided — matched nothing, and the list emptied out
on a query that is plainly on screen.

It now matches the full title and the branch as well as the label.
2026-08-08 04:27:23 +08:00
l0ng-ai 9c929fff91 Merge branch 'fix/unbindable-actions' into integration/polish 2026-08-08 04:21:36 +08:00
l0ng-ai 9ebbe2cd24 Merge branch 'fix/io-error-messages' into integration/polish 2026-08-08 04:21:36 +08:00
l0ng-ai e7093e66fb fix(keymap): make the three panel actions bindable, and stop dropping typos in silence
ShowRightPanelInfo, ShowRightPanelChanges and ShowRightPanelFiles have a
make_binding arm and are dispatched like any other action — but they had
no slot in default_bindings, which is the only list set_binding writes
into. So `"ShowRightPanelInfo": "ctrl-1"` in a hand-edited config.json
went into a branch that did nothing, and the Keybindings page, which
reads that same list, never showed them at all.

They now have empty default slots, so they appear under View and can be
bound like anything else. A binding naming an action that does not exist
— a typo, or a name from an older build — now says so in the log instead
of vanishing.

every_dispatchable_action_has_a_slot_to_bind_it_in keeps the two lists
from drifting apart again.
2026-08-08 04:11:19 +08:00
l0ng-ai 63ad270473 fix(errors): give the failures people can act on a sentence
Every host operation that fails renders as "{context}: {raw io::Error}".
Display on an io::Error answers "what happened" for someone reading a
log; it does not answer "what now" for the person who just lost a save,
and "Permission denied (os error 13)" is the shape of that gap.

The six kinds that change what you would do next — denied, gone, no
space, read-only, busy, timed out — now read as one authored sentence in
all three locales. Everything else keeps its raw detail rather than
losing it to a vague house message.

Also: "Save failed" did not say which file, and with more than one
editor tab open that is the first thing you need to know. It is now
"Could not save {name}".
2026-08-08 04:00:34 +08:00
l0ng-ai c2dcfe445f Fix the merged i18n guard: one ALL sweep, not two half-tests 2026-08-08 03:51:59 +08:00
l0ng-ai 2a9b4764bd Merge branch 'fix/first-terminal-failure' into integration/polish 2026-08-08 03:48:43 +08:00
l0ng-ai 70c82376bd Merge branch 'fix/ssh-advanced-grouping' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 03:48:43 +08:00
l0ng-ai 6ad7a25e0b Merge branch 'fix/sftp-overwrite' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 03:48:43 +08:00
l0ng-ai 0d698f21a3 Merge branch 'fix/keybind-recording-feedback' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai 966f3bebb9 Merge branch 'fix/switcher-column-hint' into integration/polish 2026-08-08 03:48:42 +08:00