# Manual Test Plan

## Agent Notes

- For UI, platform, and device-driven changes, agents should add or update the relevant manual verification steps in this document.
- Prefer concrete reproduction steps and expected results over vague test descriptions.
- When debugging, add targeted log instructions if the test depends on developer-run device validation.

## 0. Mobile Hover Styling

1. Open the app on iOS or Android
2. Tap an outline button, drawer tab icon, the Session direct-address row, and the Session Disconnect button
3. Expected: each tap still triggers its action
4. Expected: no hover background remains stuck after tap, drag, or scroll interactions
5. Expected: active, selected, destructive, and disabled states remain readable without hover styling

## 0a. Home Install Guide Tabs

1. Open the app with no saved workspaces visible on the Home screen
2. Switch between the `curl`, `claude`, `codex`, `opencode`, and `gemini` guide tabs
3. Expected: each tab shows the same install commands as the landing page
4. Tap a command line in each tab
5. Expected: the tapped command line is copied to the system clipboard without navigating away from Home
6. Long-press and drag across guide text
7. Expected: native text selection handles appear, command and comment lines are selectable, and `Copy` copies the selected text
8. Expected: switching tabs or scrolling the guide does not leave stale selection handles on screen

## 0b-Status. Connection Status Indicator

1. From Home, tap the status dot on a saved workspace card without tapping the card body
2. Expected: light haptic, the app navigates to that workspace, and the connecting view opens
3. Tap the card body outside the dot
4. Expected: the workspace still opens, but the card tap path is unchanged from before
5. In a connected workspace, tap the status dot beside the header title
6. Expected: the connecting view opens and any open workspace drawer closes
7. Open the workspace drawer and tap the status dot in the drawer header
8. Expected: the connecting view opens and the drawer closes
9. Open Quick Actions and tap the status dot on a workspace row, not the row body or `+` button
10. Expected: Quick Actions closes, that workspace becomes active, and the connecting view opens
11. While a workspace is idle or reconnecting, confirm the status dot pulses opacity and scale without shifting header layout

## 0b. Home Settings Button

1. Run a Debug build and open the Home screen
2. Tap the top-right settings icon
3. Expected: a light haptic feedback fires and the Settings screen opens
4. Run a Release build and open the Home screen
5. Expected: the settings icon is not visible and the developer Settings screen is not reachable from Home

## 0c. Developer Native Notification (iOS)

1. Run a Debug build and open Settings
2. Tap `Native Notification` in the Developer section
3. Expected: two compact in-app notification bubbles slide down near the top safe area, with the newest expanded in front and the older one peeking above it as a smaller glass bubble
4. Expected: the front bubble uses the app asset icon `AgentCodex` tinted like the notification title; the upper peek bubble uses an SF Symbol fallback icon
5. Expected: each new bubble fades in faster than it scales, then continues sliding down and settling into place with a subtle spring
6. Tap the front `Agent completed` bubble
7. Expected: it fades out faster than it scales, then finishes scaling back toward zero while sliding upward; a callback notification appears
8. Tap `Native Notification` again, then swipe the front bubble downward
9. Expected: all pending notification items expand into full bubbles, with the oldest at the top and the newest at the bottom
10. Swipe any expanded bubble upward
11. Expected: only that bubble dismisses, and the remaining bubbles move up smoothly
12. Tap `Native Notification` repeatedly
13. Expected: multiple notifications collect into the same bubble stack and all auto-close after their configured durations by default

## 0c-Android. Native Presentations And Embedded Sheet

1. Run a Debug Android build and open Settings
2. Tap the developer alert and selection presentation actions
3. Expected: native Material dialogs appear; the selection dialog shows `First Action`, `Second Action`, `Destructive Action`, and `Cancel` rows with visible row separation, not just the title
4. Expected: button callbacks fire once, and dismissing the selection reports a dismiss rather than choosing the last item
5. Tap `Native Notification`
6. Expected: native notification banners appear near the top safe area, auto-close by default, and tapping the action banner triggers the callback notification
7. Trigger the text input dialog from an existing call site
8. Expected: the native text field shows the initial value, `OK` returns the entered value, and `Cancel`/outside dismissal returns no value
9. Open a terminal file link so the native custom sheet opens
10. Expected: a Material bottom sheet appears with a grabber when requested and GPUI-rendered preview content inside the embedded sheet surface
11. From a fresh sheet open, swipe the preview content upward and downward before it reaches top, then drag downward from the top of the preview
12. Expected: inner content scrolls while not at top; when it is at top, the bottom sheet can take the downward drag for detent/dismiss handoff without preview repaint glitches during the drag or dismissal
13. Trigger the scroll-to-bottom floating button
14. Expected: the native floating button appears at the GPUI wrapper bounds and pressing it runs the Rust callback
15. Trigger dictation preview events if the call site is available
16. Expected: Android displays the preview overlay and dismiss callback, without attempting iOS-specific dictation stream interpretation

## 0c-Android-Renderer. GPUI Surface Lifecycle

1. Run a Debug Android build on a physical device
2. Connect via QR and open a terminal
3. Expected: after returning from the scanner, logcat shows `ZedraApp: window activated`, the pending ticket is processed, and connect succeeds
4. Type, scroll, and fling terminal output for at least 30 seconds
5. Expected: scrolling remains smooth and fling momentum does not continue after a drawer drag calls `prevent_default()`
5a. Slowly drag-scroll the terminal through scrollback a fraction of a row at a time
5b. Expected: the row entering at the top edge (and leaving at the bottom edge) slides in pixel by pixel; rows do not pop in only once fully visible
6. Open and close the workspace drawer while nested terminal/editor content is scrollable
7. Expected: drawer drags do not scroll the inner content, and inner vertical scroll does not move the drawer once the drawer gesture is not claimed
8. Background the app, wait 5 seconds, then foreground it
9. Expected: the existing GPUI window resumes without recreating all renderer state, and no surface validation or device-lost crash appears in logcat
10. Rotate the device to landscape left and landscape right
11. Expected: the app remains in portrait orientation and the existing workspace/session state stays visible instead of returning to the initial launch view
12. Expected: the app redraws at full physical surface resolution with `scale_factor = density`, with no fixed 0.75 render scale
13. On an Android 15 device, confirm content is not obscured by the status bar, gesture navigation handle, 3-button navigation bar, or display cutout
14. Confirm outlined buttons, cards, and input borders are visible even when their background is transparent
15. In the terminal, render `✔ ✘ ⚠ ⏺ ⏹ ⏸` and a real emoji such as `😀`
16. Expected: terminal/UI symbols render as monochrome symbol glyphs, while the real emoji renders through Android color emoji fallback before and after attempting rotation

## 0c-Android-AppIds. Debug And Release Application IDs

1. Run `./scripts/run-android.sh --target arm64-v8a`
2. Expected: Android installs and launches the debug app id `dev.zedra.app.debug` with the launcher label `Zedra Dev`
3. Expected: the launcher, app info, and recents icons show the black Zedra lightning icon instead of the default Android robot, including on round-icon launchers
4. Expected: startup logcat has no `getAppVersion` / `getAppBuildNumber` JavaException and no GPUI atlas panic during the first surface draw
5. Confirm Android release signing properties are present in global Gradle config: `ZEDRA_KEYSTORE`, `ZEDRA_KEYSTORE_ALIAS`, and `ZEDRA_KEYSTORE_PASSWORD`
6. Run `./scripts/run-android.sh --release --target arm64-v8a`
7. Expected: Android installs and launches the release app id `dev.zedra.app` with the normal app label, and it can coexist with the debug build

## 0d. Firebase GPUI Screen Views

1. Run an iOS build with Firebase Analytics enabled, or add `android/google-services.json` with the `dev.zedra.app` client and run `./scripts/run-android.sh --release --target arm64-v8a`
2. Open Home, Settings, Quick Actions, then connect to a workspace
3. Open the workspace drawer and switch through Files, Documents, Git Diff, Terminals, and Session
4. Open a non-markdown file, a markdown file, a git diff, a terminal, and the managed-agent view as the main workspace view
5. Tap terminal file links for both a source file and a markdown file so the native custom sheet opens
6. Expected: manual `screen_view` events include `screen_name` and `screen_class` for `Home`, `Settings`, `Quick Actions`, `Workspace Connecting`, `Workspace Editor`, `Workspace Markdown`, `Workspace Git Diff`, `Workspace Terminal`, each drawer tab, `Custom Sheet Editor`, and `Custom Sheet Markdown`
7. Expected: native automatic rows such as `UIViewController`, `CustomSheetViewController`, `UIAlertController`, and `ZedraQRScannerVC` on iOS or Android activity rows on Android are still present because native Firebase screen reporting remains enabled

## 0e. Developer Native Selection

1. Run a Debug iOS build and open Settings
2. Tap `Native Selection` in the Developer section
3. Expected: a native action sheet opens without crashing and shows `First Action`, `Second Action`, `Destructive Action`, and `Cancel`
4. Tap `Cancel`
5. Expected: the action sheet dismisses without running another action
6. Open `Native Selection` again, then tap outside the sheet
7. Expected: the sheet dismisses without crashing

## 1. Normal QR Scan → Connect

1. Start host daemon: `zedra start --workdir .`
2. Open app on device
3. Tap "Scan QR" — scan the terminal QR code
4. Expected: app connects, session panel shows "Connected", endpoint shown
5. Open the workspace drawer immediately after connect
6. Switch to the Session tab and confirm the `Connection` section has a right chevron indicator and wraps long status text, then tap it
7. Expected: the drawer closes and the connecting view opens for the active session with a top-right close button
8. Tap the top-right close button
9. Expected: the connecting view closes and the previous workspace content is visible
10. Expected: closing the connecting view does not fire haptic feedback
11. Reopen the connecting view, then open a file, git diff, or terminal from the workspace drawer
12. Expected: the connecting view closes and the selected workspace content is visible
13. Expected: file explorer root entries and git status are already loaded without waiting for the first drawer open to trigger them
14. Navigate to terminal — verify PTY works (shell prompt, keystrokes echo)

## 1a-Android. System Back Navigation

1. On Android, connect to a workspace and open Quick Actions from the workspace header
2. Press the system Back button or gesture
3. Expected: Quick Actions closes and the app remains on the workspace
4. Open Settings from Home, then press system Back
5. Expected: Settings returns to Home
6. Connect to a workspace, open the workspace drawer, then press system Back
7. Expected: the workspace drawer closes
8. Open the connecting overlay from the Session tab, then press system Back
9. Expected: the connecting overlay closes and the workspace content remains visible
10. Open a terminal, then a file, then a git diff, then reopen the same terminal
11. Press system Back repeatedly
12. Expected: Back visits the previous distinct main content views in order, without duplicate entries for the reopened terminal

## 1a. Host Info Subscription

1. Start host daemon: `zedra start --workdir .`
2. Connect from the app and open the workspace drawer
3. Switch to the Session tab
4. Expected within 5 seconds: CPU, RAM, uptime, and battery rows appear when the host exposes battery data
5. Leave the Session tab open for at least 15 seconds while running a CPU or memory load on the host
6. Expected: CPU/RAM values update roughly every 5 seconds without reconnecting or refreshing the drawer
7. Disconnect the app
8. Expected: host logs show no repeated host-info send errors after the stream closes

## 1b. Large File Explorer Responsiveness

1. Start host daemon in a large repository: `zedra start --workdir /path/to/large/repo`
2. Connect from the app and open the workspace drawer
3. In the File Explorer tab, expand several directories and use "Load more" until the visible tree contains hundreds of rows
4. Scroll the file explorer and repeatedly expand/collapse directories with already-loaded children
5. Expected: scrolling and toggles stay responsive, without long UI stalls or accidental file opens from loading rows
6. Tap a file row nested at least four levels deep
7. Expected: the drawer starts closing immediately without stuttering while the file loads, and file explorer rows use the same height as Git panel file rows
8. Reopen the drawer while the file remains the main workspace view
9. Expected: only the file row for the active main workspace view is highlighted, and the highlight spans the full file explorer width
10. Open a git diff or terminal as the main workspace view, then reopen the file explorer
11. Expected: the file row highlight clears because the active main workspace view is no longer that file
12. Expected: before syntax highlighting appears, code text uses a subtly dim foreground; when highlighting applies, text rows do not jump, reorder, or visibly reload

## 1c. Docs Tree Display Mode

1. Start host daemon in a repository with markdown files under both root and nested paths, including a `.git` directory
2. Connect from the app and open the workspace drawer
3. In the File Explorer tab, tap the top-right docs-tree display mode toggle
4. Expected: the docs tree builds from the host and shows markdown files with compact nested paths like `vendor/zed/docs/`
5. Collapse a nested docs directory, leave and reopen the workspace, then return to docs-tree mode
6. Expected: the same directory remains collapsed until manually expanded
7. Tap a markdown row
8. Expected: the drawer closes and the main workspace renders the selected markdown file
9. Reopen the drawer and return to docs-tree mode
10. Expected: only the active markdown file row is highlighted
11. Open a git diff or terminal as the main workspace view, then return to docs-tree mode
12. Expected: the docs-tree file highlight clears because the active main workspace view is no longer that markdown file
13. If `Load more docs` appears, tap it
14. Expected: another page is merged into the same tree without duplicating existing markdown rows
15. Scroll and collapse directories in a large docs tree
16. Expected: scrolling and toggles stay responsive without rendering the full tree at once
17. Tap the refresh icon in the docs tree footer
18. Expected: a native alert says Zedra will scan Markdown files and large workspaces may slow briefly
19. Tap `Refresh`
20. Expected: the refresh icon rotates while the current tree stays visible, then the tree is replaced after the refresh finishes
21. Expected: files inside dot-prefixed, gitignored, and common generated/dependency directories are not shown
22. Connect to an older host that does not support docs-tree RPCs
23. Expected: the docs tree shows an unsupported-host message and the refresh icon no longer stays in the building state

## 1d. Windows Host CLI

1. On an x86_64 Windows machine, run `powershell -c "irm https://zedra.dev/install.ps1 | iex"` from Command Prompt or Windows Terminal
2. Expected: `zedra.exe` is installed under `%LOCALAPPDATA%\Programs\zedra\bin`, the directory is added to the user `Path`, and `zedra --help` works from the current shell
3. Start the daemon from PowerShell: `zedra start --workdir C:\path\to\repo --detach`
4. Expected: startup succeeds, Windows may show a firewall prompt, and `daemon.lock` plus `daemon.log` are written under `%APPDATA%\zedra\workspaces\` using their respective workspace hashes
5. Run `zedra status --workdir C:\path\to\repo`
6. Expected: status shows the running daemon, endpoint, workspace path, sessions, and terminal count without Unix path assumptions
7. Run `zedra qr --workdir C:\path\to\repo`, scan from the mobile app, then disconnect and reconnect the app without scanning again
8. Expected: QR pairing succeeds, reconnect uses the saved session identity, and relay fallback still works if direct P2P is unavailable
9. Open a terminal from the app
10. Expected: a Windows PTY opens with the shell that launched the host, keystrokes echo, resize works, and commands available on `PATH` run normally
11. Stop the daemon, set `$env:ZEDRA_SHELL = "cmd.exe"` in PowerShell, restart the daemon, and open another terminal from the app
12. Expected: the terminal opens in `cmd.exe`; clear `ZEDRA_SHELL`, restart from PowerShell, and launch commands still leave an interactive PowerShell after they run
13. Run `zedra client --workdir C:\path\to\repo --count 3`
14. Expected: the CLI client authenticates without QR and prints three RTT samples
15. Run `zedra logs --workdir C:\path\to\repo`
16. Expected: recent daemon log lines are printed from the AppData workspace directory
17. Run `zedra update --version <current-release-tag> --yes` while the daemon is still running
18. Expected: the update succeeds and warns that running daemons keep using the old version until restarted
19. Run `zedra stop --workdir C:\path\to\repo`
20. Expected: the daemon exits, the lock file is removed, and a follow-up `status` reports no running daemon
21. Run `zedra update --version <current-release-tag> --yes`
22. Expected: the update downloads the Windows release asset, verifies the checksum when available, and reports that `zedra.exe` will be replaced after the command exits
23. Open a new PowerShell window and run `zedra --version`
24. Expected: the command prints the release version

## 2. QR Already Consumed

1. Start host: `zedra start --workdir .`
2. Device A scans QR → connects successfully
3. Device B scans the **same** QR
4. Expected: Device B sees "The QR code was used. Refresh it and scan again." (not a crash)
5. To pair Device B: refresh the QR code and scan again

### Static QR

1. Start host: `zedra start --workdir .`
2. Generate a static QR: `zedra qr --workdir . --static`
3. Scan it from Device A and confirm the workspace connects
4. Disconnect Device A from the host session
5. Scan the same static QR from a second clean device
6. Expected: Device B connects without "used" or "expired" QR errors
7. Stop and restart the daemon
8. Expected: the old static QR no longer works; the app says the QR expired or was replaced
9. Restart the daemon and generate a fresh static QR if needed

## 2b. QR Rescan Restarts Existing Entry

Covers the rescan path through `Workspaces::connect_ticket` →
`Workspace::restart_with_ticket`. Every scenario must end with the same
entry index (no duplicate) and the entry transitioning into `BindingEndpoint`
or beyond.

1. Start host: `zedra start --workdir .`
2. Scan QR, let StaleTimestamp or any auth failure land the entry in `Failed`
3. Refresh the QR on the host, rescan from the same device
4. Expected: existing entry restarts and reaches `Connected`; no new entry is created
5. Repeat with a deliberately stuck Authenticating phase (e.g. pause network for
   ~10s mid-auth, then rescan a fresh QR before the entry times out)
6. Expected: in-flight attempt is aborted and the fresh ticket drives a new
   Register/Authenticate round on the same entry
7. While the entry is `Connected`, rescan a still-valid QR for the same endpoint
8. Expected: just switches to the entry; no restart, no flicker of the connecting view
9. Rescan twice in rapid succession (two fresh QRs back-to-back)
10. Expected: only the last attempt is alive; no overlapping connect loops
    (`grep` logs for `start connect to` — two entries are fine, but the first
    one must be followed by abort/cancel before the second's `Connected`)

## 2a. Protocol Version Mismatch

1. Run an app build and CLI/host build that use different `ZEDRA_ALPN` versions
2. Scan the host QR from the app
3. Expected: connect view shows "Protocol mismatch, Update App or CLI"

## 3. Continue Session from Saved Workspace

1. Connect via QR (test 1 above), create at least three terminals, and note their order in the drawer Terminals tab
2. Force-close the app
3. On another client, immediately connect to the same host session using a saved workspace or a fresh host QR
4. Expected: the second client connects without several `Host occupied` / retry attempts
5. Disconnect the second client, then reopen the original app and tap the saved workspace entry in the home screen
6. Expected: reconnects using stored session ID (no QR needed); terminal
   backlog replays any missed output
7. Expected: the workspace drawer Terminals tab shows the active remote
   terminals from the host without creating a replacement terminal
8. Expected: terminal cards appear in the same order they had before force-close

## 3a. Remove Saved Workspace From Home

1. Connect via QR so a workspace card appears on Home
2. Return to Home and long-press the workspace card
3. Tap `Delete` in the native confirmation alert
4. Expected: the workspace card disappears from Home immediately
5. Force-close and relaunch the app
6. Expected: the deleted workspace card does not reappear

## 3b. Terminal Reattach Resize

1. Connect on Device A, open a terminal, and run:
   ```sh
   sh -c 'show(){ set -- $(stty size); printf "WINCH %sx%s\n" "$2" "$1"; }; trap show WINCH; show; while sleep 1; do :; done'
   ```
2. Disconnect or force-close Device A while the process keeps running
3. Reattach from Device B with a different screen ratio, or relaunch after changing simulator/device orientation
4. Expected: the terminal reattaches and prints a new `WINCH <cols>x<rows>` matching the current device viewport
5. Repeat with a non-alt AI CLI session such as `claude`, `codex`, or a `/zedra-start` resumed session
6. Expected: resumed output uses the current device width without stale wrapping or dumped resize artifacts

## 3c. Terminal Smooth Scroll Edge Rendering

1. Connect via QR and open a terminal with enough output to fill the scrollback
2. Slowly drag-scroll the terminal through scrollback a fraction of a row at a time
3. Expected: the row entering at the top edge (and the row leaving at the bottom edge) slides in pixel by pixel
4. Expected: rows do not pop in only once fully visible, and the edge gap never shows a blank partial row

## 4. Reconnect After Host Restart

1. Connect via QR, note session ID
2. Kill the host daemon (Ctrl-C or `zedra stop`)
3. Wait 5 seconds, restart host: `zedra start --workdir .`
4. Expected: app auto-reconnects (Reconnecting badge → Connected); session
   panel shows same or new session ID depending on `sessions.json` state
5. Expected: after reconnect, file explorer root entries and git status refresh asynchronously without blocking the terminal from becoming usable
6. Expected: if the restarted host syncs zero terminals, the app creates and opens a fresh terminal instead of leaving the main view on `Loading ...`

## 5. Host Unreachable → Retry

1. Connect via QR and create at least three terminals
2. In one terminal, run:
   ```sh
   sh -c 'i=0; while true; do echo "tick:$i"; i=$((i+1)); sleep 1; done'
   ```
3. In another terminal, run `cat`
4. Take the host machine offline (disable network interface)
5. Expected: badge shows `Reconnecting (N) · Ns` with a per-second countdown, up to 3 attempts
6. After 3 attempts: connect view shows "Host unreachable. Check network and host."
7. Bring host network back up, tap "Retry"
8. Expected: reconnects successfully
9. Expected: the ticking terminal continues receiving new `tick:<n>` output after reconnect, not only replayed backlog
10. In the `cat` terminal, type `after-reconnect` and press Enter
11. Expected: `after-reconnect` echoes once, proving the reattached input stream still reaches the PTY
12. Expected: the workspace drawer Terminals tab preserves the pre-reconnect terminal order
13. If the host was restarted and no remote terminals remain, expected: a fresh terminal is created and opened
14. Expected: host logs do not report reconnect teardown as `ERROR zedra_host::rpc_daemon: TermAttach: input receiver error: Io error`

## 5a. Idle Before Reconnect

1. Connect via QR and wait for the session badge to show "Connected"
2. Disable the host network interface or disconnect the client from the network without closing the app
3. Expected within about 4 seconds: session badge changes to "Idle Ns" while the session is still present; workspace status dots turn yellow and blink
4. Keep waiting
5. Expected later: normal reconnect flow still takes over (`Idle` -> `Reconnecting` -> `Disconnected` or `Connected`, depending on recovery)
6. Restore network before reconnect exhausts
7. Expected: badge returns from `Idle` to `Connected` before or during reconnect recovery

## 5b. Path Changes Do Not False Idle

1. Connect and wait for the transport badge to settle on `Relay` or `P2P`
2. Trigger a path change without fully losing connectivity
3. Example: force a relay-to-direct upgrade, or briefly switch networks and recover before reconnect starts
4. Expected: the badge may change transport type or RTT, but it should not enter `Idle` unless connection-wide inbound traffic stalls for about 4 seconds
5. Expected: path handoff can update the displayed path metadata without changing the liveness rule

## 5c. Host macOS Sleep/Wake Direct Path Recovery

1. Start the host daemon on macOS with tracing enabled and connect from a device
2. Wait for the transport badge to show `P2P`, or confirm the daemon/client logs show a selected direct path
3. Sleep the host Mac long enough for the client connection to go idle or reconnect
4. Wake the host without restarting `zedra start`
5. Expected in host logs: `net_monitor: starting endpoint recovery` followed by fresh iroh network-change/report activity
6. Expected: the client may reconnect over relay first, then upgrades back to `P2P` without restarting the host daemon

## 6. Session Occupied (Two Devices)

1. Pair Device A via QR → connected to session S
2. Start a new `zedra start` for the same workdir on the host (same session)
3. Pair Device B via the new QR → should attach to session S
4. Expected while Device A is still live: Device B is blocked with "Host occupied. Disconnect other device and retry."
5. Manually disconnect Device A, then immediately retry Device B
6. Expected: Device B attaches on the first retry
7. Reconnect Device A, then background it without quitting
8. Immediately retry Device B
9. Expected: Device B is still blocked because backgrounding alone keeps Device A's session active
10. Force-quit Device A from the app switcher, then immediately retry Device B
11. Expected: Device B attaches without several "Host occupied" retry attempts
12. Reconnect Device A again, then network-disconnect Device A without a graceful client close
13. Retry Device B after the active-client stale window
14. Expected: the host evicts the stale Device A connection without waiting for the full transport timeout

## 7. `zedra client` RTT Test

```bash
# Terminal 1
zedra start --workdir .

# Terminal 2 (same machine, same workdir)
zedra client --workdir . --count 5
```

Expected output: 5 ping rows with RLY/P2P label and RTT in ms, then statistics.

## 8. `--relay-url` Override

```bash
zedra start --workdir . --relay-url https://sg1.relay.zedra.dev
```

Expected: host connects to the specified relay; QR shows that relay URL in
`relay` field of JSON output (`--json` flag).

## 9. Terminal Hyperlink Detection

1. Connect to a session and open the terminal view
2. Run:

```bash
printf 'src/main.rs:12:3\ngit:(refactor-app-session-architecture)\nhello\nREADME\nv0.112.0\ngpt-5.4\n/model\n'
printf '\033]8;;file://%s/src/main.rs:12:3\033\\src/main.rs:12:3\033]8;;\033\\\n' "$PWD"
printf '\033]8;;https://zedra.dev\033\\zedra.dev\033]8;;\033\\\n'
```

3. Expected before tapping: only the OSC 8 `src/main.rs:12:3` and `zedra.dev` rows show a subtle underline; the plain `src/main.rs:12:3`, `git:(refactor-app-session-architecture)`, `hello`, `README`, `v0.112.0`, `gpt-5.4`, and `/model` rows do not
4. Tap the underlined OSC 8 `src/main.rs:12:3`
5. Expected: the terminal file preview opens for `src/main.rs` at line 12/column metadata and does not reuse any previous preview scroll position
6. Expected: the preview header metadata and code body both render with the app monospace font rather than a proportional fallback
7. Tap the underlined OSC 8 `zedra.dev`
8. Expected: the URL opens externally
9. Tap the plain `src/main.rs:12:3`, `git:(refactor-app-session-architecture)`, `hello`, `README`, `v0.112.0`, `gpt-5.4`, and `/model`
10. Expected: none of those tokens are treated as hyperlinks and no preview sheet opens

## 9b. Terminal Preview Sheet Gesture Ownership

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Run:

```bash
{
  printf 'fn main() {\n'
  for i in $(seq 1 80); do
    if [ "$i" = 40 ]; then
      printf '    let message = "this line is intentionally very long so the terminal preview code editor needs horizontal scrolling inside the native custom sheet without moving the sheet detent or dismissing the sheet while the drag is horizontal";\n'
    else
      printf '    println!("line %02d");\n' "$i"
    fi
  done
  printf '}\n'
} > /tmp/zedra-long-code.rs
printf '\033]8;;file:///tmp/zedra-long-code.rs:41:1\033\\/tmp/zedra-long-code.rs:41\033]8;;\033\\\n'
```

3. Tap `/tmp/zedra-long-code.rs:41`
4. Expected: the preview opens in code editor mode inside the native custom sheet with line 41 at the top of the code body
5. Expected: Rust keywords and string tokens gain syntax colors after the preview finishes parsing
6. Swipe horizontally across the long string line
7. Expected: the code scrolls sideways and the native sheet does not move or dismiss
8. Swipe mostly vertically inside the preview body
9. Expected: the preview content scrolls vertically
10. Dismiss the sheet, tap `/tmp/zedra-long-code.rs:41` again, then drag downward before line 1 is visible
11. Expected: the code scrolls toward line 1 first; the opened line is not treated as the file top
12. Scroll to the top of the preview body, then drag downward
13. Expected: the native sheet moves or dismisses normally from the top edge

## 10. Connecting Overlay Layout On Wide Screens

1. Open the app on a wide device or simulator width such as iPad or landscape iPhone
2. Start a new connection or reopen a saved workspace so the connecting overlay is visible
3. Expected: the connecting content stays horizontally centered with visible left and right padding
4. Expected: a restart connection icon appears immediately next to the title
5. Tap the restart connection icon
6. Expected: the icon rotates once, light haptic feedback fires, the current connection attempt restarts, and the overlay remains visible
7. Rotate or resize while the overlay is visible
8. Expected: the title, restart icon, badge, and details remain in a bounded centered column instead of stretching edge to edge
9. Tap `View Details`, then `Hide Details`
10. Expected: the subtitle stays horizontally centered and does not jump when details expand or collapse

## 11. Terminal Keyboard And Native Selection On iOS

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Tap a non-hyperlink area of the terminal once
3. Expected: the terminal becomes focused, the software keyboard appears, and terminal input works
4. Expected: the visible terminal surface does not show a native full-height UIKit caret
5. Hide the keyboard so the terminal is unfocused, then double-tap visible terminal output
6. Expected: double tap behaves like normal tap input: the terminal focuses and requests the keyboard, and no terminal output selection starts
7. Tap a non-hyperlink area of the terminal once
8. Expected: because the terminal is focused and the keyboard is visible, the keyboard hides and terminal focus clears
9. Tap the terminal again, then long-press visible terminal output while the keyboard is visible
10. Expected: terminal enters terminal-owned output-selection mode without dismissing the keyboard or showing an editable caret
11. Drag the native selection handles to extend and shrink the output selection
12. Expected: selection remains active, terminal output is not replaceable, and keyboard/IME state is unchanged
13. Long-press hard-wrapped output, soft-wrapped output, emoji, and CJK text, then tap `Copy`
14. Expected: copied text preserves visible hard newlines, omits soft-wrap newlines, preserves non-ASCII text, and trims trailing blank cells
15. Tap visible terminal output outside the active selection once
16. Expected: terminal output selection clears, and that same dismiss tap does not toggle terminal focus or keyboard visibility
17. Long-press an empty terminal cell
18. Expected: a custom native edit menu appears slightly above the touch point with a `Paste` action even though no output text is under the finger
19. Tap `Paste`
20. Expected: if the clipboard has text, it is sent to the PTY through terminal paste handling; if the clipboard is empty, the menu dismisses without changing terminal focus or keyboard state
21. Tap a non-hyperlink area of the already-focused terminal again
22. Expected: the keyboard hides and terminal focus clears
23. Dismiss the keyboard through a platform control or hardware-keyboard state while the terminal remains focused
24. Expected: tapping terminal text again keeps focus and requests the software keyboard
25. With the keyboard visible, drag vertically in terminal content to scroll scrollback or a terminal app that handles touch scroll
26. Expected: the terminal scrolls without dismissing the keyboard or clearing terminal focus
27. In a fresh non-alt terminal with the keyboard visible, run a slow stream such as `for i in $(seq 1 20); do echo "line $i"; sleep .2; done`
28. Expected: early output continues from the top without being pushed upward into an empty lower gap; once the occupied rows reach the keyboard edge, the terminal lifts gradually and never more than the keyboard height
29. With retained scrollback, clear the terminal using `printf '\033[2J\033[Htop\n'`
30. Expected: the cleared content stays top-aligned instead of inheriting a full keyboard lift from old scrollback; TUI-authored blank layout rows still count as occupied space
31. In a fresh non-alt terminal with no scrollback, drag upward repeatedly past the prompt
32. Expected: the prompt does not drift downward into unbounded empty space, and the scroll-to-bottom button does not appear
33. With the keyboard still visible and enough scrollback to reach history top, drag upward until scrolling stops, then keep dragging slightly
34. Expected: the oldest scrollback rows can be revealed and are not clipped above the terminal viewport

## 11-Android. Terminal Keyboard And IME

1. Connect to a session on an Android device and open the terminal view
2. Tap a non-hyperlink area of the terminal once
3. Expected: the terminal becomes focused, the software keyboard appears, and terminal input works
4. Type plain text, press backspace, press enter, and type another command
5. Expected: committed text, delete, and enter reach the PTY exactly once without opening the dictation preview
6. Use an IME that composes text, such as Vietnamese Telex or Japanese, type a short composition, and accept it
7. Expected: composing text updates without duplicating committed characters, and the accepted text reaches the PTY once
8. With the keyboard visible, tap `Esc`, `Tab`, `Enter`, and each arrow in the accessory bar
9. Expected: each accessory key reaches the PTY exactly once
10. Press and hold each accessory arrow, then release it
11. Expected: the corresponding arrow input repeats continuously while held and stops immediately on release
12. Dismiss the keyboard or background the app while holding an accessory arrow
13. Expected: repeat stops and does not resume when the keyboard or app returns
14. Tap the already-focused terminal while the keyboard is visible
15. Expected: the software keyboard dismisses and the next terminal tap reopens it

## 11a. Terminal Scroll To Bottom Native Button On iOS

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Print enough output to create scrollback, for example `seq 1 200`
3. Drag upward in the terminal until the view is several lines away from the bottom
4. Expected: a small native circular arrow-down button materializes at the lower-right of the terminal
5. Expected on iOS 26 or newer: the button uses UIKit glass; on older iOS it falls back to a native dark material
6. Tap the arrow-down button
7. Expected: the terminal scrolls to the latest output immediately, the native press feedback is visible briefly, then the button dematerializes
8. Show the software keyboard, scroll away from the bottom again, and repeat
9. Expected: the button stays above the keyboard/home indicator and still scrolls to the bottom
10. Flick terminal scrollback so it continues moving with momentum, then tap the arrow-down button while momentum is still active
11. Expected: the terminal stays pinned to the latest output instead of drifting back upward from the remaining momentum
12. While the button is visible, switch to another terminal with enough scrollback and drag away from the bottom
13. Expected: the previous terminal's button is gone, and the newly active terminal shows its own scroll-to-bottom button

## 11b. Terminal Native Dictation On iOS

1. Connect to a session on a physical iPhone and open the terminal view
2. Tap the terminal so the software keyboard appears
3. Tap the keyboard dictation microphone and dictate a short command fragment such as `echo hello`
4. Expected: recognized text appears in the native dictation preview while the terminal output stays stable during live hypothesis rewrites
5. Stop dictation
6. Expected: the dictated text commits to the PTY once when dictation finalizes, without being removed, without a stuck marked-text underline, without duplicate characters, and without a `UIDictationController` hypothesis-cancel log
7. Repeat with a longer phrase and stop immediately after the last words
8. Expected: the final committed PTY text includes the last words, with no `UIDictationController` hypothesis-cancel log
9. While preview text is visible, tap the preview bubble
10. Expected: the preview dismisses immediately and does not stay stuck above the keyboard
11. Repeat with a dictated phrase that includes a newline or return command
12. Expected: newline input is routed as terminal enter rather than leaving literal marked text behind
13. Start dictation again, then cancel or force recognition failure by stopping before speech is recognized
14. Expected: the terminal remains focused, no stale dictation text is committed, and normal keyboard typing still reaches the PTY

## 11c. Native Keyboard Suggestions On iOS

1. On iPhone or iOS simulator, disable the simulator hardware keyboard so the software keyboard is visible
2. Connect to a session and tap the terminal
3. Type a partial word such as `hell`
4. Expected: iOS native inline predictions or suggestion candidates appear when supported by the OS and keyboard settings
5. Accept a suggestion
6. Expected: the accepted text is inserted into the PTY once, without enabling a native caret, edit menu, or terminal text-selection handles
7. Resume or reconnect to an existing terminal with text already at the prompt, then press and long-press software-keyboard backspace
8. Expected: each repeated backspace is routed to the PTY and can continuously delete existing prompt text rather than stopping after the synthetic prediction context is empty
9. Dictate a short fragment, stop dictation so it commits, then press backspace
10. Expected: the dictated characters can be deleted from the PTY one character at a time
11. Type command-like text with lowercase letters, straight quotes, hyphens, or double spaces
12. Expected: autocapitalization, smart quotes, smart dashes, and smart insert/delete do not rewrite the command text
13. Open the workspace Git sidebar and focus the Commit message input
14. Type prose and accept an available native suggestion
15. Expected: the suggestion inserts into the commit message, while smart punctuation and autocapitalization remain disabled
16. Switch the software keyboard to Vietnamese Telex and type `lee`, `chaf`, `toois`, `vois`, `ddungs`, and `uw ` in the terminal
17. Expected: the PTY receives `lê`, `chà`, `tối`, `với`, `đúng`, and `ư `, without duplicate base consonants such as `llê` or `chhà`, without placing the tone on the final vowel as `tôí`, without duplicating a replayed composed cluster as `vơới`, without dropping preserved prefixes such as `đ`, without dropping the standalone composed character before the space, and without showing the dictation preview while typing or backspacing
18. Switch to a Japanese keyboard, type a short marked composition, and accept a candidate
19. Expected: the marked text commits once to the PTY, the candidate UI anchors near the terminal input area, and there is no repeated `variant selector cell index number could not be found` warning

## 11d. iOS Native Text Input Regression Matrix

1. Terminal, normal IME marked text: switch to Japanese, type a multi-character composition, move between candidates, then accept one candidate
2. Expected: preedit text is visible only as native marked text, the accepted candidate is inserted once, and cancelling composition restores the previously committed terminal input
3. Terminal, Vietnamese Telex: type `lee`, `chaf`, `toois`, `vois`, `ddungs`, and `uw ` without pausing between keys
4. Expected: output is `lê`, `chà`, `tối`, `với`, `đúng`, and `ư ` immediately when the keyboard commits each rewrite, with no duplicate replayed consonants, dropped prefixes, or composed clusters
5. Terminal, native suggestion: type `teh`, accept the keyboard suggestion `the`, then type `hel` and accept `hello`
6. Expected: replacements are sent as minimal PTY diffs, with no extra backspaces, no duplicate prefix, and no stuck native marked range
7. Terminal, dictation: dictate a phrase, stop immediately after the final word, then wait for any late final transcript update
8. Expected: streamed text updates the preview first, commits to the PTY once at finalization, late native reconciliation does not duplicate or delete PTY text, and there is no `UIDictationController` hypothesis-cancel log
9. Terminal, native dictation stream: dictate `hello, how's it going` and watch the first `insertText` word plus following `replaceRange` rewrites
10. Expected: the first word and following rewrites update the preview while the synthetic marked range remains available for UIKit reconciliation; the final transcript commits once, with no hypothesis-cancel log
11. Terminal, native dictation preview: tap the visible preview bubble before finalization
12. Expected: the preview dismisses and the terminal does not keep a stuck streamed preview state
13. Cross-flow: after a dictation commit, press backspace repeatedly, then type `hel` and accept `hello`
14. Expected: dictation cleanup does not delete already-committed terminal text, repeated backspace continues through the PTY, and the following suggestion starts from a fresh keyboard context
15. Cross-flow: start a Japanese marked composition, cancel it, then accept a native suggestion in the same terminal focus session
16. Expected: cancelled marked text does not poison the following suggestion replacement or leave stale candidate UI
17. Commit message input: type Japanese marked text, accept a candidate, then accept a native suggestion
18. Expected: normal input uses the same native IME protocol correctly, with marked text committed once and suggestions replacing only the requested range
19. Commit message input, dictation: tap the microphone, dictate a short phrase, then stop dictation
20. Expected: the dictated phrase remains in the input after UIKit commits, the final cleanup delete does not clear the field, and any late `insertDictationResult` does not duplicate the phrase

## 11e. Terminal Keyboard Accessory Arrow Repeat On iOS

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Tap each arrow button in the keyboard accessory once
3. Expected: each tap sends exactly one corresponding arrow keystroke
4. Press and hold each arrow button, then release it
5. Expected: the corresponding arrow input repeats continuously while held and stops immediately on release
6. Start holding an arrow button, then dismiss the keyboard or background the app
7. Expected: repeat stops and does not resume when the keyboard or app returns

## 11f. Terminal Keyboard Accessory After Reconnect On iOS

1. Connect to a session on iPhone or iOS simulator and open a terminal
2. Tap the terminal so the software keyboard and accessory bar are visible
3. Press `Tab`, `Enter`, and an arrow key in the accessory bar
4. Expected: each key reaches the PTY
5. Put the workspace through an idle/reconnect recovery by backgrounding the app, sleeping/waking the host, or briefly interrupting network until the badge returns to `Connected`
6. Keep the same terminal active, show the software keyboard again, then press `Tab`, `Enter`, and an arrow key in the accessory bar
7. Expected: each key still reaches the PTY after reconnect; the accessory bar does not keep sending to a stale terminal channel

## 12. Quick Action Terminal Navigation

1. Connect to a session with at least two open terminals
2. Return to the home screen
3. Open the quick action panel and tap the add icon in the connected workspace header
4. Expected: the quick action panel closes, the app switches to the workspace screen, and a new terminal becomes the main view
5. Return to the home screen
6. Open the quick action panel and tap a terminal card under the connected workspace
7. Expected: the quick action panel closes, the app switches to the workspace screen, and the tapped terminal becomes the main view
8. Repeat from the workspace screen with a different terminal card
9. Expected: the selected terminal becomes active immediately without getting stuck on the previous screen or terminal

## 12a. Drawer Terminal List Stability During Network Reports

1. Connect to a session with at least two open terminals
2. Open the workspace drawer and switch to the Terminals tab
3. Leave the drawer open until the client logs `net report changed`
4. Expected: terminal cards remain visible and stable; the list does not disappear and reappear
5. Switch to the Session tab while network/path details update, then switch back to Terminals
6. Expected: the same terminal cards are still visible and ordered consistently

## 13. Drawer Close Tap During Snap

1. Open either app drawer
2. With the soft keyboard visible, start dragging the drawer or trigger an open or close snap
3. Expected: the soft keyboard hides as soon as dragging or snapping begins
4. Trigger a close and immediately tap the dimmed backdrop while the drawer is still animating closed
5. Repeat, but tap the closing drawer panel instead of the backdrop
6. Start opening the drawer with a drag, release so it continues snapping open, then tap or drag again before the snap completes
7. Compare the release-to-open snap against the release-to-close snap
8. Drag the drawer closed and start a new open drag as soon as it looks fully closed
9. Release a drag when the drawer is only slightly away from the open or closed target
10. In a long drawer tab, try a mostly vertical swipe inside the tab content, then a mostly horizontal drawer drag from the same scrollable area
11. Expected: vertical swipes scroll the tab content without dragging the drawer; once the drawer claims a horizontal drag, the tab content does not scroll under the gesture
12. Expected: input is ignored until the current snap animation finishes; the drawer does not reverse, restart, or jump to a new position, and close unlocks immediately when the visual close ends without an extra dead interval

## 14. Git Panel Diff Navigation

1. Connect to a workspace with at least one modified file and one untracked file
2. Open the workspace drawer and switch to the Git Diff tab
3. Tap a file entry in the git panel
4. Expected: the drawer closes and the git diff view opens for the tapped file
5. Reopen the drawer and return to the Git Diff tab
6. Expected: the file entry for the currently opened diff is highlighted
7. Open a normal file or terminal as the main workspace view, then return to the Git Diff tab
8. Expected: the git diff highlight clears because the active main workspace view is no longer a git diff
9. Expected: added and removed lines are indicated by full-width background color only; the diff text does not render a leading `+` or `-`
10. Expected: added and removed backgrounds stay continuous across rows without thin gaps, including after horizontal scrolling long lines
11. Expected: the workspace header subtitle shows the git filename plus added and removed totals, and long filenames truncate instead of overflowing
12. Expected: long filenames in the git panel file list truncate with an ellipsis, while status marks and change counts remain visible
13. Tap the untracked file entry
14. Expected: the diff view shows the untracked file content as added lines
15. Long-press a file entry
16. Expected: the file action sheet opens for that entry instead of doing nothing
17. Tap the dimmed backdrop outside the action sheet
18. Expected: the native action sheet dismisses without staging or unstaging the file

## 15. Markdown List Item Wrap In Preview

1. Connect to a session and open the terminal view
2. Run:

```bash
printf '%s\n' '- This is a very long bullet item that should wrap across multiple lines in the markdown preview without clipping or forcing horizontal overflow on a phone-sized sheet.' '1. This is a very long numbered item that should also wrap within the list text column while keeping the marker visible at the left edge.' > /tmp/zedra-markdown-wrap.md
printf '/tmp/zedra-markdown-wrap.md:1\n'
```

3. Tap `/tmp/zedra-markdown-wrap.md:1`
4. Expected: the preview opens in markdown mode
5. Expected: both the bullet item and the numbered item wrap onto multiple lines inside the text column; the marker stays visible and the wrapped lines do not overflow horizontally

## 15a. Markdown Code Block Overflow In Preview

1. Connect to a session and open the terminal view
2. Run:

````bash
cat >/tmp/zedra-markdown-codeblock.md <<'EOF'
# Code Block Test

```bash
printf '%s\n' 'this-is-a-very-long-command-that-should-stay-on-one-line-and-scroll-horizontally-instead-of-wrapping-or-clipping-on-mobile'
```
EOF
printf '/tmp/zedra-markdown-codeblock.md:1\n'
````

3. Tap `/tmp/zedra-markdown-codeblock.md:1`
4. Expected: the preview opens in markdown mode
5. Expected: the fenced code block does not render a visible `bash` language label
6. Expected: code block padding and line height are compact enough for a phone-sized sheet
7. Swipe vertically starting inside the code block
8. Expected: the markdown preview scrolls vertically and the code line does not drift horizontally
9. Swipe horizontally inside the code block
10. Expected: the long command scrolls horizontally without changing the surrounding vertical scroll position

## 15b. Markdown Table Header And Overflow In Preview

1. Connect to a session and open the terminal view
2. Run:

```bash
cat >/tmp/zedra-markdown-table.md <<'EOF'
# Table Test

| Command | Description | Status |
| - | - | - |
| `printf '%s\n' value` | this is a very long description that should wrap inside the capped table column on mobile | ready |
EOF
printf '/tmp/zedra-markdown-table.md:1\n'
```

3. Tap `/tmp/zedra-markdown-table.md:1`
4. Expected: the preview opens in markdown mode
5. Expected: the table header row renders `Command`, `Description`, and `Status`
6. Expected: table cell padding is compact like the code block padding
7. Expected: the long description wraps inside the capped column instead of making the table extremely wide
8. Swipe horizontally inside the table
9. Expected: the table scrolls horizontally without changing the surrounding vertical scroll position
10. Swipe vertically starting inside the table
11. Expected: the markdown preview scrolls vertically and the table does not drift horizontally

## 15c. Markdown Mermaid Diagram In Preview

1. Connect to a session and open the terminal view
2. Run:

```bash
printf '%s\n' '# Mermaid Test' '' '```mermaid' 'flowchart LR' '  A[Start] --> B[End]' '```' > /tmp/zedra-markdown-mermaid.md
printf '/tmp/zedra-markdown-mermaid.md:1\n'
```

3. Tap `/tmp/zedra-markdown-mermaid.md:1`
4. Expected: the preview opens in markdown mode
5. Expected: a rendered flowchart appears in a card at intrinsic SVG scale (not shrunk to viewport width)
6. Expected: wide diagrams scroll horizontally inside the card, like markdown tables
7. Expected: invalid mermaid syntax falls back to monospace source with a muted error line
8. Tap `Show source` below a rendered diagram
9. Expected: the fenced mermaid source appears and remains selectable for Add to Chat
10. Open the same file from the workspace docs tree or editor
11. Expected: the main workspace markdown view renders the diagram the same way
12. Open `examples/markdown-mermaid/diagrams.md` (or copy its ER and pie sections to `/tmp`)
13. Expected: diagram canvas, nodes, and ER entity boxes use dark fills with visible borders (not white boxes on a dark card)
14. Expected: ER attribute rows, pie legend labels, and edge relationship labels use light text on dark surfaces
15. Expected: flowchart connectors and arrowheads use accent blue (`#61afef`), not dim gray, with mostly straight vertical/horizontal segments

## 15d. Markdown Bottom Padding And Link Hit Slop

1. Connect to a session and open the terminal view
2. Run:

```bash
cat >/tmp/zedra-markdown-links.md <<'EOF'
# Link Test

Tap near [Zed](https://zed.dev), including slightly outside the blue text.

Last visible line.
EOF
printf '/tmp/zedra-markdown-links.md:1\n'
```

3. Tap `/tmp/zedra-markdown-links.md:1`
4. Expected: the preview opens in markdown mode
5. Scroll to the bottom
6. Expected: there is enough bottom padding to keep `Last visible line.` above the home indicator or sheet edge
7. Tap slightly outside the visible `Zed` link text
8. Expected: the link still opens, without starting text selection

## 16. iOS Native Selection In Markdown Preview

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Run:

```bash
cat >/tmp/zedra-markdown-selection.md <<'EOF'
# Selection Test

This paragraph should support native iOS selection inside the markdown preview.

[Open Zedra](https://zedra.dev)

- First bullet item
- Second bullet item

```rust
let answer = 42;
println!("{answer}");
```
EOF
printf '/tmp/zedra-markdown-selection.md:1\n'
```

3. Tap `/tmp/zedra-markdown-selection.md:1`
4. Expected: the preview opens in markdown mode
5. Long-press inside the heading or paragraph text
6. Expected: native iOS selection handles and the system edit menu appear without any custom app long-press UI
7. Drag a selection handle downward across the bullet list and into the code block
8. Expected: the selection can extend across markdown blocks; visible list markers and code lines participate in the selected range instead of acting like dead zones
9. With the selection still active, scroll the markdown preview vertically
10. Expected: the native selection highlight and handles move with the selected text instead of staying fixed to the viewport
11. With the selection still active, tap empty space below the short markdown content inside the main view
12. Expected: the native selection handles dismiss and the markdown preview remains focused for normal scrolling
13. Repeat the selection, then tap `Copy`
14. Expected: the selection menu dismisses cleanly and the preview remains responsive to scrolling and link taps afterward
15. Tap `Open Zedra`
16. Expected: `https://zedra.dev` opens externally

## 16a. iOS Native Selection In Code Preview

1. Connect to a session on iPhone or iOS simulator and open the terminal view
2. Run:

```bash
cat >/tmp/zedra-code-selection.rs <<'EOF'
fn main() {
    let answer = 42;
    println!("{answer}");
}
EOF
printf '\033]8;;file:///tmp/zedra-code-selection.rs:1:1\033\\/tmp/zedra-code-selection.rs:1\033]8;;\033\\\n'
```

3. Tap `/tmp/zedra-code-selection.rs:1`
4. Expected: the preview opens in code editor mode
5. Expected: the code view does not show the old blue logical caret
6. Long-press inside a code line
7. Expected: native iOS selection handles and the system edit menu appear for the code text
8. Open a small code file from the workspace drawer so the code editor is visible as the main workspace view
9. Tap the workspace header drawer and quick-action buttons
10. Expected: the header buttons open their overlays immediately instead of waiting for selection recognition or starting selection in the code editor below
11. With the workspace drawer open over the code editor, tap drawer tabs and controls
12. Expected: drawer touches interact with the drawer immediately, including active tab changes, and do not start selection in the editor behind it
13. With a code selection active, tap the workspace header or open drawer controls
14. Expected: the selection dismisses and the tapped control responds immediately
15. With a code selection active, tap empty space inside the editor but outside any code text
16. Expected: the native selection dismisses instead of leaving stuck handles
17. Drag a selection handle into empty horizontal or vertical space inside the editor
18. Expected: the handle tracks to the nearest sensible text boundary instead of jumping to select all text before or after the handle
19. Tap the code view and type with a hardware keyboard
20. Expected: the file content remains unchanged because the editor is read-only

## 16b. Mention Editor Selection In Agent Terminal

1. Connect to a workspace with terminals running `claude`, `codex`, `opencode`, or another detected AI agent CLI such as `gemini`
2. Open a source file from the workspace drawer so the main workspace code editor is visible
3. Long-press inside a code line and drag the selection handles across multiple lines
4. Tap `Add to Chat` in the native selection menu next to `Copy`
5. Expected: `Add to Chat` shows the Zedra icon in the selection menu
6. Expected: a native selection sheet lists detected supported AI-agent terminals by terminal title without a `Terminal N` prefix; emoji and spinner glyphs are omitted from the picker labels, and iOS shows the bundled agent SVG icon in both light and dark appearance
7. Pick the Claude terminal
8. Expected: the main view switches to the selected terminal first, then the selected range is pasted into Claude as an `@file#Lx-Ly` mention after a short delay; it is not submitted automatically
9. Repeat for Codex, opencode, Gemini, or another detected non-shell agent
10. Expected: the main view switches to the selected terminal first, then the selected range is pasted as fenced context with the source range after a short delay; it is not submitted automatically
11. Open a markdown file and select text in a paragraph or code block
12. Drag the markdown selection handles to extend and shrink the selected range before opening the menu
13. Tap `Add to Chat`, pick an agent terminal, and verify the selected source lines are pasted into that terminal
14. Exit all supported AI-agent CLIs, select editor text, and tap `Add to Chat`
15. Expected: the native selection sheet shows `No AI agent detected` and no text is inserted

## 16c. Workspace Markdown File Rendering

1. Connect to a workspace with a `README.md` or another markdown file
2. Open the workspace drawer file list and tap the markdown file
3. Expected: the drawer closes and the main workspace editor renders the file with markdown formatting instead of the code editor line gutter
4. Expected: the workspace header subtitle shows the file's relative path, not the workspace cwd or an absolute path
5. Open a non-markdown source file from the same file list
6. Expected: the main workspace editor still opens the code editor with syntax highlighting and line numbers
7. Open a large markdown file with many headings, paragraphs, lists, and fenced code blocks
8. Expected: the rendered markdown has horizontal padding on both sides, including while scrolling
9. Expected: the workspace remains responsive while the file loads and scrolling does not repeatedly reparse or rebuild every markdown block

## 17. Native Confirmations For Terminal Delete And Session Disconnect

1. Connect to a session with at least two terminals open
2. Open the workspace drawer Terminals tab and tap the close affordance on one terminal card
3. Expected: a native confirmation alert appears with `Delete` and `Cancel`
4. Tap outside the alert
5. Expected: the alert dismisses and the terminal card remains visible
6. Trigger the same delete again and tap `Cancel`
7. Expected: the alert dismisses and the terminal card remains visible
8. Trigger the same delete again and tap `Delete`
9. Expected: the terminal card is removed from the drawer immediately, without waiting for the remote delete RPC to finish
10. Expected: if the deleted terminal was the active main view, the main view switches to another terminal
11. Delete the remaining terminals one by one
12. Expected: after the last terminal is deleted, the main view shows `No active terminal`
13. Repeat terminal deletion from the quick action panel
14. Expected: the same native confirmation alert appears there, and confirmed deletion removes the card immediately
15. Open the Session tab and tap `Disconnect`
16. Expected: a native confirmation alert appears with `Disconnect` and `Cancel`
17. Tap outside the alert
18. Expected: the alert dismisses and the session remains connected
19. Tap `Cancel`, then retry and tap `Disconnect`
20. Expected: the session disconnects only after confirmation and the app returns to Home
21. Expected: the home workspace card immediately shows the disconnected/reconnect state instead of the old connected state
22. Tap the disconnected workspace card
23. Expected: the connect view title is `Disconnected` and the subtitle is `Tap refresh to reconnect.`

## 18. Workspace Header Terminal Title + Terminal Agent Icon

Zedra currently uses OSC 1 icon-name updates as the primary active-agent signal.
Shells should emit the command name as OSC 1 when an AI CLI starts, then a
non-agent prompt/path icon name when the shell returns to prompt.
Launch-command terminals seed identity from the known command until shell OSC
metadata arrives.

1. Open a workspace, open a terminal.
2. Type `claude` (or another supported AI CLI such as `opencode`, `codex`,
   `gemini`, `amp`, `cline`, `cursor-agent`, `goose`, `hermes-agent`,
   `junie`, `kilo`, `openclaw`, `openhands`, `pi`, `qodercli`, `qwen`, or
   `trae-cli`) and observe it while running, then exit and wait for the shell
   prompt.

Expected:
- Header subtitle (below project name) updates from cwd to the terminal title.
- Header subtitle does not show an agent icon.
- Agent icon appears in the terminal card matching the running CLI, based on OSC
  1 icon-name updates rather than terminal title text.
- If the agent changes the terminal title while running, the icon remains
  stable in the terminal card.
- After the agent exits and shell returns to prompt, terminal card icon disappears;
  subtitle shows the last title (or falls back to cwd), even if the title still
  mentions the agent.
- Switching to a different terminal in the drawer updates header to the
  active terminal's title and updates the terminal card icon.
- Relaunch the client app while an AI CLI command is still running in the
  remote terminal. Expected: after reconnect/reattach, the terminal card
  restores the agent icon from host-persisted OSC 1 metadata without waiting for
  a new command to start or a new OSC 1 event to arrive.
- Start a Claude resume terminal through `/zedra-start` or another
  `launch_cmd` path such as `claude --resume <session_id>`. Expected: while
  Claude is running, the terminal card shows the Claude icon even before a shell
  prompt emits fresh OSC metadata.

## 18b. Agent Toolbar, Sessions, and Manage Views

1. Open a connected workspace and open the workspace drawer Terminals tab.
2. Expected: the top toolbar shows four icon actions: Create agent, Create
   terminal, View sessions, and Manage agents.
3. Tap `Create terminal`.
4. Expected: the drawer closes, a new shell terminal opens in the main view,
   and the terminal card appears in the drawer.
5. Tap `Create agent`.
6. Expected: a scrollable native list picker shows installed host agents with
   icon and version subtitle; unavailable agents are omitted.
7. Pick an installed agent such as Claude or Codex.
8. Expected: Zedra creates a new terminal using the host-owned launch command,
   opens it as the active main view, and the terminal card shows the matching
   agent icon once OSC metadata arrives.
9. Tap `View sessions`.
10. Expected: the drawer closes and the main view shows a unified session list
    grouped by day across Claude, Codex, and OpenCode for the current
    workspace. Each row shows agent icon, title, datetime, branch/worktree,
    transcript size, and model when available.
11. Tap a resumable session row.
12. Expected: Zedra immediately resumes the session in a new terminal and opens
    it as the active main view.
13. Tap `Manage agents`.
14. Expected: the main view shows managed agents as a list with setup/session
    counts and usage gauges when live usage is available. Tapping an agent opens
    detail with metadata, usage gauges (5h / 7d and extra spend when present),
    account fields, and a session list below.
15. In manage detail, verify discovered account fields:
    - Claude: logged in, plan (Pro/Max/Team/Enterprise when OAuth, credentials
      file, or CLI PTY is available), model, effort, permission mode, today msgs
      and total cost when `stats-cache.json` exists; no week msgs/sessions/tools
      rows. Usage gauges: with a valid `~/.claude/.credentials.json` token,
      limits come from the OAuth usage API; with Keychain-only CLI auth or an
      expired file token while the CLI still works, from PTY `/usage` scrape
      (percents should match the CLI panel; inline reset duration may be missing
      if parsing fails). Host check: `zedra agent scan usage --json`.
    - Codex: logged in, plan, plan until, account name from `auth.json`, model/
      personality from `config.toml`
    - OpenCode: config dir presence
16. Tap `Resume` on a session from manage detail.
17. Expected: same immediate resume behavior as the unified sessions view.
18. Re-open `View sessions` or `Manage agents` without tapping Refresh.
19. Expected: lists load quickly from the host startup cache (default limit 50
    sessions per agent).
20. Tap Refresh in either view.
21. Expected: the host rescans synchronous managed-agent metadata and session
    lists immediately; CLI versions may arrive a moment later (one
    `AgentInfoChanged` host event per agent) and update manage detail without
    another refresh.
22. In manage detail for an available agent, wait a few seconds after opening
    or refreshing.
23. Expected: the version line updates from `Checking…` to the real `--version`
    string when the host finishes that agent’s async version probe.
24. Open manage detail for an agent with zero sessions or a session-list error
    (for example Codex when the host scan fails).
25. Expected: the header (back, title, refresh) stays full content width and
    lines up with the metadata column; narrow error text must not shrink-wrap
    the header (see `docs/CONVENTIONS.md` GPUI flex width rules and
    `ui::subscreen_layout`).

## 19. Xcode Rust Build Target

1. Open `ios/Zedra.xcworkspace` in Xcode
2. Select an iOS simulator destination and press Build
3. Expected: the build log shows `ZedraRustFFI`, then `Building Rust for Xcode (..., iphonesimulator, ...)`, before `ProcessXCFramework`
4. Select a connected iOS device destination and press Build
5. Expected: the build log shows `ZedraRustFFI`, then `Building Rust for Xcode (..., iphoneos, ...)`, before `ProcessXCFramework`
6. Select the `Zedra Release` scheme with the connected iOS device destination and press Run
7. Expected: the Rust build log includes `Release mode enabled`, and Xcode installs and launches the production `Zedra` app
8. Run `./scripts/build-ios.sh --device --release --debug`
9. Expected: the command fails before compiling and says iOS release builds cannot enable debug flags
10. Archive the app with the Release configuration
11. Expected: the archive uses the production bundle id `dev.zedra.app`, generates a dSYM, and has Xcode Release strip/validation settings enabled

## 20. iOS Orientation Support

1. Launch the app on an iPhone or iOS simulator in portrait orientation
2. Rotate the device or simulator to landscape left and landscape right
3. Expected: Zedra remains in upright portrait orientation and does not relayout into a landscape window
4. Archive the Release build and inspect the generated `Info.plist`
5. Expected: the base `UISupportedInterfaceOrientations` key contains `UIInterfaceOrientationPortrait`
6. Expected: the iPad-specific `UISupportedInterfaceOrientations~ipad` key contains portrait, portrait upside down, landscape left, and landscape right

## 21. iOS Release logging

1. Install a **Release** build on device (not Xcode Run with debugger attached)
2. Connect via **Scan QR**; optional: `scripts/log-ios.sh --grep connect`
3. Expected: no burst of per-packet iroh/quinn trace lines (`tracing-subscriber` requires `debug-logs`)

## 22. Terminal appearance (light/dark)

1. Connect to a workspace and open a terminal running `ls` with color
2. Open Settings → Appearance and tap the **sun** segment (light mode)
3. Expected: terminal background is light; directory names and ANSI colors are readable (not washed out)
4. Run `claude` (or another session already showing Claude output) and scroll to file-reference lines such as `L123 (file.rs):`
5. Expected: highlighted paths are readable on the light background (not pale lavender)
6. Open **Codex** in the same terminal (or a dedicated Codex session)
7. Expected: the composer / user-message background pill matches the light theme (not missing or using a dark gray from stale palette)
8. From the agent picker or quick action panel, open a fresh Codex launch-command terminal
9. Expected: Codex's first rendered composer / user-message background pill is visible without waiting for terminal reattach or focus changes
10. Toggle back to **Dark**
11. Expected: terminal colors, Claude highlights, and Codex pill update without restarting the app
12. Optional: from the host, run `printf '\e[10;?\e[11;?\e\\'` inside the Zedra session and confirm replies report the current fg/bg (light: `fafafa` background). Zedra answers OSC queries on the session PTY via `ColorRequest` and the active `TerminalTheme`; it does not inject palette setup bytes into scrollback on toggle.

## 22a. Android Native Presentations Follow Theme

1. Install an Android build and open Settings.
2. Toggle Appearance to **Light**.
3. Open the agent picker, keyboard accessory bar, and a native text input or alert.
4. Expected: native surfaces use light backgrounds and dark text/icons, matching the app theme.
5. Toggle Appearance back to **Dark** and repeat the same presentations.
6. Expected: native surfaces return to dark backgrounds and light text/icons without restarting the app.

## 22b. iOS Native Presentations Follow Theme

1. Install an iOS build and open Settings.
2. Toggle Appearance to **Light**.
3. Open an alert, action sheet, agent picker, keyboard accessory bar, native text input, custom sheet, and native notification.
4. Expected: UIKit presentation chrome uses light backgrounds and dark text/icons, matching the app theme.
5. Toggle Appearance back to **Dark** and repeat the same presentations.
6. Expected: UIKit presentation chrome returns to dark backgrounds and light text/icons without restarting the app.
