Agent logs should be collapsable (vibe-kanban) (#451)
* Add collapsible agent log headers - Make process headers (Setup Script, Coding Agent, Cleanup Script) clickable to hide/show logs - Add chevron icon with rotation animation to indicate collapsed state - Filter log entries to hide those from collapsed processes while preserving virtualization - Maintain scroll position and follow-output behavior when toggling collapse - Support keyboard navigation (Enter/Space) for accessibility Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp <amp@ampcode.com> * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Commit changes from coding agent for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Cleanup (and adjust lint) * Enhance agent log collapsibility with auto-hide and dev server filtering - Filter dev server processes from logs tab to reduce noise - Auto-collapse completed setup/cleanup scripts on initial load for cleaner UX - Auto-expand scripts that restart after completion - Add process constants for type safety and consistency - Separate auto-collapsed and user-collapsed state management - Maintain scroll position and user preferences throughout Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp <amp@ampcode.com> * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Add coding agent auto-collapse for cleaner log focus - Auto-collapse all non-latest coding agents on task load for focused viewing - Detect new coding agent starts (follow-ups) and collapse previous agents - Latest coding agent always remains expanded for active monitoring - Robust latest agent detection with timestamp tie-breaking - One-shot initial collapse prevents duplicate processing - Smart follow-up detection tracks new running agents - User manual toggles permanently override auto-collapse behavior - Comprehensive state reset on attempt changes Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp <amp@ampcode.com> * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * Fix timing issue with coding agent auto-collapse - Only mark initial collapse as complete when coding agents are actually processed - Prevents race condition where flag was set before real data arrived - Ensures auto-collapse logic runs after data loads, not during empty state - Fixes issue where all coding agents remained expanded instead of latest-only Amp-Thread: https://ampcode.com/threads/T-f44b0256-de19-45e1-96cb-df755553716d Co-authored-by: Amp <amp@ampcode.com> * Cleanup script changes for task attempt 990e23da-53ff-4203-ac58-f5b28653bc1f * refactor * lints * prettier --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
59c977e235
commit
69cda33532
@@ -1,15 +1,25 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": { "browser": true, "es2020": true },
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": ["dist", ".eslintrc.json"],
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
".eslintrc.json"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react-refresh", "@typescript-eslint"],
|
||||
"plugins": [
|
||||
"react-refresh",
|
||||
"@typescript-eslint",
|
||||
"unused-imports"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
@@ -17,9 +27,19 @@
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
{
|
||||
"allowConstantExport": true
|
||||
}
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": false
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn"
|
||||
}
|
||||
}
|
||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -40,7 +40,8 @@
|
||||
"react-window": "^1.8.11",
|
||||
"rfc6902": "^5.1.2",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-debounce": "^10.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
@@ -54,6 +55,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.6.1",
|
||||
"tailwindcss": "^3.4.0",
|
||||
@@ -1122,6 +1124,15 @@
|
||||
"lowlight": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@git-diff-view/file/node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@git-diff-view/lowlight": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@git-diff-view/lowlight/-/lowlight-0.0.30.tgz",
|
||||
@@ -3805,15 +3816,6 @@
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
||||
@@ -4081,6 +4083,22 @@
|
||||
"eslint": ">=8.40"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unused-imports": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
|
||||
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
|
||||
"eslint": "^9.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
@@ -7441,6 +7459,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz",
|
||||
"integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.6.1",
|
||||
"tailwindcss": "^3.4.0",
|
||||
|
||||
@@ -11,9 +11,18 @@ interface LogEntryRowProps {
|
||||
index: number;
|
||||
style?: React.CSSProperties;
|
||||
setRowHeight?: (index: number, height: number) => void;
|
||||
isCollapsed?: boolean;
|
||||
onToggleCollapse?: (processId: string) => void;
|
||||
}
|
||||
|
||||
function LogEntryRow({ entry, index, style, setRowHeight }: LogEntryRowProps) {
|
||||
function LogEntryRow({
|
||||
entry,
|
||||
index,
|
||||
style,
|
||||
setRowHeight,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
}: LogEntryRowProps) {
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,6 +51,8 @@ function LogEntryRow({ entry, index, style, setRowHeight }: LogEntryRowProps) {
|
||||
return (
|
||||
<ProcessStartCard
|
||||
payload={entry.payload as ProcessStartPayload}
|
||||
isCollapsed={isCollapsed || false}
|
||||
onToggle={onToggleCollapse || (() => {})}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Clock, Cog, Play, Terminal, Code } from 'lucide-react';
|
||||
import { Clock, Cog, Play, Terminal, Code, ChevronDown } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ProcessStartPayload } from '@/types/logs';
|
||||
|
||||
interface ProcessStartCardProps {
|
||||
payload: ProcessStartPayload;
|
||||
isCollapsed: boolean;
|
||||
onToggle: (processId: string) => void;
|
||||
}
|
||||
|
||||
function ProcessStartCard({ payload }: ProcessStartCardProps) {
|
||||
function ProcessStartCard({
|
||||
payload,
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
}: ProcessStartCardProps) {
|
||||
const getProcessIcon = (runReason: string) => {
|
||||
switch (runReason) {
|
||||
case 'setupscript':
|
||||
@@ -40,9 +47,26 @@ function ProcessStartCard({ payload }: ProcessStartCardProps) {
|
||||
return new Date(dateString).toLocaleTimeString();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
onToggle(payload.processId);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onToggle(payload.processId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 pt-4 pb-2">
|
||||
<div className="bg-muted/50 border border-border rounded-lg p-2">
|
||||
<div
|
||||
className="bg-muted/50 border border-border rounded-lg p-2 cursor-pointer select-none hover:bg-muted/70 transition-colors"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
{getProcessIcon(payload.runReason)}
|
||||
@@ -67,6 +91,12 @@ function ProcessStartCard({ payload }: ProcessStartCardProps) {
|
||||
>
|
||||
{payload.status}
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
isCollapsed && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,275 @@
|
||||
import { useContext, useState, useRef, useCallback } from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Cog } from 'lucide-react';
|
||||
import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
TaskSelectedAttemptContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useProcessesLogs } from '@/hooks/useProcessesLogs';
|
||||
import LogEntryRow from '@/components/logs/LogEntryRow';
|
||||
import {
|
||||
shouldShowInLogs,
|
||||
isAutoCollapsibleProcess,
|
||||
isProcessCompleted,
|
||||
isCodingAgent,
|
||||
getLatestCodingAgent,
|
||||
PROCESS_STATUSES,
|
||||
} from '@/constants/processes';
|
||||
import type { ExecutionProcessStatus } from 'shared/types';
|
||||
|
||||
// Helper functions
|
||||
function addAll<T>(set: Set<T>, items: T[]): Set<T> {
|
||||
items.forEach((i: T) => set.add(i));
|
||||
return set;
|
||||
}
|
||||
|
||||
// State management types
|
||||
type LogsState = {
|
||||
userCollapsed: Set<string>;
|
||||
autoCollapsed: Set<string>;
|
||||
prevStatus: Map<string, ExecutionProcessStatus>;
|
||||
prevLatestAgent?: string;
|
||||
};
|
||||
|
||||
type LogsAction =
|
||||
| { type: 'RESET_ATTEMPT' }
|
||||
| { type: 'TOGGLE_USER'; id: string }
|
||||
| { type: 'AUTO_COLLAPSE'; ids: string[] }
|
||||
| { type: 'AUTO_EXPAND'; ids: string[] }
|
||||
| { type: 'UPDATE_STATUS'; id: string; status: ExecutionProcessStatus }
|
||||
| { type: 'NEW_RUNNING_AGENT'; id: string };
|
||||
|
||||
const initialState: LogsState = {
|
||||
userCollapsed: new Set(),
|
||||
autoCollapsed: new Set(),
|
||||
prevStatus: new Map(),
|
||||
prevLatestAgent: undefined,
|
||||
};
|
||||
|
||||
function reducer(state: LogsState, action: LogsAction): LogsState {
|
||||
switch (action.type) {
|
||||
case 'RESET_ATTEMPT':
|
||||
return { ...initialState };
|
||||
|
||||
case 'TOGGLE_USER': {
|
||||
const newUserCollapsed = new Set(state.userCollapsed);
|
||||
const newAutoCollapsed = new Set(state.autoCollapsed);
|
||||
|
||||
const isCurrentlyCollapsed =
|
||||
newUserCollapsed.has(action.id) || newAutoCollapsed.has(action.id);
|
||||
|
||||
if (isCurrentlyCollapsed) {
|
||||
// we want to EXPAND
|
||||
newUserCollapsed.delete(action.id);
|
||||
newAutoCollapsed.delete(action.id);
|
||||
} else {
|
||||
// we want to COLLAPSE
|
||||
newUserCollapsed.add(action.id);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
userCollapsed: newUserCollapsed,
|
||||
autoCollapsed: newAutoCollapsed,
|
||||
};
|
||||
}
|
||||
|
||||
case 'AUTO_COLLAPSE': {
|
||||
const newAutoCollapsed = new Set(state.autoCollapsed);
|
||||
addAll(newAutoCollapsed, action.ids);
|
||||
return {
|
||||
...state,
|
||||
autoCollapsed: newAutoCollapsed,
|
||||
};
|
||||
}
|
||||
|
||||
case 'AUTO_EXPAND': {
|
||||
const newAutoCollapsed = new Set(state.autoCollapsed);
|
||||
action.ids.forEach((id) => newAutoCollapsed.delete(id));
|
||||
return {
|
||||
...state,
|
||||
autoCollapsed: newAutoCollapsed,
|
||||
};
|
||||
}
|
||||
|
||||
case 'UPDATE_STATUS': {
|
||||
const newPrevStatus = new Map(state.prevStatus);
|
||||
newPrevStatus.set(action.id, action.status);
|
||||
return {
|
||||
...state,
|
||||
prevStatus: newPrevStatus,
|
||||
};
|
||||
}
|
||||
|
||||
case 'NEW_RUNNING_AGENT':
|
||||
return {
|
||||
...state,
|
||||
prevLatestAgent: action.id,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function LogsTab() {
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const { selectedAttempt } = useContext(TaskSelectedAttemptContext);
|
||||
const virtuosoRef = useRef<any>(null);
|
||||
|
||||
const { entries } = useProcessesLogs(attemptData.processes || [], true);
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
// Filter out dev server processes before passing to useProcessesLogs
|
||||
const filteredProcesses = useMemo(
|
||||
() =>
|
||||
(attemptData.processes || []).filter((process) =>
|
||||
shouldShowInLogs(process.run_reason)
|
||||
),
|
||||
[attemptData.processes]
|
||||
);
|
||||
|
||||
const { entries } = useProcessesLogs(filteredProcesses, true);
|
||||
|
||||
// Combined collapsed processes (auto + user)
|
||||
const allCollapsedProcesses = useMemo(() => {
|
||||
const combined = new Set(state.autoCollapsed);
|
||||
state.userCollapsed.forEach((id: string) => combined.add(id));
|
||||
return combined;
|
||||
}, [state.autoCollapsed, state.userCollapsed]);
|
||||
|
||||
// Toggle collapsed state for a process (user action)
|
||||
const toggleProcessCollapse = useCallback((processId: string) => {
|
||||
dispatch({ type: 'TOGGLE_USER', id: processId });
|
||||
}, []);
|
||||
|
||||
// Effect #1: Reset state when attempt changes
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'RESET_ATTEMPT' });
|
||||
}, [selectedAttempt?.id]);
|
||||
|
||||
// Effect #2: Handle setup/cleanup script auto-collapse and auto-expand
|
||||
useEffect(() => {
|
||||
const toCollapse: string[] = [];
|
||||
const toExpand: string[] = [];
|
||||
|
||||
filteredProcesses.forEach((process) => {
|
||||
if (isAutoCollapsibleProcess(process.run_reason)) {
|
||||
const prevStatus = state.prevStatus.get(process.id);
|
||||
const currentStatus = process.status;
|
||||
|
||||
// Auto-collapse completed setup/cleanup scripts
|
||||
const shouldAutoCollapse =
|
||||
(prevStatus === PROCESS_STATUSES.RUNNING ||
|
||||
prevStatus === undefined) &&
|
||||
isProcessCompleted(currentStatus) &&
|
||||
!state.userCollapsed.has(process.id) &&
|
||||
!state.autoCollapsed.has(process.id);
|
||||
|
||||
if (shouldAutoCollapse) {
|
||||
toCollapse.push(process.id);
|
||||
}
|
||||
|
||||
// Auto-expand scripts that restart after completion
|
||||
const becameRunningAgain =
|
||||
prevStatus &&
|
||||
isProcessCompleted(prevStatus) &&
|
||||
currentStatus === PROCESS_STATUSES.RUNNING &&
|
||||
state.autoCollapsed.has(process.id);
|
||||
|
||||
if (becameRunningAgain) {
|
||||
toExpand.push(process.id);
|
||||
}
|
||||
|
||||
// Update status tracking
|
||||
dispatch({
|
||||
type: 'UPDATE_STATUS',
|
||||
id: process.id,
|
||||
status: currentStatus,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (toCollapse.length > 0) {
|
||||
dispatch({ type: 'AUTO_COLLAPSE', ids: toCollapse });
|
||||
}
|
||||
|
||||
if (toExpand.length > 0) {
|
||||
dispatch({ type: 'AUTO_EXPAND', ids: toExpand });
|
||||
}
|
||||
}, [
|
||||
filteredProcesses,
|
||||
state.userCollapsed,
|
||||
state.autoCollapsed,
|
||||
state.prevStatus,
|
||||
]);
|
||||
|
||||
// Effect #3: Handle coding agent succession logic
|
||||
useEffect(() => {
|
||||
const latestCodingAgentId = getLatestCodingAgent(filteredProcesses);
|
||||
if (!latestCodingAgentId) return;
|
||||
|
||||
// Collapse previous agents when a new latest agent appears
|
||||
if (latestCodingAgentId !== state.prevLatestAgent) {
|
||||
// Collapse all other coding agents that aren't user-collapsed
|
||||
const toCollapse = filteredProcesses
|
||||
.filter(
|
||||
(p) =>
|
||||
isCodingAgent(p.run_reason) &&
|
||||
p.id !== latestCodingAgentId &&
|
||||
!state.userCollapsed.has(p.id) &&
|
||||
!state.autoCollapsed.has(p.id)
|
||||
)
|
||||
.map((p) => p.id);
|
||||
|
||||
if (toCollapse.length > 0) {
|
||||
dispatch({ type: 'AUTO_COLLAPSE', ids: toCollapse });
|
||||
}
|
||||
|
||||
dispatch({ type: 'NEW_RUNNING_AGENT', id: latestCodingAgentId });
|
||||
}
|
||||
}, [
|
||||
filteredProcesses,
|
||||
state.prevLatestAgent,
|
||||
state.userCollapsed,
|
||||
state.autoCollapsed,
|
||||
]);
|
||||
|
||||
// Filter entries to hide logs from collapsed processes
|
||||
const visibleEntries = useMemo(() => {
|
||||
return entries.filter((entry) =>
|
||||
entry.channel === 'process_start'
|
||||
? true
|
||||
: !allCollapsedProcesses.has(entry.processId)
|
||||
);
|
||||
}, [entries, allCollapsedProcesses]);
|
||||
|
||||
// Memoized item content to prevent flickering
|
||||
const itemContent = useCallback(
|
||||
(index: number, entry: any) => <LogEntryRow entry={entry} index={index} />,
|
||||
[]
|
||||
(index: number, entry: any) => (
|
||||
<LogEntryRow
|
||||
entry={entry}
|
||||
index={index}
|
||||
isCollapsed={
|
||||
entry.channel === 'process_start'
|
||||
? allCollapsedProcesses.has(entry.payload.processId)
|
||||
: undefined
|
||||
}
|
||||
onToggleCollapse={
|
||||
entry.channel === 'process_start' ? toggleProcessCollapse : undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
[allCollapsedProcesses, toggleProcessCollapse]
|
||||
);
|
||||
|
||||
// Handle when user manually scrolls away from bottom
|
||||
const handleAtBottomStateChange = useCallback((atBottom: boolean) => {
|
||||
setIsAtBottom(atBottom);
|
||||
}, []);
|
||||
|
||||
if (!attemptData.processes || attemptData.processes.length === 0) {
|
||||
if (!filteredProcesses || filteredProcesses.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
@@ -39,10 +285,9 @@ function LogsTab() {
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: '100%' }}
|
||||
data={entries}
|
||||
data={visibleEntries}
|
||||
itemContent={itemContent}
|
||||
followOutput={isAtBottom ? 'smooth' : false}
|
||||
atBottomStateChange={handleAtBottomStateChange}
|
||||
followOutput={true}
|
||||
increaseViewportBy={200}
|
||||
overscan={5}
|
||||
components={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import {
|
||||
Play,
|
||||
Square,
|
||||
@@ -40,12 +40,6 @@ function ProcessCard({ process }: ProcessCardProps) {
|
||||
const isConnected = isCodingAgent ? normalizedConnected : rawConnected;
|
||||
const error = isCodingAgent ? normalizedError : rawError;
|
||||
|
||||
// Auto-scroll to bottom when new logs/entries arrive
|
||||
useEffect(() => {
|
||||
if (logEndRef.current) {
|
||||
logEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [logs, entries]);
|
||||
const getStatusIcon = (status: ExecutionProcessStatus) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
|
||||
62
frontend/src/constants/processes.ts
Normal file
62
frontend/src/constants/processes.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type {
|
||||
ExecutionProcessRunReason,
|
||||
ExecutionProcessStatus,
|
||||
ExecutionProcessSummary,
|
||||
} from 'shared/types';
|
||||
|
||||
// Process run reasons
|
||||
export const PROCESS_RUN_REASONS = {
|
||||
SETUP_SCRIPT: 'setupscript' as ExecutionProcessRunReason,
|
||||
CLEANUP_SCRIPT: 'cleanupscript' as ExecutionProcessRunReason,
|
||||
CODING_AGENT: 'codingagent' as ExecutionProcessRunReason,
|
||||
DEV_SERVER: 'devserver' as ExecutionProcessRunReason,
|
||||
} as const;
|
||||
|
||||
// Process statuses
|
||||
export const PROCESS_STATUSES = {
|
||||
RUNNING: 'running' as ExecutionProcessStatus,
|
||||
COMPLETED: 'completed' as ExecutionProcessStatus,
|
||||
FAILED: 'failed' as ExecutionProcessStatus,
|
||||
KILLED: 'killed' as ExecutionProcessStatus,
|
||||
} as const;
|
||||
|
||||
// Helper functions
|
||||
export const isAutoCollapsibleProcess = (
|
||||
runReason: ExecutionProcessRunReason
|
||||
): boolean => {
|
||||
return (
|
||||
runReason === PROCESS_RUN_REASONS.SETUP_SCRIPT ||
|
||||
runReason === PROCESS_RUN_REASONS.CLEANUP_SCRIPT
|
||||
);
|
||||
};
|
||||
|
||||
export const isCodingAgent = (
|
||||
runReason: ExecutionProcessRunReason
|
||||
): boolean => {
|
||||
return runReason === PROCESS_RUN_REASONS.CODING_AGENT;
|
||||
};
|
||||
|
||||
export const isProcessCompleted = (status: ExecutionProcessStatus): boolean => {
|
||||
return (
|
||||
status === PROCESS_STATUSES.COMPLETED || status === PROCESS_STATUSES.FAILED
|
||||
);
|
||||
};
|
||||
|
||||
export const shouldShowInLogs = (
|
||||
runReason: ExecutionProcessRunReason
|
||||
): boolean => {
|
||||
return runReason !== PROCESS_RUN_REASONS.DEV_SERVER;
|
||||
};
|
||||
|
||||
export const getLatestCodingAgent = (
|
||||
processes: ExecutionProcessSummary[]
|
||||
): string | null => {
|
||||
const codingAgents = processes.filter((p) => isCodingAgent(p.run_reason));
|
||||
if (codingAgents.length === 0) return null;
|
||||
|
||||
return codingAgents.sort((a, b) =>
|
||||
a.started_at === b.started_at
|
||||
? a.id.localeCompare(b.id) // tie-break for same timestamp
|
||||
: new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
|
||||
)[0].id;
|
||||
};
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -150,6 +150,9 @@ importers:
|
||||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.5
|
||||
version: 0.4.20(eslint@8.57.1)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)
|
||||
postcss:
|
||||
specifier: ^8.4.32
|
||||
version: 8.5.6
|
||||
@@ -1719,6 +1722,15 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=8.40'
|
||||
|
||||
eslint-plugin-unused-imports@4.1.4:
|
||||
resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
|
||||
eslint: ^9.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
optional: true
|
||||
|
||||
eslint-scope@7.2.2:
|
||||
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -4419,6 +4431,12 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1):
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
|
||||
|
||||
eslint-scope@7.2.2:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
|
||||
Reference in New Issue
Block a user