Skip to content

feat(tui): your own Todos and Notes panes and the Pepper reply strip (P1b) - #167

Merged
allenhutchison merged 10 commits into
mainfrom
tui-own-todos-notes-p1b
Oct 29, 2026
Merged

allenhutchison merged 10 commits into
mainfrom
tui-own-todos-notes-p1b

Conversation

@allenhutchison

@allenhutchison allenhutchison commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Human overview

P1b of the TUI Todos/Notes design (planning/tui-room-todos-notes.md): top-level Todos and Notes rows above Pepper open your own read-only panes, your Todos list each room's open items in a section per room, and the Pepper composer stays under them with a reply strip showing the last two exchanges. Refs #166, #161, #157. No writes (P2, P3).

AI reviewer

Diff by subsystem

  • state.py: own_app/rule sidebar rows ahead of Pepper (key app:me:<slug> equals the pane key, so the tree marks it current); own_todo_sections, section rows in pane_rows, open_todo_count over rooms too; OwnApp widens JumpTarget.conversation; pane_subject, pane_placeholder, remember_exchange; DmTurn.subject/context and found_reply.
  • api_client.py: list_room_todos (RoomTodos, RoomTodosList), chat_send_stream(subject=, client_context=); turn.run_turn passes them only when set, so the CLI's other callers send the same body as before.
  • poller.py: fetch_own_todos (two reads; a failed or degraded rooms read returns rooms=None, a 401 still raises).
  • apps_pane.py: room headings, ReplyStrip (non-focusable, capped at 8 lines), strip_items, a Tab "Composer" binding shown only under own panes.
  • app.py: _open_own_pane/_show_pane, own-pane close returns to the DM, /todo /notes in the Pepper composer, Esc in the composer moves to the pane, turn subject/context, strip upkeep and a list re-read when a pane-sent turn ends (_turn_finished).
  • While an own pane is open current is the Pepper DM, so the existing DM composer, draft and turn queue are reused unchanged.

What the doc gets wrong about the code

  • GET /todos/rooms items are stored documents (item_to_json), not TodoEntry rows: no assignee/requested_by. The ask parties are rebuilt from source_ref (assignee_user_id/assignee_label, requester_id/requester_label, only when source == "request"), as asks._party does.
  • The snapshot types that route as an open object, so RoomTodos cannot be schema-checked; the # MIRROR: comment sits in the method and is allowlisted in tests/test_api_contract.py.
  • The turn path has no reply deltas, so "streamed answer" in the strip means the answer appears at the reply frame (thinking shows in the status line), as in the DM.

Departures

  • An ask no longer carries a "from request" source line (the ask line says who asked). This also changes P1a's room panes for real asks.
  • The sidebar cursor starts on the open conversation (Pepper) instead of the first row, which is now Todos.
  • A failed rooms re-read keeps the sections already shown and adds the one line.
  • SKILL.md is unchanged: it documents commands, not the TUI. README has the new keys.
  • The Pepper row's dot does not light for a reply that lands behind an own pane (the strip shows it).

Where to look hardest: open_conversation's same-conversation branch (an own pane sits over the DM, so closing must redraw the DM, not a topic); _turn_finished/_settle_turns for the strip's checking/gave-up cases; _got_own_todos.

Verification (after the last commit): uv run ruff format --check, uv run ruff check, uv run ty check src/pepper_cli/ clean; uv run pytest --cov: 2641 passed, 2 skipped, coverage 95.06 (floor 88). Snapshots: two existing updated for the new rows, two added (own Todos with strip, own Notes with a note open), stable on re-run.

  • /simplify: read inline, no changes (the four parallel agents were not launched).
  • code-review (medium): 8 findings, 4 fixed (rooms re-read wiping sections, non-object item, reply overwritten by history, jump cursor), 1 checked and not a defect (error reason does show in the DM status line under the pane), 3 left as is (sequential reads, extra sort in the count, cursor can rest on the rule row).

Checklist (repo-specific)

  • uv run ruff format --check and uv run ruff check clean (no new lint ignores)
  • uv run ty check src/pepper_cli/ clean
  • uv run pytest passes; coverage floor in .claude/maintainerd.json not lowered
  • Server-contract changes: /api/v1 usage stays additive-only, # MIRROR: comments updated, unknown fields and frames still tolerated
  • No token or credential is logged, printed, or put in a fixture
  • README updated; bundled SKILL.md N/A (no command or output change)

🤖 Generated with Claude Code

https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the unread-dot fix preserves unrelated unread replies.

Summary

Adds personal Todos and Notes panes above Pepper, with room sections in Todos and a reply strip above the shared composer.

  • Your Todos and Notes open as personal panes above Pepper.
  • Your Todos include open items grouped under each room.
  • Questions from your panes get answers in a Pepper reply strip.

Diagram

sequenceDiagram
    participant User
    participant Pane as Personal pane
    participant Pepper
    participant Tracker as DM unread tracker
    User->>Pane: Send a message
    Pane->>Pepper: Message with app or note context
    Pepper-->>Tracker: History may arrive before reply frame
    Pepper-->>Pane: Reply frame
    Pane->>Tracker: Mark only the displayed reply as seen
    Pane->>Pepper: Read the list again
Loading

Reviews (3) · Last reviewed commit: "fix(tui): a reply the strip shows is rea..." · Reviewed by Greptile

allenhutchison and others added 3 commits October 28, 2026 23:47
…(P1b)

Top-level Todos and Notes rows above Pepper (a rule, no dot, the marker while open) open your
own panes through P1a's AppPane. Your Todos list each room's open items in a section per room
(new PepperClient.list_room_todos) and merge every closed item into one fold; a failed rooms
read keeps your own items under one line. /todo and /notes in the Pepper DM open them, and
Ctrl+K lists them first.

The Pepper composer stays under your own panes and sends to the DM with subject app:todo or
app:notes (and client_context.note_id over an open note). A strip at the pane's foot holds the
current exchange and the one before it, and the list is read again when the turn ends. Tab goes
sidebar, pane, composer; Esc in the composer moves to the pane and never closes it.

Refs #166, #161, #157.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
…ts on Pepper

The strip kept saying "checking for the answer" after a dropped turn gave up until the next
poll. The sidebar's first draw rested on the first row (now Todos) instead of the open
conversation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
A failed rooms re-read keeps the sections on screen; a non-object item in /todos/rooms is
skipped; a history lookup that finds nothing no longer erases the reply the stream gave; a
Ctrl+K jump moves the sidebar cursor to the row it opened.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
Comment thread src/pepper_cli/tui/app.py Outdated
@vycari-review

vycari-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Vycari Review (shadow)

Shadow round 3 on f57c9db. This comment is non-gating and needs no response: it is collected to measure agreement with the current reviewer. The round's verdict is the Vycari Review (shadow) check run.

Open: P0 0 · P1 2 · P2 5 · confirmed fixed 0 · withdrawn 0

Judge's notes

Two P1 findings from the last round are still open at this head: opening your own Todos or Notes from a room still clears the Pepper unread dot, and after a dropped stream the reply strip can show an unrelated Pepper message as the answer. The only new finding is a P2 where the strip jumps to its bottom on every redraw.

P1 · VR-27f75f32 · Opening an own pane clears unseen Pepper replies

src/pepper_cli/tui/app.py lines 1900–1902 · symbol PepperApp._open_own_pane · lens cross-file-contract · status still-open

Entering Todos or Notes from a room calls open_conversation(PEPPER), which unconditionally clears dm.dot before the own pane replaces the DM. The updated polling logic preserves unread indicators for newly arriving hidden replies, but cannot restore this cleared indicator because the unread messages already exist in dm.entries.

Evidence: The new entry path calls open_conversation at app.py:1900-1902. That existing function clears dm.dot at app.py:1032-1034; _show_pane then hides the thread without retaining its exchanges. The follow-up fix at app.py:814-815 correctly treats an own pane as hiding the DM, but DmTracker.update at state.py:709-717 only raises the dot for newly discovered assistant IDs. Already cached unread replies therefore never restore it. This is an uncovered entry-order variant of the earlier unread-indicator finding.

Re-review (fix-incomplete): _on_dm now treats an own pane as hiding the DM and counts only the strip's replies as seen. But entering from a room still clears the dot through open_conversation(PEPPER), and that dot is never restored (C04).

Failure scenario

While viewing a room, receive a Pepper assistant message and wait for its unread dot. Open the top-level Todos or Notes row directly. The Pepper dot disappears although neither the message nor its exchange appears in the pane. Subsequent unchanged history polls leave the dot cleared, and next-unread navigation skips Pepper.

How this was verified (traced-callers)

Read open_conversation. _open_own_pane at app.py:1900-1902 calls it when the current conversation is not Pepper, and it clears the dot unconditionally. The own pane then covers the DM. Reachable from: Sidebar own_app row while viewing a room.

Proving test

Prime DM history, open a room, append an assistant message, and poll until the Pepper unread dot appears. Open the top-level Todos row and deliver another unchanged history read. Assert that the Pepper dot remains set until the user opens the Pepper conversation; this assertion fails on head.

P1 · VR-e0f9c2ac · Reply strip can show another assistant message as this turn's answer

src/pepper_cli/tui/state.py lines 936–948 · symbol DmTurn.found_reply · lens cross-file-contract · status still-open

When a turn's stream drops before the reply frame, assistant_id is None. reply_landed() then treats ANY assistant row not in the send-time snapshot as this turn's reply, and found_reply() returns the first such row in dict order. app._settle_turns() copies that text into turn.reply and marks the turn done, and app._turn_finished() then draws it in the own-pane reply strip (strip_items) directly under the user's question. The row may belong to a different message, such as a scheduled task or another client's Pepper message posted into the same DM during the checking window.

Evidence: Read app._turn_ended (app.py ~2274-2280): the else branch sets phase 'checking' with no assistant_id. app._settle_turns (app.py 2307-2331): checking + reply_landed -> turn.reply = turn.found_reply(entries) or turn.reply, phase 'done', then _turn_finished. state.DmTurn.reply_landed/found_reply (state.py 929-948): no assistant_id -> first assistant row not in snapshot, no text or time match. apps_pane.strip_items (apps_pane.py, new) renders turn.reply as the answer. The DM's own queue already had this ambiguity, but the strip is new and presents it as the answer to the user's own question. I did not find a guard that matches the reply to the sent text. Trigger needs a dropped stream and a second assistant row inside the window, so the harm is conditional, but the wrong answer is shown to the user as Pepper's reply.

Failure scenario

User opens Todos from the top-level row and types 'add buy stamps'. The stream drops before the reply frame, so the turn enters phase 'checking' with assistant_id None and a snapshot taken at send time. Meanwhile a proactive assistant message (for example a scheduled reminder) lands in the Pepper DM. On the next DM poll, reply_landed() is True, found_reply() returns the reminder's text, and the strip shows 'you: add buy stamps / Pepper: <reminder text>'. The real answer is never shown in the strip, and the turn is retired as answered.

How this was verified (traced-callers)

Same code as C01: state.py:936-948 and app.py:2323. Reachable from: own pane send with stream drop.

Proving test

Pilot test: open own Todos, send a turn with a scripted stream that closes without a reply frame, and add an assistant history row not in the turn's snapshot (text 'Reminder') before the answer row. Assert the reply strip does not show 'Reminder' as the answer to the sent text, or that the turn stays in 'checking' until its own assistant row (matched by id or text) appears. On PR head, the strip shows 'Reminder'.

P2 · VR-1d08ff2b · Reopening your own pane drops the in-flight exchange from its reply strip

src/pepper_cli/tui/app.py lines 1904–1919 · symbol PepperApp._show_pane · lens concurrency-time · status still-open

_show_pane builds a fresh AppPaneState for every open (via _open_own_pane, state.py AppPaneState(owner=None, slug=slug)), and its exchanges list defaults to empty. _send_turn records a turn in pane.exchanges (remember_exchange) only on the pane that was on screen at send time. Closing the pane (_hide_pane sets self.pane=None) discards that list. If the turn is still streaming or queued when the user reopens Todos or Notes, the new pane's strip never contains that turn, so the question and its 'waiting/…' state are not shown anywhere under the pane. When the reply lands, _on_dm sees self.pane is not None, so seen is empty and the Pepper dot lights for a reply the strip should have carried.

Evidence: app.py:1894-1902 (_open_own_pane builds AppPaneState(owner=None, slug=slug) with no exchanges), app.py:1904-1919 (_show_pane sets self.pane to that new object; no carry-over of exchanges from a previous pane), app.py:2178-2180 (_send_turn appends to pane.exchanges only when self.pane is non-None at send time), state.py:1147-1167 (exchanges default_factory=list; 'Dropped with the pane'), app.py:1928-1939 (_hide_pane sets self.pane=None), app.py:684-690 / _on_dm seen logic (seen computed only from pane.exchanges, so an empty list lights the dot). I checked for a lookup of the in-flight turn from self.turns during _render_strip: _render_strip reads only pane.exchanges, so the in-flight turn is not reached. The reply is still shown in the DM after the pane closes, which is why this is P2 rather than P1.

Failure scenario

1) User opens own Todos, types 'add buy stamps' and presses Enter; the turn starts streaming (strip shows 'you: add buy stamps / Pepper: …'). 2) User presses Esc (closes the pane, back to the DM). 3) User reopens Todos from the sidebar while Pepper is still working. The strip is empty: the question is not shown and nothing says a turn is pending. 4) The reply arrives: _turn_finished runs, _render_strip redraws an empty strip, and the Pepper row's dot lights although the reply was meant to be seen behind the pane.

How this was verified (traced-callers)

Read _open_own_pane/_show_pane/_hide_pane. Reopening after a close creates a fresh pane with default exchanges; _render_strip reads only pane.exchanges. Reachable from: Sidebar own_app row selected -> app.py:976 _open_own_pane -> _show_pane.

Proving test

Textual pilot test with a fake server whose /chat/send stream blocks before the reply frame. Open own Todos (app:me:todo), send 'add buy stamps' from the Pepper composer, press Esc, reopen Todos via its sidebar row, then assert the #reply-strip body contains 'add buy stamps' while the turn is still streaming. On PR head the assertion fails because the new AppPaneState has empty exchanges.

P2 · VR-23b3e976 · Unchanged pane refreshes interrupt reading long replies

src/pepper_cli/tui/apps_pane.py lines 153–155 · symbol AppPane.show_strip · lens concurrency-time · status new

Every strip redraw unconditionally scrolls to the bottom, including redraws caused by polling or list navigation when the exchanges have not changed. A user's scroll position is therefore discarded during ordinary pane updates.

Evidence: Successful todo reads call _render_pane at src/pepper_cli/tui/app.py:2025-2030, which always calls show_strip at app.py:1986. Selection changes take the same path at app.py:2101-2108. show_strip calls scroll_end regardless of whether reply content changed at apps_pane.py:153-155. The strip explicitly supports wheel scrolling at apps_pane.py:110-115, so its non-focusable status does not prevent this scenario.

Failure scenario

Receive a reply longer than the strip's eight-line cap, then scroll upward with the mouse wheel to read its beginning. The next 30-second pane poll redraws the unchanged strip and jumps to its bottom, moving the text being read off screen. Moving the todo selection also triggers this jump.

How this was verified (traced-callers)

The diff shows _render_pane always calling host.show_strip(...), and show_strip calling strip.scroll_end(animate=False) unconditionally. _got_todos reaches _render_pane on every successful poll. Reachable from: own Todos pane: 30s apps poll, or selection move, calls _render_pane.

Proving test

Open own Todos, complete a turn with a multiline reply exceeding the strip height, scroll the strip upward, and record scroll_y. Deliver an unchanged successful todo poll and wait for layout. Assert that scroll_y remains unchanged; the PR head instead scrolls to max_scroll_y.

P2 · VR-5e093875 · An ask may show twice on your own Todos: once in your list and again in the room's section

src/pepper_cli/tui/state.py lines 1235–1252 · symbol own_todo_sections · lens spec-adversary · status still-open

own_todo_sections concatenates your own list (GET /todos, all statuses, no mine filter) with each room's items (GET /todos/rooms) and never dedupes. The design notes (planning/cli-core-coverage.md §4.4, line 153) say an assigned room todo is stored as the assignee's own todo plus a display copy on the room's list. So an ask assigned to you would be drawn under your open items and again under its room's section, and its closed copy twice in the Closed fold. The design (planning/tui-room-todos-notes.md §6.3) says the pane should mirror todo.html, and nothing in the PR's text or tests mentions this overlap.

Evidence: Checked: own_todo_sections (state.py ~1235-1252) builds `everything` from `entries` plus every `room.items` with no id or dedup step. pane_rows keys open rows `todo:{e.id}` and does not dedupe across sources. fetch_own_todos (poller.py ~264-281) makes two independent reads with no merge. The design doc §6.3 and cli-core-coverage §4.4 line 153 describe the assignee's own todo plus a display copy, so whether the two ids are shared on the server is unverified: the server is not in the checkout. If the server gives the copy the same id, the duplicate is hidden by the existing 'todo:{id}' keys and this candidate falls away. Existing tests (test_tui_apps_state.py, test_tui_poller.py, test_tui_own_apps.py) use distinct ids and never exercise an ask in both places. Protections looked for: dedupe in own_todo_sections, fetch_own_todos, _got_own_todos, and the stored-doc copy in api_client._room_todo_from_dict; none found.

Failure scenario

You are assigned 'Book the dog sitter' in room 'Trip planning'. The server stores your own todo for it and a display copy on the room. You open Todos. Your own open list shows 'Book the dog sitter', and the 'Trip planning' section shows the same ask again. Marking one done leaves the other open, and both the own and room rows are keyed separately, so the user sees one ask as two items.

How this was verified (read-only-reasoning)

own_todo_sections does not dedupe. The server is not in the checkout and planning/cli-core-coverage.md:153 only describes the copy.

Proving test

Fake server: own_todos contains an ask item with id 'x' (source 'request'), and room_todos for ROOM_A contains a stored copy of the same ask with a different id but the same title and source_ref naming you as assignee. Open the own Todos pane and assert the title appears once in pane_rows (or in the drawn text). The test fails on the head because own_todo_sections does not dedupe.

P2 · VR-c8a64da4 · One malformed room item hides every room's todos behind a single error line

src/pepper_cli/api_client.py lines 1960–1975 · symbol ApiClient.list_room_todos · lens fail-open · status still-open

list_room_todos builds each item with _room_todo_from_dict, which calls _todo_from_dict and indexes row['id'], row['title'], row['status'], row['source'] and row['created_at'] with no default. One item missing any of these raises KeyError, which escapes the comprehension. fetch_own_todos catches KeyError and returns rooms=None, so every room's section disappears and the pane shows 'Couldn't load your rooms' todos.' even though most rooms read fine.

Evidence: api_client.py:1960-1975 comprehension has no per-item try; _todo_from_dict (api_client.py:3915-3939) uses row['id'], row['title'], row['status'], row['source'], row['created_at']. poller.py fetch_own_todos catches (httpx.RequestError, ValueError, KeyError, TypeError) and returns OwnTodos(todos, None). The user sees the explicit ROOMS_FAILED line, so the failure is not silent, but the other rooms' valid items are dropped. The per-item skip that exists for non-dict items is not applied to malformed dict items.

Failure scenario

The server returns a room listing in which one item lacks created_at (or has a null title). The user has five rooms with open todos. Their Todos pane shows only their own items and the line 'Couldn't load your rooms' todos.', with no room sections, and the 30s re-read repeats the same failure every time.

How this was verified (read-only-reasoning)

fetch_own_todos catches KeyError and returns rooms=None; the existing test asserts this for a malformed item. Reachable from: own Todos pane read.

Proving test

Fake /api/v1/todos/rooms with two rooms; in room A one item is {'id': 'x'} (or lacks created_at). Call poller.fetch_own_todos. Assert that rooms is not None and room B's items are present. On this head rooms is None, which matches the existing test's [{'id': 'x'}] case, so the test asserts the opposite of the desired behaviour.

P2 · VR-f6a91fd6 · Reply strip holds more than the two exchanges the design specifies once sends queue behind a running turn

src/pepper_cli/tui/state.py lines 1186–1200 · symbol strip_turns · lens spec-adversary · status still-open

Design §6.9 says the reply strip 'holds the exchange in flight and the one before it'. strip_turns instead keeps every turn from the one before the first unfinished turn, so while one turn streams and several more are queued, the strip grows beyond two exchanges. The PR body's 'Departures' list does not mention this change.

Evidence: planning/tui-room-todos-notes.md:352-354 (§6.9: 'It holds the exchange in flight and the one before it'). state.py strip_turns: 'Otherwise everything from the one before the first unfinished turn, so sends queued behind a running turn never push its answer out'. The pane-level cap is only the 8-line scroll in styles.tcss, not a turn bound. No decision in the pack sanctions the wider strip.

Failure scenario

Under own Todos, send Q1 (streaming), then Q2 and Q3 while Q1 is still running. The strip keeps Q1, Q2 and Q3 (the first unfinished turn is index 0, so the slice starts at 0). The spec says only Q1 and its predecessor, so the pane shows three exchanges, and the older ones scroll down the 8-line cap.

How this was verified (read-only-reasoning)

The strip_turns logic returns everything from the turn before the first unfinished one. That matches the candidate's description and is intentional. Reachable from: Send several messages from an own pane while a turn is running.

Proving test

State test: build AppPaneState(owner=None, slug='todo') with three DmTurn objects, the first phase 'streaming' and the other two 'queued'. Call remember_exchange for each. Assert len(pane.exchanges) == 2. It fails on the PR head, which returns all three.

allenhutchison and others added 6 commits October 29, 2026 00:22
The strip was drawn while a pane-sent turn was queued and again when it
ended, so it kept saying "waiting to send…" while Pepper answered, and
a dropped stream (phase checking) never reached it at all. The turn's
start and every _turn_finished now redraw the strip alone; the list
re-read still waits for the turn to be over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
The first redraws arrive faster than a refresh, and each queued its own cursor restore.
A restore for replaced nodes could run before the tree rebuilt its lines and leave the
cursor on the first row (Todos) rather than the open conversation, and a second early
redraw could then keep that default as if it were a choice. That made the two older
snapshot tests flaky (they were recorded with the cursor on Todos).

Now only the newest redraw's restore acts, it builds the lines before moving, and the
tree's default cursor is not kept until a restore has placed one. The snapshot tests wait
for the cursor before capturing; the two snapshots change only in which row is under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
Design §6.6 says unread dots still move while a pane is open, and §6.9 that a turn
already running when the pane opens does not appear in the strip. The DM poll treated
the DM as open whenever your own pane sat over it, so that turn's reply cleared the dot
and was never signalled. Now the DM counts as open only with no pane over it; the
replies the strip carries are passed as seen and don't light it; closing the pane back
to the DM clears it.

DmTurn.reply_id names a turn's assistant row (the frame's, or after a drop the new one)
and found_reply uses it.

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

The strip kept the last two submitted turns, so two sends queued behind a slow one
pushed it out before its answer arrived. Design §6.9 holds the exchange in flight and
the one before it: strip_turns keeps everything from the turn before the first
unfinished one, and narrows back to two as the queue drains (on each phase change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
After a dropped stream the turn may well have run, and its reply can still land in the
DM after the reconcile window; "(no answer)" said otherwise. The strip now reads "(no
reply yet; it may still show in the DM)" for that case and keeps "(no answer)" for an
error frame, which clears any status text the stream had shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm
The divider under Todos and Notes is a tree line, so the arrow keys could rest the
cursor on it, where Enter does nothing. The sidebar now steps past it either way.

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

allenhutchison commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Review round 1: response

Responding to Greptile review 1 (4/5 on 05a2fe0). Head is now 60158dc.

Greptile

  • P2 "Reply strip shows old status" (app.py _turn_finished): fixed in 6e3c2ea. The strip now redraws when the turn starts streaming and when it drops to checking. Tests added for both. Thread.

Other fixes this round (some from the non-gating Vycari Review shadow comment)

  • de2499f: the sidebar cursor is now placed once per redraw, after the tree's lines are built. Early redraws could leave the cursor on Todos rather than the open conversation, and that made test_room_info_card and the thread snapshot flaky (it reproduced on 05a2fe0). The two snapshots change only in which row the cursor is on.
  • af1dd95: the Pepper dot now keeps moving while your own pane hides the DM (design §6.6). Replies the strip carries count as seen, and returning to the DM clears the dot. This was VR-27f75f32.
  • 4f97785: sends queued behind a running turn no longer push its answer out of the strip (§6.9 "the exchange in flight and the one before"). This was VR-5db540c1.
  • db6712f: a dropped turn that never shows in history now reads "(no reply yet; it may still show in the DM)" rather than "(no answer)". This was VR-00ecfc6b.
  • 60158dc: the arrow keys step over the rule under Todos and Notes.

Left as is

  • The sequential reads of your own list and your rooms' lists, and the open_todo_count re-sort, are unchanged.
  • VR-5e093875: an ask shows both in your own list and in its room's section. This matches the web, which concatenates the two lists without deduping.
  • VR-c8a64da4: one malformed room item hides every room section. This matches the doc's failure posture of a single "Couldn't load your rooms' todos." line.
  • VR-e0f9c2ac: after a dropped stream, the reply is guessed as any new assistant row. That behavior predates this PR in the DM reconcile, and the strip now shows the guess. It is tracked in tui: a dropped DM turn takes any new assistant row as its reply #168 and not changed here.

Pre-flight: ruff format and check are clean, ty is clean, and pytest gave 2646 passed and 2 skipped with coverage at 95.09% (floor 88).

Comment thread src/pepper_cli/tui/app.py
…dot first

A DM read already in flight when a turn starts can come back holding the reply while the
turn still streams. Its id isn't known yet, so it lit the Pepper dot, and the reply frame
that then put it in the strip left the dot on. DmTracker now keeps the assistant rows
behind the dot; when the strip shows a turn's reply, that row is marked seen, and the dot
goes out only if nothing else lit it. A reply found after a dropped stream keeps its id
for the same purpose.

Also restores the definition line of
test_dm_tracker_drops_rows_that_leave_the_history_after_midnight, which af1dd95 dropped
by accident, folding that test's body into the one above it.

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

Copy link
Copy Markdown
Contributor Author

Review round 2: response

Responding to Greptile review 2 (5/5 on 60158dc). Head is now f57c9db.

Greptile

  • P2 "Read answers still look unread" (app.py _on_dm): this was real, and it is fixed in f57c9db. The dot now tracks the assistant rows that lit it. A reply the strip shows is marked seen, and the dot goes out only if nothing else lit it. The new test fails on 60158dc. Thread.
  • The same commit restores the def line of test_dm_tracker_drops_rows_that_leave_the_history_after_midnight. af1dd95 had dropped it by accident and folded the test's body into the test above it.

Pre-flight: ruff format and check are clean, ty is clean, and pytest gave 2650 passed and 2 skipped with coverage at 95.10% (floor 88).

@allenhutchison
allenhutchison added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit eebed01 Oct 29, 2026
5 checks passed
@allenhutchison
allenhutchison deleted the tui-own-todos-notes-p1b branch October 29, 2026 02:58
JayadityaGit pushed a commit to JayadityaGit/pepper-cli that referenced this pull request Oct 29, 2026
…todos/rooms shape, sidebar cursor (Vycari#169)

Claude-Session: https://claude.ai/code/session_01QNDeh8M8jKfHhtWGJwkxTm

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant