feat(remotion): integrate Remotion for video rendering and preview
- Added new scripts for rendering and previewing videos using Remotion, enhancing multimedia capabilities. - Updated package.json to include Remotion dependencies and commands for rendering videos in different formats. - Modified .gitignore to exclude Remotion output files, ensuring a cleaner repository. - Enhanced README to reflect new features and usage instructions for video rendering. This commit significantly improves the application's multimedia functionality by integrating Remotion, allowing users to create and preview videos seamlessly.
This commit is contained in:
parent
cf61a78bea
commit
c02c7d24cf
14 changed files with 1635 additions and 65 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -45,4 +45,4 @@ temp/
|
|||
|
||||
|
||||
eslint-fix/
|
||||
demo/*
|
||||
remotion/*
|
||||
41
README.md
41
README.md
|
|
@ -5,9 +5,9 @@
|
|||
<h1 align="center">claude-devtools</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Stop guessing. See exactly what Claude is doing.</strong>
|
||||
<strong><code>Read 3 files</code> told you nothing. This shows you everything.</strong>
|
||||
<br />
|
||||
A desktop app that turns Claude Code's opaque session logs into a visual, searchable, actionable interface.
|
||||
A desktop app that reconstructs exactly what Claude Code did — every file path, every tool call, every token — from the raw session logs already on your machine.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
|
@ -21,6 +21,16 @@
|
|||
|
||||
## Why This Exists
|
||||
|
||||
### Claude Code stopped telling you what it's doing.
|
||||
|
||||
Recent Claude Code updates replaced detailed tool output with opaque summaries. `Read 3 files`. `Searched for 1 pattern`. `Edited 2 files`. No paths, no content, no line numbers. The context usage indicator became a three-segment progress bar with no breakdown. To get the details back, the only option is `--verbose` — which dumps raw JSON, internal system prompts, and thousands of lines of noise into your terminal.
|
||||
|
||||
**There is no middle ground in the CLI.** You either see too little or too much.
|
||||
|
||||
claude-devtools restores the information that was taken away — structured, searchable, and without a single modification to Claude Code itself. It reads the raw session logs from `~/.claude/` and reconstructs the full execution trace: every file path that was read, every regex that was searched, every diff that was applied, every token that was consumed — organized into a visual interface you can actually reason about.
|
||||
|
||||
### The wrapper problem.
|
||||
|
||||
There are many GUI wrappers for Claude Code — Conductor, Craft Agents, Vibe Kanban, 1Code, ccswitch, and others. I tried them all. None of them solved the actual problem:
|
||||
|
||||
**They wrap Claude Code.** They inject their own prompts, add their own abstractions, and change how Claude behaves. If you love the terminal — and I do — you don't want that. You want Claude Code exactly as it is.
|
||||
|
|
@ -39,9 +49,13 @@ There are many GUI wrappers for Claude Code — Conductor, Craft Agents, Vibe Ka
|
|||
|
||||
## Key Features
|
||||
|
||||
### :mag: Visible Context Tracking
|
||||
### :mag: Visible Context Reconstruction
|
||||
|
||||
See exactly what's eating your context window. The **Session Context Panel** breaks down token usage across 6 categories — CLAUDE.md files, @-mentioned files, tool outputs, extended thinking, team coordination, and user messages — so you can instantly identify what's consuming tokens and optimize your workflow.
|
||||
Claude Code doesn't expose what's actually in the context window. claude-devtools reverse-engineers it.
|
||||
|
||||
The engine walks each turn of the session and reconstructs the full set of context injections — **CLAUDE.md files** (global, project, and directory-level), **@-mentioned files**, **tool call inputs and outputs**, **extended thinking**, **team coordination overhead**, and **user prompt text** — then accumulates them across turns with compaction-phase awareness. When a context reset occurs mid-session, the tracker detects the boundary, measures the token delta, and starts a new phase.
|
||||
|
||||
The result is a per-turn breakdown of estimated token attribution across 6 categories, surfaced in three places: a **Context Badge** on each assistant response, a **Token Usage popover** with percentage breakdowns, and a dedicated **Session Context Panel** with phase-filtered drill-down into every injection.
|
||||
|
||||
### :hammer_and_wrench: Rich Tool Call Inspector
|
||||
|
||||
|
|
@ -63,12 +77,31 @@ When Claude uses multi-agent orchestration, see the full picture. Teammate messa
|
|||
|
||||
Hit **Cmd+K** for a Spotlight-style command palette. Search across all sessions in a project — results show context snippets with highlighted keywords. Navigate directly to the exact message.
|
||||
|
||||
### :globe_with_meridians: SSH Remote Sessions
|
||||
|
||||
Connect to any remote machine over SSH and inspect Claude Code sessions running there — same interface, no compromise.
|
||||
|
||||
claude-devtools parses your `~/.ssh/config` for host aliases, supports agent forwarding, private keys, and password auth, then opens an SFTP channel to stream session logs from the remote `~/.claude/` directory. Each SSH host gets its own isolated service context with independent caches, file watchers, and parsers. Switching between local and remote workspaces is instant — the app snapshots your current state to IndexedDB before the switch and restores it when you return, tabs and all.
|
||||
|
||||
### :bar_chart: Multi-Pane Layout
|
||||
|
||||
Open multiple sessions side-by-side. Drag-and-drop tabs between panes, split views, and compare sessions in parallel — like a proper IDE for your AI conversations.
|
||||
|
||||
---
|
||||
|
||||
## What the CLI Hides vs. What claude-devtools Shows
|
||||
|
||||
| What you see in the terminal | What claude-devtools shows you |
|
||||
|------------------------------|-------------------------------|
|
||||
| `Read 3 files` | Exact file paths, syntax-highlighted content with line numbers |
|
||||
| `Searched for 1 pattern` | The regex pattern, every matching file, and the matched lines |
|
||||
| `Edited 2 files` | Inline diffs with added/removed highlighting per file |
|
||||
| A three-segment context bar | Per-turn token attribution across 6 categories with compaction-phase tracking |
|
||||
| Subagent output interleaved with the main thread | Isolated execution trees per agent, expandable inline with their own metrics |
|
||||
| `--verbose` JSON dump | Structured, filterable, navigable interface — no noise |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -36,7 +36,10 @@
|
|||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts"
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
|
@ -64,6 +67,10 @@
|
|||
"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",
|
||||
|
|
@ -96,6 +103,7 @@
|
|||
"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",
|
||||
|
|
|
|||
1397
pnpm-lock.yaml
1397
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
BIN
public/demo.mp4
Normal file
BIN
public/demo.mp4
Normal file
Binary file not shown.
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
BIN
resources/demo.mp4
Normal file
BIN
resources/demo.mp4
Normal file
Binary file not shown.
|
|
@ -160,8 +160,15 @@ export function registerSessionRoutes(app: FastifyInstance, services: HttpServic
|
|||
return sessionDetail;
|
||||
}
|
||||
|
||||
// Get session metadata
|
||||
const session = await services.projectScanner.getSession(safeProjectId, safeSessionId);
|
||||
const fsType = services.projectScanner.getFileSystemProvider().type;
|
||||
// In SSH mode, avoid an extra deep metadata scan before full parse.
|
||||
const session = await services.projectScanner.getSessionWithOptions(
|
||||
safeProjectId,
|
||||
safeSessionId,
|
||||
{
|
||||
metadataLevel: fsType === 'ssh' ? 'light' : 'deep',
|
||||
}
|
||||
);
|
||||
if (!session) {
|
||||
logger.error(`Session not found: ${safeSessionId}`);
|
||||
return null;
|
||||
|
|
@ -180,6 +187,7 @@ export function registerSessionRoutes(app: FastifyInstance, services: HttpServic
|
|||
parsedSession.taskCalls,
|
||||
parsedSession.messages
|
||||
);
|
||||
session.hasSubagents = subagents.length > 0;
|
||||
|
||||
// Build session detail with chunks
|
||||
sessionDetail = services.chunkBuilder.buildSessionDetail(
|
||||
|
|
|
|||
|
|
@ -224,8 +224,11 @@ async function handleGetSessionDetail(
|
|||
return sessionDetail;
|
||||
}
|
||||
|
||||
// Get session metadata
|
||||
const session = await projectScanner.getSession(safeProjectId, safeSessionId);
|
||||
const fsType = projectScanner.getFileSystemProvider().type;
|
||||
// In SSH mode, avoid an extra deep metadata scan before full parse.
|
||||
const session = await projectScanner.getSessionWithOptions(safeProjectId, safeSessionId, {
|
||||
metadataLevel: fsType === 'ssh' ? 'light' : 'deep',
|
||||
});
|
||||
if (!session) {
|
||||
logger.error(`Session not found: ${sessionId}`);
|
||||
return null;
|
||||
|
|
@ -241,6 +244,7 @@ async function handleGetSessionDetail(
|
|||
parsedSession.taskCalls,
|
||||
parsedSession.messages
|
||||
);
|
||||
session.hasSubagents = subagents.length > 0;
|
||||
|
||||
// Build session detail with chunks
|
||||
sessionDetail = chunkBuilder.buildSessionDetail(session, parsedSession.messages, subagents);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ import { subprojectRegistry } from './SubprojectRegistry';
|
|||
import type { FileSystemProvider } from '@main/services/infrastructure/FileSystemProvider';
|
||||
|
||||
const logger = createLogger('Discovery:SessionSearcher');
|
||||
const SSH_FAST_SEARCH_STAGE_LIMITS = [40, 140, 320] as const;
|
||||
const SSH_FAST_SEARCH_MIN_RESULTS = 8;
|
||||
const SSH_FAST_SEARCH_TIME_BUDGET_MS = 4500;
|
||||
|
||||
interface SearchableEntry {
|
||||
text: string;
|
||||
|
|
@ -71,8 +74,11 @@ export class SessionSearcher {
|
|||
query: string,
|
||||
maxResults: number = 50
|
||||
): Promise<SearchSessionsResult> {
|
||||
const startedAt = Date.now();
|
||||
const results: SearchResult[] = [];
|
||||
let sessionsSearched = 0;
|
||||
const fastMode = this.fsProvider.type === 'ssh';
|
||||
let isPartial = false;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return { results: [], totalMatches: 0, sessionsSearched: 0, query };
|
||||
|
|
@ -91,9 +97,7 @@ export class SessionSearcher {
|
|||
|
||||
// Get all session files
|
||||
const entries = await this.fsProvider.readdir(projectPath);
|
||||
const sessionFilesWithTime = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => {
|
||||
const sessionEntries = entries.filter((entry) => {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) return false;
|
||||
// Filter to only sessions belonging to this subproject
|
||||
if (sessionFilter) {
|
||||
|
|
@ -101,42 +105,90 @@ export class SessionSearcher {
|
|||
return sessionFilter.has(sessionId);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(async (entry) => {
|
||||
});
|
||||
const sessionFiles = await this.collectFulfilledInBatches(
|
||||
sessionEntries,
|
||||
this.fsProvider.type === 'ssh' ? 24 : 128,
|
||||
async (entry) => {
|
||||
const filePath = path.join(projectPath, entry.name);
|
||||
try {
|
||||
const stats = await this.fsProvider.stat(filePath);
|
||||
return { name: entry.name, filePath, mtimeMs: stats.mtimeMs };
|
||||
} catch {
|
||||
return null;
|
||||
const mtimeMs =
|
||||
typeof entry.mtimeMs === 'number'
|
||||
? entry.mtimeMs
|
||||
: (await this.fsProvider.stat(filePath)).mtimeMs;
|
||||
return { name: entry.name, filePath, mtimeMs };
|
||||
}
|
||||
})
|
||||
);
|
||||
const sessionFiles = sessionFilesWithTime
|
||||
.filter((entry): entry is { name: string; filePath: string; mtimeMs: number } => !!entry)
|
||||
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
sessionFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
|
||||
// Search each session file
|
||||
for (const file of sessionFiles) {
|
||||
if (results.length >= maxResults) break;
|
||||
// Search session files with bounded concurrency and staged breadth in SSH mode.
|
||||
const searchBatchSize = fastMode ? 3 : 8;
|
||||
const stageBoundaries = fastMode
|
||||
? this.buildFastSearchStageBoundaries(sessionFiles.length)
|
||||
: [sessionFiles.length];
|
||||
let searchedUntil = 0;
|
||||
let shouldStop = false;
|
||||
|
||||
for (const stageBoundary of stageBoundaries) {
|
||||
for (
|
||||
let i = searchedUntil;
|
||||
i < stageBoundary && results.length < maxResults;
|
||||
i += searchBatchSize
|
||||
) {
|
||||
if (fastMode && Date.now() - startedAt >= SSH_FAST_SEARCH_TIME_BUDGET_MS) {
|
||||
isPartial = true;
|
||||
shouldStop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = sessionFiles.slice(i, i + searchBatchSize);
|
||||
sessionsSearched += batch.length;
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
batch.map(async (file) => {
|
||||
const sessionId = extractSessionId(file.name);
|
||||
const filePath = file.filePath;
|
||||
sessionsSearched++;
|
||||
|
||||
try {
|
||||
const sessionResults = await this.searchSessionFile(
|
||||
return this.searchSessionFile(
|
||||
projectId,
|
||||
sessionId,
|
||||
filePath,
|
||||
file.filePath,
|
||||
normalizedQuery,
|
||||
maxResults - results.length
|
||||
maxResults
|
||||
);
|
||||
results.push(...sessionResults);
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of settled) {
|
||||
if (results.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
if (result.status !== 'fulfilled' || result.value.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const remaining = maxResults - results.length;
|
||||
results.push(...result.value.slice(0, remaining));
|
||||
}
|
||||
}
|
||||
|
||||
searchedUntil = stageBoundary;
|
||||
|
||||
if (shouldStop || !fastMode || results.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (stageBoundary < sessionFiles.length && results.length >= SSH_FAST_SEARCH_MIN_RESULTS) {
|
||||
isPartial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fastMode && results.length < maxResults && sessionsSearched < sessionFiles.length) {
|
||||
isPartial = true;
|
||||
}
|
||||
|
||||
if (fastMode) {
|
||||
logger.debug(
|
||||
`SSH fast search scanned ${sessionsSearched}/${sessionFiles.length} sessions in ${Date.now() - startedAt}ms (results=${results.length}, partial=${isPartial})`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -144,6 +196,7 @@ export class SessionSearcher {
|
|||
totalMatches: results.length,
|
||||
sessionsSearched,
|
||||
query,
|
||||
isPartial: fastMode ? isPartial : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error searching sessions for project ${projectId}:`, error);
|
||||
|
|
@ -311,4 +364,45 @@ export class SessionSearcher {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async collectFulfilledInBatches<T, R>(
|
||||
items: T[],
|
||||
batchSize: number,
|
||||
mapper: (item: T) => Promise<R>
|
||||
): Promise<R[]> {
|
||||
const safeBatchSize = Math.max(1, batchSize);
|
||||
const results: R[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += safeBatchSize) {
|
||||
const batch = items.slice(i, i + safeBatchSize);
|
||||
const settled = await Promise.allSettled(batch.map((item) => mapper(item)));
|
||||
for (const result of settled) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private buildFastSearchStageBoundaries(totalFiles: number): number[] {
|
||||
if (totalFiles <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const boundaries: number[] = [];
|
||||
for (const limit of SSH_FAST_SEARCH_STAGE_LIMITS) {
|
||||
const boundary = Math.min(totalFiles, limit);
|
||||
if (boundaries.length === 0 || boundary > boundaries[boundaries.length - 1]) {
|
||||
boundaries.push(boundary);
|
||||
}
|
||||
}
|
||||
|
||||
if (boundaries.length === 0) {
|
||||
boundaries.push(totalFiles);
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,12 @@ export class SubagentResolver {
|
|||
return [];
|
||||
}
|
||||
|
||||
// Parse all subagent files
|
||||
const subagents = await Promise.all(
|
||||
subagentFiles.map((filePath) => this.parseSubagentFile(filePath))
|
||||
// Parse subagent files with bounded concurrency to avoid overwhelming SFTP.
|
||||
const parseConcurrency = this.projectScanner.getFileSystemProvider().type === 'ssh' ? 4 : 24;
|
||||
const subagents = await this.collectInBatches(
|
||||
subagentFiles,
|
||||
parseConcurrency,
|
||||
async (filePath) => this.parseSubagentFile(filePath)
|
||||
);
|
||||
|
||||
// Filter out failed parses
|
||||
|
|
@ -544,4 +547,25 @@ export class SubagentResolver {
|
|||
messageCount,
|
||||
};
|
||||
}
|
||||
|
||||
private async collectInBatches<T, R>(
|
||||
items: T[],
|
||||
batchSize: number,
|
||||
mapper: (item: T) => Promise<R>
|
||||
): Promise<R[]> {
|
||||
const safeBatchSize = Math.max(1, batchSize);
|
||||
const results: R[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += safeBatchSize) {
|
||||
const batch = items.slice(i, i + safeBatchSize);
|
||||
const settled = await Promise.allSettled(batch.map((item) => mapper(item)));
|
||||
for (const result of settled) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ export interface SearchSessionsResult {
|
|||
sessionsSearched: number;
|
||||
/** Search query used */
|
||||
query: string;
|
||||
/** True when fast mode intentionally returns only a recent subset */
|
||||
isPartial?: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [totalMatches, setTotalMatches] = useState(0);
|
||||
const [searchIsPartial, setSearchIsPartial] = useState(false);
|
||||
const latestSearchRequestRef = useRef(0);
|
||||
|
||||
// Determine search mode based on whether a project is selected
|
||||
|
|
@ -200,6 +201,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
setSessionResults([]);
|
||||
setSelectedIndex(0);
|
||||
setTotalMatches(0);
|
||||
setSearchIsPartial(false);
|
||||
}
|
||||
}, [commandPaletteOpen]);
|
||||
|
||||
|
|
@ -213,6 +215,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
) {
|
||||
setSessionResults([]);
|
||||
setTotalMatches(0);
|
||||
setSearchIsPartial(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +230,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
}
|
||||
setSessionResults(searchResult.results);
|
||||
setTotalMatches(searchResult.totalMatches);
|
||||
setSearchIsPartial(!!searchResult.isPartial);
|
||||
setSelectedIndex(0);
|
||||
} catch (error) {
|
||||
if (latestSearchRequestRef.current !== requestId) {
|
||||
|
|
@ -235,6 +239,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
logger.error('Search error:', error);
|
||||
setSessionResults([]);
|
||||
setTotalMatches(0);
|
||||
setSearchIsPartial(false);
|
||||
} finally {
|
||||
if (latestSearchRequestRef.current === requestId) {
|
||||
setLoading(false);
|
||||
|
|
@ -448,7 +453,9 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
</div>
|
||||
) : sessionResults.length === 0 && !loading ? (
|
||||
<div className="px-4 py-8 text-center text-sm text-text-muted">
|
||||
No results found for "{query}"
|
||||
{searchIsPartial
|
||||
? `No fast results in recent sessions for "${query}"`
|
||||
: `No results found for "${query}"`}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2">
|
||||
|
|
@ -471,7 +478,7 @@ export const CommandPalette = (): React.JSX.Element | null => {
|
|||
{searchMode === 'projects'
|
||||
? `${filteredProjects.length} project${filteredProjects.length !== 1 ? 's' : ''}`
|
||||
: totalMatches > 0
|
||||
? `${totalMatches} result${totalMatches !== 1 ? 's' : ''}`
|
||||
? `${totalMatches} ${searchIsPartial ? 'fast ' : ''}result${totalMatches !== 1 ? 's' : ''}`
|
||||
: 'Type to search'}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -186,10 +186,11 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
|||
|
||||
// Compute CLAUDE.md stats for the session
|
||||
const projectRoot = detail?.session?.projectPath ?? '';
|
||||
const { connectionMode } = get();
|
||||
let claudeMdStats: Map<string, ClaudeMdStats> | null = null;
|
||||
let contextStats: Map<string, ContextStats> | null = null;
|
||||
let phaseInfo: ContextPhaseInfo | null = null;
|
||||
if (conversation?.items) {
|
||||
if (connectionMode !== 'ssh' && conversation?.items) {
|
||||
// Fetch real CLAUDE.md token data
|
||||
let claudeMdTokenData: Record<string, ClaudeMdFileInfo> = {};
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue