Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
nosetests.xml
ui/playwright-report/

# mypy
.mypy_cache/
Expand Down Expand Up @@ -142,3 +143,4 @@ Pipfile.lock
.tmp/
.temp/
tmpclaude-*-cwd
ui/test-results/
8 changes: 7 additions & 1 deletion server/routers/assistant_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
data = await websocket.receive_text()
message = json.loads(data)
msg_type = message.get("type")
logger.info(f"Assistant received message type: {msg_type}")
logger.debug(f"Assistant received message type: {msg_type}")

if msg_type == "ping":
await websocket.send_json({"type": "pong"})
Expand All @@ -269,18 +269,24 @@ async def assistant_chat_websocket(websocket: WebSocket, project_name: str):
elif msg_type == "start":
# Get optional conversation_id to resume
conversation_id = message.get("conversation_id")
logger.debug(f"Processing start message with conversation_id={conversation_id}")

try:
# Create a new session
logger.debug(f"Creating session for {project_name}")
session = await create_session(
project_name,
project_dir,
conversation_id=conversation_id,
)
logger.debug("Session created, starting...")

# Stream the initial greeting
async for chunk in session.start():
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"Sending chunk: {chunk.get('type')}")
await websocket.send_json(chunk)
logger.debug("Session start complete")
except Exception as e:
logger.exception(f"Error starting assistant session for {project_name}")
await websocket.send_json({
Expand Down
63 changes: 52 additions & 11 deletions server/services/assistant_chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .assistant_database import (
add_message,
create_conversation,
get_messages,
)

# Load environment variables from .env file if present
Expand Down Expand Up @@ -178,6 +179,7 @@ def __init__(self, project_name: str, project_dir: Path, conversation_id: Option
self.client: Optional[ClaudeSDKClient] = None
self._client_entered: bool = False
self.created_at = datetime.now()
self._history_loaded: bool = False # Track if we've loaded history for resumed conversations

async def close(self) -> None:
"""Clean up resources and close the Claude client."""
Expand All @@ -195,10 +197,14 @@ async def start(self) -> AsyncGenerator[dict, None]:
Initialize session with the Claude client.

Creates a new conversation if none exists, then sends an initial greeting.
For resumed conversations, skips the greeting since history is loaded from DB.
Yields message chunks as they stream in.
"""
# Track if this is a new conversation (for greeting decision)
is_new_conversation = self.conversation_id is None

# Create a new conversation if we don't have one
if self.conversation_id is None:
if is_new_conversation:
conv = create_conversation(self.project_dir, self.project_name)
self.conversation_id = conv.id
yield {"type": "conversation_created", "conversation_id": self.conversation_id}
Expand Down Expand Up @@ -260,6 +266,7 @@ async def start(self) -> AsyncGenerator[dict, None]:
model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101")

try:
logger.info("Creating ClaudeSDKClient...")
self.client = ClaudeSDKClient(
options=ClaudeAgentOptions(
model=model,
Expand All @@ -276,25 +283,35 @@ async def start(self) -> AsyncGenerator[dict, None]:
env=sdk_env,
)
)
logger.info("Entering Claude client context...")
await self.client.__aenter__()
self._client_entered = True
logger.info("Claude client ready")
except Exception as e:
logger.exception("Failed to create Claude client")
yield {"type": "error", "content": f"Failed to initialize assistant: {str(e)}"}
return

# Send initial greeting
try:
greeting = f"Hello! I'm your project assistant for **{self.project_name}**. I can help you understand the codebase, explain features, and answer questions about the project. What would you like to know?"
# Send initial greeting only for NEW conversations
# Resumed conversations already have history loaded from the database
if is_new_conversation:
# New conversations don't need history loading
self._history_loaded = True
try:
greeting = f"Hello! I'm your project assistant for **{self.project_name}**. I can help you understand the codebase, explain features, and answer questions about the project. What would you like to know?"

# Store the greeting in the database
add_message(self.project_dir, self.conversation_id, "assistant", greeting)
# Store the greeting in the database
add_message(self.project_dir, self.conversation_id, "assistant", greeting)

yield {"type": "text", "content": greeting}
yield {"type": "text", "content": greeting}
yield {"type": "response_done"}
except Exception as e:
logger.exception("Failed to send greeting")
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}
else:
# For resumed conversations, history will be loaded on first message
# _history_loaded stays False so send_message() will include history
yield {"type": "response_done"}
except Exception as e:
logger.exception("Failed to send greeting")
yield {"type": "error", "content": f"Failed to start conversation: {str(e)}"}

async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]:
"""
Expand All @@ -321,8 +338,32 @@ async def send_message(self, user_message: str) -> AsyncGenerator[dict, None]:
# Store user message in database
add_message(self.project_dir, self.conversation_id, "user", user_message)

# For resumed conversations, include history context in first message
message_to_send = user_message
if not self._history_loaded:
self._history_loaded = True
history = get_messages(self.project_dir, self.conversation_id)
# Exclude the message we just added (last one)
history = history[:-1] if history else []
# Cap history to last 35 messages to prevent context overload
history = history[-35:] if len(history) > 35 else history
if history:
# Format history as context for Claude
history_lines = ["[Previous conversation history for context:]"]
for msg in history:
role = "User" if msg["role"] == "user" else "Assistant"
content = msg["content"]
# Truncate very long messages
if len(content) > 500:
content = content[:500] + "..."
history_lines.append(f"{role}: {content}")
history_lines.append("[End of history. Continue the conversation:]")
history_lines.append(f"User: {user_message}")
message_to_send = "\n".join(history_lines)
logger.info(f"Loaded {len(history)} messages from conversation history")

try:
async for chunk in self._query_claude(user_message):
async for chunk in self._query_claude(message_to_send):
yield chunk
yield {"type": "response_done"}
except Exception as e:
Expand Down
Loading