From 663f491a3c3a8c1de20f7309ae37819ceb1e17e9 Mon Sep 17 00:00:00 2001 From: Benjamin Favre Date: Wed, 30 Sep 2026 12:41:13 +0200 Subject: [PATCH 1/4] fix: repair terminal protocol state and native cockpit focus and layout --- crates/shelldeck-terminal/src/grid.rs | 13 ++- crates/shelldeck-terminal/src/parser.rs | 94 +++++++++++++++++- crates/shelldeck-ui/src/agent_console_view.rs | 6 +- crates/shelldeck-ui/src/fleet_view.rs | 32 +++--- crates/shelldeck-ui/src/workspace/mod.rs | 4 + crates/shelldeck-ui/src/workspace/modes.rs | 1 + .../src/workspace/request_views.rs | 18 +++- crates/shelldeck-ui/src/workspace/requests.rs | 13 ++- .../src/workspace/workspaces_render.rs | 1 + docs/design/reliability-test-tracker.md | 19 ++++ docs/testing/USE_CASES.md | 34 +++++++ .../screenshots/2026-09-30/agents-before.png | Bin 0 -> 81363 bytes .../2026-09-30/agents-dark-after.png | Bin 0 -> 79917 bytes .../2026-09-30/agents-light-after.png | Bin 0 -> 83625 bytes .../2026-09-30/local-script-result.png | Bin 0 -> 91225 bytes .../2026-09-30/platform-before.png | Bin 0 -> 68189 bytes .../2026-09-30/platform-dark-after.png | Bin 0 -> 70404 bytes .../2026-09-30/platform-light-after.png | Bin 0 -> 69545 bytes .../2026-09-30/platform-small-after.png | Bin 0 -> 59170 bytes .../screenshots/2026-09-30/request-after.png | Bin 0 -> 199553 bytes .../screenshots/2026-09-30/request-before.png | Bin 0 -> 51304 bytes .../2026-09-30/request-settings-after.png | Bin 0 -> 198511 bytes .../2026-09-30/request-small-after.png | Bin 0 -> 118496 bytes .../2026-09-30/workspaces-before.png | Bin 0 -> 72987 bytes .../2026-09-30/workspaces-dark-after.png | Bin 0 -> 73387 bytes .../2026-09-30/workspaces-light-after.png | Bin 0 -> 71971 bytes docs/testing/tests-terminal.md | 4 + docs/testing/tests-ui-and-app.md | 13 +++ docs/testing/visual-qa-2026-09-30.md | 65 ++++++++++++ 29 files changed, 296 insertions(+), 21 deletions(-) create mode 100644 docs/testing/screenshots/2026-09-30/agents-before.png create mode 100644 docs/testing/screenshots/2026-09-30/agents-dark-after.png create mode 100644 docs/testing/screenshots/2026-09-30/agents-light-after.png create mode 100644 docs/testing/screenshots/2026-09-30/local-script-result.png create mode 100644 docs/testing/screenshots/2026-09-30/platform-before.png create mode 100644 docs/testing/screenshots/2026-09-30/platform-dark-after.png create mode 100644 docs/testing/screenshots/2026-09-30/platform-light-after.png create mode 100644 docs/testing/screenshots/2026-09-30/platform-small-after.png create mode 100644 docs/testing/screenshots/2026-09-30/request-after.png create mode 100644 docs/testing/screenshots/2026-09-30/request-before.png create mode 100644 docs/testing/screenshots/2026-09-30/request-settings-after.png create mode 100644 docs/testing/screenshots/2026-09-30/request-small-after.png create mode 100644 docs/testing/screenshots/2026-09-30/workspaces-before.png create mode 100644 docs/testing/screenshots/2026-09-30/workspaces-dark-after.png create mode 100644 docs/testing/screenshots/2026-09-30/workspaces-light-after.png create mode 100644 docs/testing/visual-qa-2026-09-30.md diff --git a/crates/shelldeck-terminal/src/grid.rs b/crates/shelldeck-terminal/src/grid.rs index a93c64aa..fe03b57b 100644 --- a/crates/shelldeck-terminal/src/grid.rs +++ b/crates/shelldeck-terminal/src/grid.rs @@ -662,7 +662,11 @@ impl TerminalGrid { if width == 0 { let row = self.cursor.row; self.ensure_row(row); - let target_col = if self.cursor.col > 0 { + // At the right margin the cursor stays on the last written cell + // until a spacing character resolves the pending wrap. + let target_col = if self.pending_wrap { + self.cursor.col + } else if self.cursor.col > 0 { self.cursor.col - 1 } else { 0 @@ -1678,7 +1682,14 @@ impl TerminalGrid { let rows = self.rows; let cols = self.cols; let max_scrollback = self.max_scrollback; + // These belong to the session/host, rather than the resettable screen. + // Dropping the sender disconnects PTY replies; rewinding the generation + // makes existing command-completion observers miss subsequent events. + let response_tx = self.response_tx.take(); + let prompt_mark_sequence = self.prompt_mark_sequence; *self = Self::with_scrollback(rows, cols, max_scrollback); + self.response_tx = response_tx; + self.prompt_mark_sequence = prompt_mark_sequence; } /// Adjust the maximum number of scrollback lines retained, preserving the diff --git a/crates/shelldeck-terminal/src/parser.rs b/crates/shelldeck-terminal/src/parser.rs index 1cfdb1a5..df2a0014 100644 --- a/crates/shelldeck-terminal/src/parser.rs +++ b/crates/shelldeck-terminal/src/parser.rs @@ -309,7 +309,7 @@ fn parse_osc_color(spec: &str) -> Option<(u8, u8, u8)> { } } else if let Some(rest) = spec.strip_prefix('#') { // Format: #RRGGBB - if rest.len() == 6 { + if rest.len() == 6 && rest.bytes().all(|byte| byte.is_ascii_hexdigit()) { let r = u8::from_str_radix(&rest[0..2], 16).ok()?; let g = u8::from_str_radix(&rest[2..4], 16).ok()?; let b = u8::from_str_radix(&rest[4..6], 16).ok()?; @@ -1343,4 +1343,96 @@ mod tests { // Response is 1-indexed: row 3, col 3. assert_eq!(resp, b"\x1b[3;3R"); } + + // SDTEST-1926 + #[test] + fn malformed_unicode_palette_colors_preserve_state_and_parser_recovery() { + let grid = run(2, 10, b"\x1b]4;1;#123456\x07"); + let mut processor = TerminalProcessor::new(grid.clone()); + let mut parser = vte::Parser::new(); + for color in ["#a€bc", "#abc€", "#é1234", "#xyzxyz"] { + let input = format!("\x1b]4;1;{color}\x07"); + // PTY reads can split UTF-8 and escape sequences anywhere. + for chunk in input.as_bytes().chunks(3) { + processor.process_bytes(&mut parser, chunk); + } + assert_eq!(grid.lock().get_palette_color(1), Some((0x12, 0x34, 0x56))); + } + processor.process_bytes(&mut parser, b"\x1b]4;2;#abcdef\x07OK"); + let grid = grid.lock(); + assert_eq!(grid.get_palette_color(2), Some((0xab, 0xcd, 0xef))); + assert!(row_text(&grid, 0).starts_with("OK")); + } + + // SDTEST-1927 + #[test] + fn terminal_reset_keeps_pty_replies_connected() { + let grid = Arc::new(Mutex::new(TerminalGrid::new(3, 10))); + let (tx, rx) = std::sync::mpsc::channel(); + grid.lock().set_response_tx(tx); + let mut processor = TerminalProcessor::new(grid.clone()); + let mut parser = vte::Parser::new(); + processor.process_bytes(&mut parser, b"old\r\noutput\r\nlast\r\n\x1b[1m"); + assert!(grid.lock().scrollback_len() > 0); + processor.process_bytes(&mut parser, b"\x1bc"); + { + let grid = grid.lock(); + assert_eq!(grid.scrollback_len(), 0); + assert_eq!(row_text(&grid, 0), " "); + assert!(!grid.current_attrs.bold); + } + // Shells and TUIs still need device/status replies after `reset`. + processor.process_bytes(&mut parser, b"\x1b[2;4H\x1b[6n\x1b[c"); + assert_eq!(rx.try_recv().expect("CPR after RIS"), b"\x1b[2;4R"); + assert_eq!(rx.try_recv().expect("DA after RIS"), b"\x1b[?62;22c"); + } + + // SDTEST-1928 + #[test] + fn combining_characters_at_right_margin_attach_before_wrapping() { + for (cols, text, base_col) in [(4, "abce", 3), (4, "ab界", 2), (1, "e", 0)] { + let grid = run(2, cols, text.as_bytes()); + let mut processor = TerminalProcessor::new(grid.clone()); + let mut parser = vte::Parser::new(); + processor.process_bytes(&mut parser, "\u{0301}".as_bytes()); + { + let grid = grid.lock(); + assert_eq!(grid.cells[0][base_col].combining.as_slice(), &['\u{0301}']); + assert_eq!(grid.cursor.row, 0, "combining characters must not wrap"); + for (col, cell) in grid.cells[0].iter().enumerate() { + if col != base_col { + assert!(cell.combining.is_empty(), "accent attached to column {col}"); + } + } + } + processor.process_bytes(&mut parser, b"X"); + let grid = grid.lock(); + assert_eq!(grid.cells[1][0].c, 'X'); + assert!(grid.line_flags[1].soft_wrapped); + } + } + + // SDTEST-1929 + #[test] + fn command_completion_generation_stays_monotonic_across_reset() { + let grid = run(2, 10, b"\x1b]133;C\x07before\x1b]133;D;0\x07"); + let observed = grid.lock().prompt_mark_sequence; + let mut processor = TerminalProcessor::new(grid.clone()); + let mut parser = vte::Parser::new(); + processor.process_bytes(&mut parser, b"\x1bc"); + { + let grid = grid.lock(); + assert!(grid.prompt_mark.is_none()); + assert!(grid.command_output_start.is_none()); + } + processor.process_bytes(&mut parser, b"\x1b]133;C\x07after\x1b]133;D;7\x07"); + let grid = grid.lock(); + assert!( + grid.prompt_mark_sequence > observed, + "completion must reach existing observers" + ); + assert_eq!(grid.prompt_mark, Some(PromptMark::CommandFinished(Some(7)))); + assert!(grid.command_output(10, 100).contains("after")); + assert!(!grid.command_output(10, 100).contains("before")); + } } diff --git a/crates/shelldeck-ui/src/agent_console_view.rs b/crates/shelldeck-ui/src/agent_console_view.rs index f17c43d9..0e3f116f 100644 --- a/crates/shelldeck-ui/src/agent_console_view.rs +++ b/crates/shelldeck-ui/src/agent_console_view.rs @@ -1285,10 +1285,11 @@ impl AgentConsoleView { .border_color(ShellDeckColors::border()) .child(lucide_icon("search", 11.0, ShellDeckColors::text_muted())) .child( - div().flex_1().min_w(px(0.0)).child( + div().flex().flex_col().flex_1().min_w(px(0.0)).child( Input::new(&self.navigator_search) .variant(InputVariant::Bare) .size(InputSize::Sm) + .w_full() .placeholder(t!("fleet.sessions.search").to_string()) .on_change(move |_value, cx| { parent.update(cx, |_this, cx| cx.notify()); @@ -2489,7 +2490,8 @@ impl Render for AgentConsoleView { .flex() .size_full() .min_h(px(0.0)) - .bg(ShellDeckColors::bg_primary()); + .bg(ShellDeckColors::bg_primary()) + .text_color(ShellDeckColors::text_primary()); if wide { root = root.child(self.render_navigator(cx)); } diff --git a/crates/shelldeck-ui/src/fleet_view.rs b/crates/shelldeck-ui/src/fleet_view.rs index 05a49aeb..76f52208 100644 --- a/crates/shelldeck-ui/src/fleet_view.rs +++ b/crates/shelldeck-ui/src/fleet_view.rs @@ -3416,18 +3416,18 @@ impl FleetView { list } - fn render_session_search(&self, cx: &mut Context) -> impl IntoElement { - Input::new(&self.search_state) - .size(InputSize::Sm) + fn render_session_search(&self, compact: bool, cx: &mut Context) -> impl IntoElement { + let mut input = Input::new(&self.search_state).size(InputSize::Sm); + // The desktop session column is 340 px with 10 px padding on each + // side. A definite input width prevents its text viewport from + // resolving a percentage against an intrinsic/min-content width. + if !compact { + input = input.w(px(320.0)); + } + input .placeholder(t!("fleet.sessions.search").to_string()) .clearable(true) - .prefix( - svg() - .path("icons/lucide/search.svg") - .size(px(12.0)) - .flex_shrink_0() - .text_color(ShellDeckColors::text_muted()), - ) + .prefix(lucide_icon("search", 12.0, ShellDeckColors::text_muted())) .on_change({ let entity = cx.entity(); move |value, cx| { @@ -4060,11 +4060,15 @@ impl Render for FleetView { .flex_col() .child( div() + .flex() + .flex_col() + .w_full() + .min_w(px(0.0)) .px(px(10.0)) .py(px(8.0)) .border_b_1() .border_color(ShellDeckColors::border()) - .child(self.render_session_search(cx)), + .child(self.render_session_search(compact, cx)), ) .child(self.render_pane_tabs(cx)) .child(self.render_selected_pane(cx)) @@ -4115,11 +4119,15 @@ impl Render for FleetView { )) .child( div() + .flex() + .flex_col() + .w_full() + .min_w(px(0.0)) .px(px(10.0)) .py(px(8.0)) .border_b_1() .border_color(ShellDeckColors::border()) - .child(self.render_session_search(cx)), + .child(self.render_session_search(compact, cx)), ) .child(self.render_sessions(cx)), ) diff --git a/crates/shelldeck-ui/src/workspace/mod.rs b/crates/shelldeck-ui/src/workspace/mod.rs index 316d33f8..4f739244 100644 --- a/crates/shelldeck-ui/src/workspace/mod.rs +++ b/crates/shelldeck-ui/src/workspace/mod.rs @@ -736,6 +736,9 @@ pub struct Workspace { /// right-side sheet, toggled by the "Nouvelle demande" button in the list /// header. user_new_request_sheet_open: bool, + /// Claim title focus once the sheet is visible, including after a mode + /// transition. Further renders must preserve the user's chosen field. + user_new_request_needs_focus: bool, /// While `true` the composer sheet plays its slide-out/fade-out animation. /// Cleared (along with `..open`) by a delayed task the close handler spawns. user_new_request_sheet_dismissing: bool, @@ -1530,6 +1533,7 @@ impl Workspace { issue_thread_link_action: None, _issues_poll: None, user_new_request_sheet_open: false, + user_new_request_needs_focus: false, user_new_request_sheet_dismissing: false, confirm_new_request_discard: false, new_request_discard_focus: cx.focus_handle(), diff --git a/crates/shelldeck-ui/src/workspace/modes.rs b/crates/shelldeck-ui/src/workspace/modes.rs index ef81a6e0..0b181b7c 100644 --- a/crates/shelldeck-ui/src/workspace/modes.rs +++ b/crates/shelldeck-ui/src/workspace/modes.rs @@ -252,6 +252,7 @@ impl Workspace { self.issue_selected = None; self.issue_detail = None; self.user_new_request_sheet_open = false; + self.user_new_request_needs_focus = false; self.user_new_request_sheet_dismissing = false; self.confirm_new_request_discard = false; self.new_request_discard_return_focus = None; diff --git a/crates/shelldeck-ui/src/workspace/request_views.rs b/crates/shelldeck-ui/src/workspace/request_views.rs index e92cc3fe..2b617225 100644 --- a/crates/shelldeck-ui/src/workspace/request_views.rs +++ b/crates/shelldeck-ui/src/workspace/request_views.rs @@ -967,11 +967,21 @@ impl Workspace { } pub(super) fn render_user_new_request_sheet( - &self, + &mut self, is_maximized: bool, - window: &Window, + window: &mut Window, cx: &mut Context, ) -> impl IntoElement { + if self.user_new_request_needs_focus + && self.mode_transition.is_none() + && !self.confirm_new_request_discard + { + self.user_new_request_needs_focus = false; + self.issue_title_state + .read(cx) + .focus_handle(cx) + .focus(window); + } let viewport_width = window.viewport_size().width.to_f64() as f32; let (panel_width, compact) = user_request_sheet_layout(viewport_width, self.ui_font_size); // One chip, not four. Four badges plus a 186px site select overflowed @@ -1721,9 +1731,9 @@ impl Workspace { ) .variant(ButtonVariant::Destructive) .icon(IconSource::from("trash-2")) - .on_click(move |_, _, cx| { + .on_click(move |_, window, cx| { discard_entity.update(cx, |this, cx| { - this.discard_new_request_draft(cx); + this.discard_new_request_draft(window, cx); }); }), ), diff --git a/crates/shelldeck-ui/src/workspace/requests.rs b/crates/shelldeck-ui/src/workspace/requests.rs index 4aca6489..861918d6 100644 --- a/crates/shelldeck-ui/src/workspace/requests.rs +++ b/crates/shelldeck-ui/src/workspace/requests.rs @@ -53,7 +53,9 @@ impl Workspace { } self.issue_new_source = "user"; self.reset_new_request_site_to_active(cx); + self.settings_open = false; self.user_new_request_sheet_open = true; + self.user_new_request_needs_focus = true; self.sync_issues_poll(cx); cx.notify(); } @@ -84,6 +86,8 @@ impl Workspace { self.set_mode(AppMode::User, cx); } self.user_new_request_sheet_open = true; + self.settings_open = false; + self.user_new_request_needs_focus = true; self.sync_issues_poll(cx); cx.notify(); } @@ -427,6 +431,7 @@ impl Workspace { cx.notify(); return; } + self.focus_handle.focus(window); self.dismiss_new_request_sheet(cx); } @@ -442,9 +447,14 @@ impl Workspace { cx.notify(); } - pub(super) fn discard_new_request_draft(&mut self, cx: &mut Context) { + pub(super) fn discard_new_request_draft( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { self.confirm_new_request_discard = false; self.new_request_discard_return_focus = None; + self.focus_handle.focus(window); self.dismiss_new_request_sheet(cx); } @@ -473,6 +483,7 @@ impl Workspace { return; } self.user_new_request_sheet_dismissing = true; + self.user_new_request_needs_focus = false; self.issue_attachment_generation = self.issue_attachment_generation.wrapping_add(1); self.issue_ai_request_id = self.issue_ai_request_id.wrapping_add(1); self.issue_ai_expanded = false; diff --git a/crates/shelldeck-ui/src/workspace/workspaces_render.rs b/crates/shelldeck-ui/src/workspace/workspaces_render.rs index 85695d35..b122933a 100644 --- a/crates/shelldeck-ui/src/workspace/workspaces_render.rs +++ b/crates/shelldeck-ui/src/workspace/workspaces_render.rs @@ -1043,6 +1043,7 @@ impl Render for WorkspaceHubView { .flex_col() .size_full() .bg(ShellDeckColors::bg_primary()) + .text_color(ShellDeckColors::text_primary()) .child(self.render_header(cx)) .child( div() diff --git a/docs/design/reliability-test-tracker.md b/docs/design/reliability-test-tracker.md index a736d31d..246b2397 100644 --- a/docs/design/reliability-test-tracker.md +++ b/docs/design/reliability-test-tracker.md @@ -30,6 +30,25 @@ risques par frontière observable et impose une vérification avant correction. ## Journal +- **2026-09-30 — Passe visuelle native, REL-003/006.** Sur Xvfb isolé, + parcours User/Support/Dev, terminaux et script locaux, formulaires, réglages, + navigateur local, cockpits et palette. Corrections reproduites : titre de + demande sans focus, focus conservé sur un contrôle masqué après fermeture, + recherche Platform/Agents réduite au contenu intrinsèque, texte hérité noir + sur les surfaces sombres Workspaces/Agents. Les parcours corrigés sont + revalidés en Light/Tokyo Night et fenêtres 1200 × 850 / 820 × 600. + [Preuves et limites](../testing/visual-qa-2026-09-30.md) ; SDTEST-1930..1932 + restent rouges pour l'automatisation GPUI, même si la passe manuelle réussit. + +- **2026-09-30 — REL-010, régressions reproduites puis corrigées.** + SDTEST-1926..1929 échouaient avant correction : une couleur OSC 4 Unicode + provoquait un panic de découpage UTF-8, RIS déconnectait les réponses PTY et + remettait à zéro la génération des fins de commande, et un accent reçu à la + marge droite s'attachait au caractère précédent. Les tests traversent le + véritable parseur VTE et vérifient la reprise, les réponses après reset, + l'observation de la commande suivante et le wrap différé. Les autres lacunes + protocolaires de REL-010 restent à auditer. + - **2026-08-22 — REL-005 terminé.** ShellDeck ne contient plus d'autorité d'exécution ni de compatibilité runtime. Le cockpit natif utilise le contrat partagé et le transport HTTPS authentifié. diff --git a/docs/testing/USE_CASES.md b/docs/testing/USE_CASES.md index b0236fe8..b22713a3 100644 --- a/docs/testing/USE_CASES.md +++ b/docs/testing/USE_CASES.md @@ -25,6 +25,8 @@ Writing printable bytes into the parser produces glyphs in the grid at the expected cell, advances the cursor left-to-right, and wraps at the right edge when auto-wrap is on. Combining characters attach to the previous cell; wide characters occupy two cells. +At the right margin, a combining character attaches to the last written +glyph without resolving the pending wrap; the next spacing character wraps. ### SDUC-002 — Control chars behave per VT100 @@ -120,6 +122,8 @@ CSI `?25h/l` toggles cursor visibility, observable via a public `ESC c` (RIS) clears the grid, resets attributes, clears scrollback, homes the cursor. Soft reset does the subset per VT220. +Full reset preserves the live session's PTY response channel and monotonic +command-completion generation while clearing its prompt markers and output. ### SDUC-019 — Cursor position report @@ -130,6 +134,8 @@ channel when one is wired. Truncated or invalid escape sequences are dropped without panicking and the parser recovers on the next valid byte. +Malformed OSC palette values, including fragmented non-ASCII color strings, +leave existing palette entries intact. ### SDUC-021 — URL & path detection in scrollback @@ -870,6 +876,10 @@ at a short cadence; background runtime refreshes never erase an in-flight chat. ### SDUC-475 — Coding agents run on an explicit local or SSH target +The session search has a usable text viewport, and inherited navigator/provider +labels follow the active theme in both light and dark appearances +(SDTEST-1931/1932, manual native validation). + Dev mode exposes one provider-neutral agent console for Claude Code, Codex, Automonique ACP, DeepSeek through Jcode, and Jcode's configured/default provider. Every run names a target-valid working directory (a native absolute path locally, @@ -1024,6 +1034,10 @@ Retired 2026-08-22. ShellDeck no longer registers as an execution runtime. ### SDUC-476 — Fleet observation and control use the shared platform boundary +The session search displays its placeholder and query in both the fixed desktop +column and compact full-width view, preserving text across resize and theme +changes (SDTEST-1931, manual native validation). + ShellDeck is a presentation client. It may read scoped fleet state and request typed server-side control, but it never opens a provider process, claims a job, or owns an execution lease locally. The first load is a snapshot; subsequent @@ -1097,6 +1111,11 @@ Workspace polls issues every 15s while User or Support is visible. ### SDUC-228 — User "Mes demandes" view +The New Request title receives focus once the sheet is visible, including after +a mode transition or opening from Settings. Later renders preserve the chosen +field. Clean close and explicit discard return focus to the workspace so +keyboard shortcuts continue working (SDTEST-1930, manual native validation). + `render_user_requests` shows the caller's own issues with expand-to-comment and create composer. The composer exposes a searchable site picker backed by the signed-in account's Manage directory, defaults to the active site when @@ -1913,6 +1932,8 @@ under every autonomy policy. A full plan may advance step by step only after a deduplicated `OSC 133;D` completion event; missing shell integration is framed by ShellDeck, non-zero exit stops the sequence, output is bounded, and Ctrl+C remains the stop path. +The completion generation stays monotonic across a terminal reset, so an +already attached observer can still detect the next command's completion. ### SDUC-432 — Requests and Support tickets accept image evidence through every desktop path @@ -2821,6 +2842,9 @@ host or filesystem authority. ### SDUC-489 — Manual and external-task intake share one workspace lifecycle +Workspace onboarding inherits the active primary text color so checkout labels +remain readable in dark and light themes (SDTEST-1932, manual native validation). + Manual creation and issue, pull-request, or task-prefilled creation enter the same validated launcher and produce the same resumable local workspace record. Local UUIDs are never treated as Platform identities: a durable project, @@ -3123,6 +3147,16 @@ an unavailable adapter refuses before any effect. ## Change log +- **2026-09-30**: Amended SDUC-228/475/476/489 after native X11 visual QA: + request-sheet opening/closing retains keyboard focus, cockpit searches keep a + visible text viewport, and onboarding/navigator labels inherit theme text. + SDTEST-1930..1932 record the remaining automated GPUI gaps; dated screenshots + and verified journeys are in `visual-qa-2026-09-30.md`. +- **2026-09-30**: Amended SDUC-001/018/020/431 with SDTEST-1926 through + SDTEST-1929: malformed Unicode palette colors are ignored safely, accents + remain attached at the right margin, and full reset retains PTY replies and + command-completion generations without retaining old output. + - **2026-09-10**: Amended SDUC-414 with SDTEST-1925: assistant answers are laid out at the thread's definite content width, so Markdown tables no longer collapse to one glyph per column, and compact user bubbles are sized from diff --git a/docs/testing/screenshots/2026-09-30/agents-before.png b/docs/testing/screenshots/2026-09-30/agents-before.png new file mode 100644 index 0000000000000000000000000000000000000000..ac50fcb000405bb4bda893f68226b4ae5509d93c GIT binary patch literal 81363 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