Files
udpscope/docs/superpowers/plans/2026-11-03-ui-events-dock.md

16 KiB

Events in the dock, the left pane deleted -- Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Step 4 of the badges/dock redesign (spec docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md): a compact Events section in the dock, the full events list and its actions on an events slide-over page, the left events pane / divider / header toggle deleted, and the Mallard manual, USER-GUIDE and remaining docs swept for the new layout.

Architecture: The dock gets a third block under the readouts (build_events_dock in src/ui_dock.cpp): one compact row per EventRecord (title, time, small marker; no thumbnails), a "Manage" button, one click replays. The existing pane widgets (events-pane.ui: head with Live, Compare, Accumulate, the multi-select list with thumbnails) stop being a GtkRevealer beside the plot and become the content of the events slide-over page; the list keeps its builder, thumbnail cache and selection logic. Both views are rebuilt from the same event_store->list() by the same refresh entry point.

Tech Stack: C++20, GTK4/libadwaita, the headless self-check probes (no unit tests).

Global Constraints

  • The dock is fixed at 280 px; dock event rows are ellipsized labels with fixed width-chars/max-width-chars; no thumbnails in the dock (layout must not depend on content).
  • No new overlay child; the page uses slideover_add_page(App&, id, title, widget); the page id is events.
  • "Disable, do not hide": the dock's Events block stays on screen, insensitive, when unavailable. Availability rule (deviation from the spec's "insensitive outside trigger mode", kept on purpose): the existing sync_events_availability condition (event_recording || replaying || events_cached) -- a replay or a group leaves trigger mode, and a block that greys itself under the click that started it is the trap invariants-trigger-and-pinning.md records. Document the deviation in README and docs/claude.
  • A handler that rebuilds destroys the widgets it was emitted from: use outlive_the_rebuild(widget). refresh_events_pane must keep nulling events_thumb before it removes children. update_group_buttons keeps its app.window == nullptr teardown guard.
  • model->signals() is read only under model->mutex().
  • The page content is the pane's events_box, never the GtkRevealer (an unrevealed revealer measures 0x0).
  • Selection survives marking: mark_events_on_screen updates titles in place and now also updates the dock rows' marker; it never rebuilds the page list.
  • Every new source file carries the SPDX MIT header (this plan adds none).
  • Baselines: clean build = 1 warning (icons -Woverlength-strings), incremental = 0, gtk warnings: 0, no Gtk-CRITICAL. Probes need a tour-seen config for slide-over page legs (see .superpowers/sdd/task-5-report.md, verification.md). Mocks: tools/mock_udpstreamer.py (8114), tools/mock_e1608.py (54211); run serially, kill them.
  • The session file has no events-pane field (events_pane_width is memory only); there is nothing to migrate. README's claim that the width is remembered across sessions is wrong and is removed.

Task 1: The Events block in the dock (the left pane still exists)

Adds the dock block so the app stays whole; nothing is removed.

Files:

  • Modify: src/ui_dock.cpp, src/ui_app.hpp, src/ui_events.cpp, src/ui.cpp (dock assembly, availability sync, tick), src/ui_probe.cpp, CMakeLists.txt (only if a file is added; none expected)

Interfaces:

  • Produces (declared in ui_app.hpp, defined in ui_dock.cpp):

    • GtkWidget* build_events_dock(App& app); -- vertical GtkBox: a row with the "Events" label and a "Manage" text GtkButton (calls slideover_open(app, "events"); a no-op until Task 2 registers the page), then a GtkListBox (app.dock_events_list, selection mode none, single-click activation) of compact rows, inside a GtkScrolledWindow with a fixed min-content-height/max-content-height (about 4 rows) and vscrollbar-policy automatic. Stores app.dock_events_box, app.dock_events_list, app.dock_events_manage.
    • void dock_rebuild_events(App& app); -- clears and refills dock_events_list from app.event_store->list() (newest first, as the pane does): each row = a small marker label (dock_event_marker, see below), the title (ellipsized, same text as event_row_title), the time as the pane shows it (ellipsized, dim). With no records: one dim "no events yet" row. Attaches the EventRecord to the row as "udpscope-event" (same key as the pane). Null guard: a no-op before build_events_dock ran or when app.event_store is null.
    • void dock_mark_events(App& app); -- updates each dock row's marker/title from the same data mark_events_on_screen uses (the event on screen and the group members), without rebuilding rows.
    • void dock_events_set_sensitive(App& app, bool available); -- the one place the block's sensitivity is set; sync_events_availability calls it with its own available value.
  • Row activation (single click): calls the existing replay_event(app, record) that on_event_row_activated (ui_events.cpp:961) calls, with the same argument it passes; do not duplicate the replay logic -- extract the shared body if on_event_row_activated has any.

  • Consumes: App::event_store, EventRecord, event_row_title() (ui_events.cpp:784), replay_event, mark_events_on_screen, slideover_open.

  • Step 1: Build the block. Write build_events_dock, dock_rebuild_events, dock_mark_events, dock_events_set_sensitive in ui_dock.cpp. Marker: a single-character label -- the on-screen event gets a filled marker, the group members another, others none (read how mark_events_on_screen (ui_events.cpp:796) decides the on-screen/group titles and reuse its predicate; do not invent a second one). CSS: check ui_widgets.cpp for existing .udpscope-event-* classes and dim-label before adding any.

  • Step 2: Wire it. Append build_events_dock to the dock after the readouts box (and a separator), in ui.cpp where build_readouts_dock is assembled. Call dock_rebuild_events(app) at the end of refresh_events_pane's non-empty and empty paths (every place it finishes) and from record_event unconditionally (today's record_event refreshes the pane only if the revealer is revealed; the dock must follow every latch), and dock_mark_events from mark_events_on_screen. Call dock_events_set_sensitive from sync_events_availability (ui_events.cpp:40-70) with its available result, and once after building (insensitive until available).

  • Step 3: Probe. In the existing events probe (events_probe_next, UDPSCOPE_PROBE_EVENTS_MS) print dock events: rows=<n> pane_rows=<n> same_titles=<0|1> sensitive=<0|1> dock_w=<n> after at least two latched events (the probe already produces them), where same_titles compares each dock title to the pane row's title label ("udpscope-event-title") in order, and again after a replay (on_screen marker present on exactly one dock row). Add one click on a dock row to the probe and print dock replay: clicked=<0|1> replaying=<0|1> (the dock row click starts a replay as the pane's activate does).

  • Step 4: Build; run the events probe (UDPS mock), the readouts probes (regression), and check dock_w <= 280, gtk warnings: 0.

  • Step 5: Commit (git add src/ui_dock.cpp src/ui_app.hpp src/ui_events.cpp src/ui.cpp src/ui_probe.cpp; message ui: compact events block in the dock).


Task 2: The events page; the left pane, divider and header toggle deleted

Files:

  • Modify: src/ui_events.cpp, src/ui.cpp, src/ui/main-window.ui, src/ui/header-bar.ui, src/ui/events-pane.ui, src/ui_app.hpp, src/ui_trigger.cpp (:597-601, :723-724), src/ui_layout.cpp (:380 comment), src/ui_widgets.cpp (:58-60 comment), src/ui_probe.cpp, src/ui_dock.cpp

Interfaces:

  • Consumes: Task 1's build_events_dock, dock_rebuild_events, dock_mark_events, dock_events_set_sensitive; slideover_add_page, slideover_open.

  • Produces: slide-over page events (title "Events") whose content is events_box of events-pane.ui; build_events_pane(App&) now returns that box (not a revealer); App::events_revealer, App::events_divider, App::events_pane_width, sync_events_divider, the header events_toggle and its handler are gone; events_cached/sync_events_availability keep driving the dock block's sensitivity (and the page's: set events_box insensitive with it).

  • Step 1: The page. events-pane.ui: drop the events_pane revealer so events_box is the root (rename nothing else); keep events_head, events_live, events_compare, events_accumulate, events_list. build_events_pane stops creating events_revealer. Register the page next to the cursors/measure registrations in ui.cpp: slideover_add_page(*app, "events", "Events", app->events_box) (add the events_box member if absent). record_event's refresh condition gtk_revealer_get_child_revealed becomes "the events page is open" (slideover_page(app) == "events"), because the dock refreshes independently; opening the page (the slide-over's open path or the Manage button) must refresh the list once so it is current -- put this in a slideover_set_on_open-style hook only if the slide-over already has one; otherwise refresh in the Manage button's handler and in refresh_events_pane callers that run while closed (cheap; the list is small) -- state in the report which you chose.

  • Step 2: Delete the pane. main-window.ui: remove events_divider; left_column becomes the side_view content directly (keep every id inside it); fix the header comment (lines ~6, 23, 29). ui.cpp: remove sync_events_divider, its tick call, the events_divider creation (~5201-5206), the null guard (~4998), the toggle handler (~4951-4959) and the toolbar-order probe string entry (~3010); header-bar.ui: remove events_toggle; keep the udpscope-events-symbolic icon (used by preferences.ui). ui_app.hpp: remove the members and declarations (events_divider, events_pane_width, events_revealer, the toggle pointer). sync_events_availability loses the toggle/revealer branches, keeps computing available, and drives dock_events_set_sensitive plus the page's sensitivity. Fix the comments at ui_trigger.cpp:597-601, :723-724, ui_layout.cpp:380, ui_widgets.cpp:58-60.

  • Step 3: Probes. Rewrite events_probe_next (ui_probe.cpp:5590-5860) to open the page with slideover_open(*a, "events") instead of clicking the toggle, then drive selection/Compare/Accumulate exactly as before on the page's list; delete the divider/revealer/pane_w reads; rewrite report_events_availability (ui.cpp:5712-5724) as [events] available=<0|1> dock_sensitive=<0|1> page_sensitive=<0|1> page=<id> and the events pane: dump (ui.cpp:6242-6255) as events pane: compare= accumulate= list= nested= (no revealer). Any layout dump or toolbar probe naming the toggle is updated. The plot's width must no longer change when events exist: add plot_w_stable=<0|1> comparing the plot area's allocated width before and after the first latched event (it must read 1; the old pane squeezed it).

  • Step 4: Run the events probe, the trigger probes, the layout/toolbar probes, the readouts probes, one E-1608 run; gtk warnings: 0, no Gtk-CRITICAL, in particular none from events_thumb, update_group_buttons at teardown, or the removed revealer. Re-baseline .superpowers/sdd/ui-signals-baseline-udps.txt only where this step explains a difference.

  • Step 5: Commit (git add the modified files by name; message ui: the events list is a slide-over page; the left pane, its divider and the header toggle are gone).


Task 3: Tour, context help, docs/claude, README

Files:

  • Modify: src/ui_tour.cpp, src/ui_context_help.cpp (:102-110), README.md (97, 841-850, 883, 1032, 1194, 3025-3027), docs/claude/{architecture,verification,probe-readings,invariants-gtk-and-chrome,invariants-trigger-and-pinning,invariants-plot-and-layout}.md

  • Step 1: Tour and help. The dock tour stop text mentions the Events block (what it lists, that one click replays, that Manage opens the list). The context-help hints events (anchored to &App::events_head, now on a page) and events_record follow how the cursors/measures hints were handled in step 3 (anchor on the page heading, shown while the page is open; reword "the events pane"). Run the help-figures and tour probes.

  • Step 2: docs/claude. architecture.md (chrome section: the dock now has readouts and events; page events; events no longer "reserved", ~line 105), invariants-gtk-and-chrome.md (paned/revealer traps at ~212-228 lose the events divider; teardown ~281 and events_thumb ~297, thumb cache ~309 now say "the page's list"), invariants-trigger-and-pinning.md (~441-453: the availability rule drives the dock block's sensitivity; the spec's "outside trigger mode" deviation and why), invariants-plot-and-layout.md (no events divider; the plot is never squeezed by events), verification.md (~724 and the events probe wording), probe-readings.md (dock events:, dock replay:, the new [events] and events pane: lines and what each must read; the old pane_w=240 rows and the [events] table near 1660-1675 rewritten).

  • Step 3: README. Layout/dock description, the header (no Events toggle), the events page, the single-click replay in the dock, deviations: availability rule instead of "outside trigger mode"; the dock list has no thumbnails (the page does); delete the false "remembers the width" claim; update the pending-removal note at ~3025-3027 (it is done).

  • Step 4: Run the tour and help-figure probes, one UDPS and one E-1608 run; gtk warnings: 0.

  • Step 5: Commit (code: ui: tour and help describe the events block; docs: docs: events in the dock and on its page).


Task 4: The Mallard manual and USER-GUIDE sweep for the whole redesign

Files:

  • Modify: help/C/org.udpscope.app/*.page (trigger.page 169-196 "Events pane on the left", the history button, "Drag the pane's edge"; overview.page incl. its layout description and any figure caption; signals.page, math.page, sessions.page, plus any page that mentions the side panel, the sidebar tabs ("Measure", "Signals" tab), the header's sidebar toggle, or the Events toggle), docs/USER-GUIDE.md, README.md leftovers

  • Step 1: Find every stale mention. grep -rniE "side panel|sidebar|side pane|tab switcher|\bMeasure tab|Signals tab|events pane|history button|Events toggle|drag.*(edge|divider)" help/C docs/USER-GUIDE.md README.md src/ui_*.cpp and read each hit in context; also check figures the manual embeds (help/C/org.udpscope.app/figures/ and how they are generated -- UDPSCOPE_* help-figures probe in verification.md). List every hit and its disposition in the report.

  • Step 2: Rewrite each page to the real UI: header bar (time window, Trigger, Pause, Cursors, Dock toggle, menu), the plot, the badge bar (signal badges, +, N signals, Trigger, Source), the slide-over pages (trigger, signals, signal:, cursors, measure, events, source), the dock (Readouts: cursors, measurements; Events). Read the code or docs/claude/architecture.md to be accurate; do not invent shortcuts (m toggles Measurements, g the signals page -- check src/shortcuts.cpp). Regenerate figures only if the repository already has a script that does so headlessly; otherwise say which figures remain stale.

  • Step 3: Validate the Mallard pages if yelp-check / xmllint is available (xmllint --noout help/C/org.udpscope.app/*.page); build with cmake --build build -j (the help may be compiled/installed by a target).

  • Step 4: Commit (docs: the manual and the user guide describe the badge bar, the dock and the slide-over pages).