Beside the panel (lg and up) it still covers only the chat column. Below
lg the panel is a fixed z-40 sheet, so the overlay covers the window at
z-[45] (under the z-50 mobile drawer) instead of being hidden by it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pendingImage becomes pendingAttachment (image or PDF/TXT/MD/CSV), uploaded
through POST /api/conversation/upload and sent as image_key or file_key.
- MessageInput shows an icon/name/size chip for files, an inline error for
unsupported or oversized files, and a paperclip picker.
- DropOverlay covers the chat area with "Drop to attach" while files are
dragged over the window; drags without files are ignored.
- QuestionBubble renders a saved trailing "📎 name" as a chip.
- test-fixtures/cowork/mock-server.mjs: dependency-free mock API (static
dist/, conversations, contract-shaped SSE, uploads, artifacts) for
Playwright runs without Docker or OIDC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One mock server now serves both the timeline scenarios and the artifact
panel's: per-kind fixtures from artifacts/<kind>.json, an "everything"
conversation with one saved tool message per kind, keyword scenarios
(recipe, budget, trend, philz, everything, broken, mystery, ...), a
500ing "broken" artifact, and small delays on /api/artifacts* so the
panel's loading states are visible. SPENDING now uses positive amounts
like the backend.
Also: renderers reuse ArtifactSourceRef, and the collapsed rail's
buttons say "Show <title>" so they don't share the timeline chip's
"Open <title>" accessible name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workers A/B are merged, so match what blueprints/ynab/tools.py and
conversation/agents.py actually emit:
- ynab.spending_report rows and ynab.summary.spent are positive for
spending; bars use the positive amount and a net refund (negative
row) sorts last with no bar.
- ynab.transactions start_date may be null (open-ended search); the
header reads "Through Oct 12, 2026". Amounts stay signed with
outflows as −$, and total is the net.
- Recipe image_url is never sent, search.documents has no source_ref,
memo/age_of_money/goal_target are optional; fixtures now match
(backend titles, ordering, float quantities, no empty fields).
- Mock server: drop the placeholder recipe image, positive month
spending, and a "philz" open-ended transactions scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-hand pane (440px, lg and up) with tabs, collapse rail and a
library of the conversation's artifacts; a bottom sheet below lg.
useArtifactPanel holds tabs/active and caches fetched artifacts by id;
tabs reset on conversation change.
Artifacts from tool_end auto-open on windows >= 1280px (newest wins,
and a panel the user collapsed stays collapsed). ChatScreen exposes
openArtifact for the activity timeline's onOpenArtifact and adds an
Artifacts button to the chat header.
One renderer per v1 kind (YNAB summary, category budget, spending
report, transactions; Mealie recipe, meal plan, shopping lists;
Obsidian note; document search) using CSS bars only, plus a JSON
fallback for unknown kinds or renderer errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
artifactTypes.ts mirrors the contract (worker C creates the same file).
artifactService.ts wraps GET /api/artifacts and /api/artifacts/<id>
with the shared fetchWithAuth helper.
test-fixtures/cowork/artifacts/<kind>.json holds one ToolArtifact per
v1 kind. mock-server.mjs is a dependency-free Node server that serves
dist/ and speaks the contract (SSE v2 tool events, artifacts API) for
Playwright checks without Docker or OIDC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_calendar_events / create_calendar_event never matched the "calendar"
prefix and fell back to the wrench icon; match "calendar" anywhere in the
tool name instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tool calls in a turn now render as one ActivityTimeline block: expanded
with "Simba is working…" while running (icon, label, input summary,
spinner/✓/✗, duration, Open chip for artifacts), collapsing to
"Simba worked · N steps · 4.2 s" when the turn finishes. useChat keys tool
rows by run_id (parallel calls), synthesises ids for legacy servers that
send only {tool}, and restores rows from the conversation payload.
Adds src/api/artifactTypes.ts (shared with the artifact panel) and a
dependency-free mock API server under test-fixtures/cowork/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the Makefile .PHONY conflict (desktop + ynab targets) and
regenerate the YNAB migration with aerich as 17 on top of main's
16_add_oidc_states, replacing the colliding 16_add_ynab_mirror.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Full resyncs mark mirror rows missing from the download as deleted;
YNAB omits deleted entities from non-delta responses, so deletions
made just before a full sync never reached the mirror.
- YNAB tools return errors as text instead of raising, since ToolNode
re-raises and would fail the whole chat turn. ynab_sql catches all
sqlglot errors (e.g. an unbalanced quote) and runs the SQL regenerated
from the validated tree, which also drops comments that broke the
LIMIT wrapper. Results keep duplicate column names.
- Sync fetches from YNAB before opening the DB transaction, discards the
payload if the other container synced meanwhile, re-checks freshness
under the lock so parallel tool calls share one sync, and backs off
for 60s after a failure.
- The mirror is pinned to one budget; a changed YNAB_BUDGET_ID errors
instead of mixing budgets, and an unset id stays on the budget the
first sync resolved from "last-used".
- Change log: no entries for empty month rollover rows, and id fields
are logged as names (category, payee, account). ynab_recent_changes
accepts the names it prints ("assignment") and plurals.
- Outputs sized to the 4000-char tool result cap; ynab_sql reuses the
shared admin message.
- YNAB API client moves back to utils/ynab_service.py, and the frontend
tool status labels list the new tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collapse fetchWithRefreshToken/fetchWithAuth into a single fetchWithAuth
that sends the access token, refreshes up front when none is stored, and
on a 401 refreshes once (deduplicated across concurrent calls) and retries.
The refresh token is now only sent to /api/user/refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ThinkingBubble was rendering after the messages list, so it appeared
below the partial answer while streaming. Inject it inside the messages
map just before the last streaming simba message so it sits above the
answer text. Keep the post-list fallback for when thinking is active
but no content has arrived yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Route <think>…</think> content from the llama-server stream into
thinking_delta SSE events instead of discarding it. A small look-ahead
buffer in _route_content() handles tags split across chunk boundaries.
Strip thinking tags from the persisted final_message so the DB stores
only the actual response.
Frontend: ThinkingBubble component accumulates the live reasoning text
in a scrollable panel that auto-collapses to "Thought for Xs" once the
first content token arrives. Falls back to the existing heartbeat
behaviour for models that don't emit <think> tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When llama-server is queue-backed it accepts the connection but never
produces tokens; after ~60s Caddy drops the connection and the frontend
clears loading state with no response and no error shown to the user.
Backend: replace the async-for event loop with an explicit __anext__()
iterator wrapped in asyncio.wait_for(). A 30s timeout (TTFT_TIMEOUT_SECONDS)
applies only in the window between on_chat_model_start and the first content
token — tool calls are excluded. On timeout, emits a stalled error event and
logs it rather than waiting for the proxy to kill the connection.
Frontend: separates stall errors from generic errors; shows a "Simba didn't
respond in time. Retry" pill that re-sends the original query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aborting the fetch on component unmount sent a disconnect to the server,
which cancelled the in-flight generation. Now the stream runs to
completion in the background (isMountedRef guards prevent stale state
updates), the response is persisted, and it's there when the user
returns to the conversation. stopGeneration() still aborts immediately
on explicit user cancel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
qwen3.8-27b is a reasoning model: it can think for hundreds of tokens
before any visible content streams. The SSE stream only forwarded
content deltas, so the UI showed dead air for the whole think and long
answers looked hung (users cancel ~1 min in).
The stream now emits a throttled {"type": "thinking"} heartbeat while
the current model turn has produced no content yet, and the frontend
renders a pulsing "Thinking..." pill until content or a tool call
arrives. The heartbeat also keeps bytes flowing on the SSE connection
during long thinks, so idle-timeout disconnects in the proxy chain
can't kill the stream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bump Conversation.updated_at on every message (not just the first) so
actively-used conversations rise to the top of the list endpoint's
existing -updated_at sort. Refresh the sidebar after every send so the
just-used conversation reorders live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first message in a new conversation navigated to /c/:id, which remounted
ChatScreen (two separate routes) and aborted the in-flight /stream-query fetch.
Server-side that surfaced as asyncio.CancelledError, which isn't an Exception,
so the except branch missed it and skipped the add_message_to_conversation
save — the fully-streamed answer was never persisted.
Backend: handle CancelledError and persist via a detached, shielded task so a
disconnect at any point (navigation, tab close, network blip) still commits the
answer. Frontend: serve "/" and "/c/:id" from a single catch-all ChatScreen
route so first-message navigation no longer remounts and aborts the stream;
derive the conversation id via useMatch instead of useParams.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /stream-query endpoint previously emitted only tool-lifecycle events plus
one final complete message, so the answer still appeared all at once. Handle
on_chat_model_stream events to emit incremental `content` deltas, accumulate
them for persistence fallback, and consume them on the frontend to build a live
assistant bubble (reconciled with the authoritative final text). Hide the
redundant loading indicator once tokens start arriving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework mobile navigation into a proper left slide-in drawer with a dimmed
backdrop, sharing a new SidebarContent component with the desktop sidebar so
mobile users can reach Admin/Scheduler/Sign out. The sidebar now loads only the
10 most recent conversations; a dedicated /conversations page browses the full
list with server-side search and "Load more" pagination.
- Backend: limit/offset/search query params on GET /api/conversation/
- SidebarContent shared by desktop sidebar and mobile drawer
- useConversationBrowser hook powers the dedicated page
- Remove duplicated inline conversation lists from ChatScreen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strip the card, border, shadow, and accent bar from AnswerBubble so
Simba's replies read like Claude/ChatGPT — plain full-width markdown on
the page. User messages stay bubbled for turn contrast. Widen the reading
column to max-w-3xl with roomier spacing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Give each conversation a deep-linkable URL (/c/:conversationId) so threads
survive refresh, support browser back/forward, and are shareable within a
session. The app was already an SPA but had no router.
- Add react-router-dom; wrap the app in BrowserRouter and define routes
("/" for the empty/new state, "/c/:conversationId" for a thread)
- Make the URL the source of truth for selection: selectedConversation is
derived from the :conversationId param; create/delete navigate instead of
mutating local selection state
- Load a thread's messages in a URL-keyed effect that clears immediately for
an instant switch, with a stale-response guard
- Preserve the optimistic message + in-flight stream when the first message
lazily creates a conversation and navigates into it (skip-ref keyed on the
new id, guarded against the null/home-route collision)
No backend changes: app.py already serves index.html for unmatched paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontend:
- Add stop-generation button (wire up the unused AbortController)
- Surface stream/network errors in a dismissible banner instead of console
- Add rename and delete for conversations via a per-thread menu
- Make simba mode a single controlled source; fix miscast keydown handler
- Auto-grow the message textarea; remove dead ConversationMenu component
Backend:
- Scope get/query/stream-query/rename/delete conversation routes to the
authenticated owner via a shared _get_owned_conversation helper (fixes IDOR)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enable remark-gfm in AnswerBubble so pipe-table syntax parses instead
of showing as raw text, and add table styling to markdown-content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When use_agent is enabled, the scheduler runs the message content as a
prompt through the LangChain agent and sends Simba's response instead of
the raw content. Frontend adds an Ask Simba toggle with visual indicator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend scheduled messages with a recurrence field. After sending a
recurring message, the scheduler automatically creates the next pending
occurrence. Frontend adds repeat toggle (Once/Daily/Weekly/Monthly) and
displays recurrence in the messages table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip markdown formatting (bold, italic, headers, code, links, lists) from
LLM responses before sending via iMessage. Add scheduled messages feature
with CRUD API, background scheduler loop, and admin frontend panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract logic from god components into custom hooks (useAuthCheck,
useConversations, useChat, usePresignedUrl, useAdminUsers, useOIDCAuth).
Eliminate unnecessary useEffects per React guidelines — scroll is now
imperative, isAdmin comes from useAuthCheck instead of a separate fetch.
ConversationList becomes a pure presentational component. Wrap bubble
components in React.memo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the useEffect on selectedConversation.id that race-conditions
with handleQuestionSubmit — it fetches the (still-empty) conversation
and wipes messages, sending the user back to the empty state. Refresh
conversation list after streaming completes instead to pick up the
auto-generated title.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Memoize blob URL creation to prevent leak on every keystroke, wrap
MessageInput in React.memo with stable useCallback props, remove
expensive backdrop-blur-sm from chat footer, and use instant scroll
during streaming to avoid queuing smooth scroll animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing pendingImage, onImageSelect, and onClearImage props to the
MessageInput rendered in the active chat footer, matching the homepage version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused image_url from upload response and TS type
- Remove bare except in serve_image that masked config errors as 404s
- Add error state and broken-image placeholder in QuestionBubble
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser <img> tags can't attach JWT headers, causing 401s. The image
endpoint now returns a time-limited presigned S3 URL via authenticated
API call, which the frontend fetches and uses directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now attach images in the web chat for Simba to analyze using
Ollama's gemma3 vision model. Images are stored in Garage (S3-compatible)
and displayed in chat history.
Also fixes aerich migration config by extracting TORTOISE_CONFIG into a
standalone config/db.py module, removing the stale aerich_config.py, and
adding missing MODELS_STATE to migration 3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds manifest.json, service worker with static asset caching,
resized cat icons, and meta tags for iOS/Android installability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users can now receive a unique email address (ask+<token>@domain) and
interact with Simba by sending emails. Inbound emails hit a Mailgun
webhook, are authenticated via HMAC token lookup, processed through the
LangChain agent, and replied to via the Mailgun API.
- Extract shared SIMBA_SYSTEM_PROMPT to blueprints/conversation/prompts.py
- Add email_enabled and email_hmac_token fields to User model
- Create blueprints/email with webhook, signature validation, rate limiting
- Add admin endpoints to enable/disable email per user
- Update AdminPanel with Email column, toggle, and copy-address button
- Add Mailgun env vars to .env.example
- Include database migration for new fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use 100dvh for proper mobile browser chrome handling and increase
cat icon sizes across sidebar, mobile header, and empty state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Convert date strings to datetime.date objects before passing to API (strict Pydantic validation rejects strings)
- Use txn.var_date instead of txn.date (renamed in SDK v2 to avoid Python builtin conflict)
- Migrate BudgetsApi → PlansApi and update method names accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show centered cat icon + "Ask me anything" + input when no messages
exist. Transition to scrollable messages + bottom input once chat
starts. Auto-create a conversation on first message if none selected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /me, /admin/users, and WhatsApp link/unlink endpoints
- Add AdminPanel component with user management UI
- Add userService methods for admin API calls
- Fix simba mode so cat responses appear in the message list
- Fetch userinfo endpoint for groups on OIDC callback (Authelia compat)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>