workspace/ files existed on disk but were not included in previous
incremental commit, causing git to record them as deleted. Re-adding
all workspace card components, AgentWorkspace, ActivityTimeline, and
WorkspaceCardRenderer to properly track them.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
## Fixes
- [#3] Status events now add/update a status ActivityNode in timeline (upsertWsStatusNode)
- [#4] Done callback adds done node; error callback adds error node + sets session.status="error"
- [#5] handleRegenerate fully synced with workspace: status/tool/card/done/error all handled
- [#1][#2] localStorage persistence: completed/error sessions auto-saved, lazy-loaded on demand
- [#1] handleSelectConversation preloads workspace sessions from localStorage for history messages
- Refactored completeWsSession to include done ActivityNode in timeline
- Added errorWsSession, upsertWsStatusNode, saveWsToStorage, loadWsFromStorage helpers
## Known limitation
- [#7] workspace_card merge:true not yet used by backend (all cards are append-only for now)
- History workspace recovery depends on localStorage (browser-local, not cross-device)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused the same AI response to appear twice:
1. handleSelectConversation would fetch conversation history even while
a stream was in progress (isLoading). If the fetch resolved mid-stream,
fetchConversation overwrote the conversation messages unconditionally,
writing the server-persisted AI message (with a real UUID) alongside the
still-streaming client-side message (with a temp ID), producing a duplicate.
Fix: guard the fetch with !isLoading, and add a second check inside the
.then callback so setConversations only writes history if the messages
array is still empty (protects against slow-resolving network requests).
2. onDone in streamChat could theoretically be called a second time if the
ReadableStream completed without a "done" SSE event. Fix: call
reader.cancel() immediately after onDone so the while loop exits cleanly.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add _extract_output_str() to unwrap MCP ToolMessage content objects
instead of calling str() on the raw object
- Handle read_url url param when LLM passes a list instead of a string
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add langchain-community dependency for GoogleSerperAPIWrapper
- Add serper_api_key to config with default key
- Create app/tools/serper.py with async serper_search tool
- Register "serper" key in tools/__init__.py (independent from "search"/Jina MCP)
- Add tool title and input/output summaries in chat.py
- Set SERPER_API_KEY on Azure App Service (Operation resource group)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add has_tool_activity flag to distinguish first agent pass (thinking)
from post-tool agent pass (generating)
- Replace on_chain_start debug logging with status SSE emission
- Filter to only graph-level agent nodes via graph:step: tag prefix
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- tool_start/tool_end/tool_error SSE payloads now include call_id field
derived from LangGraph run_id for reliable tool event correlation
- tool_start_ts dict keyed by call_id instead of tool_name to handle
concurrent calls to the same tool
- Added on_chain_start debug logging to observe chain names and metadata
(no SSE emission yet, observation only)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add CoT execution trace to the SSE stream: status events for stage
transitions, enriched tool_start with input_summary, tool_end with
output_summary and duration_ms, and tool_error for failed tool calls.
Helper functions _sse, _summarize_input, _summarize_output provide
human-readable summaries for each tool type.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace the three pill buttons (Auto/Flash/Pro) in GeminiInput with a
single shadcn/ui Select component. Selection logic and onSelectedModelChange
callback are unchanged; only the UI widget is swapped.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When the frontend passes a conversation_id that does not exist in the
conversations table (e.g. new conversation before first message, or
invalid ID), the INSERT into attachments failed with
ForeignKeyViolationError.
Now upload_attachment checks session.get(Conversation, conversation_id)
before inserting. If the conversation does not exist, conversation_id is
set to None and the attachment is stored as unlinked (blob path uses
_unlinked/ prefix). This avoids the FK constraint error while keeping
the file safely uploaded.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When a tool call crashes (e.g. httpx timeout in kb_search), the LangGraph
checkpoint retains an AIMessage with tool_calls but no corresponding
ToolMessage. Subsequent requests to the same conversation_id fail with:
ValueError: Found AIMessages with tool_calls that do not have a
corresponding ToolMessage
Now the except block in _stream_response detects this specific ValueError
by checking for "tool_calls" and "ToolMessage" in the error string, then
calls checkpointer.adelete_thread() to purge the corrupted thread state.
The frontend receives {"type":"error","content":"对话状态异常,已自动重置..."}
followed by {"type":"done"}, so the user can simply resend their message.
API confirmed: AsyncPostgresSaver.adelete_thread(thread_id) deletes from
checkpoints, checkpoint_blobs, and checkpoint_writes tables for the thread.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
KB Agent runs on Azure App Service which cold-starts after idle periods,
taking 20-30s to respond. The previous 15s timeout caused httpx.ReadTimeout
that crashed the ReAct agent tool node and killed the SSE stream.
Changes:
- Increase kb_agent_search_timeout_sec default from 15 to 30 in config.py
- Add single retry with 2s backoff on ReadTimeout in kb_search tool
- Catch all exceptions and return friendly Chinese error messages instead
of propagating exceptions to the agent loop
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove menu close from onMouseLeave (was closing before user reaches dropdown)
- Add global document click listener to close menu when clicking outside
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Bug 1 - Sidebar MoreHorizontal now opens a dropdown menu:
- Click three-dot icon to toggle rename/delete dropdown
- Rename: inline text input with Enter to confirm, Escape to cancel
- Delete: removes from list, calls DELETE /api/conversations/{id}
- Rename calls PATCH /api/conversations/{id} with new title
- Both use optimistic local state updates
Bug 2 - File upload chip buttons now respond to clicks:
- Added e.stopPropagation() to X (remove) and retry buttons
- Prevents click from being swallowed by parent elements
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add return_dto=None to delete_attachment decorator to fix Litestar
ImproperlyConfiguredException on 204 status code with None return type
- Catch BaseException instead of Exception in create_tables() to handle
anyio ExceptionGroup from concurrent gunicorn workers
- Enable debug=True temporarily to capture detailed error traces
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
When 2 gunicorn workers start simultaneously, both call create_all()
which races on CREATE TABLE attachments. The second worker hits a
PostgreSQL UniqueViolation on pg_type_typname_nsp_index because the
type already exists. Wrap create_tables() in try/except to handle
this gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Litestar validates that handlers with 204 status codes cannot return
a response body. The delete_attachment handler was declared as
-> Response and returned Response(content=None, status_code=204),
which caused ImproperlyConfiguredException at startup and crashed
all workers. Change return type to None and move status_code to the
@delete decorator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change FormData field name from "file" to "data" (backend expects "data")
- Pass conversation_id as query parameter instead of form field
- Add content_type and created_at to AttachmentData interface to match
backend response shape
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- lib/api.ts: add AttachmentData interface, uploadAttachment() function,
and getAttachmentDownloadUrl() helper for upcoming /api/attachments endpoint
- GeminiInput.tsx: wire + button to trigger file selection with upload
state management (uploading/done/error), file preview chips above
textarea with retry and remove controls, pass AttachmentData to parent
on submit
- GeminiMessage.tsx: add attachments field to Message interface, render
attachment list (filename + size + download link) below both user and
assistant message content
- GeminiChat.tsx: accept AttachmentData[] in handleSend, store attachments
on user messages for display
Backend /api/attachments endpoints not yet live — frontend is ready for
integration once backend agent completes the upload/download API.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add Attachment ORM model (id, conversation_id, message_id, filename, content_type, blob_url, size_bytes, created_at) with FK to conversations
- Add POST /api/attachments/upload (multipart, 50MB limit), GET /api/attachments/{id}, GET /api/attachments/{id}/download (302 to SAS URL), DELETE /api/attachments/{id}
- Add delete_blob() and generate_sas_url() to storage/blob.py for download redirect and cleanup
- Dispatch parse_attachment task to Service Bus for parseable types (PDF, images, CSV, DOCX, XLSX)
- Pre-create blob container on startup (best-effort)
- Add AttachmentOut schema and full API docs in doc/api.md
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove selected-tool chip/tag area above the input textarea; tool
selection state is now shown only via button highlight in the tools panel
- Add "Auto" as a third model option between Flash and Pro (default)
- Auto maps to "flash" when sent to the backend (which only accepts
flash|pro)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Restore Flash/Pro dual model buttons in GeminiInput (they control
speed/quality mode sent to backend, not model name)
- Restore selectedModel/onSelectedModelChange props in GeminiInput
- Reset default model back to "pro" in GeminiChat
- Replace Gemini model dropdown in GeminiTopbar with static "GPT-5.4"
label to show current model name
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The "two diamond" bug was caused by GeminiTypingIndicator (which has its
own gem icon) rendering simultaneously with GeminiMessage (also has a gem
icon) once the first token arrived. Now the typing indicator only shows
while isLoading is true AND no assistant message exists yet (last message
is not role "assistant").
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Replace Flash/Pro dual model buttons with single "gpt-5.4" label
- Backend still receives "flash" as model param (matches schema validation)
- Remove unused selectedModel/onSelectedModelChange props from GeminiInput
- Fix SSE stream parsing: process remaining buffer data when stream ends
without trailing newline (potential cause of "no return" issue)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add fetchTicketSummary() and TicketSummaryData type to lib/api.ts
- Change TicketSummary props from tickets[] array to summary object
with total, by_status, and by_priority fields
- Update GeminiChat to fetch summary data instead of ticket list
- TicketSummary now renders status and priority breakdowns from
the summary endpoint instead of computing from raw ticket data
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Litestar's {ticket_id:uuid} path parameter requires the handler
parameter to be typed as UUID, not str. This caused a 400 validation
error on GET /api/tickets/{uuid}.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
These packages were listed locally but never committed, causing
ModuleNotFoundError: No module named 'azure' on Azure deployment.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The app.storage and app.tasks packages were never committed to git,
causing ModuleNotFoundError on Azure deployment. Also adds the
document and sandbox tool modules with their config fields.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Wire next-themes ThemeProvider in layout with class strategy, defaultTheme=dark
- Define all Gemini colors as CSS custom properties (--gem-*) in globals.css
with light (:root) and dark (.dark) variants
- Replace all hardcoded hex colors across 7 Gemini components with var() refs
- Add Sun/Moon toggle button in GeminiTopbar (right side, before Settings)
- Theme persists to localStorage via next-themes (key: gem-theme)
- Dark theme renders identically to before (same hex values)
- Light theme: white bg, light gray surfaces, same blue-purple accent gradient
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Instead of pre-creating an empty placeholder assistant message when
streaming starts (which races with GeminiTypingIndicator rendering),
wait until the first SSE token arrives to create the message. This
guarantees only one row is ever visible: either the typing indicator
(before tokens) or the GeminiMessage (after first token).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
{ticket_id:str} was matching /api/tickets/summary before the literal route.
Changing to {ticket_id:uuid} restricts matching to UUID format only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace module-level _current_model global with contextvars.ContextVar
to prevent concurrent requests from overwriting each other's search
strategy (flash vs pro)
- Add GET /api/tickets/summary returning {total, by_status, by_priority}
aggregated from Gongdan API, registered before parameterized ticket routes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip rendering the empty placeholder assistant message while isLoading
is true — the GeminiTypingIndicator already shows the gem icon with
bouncing dots. Once tokens arrive and content is non-empty, the message
renders normally via GeminiMessage.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
FCR-1: Set ignoreBuildErrors to false in next.config.mjs
FCR-2: Default API_URL to production backend so static builds work correctly;
.env.local overrides to localhost for local dev
FCR-3: Remove createConversation call in handleSend; generate UUID client-side
and let backend streamChat auto-create the conversation to avoid duplicate writes
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Create deploy-frontend.yml workflow using Azure/static-web-apps-deploy@v1.
Add output: 'export' to next.config.mjs for static site generation.
Static Web App created in Operation resource group (soc-frontend, Free tier).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add frontend/lib/api.ts with typed API client for conversations, tickets, and SSE chat streaming
- Replace simulateAIResponse() with real SSE streaming via POST /api/chat/stream
- Replace MOCK_CONVERSATIONS with GET /api/conversations
- Replace MOCK_TICKETS with GET /api/tickets
- Lift activeTools and selectedModel state from GeminiInput to GeminiChat for API integration
- Load conversation messages on demand via GET /api/conversations/{id}
- Backend URL configured via NEXT_PUBLIC_API_URL in .env.local
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ticketNumber (e.g. TK-2026-296691) was being used as id, causing
GET /api/tickets/{id} to 404 on upstream Gongdan API which expects UUID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Azure Oryx could not find packages installed to .python_packages/.
Add a startup.sh script that sets PYTHONPATH to include the bundled
package directory before launching gunicorn. Update workflow to set
the Web App startup command to use startup.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous workflow installed dependencies with pip --target and zipped
them into the deployment package. Azure Oryx runtime could not find
the packages because it looks for antenv/ or __oryx_packages__/, not
.python_packages/. Switch to deploying source-only zip with az webapp
deploy, letting SCM_DO_BUILD_DURING_DEPLOYMENT=true trigger Oryx to
run pip install on the server and create the antenv virtualenv.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>