From 44a499e62cf7d51fcdfe9307d26f39d378090cfe Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 15 Feb 2026 14:32:32 +0900 Subject: [PATCH 01/11] feat(jsonl): enhance message counting logic for AIGroup interactions - Added logic to await the first main-thread assistant message after a UserGroup to accurately count AIGroup messages. - Updated tests to reflect the new message counting behavior, ensuring correct results in session file analysis. --- src/main/utils/jsonl.ts | 11 +++++++++++ test/main/utils/jsonl.test.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts index 15b2cc1f..e09a922a 100644 --- a/src/main/utils/jsonl.ts +++ b/src/main/utils/jsonl.ts @@ -328,6 +328,8 @@ export async function analyzeSessionFileMetadata( let firstUserMessage: { text: string; timestamp: string } | null = null; let firstCommandMessage: { text: string; timestamp: string } | null = null; let messageCount = 0; + // After a UserGroup, await the first main-thread assistant message to count the AIGroup + let awaitingAIGroup = false; let gitBranch: string | null = null; let activityIndex = 0; @@ -357,6 +359,15 @@ export async function analyzeSessionFileMetadata( if (isParsedUserChunkMessage(parsed)) { messageCount++; + awaitingAIGroup = true; + } else if ( + awaitingAIGroup && + parsed.type === 'assistant' && + parsed.model !== '' && + !parsed.isSidechain + ) { + messageCount++; + awaitingAIGroup = false; } if (!gitBranch && 'gitBranch' in entry && entry.gitBranch) { diff --git a/test/main/utils/jsonl.test.ts b/test/main/utils/jsonl.test.ts index e748b821..c7a1cf05 100644 --- a/test/main/utils/jsonl.test.ts +++ b/test/main/utils/jsonl.test.ts @@ -166,7 +166,7 @@ describe('jsonl', () => { expect(result.firstUserMessage?.text).toBe('hello world'); expect(result.firstUserMessage?.timestamp).toBe('2026-01-01T00:00:00.000Z'); - expect(result.messageCount).toBe(1); + expect(result.messageCount).toBe(2); expect(result.isOngoing).toBe(true); expect(result.gitBranch).toBe('feature/test'); } finally { From 8b2dbf3bcbe6cb79dc897383c558cf2ee998143e Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 15 Feb 2026 14:49:29 +0900 Subject: [PATCH 02/11] feat(context): enhance session context tracking and display - Added context consumption tracking, including total context consumed and compaction events, to the session metadata. - Introduced a new `PhaseTokenBreakdown` interface for detailed per-phase token contributions. - Updated the `SessionContextPanel` to support a ranked view of context injections, allowing users to toggle between category and ranked displays. - Implemented a `ConsumptionBadge` in the `SessionItem` component to show context consumption with a hover popover for phase breakdown details. - Enhanced session sorting options in the sidebar to allow sorting by context consumption. --- src/main/services/discovery/ProjectScanner.ts | 3 + src/main/types/domain.ts | 20 +++ src/main/utils/jsonl.ts | 90 ++++++++++++ .../components/RankedInjectionList.tsx | 138 ++++++++++++++++++ .../components/SessionContextHeader.tsx | 41 +++++- .../chat/SessionContextPanel/index.tsx | 12 +- .../chat/SessionContextPanel/types.ts | 3 + .../sidebar/DateGroupedSessions.tsx | 96 ++++++++---- .../components/sidebar/SessionItem.tsx | 73 ++++++++- src/renderer/store/slices/sessionSlice.ts | 13 +- src/renderer/types/data.ts | 8 + 11 files changed, 462 insertions(+), 35 deletions(-) create mode 100644 src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx diff --git a/src/main/services/discovery/ProjectScanner.ts b/src/main/services/discovery/ProjectScanner.ts index eb6dfab6..90884236 100644 --- a/src/main/services/discovery/ProjectScanner.ts +++ b/src/main/services/discovery/ProjectScanner.ts @@ -758,6 +758,9 @@ export class ProjectScanner { isOngoing: metadata.isOngoing, gitBranch: metadata.gitBranch ?? undefined, metadataLevel, + contextConsumption: metadata.contextConsumption, + compactionCount: metadata.compactionCount, + phaseBreakdown: metadata.phaseBreakdown, }; } diff --git a/src/main/types/domain.ts b/src/main/types/domain.ts index 287c8b1b..a14b87fd 100644 --- a/src/main/types/domain.ts +++ b/src/main/types/domain.ts @@ -64,6 +64,20 @@ export interface Project { */ export type SessionMetadataLevel = 'light' | 'deep'; +/** + * Per-phase token breakdown for compaction-aware context consumption. + */ +export interface PhaseTokenBreakdown { + /** 1-based phase number */ + phaseNumber: number; + /** Tokens added during this phase */ + contribution: number; + /** Context window at peak (pre-compaction or final) */ + peakTokens: number; + /** Tokens after compaction (undefined for the last/current phase) */ + postCompaction?: number; +} + export interface Session { /** Session UUID (JSONL filename without extension) */ id: string; @@ -89,6 +103,12 @@ export interface Session { gitBranch?: string; /** Metadata completeness level */ metadataLevel?: SessionMetadataLevel; + /** Total context consumed (compaction-aware sum of all phases) */ + contextConsumption?: number; + /** Number of compaction events */ + compactionCount?: number; + /** Per-phase token breakdown for tooltip display */ + phaseBreakdown?: PhaseTokenBreakdown[]; } /** diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts index e09a922a..9e5f7ef8 100644 --- a/src/main/utils/jsonl.ts +++ b/src/main/utils/jsonl.ts @@ -30,6 +30,7 @@ import { import { extractToolCalls, extractToolResults } from './toolExtraction'; import type { FileSystemProvider } from '../services/infrastructure/FileSystemProvider'; +import type { PhaseTokenBreakdown } from '../types/domain'; const logger = createLogger('Util:jsonl'); @@ -300,6 +301,12 @@ export interface SessionFileMetadata { messageCount: number; isOngoing: boolean; gitBranch: string | null; + /** Total context consumed (compaction-aware) */ + contextConsumption?: number; + /** Number of compaction events */ + compactionCount?: number; + /** Per-phase token breakdown */ + phaseBreakdown?: PhaseTokenBreakdown[]; } /** @@ -339,6 +346,13 @@ export async function analyzeSessionFileMetadata( // Track tool_use IDs that are shutdown responses so their tool_results are also ending events const shutdownToolIds = new Set(); + // Context consumption tracking + + let lastMainAssistantInputTokens = 0; + const compactionPhases: { pre: number; post: number }[] = []; + + let awaitingPostCompaction = false; + for await (const line of rl) { const trimmed = line.trim(); if (!trimmed) { @@ -483,6 +497,79 @@ export async function analyzeSessionFileMetadata( } } } + + // Context consumption: track main-thread assistant input tokens + if (parsed.type === 'assistant' && !parsed.isSidechain && parsed.model !== '') { + const inputTokens = parsed.usage?.input_tokens ?? 0; + if (inputTokens > 0) { + if (awaitingPostCompaction && compactionPhases.length > 0) { + compactionPhases[compactionPhases.length - 1].post = inputTokens; + awaitingPostCompaction = false; + } + lastMainAssistantInputTokens = inputTokens; + } + } + + // Context consumption: detect compaction events + if (parsed.isCompactSummary) { + compactionPhases.push({ pre: lastMainAssistantInputTokens, post: 0 }); + awaitingPostCompaction = true; + } + } + + // Compute context consumption from tracked phases + let contextConsumption: number | undefined; + let phaseBreakdown: PhaseTokenBreakdown[] | undefined; + + if (lastMainAssistantInputTokens > 0) { + if (compactionPhases.length === 0) { + // No compaction: just the final input tokens + contextConsumption = lastMainAssistantInputTokens; + phaseBreakdown = [ + { + phaseNumber: 1, + contribution: lastMainAssistantInputTokens, + peakTokens: lastMainAssistantInputTokens, + }, + ]; + } else { + phaseBreakdown = []; + let total = 0; + + // Phase 1: tokens up to first compaction + const phase1Contribution = compactionPhases[0].pre; + total += phase1Contribution; + phaseBreakdown.push({ + phaseNumber: 1, + contribution: phase1Contribution, + peakTokens: compactionPhases[0].pre, + postCompaction: compactionPhases[0].post, + }); + + // Middle phases: contribution = pre[i] - post[i-1] + for (let i = 1; i < compactionPhases.length; i++) { + const contribution = compactionPhases[i].pre - compactionPhases[i - 1].post; + total += contribution; + phaseBreakdown.push({ + phaseNumber: i + 1, + contribution, + peakTokens: compactionPhases[i].pre, + postCompaction: compactionPhases[i].post, + }); + } + + // Last phase: final tokens - last post-compaction + const lastPhase = compactionPhases[compactionPhases.length - 1]; + const lastContribution = lastMainAssistantInputTokens - lastPhase.post; + total += lastContribution; + phaseBreakdown.push({ + phaseNumber: compactionPhases.length + 1, + contribution: lastContribution, + peakTokens: lastMainAssistantInputTokens, + }); + + contextConsumption = total; + } } return { @@ -490,5 +577,8 @@ export async function analyzeSessionFileMetadata( messageCount, isOngoing: lastEndingIndex === -1 ? hasAnyOngoingActivity : hasActivityAfterLastEnding, gitBranch, + contextConsumption, + compactionCount: compactionPhases.length > 0 ? compactionPhases.length : undefined, + phaseBreakdown, }; } diff --git a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx new file mode 100644 index 00000000..14bd3d77 --- /dev/null +++ b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx @@ -0,0 +1,138 @@ +/** + * RankedInjectionList - Flat list of all context injections sorted by token size descending. + * Provides a unified view across all categories, ranked by largest token consumers. + */ + +import React, { useMemo } from 'react'; + +import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; + +import { formatTokens } from '../utils/formatting'; +import { parseTurnIndex } from '../utils/pathParsing'; + +import type { ContextInjection } from '@renderer/types/contextInjection'; + +// ============================================================================= +// Constants +// ============================================================================= + +const CATEGORY_COLORS: Record = { + 'claude-md': { bg: 'rgba(99, 102, 241, 0.15)', text: '#818cf8', label: 'CLAUDE.md' }, + 'mentioned-file': { bg: 'rgba(52, 211, 153, 0.15)', text: '#34d399', label: 'File' }, + 'tool-output': { bg: 'rgba(251, 191, 36, 0.15)', text: '#fbbf24', label: 'Tool' }, + 'thinking-text': { bg: 'rgba(167, 139, 250, 0.15)', text: '#a78bfa', label: 'Thinking' }, + 'task-coordination': { bg: 'rgba(251, 146, 60, 0.15)', text: '#fb923c', label: 'Team' }, + 'user-message': { bg: 'rgba(96, 165, 250, 0.15)', text: '#60a5fa', label: 'User' }, +}; + +// ============================================================================= +// Props +// ============================================================================= + +interface RankedInjectionListProps { + injections: ContextInjection[]; + onNavigateToTurn?: (turnIndex: number) => void; +} + +// ============================================================================= +// Helpers +// ============================================================================= + +function getInjectionDescription(injection: ContextInjection): string { + switch (injection.category) { + case 'claude-md': + return injection.displayName || injection.path; + case 'mentioned-file': + return injection.displayName; + case 'tool-output': + return `${injection.toolCount} tool${injection.toolCount !== 1 ? 's' : ''} in Turn ${injection.turnIndex + 1}`; + case 'thinking-text': + return `Turn ${injection.turnIndex + 1} thinking/text`; + case 'task-coordination': + return `Turn ${injection.turnIndex + 1} coordination`; + case 'user-message': + return injection.textPreview; + } +} + +function getInjectionTurnIndex(injection: ContextInjection): number { + switch (injection.category) { + case 'claude-md': + return parseTurnIndex(injection.firstSeenInGroup); + case 'mentioned-file': + return injection.firstSeenTurnIndex; + case 'tool-output': + case 'thinking-text': + case 'task-coordination': + case 'user-message': + return injection.turnIndex; + } +} + +// ============================================================================= +// Component +// ============================================================================= + +export const RankedInjectionList = ({ + injections, + onNavigateToTurn, +}: Readonly): React.ReactElement => { + const sortedInjections = useMemo( + () => [...injections].sort((a, b) => b.estimatedTokens - a.estimatedTokens), + [injections] + ); + + const handleNavigate = (injection: ContextInjection): void => { + if (!onNavigateToTurn) return; + const turnIndex = getInjectionTurnIndex(injection); + if (turnIndex >= 0) { + onNavigateToTurn(turnIndex); + } + }; + + return ( +
+ {sortedInjections.map((inj) => { + const categoryInfo = CATEGORY_COLORS[inj.category] ?? { + bg: 'rgba(161, 161, 170, 0.15)', + text: '#a1a1aa', + label: inj.category, + }; + const description = getInjectionDescription(inj); + + return ( + + ); + })} +
+ ); +}; diff --git a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx index 042e12cf..ae9da2e1 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx @@ -12,12 +12,13 @@ import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY, } from '@renderer/constants/cssVariables'; -import { FileText, X } from 'lucide-react'; +import { ArrowDownWideNarrow, FileText, LayoutList, X } from 'lucide-react'; import { formatTokens } from '../utils/formatting'; import { SessionContextHelpTooltip } from './SessionContextHelpTooltip'; +import type { ContextViewMode } from '../types'; import type { ContextPhaseInfo } from '@renderer/types/contextInjection'; interface SessionContextHeaderProps { @@ -28,6 +29,8 @@ interface SessionContextHeaderProps { phaseInfo?: ContextPhaseInfo; selectedPhase: number | null; onPhaseChange: (phase: number | null) => void; + viewMode: ContextViewMode; + onViewModeChange: (mode: ContextViewMode) => void; } export const SessionContextHeader = ({ @@ -38,6 +41,8 @@ export const SessionContextHeader = ({ phaseInfo, selectedPhase, onPhaseChange, + viewMode, + onViewModeChange, }: Readonly): React.ReactElement => { return (
@@ -150,6 +155,40 @@ export const SessionContextHeader = ({
)} + + {/* View mode toggle */} +
+ + View: + + + +
); }; diff --git a/src/renderer/components/chat/SessionContextPanel/index.tsx b/src/renderer/components/chat/SessionContextPanel/index.tsx index c72387bf..b03f5ad5 100644 --- a/src/renderer/components/chat/SessionContextPanel/index.tsx +++ b/src/renderer/components/chat/SessionContextPanel/index.tsx @@ -9,6 +9,7 @@ import { COLOR_BORDER, COLOR_SURFACE, COLOR_TEXT_MUTED } from '@renderer/constan import { ClaudeMdFilesSection } from './components/ClaudeMdFilesSection'; import { MentionedFilesSection } from './components/MentionedFilesSection'; +import { RankedInjectionList } from './components/RankedInjectionList'; import { SessionContextHeader } from './components/SessionContextHeader'; import { TaskCoordinationSection } from './components/TaskCoordinationSection'; import { ThinkingTextSection } from './components/ThinkingTextSection'; @@ -23,7 +24,7 @@ import { SECTION_USER_MESSAGES, } from './types'; -import type { SectionType, SessionContextPanelProps } from './types'; +import type { ContextViewMode, SectionType, SessionContextPanelProps } from './types'; import type { ClaudeMdContextInjection, MentionedFileInjection, @@ -43,6 +44,9 @@ export const SessionContextPanel = ({ selectedPhase, onPhaseChange, }: Readonly): React.ReactElement => { + // View mode: category sections or flat ranked list + const [viewMode, setViewMode] = useState('category'); + // Track which main sections are expanded const [expandedSections, setExpandedSections] = useState>( new Set([ @@ -180,6 +184,8 @@ export const SessionContextPanel = ({ phaseInfo={phaseInfo} selectedPhase={selectedPhase} onPhaseChange={onPhaseChange} + viewMode={viewMode} + onViewModeChange={setViewMode} /> {/* Content */} @@ -191,7 +197,7 @@ export const SessionContextPanel = ({ > No context injections detected in this session - ) : ( + ) : viewMode === 'category' ? ( <> + ) : ( + )} diff --git a/src/renderer/components/chat/SessionContextPanel/types.ts b/src/renderer/components/chat/SessionContextPanel/types.ts index ef1aeb4e..0c2162f2 100644 --- a/src/renderer/components/chat/SessionContextPanel/types.ts +++ b/src/renderer/components/chat/SessionContextPanel/types.ts @@ -49,6 +49,9 @@ export type SectionType = | typeof SECTION_TASK_COORDINATION | typeof SECTION_USER_MESSAGES; +/** View mode for the context panel */ +export type ContextViewMode = 'category' | 'ranked'; + // ============================================================================= // CLAUDE.md Group Types // ============================================================================= diff --git a/src/renderer/components/sidebar/DateGroupedSessions.tsx b/src/renderer/components/sidebar/DateGroupedSessions.tsx index 48b6aa47..f3412135 100644 --- a/src/renderer/components/sidebar/DateGroupedSessions.tsx +++ b/src/renderer/components/sidebar/DateGroupedSessions.tsx @@ -12,7 +12,7 @@ import { separatePinnedSessions, } from '@renderer/utils/dateGrouping'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { Calendar, Loader2, MessageSquareOff, Pin } from 'lucide-react'; +import { ArrowDownWideNarrow, Calendar, Loader2, MessageSquareOff, Pin } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { SessionItem } from './SessionItem'; @@ -50,6 +50,8 @@ export const DateGroupedSessions = (): React.JSX.Element => { sessionsTotalCount, fetchSessionsMore, pinnedSessionIds, + sessionSortMode, + setSessionSortMode, } = useStore( useShallow((s) => ({ sessions: s.sessions, @@ -62,6 +64,8 @@ export const DateGroupedSessions = (): React.JSX.Element => { sessionsTotalCount: s.sessionsTotalCount, fetchSessionsMore: s.fetchSessionsMore, pinnedSessionIds: s.pinnedSessionIds, + sessionSortMode: s.sessionSortMode, + setSessionSortMode: s.setSessionSortMode, })) ); @@ -82,43 +86,59 @@ export const DateGroupedSessions = (): React.JSX.Element => { [groupedSessions] ); + // Sessions sorted by context consumption (for most-context sort mode) + const contextSortedSessions = useMemo(() => { + if (sessionSortMode !== 'most-context') return []; + return [...sessions].sort((a, b) => (b.contextConsumption ?? 0) - (a.contextConsumption ?? 0)); + }, [sessions, sessionSortMode]); + // Flatten sessions with date headers into virtual list items const virtualItems = useMemo((): VirtualItem[] => { const items: VirtualItem[] = []; - // Add pinned section first - if (pinnedSessions.length > 0) { - items.push({ - type: 'pinned-header', - id: 'header-pinned', - }); - - for (const session of pinnedSessions) { + if (sessionSortMode === 'most-context') { + // Flat list sorted by consumption - no date headers, no pinned section + for (const session of contextSortedSessions) { items.push({ type: 'session', session, - isPinned: true, + isPinned: pinnedSessionIds.includes(session.id), id: `session-${session.id}`, }); } - } - - for (const category of nonEmptyCategories) { - // Add header item - items.push({ - type: 'header', - category, - id: `header-${category}`, - }); - - // Add session items - for (const session of groupedSessions[category]) { + } else { + // Default: date-grouped view with pinned section + if (pinnedSessions.length > 0) { items.push({ - type: 'session', - session, - isPinned: false, - id: `session-${session.id}`, + type: 'pinned-header', + id: 'header-pinned', }); + + for (const session of pinnedSessions) { + items.push({ + type: 'session', + session, + isPinned: true, + id: `session-${session.id}`, + }); + } + } + + for (const category of nonEmptyCategories) { + items.push({ + type: 'header', + category, + id: `header-${category}`, + }); + + for (const session of groupedSessions[category]) { + items.push({ + type: 'session', + session, + isPinned: false, + id: `session-${session.id}`, + }); + } } } @@ -131,7 +151,15 @@ export const DateGroupedSessions = (): React.JSX.Element => { } return items; - }, [pinnedSessions, nonEmptyCategories, groupedSessions, sessionsHasMore]); + }, [ + sessionSortMode, + contextSortedSessions, + pinnedSessionIds, + pinnedSessions, + nonEmptyCategories, + groupedSessions, + sessionsHasMore, + ]); // Estimate item size based on type const estimateSize = useCallback( @@ -273,12 +301,24 @@ export const DateGroupedSessions = (): React.JSX.Element => { className="text-xs uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }} > - Sessions + {sessionSortMode === 'most-context' ? 'By Context' : 'Sessions'} ({sessions.length} {sessionsTotalCount > sessions.length ? ` of ${sessionsTotalCount}` : ''}) +
diff --git a/src/renderer/components/sidebar/SessionItem.tsx b/src/renderer/components/sidebar/SessionItem.tsx index 84e34590..d519d8e9 100644 --- a/src/renderer/components/sidebar/SessionItem.tsx +++ b/src/renderer/components/sidebar/SessionItem.tsx @@ -4,10 +4,11 @@ * Supports right-click context menu for pane management. */ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useStore } from '@renderer/store'; +import { formatTokensCompact } from '@shared/utils/tokenFormatting'; import { formatDistanceToNowStrict } from 'date-fns'; import { MessageSquare, Pin } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; @@ -16,7 +17,7 @@ import { OngoingIndicator } from '../common/OngoingIndicator'; import { SessionContextMenu } from './SessionContextMenu'; -import type { Session } from '@renderer/types/data'; +import type { PhaseTokenBreakdown, Session } from '@renderer/types/data'; interface SessionItemProps { session: Session; @@ -46,6 +47,63 @@ function formatShortTime(date: Date): string { .replace(' year', 'y'); } +/** + * Consumption badge with hover popover showing phase breakdown. + */ +const ConsumptionBadge = ({ + contextConsumption, + phaseBreakdown, +}: Readonly<{ + contextConsumption: number; + phaseBreakdown?: PhaseTokenBreakdown[]; +}>): React.JSX.Element => { + const [showPopover, setShowPopover] = useState(false); + const badgeRef = useRef(null); + const isHigh = contextConsumption > 150_000; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- tooltip trigger via hover, not interactive + setShowPopover(true)} + onMouseLeave={() => setShowPopover(false)} + > + {formatTokensCompact(contextConsumption)} + {showPopover && phaseBreakdown && phaseBreakdown.length > 0 && ( +
+
+ Total Context: {formatTokensCompact(contextConsumption)} tokens +
+ {phaseBreakdown.length === 1 ? ( +
Context: {formatTokensCompact(phaseBreakdown[0].peakTokens)}
+ ) : ( + phaseBreakdown.map((phase) => ( +
+ Phase {phase.phaseNumber}: + {formatTokensCompact(phase.contribution)} + {phase.postCompaction != null && ( + + (compacted → {formatTokensCompact(phase.postCompaction)}) + + )} +
+ )) + )} +
+ )} +
+ ); +}; + export const SessionItem = ({ session, isActive, @@ -162,7 +220,7 @@ export const SessionItem = ({
- {/* Second line: message count + time */} + {/* Second line: message count + time + context consumption */}
· {formatShortTime(new Date(session.createdAt))} + {session.contextConsumption != null && session.contextConsumption > 0 && ( + <> + · + + + )}
diff --git a/src/renderer/store/slices/sessionSlice.ts b/src/renderer/store/slices/sessionSlice.ts index 489e8452..a7632c47 100644 --- a/src/renderer/store/slices/sessionSlice.ts +++ b/src/renderer/store/slices/sessionSlice.ts @@ -6,7 +6,7 @@ import { api } from '@renderer/api'; import { createLogger } from '@shared/utils/logger'; import type { AppState } from '../types'; -import type { Session } from '@renderer/types/data'; +import type { Session, SessionSortMode } from '@renderer/types/data'; import type { StateCreator } from 'zustand'; const logger = createLogger('Store:session'); @@ -34,6 +34,8 @@ export interface SessionSlice { sessionsLoadingMore: boolean; // Pinned sessions pinnedSessionIds: string[]; + // Sort mode + sessionSortMode: SessionSortMode; // Actions fetchSessions: (projectId: string) => Promise; @@ -48,6 +50,8 @@ export interface SessionSlice { togglePinSession: (sessionId: string) => Promise; /** Load pinned sessions from config for current project */ loadPinnedSessions: () => Promise; + /** Set session sort mode */ + setSessionSortMode: (mode: SessionSortMode) => void; } // ============================================================================= @@ -67,6 +71,8 @@ export const createSessionSlice: StateCreator = sessionsLoadingMore: false, // Pinned sessions pinnedSessionIds: [], + // Sort mode + sessionSortMode: 'recent' as SessionSortMode, // Fetch sessions for a specific project (legacy - not paginated) fetchSessions: async (projectId: string) => { @@ -317,4 +323,9 @@ export const createSessionSlice: StateCreator = set({ pinnedSessionIds: [] }); } }, + + // Set session sort mode + setSessionSortMode: (mode: SessionSortMode) => { + set({ sessionSortMode: mode }); + }, }); diff --git a/src/renderer/types/data.ts b/src/renderer/types/data.ts index 79f5268a..8bc87278 100644 --- a/src/renderer/types/data.ts +++ b/src/renderer/types/data.ts @@ -16,6 +16,7 @@ // Domain types export type { + PhaseTokenBreakdown, Project, RepositoryGroup, SearchResult, @@ -68,6 +69,13 @@ export type { TriggerToolName, } from './notifications'; +// ============================================================================= +// Session Sort Mode +// ============================================================================= + +/** Sort mode for session list in sidebar */ +export type SessionSortMode = 'recent' | 'most-context'; + // ============================================================================= // Renderer-Specific Type Guards // ============================================================================= From 9915cf5a03ca894a6757a07e8d0f60af549f7dd4 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 20:06:09 +0900 Subject: [PATCH 03/11] feat(settings): enhance Claude root management and UI updates - Implemented functionality to select and manage the local Claude root folder, allowing users to specify a custom path. - Added UI components for displaying and interacting with Claude root settings, including error handling for missing directories. - Enhanced the settings view to support dynamic updates based on user selections and improved state management for pending settings. - Refactored related components to integrate the new Claude root features seamlessly, including updates to the general settings section and connection handling. --- src/main/utils/jsonl.ts | 5 +- .../components/RankedInjectionList.tsx | 154 +++++++- .../components/dashboard/DashboardView.tsx | 23 +- src/renderer/components/layout/TabBar.tsx | 2 +- .../components/settings/SettingsView.tsx | 12 +- .../settings/sections/ConnectionSection.tsx | 334 +--------------- .../settings/sections/GeneralSection.tsx | 356 +++++++++++++++++- .../components/sidebar/SessionItem.tsx | 77 ++-- src/renderer/store/slices/configSlice.ts | 15 +- 9 files changed, 585 insertions(+), 393 deletions(-) diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts index 9e5f7ef8..1d70be2c 100644 --- a/src/main/utils/jsonl.ts +++ b/src/main/utils/jsonl.ts @@ -500,7 +500,10 @@ export async function analyzeSessionFileMetadata( // Context consumption: track main-thread assistant input tokens if (parsed.type === 'assistant' && !parsed.isSidechain && parsed.model !== '') { - const inputTokens = parsed.usage?.input_tokens ?? 0; + const inputTokens = + (parsed.usage?.input_tokens ?? 0) + + (parsed.usage?.cache_read_input_tokens ?? 0) + + (parsed.usage?.cache_creation_input_tokens ?? 0); if (inputTokens > 0) { if (awaitingPostCompaction && compactionPhases.length > 0) { compactionPhases[compactionPhases.length - 1].post = inputTokens; diff --git a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx index 14bd3d77..e7101937 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx @@ -1,16 +1,18 @@ /** - * RankedInjectionList - Flat list of all context injections sorted by token size descending. - * Provides a unified view across all categories, ranked by largest token consumers. + * RankedInjectionList - All context injections sorted by token size descending. + * Injections are shown as grouped rows (e.g., "Tool output in Turn N"). + * Tool-output rows are expandable to reveal individual tool breakdowns sorted desc. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; +import { ChevronRight } from 'lucide-react'; import { formatTokens } from '../utils/formatting'; import { parseTurnIndex } from '../utils/pathParsing'; -import type { ContextInjection } from '@renderer/types/contextInjection'; +import type { ContextInjection, ToolOutputInjection } from '@renderer/types/contextInjection'; // ============================================================================= // Constants @@ -69,6 +71,113 @@ function getInjectionTurnIndex(injection: ContextInjection): number { } } +// ============================================================================= +// Sub-components +// ============================================================================= + +/** Expandable tool-output row with breakdown sorted by token count desc. */ +const ToolOutputRankedItem = ({ + injection, + onNavigateToTurn, +}: Readonly<{ + injection: ToolOutputInjection; + onNavigateToTurn?: (turnIndex: number) => void; +}>): React.ReactElement => { + const [expanded, setExpanded] = useState(false); + const hasBreakdown = injection.toolBreakdown.length > 0; + const categoryInfo = CATEGORY_COLORS['tool-output']; + + const sortedBreakdown = useMemo( + () => [...injection.toolBreakdown].sort((a, b) => b.tokenCount - a.tokenCount), + [injection.toolBreakdown] + ); + + return ( +
+ + + {/* Expanded tool breakdown */} + {expanded && hasBreakdown && ( +
+ {sortedBreakdown.map((tool, idx) => ( + + ))} +
+ )} +
+ ); +}; + // ============================================================================= // Component // ============================================================================= @@ -82,37 +191,42 @@ export const RankedInjectionList = ({ [injections] ); - const handleNavigate = (injection: ContextInjection): void => { - if (!onNavigateToTurn) return; - const turnIndex = getInjectionTurnIndex(injection); - if (turnIndex >= 0) { - onNavigateToTurn(turnIndex); - } - }; - return ( -
+
{sortedInjections.map((inj) => { + // Tool-output: expandable row + if (inj.category === 'tool-output') { + return ( + + ); + } + + // All other categories: simple row const categoryInfo = CATEGORY_COLORS[inj.category] ?? { bg: 'rgba(161, 161, 170, 0.15)', text: '#a1a1aa', label: inj.category, }; - const description = getInjectionDescription(inj); return ( + )} - )} +
{/* Projects Grid */} diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 9cf37c90..26663710 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -387,7 +387,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { {/* Settings gear icon (Electron only - browser can't access native settings) */} {isElectronMode() && ( - - - - {isWindowsStyleDefaultPath && ( - - )} - - - {claudeRootError && ( -
-

{claudeRootError}

-
- )} - - {showWslModal && ( -
- -
- ))} - - -
- - -
- - - )} -

Connect to a remote machine to view Claude Code sessions running there diff --git a/src/renderer/components/settings/sections/GeneralSection.tsx b/src/renderer/components/settings/sections/GeneralSection.tsx index fdd1a38d..492ef7d3 100644 --- a/src/renderer/components/settings/sections/GeneralSection.tsx +++ b/src/renderer/components/settings/sections/GeneralSection.tsx @@ -1,15 +1,19 @@ /** - * GeneralSection - General settings including startup, appearance, and browser access. + * GeneralSection - General settings including startup, appearance, browser access, and local Claude root. */ import { useCallback, useEffect, useState } from 'react'; import { api } from '@renderer/api'; -import { Check, Copy, Loader2 } from 'lucide-react'; +import { confirm } from '@renderer/components/common/ConfirmDialog'; +import { useStore } from '@renderer/store'; +import { getFullResetState } from '@renderer/store/utils/stateResetHelpers'; +import { Check, Copy, FolderOpen, Laptop, Loader2, RotateCcw } from 'lucide-react'; import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components'; import type { SafeConfig } from '../hooks/useSettingsConfig'; +import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types'; import type { HttpServerStatus } from '@shared/types/api'; // Theme options @@ -39,11 +43,38 @@ export const GeneralSection = ({ const [serverLoading, setServerLoading] = useState(false); const [copied, setCopied] = useState(false); - // Fetch server status on mount + // Claude Root state + const connectionMode = useStore((s) => s.connectionMode); + const fetchProjects = useStore((s) => s.fetchProjects); + const fetchRepositoryGroups = useStore((s) => s.fetchRepositoryGroups); + + const [claudeRootInfo, setClaudeRootInfo] = useState(null); + const [updatingClaudeRoot, setUpdatingClaudeRoot] = useState(false); + const [claudeRootError, setClaudeRootError] = useState(null); + const [findingWslRoots, setFindingWslRoots] = useState(false); + const [wslCandidates, setWslCandidates] = useState([]); + const [showWslModal, setShowWslModal] = useState(false); + + // Fetch server status and Claude root info on mount useEffect(() => { void api.httpServer.getStatus().then(setServerStatus); }, []); + const loadClaudeRootInfo = useCallback(async () => { + try { + const info = await api.config.getClaudeRootInfo(); + setClaudeRootInfo(info); + } catch (error) { + setClaudeRootError( + error instanceof Error ? error.message : 'Failed to load local Claude root settings' + ); + } + }, []); + + useEffect(() => { + void loadClaudeRootInfo(); + }, [loadClaudeRootInfo]); + const handleServerToggle = useCallback(async (enabled: boolean) => { setServerLoading(true); try { @@ -64,6 +95,156 @@ export const GeneralSection = ({ setTimeout(() => setCopied(false), 2000); }, [serverUrl]); + // Claude Root handlers + const resetWorkspaceForRootChange = useCallback((): void => { + useStore.setState({ + projects: [], + repositoryGroups: [], + openTabs: [], + activeTabId: null, + selectedTabIds: [], + paneLayout: { + panes: [ + { + id: 'pane-default', + tabs: [], + activeTabId: null, + selectedTabIds: [], + widthFraction: 1, + }, + ], + focusedPaneId: 'pane-default', + }, + ...getFullResetState(), + }); + }, []); + + const applyClaudeRootPath = useCallback( + async (claudeRootPath: string | null): Promise => { + try { + setUpdatingClaudeRoot(true); + setClaudeRootError(null); + + await api.config.update('general', { claudeRootPath }); + await loadClaudeRootInfo(); + + if (connectionMode === 'local') { + resetWorkspaceForRootChange(); + await Promise.all([fetchProjects(), fetchRepositoryGroups()]); + } + } catch (error) { + setClaudeRootError(error instanceof Error ? error.message : 'Failed to update Claude root'); + } finally { + setUpdatingClaudeRoot(false); + } + }, + [ + connectionMode, + fetchProjects, + fetchRepositoryGroups, + loadClaudeRootInfo, + resetWorkspaceForRootChange, + ] + ); + + const handleSelectClaudeRootFolder = useCallback(async (): Promise => { + setClaudeRootError(null); + + const selection = await api.config.selectClaudeRootFolder(); + if (!selection) { + return; + } + + if (!selection.isClaudeDirName) { + const proceed = await confirm({ + title: 'Selected folder is not .claude', + message: `This folder is named "${selection.path.split(/[\\/]/).pop() ?? selection.path}", not ".claude". Continue anyway?`, + confirmLabel: 'Use Folder', + }); + if (!proceed) { + return; + } + } + + if (!selection.hasProjectsDir) { + const proceed = await confirm({ + title: 'No projects directory found', + message: 'This folder does not contain a "projects" directory. Continue anyway?', + confirmLabel: 'Use Folder', + }); + if (!proceed) { + return; + } + } + + await applyClaudeRootPath(selection.path); + }, [applyClaudeRootPath]); + + const handleResetClaudeRoot = useCallback(async (): Promise => { + await applyClaudeRootPath(null); + }, [applyClaudeRootPath]); + + const applyWslCandidate = useCallback( + async (candidate: WslClaudeRootCandidate): Promise => { + if (!candidate.hasProjectsDir) { + const proceed = await confirm({ + title: 'WSL path missing projects directory', + message: `"${candidate.path}" does not contain a "projects" directory. Continue anyway?`, + confirmLabel: 'Use Path', + }); + if (!proceed) { + return; + } + } + + await applyClaudeRootPath(candidate.path); + setShowWslModal(false); + }, + [applyClaudeRootPath] + ); + + const handleUseWslForClaude = useCallback(async (): Promise => { + try { + setFindingWslRoots(true); + setClaudeRootError(null); + const candidates = await api.config.findWslClaudeRoots(); + setWslCandidates(candidates); + + if (candidates.length === 0) { + const pickManually = await confirm({ + title: 'No WSL Claude paths found', + message: + 'Could not find WSL distros with Claude data automatically. Select folder manually?', + confirmLabel: 'Select Folder', + }); + if (pickManually) { + await handleSelectClaudeRootFolder(); + } + return; + } + + const candidatesWithProjects = candidates.filter((candidate) => candidate.hasProjectsDir); + if (candidatesWithProjects.length === 1) { + await applyWslCandidate(candidatesWithProjects[0]); + return; + } + + setShowWslModal(true); + } catch (error) { + setClaudeRootError( + error instanceof Error ? error.message : 'Failed to detect WSL Claude root paths' + ); + } finally { + setFindingWslRoots(false); + } + }, [applyWslCandidate, handleSelectClaudeRootFolder]); + + const isCustomClaudeRoot = Boolean(claudeRootInfo?.customPath); + const resolvedClaudeRootPath = claudeRootInfo?.resolvedPath ?? '~/.claude'; + const defaultClaudeRootPath = claudeRootInfo?.defaultPath ?? '~/.claude'; + const isWindowsStyleDefaultPath = + /^[a-zA-Z]:\\/.test(defaultClaudeRootPath) || defaultClaudeRootPath.startsWith('\\\\'); + return (

@@ -94,6 +275,175 @@ export const GeneralSection = ({ /> + +

+ Choose which local folder is treated as your Claude data root +

+ + +
+
+ {resolvedClaudeRootPath} +
+
+ Auto-detected: {defaultClaudeRootPath} +
+
+
+ +
+ + + + + {isWindowsStyleDefaultPath && ( + + )} +
+ + {claudeRootError && ( +
+

{claudeRootError}

+
+ )} + + {showWslModal && ( +
+ +
+ ))} +
+ +
+ + +
+ + + )} + (null); const isHigh = contextConsumption > 150_000; + // Calculate popover position relative to viewport for portal rendering + const popoverPosition = + showPopover && badgeRef.current + ? (() => { + const rect = badgeRef.current.getBoundingClientRect(); + return { + top: rect.top - 6, + left: rect.left + rect.width / 2, + }; + })() + : null; + return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- tooltip trigger via hover, not interactive setShowPopover(true)} onMouseLeave={() => setShowPopover(false)} > {formatTokensCompact(contextConsumption)} - {showPopover && phaseBreakdown && phaseBreakdown.length > 0 && ( -
-
- Total Context: {formatTokensCompact(contextConsumption)} tokens -
- {phaseBreakdown.length === 1 ? ( -
Context: {formatTokensCompact(phaseBreakdown[0].peakTokens)}
- ) : ( - phaseBreakdown.map((phase) => ( -
- Phase {phase.phaseNumber}: - {formatTokensCompact(phase.contribution)} - {phase.postCompaction != null && ( + {showPopover && + popoverPosition && + phaseBreakdown && + phaseBreakdown.length > 0 && + createPortal( +
+
+ Total Context: {formatTokensCompact(contextConsumption)} tokens +
+ {phaseBreakdown.length === 1 ? ( +
Context: {formatTokensCompact(phaseBreakdown[0].peakTokens)}
+ ) : ( + phaseBreakdown.map((phase) => ( +
- (compacted → {formatTokensCompact(phase.postCompaction)}) + Phase {phase.phaseNumber}: - )} -
- )) - )} -
- )} + {formatTokensCompact(phase.contribution)} + {phase.postCompaction != null && ( + + (compacted → {formatTokensCompact(phase.postCompaction)}) + + )} +
+ )) + )} +
, + document.body + )}
); }; diff --git a/src/renderer/store/slices/configSlice.ts b/src/renderer/store/slices/configSlice.ts index da32ba77..0cdc5320 100644 --- a/src/renderer/store/slices/configSlice.ts +++ b/src/renderer/store/slices/configSlice.ts @@ -20,11 +20,13 @@ export interface ConfigSlice { appConfig: AppConfig | null; configLoading: boolean; configError: string | null; + pendingSettingsSection: string | null; // Actions fetchConfig: () => Promise; updateConfig: (section: string, data: Record) => Promise; - openSettingsTab: () => void; + openSettingsTab: (section?: string) => void; + clearPendingSettingsSection: () => void; } // ============================================================================= @@ -36,6 +38,7 @@ export const createConfigSlice: StateCreator = (s appConfig: null, configLoading: false, configError: null, + pendingSettingsSection: null, // Fetch app configuration from main process fetchConfig: async () => { @@ -70,9 +73,13 @@ export const createConfigSlice: StateCreator = (s }, // Open or focus the settings tab (per-pane singleton) - openSettingsTab: () => { + openSettingsTab: (section?: string) => { const state = get(); + if (section) { + set({ pendingSettingsSection: section }); + } + // Check if settings tab exists in focused pane const focusedPane = state.paneLayout.panes.find((p) => p.id === state.paneLayout.focusedPaneId); const settingsTab = focusedPane?.tabs.find((t) => t.type === 'settings'); @@ -87,4 +94,8 @@ export const createConfigSlice: StateCreator = (s label: 'Settings', }); }, + + clearPendingSettingsSection: () => { + set({ pendingSettingsSection: null }); + }, }); From fb2d56e23f10d01e1efa4f4a35057f2457cc5c53 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 20:36:18 +0900 Subject: [PATCH 04/11] feat(chat): enhance navigation and tool highlighting in chat history - Introduced context panel navigation for user message groups and specific tools within turns, improving user experience in navigating chat history. - Added state management for context navigation tool use ID and effective highlight color, allowing distinct visual cues for context panel interactions. - Updated `ChatHistory` and `SessionContextPanel` components to support new navigation handlers and integrate deep-linking functionality for tools. - Enhanced `RankedInjectionList` to facilitate navigation to user groups and tools, providing a more interactive and user-friendly interface. --- src/renderer/components/chat/ChatHistory.tsx | 93 +++++++++++++++++- .../components/chat/ChatHistoryItem.tsx | 7 +- .../components/RankedInjectionList.tsx | 98 ++++++++++++------- .../chat/SessionContextPanel/index.tsx | 9 +- .../chat/SessionContextPanel/types.ts | 4 + .../components/settings/SettingsView.tsx | 9 +- .../sidebar/DateGroupedSessions.tsx | 40 +++++++- src/renderer/types/contextInjection.ts | 2 + src/renderer/utils/contextTracker.ts | 1 + 9 files changed, 214 insertions(+), 49 deletions(-) diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 5b60dd30..49023fea 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -252,7 +252,11 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { selectSearchMatch, }); - const effectiveHighlightToolUseId = controllerToolUseId ?? undefined; + // Local tool highlight for context panel navigation (separate from controller) + const [contextNavToolUseId, setContextNavToolUseId] = useState(null); + const effectiveHighlightToolUseId = controllerToolUseId ?? contextNavToolUseId ?? undefined; + // Use blue for context panel tool navigation, otherwise use controller's color + const effectiveHighlightColor = contextNavToolUseId ? ('blue' as const) : highlightColor; // Keep search match indices aligned with this tab's rendered conversation. // This avoids stale/global match lists after tab switches or in-place refreshes. @@ -396,6 +400,87 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { [conversation, ensureGroupVisible, setHighlightedGroupId] ); + // Handler to navigate to a user message group (preceding the AI group at turnIndex) + const handleNavigateToUserGroup = useCallback( + (turnIndex: number) => { + if (!conversation) return; + const aiItemIndex = conversation.items.findIndex( + (item) => item.type === 'ai' && item.group.turnIndex === turnIndex + ); + if (aiItemIndex < 0) return; + + // Find the user item preceding this AI group + const prevItem = aiItemIndex > 0 ? conversation.items[aiItemIndex - 1] : null; + if (prevItem?.type !== 'user') return; + + const groupId = prevItem.group.id; + const element = chatItemRefs.current.get(groupId); + if (!element) return; + + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setHighlightedGroupId(groupId); + setIsNavigationHighlight(true); + if (navigationHighlightTimerRef.current) { + clearTimeout(navigationHighlightTimerRef.current); + } + navigationHighlightTimerRef.current = setTimeout(() => { + setHighlightedGroupId(null); + setIsNavigationHighlight(false); + navigationHighlightTimerRef.current = null; + }, 2000); + }, + [conversation, setHighlightedGroupId] + ); + + // Handler to navigate to a specific tool within a turn from context panel + const handleNavigateToTool = useCallback( + (turnIndex: number, toolUseId: string) => { + if (!conversation) return; + const targetItem = conversation.items.find( + (item) => item.type === 'ai' && item.group.turnIndex === turnIndex + ); + if (targetItem?.type !== 'ai') return; + + const run = async (): Promise => { + const groupId = targetItem.group.id; + await ensureGroupVisible(groupId); + + // Set group + tool highlight immediately + setHighlightedGroupId(groupId); + setIsNavigationHighlight(true); + setContextNavToolUseId(toolUseId); + + // Wait for tool element to appear in DOM (up to 500ms) + let toolElement: HTMLElement | undefined; + const startTime = Date.now(); + while (Date.now() - startTime < 500) { + toolElement = toolItemRefs.current.get(toolUseId); + if (toolElement) break; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + // Scroll to tool element, or fall back to AI group + const scrollTarget = toolElement ?? aiGroupRefs.current.get(groupId); + if (scrollTarget) { + scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + // Clear highlight after 2s + if (navigationHighlightTimerRef.current) { + clearTimeout(navigationHighlightTimerRef.current); + } + navigationHighlightTimerRef.current = setTimeout(() => { + setHighlightedGroupId(null); + setIsNavigationHighlight(false); + setContextNavToolUseId(null); + navigationHighlightTimerRef.current = null; + }, 2000); + }; + void run(); + }, + [conversation, ensureGroupVisible, setHighlightedGroupId] + ); + // Scroll to current search result when it changes useEffect(() => { const currentMatch = currentSearchIndex >= 0 ? searchMatches[currentSearchIndex] : null; @@ -695,7 +780,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { highlightToolUseId={effectiveHighlightToolUseId} isSearchHighlight={isSearchHighlight} isNavigationHighlight={isNavigationHighlight} - highlightColor={highlightColor} + highlightColor={effectiveHighlightColor} registerChatItemRef={registerChatItemRef} registerAIGroupRef={registerAIGroupRefCombined} registerToolRef={registerToolRef} @@ -713,7 +798,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { highlightToolUseId={effectiveHighlightToolUseId} isSearchHighlight={isSearchHighlight} isNavigationHighlight={isNavigationHighlight} - highlightColor={highlightColor} + highlightColor={effectiveHighlightColor} registerChatItemRef={registerChatItemRef} registerAIGroupRef={registerAIGroupRefCombined} registerToolRef={registerToolRef} @@ -732,6 +817,8 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { onClose={() => setContextPanelVisible(false)} projectRoot={sessionDetail?.session?.projectPath} onNavigateToTurn={handleNavigateToTurn} + onNavigateToTool={handleNavigateToTool} + onNavigateToUserGroup={handleNavigateToUserGroup} totalSessionTokens={lastAiGroupTotalTokens} phaseInfo={sessionPhaseInfo ?? undefined} selectedPhase={selectedContextPhase} diff --git a/src/renderer/components/chat/ChatHistoryItem.tsx b/src/renderer/components/chat/ChatHistoryItem.tsx index d611fea1..b3b589dd 100644 --- a/src/renderer/components/chat/ChatHistoryItem.tsx +++ b/src/renderer/components/chat/ChatHistoryItem.tsx @@ -97,11 +97,10 @@ const ChatHistoryItemInner = ({ } case 'ai': { const isHighlighted = highlightedGroupId === item.group.id; - // Pass highlightToolUseId to ALL AI groups (when not search/navigation) + // Pass highlightToolUseId to ALL AI groups (when not search highlight) // Each group will check if it contains the tool and expand accordingly - // This fixes issues where timestamp matching might fail to find the correct group - const toolUseIdForGroup = - !isSearchHighlight && !isNavigationHighlight ? highlightToolUseId : undefined; + // Allowed during navigation highlights so context panel tool deep-linking works + const toolUseIdForGroup = !isSearchHighlight ? highlightToolUseId : undefined; const hl = getHighlight( isHighlighted, isSearchHighlight, diff --git a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx index e7101937..ebbe6d5f 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx @@ -2,10 +2,13 @@ * RankedInjectionList - All context injections sorted by token size descending. * Injections are shown as grouped rows (e.g., "Tool output in Turn N"). * Tool-output rows are expandable to reveal individual tool breakdowns sorted desc. + * Individual tools support deep-link navigation to the exact tool in chat. + * CLAUDE.md and File items show a copy-path button. */ import React, { useMemo, useState } from 'react'; +import { CopyButton } from '@renderer/components/common/CopyButton'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { ChevronRight } from 'lucide-react'; @@ -34,6 +37,8 @@ const CATEGORY_COLORS: Record void; + onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; + onNavigateToUserGroup?: (turnIndex: number) => void; } // ============================================================================= @@ -71,6 +76,13 @@ function getInjectionTurnIndex(injection: ContextInjection): number { } } +/** Get copyable path for path-based injections. */ +function getCopyablePath(injection: ContextInjection): string | null { + if (injection.category === 'claude-md') return injection.path; + if (injection.category === 'mentioned-file') return injection.path; + return null; +} + // ============================================================================= // Sub-components // ============================================================================= @@ -79,9 +91,11 @@ function getInjectionTurnIndex(injection: ContextInjection): number { const ToolOutputRankedItem = ({ injection, onNavigateToTurn, + onNavigateToTool, }: Readonly<{ injection: ToolOutputInjection; onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; }>): React.ReactElement => { const [expanded, setExpanded] = useState(false); const hasBreakdown = injection.toolBreakdown.length > 0; @@ -139,7 +153,9 @@ const ToolOutputRankedItem = ({ + {/* Category pill */} + + {categoryInfo.label} + + {/* Description */} + + {getInjectionDescription(inj)} + + {/* Token count */} + + {formatTokens(inj.estimatedTokens)} + + + {/* Copy path button for CLAUDE.md and File items */} + {copyPath && ( + e.stopPropagation()}> + + + )} + ); })} diff --git a/src/renderer/components/chat/SessionContextPanel/index.tsx b/src/renderer/components/chat/SessionContextPanel/index.tsx index b03f5ad5..28c540e2 100644 --- a/src/renderer/components/chat/SessionContextPanel/index.tsx +++ b/src/renderer/components/chat/SessionContextPanel/index.tsx @@ -39,6 +39,8 @@ export const SessionContextPanel = ({ onClose, projectRoot, onNavigateToTurn, + onNavigateToTool, + onNavigateToUserGroup, totalSessionTokens, phaseInfo, selectedPhase, @@ -250,7 +252,12 @@ export const SessionContextPanel = ({ /> ) : ( - + )} diff --git a/src/renderer/components/chat/SessionContextPanel/types.ts b/src/renderer/components/chat/SessionContextPanel/types.ts index 0c2162f2..b5222683 100644 --- a/src/renderer/components/chat/SessionContextPanel/types.ts +++ b/src/renderer/components/chat/SessionContextPanel/types.ts @@ -18,6 +18,10 @@ export interface SessionContextPanelProps { projectRoot?: string; /** Click Turn N to navigate to that turn */ onNavigateToTurn?: (turnIndex: number) => void; + /** Navigate to a specific tool within a turn by toolUseId */ + onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; + /** Navigate to the user message group preceding the AI group at turnIndex */ + onNavigateToUserGroup?: (turnIndex: number) => void; /** Total session tokens (input + output + cache) for comparison */ totalSessionTokens?: number; /** Phase information for phase selector */ diff --git a/src/renderer/components/settings/SettingsView.tsx b/src/renderer/components/settings/SettingsView.tsx index 330e5c07..ee4f0400 100644 --- a/src/renderer/components/settings/SettingsView.tsx +++ b/src/renderer/components/settings/SettingsView.tsx @@ -3,7 +3,7 @@ * Provides UI for managing notifications, display settings, and advanced options. */ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useStore } from '@renderer/store'; import { Loader2 } from 'lucide-react'; @@ -23,12 +23,15 @@ export const SettingsView = (): React.JSX.Element | null => { const pendingSettingsSection = useStore((s) => s.pendingSettingsSection); const clearPendingSettingsSection = useStore((s) => s.clearPendingSettingsSection); - useEffect(() => { + // Consume pending section during render (React-recommended pattern for adjusting state on prop change) + const [prevPending, setPrevPending] = useState(null); + if (pendingSettingsSection !== prevPending) { + setPrevPending(pendingSettingsSection); if (pendingSettingsSection) { setActiveSection(pendingSettingsSection as SettingsSection); clearPendingSettingsSection(); } - }, [pendingSettingsSection, clearPendingSettingsSection]); + } const { config, diff --git a/src/renderer/components/sidebar/DateGroupedSessions.tsx b/src/renderer/components/sidebar/DateGroupedSessions.tsx index f3412135..45aa32e1 100644 --- a/src/renderer/components/sidebar/DateGroupedSessions.tsx +++ b/src/renderer/components/sidebar/DateGroupedSessions.tsx @@ -3,7 +3,8 @@ * Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll. */ -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { useStore } from '@renderer/store'; import { @@ -47,7 +48,6 @@ export const DateGroupedSessions = (): React.JSX.Element => { sessionsError, sessionsHasMore, sessionsLoadingMore, - sessionsTotalCount, fetchSessionsMore, pinnedSessionIds, sessionSortMode, @@ -61,7 +61,6 @@ export const DateGroupedSessions = (): React.JSX.Element => { sessionsError: s.sessionsError, sessionsHasMore: s.sessionsHasMore, sessionsLoadingMore: s.sessionsLoadingMore, - sessionsTotalCount: s.sessionsTotalCount, fetchSessionsMore: s.fetchSessionsMore, pinnedSessionIds: s.pinnedSessionIds, sessionSortMode: s.sessionSortMode, @@ -70,6 +69,8 @@ export const DateGroupedSessions = (): React.JSX.Element => { ); const parentRef = useRef(null); + const countRef = useRef(null); + const [showCountTooltip, setShowCountTooltip] = useState(false); // Separate pinned sessions from unpinned const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo( @@ -303,10 +304,39 @@ export const DateGroupedSessions = (): React.JSX.Element => { > {sessionSortMode === 'most-context' ? 'By Context' : 'Sessions'} - + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- tooltip trigger via hover, not interactive */} + setShowCountTooltip(true)} + onMouseLeave={() => setShowCountTooltip(false)} + > ({sessions.length} - {sessionsTotalCount > sessions.length ? ` of ${sessionsTotalCount}` : ''}) + {sessionsHasMore ? '+' : ''}) + {showCountTooltip && + sessionsHasMore && + countRef.current && + createPortal( +
+ {sessions.length} loaded so far — scroll down to load more. Context sorting only ranks + loaded sessions. +
, + document.body + )} + {/* Show hidden sessions toggle - only when hidden sessions exist */} + {hasHiddenSessions && ( + + )} + {/* Sort mode toggle */} + + + + + {/* Bulk action bar - shown when sessions are selected */} + {sidebarMultiSelectActive && sidebarSelectedSessionIds.length > 0 && ( +
- - -
+ + {sidebarSelectedSessionIds.length} selected + +
+ + + {showHiddenSessions && someSelectedAreHidden && ( + + )} + +
+ + )}
{ session={item.session} isActive={selectedSessionId === item.session.id} isPinned={item.isPinned} + isHidden={item.isHidden} + multiSelectActive={sidebarMultiSelectActive} + isSelected={selectedSet.has(item.session.id)} + onToggleSelect={() => toggleSidebarSessionSelection(item.session.id)} /> )}
diff --git a/src/renderer/components/sidebar/SessionContextMenu.tsx b/src/renderer/components/sidebar/SessionContextMenu.tsx index 37acea03..9aa1073b 100644 --- a/src/renderer/components/sidebar/SessionContextMenu.tsx +++ b/src/renderer/components/sidebar/SessionContextMenu.tsx @@ -7,7 +7,7 @@ import { useEffect, useRef } from 'react'; import { MAX_PANES } from '@renderer/types/panes'; -import { Pin, PinOff } from 'lucide-react'; +import { Eye, EyeOff, Pin, PinOff } from 'lucide-react'; interface SessionContextMenuProps { x: number; @@ -17,11 +17,13 @@ interface SessionContextMenuProps { sessionLabel: string; paneCount: number; isPinned: boolean; + isHidden: boolean; onClose: () => void; onOpenInCurrentPane: () => void; onOpenInNewTab: () => void; onSplitRightAndOpen: () => void; onTogglePin: () => void; + onToggleHide: () => void; } export const SessionContextMenu = ({ @@ -29,11 +31,13 @@ export const SessionContextMenu = ({ y, paneCount, isPinned, + isHidden, onClose, onOpenInCurrentPane, onOpenInNewTab, onSplitRightAndOpen, onTogglePin, + onToggleHide, }: SessionContextMenuProps): React.JSX.Element => { const menuRef = useRef(null); @@ -55,7 +59,7 @@ export const SessionContextMenu = ({ }, [onClose]); const menuWidth = 240; - const menuHeight = 180; + const menuHeight = 204; const clampedX = Math.min(x, window.innerWidth - menuWidth - 8); const clampedY = Math.min(y, window.innerHeight - menuHeight - 8); @@ -92,6 +96,11 @@ export const SessionContextMenu = ({ icon={isPinned ? : } onClick={handleClick(onTogglePin)} /> + : } + onClick={handleClick(onToggleHide)} + />
); }; diff --git a/src/renderer/components/sidebar/SessionItem.tsx b/src/renderer/components/sidebar/SessionItem.tsx index 6345765d..2a4be156 100644 --- a/src/renderer/components/sidebar/SessionItem.tsx +++ b/src/renderer/components/sidebar/SessionItem.tsx @@ -10,7 +10,7 @@ import { createPortal } from 'react-dom'; import { useStore } from '@renderer/store'; import { formatTokensCompact } from '@shared/utils/tokenFormatting'; import { formatDistanceToNowStrict } from 'date-fns'; -import { MessageSquare, Pin } from 'lucide-react'; +import { EyeOff, MessageSquare, Pin } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { OngoingIndicator } from '../common/OngoingIndicator'; @@ -23,6 +23,10 @@ interface SessionItemProps { session: Session; isActive?: boolean; isPinned?: boolean; + isHidden?: boolean; + multiSelectActive?: boolean; + isSelected?: boolean; + onToggleSelect?: () => void; } /** @@ -112,7 +116,7 @@ const ConsumptionBadge = ({ {formatTokensCompact(phase.contribution)} {phase.postCompaction != null && ( - (compacted → {formatTokensCompact(phase.postCompaction)}) + (compacted to {formatTokensCompact(phase.postCompaction)}) )} @@ -129,24 +133,42 @@ export const SessionItem = ({ session, isActive, isPinned, + isHidden, + multiSelectActive, + isSelected, + onToggleSelect, }: Readonly): React.JSX.Element => { - const { openTab, activeProjectId, selectSession, paneCount, splitPane, togglePinSession } = - useStore( - useShallow((s) => ({ - openTab: s.openTab, - activeProjectId: s.activeProjectId, - selectSession: s.selectSession, - paneCount: s.paneLayout.panes.length, - splitPane: s.splitPane, - togglePinSession: s.togglePinSession, - })) - ); + const { + openTab, + activeProjectId, + selectSession, + paneCount, + splitPane, + togglePinSession, + toggleHideSession, + } = useStore( + useShallow((s) => ({ + openTab: s.openTab, + activeProjectId: s.activeProjectId, + selectSession: s.selectSession, + paneCount: s.paneLayout.panes.length, + splitPane: s.splitPane, + togglePinSession: s.togglePinSession, + toggleHideSession: s.toggleHideSession, + })) + ); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const handleClick = (event: React.MouseEvent): void => { if (!activeProjectId) return; + // In multi-select mode, clicks toggle selection + if (multiSelectActive && onToggleSelect) { + onToggleSelect(); + return; + } + // Cmd/Ctrl+click: open in new tab; plain click: replace current tab const forceNewTab = event.ctrlKey || event.metaKey; @@ -227,12 +249,23 @@ export const SessionItem = ({ style={{ borderColor: 'var(--color-border)', ...(isActive ? { backgroundColor: 'var(--color-surface-raised)' } : {}), + ...(isHidden ? { opacity: 0.5 } : {}), }} > - {/* First line: title + ongoing indicator + pin icon */} + {/* First line: title + ongoing indicator + pin/hidden icons */}
+ {multiSelectActive && ( + onToggleSelect?.()} + onClick={(e) => e.stopPropagation()} + className="size-3.5 shrink-0 accent-blue-500" + /> + )} {session.isOngoing && } {isPinned && } + {isHidden && } setContextMenu(null)} onOpenInCurrentPane={handleOpenInCurrentPane} onOpenInNewTab={handleOpenInNewTab} onSplitRightAndOpen={handleSplitRightAndOpen} onTogglePin={() => void togglePinSession(session.id)} + onToggleHide={() => void toggleHideSession(session.id)} />, document.body )} diff --git a/src/renderer/store/slices/sessionSlice.ts b/src/renderer/store/slices/sessionSlice.ts index a7632c47..fe1862ad 100644 --- a/src/renderer/store/slices/sessionSlice.ts +++ b/src/renderer/store/slices/sessionSlice.ts @@ -34,6 +34,12 @@ export interface SessionSlice { sessionsLoadingMore: boolean; // Pinned sessions pinnedSessionIds: string[]; + // Hidden sessions + hiddenSessionIds: string[]; + showHiddenSessions: boolean; + // Multi-select + sidebarSelectedSessionIds: string[]; + sidebarMultiSelectActive: boolean; // Sort mode sessionSortMode: SessionSortMode; @@ -52,6 +58,24 @@ export interface SessionSlice { loadPinnedSessions: () => Promise; /** Set session sort mode */ setSessionSortMode: (mode: SessionSortMode) => void; + /** Toggle hide/unhide for a session */ + toggleHideSession: (sessionId: string) => Promise; + /** Bulk hide sessions */ + hideMultipleSessions: (sessionIds: string[]) => Promise; + /** Bulk unhide sessions */ + unhideMultipleSessions: (sessionIds: string[]) => Promise; + /** Load hidden sessions from config for current project */ + loadHiddenSessions: () => Promise; + /** Toggle showing hidden sessions in sidebar */ + toggleShowHiddenSessions: () => void; + /** Toggle one session's checkbox in sidebar multi-select */ + toggleSidebarSessionSelection: (sessionId: string) => void; + /** Clear all selections and exit multi-select mode */ + clearSidebarSelection: () => void; + /** Enter/exit selection mode */ + toggleSidebarMultiSelect: () => void; + /** Bulk pin for multi-select */ + pinMultipleSessions: (sessionIds: string[]) => Promise; } // ============================================================================= @@ -71,6 +95,12 @@ export const createSessionSlice: StateCreator = sessionsLoadingMore: false, // Pinned sessions pinnedSessionIds: [], + // Hidden sessions + hiddenSessionIds: [], + showHiddenSessions: false, + // Multi-select + sidebarSelectedSessionIds: [], + sidebarMultiSelectActive: false, // Sort mode sessionSortMode: 'recent' as SessionSortMode, @@ -115,8 +145,9 @@ export const createSessionSlice: StateCreator = sessionsLoading: false, }); - // Load pinned sessions after fetching session list + // Load pinned and hidden sessions after fetching session list void get().loadPinnedSessions(); + void get().loadHiddenSessions(); } catch (error) { set({ sessionsError: error instanceof Error ? error.message : 'Failed to fetch sessions', @@ -328,4 +359,150 @@ export const createSessionSlice: StateCreator = setSessionSortMode: (mode: SessionSortMode) => { set({ sessionSortMode: mode }); }, + + // Toggle hide/unhide for a session (optimistic update) + toggleHideSession: async (sessionId: string) => { + const state = get(); + const projectId = state.selectedProjectId; + if (!projectId) return; + + const isHidden = state.hiddenSessionIds.includes(sessionId); + const previousHiddenIds = state.hiddenSessionIds; + + // Optimistic: update UI immediately + if (isHidden) { + set({ hiddenSessionIds: previousHiddenIds.filter((id) => id !== sessionId) }); + } else { + set({ hiddenSessionIds: [sessionId, ...previousHiddenIds] }); + } + + try { + if (isHidden) { + await api.config.unhideSession(projectId, sessionId); + } else { + await api.config.hideSession(projectId, sessionId); + } + } catch (error) { + // Rollback on failure + set({ hiddenSessionIds: previousHiddenIds }); + logger.error('toggleHideSession error:', error); + } + }, + + // Bulk hide sessions + hideMultipleSessions: async (sessionIds: string[]) => { + const state = get(); + const projectId = state.selectedProjectId; + if (!projectId || sessionIds.length === 0) return; + + const previousHiddenIds = state.hiddenSessionIds; + const existingSet = new Set(previousHiddenIds); + const newIds = sessionIds.filter((id) => !existingSet.has(id)); + + // Optimistic update + set({ hiddenSessionIds: [...newIds, ...previousHiddenIds] }); + + try { + await api.config.hideSessions(projectId, sessionIds); + } catch (error) { + set({ hiddenSessionIds: previousHiddenIds }); + logger.error('hideMultipleSessions error:', error); + } + }, + + // Bulk unhide sessions + unhideMultipleSessions: async (sessionIds: string[]) => { + const state = get(); + const projectId = state.selectedProjectId; + if (!projectId || sessionIds.length === 0) return; + + const previousHiddenIds = state.hiddenSessionIds; + const toRemove = new Set(sessionIds); + + // Optimistic update + set({ hiddenSessionIds: previousHiddenIds.filter((id) => !toRemove.has(id)) }); + + try { + await api.config.unhideSessions(projectId, sessionIds); + } catch (error) { + set({ hiddenSessionIds: previousHiddenIds }); + logger.error('unhideMultipleSessions error:', error); + } + }, + + // Load hidden sessions from config for current project + loadHiddenSessions: async () => { + const state = get(); + const projectId = state.selectedProjectId; + if (!projectId) { + set({ hiddenSessionIds: [] }); + return; + } + + try { + const config = await api.config.get(); + const hidden = config.sessions?.hiddenSessions?.[projectId] ?? []; + const hiddenIds = hidden.map((h) => h.sessionId); + set({ hiddenSessionIds: hiddenIds }); + } catch (error) { + logger.error('loadHiddenSessions error:', error); + set({ hiddenSessionIds: [] }); + } + }, + + // Toggle showing hidden sessions in sidebar + toggleShowHiddenSessions: () => { + set((prev) => ({ showHiddenSessions: !prev.showHiddenSessions })); + }, + + // Toggle one session's checkbox in sidebar multi-select + toggleSidebarSessionSelection: (sessionId: string) => { + set((prev) => { + const selected = prev.sidebarSelectedSessionIds; + if (selected.includes(sessionId)) { + return { sidebarSelectedSessionIds: selected.filter((id) => id !== sessionId) }; + } + return { + sidebarSelectedSessionIds: [...selected, sessionId], + sidebarMultiSelectActive: true, + }; + }); + }, + + // Clear all selections and exit multi-select mode + clearSidebarSelection: () => { + set({ sidebarSelectedSessionIds: [], sidebarMultiSelectActive: false }); + }, + + // Enter/exit selection mode + toggleSidebarMultiSelect: () => { + set((prev) => { + if (prev.sidebarMultiSelectActive) { + return { sidebarMultiSelectActive: false, sidebarSelectedSessionIds: [] }; + } + return { sidebarMultiSelectActive: true }; + }); + }, + + // Bulk pin for multi-select + pinMultipleSessions: async (sessionIds: string[]) => { + const state = get(); + const projectId = state.selectedProjectId; + if (!projectId || sessionIds.length === 0) return; + + const previousPinnedIds = state.pinnedSessionIds; + const existingSet = new Set(previousPinnedIds); + const newIds = sessionIds.filter((id) => !existingSet.has(id)); + + // Optimistic update + set({ pinnedSessionIds: [...newIds, ...previousPinnedIds] }); + + try { + // Pin each session individually (no bulk pin IPC) + await Promise.all(newIds.map((sessionId) => api.config.pinSession(projectId, sessionId))); + } catch (error) { + set({ pinnedSessionIds: previousPinnedIds }); + logger.error('pinMultipleSessions error:', error); + } + }, }); diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index 47a00364..4d405b02 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -99,6 +99,14 @@ export interface ConfigAPI { pinSession: (projectId: string, sessionId: string) => Promise; /** Unpin a session for a project */ unpinSession: (projectId: string, sessionId: string) => Promise; + /** Hide a session for a project */ + hideSession: (projectId: string, sessionId: string) => Promise; + /** Unhide a session for a project */ + unhideSession: (projectId: string, sessionId: string) => Promise; + /** Bulk hide sessions for a project */ + hideSessions: (projectId: string, sessionIds: string[]) => Promise; + /** Bulk unhide sessions for a project */ + unhideSessions: (projectId: string, sessionIds: string[]) => Promise; } export interface ClaudeRootInfo { diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts index 84a133dc..245663ab 100644 --- a/src/shared/types/notifications.ts +++ b/src/shared/types/notifications.ts @@ -276,6 +276,8 @@ export interface AppConfig { sessions: { /** Pinned sessions per project. Key is projectId, value is array of pinned sessions */ pinnedSessions: Record; + /** Hidden sessions per project. Key is projectId, value is array of hidden sessions */ + hiddenSessions: Record; }; /** SSH connection settings */ ssh?: { From 056351b8a632c3aa8f6bd660419d0bdfd310e2b9 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 22:13:24 +0900 Subject: [PATCH 06/11] feat(chat): implement subagent input and compact boundary display items - Added support for rendering 'subagent_input' and 'compact_boundary' types in the chat display components. - Introduced a new `MarkdownViewer` for displaying content in both item types. - Enhanced the `MetricsPill` and `SubagentItem` components to include phase breakdowns and isolated usage metrics. - Updated the `AIGroupDisplayItem` type to accommodate new item types and their properties. - Implemented logic to compute and display token consumption across multiple phases for subagents. --- .../components/chat/DisplayItemList.tsx | 111 +++++++++++++++ .../components/chat/items/ExecutionTrace.tsx | 126 +++++++++++++++++- .../components/chat/items/MetricsPill.tsx | 53 ++++++-- .../components/chat/items/SubagentItem.tsx | 52 ++++++-- src/renderer/types/groups.ts | 10 +- src/renderer/utils/aiGroupHelpers.ts | 110 ++++++++++++++- src/renderer/utils/displayItemBuilder.ts | 81 ++++++++++- src/renderer/utils/displaySummary.ts | 7 + 8 files changed, 530 insertions(+), 20 deletions(-) diff --git a/src/renderer/components/chat/DisplayItemList.tsx b/src/renderer/components/chat/DisplayItemList.tsx index e0e82e30..8beec61f 100644 --- a/src/renderer/components/chat/DisplayItemList.tsx +++ b/src/renderer/components/chat/DisplayItemList.tsx @@ -1,11 +1,25 @@ import React, { useCallback, useState } from 'react'; +import { + CODE_BG, + CODE_BORDER, + COLOR_TEXT_MUTED, + TOOL_CALL_BG, + TOOL_CALL_BORDER, + TOOL_CALL_TEXT, +} from '@renderer/constants/cssVariables'; +import { formatTokensCompact } from '@renderer/utils/formatters'; +import { format } from 'date-fns'; +import { ChevronRight, Layers, MailOpen } from 'lucide-react'; + +import { BaseItem } from './items/BaseItem'; import { LinkedToolItem } from './items/LinkedToolItem'; import { SlashItem } from './items/SlashItem'; import { SubagentItem } from './items/SubagentItem'; import { TeammateMessageItem } from './items/TeammateMessageItem'; import { TextItem } from './items/TextItem'; import { ThinkingItem } from './items/ThinkingItem'; +import { MarkdownViewer } from './viewers/MarkdownViewer'; import type { AIGroupDisplayItem } from '@renderer/types/groups'; import type { TriggerColor } from '@shared/constants/triggerColors'; @@ -208,6 +222,103 @@ export const DisplayItemList = ({ break; } + case 'subagent_input': { + itemKey = `input-${index}`; + const inputContent = item.content; + const inputTokenCount = item.tokenCount; + element = ( + } + label="Input" + summary={truncateText(inputContent, 80)} + tokenCount={inputTokenCount} + onClick={() => onItemClick(itemKey)} + isExpanded={expandedItemIds.has(itemKey)} + > + + + ); + break; + } + + case 'compact_boundary': { + itemKey = `compact-${index}`; + const compactContent = item.content; + const compactExpanded = expandedItemIds.has(itemKey); + element = ( +
+ + {compactExpanded && compactContent && ( +
+
+ +
+
+ )} +
+ ); + break; + } + default: return null; } diff --git a/src/renderer/components/chat/items/ExecutionTrace.tsx b/src/renderer/components/chat/items/ExecutionTrace.tsx index d6010405..2f762a4c 100644 --- a/src/renderer/components/chat/items/ExecutionTrace.tsx +++ b/src/renderer/components/chat/items/ExecutionTrace.tsx @@ -1,9 +1,24 @@ import React, { useState } from 'react'; -import { CARD_ICON_MUTED } from '@renderer/constants/cssVariables'; +import { + CARD_ICON_MUTED, + CODE_BG, + CODE_BORDER, + COLOR_TEXT_MUTED, + TOOL_CALL_BG, + TOOL_CALL_BORDER, + TOOL_CALL_TEXT, +} from '@renderer/constants/cssVariables'; import { truncateText } from '@renderer/utils/aiGroupEnhancer'; +import { formatTokensCompact } from '@renderer/utils/formatters'; +import { format } from 'date-fns'; +import { ChevronRight, Layers, MailOpen } from 'lucide-react'; +import { MarkdownViewer } from '../viewers/MarkdownViewer'; + +import { BaseItem } from './BaseItem'; import { LinkedToolItem } from './LinkedToolItem'; +import { TeammateMessageItem } from './TeammateMessageItem'; import { TextItem } from './TextItem'; import { ThinkingItem } from './ThinkingItem'; @@ -142,6 +157,115 @@ export const ExecutionTrace: React.FC = ({
); + case 'subagent_input': { + const itemId = `subagent-input-${index}`; + const isExpanded = expandedItemId === itemId; + return ( + } + label="Input" + summary={truncateText(item.content, 80)} + tokenCount={item.tokenCount} + onClick={() => handleItemClick(itemId)} + isExpanded={isExpanded} + > + + + ); + } + + case 'teammate_message': { + const itemId = `subagent-teammate-${item.teammateMessage.id}-${index}`; + const isExpanded = expandedItemId === itemId; + return ( + handleItemClick(itemId)} + isExpanded={isExpanded} + /> + ); + } + + case 'compact_boundary': { + const itemId = `subagent-compact-${index}`; + const isExpanded = expandedItemId === itemId; + return ( +
+ {/* Header — matches CompactBoundary.tsx amber styling */} + + {/* Expanded content */} + {isExpanded && item.content && ( +
+
+ +
+
+ )} +
+ ); + } + default: return null; } diff --git a/src/renderer/components/chat/items/MetricsPill.tsx b/src/renderer/components/chat/items/MetricsPill.tsx index 73213a29..ff28c668 100644 --- a/src/renderer/components/chat/items/MetricsPill.tsx +++ b/src/renderer/components/chat/items/MetricsPill.tsx @@ -15,6 +15,7 @@ import { formatTokensCompact } from '@renderer/utils/formatters'; // ============================================================================= // Types // ============================================================================= +import type { PhaseTokenBreakdown } from '@renderer/types/data'; interface MetricsPillProps { mainSessionImpact?: { @@ -30,6 +31,10 @@ interface MetricsPillProps { }; /** Label override for the right segment (e.g. "Context Window" for team members) */ isolatedLabel?: string; + /** Override isolated total (for multi-phase total consumption) */ + isolatedOverride?: number; + /** Phase breakdown for tooltip (shown when multiple phases exist) */ + phaseBreakdown?: PhaseTokenBreakdown[]; } // ============================================================================= @@ -40,6 +45,8 @@ export const MetricsPill = ({ mainSessionImpact, lastUsage, isolatedLabel, + isolatedOverride, + phaseBreakdown, }: Readonly): React.ReactElement | null => { const [showTooltip, setShowTooltip] = useState(false); const [tooltipStyle, setTooltipStyle] = useState({}); @@ -47,14 +54,21 @@ export const MetricsPill = ({ const hideTimeoutRef = useRef | null>(null); const hasMainImpact = mainSessionImpact && mainSessionImpact.totalTokens > 0; - const hasIsolated = lastUsage && lastUsage.input_tokens + lastUsage.output_tokens > 0; + const hasIsolated = + isolatedOverride != null + ? isolatedOverride > 0 + : lastUsage && lastUsage.input_tokens + lastUsage.output_tokens > 0; - const isolatedTotal = lastUsage - ? lastUsage.input_tokens + - lastUsage.output_tokens + - (lastUsage.cache_read_input_tokens ?? 0) + - (lastUsage.cache_creation_input_tokens ?? 0) - : 0; + const isolatedTotal = + isolatedOverride ?? + (lastUsage + ? lastUsage.input_tokens + + lastUsage.output_tokens + + (lastUsage.cache_read_input_tokens ?? 0) + + (lastUsage.cache_creation_input_tokens ?? 0) + : 0); + + const hasPhases = phaseBreakdown && phaseBreakdown.length > 1; const clearHideTimeout = (): void => { if (hideTimeoutRef.current) { @@ -109,7 +123,7 @@ export const MetricsPill = ({ const mainValue = hasMainImpact ? formatTokensCompact(mainSessionImpact.totalTokens) : null; const isolatedValue = hasIsolated ? formatTokensCompact(isolatedTotal) : null; - const rightLabel = isolatedLabel ?? 'Isolated Usage'; + const rightLabel = isolatedLabel ?? 'Subagent Context'; return ( <> @@ -160,6 +174,29 @@ export const MetricsPill = ({
)} + {hasPhases && + phaseBreakdown.map((phase) => ( +
+ + Phase {phase.phaseNumber} + + + {formatTokensCompact(phase.peakTokens)} + {phase.postCompaction != null && ( + + {' '} + → {formatTokensCompact(phase.postCompaction)} + + )} + +
+ ))}
= ({ return null; }, [subagent.messages]); + // Multi-phase context breakdown (for subagents with compaction) + const phaseData = useMemo(() => { + if (!subagent.messages?.length) return null; + return computeSubagentPhaseBreakdown(subagent.messages); + }, [subagent.messages]); + // Search expansion const searchExpandedSubagentIds = useStore((s) => s.searchExpandedSubagentIds); const searchCurrentSubagentItemId = useStore((s) => s.searchCurrentSubagentItemId); @@ -196,12 +203,15 @@ export const SubagentItem: React.FC = ({ // Computed values for metrics const hasMainImpact = subagent.mainSessionImpact && subagent.mainSessionImpact.totalTokens > 0; const hasIsolated = lastUsage && lastUsage.input_tokens + lastUsage.output_tokens > 0; - const isolatedTotal = lastUsage - ? lastUsage.input_tokens + - lastUsage.output_tokens + - (lastUsage.cache_read_input_tokens ?? 0) + - (lastUsage.cache_creation_input_tokens ?? 0) - : 0; + const isMultiPhase = phaseData != null && phaseData.compactionCount > 0; + const isolatedTotal = isMultiPhase + ? phaseData.totalConsumption + : lastUsage + ? lastUsage.input_tokens + + lastUsage.output_tokens + + (lastUsage.cache_read_input_tokens ?? 0) + + (lastUsage.cache_creation_input_tokens ?? 0) + : 0; // Shutdown-only team activations: minimal inline row (no metrics, no expand) if (isShutdownOnly && teamColors && subagent.team) { @@ -338,6 +348,10 @@ export const SubagentItem: React.FC = ({ mainSessionImpact={subagent.team ? undefined : subagent.mainSessionImpact} lastUsage={lastUsage ?? undefined} isolatedLabel={subagent.team ? 'Context Window' : undefined} + isolatedOverride={ + phaseData && phaseData.compactionCount > 0 ? phaseData.totalConsumption : undefined + } + phaseBreakdown={phaseData?.phases} /> {/* Duration */} @@ -453,7 +467,7 @@ export const SubagentItem: React.FC = ({
- {subagent.team ? 'Context Window' : 'Isolated Usage'} + {subagent.team ? 'Context Window' : 'Subagent Context'}
= ({
)} + + {/* Per-phase breakdown when multi-phase */} + {isMultiPhase && + phaseData.phases.map((phase) => ( +
+ + Phase {phase.phaseNumber} + + + {formatTokensCompact(phase.peakTokens)} + {phase.postCompaction != null && ( + + {' '} + → {formatTokensCompact(phase.postCompaction)} + + )} + +
+ ))} )} diff --git a/src/renderer/types/groups.ts b/src/renderer/types/groups.ts index 938af9a7..639d0436 100644 --- a/src/renderer/types/groups.ts +++ b/src/renderer/types/groups.ts @@ -253,7 +253,15 @@ export type AIGroupDisplayItem = | { type: 'subagent'; subagent: Process } | { type: 'output'; content: string; timestamp: Date; tokenCount?: number } | { type: 'slash'; slash: SlashItem } - | { type: 'teammate_message'; teammateMessage: TeammateMessage }; + | { type: 'teammate_message'; teammateMessage: TeammateMessage } + | { type: 'subagent_input'; content: string; timestamp: Date; tokenCount?: number } + | { + type: 'compact_boundary'; + content: string; + timestamp: Date; + tokenDelta?: CompactionTokenDelta; + phaseNumber: number; + }; /** * The last output in an AI Group - what user sees as "the answer". diff --git a/src/renderer/utils/aiGroupHelpers.ts b/src/renderer/utils/aiGroupHelpers.ts index f1448ba3..fb07e6d5 100644 --- a/src/renderer/utils/aiGroupHelpers.ts +++ b/src/renderer/utils/aiGroupHelpers.ts @@ -7,7 +7,7 @@ import { createLogger } from '@shared/utils/logger'; import { estimateTokens } from '@shared/utils/tokenFormatting'; -import type { Process } from '../types/data'; +import type { ParsedMessage, PhaseTokenBreakdown, Process } from '../types/data'; import type { LinkedToolItem } from '../types/groups'; const logger = createLogger('Util:aiGroupHelpers'); @@ -98,3 +98,111 @@ export function attachMainSessionImpact( } return subagents; } + +/** + * Computes multi-phase context breakdown for a subagent session. + * Mirrors the algorithm in src/main/utils/jsonl.ts:500-576. + * + * Tracks assistant input tokens across compaction events to compute + * per-phase contribution and total consumption across all phases. + * + * @param messages - Subagent's ParsedMessages + * @returns Phase breakdown with total consumption, or null if no usage data + */ +export function computeSubagentPhaseBreakdown(messages: ParsedMessage[]): { + phases: PhaseTokenBreakdown[]; + totalConsumption: number; + compactionCount: number; +} | null { + let lastMainAssistantInputTokens = 0; + let awaitingPostCompaction = false; + const compactionPhases: { pre: number; post: number }[] = []; + + for (const msg of messages) { + // Track assistant input tokens. + // Unlike jsonl.ts, we don't filter by isSidechain here because subagent messages + // all have isSidechain=true (from the parent session's perspective). + if (msg.type === 'assistant' && msg.model !== '') { + const inputTokens = + (msg.usage?.input_tokens ?? 0) + + (msg.usage?.cache_read_input_tokens ?? 0) + + (msg.usage?.cache_creation_input_tokens ?? 0); + if (inputTokens > 0) { + if (awaitingPostCompaction && compactionPhases.length > 0) { + compactionPhases[compactionPhases.length - 1].post = inputTokens; + awaitingPostCompaction = false; + } + lastMainAssistantInputTokens = inputTokens; + } + } + + // Detect compaction events + if (msg.isCompactSummary) { + compactionPhases.push({ pre: lastMainAssistantInputTokens, post: 0 }); + awaitingPostCompaction = true; + } + } + + if (lastMainAssistantInputTokens <= 0) { + return null; + } + + let phaseBreakdown: PhaseTokenBreakdown[]; + + if (compactionPhases.length === 0) { + // No compaction: single phase + phaseBreakdown = [ + { + phaseNumber: 1, + contribution: lastMainAssistantInputTokens, + peakTokens: lastMainAssistantInputTokens, + }, + ]; + return { + phases: phaseBreakdown, + totalConsumption: lastMainAssistantInputTokens, + compactionCount: 0, + }; + } + + phaseBreakdown = []; + let total = 0; + + // Phase 1: tokens up to first compaction + const phase1Contribution = compactionPhases[0].pre; + total += phase1Contribution; + phaseBreakdown.push({ + phaseNumber: 1, + contribution: phase1Contribution, + peakTokens: compactionPhases[0].pre, + postCompaction: compactionPhases[0].post, + }); + + // Middle phases: contribution = pre[i] - post[i-1] + for (let i = 1; i < compactionPhases.length; i++) { + const contribution = compactionPhases[i].pre - compactionPhases[i - 1].post; + total += contribution; + phaseBreakdown.push({ + phaseNumber: i + 1, + contribution, + peakTokens: compactionPhases[i].pre, + postCompaction: compactionPhases[i].post, + }); + } + + // Last phase: final tokens - last post-compaction + const lastPhase = compactionPhases[compactionPhases.length - 1]; + const lastContribution = lastMainAssistantInputTokens - lastPhase.post; + total += lastContribution; + phaseBreakdown.push({ + phaseNumber: compactionPhases.length + 1, + contribution: lastContribution, + peakTokens: lastMainAssistantInputTokens, + }); + + return { + phases: phaseBreakdown, + totalConsumption: total, + compactionCount: compactionPhases.length, + }; +} diff --git a/src/renderer/utils/displayItemBuilder.ts b/src/renderer/utils/displayItemBuilder.ts index b0681e70..953dc645 100644 --- a/src/renderer/utils/displayItemBuilder.ts +++ b/src/renderer/utils/displayItemBuilder.ts @@ -29,6 +29,9 @@ function getDisplayItemTimestamp(item: AIGroupDisplayItem): Date { return toDate(item.slash.timestamp); case 'teammate_message': return toDate(item.teammateMessage.timestamp); + case 'subagent_input': + case 'compact_boundary': + return toDate(item.timestamp); } } @@ -320,10 +323,76 @@ export function buildDisplayItemsFromMessages( subagents.map((s) => s.parentTaskId).filter((id): id is string => !!id) ); + // Track compaction events for compact_boundary display items + let compactionCount = 0; + + // Helper to get the last assistant's total input tokens before a given index + // Note: don't filter by isSidechain — subagent messages all have isSidechain=true + function getLastAssistantInputTokens(idx: number): number { + for (let i = idx - 1; i >= 0; i--) { + const m = messages[i]; + if (m.type === 'assistant' && m.usage && m.model !== '') { + return ( + (m.usage.input_tokens ?? 0) + + (m.usage.cache_read_input_tokens ?? 0) + + (m.usage.cache_creation_input_tokens ?? 0) + ); + } + } + return 0; + } + + // Helper to get the first assistant's total input tokens after a given index + function getFirstAssistantInputTokens(idx: number): number { + for (let i = idx + 1; i < messages.length; i++) { + const m = messages[i]; + if (m.type === 'assistant' && m.usage && m.model !== '') { + return ( + (m.usage.input_tokens ?? 0) + + (m.usage.cache_read_input_tokens ?? 0) + + (m.usage.cache_creation_input_tokens ?? 0) + ); + } + } + return 0; + } + // First pass: collect tool calls and tool results from messages - for (const msg of messages) { + for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) { + const msg = messages[messageIndex]; const msgTimestamp = toDate(msg.timestamp); + // Detect compact boundary (before regular user message handling) + if (msg.isCompactSummary) { + const preTokens = getLastAssistantInputTokens(messageIndex); + const postTokens = getFirstAssistantInputTokens(messageIndex); + const rawText = + typeof msg.content === 'string' + ? msg.content + : Array.isArray(msg.content) + ? msg.content + .filter((b: { type: string; text?: string }) => b.type === 'text') + .map((b: { type: string; text?: string }) => b.text ?? '') + .join('\n\n') + : ''; + displayItems.push({ + type: 'compact_boundary', + content: rawText, + timestamp: msgTimestamp, + tokenDelta: + preTokens > 0 + ? { + preCompactionTokens: preTokens, + postCompactionTokens: postTokens, + delta: postTokens - preTokens, + } + : undefined, + phaseNumber: compactionCount + 2, + }); + compactionCount++; + continue; + } + // Check for teammate messages (non-meta user messages with content) // One user message may contain multiple blocks if (msg.type === 'user' && !msg.isMeta) { @@ -354,6 +423,16 @@ export function buildDisplayItemsFromMessages( } continue; } + // Plain-text user message (subagent input prompt) + if (rawText.trim()) { + displayItems.push({ + type: 'subagent_input', + content: rawText.trim(), + timestamp: msgTimestamp, + tokenCount: estimateTokens(rawText), + }); + } + continue; } if (msg.type === 'assistant' && Array.isArray(msg.content)) { diff --git a/src/renderer/utils/displaySummary.ts b/src/renderer/utils/displaySummary.ts index 28c6c31d..942a8c59 100644 --- a/src/renderer/utils/displaySummary.ts +++ b/src/renderer/utils/displaySummary.ts @@ -26,6 +26,8 @@ export function buildSummary(items: AIGroupDisplayItem[]): string { subagent: 0, slash: 0, teammate_message: 0, + subagent_input: 0, + compact_boundary: 0, }; const teammateNames = new Set(); @@ -62,6 +64,11 @@ export function buildSummary(items: AIGroupDisplayItem[]): string { `${counts.teammate_message} teammate ${counts.teammate_message === 1 ? 'message' : 'messages'}` ); } + if (counts.compact_boundary > 0) { + parts.push( + `${counts.compact_boundary} ${counts.compact_boundary === 1 ? 'compaction' : 'compactions'}` + ); + } return parts.length > 0 ? parts.join(', ') : 'No items'; } From ce4116dd85031a89d7dc2e08f98cad5c3399462b Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 22:57:48 +0900 Subject: [PATCH 07/11] feat(docker): add standalone mode and Docker support - Introduced a new Docker setup for running claude-devtools in standalone mode without Electron. - Added Dockerfile and docker-compose.yml for easy deployment. - Implemented .dockerignore to exclude unnecessary files from the Docker context. - Updated package.json with new scripts for building and running the standalone server. - Enhanced README with Docker usage instructions and environment variable configurations. - Modified HttpServer to support serving static files and API in standalone mode. - Updated various components to ensure compatibility with standalone operation. --- .dockerignore | 13 + .gitignore | 1 + Dockerfile | 55 ++ README.md | 62 ++- SECURITY.md | 44 +- docker-compose.yml | 29 + knip.json | 2 + package.json | 5 +- .../services/infrastructure/HttpServer.ts | 136 +++-- src/main/standalone.ts | 193 +++++++ src/renderer/api/httpClient.ts | 4 +- src/renderer/api/index.ts | 17 +- src/renderer/components/layout/TabBar.tsx | 30 +- .../components/settings/SettingsTabs.tsx | 15 +- .../settings/sections/AdvancedSection.tsx | 74 ++- .../settings/sections/GeneralSection.tsx | 499 ++++++++++-------- vite.standalone.config.ts | 115 ++++ 17 files changed, 976 insertions(+), 318 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/main/standalone.ts create mode 100644 vite.standalone.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b1fae625 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +dist-electron +dist-standalone +out +release +.git +.claude +*.md +!README.md +!SECURITY.md +!CONTRIBUTING.md +!CODE_OF_CONDUCT.md diff --git a/.gitignore b/.gitignore index 14014352..28223886 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ # Build output dist/ dist-electron/ +dist-standalone/ out/ release/ coverage/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..540aaafe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# ============================================================================= +# claude-devtools standalone Docker image +# +# Runs the HTTP server without Electron, serving the full UI over HTTP. +# Mount your ~/.claude directory to make session data available. +# +# Build: docker build -t claude-devtools . +# Run: docker run -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-devtools +# ============================================================================= + +FROM node:20-slim AS builder + +WORKDIR /app + +# Enable corepack for pnpm +RUN corepack enable + +# Install dependencies first (better layer caching) +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Copy source and build +COPY . . +RUN pnpm standalone:build + +# ============================================================================= +# Production stage — minimal image with only the built output +# ============================================================================= +FROM node:20-slim + +WORKDIR /app + +# Enable corepack for pnpm +RUN corepack enable + +# Copy package files and install production-only dependencies +# (fastify, @fastify/cors, @fastify/static are externalized from the bundle) +COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod + +# Copy built standalone server and renderer output +COPY --from=builder /app/dist-standalone ./dist-standalone +COPY --from=builder /app/out/renderer ./out/renderer + +# Create data directory for Claude session mount +RUN mkdir -p /data/.claude + +ENV NODE_ENV=production +ENV CLAUDE_ROOT=/data/.claude +ENV HOST=0.0.0.0 +ENV PORT=3456 + +EXPOSE 3456 + +CMD ["node", "dist-standalone/index.cjs"] diff --git a/README.md b/README.md index fc4d222b..15ddbe55 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Latest Release  CI Status  Downloads  - Platform + Platform


@@ -185,6 +185,66 @@ Every tool call is paired with its result in an expandable card. Specialized vie --- +## Docker / Standalone Deployment + +Run claude-devtools without Electron — in Docker, on a remote server, or anywhere Node.js runs. + +### Quick Start (Docker Compose) + +```bash +docker compose up +``` + +Open `http://localhost:3456` in your browser. + +### Quick Start (Docker) + +```bash +docker build -t claude-devtools . +docker run -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-devtools +``` + +### Quick Start (Node.js) + +```bash +pnpm install +pnpm standalone:build +node dist-standalone/index.cjs +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CLAUDE_ROOT` | `~/.claude` | Path to the `.claude` data directory | +| `HOST` | `0.0.0.0` | Bind address | +| `PORT` | `3456` | Listen port | +| `CORS_ORIGIN` | `*` (standalone) | CORS origin policy (`*`, specific origin, or comma-separated list) | + +### Notes + +- **Real-time updates may be slower than Electron.** The Electron app uses native file system watchers with IPC for instant updates. The Docker/standalone server uses SSE (Server-Sent Events) over HTTP, which may introduce slight delays when sessions are actively being written to. +- **Custom Claude root path.** If your `.claude` directory is not at `~/.claude`, update the volume mount to point to the correct location: + ```bash + # Example: Claude root at /home/user/custom-claude-dir + docker run -p 3456:3456 -v /home/user/custom-claude-dir:/data/.claude:ro claude-devtools + + # Or with docker compose, set the CLAUDE_DIR env variable: + CLAUDE_DIR=/home/user/custom-claude-dir docker compose up + ``` + +### Security-Focused Deployment + +The standalone server has **zero** outbound network calls. For maximum isolation: + +```bash +docker run --network none -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-devtools +``` + +See [SECURITY.md](SECURITY.md) for a full audit of network activity. + +--- + ## Development
diff --git a/SECURITY.md b/SECURITY.md index ce3dc57e..a886bfa9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,9 +1,50 @@ -# Security Policy +# Security & Privacy + +## Network Activity + +claude-devtools makes **zero** outbound network calls to third-party servers. There is no telemetry, analytics, tracking, or data exfiltration of any kind. + +| Network activity | When | Mode | User-initiated | +|---|---|---|---| +| GitHub Releases API (auto-updater) | App launch | Electron only | No (automatic) | +| SSH connections | Settings > SSH | Electron only | Yes | +| HTTP server (`127.0.0.1` or `0.0.0.0`) | When enabled | Both | Yes | + +### Standalone / Docker mode + +In standalone mode (Docker or `node dist-standalone/index.cjs`), the auto-updater and SSH features are disabled entirely. The only network activity is the HTTP server listening for incoming connections on the configured port. + +## Data Handling + +- All session data is read **locally** from `~/.claude/` — it never leaves your machine. +- The app does not write to session files. Volume mounts in Docker use `:ro` (read-only) by default. +- Configuration is stored at `~/.claude/claude-devtools-config.json` on the local filesystem. +- No data is sent to Anthropic, GitHub (other than the auto-updater in Electron mode), or any other third party. + +## Docker Network Isolation + +For maximum trust, run the Docker container with `--network none`: + +```bash +docker build -t claude-devtools . +docker run --network none -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-devtools +``` + +Or with Docker Compose, uncomment `network_mode: "none"` in `docker-compose.yml`. + +## IPC & Input Validation + +- All IPC handlers validate inputs with strict path containment checks +- File reads are constrained to the project root and `~/.claude/` +- Path traversal attacks are blocked +- Sensitive credential paths are rejected ## Supported Versions + Only the latest release is supported with security fixes. ## Reporting a Vulnerability + Please report vulnerabilities privately and do not open public issues for undisclosed security problems. Include: @@ -15,6 +56,7 @@ Include: If you do not have a private contact path yet, open a minimal GitHub issue asking for a secure reporting channel without disclosing technical details. ## Disclosure Process + - We will acknowledge reports as quickly as possible. - We will validate, triage severity, and prepare a fix. - We will coordinate a release and publish advisories when appropriate. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..846f3710 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +# ============================================================================= +# claude-devtools — Docker Compose +# +# Quick start: +# docker compose up +# +# Then open http://localhost:3456 in your browser. +# +# Security note: +# The standalone server has zero outbound network calls — no telemetry, +# no analytics, no auto-updater. For maximum isolation, uncomment +# network_mode below. +# ============================================================================= + +services: + claude-devtools: + build: . + ports: + - "3456:3456" + volumes: + - ${CLAUDE_DIR:-~/.claude}:/data/.claude:ro + environment: + - NODE_ENV=production + - CLAUDE_ROOT=/data/.claude + - HOST=0.0.0.0 + - PORT=3456 + restart: unless-stopped + # Uncomment for maximum network isolation (no outbound connections): + # network_mode: "none" diff --git a/knip.json b/knip.json index 76204072..07f40ecf 100644 --- a/knip.json +++ b/knip.json @@ -2,9 +2,11 @@ "$schema": "https://unpkg.com/knip@next/schema.json", "entry": [ "src/main/index.ts", + "src/main/standalone.ts", "src/preload/index.ts", "src/renderer/main.tsx", "electron.vite.config.ts", + "vite.standalone.config.ts", "remotion/index.ts", "remotion/**/*.{ts,tsx}" ], diff --git a/package.json b/package.json index c8805d1d..e6f6cb72 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,10 @@ "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", "remotion:preview": "remotion studio remotion/index.ts", "remotion:render": "remotion render remotion/index.ts DemoVideo out/demo.mp4", - "remotion:render:gif": "remotion render remotion/index.ts DemoVideo out/demo.gif --image-format png" + "remotion:render:gif": "remotion render remotion/index.ts DemoVideo out/demo.gif --image-format png", + "standalone": "tsx src/main/standalone.ts", + "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", + "standalone:start": "node dist-standalone/index.cjs" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/src/main/services/infrastructure/HttpServer.ts b/src/main/services/infrastructure/HttpServer.ts index 1f2b9bde..5e0a19cc 100644 --- a/src/main/services/infrastructure/HttpServer.ts +++ b/src/main/services/infrastructure/HttpServer.ts @@ -13,11 +13,34 @@ import { type HttpServices, registerHttpRoutes } from '@main/http'; import { broadcastEvent } from '@main/http/events'; import { createLogger } from '@shared/utils/logger'; import Fastify, { type FastifyInstance } from 'fastify'; -import { existsSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; const logger = createLogger('Service:HttpServer'); +/** + * Resolves the renderer output directory from multiple candidate paths. + * Returns the first path that exists on disk. + */ +function resolveRendererPath(): string | null { + const candidates = [ + // Electron production paths + join(__dirname, '../../../out/renderer'), + join(__dirname, '../../renderer'), + // Standalone: dist-standalone/index.cjs → ../out/renderer + join(__dirname, '../out/renderer'), + // Fallback: relative to cwd (for standalone bundles) + join(process.cwd(), 'out/renderer'), + ]; + + // Allow explicit override via env + if (process.env.RENDERER_PATH) { + candidates.unshift(process.env.RENDERER_PATH); + } + + return candidates.find((candidate) => existsSync(candidate)) ?? null; +} + export class HttpServer { private app: FastifyInstance | null = null; private port: number = 3456; @@ -28,70 +51,87 @@ export class HttpServer { * @param services - Service instances to pass to route handlers * @param sshModeSwitchCallback - Callback for SSH mode switching * @param preferredPort - Port to try first (default 3456) + * @param host - Host to bind to (default '127.0.0.1') */ async start( services: HttpServices, sshModeSwitchCallback: (mode: 'local' | 'ssh') => Promise, - preferredPort: number = 3456 + preferredPort: number = 3456, + host: string = '127.0.0.1' ): Promise { this.app = Fastify({ logger: false }); - // Register CORS - allow all localhost origins - const localhostPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/; - await this.app.register(cors, { - origin: (origin, cb) => { - // Allow requests with no origin (same-origin, curl, etc.) - if (!origin) { - cb(null, true); - return; - } - // Allow any localhost origin - if (localhostPattern.test(origin)) { - cb(null, true); - return; - } - cb(new Error('Not allowed by CORS'), false); - }, - credentials: true, - }); - - // Register static file serving (production only) - const isDev = process.env.NODE_ENV === 'development'; - if (!isDev) { - const rendererPathCandidates = [ - join(__dirname, '../../../out/renderer'), - join(__dirname, '../../renderer'), - ]; - const rendererPath = - rendererPathCandidates.find((candidate) => existsSync(candidate)) ?? - rendererPathCandidates[0]; - await this.app.register(fastifyStatic, { - root: rendererPath, - prefix: '/', - // Don't serve index.html for API routes - wildcard: false, - }); - - // Serve index.html for all non-API routes (SPA fallback) - this.app.setNotFoundHandler(async (request, reply) => { - if (request.url.startsWith('/api/')) { - return reply.status(404).send({ error: 'Not found' }); - } - return reply.sendFile('index.html'); + // Register CORS + const corsOrigin = process.env.CORS_ORIGIN; + if (corsOrigin === '*') { + // Standalone/Docker mode: allow all origins (Docker network isolation replaces CORS) + await this.app.register(cors, { origin: true, credentials: true }); + } else if (corsOrigin) { + // Custom origin(s) from env + const origins = corsOrigin.split(',').map((o) => o.trim()); + await this.app.register(cors, { origin: origins, credentials: true }); + } else { + // Default: allow all localhost origins + const localhostPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/; + await this.app.register(cors, { + origin: (origin, cb) => { + if (!origin) { + cb(null, true); + return; + } + if (localhostPattern.test(origin)) { + cb(null, true); + return; + } + cb(new Error('Not allowed by CORS'), false); + }, + credentials: true, }); } - // Register all API routes - registerHttpRoutes(this.app, services, sshModeSwitchCallback); + // Register static file serving and SPA fallback (production only) + const isDev = process.env.NODE_ENV === 'development'; + if (!isDev) { + const rendererPath = resolveRendererPath(); + if (rendererPath) { + logger.info(`Serving static files from: ${rendererPath}`); + + // Cache index.html for SPA fallback + const indexHtml = readFileSync(join(rendererPath, 'index.html'), 'utf-8'); + + await this.app.register(fastifyStatic, { + root: rendererPath, + prefix: '/', + wildcard: false, + }); + + // Register all API routes BEFORE the not-found handler + registerHttpRoutes(this.app, services, sshModeSwitchCallback); + + // SPA fallback: serve index.html for all non-API routes + this.app.setNotFoundHandler(async (request, reply) => { + if (request.url.startsWith('/api/')) { + return reply.status(404).send({ error: 'Not found' }); + } + return reply.type('text/html').send(indexHtml); + }); + } else { + logger.warn('Renderer output directory not found, serving API only'); + registerHttpRoutes(this.app, services, sshModeSwitchCallback); + } + } else { + // Dev mode: no static serving, just API routes + registerHttpRoutes(this.app, services, sshModeSwitchCallback); + } // Try ports starting from preferredPort for (let attempt = 0; attempt <= 10; attempt++) { const tryPort = preferredPort + attempt; try { - await this.app.listen({ host: '127.0.0.1', port: tryPort }); + await this.app.listen({ host, port: tryPort }); this.port = tryPort; this.running = true; - logger.info(`HTTP server started on http://127.0.0.1:${tryPort}`); + logger.info(`HTTP server started on http://${host}:${tryPort}`); return tryPort; } catch (err: unknown) { const error = err as NodeJS.ErrnoException; diff --git a/src/main/standalone.ts b/src/main/standalone.ts new file mode 100644 index 00000000..27cd29ac --- /dev/null +++ b/src/main/standalone.ts @@ -0,0 +1,193 @@ +/** + * Standalone (non-Electron) entry point for claude-devtools. + * + * Runs the HTTP server + API without Electron, suitable for Docker + * or any headless/remote environment. The renderer is served as + * static files over HTTP. + * + * Environment variables: + * - HOST: Bind address (default '0.0.0.0') + * - PORT: Listen port (default 3456) + * - CLAUDE_ROOT: Path to .claude directory (default ~/.claude) + * - CORS_ORIGIN: CORS origin policy (default '*') + */ + +import { createLogger } from '@shared/utils/logger'; + +import { HttpServer } from './services/infrastructure/HttpServer'; +import { + getProjectsBasePath, + getTodosBasePath, + setClaudeBasePathOverride, +} from './utils/pathDecoder'; +import { + LocalFileSystemProvider, + NotificationManager, + ServiceContext, +} from './services'; + +import type { HttpServices } from './http'; +import type { SshConnectionManager } from './services/infrastructure/SshConnectionManager'; +import type { UpdaterService } from './services/infrastructure/UpdaterService'; + +const logger = createLogger('Standalone'); + +// ============================================================================= +// Configuration +// ============================================================================= + +const HOST = process.env.HOST ?? '0.0.0.0'; +const PORT = parseInt(process.env.PORT ?? '3456', 10); +const CLAUDE_ROOT = process.env.CLAUDE_ROOT; + +// Default CORS to allow all in standalone mode (Docker isolation replaces CORS) +if (!process.env.CORS_ORIGIN) { + process.env.CORS_ORIGIN = '*'; +} + +// ============================================================================= +// Stub services (Electron-only features unavailable in standalone) +// ============================================================================= + +/** No-op UpdaterService stub — auto-updater requires Electron. */ +const updaterServiceStub = { + checkForUpdates: async () => {}, + downloadUpdate: async () => {}, + quitAndInstall: () => {}, + setMainWindow: () => {}, +} as unknown as UpdaterService; + +/** No-op SshConnectionManager stub — SSH is managed per-user in the Electron app. */ +const sshConnectionManagerStub = { + getStatus: () => ({ state: 'disconnected' as const, host: null, error: null, remoteProjectsPath: null }), + getProvider: () => new LocalFileSystemProvider(), + isRemote: () => false, + connect: async () => {}, + disconnect: () => {}, + testConnection: async () => ({ success: false, error: 'SSH not available in standalone mode' }), + getConfigHosts: async () => [], + resolveHostConfig: async () => null, + dispose: () => {}, + on: () => sshConnectionManagerStub, + off: () => sshConnectionManagerStub, + emit: () => false, +} as unknown as SshConnectionManager; + +// ============================================================================= +// Application State +// ============================================================================= + +let localContext: ServiceContext; +let notificationManager: NotificationManager; +let httpServer: HttpServer; + +// ============================================================================= +// Lifecycle +// ============================================================================= + +async function start(): Promise { + logger.info('Starting standalone server...'); + + // Apply Claude root override if set + if (CLAUDE_ROOT) { + setClaudeBasePathOverride(CLAUDE_ROOT); + logger.info(`Using CLAUDE_ROOT: ${CLAUDE_ROOT}`); + } + + const projectsDir = getProjectsBasePath(); + const todosDir = getTodosBasePath(); + + logger.info(`Projects directory: ${projectsDir}`); + logger.info(`Todos directory: ${todosDir}`); + + // Create local context (the only context in standalone mode) + localContext = new ServiceContext({ + id: 'local', + type: 'local', + fsProvider: new LocalFileSystemProvider(), + projectsDir, + todosDir, + }); + localContext.start(); + + // Initialize notification manager + notificationManager = NotificationManager.getInstance(); + localContext.fileWatcher.setNotificationManager(notificationManager); + + // Create HTTP server + httpServer = new HttpServer(); + + // Wire file watcher events to SSE broadcast + localContext.fileWatcher.on('file-change', (event: unknown) => { + httpServer.broadcast('file-change', event); + }); + localContext.fileWatcher.on('todo-change', (event: unknown) => { + httpServer.broadcast('todo-change', event); + }); + + // Forward notification events to SSE + notificationManager.on('notification-new', (notification: unknown) => { + httpServer.broadcast('notification:new', notification); + }); + notificationManager.on('notification-updated', (data: unknown) => { + httpServer.broadcast('notification:updated', data); + }); + notificationManager.on('notification-clicked', (data: unknown) => { + httpServer.broadcast('notification:clicked', data); + }); + + // Build services for HTTP routes + const services: HttpServices = { + projectScanner: localContext.projectScanner, + sessionParser: localContext.sessionParser, + subagentResolver: localContext.subagentResolver, + chunkBuilder: localContext.chunkBuilder, + dataCache: localContext.dataCache, + updaterService: updaterServiceStub, + sshConnectionManager: sshConnectionManagerStub, + }; + + // No-op mode switch handler (no SSH in standalone) + const modeSwitchHandler = async () => {}; + + // Start the server + const port = await httpServer.start(services, modeSwitchHandler, PORT, HOST); + logger.info(`Standalone server running at http://${HOST}:${port}`); + logger.info('Open in your browser to view Claude Code sessions'); +} + +async function shutdown(): Promise { + logger.info('Shutting down...'); + + if (httpServer?.isRunning()) { + await httpServer.stop(); + } + + if (localContext) { + localContext.dispose(); + } + + logger.info('Shutdown complete'); + process.exit(0); +} + +// ============================================================================= +// Signal Handlers +// ============================================================================= + +process.on('SIGTERM', () => void shutdown()); +process.on('SIGINT', () => void shutdown()); + +process.on('unhandledRejection', (reason) => { + logger.error('Unhandled promise rejection:', reason); +}); + +process.on('uncaughtException', (error) => { + logger.error('Uncaught exception:', error); +}); + +// ============================================================================= +// Start +// ============================================================================= + +void start(); diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 0d038926..26f904ac 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -48,8 +48,8 @@ export class HttpAPIClient implements ElectronAPI { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- event callbacks have varying signatures private eventListeners = new Map void>>(); - constructor(port: number) { - this.baseUrl = `http://127.0.0.1:${port}`; + constructor(baseUrl: string) { + this.baseUrl = baseUrl; this.initEventSource(); } diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index 1d59170e..c314cd12 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -16,9 +16,20 @@ import { HttpAPIClient } from './httpClient'; import type { ElectronAPI } from '@shared/types/api'; -function getHttpPort(): number { +/** + * Resolves the base URL for the HTTP API client. + * + * - Electron "server mode" (browser opened via ?port=XXXX): use explicit port on 127.0.0.1 + * - Standalone/Docker (page served by the same server): use window.location.origin + * to avoid cross-origin issues (localhost vs 127.0.0.1) + */ +function getHttpBaseUrl(): string { const params = new URLSearchParams(window.location.search); - return parseInt(params.get('port') ?? '3456', 10); + const explicitPort = params.get('port'); + if (explicitPort) { + return `http://127.0.0.1:${parseInt(explicitPort, 10)}`; + } + return window.location.origin; } let httpClient: HttpAPIClient | null = null; @@ -28,7 +39,7 @@ function getImpl(): ElectronAPI { // Lazily create the HTTP client only when actually needed (browser mode). // Caching avoids creating multiple EventSource connections. if (!httpClient) { - httpClient = new HttpAPIClient(getHttpPort()); + httpClient = new HttpAPIClient(getHttpBaseUrl()); } return httpClient; } diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 25726017..6748a0a5 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -392,22 +392,20 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { )} - {/* Settings gear icon (Electron only - browser can't access native settings) */} - {isElectronMode() && ( - - )} + {/* Settings gear icon */} + {/* Context menu */} diff --git a/src/renderer/components/settings/SettingsTabs.tsx b/src/renderer/components/settings/SettingsTabs.tsx index 2eb1e9f2..4995d64e 100644 --- a/src/renderer/components/settings/SettingsTabs.tsx +++ b/src/renderer/components/settings/SettingsTabs.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; +import { isElectronMode } from '@renderer/api'; import { Bell, HardDrive, Server, Settings, Wrench } from 'lucide-react'; export type SettingsSection = 'general' | 'connection' | 'workspace' | 'notifications' | 'advanced'; @@ -13,12 +14,13 @@ interface TabConfig { id: SettingsSection; label: string; icon: React.ComponentType<{ className?: string }>; + electronOnly?: boolean; } const tabs: TabConfig[] = [ { id: 'general', label: 'General', icon: Settings }, - { id: 'connection', label: 'Connection', icon: Server }, - { id: 'workspace', label: 'Workspaces', icon: HardDrive }, + { id: 'connection', label: 'Connection', icon: Server, electronOnly: true }, + { id: 'workspace', label: 'Workspaces', icon: HardDrive, electronOnly: true }, { id: 'notifications', label: 'Notifications', icon: Bell }, { id: 'advanced', label: 'Advanced', icon: Wrench }, ]; @@ -28,10 +30,15 @@ export const SettingsTabs = ({ onSectionChange, }: Readonly): React.JSX.Element => { const [hoveredTab, setHoveredTab] = useState(null); + const isElectron = useMemo(() => isElectronMode(), []); + const visibleTabs = useMemo( + () => tabs.filter((tab) => !tab.electronOnly || isElectron), + [isElectron] + ); return (
- {tabs.map((tab) => { + {visibleTabs.map((tab) => { const Icon = tab.icon; const isActive = activeSection === tab.id; const isHovered = hoveredTab === tab.id; diff --git a/src/renderer/components/settings/sections/AdvancedSection.tsx b/src/renderer/components/settings/sections/AdvancedSection.tsx index 3511067b..85925a7d 100644 --- a/src/renderer/components/settings/sections/AdvancedSection.tsx +++ b/src/renderer/components/settings/sections/AdvancedSection.tsx @@ -2,9 +2,9 @@ * AdvancedSection - Advanced settings including config management and about info. */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { api } from '@renderer/api'; +import { api, isElectronMode } from '@renderer/api'; import appIcon from '@renderer/favicon.png'; import { useStore } from '@renderer/store'; import { CheckCircle, Code2, Download, Loader2, RefreshCw, Upload } from 'lucide-react'; @@ -26,6 +26,7 @@ export const AdvancedSection = ({ onImportConfig, onOpenInEditor, }: AdvancedSectionProps): React.JSX.Element => { + const isElectron = useMemo(() => isElectronMode(), []); const [version, setVersion] = useState(''); const updateStatus = useStore((s) => s.updateStatus); const availableVersion = useStore((s) => s.availableVersion); @@ -128,17 +129,19 @@ export const AdvancedSection = ({ Import Config - + {isElectron && ( + + )}
@@ -149,22 +152,35 @@ export const AdvancedSection = ({

claude-devtools

- + {isElectron && ( + + )} + {!isElectron && ( + + Standalone + + )}

Version {version || '...'} diff --git a/src/renderer/components/settings/sections/GeneralSection.tsx b/src/renderer/components/settings/sections/GeneralSection.tsx index 492ef7d3..cd28a176 100644 --- a/src/renderer/components/settings/sections/GeneralSection.tsx +++ b/src/renderer/components/settings/sections/GeneralSection.tsx @@ -2,9 +2,9 @@ * GeneralSection - General settings including startup, appearance, browser access, and local Claude root. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; -import { api } from '@renderer/api'; +import { api, isElectronMode } from '@renderer/api'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { useStore } from '@renderer/store'; import { getFullResetState } from '@renderer/store/utils/stateResetHelpers'; @@ -245,24 +245,36 @@ export const GeneralSection = ({ const isWindowsStyleDefaultPath = /^[a-zA-Z]:\\/.test(defaultClaudeRootPath) || defaultClaudeRootPath.startsWith('\\\\'); + const isElectron = useMemo(() => isElectronMode(), []); + return (

- - - onGeneralToggle('launchAtLogin', v)} - disabled={saving} - /> - - {window.navigator.userAgent.includes('Macintosh') && ( - - onGeneralToggle('showDockIcon', v)} - disabled={saving} - /> - + {isElectron && ( + <> + + + onGeneralToggle('launchAtLogin', v)} + disabled={saving} + /> + + {window.navigator.userAgent.includes('Macintosh') && ( + + onGeneralToggle('showDockIcon', v)} + disabled={saving} + /> + + )} + )} @@ -275,222 +287,283 @@ export const GeneralSection = ({ /> - -

- Choose which local folder is treated as your Claude data root -

+ {isElectron && ( + <> + +

+ Choose which local folder is treated as your Claude data root +

- -
-
- {resolvedClaudeRootPath} -
-
- Auto-detected: {defaultClaudeRootPath} -
-
-
+ +
+
+ {resolvedClaudeRootPath} +
+
+ Auto-detected: {defaultClaudeRootPath} +
+
+
-
- + + + + {isWindowsStyleDefaultPath && ( + )} - Select Folder - - +
- + {claudeRootError && ( +
+

{claudeRootError}

+
+ )} - {isWindowsStyleDefaultPath && ( - - )} -
+ {showWslModal && ( +
+ +
+ ))} + + +
+
- ))} + - -
- - -
- - + )} + )} - - - {serverLoading ? ( - - ) : ( - - )} - + {isElectron ? ( + <> + + + {serverLoading ? ( + + ) : ( + + )} + - {serverStatus.running && ( -
-
- - Running on - - +
+ + Running on + + + {serverUrl} + + +
+ )} + + ) : ( + <> + +
- {serverUrl} - - -
+
+ + Running on + + + {window.location.origin} + + +
+

+ Running in standalone mode. The HTTP server is always active. System notifications are + not available — notification triggers are logged in-app only. +

+ )}
); diff --git a/vite.standalone.config.ts b/vite.standalone.config.ts new file mode 100644 index 00000000..ac24a4d7 --- /dev/null +++ b/vite.standalone.config.ts @@ -0,0 +1,115 @@ +/** + * Vite build config for the standalone (non-Electron) server. + * + * Produces a single CJS bundle at dist-standalone/index.cjs that can be + * run with `node dist-standalone/index.cjs`. + */ + +import { resolve } from 'path' +import { defineConfig } from 'vite' + +import type { Plugin } from 'vite' + +// Node.js built-in modules that should be externalized +const nodeBuiltins = new Set([ + 'fs', 'path', 'os', 'events', 'stream', 'util', 'net', 'tls', + 'http', 'https', 'crypto', 'zlib', 'url', 'querystring', + 'child_process', 'buffer', 'dns', 'dgram', 'assert', 'constants', + 'readline', 'string_decoder', 'timers', 'tty', 'worker_threads' +]) + +// Packages that must be externalized because they break when bundled +// (fastify ecosystem uses internal file resolution that doesn't survive bundling) +const externalPackages = [ + 'fastify', '@fastify/cors', '@fastify/static' +] + +// Stub native .node addons (ssh2/cpu-features have JS fallbacks) +function nativeModuleStub(): Plugin { + const STUB_ID = '\0native-stub' + return { + name: 'native-module-stub', + resolveId(source) { + if (source.endsWith('.node')) return STUB_ID + return null + }, + load(id) { + if (id === STUB_ID) return 'export default {}' + return null + } + } +} + +// Stub out Electron imports with empty modules +const electronModules = new Set(['electron', 'electron-updater']) + +function electronStub(): Plugin { + const ELECTRON_STUB_ID = '\0electron-stub' + // Comprehensive stub covering all electron exports used in the codebase + const electronStubCode = ` +const noop = () => {}; +const noopClass = class {}; +const handler = { get: () => noop }; +const proxyObj = new Proxy({}, handler); +export const app = proxyObj; +export const BrowserWindow = noopClass; +export const ipcMain = { handle: noop, on: noop, removeHandler: noop }; +export const shell = { openPath: noop, openExternal: noop }; +export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) }; +export const Notification = class { show() {} }; +export default proxyObj; +` + return { + name: 'electron-stub', + // Use enforce: 'pre' to intercept before Vite's SSR externalization + enforce: 'pre', + resolveId(source) { + if (electronModules.has(source)) return ELECTRON_STUB_ID + return null + }, + load(id) { + if (id === ELECTRON_STUB_ID) return electronStubCode + return null + } + } +} + +export default defineConfig({ + plugins: [nativeModuleStub(), electronStub()], + resolve: { + alias: { + '@main': resolve(__dirname, 'src/main'), + '@shared': resolve(__dirname, 'src/shared'), + '@preload': resolve(__dirname, 'src/preload') + } + }, + ssr: { + // Force Vite to bundle these instead of externalizing them + // (SSR mode externalizes all node_modules by default) + noExternal: true + }, + build: { + outDir: 'dist-standalone', + target: 'node20', + ssr: true, + rollupOptions: { + input: { + index: resolve(__dirname, 'src/main/standalone.ts') + }, + output: { + format: 'cjs', + entryFileNames: '[name].cjs' + }, + external: (id) => { + // Externalize Node.js built-ins + if (id.startsWith('node:')) return true + if (nodeBuiltins.has(id)) return true + // Externalize packages that break when bundled + if (externalPackages.some(pkg => id === pkg || id.startsWith(pkg + '/'))) return true + return false + } + }, + minify: false, + sourcemap: true + } +}) From ea66c34ce33460157c81fe66630c999257031bd8 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 23:12:28 +0900 Subject: [PATCH 08/11] refactor(package): remove unused Remotion scripts and update HttpServer for improved static file serving - Removed obsolete Remotion preview and render scripts from package.json. - Updated HttpServer to enhance static file serving logic, ensuring proper handling of renderer paths in both development and production modes. - Added support for asarUnpack in package.json to facilitate unpacking of renderer files. --- package.json | 7 +-- .../services/infrastructure/HttpServer.ts | 58 +++++++++---------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index e6f6cb72..86e17485 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,6 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", - "remotion:preview": "remotion studio remotion/index.ts", - "remotion:render": "remotion render remotion/index.ts DemoVideo out/demo.mp4", - "remotion:render:gif": "remotion render remotion/index.ts DemoVideo out/demo.gif --image-format png", "standalone": "tsx src/main/standalone.ts", "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", "standalone:start": "node dist-standalone/index.cjs" @@ -56,7 +53,6 @@ "@dnd-kit/utilities": "^3.2.2", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.0.0", - "@remotion/light-leaks": "4.0.421", "@tanstack/react-virtual": "^3.10.8", "date-fns": "^3.6.0", "electron-updater": "^6.7.3", @@ -133,6 +129,9 @@ "package.json" ], "asar": true, + "asarUnpack": [ + "out/renderer/**" + ], "npmRebuild": false, "extraMetadata": { "main": "dist-electron/main/index.cjs" diff --git a/src/main/services/infrastructure/HttpServer.ts b/src/main/services/infrastructure/HttpServer.ts index 5e0a19cc..8c20ffe0 100644 --- a/src/main/services/infrastructure/HttpServer.ts +++ b/src/main/services/infrastructure/HttpServer.ts @@ -24,12 +24,13 @@ const logger = createLogger('Service:HttpServer'); */ function resolveRendererPath(): string | null { const candidates = [ - // Electron production paths - join(__dirname, '../../../out/renderer'), - join(__dirname, '../../renderer'), + // Electron production (asarUnpack): app.asar.unpacked/out/renderer (real filesystem) + join(__dirname, '../../out/renderer').replace('app.asar', 'app.asar.unpacked'), + // Electron production (asar fallback): app.asar/out/renderer + join(__dirname, '../../out/renderer'), // Standalone: dist-standalone/index.cjs → ../out/renderer join(__dirname, '../out/renderer'), - // Fallback: relative to cwd (for standalone bundles) + // Fallback: relative to cwd (dev mode, standalone) join(process.cwd(), 'out/renderer'), ]; @@ -89,38 +90,33 @@ export class HttpServer { }); } - // Register static file serving and SPA fallback (production only) - const isDev = process.env.NODE_ENV === 'development'; - if (!isDev) { - const rendererPath = resolveRendererPath(); - if (rendererPath) { - logger.info(`Serving static files from: ${rendererPath}`); + // Register static file serving and SPA fallback when renderer output exists. + // In dev mode this requires a prior `pnpm build`; in production/standalone it's always present. + const rendererPath = resolveRendererPath(); + if (rendererPath) { + logger.info(`Serving static files from: ${rendererPath}`); - // Cache index.html for SPA fallback - const indexHtml = readFileSync(join(rendererPath, 'index.html'), 'utf-8'); + // Cache index.html for SPA fallback + const indexHtml = readFileSync(join(rendererPath, 'index.html'), 'utf-8'); - await this.app.register(fastifyStatic, { - root: rendererPath, - prefix: '/', - wildcard: false, - }); + await this.app.register(fastifyStatic, { + root: rendererPath, + prefix: '/', + wildcard: false, + }); - // Register all API routes BEFORE the not-found handler - registerHttpRoutes(this.app, services, sshModeSwitchCallback); + // Register all API routes BEFORE the not-found handler + registerHttpRoutes(this.app, services, sshModeSwitchCallback); - // SPA fallback: serve index.html for all non-API routes - this.app.setNotFoundHandler(async (request, reply) => { - if (request.url.startsWith('/api/')) { - return reply.status(404).send({ error: 'Not found' }); - } - return reply.type('text/html').send(indexHtml); - }); - } else { - logger.warn('Renderer output directory not found, serving API only'); - registerHttpRoutes(this.app, services, sshModeSwitchCallback); - } + // SPA fallback: serve index.html for all non-API routes + this.app.setNotFoundHandler(async (request, reply) => { + if (request.url.startsWith('/api/')) { + return reply.status(404).send({ error: 'Not found' }); + } + return reply.type('text/html').send(indexHtml); + }); } else { - // Dev mode: no static serving, just API routes + logger.warn('Renderer output directory not found (run `pnpm build` first), serving API only'); registerHttpRoutes(this.app, services, sshModeSwitchCallback); } From 13d99e5968948aa299c4504515248c4d2c5b2d6b Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 23:12:51 +0900 Subject: [PATCH 09/11] chore(package): remove unused Remotion dependencies from package.json - Deleted obsolete Remotion packages from devDependencies in package.json to streamline the project and reduce unnecessary bloat. --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index 86e17485..ac50ab09 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,6 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", "@eslint/js": "^9.39.2", - "@remotion/cli": "^4.0.421", - "@remotion/google-fonts": "^4.0.421", - "@remotion/media": "^4.0.421", - "@remotion/transitions": "^4.0.421", "@tailwindcss/typography": "^0.5.19", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", @@ -109,7 +105,6 @@ "postcss": "^8.4.35", "prettier": "^3.8.1", "prettier-plugin-tailwindcss": "^0.7.2", - "remotion": "^4.0.421", "tailwindcss": "^3.4.1", "tsx": "^4.21.0", "typescript": "^5.9.3", From da1a8998fc720c284b439d6029a82c1870da756c Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 16 Feb 2026 23:27:43 +0900 Subject: [PATCH 10/11] chore: clean up project configuration and remove unused dependencies - Updated knip.json to exclude unused Remotion paths and dependencies. - Cleaned up pnpm-lock.yaml by removing obsolete Remotion packages. - Refactored TypeScript function signatures in main files for improved clarity. - Enhanced various components for better code readability and maintainability. --- knip.json | 7 +- pnpm-lock.yaml | 1393 +---------------- src/main/index.ts | 4 +- .../services/infrastructure/HttpServer.ts | 1 + src/main/standalone.ts | 15 +- .../components/RankedInjectionList.tsx | 2 +- .../components/common/ConfirmDialog.tsx | 1 + .../components/common/UpdateBanner.tsx | 12 +- .../components/layout/SidebarHeader.tsx | 4 +- .../components/sidebar/SessionItem.tsx | 29 +- src/renderer/store/index.ts | 3 +- 11 files changed, 46 insertions(+), 1425 deletions(-) diff --git a/knip.json b/knip.json index 07f40ecf..8f8f03a3 100644 --- a/knip.json +++ b/knip.json @@ -6,11 +6,9 @@ "src/preload/index.ts", "src/renderer/main.tsx", "electron.vite.config.ts", - "vite.standalone.config.ts", - "remotion/index.ts", - "remotion/**/*.{ts,tsx}" + "vite.standalone.config.ts" ], - "project": ["src/**/*.{ts,tsx}!", "remotion/**/*.{ts,tsx}!"], + "project": ["src/**/*.{ts,tsx}!"], "ignore": ["tsconfig*.json"], "paths": { "@main/*": ["./src/main/*"], @@ -18,6 +16,5 @@ "@preload/*": ["./src/preload/*"], "@shared/*": ["./src/shared/*"] }, - "ignoreDependencies": ["@remotion/light-leaks", "remotion"], "ignoreBinaries": ["pkg"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2926554f..756829c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@fastify/static': specifier: ^9.0.0 version: 9.0.0 - '@remotion/light-leaks': - specifier: 4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': specifier: ^3.10.8 version: 3.13.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -81,18 +78,6 @@ importers: '@eslint/js': specifier: ^9.39.2 version: 9.39.2 - '@remotion/cli': - specifier: ^4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/google-fonts': - specifier: ^4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/media': - specifier: ^4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/transitions': - specifier: ^4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) @@ -189,9 +174,6 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.7.2 version: 0.7.2(prettier@3.8.1) - remotion: - specifier: ^4.0.421 - version: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: ^3.4.1 version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) @@ -278,11 +260,6 @@ packages: resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.24.1': - resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.28.6': resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} engines: {node: '>=6.0.0'} @@ -407,12 +384,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -425,12 +396,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.27.2': resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} @@ -443,12 +408,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.27.2': resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} @@ -461,12 +420,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.27.2': resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} @@ -479,12 +432,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.27.2': resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} @@ -497,12 +444,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.27.2': resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} @@ -515,12 +456,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.27.2': resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} @@ -533,12 +468,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} @@ -551,12 +480,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.27.2': resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} @@ -569,12 +492,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.27.2': resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} @@ -587,12 +504,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.27.2': resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} @@ -605,12 +516,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.27.2': resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} @@ -623,12 +528,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.27.2': resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} @@ -641,12 +540,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.27.2': resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} @@ -659,12 +552,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.27.2': resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} @@ -677,12 +564,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.27.2': resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} @@ -695,24 +576,12 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.27.2': resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.27.2': resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} @@ -725,24 +594,12 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.27.2': resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} @@ -755,12 +612,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} @@ -779,12 +630,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.27.2': resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} @@ -797,12 +642,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.27.2': resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} @@ -815,12 +654,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.27.2': resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} @@ -833,12 +666,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.27.2': resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} @@ -1127,137 +954,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@remotion/bundler@4.0.421': - resolution: {integrity: sha512-3udLfwmgJeO6r0bZZ+mkSFYJ7qTWp93lQvo5W2H091uXbGl00r7DI4pfnMQQhLAubwPq+XTWd0jgp5JMhLe2cQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/cli@4.0.421': - resolution: {integrity: sha512-h2yA12Bd9NIfZpxxF5eAdhH8o9S/MZION2aXiYI3TwYmaCnjBqeorJMKFe0qWGJkTAvKAg602HEuqY3eQD19pw==} - hasBin: true - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/compositor-darwin-arm64@4.0.421': - resolution: {integrity: sha512-wtvMo81SIHhtE7RdpbAGOAYtJWgrap2jowbKsBVoAvnuLp4uBYLEkPho8nkjoG9XTTlblD02r8kyBhpUnrAwFQ==} - cpu: [arm64] - os: [darwin] - - '@remotion/compositor-darwin-x64@4.0.421': - resolution: {integrity: sha512-hmLglQL7l3CQoX0ZmIzg0ojxRQInQL810pnB/AGXcTUTVRkxeRhUc/NXTj8TEqgZgSD1ObYgGlA27ysBSl9+ig==} - cpu: [x64] - os: [darwin] - - '@remotion/compositor-linux-arm64-gnu@4.0.421': - resolution: {integrity: sha512-7FNViAhIBVn46LXdy0kahi2BVTBqXQ5B5+pKZftDY7u8oRlyVQvAn4AcbmhU5ZZ6/Dp3+Qp6NocK16bzcPGJiA==} - cpu: [arm64] - os: [linux] - - '@remotion/compositor-linux-arm64-musl@4.0.421': - resolution: {integrity: sha512-ofPJDXEiCh1l0jUEI2dBhICxjCrN2rJuiOMuhrYd9IlGc2wA1zu7I3/06uvatV5k3aakT6MHkKh+wVys4fagBQ==} - cpu: [arm64] - os: [linux] - - '@remotion/compositor-linux-x64-gnu@4.0.421': - resolution: {integrity: sha512-TtqtDMPYM3IhnU0/YgOhvEtV5VHT8FZnbrR8ZaoT8QEvaRhGhr+QesmEA7KtTTbBpqUBeYmV5AhAX9Q0rx+43A==} - cpu: [x64] - os: [linux] - - '@remotion/compositor-linux-x64-musl@4.0.421': - resolution: {integrity: sha512-ilOCMkaBAUbRE6HN4iFfsMWcXVHai8IPQocUyMfDseBV5OjIaBhqybriF6AkmzWF4Fx6ylokzl4WXX6fAwdgQg==} - cpu: [x64] - os: [linux] - - '@remotion/compositor-win32-x64-msvc@4.0.421': - resolution: {integrity: sha512-L4yygd3YWXbo2JakG5Gru4fR4IDAC6+5BAvo4tZm7gOwMUEO38fn0h+pgWMqCi1zpC1yEfdv9LpClHowcN4lYw==} - cpu: [x64] - os: [win32] - - '@remotion/google-fonts@4.0.421': - resolution: {integrity: sha512-PRJJKq2nZ6MOXibUWNCnXzEySFovvg5GdCNyxaRZYDnSUX5otT2w53A4/Fu7h7vXWVceew2OhZD8pOv/md+87A==} - - '@remotion/licensing@4.0.421': - resolution: {integrity: sha512-kSbssnwkTXDxtY/PXzu9Q6mFt9jmgNN6wygZxxH6gy01lzua1ucivSZOSrrdRRbtF0kk3BZ4EqOqW5D5ovyHXA==} - - '@remotion/light-leaks@4.0.421': - resolution: {integrity: sha512-+udVz+5zvNBiYago7KJeO7BcQJrHUarFKWpcWtwhNHbwjaBAvtZ/UGzk9ZUG6wrsUdKtkDLKK2l8wH77etJDAw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/media-parser@4.0.421': - resolution: {integrity: sha512-Pv/63mN4gnG5hP2+7ldWy67u2FoIOmN3lijEzk3w/e4b5dvJp+kWcXGbUszePbDxFF0NnJrP4clj6iLLB0M9bw==} - - '@remotion/media-utils@4.0.421': - resolution: {integrity: sha512-kL0uR8bnSW27Lg3SozKO+LP1kiFLpM40GKeAWftrzvJXbAUc5XZTEvldRbtw0YUXJvD5SsJvYH8FU0HB27N28A==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/media@4.0.421': - resolution: {integrity: sha512-YQ5r2aLIeyF+PYTtUZWeqDldqXCyBBiW9hScivrWAlj1NL7m8bk9wmqMyKj3E0e98y6osWsgPIUbh6TA+xN6SQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/paths@4.0.421': - resolution: {integrity: sha512-YFMf589aqN5OY0LifNyeDBDH9G59Ns24g31c/59E7MZs3QiKSDOasYuFgTJZCK4sKHwlhJ4DcH7d4BC/6y3GJw==} - - '@remotion/player@4.0.421': - resolution: {integrity: sha512-vZrvvl3OMmxsmIk+vqUOrcbLpNUL0Q1JIWdqcgNt0d3Jn0mK6fYCDBnAHuhiltuMSqHBguUaQL4eWbceDBsm2g==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/renderer@4.0.421': - resolution: {integrity: sha512-QYfnSq69HOHI9n3Z2lLyNK8liha2d0je6h5UnFO4Wi7WSQi5ZSey0aCkL6Mva6x5ckX/eAm8dptKUpnJWLY5uw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/shapes@4.0.421': - resolution: {integrity: sha512-qBKbo2/KlKi9wsxvtpt4uwI+HzW30ylTaSBT3nlpPKXrkA43lEMQrr5ZaDM5IZmIWe+jcg/xxlvvmVm1AGDJUQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/streaming@4.0.421': - resolution: {integrity: sha512-H5+VyDt1aKdUXkZVaggIEuXgD1kN21H1O5tLSP52Qt4/IOaGcCoVFaNKbeBZuUyM9OWeO2xHAYX/CHz8OlQt3Q==} - - '@remotion/studio-server@4.0.421': - resolution: {integrity: sha512-QiRfrK15dUL8BVDTaglU6ZWUDL4FTpH6cgM5vAji2IBNTmH9OqzyHMpTKq53DX63Z1rL3oIYKk5Sz/qHt9Cofw==} - - '@remotion/studio-shared@4.0.421': - resolution: {integrity: sha512-T2eXFHVG118LSA1QwR/g4LFHrd+VYDmFmoXWWUZKbXJwNYYtVyUBSajfgJfvprDlxVbYnGB95V0ayYVGe5deMg==} - - '@remotion/studio@4.0.421': - resolution: {integrity: sha512-aqt841T1b5PoIPuokoKvRjYCp0uG3PAcQgO8x8JOoI0OgnBU7HpTjq5Np6fIdMNOpnqMA1VrOXGWQqRHFFMkwA==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/transitions@4.0.421': - resolution: {integrity: sha512-reAeMALcLGTbWvpPgNHAOc9Gulc5ADnIZlKaiv3No4OzCbUzN8jWQtxbGCzcqQTtnaDQggkelwaB6j3swkb5Sg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@remotion/web-renderer@4.0.421': - resolution: {integrity: sha512-VY+v/9gfz4cQB0Bt/NbtlpkGuCbxoqrN/Y/1BXNyvE+cMUn4TpNy/byWKGn2qytPb2jlweDAwLUb9l9otzFzKg==} - peerDependencies: - react: '>=18.0.0' - react-dom: '>=18.0.0' - - '@remotion/webcodecs@4.0.421': - resolution: {integrity: sha512-Rto6i4ZRK4PTQcCaU0JeyL58iCVoNuwTgWxTK9vdUaE5sOX5SB6jsEO4LmQkg428oA11lbZG48A/MLvErNb7DQ==} - - '@remotion/zod-types@4.0.421': - resolution: {integrity: sha512-PBU1+OLZjUNF88XFwKKbfi7+uLKIxN9MSpxjpJHokOumOdDUnpqJv2zs/TwSBkIPFKFd40Crk+FFZk4VT0fw3A==} - peerDependencies: - zod: 3.22.3 - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1442,18 +1138,6 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/dom-mediacapture-transform@0.1.11': - resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==} - - '@types/dom-webcodecs@0.1.13': - resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==} - - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1726,73 +1410,16 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1819,14 +1446,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1840,11 +1459,6 @@ packages: peerDependencies: ajv: ^6.9.1 - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1970,10 +1584,6 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - ast-v8-to-istanbul@0.3.10: resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} @@ -2041,9 +1651,6 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2193,10 +1800,6 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - chromium-pickle-js@0.2.0: resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} @@ -2317,12 +1920,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-loader@5.2.7: - resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.27.0 || ^5.0.0 - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2391,10 +1988,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -2518,20 +2111,12 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2558,9 +2143,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2585,11 +2167,6 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -2721,10 +2298,6 @@ packages: peerDependencies: tailwindcss: ^3.4.0 - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2751,11 +2324,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -2764,10 +2332,6 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2782,14 +2346,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2927,9 +2483,6 @@ packages: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} - fs-monkey@1.0.3: - resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2980,10 +2533,6 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -2999,9 +2548,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true @@ -3134,10 +2680,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -3150,12 +2692,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - idb-keyval@6.2.2: resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} @@ -3259,11 +2795,6 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3326,10 +2857,6 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -3358,10 +2885,6 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3407,10 +2930,6 @@ packages: engines: {node: '>=10'} hasBin: true - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -3437,9 +2956,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} @@ -3481,10 +2997,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - knip@5.82.1: resolution: {integrity: sha512-1nQk+5AcnkqL40kGQXfouzAEXkTR+eSrgo/8m1d0BMei4eAzFwghoXC4gOKbACgBiCof7hE8wkBVDsEvznf85w==} engines: {node: '>=18.18.0'} @@ -3521,14 +3033,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3555,9 +3059,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.union@4.6.0: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} @@ -3675,16 +3176,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mediabunny@1.29.0: - resolution: {integrity: sha512-18B8w/rhO/ph/AFsIXvzZg8RaSQZ+ZYfJ99MZlTjDmlgCT58jV3azrnWQ/OSquYDi8q0xmn64mnfTEHgww3+zw==} - - memfs@3.4.3: - resolution: {integrity: sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==} - engines: {node: '>= 4.0.0'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3826,9 +3317,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.6: - resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3933,10 +3421,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3989,10 +3473,6 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4138,30 +3618,6 @@ packages: yaml: optional: true - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -4176,10 +3632,6 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -4276,10 +3728,6 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4303,9 +3751,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -4324,10 +3769,6 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-refresh@0.9.0: - resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==} - engines: {node: '>=0.10.0'} - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -4361,10 +3802,6 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -4397,12 +3834,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - remotion@4.0.421: - resolution: {integrity: sha512-j64KtcwObNYpxskxBPd7zDXVOwSHxH31RIM6Vs/+rUj/NBry/LXc8cNWDwFC4fYaJweOAeM7zXY16kW/9t3X+w==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4514,14 +3945,6 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - schema-utils@3.3.0: - resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} - engines: {node: '>= 10.13.0'} - - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} - scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -4536,11 +3959,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -4550,9 +3968,6 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -4612,9 +4027,6 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -4649,15 +4061,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.3: - resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} - engines: {node: '>= 8'} - - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -4753,10 +4156,6 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4768,12 +4167,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - style-loader@4.0.0: - resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==} - engines: {node: '>= 18.12.0'} - peerDependencies: - webpack: ^5.27.0 - style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -4793,10 +4186,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -4806,10 +4195,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -4822,22 +4207,6 @@ packages: temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - terser@5.46.0: resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} engines: {node: '>=10'} @@ -4858,9 +4227,6 @@ packages: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-typed-emitter@2.1.0: resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} @@ -4905,9 +4271,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -5126,41 +4489,17 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} - wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} - engines: {node: '>=10.13.0'} - - webpack@5.105.0: - resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5208,18 +4547,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -5264,9 +4591,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@3.22.3: - resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -5374,10 +4698,6 @@ snapshots: '@babel/template': 7.28.6 '@babel/types': 7.28.6 - '@babel/parser@7.24.1': - dependencies: - '@babel/types': 7.28.6 - '@babel/parser@7.28.6': dependencies: '@babel/types': 7.28.6 @@ -5587,183 +4907,120 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.25.0': - optional: true - '@esbuild/aix-ppc64@0.27.2': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.25.0': - optional: true - '@esbuild/android-arm64@0.27.2': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.25.0': - optional: true - '@esbuild/android-arm@0.27.2': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.25.0': - optional: true - '@esbuild/android-x64@0.27.2': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.25.0': - optional: true - '@esbuild/darwin-arm64@0.27.2': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.25.0': - optional: true - '@esbuild/darwin-x64@0.27.2': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.25.0': - optional: true - '@esbuild/freebsd-arm64@0.27.2': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.25.0': - optional: true - '@esbuild/freebsd-x64@0.27.2': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.25.0': - optional: true - '@esbuild/linux-arm64@0.27.2': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.25.0': - optional: true - '@esbuild/linux-arm@0.27.2': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.25.0': - optional: true - '@esbuild/linux-ia32@0.27.2': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.25.0': - optional: true - '@esbuild/linux-loong64@0.27.2': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.25.0': - optional: true - '@esbuild/linux-mips64el@0.27.2': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.25.0': - optional: true - '@esbuild/linux-ppc64@0.27.2': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.25.0': - optional: true - '@esbuild/linux-riscv64@0.27.2': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.25.0': - optional: true - '@esbuild/linux-s390x@0.27.2': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.25.0': - optional: true - '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.0': - optional: true - '@esbuild/netbsd-arm64@0.27.2': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.25.0': - optional: true - '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.0': - optional: true - '@esbuild/openbsd-arm64@0.27.2': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.25.0': - optional: true - '@esbuild/openbsd-x64@0.27.2': optional: true @@ -5773,36 +5030,24 @@ snapshots: '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.25.0': - optional: true - '@esbuild/sunos-x64@0.27.2': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.25.0': - optional: true - '@esbuild/win32-arm64@0.27.2': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.25.0': - optional: true - '@esbuild/win32-ia32@0.27.2': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.25.0': - optional: true - '@esbuild/win32-x64@0.27.2': optional: true @@ -5955,6 +5200,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -6085,224 +5331,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@remotion/bundler@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/media-parser': 4.0.421 - '@remotion/studio': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio-shared': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - css-loader: 5.2.7(webpack@5.105.0(esbuild@0.25.0)) - esbuild: 0.25.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-refresh: 0.9.0 - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - source-map: 0.7.3 - style-loader: 4.0.0(webpack@5.105.0(esbuild@0.25.0)) - webpack: 5.105.0(esbuild@0.25.0) - transitivePeerDependencies: - - '@swc/core' - - bufferutil - - supports-color - - uglify-js - - utf-8-validate - - webpack-cli - - '@remotion/cli@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/bundler': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/media-utils': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/player': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/renderer': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio-server': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio-shared': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - dotenv: 9.0.2 - minimist: 1.2.6 - prompts: 2.4.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - transitivePeerDependencies: - - '@swc/core' - - bufferutil - - supports-color - - uglify-js - - utf-8-validate - - webpack-cli - - '@remotion/compositor-darwin-arm64@4.0.421': - optional: true - - '@remotion/compositor-darwin-x64@4.0.421': - optional: true - - '@remotion/compositor-linux-arm64-gnu@4.0.421': - optional: true - - '@remotion/compositor-linux-arm64-musl@4.0.421': - optional: true - - '@remotion/compositor-linux-x64-gnu@4.0.421': - optional: true - - '@remotion/compositor-linux-x64-musl@4.0.421': - optional: true - - '@remotion/compositor-win32-x64-msvc@4.0.421': - optional: true - - '@remotion/google-fonts@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - transitivePeerDependencies: - - react - - react-dom - - '@remotion/licensing@4.0.421': {} - - '@remotion/light-leaks@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/media-parser@4.0.421': {} - - '@remotion/media-utils@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/media-parser': 4.0.421 - '@remotion/webcodecs': 4.0.421 - mediabunny: 1.29.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/media@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - mediabunny: 1.29.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/paths@4.0.421': {} - - '@remotion/player@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/renderer@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/licensing': 4.0.421 - '@remotion/streaming': 4.0.421 - execa: 5.1.1 - extract-zip: 2.0.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - source-map: 0.8.0-beta.0 - ws: 8.17.1 - optionalDependencies: - '@remotion/compositor-darwin-arm64': 4.0.421 - '@remotion/compositor-darwin-x64': 4.0.421 - '@remotion/compositor-linux-arm64-gnu': 4.0.421 - '@remotion/compositor-linux-arm64-musl': 4.0.421 - '@remotion/compositor-linux-x64-gnu': 4.0.421 - '@remotion/compositor-linux-x64-musl': 4.0.421 - '@remotion/compositor-win32-x64-msvc': 4.0.421 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@remotion/shapes@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/paths': 4.0.421 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@remotion/streaming@4.0.421': {} - - '@remotion/studio-server@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/parser': 7.24.1 - '@remotion/bundler': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/renderer': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio-shared': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - memfs: 3.4.3 - open: 8.4.2 - recast: 0.23.11 - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - semver: 7.5.3 - source-map: 0.7.3 - transitivePeerDependencies: - - '@swc/core' - - bufferutil - - react - - react-dom - - supports-color - - uglify-js - - utf-8-validate - - webpack-cli - - '@remotion/studio-shared@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - transitivePeerDependencies: - - react - - react-dom - - '@remotion/studio@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/media-utils': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/player': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/renderer': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/studio-shared': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/web-renderer': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@remotion/zod-types': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.22.3) - mediabunny: 1.29.0 - memfs: 3.4.3 - open: 8.4.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - semver: 7.5.3 - source-map: 0.7.3 - zod: 3.22.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@remotion/transitions@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/paths': 4.0.421 - '@remotion/shapes': 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/web-renderer@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@remotion/licensing': 4.0.421 - mediabunny: 1.29.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - - '@remotion/webcodecs@4.0.421': - dependencies: - '@remotion/media-parser': 4.0.421 - - '@remotion/zod-types@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.22.3)': - dependencies: - remotion: 4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - zod: 3.22.3 - transitivePeerDependencies: - - react - - react-dom - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -6447,22 +5475,6 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/dom-mediacapture-transform@0.1.11': - dependencies: - '@types/dom-webcodecs': 0.1.13 - - '@types/dom-webcodecs@0.1.13': {} - - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -6767,96 +5779,12 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - '@xmldom/xmldom@0.8.11': {} - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - abbrev@1.1.1: {} abstract-logging@2.0.1: {} - acorn-import-phases@1.0.4(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6880,10 +5808,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6892,11 +5816,6 @@ snapshots: dependencies: ajv: 6.12.6 - ajv-keywords@5.1.0(ajv@8.17.1): - dependencies: - ajv: 8.17.1 - fast-deep-equal: 3.1.3 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -7133,10 +6052,6 @@ snapshots: ast-types-flow@0.0.8: {} - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - ast-v8-to-istanbul@0.3.10: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -7192,8 +6107,6 @@ snapshots: dependencies: tweetnacl: 0.14.5 - big.js@5.2.2: {} - binary-extensions@2.3.0: {} bl@4.1.0: @@ -7412,8 +6325,6 @@ snapshots: chownr@2.0.0: {} - chrome-trace-event@1.0.4: {} - chromium-pickle-js@0.2.0: {} ci-info@3.9.0: {} @@ -7458,7 +6369,8 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@2.20.3: {} + commander@2.20.3: + optional: true commander@4.1.1: {} @@ -7522,20 +6434,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@5.2.7(webpack@5.105.0(esbuild@0.25.0)): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - loader-utils: 2.0.4 - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) - postcss-value-parser: 4.2.0 - schema-utils: 3.3.0 - semver: 7.7.3 - webpack: 5.105.0(esbuild@0.25.0) - cssesc@3.0.0: {} csstype@3.2.3: {} @@ -7594,8 +6492,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -7776,8 +6672,6 @@ snapshots: emoji-regex@9.2.2: {} - emojis-list@3.0.0: {} - encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -7787,11 +6681,6 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - env-paths@2.2.1: {} err-code@2.0.3: {} @@ -7878,8 +6767,6 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7930,34 +6817,6 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 - esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -8167,11 +7026,6 @@ snapshots: postcss: 8.5.6 tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -8228,8 +7082,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -8238,8 +7090,6 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} - estraverse@5.3.0: {} estree-util-is-identifier-name@3.0.0: {} @@ -8250,20 +7100,6 @@ snapshots: esutils@2.0.3: {} - events@3.3.0: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - expect-type@1.3.0: {} exponential-backoff@3.1.3: {} @@ -8434,8 +7270,6 @@ snapshots: dependencies: minipass: 3.3.6 - fs-monkey@1.0.3: {} - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -8495,8 +7329,6 @@ snapshots: dependencies: pump: 3.0.3 - get-stream@6.0.1: {} - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -8515,8 +7347,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -8706,8 +7536,6 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@2.1.0: {} - humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -8722,10 +7550,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - idb-keyval@6.2.2: {} ieee754@1.2.1: {} @@ -8825,8 +7649,6 @@ snapshots: is-decimal@2.0.1: {} - is-docker@2.2.1: {} - is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8879,8 +7701,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-stream@2.0.1: {} - is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -8909,10 +7729,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - isarray@1.0.0: {} isarray@2.0.5: {} @@ -8965,12 +7781,6 @@ snapshots: filelist: 1.0.4 picocolors: 1.1.1 - jest-worker@27.5.1: - dependencies: - '@types/node': 25.0.7 - merge-stream: 2.0.0 - supports-color: 8.1.1 - jiti@1.21.7: {} jiti@2.6.1: {} @@ -8987,8 +7797,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 @@ -9031,8 +7839,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kleur@3.0.3: {} - knip@5.82.1(@types/node@25.0.7)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -9077,14 +7883,6 @@ snapshots: lines-and-columns@1.2.4: {} - loader-runner@4.3.1: {} - - loader-utils@2.0.4: - dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -9103,8 +7901,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.sortby@4.7.0: {} - lodash.union@4.6.0: {} lodash@4.17.23: {} @@ -9340,17 +8136,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mediabunny@1.29.0: - dependencies: - '@types/dom-mediacapture-transform': 0.1.11 - '@types/dom-webcodecs': 0.1.13 - - memfs@3.4.3: - dependencies: - fs-monkey: 1.0.3 - - merge-stream@2.0.0: {} - merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -9585,8 +8370,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimist@1.2.6: {} - minimist@1.2.8: {} minipass-collect@1.0.2: @@ -9687,10 +8470,6 @@ snapshots: normalize-url@6.1.0: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - npmlog@6.0.2: dependencies: are-we-there-yet: 3.0.1 @@ -9752,12 +8531,6 @@ snapshots: dependencies: mimic-fn: 2.1.0 - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9923,27 +8696,6 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-selector-parser: 7.1.1 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.2.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 7.1.1 - - postcss-modules-values@4.0.0(postcss@8.5.6): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-nested@6.2.0(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -9959,11 +8711,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-value-parser@4.2.0: {} postcss@8.5.6: @@ -9995,11 +8742,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10021,10 +8763,6 @@ snapshots: quick-lru@5.1.1: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -10053,8 +8791,6 @@ snapshots: react-refresh@0.17.0: {} - react-refresh@0.9.0: {} - react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -10104,14 +8840,6 @@ snapshots: real-require@0.2.0: {} - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -10177,11 +8905,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remotion@4.0.421(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -10319,19 +9042,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - schema-utils@3.3.0: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) - scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -10345,10 +9055,6 @@ snapshots: semver@6.3.1: {} - semver@7.5.3: - dependencies: - lru-cache: 6.0.0 - semver@7.7.3: {} serialize-error@7.0.1: @@ -10356,10 +9062,6 @@ snapshots: type-fest: 0.13.1 optional: true - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - set-blocking@2.0.0: {} set-cookie-parser@2.7.2: {} @@ -10432,8 +9134,6 @@ snapshots: dependencies: semver: 7.7.3 - sisteransi@1.0.5: {} - slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -10471,12 +9171,6 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.3: {} - - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 - space-separated-tokens@2.0.2: {} split2@4.2.0: {} @@ -10598,8 +9292,6 @@ snapshots: strip-bom@3.0.0: {} - strip-final-newline@2.0.0: {} - strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -10608,10 +9300,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - style-loader@4.0.0(webpack@5.105.0(esbuild@0.25.0)): - dependencies: - webpack: 5.105.0(esbuild@0.25.0) - style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -10640,10 +9328,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2): @@ -10674,8 +9358,6 @@ snapshots: - tsx - yaml - tapable@2.3.0: {} - tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -10698,23 +9380,13 @@ snapshots: async-exit-hook: 2.0.1 fs-extra: 10.1.0 - terser-webpack-plugin@5.3.16(esbuild@0.25.0)(webpack@5.105.0): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.46.0 - webpack: 5.105.0(esbuild@0.25.0) - optionalDependencies: - esbuild: 0.25.0 - terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true test-exclude@7.0.1: dependencies: @@ -10734,8 +9406,6 @@ snapshots: dependencies: real-require: 0.2.0 - tiny-invariant@1.3.3: {} - tiny-typed-emitter@2.1.0: {} tinybench@2.9.0: {} @@ -10767,10 +9437,6 @@ snapshots: toidentifier@1.0.1: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - trim-lines@3.0.1: {} trough@2.2.0: {} @@ -11044,61 +9710,14 @@ snapshots: walk-up-path@4.0.0: {} - watchpack@2.5.1: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - wcwidth@1.0.1: dependencies: defaults: 1.0.4 - webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} - webpack-sources@3.3.3: {} - - webpack@5.105.0(esbuild@0.25.0): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.19.0 - es-module-lexer: 2.0.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(esbuild@0.25.0)(webpack@5.105.0) - watchpack: 2.5.1 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - whatwg-mimetype@3.0.0: {} - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -11171,8 +9790,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.17.1: {} - xmlbuilder@15.1.1: {} y18n@5.0.8: {} @@ -11213,8 +9830,6 @@ snapshots: dependencies: zod: 4.3.6 - zod@3.22.3: {} - zod@4.3.6: {} zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): diff --git a/src/main/index.ts b/src/main/index.ts index 8bbc53ef..6484e149 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -116,7 +116,7 @@ function wireFileWatcherEvents(context: ServiceContext): void { } // Wire file-change events to renderer and HTTP SSE - const fileChangeHandler = (event: unknown) => { + const fileChangeHandler = (event: unknown): void => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('file-change', event); } @@ -126,7 +126,7 @@ function wireFileWatcherEvents(context: ServiceContext): void { fileChangeCleanup = () => context.fileWatcher.off('file-change', fileChangeHandler); // Forward checklist-change events to renderer and HTTP SSE (mirrors file-change pattern above) - const todoChangeHandler = (event: unknown) => { + const todoChangeHandler = (event: unknown): void => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('todo-change', event); } diff --git a/src/main/services/infrastructure/HttpServer.ts b/src/main/services/infrastructure/HttpServer.ts index 8c20ffe0..597675f6 100644 --- a/src/main/services/infrastructure/HttpServer.ts +++ b/src/main/services/infrastructure/HttpServer.ts @@ -73,6 +73,7 @@ export class HttpServer { await this.app.register(cors, { origin: origins, credentials: true }); } else { // Default: allow all localhost origins + // eslint-disable-next-line security/detect-unsafe-regex -- anchored, no backtracking risk const localhostPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/; await this.app.register(cors, { origin: (origin, cb) => { diff --git a/src/main/standalone.ts b/src/main/standalone.ts index 27cd29ac..e417470d 100644 --- a/src/main/standalone.ts +++ b/src/main/standalone.ts @@ -20,11 +20,7 @@ import { getTodosBasePath, setClaudeBasePathOverride, } from './utils/pathDecoder'; -import { - LocalFileSystemProvider, - NotificationManager, - ServiceContext, -} from './services'; +import { LocalFileSystemProvider, NotificationManager, ServiceContext } from './services'; import type { HttpServices } from './http'; import type { SshConnectionManager } from './services/infrastructure/SshConnectionManager'; @@ -59,7 +55,12 @@ const updaterServiceStub = { /** No-op SshConnectionManager stub — SSH is managed per-user in the Electron app. */ const sshConnectionManagerStub = { - getStatus: () => ({ state: 'disconnected' as const, host: null, error: null, remoteProjectsPath: null }), + getStatus: () => ({ + state: 'disconnected' as const, + host: null, + error: null, + remoteProjectsPath: null, + }), getProvider: () => new LocalFileSystemProvider(), isRemote: () => false, connect: async () => {}, @@ -148,7 +149,7 @@ async function start(): Promise { }; // No-op mode switch handler (no SSH in standalone) - const modeSwitchHandler = async () => {}; + const modeSwitchHandler = async (): Promise => {}; // Start the server const port = await httpServer.start(services, modeSwitchHandler, PORT, HOST); diff --git a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx index ebbe6d5f..c7666541 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx @@ -272,7 +272,7 @@ export const RankedInjectionList = ({ {/* Copy path button for CLAUDE.md and File items */} {copyPath && ( - e.stopPropagation()}> + )} diff --git a/src/renderer/components/common/ConfirmDialog.tsx b/src/renderer/components/common/ConfirmDialog.tsx index 0da90a5a..0633e5f1 100644 --- a/src/renderer/components/common/ConfirmDialog.tsx +++ b/src/renderer/components/common/ConfirmDialog.tsx @@ -37,6 +37,7 @@ let globalResolver: ConfirmResolver = null; * Usage: * const confirmed = await confirm({ title: 'Delete?', message: 'This cannot be undone.' }); */ +// eslint-disable-next-line react-refresh/only-export-components -- imperative API shares singleton state with component export async function confirm(opts: { title: string; message: string; diff --git a/src/renderer/components/common/UpdateBanner.tsx b/src/renderer/components/common/UpdateBanner.tsx index c74968fb..c377aae1 100644 --- a/src/renderer/components/common/UpdateBanner.tsx +++ b/src/renderer/components/common/UpdateBanner.tsx @@ -33,14 +33,20 @@ export const UpdateBanner = (): React.JSX.Element | null => { > {isDownloading ? (
-
+
Updating app {clampedPercent}%
-
+
{ {/* Dismiss */}