Skip to content

feat: phase 3 - terminal UI and main wiring - #5

Open
Skeyelab wants to merge 21 commits into
mainfrom
feat/ui
Open

Skeyelab wants to merge 21 commits into
mainfrom
feat/ui

Conversation

@Skeyelab

@Skeyelab Skeyelab commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Phase 3 of the v1 plan: the Bubble Tea UI and the wiring in main.go.

What's in it

  • List screen: header with adapter status, rows (ticket, stage, flags), unlinked PRs group, scrolling, per-row hints.
  • Detail screen: ticket line, stale line, one line per PR and per applying env, flags. Scrolls; r refreshes in place.
  • main.go: --config, --version, --help; log at $XDG_STATE_HOME/app-green/app-green.log (dir 0700, file 0600); resolver runs in the background and is cancelled on quit (waits up to 2s); o opens pages with open -u (macOS) or xdg-open (Linux), only for safe http(s) URLs, never through a shell.

Decisions (asked, not assumed)

  • Detail scroll follows the selection only while it was visible; polls never snap the window back and a resize keeps the selection on screen.
  • Pipeline name shows as an extra column on detail env lines only when two applying envs share account and stage.
  • A URL with a space or other control, DEL, quote, backtick, angle bracket or backslash is never opened; the same check (ui.WebPage) guards the UI and the opener.
  • A failing check with no usable page falls to the PR's checks tab, not the ticket.
  • o with nothing openable says "nothing to open" in the footer.
  • A poll warning is logged when it first appears, not on every poll.
  • Opener is macOS and Linux only (Windows reports "not supported"). An outside SIGINT exits 0. A log file that cannot be opened stops startup with exit 1. Ctrl+C during startup is not handled.

Not in this PR

  • The f, a/x and t hints are shown but not wired; that is phase 4.

Checked

  • go test -race ./..., go vet, golangci-lint clean.
  • Run against real Jira, GitHub and two AWS accounts: the header status, list rows and stranded-PR flags looked right, and an expired SSO session shows ✗ sso expired on a fresh start. The detail screen and o targets have not been checked against real data yet.

Add internal/ui with Lip Gloss rendering for the list header and rows:
status markers (red ●, yellow ◐), stage column, first-flag reason, key
hints from a pure RowAction function, dimmed stale rows with a "~ " stage
prefix, per-source header segments, and graceful narrow widths. Styles
bind to an injectable renderer so NO_COLOR is tested against a real
color terminal profile.

Assisted-by: Claude Code:claude-opus-5-5
- f when any failing check is a re-runnable Actions run; a non-Actions
  check alone opens its URL with o.
- A key whose action has two targets across the row's flags (two
  approvals, re-runnable checks in two PRs) is not offered.
- An approval the profile cannot make (read-only env or no token) offers
  o to open the pipeline console.
- A narrow header drops OK sources, last first, before cutting, so a
  problem source is never hidden while an OK one shows.

Assisted-by: Claude Code:claude-opus-5-5
Ticket keys and titles, flag reasons and source errors come from Jira,
GitHub and AWS. clean turns tabs, newlines and carriage returns into
spaces, strips escape sequences (an OSC 52 sequence could write the
clipboard) and drops other control characters and invalid bytes before
any text is measured, cut or printed.

Assisted-by: Claude Code:claude-opus-5-5
- Flags with an obvious target offer o: unhealthy and partial prod open
  the pipeline, changes requested, ready to merge and stale review open
  the PR. flagAction has a case for every FlagKind, and a test fails on
  a kind added without one.
- A failing check with no URL of its own, in Actions or code scanning,
  offers o for the PR's checks page (ChecksPage).
- Narrow rows keep the marker and hint: the reason and title give way
  first, then the stage and key are cut.
- The pause glyph is now ‖ (stage "prod ‖", header "‖ throttled").
- The throttle countdown rounds up, so it never reads shorter than the
  wait.
- Document that a Layout is rebuilt every render; Action.Key is a switch;
  the dim test renders through a fake color terminal instead of changing
  the global profile.

Assisted-by: Claude Code:claude-opus-5-5
App is the top Bubble Tea model. It waits on the resolver's snapshot
channel (waitForSnapshot, adapted from jira-green), shows the header,
one row per chain, then an Unlinked group, and a footer key line with
a dimmed warnings count.

- loading… before the first snapshot, nothing in flight when empty.
- up/k and down/j move over chains and unlinked PRs, never onto the
  Unlinked header. The cursor follows the same ticket (or repo#number)
  when a new snapshot reorders the rows, else keeps its index.
- The cursor row is reverse video, turned back on after each reset in
  the row. Without escape codes (NO_COLOR, no color terminal) its
  marker cell shows ">" instead.
- r sends on the refresh channel without blocking; q and ctrl+c quit.
- o opens OpenTarget for a chain (the pipeline, the failing check or
  checks tab, or the PR when the row offers o, else the ticket) and the
  PR for an unlinked line. A failed open shows in the footer until the
  next key.
- The body scrolls to keep the cursor in view; header and footer stay.
- Adds bubbletea v1.3.10 and bubbles v0.21.1 (jira-green's versions).

Assisted-by: Claude Code:claude-opus-5-5
golang.org/x/text goes to v0.23.0, the version jira-green uses, so every
module the two repos share is at the same version.

Assisted-by: Claude Code:claude-opus-5-5
o now opens what the row's first flag is about, whatever its main key:
a failing check's page (else the PR's checks tab) on an f row, the
pipeline on an a/x row, the PR for a PR flag. A t row, a flag with no
target, or an unknown page opens the ticket. With two targets for the
same key, o opens the first flag's.

Assisted-by: Claude Code:claude-opus-5-5
enter on a ticket row opens its detail: the ticket line with its Jira
status, a stale line when the chain may be behind, one line per PR and
per applying env, then every flag reason. Up/down select a PR or env and
o opens it (failing checks, the PR, or the pipeline console; the ticket
when nothing is selectable). esc and backspace go back with the list
cursor unchanged. A new snapshot refreshes the detail, keeping the
selection on its PR or env, and returns to the list with a note when the
ticket is gone.

Assisted-by: Claude Code:claude-opus-5-5
A detail taller than the screen scrolls between the fixed header and
footer: the selection stays in view, the first item shows the ticket
line, and down past the last item (or with nothing selectable) scrolls
on so the flags can be read on a short screen. Untrusted text on the
detail screen is covered by a test.

Assisted-by: Claude Code:claude-opus-5-5
On a rolled-back slot, link sets SHA from the deploy that replaced the
chain's commit, so the detail line reads "(now dddd444)" instead of
looking like the chain's own commit.

Assisted-by: Claude Code:claude-opus-5-5
r asks for a refresh on the detail screen too, and its footer says so.
o acts on the selected item only while it is on screen; when scrolling
has hidden it, o opens the ticket.

Assisted-by: Claude Code:claude-opus-5-5
- o opens the browser from a command, not from Update; a failure comes
  back as openFailedMsg and shows in the footer (list and detail).
- Only absolute http and https URLs are opened; anything else falls
  back to the ticket, or opens nothing.
- Without color the cursor's ">" sits next to the marker instead of
  replacing it.
- Tiny heights no longer push out the header (bodyHeight floors at 0).
- A nil opener opens nothing instead of panicking.
- The Unlinked label is dimmed.
- Tests for a snapshot shorter than the scroll offset and for an
  escape-laden unlinked title.

Assisted-by: Claude Code:claude-opus-5-5
main.go parses --config, --version and --help, loads the config (a
missing file prints "no config at <path>; see README"), logs config
warnings to $XDG_STATE_HOME/app-green/app-green.log (0700 dir, 0600
file, append), builds the resolver with FromConfig, and runs it in the
background behind the Bubble Tea UI in the alt screen. Quitting cancels
the resolver and waits up to 2s for it. o opens pages with macOS
`open -u` or xdg-open, only for http(s) URLs, detached from the screen.

Assisted-by: Claude Code:claude-opus-5-5
Polls no longer snap the window back to a selection the user scrolled
away from, and a resize no longer hides the selection.

Assisted-by: Claude Code:claude-opus-5-5
webPage returns the parsed URL and open uses its String(), so what was
checked is what is opened. checksTarget skips a failing check whose URL
is not a web page and falls to the PR's checks tab instead of the ticket.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
…e collide

Assisted-by: Claude Code:claude-opus-5-5
ui.WebPage rejects spaces and other controls, DEL, quotes, backticks,
angle brackets and backslashes, on top of the http(s)+host check. The
opener in main.go uses the same function so the two cannot drift.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
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