Commit Graph
1763 Commits
Author SHA1 Message Date
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
Kang 6f3938c76f refactor(desktop): group remote page state by pane
Fifty-four `NyaTermApp` fields were the Remote page: Docker, the process
table and host stats, distinguished only by a `docker_` / `process_` /
`stats_` name prefix. They move into `RemoteOpsFeatureState` with one
struct per pane, named after the existing `RemoteOpsStore` projection.

Grouping them makes a symmetry visible that the flat naming hid: all
three panes carry the same refresh bookkeeping — job id, owning session
id, pending flag, consecutive failure streak and last refresh instant —
so `DockerPaneState`, `ProcessPaneState` and `StatsPaneState` now share
a recognisable shape. A future round can lift that into one struct.

The three job channels are created inside `RemoteOpsFeatureState::new`
rather than in `NyaTermApp::new`, since construction was the only place
that touched them. Two names read better in context: `processes` is
`process.items`, and `remote_stats` is `stats.data`.

`app_state/mod.rs` drops five more UI model imports. Field count goes
from 570 to 516.

The 486 rewritten accesses were anchored on `self.`/`this.` receivers
only, after confirming those are the sole receivers for every field, so
unrelated `.process_*` and `.stats*` calls on other types were left
alone. A second pass caught the accesses rustfmt had split across lines.
2026-07-26 15:20:31 +08:00
Kang 14da429f16 refactor(desktop): group quick command UI state
Twenty-seven of the fields on `NyaTermApp` were quick command panel,
overlay and editor state addressed as `self.quick_command_*`. They now
live in `QuickCommandFeatureState`, following the shape that
`ConnectionFeatureState` already validated: `list`, `editor`, `dialogs`,
`import` and `ai` sub-structs, built from a single
`QuickCommandFeatureFocus` bundle instead of seven separate
`cx.focus_handle()` fields at the app level.

The persisted collections stay where they were. `quick_commands` and
`quick_command_categories` are store-loaded data, not UI state, so they
remain on `NyaTermApp` exactly as the connections list does.

Two names get clearer in the move: the row overflow menu is `list.row_menu`
rather than a bare `quick_command_menu`, and the editor draft is
`editor.draft` rather than `quick_command_editor`, matching
`ConnectionEditorFeatureState`.

`app_state/mod.rs` no longer imports any of the eleven quick command UI
model types, which is the point: the app struct stops knowing how the
quick command panel represents its overlays. Field count drops from 585
to 570.

The 222 rewritten accesses were anchored on `.quick_command_<field>` with
a word boundary and applied longest-name-first, so helpers such as
`quick_command_category_label` and fields such as
`quick_command_editor_focus` were not caught by shorter prefixes.
2026-07-26 15:13:56 +08:00
Kang 575b59a401 refactor(desktop): remove the last #[path] declarations
The `pages` tree, `http/cloud_sync` and `models/workspace_tabs` were the
last pseudo-module roots. Each moves to `X/mod.rs` so its children
resolve by directory, and `pages/remote/docker` stops aliasing six
sibling files (`docker_containers.rs` as `mod containers`, and so on)
in favour of a normal `docker/` directory.

`nyaterm-terminal-gpui` had one redundant `#[path = "tests.rs"]` that
already pointed at the default location; it is dropped too.

`#[path = "..."]` is now absent from both crates, so the twenty-two
per-directory guards collapse into one crate-wide `check_no_matches`.
Module paths always match the directory layout again, which is what
makes `pub(in ...)` bounds mean something and what makes the next step,
replacing the `use super::*` chain with explicit imports, worth doing.
2026-07-26 15:02:51 +08:00
Kang f242c56308 refactor(desktop): nest layout, panels and inspector view modules
`layout`, `panels`, `inspector`, `formatting` and `view_widgets` were the
same pattern as the runtime areas: an `X.rs` pseudo-module root next to
an `X/` directory whose children were pulled in with `#[path]`.

Move each root to `X/mod.rs` and let the children resolve by directory,
including the nested `security_panel/panel`, `workspace/surface`,
`quick_commands_panel/panel`, `send_command_bar`, `tab_actions_overlay`
and `ai_widgets` subtrees. All five areas are guarded against new
`#[path]` declarations.

Desktop `#[path]` count drops from 178 to 90. What remains is the
`pages` tree plus two files under `models` and `http`.
2026-07-26 14:59:14 +08:00
Kang 99f972d1fa refactor(desktop): remove #[path] from the features root
The remaining twenty-one flattened declarations in `features/mod.rs`
covered the AI, commands, settings, sync, transfers, remote and
translation directories. Each becomes a real module with its own
`mod.rs`, and their `ai_runtime`, `command_runtime`,
`quick_command_runtime`, `security_runtime`, `settings_runtime`,
`cloud_sync_runtime`, `transfer_jobs` and `remote_runtime` subtrees
become directory modules as well, along with the deeper `ai_runtime/chat`,
`ai_runtime/settings` and `quick_command_runtime/import` trees.

`features/mod.rs` now contains zero `#[path]` declarations, which the
boundary script enforces, and every feature directory is guarded too.

As in the previous rounds, nesting surfaced real visibility: three
event-drain methods were `pub(super)` and only reachable because their
module used to be a flat sibling of the event pump. Ten more
`crate::features` level re-exports turned out to be unused once each
consumer sat inside the owning subtree, and were removed.

Desktop `#[path]` count drops from 247 to 178; what is left lives in the
view layer (`pages`, `panels`, `layout`, `inspector`) and in `models`.
2026-07-26 14:56:57 +08:00
Kang 4220c0a3a8 refactor(desktop): nest terminal feature modules
Eight terminal modules were declared in `features/mod.rs` through
`#[path = "terminal/..."]`, and four of them declared their own children
the same way.

Make the whole area a real module tree:

- `mod terminal;` with `features/terminal/mod.rs` owning the children.
- `terminal_runtime`, `terminal_surface`, `terminal_selection_runtime`
  and `terminal_context_menu_runtime` become directory modules,
  removing seventeen further `#[path]` declarations.
- Terminal internals are addressed as
  `crate::features::terminal::terminal_runtime` instead of a top-level
  `crate::features::terminal_runtime`.

Nesting also showed that nine prompt and terminal symbols no longer
needed a `crate::features` level alias, because every consumer now
reaches them inside the owning subtree. Those re-exports are removed,
which keeps the warning count at the existing baseline.

`features/mod.rs` is down from 52 `#[path]` declarations to 21, and the
desktop total drops from 272 to 247.
2026-07-26 14:53:04 +08:00
Kang c9b933e814 refactor(desktop): nest session feature modules
Thirteen session modules were declared in `features/mod.rs` through
`#[path = "session/..."]`, so `features/session` was a directory with no
module of its own.

Make it a real module:

- `mod session;` with `features/session/mod.rs` owning the children.
- `session_runtime` becomes a directory module, removing its two
  remaining `#[path]` declarations.
- Prompt and auth exports, plus the trzsz/zmodem session-state types
  used by `app_state`, reach `crate::features` through explicit
  re-exports instead of flattened module declarations.

Nesting immediately surfaced real visibility: four prompt-drain methods
were declared `pub(super)` and were only reachable from the event pump
because the module used to be a flat sibling. They are now
`pub(in crate::features)` on purpose rather than by accident.

`features/session` has zero `#[path]` declarations and is guarded.
Desktop `#[path]` count drops from 287 to 272.
2026-07-26 14:48:36 +08:00
Kang 1e0b990ffe refactor(desktop): nest shell feature modules
`features/mod.rs` declared twelve shell modules through
`#[path = "shell/..."]`, so the directory looked layered while the module
tree stayed flat: every shell module was a direct child of
`crate::features` and `pub(in crate::features)` meant "visible to the
whole desktop feature crate".

Make `features/shell` a real module:

- `mod shell;` with `features/shell/mod.rs` owning the twelve children.
- `event_pump` and `keybinding_runtime` become directory modules, so
  their own `#[path]` declarations go away too.
- Shell chrome exports reach the rest of `crate::features` through
  explicit re-exports in `features/shell/mod.rs` instead of twelve
  flattened module declarations.

`features/shell` now has zero `#[path]` declarations, and the boundary
script guards that instead of allowing a baseline of five. Desktop
`#[path]` count drops from 306 to 287.

The suggested order in the migration status doc is reworked to put the
module tree first: narrowing the shared prelude one symbol at a time
produced little real encapsulation while every feature directory was
still flattened into one namespace.
2026-07-26 14:44:55 +08:00
Kang a88d04d979 fix(scripts): make architecture boundary check portable
The guard was only reliable on the machine it was written on:

- `rg` prints backslash separators on Windows, so the legacy-source
  allowlist and the `use super::*` scope check never matched their
  `case` patterns and reported every file as a violation. Both call
  sites now pass `--path-separator /`.
- `IGNORECASE` is a gawk extension. Under gawk the secret-Debug scan
  matched CamelCase names such as `KeywordHighlightRule` and produced
  dozens of false positives; under mawk it was silently case-sensitive.
  Drop it so the result is the same everywhere, and note that the
  heuristic is currently weak and still needs a real triage pass.
- `\(` and `\.` inside an awk regex are implementation-defined; some
  awks strip the backslash and then fail with "unbalanced (". Use
  bracket expressions instead.
- Drop the stale `pages/connections.rs` path; that module became a
  directory earlier and `rg` failed on the missing file.
2026-07-26 14:44:25 +08:00