Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export function ActivityLogItem({ run }: ActivityLogItemProps) {
const { t, i18n } = useTranslation("openhands");
const hasConversation = !!run.conversation_id;
const hasBashCommand = !!run.bash_command_id;
const hasErrorDetail = !!run.error_detail?.trim();
const hasDebugInfo = hasBashCommand || hasErrorDetail;
// Only surface "Conversation not created" when the run has reached a
// terminal status without a conversation — i.e. the conversation truly
// will not be created (e.g. sandbox provisioning failed). While
Expand Down Expand Up @@ -73,7 +75,7 @@ export function ActivityLogItem({ run }: ActivityLogItemProps) {
setLogsOpen(true);
};

const logsButton = hasBashCommand ? (
const logsButton = hasDebugInfo ? (
<button
type="button"
onClick={handleLogsClick}
Expand Down Expand Up @@ -120,10 +122,11 @@ export function ActivityLogItem({ run }: ActivityLogItemProps) {
</div>
)}

{hasBashCommand && (
{hasDebugInfo && (
<RunLogsModal
conversationId={run.conversation_id}
bashCommandId={run.bash_command_id}
errorDetail={run.error_detail}
isOpen={logsOpen}
onClose={() => setLogsOpen(false)}
/>
Expand Down
57 changes: 57 additions & 0 deletions src/components/features/automations/detail/run-logs-modal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { RunLogsModal } from "./run-logs-modal";
import { useBashCommandLogs } from "#/hooks/query/use-bash-command-logs";

vi.mock("#/hooks/query/use-bash-command-logs", () => ({
useBashCommandLogs: vi.fn(),
}));

const mockUseBashCommandLogs = vi.mocked(useBashCommandLogs);

describe("RunLogsModal", () => {
it("appends automation error detail without replacing fetched output", () => {
mockUseBashCommandLogs.mockReturnValue({
data: [
{
id: "output-1",
command_id: "cmd-1",
bash_command_id: "cmd-1",
order: 0,
stdout: "existing stdout\n",
stderr: "",
timestamp: "2026-06-15T17:00:34.000Z",
},
],
error: null,
isFetching: false,
isPending: false,
isResolvingConversation: false,
conversationMissing: false,
sandboxIssue: null,
});

render(
<RunLogsModal
conversationId="conv-1"
bashCommandId="cmd-1"
errorDetail={"Timed out: command timed out or was killed\nstderr log"}
isOpen
onClose={() => {}}
/>,
);

expect(screen.getByTestId("run-logs-output-stdout")).toHaveTextContent(
"existing stdout",
);
expect(screen.getByTestId("run-logs-error-detail")).toHaveTextContent(
"AUTOMATIONS$DETAIL$LOGS_ERROR_DETAIL",
);
expect(screen.getByTestId("run-logs-error-detail")).toHaveTextContent(
"Timed out: command timed out or was killed",
);
expect(screen.getByTestId("run-logs-error-detail")).toHaveTextContent(
"stderr log",
);
});
});
19 changes: 19 additions & 0 deletions src/components/features/automations/detail/run-logs-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface RunLogsModalProps {
conversationId: string | null;
/** Bash command id to fetch logs for. */
bashCommandId: string | null;
/** Error detail recorded on the automation run by the automation backend. */
errorDetail?: string | null;
isOpen: boolean;
onClose: () => void;
}
Expand All @@ -50,6 +52,7 @@ function concatStream(outputs: BashOutput[], key: "stdout" | "stderr"): string {
export function RunLogsModal({
conversationId,
bashCommandId,
errorDetail,
isOpen,
onClose,
}: RunLogsModalProps) {
Expand Down Expand Up @@ -97,6 +100,8 @@ export function RunLogsModal({
const loading = isResolvingConversation || (isFetching && !outputs);
const noBashCommand = !bashCommandId;
const activeBody = activeTab === "stdout" ? stdout : stderr;
const normalizedErrorDetail = errorDetail?.trim() ?? "";
const hasErrorDetail = normalizedErrorDetail.length > 0;

const tabBaseClass =
"border-b-2 px-3 py-2 text-sm font-normal transition-colors focus:outline-none";
Expand Down Expand Up @@ -230,6 +235,20 @@ export function RunLogsModal({
)}
</pre>
)}

{hasErrorDetail && (
<section
data-testid="run-logs-error-detail"
className="mt-4 border-t border-[var(--oh-border)] pt-4"
>
<h3 className="mb-2 text-xs font-medium uppercase text-muted">
{t(I18nKey.AUTOMATIONS$DETAIL$LOGS_ERROR_DETAIL)}
</h3>
<pre className="whitespace-pre-wrap break-words text-danger">
{normalizedErrorDetail}
</pre>
</section>
)}
</div>
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -13565,6 +13565,23 @@
"uk": "Пісочниця недоступна — її могли зупинити або зменшити масштаб.",
"ca": "El sandbox no és accessible — pot haver-se aturat o reduït."
},
"AUTOMATIONS$DETAIL$LOGS_ERROR_DETAIL": {
"en": "Automation error detail",
"ja": "自動化エラー詳細",
"zh-CN": "自动化错误详情",
"zh-TW": "自動化錯誤詳細",
"ko-KR": "자동화 오류 세부 정보",
"no": "Feildetaljer for automatisering",
"it": "Dettagli errore automazione",
"pt": "Detalhes do erro da automação",
"es": "Detalle del error de automatización",
"ar": "تفاصيل خطأ الأتمتة",
"fr": "Détail de l’erreur d’automatisation",
"tr": "Otomasyon hata ayrıntısı",
"de": "Details zum Automatisierungsfehler",
"uk": "Деталі помилки автоматизації",
"ca": "Detall de l'error d'automatització"
},
"AUTOMATIONS$DETAIL$TIME_JUST_NOW": {
"en": "Just now",
"ja": "たった今",
Expand Down
Loading