Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions ELECTRON_ICON_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Electron App Icon Setup

## ✅ Completed Setup

### Icon Files Created
- **macOS**: `apps/web/public/icon.icns` (526KB) - High-quality ICNS format
- **Windows**: `apps/web/public/icon.ico` (22KB) - ICO format
- **Linux**: `apps/web/public/icon.png` (76KB) - 512x512 PNG format

### Configuration Files
- **Entitlements**: `apps/web/build/entitlements.mac.plist` - macOS code signing entitlements
- **Main Process**: `apps/web/electron/main.js` - Updated with platform-specific icon loading
- **Package Config**: `apps/web/package.json` - Updated with description, author, and icon paths

### Platform-Specific Icon Loading
The Electron main process (`main.js`) now automatically selects the appropriate icon based on the platform:
- macOS: Uses `icon.icns`
- Windows: Uses `icon.ico`
- Linux: Uses `icon.png`

### Source Material
All icons were generated from `apps/web/public/Claudable_Icon.png` (1044x1044 high-quality PNG).

## Build Configuration
The `electron-builder` configuration in `package.json` is properly set up to use:
- `public/icon.icns` for macOS builds
- `public/icon.ico` for Windows builds
- `public/icon.png` for Linux builds

## Testing
- ✅ All icon files exist and are properly formatted
- ✅ Main.js has platform-specific icon configuration
- ✅ Build configuration points to correct icon paths
- ✅ Entitlements file created for macOS code signing

## Usage
To build the Electron app with icons:
```bash
npm run build:electron # Build and package
npm run dist:electron # Create distributable package
npm run dev:electron # Run in development mode
```

The app icon will now appear properly in:
- App window title bar
- Dock/taskbar
- Alt-Tab/Cmd-Tab switcher
- Packaged app bundle
157 changes: 157 additions & 0 deletions apps/api/app/api/claude_conversations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
Claude conversation folder reader
Reads conversation logs from ~/.claude folders
"""
import os
import json
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
from fastapi import APIRouter, HTTPException
import logging

logger = logging.getLogger(__name__)

router = APIRouter()


def parse_project_path(folder_name: str) -> str:
"""Convert folder name like '-Users-jkneen-Documents-GitHub-flows-Claudable' to readable path"""
# Remove leading dash and replace dashes with slashes
if folder_name.startswith('-'):
folder_name = folder_name[1:]
return '/' + folder_name.replace('-', '/')


def read_jsonl_summary(file_path: Path) -> Optional[Dict[str, Any]]:
"""Read first few lines of JSONL to get conversation summary"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = []
for i, line in enumerate(f):
if i >= 3: # Read first 3 lines max
break
try:
lines.append(json.loads(line.strip()))
except json.JSONDecodeError:
continue

if not lines:
return None

# Extract summary and first message
summary = None
first_message = None
timestamp = None

for entry in lines:
if entry.get('type') == 'summary':
summary = entry.get('summary', 'No summary')
elif entry.get('type') == 'user' and not first_message:
msg = entry.get('message', {})
if isinstance(msg, dict):
first_message = msg.get('content', '')
timestamp = entry.get('timestamp')

return {
'summary': summary or 'No summary',
'first_message': first_message or '',
'timestamp': timestamp,
'file_name': file_path.stem
}
except Exception as e:
logger.error(f"Error reading {file_path}: {e}")
return None


@router.get("/claude-conversations")
async def get_claude_conversations():
"""Get all Claude conversations from ~/.claude folders"""
conversations = {
'user': [], # Global conversations from ~/.claude
'project': [] # Project-specific conversations (if any)
}

home_dir = Path.home()

# Read global conversations from ~/.claude/projects
global_projects_dir = home_dir / '.claude' / 'projects'
if global_projects_dir.exists():
for project_folder in global_projects_dir.iterdir():
if project_folder.is_dir():
project_path = parse_project_path(project_folder.name)
project_conversations = []

# Read all JSONL files in this project folder
for jsonl_file in project_folder.glob('*.jsonl'):
conv_data = read_jsonl_summary(jsonl_file)
if conv_data:
conv_data['id'] = jsonl_file.stem
conv_data['project_path'] = project_path
project_conversations.append(conv_data)

if project_conversations:
# Sort by timestamp (newest first)
project_conversations.sort(
key=lambda x: x.get('timestamp') or '',
reverse=True
)

conversations['user'].append({
'project_path': project_path,
'project_name': Path(project_path).name,
'conversations': project_conversations[:10] # Limit to 10 most recent per project
})

# Sort projects by most recent conversation
conversations['user'].sort(
key=lambda x: x['conversations'][0]['timestamp'] if x['conversations'] and x['conversations'][0].get('timestamp') else '',
reverse=True
)

# Check current working directory for .claude folder (project-specific)
cwd = Path.cwd()
local_claude = cwd / '.claude'
if local_claude.exists():
# For now, just note that it exists
# Project-specific conversations might be stored differently
conversations['project'] = [{
'project_path': str(cwd),
'project_name': cwd.name,
'conversations': []
}]

return conversations


@router.get("/claude-conversations/{conversation_id}")
async def get_conversation_details(conversation_id: str):
"""Get full conversation details by ID"""
home_dir = Path.home()
global_projects_dir = home_dir / '.claude' / 'projects'

# Search for the conversation file
for project_folder in global_projects_dir.iterdir():
if project_folder.is_dir():
jsonl_file = project_folder / f"{conversation_id}.jsonl"
if jsonl_file.exists():
messages = []
try:
with open(jsonl_file, 'r', encoding='utf-8') as f:
for line in f:
try:
entry = json.loads(line.strip())
if entry.get('type') in ['user', 'assistant']:
messages.append(entry)
except json.JSONDecodeError:
continue

return {
'id': conversation_id,
'project_path': parse_project_path(project_folder.name),
'messages': messages
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

raise HTTPException(status_code=404, detail="Conversation not found")
2 changes: 2 additions & 0 deletions apps/api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from app.api.project_services import router as project_services_router
from app.api.github import router as github_router
from app.api.vercel import router as vercel_router
from app.api.claude_conversations import router as claude_conversations_router
from app.core.logging import configure_logging
from app.core.terminal_ui import ui
from sqlalchemy import inspect
Expand Down Expand Up @@ -65,6 +66,7 @@ async def dispatch(self, request: Request, call_next):
app.include_router(project_services_router) # Project services API
app.include_router(github_router) # GitHub integration API
app.include_router(vercel_router) # Vercel integration API
app.include_router(claude_conversations_router, prefix="/api") # Claude conversation folder reader


@app.get("/health")
Expand Down
12 changes: 6 additions & 6 deletions apps/web/app/[project_id]/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1648,19 +1648,19 @@ export default function ChatPage({ params }: Params) {
</div>
)}

{deploymentStatus === 'ready' && publishedUrl && (
{deploymentStatus === 'ready' && publishedUrl ? (
<div className="mb-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<p className="text-sm font-medium text-green-700 dark:text-green-400 mb-2">Currently published at:</p>
<a
href={publishedUrl}
href={publishedUrl ?? undefined}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-green-600 dark:text-green-300 font-mono hover:underline break-all"
>
{publishedUrl}
</a>
</div>
)}
) : null}

{deploymentStatus === 'error' && (
<div className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
Expand Down Expand Up @@ -2228,11 +2228,11 @@ export default function ChatPage({ params }: Params) {
</div>
)}

{deploymentStatus === 'ready' && publishedUrl && (
{deploymentStatus === 'ready' && publishedUrl ? (
<div className="p-4 rounded-xl border border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20">
<p className="text-sm font-medium text-emerald-700 dark:text-emerald-400 mb-2">Published successfully</p>
<div className="flex items-center gap-2">
<a href={publishedUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-emerald-700 dark:text-emerald-300 underline break-all flex-1">
<a href={publishedUrl ?? undefined} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-emerald-700 dark:text-emerald-300 underline break-all flex-1">
{publishedUrl}
</a>
<button
Expand All @@ -2243,7 +2243,7 @@ export default function ChatPage({ params }: Params) {
</button>
</div>
</div>
)}
) : null}

{deploymentStatus === 'error' && (
<div className="p-4 rounded-xl border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
Expand Down
Loading