ux(chat): clean tool-pill results (no raw JSON, Failed/exit verbs); structured build-health status instead of 'didn't reach a clean stopping point'; label active toolbar mode

This commit is contained in:
2026-06-10 17:44:19 -07:00
parent 6fe774719a
commit a87faa2353
3 changed files with 191 additions and 20 deletions

View File

@@ -579,6 +579,83 @@ function lastToolResultsHadFailure(messages: ChatMessage[], lookback = 3) {
return false;
}
// Pull a short, human-readable error out of the most recent failing tool
// result so the build-health status can say WHAT broke (not just "didn't
// reach a clean stopping point"). Secrets are already redacted upstream.
function extractLastToolFailure(
messages: ChatMessage[],
lookback = 4,
): string | null {
const toolMsgs = messages.filter((m) => m.role === "tool").slice(-lookback);
const clean = (s: string) => s.replace(/\s+/g, " ").trim().slice(0, 160);
for (let i = toolMsgs.length - 1; i >= 0; i--) {
const raw =
typeof toolMsgs[i].content === "string"
? (toolMsgs[i].content as string)
: "";
if (!raw) continue;
try {
const p = JSON.parse(raw);
if (typeof p.error === "string" && p.error.trim()) return clean(p.error);
if (typeof p.exitCode === "number" && p.exitCode !== 0)
return clean(
`${p.stderr || p.stdout || "command failed"} (exit ${p.exitCode})`,
);
if (typeof p.code === "number" && p.code !== 0)
return clean(
`${p.stderr || p.stdout || "command failed"} (exit ${p.code})`,
);
if (p.healthCheck?.status && p.healthCheck.status >= 400)
return clean(`health check returned ${p.healthCheck.status}`);
if (p.ok === false && typeof p.message === "string")
return clean(p.message);
} catch {
if (/(econnrefused|enoent|error|failed|exception)/i.test(raw))
return clean(raw);
}
}
return null;
}
// Deterministic, STRUCTURED build-health status used when the model's own
// wrap-up comes back empty. Replaces the old vague "didn't reach a clean
// stopping point" line with: what happened + the specific blocker + a clear
// next action.
function buildHealthStatus(opts: {
loopBreakReason?: string | null;
hitRoundCap: boolean;
lastError: string | null;
toolCount: number;
}): string {
const { loopBreakReason, hitRoundCap, lastError, toolCount } = opts;
if (lastError) {
return (
`I ran ${toolCount} step${toolCount === 1 ? "" : "s"} but hit a blocker: ` +
`**${lastError}**. I didn't want to claim success on top of that. ` +
`Want me to fix that specific issue and retry?`
);
}
if (loopBreakReason) {
return (
`I kept hitting the same wall while working on this (${loopBreakReason}), ` +
`so I stopped rather than spin. Want me to try a different approach, ` +
`or take a look together?`
);
}
if (hitRoundCap) {
return (
`I made progress across ${toolCount} step${toolCount === 1 ? "" : "s"} but ran out ` +
`of room this turn before finishing. Say "continue" and I'll pick up ` +
`exactly where I left off.`
);
}
return (
`I worked through ${toolCount} step${toolCount === 1 ? "" : "s"} but didn't land a ` +
`clean result. Want me to keep going, or take a different angle?`
);
}
export async function POST(request: Request) {
await ensureChatTables();
@@ -1374,11 +1451,17 @@ export async function POST(request: Request) {
assistantTextSegments.push(summary.text);
emit({ type: "text", text: summary.text });
} else {
// Gemini returned empty — fall back to a deterministic
// status so the user never sees silent ✓ pills.
const fallback = loopBreakReason
? `I hit a loop while working on this — ${loopBreakReason}. Want me to try a different approach, or do you want to take a look?`
: `I ran a chain of ${assistantToolCalls.length} tool calls but didn't reach a clean stopping point. Want me to keep going, or take a different angle?`;
// Gemini returned empty — fall back to a deterministic but
// STRUCTURED build-health status (never a vague "didn't reach a
// clean stopping point"). It states what happened, what broke,
// and the next action, using the same signals as the telemetry
// stop_reason.
const fallback = buildHealthStatus({
loopBreakReason,
hitRoundCap: maxToolRounds > 0 && round >= maxToolRounds,
lastError: extractLastToolFailure(messages),
toolCount: assistantToolCalls.length,
});
assistantText += (assistantText ? "\n\n" : "") + fallback;
assistantTextSegments.push(fallback);
emit({ type: "text", text: fallback });
@@ -1387,7 +1470,12 @@ export async function POST(request: Request) {
emit({ type: "thinking", text: summary.thoughts });
}
} catch {
const fallback = `I ran ${assistantToolCalls.length} tool calls but the wrap-up failed. Want me to retry, or try a different approach?`;
const fallback = buildHealthStatus({
loopBreakReason,
hitRoundCap: maxToolRounds > 0 && round >= maxToolRounds,
lastError: extractLastToolFailure(messages),
toolCount: assistantToolCalls.length,
});
assistantText += (assistantText ? "\n\n" : "") + fallback;
assistantTextSegments.push(fallback);
emit({ type: "text", text: fallback });