feat(tui): run goals in Terminal Studio - #61
Merged
saikrishna321 merged 6 commits intoAug 31, 2026
Conversation
`Config` is built at import time, so a `throw` there escaped every try/catch in the CLI and Node printed a raw ZodError with a stack trace because someone typed `LLM_PROVIDER=claude`. `safeLoadConfig()` reports instead of throwing, dropping only the offending keys so their schema defaults apply and the rest of the environment survives — enough to say what is wrong, never enough to run on. `loadConfig()` still throws for the SDK, now with a readable message. `collectSetupIssues()` turns those into something SetupScreen can show and offer to fix, since the shell is the one caller that can rewrite `.env` rather than only complain about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
Bare `appclaw` and `appclaw "a goal"` now open the same multi-screen shell as `--tui`, in a new goal mode, instead of a readline prompt plus a single-screen Ink run. `store.mode` decides what a plain (non-slash) line means: one deterministic instruction to record, or one goal to run. `goal-session.ts` is the pipeline both callers share — decompose, orchestrate sub-goals, run the agent, summarise. MCP, LLM and AppResolver are dependencies rather than things it creates, and it deliberately does not own device setup, Ink mounting, `--export` writing or process exit, since those differ between a one-shot run and a resident shell. Two Ink apps cannot share stdout, so goal mode never calls `activateInk()`; `attachRunRenderer()` lends it the renderer seam and it mounts RunScreen as one of its own screens. `AgentOptions.signal` stops a run cooperatively, checked at each step boundary — a stop lands after the action in flight finishes, which is why the screen says "stopping…" rather than implying the keypress was instant. `appclaw "goal"` holds the finished screen until a key is pressed before exiting 0/1: leaving the alternate screen erases the summary and the last device frame, which is the whole point of having watched. `APPCLAW_TUI=off` and any non-TTY keep the unattended path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
A plain line in goal mode is a goal, so a standing command list is a menu
for nothing — the transcript takes its rows instead. The column's total
height is unchanged, which is what keeps the stream panel where the frame
loop expects it. The list reappears the moment the line starts with `/`.
`layout.ts` owns those numbers so MainScreen and CommandPalette cannot
disagree about how tall the prompt is.
`^r` starts or resumes the stream, `^p` pauses/resumes, `^x` closes it —
bound on both the goal prompt and the run screen by `stream-keys.ts`,
which holds the chord table, the handler and the status-bar hints
together so a binding cannot outlive its advertisement.
They are ctrl chords because the goal prompt is a focused text input,
where a bare `p` is part of the goal being typed. That is why
`PromptInput` replaces `ink-text-input`, which inserts any ctrl chord it
does not recognise as a plain letter — `^p` typed "p" and no screen
holding a focused prompt could own a shortcut. It also snaps the cursor
to the end when the value is replaced from outside, fixing history recall
and tab completion landing the next character mid-string.
StepLine's cells get `flexShrink={0}` and `wrap="truncate"`, and RunScreen
sizes from `RunWidthContext` rather than `stdout.columns`: in a column
beside the stream, Yoga reflowed the fixed-width grid onto a second row
and the summary box drew wider than the pane that clipped it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
The run screen was the only place the journey summary was drawn, so leaving it took the sub-goal breakdown, tokens and cost with it — the shell came back to the prompt holding one line, and re-running the agent was the only way to see any of it again. `summariseOutcome()` writes them into the transcript as the result entry's detail block, sub-goal results included, since what the agent found is the reason the run happened. `/export` now works in both modes, from different material: the recorded step list in record mode, the last goal run in goal mode, through the same writer `appclaw "goal" --export` uses. The resident shell is where you iterate on a goal until it does the right thing, which is exactly when you want it frozen into a deterministic spec. The run's platform is captured with the outcome rather than read at export time, so a `/device` switch in between cannot write an iOS spec for an Android run. Note: the `/export` dispatch and the transcript wiring live in `tui/commands.ts` and `tui/index.ts`, which landed with the goal-mode commit that rewrote them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
`Wordmark` comes out of WelcomeScreen so both entry screens draw the same banner and its width fallback lives in one place. The status bar clipped its breadcrumb to "Goa" between 100 and 118 columns; the breadcrumb no longer shrinks, so the hints truncate instead. Docs updated across README, CLAUDE.md, the CLI skill and the landing page's usage guide for goal mode, the stream chords, `/export` and the config behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
The previous fixture was a structurally valid Google API key. Whatever its provenance, a credential-shaped string in a test trips secret scanners and invites the next person to paste a real one in the same slot. The replacement still exercises the masking — long enough to be truncated, with four distinguishable trailing characters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 31, 2026
## [2.6.0](v2.5.0...v2.6.0) (2026-08-31) ### Features * **tui:** run goals in Terminal Studio ([#61](#61)) ([fa717e9](fa717e9))
|
🎉 This PR is included in version 2.6.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
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.
Bare
appclawandappclaw "a goal"now open Terminal Studio in a new goal mode, instead of the readline prompt plus single-screen Ink run. Same shell as--tui, same device session, same palette and history — only what a plain (non-slash) line means differs: an instruction to record, or a goal to run.That change pulled in four others: the goal pipeline had to come out of
index.tsbefore two callers could share it; the prompt had to stop eating keyboard shortcuts before goal mode could have any; a finished run had to leave its summary somewhere reachable; and a bad.envhad to stop killing the process before any of it could be reported on screen.Goal mode
store.modeis'record' | 'goal'.--tuiopens inrecord(a plain line is one deterministic instruction, appended to the recording); bareappclawopens ingoal(a plain line is a goal, run through the full planner and drawn on a newrunscreen beside the device stream)./modeswitches without dropping the session.packages/cli/src/goal-session.ts(new) —decomposeGoal→ per-sub-goal orchestration →runAgent→ journey summary. MCP, LLM and AppResolver are dependencies rather than things it creates, so the one-shot CLI and the resident shell drive the same pipeline. It deliberately does not own device setup, Ink mounting,--exportwriting or process exit — those differ between a one-shot run and a resident shell.index.tsis 429 lines lighter.attachRunRenderer()— two Ink apps cannot share stdout, so goal mode never callsactivateInk(); it borrows only the renderer seam and mountsRunScreenas one of its own screens.AgentOptions.signal— cooperative cancellation, checked at each step boundary. A stop lands after the action in flight finishes, which is why the screen says "stopping…" rather than implying the keypress was instant.appclaw "goal"holds the finished screen until a key is pressed before exiting 0/1. Leaving the alternate screen erases the summary and the last device frame, which is the whole point of having watched.APPCLAW_TUI=offkeeps the unattended path.The palette is hidden in goal mode, not filtered
A plain line there is a goal, so a standing command list is a menu for nothing — the transcript gets its rows instead. The column's total height is unchanged, which is what keeps the stream panel where the frame loop expects it. The list reappears as soon as the line starts with
/, where it is a live filter, and a wrapped prompt then borrows its extra rows from the transcript.layout.tsowns those numbers (paletteRows,transcriptRows,inputLineBudget,inputLineCount) so MainScreen and CommandPalette cannot disagree about how tall the prompt is.Stream shortcuts, and the input rewrite they needed
^rstarts or resumes the stream,^ppauses/resumes,^xcloses it — bound on both the goal prompt and the run screen bytui/stream-keys.ts, which holds the chord table, the handler and the status-bar hints together so a binding cannot outlive its advertisement. Everything the stream says about itself now names a chord rather than a slash command, because goal mode hides the palette.They are ctrl chords because the goal prompt is a focused text input, where a bare
pis part of the goal being typed. That is whycomponents/PromptInput.tsxreplacesink-text-input: that component inserts any ctrl chord it does not recognise as a plain letter (^ptypesp), so no screen holding a focused prompt could own a shortcut.PromptInputignores every ctrl/meta chord (ctrl+c included, so TuiApp still quits), leaves arrows/tab/page keys to the screen, and snaps the cursor to the end when the value is replaced from outside — which also fixes history recall and tab completion landing the next character mid-string.StreamStatusgainedpaused, genuinely distinct fromidle: closing stops the loop, removes the temp dir and deletes the transmitted kitty image, whereas pausing only gates capture and leaves the frame on screen for a free resume./exportand the journey summary survive a runThe run screen was the only place the journey summary was drawn, so leaving it took the sub-goal breakdown, tokens and cost with it — re-running the agent was the only way to see any of it again.
summariseOutcome()(tui/goal-summary.ts) writes them into the transcript as the result entry's detail block, sub-goal results included, since what the agent found is the reason the run happened./exportnow works in both modes, from different material: the recorded step list in record mode, the last goal run in goal mode, through the same writerappclaw "goal" --exportuses. The resident shell is where you iterate on a goal until it does the right thing, which is exactly when you want it frozen into a deterministic spec. The run's platform is captured with the outcome rather than read at export time, so a/deviceswitch in between cannot write an iOS spec for an Android run.A rejected
.envvalue no longer crashes with a stack traceConfigis built at import time, so athrowthere escaped every try/catch in the CLI and Node printed a rawZodErrorand a stack trace because someone typedLLM_PROVIDER=claude.safeLoadConfig()reports instead of throwing, dropping only the offending keys so their schema defaults apply and the rest of the environment survives — enough to say what is wrong, never enough to run on.loadConfig()still throws for the SDK, now with a readable message.Who reports it is decided in one place,
opensTerminalStudio(cliArgs): the shell can rewrite.env, so it gets the problem onSetupScreenand can fix it in place; every other path takesprintError+ exit 1 rather than running on defaults the user did not choose. Settings shows a rejected key's raw.envvalue rather than the default it fell back to (the typo is the thing being corrected) and appends any offending key not already listed, so "press enter to edit it here" is never a dead end.Smaller fixes
StepLinecells gotflexShrink={0}+wrap="truncate"— in a narrow column Yoga reflowed the fixed-width grid onto a second row ([1/3over0]), scrambling the row and costing the caller a row its height budget never reserved.RunScreensizes fromRunWidthContextrather thanstdout.columns, which are the same thing only when the run owns the whole terminal; in a pane the summary box drew wider than its column andoverflow: hiddensliced its border off.StatusBarbreadcrumb no longer clipped to "Goa" at 100–118 columns.Wordmarkextracted fromWelcomeScreen.Docs
README.md,CLAUDE.md,.agents/skills/use-appclaw-cli/SKILL.mdandlanding/public/usage.htmlupdated for goal mode, the chords,/exportand the config behaviour.🤖 Generated with Claude Code
https://claude.ai/code/session_0163wRmYBwSsV8xUDmuiEZio