diff --git a/README.md b/README.md
index b72a0a72..429b4acb 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,43 @@ Web/Mobile Clients
- **Gateway**: WebSocket server for remote clients
- **Hub**: Agent lifecycle and event distribution
+## Time
+
+Super Multica now uses **message-level timestamp injection** for time awareness.
+Instead of placing dynamic time text in the system prompt, user turns are stamped at runtime.
+
+```mermaid
+flowchart TD
+ A[Incoming turn] --> B{Entry point}
+ B -->|Desktop/Gateway/Cron/Subagent| C[AsyncAgent.write]
+ B -->|Heartbeat poll| D[AsyncAgent.write injectTimestamp=false]
+ C --> E{Already stamped or has 'Current time:'?}
+ E -->|Yes| F[Keep original message]
+ E -->|No| G[Prefix: [DOW YYYY-MM-DD HH:mm TZ]]
+ D --> H[Keep original heartbeat prompt]
+ F --> I[Agent.run]
+ G --> I
+ H --> I
+ I --> J[LLM receives final turn text]
+```
+
+### Injection Matrix
+
+| Path | Runtime call | Timestamp injected? | Notes |
+| --- | --- | --- | --- |
+| Desktop direct chat | `agent.write(content)` | Yes | Default behavior |
+| Gateway/remote chat | `agent.write(content)` | Yes | Same entry path as desktop |
+| `sessions_spawn` child task | `childAgent.write(task)` | Yes | Child turn gets current time context |
+| Cron `agent-turn` payload | `agent.write(cronMessage)` | Yes (guarded) | Skips if message already carries `Current time:` |
+| Heartbeat runner | `agent.write(prompt, { injectTimestamp: false })` | No | Prevents heartbeat prompt matching from breaking |
+| Internal orchestration | `writeInternal(...)` | No | Uses separate internal run path |
+
+### Why this design
+
+- Keeps system prompt cache-stable (no per-turn date churn in system prompt text)
+- Gives the model an explicit "now" reference on each user turn
+- Uses guardrails to avoid double-stamping and heartbeat regressions
+
## Scripts
```bash
diff --git a/apps/desktop/electron/ipc/hub.ts b/apps/desktop/electron/ipc/hub.ts
index ad1725db..1e5446ce 100644
--- a/apps/desktop/electron/ipc/hub.ts
+++ b/apps/desktop/electron/ipc/hub.ts
@@ -282,6 +282,7 @@ export function registerHubIpcHandlers(): void {
return
}
+
// Filter events same as Hub.consumeAgent()
const maybeMessage = (event as { message?: { role?: string } }).message
const isAssistantMessage = maybeMessage?.role === 'assistant'
diff --git a/apps/desktop/src/components/local-chat.tsx b/apps/desktop/src/components/local-chat.tsx
index 5384c063..ade9db6c 100644
--- a/apps/desktop/src/components/local-chat.tsx
+++ b/apps/desktop/src/components/local-chat.tsx
@@ -1,6 +1,10 @@
+import { useState, useCallback } from 'react'
import { Loading } from '@multica/ui/components/ui/loading'
import { ChatView } from '@multica/ui/components/chat-view'
import { useLocalChat } from '../hooks/use-local-chat'
+import { useProvider } from '../hooks/use-provider'
+import { ApiKeyDialog } from './api-key-dialog'
+import { OAuthDialog } from './oauth-dialog'
export function LocalChat() {
const {
@@ -17,8 +21,41 @@ export function LocalChat() {
sendMessage,
loadMore,
resolveApproval,
+ clearError,
} = useLocalChat()
+ const { providers, current, setProvider: switchProvider, refresh: refreshProviders } = useProvider()
+
+ // Provider config dialog state
+ const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false)
+ const [oauthDialogOpen, setOauthDialogOpen] = useState(false)
+
+ const handleConfigureProvider = useCallback(() => {
+ const providerId = current?.provider
+ if (!providerId) return
+
+ const meta = providers.find((p) => p.id === providerId)
+ if (!meta) return
+
+ if (meta.authMethod === 'oauth') {
+ setOauthDialogOpen(true)
+ } else {
+ setApiKeyDialogOpen(true)
+ }
+ }, [current, providers])
+
+ const handleProviderConfigSuccess = useCallback(async () => {
+ const providerId = current?.provider
+ if (!providerId) return
+
+ await refreshProviders()
+ await switchProvider(providerId)
+ clearError()
+ }, [current, refreshProviders, switchProvider, clearError])
+
+ // Derive provider info for dialogs
+ const currentMeta = current ? providers.find((p) => p.id === current.provider) : null
+
if (initError) {
return (
@@ -36,19 +73,48 @@ export function LocalChat() {
)
}
+ // Show "Configure" button when error is about provider/API key
+ const errorAction = error?.code === 'AGENT_ERROR' && currentMeta
+ ? { label: 'Configure', onClick: handleConfigureProvider }
+ : undefined
+
return (
-
+ <>
+
+
+ {currentMeta && currentMeta.authMethod === 'api-key' && (
+
+ )}
+
+ {currentMeta && currentMeta.authMethod === 'oauth' && (
+
+ )}
+ >
)
}
diff --git a/apps/desktop/src/hooks/use-local-chat.ts b/apps/desktop/src/hooks/use-local-chat.ts
index ec7e9677..749ee071 100644
--- a/apps/desktop/src/hooks/use-local-chat.ts
+++ b/apps/desktop/src/hooks/use-local-chat.ts
@@ -5,6 +5,7 @@ import type {
ExecApprovalRequestPayload,
ApprovalDecision,
AgentMessageItem,
+ AgentErrorEvent,
} from '@multica/sdk'
import { DEFAULT_MESSAGES_LIMIT } from '@multica/sdk'
@@ -56,10 +57,10 @@ export function useLocalChat() {
const payload = data as unknown as StreamPayload
if (!payload.event) return
- // Handle agent errors as transient UI feedback (not persisted to history)
+ // Handle agent error events
if (payload.event.type === 'agent_error') {
- const errorMsg = (payload.event as { error?: string }).error ?? 'Unknown error'
- chatRef.current.setError({ code: 'AGENT_ERROR', message: errorMsg })
+ const errorEvent = payload.event as AgentErrorEvent
+ chatRef.current.setError({ code: 'AGENT_ERROR', message: errorEvent.message })
setIsLoading(false)
return
}
@@ -141,6 +142,10 @@ export function useLocalChat() {
[],
)
+ const clearError = useCallback(() => {
+ chatRef.current.setError(null)
+ }, [])
+
return {
agentId,
initError,
@@ -155,5 +160,6 @@ export function useLocalChat() {
sendMessage,
loadMore,
resolveApproval,
+ clearError,
}
}
diff --git a/packages/hooks/src/use-gateway-chat.ts b/packages/hooks/src/use-gateway-chat.ts
index 1871de28..0986c7bb 100644
--- a/packages/hooks/src/use-gateway-chat.ts
+++ b/packages/hooks/src/use-gateway-chat.ts
@@ -51,7 +51,7 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
if (msg.action === StreamAction) {
const payload = msg.payload as StreamPayload;
if (payload.event.type === "agent_error") {
- const errorMsg = (payload.event as { error?: string }).error ?? "Unknown error";
+ const errorMsg = (payload.event as { message?: string }).message ?? "Unknown error";
chat.setError({ code: "AGENT_ERROR", message: errorMsg });
setIsLoading(false);
return;
diff --git a/packages/sdk/src/actions/stream.ts b/packages/sdk/src/actions/stream.ts
index 19aa9482..dfaf06fa 100644
--- a/packages/sdk/src/actions/stream.ts
+++ b/packages/sdk/src/actions/stream.ts
@@ -45,17 +45,17 @@ export type CompactionEndEvent = {
/** Union of all compaction events */
export type CompactionEvent = CompactionStartEvent | CompactionEndEvent;
-/** Emitted when the agent encounters an error (LLM failure, quota exceeded, etc.) */
+/** Emitted when an agent encounters an error during execution */
export type AgentErrorEvent = {
type: "agent_error";
- error: string;
+ message: string;
};
// --- Stream event types ---
/**
- * Hub forwards AgentEvent from pi-agent-core, CompactionEvent,
- * and AgentErrorEvent as-is. StreamPayload wraps them with routing metadata.
+ * Hub forwards AgentEvent from pi-agent-core, CompactionEvent, and AgentErrorEvent as-is.
+ * StreamPayload wraps them with routing metadata.
*/
export interface StreamPayload {
streamId: string;
diff --git a/packages/ui/src/components/chat-view.tsx b/packages/ui/src/components/chat-view.tsx
index 07c5a197..dcdded48 100644
--- a/packages/ui/src/components/chat-view.tsx
+++ b/packages/ui/src/components/chat-view.tsx
@@ -39,6 +39,8 @@ export interface ChatViewProps {
loadMore?: () => void;
resolveApproval: (approvalId: string, decision: "allow-once" | "allow-always" | "deny") => void;
onDisconnect?: () => void;
+ /** Optional action button in the error banner (e.g. "Configure Provider") */
+ errorAction?: { label: string; onClick: () => void };
}
export function ChatView({
@@ -54,6 +56,7 @@ export function ChatView({
loadMore,
resolveApproval,
onDisconnect,
+ errorAction,
}: ChatViewProps) {
const mainRef = useRef
(null);
const sentinelRef = useRef(null);
@@ -224,16 +227,28 @@ export function ChatView({
{error.message}
- {onDisconnect && (
-
- )}
+
+ {errorAction && (
+
+ )}
+ {onDisconnect && (
+
+ )}
+
)}
@@ -241,8 +256,8 @@ export function ChatView({