feat: Tailor workspace + background action workers (closes Phase 1 item #6) - #28
Merged
Conversation
Add build_tailor_workspace + TailorWorkspaceView / ResumeBlockView / TailoredSelection to atlas.tui.data — the pure data layer the Tailor workspace screen presents. It resolves the application and posting, reads the latest master-resume blocks, and decodes the latest tailored resume's selections (TailoredItem.model_validate) plus the latest cover letter's version/path. Pure over an open session, so it's covered without a running terminal.
Add TailorWorkspaceScreen (opened from Application detail via `t`): it shows the master-resume blocks, the latest tailored selections, and a materials summary, and runs four actions — Tailor, Cover letter, Re-render, Open. Every Atlas service is synchronous and blocks (subprocess AI, network, WeasyPrint), so each action runs in a Textual thread worker (@work(thread=True, exclusive=True, exit_on_error=False)) — the first worker in the codebase — with on_worker_state_changed refreshing + toasting on success and surfacing the service's typed errors on failure without tearing down the app. AtlasApp gains injected provider/renderer/opener/tailoring/render_config boundaries (all optional; opener defaults to default_file_opener) plus run_tailor/run_cover_letter/run_rerender/run_open (each a short session_scope + the service, mirroring the CLI construction) and an actions_enabled property — so the app runs browse-only when the AI/render boundaries aren't available. Tests drive each worker to completion via app.workers.wait_for_complete() with injected fakes + a tmp_path renders dir, covering success, worker-error, and browse-only-disabled paths.
Extend `atlas tui` to build the provider chain + renderer from config (the same sequence as `atlas tailor`) and pass them, the file opener, and the tailoring / render config into AtlasApp so the Tailor workspace's actions work. Wrapped in _build_tui_actions, which catches ConfigError / RenderError / LLMError and falls back to browse-only (all-None) with an `atlas doctor` hint — the read/track screens need no AI, so a missing key never blocks launching the TUI. The real app.run() stays the sole pragma'd edge; the best-effort build is plain branch logic covered by CliRunner tests (success + both fallback branches).
Document the final slice of item #6 (the Tailor workspace + background action workers + browse-only launch): the CHANGELOG Added entries, the README `atlas tui` description, and the STATUS what-landed block. Tick item #6 done in PROJECT §15 and mark the Phase 1 core loop complete in STATUS (top block, table row), moving "Next up" to Phase 2 (discovery daemon). Note the deferred Phase-1 optional depth (PR-2b tailoring / interactive editing) as non-blocking.
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
The final slice of Phase 1 item #6 (PROJECT.md §8, screen #4) — the Tailor workspace plus wiring the long-running AI/render/open services through Textual thread workers. With this, item #6 is complete and the Phase 1 core loop closes: onboard → master resume → scrape → score → tailor + cover letter → render → track, all from the CLI or the TUI.
Every Atlas service is synchronous and blocks — the AI provider chain runs a subprocess (Claude Code) or a network call (OpenRouter), and rendering runs WeasyPrint — so running them on Textual's event loop would freeze the UI (§8 requires long-running actions to run async). This PR adds the first worker in the codebase.
What changed
TailorWorkspaceScreen(atlas.tui.screens.tailor_workspace) — opened from Application detail (t). Shows the master-resume blocks, the latest tailored selections (content_id · included · reason · text), and a materials summary; runs four actions: Tailor (t), Cover letter (c), Re-render (r), Open (o). Read-only panes this PR — interactive editing is the deferred follow-up.@work(thread=True, exclusive=True, exit_on_error=False);on_worker_state_changedrefreshes + toasts on success and surfaces the service's typed errors (TailoringError/CoverLetterError/RenderError/FileOpenError/…) on failure without tearing down the app.AtlasAppboundaries — injectedprovider/renderer/opener/tailoring/render_config/renders_dir(all optional) +run_tailor/run_cover_letter/run_rerender/run_open(each a shortsession_scope+ the service, mirroring the CLI) + anactions_enabledproperty.atlas tuibuilds the provider chain + renderer best-effort (_build_tui_actions), catchingConfigError/RenderError/LLMErrorand launching browse-only with the Tailor actions disabled + anatlas doctorhint — the read/track screens need no AI, so a missing key never blocks the TUI.atlas.tui.data.build_tailor_workspace— the pure view-model builder (reuses the resume/tailor/coverletter getters).Decisions (confirmed up front)
AtlasApp; disable actions with a hint) rather than failing.How thread workers are tested at 100% coverage
The key finding that de-risked this:
await pilot.app.workers.wait_for_complete()awaits a thread worker deterministically insiderun_test(), andexit_on_error=False+on_worker_state_changedmakes the error path assertable. Hermeticism comes from injecting the existingFakeLLMProvider/FakePdfRenderer/FakeFileOpener+ atmp_pathrenders dir at the app boundary, so a worker never makes a real AI/render/subprocess call.Key commits
feat(tui): add the tailor-workspace view-model builderfeat(tui): add the Tailor workspace screen with thread-worker actionsfeat(cli): build the TUI's AI/render boundaries best-effortdocs: record the Tailor workspace and close Phase 1 item #6Testing
ruff format --check+ruff checkclean;mypy --strictclean on Linux and--platform win32.Pilottests drive each worker to completion and assert: the DB result (success), that nothing persisted (worker error handled), and that browse-only disables the actions.uv buildconfirms the wheel is unchanged in shape (app.tcss+ the new screen ship).No migration; no dependency changes (
textual/pytest-asynciolanded with the core-TUI PR). Closes #6.