feat(tui): Discover screen — ranked queue of scored postings - #31
Merged
Conversation
The posting-level triage state the TUI Discover queue (PROJECT.md §8 screen #2) needs, distinct from the application-level ApplicationStatus: - QueueStatus StrEnum (new/saved/dismissed) in atlas.matching.structure, co-located with Verdict. - JobPosting.queue_status column (default 'new'), with an Alembic migration chaining onto the cover_letter head. The column is NOT NULL with a server_default of 'new' so every existing posting backfills without a data migration; new inserts default to 'new' from the model. Dismissed postings are later filtered out of the ranked queue. Tests: the model default, a queue_status round-trip, and that upgrade_to_head adds the NOT NULL 'new'-defaulted column to job_posting. 100% line+branch.
The data layer beneath the TUI Discover queue (PROJECT.md §8 screen #2): - list_scored_postings(session) -> list[(JobPosting, MatchScore)]: each posting paired with its LATEST score (the max-id row per posting, via a correlated subquery — matching get_latest_match_score's newest-append-only-row-wins convention, not created_at), inner-joined so unscored postings are excluded, dismissed postings filtered out, ordered by score desc then newest posting first. The subquery aliases MatchScore and correlates JobPosting explicitly to avoid SQLAlchemy auto-correlation. - set_posting_queue_status(session, posting_id, status) in scrape.repository (co-located with JobPosting CRUD) — backs the dismiss/save actions. Tests over db_engine: score-desc ordering, id-desc tiebreak, latest-not-highest (a lower newer score ranks by the latest), dismissed excluded, saved included, unscored excluded, empty; the mutator persists each status and raises on an unknown id. 100% line+branch, mypy --strict clean (incl. win32).
The Discover queue's 'open' action launches a posting's apply URL in the browser, which the file opener can't do (default_file_opener requires the target to exist on disk). Add a UrlOpener seam mirroring FileOpener: - atlas.platform.browser: UrlOpenError, a runtime-checkable UrlOpener Protocol (__call__(url) -> None), and default_url_opener backed by webbrowser.open (raises UrlOpenError when it returns False or the platform errors), the real launcher pragma'd like default_file_opener. - Exported from atlas.platform. - FakeUrlOpener (records .opened, optional raises) in the shared conftest. Tested: the fake satisfies the Protocol, records URLs, and raises. 100% line+branch, mypy --strict clean (incl. win32).
The ranked queue of scored postings (PROJECT.md §8 screen #2) — the piece that makes the daemon's discovery/scoring work visible and actionable, closing Journey B (background discovery → review → tailor). - tui.data.build_discover_queue → DiscoverQueue/DiscoverRow: maps the ranked list_scored_postings into display rows (company, source, a salary-display string, score/verdict/rationale, queue state). - tui.screens.discover.DiscoverScreen: a DataTable ranked by fit with the AI's rationale in a detail pane (updated as the cursor moves). Enter drills into PostingDetailScreen; x dismisses (hides from the queue); s saves; o opens the apply URL via the injected UrlOpener; t tailors off the event loop (the tailor_workspace thread-worker pattern) and, on success, pushes the new application's detail. Row-position indexing keeps the row→posting mapping simple and fully covered. - PostingDetailScreen gains the §8 screen-#3 Tailor action (same worker pattern). - AtlasApp: a new 'w' Discover binding + action_discover, an injected url_opener, and set_queue_status / run_open_url methods (Dashboard stays the landing screen). Pilot tests drive every path: mount + rank + rationale pane, empty queue, enter→detail, dismiss/save persistence, open-URL success + error, tailor → application (browse-only disabled, worker error handled, empty-queue no-op), and the same for PostingDetailScreen's tailor; plus builder tests over db_engine for the salary-display variants and the dismissed filter. 100% line+branch, mypy --strict clean (incl. win32).
Update the docs for the Discover-queue slice (Definition of Done, AGENTS.md §8): - STATUS.md: bump 'Last updated' + 'Current phase'; add the 'What has landed' entry for the DiscoverScreen; drop the Discover-queue item from 'Next up' and re-order the rest (more ATS adapters, aggregators, multiple profiles, IPC); tick the Phase-2 progress row. - PROJECT.md §15: check off the scored Discover queue item. - CHANGELOG.md (Unreleased/Added): the Discover screen + actions, list_scored_postings, JobPosting.queue_status + migration, and the UrlOpener seam. - README.md: note the Discover queue (press w) in the TUI walkthrough.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Builds the Discover screen (PROJECT.md §8 screen #2): a ranked queue of scored postings in
the TUI. Until now, everything the daemon discovers and scores piled up invisibly — the TUI
had no screen listing scored postings, so a discovered posting that was never tailored was
unreachable from the UI. This closes Journey B (background discovery → review → tailor): the
daemon finds and scores jobs, and
DiscoverScreen(pressw) presents them ranked by fit withthe full §8 action set.
Key commits
feat(db): add job_posting.queue_status for the Discover queue— aQueueStatus(
new/saved/dismissed) enum + theJobPosting.queue_statuscolumn, with an Alembicmigration (
server_default='new'so existing rows backfill). Posting-level triage, distinctfrom the application-level
ApplicationStatus.feat(matching): rank scored postings by fit—list_scored_postings(each posting + itslatest
MatchScorevia a correlated subquery, scored-only, dismissed excluded, ordered byscore desc) +
set_posting_queue_status.feat(platform): add the URL-open boundary— aUrlOpenerseam (default_url_openerviawebbrowser.open) mirroring the file opener, so the queue opens a posting's apply URLcross-platform while the suite stays hermetic.
feat(tui): add the Discover screen with tailor/dismiss/save/open actions— thebuild_discover_queuebuilder +DiscoverScreen(rankedDataTable+ rationale pane) withEnter→detail,
ttailor (thread worker → new application detail),xdismiss,ssave,oopen URL; a Tailor action added to Posting detail (§8 screen docs: adopt LiteLLM + AI patterns from Resume-Matcher, add docs/agent #3); app wiring (
wbinding,action_discover, injectedurl_opener,set_queue_status,run_open_url).docs: record the Discover screen— STATUS / PROJECT §15 / CHANGELOG / README.Migration
One new Alembic migration adds
job_posting.queue_status(NOT NULL,server_default='new'),chaining onto the
add_cover_letterhead. Existing postings backfill tonew; no datamigration needed.
Reuse
AtlasApp.run_tailor(posting_id)andtailor_postingalready operate on a bare posting (theycreate the Application), so Tailor needed no new service; the thread-worker pattern comes from
tailor_workspace.py;PostingDetailScreen,verdict_style, and_fit_textare reused.Testing
Fully hermetic (AGENTS.md §6.2): builder + repository tests over the in-memory
db_engine(ranking, latest-not-highest, tiebreak, dismissed/saved/unscored filters, salary-display
variants), a
FakeUrlOpenerfor the URL boundary, and Pilot-driven TUI tests for every screenpath (mount + rank + rationale pane, empty queue, drill-in, dismiss/save persistence, open-URL
success + error, tailor → application with browse-only-disabled / worker-error / empty-queue
branches, and the same for Posting detail's tailor). Gates green:
ruff format --check,ruff check,mypy --strict(incl.--platform win32), and 895 tests at 100% line+branchcoverage. No new dependency (
webbrowseris stdlib).Design notes for review
queue_statusis a single enum-ish column (not two booleans): mutually-exclusive states,one default, one
server_default. Dismissed drops out of the queue; saved stays, flagged.DataTablehas no native expandable rows, so the AI rationale shows in a detail paneupdated on row-highlight (rows map to postings by cursor-row index).
push_screen(notswitch_screen) soescapereturns to the queue.oopens the apply URL via the newUrlOpener, not the file opener (which requires anon-disk path).
wfor Discover (d/a/qwere taken);xfor dismiss to avoid shadowingthe global
d, and becauseScreen.action_dismissalready exists (the action isaction_pass_posting).