Use chatRef to hold stable reference to useChat return value, avoiding
stale closure issues and satisfying react-hooks/exhaustive-deps rule.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add LocalChat component using useLocalChat hook that communicates with
the Hub via IPC (no Gateway required). Fix streamId extraction to use
event.message.id matching Hub behavior. Fix history to return raw
AgentMessageItem[] instead of flattened strings. Add exec approval
forwarding over IPC. Use conditional rendering for LocalChat to prevent
event leaking from remote sessions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move gateway-specific chat logic into dedicated useGatewayChat hook.
useChat remains a pure state hook with no IO. Update ChatView props,
remove legacy chat.tsx and connect-prompt.tsx.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add localApprovalHandlers map so exec approval requests can be routed
to the Electron renderer via IPC instead of requiring a Gateway
connection. Expose setLocalApprovalHandler/removeLocalApprovalHandler
and resolveExecApproval on Hub for the IPC layer to use.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract StatusWrapper to replace duplicated fullscreen/inline wrapper in
ConnectionStatus and RejectedStatus. Extract PairingHeader to replace
repeated title+description blocks in mobile and desktop views. Make
desktop container mb responsive (mb-14 sm:mb-28).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Keep needsCompaction() method from compaction-feedback PR
- Keep tool result pruning from this branch
- Add "pruning" to CompactionEndEvent.reason type in events.ts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adding explicit return type to help TypeScript resolve the type
correctly across different build configurations.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The desktop app imports from the root src directory, but turbo wasn't
tracking those files for cache hash calculation. This caused CI builds
to use stale hashes when types changed in the root src directory.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The CompactionResult type includes "pruning" as a reason but SessionEntry
did not, causing type errors when writing compaction entries.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two-phase pruning approach based on OpenClaw's microcompact-style:
- Soft Trim (30% utilization): Keep head 1500 + tail 1500 chars of large tool results
- Hard Clear (50% utilization): Replace old tool results with placeholder
Protections:
- Never prunes before first real user message (bootstrap protection)
- Protects last 3 assistant messages and their corresponding tool results
- Skips image-containing tool results
- Respects tool allow/deny lists
Enabled by default in tokens/summary compaction modes.
Replace Zustand-based message/connection stores with local state hooks.
useLocalChat now returns UseChatReturn shape with internal agentId discovery,
tool execution events, and error handling. Remote mode uses shared
useGatewayConnection + useChat + DevicePairing from packages.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document how clients receive real-time events via WebSocket and IPC,
covering message lifecycle, tool execution, and compaction events with
full data structure definitions and routing pseudocode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move all CSS out of globals.css into inline styles within each component.
Loading (Apple-style radiating lines) for passive waiting states;
Spinner (3x3 grid pulse) for active processing/execution states.
Switch device-pairing ConnectionStatus from Loading to Spinner.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Widen the LocalChatEvent.event.type union to include compaction_start
and compaction_end, fixing TS2367 comparison errors in use-local-chat.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract ChatView from web chat-page into packages/ui as a prop-driven
component (accepts UseChatReturn shape, no transport dependency)
- Move DevicePairing from apps/web to packages/ui with locally defined
ConnectionIdentity type (no @multica/hooks dependency)
- Create @multica/hooks package with useGatewayConnection and useChat
(moved from apps/web/hooks)
- Add isLoadingHistory state to useChat with skeleton loading in ChatView
- Add MulticaIcon (pure CSS asterisk via clip-path, adapts to theme)
- Slim web chat-page.tsx from 188 to 65 lines (just wires hooks to UI)
Desktop can now reuse ChatView and DevicePairing directly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wrap maybeCompact() in try/catch to ensure compaction_end always fires
- Widen multicaListeners type to match subscribeAll() callback signature
- Import CompactionEndEvent from SDK instead of inline type casts
- Add doc comment explaining reason field type difference (agent vs SDK)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Profile: add usage hint for base path
- Memory Recall: clear step-by-step instructions for search and update
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Profile: concise directory path with single usage hint
- Memory Recall: one-liner instruction matching OpenClaw style
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements a simple memory_search tool for searching memory files:
- Searches memory.md and memory/*.md files by keyword
- Returns matching lines with context (2 lines before/after)
- Supports case-sensitive/insensitive search
- Respects maxResults limit
Tool is only available when a profile is active (has profileDir).
System prompt includes memory usage guidance when tool is present.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Forward compaction_start/compaction_end events through Hub (Gateway path)
and Desktop IPC (local path) to the Zustand messages store. Adds
CompactionEvent types to the SDK, compacting/lastCompaction state to
useMessagesStore, and event routing in both connection-store and
use-local-chat.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce MulticaEvent type system parallel to pi-agent-core's AgentEvent,
with compaction_start and compaction_end events. Agent.subscribeAll() merges
both event streams. maybeCompact() now emits events bracketing the compaction
work, gated by a new SessionManager.needsCompaction() pre-check.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The original icon PNGs had pre-baked rounded corners and transparent
backgrounds. When iOS applies its own superellipse mask on Add to Home
Screen, the transparent corners were filled with white, creating a
visible white border around the icon.
Changes:
- Regenerate all icon PNGs as full-bleed (no rounded corners, opaque
background filling entire canvas) from the existing logo SVG
- Add dedicated apple-touch-icon.png at 180x180 (iOS standard size)
- Add favicon.ico for universal browser fallback
- Replace oversized app/icon.png (540x540) with 192x192 full-bleed
- Update layout.tsx to reference apple-touch-icon.png
- Add explicit purpose: "any" to 192x192 manifest entry
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ExecApprovalItem for human-in-the-loop command approval with uniform
outline buttons (Allow/Always/Deny), countdown timer, and command display.
Refine ToolCallItem and ThinkingItem: transparent by default, unified
bg-muted/30 wrapper on expand with seamless button+content integration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ThinkingItem component for displaying LLM extended thinking. Renders
as a collapsible row (matching ToolCallItem style) with expand to reveal
thinking text. MessageList now extracts and renders ThinkingContent blocks
before text content, matching the LLM output order.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add break-words to markdown-content wrapper so URLs and file paths in
<p>, <li>, <a> etc. wrap at container boundary instead of overflowing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace Zustand global stores with hook-local state for the web app.
useGatewayConnection handles client lifecycle, identity persistence,
reconnection, and keyed reset. useChat handles message history, streaming
events, tool execution, Hub error action, and exec approval requests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add @utility container (w-full max-w-4xl mx-auto) to globals.css and
create a reusable Loading spinner component for consistent loading states.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Memory is now managed through profile files (memory.md, memory/*.md) using
standard read/edit tools, following OpenClaw's file-first approach.
Changes:
- Remove memory/ folder with KV-based memory tools
- Remove group:memory from tool groups
- Update system prompt to remove memory tool references
- Update README docs to reflect file-based memory approach
Agents use workspace.md instructions to manage memory via file operations.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Log storage write errors in SessionManager.enqueue() instead of
silently swallowing them
- Remove existsSync check in ensureSessionDir to avoid TOCTOU race;
mkdirSync with recursive is idempotent when dir already exists
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The subagent announce flow reads the child's session file to extract
results, but saveMessage() is fire-and-forget (void enqueue). When
agent.run() completes, pending appendEntry writes may not have flushed
to disk yet, causing readLatestAssistantReply() to find no entries.
Add SessionManager.flush() and call it in AsyncAgent.write() after
agent.run() returns. This ensures waitForIdle() won't resolve until
all session data is on disk, fixing the "no return content" issue.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The mkdir call with { recursive: true } can throw ENOENT on macOS APFS
due to filesystem race conditions. This caused SpawnSession child agents
to silently fail — the session data was never written, so the parent
read back empty results ("子任务完成了,但没有返回内容").
Two fixes:
- Add retry logic to mkdir calls in acquireSessionWriteLock and
ensureSessionDir to handle transient ENOENT
- Add .catch() to SessionManager.enqueue() to prevent unhandled
promise rejections from fire-and-forget storage operations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a "verifying" connection state between "connected" and "registered"
so collaborators see clear feedback while waiting for the device owner
to approve their connection on Desktop.
Changes across the stack:
- Hub: verify RPC returns isNewDevice flag to distinguish new vs whitelisted
- SDK: emit "verifying" state before verify RPC, pass isNewDevice through
- Store: capture isNewDevice via onVerified, capture rejection via onError
- UI: ConnectionStatus (waiting), RejectedStatus (declined), and
verify success overlay (approved) replace the stuck scanner screen
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>