Skip to content

feat(cli): de-gutter homogeneous container listings (TTY only) - #368

Open
mikegreiling wants to merge 2 commits into
mainfrom
mg/degutter-container-listings
Open

feat(cli): de-gutter homogeneous container listings (TTY only)#368
mikegreiling wants to merge 2 commits into
mainfrom
mg/degutter-container-listings

Conversation

@mikegreiling

@mikegreiling mikegreiling commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What & why

TTY listings shed the redundant left uuid gutter where a container's title is already a first-class write handle, disambiguating a colliding LIVE twin with the fused Title [8charPrefix] suffix instead.

Ratified refinement (this revision). The first cut drew the law per-VIEW ("mixed item views keep gutters on every row"), which was one level too coarse — things area Family still showed the uuid gutter on its PROJECT rows. The law is now per-SECTION:

A section/block whose rows are ALL containers renders bare titles with the fused [8charPrefix] suffix on promotion (the same shared round-trip predicate); any list that intermixes to-do rows keeps the gutter on every row in it (project rows included).

TTY presentation only--json is byte-identical (verified by a TTY-vs-piped byte-identity test). The gutter width is still computed over the whole card/view, so the guttered to-do rows are byte-identical (the de-guttered project rows stay in the width set they no longer draw from).

Before / after (real CLI output, synthetic fixture)

things area show Family — the reported bug (projects section)

before:
  Area: ⬡ Family
    uri: things:///show?id=area-0001
    tags: #home

  proj-000  ( ) New Stuff ‹8/8› #focus     ← PROJECT rows carried the gutter
  SpA1tQwE  ( ) Split ‹0›                    ← the uuid WAS the twin disambiguator
  SpB2tZxC  ( ) Split ‹0›

  Td1oQwEr  [ ] call plumber
  Td2oZxCv  [ ] renew passport

after:
  Area: ⬡ Family
    uri: things:///show?id=area-0001
    tags: #home

  ( ) New Stuff ‹8/8› #focus               ← projects section de-guttered
  ( ) Split [SpA1tQwE] ‹0›                  ← fused suffix now disambiguates the twins
  ( ) Split [SpB2tZxC] ‹0›

  Td1oQwEr  [ ] call plumber               ← to-do section KEEPS its gutter
  Td2oZxCv  [ ] renew passport

Count chip / tags intact; the area name stays suppressed (the card header names it). The loose pseudo-area view follows the same path.

things projects --area Family (the flagged flat variant — now resolved)

before:  proj-000  ( ) New Stuff ‹0› (Family)
after:   ( ) New Stuff ‹0› (Family)
         ( ) Dup [DpA1tQwE] ‹0› (Family)   (twins fused)

De-guttered, but the (Family) context suffix STAYS — no ── ⬡ Family ── header precedes these rows, so it is the row's sole area label.

things anytime — group HEADINGS de-gutter, children keep gutters

before:                              after:
── ⬡ Work ──                         ── ⬡ Work ──
Lo0seQwE  [ ] loose direct           Lo0seQwE  [ ] loose direct       ← direct to-do KEEPS gutter

Al1phaQw  ( ) Alpha ‹2/2›            ( ) Alpha ‹2/2›                  ← heading de-guttered (header, not a row)
Ch1ldQwE  [ ] child one              Ch1ldQwE  [ ] child one          ← child to-do KEEPS gutter
Ch2ldZxC  [ ] child two              Ch2ldZxC  [ ] child two

(things areas and things projects sidebar were de-guttered in the first commit and are unchanged here.)

Audit — every TTY section classified (view → section → verdict)

View Section / block Row population Verdict
things areas whole list areas de-gutter (1st commit)
things projects (sidebar) per-area groups projects de-gutter (1st commit)
things projects --area <ref> flat list projects de-gutter (new)
things area show <ref> active-projects section projects de-gutter (new)
things area show <ref> Someday-projects block projects de-gutter (new)
things area show <ref> direct-to-dos section to-dos keep
things area show <ref> Someday direct-to-dos block to-dos keep
things area show <ref> Upcoming section projects + to-dos + templates (mixed) keep
things project show <ref> heading subheads (headers) headings de-gutter (1st commit)
things project show <ref> child rows to-dos keep
things anytime project headings (headers) headings de-gutter (new)
things anytime area-direct + project-child rows to-dos keep
things someday "own" block projects + direct to-dos (mixed) keep
things someday "From active projects" headings (headers) headings de-gutter (new)
things someday those headings' child rows to-dos keep
things today / --evening Today / This Evening projects + to-dos (mixed) keep
things upcoming date-grouped sections projects + to-dos + templates (mixed) keep
things inbox flat list to-dos keep
things search flat list mixed keep
things logbook flat list resolved rows keep
things trash flat list trashed rows keep

Group-HEADING note. The underlined project titles that head their own children ARE headers, not rows: project show subheads were already de-guttered (1st commit); the anytime and someday (From-active-projects) headings carried a gutter and are de-guttered here. renderSections — the type-correct SidebarSection[] fallback that is never reached in TTY (anytime/someday always hand back precomputed lines) but is still exercised directly by unit tests — was kept consistent with renderAnytimePreview so the two never diverge.

Untouched (locked by tests)

  • Any section that intermixes to-do rows keeps a per-section aligned gutter on every row including project rows (today/inbox/upcoming/search, an area's direct to-dos, anytime/someday children).
  • To-do rows keep gutters everywhere (a to-do's uuid is its only write handle).
  • Trash & logbook keep gutters (trashed/logged titles don't name-resolve under the liveness law).
  • The predicate is liveness-scoped: a TRASHED same-title twin never triggers a suffix.
  • --json byte-identical (TTY-vs-piped comparison, elapsedMs normalized).

Shared machinery (no fork)

fusedTitleSuffix(ref, kind, projectUuid?) and formatItem's noGutter + selfRef opts (1st commit) drive every de-guttered section, over the SAME render-time RefPromoter / roundTrips predicate the JSON *Uuid emission, the inline container hints, and the canonical-ref echo consult. New this revision: renderContainerList (the de-guttered counterpart to renderList for a flat all-container list), and noGutter/selfRef threaded through the area-show projects section and the anytime/someday/renderSections headings.

Files

  • src/cli/commands/area.ts — area-show projects section (+ someday-projects block) de-gutter
  • src/cli/commands/reads.tsprojects --area routes through renderContainerList
  • src/cli/render.tsrenderContainerList; renderAnytimePreview / renderSections / renderSomedayPreview heading de-gutter (plus 1st-commit fusedTitleSuffix / noGutter / selfRef / sidebar)
  • src/cli/commands/project.ts — heading section-header de-gutter (1st commit)
  • test/cli/e2e.test.ts — area-show / projects --area / anytime coverage (+ 1st-commit areas/projects/heading/liveness/byte-identity)
  • test/cli/render.test.ts — area-show project-row unit test flipped to de-guttered plain rows
  • test/unit/render-styling.test.tsnoGutter + selfRef mechanics (1st commit)
  • CHANGELOG.md, skills/things-cli/SKILL.md

Flagged (did not improvise)

  • Design docs (render-language.md / width-aware-tty.md) still describe the id column as always-present fixed furniture. Not in the ratified cascade, so left unedited — you may want the per-SECTION gutter law recorded there.
  • things someday's "own" block lists someday projects FIRST then direct to-dos; because they share one truncation block that CAN contain to-dos, it is treated as a mixed block and KEEPS gutters (rather than de-guttering a project-only leading sub-run). Called out in case you want that leading run treated as its own pure-container block.

⚠️ DO NOT MERGE — opened for local aesthetic evaluation per the task brief.

🤖 Generated with Claude Code

@mikegreiling
mikegreiling force-pushed the mg/degutter-container-listings branch from 38cc83b to 5f852ae Compare August 3, 2026 21:48
@mikegreiling

Copy link
Copy Markdown
Owner Author

Rebased onto origin/main (39855da)

Both commits replayed onto current main (past the read-shape doctrine arc #375#379, heading fidelity #372, #369, and the ordering-lock #380). Force-pushed rebased branch: 5f852aed9ffd60611078f744d59059400a9efc98. CI: green (1m20s). Full local npm run check also green (113 files / 2326 tests).

Conflicts & how they were resolved

Latent break the auto-merge introduced (fixed)

src/cli/commands/project.ts: #368's hunk removed the uuidCol import, but the HEADARC arc added a new use of uuidCol at the archived-heading logged-region group header — a use #368 never saw. The textual auto-merge left that call in place against a now-missing import (would not compile). See the decision below for the fix.

Decision: archived-heading group headers (new surface since this branch was cut)

The HEADARC arc (#372) added archived-heading GROUP headers in the logged region of project show --show-logged — a heading-header surface that did not exist when #368 was written. Per the rebase brief (apply #368's heading-header de-gutter there iff pre-rebase #368 de-guttered the equivalent live heading headers — it did), I de-guttered the archived-heading group header to match, for consistency and to resolve the compile break above. This is sound: resolveHeadingRef filters only trashed = 0 (not archived), so an archived-but-not-trashed heading's title still round-trips through its project-scoped resolver → the header renders bare, fusing [8char] only on a within-project title collision, exactly like a live heading header. Its logged child to-do rows keep their gutters (a to-do's uuid is its only handle). Noted in the CHANGELOG's Group-HEADING bullet.

Scope check

No #368 behavior had to be dropped or altered to survive the arc — every de-gutter target (areas, projects sidebar, projects --area, area-show projects/someday blocks, project-show + anytime/someday heading headers, gutters retained on all to-do-intermixing sections, --json byte-identity) is intact, plus the one consistency extension above. All of #368's own test locks pass (per-section law, trashed-twin liveness, gutter-lock, TTY-vs-piped byte-identity).

(Maintainer note: your local mg/degutter-container-listings checkout needs git pull --rebase — or git reset --hard origin/mg/degutter-container-listings if you have no local changes — to pick up the rewritten branch.)

mikegreiling and others added 2 commits August 4, 2026 17:00
The ratified per-view law: a row keeps its uuid gutter iff its title is
not a first-class handle for acting on it. Container titles ARE first-class
refs (name resolution + the liveness law + the fused fallback), so three
homogeneous listings shed the gutter and disambiguate colliding live twins
with the fused `Title [8charPrefix]` suffix instead:

- `things areas`   → `⬡ Family`, fused `⬡ Split [8char]` on a live twin
- `things projects` (sidebar) → `( ) ★ New Stuff ‹8/8› #home`, twins fused
- `things project show` heading subheads → `Backlog`, `Dup [8char]` on a
  within-project title collision (project-scoped predicate)

All three reuse the shared promotion machinery (new `fusedTitleSuffix` over
the render-time RefPromoter/roundTrips — the SAME predicate JSON `*Uuid`
emission, the inline container hints, and the `≡` canonical-ref echo consult;
no fork). formatItem gains `noGutter` + `selfRef` opts; the fused suffix rides
the FIXED tail so it survives width truncation.

Untouched (locked by tests): mixed item views (today/inbox/anytime/someday/
upcoming, area-show + project-show CHILD rows, search) keep per-view gutters
on every row; to-do rows keep gutters everywhere; trash and logbook keep
gutters; the predicate is liveness-scoped (a trashed same-title twin never
promotes); `--json` is byte-identical (human-render-only change).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYbJensXxHpJK1v1VDXYUD
…rojects --area, anytime/someday headings)

The shipped law ("mixed item VIEWS keep gutters on every row") was drawn one
level too coarse — `things area Family` still showed the uuid gutter on its
PROJECT rows. Ratified refinement: gutters are per-SECTION, not per-view. A
section/block whose rows are ALL containers renders bare titles with the fused
`[8charPrefix]` on promotion (same shared predicate); any list that intermixes
to-do rows keeps the gutter on every row in it.

Newly de-guttered under the refined law (all via the existing noGutter/selfRef
+ fusedTitleSuffix machinery — no fork):
- `things area show <ref>` (and the `loose` pseudo-area): the PROJECTS section
  and the Someday-projects block. The direct-to-dos block and the mixed
  Upcoming section keep gutters (they contain to-dos).
- `things projects --area <ref>`: the flat area-scoped listing (new
  `renderContainerList`); the `(Area)` suffix stays (no header names the scope).
- Group-HEADING renders (underlined project titles heading their children) in
  `things anytime` / `things someday` — headers, not rows. `project show`
  heading subheads were already de-guttered; this brings the anytime/someday
  headings to the same law. Child to-do rows under a heading keep gutters.

Width stays computed over the whole card/view, so the guttered to-do rows are
byte-identical (the de-guttered project rows remain in the width set they no
longer draw from). `--json` byte-identical (human-render-only).

Tests: flipped the area-show project-row unit test to assert plain de-guttered
rows; added e2e coverage for the area-show projects section, `projects --area`,
and anytime headings (fused-on-collision, child/direct rows stay guttered);
byte-identity + gutter-lock tests stay green. CHANGELOG + `things-cli` SKILL.md
updated to the per-SECTION law.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYbJensXxHpJK1v1VDXYUD
@mikegreiling
mikegreiling force-pushed the mg/degutter-container-listings branch from 5f852ae to c6a1b6b Compare August 4, 2026 22:22
@mikegreiling

Copy link
Copy Markdown
Owner Author

Re-rebased onto origin/main @ 8ed706c (post-0.13.0). Light rebase this time: CHANGELOG was the only conflict (the de-gutter entry now sits under the fresh post-0.13.0 ## Unreleased section); the skill wording auto-merged. All #368 test locks pass, full npm run check green, CI green on c6a1b6b. Still unmerged — awaiting the aesthetic verdict. To evaluate locally: git checkout mg/degutter-container-listings && git reset --hard origin/mg/degutter-container-listings.

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