* fix(ui): allow relative URLs in avatar validation
The isAvatarUrl check only accepted http://, https://, or data: URLs,
but the /avatar/{agentId} endpoint returns relative paths like /avatar/main.
This caused local file avatars to display as text instead of images.
Fixes avatar display for locally configured avatar files.
* fix(gateway): resolve local avatars to URL in HTML injection and RPC
The frontend fix alone wasn't enough because:
1. serveIndexHtml() was injecting the raw avatar filename into HTML
2. agent.identity.get RPC was returning raw filename, overwriting the
HTML-injected value
Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the
/avatar/{agentId} endpoint URL.
* feat(compaction): add adaptive chunk sizing and progressive fallback
- Add computeAdaptiveChunkRatio() to reduce chunk size for large messages
- Add isOversizedForSummary() to detect messages too large to summarize
- Add summarizeWithFallback() with progressive fallback:
- Tries full summarization first
- Falls back to partial summarization excluding oversized messages
- Notes oversized messages in the summary output
- Add SAFETY_MARGIN (1.2x) buffer for token estimation inaccuracy
- Reduce MIN_CHUNK_RATIO to 0.15 for very large messages
This prevents compaction failures when conversations contain
unusually large tool outputs or responses that exceed the
summarization model's context window.
* feat(ui): add compaction indicator and improve event error handling
Compaction indicator:
- Add CompactionStatus type and handleCompactionEvent() in app-tool-stream.ts
- Show '🧹 Compacting context...' toast while active (with pulse animation)
- Show '🧹 Context compacted' briefly after completion
- Auto-clear toast after 5 seconds
- Add CSS styles for .callout.info, .callout.success, .compaction-indicator
Error handling improvements:
- Wrap onEvent callback in try/catch in gateway.ts to prevent errors
from breaking the WebSocket message handler
- Wrap handleGatewayEvent in try/catch with console.error logging
to isolate errors and make them visible in devtools
These changes address UI freezes during heavy agent activity by:
1. Showing users when compaction is happening
2. Preventing uncaught errors from silently breaking the event loop
* fix(control-ui): add agentId to DEFAULT_ASSISTANT_IDENTITY
TypeScript inferred the union type without agentId when falling back to
DEFAULT_ASSISTANT_IDENTITY, causing build errors at control-ui.ts:222-223.
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
import { resolveAgentIdentity } from "../agents/identity.js";
|
|
import { loadAgentIdentity } from "../commands/agents.config.js";
|
|
import type { ClawdbotConfig } from "../config/config.js";
|
|
import { normalizeAgentId } from "../routing/session-key.js";
|
|
|
|
const MAX_ASSISTANT_NAME = 50;
|
|
const MAX_ASSISTANT_AVATAR = 200;
|
|
|
|
export const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
|
|
agentId: "main",
|
|
name: "Assistant",
|
|
avatar: "A",
|
|
};
|
|
|
|
export type AssistantIdentity = {
|
|
agentId: string;
|
|
name: string;
|
|
avatar: string;
|
|
};
|
|
|
|
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
|
|
if (typeof value !== "string") return undefined;
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return undefined;
|
|
if (trimmed.length <= maxLength) return trimmed;
|
|
return trimmed.slice(0, maxLength);
|
|
}
|
|
|
|
function isAvatarUrl(value: string): boolean {
|
|
return /^https?:\/\//i.test(value) || /^data:image\//i.test(value);
|
|
}
|
|
|
|
function looksLikeAvatarPath(value: string): boolean {
|
|
if (/[\\/]/.test(value)) return true;
|
|
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
|
|
}
|
|
|
|
function normalizeAvatarValue(value: string | undefined): string | undefined {
|
|
if (!value) return undefined;
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return undefined;
|
|
if (isAvatarUrl(trimmed)) return trimmed;
|
|
if (looksLikeAvatarPath(trimmed)) return trimmed;
|
|
if (!/\s/.test(trimmed) && trimmed.length <= 4) return trimmed;
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveAssistantIdentity(params: {
|
|
cfg: ClawdbotConfig;
|
|
agentId?: string | null;
|
|
workspaceDir?: string | null;
|
|
}): AssistantIdentity {
|
|
const agentId = normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg));
|
|
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
|
|
const configAssistant = params.cfg.ui?.assistant;
|
|
const agentIdentity = resolveAgentIdentity(params.cfg, agentId);
|
|
const fileIdentity = workspaceDir ? loadAgentIdentity(workspaceDir) : null;
|
|
|
|
const name =
|
|
coerceIdentityValue(configAssistant?.name, MAX_ASSISTANT_NAME) ??
|
|
coerceIdentityValue(agentIdentity?.name, MAX_ASSISTANT_NAME) ??
|
|
coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ??
|
|
DEFAULT_ASSISTANT_IDENTITY.name;
|
|
|
|
const avatarCandidates = [
|
|
coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR),
|
|
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR),
|
|
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR),
|
|
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR),
|
|
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR),
|
|
];
|
|
const avatar =
|
|
avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
|
|
DEFAULT_ASSISTANT_IDENTITY.avatar;
|
|
|
|
return { agentId, name, avatar };
|
|
}
|