Skip to content

fix(tui): comprehensive TUI improvements and bug fixes - #4

Merged
junhoyeo merged 24 commits into
mainfrom
junhoyeo/fix-tables
Dec 13, 2025
Merged

fix(tui): comprehensive TUI improvements and bug fixes#4
junhoyeo merged 24 commits into
mainfrom
junhoyeo/fix-tables

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Dec 13, 2025

Copy link
Copy Markdown
Owner

Summary

Major TUI improvements including visual enhancements, bug fixes, and new features.

New Features

  • Copy to Clipboard (y key): Copy selected row data (vim-style keybind)
  • Export Feedback: Status messages for copy/export operations (shown in footer for 2 seconds)
  • Stacked Bar Chart: Overview bars show usage breakdown by model color

Visual Improvements

  • Responsive Model Column: Expands based on terminal width instead of clipping
  • Sort Indicators: ▲/▼ arrows on active sort column
  • Subtle Zebra Striping: Dark gray (#232328) alternating rows for readability
  • Cleaner UI: Removed ugly borders from Models/Daily tabs, removed duplicate labels

Bug Fixes

  • Fixed Overview Tab Selection: Blue highlight now properly follows arrow key navigation
    • Root cause: Keyboard handler only updated scrollOffset, never selectedIndex
    • Selection check scrollOffset + i === selectedIndex always failed
  • Fixed Row Selection Reactivity: Blue highlight updates when navigating with ↑↓
    • Root cause: <text> elements use bg prop, not backgroundColor
    • Used createMemo inside <For> loop for proper Solid.js reactivity

Code Quality

  • Removed unused ModelListItem component (inlined due to Solid.js reactivity)
  • Dead code removal, edge case handling, performance optimizations
  • Added keyboard modifier types to opentui.d.ts

Technical Details

  • OpenTUI's <text> uses bg for background color, <box> uses backgroundColor
  • Solid.js <For> requires createMemo inside callback for external signal reactivity
  • Component boundaries can break reactivity when passing accessor props

Files Changed

  • App.tsx - Keyboard handling, copy/export with status messages
  • ModelView.tsx - Responsive columns, striping, sort cues, selection fix
  • DailyView.tsx - Striping, selection fix
  • OverviewView.tsx - Inline rendering for selection reactivity
  • BarChart.tsx - Stacked bars with model colors
  • Footer.tsx - Status message display, updated hints
  • StatsView.tsx - Remove duplicate label
  • index.ts - Removed unused ModelListItem export

@junhoyeo junhoyeo changed the title fix(tui): improve model table width [DONT MERGE] fix(tui): improve model table width Dec 13, 2025
@junhoyeo junhoyeo changed the title [DONT MERGE] fix(tui): improve model table width fix(tui): improve model table readability Dec 13, 2025
@junhoyeo junhoyeo changed the title fix(tui): improve model table readability fix(tui): stacked bar charts and model table improvements Dec 13, 2025
@junhoyeo junhoyeo changed the title fix(tui): stacked bar charts and model table improvements fix(tui): stacked bar charts, model table, and audit fixes Dec 13, 2025
- Remove dead code (redundant header functions in ModelView)
- Fix hex colors to terminal-compatible named colors
- Pre-sort bar chart models for performance (O(n) vs O(n*m))
- Handle empty models edge case in bar chart
- Remove misleading 'Shift+Drag to copy' hint
- Remove unnecessary accessor wrapper
- Clean up trailing/double blank lines
@junhoyeo
junhoyeo force-pushed the junhoyeo/fix-tables branch from 05a8ed5 to 9f91635 Compare December 13, 2025 03:39
- Add status message feedback for copy (y) and export (e) operations
- Show success/error messages in footer for 2 seconds
- Add zebra striping to DailyView for consistency with ModelView
- Add sort indicators (▲/▼) to DailyView column headers
- Add 'e export' hint to footer
Pass selectedIndex and scrollOffset as Accessor<number> instead of
number to child components. This ensures Solid.js properly tracks
changes and updates the blue selection highlight when navigating
with arrow keys.
<Index> in Solid.js tracks by index (number) rather than by reference,
which properly reacts to selectedIndex changes without requiring the
array items to change.
Following OpenCode's pattern - use createMemo inside the For callback
to properly track selectedIndex changes. This ensures the comparison
happens within a reactive context that Solid.js can track.
OpenTUI's <text> element uses 'bg' for background color, not
'backgroundColor'. This fixes the row selection highlight not
appearing when navigating with arrow keys.
- Change ModelListItem.isSelected to Accessor<boolean> for reactivity
- Pass accessor from OverviewView instead of computed boolean value
- Add bg prop to text elements for proper background rendering
Solid.js component boundaries break reactivity when passing accessor
props. Inline the rendering logic with createMemo inside the For loop,
same pattern that works in ModelView and DailyView.
The bug: In Overview tab, up/down keys only changed scrollOffset,
never selectedIndex. So the selection check always failed.

Fix:
- OverviewView: Check i() === selectedIndex() (within visible list)
- App.tsx: Properly update selectedIndex for Overview tab, and only
  scroll when selection reaches edge of visible area
Use #232328 (very dark gray) for alternating rows instead of
brightBlack which was too visible. Only odd rows get the stripe.
@junhoyeo junhoyeo changed the title fix(tui): stacked bar charts, model table, and audit fixes fix(tui): comprehensive TUI improvements and bug fixes Dec 13, 2025
@junhoyeo
junhoyeo force-pushed the junhoyeo/fix-tables branch from ca95169 to 5190223 Compare December 13, 2025 04:56
Add patterns to ignore:
- *.log and ffi_otui_debug_*.log (debug logs)
- opencodetmp/ directory (temporary files)
- *.zip (binary archives)
Remove files that should be in a separate PR:
- FRONTEND_THEMING_RESEARCH.md
- RESEARCH_SUMMARY.md
- THEMING_QUICK_REFERENCE.md
- THEMING_RESEARCH_INDEX.md

This PR should focus only on TUI improvements.
@junhoyeo
junhoyeo force-pushed the junhoyeo/fix-tables branch from 5190223 to efb43d6 Compare December 13, 2025 04:58
The component was inlined in OverviewView due to Solid.js
component boundary reactivity issues, making this file dead code.
@junhoyeo
junhoyeo merged commit ea740a0 into main Dec 13, 2025
2 checks passed
@junhoyeo
junhoyeo deleted the junhoyeo/fix-tables branch December 13, 2025 05:07
junhoyeo added a commit that referenced this pull request Jun 22, 2026
…737)

* fix(antigravity-cli): date each turn by its per-generation timestamp

parse_antigravity_cli_file stamped every gen_metadata row with the single
session-created time from trajectory_metadata_blob.#2, and the module
doc-comment claimed the per-turn proto carries only relative timings. It
doesn't: each row carries an absolute {seconds, nanos} Timestamp at
chatModel.#9.#4, the same wire shape session_created_ms already decodes.

Without this, turns written into a conversation older than the live-tail
window are stamped in the past and dropped from the live trace, and long
conversations mis-bucket across midnight. Read chatModel.#9.#4 per row
(via a shared proto_timestamp_ms) and fall back to the session-created
time only when the field is absent or zero.

Added per_generation_timestamp_overrides_session_fallback, which fails
before the change.

* fix(antigravity-cli): guard proto_timestamp_ms against i64 overflow

A malformed protobuf can carry a Timestamp whose `seconds` varint is large
enough that `seconds * 1000` overflows i64. With overflow-checks on (debug
builds), the unchecked multiply panics instead of degrading to None, which
breaks the module's malformed-data contract (every other decode path returns
None on bad input). Switch to checked_mul/checked_add and return None on
overflow.

Regression test feeds a seconds varint of i64::MAX and asserts None without a
panic, checks the in-range boundary (i64::MAX/1000) still decodes, and keeps a
normal seconds+nanos stamp decoding to ms.

* fix(antigravity-cli): range-validate Timestamp nanos in proto_timestamp_ms

Reject `nanos` outside the protobuf Timestamp spec (0..=999_999_999),
including values large enough to be negative once cast to i64. An
out-of-range nanos now marks the whole stamp malformed (None) so the
caller's `ms > 0` filter and session-timestamp fallback take over instead
of producing a skewed per-turn time. Valid-input behavior is unchanged.

Confidence: high
Scope-risk: narrow

---------

Co-authored-by: Junho Yeo <i@junho.io>
pinion05 added a commit to pinion05/tokscale that referenced this pull request Jun 23, 2026
…unhoyeo#737)

* fix(antigravity-cli): date each turn by its per-generation timestamp

parse_antigravity_cli_file stamped every gen_metadata row with the single
session-created time from trajectory_metadata_blob.junhoyeo#2, and the module
doc-comment claimed the per-turn proto carries only relative timings. It
doesn't: each row carries an absolute {seconds, nanos} Timestamp at
chatModel.junhoyeo#9.junhoyeo#4, the same wire shape session_created_ms already decodes.

Without this, turns written into a conversation older than the live-tail
window are stamped in the past and dropped from the live trace, and long
conversations mis-bucket across midnight. Read chatModel.junhoyeo#9.junhoyeo#4 per row
(via a shared proto_timestamp_ms) and fall back to the session-created
time only when the field is absent or zero.

Added per_generation_timestamp_overrides_session_fallback, which fails
before the change.

* fix(antigravity-cli): guard proto_timestamp_ms against i64 overflow

A malformed protobuf can carry a Timestamp whose `seconds` varint is large
enough that `seconds * 1000` overflows i64. With overflow-checks on (debug
builds), the unchecked multiply panics instead of degrading to None, which
breaks the module's malformed-data contract (every other decode path returns
None on bad input). Switch to checked_mul/checked_add and return None on
overflow.

Regression test feeds a seconds varint of i64::MAX and asserts None without a
panic, checks the in-range boundary (i64::MAX/1000) still decodes, and keeps a
normal seconds+nanos stamp decoding to ms.

* fix(antigravity-cli): range-validate Timestamp nanos in proto_timestamp_ms

Reject `nanos` outside the protobuf Timestamp spec (0..=999_999_999),
including values large enough to be negative once cast to i64. An
out-of-range nanos now marks the whole stamp malformed (None) so the
caller's `ms > 0` filter and session-timestamp fallback take over instead
of producing a skewed per-turn time. Valid-input behavior is unchanged.

Confidence: high
Scope-risk: narrow

---------

Co-authored-by: Junho Yeo <i@junho.io>
t1000040 referenced this pull request in tmobi-internal/tokscale Jun 30, 2026
…unhoyeo#737)

* fix(antigravity-cli): date each turn by its per-generation timestamp

parse_antigravity_cli_file stamped every gen_metadata row with the single
session-created time from trajectory_metadata_blob.#2, and the module
doc-comment claimed the per-turn proto carries only relative timings. It
doesn't: each row carries an absolute {seconds, nanos} Timestamp at
chatModel.#9.#4, the same wire shape session_created_ms already decodes.

Without this, turns written into a conversation older than the live-tail
window are stamped in the past and dropped from the live trace, and long
conversations mis-bucket across midnight. Read chatModel.#9.#4 per row
(via a shared proto_timestamp_ms) and fall back to the session-created
time only when the field is absent or zero.

Added per_generation_timestamp_overrides_session_fallback, which fails
before the change.

* fix(antigravity-cli): guard proto_timestamp_ms against i64 overflow

A malformed protobuf can carry a Timestamp whose `seconds` varint is large
enough that `seconds * 1000` overflows i64. With overflow-checks on (debug
builds), the unchecked multiply panics instead of degrading to None, which
breaks the module's malformed-data contract (every other decode path returns
None on bad input). Switch to checked_mul/checked_add and return None on
overflow.

Regression test feeds a seconds varint of i64::MAX and asserts None without a
panic, checks the in-range boundary (i64::MAX/1000) still decodes, and keeps a
normal seconds+nanos stamp decoding to ms.

* fix(antigravity-cli): range-validate Timestamp nanos in proto_timestamp_ms

Reject `nanos` outside the protobuf Timestamp spec (0..=999_999_999),
including values large enough to be negative once cast to i64. An
out-of-range nanos now marks the whole stamp malformed (None) so the
caller's `ms > 0` filter and session-timestamp fallback take over instead
of producing a skewed per-turn time. Valid-input behavior is unchanged.

Confidence: high
Scope-risk: narrow

---------

Co-authored-by: Junho Yeo <i@junho.io>
leecoder pushed a commit to leecoder/tokscale that referenced this pull request Jul 7, 2026
…-2026-06-v2

feat: update AI metric reports 2026-06
junhoyeo added a commit that referenced this pull request Aug 25, 2026
…sion window

The agy 1.1.18 `chatModel.#9.#10` payload is 8 bytes whose encoding was
never confirmed against a real database, so every candidate reading of it
is a guess that has to earn acceptance. The only gate on those guesses was
an absolute "is this a believable date" window running from 2020-01-01 to
five years out. That is not a meaningful test for a raw integer: read as a
nanosecond count the window alone covers ~2% of the u64 range, so trying
both byte orders leaves an arbitrary payload — an id, a hash, a duration —
a few percent chance of passing as a date. A false accept silently buckets
a turn into the wrong day and feeds the server-side monotonic ratchet,
which has no correction path, making it strictly worse than the known-wrong
session-start dating it replaces.

Require every inferred reading to land inside the containing session's own
lifetime as well: at or after the session-created stamp less one hour, and
at or before now plus one hour. A turn cannot predate its conversation nor
happen after we read the file, and that pair of bounds is hours or days
wide instead of a decade. When there is no positive anchor to corroborate
against, decline inference entirely and let the caller fall back as before.

The explicit `#9.#4` Timestamp is untouched: it is a confirmed
representation read off real pre-1.1.18 databases, keeps its `ms > 0`
filter, takes no session bound, and still outranks the inferred reading.

Constraint: `#9.#10`'s encoding is inferred from a field dump, not observed
Constraint: mis-dating is uncorrectable downstream; under-dating is not
Rejected: tightening only the absolute window | no absolute date range is
  narrow enough to make a raw 8-byte integer a safe timestamp
Rejected: day-wide tolerances | hands back the integer space the session
  window exists to remove
Confidence: high
Scope-risk: narrow
Directive: the one-hour tolerances are load-bearing and pinned by tests;
  widening them re-opens the false-accept surface this closes
Not-tested: a real agy 1.1.18 database — none was available, which is why
  the reading is inferred in the first place
junhoyeo added a commit that referenced this pull request Aug 25, 2026
…1.1.18 gen_metadata layout (#1196)

* fix(antigravity-cli): read the per-generation timestamp from the agy 1.1.18 gen_metadata layout

agy 1.1.18 dropped `chatModel.#9.#4`, the `{#1: seconds, #2: nanos}`
Timestamp this parser used to date each turn. `#9` now carries `#2` =
u64::MAX (an int64 -1 "unset" sentinel) plus a new `#10` holding 8
length-delimited bytes. With `#4` gone every row fell through to the
session-created stamp, so on a long-running session every turn was
bucketed to the session start date and `--today` reported zero.

`#9.#4` is still read first and unchanged, so pre-1.1.18 databases and
older installs keep their exact behaviour. When it is absent, `#9.#10`
is decoded as a nested Timestamp, a nested message holding the scalar in
field 1 (varint or fixed64), or the payload itself as 8 raw
fixed64-style bytes in either byte order. Every one of those readings is
unit-detected by magnitude and range-checked against 2020-01-01..now+5y
before it is accepted; anything outside that window is discarded and the
session-created fallback takes over. `#9.#2` is never consulted, and
u64::MAX is rejected explicitly so no path can promote the sentinel into
a date.

Constraint: no agy 1.1.18 install or gen_metadata database available, so `#10`'s encoding is inferred from a field dump in the issue, not observed
Rejected: read `#9.#2` as the new timestamp | the only value ever seen there is the u64::MAX unset sentinel
Rejected: decode the 8 bytes as an IEEE-754 f64 | any double in the 2^30-ish exponent range reads as a plausible epoch-second count, so it is the one candidate with a non-trivial false-positive rate against a non-timestamp payload
Confidence: high that pre-1.1.18 parsing is unchanged; medium that the 1.1.18 reading fires on real data
Scope-risk: narrow
Directive: keep every inferred reading behind `plausible_epoch_ms` — a wrong date silently corrupts day buckets and the monotonic ratchet, which is worse than the session-start fallback this degrades to
Not-tested: a real agy 1.1.18 `gen_metadata` row; if `#10` is neither a timestamp nor decodes in range, behaviour is identical to today's

* fix(antigravity-cli): bound inferred generation timestamps to the session window

The agy 1.1.18 `chatModel.#9.#10` payload is 8 bytes whose encoding was
never confirmed against a real database, so every candidate reading of it
is a guess that has to earn acceptance. The only gate on those guesses was
an absolute "is this a believable date" window running from 2020-01-01 to
five years out. That is not a meaningful test for a raw integer: read as a
nanosecond count the window alone covers ~2% of the u64 range, so trying
both byte orders leaves an arbitrary payload — an id, a hash, a duration —
a few percent chance of passing as a date. A false accept silently buckets
a turn into the wrong day and feeds the server-side monotonic ratchet,
which has no correction path, making it strictly worse than the known-wrong
session-start dating it replaces.

Require every inferred reading to land inside the containing session's own
lifetime as well: at or after the session-created stamp less one hour, and
at or before now plus one hour. A turn cannot predate its conversation nor
happen after we read the file, and that pair of bounds is hours or days
wide instead of a decade. When there is no positive anchor to corroborate
against, decline inference entirely and let the caller fall back as before.

The explicit `#9.#4` Timestamp is untouched: it is a confirmed
representation read off real pre-1.1.18 databases, keeps its `ms > 0`
filter, takes no session bound, and still outranks the inferred reading.

Constraint: `#9.#10`'s encoding is inferred from a field dump, not observed
Constraint: mis-dating is uncorrectable downstream; under-dating is not
Rejected: tightening only the absolute window | no absolute date range is
  narrow enough to make a raw 8-byte integer a safe timestamp
Rejected: day-wide tolerances | hands back the integer space the session
  window exists to remove
Confidence: high
Scope-risk: narrow
Directive: the one-hour tolerances are load-bearing and pinned by tests;
  widening them re-opens the false-accept surface this closes
Not-tested: a real agy 1.1.18 database — none was available, which is why
  the reading is inferred in the first place
junhoyeo added a commit that referenced this pull request Aug 25, 2026
…ded session anchor (#1202)

`read_trajectory_meta` collapsed the decoded `trajectory_metadata_blob`
created-at and the file mtime into one `i64`, and that number was used both
as the per-row timestamp fallback and as the trust anchor bounding the
inferred agy 1.1.18 `chatModel.#9.#10` reading. A file mtime is always
positive, so the `session_timestamp <= 0` guard that was supposed to decline
inference without an anchor almost never fired: databases with a missing or
undecodable metadata blob ran inference against a window built on the last
write to the file. An opaque payload decoding near the mtime was accepted,
and a genuine older turn was rejected for sitting below it.

Report the created-at as `Option<i64>` alongside the effective fallback and
thread it through `parse_gen_metadata` into `generation_timestamp_ms` and
`inferred_epoch_ms`. With no decoded created-at the `#9.#10` inference is
skipped entirely and the row keeps the fallback dating it had before the
1.1.18 layout was handled. The confirmed `#9.#4` Timestamp path returns
before any of this and is unchanged.

The existing missing-anchor test only passed 0 and -1, neither of which a
real database produces. Adds an end-to-end SQLite test covering both shapes
that yield no created-at (table absent, and a blob carrying no `#2`) with a
payload the old mtime-anchored window would have accepted.

Constraint: `#9.#10`'s 8-byte layout is inferred from a field dump, not read off a real database, so an accepted reading must be corroborated
Rejected: Keep one i64 and sentinel the mtime case | any in-band sentinel is a valid epoch-ms value
Confidence: high
Scope-risk: narrow
Directive: `session_window_ms` must only ever be handed a decoded created-at; do not reintroduce the mtime as an anchor
Not-tested: a real agy 1.1.18 database (none available; `#9.#10` remains inferred)
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