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 (
+ handleNavigate(inj)}
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
+ >
+ {/* Category pill */}
+
+ {categoryInfo.label}
+
+ {/* Description */}
+
+ {description}
+
+ {/* Token count */}
+
+ {formatTokens(inj.estimatedTokens)}
+
+
+ );
+ })}
+
+ );
+};
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:
+
+
onViewModeChange('category')}
+ className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] transition-colors"
+ style={{
+ backgroundColor:
+ viewMode === 'category' ? 'rgba(99, 102, 241, 0.2)' : COLOR_SURFACE_OVERLAY,
+ color: viewMode === 'category' ? '#818cf8' : COLOR_TEXT_MUTED,
+ }}
+ >
+
+ Category
+
+
onViewModeChange('ranked')}
+ className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] transition-colors"
+ style={{
+ backgroundColor:
+ viewMode === 'ranked' ? 'rgba(99, 102, 241, 0.2)' : COLOR_SURFACE_OVERLAY,
+ color: viewMode === 'ranked' ? '#818cf8' : COLOR_TEXT_MUTED,
+ }}
+ >
+
+ By Size
+
+
);
};
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}` : ''})
+
+ setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
+ }
+ className="ml-auto rounded p-1 transition-colors hover:bg-white/5"
+ title={sessionSortMode === 'recent' ? 'Sort by context consumption' : 'Sort by recent'}
+ style={{
+ color: sessionSortMode === 'most-context' ? '#818cf8' : 'var(--color-text-muted)',
+ }}
+ >
+
+
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 (
+
+
{
+ if (hasBreakdown) {
+ setExpanded(!expanded);
+ } else if (onNavigateToTurn) {
+ const turnIndex = getInjectionTurnIndex(injection);
+ if (turnIndex >= 0) onNavigateToTurn(turnIndex);
+ }
+ }}
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
+ >
+ {/* Expand chevron */}
+ {hasBreakdown && (
+
+ )}
+ {/* Category pill */}
+
+ {categoryInfo.label}
+
+ {/* Description */}
+
+ {getInjectionDescription(injection)}
+
+ {/* Token count */}
+
+ {formatTokens(injection.estimatedTokens)}
+
+
+
+ {/* Expanded tool breakdown */}
+ {expanded && hasBreakdown && (
+
+ {sortedBreakdown.map((tool, idx) => (
+ {
+ if (onNavigateToTurn) {
+ onNavigateToTurn(injection.turnIndex);
+ }
+ }}
+ className="flex w-full items-center gap-2 rounded px-2 py-0.5 text-left text-xs transition-colors hover:bg-white/5"
+ >
+
+ {tool.toolName}
+
+
+
+ {formatTokens(tool.tokenCount)}
+
+ {tool.isError && (
+
+ error
+
+ )}
+
+ ))}
+
+ )}
+
+ );
+};
+
// =============================================================================
// 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 (
handleNavigate(inj)}
+ onClick={() => {
+ if (onNavigateToTurn) {
+ const turnIndex = getInjectionTurnIndex(inj);
+ if (turnIndex >= 0) onNavigateToTurn(turnIndex);
+ }
+ }}
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
>
{/* Category pill */}
{categoryInfo.label}
@@ -121,7 +235,7 @@ export const RankedInjectionList = ({
className="min-w-0 flex-1 truncate text-xs"
style={{ color: COLOR_TEXT_SECONDARY }}
>
- {description}
+ {getInjectionDescription(inj)}
{/* Token count */}
{
const [searchQuery, setSearchQuery] = useState('');
+ const openSettingsTab = useStore((s) => s.openSettingsTab);
return (
@@ -415,14 +416,24 @@ export const DashboardView = (): React.JSX.Element => {
{searchQuery.trim() ? 'Search Results' : 'Recent Projects'}
- {searchQuery.trim() && (
+
+ {searchQuery.trim() && (
+ setSearchQuery('')}
+ className="text-xs text-text-muted transition-colors hover:text-text-secondary"
+ >
+ Clear search
+
+ )}
setSearchQuery('')}
- className="text-xs text-text-muted transition-colors hover:text-text-secondary"
+ onClick={() => openSettingsTab('general')}
+ className="flex items-center gap-1.5 text-xs text-text-muted transition-colors hover:text-text-secondary"
+ title="Change Claude data folder"
>
- Clear search
+
+ Change default folder
- )}
+
{/* 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() && (
openSettingsTab()}
onMouseEnter={() => setSettingsHover(true)}
onMouseLeave={() => setSettingsHover(false)}
className="rounded-md p-2 transition-colors"
diff --git a/src/renderer/components/settings/SettingsView.tsx b/src/renderer/components/settings/SettingsView.tsx
index 904149a7..330e5c07 100644
--- a/src/renderer/components/settings/SettingsView.tsx
+++ b/src/renderer/components/settings/SettingsView.tsx
@@ -3,8 +3,9 @@
* Provides UI for managing notifications, display settings, and advanced options.
*/
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { useStore } from '@renderer/store';
import { Loader2 } from 'lucide-react';
import { useSettingsConfig, useSettingsHandlers } from './hooks';
@@ -19,6 +20,15 @@ import { type SettingsSection, SettingsTabs } from './SettingsTabs';
export const SettingsView = (): React.JSX.Element | null => {
const [activeSection, setActiveSection] = useState('general');
+ const pendingSettingsSection = useStore((s) => s.pendingSettingsSection);
+ const clearPendingSettingsSection = useStore((s) => s.clearPendingSettingsSection);
+
+ useEffect(() => {
+ if (pendingSettingsSection) {
+ setActiveSection(pendingSettingsSection as SettingsSection);
+ clearPendingSettingsSection();
+ }
+ }, [pendingSettingsSection, clearPendingSettingsSection]);
const {
config,
diff --git a/src/renderer/components/settings/sections/ConnectionSection.tsx b/src/renderer/components/settings/sections/ConnectionSection.tsx
index b44f8341..8da99046 100644
--- a/src/renderer/components/settings/sections/ConnectionSection.tsx
+++ b/src/renderer/components/settings/sections/ConnectionSection.tsx
@@ -11,10 +11,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api } from '@renderer/api';
-import { confirm } from '@renderer/components/common/ConfirmDialog';
import { useStore } from '@renderer/store';
-import { getFullResetState } from '@renderer/store/utils/stateResetHelpers';
-import { FolderOpen, Laptop, Loader2, Monitor, RotateCcw, Server, Wifi, WifiOff } from 'lucide-react';
+import { Loader2, Monitor, Server, Wifi, WifiOff } from 'lucide-react';
import { SettingRow } from '../components/SettingRow';
import { SettingsSectionHeader } from '../components/SettingsSectionHeader';
@@ -26,7 +24,6 @@ import type {
SshConfigHostEntry,
SshConnectionConfig,
SshConnectionProfile,
- WslClaudeRootCandidate,
} from '@shared/types';
const authMethodOptions: readonly { value: SshAuthMethod; label: string }[] = [
@@ -37,7 +34,6 @@ const authMethodOptions: readonly { value: SshAuthMethod; label: string }[] = [
];
export const ConnectionSection = (): React.JSX.Element => {
- const connectionMode = useStore((s) => s.connectionMode);
const connectionState = useStore((s) => s.connectionState);
const connectedHost = useStore((s) => s.connectedHost);
const connectionError = useStore((s) => s.connectionError);
@@ -48,8 +44,6 @@ export const ConnectionSection = (): React.JSX.Element => {
const fetchSshConfigHosts = useStore((s) => s.fetchSshConfigHosts);
const lastSshConfig = useStore((s) => s.lastSshConfig);
const loadLastConnection = useStore((s) => s.loadLastConnection);
- const fetchProjects = useStore((s) => s.fetchProjects);
- const fetchRepositoryGroups = useStore((s) => s.fetchRepositoryGroups);
// Form state
const [host, setHost] = useState('');
@@ -70,11 +64,6 @@ export const ConnectionSection = (): React.JSX.Element => {
const [savedProfiles, setSavedProfiles] = useState([]);
const [selectedProfileId, setSelectedProfileId] = useState(null);
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);
const loadProfiles = useCallback(async () => {
try {
@@ -90,10 +79,8 @@ export const ConnectionSection = (): React.JSX.Element => {
try {
const info = await api.config.getClaudeRootInfo();
setClaudeRootInfo(info);
- } catch (error) {
- setClaudeRootError(
- error instanceof Error ? error.message : 'Failed to load local Claude root settings'
- );
+ } catch {
+ // ignore
}
}, []);
@@ -197,155 +184,9 @@ export const ConnectionSection = (): React.JSX.Element => {
await disconnectSsh();
};
- 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 isConnecting = connectionState === 'connecting';
const isConnected = connectionState === 'connected';
- const isCustomClaudeRoot = Boolean(claudeRootInfo?.customPath);
const resolvedClaudeRootPath = claudeRootInfo?.resolvedPath ?? '~/.claude';
- const defaultClaudeRootPath = claudeRootInfo?.defaultPath ?? '~/.claude';
- const isWindowsStyleDefaultPath =
- /^[a-zA-Z]:\\/.test(defaultClaudeRootPath) || defaultClaudeRootPath.startsWith('\\\\');
const inputClass = 'w-full rounded-md border px-3 py-1.5 text-sm focus:outline-none focus:ring-1';
const inputStyle = {
@@ -356,175 +197,6 @@ export const ConnectionSection = (): React.JSX.Element => {
return (
-
-
- Choose which local folder is treated as your Claude data root
-
-
-
-
-
- {resolvedClaudeRootPath}
-
-
- Auto-detected: {defaultClaudeRootPath}
-
-
-
-
-
- void handleSelectClaudeRootFolder()}
- disabled={updatingClaudeRoot}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text)',
- }}
- >
-
- {updatingClaudeRoot ? (
-
- ) : (
-
- )}
- Select Folder
-
-
-
- void handleResetClaudeRoot()}
- disabled={updatingClaudeRoot || !isCustomClaudeRoot}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text-secondary)',
- }}
- >
-
-
- Use Auto-Detect
-
-
-
- {isWindowsStyleDefaultPath && (
- void handleUseWslForClaude()}
- disabled={updatingClaudeRoot || findingWslRoots}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text-secondary)',
- }}
- >
-
- {findingWslRoots ? (
-
- ) : (
-
- )}
- Using Linux/WSL?
-
-
- )}
-
-
- {claudeRootError && (
-
- )}
-
- {showWslModal && (
-
-
setShowWslModal(false)}
- aria-label="Close WSL path modal"
- tabIndex={-1}
- />
-
-
- Select WSL Claude Root
-
-
- Detected WSL distributions and Claude root candidates
-
-
-
- {wslCandidates.map((candidate) => (
-
-
-
- {candidate.distro}
-
-
- {candidate.path}
-
- {!candidate.hasProjectsDir && (
-
No projects directory detected
- )}
-
-
void applyWslCandidate(candidate)}
- className="rounded-md px-3 py-1.5 text-xs transition-colors"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text)',
- }}
- >
- Use This Path
-
-
- ))}
-
-
-
- setShowWslModal(false)}
- className="rounded-md border px-3 py-1.5 text-xs transition-colors hover:bg-white/5"
- style={{
- borderColor: 'var(--color-border)',
- color: 'var(--color-text-secondary)',
- }}
- >
- Cancel
-
- {
- setShowWslModal(false);
- void handleSelectClaudeRootFolder();
- }}
- className="rounded-md px-3 py-1.5 text-xs transition-colors"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text)',
- }}
- >
- Select Folder Manually
-
-
-
-
- )}
-
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}
+
+
+
+
+
+ void handleSelectClaudeRootFolder()}
+ disabled={updatingClaudeRoot}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text)',
+ }}
+ >
+
+ {updatingClaudeRoot ? (
+
+ ) : (
+
+ )}
+ Select Folder
+
+
+
+ void handleResetClaudeRoot()}
+ disabled={updatingClaudeRoot || !isCustomClaudeRoot}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+
+
+ Use Auto-Detect
+
+
+
+ {isWindowsStyleDefaultPath && (
+ void handleUseWslForClaude()}
+ disabled={updatingClaudeRoot || findingWslRoots}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+
+ {findingWslRoots ? (
+
+ ) : (
+
+ )}
+ Using Linux/WSL?
+
+
+ )}
+
+
+ {claudeRootError && (
+
+ )}
+
+ {showWslModal && (
+
+
setShowWslModal(false)}
+ aria-label="Close WSL path modal"
+ tabIndex={-1}
+ />
+
+
+ Select WSL Claude Root
+
+
+ Detected WSL distributions and Claude root candidates
+
+
+
+ {wslCandidates.map((candidate) => (
+
+
+
+ {candidate.distro}
+
+
+ {candidate.path}
+
+ {!candidate.hasProjectsDir && (
+
No projects directory detected
+ )}
+
+
void applyWslCandidate(candidate)}
+ className="rounded-md px-3 py-1.5 text-xs transition-colors"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text)',
+ }}
+ >
+ Use This Path
+
+
+ ))}
+
+
+
+ setShowWslModal(false)}
+ className="rounded-md border px-3 py-1.5 text-xs transition-colors hover:bg-white/5"
+ style={{
+ borderColor: 'var(--color-border)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+ Cancel
+
+ {
+ setShowWslModal(false);
+ void handleSelectClaudeRootFolder();
+ }}
+ className="rounded-md px-3 py-1.5 text-xs transition-colors"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text)',
+ }}
+ >
+ Select Folder Manually
+
+
+
+
+ )}
+
(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 = ({
{
- if (onNavigateToTurn) {
+ if (tool.toolUseId && onNavigateToTool) {
+ onNavigateToTool(injection.turnIndex, tool.toolUseId);
+ } else if (onNavigateToTurn) {
onNavigateToTurn(injection.turnIndex);
}
}}
@@ -185,6 +201,8 @@ const ToolOutputRankedItem = ({
export const RankedInjectionList = ({
injections,
onNavigateToTurn,
+ onNavigateToTool,
+ onNavigateToUserGroup,
}: Readonly): React.ReactElement => {
const sortedInjections = useMemo(
() => [...injections].sort((a, b) => b.estimatedTokens - a.estimatedTokens),
@@ -201,50 +219,64 @@ export const RankedInjectionList = ({
key={inj.id}
injection={inj}
onNavigateToTurn={onNavigateToTurn}
+ onNavigateToTool={onNavigateToTool}
/>
);
}
- // All other categories: simple row
const categoryInfo = CATEGORY_COLORS[inj.category] ?? {
bg: 'rgba(161, 161, 170, 0.15)',
text: '#a1a1aa',
label: inj.category,
};
+ const copyPath = getCopyablePath(inj);
+
+ const handleClick = (): void => {
+ const turnIndex = getInjectionTurnIndex(inj);
+ if (turnIndex < 0) return;
+ // User messages → navigate to user group; others → navigate to AI group
+ if (inj.category === 'user-message' && onNavigateToUserGroup) {
+ onNavigateToUserGroup(turnIndex);
+ } else if (onNavigateToTurn) {
+ onNavigateToTurn(turnIndex);
+ }
+ };
return (
- {
- if (onNavigateToTurn) {
- const turnIndex = getInjectionTurnIndex(inj);
- if (turnIndex >= 0) onNavigateToTurn(turnIndex);
- }
- }}
- className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
- >
- {/* Category pill */}
-
+
- {categoryInfo.label}
-
- {/* Description */}
-
- {getInjectionDescription(inj)}
-
- {/* Token count */}
-
- {formatTokens(inj.estimatedTokens)}
-
-
+ {/* 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
+ )}
setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
diff --git a/src/renderer/types/contextInjection.ts b/src/renderer/types/contextInjection.ts
index 3ad3be56..86406839 100644
--- a/src/renderer/types/contextInjection.ts
+++ b/src/renderer/types/contextInjection.ts
@@ -72,6 +72,8 @@ export interface ToolTokenBreakdown {
tokenCount: number;
/** Whether the tool execution resulted in an error */
isError: boolean;
+ /** Tool use ID for deep-link navigation to specific tool in chat */
+ toolUseId?: string;
}
/**
diff --git a/src/renderer/utils/contextTracker.ts b/src/renderer/utils/contextTracker.ts
index d1b54aef..021afb5a 100644
--- a/src/renderer/utils/contextTracker.ts
+++ b/src/renderer/utils/contextTracker.ts
@@ -214,6 +214,7 @@ function aggregateToolOutputs(
toolName: displayName,
tokenCount: toolTokenCount,
isError: linkedTool.result?.isError ?? false,
+ toolUseId: linkedTool.id,
});
totalTokens += toolTokenCount;
}
From 12a5bf46a8cc1e6e69557fe03f803ef3d1e14c5e Mon Sep 17 00:00:00 2001
From: matt
Date: Mon, 16 Feb 2026 21:22:42 +0900
Subject: [PATCH 05/11] feat(sessions): implement session hiding and un-hiding
functionality
- Added handlers for hiding and unhiding individual and multiple sessions in the configuration.
- Updated the ConfigManager to manage hidden sessions, including methods for bulk operations.
- Enhanced the IPC channels and preload scripts to support new session visibility features.
- Integrated UI components to allow users to toggle session visibility in the sidebar and context menus.
- Updated state management to reflect hidden sessions and support multi-select actions for bulk hiding/unhiding.
---
src/main/ipc/config.ts | 134 ++++++++++--
.../services/infrastructure/ConfigManager.ts | 85 ++++++++
src/preload/constants/ipcChannels.ts | 12 ++
src/preload/index.ts | 16 ++
src/renderer/api/httpClient.ts | 8 +
src/renderer/components/layout/TabBar.tsx | 14 ++
.../components/layout/TabContextMenu.tsx | 12 ++
.../settings/hooks/useSettingsHandlers.ts | 1 +
.../sidebar/DateGroupedSessions.tsx | 191 ++++++++++++++++--
.../components/sidebar/SessionContextMenu.tsx | 13 +-
.../components/sidebar/SessionItem.tsx | 63 ++++--
src/renderer/store/slices/sessionSlice.ts | 179 +++++++++++++++-
src/shared/types/api.ts | 8 +
src/shared/types/notifications.ts | 2 +
14 files changed, 686 insertions(+), 52 deletions(-)
diff --git a/src/main/ipc/config.ts b/src/main/ipc/config.ts
index 28327d5b..f5d7a402 100644
--- a/src/main/ipc/config.ts
+++ b/src/main/ipc/config.ts
@@ -17,10 +17,7 @@
* - config:testTrigger: Test a trigger against historical session data
*/
-import {
- getAutoDetectedClaudeBasePath,
- getClaudeBasePath,
-} from '@main/utils/pathDecoder';
+import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process';
@@ -54,9 +51,8 @@ const execFileAsync = promisify(execFile);
// Get singleton instance
const configManager = ConfigManager.getInstance();
-let onClaudeRootPathUpdated:
- | ((claudeRootPath: string | null) => Promise | void)
- | null = null;
+let onClaudeRootPathUpdated: ((claudeRootPath: string | null) => Promise | void) | null =
+ null;
/**
* Response type for config operations
@@ -111,6 +107,12 @@ export function registerConfigHandlers(ipcMain: IpcMain): void {
ipcMain.handle('config:pinSession', handlePinSession);
ipcMain.handle('config:unpinSession', handleUnpinSession);
+ // Session hide handlers
+ ipcMain.handle('config:hideSession', handleHideSession);
+ ipcMain.handle('config:unhideSession', handleUnhideSession);
+ ipcMain.handle('config:hideSessions', handleHideSessions);
+ ipcMain.handle('config:unhideSessions', handleUnhideSessions);
+
// Dialog handlers
ipcMain.handle('config:selectFolders', handleSelectFolders);
ipcMain.handle('config:selectClaudeRootFolder', handleSelectClaudeRootFolder);
@@ -789,9 +791,10 @@ function decodeWslOutput(output: string | Buffer | undefined): string {
}
const hasUtf16LeBom = output.length >= 2 && output[0] === 0xff && output[1] === 0xfe;
- const decoded = hasUtf16LeBom || looksLikeUtf16Le(output)
- ? output.toString('utf16le')
- : output.toString('utf8');
+ const decoded =
+ hasUtf16LeBom || looksLikeUtf16Le(output)
+ ? output.toString('utf16le')
+ : output.toString('utf8');
return decoded.replace(/\0/g, '');
}
@@ -853,11 +856,7 @@ function parseWslDistros(stdout: string): string[] {
}
async function listWslDistros(): Promise {
- const commands: string[][] = [
- ['--list', '--quiet'],
- ['-l', '-q'],
- ['-l'],
- ];
+ const commands: string[][] = [['--list', '--quiet'], ['-l', '-q'], ['-l']];
for (const command of commands) {
try {
@@ -885,10 +884,7 @@ function stripDefaultSuffix(input: string): string {
async function resolveWslHome(distro: string): Promise {
try {
- const { stdout } = await runWsl(
- ['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'],
- 5000
- );
+ const { stdout } = await runWsl(['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'], 5000);
return normalizeWslHomePath(stdout);
} catch {
return null;
@@ -958,6 +954,102 @@ async function handleFindWslClaudeRoots(
}
}
+/**
+ * Handler for 'config:hideSession' - Hides a session for a project.
+ */
+async function handleHideSession(
+ _event: IpcMainInvokeEvent,
+ projectId: string,
+ sessionId: string
+): Promise {
+ try {
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!sessionId || typeof sessionId !== 'string') {
+ return { success: false, error: 'Session ID is required and must be a string' };
+ }
+
+ configManager.hideSession(projectId, sessionId);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in config:hideSession:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+}
+
+/**
+ * Handler for 'config:unhideSession' - Unhides a session for a project.
+ */
+async function handleUnhideSession(
+ _event: IpcMainInvokeEvent,
+ projectId: string,
+ sessionId: string
+): Promise {
+ try {
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!sessionId || typeof sessionId !== 'string') {
+ return { success: false, error: 'Session ID is required and must be a string' };
+ }
+
+ configManager.unhideSession(projectId, sessionId);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in config:unhideSession:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+}
+
+/**
+ * Handler for 'config:hideSessions' - Bulk hide sessions for a project.
+ */
+async function handleHideSessions(
+ _event: IpcMainInvokeEvent,
+ projectId: string,
+ sessionIds: string[]
+): Promise {
+ try {
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
+ return { success: false, error: 'Session IDs must be an array of strings' };
+ }
+
+ configManager.hideSessions(projectId, sessionIds);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in config:hideSessions:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+}
+
+/**
+ * Handler for 'config:unhideSessions' - Bulk unhide sessions for a project.
+ */
+async function handleUnhideSessions(
+ _event: IpcMainInvokeEvent,
+ projectId: string,
+ sessionIds: string[]
+): Promise {
+ try {
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
+ return { success: false, error: 'Session IDs must be an array of strings' };
+ }
+
+ configManager.unhideSessions(projectId, sessionIds);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in config:unhideSessions:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+}
+
// =============================================================================
// Cleanup
// =============================================================================
@@ -982,6 +1074,10 @@ export function removeConfigHandlers(ipcMain: IpcMain): void {
ipcMain.removeHandler('config:testTrigger');
ipcMain.removeHandler('config:pinSession');
ipcMain.removeHandler('config:unpinSession');
+ ipcMain.removeHandler('config:hideSession');
+ ipcMain.removeHandler('config:unhideSession');
+ ipcMain.removeHandler('config:hideSessions');
+ ipcMain.removeHandler('config:unhideSessions');
ipcMain.removeHandler('config:selectFolders');
ipcMain.removeHandler('config:selectClaudeRootFolder');
ipcMain.removeHandler('config:getClaudeRootInfo');
diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts
index e7984bf0..b8999536 100644
--- a/src/main/services/infrastructure/ConfigManager.ts
+++ b/src/main/services/infrastructure/ConfigManager.ts
@@ -191,6 +191,7 @@ export interface DisplayConfig {
export interface SessionsConfig {
pinnedSessions: Record;
+ hiddenSessions: Record;
}
export interface SshPersistConfig {
@@ -255,6 +256,7 @@ const DEFAULT_CONFIG: AppConfig = {
},
sessions: {
pinnedSessions: {},
+ hiddenSessions: {},
},
ssh: {
lastConnection: null,
@@ -746,6 +748,89 @@ export class ConfigManager {
this.saveConfig();
}
+ // ===========================================================================
+ // Session Hide Management
+ // ===========================================================================
+
+ /**
+ * Hides a session for a project.
+ * @param projectId - The project ID
+ * @param sessionId - The session ID to hide
+ */
+ hideSession(projectId: string, sessionId: string): void {
+ const hidden = this.config.sessions.hiddenSessions[projectId] ?? [];
+
+ if (hidden.some((h) => h.sessionId === sessionId)) {
+ return;
+ }
+
+ this.config.sessions.hiddenSessions[projectId] = [
+ { sessionId, hiddenAt: Date.now() },
+ ...hidden,
+ ];
+ this.saveConfig();
+ }
+
+ /**
+ * Unhides a session for a project.
+ * @param projectId - The project ID
+ * @param sessionId - The session ID to unhide
+ */
+ unhideSession(projectId: string, sessionId: string): void {
+ const hidden = this.config.sessions.hiddenSessions[projectId];
+ if (!hidden) return;
+
+ this.config.sessions.hiddenSessions[projectId] = hidden.filter(
+ (h) => h.sessionId !== sessionId
+ );
+
+ if (this.config.sessions.hiddenSessions[projectId].length === 0) {
+ delete this.config.sessions.hiddenSessions[projectId];
+ }
+
+ this.saveConfig();
+ }
+
+ /**
+ * Hides multiple sessions for a project in a single write.
+ * @param projectId - The project ID
+ * @param sessionIds - The session IDs to hide
+ */
+ hideSessions(projectId: string, sessionIds: string[]): void {
+ const hidden = this.config.sessions.hiddenSessions[projectId] ?? [];
+ const existingIds = new Set(hidden.map((h) => h.sessionId));
+ const now = Date.now();
+ const newEntries = sessionIds
+ .filter((id) => !existingIds.has(id))
+ .map((sessionId) => ({ sessionId, hiddenAt: now }));
+
+ if (newEntries.length === 0) return;
+
+ this.config.sessions.hiddenSessions[projectId] = [...newEntries, ...hidden];
+ this.saveConfig();
+ }
+
+ /**
+ * Unhides multiple sessions for a project in a single write.
+ * @param projectId - The project ID
+ * @param sessionIds - The session IDs to unhide
+ */
+ unhideSessions(projectId: string, sessionIds: string[]): void {
+ const hidden = this.config.sessions.hiddenSessions[projectId];
+ if (!hidden) return;
+
+ const toRemove = new Set(sessionIds);
+ this.config.sessions.hiddenSessions[projectId] = hidden.filter(
+ (h) => !toRemove.has(h.sessionId)
+ );
+
+ if (this.config.sessions.hiddenSessions[projectId].length === 0) {
+ delete this.config.sessions.hiddenSessions[projectId];
+ }
+
+ this.saveConfig();
+ }
+
// ===========================================================================
// SSH Profile Management
// ===========================================================================
diff --git a/src/preload/constants/ipcChannels.ts b/src/preload/constants/ipcChannels.ts
index 8d730cd4..eec0bc05 100644
--- a/src/preload/constants/ipcChannels.ts
+++ b/src/preload/constants/ipcChannels.ts
@@ -68,6 +68,18 @@ export const CONFIG_PIN_SESSION = 'config:pinSession';
/** Unpin a session */
export const CONFIG_UNPIN_SESSION = 'config:unpinSession';
+/** Hide a session */
+export const CONFIG_HIDE_SESSION = 'config:hideSession';
+
+/** Unhide a session */
+export const CONFIG_UNHIDE_SESSION = 'config:unhideSession';
+
+/** Bulk hide sessions */
+export const CONFIG_HIDE_SESSIONS = 'config:hideSessions';
+
+/** Bulk unhide sessions */
+export const CONFIG_UNHIDE_SESSIONS = 'config:unhideSessions';
+
// =============================================================================
// SSH API Channels
// =============================================================================
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 0eb5e6b9..5f59e1c8 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -36,6 +36,8 @@ import {
CONFIG_GET,
CONFIG_GET_CLAUDE_ROOT_INFO,
CONFIG_GET_TRIGGERS,
+ CONFIG_HIDE_SESSION,
+ CONFIG_HIDE_SESSIONS,
CONFIG_OPEN_IN_EDITOR,
CONFIG_PIN_SESSION,
CONFIG_REMOVE_IGNORE_REGEX,
@@ -45,6 +47,8 @@ import {
CONFIG_SELECT_FOLDERS,
CONFIG_SNOOZE,
CONFIG_TEST_TRIGGER,
+ CONFIG_UNHIDE_SESSION,
+ CONFIG_UNHIDE_SESSIONS,
CONFIG_UNPIN_SESSION,
CONFIG_UPDATE,
CONFIG_UPDATE_TRIGGER,
@@ -292,6 +296,18 @@ const electronAPI: ElectronAPI = {
unpinSession: async (projectId: string, sessionId: string): Promise => {
return invokeIpcWithResult(CONFIG_UNPIN_SESSION, projectId, sessionId);
},
+ hideSession: async (projectId: string, sessionId: string): Promise => {
+ return invokeIpcWithResult(CONFIG_HIDE_SESSION, projectId, sessionId);
+ },
+ unhideSession: async (projectId: string, sessionId: string): Promise => {
+ return invokeIpcWithResult(CONFIG_UNHIDE_SESSION, projectId, sessionId);
+ },
+ hideSessions: async (projectId: string, sessionIds: string[]): Promise => {
+ return invokeIpcWithResult(CONFIG_HIDE_SESSIONS, projectId, sessionIds);
+ },
+ unhideSessions: async (projectId: string, sessionIds: string[]): Promise => {
+ return invokeIpcWithResult(CONFIG_UNHIDE_SESSIONS, projectId, sessionIds);
+ },
},
// Deep link navigation
diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts
index b5389dcf..0d038926 100644
--- a/src/renderer/api/httpClient.ts
+++ b/src/renderer/api/httpClient.ts
@@ -436,6 +436,14 @@ export class HttpAPIClient implements ElectronAPI {
this.post('/api/config/pin-session', { projectId, sessionId }),
unpinSession: (projectId: string, sessionId: string): Promise =>
this.post('/api/config/unpin-session', { projectId, sessionId }),
+ hideSession: (projectId: string, sessionId: string): Promise =>
+ this.post('/api/config/hide-session', { projectId, sessionId }),
+ unhideSession: (projectId: string, sessionId: string): Promise =>
+ this.post('/api/config/unhide-session', { projectId, sessionId }),
+ hideSessions: (projectId: string, sessionIds: string[]): Promise =>
+ this.post('/api/config/hide-sessions', { projectId, sessionIds }),
+ unhideSessions: (projectId: string, sessionIds: string[]): Promise =>
+ this.post('/api/config/unhide-sessions', { projectId, sessionIds }),
};
// ---------------------------------------------------------------------------
diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx
index 26663710..25726017 100644
--- a/src/renderer/components/layout/TabBar.tsx
+++ b/src/renderer/components/layout/TabBar.tsx
@@ -48,6 +48,8 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
splitPane,
togglePinSession,
pinnedSessionIds,
+ toggleHideSession,
+ hiddenSessionIds,
} = useStore(
useShallow((s) => ({
pane: s.paneLayout.panes.find((p) => p.id === paneId),
@@ -72,6 +74,8 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
splitPane: s.splitPane,
togglePinSession: s.togglePinSession,
pinnedSessionIds: s.pinnedSessionIds,
+ toggleHideSession: s.toggleHideSession,
+ hiddenSessionIds: s.hiddenSessionIds,
}))
);
@@ -235,6 +239,10 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
isContextMenuTabSession && contextMenuTab?.sessionId
? pinnedSessionIds.includes(contextMenuTab.sessionId)
: false;
+ const isContextMenuTabHidden =
+ isContextMenuTabSession && contextMenuTab?.sessionId
+ ? hiddenSessionIds.includes(contextMenuTab.sessionId)
+ : false;
// Show sidebar expand button only in the leftmost pane
const isLeftmostPane = useStore(
@@ -427,6 +435,12 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
? () => togglePinSession(contextMenuTab.sessionId!)
: undefined
}
+ isHidden={isContextMenuTabHidden}
+ onToggleHide={
+ isContextMenuTabSession && contextMenuTab?.sessionId
+ ? () => toggleHideSession(contextMenuTab.sessionId!)
+ : undefined
+ }
/>
)}
diff --git a/src/renderer/components/layout/TabContextMenu.tsx b/src/renderer/components/layout/TabContextMenu.tsx
index dd7165ea..cd3fedf8 100644
--- a/src/renderer/components/layout/TabContextMenu.tsx
+++ b/src/renderer/components/layout/TabContextMenu.tsx
@@ -27,6 +27,10 @@ interface TabContextMenuProps {
isPinned?: boolean;
/** Callback to toggle pin state */
onTogglePin?: () => void;
+ /** Whether this session is currently hidden from the sidebar */
+ isHidden?: boolean;
+ /** Callback to toggle hide state */
+ onToggleHide?: () => void;
}
export const TabContextMenu = ({
@@ -44,6 +48,8 @@ export const TabContextMenu = ({
isSessionTab,
isPinned,
onTogglePin,
+ isHidden,
+ onToggleHide,
}: TabContextMenuProps): React.JSX.Element => {
const menuRef = useRef(null);
@@ -114,6 +120,12 @@ export const TabContextMenu = ({
/>
>
)}
+ {isSessionTab && onToggleHide && (
+
+ )}
diff --git a/src/renderer/components/settings/hooks/useSettingsHandlers.ts b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
index e24add0c..5d6941b0 100644
--- a/src/renderer/components/settings/hooks/useSettingsHandlers.ts
+++ b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
@@ -295,6 +295,7 @@ export function useSettingsHandlers({
},
sessions: {
pinnedSessions: {},
+ hiddenSessions: {},
},
};
diff --git a/src/renderer/components/sidebar/DateGroupedSessions.tsx b/src/renderer/components/sidebar/DateGroupedSessions.tsx
index 45aa32e1..62ee6f4c 100644
--- a/src/renderer/components/sidebar/DateGroupedSessions.tsx
+++ b/src/renderer/components/sidebar/DateGroupedSessions.tsx
@@ -1,6 +1,7 @@
/**
* DateGroupedSessions - Sessions organized by date categories with virtual scrolling.
* Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll.
+ * Supports multi-select with bulk actions and hidden session filtering.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -13,7 +14,17 @@ import {
separatePinnedSessions,
} from '@renderer/utils/dateGrouping';
import { useVirtualizer } from '@tanstack/react-virtual';
-import { ArrowDownWideNarrow, Calendar, Loader2, MessageSquareOff, Pin } from 'lucide-react';
+import {
+ ArrowDownWideNarrow,
+ Calendar,
+ CheckSquare,
+ Eye,
+ EyeOff,
+ Loader2,
+ MessageSquareOff,
+ Pin,
+ X,
+} from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { SessionItem } from './SessionItem';
@@ -25,7 +36,7 @@ import type { DateCategory } from '@renderer/types/tabs';
type VirtualItem =
| { type: 'header'; category: DateCategory; id: string }
| { type: 'pinned-header'; id: string }
- | { type: 'session'; session: Session; isPinned: boolean; id: string }
+ | { type: 'session'; session: Session; isPinned: boolean; isHidden: boolean; id: string }
| { type: 'loader'; id: string };
/**
@@ -52,6 +63,17 @@ export const DateGroupedSessions = (): React.JSX.Element => {
pinnedSessionIds,
sessionSortMode,
setSessionSortMode,
+ hiddenSessionIds,
+ showHiddenSessions,
+ toggleShowHiddenSessions,
+ sidebarSelectedSessionIds,
+ sidebarMultiSelectActive,
+ toggleSidebarSessionSelection,
+ clearSidebarSelection,
+ toggleSidebarMultiSelect,
+ hideMultipleSessions,
+ unhideMultipleSessions,
+ pinMultipleSessions,
} = useStore(
useShallow((s) => ({
sessions: s.sessions,
@@ -65,6 +87,17 @@ export const DateGroupedSessions = (): React.JSX.Element => {
pinnedSessionIds: s.pinnedSessionIds,
sessionSortMode: s.sessionSortMode,
setSessionSortMode: s.setSessionSortMode,
+ hiddenSessionIds: s.hiddenSessionIds,
+ showHiddenSessions: s.showHiddenSessions,
+ toggleShowHiddenSessions: s.toggleShowHiddenSessions,
+ sidebarSelectedSessionIds: s.sidebarSelectedSessionIds,
+ sidebarMultiSelectActive: s.sidebarMultiSelectActive,
+ toggleSidebarSessionSelection: s.toggleSidebarSessionSelection,
+ clearSidebarSelection: s.clearSidebarSelection,
+ toggleSidebarMultiSelect: s.toggleSidebarMultiSelect,
+ hideMultipleSessions: s.hideMultipleSessions,
+ unhideMultipleSessions: s.unhideMultipleSessions,
+ pinMultipleSessions: s.pinMultipleSessions,
}))
);
@@ -72,10 +105,19 @@ export const DateGroupedSessions = (): React.JSX.Element => {
const countRef = useRef(null);
const [showCountTooltip, setShowCountTooltip] = useState(false);
+ const hiddenSet = useMemo(() => new Set(hiddenSessionIds), [hiddenSessionIds]);
+ const hasHiddenSessions = hiddenSessionIds.length > 0;
+
+ // Filter out hidden sessions unless showHiddenSessions is on
+ const visibleSessions = useMemo(() => {
+ if (showHiddenSessions) return sessions;
+ return sessions.filter((s) => !hiddenSet.has(s.id));
+ }, [sessions, hiddenSet, showHiddenSessions]);
+
// Separate pinned sessions from unpinned
const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo(
- () => separatePinnedSessions(sessions, pinnedSessionIds),
- [sessions, pinnedSessionIds]
+ () => separatePinnedSessions(visibleSessions, pinnedSessionIds),
+ [visibleSessions, pinnedSessionIds]
);
// Group only unpinned sessions by date
@@ -90,8 +132,10 @@ export const DateGroupedSessions = (): React.JSX.Element => {
// 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]);
+ return [...visibleSessions].sort(
+ (a, b) => (b.contextConsumption ?? 0) - (a.contextConsumption ?? 0)
+ );
+ }, [visibleSessions, sessionSortMode]);
// Flatten sessions with date headers into virtual list items
const virtualItems = useMemo((): VirtualItem[] => {
@@ -104,6 +148,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session',
session,
isPinned: pinnedSessionIds.includes(session.id),
+ isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`,
});
}
@@ -120,6 +165,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session',
session,
isPinned: true,
+ isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`,
});
}
@@ -137,6 +183,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
type: 'session',
session,
isPinned: false,
+ isHidden: hiddenSet.has(session.id),
id: `session-${session.id}`,
});
}
@@ -156,6 +203,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
sessionSortMode,
contextSortedSessions,
pinnedSessionIds,
+ hiddenSet,
pinnedSessions,
nonEmptyCategories,
groupedSessions,
@@ -221,6 +269,32 @@ export const DateGroupedSessions = (): React.JSX.Element => {
fetchSessionsMore,
]);
+ // Bulk action helpers
+ const selectedSet = useMemo(
+ () => new Set(sidebarSelectedSessionIds),
+ [sidebarSelectedSessionIds]
+ );
+ const someSelectedAreHidden = useMemo(
+ () => sidebarSelectedSessionIds.some((id) => hiddenSet.has(id)),
+ [sidebarSelectedSessionIds, hiddenSet]
+ );
+
+ const handleBulkHide = useCallback(() => {
+ void hideMultipleSessions(sidebarSelectedSessionIds);
+ clearSidebarSelection();
+ }, [hideMultipleSessions, sidebarSelectedSessionIds, clearSidebarSelection]);
+
+ const handleBulkUnhide = useCallback(() => {
+ const hiddenSelected = sidebarSelectedSessionIds.filter((id) => hiddenSet.has(id));
+ void unhideMultipleSessions(hiddenSelected);
+ clearSidebarSelection();
+ }, [unhideMultipleSessions, sidebarSelectedSessionIds, hiddenSet, clearSidebarSelection]);
+
+ const handleBulkPin = useCallback(() => {
+ void pinMultipleSessions(sidebarSelectedSessionIds);
+ clearSidebarSelection();
+ }, [pinMultipleSessions, sidebarSelectedSessionIds, clearSidebarSelection]);
+
if (!selectedProjectId) {
return (
@@ -337,19 +411,100 @@ export const DateGroupedSessions = (): React.JSX.Element => {
,
document.body
)}
-
- setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
- }
- className="ml-auto rounded p-1 transition-colors hover:bg-white/5"
- title={sessionSortMode === 'recent' ? 'Sort by context consumption' : 'Sort by recent'}
+
+ {/* Multi-select toggle */}
+
+
+
+ {/* Show hidden sessions toggle - only when hidden sessions exist */}
+ {hasHiddenSessions && (
+
+ {showHiddenSessions ? : }
+
+ )}
+ {/* Sort mode toggle */}
+
+ setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
+ }
+ className="rounded p-1 transition-colors hover:bg-white/5"
+ title={sessionSortMode === 'recent' ? 'Sort by context consumption' : 'Sort by recent'}
+ style={{
+ color: sessionSortMode === 'most-context' ? '#818cf8' : 'var(--color-text-muted)',
+ }}
+ >
+
+
+
+
+
+ {/* Bulk action bar - shown when sessions are selected */}
+ {sidebarMultiSelectActive && sidebarSelectedSessionIds.length > 0 && (
+
+
+ {sidebarSelectedSessionIds.length} selected
+
+
+
+ Pin
+
+
+ Hide
+
+ {showHiddenSessions && someSelectedAreHidden && (
+
+ Unhide
+
+ )}
+
+
+
+
+
+ )}
{
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 = (
+
+
onItemClick(itemKey)}
+ className="group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 transition-all duration-200"
+ style={{
+ backgroundColor: TOOL_CALL_BG,
+ border: `1px solid ${TOOL_CALL_BORDER}`,
+ }}
+ aria-expanded={compactExpanded}
+ >
+
+
+
+
+
+ Compacted
+
+ {item.tokenDelta && (
+
+ {formatTokensCompact(item.tokenDelta.preCompactionTokens)} →{' '}
+ {formatTokensCompact(item.tokenDelta.postCompactionTokens)}
+
+ {' '}
+ ({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed)
+
+
+ )}
+
+ Phase {item.phaseNumber}
+
+
+ {format(new Date(item.timestamp), 'h:mm:ss a')}
+
+
+ {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 */}
+
handleItemClick(itemId)}
+ className="group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 transition-all duration-200"
+ style={{
+ backgroundColor: TOOL_CALL_BG,
+ border: `1px solid ${TOOL_CALL_BORDER}`,
+ }}
+ aria-expanded={isExpanded}
+ >
+
+
+
+
+
+ Compacted
+
+ {item.tokenDelta && (
+
+ {formatTokensCompact(item.tokenDelta.preCompactionTokens)} →{' '}
+ {formatTokensCompact(item.tokenDelta.postCompactionTokens)}
+
+ {' '}
+ ({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed)
+
+
+ )}
+
+ Phase {item.phaseNumber}
+
+
+ {format(new Date(item.timestamp), 'h:mm:ss a')}
+
+
+ {/* 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 @@
-
+
@@ -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() && (
- openSettingsTab()}
- onMouseEnter={() => setSettingsHover(true)}
- onMouseLeave={() => setSettingsHover(false)}
- className="rounded-md p-2 transition-colors"
- style={{
- color: settingsHover ? 'var(--color-text)' : 'var(--color-text-muted)',
- backgroundColor: settingsHover ? 'var(--color-surface-raised)' : 'transparent',
- }}
- title="Settings"
- >
-
-
- )}
+ {/* Settings gear icon */}
+ openSettingsTab()}
+ onMouseEnter={() => setSettingsHover(true)}
+ onMouseLeave={() => setSettingsHover(false)}
+ className="rounded-md p-2 transition-colors"
+ style={{
+ color: settingsHover ? 'var(--color-text)' : 'var(--color-text-muted)',
+ backgroundColor: settingsHover ? 'var(--color-surface-raised)' : 'transparent',
+ }}
+ title="Settings"
+ >
+
+
{/* 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
-
-
- Open in Editor
-
+ {isElectron && (
+
+
+ Open in Editor
+
+ )}
@@ -149,22 +152,35 @@ export const AdvancedSection = ({
claude-devtools
-
- {getUpdateButtonContent()}
-
+ {isElectron && (
+
+ {getUpdateButtonContent()}
+
+ )}
+ {!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}
+
+
+
-
-
void handleSelectClaudeRootFolder()}
- disabled={updatingClaudeRoot}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text)',
- }}
- >
-
- {updatingClaudeRoot ? (
-
- ) : (
-
+
+ void handleSelectClaudeRootFolder()}
+ disabled={updatingClaudeRoot}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text)',
+ }}
+ >
+
+ {updatingClaudeRoot ? (
+
+ ) : (
+
+ )}
+ Select Folder
+
+
+
+ void handleResetClaudeRoot()}
+ disabled={updatingClaudeRoot || !isCustomClaudeRoot}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+
+
+ Use Auto-Detect
+
+
+
+ {isWindowsStyleDefaultPath && (
+ void handleUseWslForClaude()}
+ disabled={updatingClaudeRoot || findingWslRoots}
+ className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+
+ {findingWslRoots ? (
+
+ ) : (
+
+ )}
+ Using Linux/WSL?
+
+
)}
- Select Folder
-
-
+
- void handleResetClaudeRoot()}
- disabled={updatingClaudeRoot || !isCustomClaudeRoot}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text-secondary)',
- }}
- >
-
-
- Use Auto-Detect
-
-
+ {claudeRootError && (
+
+ )}
- {isWindowsStyleDefaultPath && (
- void handleUseWslForClaude()}
- disabled={updatingClaudeRoot || findingWslRoots}
- className="rounded-md px-4 py-1.5 text-sm transition-colors disabled:opacity-50"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text-secondary)',
- }}
- >
-
- {findingWslRoots ? (
-
- ) : (
-
- )}
- Using Linux/WSL?
-
-
- )}
-
+ {showWslModal && (
+
+
setShowWslModal(false)}
+ aria-label="Close WSL path modal"
+ tabIndex={-1}
+ />
+
+
+ Select WSL Claude Root
+
+
+ Detected WSL distributions and Claude root candidates
+
- {claudeRootError && (
-
- )}
-
- {showWslModal && (
-
-
setShowWslModal(false)}
- aria-label="Close WSL path modal"
- tabIndex={-1}
- />
-
-
- Select WSL Claude Root
-
-
- Detected WSL distributions and Claude root candidates
-
-
-
- {wslCandidates.map((candidate) => (
-
-
-
- {candidate.distro}
-
-
+ {wslCandidates.map((candidate) => (
+
- {candidate.path}
-
- {!candidate.hasProjectsDir && (
-
No projects directory detected
- )}
-
+
+
+ {candidate.distro}
+
+
+ {candidate.path}
+
+ {!candidate.hasProjectsDir && (
+
+ No projects directory detected
+
+ )}
+
+
void applyWslCandidate(candidate)}
+ className="rounded-md px-3 py-1.5 text-xs transition-colors"
+ style={{
+ backgroundColor: 'var(--color-surface-raised)',
+ color: 'var(--color-text)',
+ }}
+ >
+ Use This Path
+
+
+ ))}
+
+
+
void applyWslCandidate(candidate)}
+ onClick={() => setShowWslModal(false)}
+ className="rounded-md border px-3 py-1.5 text-xs transition-colors hover:bg-white/5"
+ style={{
+ borderColor: 'var(--color-border)',
+ color: 'var(--color-text-secondary)',
+ }}
+ >
+ Cancel
+
+ {
+ setShowWslModal(false);
+ void handleSelectClaudeRootFolder();
+ }}
className="rounded-md px-3 py-1.5 text-xs transition-colors"
style={{
backgroundColor: 'var(--color-surface-raised)',
color: 'var(--color-text)',
}}
>
- Use This Path
+ Select Folder Manually
- ))}
+
-
-
- setShowWslModal(false)}
- className="rounded-md border px-3 py-1.5 text-xs transition-colors hover:bg-white/5"
- style={{
- borderColor: 'var(--color-border)',
- color: 'var(--color-text-secondary)',
- }}
- >
- Cancel
-
- {
- setShowWslModal(false);
- void handleSelectClaudeRootFolder();
- }}
- className="rounded-md px-3 py-1.5 text-xs transition-colors"
- style={{
- backgroundColor: 'var(--color-surface-raised)',
- color: 'var(--color-text)',
- }}
- >
- Select Folder Manually
-
-
-
-
+ )}
+ >
)}
-
-
- {serverLoading ? (
-
- ) : (
-
- )}
-
+ {isElectron ? (
+ <>
+
+
+ {serverLoading ? (
+
+ ) : (
+
+ )}
+
- {serverStatus.running && (
-
-
-
- Running on
-
-
+
+
+ Running on
+
+
+ {serverUrl}
+
+
+ {copied ? : }
+ {copied ? 'Copied' : 'Copy URL'}
+
+
+ )}
+ >
+ ) : (
+ <>
+
+
- {serverUrl}
-
-
- {copied ? : }
- {copied ? 'Copied' : 'Copy URL'}
-
-
+
+
+ Running on
+
+
+ {window.location.origin}
+
+ {
+ void navigator.clipboard.writeText(window.location.origin);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }}
+ className="ml-auto flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-white/5"
+ style={{
+ borderColor: 'var(--color-border)',
+ color: copied ? '#22c55e' : 'var(--color-text-secondary)',
+ }}
+ >
+ {copied ? : }
+ {copied ? 'Copied' : 'Copy URL'}
+
+
+
+ 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 */}
diff --git a/src/renderer/components/layout/SidebarHeader.tsx b/src/renderer/components/layout/SidebarHeader.tsx
index 79cf254f..d3cbd291 100644
--- a/src/renderer/components/layout/SidebarHeader.tsx
+++ b/src/renderer/components/layout/SidebarHeader.tsx
@@ -333,9 +333,7 @@ export const SidebarHeader = (): React.JSX.Element => {
style={
{
height: `${HEADER_ROW1_HEIGHT}px`,
- paddingLeft: isMacElectron
- ? 'var(--macos-traffic-light-padding-left, 72px)'
- : '16px',
+ paddingLeft: isMacElectron ? 'var(--macos-traffic-light-padding-left, 72px)' : '16px',
WebkitAppRegion: isMacElectron ? 'drag' : undefined,
} as React.CSSProperties
}
diff --git a/src/renderer/components/sidebar/SessionItem.tsx b/src/renderer/components/sidebar/SessionItem.tsx
index 2a4be156..8d756d86 100644
--- a/src/renderer/components/sidebar/SessionItem.tsx
+++ b/src/renderer/components/sidebar/SessionItem.tsx
@@ -61,21 +61,14 @@ const ConsumptionBadge = ({
contextConsumption: number;
phaseBreakdown?: PhaseTokenBreakdown[];
}>): React.JSX.Element => {
- const [showPopover, setShowPopover] = useState(false);
+ const [popoverPosition, setPopoverPosition] = useState<{
+ top: number;
+ left: number;
+ } | null>(null);
const badgeRef = useRef(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;
+ const showPopover = popoverPosition !== null;
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- tooltip trigger via hover, not interactive
@@ -83,8 +76,16 @@ const ConsumptionBadge = ({
ref={badgeRef}
className="tabular-nums"
style={{ color: isHigh ? 'rgb(251, 191, 36)' : undefined }}
- onMouseEnter={() => setShowPopover(true)}
- onMouseLeave={() => setShowPopover(false)}
+ onMouseEnter={() => {
+ const rect = badgeRef.current?.getBoundingClientRect();
+ if (rect) {
+ setPopoverPosition({
+ top: rect.top - 6,
+ left: rect.left + rect.width / 2,
+ });
+ }
+ }}
+ onMouseLeave={() => setPopoverPosition(null)}
>
{formatTokensCompact(contextConsumption)}
{showPopover &&
diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts
index f686fd4a..01114285 100644
--- a/src/renderer/store/index.ts
+++ b/src/renderer/store/index.ts
@@ -221,7 +221,8 @@ export function initializeNotificationListeners(): () => void {
(eventProjectBaseId == null || selectedProjectBaseId === eventProjectBaseId);
const isTopLevelSessionEvent = !event.isSubagent;
const isUnknownSessionInSidebar =
- event.sessionId == null || !state.sessions.some((session) => session.id === event.sessionId);
+ event.sessionId == null ||
+ !state.sessions.some((session) => session.id === event.sessionId);
const shouldRefreshForPotentialNewSession =
isTopLevelSessionEvent &&
matchesSelectedProject &&
From fb66b14d628d001fd027517bf17971030b3968d4 Mon Sep 17 00:00:00 2001
From: matt
Date: Mon, 16 Feb 2026 23:33:56 +0900
Subject: [PATCH 11/11] feat(sessions): add API endpoints for hiding and
unhiding sessions
- Implemented new POST endpoints for hiding and unhiding individual and bulk sessions in the configuration.
- Added error handling and validation for project and session IDs in the new API routes.
- Enhanced the existing session management functionality in the ConfigManager to support these operations.
---
src/main/http/config.ts | 88 ++++++++++++++++++++
src/main/utils/jsonl.ts | 18 ++--
src/renderer/components/chat/ChatHistory.tsx | 34 ++++----
3 files changed, 118 insertions(+), 22 deletions(-)
diff --git a/src/main/http/config.ts b/src/main/http/config.ts
index 9894d5a3..6d35c66d 100644
--- a/src/main/http/config.ts
+++ b/src/main/http/config.ts
@@ -382,6 +382,94 @@ export function registerConfigRoutes(app: FastifyInstance): void {
}
);
+ // Hide session
+ app.post<{ Body: { projectId: string; sessionId: string } }>(
+ '/api/config/hide-session',
+ async (request) => {
+ try {
+ const { projectId, sessionId } = request.body;
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!sessionId || typeof sessionId !== 'string') {
+ return { success: false, error: 'Session ID is required and must be a string' };
+ }
+
+ configManager.hideSession(projectId, sessionId);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in POST /api/config/hide-session:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+ }
+ );
+
+ // Unhide session
+ app.post<{ Body: { projectId: string; sessionId: string } }>(
+ '/api/config/unhide-session',
+ async (request) => {
+ try {
+ const { projectId, sessionId } = request.body;
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!sessionId || typeof sessionId !== 'string') {
+ return { success: false, error: 'Session ID is required and must be a string' };
+ }
+
+ configManager.unhideSession(projectId, sessionId);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in POST /api/config/unhide-session:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+ }
+ );
+
+ // Bulk hide sessions
+ app.post<{ Body: { projectId: string; sessionIds: string[] } }>(
+ '/api/config/hide-sessions',
+ async (request) => {
+ try {
+ const { projectId, sessionIds } = request.body;
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
+ return { success: false, error: 'Session IDs must be an array of strings' };
+ }
+
+ configManager.hideSessions(projectId, sessionIds);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in POST /api/config/hide-sessions:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+ }
+ );
+
+ // Bulk unhide sessions
+ app.post<{ Body: { projectId: string; sessionIds: string[] } }>(
+ '/api/config/unhide-sessions',
+ async (request) => {
+ try {
+ const { projectId, sessionIds } = request.body;
+ if (!projectId || typeof projectId !== 'string') {
+ return { success: false, error: 'Project ID is required and must be a string' };
+ }
+ if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) {
+ return { success: false, error: 'Session IDs must be an array of strings' };
+ }
+
+ configManager.unhideSessions(projectId, sessionIds);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error in POST /api/config/unhide-sessions:', error);
+ return { success: false, error: getErrorMessage(error) };
+ }
+ }
+ );
+
// Select folders - no-op in browser mode
app.post('/api/config/select-folders', async (): Promise> => {
return { success: true, data: [] };
diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts
index 1d70be2c..eaf618f0 100644
--- a/src/main/utils/jsonl.ts
+++ b/src/main/utils/jsonl.ts
@@ -562,14 +562,18 @@ export async function analyzeSessionFileMetadata(
}
// Last phase: final tokens - last post-compaction
+ // Guard: if the last compaction had no subsequent assistant message, post is 0.
+ // In that case, skip the final phase to avoid double-counting.
const lastPhase = compactionPhases[compactionPhases.length - 1];
- const lastContribution = lastMainAssistantInputTokens - lastPhase.post;
- total += lastContribution;
- phaseBreakdown.push({
- phaseNumber: compactionPhases.length + 1,
- contribution: lastContribution,
- peakTokens: lastMainAssistantInputTokens,
- });
+ if (lastPhase.post > 0) {
+ const lastContribution = lastMainAssistantInputTokens - lastPhase.post;
+ total += lastContribution;
+ phaseBreakdown.push({
+ phaseNumber: compactionPhases.length + 1,
+ contribution: lastContribution,
+ peakTokens: lastMainAssistantInputTokens,
+ });
+ }
contextConsumption = total;
}
diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx
index 49023fea..a6a4acbe 100644
--- a/src/renderer/components/chat/ChatHistory.tsx
+++ b/src/renderer/components/chat/ChatHistory.tsx
@@ -413,23 +413,27 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
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;
+ const run = async (): Promise => {
+ const groupId = prevItem.group.id;
+ await ensureGroupVisible(groupId);
+ 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);
+ 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);
+ };
+ void run();
},
- [conversation, setHighlightedGroupId]
+ [conversation, ensureGroupVisible, setHighlightedGroupId]
);
// Handler to navigate to a specific tool within a turn from context panel