Skip to content

Commit 2d75828

Browse files
committed
release: merge develop into main for v0.29.2
2 parents c6b635f + 88620a1 commit 2d75828

28 files changed

Lines changed: 1146 additions & 180 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.29.2] - 2026-04-23
9+
10+
Patch release: in-app toasts and confirm dialogs replacing 47 native `alert()`/`confirm()` calls, agent avatars in the Topics list, plus fixes for PR #30 (provider routing + docker) and the archive endpoint.
11+
12+
### Added
13+
14+
- **In-app Toast system (`useToast`)** — stackable notifications in the bottom-right corner (max 5), auto-dismiss 4s, variants `success` / `error` / `warning` / `info`. Replaces all `window.alert()` usage in the dashboard with a consistent, non-blocking pattern in the EvoNexus dark tone. Zero new dependencies (pure CSS keyframes + Context API).
15+
- **In-app Confirm dialog (`useConfirm`)** — promise-based modal with `default` / `danger` variants, keyboard support (Enter confirms, Escape cancels), focus on Cancel for danger variant (safer default). Replaces all `window.confirm()` usage.
16+
- **Agent avatars in `/topics` list** — threads now show the assigned agent's avatar (24px, same as the sidebar) instead of a generic green chat icon, matching the visual language of `ThreadsSidebar`. Shared `AgentIcon` component extracted from the sidebar for reuse.
17+
18+
### Changed
19+
20+
- **47 UX call sites migrated from native dialogs to in-app components** across `AgentChat`, `ChatSessionList`, `Backups`, `Heartbeats`, `Roles`, `Scheduler`, `Systems`, `Tasks`, `TicketDetail`, `Topics`, `Triggers`, `Users`. All messages translated to pt-BR where they were in English.
21+
- **Provider config centralized** (PR #30) — shared `provider-config.js` helper in the terminal-server centralizes loading, env var allow-listing, and model capability detection (`code` vs `chat`). Reduces duplication between `chat-bridge.js` and `claude-bridge.js`.
22+
- **Chat uses OpenAI-compatible streaming for non-Anthropic providers** (PR #30) — `/chat/completions` streaming so chat-completion style models (GPT, Gemini, custom OmniRouter) work in dashboard Chat. Anthropic keeps the existing Agent SDK flow.
23+
- **Terminal enforces code-only models for non-Anthropic providers** (PR #30) — chat-completion models are now blocked in the Terminal with a clear error directing the user to the Chat instead.
24+
- **Telegram notification helper for ADW routines** (PR #30) — `run_skill(..., notify_telegram=True)` appends a deterministic notification instruction to the skill prompt so end-of-day and good-morning routines emit exactly one Telegram message via the MCP `reply()` call. `ADWs/runner.py` also exposes a `send_telegram()` helper that posts directly via Bot API as a fallback.
25+
26+
### Fixed
27+
28+
- **Archive thread endpoint — 500 on re-archive**`shutil.move` was raising `OSError` when `memory/threads/_archive/{ticket_id}/` already existed from a previous partial archive. Now checks for existing path and falls back to a timestamped suffix; tombstone write is best-effort and wrapped in try/except; the endpoint surfaces a proper JSON error instead of a bare 500.
29+
- **Docker dashboard container starts reliably** (PR #30) — `npm install --legacy-peer-deps` in `Dockerfile.swarm.dashboard` avoids peer-dep install failures on fresh rebuilds (same pin already applied to the non-Docker install).
30+
831
## [0.29.1] - 2026-04-23
932

1033
Patch iterating on the v0.29.0 thread-areas feature: UI rebrand, navigation polish, and fixes identified by the post-release verification pass.

Dockerfile.swarm.dashboard

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
FROM node:22-alpine AS frontend-build
2323
WORKDIR /frontend
2424
COPY dashboard/frontend/package.json dashboard/frontend/package-lock.json* ./
25-
RUN npm install
25+
RUN npm install --legacy-peer-deps
2626
COPY dashboard/frontend/ ./
2727
RUN npm run build
2828

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@evoapi/evo-nexus",
3-
"version": "0.29.1",
3+
"version": "0.29.2",
44
"description": "Unofficial open source toolkit for Claude Code — AI-powered business operating system",
55
"keywords": [
66
"claude-code",

config/providers.example.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@
2222
"default_model": "anthropic/claude-sonnet-4",
2323
"requires_logout": true
2424
},
25+
"omnirouter": {
26+
"name": "OMNIROUTER",
27+
"description": "OpenAI-compatible custom provider (URL, API Key e Model).",
28+
"cli_command": "openclaude",
29+
"env_vars": {
30+
"CLAUDE_CODE_USE_OPENAI": "1",
31+
"OPENAI_BASE_URL": "",
32+
"OPENAI_API_KEY": "",
33+
"OPENAI_MODEL": ""
34+
},
35+
"default_base_url": "",
36+
"default_model": "",
37+
"requires_logout": true
38+
},
2539
"openai": {
2640
"name": "OpenAI (API Key)",
2741
"description": "GPT-4.x / GPT-5.x via API Key da OpenAI",

dashboard/backend/routes/tickets.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -870,16 +870,27 @@ def archive_thread(ticket_id: str):
870870
return jsonify({"error": "already_archived"}), 409
871871

872872
src_dir = WORKSPACE / "memory" / "threads" / ticket_id
873-
archive_dir = WORKSPACE / "memory" / "threads" / "_archive" / ticket_id
873+
archive_root = WORKSPACE / "memory" / "threads" / "_archive"
874+
archive_dir = archive_root / ticket_id
875+
876+
archive_root.mkdir(parents=True, exist_ok=True)
874877

875878
if src_dir.exists():
876-
archive_dir.parent.mkdir(parents=True, exist_ok=True)
877-
shutil.move(str(src_dir), str(archive_dir))
879+
# If archive_dir already exists (previous partial archive), rename with timestamp suffix
880+
if archive_dir.exists():
881+
archive_dir = archive_root / f"{ticket_id}.{_now().replace(':', '-').replace('.', '-')}"
882+
try:
883+
shutil.move(str(src_dir), str(archive_dir))
884+
except OSError as exc:
885+
return jsonify({"error": "archive_move_failed", "message": str(exc)}), 500
878886
# Write tombstone
879-
(archive_dir / "ARCHIVED.json").write_text(
880-
json.dumps({"ticket_id": ticket_id, "archived_at": _now(), "archived_by": current_user.username}),
881-
encoding="utf-8",
882-
)
887+
try:
888+
(archive_dir / "ARCHIVED.json").write_text(
889+
json.dumps({"ticket_id": ticket_id, "archived_at": _now(), "archived_by": current_user.username}),
890+
encoding="utf-8",
891+
)
892+
except OSError:
893+
pass # tombstone is best-effort
883894

884895
now = _now()
885896
old_status = ticket.status

dashboard/frontend/src/components/AgentChat.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useRef, useState, useCallback } from 'react'
2+
import { useToast } from './Toast'
23
import Markdown from './Markdown'
34
import { AgentAvatar } from './AgentAvatar'
45
import { useNotifications } from '../context/NotificationContext'
@@ -79,6 +80,7 @@ type Status = 'idle' | 'connecting' | 'running' | 'error'
7980

8081
export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', externalLoading = false, externalError = null, onPendingCountChange, onNeedsAttention, workingDir: _workingDir, threadTicketId: _threadTicketId, onTurnCompleted }: AgentChatProps) {
8182
const { dismissBySession } = useNotifications()
83+
const toast = useToast()
8284
const [messages, setMessages] = useState<ChatMessage[]>([])
8385
const [input, setInput] = useState('')
8486
const [status, setStatus] = useState<Status>('idle')
@@ -363,7 +365,7 @@ export default function AgentChat({ agent, sessionId, accentColor = '#00FFA7', e
363365
const ticket = await res.json()
364366
await bindTicket(ticket.id)
365367
} catch (err: any) {
366-
alert(err?.message || 'Failed to create ticket')
368+
toast.error('Falha ao criar ticket', err?.message)
367369
}
368370
}, [agent, bindTicket])
369371

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getAgentMeta } from '../lib/agent-meta'
2+
3+
interface AgentIconProps {
4+
agent: string
5+
size?: number
6+
}
7+
8+
export function AgentIcon({ agent, size = 24 }: AgentIconProps) {
9+
const meta = getAgentMeta(agent)
10+
const initials = agent
11+
.split('-')
12+
.slice(0, 2)
13+
.map(p => p[0]?.toUpperCase() ?? '')
14+
.join('')
15+
16+
if (meta.avatar) {
17+
return (
18+
<img
19+
src={meta.avatar}
20+
alt={agent}
21+
width={size}
22+
height={size}
23+
className="rounded object-cover flex-shrink-0"
24+
style={{ width: size, height: size }}
25+
onError={e => {
26+
;(e.currentTarget as HTMLImageElement).style.display = 'none'
27+
}}
28+
/>
29+
)
30+
}
31+
32+
return (
33+
<div
34+
className="rounded flex items-center justify-center flex-shrink-0 text-[9px] font-bold"
35+
style={{
36+
width: size,
37+
height: size,
38+
backgroundColor: `${meta.color}22`,
39+
color: meta.color,
40+
border: `1px solid ${meta.color}40`,
41+
}}
42+
>
43+
{initials}
44+
</div>
45+
)
46+
}

dashboard/frontend/src/components/ChatSessionList.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useRef, useState } from 'react'
2+
import { useConfirm } from './ConfirmDialog'
23
import {
34
MessageSquare,
45
Plus,
@@ -62,6 +63,7 @@ export default function ChatSessionList({
6263
onArchive,
6364
onDelete,
6465
}: ChatSessionListProps) {
66+
const confirm = useConfirm()
6567
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
6668
const [editingId, setEditingId] = useState<string | null>(null)
6769
const [editName, setEditName] = useState('')
@@ -136,9 +138,15 @@ export default function ChatSessionList({
136138
if (onArchive) onArchive(session.id, !session.archived)
137139
}
138140

139-
function handleDelete(session: ChatSession) {
141+
async function handleDelete(session: ChatSession) {
140142
setContextMenu(null)
141-
if (!window.confirm(`Deletar a conversa "${session.name}"? Esta ação não pode ser desfeita.`)) return
143+
const ok = await confirm({
144+
title: 'Deletar conversa',
145+
description: `Deletar a conversa "${session.name}"? Esta ação não pode ser desfeita.`,
146+
confirmText: 'Deletar',
147+
variant: 'danger',
148+
})
149+
if (!ok) return
142150
if (onDelete) onDelete(session.id)
143151
}
144152

0 commit comments

Comments
 (0)