listRelayTasks() expected RelayTaskInfo[] but API returns
{items:[], total:0, page:1, page_size:20}. When setTasks() received
the paginated object, tasks.map() crashed during render, triggering
the ErrorBoundary fallback "SaaS 平台加载失败".
Fix: extract .items from paginated response with Array.isArray fallback.
Also adds onError logging to ErrorBoundary wrappers for easier debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: Playwright external Chromium is not a Tauri runtime, so
isTauriRuntime() returns false. The app needs SaaS session to route
chat through relay, but tests never logged in.
Fix: Auto-detect non-Tauri mode and pre-login via SaaS API, injecting
session into localStorage before tests run.
6 tests each called saasLogin() → 6 login requests in <60s → hit 5/min/IP
rate limit on the 6th test. Now login once per worker, reuse token for all
6 tests. Reduces login API calls from 6 to 1.
- Move side panel toggle from floating button to chat header right side
(Trae Solo style) via new PanelToggleButton component
- Add px-6 py-4 padding to message list container
- Add mb-5 gap between messages for readable vertical spacing
BUG-012: Reposition side panel toggle button (top-[52px]→top-20) to
avoid overlap with header buttons in ResizableChatLayout.
BUG-013: Install @tailwindcss/typography plugin and import in index.css
to enable prose-* Markdown rendering classes in StreamingText.
BUG-007: Rewrite authStore tests to match HttpOnly cookie auth model
(login takes 1 arg, no token/refreshToken in state). Rewrite request
interceptor tests for cookie-based auth. Update bug-tracker status.
BUG-009 (P1): Add frontend DataMasking in saas-relay-client.ts
- Masks ID cards, phones, emails, money, company names before relay
- Unmasks tokens in AI response so user sees original data
- Mirrors Rust DataMasking middleware patterns
BUG-010 (P3): Send button transforms to Stop during streaming
- Shows square icon when isStreaming, calls cancelStream()
- Normal arrow icon when idle, calls handleSend()
BUG-011 (P2): Add ::timestamptz casts for old TEXT timestamp columns
- account/handlers.rs: dashboard stats query
- telemetry/service.rs: reported_at comparisons
- workers/aggregate_usage.rs: usage aggregation query
SaaS Relay was sending only the current message without conversation
history, giving LLM no context from previous turns. Root cause:
streamStore passed only `content` string to chatStream(), and
saas-relay-client hard-coded a single-element messages array.
Fix:
- GatewayClient.chatStream() opts: add `history` field
- streamStore: extract last 20 messages as history before calling chatStream
- saas-relay-client: build messages array from history + current message
- saasStore.ts: replace require('./chat/conversationStore') with await import()
to fix ReferenceError in Vite ESM environment (P1)
- main.rs: fix health check pool usage formula from max_connections - num_idle
to pool.size() - num_idle, preventing false "degraded" status (P1)
- error.rs: show detailed error messages in ZCLAW_SAAS_DEV=true mode
- Update bug tracker with BUG-003 through BUG-007
Addresses findings from deep code audit:
H-1: Pass abortController.signal to saasClient.chatCompletion() so
user-cancelled streams actually abort the HTTP connection (was only
stopping the read loop, leaving server-side SSE connection open).
H-2: ModelSelector now shows only when (!isTauriRuntime() || isLoggedIn).
Prevents decorative model list in Tauri local kernel mode where model
selection has no effect (violates CLAUDE.md §5.2).
M-1: Normalize CRLF to LF before SSE event boundary parsing (\n\n).
Prevents buffer overflow when behind nginx/CDN with CRLF line endings.
M-2: SQL window_minute comparison uses to_char(NOW()-interval, format)
instead of (NOW()-interval)::TEXT, matching the stored format exactly.
M-3: sort_candidates_by_quota uses same sliding 60s window as select_best_key.
LOW: Fix misleading invalidate_cache doc comment.
P1: onError callback now sets content to error message instead of empty string.
Previously API errors (404/429) produced empty assistant messages with only
a visual error badge — now the error text is persisted in message content.
P3: Retry button now re-sends the preceding user message via sendToGateway
instead of copying to input. Works for both virtualized and non-virtualized
message lists. Removed unused setInput prop from MessageBubble.
Also hides model selector in Tauri runtime (SaaS token pool routes models).
Root cause: conversationStore hardcoded 'glm-4-flash' as default model,
which may not exist in SaaS admin config, causing 404 on all chat requests.
- conversationStore: default currentModel to empty string (runtime-resolved)
- saasStore: after fetching available models, auto-switch currentModel
to first available if the stored model is not in the list
- SaaS relay getModel() already had fallback to first available model
Model selector was cosmetic-only in desktop mode: chatStream never passes
model param to backend. Hiding prevents user confusion and 404 errors when
selecting models not in SaaS token pool.
Also adds E2E test report covering 168 messages, 4 bugs found (P0 fixed).
Adapt ChatArea for compact/butler mode:
- Add onOpenDetail prop for expanding to full view
- Remove inline export dialog (moved to detail view)
- Replace SquarePen with ClipboardList icon
Add SimpleSidebar component for butler simple mode:
- Two tabs: 对话 / 行业资讯
- Quick suggestion buttons
- Minimal navigation
RightPanel refactoring for dual-mode support:
- Detect simple vs professional mode
- Conditional rendering based on butler mode state
- Add ?mode=rwc to pain.db SQLite URL so it creates the file on first run
- Move useUIModeStore hook before conditional returns in App.tsx to fix
React "Rendered more hooks than during the previous render" error
- Call init_pain_storage() in Tauri .setup() so pain persistence activates on boot
- Integrate useColdStart hook into FirstConversationPrompt for auto-greeting
- Add UI mode toggle section to Settings/General (already had imports)
- Add "简洁" mode switch-back button to TopBar in professional layout
- Update SemanticSkillRouter @reserved annotation to reflect active status
PainAggregator and SolutionGenerator were in-memory only, losing all
data on restart. Add PainStorage module with SQLite backend (4 tables),
dual-write strategy (hot cache + durable), and startup cache warming.
- New: pain_storage.rs — SQLite CRUD for pain_points, pain_evidence,
proposals, proposal_steps with schema initialization
- Modified: pain_aggregator.rs — global PAIN_STORAGE singleton,
init_pain_storage() for startup, dual-write in merge_or_create/update
- Modified: solution_generator.rs — same dual-write pattern via
global PAIN_STORAGE
- 20 tests passing (10 storage + 10 aggregator)
The TopBar had a gradient badge with 'Z' letter followed by the title
'ZCLAW', creating visual 'ZZCLAW'. Replaced badge with a solid gradient
square as a brand indicator without the duplicate letter.
The framer-motion AnimatePresence with mode="wait" caused the sidebar
content to get stuck on the conversations list when switching to the
agents tab. The React state updated correctly but the DOM did not
re-render. Replaced with simple conditional rendering which is more
reliable and removes the framer-motion dependency from this component.
P1: identity.rs get_identity() returns empty soul/instructions for agents
without explicit identity files. This prevents the default ZCLAW personality
from overriding agent_config.system_prompt. New get_identity_or_default()
method added for the DEFAULT agent.
P2: messaging.rs now uses agent_config.model.model when available, falling
back to global Kernel config. This allows per-agent model selection.
P2: agentStore.ts loadClones retries up to 3 times (300ms interval) when
getClient() returns null, handling the coordinator initialization race.
P2: agent_create Tauri command auto-populates identity files (soul +
instructions) from creation parameters, ensuring build_system_prompt()
has content for new agents.
Also fixes conversationStore upsertActiveConversation to persist generated
conversation IDs, preventing duplicate entries on new conversations.
- nextStep() was allowing currentStep to reach steps.length (6), past
the last step index (5), showing "步骤 7/6:" with empty content area
- On the last step, nextStep now triggers handleSubmit() directly
instead of navigating to a phantom step 6
- Footer button condition changed: "完成" shows on last step instead
of after it, keeping error/success messages visible
- Added error logging in catch block (was silently swallowing errors)
Root cause: Each LLM delta (text/thinking) triggered a synchronous
setState via updateMessages → chatStore.setState. With Kimi thinking
model emitting many deltas per frame, this caused a React render storm
that hit the maximum update depth limit.
Fix (two-layer approach):
1. streamStore: Buffer text/thinking deltas locally and flush to store
via setTimeout(0), batching multiple deltas per frame
2. chatStore: Microtask batching in injectChatStore.updateMessages to
coalesce rapid successive updates
Verified: 2-round conversation (4 messages) with Kimi thinking model
completes without crash. Previously crashed 100% on 2nd message.
h-full = 100% of parent height, but TopBar already occupies 56px above.
This caused ChatArea to overflow by 56px, pushing the input box below
the visible viewport. flex-1 + min-h-0 correctly fills remaining space
in the flex column layout.
The input area at the bottom of the chat panel lacked flex-shrink-0,
causing the flex column layout to compress it when message content filled
the Conversation area. This made the textarea only partially visible and
the scrollbar unable to reach the bottom of the input area.
- Add TODO to PainAggregator documenting in-memory-only data limitation
- Remove unused `use serde::Serialize` import from a2a.rs (already clean)
- ProposalsSection: trigger refresh on error instead of silent catch
- useButlerInsights: collect all errors instead of overwriting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PersonalityConfig with 4 dimensions: tone, proactiveness, formality, humor
- Signal detection from Chinese user messages (e.g. "说简单点" → Simple tone)
- apply_personality_adjustments() returns new immutable config
- build_personality_prompt() injects personality into system prompts
- Integrated into post_conversation_hook for automatic detection
- In-memory persistence via OnceLock (VikingStorage integration TODO)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enable multi-agent feature by default in desktop build
- Add butler delegation logic: task decomposition, expert assignment
- Add ExpertTask, DelegationResult, butler_delegate() to Director
- Add butler_delegate_task Tauri command bridging Director to frontend
- 13 Director tests passing (6 original + 7 new butler tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HIGH fixes:
- H-NS-1: Non-streaming agent_chat now builds ChatModeConfig and calls
send_message_with_chat_mode(), matching the streaming path
- H-FE-1: ClarificationCard component renders structured clarification
questions with type badge, question text, and numbered options
- H-SEM-1: SemanticSkillRouter annotated as @reserved (Phase 3 wiring)
MEDIUM fixes:
- M-SETTINGS-1: Settings menu restructured with "高级" section separator;
skills/audit/tasks/heartbeat/semantic-memory grouped under advanced
- M-MAN-1: LoopEvent→StreamChatEvent mapping completeness checklist
added as documentation comment in agent_chat_stream loop
- M-ORPH-1: Deleted orphaned Automation/ and SkillMarket/ files,
plus transitively orphaned types, hooks, and adapters
CRITICAL:
- ask_clarification now terminates Agent Loop in both run() and run_streaming()
paths, preventing the LLM from continuing after requesting user clarification
HIGH:
- SaaS relay now forwards plan_mode and subagent_enabled to backend
- GatewayClient.chatStream now supports onThinkingDelta, onSubtaskStatus,
and token-bearing onComplete — aligned with kernel-types StreamCallbacks
- ZclawStreamEvent type extended with thinking_delta, subtask_status variants
and input_tokens/output_tokens fields for token tracking via Gateway path
The connectZclawStream call in the synchronous code path (common case
when agentId is already known) was missing the subagent_enabled field,
causing Gateway-connected clients to never send the flag to the server.
When the agent writes files via the file_write tool, artifacts are now
automatically created in the artifact panel. The file extension determines
the artifact type (code/markdown/text) and syntax highlighting language.
This connects the existing ArtifactPanel UI to actual tool output data.