Skip to content

M4-36 Terminal on the desktop

No Date Ruling Reason
D-M4-36-0 2026-10-25 Built on main after M4-35, M4-24 and M2-48 landed; the changelog entry and these rulings are fragments. Differences to the brief's names, following the merged code: the server port is TerminalServerApi (the app's adapter ApiTerminalServerApi adds listHosts and listSessions behind the interface TerminalHostsApi); the step-up port is the tear-off session.stepUp.confirm; the dialogs answer TerminalPrompts with DialogTerminalPrompts; the hub of sync_engine is HostEventsHub behind SyncEngine.hostEvents with the events HostChanged, HostPresenceChanged, TerminalSessionChanged, HostRemoved and HostAccessGranted; the AppConfig getter of terminal.max_session_duration is maxSessionDuration; the client stack helper is a Go test package (TestClientStack, run only with BOTHY_CLIENTSTACK_OUT), because the suite of M2-48 builds its world through testing.T; make terminal-stack calls tools/ci/terminal_stack.py. D-W-100, D-W-114 (merged code wins over the brief's names).
D-M4-36-1 2026-10-25 The stream's identify carries protocol_version, the access token, device_id and a DPoP proof built by DpopProofFactory.forGateway for the stream URL: htm = GET, htu the HTTPS form of wss://<host><stream_path>, the nonce of terminal.hello and ath. One transport serves one tab, so the proof always names the stream the tab opened last. Binary messages only after identify; a text frame from the server afterwards blocks the tab as a protocol error and is never shown as output. asyncapi TerminalStreamIdentify; brief Design note 1; 15 8 (never through sync_engine).
D-M4-36-2 2026-10-25 Pins live in the local-only table host_key_pins (schema version 4). From version 4 on it is a preserved table of MigrationRules, so every later step keeps its rows; the survival test fills pins and checks them after each migration; the cache cleanup and the removal of targets never touch it; only wipe deletes it with the database. 15 6.1, 6.2, 23.3; brief Design note 2.
D-M4-36-3 2026-10-25 The profile claude offers only the working folders of the entry of reported_session_users whose name the person's own entry of session_users names (ownWorkdirs of terminal_ui), never another entry's folders; the chosen index goes as workdir_index. The server decides; a refusal shows the reason of terminal_denied (not_host_assignee, server_host_owner_only, i6_holder_only, no_session_user, session_limit, profile_unknown) through problemText, which now has a text per reason, and the tab keeps the reason next to the code (TerminalTabState.problemReason). 24 2.2, ruling T13, SR10 to SR12, 15 1 principle 1; brief Design note 3 and fixed requirement 10.
D-M4-36-4 2026-10-25 Host data has no table: the group "Hosts" and the tabs load listHosts, getHost and listTerminalSessions through REST and reload after host.updated, terminal_session.changed and the access.changed entries of kind host; host.presence_changed updates the connection in place. The engine's appliers of the two durable types write nothing and announce after the commit of their page. brief Scope; 04 2.1 and 8.2.
D-M4-36-5 2026-10-25 The terminal's REST client is the session's client without the StepUpInterceptor (DPoP and refresh stay), so the controller runs the step-up itself after the warnings and retries an attach once. D-M4-35-8.
D-M4-36-6 2026-10-25 One tab per own session: a session the server lists for the person on the host becomes a detached tab with "Reattach" (TerminalController.adopt, no request); the tabs live as long as the login, so leaving the view keeps the streams. A tab whose opening failed or was cancelled before a session existed goes when the next session opens. 01 D11.
D-M4-36-7 2026-10-25 The table "In the terminal" of 01 7 is data (terminal/terminal_shortcuts.dart, a test compares it with the specification) and goes to the renderer as its shortcut map (TerminalPane.shortcuts), so the app's strokes win over the shell key of the same stroke (xterm would send Ctrl+Shift+N as Ctrl+N), while every stroke without the platform modifier reaches the shell; the region around the view is KeyboardScope.passThrough of M4-24. "Font larger" also takes the "=" key and the number pad, because the plus key differs by layout. Copy and paste stay with the renderer and its paste guard. The search in the scrollback lists the matching lines from memory and keeps nothing. R-PR-10; brief Design note 4; 15 23.4.
D-M4-36-8 2026-10-25 The stack test: make terminal-stack starts the Go helper (stack of M2-48 with one host container assigned to Alex, a session user and a grant terminal, enrollment codes for two devices of Alex and one of the Owner), waits for its connection file, runs apps/client/terminal_stack_test/terminal_stack_test.dart (outside integration_test/, so no app build contains it) and stops the helper, which removes the stack. Only that entry point installs the trust of the test CA and maps the origin to the published port (an HttpOverrides); the target is not a gate. brief Design note 6.
D-M4-36-9 2026-10-25 The canary test covers the paths the app has: the decrypted rows of every table and the database file, print and debugPrint, every REST request body and path the fake server recorded (client metrics included), the event log of the fake and the metrics collector. Crash reports start only with a build target (CRASH_DSN), so the tests have no crash transport to search; their scrubbing is the test of M4-31. brief Design note 5.
D-M4-36-10 2026-10-25 On Windows the screen of terminal_ui reads only the hardware keyboard (hardwareKeyboardOnly of xterm): the Windows embedder refuses the text input client of xterm 4.0.0 because it names no view ("view ID is null"), which the first Windows run of the terminal integration test showed. Typed characters come from the key events; composition of an input method is not offered in the Windows terminal. macOS, Linux and the phones keep the text input client, so dead keys, input methods and the soft keyboard keep working. CI run of the apps job on Windows; packages/terminal_ui/test/desktop_keyboard_test.dart.