mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 23:57:27 +00:00
fix(tui): render final event error when assistant output is empty (#14687)
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/chat streaming tool-boundary text retention: merge assistant delta segments into per-run chat buffers so pre-tool text is preserved in live chat deltas/finals when providers emit post-tool assistant segments as non-prefix snapshots. (#36957) Thanks @Datyedyeguy.
|
||||
- TUI/model indicator freshness: prevent stale session snapshots from overwriting freshly patched model selection (and reset per-session freshness when switching session keys) so `/model` updates reflect immediately instead of lagging by one or more commands. (#21255) Thanks @kowza.
|
||||
- TUI/final-error rendering fallback: when a chat `final` event has no renderable assistant content but includes envelope `errorMessage`, render the formatted error text instead of collapsing to `"(no output)"`, preserving actionable failure context in-session. (#14687) Thanks @Mquarmoc.
|
||||
- OpenAI Codex OAuth/login hardening: fail OAuth completion early when the returned token is missing `api.responses.write`, and allow `openclaw models auth login --provider openai-codex` to use the built-in OAuth path even when no provider plugins are installed. (#36660) Thanks @driesvints.
|
||||
- OpenAI Codex OAuth/scope request parity: augment the OAuth authorize URL with required API scopes (`api.responses.write`, `model.request`, `api.model.read`) before browser handoff so OAuth tokens include runtime model/request permissions expected by OpenAI API calls. (#24720) Thanks @Skippy-Gunboat.
|
||||
- Gateway/remote WS break-glass hostname support: honor `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` for `ws://` hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.
|
||||
|
||||
@@ -403,6 +403,26 @@ describe("tui-event-handlers: handleAgentEvent", () => {
|
||||
expect(state.activeChatRunId).toBe("run-active");
|
||||
});
|
||||
|
||||
it("renders final error text when chat final has no content but includes event errorMessage", () => {
|
||||
const { state, chatLog, handleChatEvent } = createHandlersHarness({
|
||||
state: { activeChatRunId: null },
|
||||
});
|
||||
|
||||
handleChatEvent({
|
||||
runId: "run-error-envelope",
|
||||
sessionKey: state.currentSessionKey,
|
||||
state: "final",
|
||||
message: { content: [] },
|
||||
errorMessage: '401 {"error":{"message":"Missing scopes: model.request"}}',
|
||||
});
|
||||
|
||||
expect(chatLog.finalizeAssistant).toHaveBeenCalledTimes(1);
|
||||
const [rendered] = chatLog.finalizeAssistant.mock.calls[0] ?? [];
|
||||
expect(String(rendered)).toContain("HTTP 401");
|
||||
expect(String(rendered)).toContain("Missing scopes: model.request");
|
||||
expect(chatLog.dropAssistant).not.toHaveBeenCalledWith("run-error-envelope");
|
||||
});
|
||||
|
||||
it("drops streaming assistant when chat final has no message", () => {
|
||||
const { state, chatLog, handleChatEvent } = createHandlersHarness({
|
||||
state: { activeChatRunId: null },
|
||||
|
||||
@@ -202,7 +202,12 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
: ""
|
||||
: "";
|
||||
|
||||
const finalText = streamAssembler.finalize(evt.runId, evt.message, state.showThinking);
|
||||
const finalText = streamAssembler.finalize(
|
||||
evt.runId,
|
||||
evt.message,
|
||||
state.showThinking,
|
||||
evt.errorMessage,
|
||||
);
|
||||
const suppressEmptyExternalPlaceholder =
|
||||
finalText === "(no output)" && !isLocalRunId?.(evt.runId);
|
||||
if (suppressEmptyExternalPlaceholder) {
|
||||
|
||||
@@ -142,6 +142,7 @@ export function sanitizeRenderableText(text: string): string {
|
||||
export function resolveFinalAssistantText(params: {
|
||||
finalText?: string | null;
|
||||
streamedText?: string | null;
|
||||
errorMessage?: string | null;
|
||||
}) {
|
||||
const finalText = params.finalText ?? "";
|
||||
if (finalText.trim()) {
|
||||
@@ -151,6 +152,10 @@ export function resolveFinalAssistantText(params: {
|
||||
if (streamedText.trim()) {
|
||||
return streamedText;
|
||||
}
|
||||
const errorMessage = params.errorMessage ?? "";
|
||||
if (errorMessage.trim()) {
|
||||
return formatRawAssistantErrorForUi(errorMessage);
|
||||
}
|
||||
return "(no output)";
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,18 @@ describe("TuiStreamAssembler", () => {
|
||||
expect(finalText).toBe("Streamed");
|
||||
});
|
||||
|
||||
it("falls back to event error message when final payload has no renderable text", () => {
|
||||
const assembler = new TuiStreamAssembler();
|
||||
const finalText = assembler.finalize(
|
||||
"run-3-error",
|
||||
{ role: "assistant", content: [] },
|
||||
false,
|
||||
'401 {"error":{"message":"Missing scopes: model.request"}}',
|
||||
);
|
||||
expect(finalText).toContain("HTTP 401");
|
||||
expect(finalText).toContain("Missing scopes: model.request");
|
||||
});
|
||||
|
||||
it("returns null when delta text is unchanged", () => {
|
||||
const assembler = new TuiStreamAssembler();
|
||||
const first = assembler.ingestDelta("run-4", messageWithContent([text("Repeat")]), false);
|
||||
|
||||
@@ -174,7 +174,7 @@ export class TuiStreamAssembler {
|
||||
return state.displayText;
|
||||
}
|
||||
|
||||
finalize(runId: string, message: unknown, showThinking: boolean): string {
|
||||
finalize(runId: string, message: unknown, showThinking: boolean, errorMessage?: string): string {
|
||||
const state = this.getOrCreateRun(runId);
|
||||
const streamedDisplayText = state.displayText;
|
||||
const streamedTextBlocks = [...state.contentBlocks];
|
||||
@@ -192,6 +192,7 @@ export class TuiStreamAssembler {
|
||||
const finalText = resolveFinalAssistantText({
|
||||
finalText: shouldKeepStreamedText ? streamedDisplayText : finalComposed,
|
||||
streamedText: streamedDisplayText,
|
||||
errorMessage,
|
||||
});
|
||||
|
||||
this.runs.delete(runId);
|
||||
|
||||
@@ -23,6 +23,16 @@ describe("resolveFinalAssistantText", () => {
|
||||
}),
|
||||
).toBe("All done");
|
||||
});
|
||||
|
||||
it("falls back to formatted error text when final and streamed text are empty", () => {
|
||||
expect(
|
||||
resolveFinalAssistantText({
|
||||
finalText: "",
|
||||
streamedText: "",
|
||||
errorMessage: '401 {"error":{"message":"Missing scopes: model.request"}}',
|
||||
}),
|
||||
).toContain("HTTP 401");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tui slash commands", () => {
|
||||
|
||||
Reference in New Issue
Block a user