Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6f484ef
Merge pull request #7 from opactorai/fix/cursor-system-prompt-path
tachyon6 Aug 22, 2025
03d8865
add star history
Atipico1 Aug 23, 2025
64b47d5
docs: Add links to OPACTOR website and Twitter profile in README
tachyon6 Aug 23, 2025
f9a2362
docs: Update Twitter badge color in README
tachyon6 Aug 23, 2025
13baad6
edit readme
Atipico1 Aug 25, 2025
9b32392
fix: Remove invisible backspace character from Claudable_logo.png fil…
tachyon6 Aug 25, 2025
87f33eb
feat: Add comprehensive Codex CLI integration to unified manager
Aug 27, 2025
ea11b35
feat: Enhance project cleanup with robust process termination
Aug 27, 2025
6789488
feat: Add image handling and CLI preferences API improvements
Aug 27, 2025
7e97c4e
feat: Add comprehensive image upload support to useChat hook
Aug 27, 2025
8041e95
refactor: Improve chat components for image attachment support
Aug 27, 2025
359d522
feat: Add image thumbnail display in chat messages
Aug 27, 2025
71a73a0
fix: Properly handle images in main page project creation flow
Aug 27, 2025
0d64446
refactor: Improve chat page image handling and runAct function
Aug 27, 2025
98a5e99
feat: Update CLI selection components and type definitions
Aug 27, 2025
45605e5
docs: Update system prompt for enhanced capabilities
Aug 27, 2025
482c48b
feat: Add new OAI image assets for web and assets directory
Aug 27, 2025
2c627be
feat: Add Codex CLI support and improve CLI status checks
Aug 27, 2025
89d1265
feat: Update CLI error messages and add new assets
tachyon6 Aug 29, 2025
6b880e4
feat: Refactor CLI management and enhance SQLite migration support
Aug 29, 2025
8e34b8a
feat: Add Qwen Coder and Gemini CLI support
Aug 29, 2025
4ea73d9
feat: Add new image assets for Qwen and Gemini
Aug 29, 2025
bf20a04
feat: Integrate global settings context and update components
tachyon6 Aug 29, 2025
aaa1122
feat: Add Qwen and Gemini support in various components
tachyon6 Aug 29, 2025
f7a9a70
feat: Update Qwen Coder references and enhance CLI integration
Aug 29, 2025
8045acb
feat: Enhance UI with new assistant brand colors and typography
tachyon6 Aug 30, 2025
f78cde7
feat: Introduce ThinkingSection component to enhance chat log functio…
tachyon6 Aug 30, 2025
0d87636
feat: Enhance session management and model selection in HomePage and …
tachyon6 Aug 30, 2025
2338db7
feat: Aggiornamenti a package.json e package-lock.json per nuove dipe…
r3vs Aug 30, 2025
915d9d8
feat: Aggiunta di supporto per la terminazione dei processi su Window…
r3vs Aug 30, 2025
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
<a href="https://discord.gg/NJNbafHNQC">
<img src="https://img.shields.io/badge/Discord-Join%20Community-7289da?style=flat&logo=discord&logoColor=white" alt="Join Discord Community">
</a>
<a href="https://opactor.ai">
<img src="https://img.shields.io/badge/OPACTOR-Website-000000?style=flat&logo=web&logoColor=white" alt="OPACTOR Website">
</a>
<a href="https://twitter.com/aaron_xong">
<img src="https://img.shields.io/badge/Follow-@aaron__xong-000000?style=flat&logo=x&logoColor=white" alt="Follow Aaron">
</a>
</p>

## What is Claudable?
Expand Down Expand Up @@ -224,4 +230,19 @@ If you encounter the error: `Error output dangerously skip permissions cannot be

## License

MIT License.
MIT License.

## Upcoming Features
These features are in development and will be opened soon.
- **New CLI Agents** - Trust us, you're going to LOVE this!
- **Checkpoints for Chat** - Save and restore conversation/codebase states
- **Advanced MCP Integration** - Native integration with MCP
- **Enhanced Agent System** - Subagents, AGENTS.md integration
- **Website Cloning** - You can start a project from a reference URL.
- Various bug fixes and community PR merges

We're working hard to deliver the features you've been asking for. Stay tuned!

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=opactorai/Claudable&type=Date)](https://www.star-history.com/#opactorai/Claudable&Date)
21 changes: 21 additions & 0 deletions apps/api/app/api/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ async def upload_logo(project_id: str, body: LogoRequest, db: Session = Depends(
return {"path": f"assets/logo.png"}


@router.get("/{project_id}/{filename}")
async def get_image(project_id: str, filename: str, db: Session = Depends(get_db)):
"""Get an image file from project assets directory"""
from fastapi.responses import FileResponse

# Verify project exists
row = db.get(ProjectModel, project_id)
if not row:
raise HTTPException(status_code=404, detail="Project not found")

# Build file path
file_path = os.path.join(settings.projects_root, project_id, "assets", filename)

# Check if file exists
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Image not found")

# Return the image file
return FileResponse(file_path)


@router.post("/{project_id}/upload")
async def upload_image(project_id: str, file: UploadFile = File(...), db: Session = Depends(get_db)):
"""Upload an image file to project assets directory"""
Expand Down
142 changes: 129 additions & 13 deletions apps/api/app/api/chat/act.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from app.models.sessions import Session as ChatSession
from app.models.commits import Commit
from app.models.user_requests import UserRequest
from app.services.cli.unified_manager import UnifiedCLIManager, CLIType
from app.services.cli.unified_manager import UnifiedCLIManager
from app.services.cli.base import CLIType
from app.services.git_ops import commit_all
from app.core.websocket.manager import manager
from app.core.terminal_ui import ui
Expand All @@ -27,7 +28,9 @@

class ImageAttachment(BaseModel):
name: str
base64_data: str
# Either base64_data or path must be provided
base64_data: Optional[str] = None
path: Optional[str] = None # Absolute path to image file
mime_type: str = "image/jpeg"


Expand Down Expand Up @@ -156,11 +159,14 @@ async def execute_chat_task(
db=db
)

# Qwen Coder does not support images yet; drop them to prevent errors
safe_images = [] if cli_preference == CLIType.QWEN else images

result = await cli_manager.execute_instruction(
instruction=instruction,
cli_type=cli_preference,
fallback_enabled=project_fallback_enabled,
images=images,
images=safe_images,
model=project_selected_model,
is_initial_prompt=is_initial_prompt
)
Expand Down Expand Up @@ -318,11 +324,14 @@ async def execute_act_task(
db=db
)

# Qwen Coder does not support images yet; drop them to prevent errors
safe_images = [] if cli_preference == CLIType.QWEN else images

result = await cli_manager.execute_instruction(
instruction=instruction,
cli_type=cli_preference,
fallback_enabled=project_fallback_enabled,
images=images,
images=safe_images,
model=project_selected_model,
is_initial_prompt=is_initial_prompt
)
Expand Down Expand Up @@ -511,23 +520,94 @@ async def run_act(
ui.error(f"Project {project_id} not found", "ACT API")
raise HTTPException(status_code=404, detail="Project not found")

# Check if project is still initializing
if project.status == "initializing":
ui.error(f"Project {project_id} is still initializing", "ACT API")
raise HTTPException(status_code=400, detail="Project is still initializing. Please wait for initialization to complete.")

# Check if project has a valid repo_path
if not project.repo_path:
ui.error(f"Project {project_id} repository path not available", "ACT API")
raise HTTPException(status_code=400, detail="Project repository is not ready. Please wait for initialization to complete.")

# Determine CLI preference
cli_preference = CLIType(body.cli_preference or project.preferred_cli)
fallback_enabled = body.fallback_enabled if body.fallback_enabled is not None else project.fallback_enabled
conversation_id = body.conversation_id or str(uuid.uuid4())

# Save user instruction as message
# 🔍 DEBUG: Log incoming request data
print(f"📥 ACT Request - Project: {project_id}")
print(f"📥 Instruction: {body.instruction[:100]}...")
print(f"📥 Images count: {len(body.images)}")
print(f"📥 Images data: {body.images}")
for i, img in enumerate(body.images):
print(f"📥 Image {i+1}: {img}")
if hasattr(img, '__dict__'):
print(f"📥 Image {i+1} dict: {img.__dict__}")

# Extract image paths and build attachments for metadata/WS
image_paths = []
attachments = []
import os as _os

print(f"🔍 Processing {len(body.images)} images...")
for i, img in enumerate(body.images):
print(f"🔍 Processing image {i+1}: {img}")

img_dict = img if isinstance(img, dict) else img.__dict__ if hasattr(img, '__dict__') else {}
print(f"🔍 Image {i+1} converted to dict: {img_dict}")

p = img_dict.get('path')
n = img_dict.get('name')
print(f"🔍 Image {i+1} - path: {p}, name: {n}")

if p:
print(f"🔍 Adding path to image_paths: {p}")
image_paths.append(p)
try:
fname = _os.path.basename(p)
print(f"🔍 Processing path: {p}")
print(f"🔍 Extracted filename: {fname}")
if fname and fname.strip():
attachment = {
"name": n or fname,
"url": f"/api/assets/{project_id}/{fname}"
}
print(f"🔍 Created attachment: {attachment}")
attachments.append(attachment)
else:
print(f"❌ Failed to extract filename from: {p}")
except Exception as e:
print(f"❌ Exception processing path {p}: {e}")
pass
elif n:
print(f"🔍 Adding name to image_paths: {n}")
image_paths.append(n)
else:
print(f"❌ Image {i+1} has neither path nor name!")

print(f"🔍 Final image_paths: {image_paths}")
print(f"🔍 Final attachments: {attachments}")

# Save user instruction as message (with image paths in content for display)
message_content = body.instruction
if image_paths:
image_refs = [f"Image #{i+1} path: {path}" for i, path in enumerate(image_paths)]
message_content = f"{body.instruction}\n\n{chr(10).join(image_refs)}"

user_message = Message(
id=str(uuid.uuid4()),
project_id=project_id,
role="user",
message_type="chat",
content=body.instruction,
content=message_content,
metadata_json={
"type": "act_instruction",
"cli_preference": cli_preference.value,
"fallback_enabled": fallback_enabled,
"has_images": len(body.images) > 0
"has_images": len(body.images) > 0,
"image_paths": image_paths,
"attachments": attachments
},
conversation_id=conversation_id,
created_at=datetime.utcnow()
Expand Down Expand Up @@ -572,7 +652,7 @@ async def run_act(
"id": user_message.id,
"role": "user",
"message_type": "chat",
"content": body.instruction,
"content": message_content,
"metadata_json": user_message.metadata_json,
"parent_message_id": None,
"session_id": session.id,
Expand Down Expand Up @@ -636,18 +716,54 @@ async def run_chat(
fallback_enabled = body.fallback_enabled if body.fallback_enabled is not None else project.fallback_enabled
conversation_id = body.conversation_id or str(uuid.uuid4())

# Save user instruction as message
# Extract image paths and build attachments for metadata/WS
image_paths = []
attachments = []
import os as _os2
for img in body.images:
img_dict = img if isinstance(img, dict) else img.__dict__ if hasattr(img, '__dict__') else {}
p = img_dict.get('path')
n = img_dict.get('name')
if p:
image_paths.append(p)
try:
fname = _os2.path.basename(p)
print(f"🔍 [CHAT] Processing path: {p}")
print(f"🔍 [CHAT] Extracted filename: {fname}")
if fname and fname.strip():
attachment = {
"name": n or fname,
"url": f"/api/assets/{project_id}/{fname}"
}
print(f"🔍 [CHAT] Created attachment: {attachment}")
attachments.append(attachment)
else:
print(f"❌ [CHAT] Failed to extract filename from: {p}")
except Exception as e:
print(f"❌ [CHAT] Exception processing path {p}: {e}")
pass
elif n:
image_paths.append(n)

# Save user instruction as message (with image paths in content for display)
message_content = body.instruction
if image_paths:
image_refs = [f"Image #{i+1} path: {path}" for i, path in enumerate(image_paths)]
message_content = f"{body.instruction}\n\n{chr(10).join(image_refs)}"

user_message = Message(
id=str(uuid.uuid4()),
project_id=project_id,
role="user",
message_type="chat",
content=body.instruction,
content=message_content,
metadata_json={
"type": "chat_instruction",
"cli_preference": cli_preference.value,
"fallback_enabled": fallback_enabled,
"has_images": len(body.images) > 0
"has_images": len(body.images) > 0,
"image_paths": image_paths,
"attachments": attachments
},
conversation_id=conversation_id,
created_at=datetime.utcnow()
Expand Down Expand Up @@ -679,7 +795,7 @@ async def run_chat(
"id": user_message.id,
"role": "user",
"message_type": "chat",
"content": body.instruction,
"content": message_content,
"metadata_json": user_message.metadata_json,
"parent_message_id": None,
"session_id": session.id,
Expand Down Expand Up @@ -719,4 +835,4 @@ async def run_chat(
conversation_id=conversation_id,
status="running",
message="Chat execution started"
)
)
59 changes: 36 additions & 23 deletions apps/api/app/api/chat/cli_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

from app.api.deps import get_db
from app.models.projects import Project
from app.services.cli import UnifiedCLIManager, CLIType
from app.services.cli import UnifiedCLIManager
from app.services.cli.base import CLIType


router = APIRouter()
Expand All @@ -36,6 +37,9 @@ class CLIStatusResponse(BaseModel):
class AllCLIStatusResponse(BaseModel):
claude: CLIStatusResponse
cursor: CLIStatusResponse
codex: CLIStatusResponse
qwen: CLIStatusResponse
gemini: CLIStatusResponse
preferred_cli: str


Expand Down Expand Up @@ -164,28 +168,37 @@ async def get_all_cli_status(project_id: str, db: Session = Depends(get_db)):
if not project:
raise HTTPException(status_code=404, detail="Project not found")

# For now, return mock status data to avoid CLI manager issues
preferred_cli = getattr(project, 'preferred_cli', 'claude')

# Create mock status responses
claude_status = CLIStatusResponse(
cli_type="claude",
available=True,
configured=True,
error=None,
models=["claude-3.5-sonnet", "claude-3-opus"]
)

cursor_status = CLIStatusResponse(
cli_type="cursor",
available=False,
configured=False,
error="Not configured",
models=[]

# Build real status for each CLI using UnifiedCLIManager
manager = UnifiedCLIManager(
project_id=project.id,
project_path=project.repo_path,
session_id="status_check",
conversation_id="status_check",
db=db,
)


def to_resp(cli_key: str, status: Dict[str, Any]) -> CLIStatusResponse:
return CLIStatusResponse(
cli_type=cli_key,
available=status.get("available", False),
configured=status.get("configured", False),
error=status.get("error"),
models=status.get("models"),
)

claude_status = await manager.check_cli_status(CLIType.CLAUDE)
cursor_status = await manager.check_cli_status(CLIType.CURSOR)
codex_status = await manager.check_cli_status(CLIType.CODEX)
qwen_status = await manager.check_cli_status(CLIType.QWEN)
gemini_status = await manager.check_cli_status(CLIType.GEMINI)

return AllCLIStatusResponse(
claude=claude_status,
cursor=cursor_status,
preferred_cli=preferred_cli
)
claude=to_resp("claude", claude_status),
cursor=to_resp("cursor", cursor_status),
codex=to_resp("codex", codex_status),
qwen=to_resp("qwen", qwen_status),
gemini=to_resp("gemini", gemini_status),
preferred_cli=preferred_cli,
)
6 changes: 4 additions & 2 deletions apps/api/app/api/projects/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@ async def get_project(project_id: str, db: Session = Depends(get_db)) -> Project
features=ai_info.get('features'),
tech_stack=ai_info.get('tech_stack'),
ai_generated=ai_info.get('ai_generated', False),
initial_prompt=project.initial_prompt
initial_prompt=project.initial_prompt,
preferred_cli=project.preferred_cli,
selected_model=project.selected_model
)
except HTTPException:
raise
Expand Down Expand Up @@ -484,4 +486,4 @@ async def delete_project(project_id: str, db: Session = Depends(get_db)):
print(f"❌ Error cleaning up project files for {project_id}: {e}")
# Don't fail the whole operation if file cleanup fails

return {"message": f"Project {project_id} deleted successfully"}
return {"message": f"Project {project_id} deleted successfully"}
Loading