Skip to content

Commit b714e58

Browse files
asanmateuclaude
andcommitted
fix: exit app directly when Esc pressed during extraction/summarization with initialInput
Previously, pressing Esc while extracting or summarizing with a positional argument (tldr <url>) would drop into the interactive shell. Now it exits the app, matching the existing behavior for the q key. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fbd4eae commit b714e58

File tree

3 files changed

+98
-86
lines changed

3 files changed

+98
-86
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- App exits directly when pressing Esc during extraction/summarization if launched with a positional argument (`tldr <url>`), instead of returning to the interactive prompt
13+
1014
## [2.0.0] - 2026-02-19
1115

1216
### Added

src/App.tsx

Lines changed: 90 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -143,98 +143,106 @@ export function App({ initialInput, showConfig, editProfile, overrides }: AppPro
143143
})();
144144
}, []);
145145

146-
const processInput = useCallback(async (rawInput: string, cfg: Config) => {
147-
const activeConfig = cfg;
148-
abortRef.current?.abort();
149-
const controller = new AbortController();
150-
abortRef.current = controller;
151-
const { signal } = controller;
152-
153-
try {
154-
// Extraction phase
155-
setState("extracting");
156-
setInput(rawInput);
157-
setSummary("");
158-
setExtraction(undefined);
159-
setPendingResult(undefined);
160-
setSummaryPinned(false);
161-
setPinnedSummaries([]);
162-
163-
const result = await extract(rawInput, signal);
164-
165-
if (!result.content && !result.image) {
166-
setError({
167-
message: "Couldn't extract content from this input.",
168-
hint: result.partial
169-
? "This content may be behind a paywall. Try pasting the text directly."
170-
: undefined,
171-
});
172-
setState("error");
173-
return;
174-
}
146+
const processInput = useCallback(
147+
async (rawInput: string, cfg: Config) => {
148+
const activeConfig = cfg;
149+
abortRef.current?.abort();
150+
const controller = new AbortController();
151+
abortRef.current = controller;
152+
const { signal } = controller;
175153

176-
setExtraction(result);
154+
try {
155+
// Extraction phase
156+
setState("extracting");
157+
setInput(rawInput);
158+
setSummary("");
159+
setExtraction(undefined);
160+
setPendingResult(undefined);
161+
setSummaryPinned(false);
162+
setPinnedSummaries([]);
177163

178-
// Truncation for very long content (skip for images)
179-
let effectiveConfig = activeConfig;
180-
if (!result.image) {
181-
let content = result.content;
182-
const words = content.split(/\s+/);
183-
if (words.length > MAX_INPUT_WORDS) {
184-
content = words.slice(0, MAX_INPUT_WORDS).join(" ");
185-
result.content = content;
186-
}
164+
const result = await extract(rawInput, signal);
187165

188-
// Scale max_tokens for long content
189-
if (words.length > 10_000) {
190-
effectiveConfig = {
191-
...activeConfig,
192-
maxTokens: Math.min(activeConfig.maxTokens * 2, 4096),
193-
};
166+
if (!result.content && !result.image) {
167+
setError({
168+
message: "Couldn't extract content from this input.",
169+
hint: result.partial
170+
? "This content may be behind a paywall. Try pasting the text directly."
171+
: undefined,
172+
});
173+
setState("error");
174+
return;
194175
}
195-
}
196176

197-
// Summarization phase
198-
setState("summarizing");
199-
setIsStreaming(true);
177+
setExtraction(result);
200178

201-
const tldrResult = await summarize(
202-
result,
203-
effectiveConfig,
204-
(chunk) => setSummary((prev) => prev + chunk),
205-
signal,
206-
);
179+
// Truncation for very long content (skip for images)
180+
let effectiveConfig = activeConfig;
181+
if (!result.image) {
182+
let content = result.content;
183+
const words = content.split(/\s+/);
184+
if (words.length > MAX_INPUT_WORDS) {
185+
content = words.slice(0, MAX_INPUT_WORDS).join(" ");
186+
result.content = content;
187+
}
207188

208-
setIsStreaming(false);
189+
// Scale max_tokens for long content
190+
if (words.length > 10_000) {
191+
effectiveConfig = {
192+
...activeConfig,
193+
maxTokens: Math.min(activeConfig.maxTokens * 2, 4096),
194+
};
195+
}
196+
}
209197

210-
// Compute session paths BEFORE transitioning to result state
211-
// so that currentSession.audioPath is available if user presses 'a' quickly
212-
try {
213-
const sessionPaths = getSessionPaths(activeConfig.outputDir, result, tldrResult.summary);
214-
setCurrentSession(sessionPaths);
215-
} catch {
216-
// Non-fatal
217-
}
198+
// Summarization phase
199+
setState("summarizing");
200+
setIsStreaming(true);
201+
202+
const tldrResult = await summarize(
203+
result,
204+
effectiveConfig,
205+
(chunk) => setSummary((prev) => prev + chunk),
206+
signal,
207+
);
208+
209+
setIsStreaming(false);
210+
211+
// Compute session paths BEFORE transitioning to result state
212+
// so that currentSession.audioPath is available if user presses 'a' quickly
213+
try {
214+
const sessionPaths = getSessionPaths(activeConfig.outputDir, result, tldrResult.summary);
215+
setCurrentSession(sessionPaths);
216+
} catch {
217+
// Non-fatal
218+
}
218219

219-
// Defer persistence — store result for saving on Enter
220-
setPendingResult(tldrResult);
221-
setState("result");
222-
} catch (err) {
223-
setIsStreaming(false);
224-
// Silently return to idle on abort (ESC pressed)
225-
if (signal.aborted || (err instanceof DOMException && err.name === "AbortError")) {
226-
setState("idle");
227-
setSummary("");
228-
setExtraction(undefined);
229-
return;
220+
// Defer persistence — store result for saving on Enter
221+
setPendingResult(tldrResult);
222+
setState("result");
223+
} catch (err) {
224+
setIsStreaming(false);
225+
// Silently return on abort (ESC pressed)
226+
if (signal.aborted || (err instanceof DOMException && err.name === "AbortError")) {
227+
if (initialInput) {
228+
clearScreen();
229+
exit();
230+
return;
231+
}
232+
setState("idle");
233+
setSummary("");
234+
setExtraction(undefined);
235+
return;
236+
}
237+
const message = err instanceof Error ? err.message : "An unexpected error occurred.";
238+
setError({ message });
239+
setState("error");
240+
} finally {
241+
abortRef.current = null;
230242
}
231-
const message = err instanceof Error ? err.message : "An unexpected error occurred.";
232-
setError({ message });
233-
setState("error");
234-
} finally {
235-
abortRef.current = null;
236-
}
237-
}, []);
243+
},
244+
[clearScreen, exit, initialInput],
245+
);
238246

239247
const handleThemeChange = useCallback(async (newThemeConfig: ThemeConfig) => {
240248
setThemeConfig(newThemeConfig);

src/__tests__/app.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe("App", () => {
282282
// Group 2: Abort with ESC
283283
// -----------------------------------------------------------------------
284284
describe("abort with ESC", () => {
285-
it("ESC during extraction returns to idle", async () => {
285+
it("ESC during extraction exits app when initialInput provided", async () => {
286286
mocks.extract.mockImplementation(
287287
(_input: string, signal?: AbortSignal) =>
288288
new Promise((_resolve, reject) => {
@@ -305,15 +305,15 @@ describe("App", () => {
305305

306306
await vi.waitFor(
307307
() => {
308-
expect(instance.lastFrame()).toContain("tl;dr");
308+
expect(instance.lastFrame()).not.toContain("tl;dr");
309309
},
310310
{ timeout: 2000 },
311311
);
312312

313313
instance.unmount();
314314
});
315315

316-
it("ESC during summarization returns to idle", async () => {
316+
it("ESC during summarization exits app when initialInput provided", async () => {
317317
mocks.summarize.mockImplementation(
318318
(
319319
_result: ExtractionResult,
@@ -341,7 +341,7 @@ describe("App", () => {
341341

342342
await vi.waitFor(
343343
() => {
344-
expect(instance.lastFrame()).toContain("tl;dr");
344+
expect(instance.lastFrame()).not.toContain("tl;dr");
345345
},
346346
{ timeout: 2000 },
347347
);

0 commit comments

Comments
 (0)