Commit Graph
615 Commits
Author SHA1 Message Date
l0ng-ai e1531cdea6 revert(windows): drop the taskbar status dot (#377)
The per-window taskbar overlay badge (#355, for #199) is removed, and with
it the in-flight follow-up that was making its green "finished a turn"
state reachable: the feature is not wanted. Nothing shipped — the badge
only ever existed in Unreleased — so this is a plain removal rather than a
deprecation, and its CHANGELOG entry goes with it instead of gaining a
"Removed" counterpart.

What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the
`taskbar_status_icon` config flag and its Settings → Window & Tabs row and
strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` /
`RemoteTerminal::shell_busy` (the overlay was their only caller), the
`raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging`
feature it needed, and the feature docs in both languages. A stale
`taskbar_status_icon` left in someone's `config.json` is ignored, as any
unknown key is.

The tray badge and the in-window status dots are untouched; they were
always the ones the taskbar was mirroring.
2026-08-06 21:40:25 +08:00
l0ng-aiandl0ng-ai e3dded4be3 fix(release): keep the Inno payload for the package verifier to read (#368)
`verify-windows-package.ps1` reads the Inno staging directory to check what
lands in {app} — the compiled setup.exe cannot be read back without
innoextract, which the runners do not carry. But `bundle-windows.ps1` deleted
that directory as its last act, so the verifier has failed on every Windows
build since the check arrived in #330: "the Inno staging directory is missing".

Nightly has been red for two nights (2026-08-05, 2026-08-06) and a stable
release would fail the same way — release.yml runs the same step.

Both workflows already expected the directory to survive: their upload steps
name it among the dist/ intermediates the asset globs deliberately skip. So
this drops the removal rather than teaching the verifier to tolerate an absent
payload, which would retire the check it was added to make.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 13:14:08 +08:00
l0ng-aiandl0ng-ai 4dbbe95be9 refactor(settings): trim the About page down to what it is for (#358)
The About page carried a marketing paragraph and two multi-sentence
explanations that walked through updater internals and per-platform
support. None of it helps someone who is already running the app.

Drop the feature-list paragraph, cut the update and server text to a
single sentence each, move the tech credits to the bottom of the page,
and render the update toggle with settings_row so it matches every
other switch in Settings. The tagline and credits line now match the
README.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 12:11:39 +08:00
l0ng-aiandl0ng-ai 0c61e533b3 fix(terminal): put back the cursor ConPTY parks at the end of a repaint (#362)
conhost's VT renderer brackets every frame it paints with `?25l` … `?25h` so
the cursor does not flicker across the repaint, and it moves the cursor
explicitly just before the `?25h` only on the frames where it painted the
cursor. On the other frames the show commits wherever the last erase or write
left it — the tail of the status line, the head of a row — and the cursor
blinks there until conhost's next frame moves it back.

tty7 repaints when a batch of pty output lands, so it draws that cursor for a
frame. A TUI that repaints on a spinner produces one every tick, which reads as
a second cursor blinking in the wrong place. macOS never shows it: no ConPTY
sits in between, and the TUI itself always moves the cursor before showing it.

Measured on Windows 11 26200 from a raw ConPTY capture of a Codex session, a
110x30 pty, cursor-visible dwell per cell:

  in-box conhost: 295 ms across 42 frames parked at the end of the status line,
                  each stray corrected 7-15 ms later by the following frame
  with this fix:  that cell never appears; those frames fold back into the
                  composer cell the repaint hid the cursor on

A scanner over the stream pairs the hide with its show and marks the show as
parked when the run in between moved the cursor around to paint but did not end
on a move — nothing chose the cell it is about to appear on. The repair then
restores the cell the cursor stood on when it went invisible, which is where the
correcting frame would have put it anyway. A hide and a show more than 100 ms
apart are an application keeping the cursor off for the length of some work, not
a renderer bracketing one frame, and are left alone.

This does not cover the other ConPTY cursor artifact: conhost also samples an
application's partially written frame, and then it *does* emit an explicit move,
so the stray position is genuine — just transient — and nothing in the stream
tells it apart from a real one. Only a render-side settle would catch that.
Microsoft's ConPTY redistributable avoids both, so with the bundled pair beside
the daemon this is inert; it earns its keep on hosts without it, notably a
remote Windows server.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 11:51:50 +08:00
l0ng-aiandl0ng-ai 3ae340408b fix(windows): bundle Microsoft's ConPTY so panes can answer color queries (#360)
* fix(windows): bundle Microsoft's ConPTY so panes can answer color queries

The in-box conhost swallows a pane process's OSC 11 background query: it
never reaches tty7's emulator and no reply is ever written back, so
applications that choose a light or dark UI from the terminal background
render a dark UI under a light theme.

tty7 already answers OSC 10/11/12 from the live theme, so nothing was
missing but a pseudoconsole that forwards the question. Microsoft ships one
as a redistributable, and portable-pty already prefers a sideloaded
conpty.dll over kernel32's, so this is packaging rather than code: the pair
goes beside tty7-app.exe, where the DLL search path finds it.

Measured on Windows 11 26200, same binary, only the pair added beside it:

  in-box conhost: the terminal side never sees the query; the client times
                  out with no reply
  bundled ConPTY: the terminal side sees ESC]11;?BEL and a real pane reads
                  back rgb:efef/f1f1/f5f5 under catppuccin_latte, which is
                  the preset's exact background

The two files are one supported unit, so the release verifier fails a
package that carries only one, a mismatched pair, or the MIT notice-less
DLL. They also join PORTABLE_MANAGED_ROOTS, without which the updater would
reject every portable archive that contains them; they are deliberately not
required by verify_portable_payload, since tty7 runs without them and a
packaging slip should fail the release rather than a user's update.

build.rs stages the pair beside cargo's output so a development build does
not quietly run on the in-box host, and the daemon logs which pseudoconsole
it got.

Closes #345

* fix(windows): restage the bundled ConPTY when it goes missing

Watching only the vendored sources meant a staged copy that left the target
directory stayed gone: the build script was cached, so it never ran again to
put it back, and the build silently fell back to the in-box conhost. Cargo
treats a rerun-if-changed path that does not exist as changed, so naming the
destinations makes the staging self-healing.

Found by deleting target/debug/conpty.dll and watching the next build not
bring it back.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 11:36:02 +08:00
l0ng-aiandl0ng-ai 7c0598c7e0 fix(bell): ring the system bell on Windows (#359)
ring_system_bell() was macOS-only and returned false everywhere else, so
on Windows and Linux the Audible mode fell straight through to its visual
fallback: Visual, Audible, and the new Both were three names for one
behavior. Windows has MessageBeep, so two of those three now differ.

MB_OK plays the "Default Beep" scheme entry, which follows the user's
choice in Sound Settings rather than synthesizing a fixed tone at the
speaker the way Beep() does. The Win32 metadata files MessageBeep under
Diagnostics::Debug despite it being a user32 export, hence the extra
windows-sys feature; no new crate and no dbghelp.

Linux is left on the flash fallback on purpose: libcanberra and PipeWire
are runtime links away and XBell does nothing under Wayland.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 11:25:12 +08:00
27bb1864df feat(windows): taskbar status overlay per window (#355)
* feat(windows): taskbar status overlay per window (#199)

Stamp a colored status dot on each window's taskbar button using the
same palette as the in-window agent dots:
- blue while a shell command or agent is working,
- amber when an agent is waiting on the user,
- green when work finishes while the window is unfocused (cleared on activation).

Adds a `taskbar_status_icon` setting (default on, Windows only) and a
Settings -> Window & Tabs row. The overlay is updated by a foreground
poll that aggregates agent status and shell busy state across each
window's panes, diffing against the current taskbar badge and only
calling ITaskbarList3::SetOverlayIcon when the badge changes.

Includes unit tests for overlay priority and the done-while-unfocused
edge tracking.

* fix(taskbar): retry a failed overlay instead of caching it as drawn

Four fixes on top of the overlay:

- A failed SetOverlayIcon was still recorded in `shown`, so a badge the
  taskbar never took was remembered as drawn and never retried. Stamp now
  reports success, and a failure drops the interface so the next tick
  re-creates it — which is also what an Explorer restart needs.
- `create_failed` was a permanent latch: one CoCreateInstance failure
  killed the badge for the whole process, though Explorer may simply not
  be up yet when the first window opens. Use the tray's attempts/cooldown
  backoff instead, which this module otherwise copies.
- The overlay's accessibility description was hard-coded English in an
  app that localizes everything else. Reuse the panel and tray strings.
- Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi,
  so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon`
  already renders at 32 off macOS for the same reason.

Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and
HICON all live in Win32_UI_WindowsAndMessaging, and the build and the
taskbar tests pass without it.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-06 10:12:41 +08:00
dependabot[bot]andl0ng-ai 3235cd091c ci: bump actions/download-artifact from 7 to 8 (#325)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-06 10:07:09 +08:00
Hongwei Qinandl0ng-ai fcd8f00be6 feat(bell): add combined visual + audible terminal bell mode (#357)
* feat(bell): add combined visual + audible terminal bell mode

Currently BellMode only offers None, Visual, and Audible. Audible falls
back to Visual if the system bell cannot be rung, but there is no way to
intentionally get both at once.

Add a `Both` variant that rings the system bell *and* flashes the pane,
exposing it as a fourth option in Settings -> Terminal -> Bell. Existing
`none`/`visual`/`audible` values remain backward compatible.

Updates config serialization tests and i18n keys/translations/test list.

* fix(bell): align the settings copy and picker with the new Both mode

The bell description still enumerated three outcomes and the settings
search keywords omitted both, so the new mode was invisible to search
and contradicted by the row that labels it. Drop the picker's catch-all
onto the default instead of Both, which is the shape the PR just fixed.

Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 09:48:53 +08:00
2c462dd027 fix(windows): refresh the environment for newly created panes (#349)
Windows hands every process a private copy of the environment block at
`CreateProcess` time and never updates it. A tty7 daemon that has been up
since before an installer edited `HKCU\Environment` therefore gave a
brand-new pane its startup `PATH`, and the freshly installed command was
unresolvable until tty7 restarted (#333) — while a Windows Terminal
launched from Explorer found it, because Explorer rebuilds its own block
when it sees `WM_SETTINGCHANGE`.

Rather than chase broadcast messages, `daemon::windows_env` re-reads the
two hives Windows itself composes a process environment from — the
machine `Session Manager\Environment` and `HKCU\Environment` — at the
moment a pane is spawned, and pins the merge onto the pane's command.

The merge is a pure function over (machine, user, process, configured
overrides), so every semantic that matters is unit-testable without a
registry:

- Names are keyed case-insensitively, so a `Path` from the process block
  and a `PATH` from a hive collapse into one variable instead of reaching
  the child as two.
- `PATH` and `PSModulePath` are *combined* — machine value first, user
  value appended — which is what Windows does and what keeps a per-user
  install from shadowing the system half. Nothing is ever written back to
  the user hive; that would bake the machine half into it permanently.
- `REG_EXPAND_SZ` values are expanded against the merged map, so a user
  value naming a machine value naming a process value resolves. A
  reference that resolves to nothing is left verbatim, as Windows leaves
  it, and the chain is depth-bounded so a self-referential value cannot
  hang the spawn path.
- The machine hive's `USERNAME=SYSTEM` is dropped, as Windows drops it.
- Directories the daemon's own `PATH` held that neither hive lists stay
  reachable, appended behind the registry entries: freshening `PATH`
  should only ever add resolvable commands, never take one away.
- Configured `env` overrides are applied last and win outright.

Non-Windows builds are untouched: only the registry reader and the spawn
wiring are `cfg(windows)`, while `cfg(test)` keeps the pure merge
compiling everywhere so its tests run on every platform.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:01:37 +08:00
l0ng-aiandl0ng-ai 964576040f fix(shell): point $SHELL at the shell the pane actually runs (#348)
A pane configured to run fish still advertised the login shell, because
pane_environment() injected TERM, the TTY7_* markers and TERM_PROGRAM
but never touched SHELL -- so the pane inherited the GUI session's
login-time snapshot of it. Everything that spawns "the user's shell"
read that: tmux's default-shell started zsh inside a fish pane, and so
did sudo -s, an editor's shell escape, and any coding agent picking a
quoting dialect from $SHELL. The failure is silent -- fish rejects the
bash line, the agent's sentinel file never appears, and the rejected
text stays in the line editor to concatenate onto the next send.

Inject SHELL alongside the other markers, set to the absolute path of
the program the pane is about to exec. That program is read off argv
rather than off the shell tty7 resolved: an argv-replacing integration
injection and the parent-shell override both rewrite argv, while
CommandBuilder::get_shell() keeps answering the passwd entry.

Only an absolute path is ever written. A configured command may be bare
(the inventory keeps it bare so PATH decides which install wins), and
consumers exec $SHELL under a PATH of their own, so a bare name is
resolved against the PATH the pane will inherit -- the user's env-block
PATH when they set one -- and skipped when that finds nothing. A stale
login shell beats a name that resolves somewhere else.

An explicit SHELL in the user's env block still wins, the same
precedence TERM_PROGRAM has: tty7 describes the pane, the user's config
gets the last word.

Windows is deliberately left out. Neither cmd nor PowerShell reads
SHELL; the tools that do are the POSIX emulations (MSYS/Git Bash,
Cygwin, WSL), and they want a POSIX path, not the Windows one this
would have to give them. That also leaves the WSL pane alone, where the
pane program is wsl.exe and the distro's own login shell is the right
answer.

Native SSH panes are unaffected: they never build a local command, and
the remote sshd sets SHELL from the remote passwd entry.

Closes #342

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-05 17:53:00 +08:00
l0ng-aiandl0ng-ai ada89bb81a fix(terminal): route right-click to the app while mouse reporting is active (#347)
A TUI that turns mouse reporting on (vim with `set mouse=a`, lazygit,
tmux, …) draws its own right-button menus, and tty7 was delivering one
right-click to both consumers: `TerminalElement::register_mouse_handlers`
forwarded the press to the application, while the `.context_menu(…)` on
the terminal surface was attached unconditionally and popped tty7's own
menu over the top of it.

Gate the host menu on a single pure predicate, `should_show_context_menu`,
and call it from both sides so one click can only ever feed one consumer:
while reporting is active the unmodified right-click is the application's
alone, and Shift stays the escape hatch that reaches tty7 — the same
override Shift already provides for selection and for the wheel.

gpui-component's `ContextMenu` element owns the right mouse-down that
opens the popup: it wraps the terminal surface, so its listener fires
before any handler we can attach, and the builder closure it calls gets
no event to inspect. The verdict is therefore latched on our own right
mouse-down (safe, because the builder runs from a `window.defer` that
lands after the whole mouse dispatch has unwound) and a suppressed menu
is expressed as an item-less `PopupMenu`, which that element already
skips rendering.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-05 17:47:59 +08:00
l0ng-aiandl0ng-ai 2fa518a767 refactor(settings): rescope the About page (#350)
About had grown three sections that change system state and that nobody
looks for under "About": a PATH install, a registry write, and a daemon
restart. Two of them move out.

The `tty7` CLI goes to Agents. That page already describes tty7 <-> agent
integration in one direction (hooks reporting session status); the CLI is
the other direction, and its own description leads with "so scripts and
coding agents can drive tty7". The Loading and Unavailable arms there no
longer return early, since the CLI toggle is about this GUI's own host
rather than whichever machine the hook rows describe.

The Windows Explorer context menu goes to the installer, which is where
VS Code and Git for Windows put theirs: writing shell verbs is an
install-time decision, not a runtime preference. A task checkbox drives
new `--register-explorer-menu` / `--unregister-explorer-menu` flags, so
the key layout stays in core::explorer_context_menu instead of being
copied into the .iss. `status()` existed only to paint the settings UI
and goes with it. The uninstaller unregisters unconditionally: an install
that registered once and was later upgraded without the box ticked still
holds keys that would otherwise point at a deleted exe.

Server restart stays — it is about the app itself.

Also fixes localization the About section had skipped: eight hardcoded
English strings in the update block now have keys, and the orphaned
SettingsCheckUpdatesDesc key (which still claimed "tty7 never updates
itself", contradicted by the macOS in-app updater) is reused for a
one-line description in place of a 60-word account of the updater's
internals.

Finally, terminology in the Chinese UI. hook, agent, worktree, diff and
fork are read and spoken in English by Chinese developers, so translating
them lost more than it gained. Scrollback was worse than a style
question: 回滚 means rollback, the opposite direction. 窗格 for pane is
kept — that one is standard.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-05 17:42:21 +08:00
Hongwei Qinandl0ng-ai 41117e3828 fix(ui): show remote server errors under their switcher group (#354)
* fix(install): prefer bundled server over release download for SSH remotes

SSH remote installs used , which only checks
 and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.

Add  to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch  and  to use it.

This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit  path keeps its strict no-fallback behavior, and WSL
remains bundled-only.

Refs: future issue/PR for bundling server binaries into Windows releases.

* style(install): fix rustfmt formatting in bundled-server tests

* fix(ui): show remote server errors under their switcher group

Remote server restart/replace failures were reported through a global
modal dialog, which mixed errors from different machines together and
blocked the UI.

- Add  to keep per-host error messages.
- Rename  to ; when a
   is available, store the error under that host's key and
  expand its switcher group. Only fall back to a modal when no target is
  known.
- Read  when building switcher groups and surface the
  message in the existing per-group error block.
- Add a Dismiss button to the group error block and clear stored errors
  when the user retries or replaces the server.

Refs: #<issue-number>

* fix(ui): keep remote errors visible when the switcher is closed

The grouped error block is only on screen while the switcher is open, so
routing every failure into it silently swallowed the ones raised from the
window menu's restart-server command and from a mismatch hit mid-connect.
Fall back to the modal whenever there is no switcher to put the error in.

Also scope the Dismiss button to its own host: it retired whatever connect
flow happened to be in `self.connect`, including one still connecting to a
different machine. And clear a stored error when a fresh connect to that
host starts, so a later successful connect does not leave the group showing
a stale failure.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-05 12:46:02 +08:00
Hongwei Qinandl0ng-ai 257d13e607 fix(ui): replace remote server binary from the mismatch dialog (#352)
* fix(install): prefer bundled server over release download for SSH remotes

SSH remote installs used , which only checks
 and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.

Add  to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch  and  to use it.

This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit  path keeps its strict no-fallback behavior, and WSL
remains bundled-only.

Refs: future issue/PR for bundling server binaries into Windows releases.

* style(install): fix rustfmt formatting in bundled-server tests

* fix(ui): replace remote server binary from the mismatch dialog

The version/protocol mismatch dialog previously offered a 'Restart Server'
button that only restarted the existing daemon without replacing the
incompatible binary. This left users stuck on the same mismatch after the
restart.

- Change restart_mismatched_remote_server to call replace_remote_server
  (replace binary + restart daemon) instead of restart_remote_server.
- Add a dedicated L10nKey::RemoteMismatchReplaceServer ('Update Server' /
  '更新服务器端') and use it for the dialog's action button and detail text.
- Update the mismatch title/detail copy so it describes replacing the server
  binary rather than restarting it.

Refs: #351

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-05 12:39:49 +08:00
ARNOandl0ng-ai 8a7b2f3bea style(terminal): increase powerline half-circle segments for smoother curves (#341)
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-05 11:15:55 +08:00
Hongwei Qinandl0ng-ai 4cf3d4dad5 fix(install): prefer bundled server over release download for SSH remotes (#344)
* fix(install): prefer bundled server over release download for SSH remotes

SSH remote installs used , which only checks
 and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.

Add  to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch  and  to use it.

This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit  path keeps its strict no-fallback behavior, and WSL
remains bundled-only.

Refs: future issue/PR for bundling server binaries into Windows releases.

* style(install): fix rustfmt formatting in bundled-server tests

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-05 10:57:36 +08:00
603bca171e feat(updater): add windows updates and cross-platform nightly support (#330)
* feat(updater): add windows online updates

* feat(updater): support online updates for windows portable zip builds

f

* feat(updater): support online updates for nightly build

* fix(updater): strengthen post-download update verification

* feat(updater): support explicit stable and nightly channel switching

* fix(i18n): localize update settings ui

* fix(settings): prevent slider value labels from wrapping

* feat(updater): drop the nightly channel, refuse all-users Windows installs

Follow-up to the Windows updater work on this branch, applying maintainer
review.

Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.

Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.

An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.

Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:27:37 +08:00
4ec6b1df45 fix(terminal): upload pasted images to the remote host in SSH panes (#338)
* fix(terminal): upload pasted images to the remote host in SSH panes (#337)

* fix(terminal): stage remote clipboard images in a private dir, off the UI thread

Review follow-up to the SSH image-paste upload.

`/tmp/tty7-clipboard-<user>` is a predictable name in a world-writable
directory: any local account on the remote host could pre-create it, and
the `Mkdir` result was discarded, so tty7 would have uploaded into a
directory someone else owned — readable by them, and swappable for
another image before the pane's agent opened the path. Screenshots are
exactly the payload that must not land there.

Images now stage in `$HOME/.cache/tty7/clipboard`, resolved from the
session's own `realpath .`, and the directory is verified before anything
is uploaded into it: a symlink is refused outright (`stat` would judge it
by its target), a `chmod 0700` the daemon watched succeed is the
ownership proof — POSIX only lets the owner change a mode — and a
following `stat` must report exactly `0700`. Any doubt is a hard failure
that falls back to pasting the local path rather than uploading. The
uploaded file is chmod'd `0600` once the transfer lands.

Only a verified directory is cached, so a preparation that failed is
retried on the next paste instead of latching a "ready" flag over a
directory that was never created.

All of it moves off the UI thread. Preparing the directory, starting the
transfer and polling it are blocking daemon+SSH round trips — the
workspace route gives up after 30s, the standalone-SSH route sets no read
timeout at all — and a keystroke handler must not make them. The pane
pastes from a background task instead, which is also what lets the upload
be watched to a terminal state: the SFTP panel's history only polls while
that panel is open and drops finished jobs after 30s, so a failed upload
used to leave a dangling remote path in the line and say nothing. Now it
notifies, once, naming the host and the reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 21:09:16 +08:00
618855cf4a fix(windows): brand toast notifications with a tty7 AUMID (#340)
* fix(windows): brand toast notifications with a tty7 AUMID (#339)

* fix(windows): only write the toast shortcut where it is ours to write

The AUMID shortcut was rewritten on every launch, which broke two cases
the review caught on a real machine.

An elevated install owns `%ProgramData%\...\tty7.lnk`, so writing a
per-user copy listed "tty7" twice in the Start Menu and left an orphan
pointing at a deleted exe once the uninstaller had removed only its own.
And `cargo run` repointed the installed shortcut at `target\debug`,
permanently, for anyone who both installs tty7 and builds it.

So decide before writing. An all-users shortcut settles the question by
itself — branded if the installer stamped our AUMID on it, otherwise we
stay on the PowerShell identity, because the alternative is littering a
Start Menu we cannot clean up. Otherwise we refresh the single per-user
`tty7.lnk` Inno's default install owns anyway, and only when it is not
already ours, and never from a cargo build directory. A dev build still
brands the process for taskbar grouping, and still gets branded toasts
when an install left a stamped shortcut behind — Windows asks that the
AUMID be registered, not that it point at the process using it.

Reading a shortcut back needs `IShellLinkW::GetPath`, hence the
`Win32_Storage_FileSystem` feature; `SLGP_RAWPATH` keeps it from chasing
a moved target over the network.

Also close the window this opened. The shell indexes a new `.lnk`
asynchronously and, for an AUMID it has not seen, `Toast::show()`
reports success and drops the toast — measured, it does not return an
error. A shortcut we wrote seconds ago is therefore not yet proof of
anything, so toasts keep the PowerShell identity for half a minute after
we write one: ugly beats invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 21:09:07 +08:00
cwatanab 4c9597c7b8 fix(terminal): draw half-circle powerline caps with segmented curves (#335) 2026-08-04 20:14:44 +08:00
47aa6532f5 fix(terminal): eliminate seams between Powerline separators (#336)
* fix(terminal): eliminate seams between Powerline separators

* fix(terminal): skip the separator cover quad when the glyph is dim

The cover quad and the anti-aliased path overlap on the closing edge's
device pixel. With an opaque foreground that is a no-op, but a DIM cell
carries fg.a = 0.66, so the two compositing passes push that one column
to 1 - 0.34^2 = 0.884 alpha and tint the neighboring cell's background.
Emit the quad only for opaque separators.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 20:14:38 +08:00
a1d89a3752 fix(i18n): correct Chinese terminology and wording (#334)
* fix(i18n): correct Chinese terminology and wording

The worst one collided two different concepts: a git worktree was
translated as 工作区, the same word tty7 uses for a workspace. "Remove
Worktree" therefore read as "delete this workspace" in a destructive
confirmation, and New Worktree Tab had three different names across the
menu bar, the palette, and its own dialog. A working tree is already 工作树
everywhere else, so worktree joins it and workspace keeps 工作区 to itself.

Also:

- Forget password means "clear the stored password", not "I forgot my
  password" — 忘记密码 reads as password recovery.
- The SSH auth mode Agent (ssh-agent) was 代理, the same word as the proxy
  fields right next to it.
- "The server holding your shells" became 保存 (stores), which is not what
  the server does with them.
- Focus follows mouse had subject and object swapped.
- "over a week ago" lost its "ago".
- The shell help said to clear "Program" while the field above it is
  labelled 程序; the sidebar-grouping help said "Scratch" while the header
  itself reads 草稿.
- "Off closes straight away" was ambiguous about what closes.
- Mark Tab as Unread lost the tab in the palette.
- An SSH profile is a saved host, not a file on disk: 配置文件 → 主机配置.
- Punctuation: 帐户 → 账户, a halfwidth comma in Ln/Col, and em dashes
  inside a sentence are now —— throughout instead of a spaced —.

* fix(i18n): close remaining zh terminology gaps

Review follow-up to the terminology pass on this branch.

- Finish the profile -> 主机配置 rename. The About blurb still said 配置文件,
  and the Hosts search keywords still only matched the old term, so searching
  settings for the words the UI now shows found nothing. Settings search is a
  plain substring match over the whole keyword blob, so 主机配置 joins
  配置文件 there and both still reach the section.
- Quote UI labels with the “” the file already uses for “显示更多选项”,
  rather than the 「」 that had been introduced in two strings.
- 一周多以前 -> 一周多前, matching its four siblings: 刚刚, 分钟前, 小时前,
  天前.
- 标记标签页为未读 -> 将标签页标记为未读.
- The remove-worktree dialog said 未提交的更改 while the changes panel, the
  diff overlay and the palette all call git changes 变更. Its confirm button
  (放弃更改并删除) is rendered from the same prompt, so the two moved together
  and the dialog stays internally consistent.

The New Worktree Tab labels are left alone: 新X in the menu bar and 新建X in
the palette is the split the file already makes for New Workspace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:59:05 +08:00
5d14603722 fix(windows): advertise terminal background to TUI apps (#332)
* fix(windows): advertise terminal background to TUI apps

* refactor(windows): keep the background hint out of config.json

The daemon needs to know whether the window is light or dark when it
spawns a Windows pane, because ConPTY drops the child's OSC 11 query
before tty7's emulator can answer it. It was reading that from
`Config::theme` — a field nothing had written since it went dead — which
meant the GUI had to rewrite the user's `config.json` every time the
effective preset changed sides.

Move the hint to `appearance.json`, beside `machine.json` in the data
dir, and leave `Config::theme` exactly as it was. It is derived state:
written by the process that paints the window, read by the process that
has to describe it, and of no interest to the user. A file of its own
rather than a field on `Machine`, because the machine tree is owned by
the daemon and flushed on a timer, so a second writer would clobber the
workspaces and panes it had not seen. Absent, unreadable, and unparsable
all read as light — what the default preset is — so a daemon that starts
before the GUI has ever applied a theme describes the default window
instead of guessing.

Also silence the `unused variable` warning the hint parameter raised on
every non-Windows build, where the `COLORFGBG` block it feeds is
compiled out.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:40:20 +08:00
vkingwandl0ng-ai 0f3d176e98 fix(input): let ctrl-e accept ghost suggestions (#329)
Accept a visible inline history suggestion while preserving end-of-line behavior when no suggestion is shown.

Refs l0ng-ai/tty7#315

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-04 19:21:32 +08:00
Hongwei Qin 761e8e75c1 fix(terminal): prevent Ctrl-U after agent interrupt (#312)
* chore: reserve issue 305 draft

* fix(terminal): ignore alt-screen-only typeahead

* fix(terminal): discard typeahead at alt-screen boundaries (#305)

* fix(terminal): drop alt-screen boundary input (#305)

* fix(terminal): discard agent typeahead on interrupt (#305)
2026-08-04 18:57:40 +08:00
l0ng-aiandl0ng-ai a89d9ea45b fix(cli): diagnose unavailable agent hooks (#321)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-04 07:23:20 +08:00
l0ng-aiandl0ng-ai 7c9a2d20b6 fix(cli): terminate panes when closing tabs (#319)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-04 07:06:31 +08:00
8a342f2ca9 feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans

* feat(ui): localize search placeholders and relative time

* feat(ui): localize palette, switcher, and sftp strings

* feat(ui): localize home shortcut labels

* feat(ui): localize tray, ssh prompt, and editor strings

* feat(ui): add plural/select i18n helpers and localize sftp/settings labels

* feat(ui): localize settings search, forwards panel, and file tree

* feat(ui): localize code editor and right panel

* feat(ui): localize stop/delete workspace confirmations with plural support

* feat(ui): localize diff overlay with plural-aware summary

* feat(ui): localize pending pane, worktree prompt, and home time strings

* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings

* feat(ui): localize switcher, file_tree, machine_mirror fallback strings

* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings

* feat(ui): localize command palette strings

* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors

* feat(ui): localize remaining theme, switcher, settings, and sftp strings

* style: cargo fmt

* feat(ui): add language selector to settings

* fix(ui): refresh locales across windows

* refactor(ui): make GUI language selection explicit

* fix(ui): localize Explorer settings after merge

* fix(ui): keep persisted theme names out of the GUI locale

A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.

Also in this pass:

- Give each test thread its own locale override. The locale is process-wide and
  tests run in parallel, so the two tests that switched to zh-CN could flip the
  language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
  in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
  `auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
  setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-03 23:48:29 +08:00
l0ng-aiandl0ng-ai e8525e1b33 fix(cli): submit send enter outside paste bursts (#322)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-03 16:55:48 +08:00
b7e08c7e11 feat(windows): add optional windows explorer context menus (#310)
* add CLI support for opening directories in new tabs

f

* feat(windows): add optional windows explorer context menus

f

* fix(gui): restore missing windows and reject lossy paths

* fix(windows): harden explorer menu registration and native path handling

* fix(cli): preserve native GUI paths on Windows

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-03 15:23:59 +08:00
ARNOandthomas c2ee9483a9 feat(shell): add detected and custom shells to the new terminal menu (#311)
* feat(windows): add Nushell support

* feat(shell): add custom shell to the terminal menu

* feat(shell): add cross-platform Nushell support

* fix(shell): preserve custom arguments across shell inventories

* fix(shell): preserve configured command identity

---------

Co-authored-by: thomas <thomas@gmail.com>
2026-08-03 15:11:55 +08:00
thomas f2c0c2dcdf Merge pull request #309 from ayamir/feat/macos-in-app-updater 2026-08-03 15:04:43 +08:00
l0ng-ai a8e9230eed Merge branch 'main' into feat/macos-in-app-updater 2026-08-03 14:46:21 +08:00
60a7edf434 fix(settings): make hover highlights respond immediately (#313)
* fix(settings): make hover highlights respond immediately

f

* fix(settings): keep hover row identity stable

---------

Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-03 14:46:15 +08:00
Adeline Carterandl0ng-ai e8b419b543 fix(input): forward macOS editing shortcuts to foreground TUIs (#304)
* fix(input): forward Cmd+Backspace to foreground TUIs

* fix(input): complete foreground TUI shortcut forwarding

---------

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-03 14:29:55 +08:00
thomas f86db1563e fix(updater): keep rollback backup in staging 2026-08-03 13:45:31 +08:00
ARNO 9b62ef3f16 feat: add CLI support for opening directories in new tabs (#308)
* add CLI support for opening directories in new tabs

f

* fix(gui): restore missing windows and reject lossy paths
2026-08-03 13:33:15 +08:00
ayamir a6754b28bc feat(update): install verified macOS releases in app 2026-08-02 22:25:10 +08:00
l0ng-ai da6df709cd Remove orchestration skill setting 2026-08-02 13:12:09 +08:00
l0ng-aiandl0ng-ai 0a3accd10f fix(completion): escape inserted candidates, close spent menus, keep PATH local (#276)
Three defects in the inline completion menu, all of which produced something
wrong rather than merely unhelpful.

A candidate was inserted into the command line verbatim. A directory named
`My Documents` completed to `cd My Documents/`, which the shell resplits into
two arguments and the command breaks. `shell_escape_path` already existed for
drag-and-drop paths; completion never reached for it. `escape_candidate` wraps
it and keeps a leading `~/` unescaped, since that prefix is the user's own text
and escaping it would stop the home expansion it was typed for.

The same escape decides whether a common-prefix step is safe to write. The
prefix shared by `My Documents` and `My Music` is `My ` — writing it raw both
breaks the line and puts a space inside the open word, which closes the menu on
the next keystroke and leaves the user worse off than before the Tab. A prefix
that needs escaping now steps through the candidates instead.

A menu fed only by generators stayed armed forever when nothing matched.
`git ckout<Tab>` matches no subcommand, but git's alias generator is in flight,
so the session opens empty and waits — and the callback that would have closed
it returned early on an empty result, so the menu never learned the generator
was done. An armed empty menu swallows every later Tab instead of handing the
line to the shell. Sessions now count their generators, and the last one to
answer closes a menu that still has nothing in it.

Command completion scanned this machine's PATH in a remote pane. The remote
isolation added in 08ca3a3 covered paths and generators but deliberately left
command completion running, which was right for the builtins half and wrong for
the PATH half: `system_prof<Tab>` over SSH to Linux offered macOS's
`system_profiler`. Worse, it failed inconsistently — with no local match the
position falls through to the remote's own compsys and answers correctly, so
the bug only appeared when this machine happened to have a match. Builtins are
true on any POSIX shell and still go out; the PATH scan is now local-only.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:13:19 +08:00
l0ng-aiandl0ng-ai fc430bd872 fix(shell): a forwarding .bash_profile must not source .bashrc twice (#279)
The rcfile tty7 hands to bash replays the login-shell startup chain, but
sourced ~/.bashrc unconditionally after it. A login shell never does that
on its own — ~/.bashrc arrives only because the profile that won the chain
forwarded to it, which is how nearly every ~/.bash_profile is written. The
result was the user's whole ~/.bashrc running twice per pane: banners
printed twice, completions were sourced twice, and appends to
PROMPT_COMMAND stacked up.

Move ~/.bashrc into the same first-match-wins chain. That fixes the double
source and still keeps the fallback for a $HOME with no profile at all.

The existing test only asserted the rcfile mentions ~/.bashrc, which the
buggy version satisfied too. Add one that runs real bash against a
throwaway $HOME whose .bash_profile forwards, and counts the sourcings.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:12:50 +08:00
l0ng-aiandl0ng-ai f3b029530a fix(core): read the login shell from passwd, not the stale $SHELL (#278)
$SHELL is a snapshot the session inherits at login, so chsh never moves
it -- a GUI launch keeps reporting the shell that was current when the
user logged in, and goes on doing so until they log out. The window's
shell menu marked the wrong entry "default" for that whole stretch.

Read the passwd entry instead, via getpwuid_r -- the reentrant form,
since getpwuid returns a pointer into a shared static another thread's
lookup can overwrite. $SHELL stays as the fallback for the rare case
where the lookup fails. The three callers that each reached for the
variable on their own -- the default-name lookup, the PATH enrichment
that runs the login shell at startup, and the shell-integration kind
probe -- now share the one function.

Same commit fixes who wins a name in the menu. Candidates were login
shell, then /etc/shells, then $PATH, and dedupe keeps the first -- so
on a machine with a Homebrew bash, /etc/shells listing /bin/bash first
handed the entry to macOS's 3.2 from 2007, old enough that
bash-completion 2.x will not load against it. Probe $PATH before
/etc/shells and widen the probe list to the POSIX shells, so the menu's
"bash" is the binary typing bash would reach; /etc/shells still catches
anything installed off $PATH.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:12:29 +08:00
l0ng-aiandl0ng-ai 48c13684a8 fix(input): restore editor after interrupting tab handoff (#290)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:12:14 +08:00
l0ng-aiandl0ng-ai 75dbbf8daf fix(terminal): render ANSI text decorations (#289)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:59 +08:00
l0ng-aiandl0ng-ai 1ba7930999 fix(terminal): render ANSI dim text (#288)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:39 +08:00
l0ng-aiandl0ng-ai cbe951efd2 fix(pane): the macOS locale fallback must set LANG, not LC_CTYPE (#280)
When a pane inherits no locale at all — the usual case for a GUI-launched
process on macOS — we derive an installed UTF-8 locale and inject it. But
we injected it as LC_CTYPE, which backs only character handling. Collation,
time and numbers stayed at C, and a shell that asks setlocale(cat, "") per
category finds no variable for the rest: bash warns

  setlocale: LC_COLLATE: cannot change locale ()

once per category on every launch. zsh and fish swallow the failure, so
they merely look fine while being just as half-configured.

LANG backs every category and still loses to any LC_* the user's own rc
files set afterwards, which is what a fallback should do. LC_ALL would also
cover everything but would override those.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:27 +08:00
l0ng-aiandl0ng-ai fe3bc17f8c fix(ui): soften overlay scrollbars (#293)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-02 12:11:01 +08:00
ARNOandARNO 7d86b86d35 fix(windows): prevent daemon from inheriting policy that blocks scoop junctions (#292)
Some Windows shell brokers enforce `ProcessRedirectionTrustPolicy` on what
they launch. The daemon inherited it, every ConPTY shell under the daemon
inherited it in turn, and PowerShell could then no longer traverse a
user-created junction — which is exactly what Scoop's `current` links are.
`oh-my-posh` and `fzf` died with `Shim: Could not determine if target is a
GUI app`. Windows Terminal was unaffected because its process tree never
picked the policy up.

The policy cannot be relaxed once enabled, so the fix is to not inherit it:
when tty7 detects the enforcing bit, it creates the daemon with
`STARTUPINFOEXW` and `PROC_THREAD_ATTRIBUTE_PARENT_PROCESS` naming the
interactive desktop shell, which supplies the ordinary desktop token, device
map, and mitigation policy. The Win32 code stays isolated in
`daemon/spawn/windows.rs`, and the ordinary path still runs whenever the
policy is absent — or whenever the desktop shell cannot be borrowed, in
which case tty7 logs a warning and starts degraded rather than not at all.

Because naming a logical parent makes handle inheritance follow that
process, the daemon starts with no standard handles. `daemon::server` and
the pane reader's trace line now write to stderr in a way that tolerates
that, instead of `eprintln!`, which panics on a failed write.

ConPTY exit ordering: the process-exit monitor could observe a short-lived
shell exiting before the reader had delivered its final frame, so `Exited`
reached clients ahead of the output that preceded it. The monitor now
releases the pseudoconsole and lets the reader — which reports only after
forwarding everything up to EOF — announce the death, with a bounded window
behind it for the case where EOF never arrives because a grandchild holds
the ConPTY output pipe open.

Note this changes the daemon's token on the clean-parent path: it derives
from Explorer, so an elevated tty7 starts a medium-integrity daemon.

Co-authored-by: ARNO <ArnoChenFx@users.noreply.github.com>
2026-08-02 10:59:47 +08:00
yetoneandl0ng-ai 71417782fb test(cli): close the raw/plain capture race in the e2e plain test (#299)
capture_plain_returns_text_not_escapes gated its byte-level asserts on
the marker reaching the rendered capture, then asserted the raw capture
already carried a CR. The two captures are separate snapshots taken in
sequence, and on Windows ConPTY re-emits the echoed command in
escape-laden bursts: the marker can render (from the typed input line)
while the slightly earlier raw snapshot has yet to see a single CR —
Enter's CRLF only arrives with the command's execution. CI hit exactly
that window on x86_64-pc-windows-msvc.

Make the CR part of the settle condition the loop polls for, alongside
the marker, and name both in the timeout message so a genuine
CR-stripping regression still reads as one.

Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
2026-08-02 10:08:04 +08:00