Skip to content

fix(cli): keep sticky todo panel compact#3647

Merged
wenshao merged 6 commits into
QwenLM:mainfrom
shenyankm:sheny/fix-sticky-todo-compact
Apr 29, 2026
Merged

fix(cli): keep sticky todo panel compact#3647
wenshao merged 6 commits into
QwenLM:mainfrom
shenyankm:sheny/fix-sticky-todo-compact

Conversation

@shenyankm

Copy link
Copy Markdown
Contributor

Summary

  • What changed:

    • Keep the sticky Current tasks panel compact by limiting visible todo items based on terminal height.
    • Render sticky todo items as single-line truncated rows and show an overflow summary like ... and N more.
    • Hide the sticky panel while an inline TodoWrite result is pending, and also suppress it for very recent committed TodoWrite results that are still likely visible in the conversation.
  • Why it changed:

  • Reviewer focus:

    • Whether the sticky todo panel remains compact for long todo lists in small terminals.
    • Whether the sticky panel avoids duplicating an inline/recent TodoWrite result.
    • Whether the recent-history visibility guard is a reasonable approximation given that Ink <Static> does not expose a reliable per-item viewport API.

Validation

  • Commands run:

    cd packages/cli
    npx vitest run src/ui/utils/todoSnapshot.test.ts src/ui/components/StickyTodoList.test.tsx src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx
    npm run typecheck
    
    cd ../..
    npx prettier --check packages/cli/src/ui/components/StickyTodoList.tsx packages/cli/src/ui/components/StickyTodoList.test.tsx packages/cli/src/ui/layouts/DefaultAppLayout.tsx packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx packages/cli/src/ui/layouts/ScreenReaderAppLayout.tsx packages/cli/src/ui/layouts/ScreenReaderAppLayout.test.tsx packages/cli/src/ui/utils/todoSnapshot.ts packages/cli/src/ui/utils/todoSnapshot.test.ts
    git diff --check
    npm run build
    
  • Prompts / inputs used:

    • Reproduced locally in Windows PowerShell with a small terminal and a long todo list while the model was streaming / updating todos.
  • Expected result:

    • The sticky Current tasks panel should stay compact.
    • Long todo items should not wrap into large multi-line blocks.
    • The sticky panel should not duplicate an inline or very recent TodoWrite result.
    • Windows PowerShell should not visibly flicker from repeated bottom-area layout reflow.
  • Observed result:

    • Targeted tests passed: 4 test files, 20 tests passed.
    • npm run typecheck passed.
    • npx prettier --check ... passed.
    • git diff --check passed.
    • npm run build passed.
    • In the local Windows PowerShell comparison, the fixed terminal kept the sticky todo panel compact and avoided the flicker seen before the fix.
  • Quickest reviewer verification path:

    1. Open a small Windows PowerShell terminal.
    2. Trigger a TodoWrite result with many long todo items.
    3. Confirm the sticky Current tasks panel shows only a compact set of one-line rows.
    4. Confirm overflow is summarized as ... and N more.
    5. Confirm the sticky panel does not appear while the inline/recent TodoWrite output is still visible.
  • Evidence (output, logs, screenshots, video, JSON, before/after, etc.):

    • Side-by-side Windows PowerShell GIF: left is before the fix, right is after the fix.
    • The same comparison was posted in My eyes are blinded~~~~ #3638.

Scope / Risk

  • Main risk or tradeoff:

    • This does not implement exact pixel-level detection of whether a historical TodoWrite item is currently visible in the terminal viewport. Ink <Static> writes committed history to scrollback and does not expose a reliable per-item viewport API, so this PR uses a conservative recent-history guard instead.
  • Not covered / not validated:

    • Full interactive E2E automation across all terminal emulators.
    • macOS/Linux manual terminal verification.
  • Breaking changes / migration notes:

    • None.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️
Docker N/A N/A N/A
Podman N/A N/A N/A
Seatbelt N/A N/A N/A

Testing matrix notes:

  • Windows was validated with targeted npx vitest, npm run typecheck, root npm run build, and a manual Windows PowerShell before/after repro.
  • macOS/Linux terminal behavior was not manually validated because the reported flicker was reproduced in Windows PowerShell.
  • Docker/Podman/Seatbelt are not relevant to this CLI/TUI layout fix.

Linked Issues / Bugs

Fixes #3638.

Follow-up to #3507.
QQ20260426-213951

@yiliang114

Copy link
Copy Markdown
Collaborator

Thanks for taking this on, and thanks for the Windows PowerShell before/after repro. This is very helpful for confirming the actual flicker behavior, not just the unit-level behavior.

I reviewed #3647 locally. Overall, the direction looks good to me: keeping the sticky todo panel compact, avoiding duplicate inline/recent TodoWrite rendering, truncating long rows, and summarizing overflow items should address the main source of the bottom-area reflow from #3638.

A few small points I noticed that may be worth considering:

  • fix(cli): keep sticky todo panel compact #3647 already reduces the largest layout churn by making the sticky panel compact. One extra piece from fix(cli): stabilize sticky todo redraws #3646 that might still be useful is the AppContainer measurement stabilization: using a semantic layout key so status-only or array-identity changes do not unnecessarily trigger footer remeasurement.
  • The recent-history guard is a reasonable practical workaround given Ink <Static> limitations. It may be worth keeping an eye on whether suppressing sticky todos for the first couple of history items feels too conservative in normal use.
  • I also ran AppContainer.test.tsx locally as an extra check, since the flicker path involves footer height measurement.

Extra validation I ran on this PR:

  • cd packages/cli && npm run lint passed
  • cd packages/cli && npm run typecheck passed
  • cd packages/cli && npx vitest run src/ui/utils/todoSnapshot.test.ts src/ui/components/StickyTodoList.test.tsx src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx passed: 4 files, 20 tests
  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx passed: 50 tests
  • npm run build passed, with existing vscode-ide-companion warnings only
  • node scripts/check-build-status.js passed
  • git diff --check origin/main...HEAD passed

Happy to let this PR carry the fix path. Once you decide whether the optional AppContainer stabilization is worth carrying over, I can close #3646 to avoid duplicate work.

wenshao
wenshao previously approved these changes Apr 26, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@shenyankm

Copy link
Copy Markdown
Contributor Author

Thanks for taking this on, and thanks for the Windows PowerShell before/after repro. This is very helpful for confirming the actual flicker behavior, not just the unit-level behavior.

I reviewed #3647 locally. Overall, the direction looks good to me: keeping the sticky todo panel compact, avoiding duplicate inline/recent TodoWrite rendering, truncating long rows, and summarizing overflow items should address the main source of the bottom-area reflow from #3638.

A few small points I noticed that may be worth considering:

  • fix(cli): keep sticky todo panel compact #3647 already reduces the largest layout churn by making the sticky panel compact. One extra piece from fix(cli): stabilize sticky todo redraws #3646 that might still be useful is the AppContainer measurement stabilization: using a semantic layout key so status-only or array-identity changes do not unnecessarily trigger footer remeasurement.
  • The recent-history guard is a reasonable practical workaround given Ink <Static> limitations. It may be worth keeping an eye on whether suppressing sticky todos for the first couple of history items feels too conservative in normal use.
  • I also ran AppContainer.test.tsx locally as an extra check, since the flicker path involves footer height measurement.

Extra validation I ran on this PR:

  • cd packages/cli && npm run lint passed
  • cd packages/cli && npm run typecheck passed
  • cd packages/cli && npx vitest run src/ui/utils/todoSnapshot.test.ts src/ui/components/StickyTodoList.test.tsx src/ui/layouts/DefaultAppLayout.test.tsx src/ui/layouts/ScreenReaderAppLayout.test.tsx passed: 4 files, 20 tests
  • cd packages/cli && npx vitest run src/ui/AppContainer.test.tsx passed: 50 tests
  • npm run build passed, with existing vscode-ide-companion warnings only
  • node scripts/check-build-status.js passed
  • git diff --check origin/main...HEAD passed

Happy to let this PR carry the fix path. Once you decide whether the optional AppContainer stabilization is worth carrying over, I can close #3646 to avoid duplicate work.

Thanks for the detailed review and the extra Windows PowerShell validation.

I took your suggestion from #3646 and added a new commit: a184fd8c1 fix(cli): stabilize sticky todo redraws.

The update keeps the compact sticky todo behavior from this PR, and also incorporates the AppContainer stabilization idea:

  • sticky todo rendering now uses a semantic render key;
  • footer height measurement now depends on a layout key that excludes status-only changes;
  • setControlsHeight avoids redundant state updates;
  • StickyTodoList is memoized to reduce redraws from unrelated streaming updates;
  • added coverage for status-only sticky todo updates not triggering footer remeasurement.

I also checked the import paths after moving the max-visible-items helper into todoSnapshot, and the focused tests/typecheck/lint/build path passed locally.

wenshao
wenshao previously approved these changes Apr 26, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR is a clear superset of #3646: commit a184fd8c ("fix(cli): stabilize sticky todo redraws") matches yiliang114's stabilization commit verbatim — git diff pr-3646 pr-3647 -- AppContainer.tsx is empty. On top of that, this PR adds:

  • A recent-history visibility guard that hides the sticky panel for the first 2 history items after a TodoWrite commits. This directly addresses the gap I raised on #3646: an inline TodoWrite that has just committed but is still visible at the top of the viewport.
  • Defensive clampVisibleTodoCount / getStickyTodoMaxVisibleItems for non-finite inputs.
  • Deterministic width={contentColumnWidth} (vs flexGrow={1} in #3646) — avoids extra Ink measure passes that can re-trigger the flicker we're trying to fix.
  • height={1} (vs minHeight={1}) + explicit width + wrap="truncate-end" → guaranteed single-line rows, no edge-case wrap.
  • An AppContainer integration test asserting measureElement is not called for status-only updates. This is the test that actually proves the optimization works — unit-level layoutKey assertions don't.

I think the right call is to land this PR and close #3646. Leaving a note there.

A few items to address before merging:

Should fix

1. numberColumnWidth should be derived from visibleTodoCount, not todos.length.

const numberColumnWidth = String(todos.length).length + 2;

With todos.length = 10 and visibleTodoCount = 5, only 1.5. are ever rendered, so width 2 suffices, but the current code allocates 4 — wasting 2 columns of content space cosmetically. Suggested:

const numberColumnWidth = String(visibleTodoCount).length + 2;

2. Document the 6 in contentColumnWidth. Magic number that will trip future maintainers:

// 6 = 2 (status icon column) + 2 (border × 2) + 2 (paddingX × 2)
const contentColumnWidth = Math.max(1, width - numberColumnWidth - 6);

3. Cover the defensive paths with tests. The branches exist but aren't pinned by any test:

  • clampVisibleTodoCount(NaN) / clampVisibleTodoCount(Infinity)
  • getStickyTodoMaxVisibleItems(NaN) / getStickyTodoMaxVisibleItems(-1) / getStickyTodoMaxVisibleItems(0)

Without tests, a future refactor can silently regress these.

Worth discussing (non-blocking)

4. The recent-history guard is more aggressive than #3646. Combined with hide-while-pending, the sticky panel never appears for short turns (a TodoWrite followed by a 1-paragraph response). On long turns it eventually catches up.

A cleaner replacement is compact-sticky: instead of hiding while pending/recent, render a single fixed-height progress line (e.g. In progress: Run cli tests (3/7)):

  • Avoids the duplicate-render problem (compact ≠ inline full render).
  • Keeps the bottom anchor visible during exactly the moments users care about progress.
  • Eliminates the item-count heuristic entirely.

Concretely: getStickyTodos returns { mode: 'compact' | 'full', todos }; StickyTodoList renders a one-liner in compact; layout key is stable inside compact mode, so no extra measure churn.

Not blocking on this — landing the flicker fix first is right. Worth a follow-up issue.

5. Surface the heuristic's limitation in code, not just in the PR description. Important context for future maintainers:

// The 2-item threshold is item-count, not line-count. A single long
// gemini response can fill the viewport while still counting as one
// item, so the sticky panel may stay hidden longer than strictly
// necessary. This is acceptable — overshowing risks the duplicate-
// render bug we are fixing.
const MIN_HISTORY_ITEMS_AFTER_TODO_BEFORE_STICKY = 2;

@shenyankm

Copy link
Copy Markdown
Contributor Author

This PR is a clear superset of #3646: commit a184fd8c ("fix(cli): stabilize sticky todo redraws") matches yiliang114's stabilization commit verbatim — git diff pr-3646 pr-3647 -- AppContainer.tsx is empty. On top of that, this PR adds:

  • A recent-history visibility guard that hides the sticky panel for the first 2 history items after a TodoWrite commits. This directly addresses the gap I raised on fix(cli): stabilize sticky todo redraws #3646: an inline TodoWrite that has just committed but is still visible at the top of the viewport.
  • Defensive clampVisibleTodoCount / getStickyTodoMaxVisibleItems for non-finite inputs.
  • Deterministic width={contentColumnWidth} (vs flexGrow={1} in fix(cli): stabilize sticky todo redraws #3646) — avoids extra Ink measure passes that can re-trigger the flicker we're trying to fix.
  • height={1} (vs minHeight={1}) + explicit width + wrap="truncate-end" → guaranteed single-line rows, no edge-case wrap.
  • An AppContainer integration test asserting measureElement is not called for status-only updates. This is the test that actually proves the optimization works — unit-level layoutKey assertions don't.

I think the right call is to land this PR and close #3646. Leaving a note there.

A few items to address before merging:

Should fix

1. numberColumnWidth should be derived from visibleTodoCount, not todos.length.

const numberColumnWidth = String(todos.length).length + 2;

With todos.length = 10 and visibleTodoCount = 5, only 1.5. are ever rendered, so width 2 suffices, but the current code allocates 4 — wasting 2 columns of content space cosmetically. Suggested:

const numberColumnWidth = String(visibleTodoCount).length + 2;

2. Document the 6 in contentColumnWidth. Magic number that will trip future maintainers:

// 6 = 2 (status icon column) + 2 (border × 2) + 2 (paddingX × 2)
const contentColumnWidth = Math.max(1, width - numberColumnWidth - 6);

3. Cover the defensive paths with tests. The branches exist but aren't pinned by any test:

  • clampVisibleTodoCount(NaN) / clampVisibleTodoCount(Infinity)
  • getStickyTodoMaxVisibleItems(NaN) / getStickyTodoMaxVisibleItems(-1) / getStickyTodoMaxVisibleItems(0)

Without tests, a future refactor can silently regress these.

Worth discussing (non-blocking)

4. The recent-history guard is more aggressive than #3646. Combined with hide-while-pending, the sticky panel never appears for short turns (a TodoWrite followed by a 1-paragraph response). On long turns it eventually catches up.

A cleaner replacement is compact-sticky: instead of hiding while pending/recent, render a single fixed-height progress line (e.g. In progress: Run cli tests (3/7)):

  • Avoids the duplicate-render problem (compact ≠ inline full render).
  • Keeps the bottom anchor visible during exactly the moments users care about progress.
  • Eliminates the item-count heuristic entirely.

Concretely: getStickyTodos returns { mode: 'compact' | 'full', todos }; StickyTodoList renders a one-liner in compact; layout key is stable inside compact mode, so no extra measure churn.

Not blocking on this — landing the flicker fix first is right. Worth a follow-up issue.

5. Surface the heuristic's limitation in code, not just in the PR description. Important context for future maintainers:

// The 2-item threshold is item-count, not line-count. A single long
// gemini response can fill the viewport while still counting as one
// item, so the sticky panel may stay hidden longer than strictly
// necessary. This is acceptable — overshowing risks the duplicate-
// render bug we are fixing.
const MIN_HISTORY_ITEMS_AFTER_TODO_BEFORE_STICKY = 2;

Thanks for the detailed review. I agree with the should-fix items.

I will add a small follow-up commit to:

  • derive numberColumnWidth from visibleTodoCount;
  • document the contentColumnWidth fixed-width adjustment;
  • add tests for the defensive NaN / non-positive / infinite input paths;
  • add an inline note for the recent-history guard heuristic.

I also agree that compact-sticky is a cleaner possible follow-up, but I would prefer to keep this PR focused on landing the flicker fix first.

Comment thread packages/cli/src/ui/components/StickyTodoList.tsx Outdated
@shenyankm shenyankm requested a review from wenshao April 27, 2026 11:06
wenshao
wenshao previously approved these changes Apr 27, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

Comment thread packages/cli/src/ui/components/StickyTodoList.tsx
Comment thread packages/cli/src/ui/utils/todoSnapshot.ts
Comment thread packages/cli/src/ui/utils/todoSnapshot.ts

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@wenshao wenshao merged commit 9861114 into QwenLM:main Apr 29, 2026
24 of 25 checks passed
TaimoorSiddiquiOfficial pushed a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request Apr 29, 2026
@shenyankm shenyankm deleted the sheny/fix-sticky-todo-compact branch May 6, 2026 03:40
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
* fix(cli): keep sticky todo panel compact

* fix(cli): stabilize sticky todo redraws

* fix(cli): address sticky todo review feedback

* fix(cli): size sticky todo number column correctly

* fix(cli): address sticky todo review feedback
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.

My eyes are blinded~~~~

3 participants