Skip to content

Commit 52432fe

Browse files
committed
feat(inbox): show research task logs inline in report detail pane
- Add signal_report field to Task type (FK from backend's sig/add-signal-report-task branch) - Add signalReportId to TaskCreationInput and pass signal_report through API - Add ReportTaskLogs component that finds the backend research task (origin_product: "signal_report") for a report and embeds TaskLogsPanel - Show live-streaming task progress in the report detail pane during research - Only matches backend research tasks, not user-created "Run cloud" tasks Depends on PostHog/posthog sig/add-signal-report-task branch.
1 parent 84cdcd4 commit 52432fe

File tree

7 files changed

+250
-4
lines changed

7 files changed

+250
-4
lines changed

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,14 @@ export class PostHogAPIClient {
440440
async createTask(
441441
options: Pick<Task, "description"> &
442442
Partial<
443-
Pick<Task, "title" | "repository" | "json_schema" | "origin_product">
443+
Pick<
444+
Task,
445+
| "title"
446+
| "repository"
447+
| "json_schema"
448+
| "origin_product"
449+
| "signal_report"
450+
>
444451
> & {
445452
github_integration?: number | null;
446453
},

apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore";
5959
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6060
import { toast } from "sonner";
6161
import { ReportCard } from "./ReportCard";
62+
import { ReportTaskLogs } from "./ReportTaskLogs";
6263
import { SignalCard } from "./SignalCard";
6364
import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge";
6465
import { SignalReportSummaryMarkdown } from "./SignalReportSummaryMarkdown";
@@ -601,6 +602,7 @@ export function InboxSignalsTab() {
601602
style={{ flex: 1 }}
602603
>
603604
<Flex direction="column" gap="2" p="2" className="min-w-0">
605+
{/* ── Description ─────────────────────────────────────── */}
604606
<SignalReportSummaryMarkdown
605607
content={selectedReport.summary}
606608
fallback="No summary available."
@@ -652,6 +654,7 @@ export function InboxSignalsTab() {
652654
</Box>
653655
)}
654656

657+
{/* ── Signals ─────────────────────────────────────────── */}
655658
{signals.length > 0 && (
656659
<Box>
657660
<Text
@@ -675,6 +678,7 @@ export function InboxSignalsTab() {
675678
</Text>
676679
)}
677680

681+
{/* ── Evidence ────────────────────────────────────────── */}
678682
<Box>
679683
<Text
680684
size="1"
@@ -743,6 +747,12 @@ export function InboxSignalsTab() {
743747
</Box>
744748
</Flex>
745749
</ScrollArea>
750+
{/* ── Research task logs (bottom preview + overlay) ─────── */}
751+
<ReportTaskLogs
752+
key={selectedReport.id}
753+
reportId={selectedReport.id}
754+
reportStatus={selectedReport.status}
755+
/>
746756
</>
747757
);
748758
} else {
@@ -910,6 +920,7 @@ export function InboxSignalsTab() {
910920
flex: 1,
911921
minWidth: 0,
912922
height: "100%",
923+
position: "relative",
913924
}}
914925
>
915926
{rightPaneContent}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { TaskLogsPanel } from "@features/task-detail/components/TaskLogsPanel";
2+
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
3+
import {
4+
CaretUpIcon,
5+
CheckCircleIcon,
6+
CircleNotchIcon,
7+
XCircleIcon,
8+
} from "@phosphor-icons/react";
9+
import { Flex, Spinner, Text } from "@radix-ui/themes";
10+
import type { SignalReportStatus, Task } from "@shared/types";
11+
import { useState } from "react";
12+
13+
function useReportTask(reportId: string) {
14+
return useAuthenticatedQuery<Task | null>(
15+
["inbox", "report-task", reportId],
16+
async (client) => {
17+
const tasks = (await client.getTasks({
18+
originProduct: "signal_report",
19+
})) as unknown as Task[];
20+
return tasks.find((t) => t.signal_report === reportId) ?? null;
21+
},
22+
{
23+
enabled: !!reportId,
24+
staleTime: 10_000,
25+
},
26+
);
27+
}
28+
29+
function getTaskStatusSummary(task: Task): {
30+
label: string;
31+
color: string;
32+
icon: React.ReactNode;
33+
} {
34+
const status = task.latest_run?.status;
35+
switch (status) {
36+
case "started":
37+
case "in_progress":
38+
return {
39+
label: task.latest_run?.stage
40+
? `Running — ${task.latest_run.stage}`
41+
: "Running…",
42+
color: "var(--amber-9)",
43+
icon: <CircleNotchIcon size={14} className="animate-spin" />,
44+
};
45+
case "completed":
46+
return {
47+
label: "Completed",
48+
color: "var(--green-9)",
49+
icon: <CheckCircleIcon size={14} weight="fill" />,
50+
};
51+
case "failed":
52+
return {
53+
label: "Failed",
54+
color: "var(--red-9)",
55+
icon: <XCircleIcon size={14} weight="fill" />,
56+
};
57+
case "cancelled":
58+
return {
59+
label: "Cancelled",
60+
color: "var(--gray-9)",
61+
icon: <XCircleIcon size={14} />,
62+
};
63+
default:
64+
return {
65+
label: "Queued",
66+
color: "var(--gray-9)",
67+
icon: <Spinner size="1" />,
68+
};
69+
}
70+
}
71+
72+
const BAR_HEIGHT = 38;
73+
74+
interface ReportTaskLogsProps {
75+
reportId: string;
76+
reportStatus: SignalReportStatus;
77+
}
78+
79+
export function ReportTaskLogs({
80+
reportId,
81+
reportStatus,
82+
}: ReportTaskLogsProps) {
83+
const { data: task, isLoading } = useReportTask(reportId);
84+
const [expanded, setExpanded] = useState(false);
85+
86+
const showBar =
87+
isLoading ||
88+
!!task ||
89+
reportStatus === "in_progress" ||
90+
reportStatus === "ready";
91+
92+
if (!showBar) {
93+
return null;
94+
}
95+
96+
const hasTask = !isLoading && !!task;
97+
98+
// No task — simple in-flow status bar
99+
if (!hasTask) {
100+
return (
101+
<Flex
102+
align="center"
103+
gap="2"
104+
px="3"
105+
py="2"
106+
className="shrink-0 border-gray-5 border-t"
107+
style={{ height: BAR_HEIGHT }}
108+
>
109+
<Spinner size="1" />
110+
<Text size="1" color="gray" className="text-[12px]">
111+
{isLoading
112+
? "Loading task…"
113+
: reportStatus === "in_progress"
114+
? "Research is running…"
115+
: "No research task yet."}
116+
</Text>
117+
</Flex>
118+
);
119+
}
120+
121+
const status = getTaskStatusSummary(task);
122+
123+
return (
124+
<>
125+
{/* In-flow spacer — same height as the bar */}
126+
<div
127+
className="shrink-0 border-gray-5 border-t"
128+
style={{ height: BAR_HEIGHT }}
129+
/>
130+
131+
{/* Scrim — biome-ignore: scrim is a non-semantic dismissal target */}
132+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: scrim dismiss */}
133+
{/* biome-ignore lint/a11y/noStaticElementInteractions: scrim dismiss */}
134+
<div
135+
onClick={expanded ? () => setExpanded(false) : undefined}
136+
style={{
137+
position: "absolute",
138+
inset: 0,
139+
zIndex: 10,
140+
background: "rgba(0, 0, 0, 0.32)",
141+
opacity: expanded ? 1 : 0,
142+
transition: "opacity 0.2s ease",
143+
pointerEvents: expanded ? "auto" : "none",
144+
}}
145+
/>
146+
147+
{/* Sliding card — animates `top` to avoid a Chromium layout
148+
bug with `transform` on absolute elements in flex+scroll. */}
149+
<div
150+
style={{
151+
position: "absolute",
152+
left: 0,
153+
right: 0,
154+
bottom: 0,
155+
zIndex: 11,
156+
display: "flex",
157+
flexDirection: "column",
158+
borderTop: "1px solid var(--gray-6)",
159+
background: "var(--color-background)",
160+
pointerEvents: "none",
161+
top: expanded ? "15%" : `calc(100% - ${BAR_HEIGHT}px)`,
162+
transition: "top 0.25s cubic-bezier(0.32, 0.72, 0, 1)",
163+
}}
164+
>
165+
<button
166+
type="button"
167+
onClick={() => setExpanded((v) => !v)}
168+
style={{ pointerEvents: "auto" }}
169+
className="flex w-full shrink-0 cursor-pointer items-center gap-2 border-gray-5 border-b bg-transparent px-3 py-2 text-left transition-colors hover:bg-gray-2"
170+
>
171+
<span style={{ color: status.color }}>{status.icon}</span>
172+
<Text size="1" weight="medium" className="flex-1 text-[12px]">
173+
Research task
174+
</Text>
175+
<Text
176+
size="1"
177+
className="text-[11px]"
178+
style={{ color: status.color }}
179+
>
180+
{status.label}
181+
</Text>
182+
<span
183+
className="inline-flex text-gray-9"
184+
style={{
185+
transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
186+
transition: "transform 0.2s ease",
187+
}}
188+
>
189+
<CaretUpIcon size={12} />
190+
</span>
191+
</button>
192+
193+
<div
194+
style={{
195+
flex: 1,
196+
minHeight: 0,
197+
overflow: "hidden",
198+
pointerEvents: expanded ? "auto" : "none",
199+
}}
200+
>
201+
<TaskLogsPanel
202+
taskId={task.id}
203+
task={task}
204+
hideInput={reportStatus !== "ready"}
205+
hideCloudStatus
206+
/>
207+
</div>
208+
</div>
209+
</>
210+
);
211+
}

apps/code/src/renderer/features/sessions/components/SessionView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface SessionViewProps {
6161
slackThreadUrl?: string;
6262
compact?: boolean;
6363
isActiveSession?: boolean;
64+
/** Hide the message input and permission UI — log-only view. */
65+
hideInput?: boolean;
6466
}
6567

6668
const DEFAULT_ERROR_MESSAGE =
@@ -90,6 +92,7 @@ export function SessionView({
9092
slackThreadUrl,
9193
compact = false,
9294
isActiveSession = true,
95+
hideInput = false,
9396
}: SessionViewProps) {
9497
const showRawLogs = useShowRawLogs();
9598
const { setShowRawLogs } = useSessionViewActions();
@@ -502,7 +505,7 @@ export function SessionView({
502505
)}
503506
</Flex>
504507
</Flex>
505-
) : firstPendingPermission ? (
508+
) : hideInput ? null : firstPendingPermission ? (
506509
<Box className="border-gray-4 border-t">
507510
<Box className="mx-auto max-w-[750px] p-2">
508511
<PermissionSelector

apps/code/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,18 @@ import { useCallback, useEffect, useMemo } from "react";
2828
interface TaskLogsPanelProps {
2929
taskId: string;
3030
task: Task;
31+
/** Hide the message input — log-only view. */
32+
hideInput?: boolean;
33+
/** Hide the cloud status footer bar. */
34+
hideCloudStatus?: boolean;
3135
}
3236

33-
export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
37+
export function TaskLogsPanel({
38+
taskId,
39+
task,
40+
hideInput,
41+
hideCloudStatus,
42+
}: TaskLogsPanelProps) {
3443
const isWorkspaceLoaded = useWorkspaceLoaded();
3544
const { isPending: isCreatingWorkspace } = useCreateWorkspace();
3645
const repoKey = getTaskRepository(task);
@@ -158,14 +167,15 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
158167
hasError={hasError}
159168
errorTitle={errorTitle}
160169
errorMessage={errorMessage}
170+
hideInput={hideInput}
161171
onRetry={isCloud ? undefined : handleRetry}
162172
onNewSession={isCloud ? undefined : handleNewSession}
163173
isInitializing={isInitializing}
164174
slackThreadUrl={slackThreadUrl}
165175
/>
166176
</ErrorBoundary>
167177
</Box>
168-
{isCloud && (
178+
{isCloud && !hideCloudStatus && (
169179
<Flex
170180
align="center"
171181
justify="center"

apps/code/src/renderer/sagas/task/task-creation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface TaskCreationInput {
7272
reasoningLevel?: string;
7373
environmentId?: string;
7474
sandboxEnvironmentId?: string;
75+
signalReportId?: string;
7576
}
7677

7778
export interface TaskCreationOutput {
@@ -388,6 +389,8 @@ export class TaskCreationSaga extends Saga<
388389
input.workspaceMode === "cloud"
389390
? input.githubIntegrationId
390391
: undefined,
392+
origin_product: input.signalReportId ? "signal_report" : undefined,
393+
signal_report: input.signalReportId ?? undefined,
391394
});
392395
return result as unknown as Task;
393396
},

apps/code/src/shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface Task {
3838
repository?: string | null; // Format: "organization/repository" (e.g., "posthog/posthog-js")
3939
github_integration?: number | null;
4040
json_schema?: Record<string, unknown> | null;
41+
signal_report?: string | null;
4142
latest_run?: TaskRun;
4243
}
4344

0 commit comments

Comments
 (0)