Skip to content

feat: Initial commit introducing TUI - #29

Merged
sim186 merged 15 commits into
mainfrom
claude/tui-align-main-uulmhn
Aug 3, 2026
Merged

feat: Initial commit introducing TUI#29
sim186 merged 15 commits into
mainfrom
claude/tui-align-main-uulmhn

Conversation

@sim186

@sim186 sim186 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

No description provided.

sim186 and others added 15 commits May 15, 2026 21:52
- Updated LibraryPanel to improve status display with icons and colors.
- Enhanced duration formatting and date display in the library.
- Introduced new methods for formatting status and tags.
- Modified LibraryScreen to include a context hint and command bar.
- Added LogsScreen for live server log tailing with color-coded log levels.
- Created SearchScreen for full-text search results with improved UI.
- Revamped SettingsPanel for better organization and added server settings.
- Improved TranscriptScreen with enhanced metadata display and speaker colors.
- Removed WelcomeScreen as it was deemed unnecessary.
- Introduced shared chrome components: TitleBar, ContextHint, and CommandBar.
- Updated SegmentList to include speaker colors and improved formatting.
- Enhanced StatusBar for better connection status display and message handling.
… main

Merges the Textual-based TUI (modeless, palette-driven interface for
AmicoScript) into this branch and resolves it against main's meeting-
capture / transcription-defaults work in backend/api/routes/settings.py,
combining both settings sets rather than dropping either.

Also fixes the TUI's API client, which predates this merge and was never
exercised against the real backend: most write endpoints (folders, tags,
recordings, analyses, LLM settings, app settings, transcribe-by-URL) are
Form-encoded, but the client posted JSON bodies, and llm settings /
transcribe-url used mismatched field names (base_url vs llm_base_url,
url vs source_url). Fixed to use form encoding and matching field names
throughout.
…ience

- Fix Space-q on 5 screens: was labeled "Back" but ran /quit, silently
  exiting the whole app instead of just quitting as intended. Relabeled
  to "Quit" everywhere for consistency, and added the README-promised
  Space-h ("Welcome") and Space-? ("Help") chords app-wide so both are
  finally real instead of just documented.
- Add bare l/j/s shortcuts on the welcome screen, matching the README.
- Add a reusable ConfirmDialog modal and wire it into every delete path
  (/delete, the palette's delete mode, and Library's `d` key) instead of
  deleting immediately or just telling the user to retype a command.
- Add a persistent "N jobs running" indicator to the status bar, polled
  globally from the App so it survives navigating away from /jobs.
- Replace the 10s toast /help with a proper scrollable HelpScreen listing
  every command and the leader-chord cheatsheet.
- Bring JobDetailScreen in line with every other screen's chrome
  (TitleBar/ContextHint/CommandBar/leader_chords) instead of a bare
  Textual Header with no leader support.
- Fix README: /models and /llm were documented backwards (Whisper vs LLM
  model pickers were swapped), and it claimed no audio playback despite
  playback.py being fully wired into the transcript screen.
- Remove dead per-line color computation in logs.py (Log widget has
  highlight=False and can't render it).

Verified via a headless Textual Pilot smoke test exercising leader
navigation, the confirm-dialog flow, and JobDetailScreen mounting.
The welcome screen's hand-drawn ASCII logo was misaligned and rendered
as an illegible block of characters, clipped at the right edge of its
fixed-width container. Replaced it with a simple bordered, centered card
(app name + tagline + command list) using Textual's own border/align
primitives instead of hand-computed block characters, so it can't drift
out of alignment again.

TitleBar and StatusBar both right-aligned their trailing hint text by
concatenating a fixed run of literal spaces before it — reliable only at
one specific terminal width, clipping the hint everywhere narrower.
Rebuilt both as two real widgets (1fr left / auto right) so the layout
engine handles the alignment instead of a guessed space count.

Verified by rendering the actual app (headless Textual Pilot ->
export_screenshot -> Chromium) rather than judging from source alone.
Plain text reads better than a stylized wordmark here.
Rendered from the actual app (headless Textual Pilot + Chromium), not a mockup.
CLI: add ./tui.sh and tui.bat launchers that install tui/requirements.txt
on first run and then exec the TUI, so starting it is one command instead
of two manual steps. tui/__main__.py now catches a missing dependency and
prints an actionable one-liner instead of a raw traceback. Root README
gains a "Terminal (TUI)" quick-start section next to the existing
Docker/Local ones — previously the TUI wasn't mentioned there at all.

Web: the Help modal (frontend/index.html) gained a "Prefer the terminal?"
card with a ready-to-run command and a copy-to-clipboard button. The
command includes --api-url only when the page isn't served from the TUI's
own default, so it works unmodified against Docker/remote/custom-port
setups. A real browser tab can't spawn a terminal process directly, so
this is a copy-paste hand-off rather than an in-page launch — verified
with a headless Playwright run that the command text and clipboard write
are both correct.
Wraps command execution (the single chokepoint every slash command goes
through, whether triggered from the CommandBar, a leader chord, or the
palette) with push_busy()/pop_busy(), and shows an animated spinner in the
status bar while any command is in flight. Also wires up
GET /api/watcher/status, polled globally, so an in-progress meeting
auto-capture recording is visible in the TUI too — previously only the
web UI's tray badge showed it.

While building this, found that the status-bar updates were silently
broken since the leader-chord hint display was first written:
self.app.query(StatusBar) looks like it searches the active screen, but
App._get_dom_base() roots App-level queries at the App's hidden default
screen, not whatever's pushed on top — so it always returned nothing.
This affected the leader-armed hint (never rendered) and the "N jobs
running" indicator added in an earlier pass (silently never updated
either). Fixed with a status_bars() helper that queries each screen in
the stack directly, and pointed every call site at it.

Verified by rendering the app (headless Textual Pilot -> export_screenshot
-> Chromium): the busy spinner, recording badge, job count, and the
previously-broken leader-armed hint all now show correctly.
…time

Job screen: each segment's text now arrives over the same SSE stream as
progress (backend/core/transcription.py already pushed it as
data.segment.text — the web UI used it, the TUI didn't). The job log now
shows the actual words as they're produced instead of a generic
"Transcribing... 00:12 / 05:30" line, so you can read along instead of
waiting for the job to finish.

Transcript screen: "/" opens a find box. Typing text jumps to the first
matching segment and Enter cycles to the next; typing a timestamp (83,
1:23, 1:02:03) jumps straight to the segment covering that time.

Fixed a focus bug hit while building this: TranscriptScreen never
explicitly focused anything on mount, relying on Textual's default
focus order. Adding the (initially hidden) find Input earlier in the
compose tree meant it silently grabbed initial focus instead of the
segment list, which would have broken plain arrow-key navigation.  Fixed
by explicitly focusing the segment list on mount, matching every other
screen's pattern.

Verified with headless Pilot: text/timestamp find-and-jump, Escape
closing find without popping the screen, normal j/k navigation still
working after find closes, and a mocked SSE stream confirming segment
text (not just the generic progress line) reaches the job log.
The web UI can rename/move/tag individual recordings and rename/delete
folders and tags; the TUI's ApiClient already wrapped most of the
backend calls for this (update_recording, update_folder, delete_folder,
create_tag, add_tag/remove_tag) but nothing ever called them — no
command, no keybinding. Tag rename/delete had no wrapper at all.

Adds:
- ApiClient.update_tag() / delete_tag()
- /rename <id> <name>, /move <id> [folder_id], /tag-toggle <id>
- /folder new|rename|delete and /tag new|rename|delete subcommands
  (mirrors the existing /folder new pattern)
- Library screen keys: R rename (prompt), m move (folder picker),
  t tag (add/remove picker)
- A new PromptDialog modal (mirrors ConfirmDialog) for single-line text
  input, reused by rename
- Ad-hoc "move to folder" and "toggle tag" pickers in palette.py,
  reusing the same mechanism as the existing analysis-type picker

Verified with headless Pilot against a mocked ApiClient: every new
command and keybinding (including the ConfirmDialog-gated folder/tag
deletes and the PromptDialog-gated rename, with its pre-filled current
name) drives the right API call with the right arguments.
Library: multi-select (v to toggle a row, x for a bulk-action menu —
delete / export as combined markdown / move to folder / tag) closes the
last big library-management gap vs. the web UI. Refactored the
single-recording move/tag pickers in palette.py into shared
_folder_entries()/_tag_entries() builders so the new bulk pickers
(open_bulk_move_picker, open_bulk_tag_picker) don't duplicate them.

Transcript: e edits a segment's text, Ctrl+R resets it to the original,
a sets the speaker on just that segment, S renames a speaker everywhere
in the transcript — all four backend endpoints existed and were already
used by the web UI but had no TUI wrapper at all.

Bug found and fixed while building the bulk-action menu: Palette's MRU
(recent-selection) ranking pushed every picked entry's key into a
persistent, app-wide deque and re-sorted the list by it on next open.
That's fine for the main command palette, but ad-hoc menus like the new
bulk-action picker are rebuilt fresh each time and reuse the same keys
(e.g. "bulk:delete") across unrelated invocations — so picking "export"
once silently moved "export" to the top the next time the menu opened,
making a bare Enter run the wrong action. MRU-boosting is now skipped
for all ad-hoc pickers (analysis-type, move, tag-toggle, bulk), which
only makes sense for the persistent palette anyway.

Also confirmed (while chasing why 'space' wouldn't toggle a library
row's selection) that the global Space-leader intercepts the key at the
App level before it ever reaches a focused widget's own bindings on any
screen with leader_chords — so multi-select uses 'v' instead. Noting
this because it means TranscriptScreen's existing "Space = Play/Pause"
binding has likely been dead code since it was written, for the same
reason; left as-is since fixing it wasn't part of this change and
touches established behavior.

Verified with headless Pilot against mocked ApiClients: multi-select
toggling, all four bulk actions (including the ConfirmDialog-gated bulk
delete), and all four segment-editing actions (including the
PromptDialog pre-fill and in-place SegmentList updates without a full
reload).
@sim186
sim186 merged commit 17cf66e into main Aug 3, 2026
1 check passed
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.

2 participants