Commit Graph
1772 Commits
Author SHA1 Message Date
Kang f8fe4ef16b feat(settings): give the panels a real text input to share
Outside the connection editor every "input" in the app is a label div
over a draft string, with a focus handle per box and a hand-written key
handler — no caret, no selection, no IME, no clipboard, and a little of
it reimplemented in each panel. The search-engine editor showed the
failure plainly: the placeholder and the value were stacked inside one
box that only had room for one of them.

The connection editor solved this by owning a TextField entity per
field. Threading that map through every panel's state would be a lot of
churn, so the fields live in one registry keyed by a string id, created
the first time a panel renders one. A panel keeps no state beyond the
value it already had, and edits arrive as one event with the id
attached, which the registry routes by prefix.

The search-engine editor is the first caller: its two boxes are real
inputs now, and the key handler and focus-steering they needed are
gone. Structural changes forget the ids they invalidate — adding an
engine shifts every row's index.
2026-07-27 02:30:31 +08:00
Kang 41d8e1c314 fix(tabs): keep a session tab's title on one line
The title had `overflow_hidden` but nothing stopping it from wrapping,
so it laid itself out as a column of a few characters each and the tab —
one row tall — showed whichever line happened to land there. A local
PowerShell session read "ste", out of the middle of "System32".
2026-07-27 02:17:04 +08:00
Kang bc4dc3db4b fix(terminal): stop CJK rows from collapsing into themselves
A shaped terminal row is laid out with `force_width`, which puts glyph
*n* at `n * cell_width` no matter how wide the glyph is. A CJK character
covers two terminal columns, so every character after one was pulled a
cell to the left and the row piled up on itself — a line of Chinese was
unreadable, and so was the PowerShell banner.

Each double-width character is now followed by a space, so one glyph is
one cell again. The space advances the glyph index without painting, and
a cell's background is drawn as a rect underneath either way. Both row
paths needed it: the styled one and the plain-text fast path.
2026-07-27 02:13:37 +08:00
Kang 380ab306cf feat(connections): lay the editor's form out the way Tauri does
Captions move above their boxes, so a field's whole width is what was
typed into it rather than half a line under a label. Host, port,
username and the serial port carry the red required marker, the fields
that had one in Tauri show their placeholder again, and the host row
gives the host the room while pinning the port at a fixed width instead
of splitting the row down the middle.

The port and the other numeric fields gain the spinner Tauri has, with
the range each one is actually valid over. Typing still works — the
buttons are a shortcut, not the only way in.

Switching the connection kind rewrites the default port on the draft;
the box now hears about it, so Telnet opens on 23 rather than showing
22 while the draft says otherwise.
2026-07-27 02:00:19 +08:00
Kang aea6ed3354 feat(connections): drive the editor's selects from the keyboard
An open select now takes focus, so Up/Down walk the options, Home/End
jump to the ends, Enter takes the highlighted one and Escape closes
without changing anything. The highlight is shared with the pointer, so
moving the mouse does not leave a second, stale one behind, and it
scrolls itself into view in a long list.

The keys are answered by the popover rather than the editor surface,
because only the popover knows the option order — the choices are built
where they are rendered, and mirroring them into the runtime would be a
second source of truth. That is also why the field map handed to the
sections became a struct: reading the app entity again from inside its
own render panics.

Two things this uncovered:

- Every select rendered its current value into a box that collapsed to
  zero width, so all of them looked empty. Giving the value a flex basis
  brings back "None", "COM1", "8" and the rest.
- Enter in the group popover's "new folder" box now creates the folder
  again, and the box is emptied afterwards — it holds its own buffer, so
  clearing the draft's copy left the typed name on screen.
2026-07-27 01:43:12 +08:00
Kang 56ab3daa43 feat(ui): let the text field wrap and hold more than one line
The description box was the last field still faked with a label div,
because the widget could only lay out a single shaped line. It now
shapes through `shape_text`, so a field opted into `multi_line` wraps at
its own width, grows downward, scrolls vertically instead of sideways
and takes Enter as a newline rather than handing it to the dialog.
Up/Down move by hard line, keeping the column.

Space arrives without a `key_char` on some platforms — the terminal's
key encoder already names it for the same reason — so it is named here
too, or typing a sentence produced one long word.

Editing any field now clears a stale validation error, and the box is
`flex_none` so the form cannot shrink it into a half-drawn row.
2026-07-27 01:16:35 +08:00
Kang bfbe8beb69 fix(ui): stop the text field from typing Tab and Enter into the buffer
The field treated any key carrying a `key_char` as text. Tab's is "\t" and
Enter's is "\n", so pressing either in a single-line field inserted an invisible
control character instead of leaving the key for the dialog — a Tab typed into
the connection name looked like nothing had happened at all.

Navigation and dialog keys are now named explicitly rather than inferred from
whether the platform produced a character for them.

The description box is left off the editor's field list on purpose: it is the
one multi-line input in the dialog, and this widget is single-line, so it keeps
the legacy key routing until multi-line lands.
2026-07-27 00:25:43 +08:00
Kang 2870c05393 fix(connections): make the editor's selects real popovers
Both editor selects rendered their option list as an ordinary child of the
trigger, so opening one pushed everything below it down the dialog — the group
select shifted the icon, name and host rows every time it was touched — and the
list was clipped by whichever ancestor hid its overflow.

They now open through `deferred(anchored(..))`, the same treatment the context
menus got: laid out against the window, offset below the trigger, snapped inside
the window edges, and painted above the dialog instead of inside its flow.
Clicking outside dismisses only the select that is open, so a stale click for a
different one is ignored.

The options themselves read better: a wider panel that fits a group path or a
proxy summary without wrapping, taller before scrolling, and a tick on the
current value rather than tint alone. The "new group" input inside the group
popover is a real text field now, which is also where its placeholder moved to.
2026-07-27 00:16:43 +08:00
Kang ce1c140f65 feat(connections): give the connection editor real input fields
Every input in the new/edit-connection dialog was a label div sharing one focus
handle, with a `focused_field` enum routing keystrokes to whichever draft string
was "active". Clicking a field only moved that enum, so there was no caret, no
selection, no pointer positioning, no composition, and password masking was a
string of bullets rebuilt on each render.

Each text input is now a `TextField` entity, built when the editor opens and
cleared when it closes, so the widgets live exactly as long as the draft they
mirror. `ConnectionEditorState` stays a plain value the runtime can clone and
save from; the fields write back through their subscriptions. The password field
masks itself while the buffer keeps the secret in the clear, which is what the
save path needs. The folder-name editor gets the same treatment.

Two focus fixes fall out of this. The editor surface's blanket `on_click` focus
grab is gone: it existed to keep the old fake inputs "focused", and would now
steal focus back from whichever field the pointer just landed on, since click
follows mouse-down. And because focus is per-window, the detached editor window
claims the name field itself on its first frame rather than trusting what the
main window focused.

The labelled row is the hit target, not just the one text line inside it, and it
takes its ring from the widget's own focus rather than from an enum nothing
updates any more.
2026-07-27 00:08:37 +08:00
Kang f0655b59c1 feat(ui): add a real text field widget and use it for the connections filter
Every "input" in the app is a label div: a `div` that takes focus, swallows key
events and prints the draft string. There is no caret, no selection, no pointer
positioning and no composition, so the moment anyone tries to edit rather than
append it reads as broken — which is exactly the complaint about the filter box.

`TextField` is a real GPUI widget instead. It is an `Entity` that owns its
buffer and focus, implements `EntityInputHandler` so the platform routes IME and
clipboard through it, and paints through a custom `Element` that shapes the line
once and reuses that shaping for hit-testing, the selection quads and the caret.
Owners learn about edits by subscribing to `TextFieldEvent::Changed`, which
carries the new content, rather than reading back through the entity.

It deliberately does not claim Enter, Escape, Tab or the arrows: those are
dialog and list gestures, so they stay unconsumed for the owner's own key
handler. That is what lets the saved-connections filter keep driving result
navigation while the field handles the text.

The editing rules live in `nyaterm_core::TextEdit` — caret motion across
multi-byte boundaries, what counts as a word (hosts and paths make `.`, `-`, `/`
and `@` stops), and which end of a selection shift moves — so they are testable
without a window.

Adopting it in the filter box retires the placeholder caret drawn by splicing a
`|` into the display string, and the seven arms this panel had bolted onto
`NyaTermApp`'s `EntityInputHandler`; the widget owns its own composition now.
2026-07-26 23:43:35 +08:00
Kang 900e83af73 feat(connections): make the filter box a real input and rebuild the row layout
The filter box was a label `div` with a key handler: no caret, no IME, no
paste, no way to move the insertion point, and an identical border whether or
not it had focus — clicking it produced no visible change at all. It now
follows the pattern the app already uses for its one field with a cursor: a
`gpui::canvas` installs an `ElementInputHandler` over the field's bounds, the
`EntityInputHandler` on `NyaTermApp` gains an arm per method, and the focused
field takes the primary border. Editing is relative to the caret and steps by
character boundary — the previous `pop()` cut a byte off multi-byte input.
Reaching the field's focus state means threading `window` down to the panel
bodies, which every panel now has for the same purpose.

While a filter is active the box also drives the results: up/down walk them and
enter opens the active row, which is drawn with its own wash and ring so it
stays distinct from the selection. The active row is dropped as soon as the
filter stops matching it.

Folders now start collapsed. Seeding the expansion set with every group buried
the folder list; a filter instead opens the folders that still have hits and
restores the prior tree when it clears, once per keyword so collapsing an
auto-opened folder sticks.

The rest of the row work: names get the full width of a horizontally scrollable
list instead of wrapping inside a fixed-height row, the folder count sits
against the right edge, hover actions are connect and edit and appear
immediately rather than after the detail card's delay, and the detail card is a
real tooltip so it hangs outside the panel instead of covering the rows under
it — which retires the 350ms hover-intent machinery and its per-frame poll. The
top selection strip is gone; those actions live in the menus. A folder with no
connections is no longer swallowed by the empty state.
2026-07-26 23:23:00 +08:00
Kang e1e5fb625f fix(connections): place context menus in window space and add move-to-group
Right-click looked dead. The menu stored the `MouseDownEvent` position, which
is window-relative, then positioned itself `absolute()` inside a panel that is
both offset from the window origin and `overflow_hidden` — so it landed at the
wrong place and whatever hung past the panel edge was clipped, which in a
sidebar this narrow was usually all of it. Menus now go through
`deferred(anchored().position(..).snap_to_window_with_margin(..))`, which reads
the position against the window and paints outside the panel's clip rect.

A right-click on the list background had no menu at all; it now offers the
selection actions plus new connection, new folder and import.

"Move to group" was missing everywhere, leaving drag and drop as the only way
to reparent a connection. It hangs off the row menu, the background menu and
the header menu, moving the whole selection when the clicked row is part of it.
`move_connections_into_group` writes the new order once instead of persisting
and refreshing per connection, so the list is never observed half-moved.

The flyout is a *descendant* of the menu rather than a second overlay, so the
menu's `on_mouse_down_out` does not fire and tear the stack down as the pointer
enters it, and it flips to the left when the window edge is close. It lists the
folders indented by depth rather than nesting a hover cascade per level, which
matches the group dropdown in the connection editor.
2026-07-26 23:22:39 +08:00
Kang 8ac5401b8b fix(connections): sort folders too, and order names the way people read them
Cycling the sort button only reordered connections: `sort_groups` ignored
`ConnectionSortMode` entirely, so folders stayed put and the tree looked
unsorted. It now branches on the mode exactly as `sort_connections` does.

Both comparisons also used ASCII-lowercase ordering, which puts
`192.168.142.100` before `192.168.142.13` — and host lists are most of what
this panel sorts. `nyaterm_core::natural_compare` compares digit runs by value
without parsing them, so an arbitrarily long run cannot overflow, and falls
back to the raw comparison when case folding ties so the order stays total.
`visible_connection_ids` follows the same rule, or Shift-range selection would
walk a different list than the one on screen; it also stops descending into
collapsed folders, whose rows are not reachable.

The button now shows which mode is active instead of hiding it in a tooltip:
the glyph switches to sort-by-alpha, flips vertically for Z-A, and takes the
primary tint while a name sort is on.
2026-07-26 23:22:16 +08:00
Kang ebb81fcaa8 fix(ui): paint the svg icons that never declared a colour
`Svg::paint` does `self.path.zip(style.text.color)`, and `compute_style` starts
from `Style::default()`, whose `text.color` is `None`. A parent's
`.text_color(..)` cascades to real text but never to an `svg()` child, so a
glyph without a colour of its own was skipped entirely at paint time — no
error, no fallback, just an invisible icon still taking up layout.

59 call sites were affected: the window minimise/maximise/close controls, tab
close buttons, the title bar, the quick-command toolbar, the transfer browser
and queue, tunnels, settings and the AI panels. Shared button helpers are fixed
first, so most sites are covered by ~10 functions. Where the parent brightened
the icon on hover, the pair becomes `.group(..)` plus `.group_hover(..)`, since
a `.hover()` refinement cannot reach a child either.

`scripts/check-icon-references.sh` now fails on an `svg()` whose chain has a
`.path(..)` but no `.text_color(..)`. It checks depth 0 only: a colour that
lives solely inside `.group_hover(.., |s| s.text_color(..))` still leaves the
icon invisible until the pointer is over it.
2026-07-26 23:21:58 +08:00
Kang 852c14254f perf(terminal): borrow UTF-8 output and park the event consumer
Three costs on the hot output path:

`SessionEncoding::decode` ran every chunk through the incremental decoder and
allocated a fresh `String`, even though UTF-8 sessions — the default and the
overwhelming majority — are already valid UTF-8. Valid input is now validated
and borrowed, with only the trailing bytes of a split multi-byte sequence
carried into the next chunk.

The transport event queue was drained on a fixed interval, so a dedicated
consumer thread traded latency for wakeups. It can now park on the producer's
push, bounded by a timeout so shutdown and periodic bookkeeping stay on
schedule; the UI tick path keeps the non-blocking drain.

Graphics and the terminal snapshot model shed the copies that were feeding
those two paths.
2026-07-26 23:21:58 +08:00
Kang 5efce4e7cf feat(assets): vendor bundled icons from a pinned manifest
Icons were previously hand-added, so their provenance and licensing lived in
folklore and a mis-painted asset was invisible at runtime: GPUI's `svg()`
reduces a file to an alpha mask, and `img()` keeps its pixels, but nothing
complains when the two are swapped.

Assets are now described by `scripts/icons.manifest` and vendored by
`scripts/sync-icons.sh` from pinned upstream releases, keeping the tree
reproducible and reviewable. `icons/**` stays monochrome and `color/**` full
colour; `scripts/check-icon-references.sh` fails the build when a referenced
asset is missing or painted through the wrong element, and runs in CI.

The lookup tables move out of `formatting/` into a dedicated `features/icons`
module so a ~1000-line brand table is not rebuilt inside a render closure, and
saved connections gain `icon_auto_detect`, which fills in a blank icon from the
detected remote system without ever overwriting a deliberate choice.
2026-07-26 23:21:58 +08:00
Kang 7b12c5492f docs(architecture): record the unwired capabilities left after the sweep
Desktop dead-code warnings went 104 to 18. Name what the remaining 18
are, with the evidence that each is an unfinished feature rather than
cruft -- mostly tests covering code no UI reaches, and match arms that
still handle prompts nothing raises. This is a to-do list, not a
backlog of deletions.
2026-07-26 18:35:40 +08:00
Kang c5b18abee1 refactor(desktop): delete dead fields left by the removed render layer
command_search_draft and the terminal_surface_paint_count field were
only ever written; the live paint counter is the free function of the
same name. ProcessTableLabels lost its five column headers along with
process_table_header, and ConnectionIconDef lost the glyph fallback the
SVG path replaced.

Stopped short of the fields and variants that are not cruft: dropping
SnapshotPasswordPromptKind::CloudPush and friends would have deleted 35
live match arms that still know how to handle a prompt nothing raises
any more. That is an unfinished feature, not dead weight, and removing
it is a product call.
2026-07-26 18:35:02 +08:00
Kang bfab0940a3 refactor(desktop): delete the dead items name matching could not clear
Eighteen more items rustc reported dead that the automated pass had to
skip: helpers whose only remaining mentions were pub use re-exports
(sorted_quick_commands, tunnel_mode_label, compact_transfer_job_row,
now dropped from their mod.rs lists too), and items whose names collide
with live ones elsewhere -- next, all, title, placeholder, line_count,
select, session_id, and a second icon_action_button in the process
table that shadows the live one in the connections list.

TerminalSelection::new was in that batch and is not dead; a generic name
made the reference scan miss decorations.rs and terminal_surface_entity.
It is kept. One doc comment pointing at the deleted terminal_line_element
now refers to the paint crate instead.
2026-07-26 18:28:24 +08:00
Kang e8b8ff231e refactor(desktop): delete the superseded migration render layer
133 items that nothing reaches: the old left_*_panel / right_*_panel
render tree superseded by panel_body's *_view dispatch, the widget
helpers only it called, and the handlers only those widgets invoked.

Verified against the live paths first rather than trusting dead_code
alone: for example start_sftp_download_job survives only as a caller of
start_sftp_download_job_for_target, which the browser selection flow
uses directly.

Deleted iteratively, requiring that no source line outside the items
being removed mentions the name -- rustc's dead set is per compilation
unit, so an item dead in the lib build can still be used from cfg(test)
or re-exported.
2026-07-26 18:21:04 +08:00
Kang 75e1c60165 docs(architecture): record the ai.rs domain split
ai.rs is down from 4,032 to 1,554 lines across providers, agent, risk
and settings, so drop it from the large-file table. Note why risk is
deliberately its own small module, and that serde default_* functions
constrain how far a settings type can move from them.
2026-07-26 18:00:51 +08:00
Kang 7abad78916 refactor(core): split settings defaults and masking out of ai.rs
Move the twenty default_* serde backing functions, the AiSettings
Default impl, the legacy profile migration and the mask/merge secret
helpers into ai/settings.rs. ai.rs imports the default_* names back so
the serde attributes on the settings types still resolve.

Field defaults, the legacy profile migration and which fields count as
secrets are compatibility surface and are unchanged.
2026-07-26 17:59:55 +08:00
Kang 03d2abd4c6 refactor(core): split agent protocol and command risk out of ai.rs
ai/risk.rs holds command risk classification -- the pattern lists, the
rm/dd special cases and the label mapping. These decide whether the
agent may run a command unattended, so keeping them in one small module
makes the rules reviewable on their own.

ai/agent.rs holds the agent loop protocol: the three provider tool
schemas, the reply parsers with their fallbacks, the execution policy
and the agent prompt builders.

The pattern lists, escalation rules, tool schemas, parse fallbacks and
execution policy are unchanged.
2026-07-26 17:57:41 +08:00
Kang a673d26b56 refactor(core): split provider wire formats out of ai.rs
Move everything that knows what a provider's HTTP surface looks like
into ai/providers.rs: the five endpoint URL builders, the five request
body builders, the seven response and SSE chunk parsers, their six
extraction helpers and their nine tests.

The request and response shapes, the streaming semantics and the error
mapping are unchanged.
2026-07-26 17:54:26 +08:00
Kang 0595729ebe docs(architecture): record the method ownership rule
Grouping NyaTermApp fields into feature states did not move the methods,
so most desktop modules could still reach most desktop state through
self. Write down the rule the last four rounds followed, the two cases
it does not cover (render helpers and methods that also need a service),
and what the transfer browser round showed about how to spot a good
candidate.
2026-07-26 17:45:06 +08:00
Kang 8264da59db refactor(desktop): move remaining single-state edits onto their state
Cancelling a browser path edit and closing the job menu move to
TransferBrowserState and TransferQueueState; closing the quick command
toolbar popovers moves to QuickCommandFeatureState.

The browser event snapshot/restore pair moves to TransferFeatureState,
so a rewind can only capture and replay transfer state. The captured
fields are unchanged.
2026-07-26 17:44:37 +08:00
Kang 770dcfce52 refactor(desktop): move AI panel transitions onto their state
Seven NyaTermApp methods only rearranged self.ai. Each now lives on the
concern it belongs to: message menu and @mention tracking on AiChatState,
the two confirm dialogs on AiHistoryState and AiAgentState, the error
notice on AiPanelState, and the two that genuinely span concerns --
clear_quote and start_new_chat -- on AiFeatureState.

start_new_chat gains grouping by concern and reuses the mention reset
instead of repeating it; the fields it clears, the default-mode preview
seed and the untouched provider settings are unchanged.
2026-07-26 17:42:14 +08:00
Kang 6ddcfaa4a7 refactor(desktop): move send command bar edits onto its state
Four NyaTermApp methods only touched self.send_command: clamping the hex
guide scroll, closing the four option menus, parsing the repeat count and
formatting the interval field. They now live on SendCommandComposerState
and SendCommandOptionsState, which is the phase each actually belongs to,
and the NyaTermApp methods forward.

The hex viewport constants, the count clamp range and the live-typing
rule are unchanged.
2026-07-26 17:39:54 +08:00
Kang 21f29416e9 refactor(desktop): move security panel transitions onto its state
Eight NyaTermApp methods only rearranged self.security: closing the four
editors, cycling the OTP algorithm, cancelling a delete, closing the
unlock prompt and locking secrets. They now live on SecurityFeatureState
and the NyaTermApp methods forward and notify.

lock_secrets reuses close_unlock_prompt instead of repeating its four
assignments. Which secrets are dropped is unchanged: revealed passwords
and credentials are cleared, revealed OTP codes are display-only and
regenerate on demand, so they are left alone exactly as before.
2026-07-26 17:37:38 +08:00
Kang fe10089034 refactor(desktop): move transfer browser column resize onto its state
The three resize handlers only read and wrote self.transfer.browser, so
they now take &mut TransferBrowserState and the NyaTermApp methods are
forwarders that own the redraw. The update and finish helpers return
whether anything changed so the forwarder can skip a needless notify.

TransferBrowserColumnResizeState no longer leaks into the transfers page
module, which is what the narrower receiver buys.
2026-07-26 17:37:32 +08:00
Kang c814de10bd docs(architecture): record the storage and transport domain splits
storage.rs is down from 7,662 to 4,020 lines and transport/lib.rs from
8,418 to 4,423. Update the large-file table, the decisions section and
the suggested order to match, and say why domain cuts worked where the
earlier type-by-type extractions did not.
2026-07-26 17:23:55 +08:00
Kang 6d1c1cb49d refactor(transport): split SSH authentication out of lib.rs
Move the key/password/keyboard-interactive auth path, the OTP and prompt
classification helpers, the runtime secret request and their four unit
tests into ssh_auth.rs.

The method order, prompt classification heuristics and auto-fill rules
are unchanged.
2026-07-26 17:23:01 +08:00
Kang 1b73b0f55c refactor(transport): split SSH tunnels out of lib.rs
Move the five tunnel types, the SshTunnelManager impl, the local /
dynamic / remote forwarding loops, the SOCKS5 handshake and their two
unit tests into tunnel.rs.

Bind address handling, the SOCKS5 handshake and the per-mode forwarding
loops are unchanged.
2026-07-26 17:20:32 +08:00
Kang fe4293db9c refactor(transport): split X11 forwarding out of lib.rs
Move the five X11 types, the display resolution and xauth cookie
helpers, the auth rewriter, the channel relay and their eight unit tests
into x11.rs. lib.rs re-exports the public surface unchanged and keeps
the four internal items it still uses behind pub(super).

Display spec parsing, the cookie rewrite rules and the per-platform
error messages are unchanged.
2026-07-26 17:18:17 +08:00
Kang a5e37cab7a refactor(transport): split SFTP out of lib.rs
Move the SftpService impl, the thirty-seven browse/transfer helpers and
their ten unit tests into sftp.rs, with an explicit import list rather
than a use super::* glob.

The wire protocol, retry and resume behaviour, conflict resolution and
progress reporting are unchanged.
2026-07-26 17:15:44 +08:00
Kang 04b3c20059 refactor(core): split cloud sync settings out of storage.rs
Move the cloud sync state and settings accessors, the encrypt/decrypt
pair and the seven drive-secret helpers into storage/cloud_sync.rs.

Which fields are treated as secrets, how masked values are merged back
and the document keys are unchanged.
2026-07-26 17:11:17 +08:00
Kang f303e47acd refactor(core): split the app settings document out of storage.rs
Move load_app_settings_summary, the master password pair and the
seventeen save_*_settings methods into storage/app_settings.rs, together
with the twenty-nine json_* / normalize_* / default_* helpers that had no
caller left outside them.

Document keys, field names and defaults are unchanged.
2026-07-26 17:08:53 +08:00
Kang 8b4dbe51e3 refactor(core): split portable snapshot and config backup out of storage.rs
Move the four snapshot export/import entry points, the two config
database backup/restore methods, the raw snapshot build/apply pair and
their eight table-level helpers into storage/portable.rs.

The on-disk snapshot layout, the encrypted envelope and the merge rules
for imported settings are unchanged.
2026-07-26 17:06:34 +08:00
Kang 9c6f43281d refactor(core): split the secret vault out of storage.rs
SSH keys, OTP entries, saved passwords and saved credentials share the
credentials table and the same encrypt-on-write / decrypt-on-read shape.
Move all four CRUD groups and the two SSH key file helpers into
storage/vault.rs; eight secret types drop out of storage.rs's crate
import list.

Table name, key prefixes and record layout are unchanged.
2026-07-26 17:04:29 +08:00
Kang eddcda7ef8 refactor(core): split AI history and audit out of storage.rs
Move the ten AI chat/audit persistence methods, the session-title helper
and their two round-trip tests into storage/ai_history.rs. Ten AI types
drop out of storage.rs's crate import list as a result.

Document keys, record shapes and the trimming rules are unchanged.
2026-07-26 17:02:00 +08:00
Kang 105869adc4 refactor(core): split command history and known hosts out of storage.rs
`storage.rs` was 7662 lines with one `impl ConnectionStore` spanning
three thousand of them. Earlier rounds extracted a few pure type modules,
which barely moved the count because the types were the small part.

These two go out as whole domains instead, each taking everything that
belongs to it: table constants and key prefixes, record types, the
`*_in_txn` helpers, and — for command history — the prompt-stripping
logic that only `sanitize_history_command` used.

`known_hosts` also took `storage.rs`'s only uses of `base64`, `hmac` and
`sha1`. Three crate dependencies leaving with one domain is the sign the
seam was in the right place; a type-by-type extraction would never have
surfaced that.

Table names, key layouts, record shapes, the legacy text-doc import and
the hashed-host matching rules are unchanged — this moves code, it does
not touch the persistence contract. 166 core tests still pass, including
the known-hosts structured/hashed/raw round-trip, the match/changed/
unknown distinction, the legacy import, and command-history
normalization.

storage.rs: 7662 -> 7077 lines.
2026-07-26 16:37:42 +08:00
Kang c40c275fc2 docs(architecture): bring the suggested order up to date
Items 1, 3 and 4 are done, so the list was describing work that no longer
exists. It now records what is actually left, and two things worth
knowing before picking it up.

The `use super::*` item does not batch. Every file needs its own
dependency set resolved, so it is 355 small compiler-guided edits rather
than one sweep like the `#[path]` removal was.

And grouping `NyaTermApp` fields did not move the 236 `impl NyaTermApp`
blocks. The field count is down from 585 to 279, but most desktop modules
can still reach most desktop state through `self`; the methods are the
next structural question, not the remaining fields, which are a long tail
where the biggest domain is eighteen.
2026-07-26 16:30:29 +08:00
Kang c00d3c5963 refactor(desktop): collapse the store snapshot publish loop
The remaining three snapshots turned out to be a closed loop, so the
whole publish path goes.

`WorkspaceSnapshot` and `SessionSnapshot` were read only by
`published_core_store_snapshots_are_current`, which decided whether to
republish them. `OverlaySnapshot` was a same-render round-trip: `Render`
published it in its prologue and `overlay_host` read it back a few calls
later, falling back — when the store was empty — to an expression that
recomputed every field from the same `self` fields. `overlay_host` now
evaluates those flags directly into a local `OverlayFlags`, which is
exactly what the fallback did.

None of it had an observer. `AppShell` deliberately does not observe the
stores; the comment there records that store-observe was amplifying each
publish into an extra shell paint. So every `cx.notify()` in the publish
path was landing on nothing.

Gone with it: `publish.rs`, the publish throttle
(`should_publish_store_snapshots`, `store_snapshot_publish_due`,
`STORE_SNAPSHOT_HEARTBEAT`, `last_store_snapshot_publish_at`),
`WorkspaceStore`, `SessionStore`, and the snapshot half of
`OverlayStore`. The runtime tick loses a per-tick snapshot build and
comparison; the two slow-tick diagnostic fields that reported on it are
dropped, and `output_pressure` is still computed for the rest.

The four surviving stores each own something `NyaTermApp` does not:
`Runtime` (app runtime and native services), `WindowRuntime` (the pump),
`StartupRestore` (the restore queue) and `Overlay` (quick switch state).
`entities/` drops from 959 to 412 lines and the projection layer is gone.

Ten entity tests covered only the deleted stores; 548 tests become 538.
2026-07-26 16:19:42 +08:00
Kang fe1e19f60a docs(architecture): record what is left of the store publish loop
While tracing consumers for the previous commit two things turned up that
the next person should not have to rediscover.

`OverlaySnapshot` is a same-render round-trip. `Render` publishes it in
its prologue and `overlay_host` reads it back a few calls later; when the
snapshot is absent, the fallback recomputes every field from the same
`self` fields it was published from. That fallback is proof the renderer
never needed the store. It is not a staleness bug today only because the
publish happens earlier in the same pass.

And `AppShell` deliberately does not observe the stores — there is a
comment explaining that store-observe was amplifying every publish into
an extra shell paint — so the `cx.notify()` inside each publish has no
subscriber.

Both point at the same conclusion for `Workspace`/`Session`/`Overlay`,
but that change lands in the render path and the publish throttle, so it
belongs in its own round after the current one is verified.
2026-07-26 16:07:30 +08:00
Kang b702a79234 refactor(desktop): delete write-only entity store projections
Tracing consumers settled the half-migrated Entity Store question. Six
of the stores — `Ai`, `CloudSync`, `Connections`, `RemoteOps`,
`Settings`, `Transfer` — were write-only. Outside `entities/` they
appeared only in `app_shell::new`, where they were constructed and
stashed in `UiStoreHandles`; nothing ever read their snapshots. Every
qualifying tick built six snapshot structs, compared them, and called
`cx.notify()` for a reader that does not exist.

They are deleted, along with the code that fed them: six accessors added
purely so the projection could read state through semantic methods, and
`SettingsTab::label`, which returned hardcoded English while the actual
UI uses `i18n_key`.

What remains has a reason to exist. `RuntimeStore`, `WindowRuntimeStore`
and `StartupRestoreStore` own real state. `OverlayStore` owns quick
switch authoritatively and its snapshot is read by `root.rs` when
rendering overlays.

`WorkspaceStore` and `SessionStore` stay for now, and the doc records why
they are the honest remaining question: their snapshots are read only by
`published_core_store_snapshots_are_current`, which decides whether to
republish them. That loop is self-referential, but it also gates the
overlay publish, so untangling it is a separate change rather than a
free deletion.

Three entity tests covered only the deleted stores and go with them;
552 tests become 548.
2026-07-26 16:04:11 +08:00
Kang b7b7ea8eeb refactor(desktop): group send command bar state
Twenty-four `NyaTermApp` fields belonged to the send-command bar. They
move into `SendCommandFeatureState`, split by the three phases the bar
actually has: `composer` holds the payload being written and where the
caret is, `options` holds how that payload is interpreted and delivered
along with the menus that set those, and `progress` holds the in-flight
send — cancellation flag and the counters shown while it runs.

The flat naming interleaved all three, so a field like
`send_command_progress_round` sat between menu-open booleans and hex
scroll offsets.

`app_state` no longer imports anything from `crate::send_command`. Field
count goes from 302 to 279.
2026-07-26 15:52:54 +08:00
Kang 76ac195154 refactor(desktop): group terminal presentation state
Forty-seven `NyaTermApp` fields were terminal presentation state. They
move into `TerminalFeatureState`, split by what the code actually does
with them: `search`, the `view` runtime (live views, surfaces, frame
pipeline, scroll), `input` focus and IME, `selection` and mouse
reporting, painted `layout` geometry, `menus`, and the split/tab
`windows` tree.

This is presentation state only. Parsing, snapshots and the wire
protocol stay in `nyaterm-terminal` and `nyaterm-transport`, and nothing
here changes what is sent, decoded or drawn — the fields keep their
types and initial values, only their address changes.

`OverlaySnapshot` keeps its own `terminal_actions_open` and
`terminal_context_menu_open` projection fields with the old names; the
rewrite anchored on `self`/`this`/`app`, so `overlay.terminal_actions_open`
was left alone. Two accesses through `entity.read(cx)` needed fixing by
hand, which the compiler pointed out.

Field count goes from 348 to 302.
2026-07-26 15:45:13 +08:00
Kang 9b60f14512 refactor(desktop): group ai state by concern
Seventy `NyaTermApp` fields carried the `ai_` prefix. They cover six
unrelated concerns, now one struct each in `AiFeatureState`: provider
`settings`, the `chat` composer and transcript, session `history`, model
`discovery`, the agent loop, and `panel` chrome.

Two details worth noting for review:

`SettingsDraftSnapshot` has its own `ai_settings`, `ai_model_draft`,
`ai_base_url_draft` and `ai_secret_draft` fields with exactly the names
being moved. They are deliberately unchanged — it is a separate snapshot
type — and the rewrite was anchored on `self`/`this` receivers so
`snapshot.ai_settings` was left alone while `self.ai_settings` became
`self.ai.settings.config`.

The constructor destructures a loaded-store tuple that also binds
`ai_settings`, `ai_session_count`, `ai_message_count` and
`ai_audit_count`. Those are local bindings, not struct fields; the field
sweep removed them and the compiler caught it immediately.

`app_state` drops eight more model imports. Field count goes from 417 to
348 — down from 585 when this series started.
2026-07-26 15:38:48 +08:00
Kang c1e40fd06f refactor(desktop): group transfer state by concern
Seventy-eight `NyaTermApp` fields carried the `transfer_` prefix, which
made them look like one feature. They are not. Grouping them into
`TransferFeatureState` separates five things that merely share a panel:

- `queue`   upload/download jobs and their menus
- `browser` the SFTP listing, navigation history, selection and menus
- `file_ops` rename/move/delete/create/properties dialogs
- `editor`  the built-in remote file editor workspace
- `external_sync` handing a file to an outside editor and syncing back

plus `paths` for the manual transfer endpoints and `panel` for focus
routing and height. Their lifetimes are unrelated — an open rename
dialog has nothing to do with a running upload — and the flat prefix
hid that completely.

`app_state/mod.rs` drops twenty-five transfer UI model imports, the
largest single reduction so far. Field count goes from 494 to 417.

977 accesses were rewritten. The first pass anchored on `self`/`this`/
`app` receivers; a second pass took the remaining seven, which reach the
app through `entity.read(cx)`, after confirming no method and no other
struct shares any of these names.
2026-07-26 15:33:04 +08:00
Kang 0ee2a31f4c refactor(desktop): group security panel state
Twenty-three `NyaTermApp` fields were the security panel. They move into
`SecurityFeatureState`, split by what they actually are: the four editors
and their focus handles in `editors`, revealed passwords/credentials and
generated OTP codes in `revealed`, and the master password prompt in
`unlock`.

The `revealed` grouping is the useful part of this one. Values the user
has explicitly unmasked were previously three same-shaped maps sitting
among twenty other fields; collecting them makes it obvious that they
are display state with a shared lifetime, and gives a single place to
hang clearing behaviour later. Secrets themselves still live in
`nyaterm-core`; nothing about storage or decryption changes here.

One access came through `input_entity.read(cx)` rather than `self`, which
the receiver survey missed and the compiler caught.

Field count goes from 516 to 494.
2026-07-26 15:24:22 +08:00