Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A Human pairs the phone app with their Host by a one-time QR code, and the phone
## How to get to it (user POV)

- Computer: Bot mode → settings → Devices → Pair a phone. The dialog shows the QR code and a copy link; the Devices list has Revoke.
- Phone: the app opens on "Pair with your computer"; scan the code or paste the link, then tap Pair.
- Phone: first use opens "Pair with your computer". From an existing session: Workspaces → Computers → Pair a new computer. Scan the code or paste the link, then tap Pair. Cloud → Join by link or QR is the separate cloud invitation path; its preview identifies organization membership versus one-channel guest access. The category tabs are the first drawer row; swipe, backdrop, Android Back and accessibility escape dismiss it.

## Driving it with verify-app

Expand Down
13 changes: 13 additions & 0 deletions .github/pr-screenshots/2174-drawer-compact/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Compact workspace drawer — native evidence

Final drawer source `dc15ffd3a7003ef0c0f4110f834f02f15a990e6c`, integrated `1376b50b1f8358d9d9b22c89e55901fb5a141d24`; retained native `494a20f1eb4f0666a2c90bc3c758af4e7ff5f46b`. No native dependency/config changes. Real shared iOS Simulator screenshots, 402 × 874, English.

`before-*` is the prior title/X + mixed Add iteration at source `59fc97fd7bada6d54cbae1feacc4d93d032e4c82`. The same empty-computer/signed-out-UAT fixture, viewport and themes are used. The exposed screen behind the drawer differs; compare the drawer only. These are not screenshots of the PR base branch. Earlier base evidence remains in `../2174-drawer-tabs/` and is superseded for final appearance.

`*-default-*`, `*-intermediate-*`, `*-max-*`, `join-direct` and `pair-direct` use the final source. `cloud-max-scrolled-dark` shows the address after a swipe in the constrained middle list; the footer stayed at x0/y757.6667/w342/h78.3333. A duplicate footer under the backdrop was found during review, removed, and these final screenshots recaptured.

Naming screenshots use source `f2d0f4af3ebeecfe5df9f25d2bc1a3636340531d`, integrated `9e77d9810ad642388ed8ac8bcd9a4b9725f181c0`; naming code is identical in the final source. Native edit changed the display name to `TOB UAT`, saved, survived cold restart, then restored `tob-instance-uat` and cold-read it back. No login, credentials, Host identity or organization name changed. `pair-name*` only verifies the optional input; no pairing was redeemed. Driver keystroke injection opened Expo's development menu, so native clipboard Paste was used for this nonsecret test name.

Local checks: typecheck, focused lint/format/source-policy/release-ledger checks; 74 affected focused tests plus 14 host-look/pairing-telemetry tests at f2d0 (counts overlap). After the backdrop-only fix: typecheck and 19 drawer/style tests passed. CI status is recorded in the PR, separately from these runtime checks.

Limits: no Android/physical/VoiceOver/Human acceptance; no signed-in org/guest fixture or actual named-computer pairing in this run. The previously observed Pair-modal → cloud-services root-drawer overlay issue remains unqualified; the normal cold-open path works. This is not a full navigation or authentication pass. No production change or merge.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions .github/pr-screenshots/2174-drawer-compact/proof.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"issue": 2174,
"pr": 2175,
"source": "dc15ffd3a7003ef0c0f4110f834f02f15a990e6c",
"integrated": "1376b50b1f8358d9d9b22c89e55901fb5a141d24",
"native": "494a20f1eb4f0666a2c90bc3c758af4e7ff5f46b",
"beforeSource": "59fc97fd7bada6d54cbae1feacc4d93d032e4c82",
"beforeIntegrated": "9a73f9c21440235d8caa6fdc9f7d65d5dc6dfde0",
"namingSource": "f2d0f4af3ebeecfe5df9f25d2bc1a3636340531d",
"namingIntegrated": "9e77d9810ad642388ed8ac8bcd9a4b9725f181c0",
"viewport": "402x874",
"locale": "en",
"fixture": "empty Computer directory and signed-out public UAT cloud service",
"passed": [
"Computers/Cloud tab selection and independent contextual Add destinations",
"fixed Notifications footer in both tabs; cloud management only in Cloud",
"default/intermediate/maximum Dynamic Type in light/dark",
"maximum text middle-list swipe exposes the address while Notifications rect remains unchanged",
"left-swipe dismissal",
"local cloud name save and cold-start persistence; exact original name restored and cold-read back",
"computer optional name input; no pairing redeemed"
],
"footerRectAtMax": {
"x": 0,
"y": 757.6666717529297,
"width": 342,
"height": 78.33331298828125
},
"unverified": [
"Android native and physical phone",
"VoiceOver and Human acceptance",
"signed-in organization/member/guest rendering on this revision",
"real computer pairing with name persistence",
"full keyboard/text-size matrix and every footer destination",
"Reduce Transparency toggle"
],
"knownCaveat": "Opening cloud-services from the native Pair modal can hide the root drawer; normal cold-open path works. Regression origin is not established; full navigation acceptance is not claimed.",
"correction": "Native review at f2d0 found duplicate footer in backdrop. dc15 removed only that duplicate; all final drawer layout screenshots and contextual Add actions were recaptured. Naming code is unchanged.",
"comparisonLimit": "Before/after use the same viewport, theme, locale and drawer fixture. The exposed underlying route differs (pairing vs invitation), so comparison is limited to the drawer.",
"cleanup": [
"original UAT display name restored and cold-read back",
"driver closed; own Metro stopped",
"qa/batch-24 and original three untracked documents preserved",
"light/large and original locale restored; task clipboard cleared",
"no account/Host/Prod writes, pairing redemption, native build or merge"
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions .github/pr-screenshots/2174-drawer-tabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Drawer tabs and Add screenshots

Real shared iOS Simulator captures for #2175, 402 × 874, English. Before and after use the same retained QA computer and signed-out public UAT configuration; there are no credentials in these images. The service address shown is the public UAT endpoint.

The before view combines both lists and three direct actions. The after views show Computers and Cloud separately and the new Add chooser. The chooser has no old modal counterpart: the old direct actions are its before state. `proof.json` binds source, integrated batch and native identities.

Navigation checks reached the invitation and pairing pages without signing in, accepting an invitation or changing pairing. Maximum-text images show wrapping and a scrollable chooser; they do not establish a complete accessibility matrix. A nested native-modal route observation remains open in the proof; Android, physical phone and Human QA are unverified. The shared environment was restored after capture.
39 changes: 39 additions & 0 deletions .github/pr-screenshots/2174-drawer-tabs/proof.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"issue": 2174,
"pr": 2175,
"source": "6292fbf41123bc583f813116e349ac2788e3871a",
"baseSource": "e8472aab4a0e22c0404f99a44d6831dcfe5d0988",
"baseBatch": "bfbcc648fa789ed551c2249b1a392f9341128f04",
"afterBatch": "06c92fc78b71643f6cdd74d42d50913ea92d640a",
"main": "648c1202c44f5a5e82ce22596682934745705cef",
"native": "494a20f1eb4f0666a2c90bc3c758af4e7ff5f46b",
"viewport": "402x874",
"locale": "English",
"environment": "shared iOS 26 Simulator, retained QA computer and signed-out public UAT configuration",
"passed": [
"Computers/Cloud selected tabs and separate directories",
"one Add target with decorative icons",
"Add close returns to selected tab",
"Add opens existing invitation page",
"Add opens existing pairing page",
"normal text light/dark actual screenshots",
"maximum text wraps; Add opens, chooser scrolls, close works"
],
"unverified": [
"Android native",
"physical phone",
"Human/VoiceOver",
"new login or invitation acceptance",
"full large-text matrix and all footer actions",
"Reduce Transparency toggle in this run"
],
"observation": "After opening Pair (native modal) and following a cloud-services deep link, Workspaces did not visibly overlay the native modal. A cold reopen to the normal cloud-services route restored the working drawer. This route is not marked passed; regression origin not established.",
"cleanup": [
"driver closed",
"own Metro stopped; 8091 free",
"qa/batch-24 restored at main648c1202c",
"light/large restored",
"three original untracked QA documents retained",
"no account, pairing or Host writes; no Prod or merge"
]
}
14 changes: 9 additions & 5 deletions apps/mobile/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ References: [Apple toolbars](https://developer.apple.com/design/human-interface-

The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-with-settings-and-hosts-in-the-header.md): what the Human does most sits under the thumb, and what they do rarely sits in the header.

- **One workspace drawer, two connection types.** Computers are paired Hosts; cloud workspaces belong to an explicitly trusted TOB instance. The existing left drawer is shared across both, with Computers and Cloud workspaces sections. Cloud headers expose the same workspace switcher. A guest sees the organization initial/name and “invited channels only”; selecting it shows only current channel grants. Instance domains disambiguate identically named workspaces. The list and its join/create/pair actions scroll together at large text sizes.
- **One workspace drawer, two connection types.** Computers are paired Hosts; cloud workspaces belong to an explicitly trusted TOB instance. The existing left drawer is shared across both, with Computers and Cloud tabs. Cloud headers expose the same workspace switcher. A guest sees the organization initial/name and “invited channels only”; selecting it shows only current channel grants. Instance domains disambiguate identically named workspaces. The contextual join/pair action stays below the tabs; the directory scrolls between that header and a fixed footer.
- **Computer workspaces.** Each paired computer is a Host.
- The app stays connected to every paired Host at once.
- The Chats list and every chat show one Host at a time. The Chats header has a Slack-style band in the accent colour (a muted accent in dark mode); the list sits on a panel with rounded top corners below it.
Expand All @@ -34,13 +34,17 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
- Avatar: the initial tile (default), one of eight pixel portraits from the PersonaBot avatar generator, or a photo cropped square.
- Banner: the accent band (default), a solid colour (ten swatches or a typed hex), a two-stop gradient (six presets or typed stops), or a picture under a dark scrim.
- A custom colour goes through the accent band's steps, so dark mode mutes it the same way. The ink is white or near-black, whichever reads on every stop; a picture always takes white ink with a soft shadow.
- **Host drawer** switches Hosts. It slides in from the left, covers about 85% of the width and dims the list behind it.
- Like Slack's workspace drawer it has three parts on the `sheet` layer: a fixed "Computers" header, a scrolling list of Hosts, and a fixed footer with **Pair a new computer** (opens the pairing flow) and **Notifications** (opens notification settings), separated from the list by a divider.
- **Workspace drawer** switches between paired Computers and Cloud organization workspaces. It slides in from the left, covers about 85% of the width and dims the list behind it.
- Rounded **Computers / Cloud** tabs are the first row above one scrolling list, with no duplicate title or close button. Opening the drawer selects the current route's tab; switching tabs only changes the list, never the active computer or cloud session. Android uses opaque raised chips; iOS uses the existing glass material with Reduce Transparency, older OS and large-text fallbacks. Tabs expose their selected state to accessibility.
- One contextual plus action sits directly below the tabs, above the scrolling list: Computers opens **Pair a new computer** directly; Cloud opens **Join by link or QR** directly. There is no mixed action chooser or stacked icon group. Organization creation and service configuration live under **Manage cloud services** on the Cloud tab; Notifications stays at the bottom of a fixed footer on both tabs; Cloud service management sits above it. Only the directory scrolls.
- A cloud service is an instance/address; its organization is a cloud workspace; a channel belongs to that organization. An invitation already identifies its service and scope: workspace membership or access to only one channel. The existing preview and acceptance flow keeps this distinction explicit. No separate server-selection step or new authorization store is introduced.
- Each Host row has its Computer avatar tile (the same tile as the Host chip; the current Host's tile has an `fg` ring at radius `lg`, concentric with the tile, as Slack marks the current workspace), its name, an online/offline/checking dot and label, then one **count chip** on the right (see Count chip below). It shows **Needs you** when anything waits for the Human, because that matters most; otherwise it shows **Unread**, the count of unread conversations. A ⋯ button at the end, or a long press on the row, opens the Host's menu: Customize (name, avatar and banner), Show this computer, and Unpair, which asks for confirmation.
- The Host chip opens it, and so does a swipe right from the left edge of a tab's root screen. The edge swipe is never attached inside a pushed chat, where it stays the system Back.
- Tapping a Host switches to it and closes the drawer. Tapping the backdrop or swiping left closes it.
- Tapping a Host switches to it and closes the drawer. Tapping the backdrop, swiping left or using the accessibility escape gesture closes it. Opening focuses the selected tab.
- It takes screen-reader focus when it opens and returns focus to the Host chip when it closes. With Reduce Motion on it appears without sliding.
- Android Back closes it. Android draws the same drawer.
- Normal-size rows use a name and a smaller muted address, with one line each. Accessibility sizes may wrap. Status dots and accessible labels retain online/permission context; channel-only guests keep the invited-channel scope in their secondary label.
- Pairing accepts an optional local name using the existing computer appearance store; direct-link pairing can keep the original name, then rename in Customize computer. Cloud import accepts a display name; Manage cloud services can rename it later. These names affect only this phone and never change Host identity, organization names, trusted endpoints or sessions.
- **Tabs** are **Chats** and **Activity**. iOS uses `NativeTabs`; Android uses a custom floating bar through the JS tab navigator. There is no search tab.
- Activity counts the device's selected Human Attention types, defaulting to pending tool approvals only. Global Settings → Activity count supports multiple types: tool approvals, questions, private input, other pending actions, unread mentions/replies and other unread conversations. Selecting none keeps only the icon. Each unique pending item or unread conversation counts once; handled history is excluded. This device-local choice does not change push preferences or the app icon's push badge. Reads use the existing authenticated `humanAttention` API, including pagination, deduplication and early saturation at 100 for the 99+ display. The observer shares the current Inbox lifecycle, coalesces refreshes, retains same-selection cached counts on failure and discards stale responses after selection or Host changes. Android replaces the inactive icon; iOS uses its native badge.
- A chat is pushed above the tabs, so the tab bar hides while it is open.
Expand Down Expand Up @@ -325,6 +329,6 @@ Every editable field on iOS and Android belongs to a layout that responds to the

### Shared drawer title treatment

`DrawerHeader` is the shared header for the workspace drawer and generic bottom `Sheet`: a leading semantic title, optional decorative icon, and an independently accessible trailing close action. The iOS title uses a content-sized capsule with native glass on iOS 26; older iOS and Reduce Transparency use an opaque raised capsule. At accessibility text sizes, the title uses an opaque rounded rectangle so multiline text stays within a readable surface. Android keeps a plain left-aligned title on the opaque overlay. The close target is at least the platform touch minimum. Long titles wrap without changing the font scale; subtitles remain outside the capsule.
The workspace drawer starts with its category tabs. `DrawerHeader` supplies generic bottom `Sheet` headers: a leading semantic title, optional decorative icon, and an independently accessible trailing close action. The iOS title uses a content-sized capsule with native glass on iOS 26; older iOS and Reduce Transparency use an opaque raised capsule. At accessibility text sizes, the title uses an opaque rounded rectangle so multiline text stays within a readable surface. Android keeps a plain left-aligned title on the opaque overlay. The close target is at least the platform touch minimum. Long titles wrap without changing the font scale; subtitles remain outside the capsule.

Material is a decorative background only: text, focus, hit testing and navigation remain in the existing React Native tree. It must not capture gestures or become another accessibility element. Drawer body and grabber stay opaque and continuous. Header styling does not add dismissal controls to Settings or other native `SheetStack` routes; those retain their established handle/back policy until separately verified.
16 changes: 15 additions & 1 deletion apps/mobile/app/pair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { useCallback, useRef, useState } from 'react';
import { ActivityIndicator, Platform, Pressable, View } from 'react-native';

import { hostSession } from '../src/host/host-session';
import { hostLooks } from '../src/host/native-host-look';
import { HOST_NAME_MAX, hostLookName, withHostName } from '../src/ui/design/host-look';
import { ConnectionNameField } from '../src/ui/primitives/connection-name-field';
import { nativeCarrier } from '../src/host/native-carrier';
import { pairHost, type PairingFailure } from '../src/host/pairing';
import { mobileTelemetry } from '../src/telemetry/native';
Expand All @@ -25,6 +28,7 @@ export default function PairScreen() {
const params = useLocalSearchParams<{ p?: string }>();
const [permission, requestPermission] = useCameraPermissions();
const [link, setLink] = useState('');
const [connectionName, setConnectionName] = useState('');
const [busy, setBusy] = useState(false);
const [focused, setFocused] = useState(false);
const [error, setError] = useState<PairingFailure | undefined>();
Expand All @@ -46,7 +50,11 @@ export default function PairScreen() {
method,
deviceName: deviceName(),
carrier: nativeCarrier(),
save: hostSession.pair,
save: async (host) => {
await hostSession.pair(host);
const name = hostLookName(connectionName);
if (name) hostLooks.set(host.hostId, withHostName(hostLooks.look(host.hostId), name));
},
telemetry: mobileTelemetry,
signal: operation.signal,
});
Expand Down Expand Up @@ -85,6 +93,12 @@ export default function PairScreen() {
>
<Button label={t.cloud.title} onPress={() => router.push('/cloud-services')} />
<Text style={{ color: theme.inkSoft, fontSize: 14 }}>{t.pairHint}</Text>
<ConnectionNameField
value={connectionName}
onChange={setConnectionName}
maxLength={HOST_NAME_MAX}
disabled={busy}
/>
<View
style={{
aspectRatio: 1,
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/cloud/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ export class CloudClient {
await this.save({ ...(!changed ? old : {}), config });
return config;
});
rename = (id: string, name: string) =>
this.lock(async () => {
const entry = this.entry(id);
const config = parseInstance({ ...entry.config, name: name.trim() });
await this.save({ ...entry, config });
});
private async acceptTokens(
entry: Entry,
value: unknown,
Expand Down
Loading
Loading