Skip to content

Commit 8db49c4

Browse files
egavrindevagent
andcommitted
fix(cli): surface incomplete TUI runs
- Show actionable notices when interactive turns end empty or aborted - Mark incomplete query results as error turns instead of completed - Closes #38 Co-Authored-By: devagent <devagent@egavrin>
1 parent 8572393 commit 8db49c4

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

packages/cli/src/tui/App.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,52 @@ describe("interactive prompt scrollback and status", () => {
10161016
expect(promptPlaceholders).toBeLessThanOrEqual(2);
10171017
expect(output).toContain("Bottom line: done");
10181018
});
1019+
});
1020+
1021+
describe("interactive incomplete query status", () => {
1022+
it.each([
1023+
{
1024+
name: "empty model response",
1025+
query: "reproduce stop",
1026+
status: "empty_response",
1027+
notice: "Model returned no final response. Type /continue to retry",
1028+
},
1029+
{
1030+
name: "aborted run",
1031+
query: "cancelled work",
1032+
status: "aborted",
1033+
notice: "Run stopped before completion. Type /continue to retry",
1034+
},
1035+
] as const)("surfaces an $name as an incomplete turn", async ({ query, status, notice }) => {
1036+
const view = renderForTest(
1037+
React.createElement(App, {
1038+
bus: new EventBus(),
1039+
model: "test-model",
1040+
approvalMode: "autopilot",
1041+
cwd: "/tmp/devagent",
1042+
onClear: () => {},
1043+
onCycleApprovalMode: () => {},
1044+
onQuery: async () => ({
1045+
iterations: 2,
1046+
toolCalls: 1,
1047+
lastText: null,
1048+
status,
1049+
}),
1050+
}),
1051+
);
1052+
1053+
await settle();
1054+
await typeAndSubmit(view.stdin, query);
1055+
await waitForRenders();
1056+
1057+
const output = stripAnsi(view.stdout.readAll());
1058+
expect(output).toContain(`╭─ error ${query}`);
1059+
expect(output).toContain(notice);
1060+
expect(output).not.toContain(`╭─ completed ${query}`);
1061+
});
1062+
});
10191063

1064+
describe("interactive prompt commands", () => {
10201065
it("keeps prompt scrollback stable after idle slash-command output", async () => {
10211066
const view = renderForTest(
10221067
React.createElement(App, {

packages/cli/src/tui/App.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import type { EventBus, SafetyMode } from "@devagent/runtime";
3333

3434
export const TUI_HELP_MESSAGE = "Commands: /clear (reset), /continue (resume work), /sessions (history), /exit (quit) │ Embedded shortcuts can appear anywhere: /review, /simplify │ Shift+Enter or Option+Enter for newline │ Shift+Tab toggles safety mode";
3535
export const ITERATION_LIMIT_NOTICE = "Iteration limit exhausted. Type /continue to proceed.";
36+
const EMPTY_RESPONSE_NOTICE = "Model returned no final response. Type /continue to retry, or switch provider/model if it repeats.";
37+
const ABORTED_NOTICE = "Run stopped before completion. Type /continue to retry from the current session.";
3638

3739
// ─── Types ──────────────────────────────────────────────────
3840

@@ -183,6 +185,11 @@ async function completeSuccessfulQueryTurn(query: string, context: QueryRunConte
183185
context.flushThinking();
184186
context.flushGroup();
185187
appendQueryResult(result, context);
188+
const turnStatus = result.status === "success"
189+
? "completed"
190+
: result.status === "budget_exceeded"
191+
? "budget_exceeded"
192+
: "error";
186193
context.completeTurn(
187194
context.nextId("summary"),
188195
makeTurnSummaryPart({
@@ -191,20 +198,28 @@ async function completeSuccessfulQueryTurn(query: string, context: QueryRunConte
191198
cost: context.refs.costAccum.current,
192199
elapsedMs: Date.now() - context.refs.turnStart.current,
193200
}),
194-
{ status: result.status === "budget_exceeded" ? "budget_exceeded" : "completed", finishedAt: Date.now() },
201+
{ status: turnStatus, finishedAt: Date.now() },
195202
);
196203
}
197204

198205
function appendQueryResult(result: InteractiveQueryResult, context: QueryRunContext): void {
199206
if (result.lastText) {
200207
context.appendTurnPart(context.nextId("final"), makeFinalOutputPart(result.lastText));
201208
}
202-
if (result.status === "budget_exceeded") {
203-
context.appendTurnPart(context.nextId("budget"), makeInfoPart("status", [ITERATION_LIMIT_NOTICE]));
204-
context.addToast(ITERATION_LIMIT_NOTICE, "warning");
209+
const notice = noticeForQueryStatus(result.status);
210+
if (notice) {
211+
context.appendTurnPart(context.nextId("status"), makeInfoPart("status", [notice]));
212+
context.addToast(notice, "warning");
205213
}
206214
}
207215

216+
function noticeForQueryStatus(status: InteractiveQueryResult["status"]): string | null {
217+
if (status === "budget_exceeded") return ITERATION_LIMIT_NOTICE;
218+
if (status === "empty_response") return EMPTY_RESPONSE_NOTICE;
219+
if (status === "aborted") return ABORTED_NOTICE;
220+
return null;
221+
}
222+
208223
function completeFailedQueryTurn(err: unknown, context: QueryRunContext): void {
209224
context.appendTurnPart(
210225
context.nextId("error"),

0 commit comments

Comments
 (0)