Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f2c750c
feat: built-in agent — LLM-powered AEO analyst with chat API
arberx Mar 15, 2026
a2aead6
fix(security): Add project ownership verification to thread endpoints
arberx Mar 16, 2026
f1f5813
fix(agent): Add error handling for malformed JSON in tool call arguments
arberx Mar 16, 2026
5dc9096
perf(agent): Move dynamic imports to top-level
arberx Mar 16, 2026
2b52b09
refactor(agent): Replace circular HTTP self-calls with direct service…
arberx Mar 16, 2026
b6d2643
style(agent): Remove dead code and unused types
arberx Mar 16, 2026
fabe3e3
fix(agent): fix 5 bugs in agent loop, SSRF validation, and services
arberx Mar 16, 2026
5e516a5
fix(agent): address all review findings from PR #74
arberx Mar 17, 2026
e259d09
fix(agent): address second round of review findings
arberx Mar 17, 2026
4a4581e
feat(agent): name the agent "Aero" with soul.md and memory.md
arberx Mar 17, 2026
5725bd8
feat(agent): add per-request LLM provider selection
arberx Mar 17, 2026
dc4ae4d
feat(web): add Aero chat UI with project and provider selection
arberx Mar 17, 2026
8a1c4b9
feat(agent): add memory system, system tools, markdown rendering, and…
arberx Mar 17, 2026
840dc7d
fix(agent): reduce token bloat — compress old tool results and add re…
arberx Mar 17, 2026
0787d12
fix(agent): rewrite convertToClaudeMessages to build valid output by …
arberx Mar 17, 2026
5dc2151
feat(agent): add per-request model selection to Aero chat
arberx Mar 17, 2026
cac7294
chore: merge origin/main into feat/agent
arberx Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
988 changes: 972 additions & 16 deletions apps/web/src/App.tsx

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions apps/web/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,64 @@ export function requestIndexing(
body: JSON.stringify(body),
})
}

// ── Agent (Aero) ─────────────────────────────────────────────

export interface ApiAgentThread {
id: string
projectId: string
title: string | null
channel: string
createdAt: string
updatedAt: string
}

export interface ApiAgentMessage {
id: string
threadId: string
role: 'user' | 'assistant' | 'tool'
content: string
toolName: string | null
toolArgs: string | null
toolCallId: string | null
createdAt: string
}

export function createAgentThread(project: string, opts?: { title?: string }): Promise<ApiAgentThread> {
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads`, {
method: 'POST',
body: JSON.stringify({ title: opts?.title, channel: 'chat' }),
})
}

export function fetchAgentThreads(project: string): Promise<ApiAgentThread[]> {
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads`)
}

export function fetchAgentThread(project: string, threadId: string): Promise<ApiAgentThread & { messages: ApiAgentMessage[]; status: 'processing' | 'idle'; error: string | null }> {
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads/${encodeURIComponent(threadId)}`)
}

export function sendAgentMessage(project: string, threadId: string, message: string, provider?: string, model?: string): Promise<{ threadId: string; status: string }> {
const body: Record<string, unknown> = { message }
if (provider) body.provider = provider
if (model) body.model = model
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads/${encodeURIComponent(threadId)}/messages`, {
method: 'POST',
body: JSON.stringify(body),
})
}

export function renameAgentThread(project: string, threadId: string, title: string): Promise<ApiAgentThread> {
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads/${encodeURIComponent(threadId)}`, {
method: 'PATCH',
body: JSON.stringify({ title }),
})
}

export function deleteAgentThread(project: string, threadId: string): Promise<void> {
return apiFetch(`/projects/${encodeURIComponent(project)}/agent/threads/${encodeURIComponent(threadId)}`, {
method: 'DELETE',
body: '{}',
})
}
248 changes: 248 additions & 0 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1393,4 +1393,252 @@
@apply absolute -m-px h-px w-px overflow-hidden border-0 p-0;
clip: rect(0 0 0 0);
}

/* ── Aero (Agent Chat) ── */

.aero-page {
@apply h-full overflow-hidden flex flex-col;
}

.aero-select {
@apply bg-zinc-900/50 border border-zinc-800/60 rounded-lg px-3 py-1.5
text-[13px] text-zinc-300 outline-none
focus:border-zinc-600 transition-colors;
appearance: auto;
}

.aero-layout {
@apply flex gap-4 mt-4 flex-1 min-h-0 overflow-hidden;
}

.aero-threads {
@apply w-60 shrink-0 bg-zinc-900/20 border border-zinc-800/50 rounded-xl p-3 overflow-y-auto;
}

.aero-thread-item {
@apply flex items-start gap-2 px-2.5 py-2 rounded-lg cursor-pointer transition-colors;
}

.aero-thread-item:hover {
@apply bg-zinc-800/40;
}

.aero-thread-item.active {
@apply bg-zinc-800/60;
}

.aero-chat {
@apply flex-1 flex flex-col bg-zinc-900/30 border border-zinc-800/60 rounded-xl overflow-hidden;
}

.aero-empty {
@apply flex-1 flex flex-col items-center justify-center p-6;
}

.aero-messages {
@apply flex-1 overflow-y-auto p-4 space-y-4;
}

.aero-msg {
@apply max-w-[85%];
}

.aero-msg.user {
@apply ml-auto;
}

.aero-msg.assistant {
@apply mr-auto;
}

.aero-msg-label {
@apply text-[10px] uppercase tracking-[0.18em] text-zinc-500 mb-1;
}

.aero-msg.user .aero-msg-label {
@apply text-right;
}

.aero-msg-content {
@apply text-[13px] leading-relaxed rounded-xl px-4 py-3;
}

.aero-msg.user .aero-msg-content {
@apply bg-zinc-800/60 text-zinc-200 whitespace-pre-wrap;
}

.aero-msg.assistant .aero-msg-content {
@apply bg-zinc-900/60 text-zinc-300 border border-zinc-800/40;
}

/* Markdown prose inside assistant messages */
.aero-md p {
@apply mb-2;
}
.aero-md p:last-child {
@apply mb-0;
}

.aero-md-list {
@apply pl-5 my-1.5 space-y-0.5 list-disc;
}
.aero-md-ol {
@apply list-decimal;
}
.aero-md-list li {
@apply text-zinc-300;
}

.aero-inline-code {
@apply bg-zinc-800 text-emerald-400 px-1.5 py-0.5 rounded text-[12px] font-mono;
}

.aero-code-block {
@apply bg-zinc-950 border border-zinc-800/60 rounded-lg px-4 py-3 my-2
text-[12px] leading-relaxed font-mono text-zinc-300 overflow-x-auto;
}

.aero-error {
@apply text-[13px] text-rose-400 bg-rose-950/30 border border-rose-900/40
rounded-lg px-4 py-2.5 mr-auto max-w-[85%];
}

.aero-input-wrap {
@apply border-t border-zinc-800/60;
}

.aero-input-area {
@apply flex items-end gap-2 p-3;
}

.aero-input {
@apply flex-1 bg-zinc-900/50 border border-zinc-800/60 rounded-xl px-4 py-2.5
text-[13px] text-zinc-200 placeholder-zinc-600 outline-none
resize-none
focus:border-zinc-600 transition-colors;
min-height: 40px;
max-height: 200px;
overflow-y: auto;
}

.aero-send {
@apply p-2.5 rounded-xl bg-zinc-800/60 text-zinc-400
hover:bg-zinc-700/60 hover:text-zinc-200
disabled:opacity-30 disabled:cursor-not-allowed
transition-colors shrink-0;
}

@keyframes aero-dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}

.aero-thinking::after {
content: '...';
animation: aero-dots 1.2s steps(1) infinite;
}

/* ── Tool call cards ── */

.aero-tool-card {
@apply bg-zinc-900/40 border border-zinc-800/40 rounded-lg overflow-hidden;
border-left-width: 2px;
}

.aero-tool-card-read {
@apply border-l-zinc-600;
}

.aero-tool-card-write {
@apply border-l-amber-500;
}

.aero-tool-card-system {
@apply border-l-emerald-500;
}

.aero-tool-card-memory {
@apply border-l-blue-500;
}

.aero-tool-active {
animation: aero-tool-pulse 1.5s ease-in-out infinite;
}

@keyframes aero-tool-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}

.aero-tool-header {
@apply flex items-center justify-between w-full px-3 py-2
text-left cursor-pointer
hover:bg-zinc-800/20 transition-colors;
}

.aero-tool-header:disabled {
@apply cursor-default;
}

.aero-tool-result {
@apply px-3 pb-2.5 pt-0.5
border-t border-zinc-800/30
max-h-64 overflow-y-auto;
}

.aero-terminal-body {
@apply bg-zinc-950 border border-zinc-800/50 rounded px-3 py-2
text-[11px] leading-relaxed font-mono text-zinc-400
overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap;
}

.aero-result-pill {
@apply inline-flex items-center gap-1
rounded-full bg-zinc-800/60 border border-zinc-700/40
px-2.5 py-0.5 text-[11px] text-zinc-400;
}

.aero-result-table-wrap {
@apply overflow-x-auto;
}

.aero-result-table {
@apply w-full text-[11px];
}

.aero-result-table th {
@apply text-left text-[10px] uppercase tracking-wider text-zinc-600 pb-1 pr-3 font-medium;
}

.aero-result-table td {
@apply py-0.5 pr-3 text-zinc-500;
}

/* ── Capability pills & quick actions ── */

.aero-capability-pill {
@apply inline-flex items-center gap-1.5
rounded-full bg-zinc-900/60 border border-zinc-800/50
px-3 py-1 text-[11px] text-zinc-500;
}

.aero-quick-action {
@apply flex items-center gap-3 px-3 py-2.5
rounded-lg border border-zinc-800/30
bg-zinc-900/20
hover:bg-zinc-800/30 hover:border-zinc-700/40
transition-colors cursor-pointer;
}

.aero-quick-chips {
@apply flex flex-wrap gap-1.5 px-3 pt-2;
}

.aero-chip {
@apply rounded-full bg-zinc-800/40 border border-zinc-700/40
px-2.5 py-1 text-[11px] text-zinc-500
hover:text-zinc-300 hover:bg-zinc-800/60
cursor-pointer transition-colors;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canonry",
"private": true,
"version": "1.16.0",
"version": "1.19.0",
"type": "module",
"packageManager": "pnpm@10.28.2",
"scripts": {
Expand Down
Loading
Loading