From 4b3a6c698359b1e2b72b5e16d169247c63369919 Mon Sep 17 00:00:00 2001 From: Serena Tan Date: Thu, 10 Sep 2026 10:01:36 -0700 Subject: [PATCH] refactor(agents): render the template pane unconditionally and remove the standalone Agent Templates page Deletes the agent_template_pane flag end-to-end (a stale materialized false in existing configs made a default flip inert), renders the in-agent Template pane unconditionally, removes the standalone page, prunes retired nav pins, and deletes the retired surfaces' server routes (sessions/context, spawn clear, agent-detail DELETE). --- .github/black-baseline.txt | 1 - config-baseline.json | 14 - docs/feature-map/README.md | 6 +- .../modules/learn-cron-dashboard.md | 25 +- docs/system-specs/modules/providers.md | 1 - error-code-baseline.json | 4 +- src/kiro_crew/config/loader.py | 12 - src/kiro_crew/config/resolution.py | 10 +- src/kiro_crew/dashboard/chat_runner.py | 2 +- src/kiro_crew/dashboard/handlers/__init__.py | 2 - src/kiro_crew/dashboard/handlers/agents.py | 89 +- src/kiro_crew/dashboard/handlers/messaging.py | 20 - src/kiro_crew/dashboard/handlers/sessions.py | 6 - src/kiro_crew/dashboard/routes/agents.py | 1 - src/kiro_crew/dashboard/routes/system.py | 1 - src/kiro_crew/dashboard/server.py | 1 - src/kiro_crew/docs/agents.md | 6 +- src/kiro_crew/session.py | 4 - src/kiro_crew/session_allocation.py | 41 - .../template-pane-builtin.png | Bin 0 -> 82186 bytes .../template-pane-custom.png | Bin 0 -> 93218 bytes test/test_agent_delete_reference_guard.py | 193 --- test/test_agent_template_pane_flag.py | 67 - test/test_agents_endpoints_owner_auth.py | 1 - test/test_api_server.py | 1 - test/test_config_extra_sections.py | 23 + test/test_handlers_messaging_coverage.py | 13 - test/test_member_memory_api.py | 6 +- test/test_session.py | 97 +- test/test_session_more_coverage.py | 29 +- test/test_subagent_delivery_ttl_anchor.py | 2 +- website/playwright/capabilities.spec.ts | 13 +- website/scripts/capture-agent-skills.mjs | 215 ---- .../scripts/capture-agent-template-pane.mjs | 6 +- website/scripts/capture-agent-templates.mjs | 216 ---- website/scripts/lib/i18n-surfaces.mjs | 1 - website/src/api/client.ts | 3 - website/src/components/ChatPane.tsx | 2 +- .../components/ErrorBoundary.cov80.test.tsx | 13 + website/src/components/ErrorBoundary.tsx | 23 +- .../src/components/PinSurfaceButton.test.tsx | 2 +- website/src/hooks/useAgentTemplatePane.ts | 32 - website/src/i18n/destructiveConfirm.test.ts | 2 - website/src/i18n/en.context.json | 3 - website/src/i18n/locales/bn.json | 76 +- website/src/i18n/locales/de.json | 76 +- website/src/i18n/locales/en-XA.json | 76 +- website/src/i18n/locales/en.json | 67 +- website/src/i18n/locales/en.manual.json | 11 - website/src/i18n/locales/es.json | 77 +- website/src/i18n/locales/fr.json | 77 +- website/src/i18n/locales/hi.json | 76 +- website/src/i18n/locales/it.json | 77 +- website/src/i18n/locales/ja.json | 75 +- website/src/i18n/locales/ko.json | 75 +- website/src/i18n/locales/pt.json | 77 +- website/src/i18n/locales/ru.json | 78 +- website/src/i18n/locales/zh-CN.json | 75 +- website/src/i18n/pluralKeys.json | 1 - website/src/i18n/render-baseline.json | 5 - website/src/i18n/untranslated-baseline.json | 8 - website/src/lib/navPinned.ts | 13 +- website/src/pages/AgentsPage.tsx | 1129 ----------------- website/src/pages/CapabilitiesPage.tsx | 5 +- website/src/pages/ChatPage.tsx | 2 +- website/src/pages/KiroCrewAgentsPage.tsx | 93 +- website/src/pages/chat/ActivityViewer.tsx | 4 +- website/src/store/chatSlice.ts | 2 +- website/src/surfaces/builtins.tsx | 3 +- website/src/test/AgentTemplateDetail.test.tsx | 15 - .../src/test/AgentsPage.defaultAgent.test.tsx | 121 -- .../test/AgentsPage.harnessIdentity.test.tsx | 239 ---- .../src/test/AgentsPage.meterLabels.test.tsx | 207 --- website/src/test/AgentsPage.model.test.tsx | 143 --- .../test/AgentsPage.templateSource.test.tsx | 132 -- website/src/test/AgentsPageInspector.test.tsx | 475 ------- website/src/test/AgentsPageSkills.test.tsx | 92 -- .../src/test/App.changelogModalApply.test.tsx | 1 - website/src/test/App.creditPillError.test.tsx | 1 - .../test/App.featureRequestFailure.test.tsx | 1 - .../src/test/App.importChapterReopen.test.tsx | 1 - website/src/test/App.linuxElectron.test.tsx | 1 - website/src/test/App.metricsPopover.test.tsx | 1 - .../src/test/App.mobileCrewSwitcher.test.tsx | 1 - website/src/test/App.navLeaveGuard.test.tsx | 1 - .../App.notificationNavLeaveGuard.test.tsx | 1 - .../test/App.notificationSheetExit.test.tsx | 1 - website/src/test/App.offlinePill.test.tsx | 1 - .../src/test/App.rootSelectorScope.test.tsx | 1 - .../test/App.startupVideoSequencing.test.tsx | 1 - .../src/test/App.terminalEnabledFlag.test.tsx | 1 - .../src/test/App.terminalNavActive.test.tsx | 1 - website/src/test/App.test.tsx | 1 - website/src/test/App.topbarCapsule.test.tsx | 1 - .../test/App.topbarMetricsPending.test.tsx | 1 - website/src/test/AppRootCoverage.test.tsx | 1 - .../CapabilitiesPage.knowledgeTab.test.tsx | 1 - .../CapabilitiesPage.tabLanguage.test.tsx | 1 - website/src/test/CrewRoster.test.tsx | 8 +- website/src/test/filterInputFont.test.tsx | 1 - .../test/listDetailNarrowViewport.test.tsx | 41 - website/src/test/navPinned.retiredIds.test.ts | 36 + website/src/test/navShortcutHint.test.tsx | 1 - 103 files changed, 218 insertions(+), 4781 deletions(-) create mode 100644 temp-screenshots/agent-template-pane/template-pane-builtin.png create mode 100644 temp-screenshots/agent-template-pane/template-pane-custom.png delete mode 100644 test/test_agent_delete_reference_guard.py delete mode 100644 test/test_agent_template_pane_flag.py delete mode 100644 website/scripts/capture-agent-skills.mjs delete mode 100644 website/scripts/capture-agent-templates.mjs delete mode 100644 website/src/hooks/useAgentTemplatePane.ts delete mode 100644 website/src/pages/AgentsPage.tsx delete mode 100644 website/src/test/AgentsPage.defaultAgent.test.tsx delete mode 100644 website/src/test/AgentsPage.harnessIdentity.test.tsx delete mode 100644 website/src/test/AgentsPage.meterLabels.test.tsx delete mode 100644 website/src/test/AgentsPage.model.test.tsx delete mode 100644 website/src/test/AgentsPage.templateSource.test.tsx delete mode 100644 website/src/test/AgentsPageInspector.test.tsx delete mode 100644 website/src/test/AgentsPageSkills.test.tsx create mode 100644 website/src/test/navPinned.retiredIds.test.ts diff --git a/.github/black-baseline.txt b/.github/black-baseline.txt index 25669066880..b59311de4dc 100644 --- a/.github/black-baseline.txt +++ b/.github/black-baseline.txt @@ -362,7 +362,6 @@ test/test_acp_true_provider_error.py test/test_acp_turn_lock.py test/test_admission_gate.py test/test_agent_default_model.py -test/test_agent_delete_reference_guard.py test/test_agent_discovery.py test/test_agent_import_hoist.py test/test_agent_source_ownership_durability.py diff --git a/config-baseline.json b/config-baseline.json index cdeed16a1bc..91f6900a4e2 100644 --- a/config-baseline.json +++ b/config-baseline.json @@ -7639,20 +7639,6 @@ "enumValues": null, "defaultValue": true }, - { - "path": "agent_template_pane", - "kind": "core", - "type": "boolean", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [], - "label": "Agent template pane", - "help": "Show the crew definition panel with private fork-on-edit (soak; see #8567).", - "hasChildren": false, - "enumValues": null, - "defaultValue": false - }, { "path": "timezone", "kind": "core", diff --git a/docs/feature-map/README.md b/docs/feature-map/README.md index 4d1bfef80fb..246f52c1e1b 100644 --- a/docs/feature-map/README.md +++ b/docs/feature-map/README.md @@ -95,7 +95,7 @@ stay wired, but it is entered from the topbar bell rather than a rail row. ## Agent Capabilities -One destination, pinned to the bottom of the rail, hosting nine tabs. Every tab +One destination, pinned to the bottom of the rail, hosting eight tabs. Every tab is a `?tab=` value on `/capabilities` (`pages/CapabilitiesPage.tsx`). Any one of those tabs can also be **promoted to its own top-level rail row**, so @@ -112,9 +112,9 @@ no endpoint: the preference never leaves the browser. | Tab | What it is | Reach it | Page | Handler | Endpoints | |---|---|---|---|---|---| -| Crews | Named agent bindings — which agent, model and workspace; per-crew custom avatar (hand-picked ghost traits or an uploaded picture, edited in the crew editor's avatar builder; a crew can also wear an appearance pack, picked on the builder's Library tab — see Crew appearance library); the definition panel forks a private template copy on first edit (`components/crew/AgentTemplateDetail.tsx`, behind `agent_template_pane`) | `/capabilities?tab=crews` | `pages/KiroCrewAgentsPage.tsx`, `components/CrewAvatarBuilder.tsx`, `components/CrewAvatarLibraryTab.tsx`, `components/CrewAvatar.tsx`, `lib/appearancePacks/` | `handlers/agents.py` | `GET /api/agent/config`, `GET,PUT /api/config/default-agent`, `POST /api/agents`, `PUT,DELETE /api/agents/{name}`, `GET,POST /api/agents/{name}/avatar`, `POST /api/agents/detail/{name}/fork`, `POST /api/agents/detail/{name}/publish`, `POST /api/agents/detail/{name}/reset` | +| Crews | Named agent bindings — which agent, model and workspace; per-crew custom avatar (hand-picked ghost traits or an uploaded picture, edited in the crew editor's avatar builder; a crew can also wear an appearance pack, picked on the builder's Library tab — see Crew appearance library); the definition panel forks a private template copy on first edit (`components/crew/AgentTemplateDetail.tsx`) | `/capabilities?tab=crews` | `pages/KiroCrewAgentsPage.tsx`, `components/CrewAvatarBuilder.tsx`, `components/CrewAvatarLibraryTab.tsx`, `components/CrewAvatar.tsx`, `lib/appearancePacks/` | `handlers/agents.py` | `GET,PUT /api/config/default-agent`, `POST /api/agents`, `PUT,DELETE /api/agents/{name}`, `GET,POST /api/agents/{name}/avatar`, `POST /api/agents/detail/{name}/fork`, `POST /api/agents/detail/{name}/publish`, `POST /api/agents/detail/{name}/reset` | | Crew appearance library | The dashboard's own library of appearance packs (art only in this round; pack-carried sound cues return in a later change) a crew can wear, rooted at `/appearance-library/`. Separate from Crew Companion's library: that app is independent and keeps its own packs; the two share only the pack format, so a pack exported from the Companion gallery imports here unchanged. A crew record names a pack as `avatar: {"kind":"pack","id":...}`, and the API can list, read, import, PetDex-fetch and delete packs; deleting a pack a crew wears is refused by name unless forced. The crew editor's avatar builder carries the picker on a **Library** tab: a grid of cards (thumbnail read through the per-slot route, name, author, built-in/custom badge, format), an "Import pack (.json)…" control that posts an exported bundle, and a two-click delete on a custom pack whose 409 names the crews still wearing it (`?force=1` is deliberately not offered). **Crews wear SVG packs only in v1** — the face is an `` and core ships no Lottie or sprite player, so a pack in either format lists greyed and unselectable rather than hidden | `/capabilities?tab=crews` → a crew → Edit avatar → Library | `components/CrewAvatarLibraryTab.tsx`, `components/CrewAvatarBuilder.tsx`, `components/CrewAvatar.tsx` (the pack tier), `lib/appearancePacks/library.ts`, `lib/appearancePacks/types.ts` | `handlers/appearances.py`, `dashboard/appearances.py` (crew store), `appearance_packs/store.py` (shared store class), `appearance_packs/transfer.py` (bundle import and PetDex fetch) | `GET /api/appearances`, `GET /api/appearances/{id}`, `GET /api/appearances/{id}/slot/{slot}`, `POST /api/appearances/import`, `POST /api/appearances/petdex/fetch`, `DELETE /api/appearances/{id}` | -| Agent Templates | The harness-level agent definitions crews bind to | `?tab=templates` | `pages/AgentsPage.tsx` | `handlers/agents.py` | `GET /api/agent/config`, `GET /api/config/schema` | +| Agent Templates | The harness-level agent definitions crews bind to — viewed and edited inline in each agent's Template pane (`components/crew/AgentTemplateDetail.tsx`); the standalone tab was removed | `/capabilities?tab=crews` | `pages/KiroCrewAgentsPage.tsx` | `handlers/agents.py` | `GET /api/agents/installed`, `GET,PATCH /api/agents/detail/{name}`, `POST /api/agents/detail/{name}/fork`, `POST /api/agents/detail/{name}/publish`, `POST /api/agents/detail/{name}/reset` | | Connections | MCP servers: install, sign in, enable, scope tools | `?tab=mcp` | `pages/connections/ConnectionsPage.tsx` | `handlers/mcp.py`, `handlers/connections.py`, `handlers/mcp_discover.py` | `GET /api/mcp`, `GET /api/mcp/discover`, `POST /api/connections/mint`, `POST /api/mcp/custom` | | Skills | Installed skills, the public registry, pending candidates | `?tab=skills` | `pages/overview/SkillsTab.tsx` | `handlers/prompts.py`, `handlers/discover.py`, `handlers/skill_budget.py` | `GET,POST /api/skills`, `GET /api/skills/-/discover`, `GET /api/skills/-/pending` | | Knowledge | The document library: sources, sync, entities, graph | `?tab=knowledge` | `pages/knowledge/index.tsx` | `handlers/knowledge.py` | `GET /api/knowledge/items`, `POST /api/knowledge/sources`, `GET /api/knowledge/graph` | diff --git a/docs/system-specs/modules/learn-cron-dashboard.md b/docs/system-specs/modules/learn-cron-dashboard.md index 0b1f89fae46..dc734a7c962 100644 --- a/docs/system-specs/modules/learn-cron-dashboard.md +++ b/docs/system-specs/modules/learn-cron-dashboard.md @@ -1772,7 +1772,7 @@ identity and selected knowledge copying. **No cross-provider bridge.** There are no `/api/cc/*` routes: Kiro Crew drives one provider surface, and provider-specific MCP config is handled uniformly through the `extra_mcp_scopes()` seam (see `platform-context.md`) — the core manages the Kiro global only, and a companion re-adds its own provider scope for apply, uninstall AND discovery. On `/api/mcp/apply` the omitted-field default differs by scope family: the core `kiroGlobal` key is `omit → delete` (the bundled SPA always sends it) while every seam `f"{id}Global"` key is `omit → preserve` (defaults to current on-disk presence), so an OSS apply that omits a companion scope never deletes that provider's server; see the `extra_mcp_scopes()` contract note in `platform-context.md`. **Session sidebar ordering**: pinned sessions form a manually ordered section ahead of the automatically sorted remainder. The browser persists that order in `mc-pinned-session-order`, removes stale or duplicate keys, and appends newly pinned sessions in the selected sort's natural order. A pinned row dropped on a pinned peer in the same rendered container changes only that manual order; folder, root, status-column, and chat-reference drops retain their existing meanings. Focused pinned rows also move within their rendered container with Alt+ArrowUp or Alt+ArrowDown. A quiet double-line divider appears only when a container has both pinned and unpinned rows, and explicit search results remain relevance-ranked without the divider. -**Sessions**: GET `/api/sessions` (paginated list; opt-in `exclude_open=1` drops every session a live slot already holds open — resolved through `slot_history_key`/`slot_transcript_key` so channel tabs whose transcript is their `linked_session_key` are recognised, and applied BEFORE `total`/`has_more` so one page's arithmetic matches what it returned. The sidebar's Older-sessions pane is the only caller: it renders the complement of the tab list above it, while the full inventory stays the default for memory consolidation and the command palette's recents. The same predicate protects a session from DELETE `/api/sessions`), GET `/api/sessions/{key}` (detail), DELETE `/api/sessions/{key}` (permanent delete), GET `/api/sessions/context` (context usage), GET `/api/sessions/usage` (kiro credit usage, cached 10 min; the background refresh tries the real CodeWhisperer RTS `GetUsageLimits` API first — the true used/limit/overage — posting to a hardcoded regional host keyed off the whoami profile ARN (`codewhisperer.us-east-1.amazonaws.com` by default, `q.eu-central-1.amazonaws.com` for `eu-central-1`; any other/missing region falls back to us-east-1); reading bearer tokens from `kiro-cli`'s own SQLite auth store under keys `kirocli:odic:token` (OIDC/Builder ID), `codewhisperer:odic:token` (legacy), `kirocli:social:token` (GitHub/Google social login), and `kirocli:external-idp:token` (Identity Center/org SSO), then falls back to scraping `kiro-cli chat --no-interactive --agent kirocrew-lite /usage` stdout when the API path is unavailable and `dashboard.usage_text_scrape_enabled` is true), POST `/api/sessions/summarize` (one-line LLM summaries for a list of session keys — bounded to 8, generated on an ephemeral background session with the cheap Haiku model, best-effort; backs the `list_sessions` MCP tool's opt-in `summarize=true`), GET `/api/sessions/memory` (per-session and per-task memory footprint; returns `{sessions, tasks, totals, history}` — each session row carries: `key`, `title`, `slot_key`, `untitled`, `agent`, `channel` (the grouping dimension, resolved by `telemetry_channel_of(key)` from `kiro_crew.messaging.link` — bounded cardinality, same taxonomy as telemetry metrics), `pid`, `owns_runtime`, `rss_mb`, `procs`, `mcp`, `cpu_cores`, `prompts`, `uptime_s`, `credits` (cumulative kiro credits consumed over the spend window — `SPEND_WINDOW_DAYS` in `dashboard/handlers/usage.py`, which the Telemetry spend tab's `cost_breakdown` shares so the two surfaces cannot report different totals for one session; `null` when no measured turn exists in the window, which is semantically distinct from zero; the join is by session key, and because a slot bound to a channel or cron conversation runs its turns under `linked_session_key` while its usage rows are still filed under the dashboard `slot.key`, the lookup falls back to `DashboardState.spend_slot_by_session()` — without that reverse index those sessions report `null` despite having spent), `turns` (count of completed turns in the same window — `null` same semantics as `credits`); `totals` carries `rss_mb`, `host_mb`, `host_pct`, `runtimes`, `rss_is_upper_bound`; `history` is a ring of `{t, mb}` snapshots; `tasks` is the subagent task list from `SubagentManager.task_memory_rows()`) +**Sessions**: GET `/api/sessions` (paginated list; opt-in `exclude_open=1` drops every session a live slot already holds open — resolved through `slot_history_key`/`slot_transcript_key` so channel tabs whose transcript is their `linked_session_key` are recognised, and applied BEFORE `total`/`has_more` so one page's arithmetic matches what it returned. The sidebar's Older-sessions pane is the only caller: it renders the complement of the tab list above it, while the full inventory stays the default for memory consolidation and the command palette's recents. The same predicate protects a session from DELETE `/api/sessions`), GET `/api/sessions/{key}` (detail), DELETE `/api/sessions/{key}` (permanent delete), GET `/api/sessions/usage` (kiro credit usage, cached 10 min; the background refresh tries the real CodeWhisperer RTS `GetUsageLimits` API first — the true used/limit/overage — posting to a hardcoded regional host keyed off the whoami profile ARN (`codewhisperer.us-east-1.amazonaws.com` by default, `q.eu-central-1.amazonaws.com` for `eu-central-1`; any other/missing region falls back to us-east-1); reading bearer tokens from `kiro-cli`'s own SQLite auth store under keys `kirocli:odic:token` (OIDC/Builder ID), `codewhisperer:odic:token` (legacy), `kirocli:social:token` (GitHub/Google social login), and `kirocli:external-idp:token` (Identity Center/org SSO), then falls back to scraping `kiro-cli chat --no-interactive --agent kirocrew-lite /usage` stdout when the API path is unavailable and `dashboard.usage_text_scrape_enabled` is true), POST `/api/sessions/summarize` (one-line LLM summaries for a list of session keys — bounded to 8, generated on an ephemeral background session with the cheap Haiku model, best-effort; backs the `list_sessions` MCP tool's opt-in `summarize=true`), GET `/api/sessions/memory` (per-session and per-task memory footprint; returns `{sessions, tasks, totals, history}` — each session row carries: `key`, `title`, `slot_key`, `untitled`, `agent`, `channel` (the grouping dimension, resolved by `telemetry_channel_of(key)` from `kiro_crew.messaging.link` — bounded cardinality, same taxonomy as telemetry metrics), `pid`, `owns_runtime`, `rss_mb`, `procs`, `mcp`, `cpu_cores`, `prompts`, `uptime_s`, `credits` (cumulative kiro credits consumed over the spend window — `SPEND_WINDOW_DAYS` in `dashboard/handlers/usage.py`, which the Telemetry spend tab's `cost_breakdown` shares so the two surfaces cannot report different totals for one session; `null` when no measured turn exists in the window, which is semantically distinct from zero; the join is by session key, and because a slot bound to a channel or cron conversation runs its turns under `linked_session_key` while its usage rows are still filed under the dashboard `slot.key`, the lookup falls back to `DashboardState.spend_slot_by_session()` — without that reverse index those sessions report `null` despite having spent), `turns` (count of completed turns in the same window — `null` same semantics as `credits`); `totals` carries `rss_mb`, `host_mb`, `host_pct`, `runtimes`, `rss_is_upper_bound`; `history` is a ring of `{t, mb}` snapshots; `tasks` is the subagent task list from `SubagentManager.task_memory_rows()`) **Logs**: GET `/api/logs` (SSE), GET/POST `/api/logs/level` (runtime log level control) **Task Runner**: GET `/api/taskrunner` (status with runs[], includes `agent`), POST `/api/taskrunner` (start, optional `agent` field), POST `/api/taskrunner/cancel` (per-task or all), DELETE `/api/taskrunner/{task_id}` (remove finished run), POST `/api/taskrunner/refine` (dynamic multi-turn with tool access), GET `/api/taskrunner/refine` (status with `waiting` field), POST `/api/taskrunner/refine/cancel`, POST `/api/taskrunner/refine/answer` (answer clarifying question) **Approvals**: GET `/api/approvals` (pending list), POST `/api/approvals/{id}/approve`, POST `/api/approvals/{id}/reject` @@ -2095,7 +2095,7 @@ React 18 + TypeScript + Vite 5 + Redux Toolkit + React Router v7 + Tailwind CSS - **There is no dependency-install button.** Desktop releases already carry the recognizer and pinned `imageio-ffmpeg` decoder; the only user action is selecting an on-demand model and clicking **Download now**. A source install can need a `voice`-extra command in the gateway's OWN interpreter plus a system FFmpeg command for compressed input; project-venv binaries are not trusted executable storage. These commands are suppressed where no channel into this interpreter exists (frozen build, code-signed app bundle, pip-less or PEP 668 externally-managed python): `transcribe_unsupported` makes the page show an unsupported notice instead of a command that cannot succeed. `bundled_interpreter` also prevents any Homebrew/Winget/Apt decoder advice inside a desktop app. A platform with no prebuilt recognizer wheel is not pip-actionable; the availability `detail` on `/api/stt/status` names it. `ffmpeg_missing` remains independent of `available` for diagnostics, because a corrupt payload can leave a provider ready while an uploaded WebM cannot be decoded. The decoder itself is no longer a shell command on a host whose distribution packages no ffmpeg: `GET /api/stt/status` carries an `ffmpeg` object (`present`, `source` = bundled|system|store, `auto_fetch` = available|unsupported|bundled, the gateway's `os`/`arch`, and the live `download`), `POST /api/stt/ffmpeg/download` fetches the pinned upstream executable into the digest-verified store at `/models/ffmpeg/`, and `_ffmpeg_install_commands` returns `[]` rather than an `echo` of the ffmpeg.org URL when nothing is actionable. On a fetch failure the page hands the failure to a chat session through the existing prefill hand-off; see [stt-streaming](stt-streaming.md) for the trust model, which the store does not widen. `transcribe` additionally renders `AwsConsentGate service="transcribe"`, which is the only way the paid-service grant is recorded (deliberately no CLI verb). - Endpoints: `GET/PUT /api/config/stt`, `GET /api/stt/status`, `POST /api/stt/prepare`, `POST /api/stt/prewarm`, `POST /api/stt/ffmpeg/download`, `POST /api/stt/transcribe`, `GET /api/ws/stt`. The live dictation surface these settings drive is specified in [stt-streaming](stt-streaming.md). - **System** (`/system`) — live metrics (1s refresh): CPU %, memory used/total, network RX/TX stat cards; host info with correct Apple Silicon arch detection, load averages; memory, process, network, storage detail cards; uptime ticking every 1s via `useUptime` hook (client-side from `start_time`) -- **Agent Capabilities** (`/capabilities`; `/agents` redirects here) — merged Agents + Capabilities destination, bottom-pinned in the nav. `SidePanelLayout` tabs in order: **Agents** (agent → workspace → memory store bindings, `KiroCrewAgentsPage` embedded), **Agent Templates** (installed agent configurations/packages, `AgentsPage` embedded), **Integrations (MCP)**, **Skills**, **Hooks**, **Prompts**. The templates tab keeps the side-by-side layout: installed agents list (left) with detail panel (right, height set by inline Tailwind on the panel in `AgentsPage.tsx`). Installed agents card shows each agent with name, `SourceBadge` source pill (neutral `package`/`kirocrew`/`project`/`local` sources — the former `aim` source was generalized to `package`), model, description, skill count, MCP server count. Click to view full agent config in detail panel: system prompt, tools, auto-approved tools, MCP servers with `--aim` token colors (hover tooltip showing tool list), expandable denied commands list (`
` with all patterns). **Agent-package install / uninstall now routes through the capability seam** (`installPlugin`/`uninstallPlugin` in `providers/adapters/acp.ts` dispatch `type === 'agent'` to `/api/capability/agents/{install,uninstall}`). This replaced the earlier "intentionally NOT offered" stance: the pure-AIM `/api/aim/agents/*` + `/api/aim/update` routes were removed, but leaving `GET /api/capability/agents` as list-only meant the seam could SHOW installed agent packages and never manage them — an asymmetry with skills/MCP that forced an edition to shadow the core or mount its own routes. Bulk "Update All" remains unoffered (no `update_*` op on the seam). Skills/MCP install/uninstall still route through the capability seam (see Capability Integration); the manager returns human-friendly errors for invalid packages. The MCP registry browser is hidden entirely when no external capability manager is configured (registry → 503); when present it offers click-to-expand descriptions with all detail lines, clickable URLs (DOMPurify-sanitized), a direct Install button, and tier badges at `text-[11px]` minimum. Striped subagent table with `EmptyState` when empty, kiro credit usage card, context window usage bars per session (agent name in `--aim` color). +- **Agent Capabilities** (`/capabilities`; `/agents` redirects here) — merged Agents + Capabilities destination, bottom-pinned in the nav. `SidePanelLayout` tabs in order: **Agents** (agent → workspace → memory store bindings, `KiroCrewAgentsPage` embedded), **Integrations (MCP)**, **Skills**, **Hooks**, **Prompts**. The standalone Agent Templates tab was removed: an agent's definition (system prompt, tools, auto-approved tools, MCP servers, skills, guardrails) is now viewed and edited inline in its **Template** pane on the Agents tab (`components/crew/AgentTemplateDetail.tsx`), rendered unconditionally. **Agent-package install / uninstall now routes through the capability seam** (`installPlugin`/`uninstallPlugin` in `providers/adapters/acp.ts` dispatch `type === 'agent'` to `/api/capability/agents/{install,uninstall}`). This replaced the earlier "intentionally NOT offered" stance: the AIM-specific agent-package and update routes were removed, but leaving `GET /api/capability/agents` as list-only meant the seam could SHOW installed agent packages and never manage them — an asymmetry with skills/MCP that forced an edition to shadow the core or mount its own routes. Bulk "Update All" remains unoffered (no `update_*` op on the seam). Skills/MCP install/uninstall still route through the capability seam (see Capability Integration); the manager returns human-friendly errors for invalid packages. The MCP registry browser is hidden entirely when no external capability manager is configured (registry → 503); when present it offers click-to-expand descriptions with all detail lines, clickable URLs (DOMPurify-sanitized), a direct Install button, and tier badges at `text-[11px]` minimum. - **Tasks** (`/tasks`) — redirects to `/projects` - **Projects** (`/projects`) — autonomous multi-step task execution with left/right split layout (260px sidebar + detail/compose area). Sidebar shows compact project cards with status icons, progress bars, cancel/delete buttons, and "+ New Project" button. Compose area has two modes: ✨ Compose (free-text with refine-to-spec) and 📄 From Spec (paste/upload). Shared `AgentSelector` for agent selection. Plan generation with cancel, auto-polling for planned runs. Selected project detail view (`ProjectDetailPage`) with Idea/Tasks tabs: Idea tab shows spec content read-only with "Edit in Chat" button; Tasks tab has DAG/Phased view toggle. Action buttons: Execute/Chat/Discard (planned), Cancel (running), Restart/Schedule (completed/failed). Execute stays on project (no navigation). Session storage persistence for mode, input, spec text, and planning state. 3-second auto-refresh polling. - **Hooks** (`/hooks`) — script hook management following standard page layout: `PageHeader` + `StatCard` row (Total, Enabled, Total Runs, Errors) + `Card`/`CardTitle`/`InfoTip` wrapping a `table-striped` hooks table with `SearchInput` filter. Toggle switches for enable/disable, `Badge` status pills (ok/err/warn), `Btn` actions (▶ Test, Edit, ✕ Delete). Create/edit form uses `Card` wrapper with `Input`, styled `select` for event type, `SendBtn` for save. Test results shown in card-like panel below table with `Badge` exit status and dismissible stdout/stderr output. @@ -2204,10 +2204,23 @@ Sends a message to the user via Slack DM and dashboard notification. Exposed as ### Context Window Usage -The Agents page context window section shows per-session info: -- Agent name (purple) for custom agents, hidden for kirocrew -- Model read from agent config file when `_model` is "auto" (custom agents) -- `agent` field in API response from `GET /api/sessions/context` +Retired with the standalone Agent Templates page: the per-session context +window cards and their `GET /api/sessions/context` endpoint were removed. +Per-session memory and usage remain on `GET /api/sessions/memory`. + +### Template deletion + +Also retired with that page: the in-UI template delete and its +`DELETE /api/agents/detail/{name}` route. The design owner's rationale: an +API route with no shipped caller is dead code with a live maintenance +surface, and template deletion is a rare, deliberate act better done on disk. +Deleting a template is documented in `src/kiro_crew/docs/agents.md` — remove +its JSON file from `~/.kiro/agents/`. A crew still bound to the deleted name +does not break at session start: kiro-cli cannot resolve the missing spec and +falls back to the default agent spec, so the session runs with default +behavior until the crew is repointed. If an in-product delete returns, reuse +the removed handler from this repo's history (lock discipline, alias set, +409 reference guard) rather than re-deriving it. ### Structured monitors diff --git a/docs/system-specs/modules/providers.md b/docs/system-specs/modules/providers.md index 1b1e492a1c7..913b12ef886 100644 --- a/docs/system-specs/modules/providers.md +++ b/docs/system-specs/modules/providers.md @@ -193,7 +193,6 @@ are always present; user-configured servers from the agent config are merged in. - Provider-agnostic via factory (one provider, `AcpProvider`, over the resolved backend) - Calls `repair_agent_configs()` on gateway startup and periodically -- context_info() reports model/agent - Resume: calls `set_resume_session_id()` before `start()` ### Subagent Approval Mode Inheritance (`subagent.py`) diff --git a/error-code-baseline.json b/error-code-baseline.json index fc7b2e2b5c9..2e893ed3aca 100644 --- a/error-code-baseline.json +++ b/error-code-baseline.json @@ -1,7 +1,7 @@ { "_comment": "Error responses without a machine-readable `code`, per file. Generated - regenerate with `python test/test_error_code_contract.py --update`. This is both the CI ratchet and the Track B worklist: drive `missing_code` to zero, one PR per file or per directory, moving each frontend consumer in the same PR. Never raise a number to make CI pass. See test/test_error_code_contract.py for what each bucket means and which false negatives it accepts.", "_totals": { - "missing_code": 1128, + "missing_code": 1127, "opaque_body": 18, "dynamic_status": 39 }, @@ -52,7 +52,7 @@ "missing_code": 3 }, "dashboard/handlers/agents.py": { - "missing_code": 52 + "missing_code": 51 }, "dashboard/handlers/artifacts.py": { "dynamic_status": 2 diff --git a/src/kiro_crew/config/loader.py b/src/kiro_crew/config/loader.py index b115c3b2238..a119c068b13 100644 --- a/src/kiro_crew/config/loader.py +++ b/src/kiro_crew/config/loader.py @@ -2449,16 +2449,6 @@ class KiroCrewConfig: "Show the Connections services gallery (set false to hide it).", ), ) - #: Same shape and posture as ``connections_ui``: default-off, strict bool, - #: read live by the dashboard's useAgentTemplatePane hook. Unmodelled, the - #: masked GET strips it and the panel can never be enabled. - agent_template_pane: bool = field( - default=False, - metadata=_meta( - "Agent template pane", - "Show the crew definition panel with private fork-on-edit (soak; see #8567).", - ), - ) #: Top-level sections that were PRESENT on disk but not a JSON object, and #: were therefore coerced to defaults by :meth:`load`. #: @@ -3928,7 +3918,6 @@ def _load_resolved(cls) -> tuple[KiroCrewConfig, int]: ), auto_update=data.get("auto_update", True), connections_ui=_safe_bool(data.get("connections_ui", True), True), - agent_template_pane=_safe_bool(data.get("agent_template_pane", False), False), _degraded_sections=frozenset(_degraded | _OBSERVED_DEGRADED_SECTIONS), timezone=data.get("timezone", ""), snapshot_dir=data.get("snapshot_dir", ""), @@ -4308,7 +4297,6 @@ def to_dict(self) -> dict: # _extra_sections capture AND dropped here — losing the operator's # opt-in on the first save(). "connections_ui": self.connections_ui, - "agent_template_pane": self.agent_template_pane, } # External registries (always serialized so save() round-trips the field) d["registries"] = [asdict(r) for r in self.registries] diff --git a/src/kiro_crew/config/resolution.py b/src/kiro_crew/config/resolution.py index 2c12d55ec5c..e9739b85c39 100644 --- a/src/kiro_crew/config/resolution.py +++ b/src/kiro_crew/config/resolution.py @@ -15,11 +15,16 @@ # Top-level config.json keys that save() stamps itself rather than modelling as -# a section. They are neither parsed into a field nor round-tripped through +# a section, plus RETIRED keys older builds materialized into every saved +# config. They are neither parsed into a field nor round-tripped through # to_dict(), so every consumer that classifies top-level keys — the # _extra_sections capture below and validation.py's unrecognized-key warning — # must exclude them, or Kiro Crew warns the user about a key it wrote itself. -CONFIG_RESERVED_TOP_KEYS: frozenset = frozenset({"meta"}) +# A retired key is therefore silently ignored at load and dropped on the next +# save, never warned about and never resurrected. +# * agent_template_pane — retired: the agent editor's Template pane renders +# unconditionally now, so a stale materialized ``false`` must not warn. +CONFIG_RESERVED_TOP_KEYS: frozenset = frozenset({"meta", "agent_template_pane"}) # Top-level config.json sections this core models AND round-trips through # to_dict(). Any other top-level key found at load() is captured into @@ -79,7 +84,6 @@ "auto_update", "registries", "connections_ui", - "agent_template_pane", } ) diff --git a/src/kiro_crew/dashboard/chat_runner.py b/src/kiro_crew/dashboard/chat_runner.py index c3d88883d5c..9f3d8c6d09c 100644 --- a/src/kiro_crew/dashboard/chat_runner.py +++ b/src/kiro_crew/dashboard/chat_runner.py @@ -2556,7 +2556,7 @@ def _native_subagent_sync(state, slot, subagents, tracker, card_output=None) -> "last_activity": now, "last_tool": "", } - # Register in state-level dict so DELETE /api/spawn can cancel native cards. + # Register in state-level dict so DELETE /api/spawn/{id} can cancel native cards. _register_native_card(state, card_id, slot.key, sid) logger.debug( "native subagent spawn broadcast: id=%s agent=%s slot=%s", diff --git a/src/kiro_crew/dashboard/handlers/__init__.py b/src/kiro_crew/dashboard/handlers/__init__.py index 108dae5c0fc..77b3838337f 100644 --- a/src/kiro_crew/dashboard/handlers/__init__.py +++ b/src/kiro_crew/dashboard/handlers/__init__.py @@ -335,7 +335,6 @@ def sel(): api_slack_profile, api_slack_reactions, api_spawn, - api_spawn_clear, api_spawn_continue, api_spawn_delete, api_spawn_list, @@ -423,7 +422,6 @@ def sel(): api_sessions, api_sessions_clear, api_sessions_clearable_count, - api_sessions_context, api_sessions_health, api_sessions_memory, api_sessions_restart, diff --git a/src/kiro_crew/dashboard/handlers/agents.py b/src/kiro_crew/dashboard/handlers/agents.py index 0d07d7564aa..84a165d8164 100644 --- a/src/kiro_crew/dashboard/handlers/agents.py +++ b/src/kiro_crew/dashboard/handlers/agents.py @@ -3253,7 +3253,7 @@ def _cleanup_superseded() -> None: async def api_agent_detail(request: web.Request) -> web.Response: - """GET/DELETE/PATCH /api/agents/detail/{name} — view, delete, or update agent config.""" + """GET/PATCH /api/agents/detail/{name} — view or update agent config.""" name = request.match_info["name"] if request.method != "GET": denied = await _require_owner(request, f"agent_detail.{request.method.lower()}") @@ -3291,93 +3291,6 @@ async def api_agent_detail(request: web.Request) -> web.Response: # skip-to-next-file. try: if data.get("name") == name or f.stem == name: - if request.method == "DELETE": - if f.name in ( - "kirocrew.json", - "kirocrew-lite.json", - ): - return web.json_response({"error": "cannot delete kirocrew"}, status=400) - # The dashboard withholds delete for a template that is the - # kiro-cli fallback or is bound to a crew, but a client-side - # check cannot be race-free: its view of the config is a - # cached snapshot, so a crew bound (or the fallback moved) - # between render and click still reaches this handler. Only - # a check that reads config UNDER THE SAME LOCK the writers - # take makes the invariant hold, so this is the authority - # and the UI is now just the early, friendlier signal. - async with _get_config_lock(): - # Off-thread: this runs while the config lock is HELD, so a - # synchronous read stalls the event loop AND queues every - # other config writer behind a disk read. `to_thread` is the - # idiom already used for this in core.py / files.py. - cfg = await asyncio.to_thread(KiroCrewConfig.load) - # Every identifier this file answers to. The match above - # accepts EITHER the JSON's own "name" or the filename - # stem, so a request naming one leaves the other out of - # `name` -- and config may well record the one omitted. - # - # `declared` goes through `spec_str`, the helper this file - # already uses on the GET path: `~/.kiro/agents` is a - # SHARED directory and a structured `name` (an ACP-style - # `{"id": ...}`) is observed in the wild. It is reachable - # here via the `f.stem == name` arm of the match, and an - # unhashable set member turns this into a TypeError -- - # which the `except (JSONDecodeError, OSError)` below does - # NOT catch, so it escapes as HTTP 500 on a delete that - # should have succeeded or returned a 409. - declared = spec_str(data, "name") - aliases = {name, f.stem} - if declared: - aliases.add(declared) - # Both fields are checked because the two readers disagree: - # `agent.default_agent` is the kiro-cli fallback, while - # top-level `default_agent` is what /api/config/default-agent - # (the picker on this very page) writes. Guarding only one - # leaves the other free to dangle. - if aliases & {cfg.agent.default_agent, cfg.default_agent}: - return web.json_response( - { - "error": ( - f"Cannot delete '{name}': it is the default agent used " - "when nothing names a template. Change the default first." - ), - "code": "agent_is_default", - }, - status=409, - ) - bound = sorted( - crew - for crew, crew_cfg in cfg.agents.items() - if crew_cfg.kiro_agent in aliases - ) - if bound: - return web.json_response( - { - "error": ( - f"Cannot delete '{name}': still used by " - f"{', '.join(bound)}. Repoint or remove them first." - ), - "code": "agent_in_use", - }, - status=409, - ) - - # Unlink + prune in ONE spec-lock hold, off the loop: an - # unlocked unlink can land inside the fork refresh's - # read-modify-write, which then re-creates the file — - # with lineage pruned, the private copy surfaces as a - # shared template. (The unlink is also a blocking - # syscall that must not run on the event loop.) - def _locked_delete() -> None: - with agents_spec_lock(f.parent): - f.unlink() - with contextlib.suppress(Exception): - agent_state.prune(declared or name) - - await asyncio.to_thread(_locked_delete) - clear_list_agents_cache() - state.push_refresh("agents") - return web.json_response({"ok": True}) if request.method == "PATCH" and patch_body is not None: if "skills" in patch_body: raw_skills = patch_body["skills"] diff --git a/src/kiro_crew/dashboard/handlers/messaging.py b/src/kiro_crew/dashboard/handlers/messaging.py index 79bc5de4191..958bbd40ef3 100644 --- a/src/kiro_crew/dashboard/handlers/messaging.py +++ b/src/kiro_crew/dashboard/handlers/messaging.py @@ -1087,25 +1087,6 @@ async def api_spawn_stop_all(request: web.Request) -> web.Response: ) -async def api_spawn_clear(request: web.Request) -> web.Response: - """DELETE /api/spawn — clear all completed subagents.""" - state: DashboardState = request.app["state"] - if not state.subagents: - return web.json_response({"ok": True}) - scope, refusal = await internal_memory_scope(request, "spawn.clear") - if refusal is not None: - return refusal - done_ids = [ - a.id - for a in state.subagents.all_agents - if a.done and (scope is None or a.memory_store == scope) - ] - for aid in done_ids: - state.subagents._agents.pop(aid, None) - state.subagents._tasks.pop(aid, None) - return web.json_response({"ok": True, "cleared": len(done_ids)}) - - # ── Sessions / Notifications ── @@ -7082,7 +7063,6 @@ async def _rollback_config() -> None: "api_spawn_mark_collected", "api_spawn_list", "api_spawn_stop_all", - "api_spawn_clear", } ), resource_scoped={ diff --git a/src/kiro_crew/dashboard/handlers/sessions.py b/src/kiro_crew/dashboard/handlers/sessions.py index 1d6f7cb9c9e..817301cdbf3 100644 --- a/src/kiro_crew/dashboard/handlers/sessions.py +++ b/src/kiro_crew/dashboard/handlers/sessions.py @@ -63,12 +63,6 @@ def _sel(): return _pkg.sel() -async def api_sessions_context(request: web.Request) -> web.Response: - """GET /api/sessions/context — context usage for all active sessions.""" - state: DashboardState = request.app["state"] - return web.json_response({"sessions": state.sessions.context_info()}) - - # One sampler per process: it carries the CPU jiffy baseline and the rolling load # window, both of which are meaningless if rebuilt per request (a fresh baseline # always reports CPU as unknown, and a fresh window is always empty). diff --git a/src/kiro_crew/dashboard/routes/agents.py b/src/kiro_crew/dashboard/routes/agents.py index 3aad9c4d580..24cb1d2c2d1 100644 --- a/src/kiro_crew/dashboard/routes/agents.py +++ b/src/kiro_crew/dashboard/routes/agents.py @@ -29,7 +29,6 @@ def register(app: web.Application) -> None: app.router.add_get("/api/slash-commands", handlers.api_slash_commands) app.router.add_get("/api/agents/detail/{name}", handlers.api_agent_detail) app.router.add_patch("/api/agents/detail/{name}", handlers.api_agent_detail) - app.router.add_delete("/api/agents/detail/{name}", handlers.api_agent_detail) app.router.add_post("/api/agents/detail/{name}/fork", handlers.api_agent_fork) app.router.add_post("/api/agents/detail/{name}/publish", handlers.api_agent_publish) app.router.add_post("/api/agents/detail/{name}/reset", handlers.api_agent_reset) diff --git a/src/kiro_crew/dashboard/routes/system.py b/src/kiro_crew/dashboard/routes/system.py index e3a24f4c833..9b38839ffaa 100644 --- a/src/kiro_crew/dashboard/routes/system.py +++ b/src/kiro_crew/dashboard/routes/system.py @@ -58,7 +58,6 @@ def register(app: web.Application) -> None: app.router.add_post("/api/update/simulate", handlers.api_update_simulate) app.router.add_get("/api/sessions", handlers.api_sessions) app.router.add_delete("/api/sessions", handlers.api_sessions_clear) - app.router.add_get("/api/sessions/context", handlers.api_sessions_context) app.router.add_get("/api/sessions/memory", handlers.api_sessions_memory) app.router.add_get("/api/sessions/health", handlers.api_sessions_health) app.router.add_get("/api/sessions/usage", handlers.api_sessions_usage) diff --git a/src/kiro_crew/dashboard/server.py b/src/kiro_crew/dashboard/server.py index 059a9df0d5c..8d02ea37d48 100644 --- a/src/kiro_crew/dashboard/server.py +++ b/src/kiro_crew/dashboard/server.py @@ -1468,7 +1468,6 @@ def _register_mcp_routes(app: web.Application) -> None: app.router.add_post("/api/spawn/{agent_id}/continue", handlers.api_spawn_continue) app.router.add_post("/api/spawn/{agent_id}/steer", handlers.api_spawn_steer) app.router.add_post("/api/spawn/{agent_id}/release", handlers.api_spawn_release) - app.router.add_delete("/api/spawn", handlers.api_spawn_clear) app.router.add_get("/api/lessons", handlers.api_lessons) app.router.add_post("/api/lessons", handlers.api_lessons_create) app.router.add_delete("/api/lessons", handlers.api_lessons_delete) diff --git a/src/kiro_crew/docs/agents.md b/src/kiro_crew/docs/agents.md index 29e9e409394..92457ffe22d 100644 --- a/src/kiro_crew/docs/agents.md +++ b/src/kiro_crew/docs/agents.md @@ -45,7 +45,7 @@ Kiro Crew owns specs named `kirocrew`, `kirocrew-lite`, `kirocrew-conductor`, `k ## Custom Agents -Custom agents are JSON files in `~/.kiro/agents/`. They define their own system prompt, tools, MCP servers, and permissions. +Custom agents are JSON files in `~/.kiro/agents/`. They define their own system prompt, tools, MCP servers, and permissions. To delete a template, remove its JSON file from `~/.kiro/agents/`. There is no in-app delete. A crew still bound to the deleted name does not break: kiro-cli cannot resolve the missing spec and falls back to the default agent spec for that session, so the crew keeps running — with the default prompt and tools instead of the deleted template's. Check a template's bindings and repoint them before removing the file so no crew silently changes behavior. ```json { @@ -60,11 +60,11 @@ Custom agents are JSON files in `~/.kiro/agents/`. They define their own system ## Managing Agents -**Agent Capabilities → Agent Templates** shows installed agents with their source, tools, and MCP servers. Drop a new JSON file into `~/.kiro/agents/` and it appears automatically; the tab also provides edit and delete controls. `/agents` redirects to Agent Capabilities. +**Agent Capabilities → Agents** shows your agents; select one and open its **Template** pane to see its definition — model, system prompt, skills, tools, and MCP servers. Drop a new JSON file into `~/.kiro/agents/` and it appears automatically. `/agents` redirects to Agent Capabilities. ## Mapping Skills to an Agent -Each agent template can be given its own set of [skills](skills.md). Open **Agent Capabilities → Agent Templates**, select an agent, and use the **Skills** section to add or remove them. Every edit saves immediately. +Each agent template can be given its own set of [skills](skills.md). Open **Agent Capabilities → Agents**, select an agent, open its **Template** pane, and use the **Skills** section to add or remove them. Every edit saves immediately. Under the hood a mapped skill is a `skill://` entry in the agent's `resources`, so kiro-cli loads it natively when the agent starts: diff --git a/src/kiro_crew/session.py b/src/kiro_crew/session.py index 5fe69c48387..36e3b4ec10e 100644 --- a/src/kiro_crew/session.py +++ b/src/kiro_crew/session.py @@ -1906,10 +1906,6 @@ def _append_companion_runtime_rows(self, rows: list[dict[str, object]]) -> None: """Append background and subagent runtime process rows.""" self._allocation_boundary()._append_companion_runtime_rows(rows) - def context_info(self) -> list[dict[str, object]]: - """Return the dashboard-facing live context snapshot.""" - return self._allocation_boundary().context_info() - @staticmethod def _resolve_agent_model(agent: str) -> str: """Resolve model from agent config file. Cached at class level with diff --git a/src/kiro_crew/session_allocation.py b/src/kiro_crew/session_allocation.py index 60c05fce7ac..5d5d9051e21 100644 --- a/src/kiro_crew/session_allocation.py +++ b/src/kiro_crew/session_allocation.py @@ -736,47 +736,6 @@ def add(label: str, runtime: object, agent: str) -> None: for parent_key, runtime in list(self._subagent_runtimes.items()): add(f"Subagent runtime ({parent_key})", runtime, "") - def context_info(self) -> list[dict[str, object]]: - """Return the dashboard-facing context snapshot for live sessions.""" - result: list[dict[str, object]] = [] - for key, session in self._sessions.items(): - provider = session.provider - pct = provider.context_usage_pct() - model = "unknown" - agent = "" - if self._deps.is_claude_provider(provider): - model = provider._model or "auto" - agent = provider._agent or "" - elif self._deps.is_acp_provider(provider): - model = provider.client._model or "auto" - agent = provider.client._agent or "" - if model == "auto" and agent and agent != "kirocrew": - model = self._owner._resolve_agent_model(agent) - model = model or "auto" - - if key == self._deps.constants.background_key: - name = "Background (titles, cron, heartbeat)" - elif key.startswith("dashboard:"): - name = f"Chat ({key.split(':', 1)[1]})" - else: - name = key - - window = 0 - if hasattr(provider, "context_window_tokens"): - window = provider.context_window_tokens() - result.append( - { - "key": key, - "name": name, - "model": model, - "agent": agent, - "context_pct": round(pct, 1), - "context_window_tokens": window, - "prompts": session.prompt_count, - } - ) - return result - def record_success(self, key: str) -> None: session = self._sessions.get(self._owner._fold_key(key)) if session: diff --git a/temp-screenshots/agent-template-pane/template-pane-builtin.png b/temp-screenshots/agent-template-pane/template-pane-builtin.png new file mode 100644 index 0000000000000000000000000000000000000000..7d1b642f88f801657161777d6675a61f5e32a87f GIT binary patch literal 82186 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 lOCY3w77<7|p(c=IB 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 z zm(N)9+m2C8e%g{aq%aGt0jW@2lUrf2yuL#l^iB>bSC_1LV&uC8C5g*Y^q z^D2ZVnM9>fLXz_497iF@4B#If8>wRttA9qGPVpCDkHX7FgNVRWCSfXk&!~$_K-2=O zK&5c(8+w+6^bHelbt)5tD(18om=t_vBRx;}7qv85z_G#@A?HrC zO*nWEhB;iJp0cQY?u~Z!ml}U~`d1@JQU?Aesx=*WV9w3Gy7TJn^HVLDt)o!Jk?1jv zTQdYNd)%b{AWOa;+h%!6`;b$RvlAOjBF7No}3snuQO!mV*G89 zb@j?$>y~xN79{i`dfrf%({z^F>2}>T)7Mut_72x8Aia8mXbzHGZyEZIS0;BJ`mB)E_4-gh3UdCL zC~uUU;jB_9h9oBXE|n3`_wN`5g-H!xtyM>(tzKehdC+UEX;jLX~Cu3cW%v3^RpcI_JG)ixjP+UrMF V3d(rz)8Hr9l;t&$m9pmH{|yG{{nP*e literal 0 HcmV?d00001 diff --git a/temp-screenshots/agent-template-pane/template-pane-custom.png b/temp-screenshots/agent-template-pane/template-pane-custom.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c5028343da467f2f9088b855d219b28f375f6c GIT binary patch literal 93218 zcmcG0Wl&wg)@6|3F2UUi?(PsAg1f`T-Q6X4aJLYGTX1)`5ZocSyIgd-q;z?IucD@e?uvKEk_q?~rArB~;$MgZcpc(!)UlB?)FC)bHM*y_1m; zRrAa~$$4)?IJ6XgpH7vNo?$KrgJ+&W7iSv5YqiXwrnrIyW@{>-Mi$TFn07qj3Fon( z9%)1}u^61a%mLrqpI+}u(>OzKsLo|%<^27udEGx-fF6u{KF9d*5LQVrphr_e1|iJNq;Ao~`?uYRIWJigKSGgp1ao*|!m zP5!rjaC&pMbAQM{Bu#=0HrQX(I<`!6&W7VipT^xP?CScn2`E-0R`{y|v?v<1s61yy z91k7%>G`?!xmAzSF+rI{=shiz@xYO0xwHWK-#V-*K?z@Fc5CF_TpRe)i#!oBs!t=N zX%P_M=B(#x^jOE=i31G*^@{PP30w)oclM8~7i<)$lkECZwzfVzCk03c1XOF3So|9t zDcr{dDKzN>88o@Z;g1*yVEzW3;#nKKzXKqUX19Hxw<(jBLQ-`y|b$cQlu_|K#q z;mt7mzvhXJfPYHUY89KQNOtKA{OeZ~%|1};>j+i_{}g$F?;jx!FNnW0Lkk6wrjllm zW~1#n^XQdI#BCt`D~8@em!^iJ8K&u!V3;{V`6E*^5?+%S>x2A9sQM7*O_exp?$`uU^T+8P8)q*{q z@75BONmXc*50KjfigBVTF`}}|JXF})shv239L2~U^-w(2V8lkug%X8+xv{>4ZEf=y z7Nq&8-yXzsSvM_6AN$OefenCdW1^Cu@QjRv zQO+t-k&K=>X*Ow|Cwc+vJocqjXuO!0UG`1s060UGyx0>CMxAnyiJMAxne)3&Y~Y?0 z4V($ayJyTkN3~Z$Bvz~&7$RF#$e`(KIQ_-EW<6r{bG7a<>dOYwl;M`~$e>p%{fdWL zTPk+Q8h$oW4PZU{CBjijLY3u-UM@sB3htC6&M=HHUp&-0R3k*Q`WJ?!$NKjXx4N$L z^WuL0+}Y@xBYL>sms1fi3zMI0L`^=Xeru-f#AaGxqr+imFcO27`Uo_-17E9ER-ADs z_0AgA?ZDm=R{b2S-zDh*AFnhN199NkWYwZ>tCAw3e8o=q%_;u@XiyN2&g|AYUW?O< z3M)BDnU=88xh{y%Q=rPcJ7>CL;aEl*z5hY98^`LWJ`$_mX4@rtiyvdjQBJU@J%%9# zMT#PY^K7Pf!K{KVtF{n)({R% zMpD?8bcxdAyZATWMB-@uJb^3FFDy%;0JcD^!)?w4VuhKNV&%(_q15Pv5E)(`BWw}huZZXtvVYV zViPiRM^}Kd|!A#It`#iCqQMNX&wS9b?lq1;I zbpNqx*5&Z=3v{y$O-ALPtcp3~2&>_IlI0f7OU zpoQ-4cj8FsC=r;%#8qna1o-%^txR4>y9g~1lay--18&l}9zbs`N%b|qrV^+; zoX)PW#|jI{$$Ib3mYN*Sh0aNo?$-Nip$x>nDnqjdNl!_4y@4GD`uhoaoo{lQHmqz2 zd{c{%398%f3GVOi3^HslW=I5=f30pOoOIm3H5tE3xi)pZ^_^cS?%J)jA}6}+PbQ9- zJ>Q*z1_Mcip8C=ZeIJjp@wjq*pGGPwA$JUm^Jp#nZ+>gx6AgPqqRjP5#IgA@ja#SL| z?ihMtnHQPQjl6=Tm)pIrs{>wbUj8_@NJ>h^3tSCrq7d>=X!!-6GSHFvdpI8+sX=yJGLAg~weu!-JASG_8H%L`3HQMRg*c{ie zU*29%jA&aXDWCCpocZeO3$ml60~@}y)XS9$t33NeJy}6GwhI0Wo*Vptzgyr{c7_}stOvn3jFP`2Vi!1XBvaPb6@Pj#{GnY$R#1K z@3-nER1I2b@*Kst!LALXSr2zNWM***1D{jAm$ut$J6)j_HivZq+A{vL)|~+ygF6da zY%^q+22yxQG|aH2dJrcUm+wQ*C%CSg?J>_*XqfknO%15bO&5_D=l$s3)G&r&{+l9um40PIdoore?4>Ap9m9_KdcTFJn8V*5N8JG zoC`VcjW3ygim!zS-|P#6pEur0tI9fG<5n&$IWy#I`n#Sd-!8fzXAQwvXlSHpaJk@p zRqA>?F|<97eY4>|Q8XlPCPO$2cB?g}&vC{FU&*#A5^PudR+Bg>W_d*&rCGeo^V;83 zHQ*W>8oI9U+7QBm*XR=?NuD1~wKFmr+!~mL-cV6ZgHk246C3(IG5XgStHSXUwnEk^Z99HjE^Vs`iNsO1geKWjI=%~S_`;v4v$*hf@cOpKg@V)4R_ph_GT_Wa_~l*Yj8 z5j*1j#&i|2-_0lZD0pRk62FI;0YsdohZXy-(@ZxTF9G5T6XaWDlPKsMifzzjCOoE; zn6KDpr=DLFZzoBzF_N7wTm zzopMOt#NhXVz<@-jm62JSkUuXo5R!1p)Kx$TVFV`A+O^JhlSY-xG7-s+6zsl`3p!f zFKD~RE@s~A&&B7Lo*1(fWgUSmsq_M=B4a=Ii)arwcftGW{kPi_mzaf34X4P&vEAeO z%8fendJUV5SigZU>bJLUR8#|&GD}M?uP@083bBA!F}v0F_3e$pW5-6w+uPd{kGP{? zV`H=48`nNPwVRn?CGoondS!vzH${hL6$)aXoLY!SjtCFmBwJcIp3^ZxPeebiV0O!I zYkNLOELyyG2zawuFMYwu^?`7RzYlo!$5mT|zBo}+SIgybx*`d9hJA7{G|ao-cUanF7GEwbc zq;mWAf+L*8?ZDJ3_;5Zw+x2oJ_x7^SUQ@KmddP-%6{QpzVRKl?y-`+CVKztt27^ye zPX*UBsZW0{czLY5?Sry}Lpt?3EN%_KPx}t5J|yHM{(p=Ul^>>|P{sYghf@K;^DT~B zg%M?Er>9{la2WJLLhJbpuK3pmx4ZholCvUQmk*cYv3>epN(o~|^$U8TJ>xu0eVzwIh*H1BuX z={Aup23&j+29tp$Bcf%2FvAC%Q|c)SPZj<)5ZhurGAar@q7=Z8GOUx^P|s1rdt-59|IObUmpb@-!dw(z$Qz~&5_eVvBUa#kJuZ*}MCuSbURwyDrDj;4X*$p2!+bNS%*Niy0Qg@iA7 zyr33*Rnn!Qsp}jlNax|i^b`G`G5`IL z1l1dG@V|cjsyiNIHoiOV9diSL)=O0pahMiscMwt4^*j(}8{N^MZ@AS`BHHcD?u0ja7njV z!XTU3QTZzkU#X^{7xoiD8u6LwBHfmP7?f_O@8b}OA7mDuX-ocFSWmD*m)VF(GZE@Z zQw;a=&Ea10SgM7YSvH>=f-u3+u`u|+6ng!MS=L`r*O?IOl}~$~*usWTaO~-Q zZ+ZVQt&&)OC;$Zdq@xqAOk4JY*1@ty%hDM1%ksCshGWF0c_OxMvrHU`usbJB138?@ zZ?COK9GqRIV6RQ!91DdAi5(xU`$2;0|0B5mJ&I`o1||j&BrOR!j?}yBtedjnrdDg+ z)f)=HfCzJgPQ+mb(|>*arCkpq5_T5=eV#0@ZV#8f4txxP?LtOGh!_(!$?s{<_;O@zZyme@PcA}<(T9(PMHs68P315jDrQr@ zo9vqkyz_d44L9)#TyjdPn}=KJ9KHpMUTV)ZQ;AYuqxQE=4b`XT6^F2wu%ONJw+DhJ z9FBsfPQFM4eR(spKOv~XlubGxe0_cYAn=+JeM|8;B55>ROhl%nq?Dh^j%(}(hk8fH z=oc0O#K3>JfG{+N$^ihO-=+Mn4i8c3I>HNAbGCDyu|s=G=>FcNuXHOsd7 zGuRl9St(|>AnA~_VBBI%aPt?fIRPUj7Q9_ZhJab5dtaPVz)@5QSw>%PU+04}Ke_QL z6ElG?+A4tddwBX&C%B|W_iTm+;sRrZ$W1P{i&^+jmm4q@rY$*oa3{6a<(}+FPByXh zj%Wy@$>2n&Qx zr^n28kX=tb@pp2%YHAW_UI)^bEvmt{VG%emOGSLX zO57Ci_l;5ey>NJ+$UxA!7MXKFN6odz-Ss->(fJJ5Zi@oHISi^e`m^ECISF3k;^c0s z89j`B14pd6Zg6*Pk%_lwt^#o`F?dp3N#HgffEpL~op1VFo%KaYmyn=J;Kb^QNzTj+ zzpsypvGVjVuK03vw{kAFUGynFo5u->Nnr5mi*mf`!RaX-6H`C6R>9(y>N-g@X`~Ze z|7?|@;rex~4;xw9g=zRMqVGWo5a9T{Y=fgQ)(M`%LSf7Hv9zScKnzRU ze{)ft%WrQd=jSJI*m3mkrQSb3&&$k&gw2cocE)}gs7!g~FB09=b@eR#AfyOQytQU{ z7EChj|It=a*{Gt$a^x2kny*;Ut_lztY(EiH$45o=#L1;G$tWsX%U%s9qyEB28R&PFrzChCLgFXP@6*^vJ)9=|x&=UumAp&vP$y(>*xTgFdFvP&#t~}Q z^G>z@b-=~JPJFEIb?jU3LAL1vlm-7=>#+z_+Pn3RHdw(A0B~=HMHn%5C%;qm?I-Qliil}m!6`Hw7{zYP z+ju^^--3VY1Ki5ul>e7B@gR|hC!6ZN+r4&*ah)0t5$p~iX#9jiQacdPay`}ser2w1 zzpKlu-k7gknZ(Zax!lDeez_AD6ckifSNH6Dxvlo^`|f+bcJKdKZm1XVI+!qx)_m9b z+Wormy6&+VXU5=td~y8dU-|v}-V6zNQM|4k{uqM@V@lxqM9KeZhkL`jsJt9;^a==@ zf}@7@O+06(o!W0kqhn)Z5n8re0t8X-%~e&e*9Cpn)H#3r_~Cg`0J{hw9^CxREhO+mHy0Ow>Gi6iV$z}Nzje{sBE1Ubez&`EbFWa?X_}uh5U)WoC zbd>1dcD3$U@)p-_b`I?jEk#+c>^vWMd1XB?d;vny8bCM_(Qp`Rko zRu9ezt6%yoP4i;0OX9&$redAPYX>?2*0a|ZOh+{FBxPt`C}*VP^JL|#YKN4at5g@wT2A5RI? zL?~68-#8y39@FaZ)U@ls4<=m=D%s9QWlhgN(RVvI$@6`RQvay2VbdVjgEoLfBtUEQ zF0d`YGwExeQ6Ow2s0-DUXfrqhE6eWlNY-)*jRebveiqqQr+ZHiuST~qi z!lG!HSVvc%(96ba_fpaf%_9@#XkFJn5j0FEW;ea#&%J3kNTVN)$?`c+_yYHLmDG0aNr4tA)>ZTR6Rd(ok!bb8QG;qvz$di+WHD-WxG)o*-09ib3lQps zr|$t7j|~z`&V3G*`Z?v`JS8dG)12RUvsrS&ts3q|^^qF|D1QOgqZSXuz8mo(kJX>>8>z7(nfVcY`IUCe9*ra=slg%g~ZQ5%8l5 z3y^*Pp2Wi`vHm^uCy)Vz%ils)GikAZ%7!Qs-v52I+KNP+1Cr5yEsCNUm?kPKR3amZ z#?Tx0pr7VU{8JF*rHuT^KN0_b>Hq)jZ2nKJ35la&LI@iQx9E{2Mm|D9{F8asjX7{R z@GE8K7S?7bV(2w>#V$Psl1BPtFou!e;!MK(dHO$XVPaqu&RFyFEjRG31ul*MU87JB z$7a1+X@F+J#lrdxfUlK>u^5cLMCB-W8T~c=gDI0jQ;J>+<`22&ho>@;N=Ju>*5{T} z*}3W(24ypr32eM{#g!eE<>i=LHPaV0(=5X|5aFd|We2LaOZ#|4*mfNF1X=h=7)jU& zB-xcq0{PqrK>ya(HrYu|JlQuttssNC%{bTyztK%8 z+dajTelILUNfcgOz|~6pT3+srfdK~idgNa!vE0Kl&4(Q8_JD&SOP#{SHP^NRn7&l# zr);4t2d=CciotP-#)&hwP+iy*=f+KrEFM~xVQ~|aV8~ZW zEQIp%au^sG+`pO^E`%rcrw9ym^r%U5HMQT4;Ys)R_j1K-&_*AvZ5%XdvEZWnO)52x z?QsI$0w=2!>*wBwtPPmJOJK7@Dx<5Es*xv>uP)CQIt5q)jlU87b{7{HJfs1dHc=Ca z2)2KpgV>5|(T_BQP#lgeOCN?gVT4y$oGNSPSdOe^jA`~wfjf)Qo>Q8T=L#*55hBWt zH+`I1D6e|qIAg1D{>K>o==Kf5^D<_Jbchw+zsYmJNlBZYh`O28TWZM_M2Gp##d#Q+ zCnj57Ke*xOeuup#(3uoMePU#Y<^V_iT@$zImi%bZPwc%8H{pj0?6|SA#Q}NhJ*8@X zJ)%91Q(n>=GfQzd3H)$l-`FY@`NiPhQ)sxVA(^HmSvO=36)oXWV>`DL78T`?{T1z4 z-{~Xq;aiv?sd{y%;oxJyM3DFT`T4zZxksLDnUN~75>ZL!4XUk{V@`TpDoKyb78>cuExo zkqzSgv;Mv}DKoM8V>(as`+~|6JJoFvVY~CK#8E zvf$qXX*iM$?cWm8NHTGZzx@GU8X*RL@87pI5&!BLm&4l0*P}Mf zD;%m4X8q!*!nj1(6aVAS7oTajcXq-6$nQo=+4+V{U-7P36KnUwK zV=J9I8Xf(jTA~OI8?oxS!>F&HW1GpS%Voo1ZS!X$EDQ&gcy(%U!)1!QMQ;)iZb(8w zQ@KeQyyBH-pP1(s7i%0gi0hYP^ev_)*K19)`a~an!D}7?;^lLlZ{W6vjNDvSKkpCj z$+G5aYx?x+8;y2(6yT?02MHv8B79vI3tBknVE*$~m%WM%zmARw6r%NdpZNIrsr!Hn z!EEM6|9hW}fk&ashooE&(cG+{C+MR3e zH(|v;oy=M3m2=xc zHh76f`QFsD;9s_snMn^iR8>_aLPsA62k3|8Wu4&QU^`thCni0fi0)Rm7RvicOs9I_!CWtqG@FOTjeM4k^#QOE4z{+w=H|s6p>;DG8_Z~)SLu}0)Vac_oJC*al>|J_7}&XNmw!HP z_K4HO#=f+oU}Eu+kd*A94ZA1^flUE}6Es4I zUsHAMh22l!z_zT2AO38fAC{1SzQarp9Ujgq<_b(|%x9 zNutrRnXb@YN=*DLRdji>G|OVlrh)x6M-Ye8&=7TC#A%X{dg|c;I;6*OPhQB~$%{eP zWKo$}k7p{JpPyevMdh>}peS%!v&aH6s@@!k0J?d)Qe zX?1Fh3RDhJX98Xx&|e5Xd^iJ%MlD3m&ub784QRD{mP(Kh9FC{60%aBx(#w3HSWLU!QnB)T0}nMMZ*Md6XpeEy6od^J6(Z|K|N0|La>x4^)y zQ^=mb^!P-c@Oi*_*Ygwz%nMTFA`}m?xSjcQUY{BDIuedPB9GUsmn%(#z^z`u;-t+hG-wOoVJOVPZvtz_5*l0kX zRaG&sx;!rh^v2OdB13YVX?BPD`ur?Yipx)v1dw{n6SyW=r-B+2!2iT zz<{7_Pfq^%>lX&D>sD@fP6`>^Hk(-(IW;2_kynhm7^7il-M4RWL@65O4I82NZq54_ zqxJ-i+ZZ>I0>lAZy?UYI;^H%?hqvAyQ4y!ZNwUSluS-htB>Z4N!FvDr1}K0ecT5MO z1O)hipmx^$&|EB+wt7GDd~{5Jaw5FR%gu8_nr;+r5>WRP)$W>?)b z>B%F{Q^6F`Ld(K}rtqV@yuM|r?w7lyB(Fq_I5l-rZFBRmEmu=h69rz32mr=&WY}VJ;x3W64yZ{UkThQ9V9{M##I@P+@a}L0YcV2G1=>_x+cDv1cRFf$1wu*SYH}SmLcOL*A%R67IzXx!32|<-+1eTK(M|B6-vnS6afJN z5!qYyh)75#6{9e@qJO40Hc)5LeJTzthv+)Dx3{N%8PS?_RsV#QV*V+f3=5++k8R0; zPqwKw#EO()JAw|6h$k;gYo?&k0Gw?-M(Ep(1qy_5$Ff>=W*Itb0e2@Em^5*yeRv9J zNw|L*kMHv^;iOV2HJg$DxQ}ipqc^)XH6S0O2r9gK$U+Tw1W+Hg)Fm87(bge)l9Q7w zG|IOcLy#?28X3`~G}H?|L{nY7e89m$fbOM|OLeYvb$725@7-b^8_$Lqf$T$dRWVqn zdzL{qQVITuR>cu{_3PJmG!C<#kj7WV5(hiGa8s0_I&9OK!hz=RFnz^{uyW~k>B90F zW2uZ0%Sj`DXwA&bC|?$ooq2hqTl7%$e5PHbQMm~zb?nbfz9o{@y=<>u8kh~^2$V5&TDzPY;#KWJ70vo=jsn> zW@T<01TA~f%sco2DY3^x*X06#E!d?`Pz8}PgTqw64^U}94m$s`q&9zn_g^4_T#(#v z^7FDi;Njt8>#BZ1ofBl+k1IQ-@5<4q6qkVXCb^*JDORW}=gW;9Q4;*g;XvVZ1@`S* z)GKH@xIeD1u8I8*&{yWCn(S*kE^GKR^1*cA|YWW?&9I8QXjE|~vc>3)D^nV5#)ca;y*Vo@r?TNWzgV1fp(}* zxNx=r4^Kas<_2xbdcYV4Mn_i{c8!Rd(ppfZ%(0w_Kw_>vjp0g?4wX3aGT#udlCgI3En;TE+^B zTX=aWzm>Hj)E!Q5g_tu|HWnq3hXPp-2=F>`Ft3qozbGL&g!eky`sb#yYd zU!j2!H~a6zCMaJV04k-mb=fJqM~1kni>kFOeRiN|P*5J0!MOfUTuB5JlsgLsfq$4o zHN#l;M^cI6!^1<}*8Kbj_b=-n>wW}pRXPn)q*zFq;tmcxW2wA&aaI`T5h8t9w$znv(-$_TNYTQ^JDDe{od0ERw32;MIl4^Rv2bb5+&Z>Z8jz8XX;D zi^VCQ=LSALfssolzlXn-HeI>T=2(b=9CeyudwVjqysBs1`I?P3C%Yiy+JuN$DIPfV=ez{w^*7u4 z?Oi33F;>E(9YNvo+P=LvXiH?EvbTmA+RXkDGj zbDZ7D_q{!8+N_H4insPlK7x9i`!oVBJ=8S|eF8V5**zuwJ#96uhf0UFjg9K?kSXJ& zHbNt@v0TCZqIh8uT(P4@T?dDPUBtgoFc>#xi{S$D_2wYn-x+*my~S3_@#eZdid}w0 zz_dF%JB#e~$6Uh^PE3+l>Et*@8K4iK8+(-cjQ;SV8Hgda|J{c>8P%X5oaU8A`?YNk zFYx&FK4NB8)>O72R8a3=Hs9_k`Ni<)vyA7hMmjpn72&^bNz;w_uViyNwfSvw!UO+< zO6ghn{zJh5S;Tbz2S&}}0qCzk-6X3%@$UfGyl>{vy>+I~Tw43wbynxf@?uSm0Mw%#_K_}w#$;VWjh}{)HoV(6V2v$Z$ z`9CYv%IDZL(V5~2Fo|~ljwg3a03J{XY^<$U8+`Tyy}E_`Lur`O=z@dA(#s`X1<6xVu|ghDufPd35GCG+GkW|zk1&oY2 zy>A8`lO!)aHFa{w`6K-pvq5_|aYsjoAQO}QaU&ySr(;hrVev1exs8CAMf4O|^D%_f zf|C=k#6_FEa=J)OGv1*a}l2iJ0uV8fi6WRYNSLl$B~IX3)O%FhuMJs zgIr-@p~ZN5gUddO1bNon?Dy!(}W`yq0>JX-5%VaBTaq$5P_O6xRJOUvvN03H+8MDL7ca+I30-jNn^OHsR?JlDuFIjH*@i-+Z9Nqrl)5hQjvC)HgdceK-?F^ za-KM{$$x(I^>=g}9!VNnN`BOpbip{ zf}MfD6v#)aH{ZT8XtWUGXQN-X<=U!C)h7_iR4w+s0uUGYcy?c?q;>BeG+1(Uo$Ud>k-oI2^33bc~#R zMjXj$DXF>+$Z6WOn$V{$C2egT^{S?7>^m5Be=RvkWZGfD?)ThW<4-Pj4J&gNye9kC zimECUC6ay#35o5*TDuhxjbd)PkYKbzwKDC+pXDZpsYKHV7v~RTFoN3lXTM%v+NXp! zY!AgSl3Mfx;JsQ}qLs1*z>ldD2{!B&<5Hu^$pC4-O~l^_kf6rKTX)6ILVUSwBO`P^ zcfKvA`;D8M$~R^wGrWW_y!qD#%`7FIo$l9rN;7MFdrY-cjDn__(v+)>cE?*35h`C* zziBWNDC&`a@NjpRlKQmMGOpF2=Al_KR#>;`3Y-fn`uguP!UB7YM9jqWAo;S^+PXxP zfO{Q7TF+Kyv>&3jpq3v8++E+%(OKQ?j-+#SbQai;0XgKE_!yX&c;&UwpeB#=T>Un8 zfaTu05zMX=58L0*S$^D&ThArN8u^}QOLL%SfjZaexJB-YlxO#NGoJC1ojs5s>YWqB zDDNmOZD`2c3eh4zseyC#soGHcbFJU=2iI(wD7riCT?ReqoS(b(3PW*(K^d6vj(mru zco8VX-37JhKMy~oebHiQxJjdlvs=5v<2%Vo?jG7g8+UB|8 zQ%smsS~p}soq$=|+s&)^@uT}}TIt~SJ>)u&LOe}j0h*Frb8g87c%QmoUvmuYl@w(9 z!d7ZcKPHQ+IM=nQ)aD&Y_Df68=9piVUdv#WMve@efmnb5D*>=@mLR6zg{LQ+i)13EE=eJvd-wBayf1+ z+1g?xLZc$2X<`l}j9}Ta{p4s7GtP?|^TA7l3CiJbIv1{CCZnUHq{K?J^d}-BY9z{W zz+@(2eKI@~0yl=`JUG!Yz8+&+E-fP`o)tPrQWj|!iW1xNXl=6b`}a>~ zcY8qBr_sSN$Xhjk|N49RSInJn<0gZTb;GGvJoi*2TTzw z$M36{J|-)xSrf(_WjdS2OGj5%D9?&dl2QXwLuGTXWMsy+qhG&bq(d+Wx*CxV{Ys?S z%8@5e+%`5FV`Sg}z8$pcwZ+=Px?f-s9%ut~;|>uXHuAd0s!8+Dg379@vFrGPXa*uF z5a3479MOog0B;LwtT}-1IPvKD$T0c-ylonxXMV`fQp-|Z@;wK_I-AqBM%4CQG7i%?QD16eZi44OE$)hbwq3Mc ztT#F(yyMwlT3Em#M$gU8MktzGVlTmqZ*I#ZcJyN2`65zVa6yYi>7bQfTn1>aO``mr z&RL#s375*htL&p0C)UA7fI}_mZ2X|x;I4!;6$_C+AVvg;UA@3PvP>=Qy=?3gEieRF zaz5$kz|z6m)@<-2uw7(=HHy^ft6lv8hZPgAB$8j!6ye8&t03hb$+Qic7#+mS9@@D& zHIOoHF^2LS))`mu?VNcUoHF^X+q;P9!Fkn3@{}cZtV^2f1e*NwNS^{E2=|O-`;}(L z$1NCQ>Y2q)op4dyjWMyXvWaSZ^a|x+I(mdU1P>lJ?fWQ@rp$8` zkL!LAAGBA{IKsib*A@73wL3|a>963evwSqSu!!oCE9_uwYfD)6xqHW@5@B9Ntf(@S z1(k(ZajBp;gKWEM`Libl17;dORE}au*}>6K?~0ufnx0$b#e9!La2`<_;`Evy9iTFv z$_0-Z6SFM&Z(GIH_Z=>VM;^L*gLj!l8u)a-rGgsdMSNYD;>%KHJK5N#%*pVootm|B zvtpruyHoDG7xvRDDdaf`{mJCAfjFzd}T+|s`LHR-q9WoBOP!O_Y z>4BHzp!ds9R*x=Ju#7o>iZJ#tA715)b&7aAmRa5M@l>A3ZD-TGxWe9@N}6_4&;K;l zJcBsfZ4)7H(E6~ZudvcV?uMUscx5nH9JFzvLdJ<-&|QhrIP@G56H%BMBZWid+M0Ii zWEmR51vAL@dkTlx*R#CR>Y*_rN?gO2X~T?T$+xq$oS)o98W5zY;d9n5ig9UA0$)*e~n`#j&Bi}?6tbyo@3+~N@)3whpbPN)A#`qQNbKC#%jd&tj#QVA?y``W$+(JwZ`N5-OkY6-Y z^>^7~l_%I5Lux5WUO&pMBVa2;#m1WV!TogmN%pRi`LPUfF|k0vtlhzr>D=6h5PvH_ z7NVPMQCTxyr}77F+Ops2aRN3%lyKwM&%)@RKaYEcg(qPqL3^sl5%QXGGGAU2t7?A^ zhb><`YV^pjD>FIE_KJ!ck=6zT#iXQYqMRh-dw}EITEn?3F;F*5atq{L?^Vhh>2iM! z2SNjZ?*PpoJdC)H==@>trhF~|=RIi3lZj?My!W4Fi(KC!;3?%av};YDF3&*k7V?CU46bL;g$`i$3L}RHhV# z%w8`#Dbe`Tm79M0z%=~X<~N!?&SYMMV7MTW-un(kL~PH^&0^MGKn>+Qu3|sBy%0ZL z0bP1FHa1Y=sZ_P>>9{7>`19_alqH)tot!+ z$rQh^v=qng?3J~+<7=I^qrT|3RF@)_SkqF=%0yOB+k%d=+c=*Juvvv~;lx*~aIgc9 z^li3o7aJ2f{BM6oc4D$pQRU=4CHjGDGv~m^wyBxnG=4i92gDSIl8QUn z?a)eIA^r(CHn<4`%yQzh5eKF{8ZxB{*m__2O$i!`?2IMJSR?3g>g z`$H;|TE%LQ$43lZV%&&}LSF#bSk(0LiOY0tria4mY zf5UVw!Pqml$I=tZC}llE7;7{8%yzdM&-t92}x*eeRz4 zySU5*xnn*{P=FVIH`}RE;-p<3N9Xi@=vOrN*Ekp;fl?9UBUbKtBMi6Hqpt69(M>wV zV6x~frYX_JPON`#R<@m3Z(!iN6>&nwo)+P06iLE-UazxvtAach6XSbV45;Q|y(J|j z5qL;xl0-1nZJ<^`jnB8(`YE1|1gz4qAzPyjxHriW5fRoT12UD_%&>}|&VTQX zdtA*8xj}REuq=ng-qC}ZpD=j}b8#wcOAwf<1AcBBFx7RlqkfBmKw9NlOa)E7j{{**y}B#B(~dU zX~;f{i}y}HJp9d*2+qND#(i8wDFF085EDb|0WY5EV-=YI1IPH4{zrCYgU{bJ3|qMz zw@o&FU120A#g1qvL`ylcO|i+r{z+4I{x26`ABc!EwbBI7vHkP6D}-VGjP>*hKvgbU z2|MZuK%=8dZmi8Blc0&q<86X#Vrl=0tv=%m3hD~)7x+SFINmksbF+g3TMEaXkhz{} z);kR>td%`4gV#3;y*L^;Jlu7N1EuP%d=08aojj~nGme&J@3?@7{bydVjkKp1Gk;rP zHNNV#0rB5ltN)}j8K)2dnM@zvudPrd$r5B82@*vyZ#uS2Fe(5H1!Ls|+~yPzI^zFm z{x-a3kej3plNt6m%?Y*TbiX7tH^keNyN5$ZM}rUx*EUIc+OEUTPkPXr0`}?2`{Tzk zKfe_06mu4QrLpu}k7W~{Fd=c2iasBmCs$9$Em=@xY0x|Zd+x!}iOC>$S!buQI~^vT z2So<8sGTSAH%?Bxl$C_DU72pj&f$=7BN`|4XbHR1Q-tFfWAd@kkGXtVfU07dcz4$* za@_Q;D7Y&?1f)r0Ll@T5Z4n#{6KvE=O+sS$d^0f-YaBAM(5n?YSO)ed2}A8oRq|Y3 zS$s%sOx-MRmWKRChVP_qX%+mg5tAuCG2-LlEXKg&-yY{uqouIpd&Vx7Aa6KCr7bBt z8U_2O$kW%4H?s9xf%VC;5bQeb2s;XD*WDSKvCwBrjzYd1b-YYK3OHRbdb2|z`+8wS zL4TLab)%&vh%)s`uZjE1e@2$}IZ|RF3HTB#k!gzLi+oRq9cRl}^$%PeD7(h7jh74D4#F#4IIEwU zZ=5$ew#CdvkCHes(VAA`>Y>?0yb&i1k@-vY_J1==SGB>9kt+VxHO1E{Uwjlodnq6W z_f%S64pj#qP=o#ceM}s4bx}RhfDuS8gu#@iY4iF~EU2cm1$`*E5TRKhOhC>>PStXl zv{T@K1_0y;%$~#{OmbD5n+fxcAO8J^ib%RC78=e#W>m3I{n{UZQbZ471a)xx_#Cnh#2wMSY*K9YCkHZYU-rc1zP}Fex~sQTGZ0N`|>Q=zgjiu zG!yJ9&Yph661u#Ms$qY8dg_fHVhWhhDuI-fmF^w6sxbUBqviZJF-m%~!+-$;@lmYG z%~-#FA<@B7{R>^;UgXPj~L565r} zU97d9=YHnA=5>AMqMaQI4J5AmqbMJFOdrR8Nx@XK4&7A;oY}eAEIJ#IZ>{4w19M3b zapbUxx;p8^zECfo8_H$82WfKb(3wK@o-iU%K-MN7S9f=J8=DLnHJS{k-mR^nf#|5u zx(I#W&s*x)RH#p?E-4xlHYr$yP>-S%9HhX$rDm3-Z}0<@pmYfXiFO|yWePc0f^=Xu zS;V4TFr07i@7vzDSlkRlT?hij?;FG;R5}E&mxXI?((ESVBTUjuEYbXMz zU^tS~;EX>cIMVu~o)1gPAk9_WNs?{L0;6P*tmIcCL}unpj6PDe^S(?Maz{r;|7++0Uv_EP zNE(liUhCkREedgSr7+c?ScR!+5Xh!Y<~!^lXlT+ay@rK_1%9#toHync1#X_+9~GkW zb+J%U6Zt$M)M!ABF2=n2GM%5VqNIkApHzV%CN4e!8wU%-F^!F668?wc`1JjI@8#te z*Y@d26&x&`N`aEj&g{xbM87e49Tg27{k@=5;g#j(2$^c@1+rBH9J*cw8i+Ck(i2Ho zmO(prR~zO;6Loii^ZFKco$vN?uMzlr*b7Rc|Cc^$;*u8@i~c8eWCvEWxwz_*Zy>%1 zxK;UGZFN08$qLtb8ESr&r)qDj{G-~A%Ke$#b9AGUb98j(BJR!H#g^vS9RUh0h-|(bPf?BfSLEfpB8VsVJpuW|{ zVqsyGG22~TVLSbZP5qG`Z#{|+MwQ>g!v$>hN+R2PdS-BB=Z!Dj0#+;7ei&|dIkAvIa5_fUghQWk;IF);0rl+R#lq{w0!q}bxop|`ew>PNtvU#hXYj6qR;SHIhP3IX8~*s=4Agk=C5v;p_%ikjrh%nPN}Mq;Ni_}Z1Be! zTq%onykwX6V(#+f*7^ddNKzMba^AV}1Fz1`R$BQd{gZ^Gq_~U>Re9UjxgNo4^J8P_ z@#&;--_Mw=%zk$$RaNJ{|FPUoO3(ufa*F4mM$5w zeQUNX%QYZ?OcclI5cdQ35a2x=tz4x=7EFDKCNkLV9TNdl6=2YyrJLmhlie-mzSVF2v8PXI+^9M!z+X@R{lE!Cu&gCvurk0%iBamp!4Xd#bj}D)wp_Fo282)YUj8Ave@;a>9CvEc?5>{G~lT zC1rDc{SPa{J-ms2sgmE!yKr^FWiBhn`TbIpOqRH)UJNBTm>lqni{p((eToljI9xEA zh9b{Li>~rz?ChWO78O4$q}#Y7wrdF*e3#)h1;NE%H(K0zL&q(e5= zF@!}rLPHUv$0ahq_S?-FBrs?P#x&VJ5v2ZbPixj)io8LZ6aj(w9goh;VQn3$AdDKu zVByT!79U0(Zfu4@KF3`_H@7dx>+j51M+=7&`tE`$;(F!9H@rZ> zs#Lu8pljW-g?>p|^vmH2Ph7$!(QUS*P&$WwD*QEK)>uZ zV#yr1yt$E(aaF4oFA9zft$}7sesOSk2ORdUbn_+ebW~L}wY2DG=K`ub>qF;J;X z^f_zl7^u)>yy)%&W^8x5>DgJ?+0e-^y^SaNS75d4p;y)~2stGvEciW`JbCAt0!N$kYE2gt?-G*9Mx)Z?3O4w=#VB zZF^Vhwjr+`8|v%naqRSd{`w_T;M7F$9vT)IHuSqZ1w3+{at9${yoJ7xaYuXmDBp(t zWx?lK!9hLOIe&srYmWiIoYXP$2ZPF1Y*CxB*P=;hWoar$QCnlFEwHGp&XRW)#vO(- zwR~LBvEEO|&INIAoHZWmr}!ihx=6w;?cBX%W51t@wus29;%128_-N^9eh7T!M>B-Y z6H8OaW+gp3e<)xJg$jU!b6PVe5Kr@X^%gx@$xzAARDvhOpE7^{$j@tXiSPJp6l10C zXQhIPG+wtbBw9$UcmqsiG-5EG!AoKEkVJ}AD;?9C&ZytA<;Ta;p*jah?#=}0 z%g-`Q+lWlJA`%0`uqO4E(B){v883qUvQAyK8WYH0pVVHD2sPT4cQI);FIR2`_PFS; zy)%W?`egay3Bi;ZDuPhJ7a5GK?U&eR?x>6DWT2lhwa{%d)@on!6Ca032URpL{nFiJ zA{cnAo+ifz(yCtb`I29U-I*y{lI6I3-o($=F#J@xIl`CCttvsgDPwo)l-Y!}=h3j1 z!qqcpv>x13yb-^qbfP1Vr^Ax}QCL%Rgk3!u9R3KWmeA=^mu~50(1&`hAcf*=G{b2Z z+l%kB;lX>dA3DJ{vmsvH9Q&z#cBT$Q_pT-8 zY-tpCcaPalrowX5HP1M!;HA%LRxhsN3{G#W<6qQmJjO+cga&-Xr6g`FRy$B?)z`Xp zZYptD$cg3BmNI4V%3*7rm1!@;c65p4Y27+Il4~ic!AVz&mUld=!Z)(NoAa%xmX++@ z8vB_H2M3I3KVFcFy1M$e&QEmI5AU2E5(ugiHN5(yz_hvU_9g}P2?Wo1VHhq@;NY5o zRd%^FT1W4QXf)J6Ft82rD-#{{X+g3 z2Ja{LM@7+M?83f&%|1DWgQe}?BShoi8PnaNT{PzCM-f$%6{25uD*(Y^oJ>_l*d#OMIkx7D&+pc^V?ZbXA>_i9GRp-7FwnJda+ z$i>;QRiZ;jG@!kQgVEYn{yQWuw5_ep&5deJa1yUri*AQ@VQd+B0|`Iy)|T3#C10S` zJP-~MG4|uTuoTK{#Xqr6P={k#zYGZE8k^|v?T0V6>HSGNm#)gy4Y-c6A_uxFjQY-> zyq((SCa5BagDj>tuy$(uX3|;~JGHUiPfvxdAWX)R#3c# zhJ*!%=T=k@MtVQUnOG{Td>%|=Wn^YnCs|chRo&U#B;vN;1r~);q*mmyf_;nf2sCl# zPZ@m@%T6$+dt{7is%bei@N-3YqM)}^X>P>v1SQ9js!|);$M6F$8IVIj3{2d zv;8xe#$(fCeDfjvzqEjBtLr2JK6`3T7iKLGP#M(sFOe?XOE90LsS~z4Yv<;w!#$u` zsxTQsmVdoETJ1!oonAh`yLRrk+^%jhWlo(d0!wdr8cD=cZYva_k#&7`1|B=S3wiT@ zpU2Uwhe?kI9G3n(HlWx>X8z`>-t76V(2<>e>Ts=ppkJj}t7u~H;ojZfKtWM4q^YM< zTu7*`+<1VLXH_7biDuzXOx} zHiHH_9^QLw>G=gseF;oyiu95MSS^lp1nQmkRPQPc235>dzK+_u_2c4f0rrkYJuw&m=lo$F6+H!woyTc9Id=3EE@H+g~qqVh#l;^@La| z`g&0@P;K(2MKQ`|FX|fYSi+I)uG(I`G4Z@CS*AmSuzxCgj~2>qO&~~w`X=C&NyXUi zo>1V8YMG=#3&CcN>jBGx?Yq?Bk&~kXeZ!v}he2#^!IBiyb?#n4MbZTmvynsgG9=gs zm+mL3C*o(0AC`Vrq}A~lC#Yqh3kRKN>1jm5+84VKD4$N*Q2@))DC%x|$>BRx+dAGf z>r;eq8Znh)_*RC5X5dHyg_n51Z2#e?|LjhB!+zKML|Mv*Ip8$W0jj90UdP0T&xcXP zw{eAy4VTBZZgEG_TSH4d%Pem!XKnx5`CrRMvfvlFN^TgZY4T>ONaN6D0aurPU}%Ri z`WqPJ*Lrn>u#qxV+q4K8f)7&0=V`i@HIcDWMrMOF$%YsCtml8&o63dTybk6pKFPq; zs^}F+BxFrQu0%=%W@g|Hw1&2|PT?TrFJAC2yjvDfFaTbt5MQ$!5ip-hcUb*{yVz{cx(7-sM5Z|V) zW}^^fNyaqyW%suM;Liy z`-t4!Jb!a1eE-JQx*asLB}wFKu2)hmB!5IrDkMn0W_<6Y9GAm*D~ApK@HeH4Y1{2} z5<`qIo(%Q&tfP@9;?Iyd>`Z*685_Zf8QRHjFW~${^^b1ypv;Y7B6t-?@NsbIZ%mf2|t` zLqSP#b$MM`RmB3dCtYHr9?CcC7KTrcFbyszhX+SkZ(ul=TniG=X{fnodG#nk9Jg}@ z;cwinnFydRzcPLW`SXeWQ~{q)B&daIW$q)(jC1PF#S69_JDMDQTjwC>p_xZMezer^ zknA)A0|Nsne(l&-Tg8m+?d=J4>Lx5nl_2-U=(2lNzcrwe3@an`{Ni&-enkb}2g+_e zro-8?A@6-@pv6VA0;F53$c@11D&$S-us}?P&wZ8Z6JPeyN&Q2_9m0;)gJ%y8k26b_ zl%*viQLU`3ZW0N;SXvt20O^(s(bE(4C%6j;v8UyfqbN;3a4)( z^!F}<2gX509o|SE(nPL=7EG01tXocI=0DBN#{i@h{IQQE$?~C^9vHVPE7?>vOr7et zlPy*}P6)%rW0I58QyN@H(zRKV8h>ZoK0kTM>*XR} zZ`I+XtG9SBk)wFq93KWCA%&w-Pvn{+z7Nv0X}Iewivf1T2t2Fe7Q2lfIiZAPHC78} zg8~^Hx@xYo+p?Oor@Np205IY1#>NI|o1wt-M^Pg#5mY6jU8-nFqlY9A@FT&T_Xf&> zGtmX`(S2?GiEmxYO~$MEJjMMJ;KhoIKRN%{JwG2`U6r0l5Yc0fzYB8LOlE}fcZLOA zRa`7_ip=ey3{7qAhYQ`OOMz!EtFC@yC#Ng2HPmz^d3^Dhy$%%qPakDfZmBKL2#~4X zb2Ia{|6&E^lEWR9H*enb_Da@tk^EHFz=nT=hzNR0eeyNJH^^}DQaK>F_%>?R*~Ze0 zj)A5-L#U*rINzir%xUSo@=}?iI;Zp4b*1ZqaGK6p=v^a$M31Ng1LLP}zHkAqs|dNW z1E-Mv^PKA>K_vr;YpQH4nIiH{Fuy&eJ1dy;u@<6QjdqI~Zp_@(9-Ss`v zK|F(|{YJOu^Zktg(wCKY()K8XT-h@R7oWBmH7ad;oI@E=q;|Vh(^=ml9vu<7`w3BC zHec_zjHL1pY}r5G?yThHPbSla zcrGg*sz%EGI(ZrF9@`Sc-ic+?3# zl%5do0d%`6+-86GRnh^}-|BO$q+Xt0ko9$gOx072Xr2A9@1z|v zAUhXt2zYH>&wg|j`rpqndY4=5SgS+~CMI3&A213DlX_khakf!9P9W8gBH~Y7F!DB- zOjU(BnTvTyW88{Ya=0^*eSa}x?*0#yIuS>Dr=-^tVO>}*T{ zVxM!HnU&JDjYxjuLg!qM<3^Nk3A$cT?z_z%CqZ!L7x9r8G3Zj#{ip&q%Zr-UjY#P* zvq}7r!w$e>#KBCh;A!Y;dAK4TPN)AGd@wOY{B#puv*Niy|JVd+;e0;dX^D%ACm$-R zSy?~Neq#_aIyqbVWkTZmLb$53y76IHlb2z{tiCcM;)w3(mM zxw=3+>GNk5wdrdxLH%8r-(A3j-(t0Z>}(V$l{4RD!8n=HZ^^RbBsx#bVLXLx%&MNSd10o78kSW{p66qfsW0&- zR#treNIGW-c2w;&mdsjz%~zvi!7WB~3~1WFdTu-)3(By3@PxJ3iA~wK-!1+W@;4d& z#$b^(b#0TLo}SqI6hv!po|k>_y(?EB+bE+@LC6H&*L8U(O((}0y!RDm1zx?Zrw(nQ zuqU_=H^&Vp^g2I6U2j*L&xSl=r3Unl7ET(G>_cRI)^GdjE_NnoI9oSz$IM$Gs}R4! zVGgF%p%+K<$r_DI;^JatnvdEJNAtUhX5Bo~Hns7DgG6|EWk!2H9vf+orNJmE?{cB> z3j&>-eEhXv59}d4$7T#T&cV;?$wUC>^x+b6MI)aMW=k5Kg=nU_2x0_@3(W$_YH| zhS%u$+)d$d_|(staN2*=S>9_nsk?|7A{Jg@BD!0<@p)|ZTWLoVSZRJwM|bJ?gxAe; zeK`NHz3}XIu*7+Kvwp#dgOf1J0eEkTKlyKWm!2E%=23huAplJ@vXXHtEpLm;LCEcS znK8l}e5;>|DI_C*>E0wtrr(W09hr{3P5#CJ+IHJ$i=&F8c9AlnZ|Ue7)CMAA@A4=B zwKB&vy9u1f#IC1LfMPa3{~-<%btI%U|PmbUe2ON?P2uFV#8Hdp1B(Qe9aY zQ9mhvPkZ&DX(*JFnyC%~I8NZW?`Q`G%YFNGLuZ44>1iP;9%m{iEcvSft?nQgiAom^ z){b}ofd)__JUu;u{77-JwVobZo<2SY0Wv2Ija1J5!C|%=DmV>T+9N1O1soG~H`Ou6 zMX)m_^N4Ncs}9CZ8P(O3C}ZxTVOu!c881q-TO!iDg9o-cdwL3#RX=>8#&S-a{5>%> z2Y5xXd4J0KmQ<>+x19Sq1Pl&+ zr@!QP@m5zYliRDAr;acWaW4e{-^5G7swi8KOdCICZ$bWxasfFKQQnW#7c+cD-*9(8 z1q%LIE?qi-8)ZfCz&lo~HS9`MlSPtfN#J)HqI6K7%b9!kE6b2V@r$&BgUd~h4NR?; zj;9uH&_GdsVd;?15T%IV)XdE6tlk5|kD<|c9|s2|ITubt?W^iv`Qw5s9T?yht-ve# zwUMl%@bkQ5HZIGEf>=oWxzsK5#7Y?WVr{;w^oNUJ7&NO_?hxRbSBHc7^H(L(&`gn~ zyZ|P>=0|%8w>smZ4lsGbV>P+DzG8Xx%D!&Bwau$*dPFOFZJzyrrhsPdk=>k~wjKs` z?Z32uOyKaT=UlmW!SNadSQ_T4Gm6aB+_K-)P=BZaoDqLxC^~Oq?*k7{J9(B=Na*9M zm-^J^78X@j+NFA9pg;xe%de*Ztc}AkX#8#xL@B{1!I;WetL93|O!}p*U92m?6n<=< zEyHH6^Y19wAH(k!7!%VjK&t7R%D3vaFoC!G2V~^*2M9oOou2VAZZpXlFS#%O_@}`OpWh95TJ(^PwfXo^@z$+F(b59_7&ST*pAfP2INF(VAF_teIdI?*X)kcg)U#p2r6=Me zN{~2NSHkW^h?!$UhPYgVYWRWL^PS=s0Aw5^4_e>Auu74)uX`L%7PGI59Jm^zK!=;J zv%0O*Sa`l(uwnXp)vWo~C|UfPncVBn+6W{$6cVvAw@!_Z4z^XDQN=#`V)=WdKY4C; zmWxRb#2!tQCR8aCdKRD_UFICN0RdnEm;xH5Z@#;=T24fG5=DmBL{j{{?l3p(ob7#a z;Q(M4fm{N?f{%ZA%TYBCdExK-dypGIAy~9o5TlHH zUJVjxu6+2`ba&@wd7JLh-1izhk*hsH-38u%E?FwKdYx=hfS8?dyoIXD8p5AYptzD$6> zNYqjPY!^6W^F-e(>85a>atSMtYQlM*BIHz%Pg8{aAWip;Rs}*yU0l>d*Lsf{< z=r>Y+pqcMbAtojk5P0{WH52)OWdrj}cqqrmw`9|JcQP5P%n;ery|a~p{>J}i(d(oG z#cO9vRfm8rqj-3DGzv8_t-k)l87l)vb#8UM4>u=vj$$zH!o0@Okqj>pBO67f?$XNA zl2u0Hz}9QnAixJ{WcEXg!R60j#Fl~)CXJN7IKk+=Z8zco{Qe>xE4J|u(w&_qT!e^P zHm56XPqFFgP;kbf8Qg~3@$?yOWLJH8#BNES0gwW)M7^8Egu8mElvPwHsNPki09vV4 z%`#L~_SkO#W#GTR?Ep5h&YsTBMc0lY@JsTAz)$*u-tWB!B`s)Qe%1rNfWSieoMmnt zZ!Q_jA1#U~Ge>h--J0O(cJl9buOoqrL6n~*XZYuyY5}fqVkx2oFtf05EkVD8dbpU7 z%~;h<^ie6%2!9dO(YtkCUCm)rp8vbBH$5#aBO}OGOY5;s1lAJWd}zOFYs)wYnb@ds ze?J%B#(S-62ON7>_%@4=bGiBXQk{(beF0m2?()M(oQs!h5+^m4m8(?kfUE!pv_q&$ zh{OTJ*$$?WszZ^W`9aaKWV@ZX9x&#w)%SR)$Ma20L^P#S)-cy_RK^VB8%}{1hFWGa z8R@@fpvtYTrlg}IFbCWe(IOS_j9QiijV3^0mg}qK4Zbp|eFSl5;Mpk1XssLA=tM*D zm>Nu~m?_o{5(rj%nT07L!>s4y>#cZo14M@~58HzEz%Hu+)Xq*LE|uSp6K(_yR7@mM z5V(gJs~8~^f%K;hxkjhWHk{?zSyVzoKodtyGivEGVmX6J2!SQX$mo$kd>LhSn?U&1 zSX!^#fSpJCElbYC9-aYBn?Bd+N;D*xgc4O=;i*~ZLvwt$7#c=2@DQ=&nSx6m76t|k z?7{tWdol|s_U_*an*~elw`firDiDK0LW2s5f0ZxtH`nLVcw7^tJ2~E6?j>myRoB$y zRaCsPp`T#MWt|q<;&4zkHKnS=M>M2YVPVO=dw5{&R0b$D@Wlhd92|_bW|@3H*d6ls z{Y4B97Z)>>!SsO(k6GpKJrS=j4-!r>fIiaqut-BgYY!4eqqDPS#4Q#x3HZeex4j)J z>}s-_pnD<3e;(^0UUuA&MZl)>r{BK~Hgs=#vVVk~(e>H*m8ymL8U_F|jKBZ!7o?sX z{R!`zXts;=0esOSB?6t${{OLJ;*lx-v4m6|fHWnEHA(8Kmfz^w1q>4!A)oW+?U~^q zHEHQD6`o}s_&VXK68m%o5c*9HqfFE3LDi-Gmy|YW`%zk&k%op~oZ;J3COJ15(2j5% zFVJ-nI(E$tg&GLA3%Sjmo`Z%rtgMDWcr0nGL$8aw zS#D}4SMQSHpM#<^k@Z(FtgZ(HE2eJ}9Q4}S;Ta1xHpYL;Sa1lxApaphD)*oBY0@y! zV)I`loQ(1RjHGs#!!a6dh5R%~sZ%Zk9 z;iMLlqpGubIhS{;nUj;pO(iKS>$W}T<6VCj(*l8%gyQU!N3Wr+$?-${y~^`4+&<9J zg&}qLUp$76cwOI_&N7pdw(+YUuP^33-(e!(OisuRr||@x^%A-)KLIYk_59;|3D z8gjo8OJ}J6FaO&Tvn+pq*G&0dHL-C(2<`>K-?GO|Q-8anYFqTvnD0vev#QoV(a&(R8s zz@?;}!WmfDf=@xnarZus1JCq*wj=>D@kpRe?b++Vj=JZR6>q>gPT}*IPOh?8B*c;U zrs#RMrHXs=c=5*+9Ec=*J|Oe`6UL_LDt_hZ-eowH4oxW}lHZ*;xi9zEEE5?xWM7Wj z+8da+t=Ud%Hyq~)co&wJqYK=e2%r)IA1waFsI761WZI{jhLy+bL-kO;?e&0AJ^kT! zZQbMZsH6PPz*02VaOE%n4GBUfLJt<5$dfbV)OHw}m(8&L;I>ZDE)hvIkQ`kF+C+eW z&1}jZmqwVxmlgk<0dL&kyj0)v3?T;GD8WqtHcw5At+n6k|J6>`{UP}QNJn4Fi`O9A z_%_2S0-RJ-J6V8(Bu@h(56bu8_5iBW^Y%i@6A)P8M#4ntgvAe>{uaU6@!{ z8@g(7mfiX7p#f1`0F7o4jJwRHDfOlVh@R<4s;V=k_j}`{5owY#GPX3B9XMKbf1V$< zHCG;|y`J5lbj7onPn3y@VyE1*TzCMXcRBAAAq}uY-g0lRK zY99pwn2Gz6ope68b7cz)T5we#e#YFRGLRAi761+KktE6Fu12lKZ)0E z_NMtpTLnUqN8xkwu)>OadHuy-IC@!c)U1-Jn@*>ki;_}ff|){-9{K$?RWaNrj=S8?$zD8P>0OfPid80uxqCNaTe~|j{cec z*o-=s(kOXrfV>wb=-}YcHun;_$0@Dxe4G|AL8>85kK+S95kw*{?%z{W@Ae3|vC+{X zZWn-E0?PnN_4w)$FEmu%y(g^(0lHndASRjww;;;kjRB323bjE$cv8qc0mPfv?HO$8 zie%>x99`G5$GZ~5-8nEa8PAdCf4U@3`!q8@e;wH(@Z?AQB$%UUwOn}@2^JES_&gkd z6((2@&5z@>VB5+eLkj;A0WR)kXG|nmWMo&D+VlL>8J5I4E2}B6vcI$Q_prV$xX%=D zwp=Omd4_T^_FLOAZMmI0cy>byuH!vUv3a@yD?bi%*MF@aA0GojS)!oIj3*n|$%Q-( zMK&r%cprKMAW-|ISy>wZEbR^s(ej4IvlGsz6V(<^!1R7S_{w$=azNPogC=te~+`LjS*^9cAa_;9y}Pe@PI0 z9wjA2LuS8i(a_LX^iq1=f_2DeTg+{vNR0_Fj>Bi3{24~=vO{OG}hzIC9vR(gN`rCc|!bN=T5xy>Cnx{FmrYOLyB9|%+2B3jCC~O=YEaVLM z{-L=-8!SEv_rYb}dcEGEoV)~;IKySl&FP@IN<9CE-S|RO^JvR%3%u7(*RO!iBl99K z3#;hAw1D#qxdr~4za+@+SC?Y{@3>e%Z!qFGcV(-NB%#EuwX(Zlfo6sR^i~;k-~*K> z0YJk<`PFLnQRpI20*6RZJ7aw|snv4Spv0`Ts&c0XeAlQoE`D^3 zANBPOV1$s)wr{eowz<3H- zY0D~sJigE&17^rk+)TkYHA`nP5=RM+d@h+Ux~W`QKj$}x^uq_@nzmx+Y*=&& zzo%2=W%xtwi>sSBDDJ1TvakZP!vn6@q%}t-vm2n(5C$1d(V8U56pv})p*xoCHd@ie z(vQzYo3h~*SQMasO+oyXT~susf1{tJP@q6jAtr_NGm4r9esH-MB!xN9Q%7)So1yL0 zab0ir_sv*$4~6`exQ4!dzDq^uCs4ci;c>tr`Ax7ljEIRVQuZRxIW&^M)t@w}8mN-%>R&fk|Z*%cIiN0qCr98X@j ze;_UUsp?}+diwFtR=rwG{m%l0{Z{5ydbzj+w1qa-t_>adzw`E17csC2#+K&>5fdsaFMs_K{wMg+9f;=4 z26{H7Nfcv&TQAp!7=T9m+a)2=p9=pK9sj?ewmYdwR>8AzEOiNdx(2BLdYeemn|;0j zN_dD9eu<_J-_yFm1{$};bSf}fn_NmtkAS^^Rgg_vF538#*ghk_V_Xc3gPlC14u|bQ z6qHE4KwrplmhTRWOiFy;@Vf(~m*4dE%CQPmiqsp;&h|fBH2pzhoO6YQUkm&P0b~g0 zAvywk?nz(j)IF#kHgoFs7#6l?sYZugqFymf+V+{t!ZdL5MYFh;%BQbBg4W0ABRu!F zw{+U|sYsFQLjk;yLfYT%eZDZjVgqQ&KfEQG@z47~yUl|D9(o!Lf zsa*8n2|P7Z7?e@a(3p3J zsq?rT3ud?Xm|j)xk!99aHfCqnelMf@O5aZVKH}H@iGcUvcc~ovnua#LQjHlhFtwhm z$U8gjY6cFpj**|OtLObJ;TlMQjTsC?$o;GOy$MzzFUI^(!ekM1ke&dU)5C^ta2vZ_ zVB6NFZs+DcJ}TMS+0Cjl+^9D=YrD9NWQsgQlK?TMOnH2I-u>}bVaIw(Z?w$omi-1g2+K%jr-a&j`h zT-t}d;vZ>|W;{_H>TjSugDgNBr>(=zWMSfL{269}xx;mCX=-YI-hRmmKazmkQWM3| z`8xG<^i7ghwKpUX_~M;~0tWZg%k0h|kbJO*#*EEeQy&vP1JGr8<mYz9XRBWp>qJ&caN3D)HQgzzH@Lhm)8S9vXD-LSlGLHYg&{%J&PAF zl4Kez0}+vsIxeLCT(-mp`?Ma;IoLWlIk}KwTz?+gLSosKl?p@K>?Nl};cb2qSrN4w zRaltxviVJj1X=v~UHs=9EO;>^<;!L-j|c*u)R>CzWsaK!xTiy}UIEFdsGvwpu2n5V z^ncNIcs=n#%e)jUq``O~Bb}ZrB{6Z=R^HLp)w5wR9Om5kV45B<4|nlV^)klb?BHC- zkCq`YMBfXy0OFQ_*Vir$fI(lwG&e*4 z*4CeZ`Lmvx5o@VFqCgkFp5$@fSyr^b-%;VT9ad7C^t0*j-Wsok9pOwSF6!+3e0Yhy zqXWo#=+Q2}8?TP_@j9rxe433}Wn@8O%enX(JncUFtaOKS{_?oKxiMA682N90&zMiv zxC|u*owJlrXan@j8yXCOG&yc2YU+3FNC|LTJyocuFxRlcE5R|wC6tI?7plFG(Kt+p z8@*t!(`mW*$Sq!{P=5HZ-8{zV!K( zViICiPvAQ;xQr2rcY3-W6=vScf6{c1zg}V1Y138iI{7@m@awB}Ixux|aza(R4*2-- zBN&qdxH$eH1K=N3%apRSvmb?~EjXXd2h&C@FS+N2CMPHJFZlYViR>V)ZJl36b3-c` zi*;QeY5olQfv!aG_A3PMFFa_e;$L`BUtgct{rB&Zz2#O5{5*E&otJy+3CC~pJca_D zzFdcaK5~`LcK6Ffp2(V*xP(Jt|0|9BhJp#>7!mw#vBd@(&Q4h8u&|I1c8_XycF{%l z;}a~R24mMp87C_(jMMv_riJ4@YoE&EpliZpNL}1)>Ki;6N&Lqnt6K`E&-mON z7PfgEgI~I@SK98CdsZKHv?N}rKYYy|uTeQj_k-$#)8;N4K!5@1>NvT8lk+{Z_+R!0 z3VJXXpBE6+KUmT<+#EX2YUt=h=b5%9SpM^oQ7A525g3qzfrTE0m3u zF_8!O`;*c>zj*$&E==JUo%N?|Y-udb;X+VWmYrxt9v#fO`d|25ibH+-Mj%ZbPm$1f z%8OM`oJ>fBU94cSzOhaZ6;4SqN`_qTQLl&93fIo0q^TKh(0WOhp#4@sK~u@EBQuDa zuJJpk_QT@!@e+N)z#5?bkYUALXdgVS?1IeX%HL$db!GJfJ$e^;6@AF_FbC!KBB#0Zhy)A1#-Gb; zuqDP%o2V=cK{e2FJF8NAs$;0y=*sCly(db5rNVSX+~hGT%UFqH7975Li*Q`hpD%S7 zA^m#A*i{XBxU#8;o@;jeR4?;SQGvtECJpU?712dt;ANfHE zMkVp@01dS^IyHqffd;RnppZMIAv3w0>dWVvUz*It#+F@`6A&&+=?Yc4X-V|eQKi?5 zWJZfa)y7)UAnY$OmuPfh&FKOxw=x|`4P+hAr`55Ydy&q6_+SjYFhp{1R_Rd{L8q!> z!jgg-B;X4B)8v?`8;UEC0ZZIQP}Tn_xWl%T$79#mx8haU0?cCYV&Tc)zz{2{kCKR&8o=;Z``V9E@?gd0S^CwI^rSE#^~iZxLL18HNdsZ9l}n z*cm25;FHLlQe1x#Cb#=y^nLr+GSiV_|4s_g;fxw44vyg;^pPV~CZWe1?dDS*Hb@pEPv59@eG6 zqgM{;Jb-=-wACC*jtdv}6}dQ)3H>^izPx0WMd3KU9>o}FI*@~XG6M|G?A zl;=k7+CV7TESxfDARL}bTc>wUkfVvK=V5Q&3S+A-vczlThVgitg5S~6u>~KXkyV!V zmZi*RemC4*t^I#~U09Ysd}AXt{#y$$JlIK9mhzBDk)-Zipm>rI&ziFo7WgL6Xv8|L zLqXOLx$*L2tar_k*tBg%hb8=)XR2xn8=?6R7w2oGeGRC9@QD&FNx_U$hx67xd~B?& zu!Ms0LvBGq2R0+vS6>6jGS!8Ju{|zhUhSf!F?|IEYy0p0v?cV8E*>9a(H}nLG|(~} z|2Tl&<}B(S&w_@91+hrc>JJ?`>*_p`gfj9=OMJW1TIu$h9-59agE_y%Ntu#Se(MvA z-O(|0=)Z0--z}Xp@k<}sb`c3AG75pYDKZ1469lsFTahP6AYlER_WrRN$q8h=aA;}z zbEZc~$B-ryBqhycqHGiQ<#=Bj zv(0eKPFvezV8F=Pl{g!5?F_w|<6DP=Tn_L*-Lf(Gm_{%U1o+i^1DT8OBTQ|4?u0wq z7t=IMC14wqe5JEsJWn}T!juHp^go8Y3AHT8FA9(Wl#|)iwS%1tS64LqIW0b71_~~t zC`yU27jHj(LRI~m1KrdPV%}!@o$J8B$@{f;Ln0E0la)1j5+s6L{v1&&<%q z44@EKD;ExI@2}a$%W&VX{aO{ccQNm(`b}URU1` z?0%jZocp5rX6;_O+wlMXhWnN&1@taI-vhqv%BcLz;v%%4zj&72rx!3LmFo@VYEcNefs5cp5XXwdd%koP6=#P{K7OHH|&1D=K1lNZqy8e3p>x%XZit zY8oo)`@S!4i{2{!z+)(d)W*I<55|nAnuarbg@e50pyB@7@=s|Fl7@y&>#;`KaR_tZ z09q);+wl1Z0$ZO*7m(at|_5X15$oFRY~mZ4nP|Yg*UQlk;Vb?*9N}(u0zw;@%Is7eAq)p?@M) zs|$8xP2|ezHe2Q>Q(BqTo-fy#T+MUOIW#mhxIa+dNoNXgXH6rv>>&RWPB#SN$r}Sp zTzv`hxZWVc0AKSz1w~#bO)hh`pFUYTDi>KD=tt@r%|_8;qJo-D7A!*oG!crb!NvKx zIW$~UGXc0DQ&C}sUe`47!I>G=tf|4?WH(^e;Si@l&&$mPn@&9hKK?uN-iV3*)bO$m znma&H*^%u5_Ej7wPdTeT$qS@O%YZbcDZmiR+6aND|4{~>7~A5(Umc~iwry%#Ob;-H z>hZ?Jg?dV^YFy9fyzh94BIAq!g_o36w=cZxQ#b|@(OM}T_qe~mKTTZ3fD5^unp$nb zgeAx^uFF>k=WiGoT9{i5=@Qz9N&!!QCB!H+BuhlMT%xTdDh|0zRLlMj7ZPxHq4b*b;(YSvd_vXx`#JgE(vTA(U=b2(Ynt8(CO~i&?HW|k^P95L zv}H(usi(SFg#w*aUdA(Lsl;{$dStVA(@SeK^A@La{?*J z%S$O6eC0o%DFQ21v;q-Mvs`rbvXBmyr}mct!~RINw@5}qP6wQVJ7uit zUe(4*R#rzp^T75LJYwRE44b4;4t{;cdcv5*{S*a(#)0v8_Iu9sr``w4hU~av z`oPw-Mo=~ZIE7i&2V&x`hLdJMo)KYQ-3<#52N>wEkdWmD8#$pi^-UlMynZ|=QBLP| zI&TlSK=FLY^x?n2^!N#)iCVcwJk`uPujXQ_FLgKuU&G%t_5GvN*(ITUq;@^ibGHBP zPbB>K^O*Nd^yVgJcl~_FA0;Z%OcF2LgZargtoSln3zZUjMn)dTTM|PoVvy*$Kw#wW zIrcc#68iGTnEu%IAhm^Qe9^W;78d20u}mCj#&tf=%lZ2>yZLh!6$p3(zw%J|!17FH z7E#x>#{i2Nor^Zu6Colov7W}e6;8(w^q2knQx4 zn>2w3nC3alQeYAYStCS%26fmgY>xIm2v4Ha0)&6c z7&saX-}2((;)xH|l?4SCS&{sDg(g%cM%fH?3u{#WuIVZ@6-B3^G942jdQNe89Pzmx z5PGK!7Jgysyimau&qGKy!q*{WL7umzvhLpe- zea&M1)&+evoRNMUSg&jatnu1jOJf5UmF=yd1sDz_)zX{)W-(~S)_%@rbIe=Tx&8=W z#X$ZR6L)fDWn`bqcD>W?+vqPzz20vM=122Rmb~zmw7`99sv{+p-}KwyPf^jgBlzUk z)9h6mL*BE4z8IoVvxw0SM*oMlw+zZUYTI`ekZw^xI;BfGB_t%JySqcWq`Rd{q+1%K zySux)yWzj^dER&Sp4l^df7xG{dzia^aj$j7d7Xz4DbDiPSew-GYcq8X4O%Wp`~q=9 zjpDbR?4GOKT(41>Xy~a!f`fI*Flhb@Q&93-6{on^1XdOf;ad8M>(3{@>NBSUR-ru# ztFIiK$`PR<8{|)NT1UI+piOR-rLFUwPBc%Q-R3*ouVRMVen9al2?_a13a|j!%XXNj z;IEGE&TYCr_rpsi?EOGuhu@lA1s>2vElYOik~K$5k05QKr{NsWl{BIufJ{CJ;5@zI zPLx32^|`s~a+$h&15igw`6X_>wa+b0Z{y>O|G*H#X}0a`U}2TuHsN;cV#H%yYNnNL z-kfHdx0u(Al_#~zkk;;8*5sZf>jV=<`=8w#qR}R0=y^TgHhd20Fnk4m_K?q z?Y#hY-}rDdM7DF2A->XYX3|uN2nzx1z*AG>4>%`T6s)bvObzcYBw)$n_st7glCJq~ z2;B7hd-A@`zF9(zvuZfE>3f zV{>})1=L;VyTRZxn1rjYsv;7srH_e`#iIY=!}RG>2oELJfUd4yYJh%*CD?J^7U#aQ zGBo}A;girxHTB%{RLc)+PuNJzh?k}VE>`%vg6b0Tk00Z0Dr|`xT#p-{>DJ%i;T_6A zy3*6T%q@(iR9Ht1%2)hfgk)G3sMJ77mQn9UsKxtq({5JUh)h>uF%Pa=qZ#lRXu~9r z=>?GRz9Gj*IrgT*nQU zK$)iAeCiKeP@)6{^&qpb(9@f(hxiCrT44yfj^O=9k+6^B2xrzCaZkdk-IO?Vi3q!w@(DPbz8RD9gpWI&lMFW*#1fW2M~SL@BSWv-;&8 z<)Q(qJm}{O(2&1YFQFe%E!RhN!}e{16G@pA2O&_ep;#c1cilDOWWnwC!lNK)HR+O7 zMk&**+HxS*5oPypwy(^6QZ9?|MMJR7a=cI!M}_xe=ig}{I`q^1H6!yaEoRlNN>x(0 zt1@9Hfyh0I&6fOvsr{uDoV9JHW{5>>f?*#Glkz1rz7~UQ&{ zW#c=;z+YHtmv1jX6Nnf`N>mg}-uyqk8q%DO56-Bl+wkxS`B3TI*Qb{{?I0=u^CRgu&S%TZoIgA)UVRg zK+rxDdl1$X& z$Yj}k3F%>L-e7e)ApK6PU|=I7dw0v=^ZNenMa?!sm}wQ|dQz zk}bV{RFDZmkP7O)w9yg>M@W(rNC9~fI7U1%zM{kXA6>xqa;d}hcR{H|*I{>1d;Tb$ z`iBe_0YRx?NT*TDOOV*7hPA^oC7%=-bNFZ_09By;|E>xe=hc^QURVm-hwCe7lR{+ zzt3o4iHrB#+l)_Nu5rW+co7Izhngf-7av*_TW{Q|iim_k#q?I{byq8CIa3~g_mGk4 zX>?o$3d0V>sN3`R9zeMcs-G{RF{92uH3f(=9*ge@7(RpnWi$hrv~VQ{^q6Y<>og63 zP-k1VEmUXxjj}wYRbw-lWN*Ey(O`WvF3q>McO)etaTU2_&UBV)E)GBkOv#eSQKE*d zqqxa0=NcL))krh`R(FZ4a+yt;mnq|7VBlh6F3-;D%$wq4%`_dJFQqLdTmDrbj$%Ki zLl1$V2gAAT!nV%pYWkmlbZq1TIw4p51ZdvTnP=WNb3;Bi8FE8U2usqi2_^UGEB`2RHXp06L@~@@aiFt)rq66d-G2 zZ|bL>KXoy$`)3*if@UK)xU#`QW4sEO|9)Jk>~`h0$*MS5-dGB4-XwEe$%(7cadDdR z@;!(Me(EKo#$kH980)rmOmz)f+2xXAE3fnATMcTjK29_vo{jq%(-KYtXqwb++eOByCVfXZce&Z~Z)0dS8 zKa)Pgi94-VU$^T=S-*1;8Z6Wf`Qi3A(i}o#$Vv}{mnB?>9sO&DjL;Wh%=a4lMQhr3 zl|=wcf_im%85R}<+>L6vHTCq|#mx|iw+GT(@636#q!qU$f|PB6B!nX9K*eqdBc*zm z$T%}mrQ%#F9dxzNw`fL8S&W8L`8SHNE2KBmg}nbmJkAP${s%l(5f0Or8V2kRAb=a* zc679lRrpD~y_0ABT|P`QOt*~PPF3-n4N>w8bbNnmGS9F!!-iTv;vaD*r!39VZJWBA znm;$h)oZvF|3p0M;b7Qt4LD`Z2L(U{^yr`U6~MCwa8zukQucJ)*xAKh6wWH2VEp(g`s{;b(O&QXIW7)*&VFc zzEaZh@>KiBN*psWs9UErndMoslw&;xw!UQOl++@TYC=v>-?1j5CCEcUf|_NVOcG~8 z{I*#>AVGX8H>AYiZETF^b@8rDN{dpFm)-c4{-c=*uxpTlz7V6;7kto5Jjne}(C-3o zv#Nszq(iWvRr`l&baCUaKJ0#?tBE>y{wfMN_~Q`rzf8;+IN59LNEh!{*3}iSt>$5 zBd%qbM?zCZX_Ck4YMGy1qwE~)={I`_geuc`<=M_G8Z#u&n=yHPPT-)e$QsaPv!yfA z)|LU4*(V4{^`idQ_c0ZvBOGJKZ(LwkUp=>6g*$X1<)QNN@yR2%$rv&iGydDZCEWbM znWcr%WQAf^r{n7AW*Yysn5W!>EK1x}p5)#vP{NQ5BVV9toHR1RKtY;Z)9>G@Ocs0F z%EuvH*iaB~Bc=(fDYv<4?>-VTM=BP2^?A*gq_pdFsw>!kk&zKi{|ZQRyVe*xA4&bY%6;GQvU=)@h`5@m zAq1=f#%+c#k758JZun%C0_?8`ey`Rd$6;Yt5j9y|TRFS882I}V`pP0pvu^kLptV?B zObe`V^HNN(6F(GpWmo?rH0$aznuc?WQrygS5Qj{8Nq8ym9+gShLyo>G#of;kgfON| zl+QVG)bWcJ+f+xMW-`ezSgA(Abtq!q-7#09dQ`#0x|-apWQBY42u0YeeO%^zEfa0G zJxty~rcED^2?Y%W9P9y}aRsG``(}mi__qkJDN^4g3Lhbk2{Sf}q71%T77|da6DR!Q zO!ZCTLDg^NCkmashB3-M53cyn?II*xAyQhBFCiwT*W6QBSZZJ)a1*b0X^kEB-Y7-2 z*gq?;B;R&MsrU#59^P=;&u{MYo!9G6$&fgqL>iW4oj_ zC1ijU^UxIW)E<>W5;T zwiCTrjAsQ~r#Er0;UJyVv2%Wf{xAhHZMU=*L?VOV+xU#V85tZM8$whk5od{m+d;q3 z$^Qyz_gg*I@Dl~fd~JsyEwgXDH5J^BJWG_YXA%zmQ*i6{`hWpCGcB>;GfNT;eLXpE z1PYM>Gd38?Y8YW^2hA?Za4|S#v#lM@?_Xjhs4uWToCL_V!>utlF593_gKsoZ;Qt5* z-zJ)aNj>F)wK59B+>zx!$HqAO*$Q(%emp;YK;X3o*Hfnn)1pLtwc*HxT!vH^EPdhJ zTFaVsZuw&`HkcjNGH&8t1cE-^7q~3dN}+&NF4L5>OYx5qylB{uA#ztKwtL|4S0xpI z(dK!J5MB|7!!IeBPv9l8FHvwXMf&Ure9@-%7^IkQOedwQ6l?aLekfLD0ml@_A=X-< z@-{@4rhh`E%sM$aiq{zFClR*nN(iaA;kT z$p}W$blQ1Wn)I?QRpURGzc##p4TDv29z@^T|7>7>V!fu6E0r@Sn#}PrCh-8g0T|Hv z@Q)}WfYG(dkTyvBeBCWF>p9yppWXzu*b)len-90oT7(fygYf!I0CeN+==qJ{lMY%& zHS@FECVimWehg-OX)hjgdtd^?gF6{H)dZ3M8(#7hcBy6O@ttY7`13`A<>$HQr=Yhg zWe#PuF8opsN9DHzSm&uC%~ghvwuY71u?6_{q_Sex)`9X=cJYM3w1z{L5cTGkGO<;L zD)ve|iD^oqZpLrbfT>QNxC#YKq9WfBMqEspwvnPw8FJbzTb=?dF$v-S4M1mAr}>E`-Af@eh`e9Mch&<@$n zIV)p`sh5k>zJGX8WRa|UmuonOXV0FA_7e*Y(?ygOw;{9J=J?~Zrc-iaZK$wcClZZ0ahm-ZcSGYG?MU$!M<6dcdd;e=I0l%Us!mU`+x+G2ptJV`*XA(oD7MUH?}{BdbwAStwTbKUFsZyU&li z-9x+WB!UEwnE2gSd~601dGRQQ>nH$lza z>CZf$Tz+wh2EkIdB|PlK^Widn`#Y6-oi{k=#m5U(PyQ_C>?|a^D9<;y*%Fq@j~(Zp zPLH0$4<|+_Uk%W^crs-tm)bXNYZv(aVQkoOwX&`36{^}qTGutg=;!9<=HcmGs>b|; z&SLxI?@%l+hy#1!RbC)Ck=vOp)%Ei(?H1EZcIzbOSX1%Uiv8YGs7N=i_to(!<68AL z3et|l!=5MiDP-r5kUz~MV5GYn{AN8LA3q&8-JVK&T&5POU^{mY122KcT_x!^(6`S! zMgZ>`Tx=#XV@P5XF!Glv$Zo@UQcPZ=qlTbKoH#}_J37Gg1gXRP$*H?cFjoqXPtL8( zTyv|Q5D&WY~~;&y-j`pddhenj8wQP8aQn3>Mk;HGf2lnSHq zz9_fwBON0r;b*u{VopL7RL!MIotDAU!OlCU!Ld*#@CBcdcSe21eXnV#seX3e{v7NvB#v$mc&lE>oG*i>3G+}$0o zL5-(MMMpOTVD09tVEdj$7bjh-y>TiN&0@n+O+An$WG7{3Uy}tMqzP7D&4(*9-3fZ89vA7C;R2u+5ATV z3hGuXs9{GJt#UwRSNiEP+avaFWr@VmbKg+S&A{1S@(V3QH_B$Vw78cq6Sl<30;3i+ zyG_$br)(>@BMrX1<9l8uu!|e$S7Ew7<>NI9nZ7VPnV!!qmrn8gy>eu=M2sF{uX=cJ zP!zGe?A^0b>3eQO&pM?x(HlhKES7&-nr`WhU1;vs1ahsNbihb9`&vgR+KKB2q1q7VKOKCMcb6t~?b}PnB4<+)g zA2)lx@0Bq8B*7!2N-JrR{%~w(cwwzH=>zeMXXbWxSw{6IPqgc7@cD;$L|Xb-b7B8l z=>Fc(8On(sU<|+QkmKinZ7$EqIC`M}8a6j4n}wOVvZ^{rsI4Q5G@weTPTdmFow>go zR&Y%HZovS7IAV;>st&4PHW}HBU-x=&kzNLLc40-xX-(iq-;8t`J~-V1fpBi!Gf2rl zyFM00?rYCaHxxzWcowM#(?;f5=dVo5M2i$vG)t>Kq&~XwJr;-`FU+%g-cIckFz5vh z=Ih&iV$AGj*z0iBW-chm8%&AXD!hP0Lec$%5$soCXvBNlMsQNImz1qq6(_kcwV2cAM_hixa;o*@R(@CPx1?z_T5eS?zJhb62&*jR`A#QGO z!Ko2AYGH>-?9KTZNCvs(H5E4n?#5!p>AAJMtx*>ueUPMUTbt&U*6ij2Ta`KqO?7$Z z&@UrI$uw(l&qQG2$>L>8Mh`Vi4cuNI+KGne*M7%stx+bhI*w9Zo*b;>-z#cZ?~EfU zNgFk~YC(ILfkvcc#1IUL#;7B%NLR@bJ5#cl8Ugn)eB3ddV389WJvST>DEY*5BGY=g zP-1=Aui~HVf_C-QI;=rg57ET;bh!9((kx z+eP4Vn}jUX>@z%RtJYu7*A&+|%)(e%+1A|W$zT3bFJo0Xlm?SzN3vhZWW8h(;*$EF zgw8=@nkp8av(He6DUooWc3j5BSv}@9c(&KvAtNAI{41Ev<^I7S0)Eeq-<7$m7osGm zh1P-yv0h3{(2cGU@2KCH$Kbz#*7|^qSl{`whsdC_?rN`3oPKijSkKwF)U5Ydr-B&{ zy#DTZ*+nkIA=D;|{uZ;>8s@m=z}2Q-J9SZn=K!etL~5k`g`RpVR6cz&G&I^uW$*8j zSDKJWj-rbTMr%6CVX4?QaGq(*Y@}x9_N|f^GeB1;?Gs%4yXQX?Ug8(BjnCy492OK5 z@%R2o4bXy3iknKrNI!IZVL2GU{S8XB3Sd2(`cPHw6{{0RO3 z`N;knbL_cAJF2$4)80p_(dhSo0|7krD$J9EHD*<-J?=J>OB;s~5zTMEe{>=5;+bBo z-5QKY^IW@K7S~62;G~#Wmmz4rp0#k)8Wz5zq_@}LsyP%F4fj0v*+%8?{8n9Wj$@jH zTSOR##g-lfR|lWcWcRQ$6(o*-KX`3Je9h-JJ#erK%YA*^Ty{D_S4?|oGA=umUU!kI zwCXn&BXa(HJC>g2w2DG$B8CYc%%hK>g}*#kRVZ(IBb(UT!}m=_MRR`EqnCM1ei>A) zPR=~j-Hw($r6r{os@8u5xQk>LGSw6xJEH=sL*S31 zZ(x9pB&;|qs2YnAPfGStI-QsH6Lm#>|0sLbsIlwc`th-`Xt`*?Hi2ZE(i9VScR^QI zBNx>qnUzTj3ecqZ&vXLb=o$*Bhl0i#C{#lGcoD(=-$%*M_THM|z<*P&JU&3yh^mKT zDEW+?rHQ|$hwyyKmbT@0rf|o7TDt7Ht;T1U0B53(M)4JW6o9o=2 zxWEP_KJR)B!ScFuQG{aQ{q);X#yEbCRF~A;ArtN&Z?FHDRPO07jFs*^cSbyA?M^f3 zMFbR(!|E0=62p;2iIy`NRaRGnIbTo&HWs~SWm9Y5e){TJtl01GB~Y>>cgY{O`~5Fa zDbLfN(!LJ?z8I=%Y6FI!+DxnF?6?Q!_`Uc6u%f3=QMDnj6%o0xTG7mTPPg0ZwVe{C zyXqQ*{MG<=#l1p-9VUDNoIw3wn$PG6a$egCs|{`3BNGg6A{z>jAN%o8UlJ6oK~C(_ z76rDyOPui_%}8)QX|TO2m9)u|3hW(ZAj5cYMCDX`oUmP4@lb1!AaBn(J1bvZWJTe< zv6i$56QEnZ-5koxqbL&{Fl^F(YI}q5eriG<_&%iadGc~{-Zfr7Zxiu?8Z~Isw=`m* zp*inOh~+T~m1s44l_5`ZJMV1(zqF&K`=vC?{sC%uzctU}My`X>5_0N45K~iAP&X0A zv>}Dk8de@(TT~nCq}jUCCBtAw^80V2$ndnB{x=&tW%3Vz;}ORFochZUq?lR2_k-+X z?gpJIV%5a_JVNJN92}$h@9jvSsKBgmXy|d)+dc4y-Ld3X<%BL(fUm9|0J3inra!NV z)3<#0#W~wbwz|Y6z`)?PT6&1ravn8iK67di!GEG_@L~|rkpsraA_OXiZ9$=dzCH72 zw??qTy=e$<3@d!mLhXAB1dhNnxK>Gj&W)%nx|2@Pix?E8cQ{~naq^>a+NaP+&D!+= z5rLjBn}P*X@(p)u9?E&rjx`5&W>A+lT1*_6%mbw!XnZdjtasS4JokM{vu-Wx?HhLe zgEfrp+2!SJM9``Qm07;_jjd{qPY-p#CWgC1MC41ma}5x=IXE_IVxnR9e$DIa`^s9L zU1iW>-t<__Mfe(Rc1+-Im-@4N8f6bAvKdiGX)2us()i$KMnZ0tcaUZn{JEAKD)b5P zFKMgFq!ZaEl!Vh)%t>9x@i0o*6VQ9UxB!=J$d0~uk@x@veX@xc>k)kB7WL%?n}Cew z?&h{VtX1neOlMfjzH9~Exa47Btu~|4(5rs{fFO2TV+FrqXP^sE)HU-V@$Z1A0u+9G zPhOr`sYZIdBm5M-f9=4JcZO}=OH$LLn46z)9tl3x3HeMCqw6s5uE>9wc+wvxEs0^& z%~J^bh=TYn>$kEDiql9^57ZY4Q-6{SFklY!5axWVLg%Nay#py|=(uNBT+frn!ngGa zdr3)~{YqV39g5TP_xCXR28IBwgL2AJRV5xz=R;IbUytKZw$HZo$1Zmr7Mh(vPF9xh z`JtlPp+8!dfr2F|7gv(3^gyv;QNRc~+|B^~TMb~A z$M^mYt|jYYHg1qwu_QW>3^;u3j^CP??BcK0|K)UaD_cLRKLRz^%gak2zkFcq5ou&% zA|@cvrs23`!SI1h_cU&}hQSw)ag-5It8^Lngt^VS4&tic*IFjR!QSybPzverwlUN( z$n>1AN`F4i8Z8=6cBy9xYrM;elK#rf@lw`sR%m~3ux0I}j!y$P8^wZbXk~MygOSl2 z%_KPxB&n&~l$6d>11jCpxN3h0L7#fsS`TK);zSn{4Xuvd@zm9!+ZOXU^JG`)+Vw^l zno9se;DNEj*zVHk<3~_YhCRN$Gx|tRO+7d;&~OBIE7jBj` zfxojcPoN+o&K$o3rvM8}S@T=Tx58PhAR%-#G!+kLEgW?^dLp9HZjn)7+M=emUXI9= zh=`pe(N$1D6%Pji0RidbE${A50q0@Mp0e>%9;$$f`qCw}_|++#7(j#q;!ri7oSZBS zJ1*3ni)(Tc4$J`LOJDD_>R(<6$mY<{Z|c@$BAgj$dS#r5Ya?kxD zU|=xRqf%M>x4?EQROROOsQ%f2WtnAKT}e$zj{yr{M!@(q@8g9Y4DHSjfz_DkXew0L zUsuX?!r;P((!JEj+JN!f$#H`-0tAFlwbgu^@R{>NieDX=Es<5w=vCGqp#^U8vo%(1 zZD9Ck!no?V@tRey>dT_Xy#^4u0U%=kfDU$Wc)0U!_IBjT!fl1)vh%T*&pn<5KU`3d z!sI(Zr49%4YQ}16g|p@kL-5|@jErbk1;Cy#uqQ7m$@HPpB9+l@3JkpfvIxcuTl4L! z10B#wJ;-UicW|_K5Fq5eIB|T0gy5Q+$GQfN_@%YAuv;%OHU~$?RpoyT*MiXRH1a}1 z9l(|hVT)c=TpTk<2$4VZBe2hc#Xni$J=kjGa`}c$1X$!JZR1Sf$sy z{YnAnNU22mEhZ+6#u-qJSYw3?Ce6+5wftB)Ie|5Dvnc0n47_=>o%>zS2 z1=y*8SzX3Zb?-2y-&tvXv7x8s{NFFxvK$9=QD2?a(?)+8aks77s;Q|0(8S&3BR5?m zCFmN4^0s?cL%(@bs$Pcv1kpgYe|guFw}Uiou~6?goZk?mxfPkp1Jj1k6brCykT6$( zu3jsSjf%=(wsoLU->mPC>_+y7?O>^~U&H&N^7{=J{Lnjwjx3+{^~6C)R=cQPW8Ww3 zcF;aBW}cHxk|3v>9>?i~A$XvtHrrxU$a(iawE*uobx%)ydfnlyY-}8)#ugTSWk!>I zlcr$?%9CpgWa*Z7Grt@&?DzKfh%&x@4c5`uZ498{_Il+{t!AP&uX=F(cQ;3EfV`qK zVmZ-!yoi1b(hqYOpbwIuC4eNl(cOoX9IPk)tso$}7}s!IbU)Y`1@W0y5F(;#qf4xlRHyD(7q*M!@}S=iBtjW~%{)TwQ{=iT zcTZlWvD?rP5}u^F9#PZLeLo2|FdCjTok(VN_oZ^b813f0KalfDM$~auO|4elJ&q{w ziywSndZeVIrDpOX;54(KEv;pyGCNM=X>43{Z*>3L4>`=!?t>ZA-c1y_?b{Rx*3k7B z9nLdt&vKBu@9c0L)gpLRU$L^Y%SOws>#jo&5-|~<0WA@*XWOX>4h{jeKVZmXp#}_Q zMMd)}6k3L*^CPKoi9z71ZE72c5z-0pXR^Do(v>%Eyqq8Jp?26)`|Ni!sV?e~13pJ3 z^nNjyP*>a<*u_pXq1>%50Igz4sjscOx!FKkD4ia~@rj(%Kk?5+zRSG=)eM)-Ih@YjH* zRS>s}i73?qO)d@hRCn&5a*~plU@$UL?1yJdkxGTmY`MkZ-Vt#4yB6ll7{K051U!P- z#(VJ~Wei-5ftSrCbI`g-2B1q%mS^YF%|D~0a@*?ypevAa4kyY_%hRhzjxUTM@)2rA zT^}y@dnh1}+U?WUkPs9r;$UEayd@P%n)@tJ~iv4vIz+qX5D&w zCtDe9*oOiyFMfCT#sdaCEQB}S!W4*x5Z?Z$w^M_5+jLGl8OOiAy~D&zU&j1G}<;@%Jr6u?J zRG74MT6W0O#}&b4#$*^1BICa&kiZ{ncqydY_F@K&c`*S0wnQ-#c>ZjhxLI@KW9~)Z z1%b4&7vjYO^gRgU{rvqWJueSu(}IJ80c)4h=bN(As5ukukOn#`s*H+?4b`!{l@|Tl zDm)F%UnijL?$RX>(x^9FOn!bu(Yx)S+!*PG0mg5rPgjp9!e72jPD~X2lfQa5G{^-G zr12Pa1}mhyR|7$kJ`ms1H$Fgca_po56?@oexXLG^^ILBvE!Q_X#`J*|+i9Mx!~o85 z5;x2zxRG*DCu3ifmAvzr)c$*A#p+df^-i9$qQLF#oq~LvL6&sIZ3HJL7X+j?7-&um zdWCC&%n0OYC;4!`0-3Ga$HM|QJEQG4Z{feVaE&8x05clCqm=W`kZlA6dQmc_s+{F) zEyUwj=^l4z>l&xLcRTKC8|OgVi%?U|4AK&~V10(2!-%tCqcrUXVCEmE^s`jG4tjfX ze14xH=S9SZ^$x_uz7SsRc~LugJ3y^pWzWJeuN@-}lL%?u+sZ4iE{hnT`t|D`E!#5u zM{RL&ae4iT7aievi)^PU=E(B0mWuuEeLf(50^0=8>wO9pUI(VSG}-9ztGN0_S{$)) zv44ai8jjj$7{IXW`kNC`;lE1Oh89qi{|J4 zmzOmZU&**ppBJ9MscFOx0ZL$A#w@dy9j>X0xt3r%>)O&7O;17N(e&M?D<>^d$DgsW zU}oP1Yv_4dsO2~Ab*y$lY zu3dE!e~WhI%`TYlUG3DPDeo0`&=N zeK+uo@UHrbp6cJH)`VHXWb#`^xJKB@5QG|jR=mwJ! zSUy;qS#jBIF+Xv8F0gt1n5S!b9>aP7t~1qVu^wNqAj_(+rhfboSA(Q!&zk#B7<~YQ zuwQe>0c67Ha<9s|k;Cc2a3BF22lM*avzgVH9Kg@i>$j4Qxz8HY((nni2$UTGz3~CK z*SCGXY^fn=Q>z>`o~yi-LrFZCd))L#n&vw@aV)m1f6dBRq{!9$um%#FFGJP?U@;{o z;ek9gt#VmzYFuu5_Tr5#C@e%k<_%aF(qi8xKs{OULAk#84oIj)d}zY?zEJ%s2+*0| zh~Hb?x*>d`9NDV`SyO0{inZJx(%qduXPtV+UNdtaOf(}IC5yJptBC&ngFR+M)+iDX z|ME+){8x(QU$z}hCJuBSL^4!VRD@Y8%|4HCcP8gsl2`qiF8+@LNNMaa5s+CZ4d30W}6>~#`LxqEiJ#JGO=P4t2gphIAXJV#81-n*ej*6yL1cT)>x+>Klh zCndr}FRyGZ|a0tN5sU;_sEptLwYk%h~3H`+jk0TAk5 z%v7OpVy_c_1vqdFmoLGucd^OIW5= zs3ZZT7>xNR-)<5Iw}#SK4HtYw>YzR6JOHz&6C}P@kxme)|9yw`tgAe%^+X~C1V*Y6 zWeVLh!WgQPB6(nW`W7AotF!C=Z#~r8i-rZp$sxz@*p*k>Hg)h-+nry2Mg-`pr_Nzn zA*#}L7{B<+k)GVTMSbSxPmiTweZZ^sOFv8;+1?D#cWBBC*`D+1-rJj4(#HJ8gdc1( zX{y(-nxPu_%@=%6F;s-?52;Gj5Mk0XPAT4Y4?=wOjuKVpcp6<|hu;{xJ{=YKVSV|h zqxF)>V?)%j$vB0EkC3wtPv80P`}ecw^H6m;b!k+gX1<)i=$7>}n8zt{X6EmMz$Cjw z<6}&3LaEQ}Yz2V#sap!HYIs;44m$U~J2U8yTWbKM%mhZgsUnqU=W{-Q{@@)O9=38C z@{US5R`^U8FHVYXxC10iU?ejEn-17-VvjRHOS0}&UB@>+-G*~n)kL5L39(~t?Y(cu zKQ^@YJpsuaOLE!B%Uw+4pWh z0g!DQjfUUPbPpu+9har^b-B5R{vj6$DcujQ)k~2_Xnn~`sQ}-V*9lNm=$jt;`b)3A zU;l}6>TEDvWC5<3xX9h@O&lBuow}#L7rtDvk~SdIsr-dX&KjfMf^VoFPGw_rm8x#E zfW8U^_L?owtPBh-p{#I#N7vXm{B*98qV!D%4F@D5e{3heVhHXmMWaY!V_*c@u3=ym z1|bt8dI4+aTbP^S1P&6Q%#Q<(e^RP0s^VrK=QGrcj*hX#-2R$f4x&!NAB{6k@smud z>_*|TtHg7`KO+-b{K1T~QmG~ju<3k^{jyOoS!iy8wI6Fx`ct0ji#_8M9eggB34=BX zd?mFI$Iim?MsWaKDgJ)Ab=(8*CXc3Stav0yBQJg z>Me0lapJiB+6)(kcNSm1AX2fmEEBwbKr#(K!*?tWw~=ccV&R}_$pX2t3{H_BT!7(V zAq7^+XmBjGjOhKv=;&x3%)uImTKsite@@`1HfJ(2vN>J#B}enw5o5eQy}I!J_zYXD zH8Vd8Of2zMI;&@)*VU7(VP60TK5gI}ILbH*fVmx+ z+aPK+t-6a-v#Q%(s&{q0Ux5e-4Q02x3#}i}a(|3niPWx>aCm-PZVLxgT&EeH$ANJt z;;ydlMdxii*XsjDtk6i6(#LxR{D}CJl<&_q&6l%Vv@@6yThC38rT75?^X{$*dnwp6%F57!PkM9Hc6N7#udUZ5*$y6bbNd&S zN>%W%-%XYtJG0s@blt2gOaA#Cc)UBVEt-C|W zbN|Vz>HLc8;0Ty0C9;^2y9gQnDEl4^!LOr}(Qm4=z%X%cpXe+q+C%Vk6F~3a=4JV` zYm_!ur)LDLS?@BIpPtx24%x3%)RPotf6?>pARPK+8-Rr}M~hUe(94f|R!IWgZYSee zt(Jds5JYCM`GbH}rDvuV|9wP(+=-FQn| zOar}>7MvcT1N~Q42=U`X)H1UwD@#kQ}^yCs1u z&kcx$^X1(Or%Q$g2K22M3xGnekMm!3z~ zfHbcJ`y@LqyXx(&di{F3rrEx85-{|NpOs8%?s zSgfivskpzS+TGkfr*l|dHIHmoTsXzE|NeUF@4LjlC0Abth}CB?DLIeG4%`**^A6@}r}OY8 zQ5arlV9Ffz=L*E_=~_hOhmP3%E7hYVr0!-8t3hC4JzwS?@Yk(pL}`0iBQiSLdn<{> zY-?3eq;m4D*BQ$4bD*%`A8(2FN7ri>2gFcH%=Z-I0Te=mXkBGi-9vYVvo#4f7M-Ps zb5->okDTM5QT45st08d=^z_E>S63#o#bNR3NdCwT`)5K{9WG^ZIk3%J;Y3zz=9W2| z)sT*V1}=^%sVS&1!{N?Oi?Ft+@f$~LYxQK!kr|?sSjBaia%vYQigKG6;I7J_Vf~1Cq zh|AcCc=311@=a04ho+`RWNJb1z!1h8la@9#G|;t)vb@=09R7=$!0|&p@tH8zaq@^Q z_ir{|#jdU{b6AD-TD@d4US51ZU|@DI2sux~9;-u4ON%E0C76Z}pCLN#*WWuyt%Ro9 zcyT63MgX8$}|V!CPe{824tHN=>ca2L>65fDzYTZ@7Qb z&(Duh-Pkm(h1k=--UvtZ*Z?M0e zEajWK@7r6HEt8R%{Tcs)#fB=i@(jZ0cES%1reF}JTMF;30zW#m0;u^%UF&Xt+kSId zkA85hs+}pts?K_>&VFtaJ5dLE4HQD;kmFa#S7uWs zh6G4WE|;dF0mun}fLC{R?78db?d^7~`0U>EYA{e<7ZmybNVvMzYc{&<=H;d~rh+tb z31@+bUBdowVulf@q969OeK9gJL^Absaq(~yx)FoY(MrRd3|6SuyBhNu3rdh(->Z$T zXgz0?O`Di4&C$-vL`s`3HE=M+iv@|Layjab$MjqBSK3u2Jx=?bF=;(TUK&ZN}0dY-is zmJ1hb{*b@%-}5Tg#yo+!`T~r;`)Lvp0s9W&o>?p`3O(eYjvI#oT8oM*v+ZUf7B^@x zfYs%hE0q`*mF5+6IA0AyT)nfOV!*MvU8CwhSAy4m?kAUHTZu-a|1PHH4u|PQo+J*N z#e?fxe?10af1$P`4cD2Av97S>IGS7i{^~)e`#nCT3d$PV)pjmPymD>qv*nG=tU@9AXyH_B4{yZ*kueh-zWkZ_lr?3~7@uKlOO2ekJo zoX)|cV~h;&pWM5?t1D7dTW3qKFI%I8nfRa<5c2UcP@Igkw)ZrY`)SeM_jTvSW@Hed z;|m5L@z|`9-aAbl&ewn%HLK+lj*>g6(%yR=vn+4{&Hr6M)WG?Q#5cEQMerKSwy@d))Zsumvm)-5&?1rKG}VL|`i_Y30^!a?7ZR7oJzzQO<1b z&^RlcDJ}lczjv=S?wg>TRZ(evI>Ql6V{FiVwlqnkL?lA$8y(`{syT|6CLmZ2478TA zDl4NM*K<7I8(b~tT*Ddd9-&sl>9TSt-EvQ#9p;&V`X}21>S5IoncbZuy zlit`KEgGs{4r4XFsAftpr`P?NNEef5T2sT_1p%GZY^u0R^eq#e#3QqLZt+rssr1cE zE5~d1qN@*uN_tl*D&bO6d-18o$VVQW zf2BgPk+-P6IGoDYzpFu_g;NDO{!m+3D_E@NP+eI-&f_l^6ci-3%u9bb*C@AMUsPNS z3F#e#R!k4hwJP!JuQucJBrb6oftZw=6Ex{}MRs6!6e~(>UxW&2$M2 z9u-q@_Ve$Xwy6KCdabZkPmFgSdf?ZZipx5dp;Ah_hUcya0KiR#|9?{X(<^`!BpgOgF)t91!;u7u4!=O8%SJ-Y6rkz zi4xK^F^Nr1*3+{fi86jP5Q@s^(HwsZIoHQw66qJ2`$bdJk^&=k;`kajvR4Tl;9LK` zewH^dNaY&roqSCxa*Nzkhwe;i-AiXrZC?V@O>@Q4tXV!x#gumGHA0$D%q96T|8n7x z3crv8NCh^lr9cdfwU1KAgS|u5<>g&azvI5YgHgEAHLysHiOEbuI{!mJl;1ivHMF#p zamDskO<6f9DPf92NMdVaLw!BI;iq)$9GCIEgVKQddQD5ur;&FY`yI2CrN z0+h{X9j!~R5n35Ok+-!U|3;-}ERe>FaI^UC?2L_`CM+CHtHxHAzKsD7MdIH~j`1P5 zQ@6ovd@HQ>t-NKBm-fxcw2Z!~Xq_Uc_qQVV{kn%KDC=loC>i#kofCJiLV{gRFCuc?Q z+mI7>ap`KO`B@cfAHL^<=zibu{eGypnay&hcK0}$&FbRlam6eO^!Kb58p=%0m(8Z0 zg3dhXul4$Y(Ylp-v;VEq!&6~#SaD(D*~fiw&!YxER?#}MHIf{6Hsztc^b`K{J@|eYtAbw-NsMhjw;AP|FV7QSD!hl&5afE&} z5=}0r9blY;A($rU_vKbW0iN1Ujijv1tM$`kDqbEwRZzk2w!G@2#PrO=v7CGht9BP* z)rf*qn)L~qEdGf?zI@)0?P`@v*jtc@Go%DO>#C+pZ*OjGDJv__%+DGb80qCn(Zyqm zg+%JKov7<%oWV(U@I-w>+mja8_D;fFkjZxw7pup$xqc1x6kpVc1APW#z!^EC<=k(jCe4{|NVX=Mz#84$mwR&HR0s)6f{c>o>RJzZPsc9GCp&Z%N+y| zhK7dDRhzh;Pf3!d&%wO;NKU?cgH&33Q~Sf=wJ2rm&!5`ghZoz|Z-UlgM+^Q)Xf@Y6 zwxjw2s-hmNgNKS(Z?%P*KO(lFuDR{nhK)aWx;1o^A_h~~kyu~XUyV8DszVFqVbT3> zETSZe@u=`N==aNX8hGCHyO9%&NzBeX^Sj%rRRGomckk?Q!+fC9xq?YlcU)?_Fl>Q{rE`ZZE*C{mP%yL!G$W@eM6O`zcW+5@ zdX%tna7cm+zR^XAZcrhb7)w9R_rekRqUOT;BL7b<;E^ZQ>Z6i7>Tz?#N?933h@@n` z$n{>SdbWeJAABZ(eza+~w1nVuC^vYWKvFm-FG98(DwWu!J@oVafSj&uTb%(?j3@ES zw{td}e5`=`K!7#<45JDA^87Td%STNL0q{7IJW$}@1Jof3-+#|u4Hcjm^M$?fM;Em7 z2d~a&yE~;EBRz5U{y)^cRa9Kv_a*v8Ah^3b1b3HExVuYmf(Lg9!5xAJ3+}<)f;$8a zg$9=(!5w<@{l>p<_Z|1CyC3@IDXCGTa?aUj?={z4bIlcRAmZF;L(9PMj;#KuH$(qz zW2G1+__fkJMl35^uj`mH1H2^@xW~KMaletPSboo)i7sLscJK{pA!VEhPI|$RUz6Uw zdw(yuh7ry2r5ql{6=^L(UC)Ti{A4KDN$2)3Jte|JeSa4e+riX~3O`OGny2|^evM7U z#<`aLD`2;Lr%pFKv1H)SW=*)l-aK&(pVs*KE2?gT&A0iV9_oz+5>Ws9DV#la1djyz{*3Tp_QYgMmnBmeGK^QrS@$TKUr{FK-@HP-X z%su&R7H-{1UY-*kTjBm)77zYFD)+h$h)hl3fjna_~Uvocf8@vVhJ8ECo&c!zJJ4TVQVna*&KT+Y_oO=JGOt+4ZfI#@ z6e*%RS=}XG88?77W<*}Mmgfdd@O-Sbv-yEbc42P5AGI4zqD2dC`ZMgQ^ags87s-bG ztIyl370G5MeA=AZKkt71)ayL#IB#yNDeo^UI1TMg*4Cg-N%{SRt%1<+j*&AydAnv> zqM$h@nF+e*G;0g#Adg*RD5ZyX2mbO??u-X3xgn`TH&v|8rXgkjsM#twX!A2Zy;HyS zO6l-grt!KwJs_)+wX~@GrQltk>o*3u55+kA2^tJtj~1Xg%jO(7nCsH-`7V=kyhu(*xuILRD0Wu{=M3J zBK-BIXek#9X*09w;*A&_dA`+em^*j5k$jdxa^%rx{K}JA!tfvbx}9ch_CZrchq;n* zmOHRyzOYG9DW}*Btc)P)zGn^qx8E4C$YV8C4U4@)a;I#;3ooq(nSh`_?~RX=VU&?m05f;ZZp^yv`gn6MBO7Gb{hlQ0iH;_~utH1TMgzq{IW;?v7DbTXHa@bdbqPG4cj zBcGU@SXfvvI5Gk~)m*74^$^tXl(E(=Z6WG4sZP;~{QUVoF>wM;2lsIxHM3hT3F2GdddYT#{nuiIe8eoe3L=oJocD}LOC=vQdr*aPS*0A zu?9`Sh7;X8J2{c?e)!wb-a5OL$?w{Z>2|jSolZqTIht>F<{>7>a?%YCKN-aE^x#$t zRgTN}3RqA>d6&b589NLI-8*Fzyn!4B?BQ!TJ=sFb3pQ11Vt2_fXTYUek z7YYy`A63;aX$Wex$;mh;rTJLB1FMZybw)1V*+|NwVz16WXx?cAiT7iKw?Pc^^vTfh zBKl5Gzi!d=_lJap@Ii+e(f8l?(XC@{%%^}U^0Qpw&w%C3NS%)r1aLOt3k+Mk10oI9 z6|Gj`$w`$lJr53ke1u?h_nztLsZ*yjrps{X;@E)!jCm{hzn-Y}9Kb#zIG8=Gq`Y`^OwGb8CeIv5cd-77h# zIkz7g-6oi>kijZJg=2bP`|CULelTU%v@qTLd@mIxr4X2fW$<{r%Z4D4q|GRK8)^VI z@#7zAjR<gOosofuxA5u}I$hgL3puyTGpT3MDgJ0})Q9U>&c`pi6RY{o?j@}t9~!b{QDCC73PYyqFFuI$a#H;?uMlr4WV1SYYZ;h`!i zxxyu7d~v!w7>bCHjXJ504XCwWZ3!1`nVM3);Mgwre}o#uWz)vvbw2i|KN)tYGbVuH z^~gv_aq=eFk&qdU%PN|*H2mwzicF!9v-S0jQWndJ6@%F~r`A<9b)!Q=7U!5~KZTi( z{A6U_f@57A6YPJ=wY8&w&7UP|!*6`iUR@k5r%Xsu6tPoMic!P^A<_EdgSWJr@h2pF zGU5>#1)0k$R4@Zk0)g(E!8=`D1ih@Jr23QNi>F#irHS3t-70}bysfY-+(&~}wlp}>w?MZB>n9T=mc5ZZZ zP*nEQ5M6qREE=~-pPtTxR&PknbK%HDt`G^l2=v{r()SDVsSVOCdPflVCT)p2vS(v^)#BR@1e92FCTfarbx zBO}|v=2gJ&{);js@a7zPbUiLlv8&-h2om!$o!#vD)wHBr#Km`$SSGWpd&MHM0fFY=mX~JhBWRfVCMsrDvrcc05*Z%ZsnkpdkO`Yp^MBx8^Js|6h*jVTT)Bajep`@DFIrG1n0XnT{dP7#_ul^f}yVmue9N-;OpOe?JpMO*@N>aQ3Xzd0KPf+ z-Cv)6r6}v)QQ$fiiXW!bR+)*Pf1&Uhu}g#dY?|VjVCt2b4_ero=ytOzBLq>@+{IFc zAe^53DLz}Rj1@a^A@bt}SvbpD3PURG_g)z)#!@USt3i5ls2VfY9P=;B*3yJ@#NmUa zSZ4e|`sgpDCwLy!DX$f}%6!Db3M>A-oXJdTEBaxkVEhN$O&myb+PD3;u0AD=GR+qo z(GpmYj(t;?T!=`5)OZH2Z4T?^Gumr8sL~0gkR3`gl1d+{s zDWR*(1S{lzhWt`erXZ{bYaRAcU#}#PuBf$u>U-Uc1eG(F(?U|tVI(S=ii{p!p8d@W zi|xh;`6KMI5zo`o`%8;L_ku!|zULkJpbXYuWHVZxGM^n)oYf~m+C1?Gi<&T;#x@-{ zVgd!1M&FJGfe1wt8xV4t$Rw&5&M)ARtA|Hwr&2byB*vzo$tCfJQOi(=C6_aAnsQ@V zw3$V^B&t}au?HrpSoo5o28R`-!UjjPBPVp4a^xJ*$g;KKgO zP-P!JK#FXq;xk}6jM))9WojFyS1F{2Q)69ERIP25u;nA{kZzCON`^$*zz9lc*wc=@ zIacX7{TpxR_e!KSx>{m0J8_n6lY!(WgI`Gl|5b?ALr<8XM$$FXG2f#k9LS;(b}>Ek z_vmd+RgG;wwIy1aIZ_W0MHzPxSuy$PV1G`{)ugc{qU6D)(4nT&qzRf?m3TW$$KZ-V zjv7Q)RGuReW0^?BaL$p`z{&iMG!>3~(~h59oqKnop9meJP(0?6Wn^;1DMG2<8A>~Lyb+&yLmUr`; zRU&fazt0%3W_QE~Kl72^ASb&9D#Jg~OTcQQ-(@5_6=)3aKTO!_m@BqU*{JuJ%h5mD z__h9Jn~g?;5U@StfvyAwG|3Nl(w!(^#{;MIb z_7j!Gq?J`o^8PTviF?I({#4T@^``Ov=d^?~i$LHA5XIR+E9DPVg?VIC3WW6bJafsL zZct)1VVH>oE%LKoLFn_5uO=CrkRS<3eILLebV@yX2JiT#zf#a+)9qvg`}*p7o%Vk? zhX0Px2RCCk&2vnU^O+PMc zsBX9@sASW6Uh;fk>pWuxi45K0G9y|2UO3*Y6Hy`VcW)c939z&xre$3x?t>a5)t*C_ zRqnu$jA>wEcvUgPo;*$uFAgPvN7#Ju}m`_zuSjg&1!HVY5c-riJh0=q*Z)9cede-k6&E=!} z5==(p<&%&M8j+1KFwn2mzqyKcuv%`h^YBQ8!+LAQO!Fe!MK&Z+N=DY?6!+;jM;C8R}up1~g z_@5}5gqX<4SbvBN&WvKgQ{}u%O-dT-7~VNsY(zyt*#Et~A0e;j=*WifaKG>xw%%cT zPeWH%Rb3sprx}}>C1z!XDD3_ur=sTS8^!=QdTX)Hih=FRYgm{@tL0?NAC`-A(u|UU z*LQE`p?u%GXlQ7p^Q+5xTn^oO>MUz54EWv8PA(*zfo_<+bOyIUh=m1r`qJ%^Bx|gO zopc5|Jq;{~kAUXG{`(P=ZiDSMR6eJ8SXxQUoQQ~MQuqeuM%&}UVt}Mquj*snm-cp3 z6B9vj?#D_IGjq<$M9KdPui%h^nPtB}+AV^74rCZ%f>F^5P%Tfvtnd;iT{#%nJ zr{7)esW}qP4p@N608Mg&F1!Csal?hojP(9ZXwQ6=7gdnKKE-}umo!ZmpEOuSBu)-g z5r7|vj*V?~+G3{xyobf#)AJUWJvO%?15Idi{bd6V$wyR-qX`FQ^_yTiboKELUg@z+L^LgcOn(8P37k5D&Tbe zdvBVyz1?@lc>%ic#(g(z_?b;1ptr=4YxDI{#`8L7T^DpsnwpwYE@(AwM4t;2e|qq% z?suzgXgIOka=#q4s>4WLYEdA>B3QSh=y`L!)8QG(`xwS(qSpqKBE2w)wy<1P)mHbo{@Yb+sRUr!DH6cmoFo)t@nvNKFA+I{-~T3pPE`n$Wq0a{4=3^YP#N1D`{XWjyUQmLi3R^ z&897yuJJiNAK%c_0s=bx90opij62X>SXf#vYA=59Zw#6Io}b@4I0)zNu4HBtX9tl< zYeOQJOK-444+VTiM&{UfKgv+48f|Q?Rq6g0Ow7m#1i3Peq3$V!4Rei%(u^gdUe(b& zvfl9MNG3-5cOOF>C26HBB}p|<8;Wkp3~djDW`ZVH4#1rqq)~v-lqZ1S6w8L9lcyisZyYucp3tHmxyru@~2a|z{e?*l40~Q~j-_}M% z9#&lfzh2wcCM9^5c=r+3vw7*0!zX|2q$D`a`ED>{vDY~Dw}yA5O8vcp-Pl)8PtW>y z6{V$EF9S~7wRLyn#qcQ4E>7a5okQ`+*Iu?c$twfO;Ss#-3DkaKqK7R-$I4fHm z+cj=AHc}x@1BpGNdip1HC6er?kBX*}BKy0VdbOC4!P;2BYdb=GKA}m_C+X_yoN4^> zpemlJEXAbQCo7bSHSFuF(c`In-{Ar;7BGvOl$~-s+ve%kaEhLdp%V@|uI_e~n4ToW z1y`OD$!#=LRIKbwM~X?9oZQ?%jn+7k?LC{`2U8oc(^(gwivnaPfov>MONcPT0;Dz! zHvUiVg2;drS~SkmB$voWjDnV(K3A(!m?FN(V(YO{)l_5P3fc~DZ@rc5Ei4GeIDA>ant z#FDq{J?0rIk&=@Jr(9z+f|r|T3!GC-(vYeLE7G6NnEtu(Z1$^I%nSKN6K2%ZcnYRF z>cR?r8y9$dDq~t`h z#OfOL7-?|ekqu0jX&r;MZdQI{cu-Yy_(Q6!qY^MFwC%shbbqw01=|KDx+SNjjpLZG zqQ@^WRDyqNMr=;+anP!yrX@{6j+QG||Jll0PusKgC=!Vt%SuX{Y@8UmIvx-o5Wm`A z-T}mC>_tLS5Sl8_f20~NUKIats=F|tN{7x@mApjE8K;_qP0Ap&YE1F=N5K#+xe_ zF8cBAQyq?)7XMd+4|KhQl;G$^uE@eAAY^#+%-$2udikhC}V~p{{wFi|j8)7hx+k7A+)k;S}go z0^t@11YJ&*Epv0n>MQ78@vbxyjHZA6H?mGh(h?-5^jp;@0DwwT`F#%lu%+ix*fIRt`G3W*`>;Li(-~qb=LrF==-qPfeel zoy}u_0{Dc5p}dBMt!+6(We7)CZ*hKp!6dgPmqJKLfj-GvzoV?IH8ayrC7zS}(ojMI zMsI$&RRcSAKoR#}HoeMOHi|m6IBul+2Vg_pn2L>^oG=7xBO`6V@I^&MZaupTT~>d9 z!7iv%k`wa_3x+|!!N2x-#ZtL#VNnR8T6j1-c)D>#&-Al2h#410v!N3^Kv|6?Qy@rk zoTLStl9KXtcTCM4R8#AY$L%8S<;7okckby4Sn(T}VWR>ts}%(00`q*r@V-ko6c8pg z1UTXM=1UaP8?KI)&(6;gP>!lR&sGtFUv53zBFoFm(xr^BWq!@*4N)d1C+;)J``rB! z)6|sEX3?Ovu^>O6Yw6;(1UU?y7#a0z%-2;iQ9IUNJT z$k>>plVfy5R8>ub=r#=lgW7|&O((apapQQFP~aZ!UEAiyA29gV^Ri%bCNc2MrAaQp zFOPPwtV5M6bQ}16Z_VUgI17su(#b{Lmj z-N~t@y0^D`iXD_TKwKZx{*st6IIgd&S>tegw2k`H(V_q3c!kbVfoUMR{1sl z+RUK<&p_Mb_8v!3DV9bQj-YU-bINxhumRVOY)?v&g!2OI(R}iswaWP_V4=J5@M-i7 z^k?!c7+F|AyFRbm?P)>e$w@WWN~BSrHbuPFpbZN*A9b@>sA7eW4|?yxd^6B@D?NOv zh|bJIFf2V`&dK>Xs?N6_V%4l4!#lh@_G%gk)Zt2I@&bIa0ye~a{KmvEN*Kk&dNORc zAoJCoxZa(cdwby@KitJs2!;%Nu%yG1TtZe-k{cNr8yOns=HfC1o0Jm`93Ya^Z~00p zD@kcG4D>NE*VDOdzWd3Lp_S$(gQ?W_YMq-~P}}vOw-nV)xsVQK+lw<1FQPJ zNZgqr*66roIq4`ZyVifKn*WBeKGz9fJu`>&`1pGkh1(!`h>RQwKC>(fRhF27s9`3! z%(_HwGuhf6J`xVf$pEi=GYJWEko0J1j5)HsdKKvRkQmDh;u#Z0xKy-Pd$ZQrV6pV& ztdO_+EXiQ2&mKg)*PtVyD=vrfD-W_BN$`Jh0sgEUr-op2RC8+aG@jv(mslnz;W*4! zHC#5_pG_Qnz83_t$qSy9`>WFeleC%fIgH&WP+s=f3I30&{Z-RV(007_t~dA=ZFa z7B|AMaPuZHWds_~Y>kLMV<=r+j)yojIQp(4h=N=;e=67Q@a23wuwVbAB6wNiwyb;> z5pn8$Wr7C|cH~e12CBiag#Zs9UdFB3kAR9=+fYNoV^2|dyxbXj!Yk>N^(2A?xJ62aH6eBhQ9GXx7sE~fh@8oBm;Hf}YZmpgF%-zu}bF`SihjN3T0c~9Qp z=K5MvJcuLqMgJt8hDrDu3VbS=%Fmlmf%niSWh8Do%D8jFZe#@f9}E}AcOEoK7sard zAI0McngW#xhiSEEl~QN#=-}ND4=LZ%w4KKHdg`D!+D{-*6kl`wF`?jA8iXtQEyGh6 zOHU$OQ~yySZ#i11(sa9Mr8L7@4vt(27Tgp8s+sJypm3j31{t%cM^NE3cMr#GX>9rP z|IPMqy`*=Lh*F`cmpv%2{IeM+F+tU(+=Nlt{=Ziz^Y1r?8YJ9A5yYBILx+BT0*pDV zo`#eiJo%=~GeL!sT!_+FaPC;a53zoOtlfGKwbiiu_e#T`LmIeweiATX4=P8+IrW#h z8v4}UQP}MK&vaeR9;cs1z>%hT^Flqs%rPU0;E($TR-UIog z(pEAZp{9hG;Sz=_ArO@uzu8~4Bkh|uOa_ZZ7RwV%vZjd0*AS(FxqiuQ#}ao0b3R*g zcI~mu#i8Md4OM$hO$~ZE@xqunLwwH@!|}shjLPI>xw0{Q<(%TCY@a-NW{w2ybXi8V zn&bM9@bW_uCF<(Dx;&9eon;l>(r5F!Eh|bLI?5RZ%r4b-#x{E)vL>#!;i0B~GCr#d z$N^)F)1au2u4C;P4=QK#iHozVE6Tb}u$gTwZy7H$yyVjP?o4y5c#CdrAp!FPLR4Hk z9o2)3N?&!fGot%h-ZIp1)+>!ixw)T5M|J9#EFjWI;uwy^=9=k^Wn|=mfFgQ66?jb(9l2!ttVs0 zx9+DABxVZR(YjW~CbZm~p3P@LK-yQAaYvVNJ&en=^qGpH)-RKGS+3R4C*S%D%Z%c_vFM#0A#E8+9t=I@sk;*g=ASLy&igI9ti8%A3e|!ZxbeLHVG1*-(PWm*QTH9v~27-peh5g9nHLR!@F7>_e9W`+8rG`ygnYy5t&Uz{`ELNqjzHE z?)AFOjcGruBDgG1;jG|~B>C*Q)4&j%J4#09@gFE3lYrawBn#^kLuL^lv`%_B>PKBc z+D5FNr8@oQpl!X)BBV!ftPuA%myZ!g_idNxut~GXnBSx| z!$+}eP&(C~8UKl685?~o-zT2J9jJBeEy6^LgRWh~{R$n99&xdw#cP!!$en5VGrF8X zq+oXk10Uu7{46ru0Zz>F--+h;=!gqC&Au1EQi0?IsSd{L%9yeWHlR*Z(OCCMQ4MXE z>}yGRTpnvHnfcw&F&ce48mF$1B|~gne$?P0ZAahvVr8X9%4)Me3b{oNBW@aB-ItoM z0loGs-`Cn9kds^EqS;-KHzGWu11cCtkcS8wALf;QM)O=*50`FkFa9k zka9NF3xaw1N=$&^D(qU$9;HJvgobU1=WnU>2coH4Y&Smi6I?V1xwjcb^{6t8_Fl82 zEzxC_a`Ry!2O;WXoa9GAOd(YVv_7fmD_F7-Y`I86Vv!TJir&-RGu=%{!p z`q&OL5j*Rq30UliNSi+#YsqVn;}+jLV4KixN$H^}zf_qoaBj+E9?K^~<@@{VmHvEJY=qX|_^nY3b$}(mX`?{WoPg^eY91ZbHCNZi z@P(dxFworGW=(e*c?CG@{d>}!7r^G13=S*sd%AK=5oYS5ZlCv`XYg9K;v^Sd!V113 zjg6~Y(#A3|t0>U(MrtP?UVh? z)oZbSjL7bF+vsD7eFhGeuF5p0xRK5q#5_ZvN=f5C76x&dQw`Q;r9^gu*a&=@%Tr80 z$GSp23F2(e+>np(Sx*aMFp+~Xh@DO5*_ba-tZnG4(GaOXvL)E_+*1=rFIj!Ic{LsC zLyGTT@{RXyBYb!it^3e3F5caem!y4etYr~-=0w`yL_O9PQ&l!m zJstOCbHwT#|7Kf$2tA7@+CWT^Ur{3)yY(BvvXWVuShQ?B^CKr`8tobu8(55ey!*A= z5gB3DT$08%RpayT;{1ooIIqRa58zwiv)q|`7$W?ywi8eCk68VEsROVZAK(zJqk_p{ zf*o6ityjapMLQ`XwP|RuyHa2f%s#3zy!OYl^V#(FM0!ugny8hypEWknzA^Mi&l-!{ zwk(&X_WhmtM9G#+TDc&28KBNu8V6Tga!aZe^(AW!BvZquViC8&b4bs&DfQ>iz*|A$oUk^7ne=7~Q02S#W?LK(Imo zld|~#iVx&}piOB@M^R;seT7FxPDsG0t*uotF)>*`IKUt#1_L{TMSvWV3S$EZ0p-tb zU=0n&#zy5q^Z7i+hppw(hygX0s;VkhCawZWPZeY1($b&*3M_|4M+Zm7`m+0h$2E_Z zgoMOsdny4o2Rply^OdsV+tM268fT{(sFRHiA)%h0y!zWxP`z<-axU#0nVf{j?r?uH z43{rtHnaJHMyjQ&8zEw6XID{fH*=xRQuSZNk0y(HWMni*(Vvs(nOESE$G^Mhyc~lM zql(Cv$b|5d3ZN5U5*z5MF67-IhWS$DQF!{S{0I>HZt%bpuL3JC7K(z4fQ*h*Z?(uQ zeDnycTvg_C3wU}qT6y_!WeQ+|VM^Z0*nXI*T>a6TALP_bG+i3Uq*II8pu-1!>%Yq?mzC*0y19YI zz|ndeEqo15Zl}!;o)Qw8Zjz1mLdQ$Zo|gysZYsqx#wtK;EFl@PF%gs50_q?I1x3~1 z)_+PfHvB8gWEKzA)SP3|Yk|kG2mtwKi0ksfRG<5H7D1%Ci%fWP1+nw>{T$5bg zyZRE7((a=bLyUKb(W5w6^FAvjaQ~cLA z*|-k4Ul(>LP0y9&Oi_RZTBsM`SM#m!sj`-D4jg-qes|Q56*u30C1^`T@SW z@%TrG^5FtZVq&uWN?U-+R<2~U*T()VLoxkba?|`BCO2+@RM80r>6hn0?_DbXb?a%OvW1&g3-CP_@mWGi)%q~Ka!&(P&p{wuV>@p!Vd2R z`pA9g>zAU0Pi`s98Q{gq-^&5j5X$(pcw-Zbxe7g0DXD?o-9ZjZ#hR_PHFpMjhFABm z1Y8fCjz+CO7>5K=;wgAnU&&=g%ueIF>Zh!+YJ0c?D#i({a_C5b%m@r;sr{V8`QDk^cyE;^Ld9`kK1&LFs{@7T=lb!jh7-^hiAmHyX)@+&s$C z*rR8)!8=gSrUJ}-(95-wgn^Svg~f(;?+cyQBc;oNrlzLP_xI^M-%BhTYU{6TmbW*D zO!1UK9S~o@O|ZW&!qr0U5)iH4WT7M{v&+011$G3tUDeJdSLe9gq{Y^|N^@PQpf|~BmLa~ceH>TJ{Pd5dw-g92^`+$7%x01RQMgL9wO~yUMr` zQv5&+h%{vn;^lF2a#}aq)HkBTj9TaEw3gG8ljtxw%O){qaYhVE$dskUW*L=af6bTm zTOS{L-Y{-np>vuZqEGD%$h{c~t^hKQUP7Jo!mRw+|Bpyjy|DgN8z zp?|i#obef&k-B#Ltg}vP7 zkd{m#Z-f&yb90z~_vLf(1sSl@vz?vq2qUt=R>cu|A^Qvj{*xm583BG*HPVkBt0JANx|oa$>;xybuym1Ybr|d{o+Eumz9&fBR8A*TG8i z{2uPOZ04do`;xLvkrDn_Z}*W80_}&##e#@R8VFjf6H$&E0{}P_N|I+!kP4eCd&4X=UWAP`Zrx`LKSZuiHfed>od=L76% zuou_2r@ieJLoqvv z+7?&%VQv&!ePt>nB-DAoAKBZR-xC`1J3BP=uErsSO=*_UboJWiA!NEpkz)cwzW@Uh z(*}zkD?$d72Ba%)n#uL`_0iF;*4Cw&6-;UxNoh$1cT5K>Q(aXgB01*P)>Gdz zMb1)%=KOXyH_aRzCT7Hh7OP4oJs52cGH%QKMP@u06ZoH{5(WTo09D0QsYY3?<+yeF zPEtT6c+@{MZCrBD&gLE&d#9l4iSp z9e#k(@w6teDF=q%9y4eGfZW~PEqr(GoVa1=1A6?!=z&Gg`^(vy&!1mLW3%$`JekhQ z1}GbunAEtYvu9Sh{%J4}NP^;$^!U_WQ$97=O=R6w5?0xTDZje=)A(Mi>P{{Xo58lM-g#hJaEvn?_Xgv0JD9(_PZP>EC2FTLC4{^95Gg+`{l;f_Oqi! z0rw+FXSd6?LKYNmslYb}xw$GKfrq~wmoa+(c&=6n_0~hm< zwH{6^Wn|#S>TS+?m~erjvnUL)c(-F9hsJ>ufx^p(@EMz-_ekxZTvsitSQit5?rqOq6`QGl53w)(zTt-0?sXG75{dJHgxqDw|_t zu9u@w-o{egR->+aps4EP_SyWN*_z_hA}*E0X`j;Ac-1R|ne12Xa+J{woLZJ!+{M;n z4h425^w5a0+t(^P*-re=o?;EU7iMivEWsSB@6P*u>zgqNkGSuBn$1p%HfUoStE%4R zfDtu>ru{zmYXhsycPLscrlzJLkszcoFfaf^mA|d-hX=M5cICC~Njh}Z+B12US9W?0 z;~8va75yhhqTh77PgwJY4;R=N6Yqo+*>8&j`6Z{ z@zT95=c}#L_*PsDcdYw3Jps4O8sMLklXD>F=jXTBV4KeG`n9qWbrHF$vp>qybg~<@ zP$@IuQvH}Ex(XAWIM;y;Hj~XJB!=$~6csez2UwE|di?DUjy#~Ln&eA{)<{IcoZ;M` zZ=6D%EovW>DWmNM>8?IERedwMPv1a{j0i#UO=6mvFT4C9W1O(fjG5v8wud(RXK6&5+Au0aoW*Ml%!sQix16a%9KJ z4Q}?~Vypks{0SVhR4mDuRVyCTKu!O6iCrt2Ca^d`CgorMQ}Et>Zwg;DC&sUs)^313 zPf*&B1^%f(aFUC0~UXAlSX_emFuE@$EEUOs=b|OQ2P>PG=GlBEE{0 zkr@J_NZ_fZoXrkoH9b8&j;o^0XDIZDNxQ3|j*9QkZz_;moM!ggm`Mso7C-jf86Jg@43SmKl+Gbxl% zitZoExUKWxP}}H0+cDA4AR;E_vr7TvoSdAbhm2bNt{(?%XFc$pICw8ol9NF4X#8@l zk2&BWp*^} z&1+adsz1=X)I(#0TJxt{1HXQFbg;bRwJi!l{E+m7^uX(Fshrl(R(Z|O%b`eUCXsWq z1P!oJTI&YIu&p2CaAcsMVQr&2rH8QlH&+8mdm;Lry2BeMtQ;+zMZy!sr%G_)x2U_Jie=xiam=kvA`Bz)-o9lE zXBv1JFVaKqw%=Rw`}gGn0gmshLuaF>7cPGJ!C>~yxN9pX$q0c!QNy7s9es2(HAOYt zoAED7!hPPZ=VC%u0?GIqBH}P8r2J7oF zjz9fTFDm{uhuSolVN$)NYQ-iH5SgI-H!Kj2r{!kC8Nr=(Z}N$s zRN%g6<6Wt3tFEry3xqD%6n2GvCDN`h`)*u%tA3G;Gd}MBDZsPraxqrPd1v(FBb2$V zeRoX;AD6IDaX#8yorw|+0pGk-shnB7HC)^zBvRfir@VrQ=m3y3ACu~f zg+8PKj};Cg{O~?$jB>qB&6_u|LnC9RO+7!}2;>~Wn#|hW-geGaN49%yl^#)7F$I=U z-iC-%Omgc5p7$)U)@iBd9~nr@dZ%QeS$}&YT>R;;D9rhB=#Y#IoH$leQW$UibuY68 z@1^ID7GNyT`~<4%wnJNTDe<`XHz`}dBMsPdJ+!YmxOpfp>ra`yU0fg_1PgH&Oe`c` zYxF7hkDH^0ck9xph0*`w0^-=da+*&iSEPf{5)Jk%Uia7V+m0sQ?DU^PFa!^V!0x;_ zaEeCddSMm8`m!&^d^bc-FN2*XN=>+l)oDgrOA9wd;BuPhK%qhZd)pnzJD#+g5gM~% zeJb^taS6jonWaW)Q0uzz(S~ly%Wbz+IPGy)VkX(#VD)uBMjMmbl@x7mCa$n_3@Uni zcokR1cy;?$m6W_aRccDjK`yh**%vcZ383m~;u+vTMzHhP9)%uvJ{V5i%z|0Ct45~i z(FFOVbA<;6Ca%XAdq)Ph)19Og4}pIRs;cPt`0&N5590WXgi#%yRs{w_9_#LDgNSWo zlgeT$HT>`Qwze)#Xi0-tku9^2(S>|)R(>k?HZ=5yNUSQqx&45{J7#;U7lR zd4De_&LO?-cRl$ASkq_@{80*d+$pAsfjx!uB;(;h)%vrat;^2F)wh?bB1AIT;ftn5 z3(!Dsy$CDoxBR--C$D7zQ^@bWXZ!7RrQ1C-BcrbU%1~bZ?i*RYF%?GM_vIFoCE0<9 zkGaPS3OD~GfS3d0Aj-Aoq0JEi7I&atG(3#@`Z>Lmt$fu#byA&dQ9n!j~!0I z@x5@3wFxxxD~;}}cabaa>%EfXHHpwktW3HORX*S-Zaup}`5rEC*+3@)4j0{g&QC(q zxgEsW@4JH$_iZfgfFO$Bi7N4dCO@6W{^-H7G6wqr#L_-F98^N#I`IBZuco`0adF!J zs?ckUB@t?Hd(cIq4_ymlDR74n&$zQ_zsUo$oK(?AN`; zJGefLEFkz5%=mI;st9y_`Bs?$vHpeJ7;xE{8^s)%RMC*UAX7|&bQOOmZ6eygMnmf- z?pMMK{U**=gXslFtwy6{P8tlY^6P1W-@s{WC*wS)s}V1rt*J~l)`#V}!mGb|TqMQI zjKk)2`TG-9X{E>cC~w}s0m*Z zfV*XKYHARpIc6VjrJhZef9G4QgK51$uicdnt(9*kZ|z9XcUEV;n`d4*Wr}3)J{2*px}B>tT3Y% zO@*ho+i}-BTD|WTq;AKPzJHSKyk;Oh)#dU3?n2yyRVJp|7gHBO>N_8HGBVT0XOd!L z9mK_XiyG&(`DQGm`G4DL+%;P%qEY{*?-P#832F7uh;;SYH9+3cUqY;D9ElCW3X`Xq zwp5H>493oBh~29l2D91w!o5ef->h%1F9B=<(I9`!3r3&^mQ}x{6+p^#S3< z60;(!U$n<#z%?hWLkA(rdHlr`jVryh z!gi}tyvtYnxZ@tZs8Q0*Ssb4y*>P>+pkL}Jv)E*u*FsdZT|_9tHoR?p{Vg9NQK{n@ z^*9tY@GP3mM?Axu;TZotv%@v@_b*!Pzz1tdJLIxz2f=F466G7Bm<`pfw|3q98xI?s zjmHcQx5ygh&FT;GI&`W^$;rtyg*4^`<_C=?7|98>YMK^vYN!G(#R7E2|3Y7GoFyf6{O)1YBm_)Wp=LlVb%z z5{JECa!Q$kLs{1EiBIY6^&IJYp=;0CYG>0B&gp+8>0cuv5RlM(u1`Xz3*`sO49D8H z$I=BXFZDpzPt0ihuZFL?ewDIWi{~uh!oTdg3PoqPJK}h(2vTfV7{2@#* zC&tFwV*ha2>Gs?z38C*#p5d>C*8%TX{kz)Y;^KyfpJx+2C#eXXAT_tNv}`-S8^#9B z7oY+*A^f1f(a+Dz>r;Jx_UaBbhUh}qMg5$V;YnIn)}pSquCBM6n1~p7DL32fFee%v zmXuiOHQM&M=sh*46)L82*u*q6hPohmK15G~x(-q&VTaQesWxR1_rY?jR_mvKa+H4x zH$9|HTWUU>(EeIY0yMXu!&+;Fk8aLOJ#L3)pfJ|6w{*Ay4!Gd_b3dG~Hyu!HP@d>e z@x4(TSA6etbK`x?I66A2|M&89915{A=#PK@29?OnGWBfkpu0O4_G^n(+qI9El4Bbd0-3$yN-Q5EN z?>XCj-_L&TH`ec6>;3EZ*WOFl-kUvMbDih+_d|NMvV0s`RmF@=b$*J@k%l`w1bP5 z{j#$Wf+$_-LtP?vguOY>o^5kFCBtmPA=^vNOI=?_^Tv%c&A4)J8h4HS68t6rnF?i! z7k8F3FrdSoiz_wNujQ(@-SM`j$XHUAx($lbLKz8tt}4v>FDqe4+nDxTOOXa%{`_{- zcLJmwkf-&PAA~=r(h;fH(J%7=Nua)W@Ob(7WLqv_4)<|=jx}+`9GqH%=0N{XW07Xu z=k$%rX-nGf&>!Lvn=ya%B?k}odhPULpJ3rnwQk8%duAJ?X%iMI!{Pnh2{}Oe6qDLtXB4ei&#dVuW3iZI0wL2%c zcSJkT-!nqYOhG&!%4OO^3Es5t-;{!}|NV88N>Fg{7bZ58;o6iCs5SNkgI0TX-wnZq z;t)bI3^q1)F8c*!*x3OI)t~t|_NT=0;u9yhvOIIvvoB1Xm#bd@3%#&dSdfdczgzfS zu@MBRsFKc)_(~;?30xcrFkRYSUai65eY(Q6RWXJnwZOVtNUr0h4aj z#YF{Vg5P8kbvKTvSTrx+j8Z zKp#;wr6=ZbZ0F?E>V1aHGpU-@M<_3(W#&HpFI7pS9{teDQABWt;p9PlEc_bK7whKy zhaoYcDO|X74HZHJ>}gFB+WX{Cj&=?aD7FC&O?TVtwbMN!AiG7T6+ICi75~O}d zs5_q#0U0qyv#LsUM`MY15l3xitLtx=Uw!#yTsj-y%P~AMlIdlKKeFPqxozs?8+k`#kEDynI#~Dn76RF zR&n^f1iszo$teCRuxt%S)_PQR8$Vp>OO~#kbDu@dDP6doac+ziwkW2_N8|MtYv}1o zG&0DCr{Zcp8Dd;l$*MYa%@-A5KDnqq=yY4voF+o(VaS{ICVI_X_|MPJ8-eg`7_RW* z2h{m@+1E}gM?D}1AGzxrVHf+LEBMnVQvu0^v|D?7dvkMLD1W&(w)vNc?;uu9(Wu1| zp*NXo3%0pNbX^OI9*zby#*>j;?YaFawW&57OE6DH;$DAZ4}ImxZRfw9Ah$#{J0E|s zPNkj%5xr{N?QMWKm|^p_ZBKjT+Kzt@i774p8c3M_`uNdPRDWRq0|3Uq#I->-BHbSm zZpL!3JtoG$hVT1aPKZFKz;H1Y6c(n|ADl39<8$pq4Bx>a@*V29SEPZv+;XG9sTiQ2OtQYY-WgGx!K$n z^S$5bzq9~p)%_+Jm@2)S41|xPLP4ulDMip*N;jNUK;(mB(o+^=!DF1(SG89Y{$xwt z{f)*Mj?^QN?02N}Le6yj%8Z0SV~NrX_54|LWHq6+v?PA&ywc!xj_vJS9F;4{_0x}0 zGHh~w=)1j<{|(*oAm-{uu3JoGF>WJ^ zLd3q6C(&CPd9aC;>8ffPGxF!w!_m3!N}|6G41Ccm7@?|cqg||L{uE0L^7SO%Cmc6$ z6WzI9wD-CFkr*1<5pDB#Q&yXDFju@5?>txRw)@bkfYV12KrelflXt`6jFwenO}tcq zxY|-T@A8yLnxEEZU*&XqmsO+CJn_v!`;**m37d|u6Kqii=qR+&9ElBM7Q$cMOPjn; zX!U#!zJ7SR+xfD=YvcH$8fzev;T@&8)mV>E>Ff}HRs}z?*o*JL^lK>^n2ER4=S7xU zW*N1|#+60+p5*Mjn=v&OD^z};@Q*Ltk6F;?LF@do!)z&9DW z17AE+*wU|OzZ4c95NxckH@Ew988n~Xyo*~B!S8c6N{a1*wAB!^n9>tgm1qE|?&ZB> zK2Ih50*Kll(VkHVSu#=t&J`ZR^0$apDCzl}Q#lDkoQyYPnLWg|pEtD)mv0o$RgCGgrhC1ruC5+y zHgi#JEkzUOn1~s*Ek`^G!ojgtu4<#Ye)Y*&)*#u9Uc7j*!jXidI-`AB_~i>x3pEL% z!E1oQY-751Jhka20h|s6q!k8!B?_HLrfXf~71BJMHE;CQ zfiQazYhENlb)Pw!mgAC;1T#Ivyi&ej`mR}`s z_ioP-DN?j`w1K2a*+U)=_O!6TgpAWxz46)QbDFhdAvGtv)9JZa)0Y& z7C4l+bwtNdYBw#jvvUo>&t2Y83PPm?Ajpxyq6ix9eLiRR(bkdZdQ*l^kcsq%Ed*Ix zLuu`M;QfdCF0HTs9(kJU%|+jcNSDPrN-@9AkMJ8CTVYF6*)n>Ic$8k@cY4cs9NZu6 zPW*4udgyh0qFD4iEknm~3Q3@>vk{(uHj%=oChk?A3g*(0x{R~e~TxRV2fE?>k>gN*3JPvO0i;6O5W}B zu$A|dx1KI}Subo%?S8yoZ8VOX_;QAd2RCZ$7$PE$JZ*WIAV8l4l7sz6MpPkcvJRBe zgjMMa^+0$@Gwxc<**thb22B+3p~2)C;&Ga4larJ4Dly)J3v|$j5rabZy=K`XManT- zG691r^rkX^rY-4vdzeYIG~*L+0#z>e@)2W>R=Ys#?q67(ML>_CU^DRJ$3>-X&?6M0 zJTK`i54ZN_OBi%|avlU?KU)Jj2>CXI?7zzz{qK$lYfe_*d1aZGf{1@w&Py_*cU4TN z2E*?alCOS`2xwv$`+#-AQX`v$^S%?X%9lt6l@-g@yPw1hLomM`gS7=;A=dTlQ2P{d zui`n)lDN3)3N3>^6Fv48LB94-F2j9Pr36=Q;0mbyz1*;ETwGp!R|~}viB59sI8LJV zK5QD>(pLUx)9`l}z<&%djlZ-qRvce&KTHx`31f$5qql=zYFwuk(r;&*{md z(v=?flX@2}6mnGn&}%1`=_eWm?;d`+5B?4R^0c0ksT&3-atSwmpEf+wDi*=tSaWUo}!ac3Uv;5sxM*mIL%ViBk7L7 zpLs3Q?90Zqq^?sSo^muoa#h{Ph`ZsSyGR1h&6%YFOL3sIqMXuTLO?G?&|WH~XtAm7 zS8J;u3geJr)Vu}AG7JXCK$QVc$SCbF)Y(yY@7y-y9c6(xuCzcVx$%?w2AmIXX3g<| z6eK%PXL7*M@U!@sDOKGZG3k|>jM}x|`!mZYET5If{nXZ2VIzo!L zE-t|{uy?#cDkkun8&^=sx{vsx`#t@2r?ecH9H54np7#R{M(nz>$J4CW z-;aBcdV(8{-Yd^hR2vKyaLv&bq|ohKe5Ms(_&GWl^IMM~JYnG=vm-d?*;|Xa?WT+sMbruDon;->{^w?< z#^OK9W=mTpCu{QSUT9_RBt3m24b-j9(%Y2@M`%=ty5iC;2^pEo6{eDmIb1d3<6|%t z=*3(7NZF^6*bO?sypKj){y!$YMb_9kJCpOe;sdxSGY1&EB5QW7wgfy|xsE{A7qC@X zu6m?lki-u!Ew}|EOv5k+bDo6(g%Ff%59v*s2{Q{_%+qF^?V*S<2JhEMOUms z4<9~ArS1yErJVe4>tKdM162uz*$gwh<1irpgntTcl>rjeg&Wp44iwO9Ue{~N=z{{6 zKjRa=9}YS4v9|bpIf0N+HGmoC_sBpEE=@{Sfgu?9mEuL$zyR5Be(bSzV-kmKfEM^Y zs*JM2MJjg^xgkGNQfvbwanVL_Gc;`c>k$i!Dhn|=d7N9d+wHS6)8%CrnfDHh3g)k0 z?}JjHovDU}jTPT!EK-g#s});gW4|?Um?epLgsY&I@1Dz^%_hdy$Q%b5CeC{Z%bJ)^f_x?HHNY@0{_y^!ASeyWa zB;9jE1_taE*l?_V%a4y2A3;m(ann{oetx2EqV04|fzds%)7`{ti~05q(;qLO6GoL& z7OR3%qB3ODy^l|M|9&p3a;N%xmCJBVZjm&d{P%J*z&Q9;(#%*Y8XpzZW(d_{B&4G; z%<4}P8ULYLHj`si1ouR&xV9dC4!qCPQWo ztb>5R9ba~0a3lXmhS8$dD#TFY`h`Wz|MOq0`l=<5C~B2)B2~H!#D| z1&)l(UagN>S^ok{Qrtiqty=Fe7>t`~w|}%(4PjVmJ%c=W)yVZ?^xI4AQ%~JM1OT(c zsqX2PLXUp1CxFzXR!Cw%zfV7kEq)3CO^3Rcv<8{RyL=ceiVWR zchK-ARNvO#l_qXwOin>zh|V_oWMK|2bYLMv!@bFO4C+PjO%3W^orqeJ*=6nsKJdf; zp%n98SjXHlKPnk)c_eTyZ+(`bKLf{cEEf+CqQ+!{!H5B!XpSZY7ayU&-yPYRnV?Wf zqD973j!9M&V1BW{ve0Sd$K90K$_V&Gg$(J4FGOf7!T&EJn*W(Dy+}?UMTSEuKGM?z z$h}*j7kG0d%7Q{iSN9GMP3vC|rg#d+4?eNd4u_lR^-A%H)U_2p$O%HI`^q*o6bkJj zTKNmJ)A=C@nW(6a)8JVaGBdN4us*dTC5>747jr*$UDr5F1`<}KZ3=~diYGESI42m{ z56;jGznqkgK>;#$NH#>*y8smsLB>QF400kRCwGGj6zQXY&KuIJ&5x?LXaUl3UZZM; zttfj@;a~4S5dWRg1LG~BzX)f=%hKTEwxm-zrs~ip$Z=_ovNW6$o{x-u7aN*3? zj}D?i4iy#7-Wkd}i z3O@Q^yAjKL|J2n*(4r?Tpb6Wr9M0;S0vAuOHT%P`m5_ELAug7I-ce_G`0?beI(OSxpjo#CG0=82lRhFQsLPEWO7~^)( zT!jtd<7aRy-kPiqQArAqI%!Y6Mp8RpISYd~a2_4)SsV>VbniXhh9@Q^`RMJ5{6LgJ zc+eU%0jF+V8FZ^A^bi|52Wm$_YnG0VP93zj227!=<{vcfG(0iiefs9jFpw25U=C`3 z#WL~P#C$CuZT8=BtK0{8F={O^g`OcpSy29QLfF5wfR%y?H@7oES)kJEhX5tMi6iY8 znr64yIsR_@sw6C|7c`AEg10|^qHg{1Dnf~-rEI?e<@-f+oKwHX92wk1^~c)Epx`k#K#A? zaY_G|+TYIZ1zE(%DLWl#k_Ht~M@{Fnpyfh!f1~JI{nXC)dqhn7cX-z-XGJkG(IDU^ zE*P!MVcw^79XA#URfKYRM9)7>;_YANBm z`rp)GfY~-!&L`Kcb#ANJ0XP-t{S1YwVYYc-&B~gIdm#^VB`(=OJBT$W7 zOR%xAHUG>DE9fnxu7z&Ga|)`yYQJr$A8%7a z&|$=CP-W03|AOJsqqw=DfJtaC5PNaFo$X#5eiqIgmNTME-xsu#l9IAB6$H@_xf1H5 zuH#ts@1EjG=plMYPELON>~_yVC>q#tx}0)LypbD& zP0c?t_{(A_^H+!j(iqvZRu!4bwW&lFAOR;AcHXACPyQ?WiKbC|9Sl^}fv@E5@i7fo zs)UFyxI-o+Y;4Ar5;*k63h5$pLq2_4?$<89$V%bi;o<&+%o#GjOg`Kyv7PlguZLdW zg160ETNzq*H4JttgdXr^d^#4tn&(NB?g*ofxMp~`I=JqSn3(8`6+ighB>g7qr9oLs zFeT!A;&Z=gf1)p40^l8-dh+dSN!Vh2t}apI0TEN>jwhdA(T0ASH=F`00$8)NzpcBa zp>Abm1wr^@SdVOEh+pm+0gjB0V8<|~col^=@SrN69D`II!9MD4Fw9zCLd;)H?8`S+S&=$&iY3V0ioK`d-+ zk^=+p-hqCFVQfe1>SAF=!}@^yUOWSo^H}jI(&EGJBITfs4rW@;rd#wD6;q%P8SxDQ zLZuFljwDr^#@0(Y#ifn;o%ea;UIcXRNVaIboqq_WfV8a~lpo=Q4V*A3#TOrje0N;w zIojJ>8>^?R<=Ete-udw1XY-JhHl?LDsiENuPF*B027Pze{f|2yXRR)m)p3u{@dc01 zwx#I&tZD z<3;T;k?aBWWZ{r(`*A@bkuWgu_{xG#+Ll)Zh_b=@dlZOb$Cy407d!DpaY#Tx0S<29 zx-CfkN0)K}*w|^x9n7Wn)(v1WaPuAcK>k~v7r*uN^&cE0q7_0}P#awJI+v3_bbT%Y zX!Hx;KxrwiKvjdcI7^+~+>HPsP-Fv0NyjWlvwrSe$L-NP4X_P&baZ@`BpfD?OX_d| zx7T5apu%l?6oB)<@(l}d)pHXwlR4p}TzdA;xg;AQZ8OzG-y$RJ=1+gEhZHnTw@|N4 zfqm%j#1O>Z#&}Vlo|KpJhn7Ixg~LE$b8RUQlthSKHQV;=3MhsVbq*sl9ICdn4JhzS zN7xgBiJ;l1NVhiJUtU&ry2iQI%gzJDC}pcYAlEmw34fTcU5h(vIvegpL6KSVI_(|b z6MU9OY%J0DyWz`*wudk{DA9x6gTHmLnKd{e{hjE&xJwY1e~UmGQPCCB(HL{Z$IoxG zG>plOx&1qWR;&=h^X1EDsw661okY%eb$)&%g%Ef*i@ZACMsI(2k+`F}ItgZ!!e1bx z)@SyJkcf`umE}eH7xGd{)s0irQSYSsuGvo1d%UvR@=({F9l)4t`|49PRZkh-cDaj+ zaE+-D;;TrjaEAGqni`L3Du9cmYn6kpASQfED*ZP1L-*TAI)}JWrN1?QF6Q*g>}+%B zy-CK0%yhbe@a>V{H%+6$W?SB;2m5*hKE<`t5q|Seio{n3@CIXw8Pe%9@Riog3n$M* zh!|yN^{TQ#s!Qi$4lUm>T%e5lH=JM<7F0hV{ZXXMtg6yrOLsU;`Yxl(&TMVU=g$r+ z#}GN@Fi4QrTopebcW-z1Ie>eDVDLCM&jw`{AU<(~Hw5isP=^$Fu^FY%zrJuBwVaAK zym6(JEci=w1WT~wT{{J=HxrHaHmb+)h18RaG` zSS2KUmUdX}DL)+?{UXY;yQ2~Kz`ePc*rTIJ%Q~v2`uX$ca1o=#S^Jy4>XZxwoDFcw zg9q!fLHUq4j83kx<;{7AwdU_2@hV$cl?JE2+pcx{hTQ!s47J*8Yb{sD8U|5O@E{8e z_-$Fdn`}B?<2taV6E|hbx1LB@l_HKhJ21UGy&_{)o(UMif?=-Q4D)X=9?U{s5-)F+o*seZUF;q=Pl}d4387<{bnjHh2)EOYn(l=u`-Wk#+|Mclk6go#$ z&H_R)O9d7cn?w@aYF&8y8O2}=^J^R&+08e&`=2m-cI}j+5^A+yIAOQQUR_7o876Tl zon>MXUKSQ|V9T5PT_s%k0N)2+*J#Sz6XIa}PGGea1n!ATFb_8Hx%7c`=KM_boK&^u zo=;fnyL`E-`b%U&5m##Jc~@B8BNG3yre%o-*cTDaDTa-ei0}DHQ%Tid!0$m9=G5P$fWte2mU{4SBO-%(o;4xcG zc5C+A1MU#fg69I>K>4pRE1X&BE|+i$P2#)XqBU$!Gj#%`l23W5H|3JI^`=~@#iZi(__;XDvmR3O-?Eo3ey%M7hZv# za)iVg%Y}{Lx%JYK)p(S%_TgN*JyAQ~snD()niB|?;=i#VvW{qojU~grqRg(V+||XN zFJ1z>M7(YMP?fra+`4FozSUI#W(2SQfB=XQU!Qx*#BAlk^@X{)LXK)uB2b{w*)@sz zi!xN8t(ArLMq9o927iHlyL=U}cgkafV!ZxpkXLu>)*XF!;Pa3RUrgKsK&OeVCk(c} z5-HeZl3^vVG|X7V{O+*nd_7b{U;mm?*PlHB5fNcIMB`w&*Q_u81M6KmTCklQZHnXv zc0saSqhT#JwhE6E8&`&p#eZE?EQu&7Ls(^YX6k8#?A~8z%cSJi^0P1^B08?Nr3Nc{ zm~+j_v!GY{C!`gm&QD(%b#o>*4rG|Q_=$k`x@^fCln!L8A&me@x1T{aB(-3q?@!$% zsp-2{1a{O$FmN}hvb(Hvznp|*U)AtR{kY5U1PlhErkj*>!n9dNs=DDg6a#VE73Mh} zraS60t6V|sc2m<;P+f#I{9G!Hj?!o@k~p{I<)pba2lYU$I#3kq6;G@=>o0~97G_&> z37%J2Aeh+s)KIU(8q)Tj>Z^9>2jVv6@Ke5alyjT8J?OETy zmmi$~zVdFR7l1UI;S&L2J_Z+mZxpay!0j^-mj*ePMH}gGb)2vhdHmBb0xedk$AwnA zEiBLY96!muUIf%`wyP!PL^A>-s34tXg9bsAfsN)i8xi-GTsTK752_QOtF{hL)0UB4t z@i-Lt^wRB)Qm!7(S)Sm8g|rp#4`y_X73yhgy(Wa76uMW8`V__qx-|oRGc2tKXq`DL zMbiObxPe)RlF;2~v4(5*@R;nY%7uWHYUop(@p0d~c%!H4nH2mNFRAY444*vsJVU{P^u2Fgkikaeu0>%lP37U08n!*U@`boudts zbid9{h1O_uxnti|ONvS?y4wN8>!tU|%9@`XP3UzuoFRd@{OY7OytuwxB(z-$m8Lc* zK;a3~so$0A2g80FIDsZCU9hPSr*1*XRQox}!N+ryb1g$STKRfdlMD_Y?k&(4r00!t z>&%m$XfpS5FS&$d`=EfkZ2xdwzC78>x$4#k2q`fNL*vmB$duU$+m8km@lxOA=#=s) z`VEini33~i_zRSI&CUvUe7wp6bo7d%=$wFs4+trG5_-2Zn_yH*V&NW$Q?9t{mI0yU%#;3YzF_Y_5uIJpyB{Ax)?~+ zFLa+V%7HB=9}kZaMf4xqGuANGTK|-p7(DR?C@Rk{>pBCetv9LS~4VC zB_dgj_fAxVk*@O(59X3$*71Dp7GC;=_K3W_FZB@%n%*RRDq(nDk{MHK5Vv&*->PEpFV{mz1q5>iI<98wEt}l)uHqTuiH+ArJ^52=T=+b z)CQWA#|3;3=z(6S(UMvN7nRGdpBg}ZG`F;LUp|bJP%j+X0~=7YBr!9R!XR*W(yE2( z%7O4b8Y-$f*S(Imwl?(2*kK7X0~tIIU`D5{vGU?#yV)5)2yV$6faCHXKw~oc#x;>L z{68a@Kae&5<@bb?yI0RDg5si|x71Nv0<&v2(qZaFsNq)lEux+J^LL(}H<;`=#qH;B zVJU{R-df^o@9y5%y7I^ZvqYbE>B0W~P2U^TR8-KdsdA%pTu8T*qvQE9!X%dE+b^c@ z6wO+kJlmMStd8iMH+6OK5U>KD_J+yyCuKuw%gVvf;46dnadd2ZfKpJG?I}r#7mA84 ziVzec+{@A0k&~0n;Aow1l@csuKx%a1A0GD^pK0YE9Cvce-}rtFOp{IAYyI%kthc;~ z`36^cTJmd)y)@rIv<6fYE`pTFLXM7(`1rYP5I=e!jS_&!=|X#5K;7|ER4O70 zb0Gh-#aGJfO8H!~y=J-3BM-dOO9&^H!xHeQh4V8EktrFx$$0jj_en?$BFEduOvl4@ zMWCC;vxR%aO@-Y91NhX-08!@6NkOK45FJ_!_4ZPIZ8$_()M$Tk8cfb(wi-)xeIR>W z-V2y9i*RV;A|v5|_`|HPuV?;Gp+ba*L&*R3?S)w?Jxx_!APMJwGAcr4MRaET!1N;l9IA?1G#zd>T0Ige#9yRRK}V0#3x%<~7J%O`8IlLLpMCrN(=o_lVlx zYHM3*Gkw%xb6gt&{cLP`m1*r;6nyT)mx-blpPoKgJP=BuIhqZgtWY$fv_$YF_lgpn zy(E)J%gl7@9H6tFE3V?44iXTz+UH zWU-%V8{>5jwDocLQK5~kIgDe69{Vgpw@3-?>19CD+f97Dt42wlk{l6e_(o? zI1yw;K1ye_Z#OC@XGlb31PN(Nx;EqQyR>!NHe;GBsnrm?2B#|_6T*(|Y`3DDs6Un$ zC|+$1$t^%FruZlfLW;3KBm2K8+#J1JFhlEtruHvPFW+4ZG6R!_+4y&YEXU~f1G;cd zr_R6eR-OQu?RASJ0!jX(c8nXhq*p1WiSG}s?cY!h641&!3xK!U;fXie7_3=qm1mmQkz zm$oxiY41oYFb<->eVeD4swt<3L4E>hpWaW0aDN&T*7uNBkjH~*(pI8+|Bx24A;oBa zv_2~0(o9H5s6-{Pc2aC&=|UFv`JWPzF}57u1l?n{vbN3*OGbPTtJun|x8iMH)+derf?WIMR2$Jv;Amnf17v^%Z!J=VW}pJlS6S^oa&rcJ4_=GI`8He(iA9 zZ_1~?JDYp-yz0xIr}bEqlH50hOpcjYSO@_YacYPT%f{5weQoF=#E}A+K*eDb4%jID zp9&~Kj!&VF&3}^Xq&{CfJw-lv=8KT;v9Yr9O_PQyg;o?3bxr0jw-!{p2jLWbk}-tp z0#tcptVr$KKAFxYc{pzc-7x-0Is3Qg4+$AKY)5N!0kSe=xMXNA+jJB#r*M_jSx^xV z#mPW^)q|#T(Tf@)%kG}t#Fhzg+I(NE#`q+^g(;{_PS#&`(@8~zjzXsX9aL)CfLDqO zyw!gHcg;o(_q7ojf!EQTVaQ0xDA6$|k{_|+jD0Kb8Ev5zVoYB24RAtPWI$?pw<(ms zCO|Om0iB;r&b|qaqml>9uQjUgTb|iAW<6}cG|P@s&hJ_*xU!*tU zc@t%y%+J8eO6jvLQT6KN0+=gWCA75>mCJpS>*xJydG@%?3fIpcbHot_`*~$PW#|cA z6s_f$Mmvv7hy3ij0-fc;Dc06=j~Fa~wsIs(oZxF82JD&;o>#G4mp?!FW~o)D05jyr zK@K6I?KWHo~(Vr=85f5>&UE18fdI7>8_3()*hdN7s z_tWG{ktm|DTDc6s>)*Y7+feg4M@;3u#ZgmW2wC{994xBFC3wQ~>!saXhj&u|gAuR( z?`w)!So~Pub@ERemAW}df}cgD(P^8TvkdJ~ea`KdfKPAY&Og6$wokXOrTKzceF1!^ zBQBCP%OmGhZEK#{Fj_-hv%W;M@gt5Y7_3o-KOQ70n`?5!@Z{RI669_dmRZcyo%Ous zv3n#dF3Q1?ET+*eBhQc)HX^(YU+zuG6FH0c{R3zez8IZ1*^$4!J{%)-h~B)Z&Q-6% z8bSS3_S6x5!gBq?^;nvV_0%W^y@2S68px~>pA?yfnkV? z8f>k4;M~~qKv0*wb?46S8~Qc|+dH%0yjT+aultj*Ypr@0Rctl?mIb$4t`C$O6gS-@ zu1Y+6-~vvZ>fq4~Zr9EDeuL>#?qmL8(8bUagw{onZpJm+e;lv?sO*^l00VX)H>hMLh%lZg`bjrH_2t4@2aN9r+ ztQicmoHmJ@?Xrw?DM00?Rqy0z#D({hFhtvZXqb5YLlD~!)&X}O@34E4`etga{NrG0m2WoYrP>B&z`}jYpW1rFuxRUod(xm^MK}=FLVKrG`kPx?oO^=X z=vjw>XEf8S*uvCwdEME7jd0@j;%?*B@m*@gr{7qC8~_kWTroeoI+dcZF?ayQ+>J9- zL|4E69OfQS9=I(Jy7%>*biM##%3J5sH!qJ>j@M?kp@2iL5{lXsD&=wtS{s*d9IUj} z!#0^83i*_#&XV}V%@S+`+qWiRkPhvGWe`!bJJ8fJD)Az)4D++F1fZTXzFdl^Q2upV zm>=#e`J5klxS;}^!731R4i{b$YUP*}C5u3}em2hnl_O-Rst|0|sf7qXtzP#BYw)S_ z{&xWkp-Vy{tVP>oFInAOJ?B!-e`nxTS|dWIYaBcJR}{>xoD_)isSpA-VtZj)%gw-5E;C?&1P<9yqaT@>+cV{X7SI3B zRATBCg*`>vSKpUpqJ%a++#sjnwb8w*mSIQ1YnA3T{Vy%S8yZMLq27_q*($6MyQ1J> zj*LnmCeK@kD=JZs0TP!#KjMUx9juXB&*C?JjHcSxDkCU7jzJpG;oAB9l+Xd0nB8Qs z&gcwZIgH`?(V4YSIPRgH7#=%7_{-hjfe;HEKb( z*7@Y{C@MF&B?IMGM$B}<&0H`E?=2sa?i^oD8Yf~5Crj~K9xiD-g;gd5-!oO0x-eiZ)97ZS{Vm|?|*_Vtot^d*j&gMhv?ZtlHc3lVvbJmWLi&&Y;r83V0 zB`)R@g_GrNc8kf=S6oOps=Y7ax^J{m+S+N&yu4u%m~<2a2lE9C!FkML<4cR1(Md^; zs7YrziHIYDY>LC3UGM`Qro7RX$X7OqrvXT0$2>XcQx5}+@0TxLc0KQo%r;AgjE#+f zE`gzRc^|<%d)II%jzw$ok~i8T+c_Ma)hvofv7SJXOxizKNEb$S)I#xpSMk#6_>7*L zg5PtM!RNiyT@8&GpjvWgT3}&fg2g&0v6%}86E*P>h@LXYxcj#-K%oVse2KF|qA)Nq z{LurJn(!vjJ4L{-_|u5&H_6jxuegCBltt9xaJPb5+X_Vgme1}Iw2U;)AVbLnymdfx zRc?&+-`GvC^e}+d){^gfbhM86xzVpzb3nWrRcx$Omh=jl zJwEEgagL)7*UXs_53r(Xab(Tqv+-3sRfMj z4lnSic=2<7%xYYOuW1C|Ts_N=AB2dp!+B_%Ks^@90fHeR}3+P&Schr%x*)Nj+ z?mR0P_JnS4rgo>TgPDs87_GcAFmwNAE&cxqTYw!j8uKvoVP?!U>O8%-AVa@5Cmks4 z=j7_qB!VqpgzafE@cRSkj|X4-Goz))*5&O1NxW``vkhJ^dtcN8!ZAg>;B}gEM-cp? zE##{NckSO$jV9?wh0Plo`yeTte`HJm!(tx2fn*mboQ;*RZTbT$1l_v` zsizIu2Oji_KdeGkVSh3YrR1Fqr0gqjkvIVSEnyhKtGT?t%WjbOn_OOv?0 zmYaM7e<&rS8|nV9pw<+X+YiC$3|;_Cp()C*Uke+r;!`V<@wuylAg9G=Z*JY7OS$@e z2ro55N1?90G4n)?K52EcW?)0@v#Jsj4E^CU$RLz?VL3U^&VN5(Lz@eYYI}OWHhM%< ziUg6-!M!z88Xw|C<^lU+jHQ)e4lU~MD%PNfReJ7L-1(7pxwrT zK(Y&L@rPQ`s2Yi}J^oW-65_5;COMBcU-`{9+x-Vxn*q6pVN!VEv9vvn79(jJuDC^5 zL)bM~h{4t6%dh8Bc!78B+=lsAg>NG&X)8BDHkfq9H4Q_k(u;)n*yW`qud}uxKv7cP zS9-2gLkSh&;W*YP-Y$=ArU-AqiVeK1HYcGM0;U+Ws10D>uLh`$6(U=sG#4 zjvfFZWHCOLVYcX^IR;&GO=F!t;I>;>4^c~12gNVuBrjC^?*ki+7Sk7MaVN60tnigD z#Y7;g>A%3Zw6Jo zDA35si+E!GP?eB8{xH<(MX`in##MU>hi)Cf=)6EMlZ;T12ANu*&|i5`k~%t6HS`EP zzQXSQemu$$$$l^~mPNQ54b~2Pf!*uOEI}!2rvSWR_{{ALS!S@+Ac+AcNvUYA9u-y9 zRwbimApCD68rl!G=vB6v$mxc66N;naCWRQVKUtH4a-#&&c7> zcSUR$&=;HAiHGxWAla2DB}H-i7d-=-VzAWg@s)a_?Fgi>&tYG?I;Vi!Vd+K|Y z)P5MF=asgwMIrD_`?ji=w0yLBeeS;ot2=5}{EV4vsX%-rUNSZ_3Ia?q=cp4W(+{wp z4NA9$K)5pr0eG$QJw&V0B24h!5<0|Z02>n+(hzqmJM0LD=nj_qN5!%Uh^U9l6ZQ=r zEpm}~?TRNzuKlr?MNKy8zl1`=k)OUtfe1ykS8NcR3Qh~)vcru1p}MAO3e{XT;MjE#W;z>(eow*%-W-Z8QEj zLhA=VJb26Z*=o&T44Nc#sw4eD+(|z5o>;_MfceH-39j$@T8r z@ba5+0RkPN-90^14UqG@v|d#v2vZq;5tq%-@K5d5z$px(72aNUzp6@}%k^1k$044n zaXw!iG_o2mVrPGJuh!!PK4>JYeh64rQqH%c)~CvhT7U@-rd|sP4MiINng=llZOSS# zGK+0kq|R(}Ht=bGtl0z0?fWz|O}HDUNTl~w#{R*Z>`_}*Z&`mI@ZSQ*hVnibJ3JcU z9)M#G8pQ<`c<)6J2xr&5T+mpgW5CEQ)oeZF_lu0VxszDxmi6V_fOM=GFo{h*e$V{_ zyh|VSiMZ|#04#ZQ>BIHwOzljSHZz7!lm8JR^h;c*5N7ZeY@pEzKiO(>?(OJEDxIl1 zw~fxI0X{1Y)~vtBnG9+Kw2sdf_r{d;UA#Pb4S|0zMWcWtk%s1-7kXD4n!siAXIV>6 zmuYV#G|>R*kf-&jvN(_njnS>LqvPXKLxA((UsH)l)?ZVJ{K;pl6F1|L8#ixG7*kSE ziI@Oc8N>&GhQ6$DJLcU zs-r9;@ZzJu*lQkH;vu8(NrhP0EX$q(`2G>)W8Kdl#N(5?X=p@QXKQEKVwUDe`1+sO zHkTQDzwmT^S?Vc1RwF}=gr~#?Mks8oJ$jc8c0Hzji62}$;F|j=7m4g{v8m~+i-lKW zr0Lw51K_`yrEIV+X=JrQF2SwE|BVPWu7(~plpqRwL>48W(!g((keuLbEh|OsQ7#vT zrWj|s(I$wZiNApff@*3WWTl@cvL&8<33xfN_8Ee!4UW2}%-ryXJ7?yV60Eruc(-Q*F;bcfkT=SD0udeTr(RgDJ8 z!*OH@pkr4m9y{^ae!t`4d$)OXT|;15Rb;&!U9;Ni#KgohGE*-V z6hM zt*x!awFWfiJtQ=+3?kv%zJqX6BS)J5fj zc_1lC_vq~9%NKik($dma8}V4AA#F-ZeK$V5?_W`5By<)sg+d)jf@SzfKAOK`Api${ zA{*%mgFzZe04}+ywe@RF&G*XID}~e`-8a@?ePnH=KjrAiO82b_9WjN?L{0EZC+Br| z^_#4|d-wJ*m_ez+!t!#3TQ^g5B_8HB9QNkis8|l}2i%^i>=h`*Q z{%10hs*LsFel*WszBJGXi(UJA{pN&4zJP#$+4y!6Mw?s%|0F0h&y)$Uw4Q~57|)mT ze7uE!|37%8`0pQD-vRqjU?dc>)2Dr-vGkZ;TH4XEbg1ODj1mhrre#FpSX^n=gd1QS z`K&HmWo8>V@0MYSdTfW**GnsOgf|wPmY0>?dXzoaqGz;HR$h*EZ|(HFYZf@t@$dA; zif>$^JUTt*z9TAaW##47^vYi_8D(%}rpkp;Q4q3ROLwfJAL}8OQ2{?o78{!`TR8w9 zgCLKWPoMqF$OI!?$|>v{8SljKBxR}Q3Dc^Z`(H8_nzH5 zbHjrY9jhqrsGsb#J&+HQ*xICaRZ}C6Bj3zh<+0hO0WlPyBl}27M|b*RdspE4<3$JT z-|5;VOEU$Y@Zim!9?-CTbg?M~Uu*vOF*MZa)*&AV5Q_(oV|Fs_%?1U>vDiW-Lq^E} z4RiC@#OT#hm)OUfiGb00`utSg~_|O zDYG5)A(LgYq8gf-sgQe#>hd%xG3Q6Ldz0qc9_LNI0f+8$vK0gGZMjmfp2F5Cc~i@6 zH`E)v*DGB;DlsvwCCmr(kFehxO#D8kW7n=&A4pqhz4syK#%%~ z=Tud2MMVYp^Hy%Sc zA!14o+AF)3urwm*Nz=?Eh=MxVOC+bTFz-> zOPdpLrS^&yD{j5-?dz+0I`!Zy0WaRn5T3`=fR{@phbTrl)!6)+F|(^m$A9~o9v)%i z&A^Mw@9Y+rm6e@2Y0{eNI=T7vFCVc^^hmjOsz=vR&P2;d$=G=F%<1BA?@yUwQFTSy zluIz&^u+l+FCNnWYhHTYwX`?J_5aV;JFSA`ofa-w|+(E|3O;@;e!J{y+V6kJH)*|k!B>g$V-TlHdhb-J{5I30T5 zcC{|QGID0%){XX^6F>Gy7XPS`6G}KCw{m*3imaYAlFV5L(TV{e@>C)|*#AuHF13@~2DOT-B8Z zOHO)w`)=I0G4t9PZ9TnhJHv~=ieFH!b4s|fZL7Ufov`rZw0UvI`(zX2vz}e#F)Vj$ z`{W&LUGk!1qfIGCXjE9(l3x)^x2*8cFsj+0arMH2#)kd_8U`zzQ=Zf}Uk?rCD*N~5 zjnBe|%|C7L*cN>*@}C>T_f2sR@az+P;Kg_W84}M$C2TB&L_;MehJ}QMl)b<9`QP5^ zO_P$G7Xpu{0o}s!MDp{U_JEcRT-@CKa@LoCyUAWsEj< z`ftw7t^Pjsetbe+US48i=azt-uRdz}0H@%9S2zXU{$zN@y#Ai`j2Q-rUSVNrB_$yd z5i{OtRaR6Ty|;h*j2{oaZ{A({`sJfXi~a@ka&Ca$sB2aD_Se-J)q#3?>wqW1ALUrE zXV0Gl2Y1zH0y377q26)5lIZHZ(G~O*qChaq_R(2HC*vS&8$sJHCDCoX-%saq)4!>l$lf zL9GPG4;3{n{(nxL;0 z1e^h>Y7wyIXl>(||3h`Dk#MpxFpU7kY`FT5i%WJ1+G*B{FO3LOF%ki_`hiBOD{o|J z6h5T>wE0QvB_{7;Z;1Mgb`cRL4Hmx{VQm3Ll{-ViQWpJ+Gz6QzXyV2rE>STZ2RZ}W z0&G$PVyu??eBazD^?6#YNzu&)7h5ARR)y*&V#O9r;f_y9B4sR!q%PkK) zlTwmW6A!n!UH$B@(|BzA)2Gk9mIfII7w-vQ|9jx+ssofP77o7(m_7x z6EKsMkl^4cPz7#V=;}^&eK~XGr>>&2#f@wO`8r-o(^lB6Q9oq#)8?0B z63AI5eqCCwz{~3@D KiroCrewConfig: - seen.append(threading.get_ident()) - cfg = KiroCrewConfig() - cfg.agent.default_agent = "kirocrew" - cfg.default_agent = "kirocrew" - return cfg - - f = _agent_file(tmp_path, "scratch") - with patch("kiro_crew.agent.KIRO_AGENTS_DIR", tmp_path), patch.object( - KiroCrewConfig, "load", staticmethod(_load) - ): - resp = await api_agent_detail(_delete_request("scratch")) - - assert resp.status == 200 - assert not f.exists() - assert seen, "the guard never loaded config" - assert seen[0] != loop_thread, "config was loaded on the event-loop thread" diff --git a/test/test_agent_template_pane_flag.py b/test/test_agent_template_pane_flag.py deleted file mode 100644 index 6fd03ea0c4a..00000000000 --- a/test/test_agent_template_pane_flag.py +++ /dev/null @@ -1,67 +0,0 @@ -"""``agent_template_pane`` must survive load() -> masked GET -> frontend predicate. - -The crew definition panel (private fork-on-edit) is gated behind -``agent_template_pane: true`` as a TOP-LEVEL key in the running instance's -``config.json``. The frontend reads that flag live off ``GET -/api/config/kirocrew`` (``website/src/hooks/useAgentTemplatePane.ts`` requires -the value to be exactly ``true``), so the whole feature turns on the key -reaching the browser in that response body — the same launch-blocker shape -``connections_ui`` had (see test_connections_ui_flag.py for the full rationale -on why an unmodelled key never gets there). -""" - -from __future__ import annotations - -import json - -from kiro_crew.config import loader as L -from kiro_crew.config.loader import KiroCrewConfig - -# The one spelling the frontend hook and the feature map use. -FLAG = "agent_template_pane" - - -def _point_loader_at(tmp_path, monkeypatch, data: dict) -> None: - cfgp = tmp_path / "config.json" - cfgp.write_text(json.dumps(data), encoding="utf-8") - monkeypatch.setattr(L, "config_path", lambda: cfgp) - monkeypatch.setattr(L, "config_dir", lambda: tmp_path) - monkeypatch.setattr(L, "config_local_path", lambda: tmp_path / "config.local.json") - - -def _masked(cfg: KiroCrewConfig) -> dict: - from kiro_crew.dashboard.handlers.core import _masked_config_dict - - return _masked_config_dict(cfg) - - -def test_flag_set_true_reaches_the_masked_get(tmp_path, monkeypatch): - """The launch blocker: ``true`` on disk must arrive in the browser's copy.""" - _point_loader_at(tmp_path, monkeypatch, {FLAG: True}) - cfg = KiroCrewConfig.load() - - assert cfg.agent_template_pane is True - # Modelled, therefore NOT swept up as an unknown section... - assert FLAG not in cfg._extra_sections - # ...and therefore present in the browser-facing view, strict-true. - assert _masked(cfg).get(FLAG) is True - - -def test_flag_defaults_off_and_rejects_truthy_strings(tmp_path, monkeypatch): - """Default-off; a string "true" must NOT coerce (frontend is === true).""" - _point_loader_at(tmp_path, monkeypatch, {}) - assert KiroCrewConfig.load().agent_template_pane is False - - _point_loader_at(tmp_path, monkeypatch, {FLAG: "true"}) - cfg = KiroCrewConfig.load() - assert cfg.agent_template_pane is False - assert _masked(cfg).get(FLAG) is False - - -def test_flag_round_trips_through_save(tmp_path, monkeypatch): - """save() must serialize the field so an operator's opt-in survives.""" - _point_loader_at(tmp_path, monkeypatch, {FLAG: True}) - cfg = KiroCrewConfig.load() - cfg.save() - on_disk = json.loads((tmp_path / "config.json").read_text(encoding="utf-8")) - assert on_disk[FLAG] is True diff --git a/test/test_agents_endpoints_owner_auth.py b/test/test_agents_endpoints_owner_auth.py index 8cd506f6a53..884f5e460fe 100644 --- a/test/test_agents_endpoints_owner_auth.py +++ b/test/test_agents_endpoints_owner_auth.py @@ -44,7 +44,6 @@ ("POST", "/api/capability/agents/uninstall"), ("POST", "/api/capability/plugins/sync"), ("PATCH", "/api/agents/detail/{name}"), - ("DELETE", "/api/agents/detail/{name}"), ("POST", "/api/agents"), ("POST", "/api/agents/sync"), ("PUT", "/api/agents/{name}"), diff --git a/test/test_api_server.py b/test/test_api_server.py index 5713e22ed66..5d86e9bb866 100644 --- a/test/test_api_server.py +++ b/test/test_api_server.py @@ -61,7 +61,6 @@ def test_all_mcp_routes_registered(self, tmp_path): ("GET", "/api/spawn"), ("GET", "/api/spawn/{agent_id}"), ("DELETE", "/api/spawn/{agent_id}"), - ("DELETE", "/api/spawn"), ("GET", "/api/lessons"), ("POST", "/api/lessons"), ("DELETE", "/api/lessons"), diff --git a/test/test_config_extra_sections.py b/test/test_config_extra_sections.py index 4adb3fb520b..0f91ae7a3f3 100644 --- a/test/test_config_extra_sections.py +++ b/test/test_config_extra_sections.py @@ -45,6 +45,29 @@ def test_unknown_section_round_trips(tmp_path, monkeypatch): assert cfg.to_dict().get("amazon") == {"midway_flags": "-o -s", "n": [1, 2]} +def test_retired_key_is_ignored_dropped_and_never_warned(tmp_path, monkeypatch, caplog): + """A stale materialized ``agent_template_pane: false`` cannot disable anything. + + Older builds wrote the key into every saved config, so flipping a default + could never reach existing installs. The key is retired instead: the pane + renders unconditionally, and a leftover ``false`` on disk must be (a) not + parsed into any field, (b) not captured/round-tripped as an extra section, + and (c) not reported as an unrecognized top-level key on every load. + """ + cfgp = tmp_path / "config.json" + cfgp.write_text(json.dumps({"agent": {"provider": "acp"}, "agent_template_pane": False})) + monkeypatch.setattr(L, "config_path", lambda: cfgp) + monkeypatch.setattr(L, "config_dir", lambda: tmp_path) + monkeypatch.setattr(L, "config_local_path", lambda: tmp_path / "config.local.json") + + with caplog.at_level("WARNING", logger="kiro_crew.config.loader"): + cfg = KiroCrewConfig.load() + assert not hasattr(cfg, "agent_template_pane") + assert "agent_template_pane" not in cfg._extra_sections + assert "agent_template_pane" not in cfg.to_dict() + assert "agent_template_pane" not in caplog.text + + def test_extra_sections_never_clobbers_a_known_section(): """A stale/hostile capture of a known key must not overwrite the real one.""" c = KiroCrewConfig() diff --git a/test/test_handlers_messaging_coverage.py b/test/test_handlers_messaging_coverage.py index 11dcbeeaaa9..2e44c13c475 100644 --- a/test/test_handlers_messaging_coverage.py +++ b/test/test_handlers_messaging_coverage.py @@ -841,18 +841,6 @@ def test_removes_an_already_finished_agent(self) -> None: assert mgr._agents == {} and mgr._tasks == {} -class TestApiSpawnClear: - def test_ok_without_manager(self) -> None: - assert _payload(_run(mod.api_spawn_clear, _Req(_state()))) == {"ok": True} - - def test_clears_only_finished_agents(self) -> None: - mgr = _mgr(all_agents=[_info(id="run", done=False), _info(id="fin", done=True)]) - mgr._agents = {"run": _info(), "fin": _info()} - resp = _run(mod.api_spawn_clear, _Req(_state(subagents=mgr))) - assert _payload(resp) == {"ok": True, "cleared": 1} - assert list(mgr._agents) == ["run"] - - class TestApiSpawnStopAll: def test_requires_manager(self) -> None: resp = _run(mod.api_spawn_stop_all, _Req(_state(), {"slot": "chat-1"})) @@ -1966,7 +1954,6 @@ def test_module_exposes_every_route_handler_under_test() -> None: "api_spawn_list", "api_spawn_retry", "api_spawn_delete", - "api_spawn_clear", "api_notification_channels", "api_notification_channel_settings", "api_slack_pins", diff --git a/test/test_member_memory_api.py b/test/test_member_memory_api.py index 6eaa540c59a..ac81e34ddf9 100644 --- a/test/test_member_memory_api.py +++ b/test/test_member_memory_api.py @@ -1071,7 +1071,7 @@ async def test_private_continue_authenticates_parent_before_cwd_read(env, member @pytest.mark.asyncio -async def test_private_spawn_list_and_clear_only_touch_own_runs(env, member_proof): +async def test_private_spawn_list_only_shows_own_runs(env, member_proof): from kiro_crew.dashboard.handlers import messaging def row(name, store): @@ -1099,10 +1099,6 @@ def row(name, store): response = await messaging.api_spawn_list(request(env, internal=True, proof=member_proof)) assert [r["id"] for r in json.loads(response.text)["agents"]] == ["alice-run"] assert "result-bob" not in response.text and "result-global" not in response.text - cleared = await messaging.api_spawn_clear(request(env, internal=True, proof=member_proof)) - assert json.loads(cleared.text)["cleared"] == 1 - assert set(env.state.subagents._agents) == {"bob-run", "global-run"} - assert set(env.state.subagents._tasks) == {"bob-run", "global-run"} @pytest.mark.asyncio diff --git a/test/test_session.py b/test/test_session.py index 8de15b9fca2..01cf2913958 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -2504,69 +2504,8 @@ async def test_discard_shutdown_exception_still_clears_sid(self, cfg): mock_clear.assert_called_once_with("k1") -class TestContextInfo: - """Tests for context_info() and _resolve_agent_model().""" - - @pytest.mark.asyncio - async def test_context_info_basic(self, cfg): - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - await mgr.get_or_create("dashboard:slot0") - mgr.release("dashboard:slot0") - mgr._sessions["dashboard:slot0"].prompt_count = 5 - - info = mgr.context_info() - assert len(info) == 1 - entry = info[0] - assert entry["key"] == "dashboard:slot0" - assert entry["name"] == "Chat (slot0)" - assert entry["prompts"] == 5 - assert entry["context_pct"] == 0.0 - await mgr.close_all() - - @pytest.mark.asyncio - async def test_context_info_background_key_name(self, cfg): - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - await mgr.start_pool() - info = mgr.context_info() - bg_entry = next(e for e in info if e["key"] == BACKGROUND_KEY) - assert "Background" in bg_entry["name"] - await mgr.close_all() - - @pytest.mark.asyncio - async def test_context_info_non_dashboard_key(self, cfg): - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - await mgr.get_or_create("slack:thread123") - mgr.release("slack:thread123") - info = mgr.context_info() - entry = next(e for e in info if e["key"] == "slack:thread123") - assert entry["name"] == "slack:thread123" - await mgr.close_all() - - @pytest.mark.asyncio - async def test_context_info_with_acp_provider(self, cfg): - """AcpProvider path extracts model and agent from client.""" - from unittest.mock import MagicMock - - from kiro_crew.providers.acp import AcpProvider - from kiro_crew.session import _Session - - mock_provider = MagicMock(spec=AcpProvider) - mock_provider.context_usage_pct = MagicMock(return_value=45.0) - mock_provider.shutdown = AsyncMock() - mock_provider.client = MagicMock() - mock_provider.client._model = "sonnet-4" - mock_provider.client._agent = "kirocrew" - mock_provider.client._session_id = None - - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - mgr._sessions["k1"] = _Session(provider=mock_provider, prompt_count=3) - - info = mgr.context_info() - entry = info[0] - assert entry["model"] == "sonnet-4" - assert entry["agent"] == "kirocrew" - assert entry["context_pct"] == 45.0 - await mgr.close_all() +class TestResolveAgentModelResolution: + """Tests for _resolve_agent_model().""" def test_resolve_agent_model_cache_miss_returns_auto(self, cfg): # Clear cache if exists @@ -2593,8 +2532,7 @@ def test_resolve_agent_model_coerces_non_string_spec(self, cfg, tmp_path): ``~/.kiro/agents`` is shared with other tools; an ACP-style ``{"id": ...}`` here would be CACHED and then handed to - ``/api/sessions/context`` (the dashboard calls ``.replace()`` on it) and - to the pooled-model comparison in ``claim_pooled``. This method is + the pooled-model comparison in ``claim_pooled``. This method is annotated ``-> str`` and must honour that. """ import json @@ -2616,8 +2554,8 @@ def test_resolve_agent_model_refuses_an_oversized_spec(self, tmp_path, monkeypat ``~/.kiro/agents`` is user-writable and shared with kiro-cli, so an oversized "agent config" there must be refused rather than slurped into - memory — and this resolution is CACHED and served to - ``/api/sessions/context``, so it is not a rare corner. + memory — and this resolution is CACHED and reused on every later + lookup, so it is not a rare corner. Exercised with a LOWERED cap rather than a real 50 MB fixture; the property is that the cap is consulted, not its value. Paired with the @@ -4404,31 +4342,6 @@ async def test_ensure_background_idempotent(self, cfg): await mgr.close_all() -class TestContextInfoBasic: - @pytest.mark.asyncio - async def test_returns_session_info(self, cfg): - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - await mgr.get_or_create("dashboard:slot0") - mgr.release("dashboard:slot0") - info = mgr.context_info() - assert len(info) >= 1 - slot_info = [i for i in info if i["key"] == "dashboard:slot0"] - assert len(slot_info) == 1 - assert slot_info[0]["context_pct"] == 0.0 - assert "Chat" in slot_info[0]["name"] - await mgr.close_all() - - @pytest.mark.asyncio - async def test_background_session_name(self, cfg): - mgr = SessionManager(cfg, provider_factory=_mock_provider_factory()) - await mgr._ensure_background() - info = mgr.context_info() - bg_info = [i for i in info if i["key"] == BACKGROUND_KEY] - assert len(bg_info) == 1 - assert "Background" in bg_info[0]["name"] - await mgr.close_all() - - class TestCleanupLoopResilience: """Tests that _cleanup_loop survives _expire_idle exceptions.""" diff --git a/test/test_session_more_coverage.py b/test/test_session_more_coverage.py index c70f89d4e16..0b9430bc3c9 100644 --- a/test/test_session_more_coverage.py +++ b/test/test_session_more_coverage.py @@ -250,34 +250,7 @@ async def test_a_raising_recycle_callback_never_escapes(self, mgr) -> None: await mgr._fire_recycle_callback("dashboard:a", reason="rss") -# ── context_info / _resolve_agent_model ────────────────────────────────────── - - -class TestContextInfoModelResolution: - @pytest.mark.asyncio - async def test_an_auto_model_on_a_named_agent_is_resolved_from_agent_json( - self, mgr - ) -> None: - """``client._model == "auto"`` is not a model the dashboard can show, so - a named (non-``kirocrew``) agent falls through to its JSON pin.""" - from kiro_crew.providers.acp import AcpProvider - - provider = MagicMock(spec=AcpProvider) - provider.context_usage_pct = MagicMock(return_value=12.0) - provider.context_window_tokens = MagicMock(return_value=200_000) - provider.shutdown = AsyncMock() - provider.client = MagicMock() - provider.client._model = "auto" - provider.client._agent = "researcher" - _register(mgr, "dashboard:slot1", provider=provider) - - with patch.object( - SessionManager, "_resolve_agent_model", staticmethod(lambda a: "sonnet-9") - ): - info = mgr.context_info() - - assert info[0]["model"] == "sonnet-9" - assert info[0]["agent"] == "researcher" +# ── _resolve_agent_model ────────────────────────────────────── class TestResolveAgentModel: diff --git a/test/test_subagent_delivery_ttl_anchor.py b/test/test_subagent_delivery_ttl_anchor.py index 490efc28708..17552b03aaa 100644 --- a/test/test_subagent_delivery_ttl_anchor.py +++ b/test/test_subagent_delivery_ttl_anchor.py @@ -428,7 +428,7 @@ async def test_an_evicted_run_still_waits_for_its_teardown(self, agent_root): _finished_run(info.id, agent_root) gate = asyncio.Event() mgr._teardown_gates[info.id] = gate - # Evicted exactly as api_spawn_clear does it: both records, together. + # Evicted the way a bulk clear would: both records, together. mgr._agents.pop(info.id, None) mgr._tasks.pop(info.id, None) diff --git a/website/playwright/capabilities.spec.ts b/website/playwright/capabilities.spec.ts index f111562443c..8d999b23486 100644 --- a/website/playwright/capabilities.spec.ts +++ b/website/playwright/capabilities.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' /** * /capabilities — Agent Capabilities page. - * SidePanelLayout with 7 tabs: Agents, Agent Templates, Connections, + * SidePanelLayout with 6 tabs: Agents, Connections, * Skills, Steering, Hooks, Prompts. Default tab is "crews" (KiroCrewAgentsPage). * * Covers: page load + heading, tab navigation with content change assertion, @@ -28,10 +28,10 @@ test.describe('Capabilities Page — /capabilities', () => { await expect(page.locator('#main-content').getByText('Agents you chat with', { exact: false })).toBeVisible({ timeout: 5000 }) }) - test('shows all 7 tab buttons in the side nav', async ({ page }) => { + test('shows all 6 tab buttons in the side nav', async ({ page }) => { // Tab buttons inside the nav panel — look inside #main-content nav const nav = page.locator('#main-content nav') - const tabs = ['Agents', 'Agent Templates', 'Connections', 'Skills', 'Steering', 'Hooks', 'Prompts'] + const tabs = ['Agents', 'Connections', 'Skills', 'Steering', 'Hooks', 'Prompts'] for (const label of tabs) { await expect(nav.getByRole('button', { name: label, exact: true })).toBeVisible({ timeout: 5000 }) } @@ -80,13 +80,6 @@ test.describe('Capabilities Page — /capabilities', () => { await expect(page.getByRole('button', { name: /\+ new hook/i })).toBeVisible({ timeout: 10000 }) }) - test('switching to Agent Templates tab renders installed agents', async ({ page }) => { - await page.locator('#main-content nav').getByRole('button', { name: 'Agent Templates', exact: true }).click() - await page.waitForURL('**/capabilities?tab=templates', { timeout: 5000 }) - // AgentsPage renders "Installed Agents" heading text - await expect(page.locator('#main-content').getByText('Installed Agents')).toBeVisible({ timeout: 10000 }) - }) - test('create and delete crew round-trip via the editor panel', async ({ page, request }) => { // Both mutations now live in the side sheet: creation is New crew → dialog → // Create, deletion is card → dialog → Delete crew. Driving them through the diff --git a/website/scripts/capture-agent-skills.mjs b/website/scripts/capture-agent-skills.mjs deleted file mode 100644 index 17bf67f92a3..00000000000 --- a/website/scripts/capture-agent-skills.mjs +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Screenshot harness for mapping skills to an agent template. - * - * Runs the REAL built SPA (website/dist) behind a tiny in-process static server - * with SPA fallback, and answers every /api/** call from fixtures via Playwright - * route interception. No gateway, no dashboard auth, no kiro-cli spawn. - * - * The client code under test is unmodified — only the network is stubbed — so - * Agent Capabilities > Agent Templates, the Skills chips, the add-skill - * dropdown, and the PATCH round-trip are exercised exactly as they run in - * production. The stubbed PATCH mutates the fixture, so the after-shot shows - * the real re-render, not a mock. - * - * Usage: node scripts/capture-agent-skills.mjs [outDir] - */ -import { chromium } from 'playwright' -import { mkdirSync, readFileSync, existsSync, statSync } from 'node:fs' -import { createServer } from 'node:http' -import { join, extname } from 'node:path' -import { fileURLToPath } from 'node:url' -import { json, logPageProblems, stubDashboardApi } from './lib/stub-dashboard-api.mjs' - -const OUT = process.argv[2] || '/tmp/agent-skills-shots' -// fileURLToPath, not URL.pathname: on Windows .pathname yields "/C:/…", which -// join() then turns into an invalid "\C:\…" and every read fails with ENOENT. -const DIST = fileURLToPath(new URL('../dist/', import.meta.url)) - -mkdirSync(OUT, { recursive: true }) - -const MIME = { - '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', - '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', - '.woff2': 'font/woff2', '.woff': 'font/woff', '.ico': 'image/x-icon', -} - -/** Static server with index.html fallback so /capabilities deep-links resolve. */ -function serveDist() { - return new Promise(resolve => { - const srv = createServer((req, res) => { - const rel = decodeURIComponent(new URL(req.url, 'http://x').pathname).replace(/^\/+/, '') - let file = join(DIST, rel) - if (!rel || !existsSync(file) || statSync(file).isDirectory()) file = join(DIST, 'index.html') - res.writeHead(200, { 'Content-Type': MIME[extname(file)] || 'application/octet-stream' }) - res.end(readFileSync(file)) - }) - srv.listen(0, '127.0.0.1', () => resolve({ srv, base: `http://127.0.0.1:${srv.address().port}` })) - }) -} - -const SKILLS = [ - { key: 'babysit', name: 'babysit', description: 'Same-session monitoring loop for PRs and CI runs', path: '/home/user/.kiro/crew/skills/babysit/SKILL.md', source: 'kirocrew' }, - { key: 'prepare-pr', name: 'prepare-pr', description: 'Drive working-tree changes to a review-ready pull request', path: '/home/user/.kiro/crew/skills/prepare-pr/SKILL.md', source: 'kirocrew' }, - { key: 'rubber-duck', name: 'rubber-duck', description: 'Adversarial review that turns explaining out loud into a hallucination check', path: '/home/user/.kiro/crew/skills/rubber-duck/SKILL.md', source: 'kirocrew' }, - { key: 'widgets', name: 'widgets', description: 'Render rich HTML inline via mcwidget tags', path: '/home/user/.kiro/crew/skills/widgets/SKILL.md', source: 'kirocrew' }, - { key: 'kiro-user/pod-e2e', name: 'pod-e2e', description: 'Run end-to-end tests against an isolated throwaway pod', path: '/home/user/.kiro/skills/pod-e2e/SKILL.md', source: 'kiro-user' }, - { key: 'kiro-user/llm-council', name: 'llm-council', description: 'Convene a cross-vendor LLM council for hard decisions', path: '/home/user/.kiro/skills/llm-council/SKILL.md', source: 'kiro-user' }, -] - -/** Mutated by the stubbed PATCH so the after-shot renders real state. */ -const mapping = { - kirocrew: [], - 'code-reviewer': ['prepare-pr', 'rubber-duck'], - 'release-captain': [], -} -const UNMANAGED = { 'release-captain': ['skill://~/.kiro/skills/*/SKILL.md'] } - -const AGENTS = [ - { name: 'kirocrew', description: 'Autonomous personal AI agent', source: 'kirocrew', model: 'claude-opus-4.8', mcp_servers: ['kirocrew-core', 'kirocrew-cron'], filename: 'kirocrew.json' }, - { name: 'code-reviewer', description: 'Reviews code changes against the repo conventions', source: 'builtin', model: 'claude-sonnet-4.5', mcp_servers: [], filename: 'code-reviewer.json' }, - { name: 'release-captain', description: 'Cuts releases and babysits the pipeline', source: 'builtin', model: 'auto', mcp_servers: [], filename: 'release-captain.json' }, -] - -const DETAIL = { - kirocrew: { - name: 'kirocrew', - description: 'Autonomous personal AI agent', - model: 'claude-opus-4.8', - tools: ['execute_bash', 'fs_read', 'fs_write', 'code', 'grep', 'glob'], - mcpServers: { 'kirocrew-core': {}, 'kirocrew-cron': {} }, - }, - 'code-reviewer': { - name: 'code-reviewer', - description: 'Reviews code changes against the repo conventions', - model: 'claude-sonnet-4.5', - tools: ['fs_read', 'grep', 'glob'], - allowedTools: ['fs_read', 'grep'], - }, - 'release-captain': { - name: 'release-captain', - description: 'Cuts releases and babysits the pipeline', - model: 'auto', - tools: ['fs_read', 'execute_bash'], - }, -} - -function installed() { - return AGENTS.map(a => ({ - ...a, - skills: [ - ...(mapping[a.name] || []).map(k => k.split('/').pop()), - ...(UNMANAGED[a.name] || []).map(() => '*'), - ], - })) -} - -const { srv, base } = await serveDist() -const browser = await chromium.launch() -const context = await browser.newContext({ - viewport: { width: 1500, height: 950 }, - // Chips and labels are 11–13px type; a 1x shot renders soft on GitHub. - deviceScaleFactor: 2, -}) -const page = await context.newPage() - -// Feature fixtures only; boot-path endpoints come from the shared stub. -const extra = async (path, route) => { - const method = route.request().method() - - if (path.startsWith('/api/agents/detail/')) { - const name = decodeURIComponent(path.split('/').pop()) - if (method === 'PATCH') { - const body = JSON.parse(route.request().postData() || '{}') - if (Array.isArray(body.skills)) mapping[name] = body.skills - await json(route, { ok: true, model: DETAIL[name]?.model || '', skills: mapping[name] }) - return true - } - await json(route, { - ...(DETAIL[name] || { name }), - skills: mapping[name] || [], - unmanaged_skills: UNMANAGED[name] || [], - }) - return true - } - if (path === '/api/agents/installed') { await json(route, installed()); return true } - if (path === '/api/skills') { await json(route, SKILLS); return true } - if (path === '/api/config/default-agent') { await json(route, { default_agent: 'kirocrew' }); return true } - if (path.startsWith('/api/agent-metadata/')) { await json(route, { content: '' }); return true } - if (path === '/api/mcp/probe') { await json(route, []); return true } - if (path === '/api/spawn') { await json(route, { agents: [] }); return true } - if (path === '/api/sessions/context') { await json(route, { sessions: [] }); return true } - if (path === '/api/sessions/usage') { await json(route, { usage: null }); return true } - if (path === '/api/models') { - await json(route, [ - { model_name: 'auto', description: 'Let Kiro choose' }, - { model_name: 'claude-opus-4.8', description: 'Most capable' }, - { model_name: 'claude-sonnet-4.5', description: 'Balanced' }, - ]) - return true - } - return false -} -logPageProblems(page) -await stubDashboardApi(page, { extra }) - -async function load() { - await page.goto(base + '/capabilities?tab=templates', { waitUntil: 'domcontentloaded' }) - await page.waitForTimeout(2400) -} - -const shot = async name => { - await page.screenshot({ path: `${OUT}/${name}.png` }) - console.log('wrote', `${OUT}/${name}.png`) -} - -/** Crop to the Installed Agents card — the list + detail panel are the story. */ -async function card(name, pad = 16) { - const heading = page.getByRole('heading', { name: /Installed Agents/ }) - if (await heading.count()) { - const hb = await heading.first().boundingBox() - if (hb) { - const y0 = Math.max(0, hb.y - pad * 2) - await page.screenshot({ - path: `${OUT}/${name}.png`, - clip: { x: Math.max(0, hb.x - pad * 2), y: y0, width: 1440, height: Math.min(950 - y0, 560) }, - }) - console.log('wrote', `${OUT}/${name}.png`) - return - } - } - await shot(name) -} - -async function selectAgent(name) { - await page.getByText(name, { exact: true }).first().click() - await page.waitForTimeout(900) -} - -await load() - -// 1. An agent with no mapping: the honest empty state, not a silent blank. -await selectAgent('kirocrew') -await shot('01-agent-templates-no-mapping') -await card('02-no-mapping-card') - -// 2. An agent with skills already mapped — chips carry catalog display names. -await selectAgent('code-reviewer') -await card('03-mapped-skills-chips') - -// 3. The add-skill dropdown, filtered to the catalog minus what's mapped. -await page.getByRole('button', { name: /add skill/i }).click() -await page.waitForTimeout(500) -await shot('04-add-skill-dropdown') - -// 4. After adding: the chip row re-renders from the PATCH response. -await page.getByRole('option', { name: /babysit/i }).click() -await page.waitForTimeout(900) -await card('05-after-add') - -// 5. A hand-authored wildcard mapping: read-only, no remove control. -await selectAgent('release-captain') -await card('06-unmanaged-wildcard') - -await browser.close() -srv.close() -console.log('done') diff --git a/website/scripts/capture-agent-template-pane.mjs b/website/scripts/capture-agent-template-pane.mjs index 4a422573e89..36c88d0a664 100644 --- a/website/scripts/capture-agent-template-pane.mjs +++ b/website/scripts/capture-agent-template-pane.mjs @@ -1,6 +1,6 @@ /** - * Screenshot harness for the agent-template PANEL in the crew editor - * (flag `agent_template_pane`): the template selector is the header bar of a + * Screenshot harness for the agent-template PANEL in the crew editor: + * the template selector is the header bar of a * panel containing the definition it names. * * Runs the REAL built SPA (website/dist) behind the shared in-process static @@ -118,7 +118,7 @@ try { await stubDashboardApi(page, { extra: async (path, route) => { if (path === '/api/config/kirocrew') { - await json(route, { ...KIROCREW_CONFIG_FIXTURE, agent_template_pane: true }) + await json(route, KIROCREW_CONFIG_FIXTURE) return true } if (path === '/api/agents') { diff --git a/website/scripts/capture-agent-templates.mjs b/website/scripts/capture-agent-templates.mjs deleted file mode 100644 index 51277025095..00000000000 --- a/website/scripts/capture-agent-templates.mjs +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Screenshot harness for Agent Capabilities > Agent Templates. - * - * Runs the REAL built SPA (website/dist) behind the shared in-process static - * server and answers every /api/** call from fixtures via Playwright route - * interception — gateway-free, no kiro-cli, no dashboard auth. - * - * Shoots the four states the redesign is about: the roster + Overview pane, the - * Guardrails pane (a list that used to scroll inside a 420px box behind a - *
), the armed delete confirm, and the empty roster. - * - * Usage: node scripts/capture-agent-templates.mjs [outDir] [prefix] - * Run against the branch (after) and against a main build (before). - */ -import { chromium } from 'playwright' -import { mkdirSync } from 'node:fs' -import { serveDist } from './lib/serve-dist.mjs' -import { json, logPageProblems, stubDashboardApi } from './lib/stub-dashboard-api.mjs' - -const OUT = process.argv[2] || '../temp-screenshots/agent-templates' -const PREFIX = process.argv[3] || 'after' - -mkdirSync(OUT, { recursive: true }) - -const INSTALLED = [ - { - name: 'kirocrew', - description: 'Full crew agent — memory, crons, subagents, browser and the whole skill catalog.', - source: 'kirocrew', - model: 'claude-opus-5', - skills: ['prepare-pr', 'babysit', 'llm-council'], - mcp_servers: ['kirocrew-core', 'playwright'], - filename: 'kirocrew.json', - kirocrew_owned: true, - }, - { - name: 'kirocrew-lite', - description: 'Cheap, fast variant for cron jobs and one-shot checks.', - source: 'kirocrew', - model: '', - skills: ['babysit'], - mcp_servers: [], - filename: 'kirocrew-lite.json', - kirocrew_owned: true, - }, - { - name: 'reviewer', - description: 'Reads a diff and answers with findings only.', - source: 'builtin', - model: 'claude-sonnet-4.6', - skills: ['adversarial-review'], - mcp_servers: [], - filename: 'reviewer.json', - kirocrew_owned: false, - }, - { - name: 'scratch', - description: 'A copy to experiment on. No crew points at it, so it can be deleted.', - source: 'builtin', - model: '', - skills: [], - mcp_servers: [], - filename: 'scratch.json', - kirocrew_owned: false, - }, - { - name: 'oncall-triage', - description: 'Reads tickets, correlates alarms, drafts the first update.', - source: 'package', - model: 'claude-sonnet-4.6', - skills: ['mossy'], - mcp_servers: ['builder-mcp'], - package: 'oncall-radar', - filename: 'local-oncall-radar.json', - kirocrew_owned: false, - }, -] - -const DETAIL = { - kirocrew: { - prompt: 'file://~/.kiro/crew/prompts/kirocrew.md', - tools: ['fs_read', 'fs_write', 'execute_bash', 'use_aws', 'report_issue'], - allowedTools: ['fs_read', 'use_aws'], - mcpServers: { 'kirocrew-core': {}, playwright: { args: ['--include-tools', 'browser_navigate,browser_click'] } }, - toolsSettings: { - execute_bash: { - deniedCommands: [ - 'rm\\s+-rf\\s+/', - 'DROP\\s+TABLE', - 'git\\s+push\\s+.*--force', - 'curl\\s+.*\\|\\s*sh', - ':\\(\\)\\{\\s*:\\|:&\\s*\\};:', - ], - }, - }, - skills: ['kiro-user/prepare-pr', 'kiro-user/babysit', 'kiro-user/llm-council'], - unmanaged_skills: ['skill://~/custom/*'], - }, - reviewer: { - prompt: 'You review diffs. Report findings with a severity and a file:line. Never rewrite the code.', - tools: ['fs_read'], - allowedTools: ['fs_read'], - skills: ['kiro-user/adversarial-review'], - unmanaged_skills: [], - }, -} - -const CREWS = [ - { name: 'default', kiro_agent: 'kirocrew', workspace: 'default', memory_store: 'default', description: '', source: 'user' }, - { name: 'oncall', kiro_agent: 'kirocrew', workspace: 'oncall', memory_store: 'oncall', description: '', source: 'user' }, - { name: 'research', kiro_agent: 'reviewer', workspace: 'research', memory_store: 'research', description: '', source: 'user' }, -] - -/** Endpoints only the Agent Templates tab needs, layered on the boot stubs. */ -function templatesApi(installed) { - return async (path, route) => { - if (path === '/api/agents/installed') return json(route, installed), true - if (path.startsWith('/api/agents/detail/')) { - const name = decodeURIComponent(path.slice('/api/agents/detail/'.length)) - const listed = installed.find(a => a.name === name) || {} - return json(route, { ...listed, ...(DETAIL[name] || {}) }), true - } - if (path.startsWith('/api/agent-metadata/')) { - const name = decodeURIComponent(path.slice('/api/agent-metadata/'.length)) - const content = name === 'kirocrew' - ? 'Use for long multi-step engineering work: repo changes, PR loops, release ops.' - : '' - return json(route, { content }), true - } - if (path === '/api/agents') return json(route, { agents: CREWS, default_agent: 'default' }), true - if (path === '/api/config/default-agent') return json(route, { default_agent: 'kirocrew' }), true - if (path === '/api/sessions/context') return json(route, { sessions: [] }), true - if (path === '/api/sessions/usage') return json(route, { usage: null }), true - if (path === '/api/spawn') return json(route, { agents: [] }), true - if (path === '/api/mcp/probe') return json(route, []), true - if (path === '/api/skills') return json(route, []), true - return false - } -} - -async function openTemplatesTab(page, base, api) { - await stubDashboardApi(page, { extra: api }) - await page.goto(base + '/capabilities?tab=templates', { waitUntil: 'domcontentloaded' }) - const tab = page.locator('#main-content nav').getByRole('button', { name: 'Agent Templates', exact: true }) - await tab.waitFor({ state: 'visible', timeout: 15000 }) - await tab.click() -} - -async function main() { - const { srv, base } = await serveDist() - const browser = await chromium.launch() - const context = await browser.newContext({ - viewport: { width: 1400, height: 900 }, - deviceScaleFactor: 2, // 12-13px type renders soft at 1x on GitHub - }) - const page = await context.newPage() - logPageProblems(page) - - await openTemplatesTab(page, base, templatesApi(INSTALLED)) - - // Roster content, in whichever DOM the build under test uses: the redesign's - // listbox rows or main's plain list. Matching both keeps a `before` run - // against main from hanging for 15s and then failing. - await page.locator('#main-content [role="option"], #main-content .list-selected') - .first().waitFor({ state: 'visible', timeout: 15000 }) - await page.waitForTimeout(400) - await page.screenshot({ path: `${OUT}/${PREFIX}-overview.png` }) - const shot = [`${PREFIX}-overview.png`] - - // Guardrails pane — the state that most needed the room. - const guardrails = page.getByRole('tab', { name: /guardrails/i }) - if (await guardrails.count()) { - await guardrails.click() - await page.waitForTimeout(250) - await page.screenshot({ path: `${OUT}/${PREFIX}-guardrails.png` }) - shot.push(`${PREFIX}-guardrails.png`) - } - - // Delete withheld, and saying why: `reviewer` is bound by a crew, so the - // guard refuses it rather than leaving a dangling reference behind. - const boundRow = page.locator('#main-content [role="option"]', { hasText: 'reviewer' }).first() - if (await boundRow.count()) { - await boundRow.click() - await page.waitForTimeout(400) - await page.screenshot({ path: `${OUT}/${PREFIX}-delete-blocked.png` }) - shot.push(`${PREFIX}-delete-blocked.png`) - } - - // Armed delete, on the one template nothing points at. - const scratchRow = page.locator('#main-content [role="option"]', { hasText: 'scratch' }).first() - if (await scratchRow.count()) { - await scratchRow.click() - const del = page.getByTestId('delete-template') - await del.waitFor({ state: 'visible', timeout: 15000 }) - await del.click() - await page.getByTestId('confirm-delete-template').waitFor({ state: 'visible', timeout: 15000 }) - await page.waitForTimeout(200) - await page.screenshot({ path: `${OUT}/${PREFIX}-delete-confirm.png` }) - shot.push(`${PREFIX}-delete-confirm.png`) - } - - // Empty roster, in its own page so the stubs stay simple. - const empty = await context.newPage() - logPageProblems(empty) - await openTemplatesTab(empty, base, templatesApi([])) - await empty.waitForTimeout(600) - await empty.screenshot({ path: `${OUT}/${PREFIX}-empty.png` }) - shot.push(`${PREFIX}-empty.png`) - - console.log(`wrote ${shot.map(f => `${OUT}/${f}`).join(', ')}`) - - await browser.close() - srv.close() -} - -main().catch(err => { console.error(err); process.exit(1) }) diff --git a/website/scripts/lib/i18n-surfaces.mjs b/website/scripts/lib/i18n-surfaces.mjs index efbbfcd770a..d99d05e97c7 100644 --- a/website/scripts/lib/i18n-surfaces.mjs +++ b/website/scripts/lib/i18n-surfaces.mjs @@ -121,7 +121,6 @@ export const SURFACES = [ { id: 'capabilities-hooks', url: '/capabilities?tab=hooks' }, { id: 'capabilities-prompts', url: '/capabilities?tab=prompts' }, { id: 'capabilities-steering', url: '/capabilities?tab=steering' }, - { id: 'capabilities-templates', url: '/capabilities?tab=templates' }, { id: 'developer-archive', url: '/developer?tab=archive' }, { id: 'developer-config', url: '/developer?tab=config' }, { id: 'developer-logs', url: '/developer?tab=logs' }, diff --git a/website/src/api/client.ts b/website/src/api/client.ts index 95edbb4915d..ba509ef9509 100644 --- a/website/src/api/client.ts +++ b/website/src/api/client.ts @@ -2952,7 +2952,6 @@ export const api = { * restart, but against a config that may not match the sources. */ mcp_sync_ok: boolean }>, - sessionsContext: () => fetch('/api/sessions/context').then(j), sessionsMemory: () => fetch('/api/sessions/memory').then(j) as Promise<{ sessions: { key: string; title: string; slot_key: string; untitled: boolean @@ -2988,7 +2987,6 @@ export const api = { // vice versa). May reject with origin_missing / stale_binding / not_a_private_copy // / ambiguous_template_name / rebind_failed. agentReset: (name: string, crew: string) => fetch('/api/agents/detail/' + encodeURIComponent(name) + '/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ crew }) }).then(j), - agentDelete: (name: string) => fetch('/api/agents/detail/' + encodeURIComponent(name), { method: 'DELETE' }).then(j), // KiroCrew agents // sessionKey identifies the CHAT SLOT whose project scope applies. The // server resolves project-local agents through @@ -3743,7 +3741,6 @@ export const api = { spawnDelete: (id: string) => del('/api/spawn/' + encodeURIComponent(id)).then(j), spawnStopAll: (slot: string) => post('/api/spawn/stop-all', { slot }).then(j), spawnRetry: (id: string) => post('/api/spawn/' + encodeURIComponent(id) + '/retry', {}).then(j), - spawnClear: () => del('/api/spawn').then(j), approvals: (): Promise<{ id: string; source?: string; tool?: string; tool_input?: string; tool_call_id?: string; slot?: string; ts?: number }[]> => fetch('/api/approvals').then(j), resolveApproval: (id: string, action: 'approve' | 'reject' | 'reject_once') => post('/api/approvals/' + encodeURIComponent(id) + '/' + action, {}).then(j), /** Question cards still awaiting an answer, for rehydration after a reload or diff --git a/website/src/components/ChatPane.tsx b/website/src/components/ChatPane.tsx index 955a0ade3a9..5c8bf10f0b1 100644 --- a/website/src/components/ChatPane.tsx +++ b/website/src/components/ChatPane.tsx @@ -1508,7 +1508,7 @@ export default function ChatPane({ { switchAgent(name); agentDD.setOpen(false) }} /> toggleDefaultAgent(paneAgentName)} /> - { agentDD.setOpen(false); navigate('/capabilities?tab=templates') }} /> + { agentDD.setOpen(false); navigate('/capabilities?tab=crews') }} /> , document.body, )} diff --git a/website/src/components/ErrorBoundary.cov80.test.tsx b/website/src/components/ErrorBoundary.cov80.test.tsx index 718221ec8e7..376042d1641 100644 --- a/website/src/components/ErrorBoundary.cov80.test.tsx +++ b/website/src/components/ErrorBoundary.cov80.test.tsx @@ -145,4 +145,17 @@ describe('ErrorBoundary', () => { render() expect(vi.mocked(recordError).mock.calls[0][0]).toMatchObject({ message: 'ZzqNamedError' }) }) + + it('retryOnly suppresses the Ask-the-agent hand-off but keeps Try Again', () => { + // Inside an editor holding unsaved state (the crew sheet's template pane), + // the hand-off is a hard navigation that discards the draft — retryOnly is + // the contract that a contained crash cannot turn into data loss. + const { rerender } = render( + , + ) + expect(screen.queryByRole('button', { name: 'Ask the agent' })).not.toBeInTheDocument() + rerender() + fireEvent.click(screen.getByRole('button', { name: 'Try Again' })) + expect(screen.getByText('zzq-recovered')).toBeInTheDocument() + }) }) diff --git a/website/src/components/ErrorBoundary.tsx b/website/src/components/ErrorBoundary.tsx index 04e08835c8f..ad244fa8403 100644 --- a/website/src/components/ErrorBoundary.tsx +++ b/website/src/components/ErrorBoundary.tsx @@ -20,6 +20,13 @@ interface Props { root?: boolean /** Optional label to attribute logged errors to a region of the app. */ scope?: string + /** + * When true, the fallback offers Try Again only — no "Ask the agent" + * hand-off. Use inside editors holding unsaved state: the hand-off is a + * hard navigation to /chat, which unmounts the editor and discards the + * draft, turning a contained render crash into data loss. + */ + retryOnly?: boolean } export default class ErrorBoundary extends Component { @@ -114,12 +121,16 @@ export default class ErrorBoundary extends Component {
{this.state.error.message}
{/* "Ask the agent" is the primary here on purpose: this is an agent app, - and after a crash the agent is likelier to resolve it than a retry. */} - + and after a crash the agent is likelier to resolve it than a retry. + Suppressed under retryOnly — the hand-off is a hard navigation that + would discard a surrounding editor's unsaved state. */} + {!this.props.retryOnly && ( + + )}
diff --git a/website/src/components/PinSurfaceButton.test.tsx b/website/src/components/PinSurfaceButton.test.tsx index bca6e073bb9..8362e80e242 100644 --- a/website/src/components/PinSurfaceButton.test.tsx +++ b/website/src/components/PinSurfaceButton.test.tsx @@ -158,7 +158,7 @@ describe('PinSurfaceButton at the cap', () => { // a title attribute never appears in a screenshot. localStorage.setItem( NAV_PINNED_KEY, - JSON.stringify(['capabilities-crews', 'capabilities-templates', 'capabilities-skills', 'capabilities-mcp', 'capabilities-knowledge']), + JSON.stringify(['capabilities-crews', 'capabilities-hooks', 'capabilities-skills', 'capabilities-mcp', 'capabilities-knowledge']), ) renderAt('/capabilities?tab=steering') diff --git a/website/src/hooks/useAgentTemplatePane.ts b/website/src/hooks/useAgentTemplatePane.ts deleted file mode 100644 index 3cb795b210d..00000000000 --- a/website/src/hooks/useAgentTemplatePane.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * The `agent_template_pane` opt-in flag. - * - * The agent editor's Agent Template pane holds only the binding dropdown, so the - * definition it names — system prompt, skills, tools, MCP servers, guardrails — - * is readable only on the separate Agent Templates tab. Behind this flag the - * pane also renders that definition inline, which is the step that lets the - * standalone tab be retired. - * - * Held behind a flag because it changes where a shipped surface's content lives: - * set `agent_template_pane: true` in the running instance's - * `$KIROCREW_HOME/config.json`. Config is read live, so no gateway restart is - * needed. - * - * Same shape as `useConnectionsUi` on purpose — one predicate, one - * `['kirocrewConfig']` cache entry, so every surface gets the same answer. - */ -import { useQuery } from '@tanstack/react-query' -import { api } from '../api/client' - -const AGENT_TEMPLATE_PANE_FLAG = 'agent_template_pane' - -/** Absent config, a failed fetch, and truthy-but-not-`true` all resolve to false. */ -export function agentTemplatePaneEnabled(config: unknown): boolean { - return (config as Record | undefined)?.[AGENT_TEMPLATE_PANE_FLAG] === true -} - -/** Live flag value, off the shared `['kirocrewConfig']` query cache. */ -export function useAgentTemplatePaneEnabled(): boolean { - const { data } = useQuery({ queryKey: ['kirocrewConfig'], queryFn: () => api.kirocrewConfig() }) - return agentTemplatePaneEnabled(data) -} diff --git a/website/src/i18n/destructiveConfirm.test.ts b/website/src/i18n/destructiveConfirm.test.ts index a999f60bdf9..1a87ff01132 100644 --- a/website/src/i18n/destructiveConfirm.test.ts +++ b/website/src/i18n/destructiveConfirm.test.ts @@ -270,8 +270,6 @@ export const CONFIRM_OPERAND_KEY_EXEMPTIONS: Record = { 'apps.mochi.approval.inline_ask': 'the {{tool}} operand renders as a styled chip via renderAroundTool, ' + 'so glyph quotes would double-decorate it (#5725)', - 'pages.agentsPage.delete_the_template_named_confirm': - 'kind word "template" sits next to the operand (#4657)', 'pages.kiroCrewAgentsPage.delete_crew_named_confirm': 'kind word "crew" sits next to the operand (#4657)', 'apps.awsControl.console.backup_restore_foreign_confirm': diff --git a/website/src/i18n/en.context.json b/website/src/i18n/en.context.json index 96019dafb57..43712e9010b 100644 --- a/website/src/i18n/en.context.json +++ b/website/src/i18n/en.context.json @@ -242,9 +242,6 @@ "lib.effort.low": "Reasoning-effort level in the chat model picker: the least thinking time, fastest answers. An adjective naming an amount of effort, not a volume or a priority.", "lib.effort.max": "Reasoning-effort level in the chat model picker: the highest setting the model offers. Short for \"maximum\" — the top of a scale, not a verb.", "lib.sttProviders.downloading_decoder": "Progress line on the Voice settings page while the audio decoder (ffmpeg) is being fetched. Deliberately a separate key from the speech-model bar beside it, because both transfers can run at once and each has to name what it is downloading.", - "pages.agentsPage.id": "Column header over the short generated identifier of a spawned subagent run, e.g. \"470cf2d9\".", - "pages.agentsPage.status": "Column header over a subagent run state: Running, Done or Failed.", - "pages.agentsPage.task": "Column header over the one-line description of what a spawned subagent was asked to do. A noun, not the verb \"to task\".", "pages.appDetailPage.github_stars": "aria-label on the star icon beside the abbreviated GitHub stargazer count in the app detail page's hero subtitle. GitHub repository stars, not a rating.", "pages.appDetailPage.github_stars_2": "Row label in the app detail page's Details metadata card, with the exact stargazer count interpolated as {{value}}. GitHub repository stars, not a rating.", "pages.appDetailPage.v": "The \"v\" that prefixes a version number, rendered immediately before the digits with no space (`v1.2.3`). Most languages leave this untranslated.", diff --git a/website/src/i18n/locales/bn.json b/website/src/i18n/locales/bn.json index 0105da09a4a..469a7f32927 100644 --- a/website/src/i18n/locales/bn.json +++ b/website/src/i18n/locales/bn.json @@ -8489,88 +8489,16 @@ }, "pages": { "agentsPage": { - "account": "অ্যাকাউন্ট", - "account_not_reported": "রিপোর্ট করা হয়নি", - "account_not_reported_help": "সাইন-ইন করা পরিচয়কে যে অ্যাকাউন্টে এই ক্রেডিট বিল করা হয় তার সঙ্গে যুক্ত করা গেলে দেখানো হয়। এই সংযোগ সবসময় পাওয়া যায় না, তাই ক্রেডিট দেখা গেলেও এটি রিপোর্ট না করা অবস্থায় থাকতে পারে।", - "agent_templates": "এজেন্ট টেমপ্লেট", "auto_approved": "স্বয়ংক্রিয়ভাবে অনুমোদিত", - "awaiting_first_prompt": "প্রথম প্রম্পটের অপেক্ষায়", - "clear_completed": "সম্পন্নগুলো মুছুন", - "consumption_for_current_billing_period": "বর্তমান বিলিং সময়কালে {{provider}} ব্যবহার। 10 মিনিট ক্যাশ করা।", - "context_window_usage": "কনটেক্সট উইন্ডো ব্যবহার", - "context_window_usage_tip": "প্রতিটি সক্রিয় {{label}} সেশনের লাইভ কনটেক্সট উইন্ডো ব্যবহার। কাস্টম এজেন্ট তাদের কনফিগার করা মডেল দেখায়। 90%-এ কমপ্যাকশন শুরু হয়।", "could_not_load_this_agent_s_configuration_skills": "এই এজেন্টের কনফিগারেশন লোড করা যায়নি — ওভাররাইট হওয়া এড়াতে\n স্কিল লুকানো হয়েছে। আবার চেষ্টা করতে এজেন্টটি পুনরায় নির্বাচন করুন।", - "critical": "গুরুতর", - "default": "ডিফল্ট", - "delete_subagent": "সাবএজেন্ট {{id}} মুছে ফেলো", - "denied_commands": "নিষিদ্ধ কমান্ড", - "done": "সম্পন্ন", - "est_cost": "আনু. খরচ:", "failed": "ব্যর্থ", - "filter_models": "মডেল ফিল্টার করুন", - "harness": "হার্নেস", - "healthy": "স্বাভাবিক", - "high": "উচ্চ", - "idle": "নিষ্ক্রিয়", - "installed_agents": "ইনস্টল করা এজেন্ট", - "loading_agents": "এজেন্ট লোড হচ্ছে…", "mcp_servers": "MCP সার্ভার", - "model_list": "মডেলের তালিকা", - "no_active_sessions": "কোনো সক্রিয় সেশন নেই", - "no_subagents": "কোনো সাবএজেন্ট নেই", - "overage_credits": "অতিরিক্ত ক্রেডিট:", - "plan_credits": "প্ল্যান ক্রেডিট", - "prompt_one": "{{count}}টি প্রম্পট", - "prompt_other": "{{count}}টি প্রম্পট", - "req": "/req", - "resets": "রিসেট হবে", - "running": "চলছে", - "select_an_agent_to_view_details": "বিস্তারিত দেখতে একটি এজেন্ট নির্বাচন করুন", "skills": "স্কিল", - "spawn_tasks_from_chat_or_cli": "চ্যাট বা CLI থেকে টাস্ক চালু করুন", - "subagents": "সাবএজেন্ট", "system_prompt": "সিস্টেম প্রম্পট", - "tools": "টুল", - "tools_2": "টুল", - "type_to_filter": "ফিল্টার করতে টাইপ করুন…", - "usage": "ব্যবহার", - "at_a_glance": "এক নজরে", - "cancel": "বাতিল", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI চ্যাট, চ্যাট চ্যানেলের থ্রেড আর ওয়ার্ম-পুল সেশন এটিতে ফিরে আসে — এজেন্ট নিজের টেমপ্লেট বলে দেয়", "comes_with_the_template_read_only_here": "টেমপ্লেটের সাথেই আসে — এখানে শুধু দেখা যায়", - "config_file": "কনফিগ ফাইল", - "delete_template": "টেমপ্লেট মুছে ফেলুন", - "delete_the_template_named_confirm": "{{name}} টেমপ্লেট মুছে ফেলবেন? এর কনফিগ ফাইল মুছে যাবে এবং এটি ফেরানো যাবে না।", - "editing_this_template_changes_how_they_behave_in_": "এই টেমপ্লেট বদলালে পরের সেশনে তাদের আচরণ বদলে যাবে", - "filter_templates": "টেমপ্লেট ফিল্টার করুন", - "from_package": "প্যাকেজ থেকে", "guardrails": "সুরক্ষা নিয়ম", - "id": "ID", - "model": "মডেল", - "no_agent_templates_yet": "এখনো কোনো এজেন্ট টেমপ্লেট নেই", - "no_crews_use_this_template_yet": "এখনো কোনো এজেন্ট এই টেমপ্লেট ব্যবহার করে না", - "no_default_template": "কোনো ডিফল্ট টেমপ্লেট নেই", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "এখানে কোনো মডেল পিন করা নেই — ইনহেরিট করা এজেন্ট গ্লোবাল ডিফল্টে ফিরে যাবে", - "no_templates_match_your_filter": "আপনার ফিল্টারের সাথে কোনো টেমপ্লেট মেলে না", "not_set_for_this_template": "এই টেমপ্লেটের জন্য সেট করা নেই", - "overview": "ওভারভিউ", - "package": "প্যাকেজ", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "এখানে পিন করা — যে এজেন্ট ইনহেরিট করা আছে সেটি এই মডেল ব্যবহার করবে", - "source": "সোর্স", - "status": "স্ট্যাটাস", - "task": "টাস্ক", - "templates_come_with_kirocrew_or_with_an_app_you_i": "টেমপ্লেট {{productName}}-এর সাথে আসে, বা আপনি ইনস্টল করা কোনো অ্যাপের সাথে", - "this_is_the_fallback_template_point_the_picker_a": "ফলব্যাক টেমপ্লেট মোছা যায় না — আগে উপরের সিলেক্টরটি অন্য টেমপ্লেটে নিয়ে যান", - "tools_and_mcp": "টুল ও MCP", - "used_by": "যেখানে ব্যবহৃত", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "{{crews}} এটি ব্যবহার করছে — মোছার আগে এজেন্ট ট্যাবে গিয়ে তাদের অন্য টেমপ্লেটে নিয়ে যান", - "what_a_crew_starts_from_its_model_skills_tools_mc": "একটি এজেন্ট যা থেকে শুরু করে — তার মডেল, স্কিল, টুল, MCP সার্ভার আর সুরক্ষা নিয়ম", - "when_nothing_names_a_template_use": "যখন কোনো টেমপ্লেট বলা হয়নি, তখন ব্যবহার করা হবে", - "where_it_comes_from": "এটি কোথা থেকে এসেছে", - "yes_delete_it": "হ্যাঁ, মুছে ফেলুন", - "retry": "আবার চেষ্টা করুন", - "delete_unavailable_references_failed": "ক্রু রোস্টার বা ফলব্যাক-টেমপ্লেট সেটিং লোড করা যায়নি, তাই সেগুলি লোড না হওয়া পর্যন্ত এই টেমপ্লেট মোছা যাবে না।", - "failed_to_load_agents": "এজেন্ট লোড করা যায়নি" + "tools_and_mcp": "টুল ও MCP" }, "aidlc": { "dagView": { @@ -9195,8 +9123,6 @@ "skills_label": "স্কিল", "steering_description": "~/.kiro/steering এবং আপনার প্রকল্পের .kiro/steering থেকে Markdown নিয়ম, প্রতিটি ফাইলের অন্তর্ভুক্তির মোড অনুযায়ী প্রযোজ্য", "steering_label": "স্টিয়ারিং", - "templates_description": "প্রতিটি এজেন্ট কী থেকে শুরু করে তা পরিচালনা করুন — তার মডেল, স্কিল, টুল, MCP সার্বার আর সুরক্ষা নিয়ম", - "templates_label": "এজেন্ট টেমপ্লেট", "knowledge_label": "নলেজ", "knowledge_description": "ডকুমেন্ট ও সোর্স যা এজেন্ট উত্তর দেওয়ার সময় খুঁজতে পারে", "group_agent": "এজেন্ট", diff --git a/website/src/i18n/locales/de.json b/website/src/i18n/locales/de.json index e4b706de4b0..9f4f5636560 100644 --- a/website/src/i18n/locales/de.json +++ b/website/src/i18n/locales/de.json @@ -8489,88 +8489,16 @@ }, "pages": { "agentsPage": { - "account": "Konto", - "account_not_reported": "Nicht gemeldet", - "account_not_reported_help": "Wird angezeigt, sobald die angemeldete Identität dem Konto zugeordnet werden kann, dem diese Credits in Rechnung gestellt werden. Diese Zuordnung ist nicht immer verfügbar, daher kann dies auch dann ungemeldet bleiben, wenn Credits angezeigt werden.", - "agent_templates": "Agenten-Vorlagen", "auto_approved": "Automatisch genehmigt", - "awaiting_first_prompt": "wartet auf ersten Prompt", - "clear_completed": "Abgeschlossene löschen", - "consumption_for_current_billing_period": "{{provider}}-Verbrauch für den aktuellen Abrechnungszeitraum. 10 Min. zwischengespeichert.", - "context_window_usage": "Nutzung des Kontextfensters", - "context_window_usage_tip": "Live-Auslastung des Kontextfensters pro aktiver {{label}}-Sitzung. Eigene Agenten zeigen ihr konfiguriertes Modell. Die Verdichtung wird bei 90 % ausgelöst.", "could_not_load_this_agent_s_configuration_skills": "Die Konfiguration dieses Agenten konnte nicht geladen werden — Skills werden ausgeblendet, um zu\n vermeiden, dass sie überschrieben werden. Wählen Sie den Agenten erneut aus, um es noch einmal zu versuchen.", - "critical": "Kritisch", - "default": "Standard", - "delete_subagent": "Subagent {{id}} löschen", - "denied_commands": "Verbotene Befehle", - "done": "Fertig", - "est_cost": "Gesch. Kosten:", "failed": "Fehlgeschlagen", - "filter_models": "Modelle filtern", - "harness": "Harness", - "healthy": "Fehlerfrei", - "high": "Hoch", - "idle": "Inaktiv", - "installed_agents": "Installierte Agenten", - "loading_agents": "Agenten werden geladen…", "mcp_servers": "MCP-Server", - "model_list": "Modellliste", - "no_active_sessions": "Keine aktiven Sitzungen", - "no_subagents": "Keine Subagenten", - "overage_credits": "Zusatz-Credits:", - "plan_credits": "Plan-Credits", - "prompt_one": "{{count}} Prompt", - "prompt_other": "{{count}} Prompts", - "req": "/Anfr.", - "resets": "Zurücksetzungen", - "running": "Läuft", - "select_an_agent_to_view_details": "Wählen Sie einen Agenten, um Details anzuzeigen", "skills": "Skills", - "spawn_tasks_from_chat_or_cli": "Aufgaben aus dem Chat oder der CLI starten", - "subagents": "Subagenten", "system_prompt": "System-Prompt", - "tools": "Werkzeuge", - "tools_2": "Werkzeuge", - "type_to_filter": "Zum Filtern tippen…", - "usage": "Verbrauch", - "at_a_glance": "Auf einen Blick", - "cancel": "Abbrechen", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI-Chat, Threads in Chat-Kanälen und Warm-Pool-Sitzungen greifen darauf zurück — Agenten nennen ihre eigene", "comes_with_the_template_read_only_here": "Kommt mit der Vorlage — hier nur lesbar", - "config_file": "Konfigurationsdatei", - "delete_template": "Vorlage löschen", - "delete_the_template_named_confirm": "Vorlage {{name}} löschen? Ihre Konfigurationsdatei wird entfernt und das lässt sich nicht rückgängig machen.", - "editing_this_template_changes_how_they_behave_in_": "Wenn du diese Vorlage änderst, verhalten sie sich in ihrer nächsten Sitzung anders", - "filter_templates": "Vorlagen filtern", - "from_package": "Aus Paket", "guardrails": "Schutzregeln", - "id": "ID", - "model": "Modell", - "no_agent_templates_yet": "Noch keine Agent-Vorlagen", - "no_crews_use_this_template_yet": "Noch kein Agent verwendet diese Vorlage", - "no_default_template": "Keine Standardvorlage", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Hier ist kein Modell festgelegt — ein Agent mit „Geerbt“ fällt auf den globalen Standard zurück", - "no_templates_match_your_filter": "Keine Vorlage passt zum Filter", "not_set_for_this_template": "Für diese Vorlage nicht gesetzt", - "overview": "Übersicht", - "package": "Paket", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Hier festgelegt — ein Agent mit „Geerbt“ verwendet dieses Modell", - "source": "Quelle", - "status": "Status", - "task": "Aufgabe", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Vorlagen kommen mit {{productName}} oder mit einer App, die du installierst", - "this_is_the_fallback_template_point_the_picker_a": "Die Rückfall-Vorlage kann nicht gelöscht werden — richte den Auswähler oben zuerst auf eine andere", - "tools_and_mcp": "Tools und MCP", - "used_by": "Verwendet von", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Wird von {{crews}} verwendet — richte sie im Tab Agenten erst auf eine andere Vorlage", - "what_a_crew_starts_from_its_model_skills_tools_mc": "Womit ein Agent startet — sein Modell, seine Skills, seine Tools, seine MCP-Server und seine Schutzregeln", - "when_nothing_names_a_template_use": "Wenn nichts eine Vorlage nennt, verwende", - "where_it_comes_from": "Woher sie kommt", - "yes_delete_it": "Ja, löschen", - "retry": "Wiederholen", - "delete_unavailable_references_failed": "Die Crew-Liste oder die Einstellung für die Fallback-Vorlage konnte nicht geladen werden, daher kann diese Vorlage erst gelöscht werden, wenn sie geladen sind.", - "failed_to_load_agents": "Agenten konnten nicht geladen werden" + "tools_and_mcp": "Tools und MCP" }, "aidlc": { "dagView": { @@ -9195,8 +9123,6 @@ "skills_label": "Fähigkeiten", "steering_description": "Markdown-Konventionen aus ~/.kiro/steering und dem .kiro/steering deines Projekts, angewendet je nach Einbindungsmodus der Datei", "steering_label": "Steuerung", - "templates_description": "Verwalte, womit jeder Agent startet: Modell, Skills, Tools, MCP-Server und Schutzregeln", - "templates_label": "Agent-Vorlagen", "knowledge_label": "Wissen", "knowledge_description": "Dokumente und Quellen, die der Agent beim Antworten durchsuchen kann", "group_agent": "Agent", diff --git a/website/src/i18n/locales/en-XA.json b/website/src/i18n/locales/en-XA.json index ac07dbe7133..26358225a3c 100644 --- a/website/src/i18n/locales/en-XA.json +++ b/website/src/i18n/locales/en-XA.json @@ -8359,88 +8359,16 @@ }, "pages": { "agentsPage": { - "account": "[Àççøùñţ ···········]", - "account_not_reported": "[Ñøţ ŕèþøŕţèð ···········]", - "account_not_reported_help": "[Şĥøẁñ øñçè ţĥè şìğñèð-ìñ ìðèñţìţý çàñ ƀè ţìèð ţø ţĥè àççøùñţ ţĥèşè çŕèðìţş àŕè ƀìĺĺèð ţø. Ţĥàţ ĺìñķ ìş ñøţ àĺẁàýş àṽàìĺàƀĺè, şø ţĥìş çàñ şţàý ùñŕèþøŕţèð èṽèñ ẁĥìĺè çŕèðìţş àŕè şĥøẁñ. ·······················································]", - "agent_templates": "[Àğèñţ Ţèɱþĺàţèş ··············]", "auto_approved": "[Àùţø-Àþþŕøṽèð ············]", - "awaiting_first_prompt": "[àẁàìţìñğ ƒìŕşţ þŕøɱþţ ···············]", - "clear_completed": "[Çĺèàŕ çøɱþĺèţèð ··············]", - "context_window_usage": "[Çøñţèẋţ Ẁìñðøẁ Ùşàğè ··················]", "could_not_load_this_agent_s_configuration_skills": "[Çøùĺð ñøţ ĺøàð ţĥìş àğèñţ'ş çøñƒìğùŕàţìøñ — şķìĺĺş àŕè ĥìððèñ ţø àṽøìð\n øṽèŕẁŕìţìñğ ţĥèɱ. Ŕèşèĺèçţ ţĥè àğèñţ ţø ŕèţŕý. ··········································]", - "critical": "[Çŕìţìçàĺ ············]", - "denied_commands": "[Ðèñìèð Çøɱɱàñðş ··············]", - "done": "[Ðøñè ······]", - "est_cost": "[Èşţ. çøşţ: ···············]", "failed": "[Ƒàìĺèð ·········]", - "filter_models": "[Ƒìĺţèŕ ɱøðèĺş ············]", - "harness": "[Ĥàŕñèşş ···········]", - "healthy": "[Ĥèàĺţĥý ···········]", - "high": "[Ĥìğĥ ······]", - "idle": "[Ìðĺè ······]", - "installed_agents": "[Ìñşţàĺĺèð Àğèñţş ··············]", - "loading_agents": "[Ĺøàðìñğ àğèñţş… ··············]", "mcp_servers": "[ṀÇÞ Şèŕṽèŕş ··········]", - "model_list": "[Ṁøðèĺ ĺìşţ ···············]", - "no_active_sessions": "[Ñø àçţìṽè şèşşìøñş ················]", - "no_subagents": "[Ñø şùƀàğèñţş ···········]", - "overage_credits": "[Øṽèŕàğè çŕèðìţş: ··············]", - "plan_credits": "[Þĺàñ Çŕèðìţş ···········]", - "prompt_one": "[{{count}} þŕøɱþţ ···········]", - "prompt_other": "[{{count}} þŕøɱþţş ············]", - "req": "[/ŕèǫ ······]", - "resets": "[ŕèşèţş ·········]", - "running": "[Ŕùññìñğ ···········]", - "select_an_agent_to_view_details": "[Şèĺèçţ àñ àğèñţ ţø ṽìèẁ ðèţàìĺş ················]", "skills": "[Şķìĺĺş ·········]", - "spawn_tasks_from_chat_or_cli": "[Şþàẁñ ţàşķş ƒŕøɱ çĥàţ øŕ ÇĹÌ ····················]", - "subagents": "[Şùƀàğèñţş ··············]", "system_prompt": "[Şýşţèɱ Þŕøɱþţ ············]", - "tools": "[Ţøøĺş ········]", - "tools_2": "[ţøøĺş ········]", - "type_to_filter": "[Ţýþè ţø ƒìĺţèŕ… ··············]", - "usage": "[Ùşàğè ········]", - "at_a_glance": "[Àţ à ğĺàñçè ··········]", - "cancel": "[Çàñçèĺ ·········]", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "[ÇĹÌ çĥàţ, çĥàţ-çĥàññèĺ ţĥŕèàðş àñð ẁàŕɱ-þøøĺ şèşşìøñş ƒàĺĺ ƀàçķ ţø ţĥìş — àğèñţş ñàɱè ţĥèìŕ øẁñ ·····························]", "comes_with_the_template_read_only_here": "[Çøɱèş ẁìţĥ ţĥè ţèɱþĺàţè — ŕèàð-øñĺý ĥèŕè ····················]", - "config_file": "[Çøñƒìğ ƒìĺè ··········]", - "delete_template": "[Ðèĺèţè ţèɱþĺàţè ··············]", - "delete_the_template_named_confirm": "[Ðèĺèţè ţĥè ţèɱþĺàţè {{name}}? Ìţş çøñƒìğ ƒìĺè ìş ŕèɱøṽèð àñð ţĥìş çàññøţ ƀè ùñðøñè. ·······················]", - "editing_this_template_changes_how_they_behave_in_": "[Èðìţìñğ ţĥìş ţèɱþĺàţè çĥàñğèş ĥøẁ ţĥèý ƀèĥàṽè ìñ ţĥèìŕ ñèẋţ şèşşìøñ ·······················]", - "filter_templates": "[Ƒìĺţèŕ ţèɱþĺàţèş ··············]", - "from_package": "[Ƒŕøɱ þàçķàğè ···········]", "guardrails": "[Ğùàŕðŕàìĺş ···············]", - "id": "[ÌÐ ···]", - "model": "[Ṁøðèĺ ········]", - "no_agent_templates_yet": "[Ñø àğèñţ ţèɱþĺàţèş ýèţ ···············]", - "no_crews_use_this_template_yet": "[Ñø àğèñţş ùşè ţĥìş ţèɱþĺàţè ýèţ ················]", - "no_default_template": "[Ñø ðèƒàùĺţ ţèɱþĺàţè ·················]", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "[Ñø ɱøðèĺ þìññèð ĥèŕè — àñ àğèñţ şèţ ţø ìñĥèŕìţ ƒàĺĺş ƀàçķ ţø ţĥè ğĺøƀàĺ ðèƒàùĺţ ························]", - "no_templates_match_your_filter": "[Ñø ţèɱþĺàţèş ɱàţçĥ ýøùŕ ƒìĺţèŕ ·····················]", "not_set_for_this_template": "[Ñøţ şèţ ƒøŕ ţĥìş ţèɱþĺàţè ··················]", - "overview": "[Øṽèŕṽìèẁ ············]", - "package": "[Þàçķàğè ···········]", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "[Þìññèð ĥèŕè — àñ àğèñţ şèţ ţø ìñĥèŕìţ ùşèş ţĥìş ɱøðèĺ ···················]", - "source": "[Şøùŕçè ·········]", - "status": "[Şţàţùş ·········]", - "task": "[Ţàşķ ······]", - "templates_come_with_kirocrew_or_with_an_app_you_i": "[Ţèɱþĺàţèş çøɱè ẁìţĥ {{productName}} øŕ ẁìţĥ àñ àþþ ýøù ìñşţàĺĺ ························]", - "this_is_the_fallback_template_point_the_picker_a": "[Ţĥè ƒàĺĺƀàçķ ţèɱþĺàţè çàññøţ ƀè ðèĺèţèð — þøìñţ ţĥè þìçķèŕ àƀøṽè àţ àñøţĥèŕ øñè ƒìŕşţ ··························]", - "tools_and_mcp": "[Ţøøĺş & ṀÇÞ ··········]", - "used_by": "[Ùşèð Ɓý ···········]", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "[Ùşèð ƀý {{crews}} — ŕèþøìñţ ţĥèɱ øñ ţĥè Àğèñţş ţàƀ ƀèƒøŕè ðèĺèţìñğ ţĥìş ······················]", - "what_a_crew_starts_from_its_model_skills_tools_mc": "[Ẁĥàţ àñ àğèñţ şţàŕţş ƒŕøɱ — ìţş ɱøðèĺ, şķìĺĺş, ţøøĺş, ṀÇÞ şèŕṽèŕş àñð ğùàŕðŕàìĺş ························]", - "when_nothing_names_a_template_use": "[Ẁĥèñ ñøţĥìñğ ñàɱèş à ţèɱþĺàţè, ùşè ·················]", - "where_it_comes_from": "[Ẁĥèŕè ìţ çøɱèş ƒŕøɱ ·················]", - "yes_delete_it": "[Ýèş, ðèĺèţè ìţ ·············]", - "consumption_for_current_billing_period": "[{{provider}} çøñşùɱþţìøñ ƒøŕ ţĥè çùŕŕèñţ ƀìĺĺìñğ þèŕìøð. Çàçĥèð 10 ɱìñ. ·····················]", - "context_window_usage_tip": "[Ĺìṽè çøñţèẋţ ẁìñðøẁ ùţìĺìžàţìøñ þèŕ àçţìṽè {{label}} şèşşìøñ. Çùşţøɱ àğèñţş şĥøẁ ţĥèìŕ çøñƒìğùŕèð ɱøðèĺ. Çøɱþàçţìøñ ţŕìğğèŕş àţ 90%. ·····································]", - "default": "[Ðèƒàùĺţ ···········]", - "delete_subagent": "[Ðèĺèţè şùƀàğèñţ {{id}} ··············]", - "retry": "[Ŕèţŕý ········]", - "delete_unavailable_references_failed": "[Çøùĺðñ'ţ ĺøàð ţĥè çŕèẁ ŕøşţèŕ øŕ ţĥè ƒàĺĺƀàçķ-ţèɱþĺàţè şèţţìñğ, şø ðèĺèţìñğ ţĥìş ţèɱþĺàţè ìş ùñàṽàìĺàƀĺè ùñţìĺ ţĥèý ĺøàð. ····································]", - "failed_to_load_agents": "[Ƒàìĺèð ţø ĺøàð àğèñţş ···············]" + "tools_and_mcp": "[Ţøøĺş & ṀÇÞ ··········]" }, "aidlc": { "dagView": { @@ -9053,8 +8981,6 @@ "agent_capabilities": "[Àğèñţ Çàþàƀìĺìţìèş ················]", "crews_label": "[Àğèñţş ·········]", "crews_description": "[Àğèñţş ýøù çĥàţ ẁìţĥ, èàçĥ ẁìţĥ ìţş øẁñ ẁøŕķşþàçè ·························]", - "templates_label": "[Àğèñţ Ţèɱþĺàţèş ··············]", - "templates_description": "[Ṁàñàğè ẁĥàţ èàçĥ àğèñţ şţàŕţş ƒŕøɱ: ìţş ɱøðèĺ, şķìĺĺş, ţøøĺş, ṀÇÞ şèŕṽèŕş, àñð ğùàŕðŕàìĺş ···························]", "connections_label": "[Çøññèçţìøñş ··········]", "connections_description": "[Çøññèçţ øƒƒìçìàĺ şèŕṽìçèş àñð ɱàñàğè çùşţøɱ ṀÇÞ şèŕṽèŕş ···················]", "skills_label": "[Şķìĺĺş ·········]", diff --git a/website/src/i18n/locales/en.json b/website/src/i18n/locales/en.json index 9cf6584b857..9313393bb12 100644 --- a/website/src/i18n/locales/en.json +++ b/website/src/i18n/locales/en.json @@ -5541,81 +5541,16 @@ }, "pages": { "agentsPage": { - "account": "Account", - "account_not_reported": "Not reported", - "account_not_reported_help": "Shown once the signed-in identity can be tied to the account these credits are billed to. That link is not always available, so this can stay unreported even while credits are shown.", - "agent_templates": "Agent Templates", "auto_approved": "Auto-Approved", - "awaiting_first_prompt": "awaiting first prompt", - "clear_completed": "Clear completed", - "context_window_usage": "Context Window Usage", "could_not_load_this_agent_s_configuration_skills": "Could not load this agent's configuration — skills are hidden to avoid\n overwriting them. Reselect the agent to retry.", - "critical": "Critical", - "denied_commands": "Denied Commands", - "done": "Done", - "est_cost": "Est. cost:", "failed": "Failed", - "filter_models": "Filter models", - "harness": "Harness", - "healthy": "Healthy", - "high": "High", - "idle": "Idle", - "installed_agents": "Installed Agents", - "loading_agents": "Loading agents…", "mcp_servers": "MCP Servers", - "model_list": "Model list", - "no_active_sessions": "No active sessions", - "no_subagents": "No subagents", - "overage_credits": "Overage credits:", - "plan_credits": "Plan Credits", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompts", - "req": "/req", - "resets": "resets", - "running": "Running", - "select_an_agent_to_view_details": "Select an agent to view details", "skills": "Skills", - "spawn_tasks_from_chat_or_cli": "Spawn tasks from chat or CLI", - "subagents": "Subagents", "system_prompt": "System Prompt", - "tools": "Tools", - "tools_2": "tools", - "type_to_filter": "Type to filter…", - "usage": "Usage", - "at_a_glance": "At a glance", - "cancel": "Cancel", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI chat, chat-channel threads and warm-pool sessions fall back to this — agents name their own", "comes_with_the_template_read_only_here": "Comes with the template — read-only here", - "config_file": "Config file", - "delete_template": "Delete template", - "delete_the_template_named_confirm": "Delete the template {{name}}? Its config file is removed and this cannot be undone.", - "editing_this_template_changes_how_they_behave_in_": "Editing this template changes how they behave in their next session", - "filter_templates": "Filter templates", - "from_package": "From package", "guardrails": "Guardrails", - "id": "ID", - "model": "Model", - "no_agent_templates_yet": "No agent templates yet", - "no_crews_use_this_template_yet": "No agents use this template yet", - "no_default_template": "No default template", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "No model pinned here — an agent set to inherit falls back to the global default", - "no_templates_match_your_filter": "No templates match your filter", "not_set_for_this_template": "Not set for this template", - "overview": "Overview", - "package": "Package", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Pinned here — an agent set to inherit uses this model", - "source": "Source", - "status": "Status", - "task": "Task", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Templates come with {{productName}} or with an app you install", - "this_is_the_fallback_template_point_the_picker_a": "The fallback template cannot be deleted — point the picker above at another one first", - "tools_and_mcp": "Tools & MCP", - "used_by": "Used By", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Used by {{crews}} — repoint them on the Agents tab before deleting this", - "what_a_crew_starts_from_its_model_skills_tools_mc": "What an agent starts from — its model, skills, tools, MCP servers and guardrails", - "when_nothing_names_a_template_use": "When nothing names a template, use", - "where_it_comes_from": "Where it comes from", - "yes_delete_it": "Yes, delete it" + "tools_and_mcp": "Tools & MCP" }, "aidlc": { "dagView": { diff --git a/website/src/i18n/locales/en.manual.json b/website/src/i18n/locales/en.manual.json index 20789b36466..ff92bb894eb 100644 --- a/website/src/i18n/locales/en.manual.json +++ b/website/src/i18n/locales/en.manual.json @@ -3155,15 +3155,6 @@ "crew_members": "Crew Members" }, "pages": { - "agentsPage": { - "consumption_for_current_billing_period": "{{provider}} consumption for the current billing period. Cached 10 min.", - "context_window_usage_tip": "Live context window utilization per active {{label}} session. Custom agents show their configured model. Compaction triggers at 90%.", - "default": "Default", - "delete_subagent": "Delete subagent {{id}}", - "retry": "Retry", - "delete_unavailable_references_failed": "Couldn't load the crew roster or the fallback-template setting, so deleting this template is unavailable until they load.", - "failed_to_load_agents": "Failed to load agents" - }, "aidlc": { "taskDetailPanel": { "depend_on_task": "Depend on Task {{n}}: {{title}}", @@ -3401,8 +3392,6 @@ "capabilitiesPage": { "crews_label": "Agents", "crews_description": "Agents you chat with, each with its own workspace", - "templates_label": "Agent Templates", - "templates_description": "Manage what each agent starts from: its model, skills, tools, MCP servers, and guardrails", "connections_label": "Connections", "connections_description": "Connect official services and manage custom MCP servers", "skills_label": "Skills", diff --git a/website/src/i18n/locales/es.json b/website/src/i18n/locales/es.json index ae411e2c26d..5e3eff56758 100644 --- a/website/src/i18n/locales/es.json +++ b/website/src/i18n/locales/es.json @@ -8604,89 +8604,16 @@ }, "pages": { "agentsPage": { - "account": "Cuenta", - "account_not_reported": "No informado", - "account_not_reported_help": "Se muestra cuando la identidad con la que has iniciado sesión puede vincularse a la cuenta a la que se facturan estos créditos. Ese vínculo no siempre está disponible, por lo que esto puede seguir sin informarse aunque se muestren los créditos.", - "agent_templates": "Plantillas de agente", "auto_approved": "Aprobado automáticamente", - "awaiting_first_prompt": "esperando el primer prompt", - "clear_completed": "Borrar completadas", - "consumption_for_current_billing_period": "Consumo de {{provider}} en el periodo de facturación actual. En caché 10 min.", - "context_window_usage": "Uso de la ventana de contexto", - "context_window_usage_tip": "Uso en vivo de la ventana de contexto por cada sesión activa de {{label}}. Los agentes personalizados muestran el modelo que tienen configurado. La compactación se activa al 90%.", "could_not_load_this_agent_s_configuration_skills": "No se pudo cargar la configuración de este agente: las habilidades se ocultan para evitar\n sobrescribirlas. Vuelve a seleccionar el agente para reintentar.", - "critical": "Crítico", - "default": "Predeterminado", - "delete_subagent": "Eliminar el subagente {{id}}", - "denied_commands": "Comandos denegados", - "done": "Completado", - "est_cost": "Coste est.:", "failed": "Fallido", - "filter_models": "Filtrar modelos", - "harness": "Harness", - "healthy": "Correcto", - "high": "Alto", - "idle": "Inactivo", - "installed_agents": "Agentes instalados", - "loading_agents": "Cargando agentes…", "mcp_servers": "Servidores MCP", - "model_list": "Lista de modelos", - "no_active_sessions": "No hay sesiones activas", - "no_subagents": "No hay subagentes", - "overage_credits": "Créditos excedentes:", - "plan_credits": "Créditos del plan", - "prompt_many": "{{count}} prompts", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompts", - "req": "/req", - "resets": "se restablece", - "running": "En ejecución", - "select_an_agent_to_view_details": "Selecciona un agente para ver los detalles", "skills": "Habilidades", - "spawn_tasks_from_chat_or_cli": "Lanza tareas desde el chat o la CLI", - "subagents": "Subagentes", "system_prompt": "Prompt del sistema", - "tools": "Herramientas", - "tools_2": "herramientas", - "type_to_filter": "Escribe para filtrar…", - "usage": "Uso", - "at_a_glance": "De un vistazo", - "cancel": "Cancelar", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "El chat de CLI, los hilos de los canales de chat y las sesiones del warm pool recurren a esta; los agentes indican la suya", "comes_with_the_template_read_only_here": "Viene con la plantilla: aquí es solo de lectura", - "config_file": "Archivo de configuración", - "delete_template": "Eliminar la plantilla", - "delete_the_template_named_confirm": "¿Eliminar la plantilla {{name}}? Su archivo de configuración se borra y no se puede deshacer.", - "editing_this_template_changes_how_they_behave_in_": "Editar esta plantilla cambia cómo se comportan en su próxima sesión", - "filter_templates": "Filtrar plantillas", - "from_package": "Del paquete", "guardrails": "Límites de seguridad", - "id": "ID", - "model": "Modelo", - "no_agent_templates_yet": "Todavía no hay plantillas de agente", - "no_crews_use_this_template_yet": "Todavía ningún agente usa esta plantilla", - "no_default_template": "Sin plantilla por defecto", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Aquí no hay modelo fijado: un agente configurado como heredado cae al valor global por defecto", - "no_templates_match_your_filter": "Ninguna plantilla coincide con el filtro", "not_set_for_this_template": "No está definido para esta plantilla", - "overview": "Resumen", - "package": "Paquete", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Fijado aquí: un agente configurado como heredado usa este modelo", - "source": "Origen", - "status": "Estado", - "task": "Tarea", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Las plantillas vienen con {{productName}} o con una app que instales", - "this_is_the_fallback_template_point_the_picker_a": "La plantilla de reserva no se puede eliminar: primero apunta el selector de arriba a otra", - "tools_and_mcp": "Herramientas y MCP", - "used_by": "Usado por", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Lo usan {{crews}}: apúntalos a otra plantilla en la pestaña Agentes antes de eliminarlo", - "what_a_crew_starts_from_its_model_skills_tools_mc": "Con lo que arranca un agente: su modelo, sus skills, sus herramientas, sus servidores MCP y sus límites de seguridad", - "when_nothing_names_a_template_use": "Cuando nada indica una plantilla, usar", - "where_it_comes_from": "De dónde viene", - "yes_delete_it": "Sí, eliminarlo", - "retry": "Reintentar", - "delete_unavailable_references_failed": "No se pudo cargar la lista del equipo ni el ajuste de plantilla de respaldo, así que no se puede eliminar esta plantilla hasta que se carguen.", - "failed_to_load_agents": "No se pudieron cargar los agentes" + "tools_and_mcp": "Herramientas y MCP" }, "aidlc": { "dagView": { @@ -9324,8 +9251,6 @@ "skills_label": "Habilidades", "steering_description": "Convenciones Markdown de ~/.kiro/steering y del .kiro/steering de tu proyecto, aplicadas según el modo de inclusión de cada archivo", "steering_label": "Directrices", - "templates_description": "Gestiona con lo que arranca cada agente: su modelo, sus skills, sus herramientas, sus servidores MCP y sus límites de seguridad", - "templates_label": "Plantillas de agente", "knowledge_label": "Conocimiento", "knowledge_description": "Documentos y fuentes que el agente puede consultar al responder", "group_agent": "Agente", diff --git a/website/src/i18n/locales/fr.json b/website/src/i18n/locales/fr.json index 526ec0b1197..2b3155878e7 100644 --- a/website/src/i18n/locales/fr.json +++ b/website/src/i18n/locales/fr.json @@ -8604,89 +8604,16 @@ }, "pages": { "agentsPage": { - "account": "Compte", - "account_not_reported": "Non communiqué", - "account_not_reported_help": "Affiché dès que l'identité connectée peut être reliée au compte auquel ces crédits sont facturés. Ce lien n'est pas toujours disponible, ce champ peut donc rester non communiqué même lorsque les crédits s'affichent.", - "agent_templates": "Modèles d'agent", "auto_approved": "Approuvé automatiquement", - "awaiting_first_prompt": "en attente du premier prompt", - "clear_completed": "Effacer les tâches terminées", - "consumption_for_current_billing_period": "Consommation {{provider}} pour la période de facturation en cours. Mise en cache pendant 10 min.", - "context_window_usage": "Utilisation de la fenêtre de contexte", - "context_window_usage_tip": "Utilisation en direct de la fenêtre de contexte pour chaque session {{label}} active. Les agents personnalisés affichent leur modèle configuré. Le compactage se déclenche à 90 %.", "could_not_load_this_agent_s_configuration_skills": "Impossible de charger la configuration de cet agent — les compétences sont masquées pour éviter\n de les écraser. Sélectionnez à nouveau l'agent pour réessayer.", - "critical": "Critique", - "default": "Par défaut", - "delete_subagent": "Supprimer le sous-agent {{id}}", - "denied_commands": "Commandes refusées", - "done": "Terminé", - "est_cost": "Coût est. :", "failed": "Échec", - "filter_models": "Filtrer les modèles", - "harness": "Harness", - "healthy": "Sain", - "high": "Élevé", - "idle": "Inactif", - "installed_agents": "Agents installés", - "loading_agents": "Chargement des agents…", "mcp_servers": "Serveurs MCP", - "model_list": "Liste des modèles", - "no_active_sessions": "Aucune session active", - "no_subagents": "Aucun sous-agent", - "overage_credits": "Crédits de dépassement :", - "plan_credits": "Crédits du forfait", - "prompt_many": "{{count}} prompts", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompts", - "req": "/req", - "resets": "réinitialisation", - "running": "En cours", - "select_an_agent_to_view_details": "Sélectionnez un agent pour voir les détails", "skills": "Compétences", - "spawn_tasks_from_chat_or_cli": "Lancez des tâches depuis le chat ou la CLI", - "subagents": "Sous-agents", "system_prompt": "Prompt système", - "tools": "Outils", - "tools_2": "outils", - "type_to_filter": "Saisissez pour filtrer…", - "usage": "Utilisation", - "at_a_glance": "En bref", - "cancel": "Annuler", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "Le chat CLI, les fils des canaux de chat et les sessions du warm pool retombent sur celui-ci ; les agents indiquent le leur", "comes_with_the_template_read_only_here": "Fourni avec le modèle : ici, en lecture seule", - "config_file": "Fichier de configuration", - "delete_template": "Supprimer le modèle", - "delete_the_template_named_confirm": "Supprimer le modèle {{name}} ? Son fichier de configuration est supprimé et c’est irréversible.", - "editing_this_template_changes_how_they_behave_in_": "Modifier ce modèle change leur comportement à la prochaine session", - "filter_templates": "Filtrer les modèles", - "from_package": "Du paquet", "guardrails": "Garde-fous", - "id": "ID", - "model": "Modèle", - "no_agent_templates_yet": "Pas encore de modèle d’agent", - "no_crews_use_this_template_yet": "Aucun agent n’utilise encore ce modèle", - "no_default_template": "Aucun modèle par défaut", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Aucun modèle épinglé ici : un agent réglé sur hérité retombe sur la valeur par défaut globale", - "no_templates_match_your_filter": "Aucun modèle ne correspond au filtre", "not_set_for_this_template": "Pas défini pour ce modèle", - "overview": "Vue d'ensemble", - "package": "Package", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Épinglé ici : un agent réglé sur hérité utilise ce modèle", - "source": "Source", - "status": "Statut", - "task": "Tâche", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Les modèles arrivent avec {{productName}} ou avec une app que tu installes", - "this_is_the_fallback_template_point_the_picker_a": "Le modèle de secours ne peut pas être supprimé : pointe d’abord le sélecteur ci-dessus sur un autre", - "tools_and_mcp": "Outils et MCP", - "used_by": "Utilisé par", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Utilisé par {{crews}} : repointe-les dans l’onglet Agents avant de le supprimer", - "what_a_crew_starts_from_its_model_skills_tools_mc": "Ce dont part un agent : son modèle, ses skills, ses outils, ses serveurs MCP et ses garde-fous", - "when_nothing_names_a_template_use": "Quand rien n’indique de modèle, utiliser", - "where_it_comes_from": "D’où il vient", - "yes_delete_it": "Oui, supprimer", - "retry": "Réessayer", - "delete_unavailable_references_failed": "Impossible de charger la liste de l'équipe ou le réglage du modèle de secours ; la suppression de ce modèle est indisponible jusqu'à leur chargement.", - "failed_to_load_agents": "Échec du chargement des agents" + "tools_and_mcp": "Outils et MCP" }, "aidlc": { "dagView": { @@ -9324,8 +9251,6 @@ "skills_label": "Compétences", "steering_description": "Conventions Markdown de ~/.kiro/steering et du .kiro/steering de votre projet, appliquées selon le mode d’inclusion de chaque fichier", "steering_label": "Directives", - "templates_description": "Gère ce dont part chaque agent : son modèle, ses skills, ses outils, ses serveurs MCP et ses garde-fous", - "templates_label": "Modèles d'agent", "knowledge_label": "Connaissances", "knowledge_description": "Documents et sources que l'agent peut consulter pour répondre", "group_agent": "Agent", diff --git a/website/src/i18n/locales/hi.json b/website/src/i18n/locales/hi.json index abd83f33d16..ba9928db170 100644 --- a/website/src/i18n/locales/hi.json +++ b/website/src/i18n/locales/hi.json @@ -8489,88 +8489,16 @@ }, "pages": { "agentsPage": { - "account": "खाता", - "account_not_reported": "रिपोर्ट नहीं किया गया", - "account_not_reported_help": "साइन-इन पहचान को उस खाते से जोड़ा जा सकने पर दिखाया जाता है जिस पर ये क्रेडिट बिल किए जाते हैं। यह संबंध हमेशा उपलब्ध नहीं होता, इसलिए क्रेडिट दिखने पर भी यह अनरिपोर्टेड रह सकता है।", - "agent_templates": "एजेंट टेम्पलेट", "auto_approved": "स्वतः-स्वीकृत", - "awaiting_first_prompt": "पहले प्रॉम्प्ट की प्रतीक्षा", - "clear_completed": "पूर्ण हुए साफ़ करें", - "consumption_for_current_billing_period": "मौजूदा बिलिंग अवधि के लिए {{provider}} की खपत। 10 मिनट तक कैश रहती है।", - "context_window_usage": "कॉन्टेक्स्ट विंडो उपयोग", - "context_window_usage_tip": "हर सक्रिय {{label}} सत्र के लिए लाइव कॉन्टेक्स्ट विंडो उपयोग। कस्टम एजेंट अपना कॉन्फ़िगर किया मॉडल दिखाते हैं। 90% पर कॉम्पैक्शन शुरू हो जाता है।", "could_not_load_this_agent_s_configuration_skills": "इस एजेंट का कॉन्फ़िगरेशन लोड नहीं हो सका — उन्हें ओवरराइट होने से बचाने के लिए\n कौशल छिपाए गए हैं। दोबारा कोशिश करने के लिए एजेंट को फिर से चुनें।", - "critical": "गंभीर", - "default": "डिफ़ॉल्ट", - "delete_subagent": "सबएजेंट {{id}} हटाएँ", - "denied_commands": "अस्वीकृत कमांड", - "done": "पूर्ण", - "est_cost": "अनु. लागत:", "failed": "विफल", - "filter_models": "मॉडल फ़िल्टर करें", - "harness": "हार्नेस", - "healthy": "स्वस्थ", - "high": "उच्च", - "idle": "निष्क्रिय", - "installed_agents": "इंस्टॉल किए गए एजेंट", - "loading_agents": "एजेंट लोड हो रहे हैं…", "mcp_servers": "MCP सर्वर", - "model_list": "मॉडल सूची", - "no_active_sessions": "कोई सक्रिय सत्र नहीं", - "no_subagents": "कोई सब-एजेंट नहीं", - "overage_credits": "अतिरिक्त क्रेडिट:", - "plan_credits": "प्लान क्रेडिट", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompts", - "req": "/req", - "resets": "रीसेट होगा", - "running": "चल रहा है", - "select_an_agent_to_view_details": "विवरण देखने के लिए एजेंट चुनें", "skills": "स्किल", - "spawn_tasks_from_chat_or_cli": "चैट या CLI से कार्य स्पॉन करें", - "subagents": "सब-एजेंट", "system_prompt": "सिस्टम प्रॉम्प्ट", - "tools": "टूल", - "tools_2": "टूल", - "type_to_filter": "फ़िल्टर करने के लिए टाइप करें…", - "usage": "उपयोग", - "at_a_glance": "एक नज़र में", - "cancel": "रद्द करें", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI चैट, चैट चैनल के थ्रेड और वॉर्म-पूल सेशन इस पर आते हैं — एजेंट अपना खुद चुनते हैं", "comes_with_the_template_read_only_here": "टेम्पलेट के साथ आता है — यहाँ सिर्फ़ पढ़ा जा सकता है", - "config_file": "कॉन्फ़िग फ़ाइल", - "delete_template": "टेम्पलेट हटाएं", - "delete_the_template_named_confirm": "टेम्पलेट {{name}} हटाना है? इसकी कॉन्फ़िग फ़ाइल हट जाएगी और इसे वापस नहीं लाया जा सकता।", - "editing_this_template_changes_how_they_behave_in_": "इस टेम्पलेट को बदलने से अगले सत्र में उनका व्यवहार बदल जाएगा", - "filter_templates": "टेम्पलेट फ़िल्टर करें", - "from_package": "पैकेज से", "guardrails": "सुरक्षा नियम", - "id": "ID", - "model": "मॉडल", - "no_agent_templates_yet": "अभी कोई एजेंट टेम्पलेट नहीं", - "no_crews_use_this_template_yet": "अभी कोई एजेंट इस टेम्पलेट को इस्तेमाल नहीं करता", - "no_default_template": "कोई डिफ़ॉल्ट टेम्पलेट नहीं", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "यहाँ कोई मॉडल पिन नहीं है — जो एजेंट विरासत पर सेट है वह ग्लोबल डिफ़ॉल्ट पर चला जाएगा", - "no_templates_match_your_filter": "इस फ़िल्टर से कोई टेम्पलेट मेल नहीं खाता", "not_set_for_this_template": "इस टेम्पलेट के लिए सेट नहीं है", - "overview": "अवलोकन", - "package": "पैकेज", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "यहाँ पिन किया गया — जो एजेंट विरासत पर सेट है वह यही मॉडल इस्तेमाल करेगा", - "source": "स्रोत", - "status": "स्थिति", - "task": "कार्य", - "templates_come_with_kirocrew_or_with_an_app_you_i": "टेम्पलेट {{productName}} के साथ आते हैं, या किसी इंस्टॉल किए गए ऐप के साथ", - "this_is_the_fallback_template_point_the_picker_a": "फ़ॉलबैक टेम्पलेट हटाया नहीं जा सकता — पहले ऊपर वाले सेलेक्टर को किसी दूसरे टेम्पलेट पर लगाओ", - "tools_and_mcp": "टूल और MCP", - "used_by": "द्वारा उपयोग", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "{{crews}} इसे इस्तेमाल करते हैं — हटाने से पहले एजेंट टैब में उन्हें दूसरे टेम्पलेट पर लगाओ", - "what_a_crew_starts_from_its_model_skills_tools_mc": "जिससे एजेंट शुरू होता है — उसका मॉडल, स्किल, टूल, MCP सर्वर और सुरक्षा नियम", - "when_nothing_names_a_template_use": "जब कोई टेम्पलेट तय न हो, तब इस्तेमाल करो", - "where_it_comes_from": "यह कहाँ से आया", - "yes_delete_it": "हाँ, हटा दो", - "retry": "फिर कोशिश करें", - "delete_unavailable_references_failed": "क्रू रोस्टर या फ़ॉलबैक-टेम्पलेट सेटिंग लोड नहीं हो सकी, इसलिए वे लोड होने तक यह टेम्पलेट हटाना उपलब्ध नहीं है।", - "failed_to_load_agents": "एजेंट लोड करने में विफल" + "tools_and_mcp": "टूल और MCP" }, "aidlc": { "dagView": { @@ -9195,8 +9123,6 @@ "skills_label": "स्किल्स", "steering_description": "~/.kiro/steering और तुम्हारी परियोजना के .kiro/steering से Markdown नियम, हर फ़ाइल के समावेशन मोड के अनुसार लागू", "steering_label": "स्टीयरिंग", - "templates_description": "संभालो कि हर एजेंट किससे शुरू होता है — उसका मॉडल, स्किल, टूल, MCP सर्वर और सुरक्षा नियम", - "templates_label": "एजेंट टेम्प्लेट", "knowledge_label": "नॉलेज", "knowledge_description": "दस्तावेज़ और स्रोत जिन्हें एजेंट जवाब देते समय खोज सकता है", "group_agent": "एजेंट", diff --git a/website/src/i18n/locales/it.json b/website/src/i18n/locales/it.json index aaed50f0503..8ef98d47a49 100644 --- a/website/src/i18n/locales/it.json +++ b/website/src/i18n/locales/it.json @@ -8604,89 +8604,16 @@ }, "pages": { "agentsPage": { - "account": "Account", - "account_not_reported": "Non riportato", - "account_not_reported_help": "Mostrato quando l'identità con cui hai effettuato l'accesso può essere collegata all'account a cui vengono addebitati questi crediti. Tale collegamento non è sempre disponibile, quindi questo campo può restare non riportato anche quando i crediti sono visibili.", - "agent_templates": "Modelli di agente", "auto_approved": "Approvato automaticamente", - "awaiting_first_prompt": "in attesa del primo prompt", - "clear_completed": "Cancella completati", - "consumption_for_current_billing_period": "Consumo di {{provider}} per il periodo di fatturazione corrente. In cache per 10 min.", - "context_window_usage": "Utilizzo della finestra di contesto", - "context_window_usage_tip": "Utilizzo in tempo reale della finestra di contesto per ogni sessione {{label}} attiva. Gli agenti personalizzati mostrano il modello configurato. La compattazione si attiva al 90%.", "could_not_load_this_agent_s_configuration_skills": "Impossibile caricare la configurazione di questo agente — le skill sono nascoste per evitare\n di sovrascriverle. Riseleziona l'agente per riprovare.", - "critical": "Critico", - "default": "Predefinito", - "delete_subagent": "Elimina il subagente {{id}}", - "denied_commands": "Comandi bloccati", - "done": "Fatto", - "est_cost": "Costo stimato:", "failed": "Non riuscito", - "filter_models": "Filtra i modelli", - "harness": "Harness", - "healthy": "Integro", - "high": "Alto", - "idle": "Inattivo", - "installed_agents": "Agenti installati", - "loading_agents": "Caricamento degli agenti…", "mcp_servers": "Server MCP", - "model_list": "Elenco dei modelli", - "no_active_sessions": "Nessuna sessione attiva", - "no_subagents": "Nessun subagente", - "overage_credits": "Crediti di eccedenza:", - "plan_credits": "Crediti del piano", - "prompt_many": "{{count}} prompt", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompt", - "req": "/rich.", - "resets": "reimpostazioni", - "running": "In esecuzione", - "select_an_agent_to_view_details": "Seleziona un agente per vederne i dettagli", "skills": "Skill", - "spawn_tasks_from_chat_or_cli": "Avvia attività dalla chat o dalla CLI", - "subagents": "Subagenti", "system_prompt": "Prompt di sistema", - "tools": "Strumenti", - "tools_2": "strumenti", - "type_to_filter": "Digita per filtrare…", - "usage": "Utilizzo", - "at_a_glance": "In sintesi", - "cancel": "Annulla", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "La chat da CLI, i thread dei canali di chat e le sessioni del warm pool ricadono su questo; gli agenti indicano il proprio", "comes_with_the_template_read_only_here": "Arriva con il modello: qui è in sola lettura", - "config_file": "File di configurazione", - "delete_template": "Elimina il modello", - "delete_the_template_named_confirm": "Eliminare il modello {{name}}? Il suo file di configurazione viene rimosso e non si può annullare.", - "editing_this_template_changes_how_they_behave_in_": "Modificare questo modello cambia come si comportano nella sessione successiva", - "filter_templates": "Filtra i modelli", - "from_package": "Dal pacchetto", "guardrails": "Regole di sicurezza", - "id": "ID", - "model": "Modello", - "no_agent_templates_yet": "Ancora nessun modello di agente", - "no_crews_use_this_template_yet": "Ancora nessun agente usa questo modello", - "no_default_template": "Nessun modello predefinito", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Nessun modello fissato qui: un agente impostato su ereditato ricade sul valore predefinito globale", - "no_templates_match_your_filter": "Nessun modello corrisponde al filtro", "not_set_for_this_template": "Non impostato per questo modello", - "overview": "Panoramica", - "package": "Pacchetto", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Fissato qui: un agente impostato su ereditato usa questo modello", - "source": "Origine", - "status": "Stato", - "task": "Attività", - "templates_come_with_kirocrew_or_with_an_app_you_i": "I modelli arrivano con {{productName}} o con un’app che installi", - "this_is_the_fallback_template_point_the_picker_a": "Il modello di riserva non si può eliminare: prima punta il selettore sopra su un altro", - "tools_and_mcp": "Strumenti e MCP", - "used_by": "Utilizzato da", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Usato da {{crews}}: nella scheda Agenti puntali su un altro modello prima di eliminarlo", - "what_a_crew_starts_from_its_model_skills_tools_mc": "Da cosa parte un agente: il suo modello, le sue skill, i suoi strumenti, i suoi server MCP e le sue regole di sicurezza", - "when_nothing_names_a_template_use": "Quando nulla indica un modello, usa", - "where_it_comes_from": "Da dove arriva", - "yes_delete_it": "Sì, elimina", - "retry": "Riprova", - "delete_unavailable_references_failed": "Non è stato possibile caricare l'elenco della crew o l'impostazione del modello di riserva, quindi l'eliminazione di questo modello non è disponibile finché non vengono caricati.", - "failed_to_load_agents": "Caricamento degli agenti non riuscito" + "tools_and_mcp": "Strumenti e MCP" }, "aidlc": { "dagView": { @@ -9324,8 +9251,6 @@ "skills_label": "Competenze", "steering_description": "Convenzioni Markdown da ~/.kiro/steering e dal .kiro/steering del tuo progetto, applicate secondo la modalità di inclusione di ogni file", "steering_label": "Direttive", - "templates_description": "Gestisci da cosa parte ogni agente: il suo modello, le sue skill, i suoi strumenti, i suoi server MCP e le sue regole di sicurezza", - "templates_label": "Modelli di agente", "knowledge_label": "Conoscenza", "knowledge_description": "Documenti e fonti che l'agente può consultare quando risponde", "group_agent": "Agente", diff --git a/website/src/i18n/locales/ja.json b/website/src/i18n/locales/ja.json index 62479162ef2..6447f81c509 100644 --- a/website/src/i18n/locales/ja.json +++ b/website/src/i18n/locales/ja.json @@ -8374,87 +8374,16 @@ }, "pages": { "agentsPage": { - "account": "アカウント", - "account_not_reported": "報告なし", - "account_not_reported_help": "サインイン中の識別情報を、これらのクレジットが請求されるアカウントに結び付けられた場合に表示されます。この結び付けは常に得られるわけではないため、クレジットが表示されていても報告なしのままになることがあります。", - "agent_templates": "エージェントテンプレート", "auto_approved": "自動承認済み", - "awaiting_first_prompt": "最初のプロンプトを待機中", - "clear_completed": "完了を消去", - "consumption_for_current_billing_period": "現在の請求期間における {{provider}} の使用量。キャッシュは 10 分間有効です。", - "context_window_usage": "コンテキストウィンドウの使用", - "context_window_usage_tip": "アクティブな {{label}} セッションごとのコンテキストウィンドウ使用率をリアルタイムに表示します。カスタムエージェントは設定済みのモデルを表示します。圧縮は 90% で作動します。", "could_not_load_this_agent_s_configuration_skills": "このエージェントの設定を読み込めません。スキルは上書きを避けるため非表示になっています。\n エージェントを再度選択して再試行してください。", - "critical": "重大", - "default": "デフォルト", - "delete_subagent": "サブエージェント {{id}} を削除", - "denied_commands": "拒否されたコマンド", - "done": "完了", - "est_cost": "推定コスト:", "failed": "失敗", - "filter_models": "モデルをフィルタリング", - "harness": "ハーネス", - "healthy": "正常", - "high": "高", - "idle": "アイドル", - "installed_agents": "インストール済みエージェント", - "loading_agents": "エージェント読み込み中…", "mcp_servers": "MCP サーバー", - "model_list": "モデルリスト", - "no_active_sessions": "アクティブなセッションなし", - "no_subagents": "サブエージェントなし", - "overage_credits": "超過クレジット:", - "plan_credits": "プランクレジット", - "prompt_other": "{{count}}個のプロンプト", - "req": "/req", - "resets": "リセット", - "running": "実行中", - "select_an_agent_to_view_details": "詳細を表示するエージェントを選択", "skills": "スキル", - "spawn_tasks_from_chat_or_cli": "チャットまたはCLIからタスクを生成", - "subagents": "サブエージェント", "system_prompt": "システムプロンプト", - "tools": "ツール", - "tools_2": "ツール", - "type_to_filter": "フィルタリングに入力…", - "usage": "使用", - "at_a_glance": "ひと目で", - "cancel": "キャンセル", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI チャット、チャットチャンネルのスレッド、ウォームプールのセッションはこれにフォールバックします — エージェントは独自に指定します", "comes_with_the_template_read_only_here": "テンプレートに付属 — ここでは読み取り専用です", - "config_file": "設定ファイル", - "delete_template": "テンプレートを削除", - "delete_the_template_named_confirm": "テンプレート {{name}} を削除しますか?設定ファイルが削除され、元に戻せません。", - "editing_this_template_changes_how_they_behave_in_": "このテンプレートを編集すると、次のセッションでの動作が変わります", - "filter_templates": "テンプレートを絞り込む", - "from_package": "パッケージ由来", "guardrails": "ガードレール", - "id": "ID", - "model": "モデル", - "no_agent_templates_yet": "エージェントテンプレートはまだありません", - "no_crews_use_this_template_yet": "このテンプレートを使用しているエージェントはまだありません", - "no_default_template": "デフォルトテンプレートなし", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "ここではモデルが固定されていません — 継承に設定されたエージェントはグローバルデフォルトにフォールバックします", - "no_templates_match_your_filter": "絞り込み条件に一致するテンプレートがありません", "not_set_for_this_template": "このテンプレートでは設定されていません", - "overview": "概要", - "package": "パッケージ", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "ここで固定 — 継承に設定されたエージェントはこのモデルを使用します", - "source": "ソース", - "status": "ステータス", - "task": "タスク", - "templates_come_with_kirocrew_or_with_an_app_you_i": "テンプレートは {{productName}} に付属するか、インストールしたアプリに付属します", - "this_is_the_fallback_template_point_the_picker_a": "フォールバックテンプレートは削除できません — 先に上のピッカーで別のテンプレートを指定してください", - "tools_and_mcp": "ツールと MCP", - "used_by": "使用元", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "{{crews}} が使用しています — 削除する前にエージェントタブで参照先を変更してください", - "what_a_crew_starts_from_its_model_skills_tools_mc": "エージェントの出発点 — モデル、スキル、ツール、MCP サーバー、ガードレール", - "when_nothing_names_a_template_use": "テンプレートが指定されていない場合に使用", - "where_it_comes_from": "提供元", - "yes_delete_it": "はい、削除します", - "retry": "再試行", - "delete_unavailable_references_failed": "クルー名簿またはフォールバックテンプレート設定を読み込めなかったため、読み込まれるまでこのテンプレートは削除できません。", - "failed_to_load_agents": "エージェントの読み込みに失敗しました" + "tools_and_mcp": "ツールと MCP" }, "aidlc": { "dagView": { @@ -9066,8 +8995,6 @@ "skills_label": "スキル", "steering_description": "~/.kiro/steering とプロジェクトの .kiro/steering の Markdown 規約。各ファイルのインクルードモードに従って適用されます", "steering_label": "ステアリング", - "templates_description": "各エージェントの出発点を管理。モデル、スキル、ツール、MCP サーバー、ガードレール", - "templates_label": "エージェントテンプレート", "knowledge_label": "ナレッジ", "knowledge_description": "回答時にエージェントが検索できるドキュメントとソース", "group_agent": "エージェント", diff --git a/website/src/i18n/locales/ko.json b/website/src/i18n/locales/ko.json index 46ff0e2378f..e624e804704 100644 --- a/website/src/i18n/locales/ko.json +++ b/website/src/i18n/locales/ko.json @@ -8374,87 +8374,16 @@ }, "pages": { "agentsPage": { - "account": "계정", - "account_not_reported": "보고되지 않음", - "account_not_reported_help": "로그인한 신원을 이 크레딧이 청구되는 계정과 연결할 수 있을 때 표시됩니다. 이 연결이 항상 가능한 것은 아니므로 크레딧이 표시되는 동안에도 보고되지 않은 상태로 남을 수 있습니다.", - "agent_templates": "에이전트 템플릿", "auto_approved": "자동 승인됨", - "awaiting_first_prompt": "첫 프롬프트 대기 중", - "clear_completed": "완료 항목 지우기", - "consumption_for_current_billing_period": "현재 청구 기간의 {{provider}} 사용량입니다. 캐시는 10분간 유지됩니다.", - "context_window_usage": "컨텍스트 윈도 사용량", - "context_window_usage_tip": "활성 {{label}} 세션별 컨텍스트 윈도 사용률을 실시간으로 보여 줍니다. 커스텀 에이전트는 설정된 모델을 표시합니다. 압축은 90%에서 시작됩니다.", "could_not_load_this_agent_s_configuration_skills": "이 에이전트의 설정을 불러오지 못했습니다 — 덮어쓰기를 막기 위해\n 스킬은 숨겨집니다. 에이전트를 다시 선택해 재시도하세요.", - "critical": "심각", - "default": "기본", - "delete_subagent": "서브에이전트 {{id}} 삭제", - "denied_commands": "거부된 명령", - "done": "완료", - "est_cost": "예상 비용:", "failed": "실패", - "filter_models": "모델 필터", - "harness": "하네스", - "healthy": "정상", - "high": "높음", - "idle": "유휴", - "installed_agents": "설치된 에이전트", - "loading_agents": "에이전트 불러오는 중…", "mcp_servers": "MCP 서버", - "model_list": "모델 목록", - "no_active_sessions": "활성 세션 없음", - "no_subagents": "서브에이전트 없음", - "overage_credits": "초과 크레딧:", - "plan_credits": "플랜 크레딧", - "prompt_other": "프롬프트 {{count}}개", - "req": "/req", - "resets": "초기화", - "running": "실행 중", - "select_an_agent_to_view_details": "세부 정보를 보려면 에이전트를 선택하세요", "skills": "스킬", - "spawn_tasks_from_chat_or_cli": "채팅 또는 CLI에서 작업 생성", - "subagents": "서브에이전트", "system_prompt": "시스템 프롬프트", - "tools": "도구", - "tools_2": "도구", - "type_to_filter": "입력해 필터링…", - "usage": "사용량", - "at_a_glance": "한눈에 보기", - "cancel": "취소", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI 채팅, 채팅 채널 스레드, 웜 풀 세션이 여기로 폴백합니다 — 에이전트는 각자 지정합니다", "comes_with_the_template_read_only_here": "템플릿에 포함됨 — 여기서는 읽기 전용입니다", - "config_file": "설정 파일", - "delete_template": "템플릿 삭제", - "delete_the_template_named_confirm": "템플릿 {{name}}을(를) 삭제하시겠습니까? 설정 파일이 제거되며 되돌릴 수 없습니다.", - "editing_this_template_changes_how_they_behave_in_": "이 템플릿을 편집하면 다음 세션에서의 동작이 바뀝니다", - "filter_templates": "템플릿 필터", - "from_package": "패키지 제공", "guardrails": "가드레일", - "id": "ID", - "model": "모델", - "no_agent_templates_yet": "아직 에이전트 템플릿이 없습니다", - "no_crews_use_this_template_yet": "아직 이 템플릿을 사용하는 에이전트가 없습니다", - "no_default_template": "기본 템플릿 없음", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "여기에 고정된 모델이 없습니다 — 상속으로 설정된 에이전트는 전역 기본값으로 폴백합니다", - "no_templates_match_your_filter": "필터와 일치하는 템플릿이 없습니다", "not_set_for_this_template": "이 템플릿에는 설정되지 않았습니다", - "overview": "개요", - "package": "패키지", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "여기에 고정됨 — 상속으로 설정된 에이전트는 이 모델을 사용합니다", - "source": "소스", - "status": "상태", - "task": "작업", - "templates_come_with_kirocrew_or_with_an_app_you_i": "템플릿은 {{productName}}에 포함되거나 설치한 앱과 함께 제공됩니다", - "this_is_the_fallback_template_point_the_picker_a": "폴백 템플릿은 삭제할 수 없습니다 — 먼저 위의 선택기에서 다른 템플릿을 지정하세요", - "tools_and_mcp": "도구 및 MCP", - "used_by": "사용 중인 곳", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "{{crews}}에서 사용 중입니다 — 삭제하기 전에 에이전트 탭에서 참조를 변경하세요", - "what_a_crew_starts_from_its_model_skills_tools_mc": "에이전트의 출발점 — 모델, 스킬, 도구, MCP 서버, 가드레일", - "when_nothing_names_a_template_use": "템플릿이 지정되지 않은 경우 사용", - "where_it_comes_from": "출처", - "yes_delete_it": "예, 삭제합니다", - "retry": "다시 시도", - "delete_unavailable_references_failed": "크루 명단이나 대체 템플릿 설정을 불러오지 못해, 불러올 때까지 이 템플릿을 삭제할 수 없습니다.", - "failed_to_load_agents": "에이전트를 불러오지 못했습니다" + "tools_and_mcp": "도구 및 MCP" }, "aidlc": { "dagView": { @@ -9066,8 +8995,6 @@ "skills_label": "스킬", "steering_description": "~/.kiro/steering 및 프로젝트의 .kiro/steering에 있는 Markdown 규칙. 각 파일의 포함 모드에 따라 적용됩니다", "steering_label": "스티어링", - "templates_description": "각 에이전트가 시작하는 지점을 관리합니다: 모델, 스킬, 도구, MCP 서버, 가드레일", - "templates_label": "에이전트 템플릿", "knowledge_label": "지식", "knowledge_description": "에이전트가 답변할 때 검색할 수 있는 문서와 소스", "group_agent": "에이전트", diff --git a/website/src/i18n/locales/pt.json b/website/src/i18n/locales/pt.json index 666506a9fe5..9268474c6c4 100644 --- a/website/src/i18n/locales/pt.json +++ b/website/src/i18n/locales/pt.json @@ -8604,89 +8604,16 @@ }, "pages": { "agentsPage": { - "account": "Conta", - "account_not_reported": "Não informado", - "account_not_reported_help": "Exibido quando a identidade conectada pode ser vinculada à conta à qual esses créditos são cobrados. Esse vínculo não está sempre disponível, portanto isso pode permanecer não informado mesmo quando os créditos aparecem.", - "agent_templates": "Modelos de agente", "auto_approved": "Aprovado automaticamente", - "awaiting_first_prompt": "aguardando o primeiro prompt", - "clear_completed": "Limpar concluídas", - "consumption_for_current_billing_period": "Consumo de {{provider}} no período de faturamento atual. Em cache por 10 min.", - "context_window_usage": "Uso da janela de contexto", - "context_window_usage_tip": "Utilização da janela de contexto em tempo real por sessão ativa do {{label}}. Agentes personalizados mostram o modelo configurado. A compactação é acionada em 90%.", "could_not_load_this_agent_s_configuration_skills": "Não foi possível carregar a configuração deste agente — as habilidades estão ocultas para evitar\n sobrescrevê-las. Selecione o agente novamente para tentar de novo.", - "critical": "Crítico", - "default": "Padrão", - "delete_subagent": "Excluir o subagente {{id}}", - "denied_commands": "Comandos negados", - "done": "Concluído", - "est_cost": "Custo est.:", "failed": "Falhou", - "filter_models": "Filtrar modelos", - "harness": "Harness", - "healthy": "Saudável", - "high": "Alto", - "idle": "Inativo", - "installed_agents": "Agentes instalados", - "loading_agents": "Carregando agentes…", "mcp_servers": "Servidores MCP", - "model_list": "Lista de modelos", - "no_active_sessions": "Nenhuma sessão ativa", - "no_subagents": "Nenhum subagente", - "overage_credits": "Créditos excedentes:", - "plan_credits": "Créditos do plano", - "prompt_many": "{{count}} prompts", - "prompt_one": "{{count}} prompt", - "prompt_other": "{{count}} prompts", - "req": "/req", - "resets": "renova em", - "running": "Em execução", - "select_an_agent_to_view_details": "Selecione um agente para ver os detalhes", "skills": "Habilidades", - "spawn_tasks_from_chat_or_cli": "Crie tarefas pelo chat ou pela CLI", - "subagents": "Subagentes", "system_prompt": "Prompt do sistema", - "tools": "Ferramentas", - "tools_2": "ferramentas", - "type_to_filter": "Digite para filtrar…", - "usage": "Uso", - "at_a_glance": "Visão rápida", - "cancel": "Cancelar", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "O chat da CLI, as threads dos canais de chat e as sessões do warm pool recorrem a este; os agentes indicam o seu", "comes_with_the_template_read_only_here": "Vem com o modelo: aqui é somente leitura", - "config_file": "Arquivo de configuração", - "delete_template": "Excluir o modelo", - "delete_the_template_named_confirm": "Excluir o modelo {{name}}? O arquivo de configuração dele é removido e não dá para desfazer.", - "editing_this_template_changes_how_they_behave_in_": "Editar este modelo muda como eles se comportam na próxima sessão", - "filter_templates": "Filtrar modelos", - "from_package": "Do pacote", "guardrails": "Limites de segurança", - "id": "ID", - "model": "Modelo", - "no_agent_templates_yet": "Nenhum modelo de agente ainda", - "no_crews_use_this_template_yet": "Nenhum agente usa este modelo ainda", - "no_default_template": "Sem modelo padrão", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Nenhum modelo fixado aqui: um agente definido como herdado cai no padrão global", - "no_templates_match_your_filter": "Nenhum modelo corresponde ao filtro", "not_set_for_this_template": "Não definido para este modelo", - "overview": "Visão geral", - "package": "Pacote", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Fixado aqui: um agente definido como herdado usa este modelo", - "source": "Origem", - "status": "Status", - "task": "Tarefa", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Os modelos vêm com o {{productName}} ou com um app que você instala", - "this_is_the_fallback_template_point_the_picker_a": "O modelo de reserva não pode ser excluído: aponte primeiro o seletor acima para outro", - "tools_and_mcp": "Ferramentas e MCP", - "used_by": "Usado por", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Usado por {{crews}}: aponte-os para outro modelo na aba Agentes antes de excluir", - "what_a_crew_starts_from_its_model_skills_tools_mc": "Com o que um agente começa: o modelo, as skills, as ferramentas, os servidores MCP e os limites de segurança", - "when_nothing_names_a_template_use": "Quando nada indica um modelo, usar", - "where_it_comes_from": "De onde ele vem", - "yes_delete_it": "Sim, excluir", - "retry": "Tentar novamente", - "delete_unavailable_references_failed": "Não foi possível carregar a lista da equipe nem a configuração do modelo de fallback, então excluir este modelo fica indisponível até que carreguem.", - "failed_to_load_agents": "Falha ao carregar os agentes" + "tools_and_mcp": "Ferramentas e MCP" }, "aidlc": { "dagView": { @@ -9324,8 +9251,6 @@ "skills_label": "Habilidades", "steering_description": "Convenções Markdown de ~/.kiro/steering e do .kiro/steering do seu projeto, aplicadas conforme o modo de inclusão de cada arquivo", "steering_label": "Diretrizes", - "templates_description": "Gerencie com o que cada agente começa: o modelo, as skills, as ferramentas, os servidores MCP e os limites de segurança", - "templates_label": "Modelos de agente", "knowledge_label": "Conhecimento", "knowledge_description": "Documentos e fontes que o agente pode consultar ao responder", "group_agent": "Agente", diff --git a/website/src/i18n/locales/ru.json b/website/src/i18n/locales/ru.json index 146054998d7..791294cf1a1 100644 --- a/website/src/i18n/locales/ru.json +++ b/website/src/i18n/locales/ru.json @@ -8719,90 +8719,16 @@ }, "pages": { "agentsPage": { - "account": "Аккаунт", - "account_not_reported": "Не сообщается", - "account_not_reported_help": "Отображается, когда вошедшую личность удаётся связать с аккаунтом, на который списываются эти кредиты. Такая связь доступна не всегда, поэтому поле может оставаться без данных даже при отображении кредитов.", - "agent_templates": "Шаблоны агентов", "auto_approved": "Разрешено автоматически", - "awaiting_first_prompt": "ожидание первого запроса", - "clear_completed": "Очистить завершённые", - "consumption_for_current_billing_period": "Потребление {{provider}} за текущий расчётный период. Кэш 10 мин.", - "context_window_usage": "Использование контекстного окна", - "context_window_usage_tip": "Использование контекстного окна в реальном времени для каждой активной сессии {{label}}. У своих агентов показывается настроенная для них модель. Сжатие запускается на 90%.", "could_not_load_this_agent_s_configuration_skills": "Не удалось загрузить конфигурацию этого агента — навыки скрыты, чтобы\n не перезаписать их. Выберите агента заново, чтобы повторить попытку.", - "critical": "Критический", - "default": "По умолчанию", - "delete_subagent": "Удалить субагента {{id}}", - "denied_commands": "Запрещённые команды", - "done": "Готово", - "est_cost": "Прибл. стоимость:", "failed": "Ошибка", - "filter_models": "Фильтр моделей", - "harness": "Окружение", - "healthy": "В порядке", - "high": "Высокий", - "idle": "Простой", - "installed_agents": "Установленные агенты", - "loading_agents": "Загрузка агентов…", "mcp_servers": "Серверы MCP", - "model_list": "Список моделей", - "no_active_sessions": "Нет активных сессий", - "no_subagents": "Нет субагентов", - "overage_credits": "Кредиты сверх лимита:", - "plan_credits": "Кредиты тарифа", - "prompt_few": "{{count}} промпта", - "prompt_many": "{{count}} промптов", - "prompt_one": "{{count}} промпт", - "prompt_other": "{{count}} промпта", - "req": "/req", - "resets": "сброс", - "running": "Выполняется", - "select_an_agent_to_view_details": "Выберите агента, чтобы посмотреть подробности", "skills": "Навыки", - "spawn_tasks_from_chat_or_cli": "Запускайте задачи из чата или CLI", - "subagents": "Субагенты", "system_prompt": "Системный промпт", - "tools": "Инструменты", - "tools_2": "инструментов", - "type_to_filter": "Введите текст для фильтра…", - "usage": "Использование", - "at_a_glance": "Кратко", - "cancel": "Отмена", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "Чат в CLI, ветки в чат-каналах и сессии из тёплого пула берут этот шаблон — агенты указывают свой", "comes_with_the_template_read_only_here": "Идёт вместе с шаблоном — здесь только для чтения", - "config_file": "Файл настроек", - "delete_template": "Удалить шаблон", - "delete_the_template_named_confirm": "Удалить шаблон {{name}}? Его файл настроек будет удалён, отменить это нельзя.", - "editing_this_template_changes_how_they_behave_in_": "Изменение шаблона поменяет их поведение в следующей сессии", - "filter_templates": "Фильтр шаблонов", - "from_package": "Из пакета", "guardrails": "Правила безопасности", - "id": "ID", - "model": "Модель", - "no_agent_templates_yet": "Пока нет шаблонов агентов", - "no_crews_use_this_template_yet": "Пока ни один агент не использует этот шаблон", - "no_default_template": "Без шаблона по умолчанию", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "Здесь модель не закреплена: агент с наследованием возьмёт глобальное значение по умолчанию", - "no_templates_match_your_filter": "Нет шаблонов по этому фильтру", "not_set_for_this_template": "Для этого шаблона не задано", - "overview": "Обзор", - "package": "Пакет", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "Закреплено здесь: агент с наследованием использует эту модель", - "source": "Источник", - "status": "Статус", - "task": "Задача", - "templates_come_with_kirocrew_or_with_an_app_you_i": "Шаблоны идут вместе с {{productName}} или с приложением, которое вы установили", - "this_is_the_fallback_template_point_the_picker_a": "Резервный шаблон нельзя удалить — сначала выберите в списке выше другой", - "tools_and_mcp": "Инструменты и MCP", - "used_by": "Используется", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "Используется: {{crews}} — перед удалением переключите их на другой шаблон на вкладке Агенты", - "what_a_crew_starts_from_its_model_skills_tools_mc": "С чего начинает агент: модель, навыки, инструменты, MCP-серверы и правила безопасности", - "when_nothing_names_a_template_use": "Когда шаблон не указан, использовать", - "where_it_comes_from": "Откуда он взялся", - "yes_delete_it": "Да, удалить", - "retry": "Повторить", - "delete_unavailable_references_failed": "Не удалось загрузить список команды или настройку резервного шаблона, поэтому удаление этого шаблона недоступно, пока они не загрузятся.", - "failed_to_load_agents": "Не удалось загрузить агентов" + "tools_and_mcp": "Инструменты и MCP" }, "aidlc": { "dagView": { @@ -9453,8 +9379,6 @@ "skills_label": "Навыки", "steering_description": "Markdown-соглашения из ~/.kiro/steering и .kiro/steering вашего проекта, применяются по режиму включения каждого файла", "steering_label": "Направляющие правила", - "templates_description": "Управляйте тем, с чего начинает каждый агент: модель, навыки, инструменты, MCP-серверы и правила безопасности", - "templates_label": "Шаблоны агентов", "knowledge_label": "Знания", "knowledge_description": "Документы и источники, которые агент может искать при ответе", "group_agent": "Агент", diff --git a/website/src/i18n/locales/zh-CN.json b/website/src/i18n/locales/zh-CN.json index 20815369494..2417e2df701 100644 --- a/website/src/i18n/locales/zh-CN.json +++ b/website/src/i18n/locales/zh-CN.json @@ -8374,87 +8374,16 @@ }, "pages": { "agentsPage": { - "account": "账户", - "account_not_reported": "未报告", - "account_not_reported_help": "当已登录身份能够与这些额度的计费账户建立关联时显示。该关联并非总能获得,因此即使显示了额度,此处仍可能保持未报告。", - "agent_templates": "代理模板", "auto_approved": "自动批准", - "awaiting_first_prompt": "等待首个提示词", - "clear_completed": "清除已完成", - "consumption_for_current_billing_period": "{{provider}} 在当前计费周期内的用量。缓存 10 分钟。", - "context_window_usage": "上下文窗口用量", - "context_window_usage_tip": "每个活跃 {{label}} 会话的实时上下文窗口用量。自定义代理会显示其配置的模型。达到 90% 时触发压缩。", "could_not_load_this_agent_s_configuration_skills": "无法加载此代理的配置 — 已隐藏技能以避免\n 覆盖它们。请重新选择该代理后重试。", - "critical": "严重", - "default": "默认", - "delete_subagent": "删除子代理 {{id}}", - "denied_commands": "拒绝的命令", - "done": "已完成", - "est_cost": "预估费用:", "failed": "失败", - "filter_models": "筛选模型", - "harness": "运行环境", - "healthy": "正常", - "high": "高", - "idle": "空闲", - "installed_agents": "已安装代理", - "loading_agents": "加载代理…", "mcp_servers": "MCP 服务器", - "model_list": "模型列表", - "no_active_sessions": "无活跃会话", - "no_subagents": "无子代理", - "overage_credits": "超额额度:", - "plan_credits": "套餐额度", - "prompt_other": "{{count}} 条提示词", - "req": "/req", - "resets": "重置", - "running": "运行中", - "select_an_agent_to_view_details": "选择一个代理查看详情", "skills": "技能", - "spawn_tasks_from_chat_or_cli": "从聊天或 CLI 派生任务", - "subagents": "子代理", "system_prompt": "系统提示词", - "tools": "工具", - "tools_2": "工具", - "type_to_filter": "输入以筛选…", - "usage": "用量", - "at_a_glance": "一览", - "cancel": "取消", - "cli_chat_chat_channel_threads_and_warm_pool_sessio": "CLI 对话、聊天渠道的会话和预热进程会回退到这个模板 —— 代理会自己指定", "comes_with_the_template_read_only_here": "随模板提供 —— 这里只能查看", - "config_file": "配置文件", - "delete_template": "删除模板", - "delete_the_template_named_confirm": "删除模板 {{name}}?它的配置文件会被删掉,而且无法撤销。", - "editing_this_template_changes_how_they_behave_in_": "修改这个模板会改变它们下一次会话里的行为", - "filter_templates": "筛选模板", - "from_package": "来自包", "guardrails": "防护规则", - "id": "ID", - "model": "模型", - "no_agent_templates_yet": "还没有 Agent 模板", - "no_crews_use_this_template_yet": "还没有代理用这个模板", - "no_default_template": "不设默认模板", - "no_model_pinned_here_a_crew_set_to_inherit_falls_": "这里没有固定模型 —— 设为继承的代理会退回到全局默认值", - "no_templates_match_your_filter": "没有模板符合筛选条件", "not_set_for_this_template": "这个模板没有设置", - "overview": "概览", - "package": "包", - "pinned_here_a_crew_set_to_inherit_uses_this_model": "已在这里固定 —— 设为继承的代理会用这个模型", - "source": "来源", - "status": "状态", - "task": "任务", - "templates_come_with_kirocrew_or_with_an_app_you_i": "模板随 {{productName}} 一起提供,也可能来自你安装的应用", - "this_is_the_fallback_template_point_the_picker_a": "兜底模板不能删除 —— 请先把上方的选择器指向别的模板", - "tools_and_mcp": "工具与 MCP", - "used_by": "使用者", - "used_by_crews_repoint_them_on_the_crews_tab_firs": "{{crews}} 正在使用 —— 删除前请先在代理页签里把它们指向别的模板", - "what_a_crew_starts_from_its_model_skills_tools_mc": "代理启动时用的东西 —— 模型、技能、工具、MCP 服务器和防护规则", - "when_nothing_names_a_template_use": "当没有指定模板时,使用", - "where_it_comes_from": "它来自哪里", - "yes_delete_it": "是的,删除它", - "retry": "重试", - "delete_unavailable_references_failed": "无法加载团队名册或回退模板设置,在它们加载完成前无法删除此模板。", - "failed_to_load_agents": "加载代理失败" + "tools_and_mcp": "工具与 MCP" }, "aidlc": { "dagView": { @@ -9066,8 +8995,6 @@ "skills_label": "技能", "steering_description": "来自 ~/.kiro/steering 和项目 .kiro/steering 的 Markdown 约定,按每个文件的包含模式生效", "steering_label": "引导规则", - "templates_description": "管理每个代理启动时用的东西:模型、技能、工具、MCP 服务器和防护规则", - "templates_label": "Agent 模板", "knowledge_label": "知识库", "knowledge_description": "智能体回答时能检索到的文档与来源", "group_agent": "智能体", diff --git a/website/src/i18n/pluralKeys.json b/website/src/i18n/pluralKeys.json index 342559aa2ec..950584a883d 100644 --- a/website/src/i18n/pluralKeys.json +++ b/website/src/i18n/pluralKeys.json @@ -114,7 +114,6 @@ "components.skillBrowserModal.file", "components.webPreviewPanel.annotate_clear_confirm", "components.webPreviewPanel.annotations_count", - "pages.agentsPage.prompt", "pages.aidlc.taskDetailPanel.task_2", "pages.appsPage.agent", "pages.appsPage.app", diff --git a/website/src/i18n/render-baseline.json b/website/src/i18n/render-baseline.json index 013bf5074da..e9cdd6d1aa4 100644 --- a/website/src/i18n/render-baseline.json +++ b/website/src/i18n/render-baseline.json @@ -231,11 +231,6 @@ "layout": 0, "latent": 0 }, - "capabilities-templates": { - "text": 0, - "layout": 0, - "latent": 0 - }, "developer-archive": { "text": 0, "layout": 0, diff --git a/website/src/i18n/untranslated-baseline.json b/website/src/i18n/untranslated-baseline.json index b0312d972e9..2737a0a62f4 100644 --- a/website/src/i18n/untranslated-baseline.json +++ b/website/src/i18n/untranslated-baseline.json @@ -512,14 +512,6 @@ "prose": 1, "template": 20 }, - "pages/AgentsPage.tsx": { - "total": 12, - "array": 2, - "expression": 3, - "object-prop": 1, - "prose": 3, - "template": 3 - }, "pages/aidlc/TaskDetailPanel.tsx": { "total": 2, "template": 2 diff --git a/website/src/lib/navPinned.ts b/website/src/lib/navPinned.ts index 113fe9cb7f6..d1e066b8380 100644 --- a/website/src/lib/navPinned.ts +++ b/website/src/lib/navPinned.ts @@ -34,6 +34,15 @@ export const NAV_PINNED_KEY = 'mc-nav-pinned' /** Window event dispatched after every persisted change to the pinned set. */ export const NAV_PINNED_CHANGED_EVENT = 'mc:nav-pinned-changed' +/** + * navIds of formerly-pinnable surfaces that were retired from the registry. + * A pin recorded before its surface was retired renders no rail row yet would + * still hold one of the NAV_PINNED_LIMIT slots forever — with the surface gone + * there is no pin control left to release it. Reads drop these ids BEFORE the + * cap applies, and the next write persists the pruned set. + */ +const RETIRED_NAV_PINS = new Set(['capabilities-templates']) + /** * How many sub-items may be promoted at once. * @@ -63,7 +72,9 @@ export function readNavPinned(): Set { const parsed: unknown = JSON.parse(raw) if (!Array.isArray(parsed)) return new Set() return new Set( - parsed.filter((id): id is string => typeof id === 'string').slice(0, NAV_PINNED_LIMIT), + parsed + .filter((id): id is string => typeof id === 'string' && !RETIRED_NAV_PINS.has(id)) + .slice(0, NAV_PINNED_LIMIT), ) } catch { return new Set() diff --git a/website/src/pages/AgentsPage.tsx b/website/src/pages/AgentsPage.tsx deleted file mode 100644 index a26188bf2d2..00000000000 --- a/website/src/pages/AgentsPage.tsx +++ /dev/null @@ -1,1129 +0,0 @@ -import { Fragment, useState, useEffect, useLayoutEffect, useMemo, useRef, type ReactNode } from 'react' -import { useQuery, useMutation } from '@tanstack/react-query' -import { Star, Brain, Plug, Pin, Package, Lock, Hourglass, Bot, ChevronDown, LayoutTemplate, X } from 'lucide-react' -import { createPortal } from 'react-dom' -import { api } from '../api/client' -import { defaultAgentQuery } from '../api/defaultAgentQuery' -import type { SubagentInfo } from '../types' -import Clickable from '../components/Clickable' -import { SourceBadge, PageHeader, EmptyState, Btn, Input, SearchInput, Card, CardTitle, Badge } from '../components/ui' -import { templateSourceLabel, templateSourceBadge } from '../lib/templateSource' -import ModelDropdownList from '../components/ModelDropdownList' -import AgentSkillsEditor from '../components/AgentSkillsEditor' -import SimpleSelect from '../components/SimpleSelect' -import CrewAvatar from '../components/CrewAvatar' -import type { KiroCrewAgent } from '../components/AgentSelector' -import InfoTip from '../components/InfoTip' -import ListDetailBack from '../components/ListDetailBack' -import ErrorNotice from '../components/ErrorNotice' -import { useProvider } from '../providers' -import { useFilteredDropdown } from '../hooks/useFilteredDropdown' -import { useAvailableModels } from '../hooks/useAvailableModels' -import { useListboxKeyboard } from '../hooks/useListboxKeyboard' -import { useListDetailView } from '../hooks/useListDetailView' -import { fmtPercent } from '../i18n/format' -import { formatCost } from '../utils/formatCost' - -import { i18nT } from '../i18n/t' -import { errMessage } from '../utils/thunkError' -/** Human text for a rejected request or caught exception — the one string every - * ErrorNotice on this page keys on. */ -/** `null` for "no failure" (so `` renders nothing), else - * the shared reader's text with a fallback for a message-less rejection. */ -const failText = (e: unknown): string | null => (e == null ? null : errMessage(e) || i18nT('components.errorBoundary.something_went_wrong')) - -function fmtTokens(n: number): string { - return n >= 1_000_000 ? (n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1) + 'M' : (n / 1_000).toFixed(0) + 'K' -} - -function barColor(pct: number): string { - if (pct >= 90) return 'bg-danger' - if (pct >= 70) return 'bg-warn' - return 'bg-accent' -} - -function barGlow(pct: number): string { - if (pct >= 90) return 'shadow-[0_0_8px_var(--danger)]' - if (pct >= 70) return 'shadow-[0_0_8px_var(--warn)]' - return 'shadow-[0_0_8px_var(--accent-glow)]' -} - -/** Text colour for the part of a label that lands ON `barColor`'s fill. - * - * Same thresholds as `barColor`, resolved to the fill's paired foreground token. - * `themeFillForeground.test.ts` holds every BUILT-IN theme's pair to a 4.5:1 - * floor, and to being no worse than `--text-strong` on the same fill. An - * installed theme pack can still declare a low-contrast `--accent-fg` and these - * labels inherit it — the same exposure every `text--fg` consumer has, not - * a guarantee this component can make on its own. - */ -function barFg(pct: number): string { - if (pct >= 90) return 'text-danger-fg' - if (pct >= 70) return 'text-warn-fg' - return 'text-accent-fg' -} - -/** The percentage / amount labels drawn on top of a meter bar. - * - * A single static colour cannot work here: the labels overlap both the - * saturated fill and the empty track, and which one is under a given glyph - * moves with the value. `text-text-strong` alone is near-black in every light - * theme, so at 62% the percentage read as dark-on-purple. - * - * So the row is painted twice with complementary clips — off-fill colour on - * the track side, `barFg` on the fill side — cut at exactly the fill's edge. - * A glyph straddling that edge is split down the middle, which keeps contrast - * at every value with no measuring, no resize observer, and no threshold to - * guess. `className` carries the bar's own transition duration so the cut - * travels WITH the fill instead of jumping ahead of it. - */ -function BarLabels({ pct, left, right, className = '' }: { pct: number; left: React.ReactNode; right: React.ReactNode; className?: string }) { - // Match the fill's own `Math.max(pct, 1)` floor so the two edges coincide, - // and clamp the top end: `inset()` past 100% (or NaN from a malformed - // context_pct) drops the clip entirely, which would paint BOTH layers over - // each other and lose the split. - const edge = Number.isFinite(pct) ? Math.min(Math.max(pct, 1), 100) : 1 - const row = `absolute inset-0 flex items-center justify-between ${className}` - const labels = <>{left}{right} - return ( - <> -
{labels}
- - - ) -} - -/** Strip the provider prefix and version suffix a model id carries. */ -function shortModel(model: string): string { - return model === 'auto' ? 'auto' : model.replace(/^(us\.|anthropic\.|amazon\.)/, '').replace(/-v\d+:\d+$/, '') -} - -interface CtxSession { key: string; name: string; model: string; agent?: string; context_pct: number; context_window_tokens?: number; prompts: number } - -/** - * A safe display string for an agent's `model`. - * - * The backend now coerces `model` to a string on both the installed-list and - * detail endpoints, but `api.agentDetail` is otherwise a pass-through of a - * user-editable JSON spec from a SHARED directory that other tools write into. - * A non-string that slips through (e.g. an ACP-style `{"id": "..."}`) rendered - * as a JSX child throws React error #31 and puts the whole Agent Templates tab - * into the error boundary — one bad file hiding every other agent. Belt and - * braces: anything that is not a string degrades to `auto` for that one row. - */ -function modelLabel(model: unknown): string { - return typeof model === 'string' && model ? model : 'auto' -} - - -/** Shape of an installed-agent list item (also the `api.agentsInstalled` element). */ -interface InstalledAgent { - name: string - description: string - source: string - model: string - skills: string[] - mcp_servers: string[] - package?: string - filename?: string - /** Display-only: the file is one Kiro Crew maintains itself. Outranks a - * package-shaped filename when labelling where the template came from. */ - kirocrew_owned?: boolean -} - -/** - * Fields the detail pane reads off the selected agent. It's populated from - * `api.agentDetail` (rich, dynamic backend payload) or, as a fallback, from an - * installed-list item — so detail-only fields are optional and the installed - * item's fields are folded in. - */ -interface AgentDetail extends Partial { - name: string - prompt?: string - tools?: string[] - allowedTools?: string[] - mcpServers?: Record - toolsSettings?: { execute_bash?: { deniedCommands?: string[] } } - /** `skill://` resources the catalog editor cannot express (wildcards, foreign paths). */ - unmanaged_skills?: string[] -} - -/** Which pane of the inspector is showing. */ -type DetailTab = 'overview' | 'skills' | 'tools' | 'prompt' | 'guardrails' - - -/** One section inside a detail tab: a small caps heading over its content. */ -function Section({ title, children }: { title: ReactNode; children: ReactNode }) { - return ( -
-

{title}

- {children} -
- ) -} - -/** Label → value pair used by the provenance block. */ -function Kv({ label, value }: { label: string; value: ReactNode }) { - return ( - <> - {label} - {value} - - ) -} - -/** A count chip in "At a glance" — clicking it opens the tab that owns the detail. */ -function GlanceChip({ label, count, tone, onClick }: { label: string; count: number; tone?: 'ok' | 'aim' | 'danger'; onClick: () => void }) { - const toneCls = tone === 'ok' - ? 'text-ok border-ok/35 bg-ok-subtle' - : tone === 'aim' - ? 'text-aim border-aim/35 bg-aim-subtle' - : tone === 'danger' - ? 'text-danger border-danger/35 bg-danger-subtle' - : 'text-text border-border bg-bg-elevated' - return ( - - {count} - {label} - - ) -} - -/** Live context-window utilisation per active session. */ -function ContextUsageCard({ ctx, installed, error, quiet }: { ctx: CtxSession[]; installed: InstalledAgent[]; error?: string | null; quiet?: boolean }) { - const provider = useProvider() - return ( - - {i18nT('pages.agentsPage.context_window_usage')} - {/* A failed poll must not read as "no active sessions": the list below is - empty in both cases. Read-only card, so the hand-off loses nothing. */} - {/* `quiet`: the same failure is already named by the page-level notice - above, so the sentence is not repeated — but an empty list under a - failed read is still unknown, never "no active sessions". */} - {!quiet && } - {ctx.length === 0 ? (error ? null :

{i18nT('pages.agentsPage.no_active_sessions')}

) : ( -
- {ctx.map(s => { - // Prefer the real served window the backend reports (the one - // the pct was actually computed against). Fall back to the - // model-id heuristic only when the backend hasn't reported a - // window yet (e.g. before the first usage_update). Using the - // heuristic unconditionally can inflate the token text ~5x when - // a "[1m]" model is actually served at 200k by the backend. - const maxTokens = s.context_window_tokens && s.context_window_tokens > 0 - ? s.context_window_tokens - : provider.getContextWindow(s.model) - const usedTokens = Math.round(maxTokens * s.context_pct / 100) - const pct = Math.min(s.context_pct, 100) - const awaiting = s.context_pct === 0 && s.prompts === 0 - return ( -
-
-
{s.name}
-
{s.agent && s.agent !== 'kirocrew' ? a.name === s.agent)?.source === 'kirocrew' ? 'text-accent' : 'text-aim'}`}>{s.agent} : null}{shortModel(s.model)}
-
-
- {awaiting ? ( -
{i18nT('pages.agentsPage.awaiting_first_prompt')}
- ) : (<> -
5 ? barGlow(pct) : ''}`} - style={{ width: `${Math.max(pct, 1)}%` }} - /> - {fmtTokens(usedTokens)} / {fmtTokens(maxTokens)}} - /> - )} -
-
- {i18nT('pages.agentsPage.prompt', { count: s.prompts })} - {awaiting ? <> {i18nT('pages.agentsPage.idle')} : pct >= 90 ? <> {i18nT('pages.agentsPage.critical')} : pct >= 70 ? <> {i18nT('pages.agentsPage.high')} : <> {i18nT('pages.agentsPage.healthy')}} -
-
- ) - })} -
- )} - - ) -} - -interface SessionsUsage { - plan?: string - resets?: string - credits_used?: number | null - credits_plan?: number | null - credits_overage?: number | null - cost_usd?: number | null - overage_rate?: number | string | null - email?: string - account?: string - account_type?: string -} - -/** Mask an account email down to a recognizable-but-not-readable form. - * - * The Agents page is a common screen-share surface, so the full address is not - * rendered: the first local-part character plus the domain is enough to confirm - * WHICH account is signed in, which is the only question this row answers. - * Returns null for input that cannot be masked, so callers omit the row rather - * than showing a placeholder that reads as a real value. - */ -export function maskAccountEmail(raw: string | undefined): string | null { - const value = (raw || '').trim() - if (!value) return null - const at = value.lastIndexOf('@') - // Nothing before the '@' means there is no local-part character to keep, so - // there is no recognizable-but-masked form to show at all. - if (at === 0) return null - // No domain to anchor on (whoami reported a bare handle): mask all but the - // first character rather than leaking the whole string. - if (at < 0) return `${value[0]}•••` - return `${value[0]}•••${value.slice(at)}` -} - -/** Render guard for a provider-supplied label that may or may not be an address. - * - * Masks anything address-shaped and passes everything else through unchanged, so - * a label's own semantics decide nothing about whether an address can reach the - * DOM. Returns null for an empty value so callers omit the row or segment. - */ -export function addressSafeLabel(raw: string | undefined): string | null { - const value = (raw || '').trim() - if (!value) return null - return value.includes('@') ? maskAccountEmail(value) : value -} - -/** Localized label for the auth-type enum `whoami` reports. - * - * `account_type` arrives as a code identifier (`IamIdentityCenter`, `BuilderId`, - * `Social`). The top-bar credit modal already maps these to localized prose, so - * the same keys are reused here — otherwise one surface shows a camelCase - * identifier while the other shows prose for the same account. An unrecognized - * value falls through to the address guard, since a value outside the known set - * is not covered by the field's contract. - */ -export function authTypeLabel(raw: string | undefined): string | null { - switch (raw) { - case 'IamIdentityCenter': - return i18nT('app.iam_identity_center') - case 'BuilderId': - return i18nT('app.builder_id') - case 'Social': - return i18nT('app.social_login') - default: - return addressSafeLabel(raw) - } -} - -/** The Account row's display value, preferring the email over the profile name. - * - * `usage.account` is the org profile's display name straight from the provider - * (`profileName` / `profileDisplayName`), i.e. an arbitrary server-supplied - * label — and orgs do name profiles after a person's address. Routing it through - * the same guard is what stops a profile name being a way around the masking the - * email path applies. - */ -export function accountDisplayValue(usage: Pick): string | null { - return maskAccountEmail(usage.email) || addressSafeLabel(usage.account) -} - -/** Plan credits and spend for the current billing period. */ -function ProviderUsageCard({ usage }: { usage: SessionsUsage }) { - const provider = useProvider() - // The backend attaches identity only when it can tie the signed-in account to - // the one these credits are billed to. The API path gates that merge on a - // matching profile ARN; the text-scrape path establishes it instead by - // resolving whoami adjacently to the scrape. Neither always succeeds, so an - // absent account is a legitimate steady state, not an error to retry. - const account = accountDisplayValue(usage) - // `account_type` is a code identifier from whoami, mapped to localized prose so - // this row reads the same as the credit modal. The mapper address-guards any - // value outside the known enum. - const authType = authTypeLabel(usage.account_type) - return ( - -
- {provider.displayName} {i18nT('pages.agentsPage.usage')} -
- {usage.plan && {usage.plan}} - {usage.resets && {i18nT('pages.agentsPage.resets')} {usage.resets}} -
-
-
-
{i18nT('pages.agentsPage.harness')}
-
- {provider.displayName} - {authType && · {authType}} -
-
{i18nT('pages.agentsPage.account')}
-
- {account || ( - - {i18nT('pages.agentsPage.account_not_reported')} - - - )} -
-
- {usage.credits_used != null && usage.credits_plan != null && (() => { - const creditsUsed = usage.credits_used as number - const creditsPlan = usage.credits_plan as number - const pctRaw = creditsPlan > 0 ? (creditsUsed / creditsPlan) * 100 : 0 - const pct = Math.min(pctRaw, 100) - // credits_used is the true total; credits_overage the amount above - // plan (fall back to used - plan when the source omits it). - const overage = usage.credits_overage ?? Math.max(0, creditsUsed - creditsPlan) - const hasOverage = overage > 0 - // barColor/barGlow already encode this exact ladder for the context bars. - const color = barColor(pct) - const glow = barGlow(pct) - return ( -
-
{i18nT('pages.agentsPage.plan_credits')}
-
-
5 ? glow : ''}`} style={{ width: `${Math.max(pct, 1)}%` }} /> - {creditsUsed.toFixed(0)} / {creditsPlan.toFixed(0)}} - /> -
-
-
- {hasOverage && {i18nT('pages.agentsPage.overage_credits')} {overage.toFixed(1)}} -
-
- {i18nT('pages.agentsPage.est_cost')} {formatCost(usage.cost_usd)} - {usage.overage_rate && ${usage.overage_rate}{i18nT('pages.agentsPage.req')}} -
-
-
- ) - })()} - - ) -} - -/** Spawned subagent runs and their state. */ -function SubagentsCard({ agents, error, quiet, onDismissError, onClear, onDelete }: { agents: SubagentInfo[]; error?: string | null; quiet?: boolean; onDismissError?: () => void; onClear: () => void; onDelete: (id: string) => void }) { - return ( - - {i18nT('pages.agentsPage.subagents')} {agents.some(a => a.done) && {i18nT('pages.agentsPage.clear_completed')}} - {/* Status panel with nothing unsaved, so the hand-off is safe. A failed - list fetch would otherwise render as an empty table, and a failed - clear/delete would leave the row in place with no explanation. */} - {/* `quiet`: see ContextUsageCard — the page-level notice already names - this failure; the empty state below still stays off while it stands. */} - {!quiet && } - {[i18nT('pages.agentsPage.id'), i18nT('pages.agentsPage.task'), i18nT('pages.agentsPage.status'), ''].map(h => )} - {agents.length === 0 ? (error ? null : ) : agents.map(a => ( - - - - - {/* The badge alone said "failed" and hid WHY. The run is over and the - text is already persisted server-side, so handing it off loses nothing. */} - {a.done && a.error && ( - - )} - - ))}
{h}
} title={i18nT('pages.agentsPage.no_subagents')} subtitle={i18nT('pages.agentsPage.spawn_tasks_from_chat_or_cli')} />
{a.id}{a.task}{a.done ? (a.error ? {i18nT('pages.agentsPage.failed')} : {i18nT('pages.agentsPage.done')}) : {i18nT('pages.agentsPage.running')}} onDelete(a.id)}>
-
- ) -} - -export default function AgentsPage({ embedded }: { embedded?: boolean } = {}) { - const { data: spawnData, error: spawnListError, refetch: refetchSpawn } = useQuery({ - queryKey: ['spawn-list'], - queryFn: () => api.spawnList(), - }) - const agents: SubagentInfo[] = spawnData?.agents || [] - - const { data: ctx = [], error: ctxError } = useQuery({ - queryKey: ['sessions-context'], - queryFn: () => api.sessionsContext().then(d => d.sessions || []), - refetchInterval: 15000, - }) - - // A rejection is left to react-query here (no `.catch(() => null)`), so a - // failed fetch renders as a failure below rather than as "no plan". - const { data: usage = null, error: usageError } = useQuery({ - queryKey: ['sessions-usage'], - queryFn: () => api.sessionsUsage().then(d => (d.usage && Number.isFinite(d.usage.credits_plan)) ? d.usage : null), - }) - - const { data: installed = [], isPending: installedLoading, error: installedError, refetch: refetchInstalled } = useQuery({ - queryKey: ['agents-installed'], - queryFn: async () => { - const a = await api.agentsInstalled() - if (!Array.isArray(a)) return [] - ;(a as InstalledAgent[]).sort((x, y) => { - if (x.name === 'kirocrew') return -1; if (y.name === 'kirocrew') return 1 - if (x.name === 'kirocrew-lite') return -1; if (y.name === 'kirocrew-lite') return 1 - return x.name.localeCompare(y.name) - }) - return a as InstalledAgent[] - }, - }) - - const { data: mcpTools = {}, error: mcpToolsError } = useQuery({ - queryKey: ['mcp-tools'], - queryFn: async () => { - const probed = await api.mcpProbeCache() - if (!Array.isArray(probed)) return {} - const map: Record = {} - for (const s of probed) if (s.name && s.tools?.length) map[s.name] = s.tools - return map - }, - }) - - /** Crews, so a template can name who it affects. Read-only here — a crew's - * own bindings are edited on the Crews tab. - * - * Deliberately the SAME key and the SAME cached shape as the Crews tab - * (`KiroCrewAgentsPage.tsx`): one shared QueryClient keeps one value per - * key, so an observer that stored the unwrapped array here would be handed - * to that page as `agentsData` (rendering an empty roster) and its object to - * this one (`crews.filter` on an object throws during render). Storing the - * response verbatim and selecting from it also dedupes the fetch. */ - const { data: crewsData, isError: crewsFailed, refetch: refetchCrews } = useQuery<{ agents?: KiroCrewAgent[]; default_agent?: string }>({ - queryKey: ['kirocrew-agents'], - queryFn: () => api.kirocrewAgents(), - }) - const crews = useMemo(() => crewsData?.agents ?? [], [crewsData]) - - const { data: defaultAgentData, isError: defaultFailed, refetch: refetchDefault } = useQuery(defaultAgentQuery) - const defaultAgent = defaultAgentData ?? '' - - const [selectedAgent, setSelectedAgent] = useState(null) - // Narrow viewport shows one pane at a time; a desktop shows both. - const { isMobile, showList, showDetail, openDetail, closeDetail } = useListDetailView() - const [tab, setTab] = useState('overview') - const [filter, setFilter] = useState('') - const [confirmDelete, setConfirmDelete] = useState(false) - const [deleteError, setDeleteError] = useState(null) - /** A failed model pin. Without it the dropdown silently kept the old value. */ - const [modelError, setModelError] = useState(null) - /** A failed fallback-template change (the picker snaps back on refetch, or not at all). */ - const [defaultError, setDefaultError] = useState(null) - const modelOptions = useAvailableModels() - const { open: modelDropOpen, setOpen: setModelDropOpen, filter: modelFilter, setFilter: setModelFilter, dropdownRef: modelDropRef, inputRef: modelInputRef, filtered: filteredModels } = useFilteredDropdown(modelOptions) - // Roving-focus keyboard nav for the model dropdown (shared with StyledSelect/AgentSelector). - const { onListKeyDown: onModelListKeyDown } = useListboxKeyboard({ - open: modelDropOpen, - dropdownRef: modelDropRef, - inputRef: modelInputRef, - hasFilterInput: true, - filteredCount: filteredModels.length, - onEnterSingleMatch: () => { - if (!selectedAgent) return - const m = filteredModels[0].name === 'auto' ? '' : filteredModels[0].name - patchModelMut.mutate({ name: selectedAgent.name, model: m }) - setModelDropOpen(false) - modelBtnRef.current?.focus() - }, - closeToTrigger: () => { setModelDropOpen(false); modelBtnRef.current?.focus() }, - }) - const modelBtnRef = useRef(null) - const patchModelMut = useMutation({ - mutationFn: ({ name, model }: { name: string; model: string }) => api.agentPatch(name, { model }), - onSuccess: (_r, { model }) => { setSelectedAgent(prev => (prev ? { ...prev, model } : prev)); setModelError(null); refetchInstalled() }, - onError: (err: unknown) => setModelError(errMessage(err) || i18nT('components.errorBoundary.something_went_wrong')), - }) - const deleteAgentMut = useMutation({ - mutationFn: (name: string) => api.agentDelete(name), - // Deleting the agent you were looking at must also leave its detail pane. - // On a phone the panes are exclusive, so clearing the selection alone left - // `detailOpen` true with nothing selected: the placeholder branch renders - // without the Back control (that lives in the `selectedAgent` branch) and - // the roster — including its filter — is inside the hidden list pane, so - // there was no in-page way back. - onSuccess: (_r, name) => { - if (selectedAgent?.name === name) { setSelectedAgent(null); closeDetail() } - setDeleteError(null) - refetchInstalled() - }, - /* The handler — not this page — is the authority on references: it checks - * the fallback and crew bindings under the same config lock the writers - * take, so it refuses (409) races this cached snapshot cannot see. Show its - * reason rather than failing silently, and refetch both reference queries: - * that re-derives `blockedBy`, so the control the user just used disappears - * on its own instead of inviting a second identical attempt. */ - onError: (err: unknown) => { - setDeleteError(err instanceof Error ? err.message : String(err)) - setConfirmDelete(false) - refetchDefault() - refetchCrews() - }, - }) - const spawnClearMut = useMutation({ mutationFn: () => api.spawnClear(), onSuccess: () => refetchSpawn() }) - const spawnDeleteMut = useMutation({ mutationFn: (id: string) => api.spawnDelete(id), onSuccess: () => refetchSpawn() }) - // One surface for the card: the list fetch, or whichever action last failed. - const spawnActionError = failText(spawnClearMut.error) ?? failText(spawnDeleteMut.error) - const spawnError = failText(spawnListError) ?? spawnActionError - // One transport failure takes every read on this page down at once. The - // roster notice (with its Retry) is the page-level report of that; a card - // whose own failure reads identically is then left to its content, so the - // same sentence is not stacked four times. A card failing for a DIFFERENT - // reason still names it. - const rosterFailure = failText(installedError) - const isRosterFailure = (msg: string | null) => !!msg && msg === rosterFailure - - const setDefaultMut = useMutation({ - mutationFn: (next: string) => api.setDefaultAgent(next), - onSuccess: () => { setDefaultError(null); refetchDefault() }, - onError: (err: unknown) => setDefaultError(errMessage(err) || i18nT('components.errorBoundary.something_went_wrong')), - }) - - /** Monotonic id for the in-flight detail fetch. Without it the LAST response - * wins rather than the LAST CLICK: a slow fetch for B landing after C was - * selected would swap the pane to B while `confirmDelete` — cleared only by - * the name-change layout effect below — is still armed, so the confirm names and - * targets B for a commit, and a click there deletes B's config file. */ - const selectSeq = useRef(0) - - const select = async (a: InstalledAgent) => { - const seq = ++selectSeq.current - // Drill into the detail before awaiting: while narrow the detail pane - // replaces the list, so waiting for the fetch would leave the tap with no - // visible effect for as long as it takes. - openDetail() - // Synchronous, not effect-deferred: arming is destructive, so it must be - // dropped the moment a different template is asked for. - setConfirmDelete(false) - // The refusal named the PREVIOUS template; keeping it would misattribute. - setDeleteError(null) - setTab('overview') - try { - const d = await api.agentDetail(a.name) - if (seq !== selectSeq.current) return - setSelectedAgent(d) - } catch { - if (seq !== selectSeq.current) return - // List rows carry display NAMES; the editor round-trips catalog KEYS, so - // drop them rather than offer unsavable chips. - setSelectedAgent({ ...a, skills: undefined }) - } - } - - // Auto-open detail for first installed agent - const { data: initialAgentDetail, error: initialDetailError, refetch: refetchInitialDetail } = useQuery({ - queryKey: ['agent-detail', installed[0]?.name], - queryFn: () => api.agentDetail(installed[0]!.name), - enabled: !selectedAgent && installed.length > 0, - }) - // `selectSeq.current === 0` matters as much as `!selectedAgent`: once the user - // has clicked a row, a slow first-template fetch resolving late must not take - // the pane back (which could seat an armed confirm on the wrong template). - useEffect(() => { - if (initialAgentDetail && !selectedAgent && selectSeq.current === 0) setSelectedAgent(initialAgentDetail) - }, [initialAgentDetail, selectedAgent]) - - // Backstop for the selections `select()` does not make — the auto-open query - // above, and a delete that clears the pane. A layout effect resets the pane - // before its newly rendered tabs can be activated; `select()` disarms - // synchronously before awaiting detail. - const selectedName = selectedAgent?.name - useLayoutEffect(() => { setTab('overview'); setConfirmDelete(false); setDeleteError(null); setModelError(null) }, [selectedName]) - - /** Arming the delete unmounts the button that was focused, which would drop - * focus to and leave a keyboard user nowhere. Focus moves onto - * CANCEL, not the confirm: Enter fires a native button's click on keydown - * and auto-repeats, so parking focus on "Yes, delete it" would let one held - * Enter arm and then delete in a single gesture — defeating the two-step - * this replaced a native confirm() with. The bar is role="alert", so its - * copy is announced either way. */ - const cancelBtnRef = useRef(null) - useEffect(() => { if (confirmDelete) cancelBtnRef.current?.focus() }, [confirmDelete]) - - const filtered = useMemo(() => { - const q = filter.trim().toLowerCase() - if (!q) return installed - return installed.filter(a => [a.name, a.description, a.package, modelLabel(a.model), ...(a.skills || []), ...(a.mcp_servers || [])] - .filter(Boolean).join(' ').toLowerCase().includes(q)) - }, [installed, filter]) - - /** Non-package templates first, then one group per package. */ - const groups = useMemo(() => { - const loose = filtered.filter(a => a.source !== 'package') - const byPackage = filtered.filter(a => a.source === 'package').reduce>((g, a) => { - const key = a.package || a.name; (g[key] ||= []).push(a); return g - }, {}) - return { loose, byPackage } - }, [filtered]) - - const listed = installed.find(a => a.name === selectedAgent?.name) - const usedBy = useMemo( - () => (selectedName ? crews.filter(c => c.kiro_agent === selectedName) : []), - [crews, selectedName], - ) - /** Deleting a template something still points at leaves a dangling reference, - * and the next session that resolves it fails to start — a crew bound to it - * boots from nothing, and the fallback name is handed to kiro-cli as an - * agent id. So the guard is not just "who owns this file" but "is anything - * still pointing at it", which this page can finally answer because it reads - * both the fallback setting and the crew roster. - * - * Unresolved data counts as "pointing at it", not as "nothing points at it": - * an unresolved query leaves `usedBy` empty and `defaultAgent` '', which - * would read as clear. An in-flight `setDefaultMut` is the same hazard with a - * narrower window — the PUT can land before the refetch, so `defaultAgent` - * still names the OLD template while config already names this one. */ - /* `isError` is as load-bearing as the undefined check. A REFETCH that fails - * keeps the last good data, so `crewsData`/`defaultAgentData` stay defined - * and the mutation settles — the guard would then compare against a name the - * server no longer has. Stale-and-known-stale is unknown, so it blocks. */ - const crewsLoaded = crewsData !== undefined && !crewsFailed - const defaultLoaded = defaultAgentData !== undefined && !defaultFailed - const blockedBy: 'owner' | 'default' | 'crews' | 'unloaded' | null = - !listed ? 'owner' - : (listed.source === 'kirocrew' || listed.source === 'package') ? 'owner' - : (!defaultLoaded || !crewsLoaded || setDefaultMut.isPending) ? 'unloaded' - : defaultAgent === listed.name ? 'default' - : usedBy.length > 0 ? 'crews' - : null - const deletable = blockedBy === null - const modelPinned = modelLabel(selectedAgent?.model) !== 'auto' - - const renderRow = (a: InstalledAgent) => ( - // The shared control, not a raw
, document.body )} diff --git a/website/src/pages/KiroCrewAgentsPage.tsx b/website/src/pages/KiroCrewAgentsPage.tsx index ffc9afcae42..3a1d091d10e 100644 --- a/website/src/pages/KiroCrewAgentsPage.tsx +++ b/website/src/pages/KiroCrewAgentsPage.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy, useState, useEffect, useCallback, useMemo, useRef } from 'react' +import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { ApiError } from '../api/apiError' import { Boxes, FolderOpen, Database, Sparkles, Plus, MessageSquare, Users, Star, LayoutGrid, Rows3, UserPen } from 'lucide-react' @@ -16,6 +16,7 @@ import { Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '../components/ui/dialog' import SegmentedControl from '../components/SegmentedControl' +import ErrorBoundary from '../components/ErrorBoundary' import InfoTip from '../components/InfoTip' import { FOCUSABLE } from '../hooks/useDialogFocusTrap' import SimpleSelect from '../components/SimpleSelect' @@ -28,10 +29,7 @@ import CrewWakeSection from '../components/CrewWakeSection' import CrewWebhookSection from '../components/CrewWebhookSection' import CrewEditorRail from '../components/crew/CrewEditorRail' import CrewOverviewPane from '../components/crew/CrewOverviewPane' -// Lazy: the panel is behind the `agent_template_pane` flag, so its code -// stays out of the main chunk until a crew editor actually renders it. -const AgentTemplateDetail = lazy(() => import('../components/crew/AgentTemplateDetail')) -import { useAgentTemplatePaneEnabled } from '../hooks/useAgentTemplatePane' +import AgentTemplateDetail from '../components/crew/AgentTemplateDetail' import { useCrewEditorSections, type CrewPaneKey } from '../components/crew/crewEditorSections' import { wakesCrew, crewWakeQueryKey, crewWebhooksQueryKey, webhookBoundToCrew, webhookCanCallIn } from '../components/crew/wakesCrew' import type { CronJob } from '../types' @@ -890,7 +888,6 @@ export default function KiroCrewAgentsPage({ embedded }: { embedded?: boolean } // picker so the list is fetched once. INHERIT_MODEL leads so "no pin" is the // obvious choice rather than an absent option. const availableModels = useAvailableModels() - const templatePaneEnabled = useAgentTemplatePaneEnabled() const modelOptions = [ INHERIT_MODEL, ...(availableModels || []).map((m: { name: string }) => m.name).filter((n: string) => n && n !== INHERIT_MODEL), @@ -1862,7 +1859,7 @@ export default function KiroCrewAgentsPage({ embedded }: { embedded?: boolean } // changes" footer would advertise a second, contradictory save model over the // pane's own "saved as you go" copy. Hide the footer there — the dialog's // built-in ✕ still closes it, and the pane surfaces its own errors. - const templatePaneActive = !creating && templatePaneEnabled && pane === 'template' + const templatePaneActive = !creating && pane === 'template' return ( <> @@ -2233,55 +2230,43 @@ export default function KiroCrewAgentsPage({ embedded }: { embedded?: boolean } )} {pane === 'template' && ( - <> - {templatePaneEnabled ? ( - /* The panel owns the selector: the template picker is - the header bar of the container holding the - definition it names (v5 design, usability-reviewed). - The crew's private copy is filtered from the shared - catalog; the panel re-adds the current binding when - needed. */ - - <> - {/* No askAgent hand-off: it navigates to /chat, - unmounting this sheet and destroying its - unsaved pane edits (dirtyPanes). Errors inside - the editor render in place, never as a - hand-off. */} - - m.name).filter(Boolean)} - crew={editing || undefined} - onForked={setKiroAgent} - options={kiroAgentOptions} - onSelect={persistTemplateSwitch} - onRebound={setKiroAgent} - provenance={templateProvenance} - fieldLabel={provider.labels.agentTemplateField} - onSaveChain={onPaneSaveChain} - /> - - - ) : ( - + <> + {/* No askAgent hand-off: it navigates to /chat, + unmounting this sheet and destroying its + unsaved pane edits (dirtyPanes). Errors inside + the editor render in place, never as a + hand-off. */} + + m.name).filter(Boolean)} + crew={editing || undefined} + onForked={setKiroAgent} + options={kiroAgentOptions} + onSelect={persistTemplateSwitch} + onRebound={setKiroAgent} provenance={templateProvenance} + fieldLabel={provider.labels.agentTemplateField} + onSaveChain={onPaneSaveChain} /> - )} - + + )} {pane === 'model' && ( diff --git a/website/src/pages/chat/ActivityViewer.tsx b/website/src/pages/chat/ActivityViewer.tsx index ca44f843a0e..c7d69b9c701 100644 --- a/website/src/pages/chat/ActivityViewer.tsx +++ b/website/src/pages/chat/ActivityViewer.tsx @@ -919,8 +919,8 @@ export default function ActivityViewer({ subagents, toolLog, open, onToggle, slo }, [failedRetryableIds]) const dismissDone = useCallback(() => { // Slot-scoped by construction: delete exactly this slot's terminal cards - // by id — the global DELETE /api/spawn clear would nuke other sessions' - // completed agents too (their cards would 404 on status/output). + // by id via DELETE /api/spawn/{id}. There is no global clear route, so a + // cross-session wipe is not reachable from here. // The local clear stays optimistic; a refused delete is reported so the // user knows the card still exists server-side. setBatchError(null) diff --git a/website/src/store/chatSlice.ts b/website/src/store/chatSlice.ts index 91d6d7a94da..60244fb4fdc 100644 --- a/website/src/store/chatSlice.ts +++ b/website/src/store/chatSlice.ts @@ -4509,7 +4509,7 @@ const chatSlice = createSlice({ state.selectedSubagentId = action.payload }, /** "Dismiss done": drop terminal cards for a slot (backend clear is the - * caller's job via DELETE /api/spawn; this trims the local view). */ + * caller's job via per-id DELETE /api/spawn/{id}; this trims the local view). */ clearTerminalSubagents(state, action: PayloadAction<{ slot: string }>) { const slot = action.payload.slot if (isUnsafeKey(slot)) return diff --git a/website/src/surfaces/builtins.tsx b/website/src/surfaces/builtins.tsx index 89ff8b97e17..692491024cd 100644 --- a/website/src/surfaces/builtins.tsx +++ b/website/src/surfaces/builtins.tsx @@ -7,7 +7,7 @@ * Order in this file = order in the rail (within each group). Add new * built-in surfaces here; do not add hardcoded badge logic to `App.tsx`. */ -import { MessageSquare, Bell, Component, CalendarDays, Settings, ClipboardCheck, Compass, Webhook, Users, LayoutTemplate, BookOpen, Link2, Library, MessageSquareText, Workflow, ScrollText, Bot } from 'lucide-react' +import { MessageSquare, Bell, Component, CalendarDays, Settings, ClipboardCheck, Compass, Webhook, Users, BookOpen, Link2, Library, MessageSquareText, Workflow, ScrollText, Bot } from 'lucide-react' import type { ReactElement } from 'react' import { createSelector } from '@reduxjs/toolkit' import { KiroGhostMark } from '../components/KiroGhostMark' @@ -223,7 +223,6 @@ registerBuiltinSurface({ // copy, and the strict i18n config looks inside ALL-CAPS module constants. const CAPABILITY_SUB_ITEMS: readonly { tab: string; labelKey: string; label: string; icon: ReactElement }[] = [ { tab: 'crews', labelKey: 'pages.capabilitiesPage.crews_label', label: surfaceMachineValue('Crews'), icon: }, - { tab: 'templates', labelKey: 'pages.capabilitiesPage.templates_label', label: surfaceMachineValue('Agent Templates'), icon: }, { tab: 'skills', labelKey: 'pages.capabilitiesPage.skills_label', label: surfaceMachineValue('Skills'), icon: }, { tab: 'mcp', labelKey: 'pages.capabilitiesPage.connections_label', label: surfaceMachineValue('Connections'), icon: }, { tab: 'knowledge', labelKey: 'pages.capabilitiesPage.knowledge_label', label: surfaceMachineValue('Knowledge'), icon: }, diff --git a/website/src/test/AgentTemplateDetail.test.tsx b/website/src/test/AgentTemplateDetail.test.tsx index ad3bcecb419..9dd2702f6eb 100644 --- a/website/src/test/AgentTemplateDetail.test.tsx +++ b/website/src/test/AgentTemplateDetail.test.tsx @@ -28,13 +28,11 @@ const mockApi = vi.hoisted(() => ({ agentPublish: vi.fn(), agentReset: vi.fn(), updateKirocrewAgent: vi.fn(), - agentDelete: vi.fn(), skills: vi.fn(), })) vi.mock('../api/client', () => ({ api: mockApi })) import AgentTemplateDetail from '../components/crew/AgentTemplateDetail' -import { agentTemplatePaneEnabled } from '../hooks/useAgentTemplatePane' const DENIED = Array.from({ length: 11 }, (_, i) => `denied-${i}`) const FIELD_LABEL = 'Agent Template' @@ -105,7 +103,6 @@ beforeEach(() => { mockApi.agentPublish.mockResolvedValue({ template: 'published-name' }) mockApi.agentReset.mockResolvedValue({}) mockApi.updateKirocrewAgent.mockResolvedValue({}) - mockApi.agentDelete.mockResolvedValue({}) // A plain array, which is what the endpoint really answers — an object with an // `agents` key silently blanks provenance, so the fixture has to match. mockApi.agentsInstalled.mockResolvedValue([ @@ -128,17 +125,6 @@ beforeEach(() => { }) }) -describe('agent_template_pane flag', () => { - it('is off unless the config says exactly true', () => { - // A truthy-but-not-true value must not open a surface that relocates - // shipped content — same contract as the connections_ui flag. - expect(agentTemplatePaneEnabled(undefined)).toBe(false) - expect(agentTemplatePaneEnabled({})).toBe(false) - expect(agentTemplatePaneEnabled({ agent_template_pane: 'yes' })).toBe(false) - expect(agentTemplatePaneEnabled({ agent_template_pane: true })).toBe(true) - }) -}) - describe('the header bar is the template selector', () => { it('labels the selector "Template" and shows the bound template as its value', async () => { renderPane() @@ -309,7 +295,6 @@ describe('the crew\'s own copy bound', () => { expect(mockApi.agentReset).toHaveBeenCalledWith('atlas-crewA', 'crewA'), ) expect(mockApi.updateKirocrewAgent).not.toHaveBeenCalled() - expect(mockApi.agentDelete).not.toHaveBeenCalled() await waitFor(() => expect(onSelect).toHaveBeenCalledWith('atlas')) }) diff --git a/website/src/test/AgentsPage.defaultAgent.test.tsx b/website/src/test/AgentsPage.defaultAgent.test.tsx deleted file mode 100644 index e5d35412eae..00000000000 --- a/website/src/test/AgentsPage.defaultAgent.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * The Agent Templates page's default-template control. - * - * This used to be a star pill on every row, which had to mean both "this is the - * default" (state) and "make this the default" (control) with the same glyph, - * and a StatCard above the list restating the answer. It is now one picker: the - * assertions below are about that single place naming the current default and - * writing the new one without a Save step. - * - * The label is asserted verbatim on purpose. This control writes - * `config.agent.default_agent`, which is the fallback kiro-cli agent for CLI - * chat, chat-channel threads and warm-pool sessions — NOT what a dashboard - * session uses (that comes from its crew). A label reading "new sessions use" - * would describe a different setting than the one being written. - */ -import { render, screen, waitFor, fireEvent } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - agentMetadata: vi.fn(), - kirocrewAgents: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 1, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([{ name: 'claude-opus-4.8', description: '' }]), - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -const mkAgent = (name: string) => ({ - name, - description: `${name} agent`, - source: 'builtin', - model: 'claude-opus-4.8', - skills: [], - mcp_servers: [], - filename: `${name}.json`, -}) - -function renderPage() { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([mkAgent('kirocrew'), mkAgent('fable')]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.agentMetadata.mockResolvedValue({ content: '' }) - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - mockApi.skills.mockResolvedValue([]) - mockApi.agentDetail.mockResolvedValue({ ...mkAgent('kirocrew'), unmanaged_skills: [] }) - mockApi.setDefaultAgent.mockResolvedValue({ ok: true, default_agent: '' }) -}) - -describe('AgentsPage default template', () => { - it('names the current default in one place', async () => { - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'fable' }) - renderPage() - - const picker = await screen.findByRole('combobox', { name: 'When nothing names a template, use' }) - expect(picker).toHaveTextContent('fable') - // The state is no longer duplicated onto every row as a control. - expect(screen.queryByText('Set as default')).not.toBeInTheDocument() - }) - - it('writes the pick immediately, with no Save step', async () => { - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'kirocrew' }) - renderPage() - - fireEvent.click(await screen.findByRole('combobox', { name: 'When nothing names a template, use' })) - fireEvent.click(await screen.findByRole('option', { name: 'fable' })) - - await waitFor(() => expect(mockApi.setDefaultAgent).toHaveBeenCalledWith('fable')) - expect(mockApi.agentPatch).not.toHaveBeenCalled() - }) - - it('can clear the default from the same picker', async () => { - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'kirocrew' }) - renderPage() - - fireEvent.click(await screen.findByRole('combobox', { name: 'When nothing names a template, use' })) - fireEvent.click(await screen.findByRole('option', { name: 'No default template' })) - - await waitFor(() => expect(mockApi.setDefaultAgent).toHaveBeenCalledWith('')) - }) - - it('is absent when there are no templates to choose between', async () => { - mockApi.agentsInstalled.mockResolvedValue([]) - mockApi.defaultAgent.mockResolvedValue({ default_agent: '' }) - renderPage() - - expect(await screen.findByText('No agent templates yet')).toBeInTheDocument() - expect(screen.queryByRole('combobox', { name: 'When nothing names a template, use' })).not.toBeInTheDocument() - }) -}) diff --git a/website/src/test/AgentsPage.harnessIdentity.test.tsx b/website/src/test/AgentsPage.harnessIdentity.test.tsx deleted file mode 100644 index 732708b9874..00000000000 --- a/website/src/test/AgentsPage.harnessIdentity.test.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/** - * The Agents page usage card's harness + account rows. - * - * These rows answer one question before work starts: which harness am I on, and - * which account will be billed. Two properties are load-bearing and are pinned - * here because neither is visible from reading the markup alone. - * - * The email is MASKED. This card is a routine screen-share surface, so the full - * address must not reach the DOM even though the API returns it — asserting only - * that the masked form appears would still pass if the raw address were rendered - * alongside it, so the raw address is asserted absent too. - * - * An absent account is a STEADY STATE, not a failure. The backend attaches - * identity only when the signed-in account provably matches the one the credits - * were billed to, and individual / Builder ID accounts carry no profile ARN to - * match on, so they never resolve. The card must say so rather than rendering an - * empty row that reads as a loading or broken state. - */ -import { render, screen, waitFor } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - agentMetadata: vi.fn(), - kirocrewAgents: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 1, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - displayName: 'ACP', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([]), - }), -})) - -import AgentsPage, { maskAccountEmail, accountDisplayValue, addressSafeLabel, authTypeLabel } from '../pages/AgentsPage' - -const mkAgent = (name: string) => ({ - name, - description: `${name} agent`, - source: 'builtin', - model: 'claude-opus-4.8', - skills: [], - mcp_servers: [], - filename: `${name}.json`, -}) - -function renderPage() { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([mkAgent('kirocrew')]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.agentMetadata.mockResolvedValue({ content: '' }) - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - mockApi.skills.mockResolvedValue([]) - mockApi.agentDetail.mockResolvedValue({ ...mkAgent('kirocrew'), unmanaged_skills: [] }) -}) - -describe('maskAccountEmail', () => { - it('keeps the first character and the domain, dropping the rest of the local part', () => { - expect(maskAccountEmail('alice@example.com')).toBe('a•••@example.com') - }) - - it('masks a bare handle that carries no domain to anchor on', () => { - expect(maskAccountEmail('kseam')).toBe('k•••') - }) - - it('masks on the LAST @ so a local part containing one cannot leak', () => { - expect(maskAccountEmail('a@b@example.com')).toBe('a•••@example.com') - }) - - it('returns null for input with nothing to mask, so callers omit the row', () => { - expect(maskAccountEmail(undefined)).toBeNull() - expect(maskAccountEmail('')).toBeNull() - expect(maskAccountEmail(' ')).toBeNull() - // A leading '@' has no local-part character to keep. - expect(maskAccountEmail('@example.com')).toBeNull() - }) -}) - -describe('addressSafeLabel', () => { - it('passes a non-address label through unchanged', () => { - expect(addressSafeLabel('IamIdentityCenter')).toBe('IamIdentityCenter') - expect(addressSafeLabel('BuilderId')).toBe('BuilderId') - }) - - it('masks a label that is address-shaped', () => { - expect(addressSafeLabel('alice@example.com')).toBe('a•••@example.com') - }) - - it('returns null for an empty or whitespace-only label', () => { - expect(addressSafeLabel(undefined)).toBeNull() - expect(addressSafeLabel(' ')).toBeNull() - }) -}) - -describe('authTypeLabel', () => { - // The raw enum reaching the UI is the defect this guards: users would read - // "IamIdentityCenter" here while the credit modal says "IAM Identity Center". - it('maps the known auth-type enum to the same localized prose the credit modal uses', () => { - expect(authTypeLabel('IamIdentityCenter')).toBe('IAM Identity Center') - expect(authTypeLabel('BuilderId')).toBe('Builder ID') - expect(authTypeLabel('Social')).toBe('Social login') - }) - - it('address-guards a value outside the known enum', () => { - expect(authTypeLabel('alice@example.com')).toBe('a•••@example.com') - expect(authTypeLabel('SomethingNew')).toBe('SomethingNew') - expect(authTypeLabel(undefined)).toBeNull() - }) -}) - -describe('accountDisplayValue', () => { - it('prefers the email over the profile name', () => { - expect(accountDisplayValue({ email: 'alice@example.com', account: 'eng-profile' })) - .toBe('a•••@example.com') - }) - - it('renders a profile name that is not address-shaped verbatim', () => { - expect(accountDisplayValue({ account: 'kiro-eng-profile' })).toBe('kiro-eng-profile') - }) - - // The profile name is an arbitrary provider-supplied label, so it must not be - // a way around the masking the email path applies. - it('masks a profile name that IS address-shaped', () => { - expect(accountDisplayValue({ account: 'alice@example.com' })).toBe('a•••@example.com') - }) - - it('reports nothing when an address-shaped profile name cannot be masked', () => { - expect(accountDisplayValue({ account: '@example.com' })).toBeNull() - }) - - it('reports nothing when neither field is present', () => { - expect(accountDisplayValue({})).toBeNull() - expect(accountDisplayValue({ account: ' ' })).toBeNull() - }) -}) - -describe('AgentsPage harness and account rows', () => { - it('renders the harness with its auth type and the MASKED account, never the raw address', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { - plan: 'KIRO PRO', - credits_used: 120, - credits_plan: 1000, - email: 'alice@example.com', - // The REAL payload shape: whoami emits the code identifier, not prose. - account_type: 'IamIdentityCenter', - }, - }) - renderPage() - - await waitFor(() => expect(screen.getByText('a•••@example.com')).toBeInTheDocument()) - expect(screen.getByText('Harness')).toBeInTheDocument() - expect(screen.getByText('Account')).toBeInTheDocument() - // Localized prose, not the camelCase enum. - expect(screen.getByText(/IAM Identity Center/)).toBeInTheDocument() - expect(document.body.textContent).not.toContain('IamIdentityCenter') - // The raw address must not reach the DOM anywhere on the page. - expect(screen.queryByText(/alice@example\.com/)).not.toBeInTheDocument() - expect(document.body.textContent).not.toContain('alice@example.com') - }) - - it('says the account is not reported when the backend could not prove one', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { plan: 'KIRO PRO', credits_used: 120, credits_plan: 1000 }, - }) - renderPage() - - await waitFor(() => expect(screen.getByText('Not reported')).toBeInTheDocument()) - expect(screen.getByText('Account')).toBeInTheDocument() - }) - - it('falls back to the org profile name when no email is exposed', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { plan: 'KIRO PRO', credits_used: 120, credits_plan: 1000, account: 'kiro-eng-profile' }, - }) - renderPage() - - await waitFor(() => expect(screen.getByText('kiro-eng-profile')).toBeInTheDocument()) - expect(screen.queryByText('Not reported')).not.toBeInTheDocument() - }) - - // The profile-name fallback bypassed masking entirely at one point. Asserting - // on the masked form alone would not have caught it, since the raw value can - // be rendered alongside; the raw address is asserted absent from the whole DOM. - it('masks an address-shaped profile name, keeping the raw address out of the DOM', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { - plan: 'KIRO PRO', credits_used: 120, credits_plan: 1000, - account: 'alice@example.com', - }, - }) - renderPage() - - await waitFor(() => expect(screen.getByText('a•••@example.com')).toBeInTheDocument()) - expect(document.body.textContent).not.toContain('alice@example.com') - }) - - // The Harness row renders a provider-supplied label too, so it is held to the - // same invariant: no field on this card puts a full address on screen. - it('masks an address-shaped account_type on the Harness row', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { - plan: 'KIRO PRO', credits_used: 120, credits_plan: 1000, - account_type: 'alice@example.com', - }, - }) - renderPage() - - await waitFor(() => expect(screen.getByText(/a•••@example\.com/)).toBeInTheDocument()) - expect(document.body.textContent).not.toContain('alice@example.com') - }) -}) diff --git a/website/src/test/AgentsPage.meterLabels.test.tsx b/website/src/test/AgentsPage.meterLabels.test.tsx deleted file mode 100644 index c74e7393d00..00000000000 --- a/website/src/test/AgentsPage.meterLabels.test.tsx +++ /dev/null @@ -1,207 +0,0 @@ -/** - * The numbers drawn ON the Agents page meter bars (context window, plan credits). - * - * These labels overlap BOTH the saturated fill and the empty track, and which - * one sits under a given glyph moves with the value — so a single static colour - * is unreadable at one end of the range. `text-text-strong` is near-black in - * every light theme, which is how a 62% context bar shipped as dark-on-purple. - * - * The fix paints the row twice with complementary clips: track side in - * `text-text-strong`, fill side in the fill's own paired foreground token - * (`--accent-fg` / `--warn-fg` / `--danger-fg`, each theme declaring black or - * white to suit its own fill). What is pinned here is the pairing — the fill-side - * colour must follow the SAME threshold ladder as the fill colour, or a bar goes - * back to low contrast at exactly the values that matter most (>=90% is the - * compaction warning) — and the geometry, since the two clips must meet at the - * fill's edge with no gap and no double-painted overlap. - */ -import { render, screen, waitFor } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - agentMetadata: vi.fn(), - kirocrewAgents: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 1, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - displayName: 'ACP', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - getContextWindow: () => 200_000, - fetchAvailableModels: () => Promise.resolve([]), - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -const KIROCREW = { - name: 'kirocrew', - description: 'kirocrew agent', - source: 'builtin', - model: 'claude-opus-4.8', - skills: [], - mcp_servers: [], - filename: 'kirocrew.json', -} - -function renderPage() { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -const session = (pct: number) => ({ - key: 'chat-1', name: 'Chat (chat-1)', model: 'claude-opus-4.8', - context_pct: pct, context_window_tokens: 200_000, prompts: 8, -}) - -/** The two label layers of the bar that carries `text`, outermost first. */ -function layers(text: string): HTMLElement[] { - return screen.getAllByText(text).map(el => el.parentElement as HTMLElement) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([KIROCREW]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.agentMetadata.mockResolvedValue({ content: '' }) - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - mockApi.skills.mockResolvedValue([]) - mockApi.agentDetail.mockResolvedValue({ ...KIROCREW, unmanaged_skills: [] }) -}) - -describe('meter bar labels adapt to the fill underneath them', () => { - it('splits the row at the fill edge: track side strong, fill side on-accent', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(62.4)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('62.4%')).toHaveLength(2)) - const [track, fill] = layers('62.4%') - - expect(track.className).toContain('text-text-strong') - expect(fill.className).toContain('text-accent-fg') - // Complementary clips meeting at 62.4%: no gap, no overlap. - expect(track.style.clipPath).toBe('inset(0 0 0 62.4%)') - expect(fill.style.clipPath).toBe('inset(0 37.6% 0 0)') - }) - - it('uses the warn foreground once the fill turns warn', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(75)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('75.0%')).toHaveLength(2)) - const [, fill] = layers('75.0%') - expect(fill.className).toContain('text-warn-fg') - expect(fill.className).not.toContain('text-accent-fg') - }) - - it('uses the danger foreground once the fill turns danger', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(95)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('95.0%')).toHaveLength(2)) - const [, fill] = layers('95.0%') - expect(fill.className).toContain('text-danger-fg') - }) - - // A bar can be empty (0% context, or a fresh billing period). The fill is - // floored at 1% so it stays visible as a dot; the clips must use the SAME - // floor, or the label would be painted in the on-fill colour over bare track. - it('clips to the fill floor, not the raw value, when the bar is empty', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [{ ...session(0), prompts: 3 }] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('0.0%')).toHaveLength(2)) - const [track, fill] = layers('0.0%') - expect(track.style.clipPath).toBe('inset(0 0 0 1%)') - expect(fill.style.clipPath).toBe('inset(0 99% 0 0)') - }) - - // Overage: the bar is pinned at 100% while the label reads past it. The clip - // has to follow the BAR, so the whole label sits in the on-fill colour. - it('paints the whole label on-fill for an over-plan credits bar', async () => { - mockApi.sessionsUsage.mockResolvedValue({ - usage: { plan: 'KIRO PRO', credits_used: 80_237, credits_plan: 10_000, credits_overage: 70_237 }, - }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('802%')).toHaveLength(2)) - const [track, fill] = layers('802%') - expect(fill.className).toContain('text-danger-fg') - expect(fill.style.clipPath).toBe('inset(0 0% 0 0)') - expect(track.style.clipPath).toBe('inset(0 0 0 100%)') - }) - - // `context_pct` is a plain number off the wire and the page clamps it to 100 - // for the bar; the clip has to land on the same 100, and an `inset()` past - // 100% would be invalid — a dropped clip paints BOTH layers full-width, one - // over the other, which is the failure this component exists to avoid. - it('clamps a pct the backend sent out of range instead of dropping the clip', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(140)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('100.0%')).toHaveLength(2)) - const [track, fill] = layers('100.0%') - expect(fill.style.clipPath).toBe('inset(0 0% 0 0)') - expect(track.style.clipPath).toBe('inset(0 0 0 100%)') - }) - - it('falls back to the fill floor when pct is not a number', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(Number.NaN)] }) - renderPage() - - // fmtPercent renders a non-finite ratio as an em dash. - await waitFor(() => expect(screen.getAllByText('—')).toHaveLength(2)) - const [track, fill] = layers('—') - expect(track.style.clipPath).toBe('inset(0 0 0 1%)') - expect(fill.style.clipPath).toBe('inset(0 99% 0 0)') - }) - - // The fill animates over 700ms (1000ms for credits). If the clip snapped to - // the new value immediately, the label would spend that window painted in the - // on-fill colour over track that has not filled yet — the original bug, just - // transient. - it('moves the cut on the same transition as the fill it follows', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(62.4)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('62.4%')).toHaveLength(2)) - for (const layer of layers('62.4%')) { - expect(layer.className).toContain('transition-[clip-path,color]') - expect(layer.className).toContain('duration-700') - } - }) - - // The duplicate exists only to be looked at. Leaving it in the accessibility - // tree would read every figure on the card twice. - it('hides the duplicated layer from assistive tech', async () => { - mockApi.sessionsContext.mockResolvedValue({ sessions: [session(62.4)] }) - renderPage() - - await waitFor(() => expect(screen.getAllByText('62.4%')).toHaveLength(2)) - const [track, fill] = layers('62.4%') - expect(track).not.toHaveAttribute('aria-hidden') - expect(fill).toHaveAttribute('aria-hidden', 'true') - }) -}) diff --git a/website/src/test/AgentsPage.model.test.tsx b/website/src/test/AgentsPage.model.test.tsx deleted file mode 100644 index eb91765400b..00000000000 --- a/website/src/test/AgentsPage.model.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Agent Templates must survive a foreign-shaped `model` on any single agent. - * - * `~/.kiro/agents` is a SHARED directory: other tools (ACP adapters, IDE - * plugins) write their own specs there, and not all of them spell `model` as a - * plain string. An ACP-style `{ "id": "anthropic:claude-opus-4-8" }` rendered - * as a JSX child throws React error #31 ("Objects are not valid as a React - * child"), which put the WHOLE tab into the error boundary — every other - * agent's row, the context panel and the subagents table included. - * - * The backend now coerces `model` on both `/api/agents/installed` and - * `/api/agents/detail/{name}`, but `agentDetail` is otherwise a pass-through of - * a user-editable spec, so the page keeps its own guard. These tests drive the - * page with the raw bad shape to prove the guard, not the coercion. - */ -import { render, screen, waitFor, fireEvent } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 0, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([{ name: 'claude-opus-4.8', description: '' }]), - getContextWindow: () => 200_000, - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -/** The exact shape observed in the wild (`quickwork_acp_kiro.json`). */ -const ACP_MODEL = { id: 'anthropic:claude-opus-4-8' } - -const mkAgent = (name: string, model: unknown) => ({ - name, - description: `${name} agent`, - source: 'builtin', - model, - skills: [], - mcp_servers: [], - filename: `${name}.json`, -}) - -function renderPage() { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.skills.mockResolvedValue([]) - mockApi.defaultAgent.mockResolvedValue({ default_agent: '' }) - mockApi.setDefaultAgent.mockResolvedValue({ ok: true, default_agent: '' }) -}) - -describe('AgentsPage model rendering', () => { - it('renders the list when one agent has an object model, keeping its siblings', async () => { - mockApi.agentsInstalled.mockResolvedValue([ - mkAgent('kirocrew', 'claude-opus-4.8'), - mkAgent('quickwork_acp_kiro', ACP_MODEL), - ]) - mockApi.agentDetail.mockResolvedValue({ ...mkAgent('kirocrew', 'claude-opus-4.8'), unmanaged_skills: [] }) - - renderPage() - - // The bad row is present and shows the "no pin" label rather than crashing. - expect(await screen.findByText('quickwork_acp_kiro')).toBeInTheDocument() - // The well-formed sibling is the real regression risk: pre-fix it vanished - // with the rest of the tab, because one bad child unmounts the whole tree. - // getAllByText, not getByText — the auto-opened detail panel repeats the - // selected agent's name and model alongside its list row. - expect(screen.getAllByText('kirocrew').length).toBeGreaterThanOrEqual(1) - expect(screen.getAllByText('claude-opus-4.8').length).toBeGreaterThanOrEqual(1) - expect(screen.getAllByText('auto').length).toBeGreaterThanOrEqual(1) - }) - - it('renders the detail model chip when the selected agent has an object model', async () => { - mockApi.agentsInstalled.mockResolvedValue([mkAgent('quickwork_acp_kiro', ACP_MODEL)]) - // agentDetail passes the spec through, so the detail panel sees the raw shape. - mockApi.agentDetail.mockResolvedValue({ - ...mkAgent('quickwork_acp_kiro', ACP_MODEL), - skills: [], - unmanaged_skills: [], - }) - - renderPage() - - fireEvent.click(await screen.findByText('quickwork_acp_kiro')) - // Both the row label and the detail chip degrade to "auto". - await waitFor(() => expect(screen.getAllByText('auto').length).toBeGreaterThanOrEqual(2)) - }) - - it('shows auto for a null model instead of a blank', async () => { - mockApi.agentsInstalled.mockResolvedValue([mkAgent('nullish', null)]) - mockApi.agentDetail.mockResolvedValue({ ...mkAgent('nullish', null), unmanaged_skills: [] }) - - renderPage() - - expect(await screen.findByText('nullish')).toBeInTheDocument() - expect(screen.getAllByText('auto').length).toBeGreaterThanOrEqual(1) - }) - - it('renders the detail panel when the selected agent has an object description', async () => { - // `description` is rendered as a JSX child too, and an object is truthy — so - // a bare `&&` guard let it through and crashed the tab the same way `model` - // did. This is the class, not a second special case. - const bad = { ...mkAgent('weird-desc', 'claude-opus-4.8'), description: { text: 'hi' } } - mockApi.agentsInstalled.mockResolvedValue([bad]) - mockApi.agentDetail.mockResolvedValue({ ...bad, skills: [], unmanaged_skills: [] }) - - renderPage() - - fireEvent.click(await screen.findByText('weird-desc')) - // The row and the detail panel both survive; the unusable description is - // simply not shown rather than taking the tree down. - await waitFor(() => expect(screen.getAllByText('claude-opus-4.8').length).toBeGreaterThanOrEqual(1)) - expect(screen.queryByText('[object Object]')).not.toBeInTheDocument() - }) -}) diff --git a/website/src/test/AgentsPage.templateSource.test.tsx b/website/src/test/AgentsPage.templateSource.test.tsx deleted file mode 100644 index 2094b0d2285..00000000000 --- a/website/src/test/AgentsPage.templateSource.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Agent Templates page — where a template came from, in words. - * - * The page printed the `source` field itself, so every language showed the - * internal identifiers `kirocrew`, `package` and `builtin`. Two surfaces say it: - * a badge beside the template name and the Source row under "Where it comes - * from"; both must resolve through the shared label helper so they cannot drift - * apart. The fixtures below use the values discovery actually assigns. - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - agentMetadata: vi.fn(), - kirocrewAgents: vi.fn(), - skills: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 0, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - displayName: 'Kiro', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([]), - getContextWindow: () => 200_000, - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -/** `kirocrew.json` is one of the constant filenames Kiro Crew maintains itself. */ -const OWNED = { - name: 'kirocrew', description: 'Full crew agent', source: 'kirocrew', model: '', - skills: [], mcp_servers: [], filename: 'kirocrew.json', kirocrew_owned: true, -} -/** A package install: `{package}-{name}.json`. */ -const FROM_PACKAGE = { - name: 'oncall-triage', description: 'Triages pages', source: 'package', model: '', - skills: [], mcp_servers: [], package: 'oncall-radar', - filename: 'local-oncall-radar-oncall-triage.json', kirocrew_owned: false, -} -/** - * Discovery labels every plain `{name}.json` `builtin` — the fallback for a file - * it cannot attribute, which is what a hand-written spec looks like. So this is - * the case whose rendered word must NOT be "Built-in". - */ -const PLAIN_FILE = { - name: 'reviewer', description: 'Reviews code', source: 'builtin', model: '', - skills: [], mcp_servers: [], filename: 'reviewer.json', kirocrew_owned: false, -} - -function renderPage() { - const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -async function open(name: string) { - const roster = await screen.findByRole('listbox', { name: 'Installed Agents' }) - const rows = await within(roster).findAllByRole('option') - const row = rows.find(r => r.textContent?.startsWith(name)) - if (!row) throw new Error(`no roster row for ${name}`) - fireEvent.click(row) - await waitFor(() => expect(screen.getByRole('tablist')).toBeInTheDocument()) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([OWNED, FROM_PACKAGE, PLAIN_FILE]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'kirocrew' }) - mockApi.agentMetadata.mockResolvedValue({ content: '' }) - mockApi.skills.mockResolvedValue([]) - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - mockApi.agentDetail.mockImplementation((name: string) => { - const row = [OWNED, FROM_PACKAGE, PLAIN_FILE].find(a => a.name === name) ?? PLAIN_FILE - return Promise.resolve({ ...row, tools: [], unmanaged_skills: [] }) - }) -}) - -describe('agent templates page — source is shown as a word, not a field value', () => { - it('calls a template Kiro Crew maintains itself Built-in, in both places', async () => { - renderPage() - await open('kirocrew') - - expect(await screen.findByText('Where it comes from')).toBeInTheDocument() - // The header badge and the Source row: two independent render paths. - expect(screen.getAllByText('Built-in')).toHaveLength(2) - }) - - it('names the actual package on the badge, keeping the category word in the detail row', async () => { - renderPage() - await open('oncall-triage') - - expect(await screen.findByText('Where it comes from')).toBeInTheDocument() - // The badge and the dedicated Package row both carry the package name. - expect(screen.getAllByText('oncall-radar').length).toBeGreaterThanOrEqual(2) - // The Source detail row keeps the category word, so it does not repeat the - // Package row right below it. - expect(screen.getAllByText('Package').length).toBeGreaterThanOrEqual(1) - // The raw lowercase field value never renders. - expect(screen.queryByText('package')).toBeNull() - }) - - it('calls an unattributable plain spec Custom, never Built-in', async () => { - renderPage() - await open('reviewer') - - expect(await screen.findByText('Where it comes from')).toBeInTheDocument() - expect(screen.getAllByText('Custom')).toHaveLength(2) - // `builtin` is discovery's fallback, so claiming it ships with the product - // would be a lie about a file the user may well have written. - expect(screen.queryByText('Built-in')).toBeNull() - expect(screen.queryByText('builtin')).toBeNull() - }) -}) diff --git a/website/src/test/AgentsPageInspector.test.tsx b/website/src/test/AgentsPageInspector.test.tsx deleted file mode 100644 index a1e872854a1..00000000000 --- a/website/src/test/AgentsPageInspector.test.tsx +++ /dev/null @@ -1,475 +0,0 @@ -/** - * Agent Templates inspector — roster, tabs, provenance, delete. - * - * The tab exposed one template's whole spec through a 420px-tall box: prompt, - * tool chips, MCP chips and 96 denied-command patterns all scrolled inside it, - * and delete went through a native `confirm()`. These tests pin the behaviour of - * the replacement rather than its markup — every query is by accessible name or - * an explicit test id so a restyle cannot turn the suite red. - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - agentMetadata: vi.fn(), - agentMetadataSave: vi.fn(), - kirocrewAgents: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - agentDelete: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 0, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - displayName: 'Kiro', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([{ name: 'claude-opus-4.8', description: '' }]), - getContextWindow: () => 200_000, - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -const BUILTIN = { - name: 'kirocrew', - description: 'Full crew agent', - source: 'kirocrew', - model: 'claude-opus-4.8', - skills: ['prepare-pr'], - mcp_servers: ['kirocrew-core'], - filename: 'kirocrew.json', -} -const USER_TEMPLATE = { - name: 'reviewer', - description: 'Reviews code', - source: 'user', - model: '', - skills: [], - mcp_servers: [], - filename: 'reviewer.json', -} -const SCRATCH_TEMPLATE = { - name: 'scratch', - description: 'Nothing points at this one', - source: 'user', - model: '', - skills: [], - mcp_servers: [], - filename: 'scratch.json', -} -const PACKAGE_TEMPLATE = { - name: 'oncall-triage', - description: 'Triages pages', - source: 'package', - model: 'claude-sonnet-4.6', - skills: [], - mcp_servers: [], - package: 'oncall-radar', - filename: 'local-oncall-radar.json', -} - -const BUILTIN_DETAIL = { - ...BUILTIN, - prompt: 'file://~/.kiro/crew/prompts/kirocrew.md', - tools: ['fs_read', 'fs_write'], - allowedTools: ['fs_read'], - mcpServers: { 'kirocrew-core': {} }, - toolsSettings: { execute_bash: { deniedCommands: ['rm -rf /', 'DROP TABLE'] } }, - skills: ['kiro-user/prepare-pr'], - unmanaged_skills: [], -} - -function renderPage(qc?: QueryClient) { - const client = qc ?? new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -/** Select a template in the roster and wait for its detail to land. - * Queried inside the roster listbox: the selected template's name also appears - * in the detail header, so a bare text query is ambiguous by design. */ -async function open(name: string) { - const roster = await screen.findByRole('listbox', { name: 'Installed Agents' }) - const rows = await within(roster).findAllByRole('option') - const row = rows.find(r => r.textContent?.startsWith(name)) - if (!row) throw new Error(`no roster row for ${name}`) - fireEvent.click(row) - await waitFor(() => expect(screen.getByRole('tablist')).toBeInTheDocument()) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([BUILTIN, USER_TEMPLATE, PACKAGE_TEMPLATE, SCRATCH_TEMPLATE]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'kirocrew' }) - mockApi.agentMetadata.mockResolvedValue({ content: '' }) - mockApi.skills.mockResolvedValue([]) - mockApi.agentDelete.mockResolvedValue({ ok: true }) - mockApi.kirocrewAgents.mockResolvedValue({ - agents: [ - { name: 'default', kiro_agent: 'kirocrew', workspace: 'default', memory_store: 'default', description: '', source: 'user' }, - { name: 'oncall', kiro_agent: 'kirocrew', workspace: 'oncall', memory_store: 'oncall', description: '', source: 'user' }, - { name: 'research', kiro_agent: 'reviewer', workspace: 'research', memory_store: 'research', description: '', source: 'user' }, - ], - default_agent: 'default', - }) - mockApi.agentDetail.mockImplementation((name: string) => { - if (name === 'kirocrew') return Promise.resolve(BUILTIN_DETAIL) - if (name === 'reviewer') return Promise.resolve({ ...USER_TEMPLATE, tools: ['fs_read'], unmanaged_skills: [] }) - if (name === 'scratch') return Promise.resolve({ ...SCRATCH_TEMPLATE, tools: ['fs_read'], unmanaged_skills: [] }) - return Promise.resolve({ ...PACKAGE_TEMPLATE, unmanaged_skills: [] }) - }) -}) - -describe('agent templates inspector — tabs', () => { - it('gives the prompt and the guardrails a pane each instead of one scroll box', async () => { - renderPage() - await open('kirocrew') - - // Overview is what a newly-selected template opens on. - expect(await screen.findByText('Where it comes from')).toBeInTheDocument() - expect(screen.queryByText('rm -rf /')).not.toBeInTheDocument() - - fireEvent.click(screen.getByRole('tab', { name: /system prompt/i })) - expect(await screen.findByText(/prompts\/kirocrew\.md/)).toBeInTheDocument() - - fireEvent.click(screen.getByRole('tab', { name: /guardrails/i })) - expect(await screen.findByText('rm -rf /')).toBeInTheDocument() - expect(screen.getByText('DROP TABLE')).toBeInTheDocument() - expect(screen.getByText(/read-only here/i)).toBeInTheDocument() - }) - - it('sends an At a glance count to the tab that owns the detail', async () => { - renderPage() - await open('kirocrew') - - fireEvent.click(await screen.findByRole('button', { name: /2 tools/i })) - - expect(await screen.findByText('fs_write')).toBeInTheDocument() - }) - - it('reopens on Overview when the selection moves, not on the previous tab', async () => { - renderPage() - await open('kirocrew') - - fireEvent.click(screen.getByRole('tab', { name: /guardrails/i })) - expect(await screen.findByText('rm -rf /')).toBeInTheDocument() - - await open('reviewer') - await waitFor(() => expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true')) - }) -}) - -describe('agent templates inspector — overview', () => { - it('says which model the pin resolves to, in words', async () => { - renderPage() - await open('kirocrew') - - expect(await screen.findByText(/an agent set to inherit uses this model/i)).toBeInTheDocument() - - await open('reviewer') - expect(await screen.findByText(/falls back to the global default/i)).toBeInTheDocument() - }) - - it('names the crews an edit would change', async () => { - renderPage() - await open('kirocrew') - - expect(await screen.findByText('Used By')).toBeInTheDocument() - expect(screen.getByText('default')).toBeInTheDocument() - expect(screen.getByText('oncall')).toBeInTheDocument() - // Bound to `reviewer`, so it must not be listed under this template. - expect(screen.queryByText('research')).not.toBeInTheDocument() - }) - - it('says so plainly when no crew is bound to the template', async () => { - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - renderPage() - await open('kirocrew') - - expect(await screen.findByText('No agents use this template yet')).toBeInTheDocument() - }) -}) - -describe('agent templates inspector — roster filter', () => { - it('narrows the roster and says when nothing matches', async () => { - renderPage() - const roster = await screen.findByRole('listbox', { name: 'Installed Agents' }) - await waitFor(() => expect(within(roster).getAllByRole('option')).toHaveLength(4)) - - const filter = screen.getByRole('textbox', { name: 'Filter templates' }) - fireEvent.change(filter, { target: { value: 'oncall' } }) - - await waitFor(() => expect(within(roster).getAllByRole('option')).toHaveLength(1)) - expect(within(roster).getByRole('option')).toHaveTextContent('oncall-triage') - - fireEvent.change(filter, { target: { value: 'zzz' } }) - expect(await screen.findByText('No templates match your filter')).toBeInTheDocument() - }) -}) - -describe('agent templates inspector — shared crews cache', () => { - /* The Crews tab owns ['kirocrew-agents', ] and stores the whole - response object. One QueryClient keeps ONE value per key, so if this page - stored the unwrapped array instead, whichever tab mounted first would hand - the other the wrong shape: `crews.filter` on an object throws during - render here, and `agentsData?.agents` on an array silently empties the - Crews roster. Both directions are asserted. */ - it('reads and leaves the cache in the shape the Crews tab stores', async () => { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - qc.setQueryData(['kirocrew-agents'], { - agents: [{ name: 'default', kiro_agent: 'kirocrew', workspace: 'default', memory_store: 'default', description: '', source: 'user' }], - default_agent: 'default', - }) - - renderPage(qc) - await open('kirocrew') - - expect(await screen.findByText('Used By')).toBeInTheDocument() - expect(screen.getByText('default')).toBeInTheDocument() - const cached = qc.getQueryData(['kirocrew-agents']) - expect(Array.isArray(cached)).toBe(false) - expect(cached).toMatchObject({ default_agent: 'default' }) - }) -}) - -describe('agent templates inspector — stale detail responses', () => { - it('ignores a detail response that lands after the selection moved', async () => { - let landReviewer = () => {} - mockApi.agentDetail.mockImplementation((name: string) => { - if (name === 'reviewer') { - return new Promise(resolve => { - landReviewer = () => resolve({ ...USER_TEMPLATE, tools: ['fs_read'], unmanaged_skills: [] }) - }) - } - if (name === 'kirocrew') return Promise.resolve(BUILTIN_DETAIL) - return Promise.resolve({ ...PACKAGE_TEMPLATE, unmanaged_skills: [] }) - }) - - renderPage() - await open('kirocrew') - - // reviewer's fetch hangs; the user gives up and picks the package template. - const roster = await screen.findByRole('listbox', { name: 'Installed Agents' }) - const rows = within(roster).getAllByRole('option') - fireEvent.click(rows.find(r => r.textContent?.startsWith('reviewer'))!) - fireEvent.click(rows.find(r => r.textContent?.startsWith('oncall-triage'))!) - await waitFor(() => expect(screen.getAllByText('oncall-radar').length).toBeGreaterThan(0)) - - landReviewer() - - // The pane still belongs to the last CLICK, not the last RESPONSE — and the - // template the stale response named offers no delete, so a retargeted - // confirm cannot appear either. - await waitFor(() => expect(screen.getAllByText('oncall-radar').length).toBeGreaterThan(0)) - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - expect(screen.queryByTestId('confirm-delete-template')).not.toBeInTheDocument() - }) - - it('disarms a confirmed delete synchronously when another template is picked', async () => { - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - expect(screen.getByTestId('confirm-delete-template')).toBeInTheDocument() - - // Not awaited: the disarm must happen on the click, not after the next - // template's detail resolves and the passive effect runs. - const roster = screen.getByRole('listbox', { name: 'Installed Agents' }) - const row = within(roster).getAllByRole('option').find(r => r.textContent?.startsWith('oncall-triage'))! - fireEvent.click(row) - expect(screen.queryByTestId('confirm-delete-template')).not.toBeInTheDocument() - expect(mockApi.agentDelete).not.toHaveBeenCalled() - }) -}) - -describe('agent templates inspector — delete', () => { - it('parks focus on Cancel, never on the destructive confirm', async () => { - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - - /* Enter fires a native button's click on keydown AND auto-repeats, so focus - on "Yes, delete it" would let one held Enter arm and delete in a single - gesture — the two-step would exist in markup only. */ - expect(screen.getByTestId('cancel-delete-template')).toHaveFocus() - expect(screen.getByTestId('confirm-delete-template')).not.toHaveFocus() - }) - - it('asks twice before removing a config file', async () => { - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - // Arming is not deleting. - expect(mockApi.agentDelete).not.toHaveBeenCalled() - expect(screen.getByText(/Delete the template scratch\?/)).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('cancel-delete-template')) - expect(mockApi.agentDelete).not.toHaveBeenCalled() - - fireEvent.click(await screen.findByTestId('delete-template')) - fireEvent.click(screen.getByTestId('confirm-delete-template')) - await waitFor(() => expect(mockApi.agentDelete).toHaveBeenCalledWith('scratch')) - }) - - it('disarms a half-confirmed delete when the selection moves', async () => { - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - expect(screen.getByTestId('confirm-delete-template')).toBeInTheDocument() - - await open('oncall-triage') - await waitFor(() => expect(screen.queryByTestId('confirm-delete-template')).not.toBeInTheDocument()) - }) - - it('offers no delete for a built-in or a package template', async () => { - renderPage() - await open('kirocrew') - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - - await open('oncall-triage') - await waitFor(() => expect(screen.getAllByText('oncall-radar').length).toBeGreaterThan(0)) - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - }) - - /* Deleting a template something still points at leaves a dangling reference: - a crew bound to it boots from nothing, and the fallback name is handed to - kiro-cli as an agent id. Both guards say what to do first, because a button - that silently disappears teaches nothing. */ - it('refuses to delete a template a crew is still bound to, and says which', async () => { - mockApi.kirocrewAgents.mockResolvedValue({ - agents: [{ name: 'research', kiro_agent: 'reviewer', workspace: 'r', memory_store: 'r', description: '', source: 'user' }], - default_agent: 'default', - }) - renderPage() - await open('reviewer') - - expect(await screen.findByText(/research .*repoint them on the Agents tab/i)).toBeInTheDocument() - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - }) - - it('refuses to delete the fallback template', async () => { - mockApi.defaultAgent.mockResolvedValue({ default_agent: 'scratch' }) - mockApi.kirocrewAgents.mockResolvedValue({ agents: [], default_agent: '' }) - renderPage() - await open('scratch') - - expect(await screen.findByText(/fallback template cannot be deleted/i)).toBeInTheDocument() - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - }) - - it('withholds delete until the crew roster has actually loaded', async () => { - // An unresolved query makes `usedBy` empty, which must not read as - // "nothing uses it" — that is exactly how a bound template gets deleted. - let landCrews: (v: unknown) => void = () => {} - mockApi.kirocrewAgents.mockImplementation(() => new Promise(resolve => { landCrews = resolve })) - renderPage() - await open('reviewer') - - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - - landCrews({ agents: [], default_agent: '' }) - expect(await screen.findByTestId('delete-template')).toBeInTheDocument() - }) - - it('withholds delete until the fallback setting has actually loaded', async () => { - // Same hole on the other query: an unresolved default reads as '' , which - // would never equal the selected name, so the fallback template would be - // offered for deletion on first paint. - let landDefault: (v: unknown) => void = () => {} - mockApi.defaultAgent.mockImplementation(() => new Promise(resolve => { landDefault = resolve })) - renderPage() - await open('scratch') - - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - - landDefault({ default_agent: 'scratch' }) - expect(await screen.findByText(/fallback template cannot be deleted/i)).toBeInTheDocument() - expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument() - }) - - it('surfaces a server refusal and re-syncs instead of failing silently', async () => { - // The handler checks references under the config lock, so it can refuse a - // delete this page's cached snapshot thought was fine. That refusal has to - // be visible, and the stale snapshot has to be re-read. - // api/client raises ApiError, whose `message` is already the unwrapped - // server text — a plain Error models exactly what the page reads. - mockApi.agentDelete.mockRejectedValue( - new Error("Cannot delete 'scratch': still used by researcher."), - ) - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - fireEvent.click(screen.getByTestId('confirm-delete-template')) - - expect(await screen.findByText(/still used by researcher/)).toBeInTheDocument() - // Re-synced, so the next render derives blockedBy from fresh data. - await waitFor(() => expect(mockApi.kirocrewAgents.mock.calls.length).toBeGreaterThan(1)) - // And the armed confirm is dropped — no second identical attempt on one click. - expect(screen.queryByTestId('confirm-delete-template')).not.toBeInTheDocument() - }) - - it('withholds delete when a reference REFETCH fails, leaving stale data behind', async () => { - // The sharp case: a failed refetch keeps the last good data, so the mutation - // settles and `crewsData` stays defined — an undefined-check alone would - // keep comparing against a roster the server may no longer agree with. - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - qc.setQueryData(['kirocrew-agents'], { agents: [], default_agent: '' }) - - renderPage(qc) - await open('scratch') - expect(await screen.findByTestId('delete-template')).toBeInTheDocument() - - mockApi.kirocrewAgents.mockRejectedValue(new Error('network')) - await qc.refetchQueries({ queryKey: ['kirocrew-agents'] }) - - await waitFor(() => expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument()) - }) - - it('withholds delete while a fallback change is still in flight, and disarms', async () => { - // The PUT can land before the refetch, so `defaultAgent` still names the - // OLD template while config already names this one. - let landWrite: (v: unknown) => void = () => {} - mockApi.setDefaultAgent.mockImplementation(() => new Promise(resolve => { landWrite = resolve })) - renderPage() - await open('scratch') - - fireEvent.click(await screen.findByTestId('delete-template')) - expect(screen.getByTestId('confirm-delete-template')).toBeInTheDocument() - - fireEvent.click(screen.getByRole('combobox', { name: 'When nothing names a template, use' })) - fireEvent.click(await screen.findByRole('option', { name: 'scratch' })) - - // Armed state is dropped at the source, and delete stays withheld until the - // write settles and the refetched setting can be compared. - expect(screen.queryByTestId('confirm-delete-template')).not.toBeInTheDocument() - await waitFor(() => expect(screen.queryByTestId('delete-template')).not.toBeInTheDocument()) - expect(mockApi.agentDelete).not.toHaveBeenCalled() - - landWrite({ ok: true, default_agent: 'scratch' }) - }) -}) diff --git a/website/src/test/AgentsPageSkills.test.tsx b/website/src/test/AgentsPageSkills.test.tsx deleted file mode 100644 index 78cbfa94745..00000000000 --- a/website/src/test/AgentsPageSkills.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, waitFor, fireEvent } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -/* ── Mock api/client BEFORE the page imports ── */ -const mockApi = vi.hoisted(() => ({ - spawnList: vi.fn(), - sessionsContext: vi.fn(), - sessionsUsage: vi.fn(), - agentsInstalled: vi.fn(), - mcpProbeCache: vi.fn(), - defaultAgent: vi.fn(), - agentDetail: vi.fn(), - skills: vi.fn(), - agentPatch: vi.fn(), - spawnClear: vi.fn(), - spawnDelete: vi.fn(), - setDefaultAgent: vi.fn(), -})) -vi.mock('../api/client', () => ({ api: mockApi })) -vi.mock('../store', () => ({ useAppSelector: (fn: (s: unknown) => unknown) => fn({ dashboard: { status: { sessions: 1, subagents: 0 }, refreshTrigger: 0 } }) })) -vi.mock('../providers', () => ({ - useProvider: () => ({ - id: 'acp', - capabilities: { agentTemplates: true }, - labels: { sessionProcess: 'ACP subprocess', configFile: 'agent.json' }, - fetchAvailableModels: () => Promise.resolve([{ name: 'claude-opus-4.8', description: '' }]), - }), -})) - -import AgentsPage from '../pages/AgentsPage' - -const AGENT = { - name: 'specialist', - description: 'Reviews code', - source: 'builtin', - model: 'claude-opus-4.8', - // The LIST row carries display NAMES, not catalog keys — feeding these to the - // editor would offer chips that cannot be saved. - skills: ['prepare-pr', 'rubber-duck'], - mcp_servers: [], - filename: 'specialist.json', -} - -function renderPage() { - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - return render( - - - , - ) -} - -beforeEach(() => { - Object.values(mockApi).forEach(fn => fn.mockReset()) - mockApi.spawnList.mockResolvedValue({ agents: [] }) - mockApi.sessionsContext.mockResolvedValue({ sessions: [] }) - mockApi.sessionsUsage.mockResolvedValue({ usage: null }) - mockApi.agentsInstalled.mockResolvedValue([AGENT]) - mockApi.mcpProbeCache.mockResolvedValue([]) - mockApi.defaultAgent.mockResolvedValue({ default_agent: '' }) - mockApi.skills.mockResolvedValue([{ key: 'babysit', name: 'babysit', description: '' }]) - mockApi.agentPatch.mockResolvedValue({ ok: true }) -}) - -describe('AgentsPage skills section', () => { - it('offers the editor when agent detail loaded', async () => { - mockApi.agentDetail.mockResolvedValue({ ...AGENT, skills: ['kiro-user/prepare-pr'], unmanaged_skills: [] }) - renderPage() - // The editor lives behind the Skills tab of the inspector now, so the - // prompt, tool chips and guardrails no longer share one scroll box with it. - fireEvent.click(await screen.findByRole('tab', { name: /skills/i })) - expect(await screen.findByRole('button', { name: /add skill/i })).toBeInTheDocument() - }) - - it('hides the editor — never an empty enabled one — when agent detail fails', async () => { - // The real mapping is UNKNOWN here. An empty-but-enabled editor is - // destructive: add/remove PATCH the complete desired key list and the - // backend fully replaces the managed skill:// set, so a single "Add skill" - // click over unknown state would delete every real mapping on disk. - mockApi.agentDetail.mockRejectedValue(new Error('500 boom')) - renderPage() - - // The fallback only triggers on an explicit selection whose detail fetch fails. - fireEvent.click(await screen.findByText('specialist')) - fireEvent.click(await screen.findByRole('tab', { name: /skills/i })) - - await waitFor(() => expect(screen.getByText(/Could not load this agent/i)).toBeInTheDocument()) - expect(screen.queryByRole('button', { name: /add skill/i })).not.toBeInTheDocument() - expect(mockApi.agentPatch).not.toHaveBeenCalled() - }) -}) diff --git a/website/src/test/App.changelogModalApply.test.tsx b/website/src/test/App.changelogModalApply.test.tsx index 6b16b0f8342..f4f2ecfecf2 100644 --- a/website/src/test/App.changelogModalApply.test.tsx +++ b/website/src/test/App.changelogModalApply.test.tsx @@ -21,7 +21,6 @@ import App from '../App' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.creditPillError.test.tsx b/website/src/test/App.creditPillError.test.tsx index eba8af7081e..80516182c27 100644 --- a/website/src/test/App.creditPillError.test.tsx +++ b/website/src/test/App.creditPillError.test.tsx @@ -18,7 +18,6 @@ import App from '../App' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.featureRequestFailure.test.tsx b/website/src/test/App.featureRequestFailure.test.tsx index 374f3569410..effa51f9995 100644 --- a/website/src/test/App.featureRequestFailure.test.tsx +++ b/website/src/test/App.featureRequestFailure.test.tsx @@ -23,7 +23,6 @@ import App from '../App' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.importChapterReopen.test.tsx b/website/src/test/App.importChapterReopen.test.tsx index e8970a94ceb..5c3f2fc7dda 100644 --- a/website/src/test/App.importChapterReopen.test.tsx +++ b/website/src/test/App.importChapterReopen.test.tsx @@ -49,7 +49,6 @@ vi.mock('../hooks/useTheme', () => ({ vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.linuxElectron.test.tsx b/website/src/test/App.linuxElectron.test.tsx index ab8549833a1..ffe57902dab 100644 --- a/website/src/test/App.linuxElectron.test.tsx +++ b/website/src/test/App.linuxElectron.test.tsx @@ -25,7 +25,6 @@ vi.hoisted(() => { vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.metricsPopover.test.tsx b/website/src/test/App.metricsPopover.test.tsx index 18159f73237..306f37e8fed 100644 --- a/website/src/test/App.metricsPopover.test.tsx +++ b/website/src/test/App.metricsPopover.test.tsx @@ -19,7 +19,6 @@ import { renderWithProviders } from './helpers' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.mobileCrewSwitcher.test.tsx b/website/src/test/App.mobileCrewSwitcher.test.tsx index dd77d3e378d..0e7ea303aea 100644 --- a/website/src/test/App.mobileCrewSwitcher.test.tsx +++ b/website/src/test/App.mobileCrewSwitcher.test.tsx @@ -18,7 +18,6 @@ import App from '../App' vi.mock('../hooks/useIsMobile', () => ({ useIsMobile: () => true })) vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.navLeaveGuard.test.tsx b/website/src/test/App.navLeaveGuard.test.tsx index 080707274f2..336bbd97ba6 100644 --- a/website/src/test/App.navLeaveGuard.test.tsx +++ b/website/src/test/App.navLeaveGuard.test.tsx @@ -27,7 +27,6 @@ import { NavigationLeaveGuardProvider } from '../components/NavigationLeaveGuard // ROW, not about any page's content. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.notificationNavLeaveGuard.test.tsx b/website/src/test/App.notificationNavLeaveGuard.test.tsx index 03f3551cefe..47348f83152 100644 --- a/website/src/test/App.notificationNavLeaveGuard.test.tsx +++ b/website/src/test/App.notificationNavLeaveGuard.test.tsx @@ -27,7 +27,6 @@ import type { Notification } from '../types' // client so App mounts without real network. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.notificationSheetExit.test.tsx b/website/src/test/App.notificationSheetExit.test.tsx index 1391ea692f2..c6f61d99891 100644 --- a/website/src/test/App.notificationSheetExit.test.tsx +++ b/website/src/test/App.notificationSheetExit.test.tsx @@ -14,7 +14,6 @@ import { renderWithProviders } from './helpers' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.offlinePill.test.tsx b/website/src/test/App.offlinePill.test.tsx index ed04d708a89..287c679a8bc 100644 --- a/website/src/test/App.offlinePill.test.tsx +++ b/website/src/test/App.offlinePill.test.tsx @@ -18,7 +18,6 @@ import App from '../App' // where each test controls it explicitly. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.rootSelectorScope.test.tsx b/website/src/test/App.rootSelectorScope.test.tsx index e1dbd2516c1..bf78044965a 100644 --- a/website/src/test/App.rootSelectorScope.test.tsx +++ b/website/src/test/App.rootSelectorScope.test.tsx @@ -46,7 +46,6 @@ vi.mock('../utils/terminalPopout', async importOriginal => { // The root is heavy and the routes under it are irrelevant to selector scope. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.startupVideoSequencing.test.tsx b/website/src/test/App.startupVideoSequencing.test.tsx index 9e69ba9e6f3..7315d661b1b 100644 --- a/website/src/test/App.startupVideoSequencing.test.tsx +++ b/website/src/test/App.startupVideoSequencing.test.tsx @@ -22,7 +22,6 @@ import { resetStartupVideoLaunchGuardForTests } from '../components/startupVideo vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.terminalEnabledFlag.test.tsx b/website/src/test/App.terminalEnabledFlag.test.tsx index 6d73cae7558..a927ce2803f 100644 --- a/website/src/test/App.terminalEnabledFlag.test.tsx +++ b/website/src/test/App.terminalEnabledFlag.test.tsx @@ -34,7 +34,6 @@ import { isTerminalEnabled, setTerminalEnabledFlag } from '../utils/terminalRegi // /api/terminal/sessions endpoint this test cares about. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.terminalNavActive.test.tsx b/website/src/test/App.terminalNavActive.test.tsx index acf73a71f7a..4ef46a6dd62 100644 --- a/website/src/test/App.terminalNavActive.test.tsx +++ b/website/src/test/App.terminalNavActive.test.tsx @@ -29,7 +29,6 @@ import { __resetBottomTerminal, openBottomTerminal } from '../hooks/useBottomTer // canvas/WebGL for. The test only cares about the NAV ROW, not the shell. vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.test.tsx b/website/src/test/App.test.tsx index 564b232b85f..1a01e794ad9 100644 --- a/website/src/test/App.test.tsx +++ b/website/src/test/App.test.tsx @@ -51,7 +51,6 @@ function topbarTracks(): { sides: string[]; search: string } { // Mock all page components to isolate routing vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () =>
SystemPage
})) -vi.mock('../pages/AgentsPage', () => ({ default: () =>
AgentsPage
})) vi.mock('../pages/ProjectsPage', () => ({ default: () =>
ProjectsPage
})) vi.mock('../pages/LogsPage', () => ({ default: () =>
LogsPage
})) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () =>
MCAgentsPage
})) diff --git a/website/src/test/App.topbarCapsule.test.tsx b/website/src/test/App.topbarCapsule.test.tsx index cdea99058c7..610807fce3f 100644 --- a/website/src/test/App.topbarCapsule.test.tsx +++ b/website/src/test/App.topbarCapsule.test.tsx @@ -15,7 +15,6 @@ import { setDesktopUpdateAvailable } from '../store/dashboardSlice' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/App.topbarMetricsPending.test.tsx b/website/src/test/App.topbarMetricsPending.test.tsx index 123c4fddc29..a4392dbfc2c 100644 --- a/website/src/test/App.topbarMetricsPending.test.tsx +++ b/website/src/test/App.topbarMetricsPending.test.tsx @@ -37,7 +37,6 @@ import { safeSetItem } from '../utils/safeStorage' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null })) diff --git a/website/src/test/AppRootCoverage.test.tsx b/website/src/test/AppRootCoverage.test.tsx index 9ad71691a3c..0f2ffe8c85a 100644 --- a/website/src/test/AppRootCoverage.test.tsx +++ b/website/src/test/AppRootCoverage.test.tsx @@ -18,7 +18,6 @@ import { setUpdateProgress, sseConnected, sseDisconnected } from '../store/dashb vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () =>
LogsPage
})) vi.mock('../pages/DeveloperPage', () => ({ default: () =>
DeveloperPage
})) diff --git a/website/src/test/CapabilitiesPage.knowledgeTab.test.tsx b/website/src/test/CapabilitiesPage.knowledgeTab.test.tsx index 99ba119eb84..667cba88c17 100644 --- a/website/src/test/CapabilitiesPage.knowledgeTab.test.tsx +++ b/website/src/test/CapabilitiesPage.knowledgeTab.test.tsx @@ -17,7 +17,6 @@ import React from 'react' // Tab bodies are irrelevant; the rail + pane wiring is what is under test. vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () =>
})) -vi.mock('../pages/AgentsPage', () => ({ default: () =>
})) vi.mock('../pages/HooksPage', () => ({ default: () =>
})) vi.mock('../pages/connections/ConnectionsPage', () => ({ default: () =>
})) vi.mock('../pages/overview', () => ({ diff --git a/website/src/test/CapabilitiesPage.tabLanguage.test.tsx b/website/src/test/CapabilitiesPage.tabLanguage.test.tsx index 2545dba7a45..30c104b5766 100644 --- a/website/src/test/CapabilitiesPage.tabLanguage.test.tsx +++ b/website/src/test/CapabilitiesPage.tabLanguage.test.tsx @@ -19,7 +19,6 @@ import React from 'react' // The tab bodies are irrelevant here and each pulls its own fetches; the rail is // what is under test. vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () =>
})) -vi.mock('../pages/AgentsPage', () => ({ default: () =>
})) vi.mock('../pages/HooksPage', () => ({ default: () =>
})) vi.mock('../pages/connections/ConnectionsPage', () => ({ default: () =>
})) vi.mock('../pages/overview', () => ({ diff --git a/website/src/test/CrewRoster.test.tsx b/website/src/test/CrewRoster.test.tsx index f1ca2c22ed7..b13daf6777e 100644 --- a/website/src/test/CrewRoster.test.tsx +++ b/website/src/test/CrewRoster.test.tsx @@ -66,6 +66,7 @@ vi.mock('framer-motion', async () => { const mockApi = vi.hoisted(() => ({ kirocrewAgents: vi.fn(), agentsInstalled: vi.fn(), + agentDetail: vi.fn(), workspaces: vi.fn(), kirocrewConfig: vi.fn(), createWorkspace: vi.fn(), @@ -151,6 +152,9 @@ beforeEach(() => { vi.clearAllMocks() mockApi.kirocrewAgents.mockResolvedValue(AGENTS_RESPONSE) mockApi.agentsInstalled.mockResolvedValue(INSTALLED_RESPONSE) + mockApi.agentDetail.mockImplementation((name: string) => + Promise.resolve({ name, skills: [] }), + ) mockApi.workspaces.mockResolvedValue(WORKSPACES_RESPONSE) mockApi.kirocrewConfig.mockResolvedValue(CONFIG_RESPONSE) mockApi.agentResolvedModel.mockResolvedValue({ model: '', pinned: false, kiro_agent: 'kirocrew' }) @@ -550,7 +554,9 @@ describe('crew editor — opening', () => { expect(within(sheet).queryByRole('combobox', { name: 'Memory Store' })).not.toBeInTheDocument() gotoPane(sheet, 'template') - expect(within(sheet).getByRole('combobox', { name: 'Agent Template' })).toHaveTextContent('oncall-agent') + // The pane's header selector carries the same accessible name, so use + // findByRole to await the pane render before asserting. + expect(await within(sheet).findByRole('combobox', { name: 'Agent Template' })).toHaveTextContent('oncall-agent') gotoPane(sheet, 'model') expect(within(sheet).getByRole('combobox', { name: 'Edit default model' })).toHaveTextContent('claude-opus-5') diff --git a/website/src/test/filterInputFont.test.tsx b/website/src/test/filterInputFont.test.tsx index 2d0e1b62406..8ccef4a9d43 100644 --- a/website/src/test/filterInputFont.test.tsx +++ b/website/src/test/filterInputFont.test.tsx @@ -100,7 +100,6 @@ describe('dropdown filter inputs follow the Font Family setting', () => { 'components/ModelEffortDropdown.tsx', 'components/AgentSelector.tsx', 'components/AgentSkillsEditor.tsx', - 'pages/AgentsPage.tsx', 'pages/ChatPage.tsx', ] const offenders: string[] = [] diff --git a/website/src/test/listDetailNarrowViewport.test.tsx b/website/src/test/listDetailNarrowViewport.test.tsx index 8f9039e4fac..89ea4a83354 100644 --- a/website/src/test/listDetailNarrowViewport.test.tsx +++ b/website/src/test/listDetailNarrowViewport.test.tsx @@ -212,47 +212,6 @@ describe('SteeringTab across a viewport change', () => { }) }) -/** - * Clearing the selection must also leave the detail pane. - * - * With exclusive panes, `selectedAgent = null` while `detailOpen` stays true - * renders the "select an agent" placeholder — a branch that carries no Back - * control, since Back lives in the `selectedAgent` truthy branch — with the - * roster and its filter inside the hidden list pane. That combination leaves a - * phone user with no in-page way back, which is what deleting the agent you were - * viewing used to do. - * - * Asserted over the source because the invariant is about EVERY path that nulls - * the selection, including ones added later: a rendered test would only cover - * the delete path that exists today. - */ -describe('AgentsPage selection clearing', () => { - it('pairs every setSelectedAgent(null) with closeDetail()', async () => { - const src = (await import('../pages/AgentsPage.tsx?raw')).default as string - const clears = [...src.matchAll(/setSelectedAgent\(null\)/g)] - expect(clears.length, 'expected at least one path that clears the selection') - .toBeGreaterThan(0) - for (const match of clears) { - // The pane exit has to be in the same statement/handler, not merely - // somewhere in the file. - const window = src.slice(match.index!, match.index! + 200) - expect(window, `setSelectedAgent(null) at ${match.index} without closeDetail()`) - .toMatch(/closeDetail\(\)/) - } - }) - - it('keeps Back out of the placeholder branch, so the exit must be the handler', async () => { - const src = (await import('../pages/AgentsPage.tsx?raw')).default as string - // Documents WHY the fix belongs in the mutation handler. If a future change - // renders Back in the placeholder too, this can be revisited deliberately - // rather than by accident. - const placeholder = src.indexOf('select_an_agent_to_view_details') - expect(placeholder, 'placeholder branch not found').toBeGreaterThan(-1) - const branch = src.slice(placeholder - 400, placeholder + 200) - expect(branch).not.toMatch(/ListDetailBack/) - }) -}) - /** * Back sits on its own row, not in the header's action row. * diff --git a/website/src/test/navPinned.retiredIds.test.ts b/website/src/test/navPinned.retiredIds.test.ts new file mode 100644 index 00000000000..5c02d63913a --- /dev/null +++ b/website/src/test/navPinned.retiredIds.test.ts @@ -0,0 +1,36 @@ +/** + * A pin recorded for a surface that was later RETIRED from the registry must + * not survive the read: with no rail row and no pin control left, it would + * hold one of the NAV_PINNED_LIMIT slots forever and keep every remaining + * pin control disabled at the cap. `readNavPinned` drops retired ids BEFORE + * the cap applies (`capabilities-templates` is the retired Agent Templates + * tab), so a full set of live pins still fits. + */ +import { describe, it, expect, beforeEach } from 'vitest' + +import { readNavPinned, toggleNavPinned, NAV_PINNED_KEY, NAV_PINNED_LIMIT } from '../lib/navPinned' + +describe('retired surface ids in the persisted pinned set', () => { + beforeEach(() => localStorage.clear()) + + it('drops a retired id on read, before the cap applies', () => { + const live = ['capabilities-crews', 'capabilities-skills', 'capabilities-mcp', 'capabilities-hooks'] + expect(live.length + 1).toBeLessThanOrEqual(NAV_PINNED_LIMIT) + localStorage.setItem(NAV_PINNED_KEY, JSON.stringify(['capabilities-templates', ...live])) + const pinned = readNavPinned() + expect(pinned.has('capabilities-templates')).toBe(false) + for (const id of live) expect(pinned.has(id)).toBe(true) + }) + + it('a ghost pin no longer consumes a cap slot: a new pin is accepted', () => { + localStorage.setItem( + NAV_PINNED_KEY, + JSON.stringify(['capabilities-templates', 'a', 'b', 'c', 'd']), + ) + // 5 stored ids, but one is retired — the set reads as 4, so this succeeds. + expect(toggleNavPinned('capabilities-workflows')).toBe(true) + const pinned = readNavPinned() + expect(pinned.has('capabilities-workflows')).toBe(true) + expect(pinned.has('capabilities-templates')).toBe(false) + }) +}) diff --git a/website/src/test/navShortcutHint.test.tsx b/website/src/test/navShortcutHint.test.tsx index d087643a99e..03de5651c07 100644 --- a/website/src/test/navShortcutHint.test.tsx +++ b/website/src/test/navShortcutHint.test.tsx @@ -41,7 +41,6 @@ import '../surfaces/builtins' vi.mock('../pages/ChatPage', () => ({ default: () =>
ChatPage
})) vi.mock('../pages/SystemPage', () => ({ default: () => null })) -vi.mock('../pages/AgentsPage', () => ({ default: () => null })) vi.mock('../pages/ProjectsPage', () => ({ default: () => null })) vi.mock('../pages/LogsPage', () => ({ default: () => null })) vi.mock('../pages/KiroCrewAgentsPage', () => ({ default: () => null }))