From d5628f754d85c85c3ca6ba4303df25919f7933c5 Mon Sep 17 00:00:00 2001 From: Viet Tran Date: Thu, 9 Apr 2026 21:29:48 +0700 Subject: [PATCH] =?UTF-8?q?feat(v3):=20core=20architecture=20redesign=20?= =?UTF-8?q?=E2=80=94=20pipeline,=20memory,=20vault,=20evolution,=20provide?= =?UTF-8?q?rs,=20orchestration=20(#792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ci): skip CI condition in semantic-release for main branch go-semantic-release auto-detects the default branch from GitHub API (which is dev), but releases are cut from main. The CI condition rejects runs on non-default branches. Use --no-ci to bypass this check since the workflow already gates on push to main. * docs: document CI/CD pipelines, release flow, and v2.66.0 changelog - CLAUDE.md: add CI/CD & Releases section with workflow table, tag patterns, Docker variants, beta/desktop release commands - CONTRIBUTING.md: expand Releases section with standard (auto), beta (manual tag), and desktop release workflows - docs/17-changelog.md: add v2.66.0 entry covering IDOR fix, BytePlus provider, per-agent grants, beta pipeline, and CI fixes * fix(telegram): handle group-to-supergroup migration seamlessly When a Telegram group upgrades to a supergroup, the chat ID changes and all existing references become stale. This caused send failures (400), orphaned sessions, and required manual re-pairing. Add dual-path migration handling: - Proactive: intercept inbound MigrateToChatID before isServiceMessage - Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry DB migration updates in a single transaction (scoped by tenant + channel): - paired_devices: sender_id, chat_id - sessions: session_key, user_id - channel_contacts: sender_id - channel_pending_messages: history_key Also invalidates in-memory caches (approvedGroups, pairingReplySent, groupHistory) and handles media sends via migration retry in Send(). * fix(tools): quote-aware shell operator detection in credentialed exec (#700) (#702) * fix(tools): quote-aware shell operator detection in credentialed exec (#700) - Replace detectShellOperators with detectUnquotedShellOperators in credentialed exec path — respects single/double quoting so that characters like | inside argument values (e.g. --jq '.[0] | .name') are not falsely flagged as shell operators - Pass raw command string (preserving quotes) to executeCredentialed instead of reconstructing from parsed args - Downgrade "no credential found" log from Warn to Debug (fires for every non-credentialed command, too noisy at Warn) - Add extractUnquotedSegments() helper with comprehensive tests * fix(tools): handle backslash escape outside quotes in shell operator detection extractUnquotedSegments did not handle \ as an escape character outside of quotes, causing \" to incorrectly enter double-quote mode. This hid subsequent shell operators from detection (e.g. gh \"arg\" | env would not detect the unquoted pipe). Add backslash escape handling in the unquoted state to match go-shellwords parsing behavior. Both \ and the escaped character are emitted as unquoted content so operator detection still catches them. --------- Co-authored-by: viettranx * feat(infra): tracing recovery, browser cleanup, CLI fixes, UI workspace split (#709) - Tracing: recover stale running traces/spans on startup (PG + SQLite) - Browser: Chrome orphan cleanup via launcher PID, timeouts, Leakless - Claude CLI: WaitDelay 5s + context-cancel early exit - Agent loop: safety-net defer to finalize orphan root traces - UI: split workspace sharing into separate Memory and KG toggles - Minor: for-range idiom, min() builtin * fix(prompt): skip credentialed CLI context when exec tool is denied Agents with exec in their deny list cannot run CLI commands, so injecting wrangler/gh credential context is misleading — the LLM sees instructions for tools it cannot use. Gate the section on exec being present in the filtered tool list. * fix(ui): improve traces table layout and readability Compact columns: status as icon-only, merge time+duration into one column. Truncate long user IDs, clean tags from preview, move badges to second line. * fix(security): harden exec path exemption matching - Add absolute path exemption for dataDir/skills-store/ (fixes skill scripts using absolute paths like /app/data/skills-store/ being denied) - Strip surrounding quotes before prefix matching (LLMs often quote paths) - Reject path traversal ("..") in exempt fields to prevent escape - Switch from "any field exempt → skip" to per-field matching: only exempt if ALL fields that match the deny pattern are individually exempt - Closes pipe/comment bypass vectors where an exempt path in one argument would exempt the entire command including non-exempt paths Includes 27 test cases covering: legitimate access, quoted paths, path traversal, unicode bypass, pipe/comment bypass, mixed args. * feat(whatsapp): add native WhatsApp channel with whatsmeow (#720) Replace Node.js Baileys bridge with native go.mau.fi/whatsmeow — zero external dependencies. QR auth, media support, markdown formatting, typing indicators, dual JID/LID identity, group policies, pairing. Resolves #703 * fix(chat): load message history on first conversation click (#730) * fix(chat): load message history when selecting existing conversation from clean state The skipNextHistoryRef was unconditionally set when sessionKey transitioned from empty to non-empty. This prevented loadHistory() from running when clicking an existing conversation from the initial /chat page. The skip was only intended for the new-chat send flow where the optimistic message is already displayed. Guard the skip with expectingRunRef so it only activates when a message send is in flight. Closes #729 * docs: add UI diff evidence for PR #730 Before/after screenshots and HTML comparison report showing first conversation click behavior fix. * fix(permissions): use cron-specific permission check for cron tool (#725) * fix(security): harden exec path exemption matching (#721) - Add absolute path exemption for dataDir/skills-store/ (fixes skill scripts using absolute paths like /app/data/skills-store/ being denied) - Strip surrounding quotes before prefix matching (LLMs often quote paths) - Reject path traversal ("..") in exempt fields to prevent escape - Switch from "any field exempt → skip" to per-field matching: only exempt if ALL fields that match the deny pattern are individually exempt - Closes pipe/comment bypass vectors where an exempt path in one argument would exempt the entire command including non-exempt paths Includes 27 test cases covering: legitimate access, quoted paths, path traversal, unicode bypass, pipe/comment bypass, mixed args. * fix(permissions): use cron-specific permission check for cron tool Cron tool was hardcoded to check `file_writer` configType via CheckFileWriterPermission(), ignoring the `cron` configType that the UI actually saves when granting cron permissions. This caused agents in group chats to be denied cron access even with correct permission configured. Add ConfigTypeCron constant and CheckCronPermission() that checks `cron` configType first, falling back to `file_writer`. --------- Co-authored-by: Viet Tran * fix(exec): allow uploaded files in active workspaces (#748) Shell-aware command parsing, dynamic workspace exemptions, and symlink canonicalization for exec path denial. Fixes #739. * refactor(exec): extract path exemption logic to separate file Move shell-aware parsing, dynamic workspace exemptions, path alias variants, and canonicalization functions from shell.go (688 LOC) to shell_path_exemption.go (284 LOC) for maintainability. * feat(agent): centralized tenant user identity resolution for credentials Add CredentialUserID context key that resolves channel contacts to merged tenant users for credential lookups (SecureCLI, MCP). Keeps UserID unchanged for session/workspace scoping. Resolves group senders, group contacts, and unresolved DMs via ContactStore with 60s TTL cache. * fix(ui): improve traces table column layout Add width constraints and whitespace-nowrap to prevent column wrapping on narrow viewports. Cherry-picked from dev-v3. * fix(ui): enable search filtering on knowledge graph view (#758) Search box on /knowledge-graph only filtered table view. Add useMemo client-side filtering of graph entities by name/type/description, only show relations where both endpoints match. Closes #759 * feat: add zoom controls to knowledge graph (#757) Add +/- buttons and percentage display to the knowledge graph stats bar so users can zoom without relying solely on mouse wheel. Uses existing react-force-graph-2D zoom() API with 1.5x step and 300ms animation. Closes #755 Co-authored-by: Claude Opus 4.6 * fix(providers): use DB name for ClaudeCLI, ACP, and Anthropic registration Provider Name() methods returned hardcoded strings, so DB-registered providers with custom names got wrong registry key — causing "provider not found" fallback. Add WithClaudeCLIName/WithACPName/WithAnthropicName options, pass p.Name from DB registration paths. Config-based paths keep hardcoded defaults. Closes #742 * fix(agent): correct soft-trim head/tail budget allocation when tail is important (#723) Co-authored-by: quxy5 * feat(v3): core architecture redesign — pipeline, memory, vault, evolution, providers, orchestration (#790) * feat(v3): add core interface contracts and migration for v3 redesign Foundation interfaces: TokenCounter, WorkspaceContext, DomainEventBus, ProviderAdapter/Capabilities. Pipeline: Stage, RunState, MessageBuffer, substates, Pipeline orchestrator. Memory: EpisodicStore, AutoInjector, KG temporal extensions, consolidation workers. System integration: PromptConfig, ToolCapability, Retriever. Orchestration: OrchestrationMode, EvolutionMetrics/SuggestionStore. Migration 000037: episodic_summaries, evolution tables, KG temporal columns. Schema version 36→37. * refactor(plans): mark all v3 design phases complete with file references * fix(v3): address code review findings on design contracts - C1: add missing l0_abstract column to episodic_summaries migration - C2: align EpisodicSummary ID/TenantID/AgentID to uuid.UUID - H1: document tenant_id scoping requirement on EpisodicStore - H2: add UNIQUE constraint on (agent_id, user_id, source_id) for dedup - H4: clarify ProviderAdapter vs Provider relationship in doc - M3: set state.ExitCode on BreakLoop/AbortRun in pipeline - M6: store full PipelineConfig in Pipeline struct - Edge: add WHERE embedding IS NOT NULL on HNSW index * fix(v3): second-pass review fixes - H1: use context.WithoutCancel for finalize + set ExitCode on ctx cancel - H2: use utf8.RuneCountInString consistently in FallbackCounter - H3: longest-prefix-match in ModelContextWindow (prevents wrong tokenizer) - H4: return unsubscribe cleanup func from consolidation.Register * feat(v3): implement DomainEventBus with worker pool, dedup, and retry Worker pool processes events from buffered channel. SourceID-based dedup prevents duplicate processing. Exponential backoff retry on handler error. Panic recovery per handler. Graceful shutdown via Drain(). 8/8 tests pass with race detector. * feat(v3): implement ProviderAdapter for Anthropic, OpenAI, DashScope, Codex Add CapabilitiesAware to all 6 providers. Create ProviderAdapter implementations that delegate to existing buildRequestBody/parseResponse for DRY. ClaudeCLI and ACP get capabilities only (subprocess transport). DashScope wraps OpenAI adapter with StreamWithTools=false override. * feat(v3): implement WorkspaceContext Resolver for 6 scenarios Stateless resolver produces immutable WorkspaceContext at run start. Handles personal/group/predefined/team-shared/team-isolated/delegation. Wired into loop_context.go behind v3PipelineEnabled flag (additive, v2 path unchanged). Includes delegation path boundary check, master tenant bypass, and tenant slug path composition. * feat(v3): implement tiktoken TokenCounter with BPE encoding + cache Adds tiktoken-go for accurate cl100k_base/o200k_base token counting. Per-message FNV-1a hash cache avoids re-encoding unchanged history. Falls back to rune/3 heuristic for unknown models. NewTokenCounter factory selects implementation at build time. * feat(v3): promote 12 other_config JSONB fields to dedicated agent columns Extract emoji, agent_description, thinking_level, max_tokens, self_evolve, skill_evolve, skill_nudge_interval, reasoning_config, workspace_sharing, chatgpt_oauth_routing, shell_deny_groups, and kg_dedup_config from the catch-all other_config JSONB into proper columns with DB-level types and defaults. - Migration: PG (000037) + SQLite (schema v6→7) with backfill - Go: AgentData struct + simplified Parse* methods - Store: SELECT/INSERT/scan updated for both PG and SQLite - Gateway: create/update handlers accept promoted fields - HTTP: export/import with legacy backward compat - Web UI: all 15 frontend files read/write from top level * feat(v3): implement Knowledge Vault with unified search, wikilinks, and FS sync Migration 000038 adds vault_documents (FTS+pgvector), vault_links, vault_versions tables. VaultStore interface with PG implementation for document CRUD, hybrid FTS+vector search, and bidirectional link management. All queries enforce tenant_id isolation including JOIN-based scoping on link operations. FS sync layer: SHA-256 content hashing, VaultInterceptor hooks into write_file/ read_file for auto-registration and lazy sync, fsnotify watcher with 500ms debounce. Wikilink engine parses [[target]] syntax, resolves targets via 3-step strategy, and maintains vault_links on write. VaultSearchService fans out queries across vault, episodic, and KG stores in parallel with per-source score normalization and weighted merge. AutoInjector and Retriever implementations for pipeline integration. Three agent tools: vault_search (unified discovery), vault_link (explicit linking), vault_backlinks (dependency tracing). Feature-flagged via v3_vault_enabled agent setting. * feat(v3): wire vault into gateway startup + add unit tests Wire VaultStore embedding provider, VaultSearchService, VaultInterceptor on read/write tools, and register vault_search/vault_link/vault_backlinks tools in gateway_vault_wiring.go. All wiring gated by stores.Vault != nil. Add 28 unit tests for ContentHash, ContentHashFile, and ExtractWikilinks covering edge cases, unicode, display text, context windows, and offsets. * feat(v3): implement stage-based pipeline loop with 8 pluggable stages Decompose monolithic agent loop into internal/pipeline/ package: - 6 stages: Context, Think, Prune+MemoryFlush, Tool, Observe+Checkpoint, Finalize - Foundation types: Stage interface, RunState with 7 typed substates, MessageBuffer - Pipeline orchestrator with setup/iteration/finalize 3-phase execution - Callback-based PipelineDeps avoids circular import with agent package - Feature-flagged via v3PipelineEnabled in Loop.Run() - All 7 exit conditions preserved (no tools, max iter, truncation, loop kill, read-only streak, tool budget, ctx cancel) * feat(v3): wire pipeline callbacks to Loop methods + add 71 unit tests Wire 15 of 17 PipelineDeps callbacks from Loop methods via closures: - Context: LoadContextFiles, BuildMessages, EnrichMedia, InjectReminders - Think: BuildFilteredTools, CallLLM (stream/sync) - Prune: PruneMessages, CompactMessages - Memory: RunMemoryFlush - Finalize: SanitizeContent, FlushMessages, UpdateMetadata, BootstrapCleanup, MaybeSummarize - Remaining: ExecuteToolCall, CheckReadOnly (deep loop.go integration) Add comprehensive test suite (71 tests, all passing with -race): - MessageBuffer: 10 tests (append, flush, replace, counts) - Pipeline.Run: 14 tests (3-phase flow, exit conditions, ctx cancel) - Stage tests: 47 tests (ThinkStage nudges/truncation, PruneStage budget, ToolStage parallel/exit, ObserveStage content, CheckpointStage interval, FinalizeStage cleanup) * feat(v3): wire remaining 2 callbacks (ExecuteToolCall, CheckReadOnly) Complete callback wiring — 17/17 PipelineDeps callbacks now active: - ExecuteToolCall: resolves tool name, executes via registry, processes result via existing processToolResult with loop detection bridge - CheckReadOnly: delegates to checkReadOnlyStreak via bridge runState - Bridge runState shares loop detection state between pipeline and agent * fix(v3): eliminate data race in tool execution + capture injected messages - Remove parallel tool execution path — serialize all tool calls to avoid data races on shared bridgeRS (loop detector, media results, deliverables) - Loop kill checked after each tool (mid-batch early exit) - BuildFilteredTools: capture and append injected tool-awareness messages - Rename test to reflect sequential execution * feat(v3): wire ResolveWorkspace, safe parallel tools, ContextStage tests - Wire ResolveWorkspace callback via workspace.NewResolver() with ResolveParams from Loop fields (no longer a nil stub) - Re-add safe parallel tool execution: split into ExecuteToolRaw (parallel I/O) + ProcessToolResult (sequential state mutation) with opaque rawData pass-through (no double execution) - Add 12 unit tests for ContextStage (8) + MemoryFlushStage (3) - Split tool callbacks to loop_pipeline_tool_callbacks.go (under 200 lines) - Capture buildFilteredTools injected messages * feat(v3): add episodic memory store + temporal KG columns Phase 1 — Episodic Store: - Migration 000039: episodic_summaries table with pgvector, FTS, L0 abstracts - EpisodicStore PG impl: CRUD, hybrid FTS+vector search, ExistsBySourceID, PruneExpired. Idempotent via source_id UNIQUE constraint. Phase 2 — Temporal KG: - Migration 000040: valid_from/valid_until on kg_entities + kg_relations, partial indexes for current-facts queries, epoch→timestamptz backfill - ListEntitiesTemporal: current-only, point-in-time, or include-expired modes - SupersedeEntity: atomic expire-old + insert-new in single transaction Schema version bumped to 40. * fix(v3): review fixes for episodic store + temporal KG - C1: Fix column name mismatch turn_count vs message_count in Go SQL - C2: Remove redundant migration 000040 (000037 already adds temporal KG columns) - H1: Use time.Time not int64 for TIMESTAMPTZ columns in SupersedeEntity - H2: Add tenant_id scoping to Get/Delete for tenant isolation - M2: Fix scanEntityTemporal to convert TIMESTAMPTZ→UnixMilli correctly - L1: Remove unused uuid import from episodic_search.go - Schema version corrected to 39 (only 000039 is new) * feat(v3): implement consolidation pipeline with 3 event-driven workers Event chain: session.completed → EpisodicWorker → episodic.created → SemanticWorker → entity.upserted → DedupWorker - EpisodicWorker: reuses compaction summary or calls LLM, generates L0 abstract (extractive), idempotent via source_id check - SemanticWorker: extracts KG facts from episodic summary via existing Extractor, sets temporal valid_from, publishes entity.upserted - DedupWorker: runs DedupAfterExtraction on new entity IDs (terminal) - L0 abstract: sentence-based extraction (~50 tokens), no LLM needed - All workers registered via DomainEventBus.Subscribe() * feat(v3): implement progressive loading with L0 auto-inject + unified search - AutoInjector: searches episodic store, builds L0 prompt section (~200 tokens), skips trivial messages via stopword filter - L1Cache: in-memory LRU (500 entries, 1h TTL) for structured overviews - UnifiedSearch: cross-tier search merging episodic + document results by score - ContextStage integration: AutoInject callback appends memory section to system prompt - MemorySection field added to ContextState for observability * feat(v3): add memory_expand tool for L2 episodic retrieval New tool: memory_expand(id) returns full episodic summary with metadata. Complements memory_search L0/L1 results with deep L2 access. Nil-safe: returns error message when episodic store not available. Gateway wiring + memory_search depth param + kg_search temporal param deferred to runtime integration phase. * feat(v3): complete Phase 5 — tool extensions + gateway wiring - memory_search: add depth param + episodic tier search merged with docs - kg_search: add as_of temporal param, use ListEntitiesTemporal - memory_expand: registered in gateway startup - Gateway: Episodic field in Stores, PGEpisodicStore in factory, embedding provider wired, tools connected to episodic store * fix(v3): Phase 3 review fixes — tenant isolation + AutoInject args - C1: Add tenant_id filter to ftsSearch, vectorSearch, List queries (prevents cross-tenant episodic memory leaks) - C2: Fix AutoInject callback signature — agent/tenant captured by closure, only userMessage + userID passed explicitly - H1: Add tenant_id to List query * feat(v3): wire per-agent v3 flags from DB into dual-mode gate Parse v3_pipeline_enabled, v3_memory_enabled, v3_retrieval_enabled from agent other_config JSONB via ParseV3Flags(). Resolver now sets all flags on LoopConfig so the existing gate in loop_run.go reads from DB. - V3Flags struct + ParseV3Flags() + ValidateV3Flags() in store layer - v3MemoryEnabled/v3RetrievalEnabled added to Loop, LoopConfig, PipelineConfig - Auto-inject gated on V3RetrievalEnabled (was unconditional) - Structured perf logging for v3 pipeline runs - v3 flag validation on both WS agent.update and HTTP PUT endpoints * feat(v3): wire AutoInjector into pipeline for L0 memory auto-inject Create AutoInjector at gateway startup from episodic store, pass through ResolverDeps → LoopConfig → Loop. Pipeline adapter builds AutoInject callback capturing agent/tenant context via closure. ContextStage already gates on V3RetrievalEnabled + AutoInject != nil. * feat(v3): add tool metadata map + capability-based deny rules Registry gains per-tool ToolMetadata map with RegisterWithMetadata() and GetMetadata() (infers defaults from tool name when not explicit). PolicyEngine gains DenyCapability() for RBAC integration — tools with denied capabilities filtered at step 8 after existing 7-step pipeline. * fix(v3): add RWMutex to PolicyEngine capability deny fields DenyCapability() and SetRegistry() now guarded by sync.RWMutex. FilterTools reads snapshot under RLock. Prevents data race when capability rules are modified concurrently with tool filtering. * feat(v3): implement delegate tool for inter-agent task delegation New `delegate` tool wraps existing agent_links infrastructure (CanDelegate, DelegateTargets). Supports async (fire-and-forget) and sync (block with timeout) modes. Permission checked via AgentLinkStore. Events emitted: delegate.sent/completed/failed. DelegateRunFunc injected by gateway to avoid circular dependency. * feat(v3): complete 3 deferred implementations 1. OrchestrationMode resolution: ResolveOrchestrationMode() checks team membership → delegate links → spawn (priority order). 2. PG EvolutionMetricsStore: RecordMetric, QueryMetrics, aggregate tool/retrieval metrics, TTL cleanup. All queries tenant-scoped. 3. BridgePromptBuilder: implements PromptBuilder interface by delegating to existing BuildSystemPrompt(). Appends v3 memory L0 section when enabled. Ready for template engine swap later. * fix(v3): address code review findings on commits 5-6 - C1: CanDelegate now tenant-scoped (fail-closed on missing tenant) - H1: Sync delegate timeout capped at 600s - H2: Async goroutine gets 10min deadline (prevents leaks) - H3: JSONB casts use COALESCE/NULLIF guards (handles missing fields) - M1/M2: Remove dead code (formatVaultSection, memoryL0ToStrings) * fix(teams): stop auto-creating agent_links for team members Teams use agent_team_members table directly — agent_links caused context confusion between team dispatch and delegation systems. - Remove autoCreateTeamLinks() calls from team create + member add - Remove link cleanup from member remove - Remove dead autoCreateTeamLinks() function - Append DELETE to migration 000039: clear team-created agent_links * fix(v3): tenant isolation for all agent_links queries + PromptBuilder Instructions - DelegateTargets, GetLinkBetween, SearchDelegateTargets, SearchDelegateTargetsByEmbedding, DeleteTeamLinksForAgent all now scoped by tenant_id (fail-closed on missing tenant) - BridgePromptBuilder now maps Instructions/InstructionContent to AGENTS.md context file (was silently dropped) * feat(v3): wire orchestration mode + evolution metrics into agent loop - Orchestration mode: resolver resolves mode from team/links, tool filter hides delegate/team_tasks based on mode, prompt builder injects delegation targets section - Evolution metrics: non-blocking goroutine records tool execution metrics (name, success, duration) via EvolutionMetricsStore in both v2 loop and v3 pipeline paths (sequential + parallel) - Fix review findings: tenant ID propagated via store.WithTenantID in background goroutine, 5s timeout prevents goroutine leak * feat(v3): implement suggestion engine with pluggable analysis rules - PG EvolutionSuggestionStore: CRUD for agent_evolution_suggestions table - SuggestionEngine: aggregates 7-day metrics, runs rules, deduplicates pending suggestions per type before creating new ones - 3 initial rules: LowRetrievalUsage (usage_rate<0.2), ToolFailure (success_rate<0.1), RepeatedTool (>100 calls/week → suggest skill) - EventSuggestionCreated event type added to eventbus - Cron wiring deferred to gateway startup integration pass * feat(v3): implement auto-adapt guardrails with apply/rollback - AdaptationGuardrails: max delta per cycle, min data points, locked params, rollback-on-drop percentage - ApplySuggestion: applies threshold suggestions to agent other_config JSONB, stores baseline for rollback - RollbackSuggestion: restores baseline values from suggestion params - EvaluateApplied: compares post-apply metrics to baseline, auto-rolls back when quality drops beyond threshold - Scope limited to retrieval params only (never security settings) * feat(v3): wire evolution stores + daily/weekly cron for suggestions - Add EvolutionMetrics + EvolutionSuggestions to Stores struct + PG factory - Wire EvolutionMetricsStore into ResolverDeps (cmd/gateway_managed.go) - Add gateway_evolution_cron.go: daily suggestion analysis + weekly evaluation/rollback for applied suggestions - Cron runs as background goroutine with 5-min timeout per cycle * fix(v3): address code review findings on evolution engine - C1: persist baseline parameters before marking suggestion as applied (was building map but never saving — rollback would always fail) - H1: add tenant_id isolation to UpdateSuggestionStatus, GetSuggestion, and new UpdateSuggestionParameters method * test(v3): add unit tests for orchestration, suggestions, guardrails, prompt - orchestration_mode_test: orchModeDenyTools (4 modes) + ResolveOrchestrationMode (4 scenarios with mock stores) - suggestion_rules_test: LowRetrievalUsage, ToolFailure, RepeatedTool with threshold boundary tests (at/below/above min data points) - evolution_guardrails_test: DefaultGuardrails values + CheckGuardrails (insufficient data, locked params, zero-min fallback) - prompt_builder_orchestration_test: BridgePromptBuilder orchestration section presence/absence across 4 scenarios + target content verification * test(v3): add integration tests for evolution metrics + suggestions - Test helper: shared PG connection with sync.Once migration, per-test tenant+agent seed with cleanup - Evolution metrics: RecordMetric, AggregateToolMetrics (success rate), Cleanup (TTL deletion) - Evolution suggestions: full CRUD, UpdateSuggestionParameters (baseline persist), tenant isolation (cross-tenant read blocked) - Pipeline E2E: seed 25 failed tools + 55 low-usage retrievals, verify SuggestionEngine creates suggestions, verify dedup on second run - Fix: migration 039 de-duped (episodic_summaries already in 037) - Fix: NULL reviewed_by scan via sql.NullString * feat(v3): add HTTP API handlers for evolution, vault, episodic, orchestration, v3-flags 5 new handler files exposing v3 backend stores as REST endpoints: - evolution_handlers.go: metrics query/aggregate + suggestions CRUD - vault_handlers.go: cross-agent document listing + search + links - episodic_handlers.go: episodic summaries list + hybrid search - orchestration_handlers.go: computed mode + delegate targets (read-only) - v3_flags_handlers.go: per-agent v3 feature flag get/toggle Store fixes from code review: - episodic FTS: use inline to_tsvector (no stored tsv column) - episodic: conditional user_id filter in List + Search (admin view) - episodic: add tenant_id to ExistsBySourceID + PruneExpired - evolution: require tenant_id in context (no struct fallback) - evolution: check RowsAffected on suggestion updates - vault: optional agent_id filter in ListDocuments (cross-agent) * feat(v3): add web UI for evolution tab, v3 settings, vault page, episodic memory Agent Detail enhancements: - V3 Settings section: pipeline/memory/retrieval flag toggles - Orchestration section: mode badge + delegate targets display - Evolution section: added metrics + suggestions v3 flag toggles - Evolution tab: Recharts metrics charts + suggestion review table with approve/reject/rollback actions + guardrails card New pages: - /vault: Knowledge Vault document registry with cross-agent listing, hybrid search dialog, document detail with wikilinks - Memory page: added Episodic Memory tab with summary cards, expandable details, key topic badges, and hybrid search Infrastructure: - HttpClient: added patch() method - Query keys: v3Flags, orchestration, evolution namespaces - 4 new hooks: use-v3-flags, use-orchestration, use-evolution-metrics, use-evolution-suggestions, use-vault, use-episodic - i18n: vault namespace (en/vi/zh), agents + memory keys updated - Reused formatRelativeTime from lib/format.ts (eliminated 3 duplicates) * refactor(http): add bindJSON helper and migrate all decode call sites Replace 36 json.NewDecoder(r.Body).Decode + error blocks with bindJSON across 20 HTTP handler files. Standardizes decode error responses to structured writeError format. Fixes unchecked decode in handleIndexAll. * refactor(store): adopt sqlx for PG scan operations (Phase 1+2) Add jmoiron/sqlx v1.4.0 with camelToSnake json tag mapper. Migrate scan-heavy PG store methods to sqlx Get/Select: - tracing.go: GetTrace, ListTraces, ListChildTraces, GetTraceSpans, GetCostSummary - heartbeat.go: Get, ListDue, ListLogs - providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders - mcp_servers.go: GetServer, GetServerByName, ListServers - pairing.go: ListPending, ListPaired - agents_export_queries.go: 5 export functions - agents_export_team_queries.go: exportTeamMembers, ExportAgentLinks All writes (INSERT/UPDATE/DELETE), execMapUpdate, and dynamic WHERE builders remain raw SQL. Zero behavior change. * refactor(store): adopt sqlx for SQLite scan operations (Phase 3) Migrate SQLite store scan methods to sqlx Get/Select: - providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders - tenants.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser, ListUsers, ListUserTenants - mcp_servers.go: GetServer, GetServerByName, ListServers Create sqlx_scan_structs.go with sqliteTime-aware scan structs (providerRow, tenantRow, tenantUserRow, mcpServerRow) to handle SQLite TEXT timestamp parsing via StructScan. * refactor(store): migrate PG bulk scan operations to sqlx (Phase 4) Migrate scan-heavy methods across 6 PG store files: - tenant_store.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser, ListUsers, ListUserTenants — removed 3 scan helpers - teams.go: ListTeams, GetTeam, ListMembers, ListMembersByTenant - teams_tasks_activity.go: ListComments, ListEvents, ListFollowUps - pending_message_store.go: ListPending, ListByHistoryKey - skills_grants.go: ListAgentGrants - config_permissions.go: CheckPermission ~20 scan ops converted. Files with encryption post-processing, pq.Array, pgvector, or dynamic SQL kept raw. * refactor(store): extract shared CamelToSnake mapper, add UUIDArray usage note - Move camelToSnake to internal/store/column_mapper.go (DRY) - Both pg and sqlitestore packages now import shared CamelToSnake - Add planned-use comment on UUIDArray type * refactor(cli): migrate commands from config.json to HTTP API, add providers/setup/TUI - Add unified HTTP client (gateway_http_client.go) with auth, error parsing, typed generics - Rewrite agent list/add/delete to use gateway HTTP API instead of config.json - Rewrite channels list to HTTP API, add channels add/delete subcommands - Replace models command with full providers CRUD (list/add/update/delete/verify) - Add setup wizard command (provider → agent → channel post-onboard flow) - Add Bubble Tea TUI behind build tag (tui/!tui with noop fallback) - Update onboard next-steps to mention goclaw setup - Add build-tui Makefile target - Fix URL path injection (url.PathEscape on all user-supplied path segments) - Fix UTF-8 truncation in skills description display * refactor(store): add explicit db struct tags, fix sqlx mapper for heartbeat scan error Switch sqlx mapper from NewMapperFunc (which only applies CamelToSnake to field names, not tag values) to NewMapperFunc("db", CamelToSnake) with explicit db:"column_name" tags on all store structs. Root cause: NewMapperFunc("json", fn) sets mapFunc but not tagMapFunc, so camelCase json tags like "agentId" were used as-is instead of being converted to "agent_id", causing "missing destination name" scan errors. Fix: use db struct tags as the source of truth for column mapping. Every DB entity field gets db:"column_name", nested JSON configs and runtime-only structs get db:"-". * test(store): add integration tests for 13 store interfaces (70 tests) Cover Tier 1 (critical) + Tier 2 (security) stores with integration tests running against pgvector pg18. Coverage from 2.4% to ~54%. Stores tested: Session, Agent, Team/Task, Memory, KnowledgeGraph, Vault, MCP Server, API Key, ConfigPermission, Contact. Infrastructure: fixture builders (seedTeam, seedMCPServer, etc.), mock EmbeddingProvider, multi-tenant helpers, expanded cleanup. * fix(store): resolve NULL scan bugs in MCP server and task metadata - mcp_servers: COALESCE nullable TEXT columns (display_name, command, url, api_key, tool_prefix) to prevent sqlx scan failures - mcp_servers_access: COALESCE nullable JSONB columns in ListAgentGrants (tool_allow, tool_deny, config_overrides) to prevent silent row drops - teams_tasks: default task metadata to '{}' instead of nil to satisfy NOT NULL constraint on CreateTask - sqlx_helpers: export InitSqlx for integration test setup * feat(pipeline): fix v3 pipeline context injection, tracing, KG temporal filters - Pipeline context: add InjectContext + LoadSessionHistory callbacks to ContextStage, propagate enriched ctx via state.Ctx for iteration stages - Pipeline tracing: wrap makeCallLLM with emitLLMSpanStart/End, wrap makeExecuteToolCall/Raw with emitToolSpanStart/End - Token counter: switch pipeline from FallbackCounter to TiktokenCounter - KG temporal: add valid_until IS NULL filter to all entity/relation queries (list, search, vector, FTS, traversal CTE, stats) - Skills: add SkillEmbedder interface for future hybrid BM25+vector search - Cache: remove unused tenantResolve dead code from PermissionCache - Store: fix NULL scan bugs in tracing metadata and agent skill_nudge - Test: add TestStoreKG_TemporalFilter integration test - UI: add v3 version badge, evolution section, memory/traces improvements * refactor(store): migrate KG store from raw sql.Rows to sqlx StructScan Migrate 6 knowledge graph store files from manual rows.Scan() to pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs. - Add entityRow, relationRow, traversalRow, dedupCandidateRow structs with json.RawMessage for jsonb and time.Time for timestamptz columns - Add toEntity()/toRelation() converters (UnixMilli + json.Unmarshal) - Add sqlxTx() helper for wrapping *sql.Tx with sqlx mapper - Fix ScanDuplicates passing time.Now().Unix() to TIMESTAMPTZ column - Fix ListEntitiesTemporal missing tenant scope (scopeClause) - Fix SupersedeEntity missing tenant scope and tenant_id on INSERT - Fix DedupCandidate.CreatedAt using Unix() instead of UnixMilli() - Update agents_export_queries.go to reuse new scan row structs - Net -160 lines of manual scan boilerplate removed * refactor(store): migrate memory, skills, agents, sessions, mcp, cron, vault stores to sqlx Batch migration of 19 store files from raw rows.Scan() to pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs. Groups migrated: - Memory: memory_docs, memory_admin, memory_search, memory_embedding_cache - Episodic: episodic_search, episodic_summaries - Skills: skills, skills_admin, skills_embedding, skills_export_queries - Agents: agents (backfill+shares), agents_context, agents_export_team_standalone - Sessions: sessions_list (List, ListPaged, ListPagedRich) - MCP: mcp_servers_access, mcp_export_queries - Cron: cron_exec (GetRunLog) - Vault: vault_documents (ListDocuments, ftsSearch, vectorSearch) - Tenant: tenant_configs (ListDisabled, ListAll) 7 new scan row files created. Net -510 lines of manual scan boilerplate. INSERT/UPDATE/DELETE and scalar COUNT queries kept as raw SQL. * fix(store): fix 3 sqlx scan struct db tag issues found by audit - Fix vault FTS alias mismatch: `AS rank` → `AS score` (critical: runtime scan error) - Fix episodic key_topics type: json.RawMessage → pq.StringArray (TEXT[] column) - Fix agentShareRow.CreatedAt: string → time.Time, wire to output struct * feat(providers): implement Wave 2 provider resilience and intelligence 9-phase implementation covering: - Request middleware chain with composable body transformers - OpenAI prompt caching, service tier, and fast mode middlewares - Error classification (9 categories) with two-tier failover - Model registry with forward-compat resolvers (Anthropic + OpenAI) - Embedding providers (OpenAI + Voyage) with 1536-dim validation - Cooldown/probe system with per-provider:model state tracking - Markdown-aware chunking shared across 5 channels - Session recall via FTS + pgvector on episodic summaries - Dreaming/promotion pipeline for long-term memory consolidation Migrations: 000040 (episodic search index), 000041 (promoted_at column) Schema version: 39 → 41 * feat(providers): wire model registry into gateway provider construction Create InMemoryRegistry with Anthropic + OpenAI forward-compat resolvers at gateway startup. Pass to all Anthropic and OpenAI providers created from both config and DB sources. * feat(consolidation): wire DomainEventBus and consolidation pipeline Create DomainEventBus at gateway startup, thread through resolver → LoopConfig → Loop → PipelineDeps. Emit session.completed event after each run finalization. Register consolidation pipeline (episodic → semantic → KG dedup → dreaming) with event bus subscriptions. * fix(store): fix episodic key_topics pq.Array, ON CONFLICT, and migration 040 immutability - episodic_summaries.go Create: json.Marshal(KeyTopics) → pq.Array (text[] column) - episodic_search.go scanEpisodic/scanEpisodicRow: json.RawMessage → pq.StringArray - episodic_summaries.go Create: ON CONFLICT add WHERE source_id IS NOT NULL for partial index - migration 040: add immutable_array_to_string wrapper (array_to_string is STABLE in PG) * test(store): add 17 integration tests for skills, cron, episodic, tenant configs - Skills store: 6 tests (CRUD, grants, tenant isolation) - Cron store: 4 tests (job CRUD, run log sqlx scan, pagination, tenant isolation) - Episodic store: 4 tests (summary CRUD, list, FTS search, tenant isolation) - Tenant configs: 3 tests (tool/skill disable, list, tenant isolation) - Test helper: add cleanup for skills, cron, episodic tables * fix(permissions): use cron-specific permission check for cron tool (#725) * fix(security): harden exec path exemption matching (#721) - Add absolute path exemption for dataDir/skills-store/ (fixes skill scripts using absolute paths like /app/data/skills-store/ being denied) - Strip surrounding quotes before prefix matching (LLMs often quote paths) - Reject path traversal ("..") in exempt fields to prevent escape - Switch from "any field exempt → skip" to per-field matching: only exempt if ALL fields that match the deny pattern are individually exempt - Closes pipe/comment bypass vectors where an exempt path in one argument would exempt the entire command including non-exempt paths Includes 27 test cases covering: legitimate access, quoted paths, path traversal, unicode bypass, pipe/comment bypass, mixed args. * fix(permissions): use cron-specific permission check for cron tool Cron tool was hardcoded to check `file_writer` configType via CheckFileWriterPermission(), ignoring the `cron` configType that the UI actually saves when granting cron permissions. This caused agents in group chats to be denied cron access even with correct permission configured. Add ConfigTypeCron constant and CheckCronPermission() that checks `cron` configType first, falling back to `file_writer`. --------- Co-authored-by: Viet Tran * fix(chat): load message history on first conversation click (#730) * fix(chat): load message history when selecting existing conversation from clean state The skipNextHistoryRef was unconditionally set when sessionKey transitioned from empty to non-empty. This prevented loadHistory() from running when clicking an existing conversation from the initial /chat page. The skip was only intended for the new-chat send flow where the optimistic message is already displayed. Guard the skip with expectingRunRef so it only activates when a message send is in flight. Closes #729 * docs: add UI diff evidence for PR #730 Before/after screenshots and HTML comparison report showing first conversation click behavior fix. * feat(whatsapp): port native WhatsApp channel with whatsmeow from dev Cherry-pick 0db1e93a with manual conflict resolution: - cmd/channels_cmd.go: kept dev-v3 HTTP API approach (not config-based) - go.mod/go.sum: merged deps, ran go mod tidy * feat(ui): v3 web UI enhancement — branded loading, rich markdown, vault graph, sidebar polish - Branded loading: HTML pre-loader with logo pulse/shimmer, fade-out on app ready, PageLoader logo swap - Rich markdown: wikilinks, mermaid (lazy-loaded), math/KaTeX, callouts/admonitions plugins - Vault graph: force-directed document graph view with table/graph toggle - Agent filters: type filter (open/predefined) on agents page - Sidebar: tenant name + role badge in footer - Query keys: add vault + episodic entries * fix(ui): address code review — mermaid XSS, Safari compat, cache key - Change mermaid securityLevel from "loose" to "strict" (XSS prevention) - Add requestIdleCallback fallback for Safari < 17 - Fix vault all-links query key to use sorted doc IDs (stale cache fix) - Remove dead abortRef from MermaidBlock - Add prefers-reduced-motion to loader animation * docs: update CLAUDE.md with v3 architecture + complete changelog - Add 10 new v3 internal modules to project structure (pipeline, eventbus, consolidation, tokencount, vault, workspace, etc.) - Add native WhatsApp channel, edition system, providerresolve, updater to structure - Update Key Patterns with 8 v3-specific patterns (pipeline, eventbus, 3-tier memory, vault, evolution, orchestration, middleware) - Add comprehensive V3 Redesign section to changelog covering: - 8-stage pipeline with dual-mode gate - DomainEventBus + consolidation workers - 3-tier memory (working/episodic/semantic) - Knowledge Vault with wikilinks - Self-evolution engine - Orchestration + delegate tool - WorkspaceContext resolver - ModelRegistry + provider adapter - Feature flags - Request middleware - sqlx migration + 70+ integration tests * fix(security): harden file path validation, tenant isolation, and tool access - handleSign: validate path within workspace/dataDir before signing HMAC token - handleSign: enforce tenant-scoped restriction for RBAC-enabled editions - handleServe: add workspace/dataDir boundary check for ft= signed requests - handleServe: remove cross-tenant findInWorkspace fallback for ft= requests - TenantDataDir/TenantWorkspace: guard against empty slug resolving to parent - exec tool: add tenants/ to AllowPathExemptions for tenant skill execution - list_files: add AllowPaths support and wire skills directory access * docs(v3): update 16 docs + add 2 new docs for v3 architecture Update all docs/ to reflect v3 implementation (64 commits, 31K insertions): - 00: architecture overview with 7 new packages - 01: agent loop with pipeline, orchestration, evolution - 02: providers with Wave 2 resilience (middleware, failover, registry) - 03: tools with delegate, vault_search, vault_link, memory_expand - 04,08,10,11,14,23: targeted v3 additions - 06: store with 6 new tables, promoted columns, sqlx - 07: memory with 3-tier architecture, consolidation pipeline - 18,19: HTTP/WS API with v3 endpoints and methods - 21: evolution system (metrics, suggestions, auto-adapt) - model-steering: model registry relationship New docs: - 22-v3-http-endpoints.md: 12 v3 HTTP endpoints - 24-knowledge-vault.md: vault architecture, wikilinks, search * feat(v3): wire delegate tool, fix pipeline callbacks, clean dead code Wire delegate tool end-to-end: - Register DelegateTool in gateway with DelegateRunFunc - Implement runFn: resolve target agent, build session key, propagate tracing - Add async announce via msgBus (reuses subagent announce handler) - Populate context fields (TenantID, Channel, ChatID) for routing - Set DelegationID + ParentAgentID on RunRequest for event correlation Fix pipeline callbacks: - BreakLoop now completes remaining stages in current iteration - EnrichMedia signature updated to use RunState for message buffer access - Add non-streaming event emission for channel compatibility - Fix user message flush tracking in v3 pipeline Clean dead code (510 LOC removed): - Delete memory/l1_cache.go, unified_search.go (superseded by vault search) - Delete vault/auto_injector_impl.go, retriever_impl.go (never wired) - Delete vault/sync_worker.go (never started) - Remove orphaned EventMemoryLint, EventSuggestionCreated constants * feat(v3): finalize stage — emit session.completed, NO_REPLY, strip directives - Emit session.completed event for consolidation pipeline (episodic → semantic → dreaming) - Detect NO_REPLY before flush so silent content is persisted for context - Strip [[...]] message directives from user-facing content (v2 parity) - Wire StripMessageDirectives, IsSilentReply, EmitSessionCompleted callbacks * feat(vault): full CRUD — backend endpoints, UI dialogs, content preview Backend (5 new HTTP endpoints): - POST/PUT/DELETE /v1/agents/{id}/vault/documents — create, update, delete - POST/DELETE /v1/agents/{id}/vault/links — create, delete - Server-side validation for doc_type and scope enums - Agent ownership verification on link creation - FK cascade handles link cleanup on document delete Frontend (React): - Create document dialog (title, path, type, scope) - Edit mode in detail dialog (inline title/type/scope editing) - Delete document with confirmation - Create link dialog (from/to doc, link type, context) - Delete link with inline confirmation on badges - Content preview (collapsible, lazy-loads via /v1/storage/files/) - Mutation hooks with query invalidation - i18n keys for en/vi/zh * fix(v3): critical pipeline parity fixes — ChatRequest, reasoning, passback, media - Enrich ChatRequest with all provider options (temperature, sessionKey, agentID, userID, channel, workspace, tenantID) matching v2 - Add ResolveReasoningDecision for thinking models (o3, DeepSeek-R1, Kimi) - Wire uniquifyToolCallIDs to prevent OpenAI 400 on duplicate IDs - Add assistant message passback (Phase, RawAssistantContent) for Anthropic - Emit block.reply for intermediate content (non-streaming channels) - Add ContentSuffix append + ForwardMedia merge in FinalizeStage - Build final assistant message with MediaRefs for session persistence - Use effectiveMaxTokens() + OptMaxTokens constant - Guard truncation retry on len(ToolCalls) > 0 + parseError check - Accumulate ThinkingTokens in usage - Deduplicate emitRun closure (shared from callbackSet) - Fix EnrichMedia to receive RunState with actual messages - Persist user message in makeFlushMessages (matching v2) * fix(store): coerce NOT NULL JSONB columns to empty object on agent update When switching provider away from ChatGPT OAuth, the UI sends chatgpt_oauth_routing: null which violates the NOT NULL constraint. Coerce null → '{}' for all NOT NULL JSONB promoted columns: chatgpt_oauth_routing, reasoning_config, workspace_sharing, shell_deny_groups, kg_dedup_config. * feat(v3): v3 info modal redesign, agent links CRUD tab, sidebar rename - Rewrite v3 info modal with 8 feature cards (pipeline, memory, retrieval, vault, evolution, orchestration, resilience, registry) with v2→v3 comparisons, stat badges, full i18n (en/vi/zh) - Add tabbed layout to teams page: Agent Teams | Agent Links tabs - Agent Links tab with full CRUD via existing WS RPC methods (list/create/edit/delete) with Radix Select, Combobox, mutual agent exclusion in create dialog - Sidebar menu renamed to "Agent Link & Team" - Backend: add source_display_name, source_emoji, target_emoji to agent link joined queries for consistent display * fix(store): include personal chats in cron delivery targets Add 'user' contact_type to ListDeliveryTargets SQL filter in both PG and SQLite stores. Previously only group/topic contacts appeared in cron channel/chat dropdowns. * fix(v3): duplicate messages, missing thinking, span numbering - ThinkStage: skip AppendPending for final answer (no tool calls), let FinalizeStage build the definitive message with sanitization and MediaRefs — fixes duplicate assistant messages in session history - RunResult: add Thinking field, propagate through v2 finalizeRun, v3 convertRunResult, run.completed event, and chat.send response - UI: capture thinkingRef before clearing on run.completed, include in final message object so thinking renders without page refresh - Span numbering: pass Iteration+1 in v3 callback to match v2's 1-based iteration display in trace span names * fix(v3): wire delegation targets into BuildSystemPrompt buildOrchestrationSection() was implemented and tested but never wired into the actual BuildSystemPrompt() flow. Only the unused BridgePromptBuilder had it. Add DelegateTargets + OrchMode fields to SystemPromptConfig and inject "## Delegation Targets" section so agents with agent_links see their delegation targets in prompt. * fix(v3): sync mediaResults from bridgeRS to pipeline state syncBridgeToState copied loopKilled, asyncToolCalls, deliverables from the v2 bridgeRS but missed mediaResults. Tool results with MEDIA: prefix were extracted by processToolResult into bridgeRS but never propagated to state.Tool.MediaResults — causing FinalizeStage to produce empty MediaRefs and RunResult.Media. * fix(v3): populate SessionCompletedPayload in session.completed events Both v2 loop and v3 pipeline emitted session.completed events with nil Payload, causing episodicWorker type assertion to fail silently. Episodic summaries were never created. - Expand EmitSessionCompleted callback to pass msgCount, tokensUsed, compactionCount from pipeline state - V3 path: build payload from state.Messages.TotalLen(), state.Think.TotalUsage, state.Compact.CompactionCount - V2 path: build payload from history + rs.totalUsage + sessions.GetCompactionCount() * fix(v3): remaining pipeline parity gaps — skill postscript, team task count - Add SkillPostscript callback to PipelineDeps + FinalizeStage, matching v2's skill evolution nudge after complex tool runs - Wire makeSkillPostscript() in adapter with same logic as v2 (skillEvolve + skillNudgeInterval + totalToolCalls threshold) - Sync teamTaskCreates from bridgeRS to pipeline EvolutionState (was already syncing teamTaskSpawns but missed creates) * fix(evolution): add JSON struct tags to metric aggregates ToolAggregate and RetrievalAggregate had no JSON tags, causing Go to marshal field names as PascalCase (ToolName, CallCount) while the UI expects snake_case (tool_name, call_count). Charts rendered empty containers with no data bars. Also change AvgDuration (time.Duration) to AvgDurationMs (float64) for JSON-friendly serialization — time.Duration marshals as nanoseconds which is unusable in frontend. * fix(v3): add debug logging to episodic worker for consolidation pipeline Add INFO/WARN logs at entry, summary decision, and creation points to diagnose why episodic_summaries table stays empty in production. * chore: silence noisy tenant_cache debug logs * refactor(ui): replace v3 settings section with engine version picker + tabbed info modal Replace flat toggle list with radio-style version cards (v2/v3) and feature mini-cards. Redesign v3 info modal from single scroll to 3-tab layout (Core Engine, Memory & Knowledge, Orchestration) with Lucide icons and v2→v3 comparison cards. - Add batchUpdate to use-v3-flags hook for atomic v2 switch - Create engine-version-section with VersionCard + FeatureMiniCard - Create v3-info-modal/ with 5 modular components (<45 LOC each) - Add i18n keys (detail.engine + v3Info.tabs) for en/vi/zh - Wire into agent-overview-tab, agent-header, agent-card - Delete v3-settings-section.tsx + agent-v3-info-modal.tsx * feat(v3): pass media files through delegate tool results Delegate tool now carries media files (images, audio, etc.) produced by the delegatee back to the parent agent. DelegateRunFunc returns DelegateResult{Content, Media} instead of plain string. - Add DelegateResult struct with Content + Media fields - Convert agent.MediaResult to bus.MediaFile in gateway wire - Attach media to sync result and async announce message - Add MediaCount to DelegateCompletedPayload for observability - Set MetaParentAgent in async announce metadata * merge: bring main bug fixes into dev-v3 Cherry-pick 8 commits from origin/main: - fix(telegram): handle group-to-supergroup migration (#698) - feat(providers): add OpenRouter identification headers (#705) - fix: deterministic prompt ordering for LLM cache hit (#719) - fix(security): harden exec path exemption matching (#721) - fix: invalidate storage size cache on delete and move (#726) - fix: use errors.Is() for sentinel comparisons (#727) - fix(desktop): add defaultValues to form dialogs (#737) - Release: credential resolver, WhatsApp native, exec hardening (#754) Conflict resolution: - store/pg exports: accept main's errors.Is() additions - shell.go: accept main's extracted matchesAnyPathExemption helper - gateway_providers: merge both WithAnthropicName + WithAnthropicRegistry - loop_types + resolver: merge v3 fields (OrchMode, DelegateTargets, EvolutionMetricsStore) with main's new UserResolver/ContactStore - gateway_setup: keep dev-v3's tenant-scoped path exemptions - channels_cmd: keep dev-v3's HTTP API approach * feat(v3): add foundation packages for architecture refactor (Phase 1) Purely additive — zero changes to existing files. Creates shared types and helpers that Phase 2-4 will migrate callers to: - internal/store/base/: Dialect interface, BuildMapUpdate, nullable/JSON helpers, scope clause builder, table metadata (44 tests) - internal/orchestration/: ChildResult capture from v2/v3, media type conversion with round-trip tests (10 tests) - internal/providers/sse_reader.go: Shared SSE scanner replacing inline bufio.Scanner boilerplate in 3 providers (8 tests) * refactor(store): unify pg/ and sqlitestore/ helpers via base/ package (Phase 2) - Create PG and SQLite dialect implementations (base.Dialect interface) - Replace 15 duplicate helpers in pg/helpers.go with aliases to base.* - Replace 13 duplicate helpers in sqlitestore/helpers.go with aliases - Rewrite execMapUpdate and execMapUpdateWhereTenant to use base.BuildMapUpdate with dialect-specific placeholders - Rewrite scopeClause/scopeClauseAlias as thin wrappers around base.BuildScopeClause - Remove duplicate execMapUpdateWhereTenant from pg/agents.go pg/helpers.go: 267→175 LOC, sqlitestore/helpers.go: 226→130 LOC * refactor(orch): extract BatchQueue[T] generic for announce queues (Phase 3) - Create internal/orchestration/batch_queue.go: generic producer-consumer queue replacing duplicated sync.Map+mutex pattern (10 tests, race-safe) - Simplify cmd/gateway_announce_queue.go: team queue uses BatchQueue, removes announceQueueState/getOrCreate/drain/tryFinish (~40 LOC saved) - Simplify cmd/gateway_subagent_announce_queue.go: subagent queue uses BatchQueue, same pattern reduction (~40 LOC saved) - Update cmd/gateway_consumer_handlers.go: callers use new signatures * fix: remove defer accumulation in announce loop, clean comment tombstone - Remove `defer ptd.ReleaseTeamLock()` inside for loop that accumulated deferred calls per iteration (explicit ReleaseTeamLock already called) - Remove dead comment tombstone in pg/agents.go * fix: scope defer in announce loop via closure for panic safety Wrap announce processing in closure so defer ptd.ReleaseTeamLock() runs once per iteration instead of accumulating. Explicit release still called for normal path; defer catches panics. * refactor(providers): wire shared SSEScanner into 3 providers (Phase 4b) Replace inline bufio.Scanner+SSE parsing boilerplate with shared SSEScanner from sse_reader.go in: - openai.go: data-only SSE (OpenAI, DashScope, Kimi) - codex.go: data-only SSE (OpenAI Codex) - anthropic_stream.go: event+data SSE (uses EventType() for switch) Removes ~24 LOC of duplicated scanner setup + manual line parsing. * refactor(agent): force v3 pipeline, remove v2 runLoop (Phase 4A) - Delete runLoop() from loop.go (~745 LOC removed), keep shared helpers (resolveToolCallName, hasParseErrors, truncateToolArgs) - Remove v2/v3 gate in loop_run.go: always call runViaPipeline() - Remove v3PipelineEnabled field from Loop struct + LoopConfig + resolver - Always resolve workspace in loop_context.go (was behind v3 gate) - Deprecate PipelineEnabled in V3Flags (kept for backward compat parsing) All agents now always use v3 pipeline. No behavioral change for agents that already had v3_pipeline_enabled=true (which was all production agents). * refactor(gateway): decompose gateway.go from 1295 to 476 LOC (Phase 4B) Extract sections of runGateway() into focused files: - gateway_deps.go: gatewayDeps struct for shared state - gateway_http_wiring.go: wireHTTPHandlersOnServer (~207 LOC) - gateway_events.go: event subscribers + teamTaskEventType (~367 LOC) - gateway_lifecycle.go: signal handling, shutdown, server start (~222 LOC) - gateway_tools_wiring.go: cron/heartbeat/session tool wiring (~116 LOC) Also extracted: startCronAndHeartbeat, makeDelegateAnnounceCallback. Pure structural refactoring — no behavior change. * test(agent): add v3 force migration guard tests Verify v2 runLoop is deleted, V3PipelineEnabled field removed from LoopConfig, and V3Flags backward compat parsing still works. Compile-time guards prevent accidental re-introduction of v2 code. * feat(delegate): wire ChildResult + fix media passthrough (Phase 3 gap) - Use orchestration.CaptureFromRunResult in delegate run callback to standardize result capture via ChildResult - Use MediaResultToBusFiles in CaptureFromRunResult (DRY) - Fix delegate_tool.go metadata key: delegate_id → delegation_id * refactor(ui): remove v2/v3 pipeline toggle, always show V3 (Phase 4E) - engine-version-section.tsx: remove pipeline toggle, show V3 read-only - use-agent-version.ts: always return "v3" (no flag check) - use-v3-flags.ts: remove v3_pipeline_enabled from toggleable flags - Update i18n strings (en/vi/zh): remove v2-specific tooltip text * test: add Phase 5 test infrastructure for v3 architecture refactor - pg/pg_dialect_test.go: 4 tests for PG Dialect (placeholder, transform, returning, interface compliance) - sse_reader_test.go: 4 edge case tests (empty data, scanner error, event type persistence, no data after [DONE]) - gateway_announce_format_test.go: 10 tests for team + subagent announce formatting (single/batch, success/failure, snapshot) Coverage: base/ 96%, orchestration/ 100%, providers/ 57% * chore: remove stale runLoop references + apply go fix (Phase 6) - Update comments referencing deleted runLoop in pipeline callbacks, loop_types, stage.go - go fix: reflect.TypeFor, range over int, strings.Builder * docs: update architecture docs for v3 refactor completion (Phase 6) - CLAUDE.md: add store/base/, orchestration/ to project structure; remove v2 runLoop and dual-mode gate references; rename Pipeline (v3) to Pipeline; add SSEScanner and BatchQueue to key patterns - docs/00-architecture-overview.md: update module map with new packages, remove [V3] markers (now standard) - docs/17-changelog.md: add V3 Architecture Refactor entry (6 phases) * feat(evolution): skill draft auto-generation + go fix cleanup - Add skill draft template generation from evolution suggestions - Wire skill apply endpoint in evolution HTTP handlers - Apply go fix across codebase (range-over-int, reflect.TypeFor, etc.) - Minor refactors: simplify switch/case, reduce string builder allocs - Gateway deps: add skills loader field * perf(prompt): deterministic tool order + Anthropic cache boundary split Sort tool names in buildToolingSection for cache-stable output. Insert GOCLAW_CACHE_BOUNDARY marker before Time section; Anthropic provider splits system prompt into 2 blocks (stable cached, dynamic not). Backward compat: no marker → single cached block. * perf(prompt): optimize cache boundary position + add Execution Bias Move Memory Recall and stable context files (AGENTS.md, TOOLS.md, USER_PREDEFINED.md) above cache boundary. Dynamic per-user files (USER.md, BOOTSTRAP.md) stay below. Add Execution Bias section (full mode only) forcing action-oriented tool use. Fix duplicate header when Project Context split across boundary. * feat(prompt): add PromptMode task/none + 3-layer resolution Expand PromptMode from full|minimal to full|task|minimal|none. Task mode = enterprise automation: keeps Tooling, Execution Bias, Safety-slim, Persona-slim, Skills-search, MCP-search, Memory-slim, Workspace, Runtime, Delegation. Drops verbose sections (Tool Call Style, Self-Evolution, Spawning, Recency, etc.). None mode returns identity line only. 3-layer mode resolution: runtime override > auto-detect (subagent/cron) > agent config (other_config.prompt_mode) > default (full). * feat(prompt): provider prompt contributions (stable/dynamic/overrides) Add PromptContribution struct + PromptContributor interface for provider-specific prompt customizations. Providers can inject StablePrefix (before cache boundary), DynamicSuffix (after boundary), or override sections by ID (e.g. execution_bias). Nil-safe: providers that don't implement the interface get default behavior. * feat(prompt): pinned skills with hybrid inline+search mode Add per-agent pinned_skills config (max 10, from other_config JSONB). Pinned skills always inline in prompt via BuildPinnedSummary. Non-pinned discovered via skill_search. Hybrid section shows both pinned XML and search instructions. Works in full and task modes. * fix(prompt): validate prompt_mode from DB before cast Reject unknown prompt_mode values from OtherConfig JSONB (e.g. typos like "taks") by checking against validPromptModes set. Invalid values default to "" (full mode). Prevents broken prompts where no mode flags match. * fix(prompt): wire SectionIDToolCallStyle for provider override Tool Call Style section now uses sectionContent() like Execution Bias, allowing providers to override it via PromptContribution.SectionOverrides. * feat(store): implement 9 SQLite store backends for v3 parity Close feature gap between PostgreSQL and SQLite (desktop/lite) editions. 96 methods across 9 stores: AgentLinks, SubagentTasks, SecureCLI, SecureCLIGrants, EvolutionMetrics, EvolutionSuggestions, Episodic, KnowledgeGraph, Vault. Schema v8→v9 adds 4 tables. Key design decisions: - LIKE-based search replaces tsvector/FTS5 (unavailable in modernc.org/sqlite) - Go-side StringSimilarity for KG dedup (replaces pgvector cosine) - Recursive CTE traversal with comma-delimited path cycle detection - AES-256-GCM encryption on all SecureCLI read/write paths - F15: SecureCLI disabled when EncryptionKey empty - ON CONFLICT DO UPDATE (never INSERT OR REPLACE) to preserve FK cascades * docs(store): add SQLite parity section to store data model docs Document 9 new SQLite store implementations, schema v9, and feature parity gaps (LIKE vs FTS, Jaro-Winkler vs vector dedup). * fix(docker): skip web-builder stage when ENABLE_EMBEDUI=false Use BuildKit conditional stage pattern so web-builder is not executed when embedding is disabled. Also update pnpm-lock.yaml for 6 new deps that were missing from the lockfile (markdown/math/mermaid packages). * feat(vault): embed metadata.summary for richer vector search Include summary from metadata JSONB in embedding text (title + path + summary) for better semantic search. Update tsvector generated column to include summary in FTS index. Zero new columns — summary stored in existing metadata JSONB field. Backward compat: docs without summary still embed title+path only. * fix(vault-ui): wider detail dialog, markdown rendering, create tooltip - Detail dialog: sm:max-w-lg → sm:max-w-2xl, content area 200→300px - Content preview: render markdown via MarkdownRenderer instead of
- Create button: add tooltip explaining "select agent first" when disabled

* feat(ui): prompt_mode dropdown + pinned_skills multi-select

Add PromptSettingsSection to agent overview tab:
- prompt_mode: Select dropdown (full/task/minimal/none)
- pinned_skills: Tag input with max 10, click to remove
Both save to agent other_config JSONB via existing onUpdate flow.
Self-contained save button appears only when values change.

* fix(vault-ui): enlarge dialog to max-w-4xl, constrain markdown heading sizes

Dialog sm:max-w-2xl → sm:max-w-4xl. Markdown headings capped at
text-base/text-sm via Tailwind child selectors to prevent oversized
h1/h2 in content preview. Content area raised to 400px.

* refactor(ui): move pinned skills to dedicated section with skill select

Split pinned_skills out of PromptSettingsSection into PinnedSkillsSection.
Uses useAgentSkills hook for proper dropdown with granted skills list.
Placed right below SkillsSection in agent overview tab. Badge chips
with X to remove, Select dropdown to add. Max 10 enforced.

* fix(ui): add border wrap to capabilities section for visual consistency

* feat(ui): redesign prompt mode as compact cards, replace v3 badge

- Rewrite prompt settings from select dropdown to 2×2 compact cards
  with lucide icons (Zap/Wrench/Package/CircleOff) and ring selection
- Move prompt settings to top of agent overview tab
- Replace v3 badge with prompt mode badge in header, card, and list row
- Add i18n keys for prompt mode labels/descriptions (en/vi/zh)
- Extract readPromptMode() to shared agent-display-utils
- Remove dead useAgentVersion hook and v3Tooltip/v2Tooltip i18n keys
- Remove v3 badge from EngineVersionSection (keep feature toggles)
- Use cn() for badge class composition with twMerge safety

* feat(ui): add section tags and token estimates to prompt mode cards

Each card now shows which system prompt sections are included
(Persona, Tools, Safety, Skills, MCP, Memory, etc.) as compact
tags, plus estimated base token range (~2-4K for full, ~10 for none).
Helps users understand the impact of each mode at a glance.

* fix(ui): correct token estimates with actual tiktoken measurements

Measured via tiktoken (cl100k) on realistic config:
full=~1.7K+, task=~1.1K+, minimal=~660, none=~6 base tokens.
The "+" suffix indicates context files/skills/MCP add more.

* fix(ui): use tiktoken-measured token ranges for prompt mode cards

Measured across 3 scenarios (bare/typical/heavy) via tiktoken cl100k:
full=~500-2.9K, task=~350-1.2K, minimal=~350-820, none=~6 tokens.
Ranges reflect real configs from minimal (3 tools) to heavy
(10 tools, long persona, MCP, sandbox, pinned skills).

* fix(ui): use production-measured token counts for prompt mode cards

Measured via tiktoken on real production agent (tieu-ho) with
4 context files (AGENTS.md, SOUL.md, IDENTITY.md, USER_PREDEFINED.md),
14 tools, memory, KG, skills, Telegram channel:
full=~3.1K, task=~2.2K, minimal=~1.9K, none=~6 tokens.

* refactor(ui): accurate section tags per prompt mode from systemprompt.go

Section tags now match exact gating logic in BuildSystemPrompt():
- full: persona, tools, exec bias, call style, safety, skills,
  MCP, memory, sandbox, evolution, channel hints (11 sections)
- task: style echo, tools, exec bias, safety (slim), skills (search),
  MCP (search), memory (slim) (7 sections)
- minimal: tools, safety (2 sections + shared context files)
- none: no tags shown, no token count (trivially ~6 tokens)

Removed workspace/identityOnly tags (shared across all modes).

* feat(ui): replace engine version toggles with V3 capabilities modal

- Remove 3 toggle switches (Pipeline, Memory, Retrieval) from agent
  detail — all features are now always-on for v3 agents
- Replace with compact static badges layout
- New V3 Capabilities modal with 4 tabs:
  Pipeline (8-stage flow), Memory (L0/L1/L2 tiers),
  Knowledge (KG, Vault, Dreaming), Orchestration (Delegate, Evolution)
- Each tab has Lucide icons and technical descriptions of how
  features actually work
- Separate i18n namespace v3-capabilities in en/vi/zh locales

* refactor(ui): move V3 badge to agent header, remove engine version section

- Add clickable V3 badge next to agent name in header
- Clicking opens V3 Capabilities modal
- Remove Engine Version section from overview tab entirely
- Remove unused EngineVersionSection import

* feat: v3 prompt engine overhaul — 7-phase restructuring

Phase 1: Fix mode resolution — subagent/cron cap at task (not minimal),
heartbeat stays minimal, pinned skills injected in all modes, USER_PREDEFINED
added to minimal allowlist.

Phase 2: Context file restructuring — new CAPABILITIES.md (domain expertise,
separated from SOUL.md), AGENTS_MINIMAL.md for heartbeat sessions, both added
to stable context files and minimal allowlist.

Phase 3: Summoner update — all 4 prompt builders generate CAPABILITIES.md,
fallback 2-call stores capabilities alongside SOUL.md.

Phase 4: Open agent deprecation — creation silently upgrades open→predefined
in both HTTP and WS endpoints.

Phase 5: Bootstrap auto-contact — sender name from channel metadata injected
into bootstrap context for 1-turn onboarding (DM only).

Phase 6: System prompt preview — GET /v1/agents/{id}/system-prompt-preview
endpoint with mode param, token counting, section parsing.

Phase 7: Agent creation UX — removed open agent type toggle, schema simplified,
description always required, prompt mode cards updated with v3 token estimates.

* fix(vault-ui): redesign detail dialog and link dialog UX

- Vault detail: show content preview by default (not collapsed),
  move type/scope to header badges, hash to subtle footer
- Link dialog: replace native  update({ maxHistoryShare: numOrUndef(e.target.value) })}
+              className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent"
+            />
+          
+          
+ + update({ keepLastMessages: numOrUndef(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+ +
+
+ {t('configSections.compaction.memoryFlush')} +

{t('configSections.compaction.memoryFlushTip')}

+
+ update({ memoryFlush: { ...value.memoryFlush, enabled: v } })} + /> +
+ + + ) +} diff --git a/ui/desktop/frontend/src/components/agents/config-section.tsx b/ui/desktop/frontend/src/components/agents/config-section.tsx new file mode 100644 index 00000000..0f926fd1 --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/config-section.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next' +import { Switch } from '../common/Switch' + +interface ConfigSectionProps { + title: string + description: string + enabled: boolean + onToggle: (v: boolean) => void + children: React.ReactNode +} + +export function ConfigSection({ title, description, enabled, onToggle, children }: ConfigSectionProps) { + const { t } = useTranslation('agents') + + return ( +
+
+
+

{title}

+

{description}

+
+ +
+ {enabled ? ( +
{children}
+ ) : ( +

{t('config.usingGlobalDefaults')}

+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/context-pruning-section.tsx b/ui/desktop/frontend/src/components/agents/context-pruning-section.tsx new file mode 100644 index 00000000..77146510 --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/context-pruning-section.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ConfigSection } from './config-section' +import { Switch } from '../common/Switch' +import { numOrUndef } from '../../lib/format' +import type { ContextPruningConfig } from '../../types/agent' + +interface ContextPruningSectionProps { + enabled: boolean + value: ContextPruningConfig + onToggle: (v: boolean) => void + onChange: (v: ContextPruningConfig) => void +} + +function NumInput({ label, value, hint, step, onChange }: { + label: string; value?: number; hint?: string; step?: number + onChange: (v: number | undefined) => void +}) { + return ( +
+ + onChange(numOrUndef(e.target.value))} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> + {hint &&

{hint}

} +
+ ) +} + +export function ContextPruningSection({ enabled, value, onToggle, onChange }: ContextPruningSectionProps) { + const { t } = useTranslation('agents') + const [showAdvanced, setShowAdvanced] = useState(false) + + const update = (patch: Partial) => onChange({ ...value, ...patch }) + + return ( + + update({ keepLastAssistants: v })} + /> + + + + {showAdvanced && ( +
+
+ update({ softTrimRatio: v })} /> + update({ hardClearRatio: v })} /> +
+
+ update({ softTrim: { ...value.softTrim, maxChars: v } })} /> + update({ softTrim: { ...value.softTrim, headChars: v } })} /> + update({ softTrim: { ...value.softTrim, tailChars: v } })} /> +
+
+ {t('configSections.contextPruning.hardClear')} + update({ hardClear: { enabled: v } })} + /> +
+
+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/evolution-guardrails-card.tsx b/ui/desktop/frontend/src/components/agents/evolution-guardrails-card.tsx new file mode 100644 index 00000000..b9d8974d --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/evolution-guardrails-card.tsx @@ -0,0 +1,42 @@ +import { useTranslation } from 'react-i18next' +import type { AdaptationGuardrails } from '../../types/evolution' + +interface EvolutionGuardrailsCardProps { + guardrails: AdaptationGuardrails +} + +export function EvolutionGuardrailsCard({ guardrails }: EvolutionGuardrailsCardProps) { + const { t } = useTranslation('agents') + return ( +
+
+ + + +

{t('detail.evolutionTab.guardrails')}

+
+
+
+

{guardrails.max_delta_per_cycle}

+

{t('detail.evolutionTab.maxDelta')}

+
+
+

{guardrails.min_data_points}

+

{t('detail.evolutionTab.minDataPoints')}

+
+
+

{guardrails.rollback_on_drop_pct}%

+

{t('detail.evolutionTab.rollbackDrop')}

+
+
+ {guardrails.locked_params.length > 0 && ( +
+ Locked: + {guardrails.locked_params.map((p) => ( + {p} + ))} +
+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/evolution-metrics-display.tsx b/ui/desktop/frontend/src/components/agents/evolution-metrics-display.tsx new file mode 100644 index 00000000..cf5e6bde --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/evolution-metrics-display.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from 'react-i18next' +import type { ToolAggregate, RetrievalAggregate } from '../../types/evolution' + +interface EvolutionMetricsDisplayProps { + toolAggs: ToolAggregate[] + retrievalAggs: RetrievalAggregate[] + loading: boolean +} + +function BarRow({ label, value, color }: { label: string; value: number; color: string }) { + return ( +
+ {label} +
+
+
+ {value.toFixed(0)}% +
+ ) +} + +export function EvolutionMetricsDisplay({ toolAggs, retrievalAggs, loading }: EvolutionMetricsDisplayProps) { + const { t } = useTranslation('agents') + if (loading) { + return ( +
+
+
+
+ ) + } + + const noData = toolAggs.length === 0 && retrievalAggs.length === 0 + if (noData) { + return

{t('detail.evolutionTab.noMetrics')}

+ } + + return ( +
+ {toolAggs.length > 0 && ( +
+

{t('detail.evolutionTab.toolSuccess')}

+
+ {toolAggs.map((a) => ( + + ))} +
+
+ )} + + {retrievalAggs.length > 0 && ( +
+

{t('detail.evolutionTab.retrievalQuality')}

+
+ {retrievalAggs.map((a) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/evolution-section-expanded.tsx b/ui/desktop/frontend/src/components/agents/evolution-section-expanded.tsx new file mode 100644 index 00000000..8f49090d --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/evolution-section-expanded.tsx @@ -0,0 +1,107 @@ +import { useTranslation } from 'react-i18next' +import { Switch } from '../common/Switch' +import { useV3Flags } from '../../hooks/use-v3-flags' + +interface EvolutionSectionExpandedProps { + agentId: string + selfEvolve: boolean + onSelfEvolveChange: (v: boolean) => void + skillLearning: boolean + onSkillLearningChange: (v: boolean) => void + skillNudgeInterval: number + onSkillNudgeIntervalChange: (v: number) => void +} + +export function EvolutionSectionExpanded({ + agentId, selfEvolve, onSelfEvolveChange, + skillLearning, onSkillLearningChange, + skillNudgeInterval, onSkillNudgeIntervalChange, +}: EvolutionSectionExpandedProps) { + const { t } = useTranslation('agents') + const { flags, loading, toggleFlag } = useV3Flags(agentId) + + return ( +
+

{t('detail.evolution')}

+ + {/* Self-evolve toggle */} +
+
+
+ ✨ + {t('general.selfEvolution')} +
+

{t('general.selfEvolutionLabel')}

+
+ +
+ + {selfEvolve && ( +
+

{t('general.selfEvolutionInfo')}

+
+ )} + + {/* Skill learning toggle */} +
+
+
+ 📚 + {t('general.skillLearning')} +
+

{t('general.skillLearningLabel')}

+
+ +
+ + {skillLearning && ( +
+ + onSkillNudgeIntervalChange(Number(e.target.value) || 0)} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +

{t('general.skillNudgeIntervalHint')}

+
+ )} + + {/* V3 engine flags */} +
+
+

{t('general.v3Flags')}

+ {t('general.v3FlagsHint')} +
+ + {loading ? ( +
+ ) : ( +
+
+
+ {t('general.evolutionMetrics')} +

{t('general.evolutionMetricsLabel')}

+
+ toggleFlag('self_evolution_metrics', v)} + /> +
+
+
+ {t('general.evolutionSuggestions')} +

{t('general.evolutionSuggestionsLabel')}

+
+ toggleFlag('self_evolution_suggestions', v)} + /> +
+
+ )} +
+
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/evolution-suggestions-list.tsx b/ui/desktop/frontend/src/components/agents/evolution-suggestions-list.tsx new file mode 100644 index 00000000..331435bd --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/evolution-suggestions-list.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { formatRelativeTime } from '../../lib/format' +import { ConfirmDialog } from '../common/ConfirmDialog' +import type { EvolutionSuggestion } from '../../types/evolution' + +interface EvolutionSuggestionsListProps { + suggestions: EvolutionSuggestion[] + loading: boolean + onUpdateStatus: (id: string, status: 'approved' | 'rejected' | 'rolled_back') => Promise +} + +const TYPE_COLORS: Record = { + threshold: 'bg-blue-500/10 text-blue-600', + tool_order: 'bg-orange-500/10 text-orange-600', + skill_add: 'bg-green-500/10 text-green-600', + memory_prune: 'bg-surface-tertiary text-text-muted', +} + +const STATUS_COLORS: Record = { + pending: 'bg-yellow-500/10 text-yellow-600', + approved: 'bg-blue-500/10 text-blue-600', + applied: 'bg-green-500/10 text-green-600', + rejected: 'bg-red-500/10 text-red-600', + rolled_back: 'bg-surface-tertiary text-text-muted', +} + +export function EvolutionSuggestionsList({ suggestions, loading, onUpdateStatus }: EvolutionSuggestionsListProps) { + const { t } = useTranslation('agents') + const [confirm, setConfirm] = useState<{ id: string; action: 'approved' | 'rejected' | 'rolled_back' } | null>(null) + + if (loading) return
+ if (suggestions.length === 0) return

{t('detail.evolutionTab.noSuggestions')}

+ + return ( +
+

{t('detail.evolutionTab.suggestions')}

+
+ {suggestions.map((s) => ( +
+
+
+ + {s.suggestion_type} + + + {s.status} + +
+ {formatRelativeTime(s.created_at)} +
+

{s.suggestion}

+ {s.rationale &&

{s.rationale}

} +
+ {s.status === 'pending' && ( + <> + + + + )} + {s.status === 'applied' && ( + + )} +
+
+ ))} +
+ + setConfirm(null)} + title={confirm ? `${confirm.action === 'approved' ? 'Approve' : confirm.action === 'rejected' ? 'Reject' : 'Rollback'} Suggestion` : ''} + description="This action will update the suggestion status." + variant={confirm?.action === 'rejected' ? 'destructive' : 'default'} + onConfirm={async () => { + if (confirm) await onUpdateStatus(confirm.id, confirm.action) + setConfirm(null) + }} + /> +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/evolution-tab.tsx b/ui/desktop/frontend/src/components/agents/evolution-tab.tsx new file mode 100644 index 00000000..7bd9dad9 --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/evolution-tab.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useV3Flags } from '../../hooks/use-v3-flags' +import { useEvolutionMetrics } from '../../hooks/use-evolution-metrics' +import { useEvolutionSuggestions } from '../../hooks/use-evolution-suggestions' +import { EvolutionMetricsDisplay } from './evolution-metrics-display' +import { EvolutionSuggestionsList } from './evolution-suggestions-list' +import { EvolutionGuardrailsCard } from './evolution-guardrails-card' +import type { AdaptationGuardrails } from '../../types/evolution' + +interface EvolutionTabProps { + agentId: string + agentOtherConfig?: Record | null +} + +const DEFAULT_GUARDRAILS: AdaptationGuardrails = { + max_delta_per_cycle: 0.1, + min_data_points: 100, + rollback_on_drop_pct: 20, + locked_params: [], +} + +const TIME_RANGES = ['7d', '30d', '90d'] as const +type TimeRange = (typeof TIME_RANGES)[number] + +export function EvolutionTab({ agentId, agentOtherConfig }: EvolutionTabProps) { + const { t } = useTranslation('agents') + const { flags, loading: flagsLoading } = useV3Flags(agentId) + const [timeRange, setTimeRange] = useState('7d') + const { toolAggs, retrievalAggs, loading: metricsLoading } = useEvolutionMetrics(agentId, timeRange) + const { suggestions, loading: suggestionsLoading, updateStatus } = useEvolutionSuggestions(agentId) + + const guardrails = (agentOtherConfig?.evolution_guardrails as AdaptationGuardrails) ?? DEFAULT_GUARDRAILS + + // Empty state when metrics not enabled + if (!flagsLoading && !flags.self_evolution_metrics) { + return ( +
+ ✨ +

{t('detail.evolutionTab.notEnabled')}

+

{t('detail.evolutionTab.notEnabledHint')}

+
+ ) + } + + return ( +
+ {/* Time range selector */} +
+ {TIME_RANGES.map((r) => ( + + ))} +
+ + +
+ +
+ +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/orchestration-section.tsx b/ui/desktop/frontend/src/components/agents/orchestration-section.tsx new file mode 100644 index 00000000..f0663dd6 --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/orchestration-section.tsx @@ -0,0 +1,58 @@ +import { useOrchestration } from '../../hooks/use-orchestration' + +interface OrchestrationSectionProps { + agentId: string +} + +const MODE_COLORS: Record = { + spawn: 'bg-surface-tertiary text-text-muted', + delegate: 'bg-accent/10 text-accent', + team: 'bg-purple-500/10 text-purple-600', +} + +export function OrchestrationSection({ agentId }: OrchestrationSectionProps) { + const { mode, delegateTargets, team, loading } = useOrchestration(agentId) + + if (loading) return null + + return ( +
+
+
+ + + + +

Orchestration

+
+ + {mode} + +
+ + {delegateTargets.length > 0 && ( +
+

Delegate Targets

+
+ {delegateTargets.map((dt) => ( + + {dt.display_name || dt.agent_key} + + ))} +
+
+ )} + + {team && ( +
+

Team

+ {team} +
+ )} + + {delegateTargets.length === 0 && !team && mode === 'spawn' && ( +

Standard spawn mode — no delegation configured

+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/pinned-skills-section.tsx b/ui/desktop/frontend/src/components/agents/pinned-skills-section.tsx new file mode 100644 index 00000000..737e74d1 --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/pinned-skills-section.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next' +import { useAgentSkills } from '../../hooks/use-agent-skills' + +const MAX_PINNED = 10 + +interface PinnedSkillsSectionProps { + agentId: string + pinned: string[] + onPinnedChange: (pinned: string[]) => void +} + +export function PinnedSkillsSection({ agentId, pinned, onPinnedChange }: PinnedSkillsSectionProps) { + const { t } = useTranslation('agents') + const { skills, loading } = useAgentSkills(agentId) + const grantedSkills = skills.filter((s) => s.granted) + const availableSkills = grantedSkills.filter((s) => !pinned.includes(s.slug)) + + const skillName = (slug: string): string => { + const found = skills.find((s) => s.slug === slug) + return found?.name ?? slug + } + + if (loading) return
+ + return ( +
+
+ 📌 +

{t('detail.pinnedSkills.title')}

+ ({pinned.length}/{MAX_PINNED}) +
+ + {pinned.length > 0 && ( +
+ {pinned.map((slug) => ( + onPinnedChange(pinned.filter((s) => s !== slug))} + className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-md bg-surface-tertiary text-text-secondary cursor-pointer hover:bg-error/10 hover:text-error transition-colors" + > + {skillName(slug)} + + + + + ))} +
+ )} + + {pinned.length < MAX_PINNED && availableSkills.length > 0 && ( + + )} + +

{t('detail.pinnedSkills.hint')}

+
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/prompt-mode-section.tsx b/ui/desktop/frontend/src/components/agents/prompt-mode-section.tsx new file mode 100644 index 00000000..4f38de0d --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/prompt-mode-section.tsx @@ -0,0 +1,74 @@ +interface PromptModeSectionProps { + mode: string + onModeChange: (mode: string) => void +} + +const PROMPT_MODES = ['full', 'task', 'minimal', 'none'] as const + +const MODE_SECTIONS: Record = { + full: ['persona', 'tools', 'exec-bias', 'call-style', 'safety', 'skills', 'mcp', 'memory', 'sandbox', 'evolution', 'channel'], + task: ['style', 'tools', 'exec-bias', 'safety-sm', 'skill-search', 'mcp-search', 'memory-sm'], + minimal: ['tools', 'safety', 'pinned'], + none: [], +} + +const MODE_TOKENS: Record = { + full: '~3.2K', task: '~2.3K', minimal: '~1.4K', none: '~6', +} + +const MODE_ICONS: Record = { + full: '⚡', task: '🔧', minimal: '📦', none: '⛔', +} + +const MODE_DESC: Record = { + full: 'Complete system prompt with all sections', + task: 'Optimized for sub-agent and cron tasks', + minimal: 'Essential tools and safety only', + none: 'No system prompt injected', +} + +export function PromptModeSection({ mode, onModeChange }: PromptModeSectionProps) { + const sections = MODE_SECTIONS[mode] ?? [] + + return ( +
+

System Prompt Mode

+ + {/* 2x2 mode grid */} +
+ {PROMPT_MODES.map((m) => ( + + ))} +
+ + {/* Section badges */} + {sections.length > 0 && ( +
+ {sections.map((s) => ( + + {s} + + ))} +
+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/sandbox-section.tsx b/ui/desktop/frontend/src/components/agents/sandbox-section.tsx new file mode 100644 index 00000000..71899d9c --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/sandbox-section.tsx @@ -0,0 +1,99 @@ +import { useTranslation } from 'react-i18next' +import { ConfigSection } from './config-section' +import { Switch } from '../common/Switch' +import { numOrUndef } from '../../lib/format' +import type { SandboxConfig } from '../../types/agent' + +interface SandboxSectionProps { + enabled: boolean + value: SandboxConfig + onToggle: (v: boolean) => void + onChange: (v: SandboxConfig) => void +} + +export function SandboxSection({ enabled, value, onToggle, onChange }: SandboxSectionProps) { + const { t } = useTranslation('agents') + const update = (patch: Partial) => onChange({ ...value, ...patch }) + + const selectCls = 'w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent' + const inputCls = selectCls + + return ( + +
+

+ Requires Docker installed locally. Sandbox containers run on this machine. +

+
+ +
+
+ + +
+
+ + +
+
+ +
+ + update({ image: e.target.value || undefined })} + className={`${inputCls} font-mono`} + /> +
+ +
+
+ + +
+
+ + update({ timeout_sec: numOrUndef(e.target.value) })} className={inputCls} /> +
+
+ +
+
+ + update({ memory_mb: numOrUndef(e.target.value) })} className={inputCls} /> +
+
+ + update({ cpus: numOrUndef(e.target.value) })} className={inputCls} /> +
+
+ +
+ {t('configSections.sandbox.networkEnabled')} + update({ network_enabled: v })} + /> +
+
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/subagents-section.tsx b/ui/desktop/frontend/src/components/agents/subagents-section.tsx new file mode 100644 index 00000000..0a8a663f --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/subagents-section.tsx @@ -0,0 +1,80 @@ +import { useTranslation } from 'react-i18next' +import { ConfigSection } from './config-section' +import { numOrUndef } from '../../lib/format' +import type { SubagentsConfig } from '../../types/agent' + +interface SubagentsSectionProps { + enabled: boolean + value: SubagentsConfig + onToggle: (v: boolean) => void + onChange: (v: SubagentsConfig) => void +} + +export function SubagentsSection({ enabled, value, onToggle, onChange }: SubagentsSectionProps) { + const { t } = useTranslation('agents') + const update = (patch: Partial) => onChange({ ...value, ...patch }) + + return ( + +
+
+ + update({ maxConcurrent: Math.min(numOrUndef(e.target.value) ?? 2, 2) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +

Lite limit: 2

+
+
+ + +

Lite limit: 1

+
+
+ +
+
+ + update({ maxChildrenPerAgent: numOrUndef(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+
+ + update({ archiveAfterMinutes: numOrUndef(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+
+ +
+ + update({ model: e.target.value || undefined })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent font-mono" + /> +
+
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/thinking-section.tsx b/ui/desktop/frontend/src/components/agents/thinking-section.tsx new file mode 100644 index 00000000..2fd9d36d --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/thinking-section.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from 'react-i18next' +import type { ReasoningOverrideMode } from '../../types/agent' + +interface ThinkingSectionProps { + reasoningMode: ReasoningOverrideMode + thinkingLevel: string + onReasoningModeChange: (v: ReasoningOverrideMode) => void + onThinkingLevelChange: (v: string) => void +} + +const LEVELS = [ + { key: 'off', label: 'Off', desc: 'No extended thinking' }, + { key: 'low', label: 'Low', desc: '~4K token budget' }, + { key: 'medium', label: 'Medium', desc: '~10-16K token budget' }, + { key: 'high', label: 'High', desc: '~32K token budget' }, +] as const + +export function ThinkingSection({ reasoningMode, thinkingLevel, onReasoningModeChange, onThinkingLevelChange }: ThinkingSectionProps) { + const { t } = useTranslation('agents') + + return ( +
+

{t('configSections.thinking.title')}

+

{t('configSections.thinking.description')}

+ + {/* Inherit / Custom toggle */} +
+ {(['inherit', 'custom'] as const).map((m) => ( + + ))} +
+ + {reasoningMode === 'custom' ? ( +
+ +
+ {LEVELS.map((lv) => ( + + ))} +
+

+ {LEVELS.find((l) => l.key === thinkingLevel)?.desc} +

+
+ ) : ( +

Using provider defaults

+ )} +
+ ) +} diff --git a/ui/desktop/frontend/src/components/agents/tool-policy-section.tsx b/ui/desktop/frontend/src/components/agents/tool-policy-section.tsx new file mode 100644 index 00000000..e4c3e21c --- /dev/null +++ b/ui/desktop/frontend/src/components/agents/tool-policy-section.tsx @@ -0,0 +1,92 @@ +import { useTranslation } from 'react-i18next' +import { ConfigSection } from './config-section' +import type { ToolPolicyConfig } from '../../types/agent' + +interface ToolPolicySectionProps { + enabled: boolean + value: ToolPolicyConfig + onToggle: (v: boolean) => void + onChange: (v: ToolPolicyConfig) => void +} + +function tagsToArray(s: string): string[] | undefined { + const trimmed = s.trim() + if (!trimmed) return undefined + return trimmed.split(',').map((t) => t.trim()).filter(Boolean) +} + +function arrayToTags(arr?: string[]): string { + return arr?.join(', ') ?? '' +} + +export function ToolPolicySection({ enabled, value, onToggle, onChange }: ToolPolicySectionProps) { + const { t } = useTranslation('agents') + const update = (patch: Partial) => onChange({ ...value, ...patch }) + + return ( + +
+ + +
+ +
+ + update({ toolCallPrefix: e.target.value || undefined })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary font-mono focus:outline-none focus:ring-1 focus:ring-accent" + /> +

{t('configSections.toolPolicy.toolCallPrefixHint')}

+
+ +
+ + update({ allow: tagsToArray(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+ +
+ + update({ deny: tagsToArray(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+ +
+ + update({ alsoAllow: tagsToArray(e.target.value) })} + className="w-full bg-surface-tertiary border border-border rounded-lg px-3 py-2 text-base md:text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+
+ ) +} diff --git a/ui/desktop/frontend/src/hooks/use-agent-detail-state.ts b/ui/desktop/frontend/src/hooks/use-agent-detail-state.ts new file mode 100644 index 00000000..75a43cf6 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-agent-detail-state.ts @@ -0,0 +1,167 @@ +import { useState, useCallback } from 'react' +import type { + AgentData, ContextPruningConfig, SubagentsConfig, ToolPolicyConfig, + SandboxConfig, AgentReasoningConfig, ReasoningOverrideMode, +} from '../types/agent' + +export function useAgentDetailState( + agent: AgentData, + onSave: (id: string, updates: Partial) => Promise, + onClose: () => void, +) { + // --- Identity --- + const [emoji, setEmoji] = useState((agent.other_config?.emoji as string) ?? '🤖') + const [displayName, setDisplayName] = useState(agent.display_name ?? '') + const [description, setDescription] = useState((agent.other_config?.description as string) ?? '') + const [status, setStatus] = useState(agent.status ?? 'active') + const [isDefault, setIsDefault] = useState(agent.is_default ?? false) + + // --- Model --- + const [provider, setProvider] = useState(agent.provider) + const [model, setModel] = useState(agent.model) + const [contextWindow, setContextWindow] = useState(agent.context_window ?? 200000) + const [maxToolIterations, setMaxToolIterations] = useState(agent.max_tool_iterations ?? 25) + + // --- Evolution --- + const [selfEvolve, setSelfEvolve] = useState(!!(agent.other_config?.self_evolve)) + const [skillLearning, setSkillLearning] = useState(!!(agent.other_config?.skill_learning)) + const [skillNudgeInterval, setSkillNudgeInterval] = useState( + (agent.other_config?.skill_nudge_interval as number) ?? 15, + ) + + // --- Prompt mode --- + const [promptMode, setPromptMode] = useState((agent.other_config?.prompt_mode as string) || 'full') + + // --- Thinking --- + const reasoning = (agent.other_config?.reasoning ?? {}) as AgentReasoningConfig + const [reasoningMode, setReasoningMode] = useState(reasoning.override_mode ?? 'inherit') + const [thinkingLevel, setThinkingLevel] = useState(reasoning.effort ?? 'off') + + // --- Context pruning --- + const [pruningEnabled, setPruningEnabled] = useState(agent.context_pruning != null) + const [pruningConfig, setPruningConfig] = useState(agent.context_pruning ?? {}) + + // --- Compaction --- + const [compactionConfig, setCompactionConfig] = useState(agent.compaction_config ?? {}) + + // --- Subagents --- + const [subEnabled, setSubEnabled] = useState(agent.subagents_config != null) + const [subConfig, setSubConfig] = useState(agent.subagents_config ?? {}) + + // --- Tool policy --- + const [toolsEnabled, setToolsEnabled] = useState(agent.tools_config != null) + const [toolsConfig, setToolsConfig] = useState(agent.tools_config ?? {}) + + // --- Sandbox --- + const [sandboxEnabled, setSandboxEnabled] = useState(agent.sandbox_config != null) + const [sandboxConfig, setSandboxConfig] = useState(agent.sandbox_config ?? {}) + + // --- Pinned skills --- + const [pinnedSkills, setPinnedSkills] = useState( + (agent.other_config?.pinned_skills as string[]) ?? [], + ) + + // --- Save state --- + const [saveBlocked, setSaveBlocked] = useState(false) + const [saving, setSaving] = useState(false) + const [saveError, setSaveError] = useState('') + + const handleSave = useCallback(async () => { + setSaving(true) + setSaveError('') + try { + const otherConfig: Record = { ...agent.other_config } + if (emoji) otherConfig.emoji = emoji + if (description.trim()) otherConfig.description = description.trim() + else delete otherConfig.description + otherConfig.self_evolve = selfEvolve + + // Skill learning + otherConfig.skill_learning = skillLearning + if (skillLearning && skillNudgeInterval > 0) { + otherConfig.skill_nudge_interval = skillNudgeInterval + } else { + delete otherConfig.skill_nudge_interval + } + + // Prompt mode + if (promptMode && promptMode !== 'full') { + otherConfig.prompt_mode = promptMode + } else { + delete otherConfig.prompt_mode + } + + // Thinking / reasoning + if (reasoningMode === 'custom') { + otherConfig.reasoning = { override_mode: 'custom', effort: thinkingLevel } + } else { + delete otherConfig.reasoning + } + + // Pinned skills + if (pinnedSkills.length > 0) { + otherConfig.pinned_skills = pinnedSkills + } else { + delete otherConfig.pinned_skills + } + + await onSave(agent.id, { + display_name: displayName.trim() || undefined, + provider, + model, + context_window: contextWindow, + max_tool_iterations: maxToolIterations, + is_default: isDefault, + status, + other_config: otherConfig, + context_pruning: pruningEnabled ? pruningConfig : null, + compaction_config: compactionConfig, + subagents_config: subEnabled ? subConfig : null, + tools_config: toolsEnabled ? toolsConfig : null, + sandbox_config: sandboxEnabled ? sandboxConfig : null, + }) + onClose() + } catch (err) { + setSaveError(err instanceof Error ? err.message : 'Failed to save') + } finally { + setSaving(false) + } + }, [ + agent, emoji, displayName, description, selfEvolve, skillLearning, skillNudgeInterval, + promptMode, reasoningMode, thinkingLevel, pinnedSkills, + provider, model, contextWindow, maxToolIterations, isDefault, status, + pruningEnabled, pruningConfig, compactionConfig, + subEnabled, subConfig, toolsEnabled, toolsConfig, sandboxEnabled, sandboxConfig, + onSave, onClose, + ]) + + return { + // Identity + emoji, setEmoji, displayName, setDisplayName, description, setDescription, + status, setStatus, isDefault, setIsDefault, + // Model + provider, setProvider, model, setModel, + contextWindow, setContextWindow, maxToolIterations, setMaxToolIterations, + // Evolution + selfEvolve, setSelfEvolve, skillLearning, setSkillLearning, + skillNudgeInterval, setSkillNudgeInterval, + // Prompt mode + promptMode, setPromptMode, + // Thinking + reasoningMode, setReasoningMode, thinkingLevel, setThinkingLevel, + // Pruning + pruningEnabled, setPruningEnabled, pruningConfig, setPruningConfig, + // Compaction + compactionConfig, setCompactionConfig, + // Subagents + subEnabled, setSubEnabled, subConfig, setSubConfig, + // Tool policy + toolsEnabled, setToolsEnabled, toolsConfig, setToolsConfig, + // Sandbox + sandboxEnabled, setSandboxEnabled, sandboxConfig, setSandboxConfig, + // Pinned skills + pinnedSkills, setPinnedSkills, + // Save + saveBlocked, setSaveBlocked, saving, saveError, handleSave, + } +} diff --git a/ui/desktop/frontend/src/hooks/use-agent-skills.ts b/ui/desktop/frontend/src/hooks/use-agent-skills.ts new file mode 100644 index 00000000..e2874ff5 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-agent-skills.ts @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from 'react' +import { getApiClient, isApiClientReady } from '../lib/api' + +export interface AgentSkill { + slug: string + name: string + granted: boolean +} + +export function useAgentSkills(agentId: string) { + const [skills, setSkills] = useState([]) + const [loading, setLoading] = useState(true) + + const fetchSkills = useCallback(async () => { + if (!isApiClientReady()) { setLoading(false); return } + try { + const res = await getApiClient().get<{ skills: AgentSkill[] | null }>( + `/v1/agents/${agentId}/skills`, + ) + setSkills(res.skills ?? []) + } catch (err) { + console.error('Failed to fetch agent skills:', err) + } finally { + setLoading(false) + } + }, [agentId]) + + useEffect(() => { fetchSkills() }, [fetchSkills]) + + return { skills, loading, refetch: fetchSkills } +} diff --git a/ui/desktop/frontend/src/hooks/use-evolution-metrics.ts b/ui/desktop/frontend/src/hooks/use-evolution-metrics.ts new file mode 100644 index 00000000..1594d614 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-evolution-metrics.ts @@ -0,0 +1,37 @@ +import { useState, useEffect, useCallback } from 'react' +import { getApiClient, isApiClientReady } from '../lib/api' +import type { ToolAggregate, RetrievalAggregate, AggregatedMetrics } from '../types/evolution' + +export function useEvolutionMetrics(agentId: string, timeRange: '7d' | '30d' | '90d') { + const [toolAggs, setToolAggs] = useState([]) + const [retrievalAggs, setRetrievalAggs] = useState([]) + const [loading, setLoading] = useState(true) + + const fetchMetrics = useCallback(async () => { + if (!isApiClientReady()) { setLoading(false); return } + setLoading(true) + try { + const since = new Date(Date.now() - parseDays(timeRange) * 86400000).toISOString() + const res = await getApiClient().getWithParams( + `/v1/agents/${agentId}/evolution/metrics`, + { aggregate: 'true', since }, + ) + setToolAggs(res.tool_aggregates ?? []) + setRetrievalAggs(res.retrieval_aggregates ?? []) + } catch (err) { + console.error('Failed to fetch evolution metrics:', err) + } finally { + setLoading(false) + } + }, [agentId, timeRange]) + + useEffect(() => { fetchMetrics() }, [fetchMetrics]) + + return { toolAggs, retrievalAggs, loading } +} + +const RANGE_DAYS: Record = { '7d': 7, '30d': 30, '90d': 90 } + +function parseDays(range: string): number { + return RANGE_DAYS[range] ?? 7 +} diff --git a/ui/desktop/frontend/src/hooks/use-evolution-suggestions.ts b/ui/desktop/frontend/src/hooks/use-evolution-suggestions.ts new file mode 100644 index 00000000..ba8f90c3 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-evolution-suggestions.ts @@ -0,0 +1,38 @@ +import { useState, useEffect, useCallback } from 'react' +import { getApiClient, isApiClientReady } from '../lib/api' +import { toast } from '../stores/toast-store' +import type { EvolutionSuggestion } from '../types/evolution' + +export function useEvolutionSuggestions(agentId: string) { + const [suggestions, setSuggestions] = useState([]) + const [loading, setLoading] = useState(true) + + const fetchSuggestions = useCallback(async () => { + if (!isApiClientReady()) { setLoading(false); return } + try { + const res = await getApiClient().getWithParams<{ suggestions: EvolutionSuggestion[] | null }>( + `/v1/agents/${agentId}/evolution/suggestions`, + { limit: '100' }, + ) + setSuggestions(res.suggestions ?? []) + } catch (err) { + console.error('Failed to fetch evolution suggestions:', err) + } finally { + setLoading(false) + } + }, [agentId]) + + useEffect(() => { fetchSuggestions() }, [fetchSuggestions]) + + const updateStatus = useCallback(async (suggestionId: string, status: 'approved' | 'rejected' | 'rolled_back') => { + try { + await getApiClient().patch(`/v1/agents/${agentId}/evolution/suggestions/${suggestionId}`, { status }) + setSuggestions((prev) => prev.map((s) => s.id === suggestionId ? { ...s, status } : s)) + } catch (err) { + console.error('Failed to update suggestion:', err) + toast.error('Failed to update suggestion', (err as Error).message) + } + }, [agentId]) + + return { suggestions, loading, updateStatus } +} diff --git a/ui/desktop/frontend/src/hooks/use-orchestration.ts b/ui/desktop/frontend/src/hooks/use-orchestration.ts new file mode 100644 index 00000000..433b4754 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-orchestration.ts @@ -0,0 +1,38 @@ +import { useState, useEffect, useCallback } from 'react' +import { getApiClient, isApiClientReady } from '../lib/api' + +export interface DelegateTarget { + agent_key: string + display_name: string +} + +export interface OrchestrationInfo { + mode: string + delegate_targets: DelegateTarget[] | null + team: string | null +} + +export function useOrchestration(agentId: string) { + const [mode, setMode] = useState('spawn') + const [delegateTargets, setDelegateTargets] = useState([]) + const [team, setTeam] = useState(null) + const [loading, setLoading] = useState(true) + + const fetchOrchestration = useCallback(async () => { + if (!isApiClientReady()) { setLoading(false); return } + try { + const res = await getApiClient().get(`/v1/agents/${agentId}/orchestration`) + setMode(res.mode ?? 'spawn') + setDelegateTargets(res.delegate_targets ?? []) + setTeam(res.team ?? null) + } catch (err) { + console.error('Failed to fetch orchestration:', err) + } finally { + setLoading(false) + } + }, [agentId]) + + useEffect(() => { fetchOrchestration() }, [fetchOrchestration]) + + return { mode, delegateTargets, team, loading } +} diff --git a/ui/desktop/frontend/src/hooks/use-v3-flags.ts b/ui/desktop/frontend/src/hooks/use-v3-flags.ts new file mode 100644 index 00000000..dd2741f8 --- /dev/null +++ b/ui/desktop/frontend/src/hooks/use-v3-flags.ts @@ -0,0 +1,51 @@ +import { useState, useEffect, useCallback } from 'react' +import { getApiClient, isApiClientReady } from '../lib/api' +import { toast } from '../stores/toast-store' + +export interface V3Flags { + v3_pipeline_enabled: boolean + v3_memory_enabled: boolean + v3_retrieval_enabled: boolean + self_evolution_metrics: boolean + self_evolution_suggestions: boolean +} + +const DEFAULT_FLAGS: V3Flags = { + v3_pipeline_enabled: false, + v3_memory_enabled: false, + v3_retrieval_enabled: false, + self_evolution_metrics: false, + self_evolution_suggestions: false, +} + +export function useV3Flags(agentId: string) { + const [flags, setFlags] = useState(DEFAULT_FLAGS) + const [loading, setLoading] = useState(true) + + const fetchFlags = useCallback(async () => { + if (!isApiClientReady()) { setLoading(false); return } + try { + const res = await getApiClient().get(`/v1/agents/${agentId}/v3-flags`) + setFlags(res) + } catch (err) { + console.error('Failed to fetch v3 flags:', err) + } finally { + setLoading(false) + } + }, [agentId]) + + useEffect(() => { fetchFlags() }, [fetchFlags]) + + const toggleFlag = useCallback(async (key: keyof V3Flags, value: boolean) => { + setFlags((prev) => ({ ...prev, [key]: value })) + try { + await getApiClient().patch(`/v1/agents/${agentId}/v3-flags`, { [key]: value }) + } catch (err) { + console.error('Failed to toggle v3 flag:', err) + setFlags((prev) => ({ ...prev, [key]: !value })) + toast.error('Failed to update flag', (err as Error).message) + } + }, [agentId]) + + return { flags, loading, toggleFlag } +} diff --git a/ui/desktop/frontend/src/i18n/locales/en/agents.json b/ui/desktop/frontend/src/i18n/locales/en/agents.json index 4f7deeb4..7b04a494 100644 --- a/ui/desktop/frontend/src/i18n/locales/en/agents.json +++ b/ui/desktop/frontend/src/i18n/locales/en/agents.json @@ -90,7 +90,8 @@ "links": "Links", "skills": "Skills", "instances": "Instances", - "permissions": "Permissions" + "permissions": "Permissions", + "evolution": "Evolution" }, "summonFailed": "Summon Failed", "evolving": "Evolving", @@ -103,7 +104,37 @@ "skills": "Skills", "evolution": "Evolution", "capabilities": "Capabilities", - "llmSeesAs": "LLM sees this as" + "llmSeesAs": "LLM sees this as", + "prompt": { + "title": "System Prompt Mode" + }, + "orchestration": { + "title": "Orchestration", + "delegateTargets": "Delegate Targets", + "team": "Team", + "noDelegates": "Standard spawn mode — no delegation configured" + }, + "evolutionTab": { + "notEnabled": "Evolution not enabled", + "notEnabledHint": "Enable \"Evolution Metrics\" in the Agent tab to start collecting data.", + "toolSuccess": "Tool Success Rate", + "retrievalQuality": "Retrieval Quality", + "noMetrics": "No metrics data collected yet", + "suggestions": "Suggestions", + "noSuggestions": "No suggestions generated yet", + "approve": "Approve", + "reject": "Reject", + "rollback": "Rollback", + "guardrails": "Adaptation Guardrails", + "maxDelta": "Max Delta / Cycle", + "minDataPoints": "Min Data Points", + "rollbackDrop": "Rollback on Drop" + }, + "pinnedSkills": { + "title": "Pinned Skills", + "addPlaceholder": "Add a skill...", + "hint": "Pinned skills are always inlined in the system prompt. Others use skill_search." + } }, "general": { "selfEvolution": "Self-Evolution", @@ -116,6 +147,12 @@ "skillLearningInfo": "After complex tasks, the agent may suggest saving the workflow as a reusable skill. User approval is always required before any skill is created. Skills are private to the owner by default.", "skillNudgeIntervalLabel": "Nudge after N tool calls", "skillNudgeIntervalHint": "After this many tool calls, the agent will suggest saving the workflow as a skill. Set to 0 to disable post-task suggestions (system prompt guidance still shown).", + "v3Flags": "V3 Engine Flags", + "v3FlagsHint": "These flags are saved independently from the agent config.", + "evolutionMetrics": "Evolution Metrics", + "evolutionMetricsLabel": "Collect tool success rates and retrieval quality data", + "evolutionSuggestions": "Evolution Suggestions", + "evolutionSuggestionsLabel": "Generate optimization suggestions from metrics", "budget": "Monthly Budget", "budgetLabel": "Budget limit (USD)", "budgetHint": "Optional monthly spending cap. Agent will reject requests when exceeded. Leave empty for unlimited.", diff --git a/ui/desktop/frontend/src/i18n/locales/vi/agents.json b/ui/desktop/frontend/src/i18n/locales/vi/agents.json index 46757c6c..39f5f39c 100644 --- a/ui/desktop/frontend/src/i18n/locales/vi/agents.json +++ b/ui/desktop/frontend/src/i18n/locales/vi/agents.json @@ -90,7 +90,8 @@ "links": "Liên kết", "skills": "Skill", "instances": "Phiên bản người dùng", - "permissions": "Phân quyền" + "permissions": "Phân quyền", + "evolution": "Tiến hóa" }, "summonFailed": "Triệu hồi thất bại", "evolving": "Đang tiến hóa", @@ -103,7 +104,37 @@ "skills": "Kỹ năng", "evolution": "Tiến hóa", "capabilities": "Khả năng", - "llmSeesAs": "LLM nhìn thấy đây là" + "llmSeesAs": "LLM nhìn thấy đây là", + "prompt": { + "title": "Chế độ System Prompt" + }, + "orchestration": { + "title": "Điều phối", + "delegateTargets": "Mục tiêu ủy quyền", + "team": "Nhóm", + "noDelegates": "Chế độ spawn tiêu chuẩn — chưa cấu hình ủy quyền" + }, + "evolutionTab": { + "notEnabled": "Tiến hóa chưa được bật", + "notEnabledHint": "Bật \"Chỉ số Tiến hóa\" trong tab Agent để bắt đầu thu thập dữ liệu.", + "toolSuccess": "Tỉ lệ thành công công cụ", + "retrievalQuality": "Chất lượng truy xuất", + "noMetrics": "Chưa có dữ liệu chỉ số", + "suggestions": "Đề xuất", + "noSuggestions": "Chưa có đề xuất nào", + "approve": "Duyệt", + "reject": "Từ chối", + "rollback": "Hoàn tác", + "guardrails": "Rào chắn thích ứng", + "maxDelta": "Delta tối đa / chu kỳ", + "minDataPoints": "Số điểm dữ liệu tối thiểu", + "rollbackDrop": "Hoàn tác khi giảm" + }, + "pinnedSkills": { + "title": "Kỹ năng ghim", + "addPlaceholder": "Thêm kỹ năng...", + "hint": "Kỹ năng ghim luôn được nhúng vào system prompt. Các kỹ năng khác dùng skill_search." + } }, "general": { "selfEvolution": "Tự tiến hóa", @@ -116,6 +147,12 @@ "skillLearningInfo": "Sau các tác vụ phức tạp, agent có thể đề xuất lưu quy trình thành kỹ năng tái sử dụng. Luôn cần sự đồng ý của người dùng trước khi tạo kỹ năng. Kỹ năng mặc định là riêng tư.", "skillNudgeIntervalLabel": "Nhắc sau N lệnh tool", "skillNudgeIntervalHint": "Sau số lượng lệnh tool này, agent sẽ đề xuất lưu quy trình thành kỹ năng. Đặt 0 để tắt đề xuất sau tác vụ (hướng dẫn trong system prompt vẫn hiển thị).", + "v3Flags": "Cờ V3 Engine", + "v3FlagsHint": "Các cờ này được lưu độc lập với cấu hình agent.", + "evolutionMetrics": "Chỉ số Tiến hóa", + "evolutionMetricsLabel": "Thu thập tỉ lệ thành công công cụ và chất lượng truy xuất", + "evolutionSuggestions": "Đề xuất Tiến hóa", + "evolutionSuggestionsLabel": "Tạo đề xuất tối ưu từ chỉ số", "budget": "Ngân sách hàng tháng", "budgetLabel": "Giới hạn ngân sách (USD)", "budgetHint": "Giới hạn chi tiêu hàng tháng tùy chọn. Agent sẽ từ chối yêu cầu khi vượt quá. Để trống cho không giới hạn.", diff --git a/ui/desktop/frontend/src/i18n/locales/zh/agents.json b/ui/desktop/frontend/src/i18n/locales/zh/agents.json index d0f37e5a..04c0424b 100644 --- a/ui/desktop/frontend/src/i18n/locales/zh/agents.json +++ b/ui/desktop/frontend/src/i18n/locales/zh/agents.json @@ -90,7 +90,8 @@ "links": "链接", "skills": "Skill", "instances": "用户实例", - "permissions": "权限" + "permissions": "权限", + "evolution": "进化" }, "summonFailed": "召唤失败", "evolving": "进化中", @@ -103,7 +104,37 @@ "skills": "技能", "evolution": "进化", "capabilities": "能力", - "llmSeesAs": "LLM会看到这个为" + "llmSeesAs": "LLM会看到这个为", + "prompt": { + "title": "系统提示模式" + }, + "orchestration": { + "title": "编排", + "delegateTargets": "委派目标", + "team": "团队", + "noDelegates": "标准生成模式 — 未配置委派" + }, + "evolutionTab": { + "notEnabled": "进化未启用", + "notEnabledHint": "在 Agent 选项卡中启用「进化指标」以开始收集数据。", + "toolSuccess": "工具成功率", + "retrievalQuality": "检索质量", + "noMetrics": "暂无指标数据", + "suggestions": "建议", + "noSuggestions": "暂无建议", + "approve": "批准", + "reject": "拒绝", + "rollback": "回滚", + "guardrails": "适应护栏", + "maxDelta": "每周期最大变化", + "minDataPoints": "最小数据点", + "rollbackDrop": "回滚触发降幅" + }, + "pinnedSkills": { + "title": "固定技能", + "addPlaceholder": "添加技能...", + "hint": "固定的技能始终内联在系统提示中。其他技能使用 skill_search。" + } }, "general": { "selfEvolution": "自我进化", @@ -116,6 +147,12 @@ "skillLearningInfo": "在复杂任务后,Agent可能会建议将工作流程保存为可重用技能。创建技能前始终需要用户批准。技能默认为私有。", "skillNudgeIntervalLabel": "N 次工具调用后提醒", "skillNudgeIntervalHint": "达到此工具调用次数后,Agent会建议将工作流程保存为技能。设为 0 可禁用任务后建议(系统提示词中的指导仍然显示)。", + "v3Flags": "V3 引擎标志", + "v3FlagsHint": "这些标志独立于Agent配置保存。", + "evolutionMetrics": "进化指标", + "evolutionMetricsLabel": "收集工具成功率和检索质量数据", + "evolutionSuggestions": "进化建议", + "evolutionSuggestionsLabel": "根据指标生成优化建议", "budget": "月度预算", "budgetLabel": "预算限额 (USD)", "budgetHint": "可选的月度支出上限。超出后 Agent 将拒绝请求。留空表示无限制。", diff --git a/ui/desktop/frontend/src/lib/api.ts b/ui/desktop/frontend/src/lib/api.ts index 5e59683d..1d2bcbd8 100644 --- a/ui/desktop/frontend/src/lib/api.ts +++ b/ui/desktop/frontend/src/lib/api.ts @@ -66,6 +66,11 @@ class ApiClient { return this.request('GET', path) } + async getWithParams(path: string, params?: Record): Promise { + const qs = params ? '?' + new URLSearchParams(params).toString() : '' + return this.request('GET', `${path}${qs}`) + } + async post(path: string, body?: unknown): Promise { return this.request('POST', path, body) } diff --git a/ui/desktop/frontend/src/lib/format.ts b/ui/desktop/frontend/src/lib/format.ts index 81957caf..1da70b16 100644 --- a/ui/desktop/frontend/src/lib/format.ts +++ b/ui/desktop/frontend/src/lib/format.ts @@ -30,3 +30,16 @@ export function truncate(text: string, maxLen: number): string { if (text.length <= maxLen) return text return text.slice(0, maxLen - 1) + '\u2026' } + +export function formatRelativeTime(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime() + if (diff < 60000) return 'just now' + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago` + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago` + return `${Math.floor(diff / 86400000)}d ago` +} + +export function numOrUndef(v: string): number | undefined { + const n = Number(v) + return isNaN(n) || v === '' ? undefined : n +} diff --git a/ui/desktop/frontend/src/types/agent.ts b/ui/desktop/frontend/src/types/agent.ts index 6628f362..88ccdaa6 100644 --- a/ui/desktop/frontend/src/types/agent.ts +++ b/ui/desktop/frontend/src/types/agent.ts @@ -24,6 +24,50 @@ export interface CompactionConfig { } } +export interface ContextPruningConfig { + mode?: 'off' | 'cache-ttl' + keepLastAssistants?: number + softTrimRatio?: number + hardClearRatio?: number + softTrim?: { maxChars?: number; headChars?: number; tailChars?: number } + hardClear?: { enabled?: boolean } +} + +export interface SubagentsConfig { + maxConcurrent?: number + maxSpawnDepth?: number + maxChildrenPerAgent?: number + archiveAfterMinutes?: number + model?: string +} + +export interface ToolPolicyConfig { + profile?: string + allow?: string[] + deny?: string[] + alsoAllow?: string[] + toolCallPrefix?: string +} + +export interface SandboxConfig { + mode?: 'off' | 'non-main' | 'all' + image?: string + workspace_access?: 'none' | 'ro' | 'rw' + scope?: 'session' | 'agent' | 'shared' + timeout_sec?: number + memory_mb?: number + cpus?: number + network_enabled?: boolean +} + +export type ReasoningOverrideMode = 'inherit' | 'custom' + +export interface AgentReasoningConfig { + override_mode?: ReasoningOverrideMode + effort?: string + fallback?: 'downgrade' | 'provider_default' | 'off' +} + // --- Main agent data --- export interface AgentData { @@ -47,6 +91,10 @@ export interface AgentData { // Per-agent JSONB configs (null/undefined = use global defaults) memory_config?: MemoryConfig | null compaction_config?: CompactionConfig | null + context_pruning?: ContextPruningConfig | null + tools_config?: ToolPolicyConfig | null + sandbox_config?: SandboxConfig | null + subagents_config?: SubagentsConfig | null other_config?: Record | null tenant_id?: string } diff --git a/ui/desktop/frontend/src/types/evolution.ts b/ui/desktop/frontend/src/types/evolution.ts new file mode 100644 index 00000000..f7f519e3 --- /dev/null +++ b/ui/desktop/frontend/src/types/evolution.ts @@ -0,0 +1,40 @@ +// Evolution types matching web UI + Go backend JSON responses + +export interface ToolAggregate { + tool_name: string + call_count: number + success_rate: number + avg_duration_ms: number +} + +export interface RetrievalAggregate { + source: string + query_count: number + usage_rate: number + avg_score: number +} + +export interface AggregatedMetrics { + tool_aggregates: ToolAggregate[] | null + retrieval_aggregates: RetrievalAggregate[] | null +} + +export interface EvolutionSuggestion { + id: string + agent_id: string + suggestion_type: string + suggestion: string + rationale: string + parameters: Record | null + status: string + reviewed_by: string | null + reviewed_at: string | null + created_at: string +} + +export interface AdaptationGuardrails { + max_delta_per_cycle: number + min_data_points: number + rollback_on_drop_pct: number + locked_params: string[] +} diff --git a/ui/web/index.html b/ui/web/index.html index 4c4c0245..a9c03efd 100644 --- a/ui/web/index.html +++ b/ui/web/index.html @@ -7,9 +7,32 @@ + GoClaw Dashboard + +
+
+ +
+
+
diff --git a/ui/web/package.json b/ui/web/package.json index c141983a..e78ae991 100644 --- a/ui/web/package.json +++ b/ui/web/package.json @@ -26,6 +26,7 @@ "i18next": "^26.0.1", "jszip": "^3.10.1", "lucide-react": "^1.7.0", + "mermaid": "^11.14.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -36,20 +37,25 @@ "react-router": "^7.13.2", "recharts": "^3.8.1", "rehype-highlight": "^7.0.2", + "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", "tailwind-merge": "^3.5.0", + "unist-util-visit": "^5.1.0", "zod": "^4.3.6", "zustand": "^5.0.12" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", "@types/d3-force": "^3.0.10", + "@types/mdast": "^4.0.4", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^6.0.1", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", + "unified": "^11.0.5", "vite": "^8.0.3", "ws": "^8.20.0" }, diff --git a/ui/web/pnpm-lock.yaml b/ui/web/pnpm-lock.yaml index d9625736..6668d561 100644 --- a/ui/web/pnpm-lock.yaml +++ b/ui/web/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: lucide-react: specifier: ^1.7.0 version: 1.7.0(react@19.2.4) + mermaid: + specifier: ^11.14.0 + version: 11.14.0 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -86,12 +89,21 @@ importers: rehype-highlight: specifier: ^7.0.2 version: 7.0.2 + rehype-katex: + specifier: ^7.0.1 + version: 7.0.1 remark-gfm: specifier: ^4.0.1 version: 4.0.1 + remark-math: + specifier: ^6.0.0 + version: 6.0.0 tailwind-merge: specifier: ^3.5.0 version: 3.5.0 + unist-util-visit: + specifier: ^5.1.0 + version: 5.1.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -105,6 +117,9 @@ importers: '@types/d3-force': specifier: ^3.0.10 version: 3.0.10 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -123,6 +138,9 @@ importers: typescript: specifier: ^6.0.2 version: 6.0.2 + unified: + specifier: ^11.0.5 + version: 11.0.5 vite: specifier: ^8.0.3 version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1) @@ -132,10 +150,31 @@ importers: packages: + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/cst-dts-gen@12.0.0': + resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} + + '@chevrotain/gast@12.0.0': + resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} + + '@chevrotain/regexp-to-ast@12.0.0': + resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} + + '@chevrotain/types@12.0.0': + resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} + + '@chevrotain/utils@12.0.0': + resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -187,6 +226,12 @@ packages: peerDependencies: react-hook-form: ^7.55.0 + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -203,6 +248,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} + '@napi-rs/wasm-runtime@1.1.2': resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} peerDependencies: @@ -1147,33 +1195,96 @@ packages: '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + '@types/d3-force@3.0.10': resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + '@types/d3-scale@4.0.9': resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + '@types/d3-shape@3.1.8': resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + '@types/d3-time@3.0.4': resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1183,9 +1294,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1203,6 +1320,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1218,6 +1338,9 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1235,6 +1358,11 @@ packages: resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} engines: {node: '>=12'} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -1264,6 +1392,15 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chevrotain-allstar@0.4.1: + resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} + peerDependencies: + chevrotain: ^12.0.0 + + chevrotain@12.0.0: + resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} + engines: {node: '>=22.0.0'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1274,6 +1411,17 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1281,6 +1429,12 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1289,17 +1443,54 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.2: + resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + d3-binarytree@1.0.2: resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + d3-dispatch@3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} @@ -1308,10 +1499,19 @@ packages: resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} engines: {node: '>=12'} + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + d3-force-3d@3.0.6: resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==} engines: {node: '>=12'} @@ -1324,6 +1524,14 @@ packages: resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} engines: {node: '>=12'} + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} @@ -1331,14 +1539,28 @@ packages: d3-octree@1.1.0: resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + d3-path@3.1.0: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + d3-scale-chromatic@3.1.0: resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} engines: {node: '>=12'} @@ -1351,6 +1573,9 @@ packages: resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} engines: {node: '>=12'} + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + d3-shape@3.2.0: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} @@ -1377,9 +1602,19 @@ packages: resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} engines: {node: '>=12'} + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1395,6 +1630,9 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1409,10 +1647,17 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dompurify@3.3.3: + resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} @@ -1472,9 +1717,27 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -1484,6 +1747,9 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} @@ -1502,6 +1768,10 @@ packages: typescript: optional: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -1521,6 +1791,9 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -1562,6 +1835,23 @@ packages: resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} engines: {node: '>=12'} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + langium@4.2.2: + resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -1663,6 +1953,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -1687,6 +1982,9 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -1708,6 +2006,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -1732,6 +2033,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -1792,6 +2096,9 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + motion-dom@12.38.0: resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} @@ -1810,12 +2117,24 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1823,6 +2142,15 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -1985,9 +2313,15 @@ packages: rehype-highlight@7.0.2: resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -2000,14 +2334,26 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.0.0-rc.12: resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2036,6 +2382,9 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -2052,6 +2401,10 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2062,6 +2415,10 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2070,6 +2427,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -2085,6 +2445,9 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -2122,6 +2485,13 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2178,6 +2548,29 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -2216,8 +2609,30 @@ packages: snapshots: + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.1.1 + '@babel/runtime@7.29.2': {} + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/cst-dts-gen@12.0.0': + dependencies: + '@chevrotain/gast': 12.0.0 + '@chevrotain/types': 12.0.0 + + '@chevrotain/gast@12.0.0': + dependencies: + '@chevrotain/types': 12.0.0 + + '@chevrotain/regexp-to-ast@12.0.0': {} + + '@chevrotain/types@12.0.0': {} + + '@chevrotain/utils@12.0.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -2281,6 +2696,14 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.72.1(react@19.2.4) + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2300,6 +2723,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mermaid-js/parser@1.1.0': + dependencies: + langium: 4.2.2 + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: '@emnapi/core': 1.9.1 @@ -3223,30 +3650,121 @@ snapshots: '@types/d3-array@3.2.2': {} + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + '@types/d3-color@3.1.3': {} + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + '@types/d3-ease@3.0.2': {} + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + '@types/d3-force@3.0.10': {} + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 '@types/d3-path@3.1.1': {} + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + '@types/d3-scale@4.0.9': dependencies: '@types/d3-time': 3.0.4 + '@types/d3-selection@3.0.11': {} + '@types/d3-shape@3.1.8': dependencies: '@types/d3-path': 3.1.1 + '@types/d3-time-format@4.0.3': {} + '@types/d3-time@3.0.4': {} '@types/d3-timer@3.0.2': {} + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -3257,10 +3775,14 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/katex@0.16.8': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -3279,6 +3801,9 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -3291,6 +3816,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -3298,6 +3828,8 @@ snapshots: accessor-fn@1.5.3: {} + acorn@8.16.0: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -3320,6 +3852,19 @@ snapshots: character-reference-invalid@2.0.1: {} + chevrotain-allstar@0.4.1(chevrotain@12.0.0): + dependencies: + chevrotain: 12.0.0 + lodash-es: 4.18.1 + + chevrotain@12.0.0: + dependencies: + '@chevrotain/cst-dts-gen': 12.0.0 + '@chevrotain/gast': 12.0.0 + '@chevrotain/regexp-to-ast': 12.0.0 + '@chevrotain/types': 12.0.0 + '@chevrotain/utils': 12.0.0 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -3328,22 +3873,74 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + + confbox@0.1.8: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cssesc@3.0.0: {} csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.2 + + cytoscape-fcose@2.2.0(cytoscape@3.33.2): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.2 + + cytoscape@3.33.2: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + d3-array@3.2.4: dependencies: internmap: 2.0.3 + d3-axis@3.0.0: {} + d3-binarytree@1.0.2: {} + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + d3-color@3.1.0: {} + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + d3-dispatch@3.0.1: {} d3-drag@3.0.0: @@ -3351,8 +3948,18 @@ snapshots: d3-dispatch: 3.0.1 d3-selection: 3.0.0 + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + d3-ease@3.0.1: {} + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + d3-force-3d@3.0.6: dependencies: d3-binarytree: 1.0.2 @@ -3369,16 +3976,33 @@ snapshots: d3-format@3.1.2: {} + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 d3-octree@1.1.0: {} + d3-path@1.0.9: {} + d3-path@3.1.0: {} + d3-polygon@3.0.1: {} + d3-quadtree@3.0.1: {} + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + d3-scale-chromatic@3.1.0: dependencies: d3-color: 3.1.0 @@ -3394,6 +4018,10 @@ snapshots: d3-selection@3.0.0: {} + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + d3-shape@3.2.0: dependencies: d3-path: 3.1.0 @@ -3425,8 +4053,48 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + date-fns@4.1.0: {} + dayjs@1.11.20: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -3437,6 +4105,10 @@ snapshots: dependencies: character-entities: 2.0.2 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -3447,11 +4119,17 @@ snapshots: dependencies: dequal: 2.0.3 + dompurify@3.3.3: + optionalDependencies: + '@types/trusted-types': 2.0.7 + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 tapable: 2.3.2 + entities@6.0.1: {} + es-toolkit@1.45.1: {} escape-string-regexp@5.0.0: {} @@ -3506,10 +4184,49 @@ snapshots: graceful-fs@4.2.11: {} + hachure-fill@0.5.2: {} + + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + hast-util-is-element@3.0.0: dependencies: '@types/hast': 3.0.4 + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -3541,6 +4258,14 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + highlight.js@11.11.1: {} html-parse-stringify@3.0.1: @@ -3555,6 +4280,10 @@ snapshots: optionalDependencies: typescript: 6.0.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + immediate@3.0.6: {} immer@10.2.0: {} @@ -3567,6 +4296,8 @@ snapshots: inline-style-parser@0.2.7: {} + internmap@1.0.1: {} + internmap@2.0.3: {} is-alphabetical@2.0.1: {} @@ -3601,6 +4332,25 @@ snapshots: dependencies: lodash-es: 4.18.1 + katex@0.16.45: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + langium@4.2.2: + dependencies: + '@chevrotain/regexp-to-ast': 12.0.0 + chevrotain: 12.0.0 + chevrotain-allstar: 0.4.1(chevrotain@12.0.0) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -3678,6 +4428,8 @@ snapshots: markdown-table@3.0.4: {} + marked@16.4.2: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -3759,6 +4511,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -3831,6 +4595,30 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mermaid@11.14.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 1.1.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) + cytoscape-fcose: 2.2.0(cytoscape@3.33.2) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.3.3 + katex: 0.16.45 + khroma: 2.1.0 + lodash-es: 4.18.1 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -3908,6 +4696,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.45 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -4022,6 +4820,13 @@ snapshots: transitivePeerDependencies: - supports-color + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + motion-dom@12.38.0: dependencies: motion-utils: 12.36.0 @@ -4034,6 +4839,8 @@ snapshots: object-assign@4.1.1: {} + package-manager-detector@1.6.0: {} + pako@1.0.11: {} parse-entities@4.0.2: @@ -4046,10 +4853,31 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-data-parser@0.1.0: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -4280,6 +5108,16 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.45 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -4291,6 +5129,15 @@ snapshots: transitivePeerDependencies: - supports-color + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -4316,6 +5163,8 @@ snapshots: reselect@5.1.1: {} + robust-predicates@3.0.3: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: '@oxc-project/types': 0.122.0 @@ -4340,8 +5189,19 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + safe-buffer@5.1.2: {} + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} set-cookie-parser@2.7.2: {} @@ -4369,6 +5229,8 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + stylis@4.3.6: {} + tailwind-merge@3.5.0: {} tailwindcss@4.2.2: {} @@ -4379,6 +5241,8 @@ snapshots: tinycolor2@1.6.0: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -4388,10 +5252,14 @@ snapshots: trough@2.2.0: {} + ts-dedent@2.2.0: {} + tslib@2.8.1: {} typescript@6.0.2: {} + ufo@1.6.3: {} + undici-types@7.18.2: {} unified@11.0.5: @@ -4417,6 +5285,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -4453,6 +5326,13 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -4497,6 +5377,25 @@ snapshots: void-elements@3.1.0: {} + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + + web-namespaces@2.0.1: {} + ws@8.20.0: {} zod@4.3.6: {} diff --git a/ui/web/src/adapters/vault-graph-adapter.ts b/ui/web/src/adapters/vault-graph-adapter.ts new file mode 100644 index 00000000..5acd027c --- /dev/null +++ b/ui/web/src/adapters/vault-graph-adapter.ts @@ -0,0 +1,103 @@ +import type { VaultDocument, VaultLink } from "@/types/vault"; + +// Colors per vault document type +export const VAULT_TYPE_COLORS: Record = { + context: "#3b82f6", // blue + memory: "#8b5cf6", // purple + note: "#eab308", // yellow + skill: "#22c55e", // green + episodic: "#f97316", // orange + media: "#ef4444", // red +}; +const DEFAULT_COLOR = "#9ca3af"; + +export interface VaultGraphNode { + id: string; + title: string; + docType: string; + color: string; + neighbors: Set; + linkIds: Set; + degree: number; + x?: number; + y?: number; +} + +export interface VaultGraphLink { + id: string; + source: string; + target: string; + label: string; +} + +export interface VaultGraphData { + nodes: VaultGraphNode[]; + links: VaultGraphLink[]; +} + +/** Limit documents by degree centrality (highest-connected first). */ +export function limitVaultDocsByDegree( + docs: VaultDocument[], + links: VaultLink[], + nodeLimit: number, +): VaultDocument[] { + if (docs.length <= nodeLimit) return docs; + const ids = new Set(docs.map((d) => d.id)); + const deg = new Map(); + for (const l of links) { + if (ids.has(l.from_doc_id)) deg.set(l.from_doc_id, (deg.get(l.from_doc_id) ?? 0) + 1); + if (ids.has(l.to_doc_id)) deg.set(l.to_doc_id, (deg.get(l.to_doc_id) ?? 0) + 1); + } + return [...docs].sort((a, b) => (deg.get(b.id) ?? 0) - (deg.get(a.id) ?? 0)).slice(0, nodeLimit); +} + +/** Build graph data from vault documents and their links. */ +export function buildVaultGraphData( + documents: VaultDocument[], + links: VaultLink[], +): VaultGraphData { + const docIds = new Set(documents.map((d) => d.id)); + + // Build degree map + const degreeMap = new Map(); + for (const link of links) { + if (docIds.has(link.from_doc_id)) { + degreeMap.set(link.from_doc_id, (degreeMap.get(link.from_doc_id) ?? 0) + 1); + } + if (docIds.has(link.to_doc_id)) { + degreeMap.set(link.to_doc_id, (degreeMap.get(link.to_doc_id) ?? 0) + 1); + } + } + + const nodes: VaultGraphNode[] = documents.map((d) => ({ + id: d.id, + title: d.title || d.path.split("/").pop() || d.id.slice(0, 8), + docType: d.doc_type, + color: VAULT_TYPE_COLORS[d.doc_type] ?? DEFAULT_COLOR, + neighbors: new Set(), + linkIds: new Set(), + degree: degreeMap.get(d.id) ?? 0, + })); + + const nodeMap = new Map(nodes.map((n) => [n.id, n])); + + // Only include links where both endpoints exist in our document set + const graphLinks: VaultGraphLink[] = []; + for (const link of links) { + const src = nodeMap.get(link.from_doc_id); + const tgt = nodeMap.get(link.to_doc_id); + if (!src || !tgt) continue; + src.neighbors.add(tgt.id); + tgt.neighbors.add(src.id); + src.linkIds.add(link.id); + tgt.linkIds.add(link.id); + graphLinks.push({ + id: link.id, + source: link.from_doc_id, + target: link.to_doc_id, + label: link.link_type, + }); + } + + return { nodes, links: graphLinks }; +} diff --git a/ui/web/src/api/http-client.ts b/ui/web/src/api/http-client.ts index ee5d8f58..2418ec39 100644 --- a/ui/web/src/api/http-client.ts +++ b/ui/web/src/api/http-client.ts @@ -29,6 +29,13 @@ export class HttpClient { }); } + async patch(path: string, body?: unknown): Promise { + return this.request(this.buildUrl(path), { + method: "PATCH", + body: body ? JSON.stringify(body) : undefined, + }); + } + async delete(path: string): Promise { return this.request(this.buildUrl(path), { method: "DELETE" }); } diff --git a/ui/web/src/components/agents/v3-capabilities-modal/capability-card.tsx b/ui/web/src/components/agents/v3-capabilities-modal/capability-card.tsx new file mode 100644 index 00000000..39a01f90 --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/capability-card.tsx @@ -0,0 +1,28 @@ +import type { LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface CapabilityCardProps { + icon: LucideIcon; + title: string; + description: string; + className?: string; +} + +export function CapabilityCard({ + icon: Icon, + title, + description, + className, +}: CapabilityCardProps) { + return ( +
+
+ + {title} +
+

+ {description} +

+
+ ); +} diff --git a/ui/web/src/components/agents/v3-capabilities-modal/knowledge-tab.tsx b/ui/web/src/components/agents/v3-capabilities-modal/knowledge-tab.tsx new file mode 100644 index 00000000..46c6e04d --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/knowledge-tab.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next"; +import { GitFork, Library, Moon } from "lucide-react"; +import { CapabilityCard } from "./capability-card"; + +export function KnowledgeTab() { + const { t } = useTranslation("v3-capabilities"); + + return ( +
+ + + +
+ ); +} diff --git a/ui/web/src/components/agents/v3-capabilities-modal/memory-tab.tsx b/ui/web/src/components/agents/v3-capabilities-modal/memory-tab.tsx new file mode 100644 index 00000000..e91a5b60 --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/memory-tab.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from "react-i18next"; +import { + Brain, + MessageSquare, + BookOpen, + Network, + ArrowDown, +} from "lucide-react"; +import { CapabilityCard } from "./capability-card"; + +export function MemoryTab() { + const { t } = useTranslation("v3-capabilities"); + + return ( +
+
+
+ +

{t("memory.title")}

+
+

+ {t("memory.description")} +

+
+ + + +
+
+ + session.completed +
+
+ + + +
+
+ + episodic.created +
+
+ + + +

+ {t("memory.eventFlow")} +

+
+ ); +} diff --git a/ui/web/src/components/agents/v3-capabilities-modal/orchestration-tab.tsx b/ui/web/src/components/agents/v3-capabilities-modal/orchestration-tab.tsx new file mode 100644 index 00000000..652b9fee --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/orchestration-tab.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next"; +import { Users, TrendingUp } from "lucide-react"; +import { CapabilityCard } from "./capability-card"; + +export function OrchestrationTab() { + const { t } = useTranslation("v3-capabilities"); + + return ( +
+ + +
+ ); +} diff --git a/ui/web/src/components/agents/v3-capabilities-modal/pipeline-tab.tsx b/ui/web/src/components/agents/v3-capabilities-modal/pipeline-tab.tsx new file mode 100644 index 00000000..300f8c28 --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/pipeline-tab.tsx @@ -0,0 +1,95 @@ +import { useTranslation } from "react-i18next"; +import { + Workflow, + Brain, + Scissors, + Wrench, + Eye, + Save, + Flag, + ChevronRight, +} from "lucide-react"; + +export function PipelineTab() { + const { t } = useTranslation("v3-capabilities"); + + const iterationStages = [ + { icon: Brain, key: "think" }, + { icon: Scissors, key: "prune" }, + { icon: Wrench, key: "tools" }, + { icon: Eye, key: "observe" }, + { icon: Save, key: "checkpoint" }, + ] as const; + + return ( +
+
+
+ +

{t("pipeline.title")}

+
+

+ {t("pipeline.description")} +

+
+ + {/* Setup */} +
+
+ + + {t("pipeline.setup")} + +
+

+ {t("pipeline.setupDesc")} +

+
+ + {/* Iteration loop */} +
+
+ + {t("pipeline.iteration")} + + + {t("pipeline.iterationDesc")} + +
+
+ {iterationStages.map(({ icon: Icon, key }, i) => ( +
+
+ +
+

+ {t(`pipeline.${key}`)} +

+

+ {t(`pipeline.${key}Desc`)} +

+
+
+ {i < iterationStages.length - 1 && ( + + )} +
+ ))} +
+
+ + {/* Finalize */} +
+
+ + + {t("pipeline.finalize")} + +
+

+ {t("pipeline.finalizeDesc")} +

+
+
+ ); +} diff --git a/ui/web/src/components/agents/v3-capabilities-modal/v3-capabilities-modal.tsx b/ui/web/src/components/agents/v3-capabilities-modal/v3-capabilities-modal.tsx new file mode 100644 index 00000000..25f53fe7 --- /dev/null +++ b/ui/web/src/components/agents/v3-capabilities-modal/v3-capabilities-modal.tsx @@ -0,0 +1,62 @@ +import { useTranslation } from "react-i18next"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { PipelineTab } from "./pipeline-tab"; +import { MemoryTab } from "./memory-tab"; +import { KnowledgeTab } from "./knowledge-tab"; +import { OrchestrationTab } from "./orchestration-tab"; + +interface V3CapabilitiesModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function V3CapabilitiesModal({ + open, + onOpenChange, +}: V3CapabilitiesModalProps) { + const { t } = useTranslation("v3-capabilities"); + + return ( + + + + {t("title")} + {t("subtitle")} + + + + + {t("tabs.pipeline")} + {t("tabs.memory")} + {t("tabs.knowledge")} + + {t("tabs.orchestration")} + + + + + + + + + + + + + + + + + +

{t("note")}

+
+
+ ); +} diff --git a/ui/web/src/components/agents/v3-info-modal/v3-feature-card.tsx b/ui/web/src/components/agents/v3-info-modal/v3-feature-card.tsx new file mode 100644 index 00000000..278444bf --- /dev/null +++ b/ui/web/src/components/agents/v3-info-modal/v3-feature-card.tsx @@ -0,0 +1,28 @@ +import type { LucideIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +interface V3FeatureCardProps { + icon: LucideIcon; + iconColor: string; + title: string; + stat: string; + comparison: string; + description: string; +} + +export function V3FeatureCard({ icon: Icon, iconColor, title, stat, comparison, description }: V3FeatureCardProps) { + return ( +
+ +
+
+

{title}

+ {stat} +
+

{comparison}

+

{description}

+
+
+ ); +} diff --git a/ui/web/src/components/agents/v3-info-modal/v3-info-modal.tsx b/ui/web/src/components/agents/v3-info-modal/v3-info-modal.tsx new file mode 100644 index 00000000..fb21b477 --- /dev/null +++ b/ui/web/src/components/agents/v3-info-modal/v3-info-modal.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { V3InfoTabCore } from "./v3-info-tab-core"; +import { V3InfoTabMemory } from "./v3-info-tab-memory"; +import { V3InfoTabOrchestration } from "./v3-info-tab-orchestration"; + +interface V3InfoModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function V3InfoModal({ open, onOpenChange }: V3InfoModalProps) { + const { t } = useTranslation("agents"); + + return ( + + + + + {t("v3Info.title")} + v3 + + {t("v3Info.subtitle")} + + + + + {t("v3Info.tabs.core")} + {t("v3Info.tabs.memory")} + {t("v3Info.tabs.orchestration")} + + + + + + + +

{t("v3Info.note")}

+
+
+ ); +} diff --git a/ui/web/src/components/agents/v3-info-modal/v3-info-tab-core.tsx b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-core.tsx new file mode 100644 index 00000000..851cdbb9 --- /dev/null +++ b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-core.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from "react-i18next"; +import { Workflow, Shield, BookOpen } from "lucide-react"; +import { V3FeatureCard } from "./v3-feature-card"; + +const FEATURES = [ + { key: "pipeline", icon: Workflow, iconColor: "text-blue-500" }, + { key: "resilience", icon: Shield, iconColor: "text-emerald-500" }, + { key: "registry", icon: BookOpen, iconColor: "text-violet-500" }, +] as const; + +export function V3InfoTabCore() { + const { t } = useTranslation("agents"); + return ( +
+ {FEATURES.map(({ key, icon, iconColor }) => ( + + ))} +
+ ); +} diff --git a/ui/web/src/components/agents/v3-info-modal/v3-info-tab-memory.tsx b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-memory.tsx new file mode 100644 index 00000000..5026b109 --- /dev/null +++ b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-memory.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from "react-i18next"; +import { Brain, Search, Library } from "lucide-react"; +import { V3FeatureCard } from "./v3-feature-card"; + +const FEATURES = [ + { key: "memory", icon: Brain, iconColor: "text-pink-500" }, + { key: "retrieval", icon: Search, iconColor: "text-cyan-500" }, + { key: "vault", icon: Library, iconColor: "text-amber-500" }, +] as const; + +export function V3InfoTabMemory() { + const { t } = useTranslation("agents"); + return ( +
+ {FEATURES.map(({ key, icon, iconColor }) => ( + + ))} +
+ ); +} diff --git a/ui/web/src/components/agents/v3-info-modal/v3-info-tab-orchestration.tsx b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-orchestration.tsx new file mode 100644 index 00000000..6b7872df --- /dev/null +++ b/ui/web/src/components/agents/v3-info-modal/v3-info-tab-orchestration.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next"; +import { GitBranch, TrendingUp } from "lucide-react"; +import { V3FeatureCard } from "./v3-feature-card"; + +const FEATURES = [ + { key: "orchestration", icon: GitBranch, iconColor: "text-indigo-500" }, + { key: "evolution", icon: TrendingUp, iconColor: "text-orange-500" }, +] as const; + +export function V3InfoTabOrchestration() { + const { t } = useTranslation("agents"); + return ( +
+ {FEATURES.map(({ key, icon, iconColor }) => ( + + ))} +
+ ); +} diff --git a/ui/web/src/components/chat/agent-picker-prompt.tsx b/ui/web/src/components/chat/agent-picker-prompt.tsx index 55f42180..6494cdac 100644 --- a/ui/web/src/components/chat/agent-picker-prompt.tsx +++ b/ui/web/src/components/chat/agent-picker-prompt.tsx @@ -10,7 +10,7 @@ interface AgentPickerPromptProps { } function agentEmoji(agent: AgentData): string | undefined { - return (agent.other_config?.emoji as string) || undefined; + return agent.emoji || undefined; } export function AgentPickerPrompt({ onSelect }: AgentPickerPromptProps) { @@ -26,7 +26,7 @@ export function AgentPickerPrompt({ onSelect }: AgentPickerPromptProps) { .then((res) => { setAgents((res.agents ?? []).filter((a) => a.status === "active")); }) - .catch(() => {}); + .catch((err) => console.error("[AgentPickerPrompt] fetch agents failed:", err)); }, [http, connected]); return ( diff --git a/ui/web/src/components/chat/agent-selector.tsx b/ui/web/src/components/chat/agent-selector.tsx index d6c2a8f7..f67d6a5d 100644 --- a/ui/web/src/components/chat/agent-selector.tsx +++ b/ui/web/src/components/chat/agent-selector.tsx @@ -11,9 +11,9 @@ interface AgentSelectorProps { onChange: (agentId: string) => void; } -/** Extract emoji from agent's other_config JSONB */ +/** Extract emoji from agent top-level field */ function agentEmoji(agent: AgentData): string | undefined { - return (agent.other_config?.emoji as string) || undefined; + return agent.emoji || undefined; } export function AgentSelector({ value, onChange }: AgentSelectorProps) { @@ -34,7 +34,7 @@ export function AgentSelector({ value, onChange }: AgentSelectorProps) { const active = (res.agents ?? []).filter((a) => a.status === "active"); setAgents(active); }) - .catch(() => {}); + .catch((err) => console.error("[AgentSelector] fetch agents failed:", err)); }, [http, connected]); useLayoutEffect(() => { diff --git a/ui/web/src/components/chat/chat-top-bar.tsx b/ui/web/src/components/chat/chat-top-bar.tsx index 31534fdc..527977a5 100644 --- a/ui/web/src/components/chat/chat-top-bar.tsx +++ b/ui/web/src/components/chat/chat-top-bar.tsx @@ -38,7 +38,7 @@ export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks, on .then((res) => { const found = (res.agents ?? []).find((a) => a.agent_key === agentId); if (found) { - const emoji = (found.other_config?.emoji as string) || undefined; + const emoji = found.emoji || undefined; setAgent({ name: found.display_name || found.agent_key, emoji }); } else { setAgent({ name: agentId }); diff --git a/ui/web/src/components/chat/message-bubble.tsx b/ui/web/src/components/chat/message-bubble.tsx index 4366a3c9..e52f11f1 100644 --- a/ui/web/src/components/chat/message-bubble.tsx +++ b/ui/web/src/components/chat/message-bubble.tsx @@ -76,7 +76,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
)} {message.timestamp && ( -
+
{new Intl.DateTimeFormat([], { timeZone: resolveTimezone(timezone), hour: "numeric", diff --git a/ui/web/src/components/chat/session-switcher.tsx b/ui/web/src/components/chat/session-switcher.tsx index 2e807843..77543a05 100644 --- a/ui/web/src/components/chat/session-switcher.tsx +++ b/ui/web/src/components/chat/session-switcher.tsx @@ -87,7 +87,7 @@ export const SessionSwitcher = memo(function SessionSwitcher({ sessions, activeK
{label}
-
+
{session.messageCount} {tc("messages")} · {formatRelativeTime(session.updated)} diff --git a/ui/web/src/components/chat/task-panel.tsx b/ui/web/src/components/chat/task-panel.tsx index 3120100e..86041aa3 100644 --- a/ui/web/src/components/chat/task-panel.tsx +++ b/ui/web/src/components/chat/task-panel.tsx @@ -75,14 +75,14 @@ function TaskCard({ task }: { task: ActiveTeamTask }) { style={{ width: `${Math.min(pct, 100)}%` }} />
- {pct}% + {pct}%
)} {/* Step message — full text, no truncation */} {task.progressStep && ( -

+

{task.progressStep}

)} diff --git a/ui/web/src/components/chat/tool-call-card.tsx b/ui/web/src/components/chat/tool-call-card.tsx index 6935d35b..122f9fd6 100644 --- a/ui/web/src/components/chat/tool-call-card.tsx +++ b/ui/web/src/components/chat/tool-call-card.tsx @@ -55,16 +55,16 @@ export function ToolCallCard({ entry, compact }: ToolCallCardProps) { )} {entry.arguments && Object.keys(entry.arguments).length > 0 && (
-
{t("toolArguments")}
-
+              
{t("toolArguments")}
+
                 {JSON.stringify(entry.arguments, null, 2)}
               
)} {entry.result && (
-
{t("toolResult")}
-
+              
{t("toolResult")}
+
                 {entry.result}
               
@@ -103,5 +103,5 @@ function PhaseLabel({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSki completed: "text-blue-500", error: "text-red-500", }; - return {labels[phase] ?? phase}; + return {labels[phase] ?? phase}; } diff --git a/ui/web/src/components/layout/connection-status.tsx b/ui/web/src/components/layout/connection-status.tsx index 94b80130..bcc21786 100644 --- a/ui/web/src/components/layout/connection-status.tsx +++ b/ui/web/src/components/layout/connection-status.tsx @@ -3,27 +3,51 @@ import { useAuthStore } from "@/stores/use-auth-store"; import { cn } from "@/lib/utils"; import { cleanVersion } from "@/lib/clean-version"; +const ROLE_STYLES: Record = { + admin: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300", + owner: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", + operator: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", + viewer: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300", +}; + export function ConnectionStatus({ collapsed }: { collapsed?: boolean }) { const { t } = useTranslation("common"); const connected = useAuthStore((s) => s.connected); const serverVersion = useAuthStore((s) => s.serverInfo?.version); + const tenantName = useAuthStore((s) => s.tenantName); + const role = useAuthStore((s) => s.role); return ( -
- - {!collapsed && ( - - {connected ? t("connected") : t("disconnected")} - {connected && serverVersion && ( - · {cleanVersion(serverVersion)} +
+ {/* Tenant + role (expanded only) */} + {!collapsed && tenantName && ( +
+ {tenantName} + {role && ( + + {role} + )} - +
)} + + {/* Connection status */} +
+ + {!collapsed && ( + + {connected ? t("connected") : t("disconnected")} + {connected && serverVersion && ( + · {cleanVersion(serverVersion)} + )} + + )} +
); } diff --git a/ui/web/src/components/layout/sidebar.tsx b/ui/web/src/components/layout/sidebar.tsx index 3b42745b..7164bf43 100644 --- a/ui/web/src/components/layout/sidebar.tsx +++ b/ui/web/src/components/layout/sidebar.tsx @@ -27,6 +27,8 @@ import { KeyRound, Building2, ArrowLeftRight, + FileArchive, + DatabaseBackup, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { SidebarGroup } from "./sidebar-group"; @@ -110,6 +112,7 @@ export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) { + @@ -135,6 +138,9 @@ export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) { )} + {isOwner && ( + + )} )} diff --git a/ui/web/src/components/shared/file-browser.tsx b/ui/web/src/components/shared/file-browser.tsx index 6ede1210..ae036a0e 100644 --- a/ui/web/src/components/shared/file-browser.tsx +++ b/ui/web/src/components/shared/file-browser.tsx @@ -28,7 +28,7 @@ function FileActions({ const { t } = useTranslation("common"); return (
- + {formatSize(size)} {onDownload && ( diff --git a/ui/web/src/components/shared/file-tree.tsx b/ui/web/src/components/shared/file-tree.tsx index 62ba47d6..381d374d 100644 --- a/ui/web/src/components/shared/file-tree.tsx +++ b/ui/web/src/components/shared/file-tree.tsx @@ -64,7 +64,7 @@ export function TreeItem({ ); const sizeLabel = showSize && (node.isDir ? 0 : node.size) > 0 && ( - + {formatSize(node.size)} ); diff --git a/ui/web/src/components/shared/file-viewer-panels.tsx b/ui/web/src/components/shared/file-viewer-panels.tsx index 935a6e45..8e942f46 100644 --- a/ui/web/src/components/shared/file-viewer-panels.tsx +++ b/ui/web/src/components/shared/file-viewer-panels.tsx @@ -66,7 +66,7 @@ export function CodeViewer({ content, language }: { content: string; language: s return (
-
+
{language || "text"} diff --git a/ui/web/src/components/shared/markdown-callout-block.tsx b/ui/web/src/components/shared/markdown-callout-block.tsx new file mode 100644 index 00000000..c2ce4530 --- /dev/null +++ b/ui/web/src/components/shared/markdown-callout-block.tsx @@ -0,0 +1,86 @@ +/** + * CalloutBlock — styled callout/admonition block rendered from [!type] blockquotes. + * Supports: note, info, warning, tip, danger, important + * Full dark mode support via Tailwind dark: variants. + */ +import { Info, AlertTriangle, Lightbulb, AlertOctagon, Star } from "lucide-react"; +import type { CalloutType } from "@/lib/remark-callouts"; + +interface CalloutConfig { + icon: React.ElementType; + borderColor: string; + bgColor: string; + titleColor: string; + iconColor: string; +} + +const CALLOUT_CONFIG: Record = { + note: { + icon: Info, + borderColor: "border-blue-400 dark:border-blue-500", + bgColor: "bg-blue-50 dark:bg-blue-950/40", + titleColor: "text-blue-700 dark:text-blue-300", + iconColor: "text-blue-500 dark:text-blue-400", + }, + info: { + icon: Info, + borderColor: "border-blue-400 dark:border-blue-500", + bgColor: "bg-blue-50 dark:bg-blue-950/40", + titleColor: "text-blue-700 dark:text-blue-300", + iconColor: "text-blue-500 dark:text-blue-400", + }, + warning: { + icon: AlertTriangle, + borderColor: "border-amber-400 dark:border-amber-500", + bgColor: "bg-amber-50 dark:bg-amber-950/40", + titleColor: "text-amber-700 dark:text-amber-300", + iconColor: "text-amber-500 dark:text-amber-400", + }, + tip: { + icon: Lightbulb, + borderColor: "border-green-400 dark:border-green-500", + bgColor: "bg-green-50 dark:bg-green-950/40", + titleColor: "text-green-700 dark:text-green-300", + iconColor: "text-green-500 dark:text-green-400", + }, + danger: { + icon: AlertOctagon, + borderColor: "border-red-400 dark:border-red-500", + bgColor: "bg-red-50 dark:bg-red-950/40", + titleColor: "text-red-700 dark:text-red-300", + iconColor: "text-red-500 dark:text-red-400", + }, + important: { + icon: Star, + borderColor: "border-purple-400 dark:border-purple-500", + bgColor: "bg-purple-50 dark:bg-purple-950/40", + titleColor: "text-purple-700 dark:text-purple-300", + iconColor: "text-purple-500 dark:text-purple-400", + }, +}; + +interface CalloutBlockProps { + calloutType?: string; + calloutTitle?: string; + children?: React.ReactNode; +} + +export function CalloutBlock({ calloutType, calloutTitle, children }: CalloutBlockProps) { + const type = (calloutType as CalloutType) ?? "note"; + const config = CALLOUT_CONFIG[type] ?? CALLOUT_CONFIG.note; + const Icon = config.icon; + + return ( +
+
+ + {calloutTitle} +
+
+ {children} +
+
+ ); +} diff --git a/ui/web/src/components/shared/markdown-code-block.tsx b/ui/web/src/components/shared/markdown-code-block.tsx index a9557d5c..f9121a33 100644 --- a/ui/web/src/components/shared/markdown-code-block.tsx +++ b/ui/web/src/components/shared/markdown-code-block.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { Check, Copy } from "lucide-react"; import { useClipboard } from "@/hooks/use-clipboard"; +import { cn } from "@/lib/utils"; export function CodeBlock({ className, @@ -20,7 +21,7 @@ export function CodeBlock({ return (
-
+
{lang || "code"} + ); + } + + return ( + + + {target} + + ); +} diff --git a/ui/web/src/components/shared/provider-model-select.tsx b/ui/web/src/components/shared/provider-model-select.tsx index 5e27ce04..265bbffa 100644 --- a/ui/web/src/components/shared/provider-model-select.tsx +++ b/ui/web/src/components/shared/provider-model-select.tsx @@ -151,7 +151,7 @@ export function ProviderModelSelect({ {p.display_name || p.name} {poolOwnership.membersByOwner.has(p.name) && ( - + {t("providers:list.poolBadge")} )} diff --git a/ui/web/src/components/shared/tool-name-select.tsx b/ui/web/src/components/shared/tool-name-select.tsx index b26ed7cc..39d00d6f 100644 --- a/ui/web/src/components/shared/tool-name-select.tsx +++ b/ui/web/src/components/shared/tool-name-select.tsx @@ -155,7 +155,7 @@ export function ToolNameSelect({ > {grouped.builtin.length > 0 && ( <> -
+
{t("builtinTools")}
{grouped.builtin.map((t) => ( @@ -167,7 +167,7 @@ export function ToolNameSelect({ className="hover:bg-accent hover:text-accent-foreground flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none" > {t.displayName} - {t.name} + {t.name} ))} diff --git a/ui/web/src/components/ui/input.tsx b/ui/web/src/components/ui/input.tsx index c4b77ded..76019331 100644 --- a/ui/web/src/components/ui/input.tsx +++ b/ui/web/src/components/ui/input.tsx @@ -1,21 +1,38 @@ import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" -function Input({ className, type, ...props }: React.ComponentProps<"input">) { +const inputVariants = cva( + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + size: { + default: "h-9", + sm: "h-8 text-sm", + lg: "h-10", + }, + }, + defaultVariants: { + size: "default", + }, + } +) + +function Input({ + className, + type, + size, + ...props +}: React.ComponentProps<"input"> & VariantProps) { return ( ) } -export { Input } +export { Input, inputVariants } diff --git a/ui/web/src/components/ui/select.tsx b/ui/web/src/components/ui/select.tsx index 49ddbc15..1f9f2171 100644 --- a/ui/web/src/components/ui/select.tsx +++ b/ui/web/src/components/ui/select.tsx @@ -3,9 +3,25 @@ import * as React from "react" import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" import { Select as SelectPrimitive } from "radix-ui" +import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" +const selectTriggerVariants = cva( + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base md:text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + size: { + default: "h-9", + sm: "h-8", + }, + }, + defaultVariants: { + size: "default", + }, + } +) + function Select({ ...props }: React.ComponentProps) { @@ -29,17 +45,11 @@ function SelectTrigger({ size = "default", children, ...props -}: React.ComponentProps & { - size?: "sm" | "default" -}) { +}: React.ComponentProps & VariantProps) { return ( {children} @@ -186,5 +196,6 @@ export { SelectScrollUpButton, SelectSeparator, SelectTrigger, + selectTriggerVariants, SelectValue, } diff --git a/ui/web/src/components/ui/textarea.tsx b/ui/web/src/components/ui/textarea.tsx index 210bfc56..cff7d033 100644 --- a/ui/web/src/components/ui/textarea.tsx +++ b/ui/web/src/components/ui/textarea.tsx @@ -1,18 +1,36 @@ import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { +const textareaVariants = cva( + "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + { + variants: { + size: { + default: "min-h-16", + sm: "min-h-10", + lg: "min-h-24", + }, + }, + defaultVariants: { + size: "default", + }, + } +) + +function Textarea({ + className, + size, + ...props +}: React.ComponentProps<"textarea"> & VariantProps) { return (