== User stories == US1: Контекст с дашборда/датасета → /agent с URL params US2: Guardrails card — env badge, 7 risk tones, countdown, permission_denied US3: Tools optimization — retry, timeout, summarise, RBAC + context affinity == Backend == - _context.py (NEW): UIContext validation (7 checks) - _tool_filter.py (NEW): RBAC + context affinity pipeline - _confirmation.py: build_confirmation_contract_v2, permission_denied_payload - tools.py: superset_list_databases, retry/summarise/timeout wrappers - app.py: _inject_uicontext, _inject_env_id_into_tools, database prefetch в runtime context - _persistence.py: prefetch_databases() - agent_superset_explore.py: GET /databases endpoint - _llm_async_http.py, _persistence.py: fix double /v1 в LLM URL (LM Studio Unexpected endpoint) == Frontend == - AgentChatModel.svelte.ts: 5 atoms, 3 actions, countdown, context - AgentChat.svelte: production banner, process steps, debug panel - ConfirmationCard.svelte: 7 risk tones, permission_denied, countdown - ToolCallCard.svelte: retrying/timeout/cancelled states - StreamProcessor.svelte.ts: tool_retry, timeout, permission_denied - TopNavbar: sparkles icon + Ассистент - sidebarNavigation: AI section - DashboardHeader, datasets/+page: contextual AI buttons - Icon: sparkles, brain, cpu icons - tailwind: assistant category colors - i18n: en/ru nav keys == Tests == - 159 backend agent tests (+16 US3: retry, timeout, summarise, contracts) - 2544 frontend tests (+11 model + component tests) - 15 JSON fixtures (10 API + 5 model) == Specs == - specs/035-agent-chat-context/: spec, UX, plan, tasks, research, data-model, contracts, quickstart, traceability, fixtures, checklists Closes #035
578 lines
19 KiB
Markdown
578 lines
19 KiB
Markdown
#region ModelChanges [C:3] [TYPE ADR] [SEMANTICS ux,model-changes,agent-chat]
|
||
@defgroup Ux Precise edit instructions for existing model/component files. Implementation target for /speckit.implement.
|
||
|
||
## File 1: `frontend/src/lib/models/AgentChatTypes.ts`
|
||
|
||
**ADD** after existing types:
|
||
|
||
```typescript
|
||
// ── UI Context (035-agent-chat-context) ─────────────────────────
|
||
|
||
/** Structured context about the user's current page — passed from frontend to agent on each message. */
|
||
export interface UIContext {
|
||
objectType: "dashboard" | "dataset" | "migration" | null;
|
||
objectId: string | null;
|
||
objectName: string | null;
|
||
envId: string | null;
|
||
route: string;
|
||
contextVersion: number;
|
||
}
|
||
|
||
/** Maps objectType to tool operation names — server-side, shown in debug panel. */
|
||
export interface ToolAffinityEntry {
|
||
operation: string;
|
||
active: boolean; // true = tool available for this context
|
||
reason?: string; // why excluded (e.g. "dataset tool")
|
||
}
|
||
|
||
/** Extended confirmation metadata from backend SSE. */
|
||
export interface ConfirmMetadataV2 {
|
||
type: "confirm_required";
|
||
thread_id: string;
|
||
tool_name: string;
|
||
tool_args: Record<string, unknown>;
|
||
risk: "read" | "write";
|
||
risk_level: "safe" | "guarded" | "dangerous";
|
||
dangerous: boolean;
|
||
env_context: "prod" | "staging" | "dev" | null;
|
||
permission_granted: boolean;
|
||
required_role: string | null;
|
||
alternatives: Array<{ action: string; prompt: string }> | null;
|
||
prompt?: string;
|
||
}
|
||
```
|
||
|
||
**EXTEND** `ToolCall` interface — add `"retrying" | "timeout" | "cancelled"` to `status` union:
|
||
|
||
```typescript
|
||
export interface ToolCall {
|
||
id: string;
|
||
name: string;
|
||
status: "executing" | "retrying" | "completed" | "failed" | "timeout" | "cancelled";
|
||
input?: Record<string, unknown>;
|
||
output?: { result: string };
|
||
error?: string;
|
||
retryCount?: number;
|
||
maxRetries?: number;
|
||
timeoutSeconds?: number;
|
||
}
|
||
```
|
||
|
||
**EXTEND** `StreamingState` union — add `"disconnected_permanent"` (already exists in model, ensure type includes it):
|
||
|
||
```typescript
|
||
export type StreamingState = "idle" | "streaming" | "awaiting_confirmation" | "error" | "disconnected" | "disconnected_permanent";
|
||
```
|
||
|
||
---
|
||
|
||
## File 2: `frontend/src/lib/models/AgentChatModel.svelte.ts`
|
||
|
||
### 2a. ADD import (top of file):
|
||
|
||
```typescript
|
||
import type { UIContext, ToolAffinityEntry, ConfirmMetadataV2 } from "./AgentChatTypes.js";
|
||
```
|
||
|
||
### 2b. ADD atoms (after existing `envId` atom, ~line 113):
|
||
|
||
```typescript
|
||
// ── UI Context (035-agent-chat-context) ────────────────────────
|
||
uiContext: UIContext | null = $state(null);
|
||
toolAffinity: ToolAffinityEntry[] = $state([]);
|
||
dangerousCountdown: number = $state(0);
|
||
dangerousCountdownActive: boolean = $state(false);
|
||
/** Tracks env from EnvSelector (may differ from uiContext.envId during dialog). */
|
||
_activeEnvId: string | null = $state(null);
|
||
/** Alternatives suggested by backend when permission denied. */
|
||
permissionAlternatives: Array<{ action: string; prompt: string }> | null = $state(null);
|
||
```
|
||
|
||
### 2c. ADD derived (after existing derived block, ~line 317):
|
||
|
||
```typescript
|
||
// ── Prod context detection ─────────────────────────────────────
|
||
/** True when either uiContext or active env selector is prod. */
|
||
isProdContext = $derived(
|
||
(this.uiContext?.envId === "prod") || this._activeEnvId === "prod"
|
||
);
|
||
showProdWarning = $derived(this.isProdContext);
|
||
|
||
// ── Context pill label (extended, replaces simple envId label) ─
|
||
contextPillLabel = $derived.by((): string => {
|
||
const env = this._activeEnvId || this.uiContext?.envId || "";
|
||
if (!this.uiContext || !this.uiContext.objectType) {
|
||
return env ? `🌐 ${env}` : "⚪ Контекст не выбран";
|
||
}
|
||
const typeIcons: Record<string, string> = {
|
||
dashboard: "📋", dataset: "📊", migration: "🔄",
|
||
};
|
||
const icon = typeIcons[this.uiContext.objectType] || "📄";
|
||
const idLabel = this.uiContext.objectId ? ` #${this.uiContext.objectId}` : "";
|
||
return `${icon} ${this.uiContext.objectType}${idLabel} · ${env}`;
|
||
});
|
||
|
||
// ── Process steps (extended — first step uses contextPillLabel) ──
|
||
processSteps = $derived.by((): AgentProcessStep[] => {
|
||
const hasTools = this.activeToolCalls.length > 0 || this.messages.some((msg) => (msg.toolCalls || []).length > 0);
|
||
const hasAssistantResult = this.messages.some((msg) => msg.role === "assistant" && msg.text.trim().length > 0);
|
||
const failed = this.streamingState === "error";
|
||
return [
|
||
{
|
||
id: "context",
|
||
label: this.contextPillLabel,
|
||
state: (this.uiContext || this._activeEnvId) ? (failed ? "blocked" : "done") : (failed ? "blocked" : "idle"),
|
||
},
|
||
// ... rest of existing steps unchanged ...
|
||
];
|
||
});
|
||
|
||
// ── Confirmation risk (extended — use backend metadata) ────────
|
||
// НЕ менять существующий $derived — только убедиться что он читает
|
||
// pendingConfirmationRisk, pendingRiskLevel из StreamProcessor
|
||
```
|
||
|
||
### 2d. ADD actions (before `sendMessage`, ~line 410):
|
||
|
||
```typescript
|
||
// ── Context actions (035-agent-chat-context) ────────────────────
|
||
|
||
/** Set UI context from URL params on page mount. */
|
||
setUIContextFromParams(params: URLSearchParams): void {
|
||
const objectType = params.get("objectType") as UIContext["objectType"] || null;
|
||
const objectId = params.get("objectId") || null;
|
||
const objectName = params.get("objectName") || null;
|
||
const envId = params.get("envId") || null;
|
||
const route = params.get("route") || "/agent";
|
||
this.uiContext = {
|
||
objectType, objectId, objectName, envId, route, contextVersion: 1,
|
||
};
|
||
this._activeEnvId = envId;
|
||
log("AgentChat.Model", "REASON", "UI context set from params", {
|
||
objectType, objectId, envId,
|
||
});
|
||
}
|
||
|
||
/** Update environment from EnvSelector during dialog. */
|
||
updateActiveEnv(envId: string | null): void {
|
||
this._activeEnvId = envId;
|
||
// Note: envId for tool calls is still passed separately — this tracks for UI only
|
||
}
|
||
|
||
/** Start dangerous operation countdown. Called by StreamProcessor. */
|
||
startDangerousCountdown(): void {
|
||
this.dangerousCountdown = 10;
|
||
this.dangerousCountdownActive = true;
|
||
const interval = setInterval(() => {
|
||
this.dangerousCountdown--;
|
||
if (this.dangerousCountdown <= 0) {
|
||
this.dangerousCountdownActive = false;
|
||
clearInterval(interval);
|
||
}
|
||
}, 1000);
|
||
}
|
||
```
|
||
|
||
### 2e. UPDATE `resetStreamingState` (internal helper) — add context reset:
|
||
|
||
```typescript
|
||
// В методе, который сбрасывает состояние перед новым стримом:
|
||
private _resetForStream(): void {
|
||
// ... existing resets ...
|
||
this.dangerousCountdown = 0;
|
||
this.dangerousCountdownActive = false;
|
||
this.permissionAlternatives = null;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## File 3: `frontend/src/lib/models/AgentChat.StreamProcessor.svelte.ts`
|
||
|
||
### 3a. ADD new metadata handlers (in `_handleMetadata` switch):
|
||
|
||
```typescript
|
||
// После существующих case "tool_error":
|
||
case "tool_retry": {
|
||
const toolCall = host.activeToolCalls.find(t => t.name === meta.tool);
|
||
if (toolCall) {
|
||
toolCall.status = "retrying";
|
||
toolCall.retryCount = meta.attempt;
|
||
toolCall.maxRetries = meta.max_attempts;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case "tool_timeout": {
|
||
const toolCall = host.activeToolCalls.find(t => t.name === meta.tool);
|
||
if (toolCall) {
|
||
toolCall.status = "timeout";
|
||
toolCall.timeoutSeconds = meta.timeout_seconds;
|
||
}
|
||
break;
|
||
}
|
||
|
||
// Расширить существующий case "confirm_required":
|
||
case "confirm_required": {
|
||
host.pendingToolName = meta.tool_name;
|
||
host.pendingToolArgs = meta.tool_args || {};
|
||
host.pendingConfirmationRisk = meta.risk === "read" ? "read" : "write";
|
||
host.pendingRiskLevel = meta.risk_level;
|
||
host.confirmationMessage = meta.prompt || null;
|
||
host.streamingState = "awaiting_confirmation";
|
||
// NEW fields:
|
||
if (meta.dangerous) {
|
||
host.startDangerousCountdown?.();
|
||
}
|
||
if (!meta.permission_granted) {
|
||
host.permissionAlternatives = meta.alternatives;
|
||
host.pendingRiskLevel = "permission_denied";
|
||
}
|
||
break;
|
||
}
|
||
```
|
||
|
||
### 3b. EXTEND `handleResumeResponse` — cancelled tool card:
|
||
|
||
```typescript
|
||
// В обработчике deny/confirm_resolved с result "denied":
|
||
// Добавить ToolCall в activeToolCalls со статусом "cancelled"
|
||
// чтобы карточка «Отменено» появилась в чате
|
||
if (meta.type === "confirm_resolved" && meta.result === "denied") {
|
||
const toolName = host.pendingToolName || "unknown_action";
|
||
host.activeToolCalls = [...host.activeToolCalls, {
|
||
id: `cancelled-${Date.now()}`,
|
||
name: toolName,
|
||
status: "cancelled",
|
||
input: host.pendingToolArgs as Record<string, unknown> | undefined,
|
||
}];
|
||
host.pendingToolName = null;
|
||
host.pendingToolArgs = {};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## File 4: `frontend/src/routes/agent/+page.svelte`
|
||
|
||
### 4a. ADD onMount context init:
|
||
|
||
```svelte
|
||
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import { page } from "$app/stores";
|
||
|
||
// ... existing imports ...
|
||
|
||
onMount(() => {
|
||
// Read context from URL params
|
||
const params = new URLSearchParams($page.url.search);
|
||
model.setUIContextFromParams(params);
|
||
model.checkLlmStatus();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
### 4b. ADD EnvSelector integration:
|
||
|
||
```svelte
|
||
<!-- Если EnvSelector присутствует на странице агента: -->
|
||
<EnvSelector
|
||
selectedEnvId={model._activeEnvId}
|
||
onEnvChange={(envId) => model.updateActiveEnv(envId)}
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
## File 5: `frontend/src/lib/components/agent/AgentChat.svelte`
|
||
|
||
### 5a. ADD prod background binding (корневой div):
|
||
|
||
```svelte
|
||
<div
|
||
class="h-full flex flex-col min-h-0 {model.showProdWarning ? 'bg-warning-light' : 'bg-surface-page'}"
|
||
>
|
||
```
|
||
|
||
### 5b. ADD prod banner (перед header):
|
||
|
||
```svelte
|
||
{#if model.showProdWarning}
|
||
<div class="bg-destructive text-white text-xs font-bold text-center py-0.5 shrink-0" role="status" aria-live="polite">
|
||
<Icon name="warning" size={12} class="inline mr-1" />
|
||
PRODUCTION
|
||
</div>
|
||
{/if}
|
||
```
|
||
|
||
### 5c. UPDATE process steps rendering — первый шаг использует contextPillLabel:
|
||
|
||
```svelte
|
||
<!-- Вместо хардкода "Контекст: ss-dev" использовать model.contextPillLabel -->
|
||
{#each model.processSteps as step}
|
||
<span class="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-medium {processStepClasses(step.state)}">
|
||
{step.label}
|
||
</span>
|
||
{/each}
|
||
```
|
||
|
||
---
|
||
|
||
## File 6: `frontend/src/lib/components/assistant/ConfirmationCard.svelte`
|
||
|
||
### 6a. ADD env badge to header:
|
||
|
||
```svelte
|
||
{#if meta.env_context}
|
||
<span class="rounded px-1.5 py-0.5 font-mono text-[11px] font-semibold
|
||
{meta.env_context === 'prod' ? 'bg-destructive-light text-destructive' : ''}
|
||
{meta.env_context === 'staging' ? 'bg-warning-light text-warning' : ''}
|
||
{meta.env_context === 'dev' ? 'bg-info-light text-info' : ''}
|
||
">
|
||
{meta.env_context === 'prod' ? '👑 PRODUCTION' : meta.env_context}
|
||
</span>
|
||
{/if}
|
||
```
|
||
|
||
### 6b. ADD dangerous countdown logic:
|
||
|
||
```svelte
|
||
<script lang="ts">
|
||
// ... existing ...
|
||
|
||
let dangerousTimer = $state(10);
|
||
let timerInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
$effect(() => {
|
||
if (model.streamingState === "awaiting_confirmation" && model.pendingRiskLevel === "dangerous") {
|
||
dangerousTimer = 10;
|
||
timerInterval = setInterval(() => {
|
||
dangerousTimer--;
|
||
if (dangerousTimer <= 0) {
|
||
if (timerInterval) clearInterval(timerInterval);
|
||
timerInterval = null;
|
||
}
|
||
}, 1000);
|
||
}
|
||
return () => {
|
||
if (timerInterval) clearInterval(timerInterval);
|
||
};
|
||
});
|
||
</script>
|
||
|
||
<!-- Кнопка подтверждения для dangerous: -->
|
||
<Button
|
||
variant="destructive"
|
||
size="sm"
|
||
isLoading={loading === "confirming"}
|
||
disabled={loading !== "idle" || dangerousTimer > 0}
|
||
onclick={handleConfirm}
|
||
>
|
||
{dangerousTimer > 0
|
||
? `💀 Удалить (${dangerousTimer})`
|
||
: loading === "confirming"
|
||
? "Подтверждение…"
|
||
: "💀 Удалить"}
|
||
</Button>
|
||
```
|
||
|
||
### 6c. ADD permission_denied state (alternatives list):
|
||
|
||
```svelte
|
||
{#if !meta.permission_granted}
|
||
<div class="mb-3 rounded-lg border border-border bg-surface-page px-3.5 py-2.5">
|
||
<p class="text-sm font-medium text-text mb-1">
|
||
🔒 Недостаточно прав
|
||
</p>
|
||
<p class="text-xs text-text-muted mb-2">
|
||
Требуется роль: <code class="rounded bg-surface-muted px-1 py-0.5 font-mono text-destructive">{meta.required_role}</code>
|
||
</p>
|
||
{#if meta.alternatives?.length}
|
||
<p class="text-xs font-medium text-text mb-1">💡 Альтернативы:</p>
|
||
<ul class="space-y-1">
|
||
{#each meta.alternatives as alt}
|
||
<li class="text-xs text-text-muted">• {alt.prompt}</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
</div>
|
||
<Button variant="ghost" size="sm" onclick={handleClosePermissionDenied}>Закрыть</Button>
|
||
{/if}
|
||
```
|
||
|
||
---
|
||
|
||
## File 7: `frontend/src/lib/components/assistant/ToolCallCard.svelte`
|
||
|
||
### 7a. ADD new states:
|
||
|
||
```svelte
|
||
<script lang="ts">
|
||
export let onRetry: (() => void) | undefined = undefined;
|
||
export let onCancelTimeout: (() => void) | undefined = undefined;
|
||
</script>
|
||
|
||
{#if tool.status === "retrying"}
|
||
<div class="flex items-center gap-2">
|
||
<span class="animate-spin">⟳</span>
|
||
<span class="text-xs text-text-muted">
|
||
Повторная попытка... ({tool.retryCount}/{tool.maxRetries})
|
||
</span>
|
||
</div>
|
||
{:else if tool.status === "timeout"}
|
||
<div class="flex items-center gap-2">
|
||
<span>⏱️</span>
|
||
<span class="text-xs text-warning">Превышено время ожидания ({tool.timeoutSeconds}с)</span>
|
||
</div>
|
||
{#if onRetry}
|
||
<Button variant="secondary" size="xs" onclick={onRetry} class="mt-1">
|
||
🔄 Повторить
|
||
</Button>
|
||
{/if}
|
||
{:else if tool.status === "cancelled"}
|
||
<div class="flex items-center gap-2">
|
||
<span>🚫</span>
|
||
<span class="text-xs text-text-muted">Отменено пользователем</span>
|
||
</div>
|
||
{/if}
|
||
```
|
||
|
||
---
|
||
|
||
## File 8: `frontend/src/lib/stores/assistantChat.svelte.ts`
|
||
|
||
### 8a. ADD initialContext field:
|
||
|
||
```typescript
|
||
// Добавить в существующий объект assistantChatStore:
|
||
initialContext: UIContext | null = $state(null);
|
||
|
||
// Добавить action:
|
||
setInitialContext(ctx: UIContext | null) {
|
||
this.initialContext = ctx;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## File 9: `backend/src/agent/app.py`
|
||
|
||
### 9a. ADD uicontext parameter to agent_handler:
|
||
|
||
```python
|
||
# В сигнатуре agent_handler:
|
||
async def agent_handler(
|
||
message: dict,
|
||
history: list,
|
||
conversation_id: str | None = None,
|
||
uicontext_str: str | None = None, # NEW — JSON string from frontend
|
||
user_id: str | None = None,
|
||
user_jwt: str | None = None,
|
||
env_id: str | None = None,
|
||
):
|
||
# Parse uicontext:
|
||
uicontext = None
|
||
if uicontext_str:
|
||
try:
|
||
uicontext = json.loads(uicontext_str)
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Inject into runtime context:
|
||
if uicontext:
|
||
runtime_parts = _build_agent_context(env_id) # existing
|
||
runtime_parts = _inject_uicontext(runtime_parts, uicontext) # NEW
|
||
|
||
# Filter tools by objectType:
|
||
tools = get_all_tools()
|
||
if uicontext and uicontext.get("objectType"):
|
||
tools = _filter_tools_by_context(tools, uicontext["objectType"])
|
||
```
|
||
|
||
### 9b. NEW helper function:
|
||
|
||
```python
|
||
def _inject_uicontext(runtime_context: str, uicontext: dict) -> str:
|
||
"""Add user-facing context to the runtime context block."""
|
||
lines = [runtime_context]
|
||
lines.append("\n[USER CONTEXT]")
|
||
lines.append(f"User is currently on page: {uicontext.get('route', 'unknown')}")
|
||
if uicontext.get("objectType") and uicontext.get("objectId"):
|
||
lines.append(f"Active object: {uicontext['objectType']} id={uicontext['objectId']}")
|
||
if uicontext.get("objectName"):
|
||
lines.append(f"Object name: {uicontext['objectName']}")
|
||
if uicontext.get("envId"):
|
||
lines.append(f"Current environment: {uicontext['envId']}")
|
||
lines.append("[/USER CONTEXT]")
|
||
return "\n".join(lines)
|
||
```
|
||
|
||
---
|
||
|
||
## File 10: `backend/src/agent/_tool_filter.py` (NEW)
|
||
|
||
```python
|
||
# backend/src/agent/_tool_filter.py
|
||
# #region AgentChat.ToolFilter [C:3] [TYPE Module] [SEMANTICS agent-chat,tools,filter,context]
|
||
# @defgroup AgentChat Context-aware tool filtering.
|
||
|
||
from typing import Any
|
||
|
||
# Mapping: objectType → set of relevant tool operations
|
||
_CONTEXT_TOOL_AFFINITY: dict[str, set[str]] = {
|
||
"dashboard": {
|
||
"search_dashboards", "get_health_summary", "deploy_dashboard",
|
||
"run_llm_validation", "run_llm_documentation", "execute_migration",
|
||
"create_branch", "commit_changes", "show_capabilities",
|
||
"get_task_status", "list_environments",
|
||
},
|
||
"dataset": {
|
||
"superset_explore_database", "superset_format_sql",
|
||
"superset_audit_permissions", "superset_execute_sql",
|
||
"superset_create_dataset", "search_dashboards",
|
||
"show_capabilities", "get_task_status", "list_environments",
|
||
},
|
||
"migration": {
|
||
"execute_migration", "search_dashboards", "get_health_summary",
|
||
"deploy_dashboard", "show_capabilities", "list_environments",
|
||
},
|
||
}
|
||
|
||
def filter_tools_by_context(tools: list[Any], object_type: str | None) -> list[Any]:
|
||
"""Filter tool list to context-relevant tools only."""
|
||
if not object_type or object_type not in _CONTEXT_TOOL_AFFINITY:
|
||
return tools # no filter — return all tools
|
||
|
||
allowed = _CONTEXT_TOOL_AFFINITY[object_type]
|
||
# Always include show_capabilities
|
||
allowed.add("show_capabilities")
|
||
return [t for t in tools if t.name in allowed]
|
||
|
||
def get_tool_affinity(object_type: str | None) -> list[dict[str, Any]]:
|
||
"""Return affinity list for debug panel (all tools, marked active/inactive)."""
|
||
# Used by debug endpoint
|
||
...
|
||
# #endregion AgentChat.ToolFilter
|
||
```
|
||
|
||
---
|
||
|
||
## Итого по изменениям
|
||
|
||
| Файл | Тип изменения | Строк (±) |
|
||
|------|:------------:|:---------:|
|
||
| `AgentChatTypes.ts` | Расширение типов | +35 |
|
||
| `AgentChatModel.svelte.ts` | Добавление атомов/derived/actions | +60 |
|
||
| `AgentChat.StreamProcessor.svelte.ts` | Новые handler'ы | +35 |
|
||
| `AgentChat.svelte` | Prod bg + banner + pill | +15 |
|
||
| `ConfirmationCard.svelte` | Новые состояния + countdown | +50 |
|
||
| `ToolCallCard.svelte` | Новые состояния + retry button | +25 |
|
||
| `assistantChat.svelte.ts` | Поле initialContext | +5 |
|
||
| `agent/+page.svelte` | onMount context init | +10 |
|
||
| `backend: app.py` | uicontext parsing | +25 |
|
||
| `backend: _tool_filter.py` | Новый файл | +40 |
|
||
| **Всего** | | **~300 строк** |
|
||
|
||
#endregion ModelChanges
|