From d65a44b2a3d8e1cf566d523cf68b38ce4c3a687a Mon Sep 17 00:00:00 2001 From: MocA-Love Date: Sat, 4 Apr 2026 12:55:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(desktop):=20GitHub=20Actions=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=82=92=E3=83=8D=E3=82=A4=E3=83=86=E3=82=A3=E3=83=96?= =?UTF-8?q?=E3=83=9A=E3=82=A4=E3=83=B3=E3=81=A7=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review タブの Checks セクションに「View logs」ボタンを追加し、 GitHub Actions のジョブログをアプリ内のネイティブペインで閲覧可能にした。 - 新しいペインタイプ `action-logs` を追加 - 左サイドバーに全ジョブ一覧(ステータスアイコン付き、リサイズ可能) - 右側にステップごとの開閉式ログビューア - ANSI カラーコードを HTML に変換して色付き表示(ansi_up) - ステップごとの所要時間表示 - ジョブヘッダーに Re-run failed / Re-run all ボタン - ログ検索機能(ツールバーの虫眼鏡アイコン) - 設定メニュー(タイムスタンプ表示切替、GitHub で開く、raw ログ表示) - tRPC バックエンドで構造化ログ取得(ステップメタデータ + ログを並列取得・分割) --- apps/desktop/package.json | 1 + .../src/lib/trpc/routers/ui-state/index.ts | 13 + .../workspaces/procedures/git-status.ts | 30 + .../routers/workspaces/utils/github/github.ts | 101 +++ .../routers/workspaces/utils/github/index.ts | 1 + .../TabView/ActionLogsPane/ActionLogsPane.tsx | 590 ++++++++++++++++++ .../TabView/ActionLogsPane/index.ts | 1 + .../ContentView/TabsContent/TabView/index.tsx | 17 + .../components/ReviewPanel/ReviewPanel.tsx | 30 + .../desktop/src/renderer/stores/tabs/store.ts | 52 ++ .../desktop/src/renderer/stores/tabs/types.ts | 5 + .../desktop/src/renderer/stores/tabs/utils.ts | 37 ++ apps/desktop/src/shared/tabs-types.ts | 23 +- bun.lock | 3 + 14 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/ActionLogsPane.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/index.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c91df0e0065..f55c71883f1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -149,6 +149,7 @@ "@xterm/headless": "6.1.0-beta.195", "@xterm/xterm": "6.1.0-beta.195", "ai": "^6.0.0", + "ansi_up": "^6.0.6", "better-auth": "1.4.18", "better-sqlite3": "12.6.2", "bindings": "^1.5.0", diff --git a/apps/desktop/src/lib/trpc/routers/ui-state/index.ts b/apps/desktop/src/lib/trpc/routers/ui-state/index.ts index 4710ed60be8..5fe7e34ffbc 100644 --- a/apps/desktop/src/lib/trpc/routers/ui-state/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ui-state/index.ts @@ -51,6 +51,7 @@ const paneSchema = z.object({ "devtools", "git-graph", "database-explorer", + "action-logs", ]), name: z.string(), isNew: z.boolean().optional(), @@ -99,6 +100,18 @@ const paneSchema = z.object({ connectionId: z.string().nullable(), }) .optional(), + actionLogs: z + .object({ + jobs: z.array( + z.object({ + detailsUrl: z.string(), + name: z.string(), + status: z.enum(["success", "failure", "pending", "skipped", "cancelled"]), + }), + ), + initialJobIndex: z.number().optional(), + }) + .optional(), workspaceRun: z .object({ workspaceId: z.string(), diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts index 7cb48317fdf..f5e0c0d8f57 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts @@ -26,6 +26,7 @@ import { clearGitHubCachesForWorktree, extractNwoFromUrl, fetchCheckJobSteps, + fetchStructuredJobLogs, fetchGitHubPRComments, fetchGitHubPRStatus, getRepoContext, @@ -1570,5 +1571,34 @@ export const createGitStatusProcedures = () => { return fetchCheckJobSteps(repoPath, input.detailsUrl); }), + + getJobLogs: publicProcedure + .input( + z.object({ + workspaceId: z.string(), + detailsUrl: z.string(), + }), + ) + .query(async ({ input }) => { + const workspace = getWorkspace(input.workspaceId); + if (!workspace) { + return []; + } + + const worktree = workspace.worktreeId + ? getWorktree(workspace.worktreeId) + : null; + + let repoPath: string | null = worktree?.path ?? null; + if (!repoPath && workspace.type === "branch") { + const project = getProject(workspace.projectId); + repoPath = project?.mainRepoPath ?? null; + } + if (!repoPath) { + return []; + } + + return fetchStructuredJobLogs(repoPath, input.detailsUrl); + }), }); }; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts index c95d0e6c840..c7030ed8caa 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts @@ -491,3 +491,104 @@ export async function fetchCheckJobSteps( return []; } } + +export interface StructuredJobStep { + name: string; + number: number; + status: "queued" | "in_progress" | "completed"; + conclusion: string | null; + durationSeconds: number | null; + logs: string; +} + +/** + * Fetches job step metadata and logs, returning structured per-step data. + */ +export async function fetchStructuredJobLogs( + worktreePath: string, + detailsUrl: string, +): Promise { + const jobId = parseJobIdFromUrl(detailsUrl); + const nwo = parseNwoFromActionsUrl(detailsUrl); + if (!jobId || !nwo) { + return []; + } + + try { + const [jobResult, logsResult] = await Promise.all([ + execWithShellEnv( + "gh", + ["api", `repos/${nwo}/actions/jobs/${jobId}`], + { cwd: worktreePath }, + ), + execWithShellEnv( + "gh", + ["api", `repos/${nwo}/actions/jobs/${jobId}/logs`], + { cwd: worktreePath, maxBuffer: 10 * 1024 * 1024 }, + ), + ]); + + const raw: unknown = JSON.parse(jobResult.stdout.trim()); + const result = GHJobResponseSchema.safeParse(raw); + if (!result.success || !result.data.steps) { + return []; + } + + const steps = result.data.steps; + const rawLogs = logsResult.stdout; + + // Parse raw logs into per-step sections. + // GitHub log format: each line starts with a timestamp like "2024-01-01T00:00:00.0000000Z " + // Steps are separated by ##[group] / ##[endgroup] markers, but these aren't always reliable. + // Instead, match by step started_at/completed_at time ranges. + const logLines = rawLogs.split("\n"); + const stepLogs: Map = new Map(); + + // Build time ranges for each step + const stepRanges = steps.map((step) => ({ + number: step.number, + start: step.started_at ? new Date(step.started_at).getTime() : 0, + end: step.completed_at ? new Date(step.completed_at).getTime() : Number.POSITIVE_INFINITY, + })); + + for (const line of logLines) { + const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s/); + if (!tsMatch) continue; + const lineTime = new Date(tsMatch[1]).getTime(); + const lineContent = line.slice(tsMatch[0].length); + + // Find which step this line belongs to + for (const range of stepRanges) { + if (lineTime >= range.start && lineTime <= range.end + 1000) { + if (!stepLogs.has(range.number)) { + stepLogs.set(range.number, []); + } + stepLogs.get(range.number)!.push(lineContent); + break; + } + } + } + + return steps.map((step) => { + let durationSeconds: number | null = null; + if (step.started_at && step.completed_at) { + durationSeconds = Math.round( + (new Date(step.completed_at).getTime() - + new Date(step.started_at).getTime()) / + 1000, + ); + } + return { + name: step.name, + number: step.number, + status: step.status, + conclusion: step.conclusion ?? null, + durationSeconds, + logs: stepLogs.get(step.number)?.join("\n") ?? "", + }; + }); + } catch (err) { + console.error("[fetchStructuredJobLogs] Failed:", err); + return []; + } +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts index 6589eec35bb..7026684b18a 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts @@ -2,6 +2,7 @@ export type { PullRequestCommentsTarget } from "./github"; export { clearGitHubCachesForWorktree, fetchCheckJobSteps, + fetchStructuredJobLogs, fetchGitHubPRComments, fetchGitHubPRStatus, resolveReviewThread, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/ActionLogsPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/ActionLogsPane.tsx new file mode 100644 index 00000000000..0899fa77eff --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/ActionLogsPane.tsx @@ -0,0 +1,590 @@ +import { AnsiUp } from "ansi_up"; +import { useCallback, useRef, useState } from "react"; +import type { MosaicBranch } from "react-mosaic-component"; +import { + LuCheck, + LuChevronRight, + LuCircleSlash, + LuExternalLink, + LuLoaderCircle, + LuMinus, + LuRefreshCw, + LuSearch, + LuSettings, + LuX, +} from "react-icons/lu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; +import { Button } from "@superset/ui/button"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useTabsStore } from "renderer/stores/tabs/store"; +import { cn } from "@superset/ui/utils"; +import { toast } from "@superset/ui/sonner"; +import type { ActionLogsJob } from "shared/tabs-types"; +import { BasePaneWindow, PaneToolbarActions } from "../components"; + +// ── Status icon configs ── + +const statusIcon = { + success: { icon: LuCheck, className: "text-emerald-500" }, + failure: { icon: LuX, className: "text-red-500" }, + pending: { icon: LuLoaderCircle, className: "text-amber-500 animate-spin" }, + skipped: { icon: LuMinus, className: "text-muted-foreground" }, + cancelled: { icon: LuCircleSlash, className: "text-muted-foreground" }, +} as const; + +const stepStatusIcon = { + success: statusIcon.success, + failure: statusIcon.failure, + cancelled: statusIcon.cancelled, + skipped: statusIcon.skipped, + in_progress: statusIcon.pending, + queued: { icon: LuLoaderCircle, className: "text-muted-foreground" }, +} as const; + +function getStepIcon(status: string, conclusion: string | null) { + if (status === "completed" && conclusion) { + return ( + stepStatusIcon[conclusion as keyof typeof stepStatusIcon] ?? + stepStatusIcon.success + ); + } + return ( + stepStatusIcon[status as keyof typeof stepStatusIcon] ?? + stepStatusIcon.queued + ); +} + +function formatDuration(seconds: number | null): string { + if (seconds === null) return ""; + if (seconds < 60) return `${seconds}s`; + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return s > 0 ? `${m}m ${s}s` : `${m}m`; +} + +// ── ANSI rendering ── + +const ansiUp = new AnsiUp(); +ansiUp.use_classes = false; + +function renderAnsiLine(line: string): string { + // Strip ##[group], ##[endgroup], ##[command] etc. markers + const cleaned = line.replace( + /##\[(group|endgroup|command|error|warning|notice|debug)\]/g, + "", + ); + return ansiUp.ansi_to_html(cleaned); +} + +// ── Job steps component ── + +interface JobStepsProps { + workspaceId: string; + detailsUrl: string; + jobName: string; + jobStatus: string; + showTimestamps: boolean; + searchQuery: string; +} + +function JobSteps({ + workspaceId, + detailsUrl, + jobName, + jobStatus, + showTimestamps, + searchQuery, +}: JobStepsProps) { + const [expandedSteps, setExpandedSteps] = useState>(new Set()); + const rerunMutation = + electronTrpc.workspaces.rerunPullRequestChecks.useMutation(); + const trpcUtils = electronTrpc.useUtils(); + + const { data: steps, isLoading } = + electronTrpc.workspaces.getJobLogs.useQuery( + { workspaceId, detailsUrl }, + { staleTime: 30_000 }, + ); + + const toggleStep = (n: number) => { + setExpandedSteps((prev) => { + const next = new Set(prev); + next.has(n) ? next.delete(n) : next.add(n); + return next; + }); + }; + + const handleRerun = async (mode: "all" | "failed") => { + try { + const result = await rerunMutation.mutateAsync({ + workspaceId, + mode, + }); + toast.success( + `Re-running ${mode === "failed" ? "failed" : "all"} jobs (${result.rerunCount})`, + ); + void trpcUtils.workspaces.getReviewStatus.invalidate(); + } catch { + toast.error("Failed to re-run jobs"); + } + }; + + if (isLoading) { + return ( +
+ + Loading logs... +
+ ); + } + + if (!steps || steps.length === 0) { + return ( +
+ No logs available +
+ ); + } + + const totalSeconds = steps.reduce( + (sum, s) => sum + (s.durationSeconds ?? 0), + 0, + ); + const { icon: JobIcon, className: jobIconClass } = + statusIcon[jobStatus as keyof typeof statusIcon] ?? statusIcon.pending; + + const lowerQuery = searchQuery.toLowerCase(); + + return ( +
+ {/* Job header */} +
+
+
+ + + {jobName} + +
+ {totalSeconds > 0 && ( +

+ {formatDuration(totalSeconds)} +

+ )} +
+
+ + +
+
+ + {/* Steps */} + {steps.map((step) => { + const isExpanded = expandedSteps.has(step.number); + const { icon: StepIcon, className: iconClass } = getStepIcon( + step.status, + step.conclusion, + ); + const rawLines = step.logs ? step.logs.split("\n") : []; + + // Filter lines by search query + const filteredLines = + lowerQuery && rawLines.length > 0 + ? rawLines.filter((l) => + l.toLowerCase().includes(lowerQuery), + ) + : rawLines; + + const matchesSearch = + !lowerQuery || + step.name.toLowerCase().includes(lowerQuery) || + filteredLines.length > 0; + + if (!matchesSearch) return null; + + return ( +
+ + {isExpanded && filteredLines.length > 0 && ( +
+
+									{filteredLines.map((line, i) => {
+										// Parse timestamp from original line if showTimestamps
+										const tsMatch = showTimestamps
+											? line.match(
+													/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s/,
+												)
+											: null;
+										const displayLine = tsMatch
+											? line
+											: line.replace(
+													/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s/,
+													"",
+												);
+
+										return (
+											
+ + {i + 1} + + +
+ ); + })} +
+
+ )} + {isExpanded && filteredLines.length === 0 && ( +
+ {lowerQuery + ? "No matching lines" + : "No log output"} +
+ )} +
+ ); + })} +
+ ); +} + +// ── Main pane ── + +interface ActionLogsPaneProps { + paneId: string; + path: MosaicBranch[]; + tabId: string; + workspaceId: string; + splitPaneAuto: ( + tabId: string, + sourcePaneId: string, + dimensions: { width: number; height: number }, + path?: MosaicBranch[], + ) => void; + removePane: (paneId: string) => void; + setFocusedPane: (tabId: string, paneId: string) => void; + onPopOut?: () => void; +} + +export function ActionLogsPane({ + paneId, + path, + tabId, + workspaceId, + splitPaneAuto, + removePane, + setFocusedPane, + onPopOut, +}: ActionLogsPaneProps) { + const pane = useTabsStore((s) => s.panes[paneId]); + const jobs: ActionLogsJob[] = pane?.actionLogs?.jobs ?? []; + const initialIndex = pane?.actionLogs?.initialJobIndex ?? 0; + const [selectedIndex, setSelectedIndex] = useState(initialIndex); + const [showTimestamps, setShowTimestamps] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [searchOpen, setSearchOpen] = useState(false); + const searchInputRef = useRef(null); + const [sidebarWidth, setSidebarWidth] = useState(208); + const isDragging = useRef(false); + const selectedJob = jobs[selectedIndex]; + + const browserUrl = selectedJob?.detailsUrl?.match( + /(https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/\d+\/job\/\d+)/, + )?.[1]; + + const runUrl = selectedJob?.detailsUrl?.match( + /(https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/\d+)/, + )?.[1]; + + const handleResizeStart = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + isDragging.current = true; + const startX = e.clientX; + const startWidth = sidebarWidth; + + const onMouseMove = (ev: MouseEvent) => { + const newWidth = Math.min( + 400, + Math.max(120, startWidth + ev.clientX - startX), + ); + setSidebarWidth(newWidth); + }; + const onMouseUp = () => { + isDragging.current = false; + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }, + [sidebarWidth], + ); + + const handleToggleSearch = useCallback(() => { + setSearchOpen((prev) => { + if (!prev) { + setTimeout(() => searchInputRef.current?.focus(), 0); + } else { + setSearchQuery(""); + } + return !prev; + }); + }, []); + + // Get raw logs URL for "View raw logs" + const rawLogsUrl = browserUrl ? `${browserUrl}?pr=` : undefined; + + return ( + ( +
+ + Action Logs + + +
+ {/* Search */} + {searchOpen && ( +
+ + + setSearchQuery(e.target.value) + } + className="h-5 w-36 bg-transparent text-xs outline-none placeholder:text-muted-foreground/60" + onKeyDown={(e) => { + if (e.key === "Escape") { + handleToggleSearch(); + } + }} + /> + {searchQuery && ( + + )} +
+ )} + + + {/* Settings dropdown */} + + + + + + + setShowTimestamps((p) => !p) + } + > + {showTimestamps ? "Hide" : "Show"}{" "} + timestamps + + + {browserUrl && ( + + + View on GitHub + + + )} + {rawLogsUrl && ( + + + View raw logs + + + )} + + + + +
+
+ )} + > + {jobs.length === 0 ? ( +
+ No jobs configured +
+ ) : ( +
+ {/* Job sidebar */} +
+
+ All jobs +
+ {jobs.map((job, i) => { + const { icon: JobIcon, className: jobIconClass } = + statusIcon[job.status]; + return ( + + ); + })} + {/* Resize handle */} +
+
+ {/* Step detail */} +
+ {selectedJob && ( + + )} +
+
+ )} + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/index.ts new file mode 100644 index 00000000000..e4242bf980e --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ActionLogsPane/index.ts @@ -0,0 +1 @@ +export { ActionLogsPane } from "./ActionLogsPane"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx index bab84ee818a..5673f14a3ec 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx @@ -26,6 +26,7 @@ import { MosaicSplitOverlay } from "./components"; import { DatabaseExplorerPane } from "./DatabaseExplorerPane"; import { DevToolsPane } from "./DevToolsPane"; import { FileViewerPane } from "./FileViewerPane"; +import { ActionLogsPane } from "./ActionLogsPane"; import { GitGraphPane } from "./GitGraphPane"; import { TabPane } from "./TabPane"; @@ -314,6 +315,22 @@ export function TabView({ tab }: TabViewProps) { ); } + // Route action-logs panes + if (paneInfo.type === "action-logs") { + return ( + handlePopOut(paneId)} + /> + ); + } + // Route git-graph panes if (paneInfo.type === "git-graph") { return ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx index 3b7e1c5ca0f..854b1a9fcec 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx @@ -26,6 +26,7 @@ import { LuChevronDown, LuCode, LuCopy, + LuExternalLink, LuLoaderCircle, LuRefreshCw, LuX, @@ -136,6 +137,7 @@ export function ReviewPanel({ const resolvedWorkspaceId = useWorkspaceId(); const trpcUtils = electronTrpc.useUtils(); const addBrowserTab = useTabsStore((s) => s.addBrowserTab); + const addActionLogsTab = useTabsStore((s) => s.addActionLogsTab); const handleOpenUrl = useCallback( (url: string, e: React.MouseEvent) => { e.preventDefault(); @@ -1094,6 +1096,34 @@ export function ReviewPanel({ {actionChecks.length > 0 ? (
+ {resolvedWorkspaceId && ( + + )} - - setShowTimestamps((p) => !p) - } - > - {showTimestamps ? "Hide" : "Show"}{" "} - timestamps + setShowTimestamps((p) => !p)}> + {showTimestamps ? "Hide" : "Show"} timestamps {browserUrl && ( @@ -530,7 +511,7 @@ export function ActionLogsPane({
{/* Job sidebar */}
@@ -551,21 +532,16 @@ export function ActionLogsPane({ )} onClick={() => setSelectedIndex(i)} > - - - {job.name} - + + {job.name} ); })} {/* Resize handle */} -
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx index 5673f14a3ec..d77d4f83fe1 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx @@ -20,13 +20,13 @@ import { getPaneIdSetForTab, } from "renderer/stores/tabs/utils"; import { useTheme } from "renderer/stores/theme"; +import { ActionLogsPane } from "./ActionLogsPane"; import { BrowserPane } from "./BrowserPane"; import { ChatPane } from "./ChatPane"; import { MosaicSplitOverlay } from "./components"; import { DatabaseExplorerPane } from "./DatabaseExplorerPane"; import { DevToolsPane } from "./DevToolsPane"; import { FileViewerPane } from "./FileViewerPane"; -import { ActionLogsPane } from "./ActionLogsPane"; import { GitGraphPane } from "./GitGraphPane"; import { TabPane } from "./TabPane"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx index 854b1a9fcec..591c07d78f2 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ReviewPanel/ReviewPanel.tsx @@ -1104,9 +1104,9 @@ export function ReviewPanel({ className="h-6 px-2 text-[10px]" onClick={() => { const jobs = actionChecks - .filter((c) => c.url) + .filter((c): c is typeof c & { url: string } => !!c.url) .map((c) => ({ - detailsUrl: c.url!, + detailsUrl: c.url, name: c.name, status: c.status, })); diff --git a/apps/desktop/src/renderer/stores/tabs/store.ts b/apps/desktop/src/renderer/stores/tabs/store.ts index e023550bdd9..7442628c040 100644 --- a/apps/desktop/src/renderer/stores/tabs/store.ts +++ b/apps/desktop/src/renderer/stores/tabs/store.ts @@ -32,6 +32,7 @@ import { applyFileViewerOpenOptionsToPane, buildMultiPaneLayout, type CreatePaneOptions, + createActionLogsTabWithPane, createBrowserPane, createBrowserTabWithPane, createChatPane, @@ -39,7 +40,6 @@ import { createDatabaseExplorerTabWithPane, createDevToolsPane, createFileViewerPane, - createActionLogsTabWithPane, createGitGraphTabWithPane, createPane, createTabWithPane, @@ -1760,7 +1760,11 @@ export const useTabsStore = create()( addActionLogsTab: ( workspaceId: string, - jobs: Array<{ detailsUrl: string; name: string; status: "success" | "failure" | "pending" | "skipped" | "cancelled" }>, + jobs: Array<{ + detailsUrl: string; + name: string; + status: "success" | "failure" | "pending" | "skipped" | "cancelled"; + }>, initialJobIndex?: number, ) => { const state = get(); @@ -1772,14 +1776,11 @@ export const useTabsStore = create()( ); const currentActiveId = state.activeTabIds[workspaceId]; - const historyStack = - state.tabHistoryStacks[workspaceId] || []; + const historyStack = state.tabHistoryStacks[workspaceId] || []; const newHistoryStack = currentActiveId ? [ currentActiveId, - ...historyStack.filter( - (id) => id !== currentActiveId, - ), + ...historyStack.filter((id) => id !== currentActiveId), ] : historyStack; diff --git a/apps/desktop/src/renderer/stores/tabs/types.ts b/apps/desktop/src/renderer/stores/tabs/types.ts index dc157422e59..1ed7926b7d3 100644 --- a/apps/desktop/src/renderer/stores/tabs/types.ts +++ b/apps/desktop/src/renderer/stores/tabs/types.ts @@ -217,7 +217,11 @@ export interface TabsStore extends TabsState { ) => { tabId: string; paneId: string }; addActionLogsTab: ( workspaceId: string, - jobs: Array<{ detailsUrl: string; name: string; status: "success" | "failure" | "pending" | "skipped" | "cancelled" }>, + jobs: Array<{ + detailsUrl: string; + name: string; + status: "success" | "failure" | "pending" | "skipped" | "cancelled"; + }>, initialJobIndex?: number, ) => { tabId: string; paneId: string }; openInBrowserPane: (workspaceId: string, url: string) => void; diff --git a/apps/desktop/src/renderer/stores/tabs/utils.ts b/apps/desktop/src/renderer/stores/tabs/utils.ts index ae7beacad53..b69b4857138 100644 --- a/apps/desktop/src/renderer/stores/tabs/utils.ts +++ b/apps/desktop/src/renderer/stores/tabs/utils.ts @@ -11,6 +11,8 @@ import { } from "shared/changes-types"; import { hasRenderedPreview, isHtmlFile, isImageFile } from "shared/file-types"; import { + type ActionLogsJob, + type ActionLogsPaneState, acknowledgedStatus, type BrowserPaneState, type DatabaseExplorerPaneState, @@ -19,8 +21,6 @@ import { type FileViewerMode, type FileViewerState, type GitGraphPaneState, - type ActionLogsJob, - type ActionLogsPaneState, } from "shared/tabs-types"; import type { AddChatTabOptions,