Skip to content

Integrate keymap/native hints with accepted sidebar, tab and reader UI - #1305

Merged
trissim merged 15 commits into
mainfrom
work/toad-keymap--P888--toad
Oct 28, 2026
Merged

trissim merged 15 commits into
mainfrom
work/toad-keymap--P888--toad

Conversation

@trissim

@trissim trissim commented Oct 28, 2026 •

Copy link
Copy Markdown

Summary in numbers

15 commits above fresh main, including one ordinary two-parent integration merge; 30 files, +621/-160 (git diff --stat origin/main...HEAD). Two accepted source heads retained, one shared import conflict resolved, zero new tests or runtime operations by this executor. Same P888 / same substantive #1305 / same branch: no replacement PR or allocation.

Frozen combined head: 6ec2e88fe58716779d9bee94d3ee12ebc6d891ae.
Fresh registered main: 5fa29f3770a040af4686ede11f8584c6646d55c0 (fetched before combination and again before commit; ancestor retained).

Accepted source Exact source head Combined relationship
#1305, keymap/native context activation 87ac98cd6f09b9e5691d55cf720b8971504792a5 First parent, retained ancestor
#1303, sidebar/tab/reader UI d12963a9d35bd4a55c1d7d18148c05fa795ec9be Second parent, retained ancestor

Both source SHAs were fetched independently through their real pull refs and compared to FETCH_HEAD. Forge readback before combination showed both OPEN/draft/unarmed. #1303's exact-head agent-comms/review was SUCCESS. #1305's exact-head review history records source SUCCESS at 17:24:37Z, then root's REVIEWING/PENDING at 20:37:00Z for this continuation. Those old approvals do not approve the combined head. At source delivery this PR remained draft/unarmed. Root has since authorized promotion of this exact combined head in message 35183; source #1303 remains draft/unarmed and is not separately queued.

Integration: one conflict, no discarded member

The sole conflict was src/toad/widgets/comms_sidebar.py's adjacent activation/tone imports. Resolution:

from textual._activation import ContextActivation, PrimaryActivation

Do not restore ui_theme.scheme.AlternateRowToneColour there: #1303 moved tone ownership into AlternatingTone and declared row families. No whole-file ours/theirs selection, cherry-pick, squash, synthetic click, compatibility alias, second action/tone implementation or new target registry.

The merged sidebar differs from accepted #1303 only by #1305's existing four context-activation hunks: the import, the capability parent, context_activation(offset), and physical button-3 routing through that method. All other member files are automatically merged. Relative to #1303, the complete changed-file list is exactly #1305's original five files. #1302, #1304 and all other unreviewed source are excluded. Settings-file changes present here are #1303's existing ToadNode migration, not #1304's forms/preferences work.

comms_sidebar.py:30: two capabilities, one import. Git supplied the argument; neither parent was sent to an orphanage.

1. F1 catalog: acquired once, reused by the screen

Before: KeymapRow.defaults/effective rebuilt a one-mode catalog while F1 displayed an all-mode catalog. Selecting native Left in an unbound mode could lose the displayed invocation and crash strict lookup.

After: KeymapScreen captures one KeymapCatalog and uses it for declarations, filtering, table display, selected_rows and picked. Rows retain their identity and binding ID. Deleted: seven lines of competing row-owned catalog reconstruction. Strict indexing remains; empty cells display a dash, are noneditable and select no row. Remap, conflict, reset, save and cursor policies are retained. Model-owned one-mode conflict acquisition remains because its scope is different.

Prior source census: 621 source/test modules, zero parse omissions. Root's existing caller migration 030ff1ada24add510cd34a8391319f10c28a91b2 is preserved, assertions included; this executor did not edit tests.

2. Left/context hints: native activation owns the effect

Before: CommsRow's physical right-click called its menu directly; its native capability exposed only primary opening.

After: CommsRow composes PrimaryActivation and native ContextActivation. Primary remains open_selected. Its one context_activation(offset: Offset) calls show_menu(self.navigation_tree, offset). Physical button 3 stops once, invokes that same method and returns. Existing navigation/action scopes and menu lifecycle remain owners; no duplicate selected row or popup adapter.

Native Textual #204 has already landed on its main at 965e920bd2202c5c5be97a78039e49d41455a9d4 (root's recorded integration). This carrier neither vendors Textual nor pins a feature revision. Native f/Ctrl+F intents share HintHost acquisition, the published paint walk and HintSession. OptionList owns actual Option identity, membership/enabled/cell revalidation and selection; style handlers own their single click dispatch. The primary remap identity remains HintHost.action_show_hints; the contextual method has its own identity. Insert/editor consumption and terminal Ctrl+F priority are unchanged.

Native companion metadata remains unchanged and open: P883 owns recording Textual #204 through the corrected Core operation. No live P888 link or claim was fabricated or altered here.

3. Sidebar tones: list placement, declared row capabilities

Before: CommsRow held an alternate reactive, watcher and conditional tone. Channel headers had no separate channel hue pair.

After: #1303's AlternatingTone owns parity and repaint. ListedRow(CommsRow, AlternatingTone) declares the neutral pair; ChannelRow and ChannelHeader(AlternatingTone, SidebarHeader) declare the channel hue pair. ChannelGroup.header_type and header annotation remain; order_groups places the headers. ThreadStatusRow(ListedRow, ToadNode) and ThreadRow's existing-tab opening behavior remain. Deleted: CommsRow's alternate reactive, watcher, tone branch and displaced cast/getter copies. Channel hue colours derive from the existing theme colour family, not copied RGB values.

4. Tabs and switch toast: geometry and the existing clock

Before: the underline was viewport-wide while overflowing tab cells extended beyond it. A selected tab could lie outside its underline. Refreshing harder cannot repair the wrong geometry.

After: #1303's TabCells contains both titles and underline and spans all cells or the viewport, whichever is wider. The underline marks label plus close-button cells and slides on the existing animation clock. Opening/moving labels use the existing NEW_TEXT effect. Deleted: the whole-track NEW_TEXT flash on each switch.

SwitchToast, mounted once by WorkspaceChrome, observes SessionSelected and briefly paints the chosen name without taking focus or pointer input. LargeText extends Textual's existing glyphs only where missing; the name uses ordinary text when it will not fit. Rapid switching restarts the existing effect. No second animation timer was added.

5. Arriving messages: reader position owns the destination

Before: following a new message taller than the viewport landed at its tail.

After: #1303's ArrivalPosition is a ReaderPosition member. Tail following hands over to the first shown arriving message; existing kept positions and explicit End retain their behavior. A short message follows its tail; a tall one places its head using the existing geometry owner and stops following. ARRIVAL_REVEAL and NEW_TEXT run on the same existing clock; bursts that nobody sees do not start a reveal. Clock ticks without new arrivals skip the scan of loaded fragments. Remaining known cost: visible arrival pages repaint at the existing effect cadence. This source behavior is not a measured 16ms result or a proven history-hang repair.

6. ToadApp ownership and retained root caller migrations

#1303 replaces 16 casts and 13 per-class app getters with the existing ToadNode, not another hosted mixin. All of those automatically merged changes remain.

Root's d12963a9d35bd4a55c1d7d18148c05fa795ec9be updates two existing controls: channel-scrolling assertions crop the actual message area rather than sidebar ink; layout restoration waits for the transient toast to retire. Its source Debt ratchet was SUCCESS; its hosted broad suite was FAILURE. Root owns the selected-control qualification/results; this executor has not independently run or upgraded those results. Both test migrations are retained byte-for-byte from d129.

Verification: source integration, not application acceptance

Performed for this combined carrier:

  • Actual origin/allocation, clean owned checkout and full local head inspected.
  • Both exact forge source states, review history and pull-ref SHAs read before combination.
  • Fresh-main and both exact-source ancestor checks returned success.
  • Complete merged diff read, including automatically merged callers; only the actual import conflict was edited.
  • Explicit-refspec normal push completed; full local, ls-remote and forge heads agree.

Combined qualification and promotion authorization

For exact head 6ec2e88fe58716779d9bee94d3ee12ebc6d891ae, root's message 35183 reports 7 combined controls passed in 30.12 seconds and root source OK, and explicitly authorizes marking this existing carrier ready and requesting normal auto-merge. Those controls were root-run, not repeated by this executor.

Forge readback independently confirms Debt ratchet SUCCESS, workflow 40765099329, job 123086999217. The broad suite job 123086998913 was IN_PROGRESS at promotion readback. Its previous failures are not waived or declared baseline. The same readback still showed agent-comms/review PENDING; root's reported OK is not substituted for the actual required GitHub status. That existing status remains the promotion gate; normal auto-merge must honor it, with no bypass.

No combined tests, imports/compilation, UI, installed acceptance or performance run by this executor. No pytest, checker, ratchet, new tests/fixtures, package resolution/install, provider/native input, replay, service or additional agent operation. Root's seven-control result and the hosted Debt success do not establish installed-window behavior or performance.

Historical source results are not combined results:

Root owns one combined consumer qualification, whole-head verdict, normal merge-queue arming and subsequent paired installation. Real saved-session acceptance comes after installation, never as a circular pre-merge hold: native left/context operations on thread/channel rows and inside actual menus; disabled/clipped/removed targets; current-modal scope; Escape/focus; remaps and Insert typing; link open/copy; F1 empty/bound Left selection; sidebar tones, existing-tab return, underline overflow, reader arrival and toast behavior. No claim that P885's 16ms target, history hang or settings issue is solved.

Assumed: normal GitHub auto-merge honors the existing required agent-comms/review status; root owns the remaining broad-suite disposition, preserves source #1303 until its accepted work lands, then performs one stack installation and real-window acceptance. Promotion is explicitly authorized by root's message 35183, not an inference from old member approvals. P883 still owns the missing native companion metadata. P888 remains open until its actual application acceptance.

trissim and others added 13 commits October 28, 2026 11:12
Thread rows already alternate between the sidebar tone and one step off it.
Channel headers now alternate on their own pair, stepped from the sidebar
tone toward the theme's primary hue, so the channel list and its threads read
as two families without separator rows.

One mechanism owns alternation: AlternatingTone (tone_paint.py) holds the
row's parity and its declared tone pair. Thread rows keep the neutral pair;
ChannelHeader declares the hue pair. The reactive flag and the tone branch on
CommsRow are deleted. The hue tones are derived colours declared beside the
other Textual-backend colours; native ANSI palettes resolve them to the
background like every derived tone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
Root cause of the desync: the underline was a 1fr child of the scrolling tab
strip, so it was exactly one viewport wide while the cells it marks are as
wide as all tabs. Once tabs overflowed, any selected tab past the first
viewport width of cells had a span beyond the underline's last column and was
drawn with no underline at all (probe: strip 100 wide, cells 206, selected
span 154-167, underline 0-100).

The cells and the underline now share one parent, TabCells, which is as wide
as the cells or the strip, whichever is wider; the underline spans exactly the
cells' columns by layout. The marked span is the tab's whole cell (label and
close button), not its text.

Motion: when the marked cells move (another tab chosen, a tab opened, closed,
renamed or reordered) the underline slides from what it drew to the new
cells with TAB_SLIDE on the app clock, one row repainted per frame. Tabs that
open or move rise into place with the existing NEW_TEXT fade. The whole-track
NEW_TEXT fade that flashed the full underline on every switch is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
… out

Choosing a tab (or a sidebar thread or channel that opens one) puts its name
in a framed toast centred over the workspace. Its frame, surface and name
rise from the background, hold, and fall back on the app clock (Swell:
0.12 s rise, 0.5 s hold, 0.35 s fall, 30 fps), so the reader sees where they
landed without hunting for the underline.

It never takes focus or pointer input (can_focus off, hit_test_transparent),
a new choice while it shows replaces the name and restarts the swell, and it
leaves the layout when the swell ends. With animation off the clock reports
the swell finished as soon as it starts, and the toast hides at once.

EffectPhase now declares whether an effect is in motion, so a widget reads
that from the phase family instead of comparing phase objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
…in view

Before: an append at the followed end kept the window on the tail, so a
message taller than the window appeared as one block already scrolled to its
last row, and the reader scrolled up to find who sent it.

Now the first message of an arriving burst is the reader's position for the
layout that adds it (ArrivalPosition, a member of the ReaderPosition family
beside FragmentPosition and BlockPosition). Its page is a geometry target, so
that same layout places it: a message that fits follows the tail exactly as
before; one taller than the window lands with its first row at the window's
top and following stops there, the same rule message_head_hidden already
applies to a clicked tall message. Reader movement during the change wins.

Each burst also uncovers its rows from each message's first row down, one
window height per sweep (ARRIVAL_REVEAL, NEW_TEXT's duration, 60 fps, on the
app clock), alongside the existing rise-from-background fade. Arrivals inside
a running burst join it rather than restarting it; once the sweep ends every
row is uncovered, and with animation off nothing is ever covered. The effect
phase family now starts the reveal where it already decided whether arrivals
begin a new burst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
The fact "this node's app is a ToadApp" was restated 16 times as
cast("ToadApp", self.app) (12 in widgets/side_bar.py) and 13 more times as a
per-class `app = getters.app(ToadApp)` descriptor, each with its own runtime
import of toad.app. ToadHosted (widget_view.py, beside WorkspaceHosted)
declares it once for readers and type checkers; the classes that read Toad's
app inherit it and every cast and per-class descriptor is deleted, along with
the imports they alone needed. Nothing changes at run time: `app` is the
node's own.

NRA flagged the casts as Toad's largest restated-identity finding
(nra-ranked-toad-formbridge.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
…ith its reveal

Review of 4c46986 found three defects, all fixed here:

- A tall arrival snapped back to the tail. place_reader stores placed(), whose
  default calls moved(window, 0); inside the open change that is shifted(0),
  which returned TailPosition, so the next placement re-anchored the window.
  ArrivalPosition.placed now keeps what it placed: the message head as a
  FragmentPosition, or the tail. shifted() keeps the arrival; the reader
  leaving the tail replaces it through left_tail like any kept position.
- An arrival hidden by the transcript filter resolved to the previous drawn
  message and could pull the reader up to it. hold_arrival takes the first
  arrival the page shows, and only for a shown window.
- Only following hands the reader to an arrival: ReaderPosition.arrived keeps
  any kept position, TailPosition answers ArrivalPosition, and LatestPosition
  (End pressed, awaiting its source) keeps itself.
- Covered rows never paint NEW_TEXT, which was what started it, so a burst
  could restart its reveal on every arrival while unfocused and keep rows
  blank. arrivals_began starts the fade with the reveal, so the burst is
  decided by a running phase.

Cost: the reveal runs at NEW_TEXT's 30 fps, and a clock repaint with no new
arrivals no longer scans every loaded fragment of every page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
…creen

Second review pass risks, both closed:

- ArrivalPosition no longer turns into a FragmentPosition after its first
  placement. A FragmentPosition finds its page only once that page has been
  published, so a later layout in the same change could move the rows above
  the head with nothing placing it. The arrival keeps itself (shifted returns
  it) until its change ends, so every layout of the change places it from its
  own page; the inherited placed() then reads where the reader is.
- A burst that begins while the history is off screen starts no fade or
  reveal: queued, they would start at the first paint and cover rows the
  reader already saw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
The toast drew the name at body size, which is not the glance-readable name
the toast exists for. The name is now drawn three rows tall (LargeText) when
it fits the screen with the toast's margins, and at normal size otherwise.

Textual owns a 3x3 box-drawing font only for digits, A-F and a few signs
(textual/renderables/digits.py: DIGITS, DIGITS3X3); Rich and the installed
packages have no letter font. LargeText reads Textual's glyphs for those
characters and declares only the letters and signs Textual lacks, in the same
light style; lower case draws as upper case, anything else at normal size on
the middle row.

The right-edge offset in the earlier screenshots was the rasterizer, not the
toast: every row of the toast strip is exactly the box width with the border
in its first and last cells (probe: widths {69}, right column ╮│││││╯), and
the SVG places the right border at the corner's column. rsvg-convert ignores
the SVG's textLength and falls back to a font whose box glyphs advance
narrower; Chromium renders the same SVG aligned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
… ratchet debt

The debt ratchet on a0fe52b failed on debt this branch introduced:

- ToadHosted duplicated ToadNode (app_node.py), which already declares
  `app: ToadApp` and which DeclaredWidgetActions, SessionScope and others use.
  Every class now inherits ToadNode and ToadHosted is deleted; ToadNode sits
  last in each base list so the MRO stays consistent. This also clears the
  mypy errors the typing-only property caused.
- AlternatingTone declared None in its tone pair. Rows now name both tones:
  ListedRow (a row its list orders) alternates on SidebarToneColour and
  AlternateRowToneColour; ChannelRow, the channel rows the welcome screen
  lists, alternates on the channel hue pair; the channel group header keeps
  ChannelHeader. A channel's title row inside its header is no longer an
  alternating row at all, so it never paints over the header's tone.
  SidebarGroup's rows are bound to ListedRow, the family that can be placed.
- ThreadRow.open_selected spelled absence as None twice through a mode_name
  property; it now asks whether the open view's mode has a navigation entry
  whose session is tracked, which is what session_details answered.
- selected_span and the toast's tab lookup no longer test for None.
- The toast is a plain widget (CSS position/overlay, styles for its size), not
  a WidgetView, and reads the app through ToadNode.
- effect_frame is generic per effect, so a history reads its Amount reveal and
  its Mix fade through one method.
- New quoted annotations in history_anchor.py are unquoted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
The deleted-interface guard (tests/guards/test_deleted_interface_mechanisms.py)
seals the retired cursor family's `follow` name; the underline method that
slides to the selected cells now says what it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
@trissim trissim changed the title Keep F1 selection on its screen-owned all-mode catalog Keep F1 catalog scope and share native contextual sidebar activation Oct 28, 2026
@trissim trissim changed the title Keep F1 catalog scope and share native contextual sidebar activation Integrate keymap/native hints with accepted sidebar, tab and reader UI Oct 28, 2026
@trissim
trissim marked this pull request as ready for review October 28, 2026 20:45
@trissim
trissim enabled auto-merge October 28, 2026 20:45
Copilot AI balanced review requested due to automatic review settings October 28, 2026 20:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@trissim
trissim merged commit 61fddea into main Oct 28, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants