// Teklens.AI — Settings ▸ Skills & Agents.
// Replaces the old single "Plugins" nav item with two reusable-entity collections:
//   • Skills  — the user-facing routed capability (description drives WHEN it runs),
//               owns the OUTPUT (none / markdown / html / widget, optionally templated).
//   • Agents  — reusable investigators that return DATA to the thread; no user output.
// Scope-aware: Workspace (built-in locked except template; custom full) and Project
// (workspace items inherited read-only except template override; project-specific full).
// Reuses globals: Icon, Field, Picker, SettingsToggle, IconPicker, SETTINGS_INPUT.

// ============================================================
//  Vocabulary — badges & output types
// ============================================================
// Skills and agents each render ONE canonical icon — never per-item.
// These match the Settings menu (Skills = puzzle, Agents = robot).
const SKILL_ICON = "puzzle";
const AGENT_ICON = "robot";

const SA_OUTPUT = {
  none:     { label: "Dynamic",    short: "Dynamic",  icon: "sparkles", tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
  markdown: { label: "Markdown",   short: "Markdown", icon: "file",    tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
  html:     { label: "HTML",       short: "HTML",     icon: "code",     tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
  widget:   { label: "Widget",     short: "Widget",   icon: "grid",     tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
};

// Lineage is a neutral attribute too — the icon differentiates, not a hue.
const SA_LINEAGE = {
  "built-in":  { label: "Built-in",  icon: "shield", tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
  custom:      { label: "Custom",    icon: "wand",   tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
  inherited:   { label: "Inherited", icon: "merge",  tint: "var(--text-muted)", bg: "var(--surface-inset)" },
  project:     { label: "Project",   icon: "grid",   tint: "var(--text-2)", bg: "color-mix(in srgb, var(--text-faint) 16%, transparent)" },
};

const SA_MODELS = ["Claude Sonnet 4.5", "Claude Opus 4.1", "Gemini 3.1 Pro", "GPT-5.2", "Grok 4"];

function LineageBadge({ lineage, overridden }) {
  const l = SA_LINEAGE[lineage] || SA_LINEAGE.custom;
  return (
    <span className="inline-flex items-center gap-1.5 shrink-0">
      <MetaChip icon={l.icon}>{l.label}</MetaChip>
      {overridden && (
        <span className="inline-flex items-center gap-1 text-[10.5px] font-medium px-2 py-0.5 rounded-[4px] border text-[var(--accent)]" style={{ borderColor: "color-mix(in srgb, var(--accent) 45%, transparent)" }}>
          <Icon name="edit-line" size={11} /> Overridden
        </span>
      )}
    </span>
  );
}

function OutputBadge({ output }) {
  const o = SA_OUTPUT[output] || SA_OUTPUT.none;
  return <MetaChip icon={o.icon}>{o.short}</MetaChip>;
}

// Whether an item is read-only in the current scope (and what *can* still be changed).
// Workspace: built-in is locked except its template. Custom is fully editable.
// Project:   everything inherited is read-only except template override + enable toggle.
function saLockState(item, scope) {
  if (scope === "project" && item.lineage !== "project")
    return { locked: true, templateEditable: true, reason: "inherited" };
  if (scope === "workspace" && item.lineage === "built-in")
    return { locked: true, templateEditable: true, reason: "built-in" };
  return { locked: false, templateEditable: true, reason: null };
}

// In Project scope, a workspace item is presented as "inherited".
function saEffLineage(item, scope) {
  if (scope === "project" && item.lineage !== "project") return "inherited";
  return item.lineage;
}

// ============================================================
//  Seeds
// ============================================================
// Source: docs/design/skills-catalog.json — agents are reusable investigators; they
// never choose output format. No tools concept (capabilities are automatic at runtime).
const AGENTS_SEED = [
  { id: "code-analysis", name: "Codebase Investigator", lineage: "built-in", icon: "code", model: "Current Conversation model",
    desc: "Traces architecture, ownership, and dependencies across the connected repositories to explain how a part of the codebase works. Pick this when a skill needs grounded understanding of implementation, structure, call graphs, or risk areas before producing output.",
    instructions: "You investigate the connected repositories. Locate the modules relevant to the request, follow imports and call graphs, and identify the components involved, how they interact, who owns them, and where the risks are. Cite exact file paths as evidence. Return a structured summary of components, responsibilities, dependencies, owners, and risk areas. Do not propose final user-facing wording — return findings only; the calling skill formats the output." },
  { id: "find-experts", name: "Expert Finder", lineage: "built-in", icon: "search", model: "Current Conversation model",
    desc: "Identifies and ranks subject-matter experts for a code area using git history and code ownership. Pick this when a skill needs to know who to assign work to, who to consult, or who owns a part of the system.",
    instructions: "Given a topic or code area, find the files involved, analyze their git history and line ownership, and aggregate contributions per author. Filter out bots and automation accounts. Rank contributors by depth of involvement — commits, lines owned, and recency. Return a ranked list of experts, each with the ownership evidence behind the ranking. Return findings only — the calling skill renders them." },
  { id: "estimate-feature", name: "Feature Estimator", lineage: "built-in", icon: "lab", model: "Current Conversation model",
    desc: "Assesses the complexity, effort, risk, and impact of a proposed feature or change, grounded in the actual codebase. Pick this when a skill needs a defensible estimate or a scoping breakdown.",
    instructions: "Investigate the area a feature would touch, identify the work involved, and surface unknowns and dependencies. Assess complexity, effort, risk, and impact, breaking the work into parts where that adds clarity. Ground every judgment in evidence from the code and its history. Return a structured estimate with the reasoning behind it. Return findings only." },
  { id: "release-scanner", name: "Release Scanner", lineage: "custom", icon: "branch", model: "Current Conversation model",
    desc: "Collects everything that shipped in a release window — merged pull requests, commits, and closed tickets — and groups them by type. Pick this when a skill needs the raw change set behind a release.",
    instructions: "Given a release window (since the last tag or a date range), gather the merged pull requests, commits, and closed Jira issues in that range. De-duplicate, drop noise (chore, CI, dependency bumps) unless explicitly asked to include it, and classify each item as a feature, fix, improvement, or breaking change. Attach the author and any linked ticket. Return a structured, grouped change set. Return findings only." },
  { id: "risk-assessor", name: "Risk Assessor", lineage: "custom", icon: "target", model: "Current Conversation model",
    desc: "Evaluates a change area for delivery and technical risk — coupling/blast radius, test-coverage gaps, ownership concentration, and code churn. Pick this when a skill needs a risk picture rather than a feature explanation.",
    instructions: "For the given area, assess risk along several axes: blast radius and coupling, test-coverage gaps, ownership concentration (bus factor), recent churn and instability, and external dependencies. Ground each judgment in evidence — specific files, history, and owners. Assign a severity and likelihood to each risk and explain the reasoning. Return a structured risk assessment. Return findings only." },
  { id: "component-mapper", name: "Component Mapper", lineage: "custom", icon: "hierarchy", model: "Current Conversation model",
    desc: "Discovers components and features and the relationships between them from code structure, imports, and naming. Pick this when a skill needs a map of the whole system rather than a deep dive into one part.",
    instructions: "Cluster the codebase into components and features based on directory structure, imports, and naming conventions. For each component, capture its purpose, key files, owners, and the components it depends on. Identify the edges between components. Return a structured component map suitable for diagramming. Return findings only." },
  { id: "feedback-miner", name: "Feedback Miner", lineage: "custom", icon: "message", model: "Current Conversation model",
    desc: "Mines connected sources (tickets, docs, comments) for recurring themes related to a topic. Pick this when a skill needs the voice-of-the-user signal behind a feature or problem.",
    instructions: "Search the connected sources for material related to the topic. Extract recurring themes, pain points, and requests, each with a representative quote and a source reference. Group by theme and indicate the strength of the signal (how often it recurs). Return a structured themes summary. Return findings only." },
  { id: "sprint-analyst", name: "Sprint Analyst", lineage: "custom", icon: "calendar", model: "Current Conversation model",
    desc: "Analyzes sprint and issue data to surface scope, progress, spillover, and velocity trends. Pick this when a skill needs the state of work in flight rather than codebase facts.",
    instructions: "Given a sprint or board, gather its issues with statuses, points, assignees, and history. Compute scope (committed vs added mid-sprint), progress (done / in-progress / blocked), spillover carried from prior sprints, and a velocity trend across recent sprints. Flag at-risk items — blocked, unassigned, or oversized. Return a structured sprint summary. Return findings only." },
  { id: "dependency-auditor", name: "Dependency Auditor", lineage: "custom", icon: "shield", model: "Current Conversation model",
    desc: "Inventories third-party dependencies across the repositories and flags outdated, duplicated, or risky versions. Pick this when a skill needs the supply-chain and version picture.",
    instructions: "Scan the repositories' manifests and lockfiles to inventory dependencies and their versions. Identify outdated packages, duplicate versions across workspaces, and unmaintained or known-risky ones. Note where each dependency is used. Return a structured audit grouped by severity. Return findings only." },
  { id: "doc-synthesizer", name: "Doc Synthesizer", lineage: "custom", icon: "book", model: "Current Conversation model",
    desc: "Gathers and synthesizes content from connected documentation sources (Confluence, SharePoint, docs) on a topic. Pick this when a skill needs written or tribal knowledge rather than code.",
    instructions: "Search the connected documentation sources for material on the topic. Read the most relevant pages, reconcile overlaps and contradictions, and synthesize a coherent summary with citations back to each source. Call out gaps where documentation is missing or stale. Return a structured synthesis. Return findings only." },
];

// Source: docs/design/skills-catalog.json — skills are user-facing, own the output,
// and reference AGENTS (never tools) for grounding.
const SKILLS_SEED = [
  { id: "code-analysis", name: "Code Analysis", lineage: "built-in", icon: "code", enabled: true, output: "none", gatedBy: "git",
    useTemplate: false, templateOverridden: false,
    availability: "Requires a linked Git repository",
    desc: "Investigate code architecture, patterns, and implementation details. Use when the user asks \"how does X work?\", \"what would be affected by changing Y?\", or \"what's the architecture of Z?\"",
    instructions: "Start the code-analysis agent and then wait for its results before answering. If the first pass isn't sufficient, run another investigation with a more specific question. Run multiple agents in parallel when useful.",
    agents: ["code-analysis"], template: null },
  { id: "find-experts", name: "Find Experts", lineage: "built-in", icon: "entity", enabled: true, output: "widget", gatedBy: "git",
    availability: "Requires a linked Git repository", bespoke: true, widgetTool: "display_experts",
    desc: "Identify subject matter experts by analyzing git history and code ownership. Use when the user asks \"who knows X?\", \"who should I assign this to?\", or \"who should I talk to about Y?\"",
    instructions: "To find experts: start_agent with agent=\"find-experts\", then wait_for_agent to get the results, then display_experts with the result data.",
    agents: ["find-experts"],
    template: "Ranked expert cards. Each card: name, linked identity (GitHub/Jira), ownership strength, last active, and top files owned. Header shows the query and how many files were analyzed.",
    schema: `{\n  "query": "string",\n  "filesAnalyzed": "number",\n  "experts": [{ "name": "string", "email": "string", "commits": "number", "linesOwned": "number", "lastActive": "string", "topFiles": ["string"] }]\n}`,
    sample: `{\n  "query": "search ranking",\n  "filesAnalyzed": 142,\n  "experts": [\n    { "name": "Maya Chen", "email": "maya@teklens.ai", "commits": 214, "linesOwned": 3800, "lastActive": "2 days ago", "topFiles": ["search/ranker.ts", "search/index.ts"] },\n    { "name": "Tomas Vidal", "email": "tomas@teklens.ai", "commits": 97, "linesOwned": 1600, "lastActive": "1 week ago", "topFiles": ["search/query.ts"] }\n  ]\n}` },
  { id: "estimate-feature", name: "Estimate Feature", lineage: "built-in", icon: "lab", enabled: true, output: "widget", gatedBy: "git",
    availability: "Requires a linked Git repository", bespoke: true, widgetTool: "display_estimation",
    desc: "Estimate effort, complexity, and risk for feature work. Use when the user asks \"how big is this?\", \"estimate this feature\", \"what's the effort?\", or \"how complex would X be?\".",
    instructions: "Before estimating, investigate the codebase first — estimates grounded in real code are far more accurate than guesses. Ask 1-2 clarifying questions only if there's genuine ambiguity about scope.\n\nTo run estimation: start_agent with agent=\"estimate-feature\", then wait_for_agent to get the results, then display_estimation with the result data.\n\n**Presenting results:** Lead with the story point estimate and confidence level. Explain what drives complexity in PM terms — number of affected areas, risk of regressions, test coverage gaps. When confidence is below 70%, flag the uncertainty and explain what's missing.\n\n**Next steps to offer:** Draft a Jira ticket with the estimation data pre-filled, find experts for the riskiest area, or break a large estimate into smaller scoped stories.",
    agents: ["estimate-feature"],
    template: "Estimation card: headline complexity and effort, an impact indicator, a list of risks, and an optional breakdown of the work into parts.",
    schema: `{\n  "complexity": "string",\n  "effort": "string",\n  "impact": "string",\n  "risks": ["string"],\n  "breakdown": [{ "part": "string", "effort": "string" }]\n}`,
    sample: `{\n  "complexity": "Medium",\n  "effort": "~8 points",\n  "impact": "High",\n  "risks": ["Reindex backfill has no rollback path", "Tokenizer change affects every locale"],\n  "breakdown": [\n    { "part": "Index schema migration", "effort": "3 pts" },\n    { "part": "Reindex job", "effort": "5 pts" }\n  ]\n}` },
  { id: "story-drafting", name: "Story Drafting", lineage: "built-in", icon: "story", enabled: true, output: "widget", essential: true,
    createsArtifact: true, artifactType: "Story",
    availability: "Requires a linked Git repository OR a linked Jira project", bespoke: true, widgetTool: "draft_jira_ticket",
    templatableBody: true, useTemplate: true, templateOverridden: false,
    bodyTemplate: "## User Story\n{As a [role], I want [capability], so that [benefit].}\n\n## Background\n{1–2 sentences on why this is needed; link the triggering conversation or evidence.}\n\n## Acceptance Criteria\n{Bulleted, testable conditions — each independently verifiable.}\n\n## Technical Notes\n{Affected components and files, patterns to follow, risks surfaced during investigation.}\n\n## Out of Scope\n{What this ticket explicitly does not cover.}",
    bodyExample: "## User Story\nAs a user, I want to attach screenshots and files to a message, so that the agent has visual context for my request.\n\n## Background\nRaised in the spec-review for **TEK-140**. Users paste image URLs by hand today, which the agent cannot read.\n\n## Acceptance Criteria\n- Drag-and-drop upload onto the composer\n- 50 MB per-file limit with an inline error when exceeded\n- Thumbnail preview before send\n- Attachments persist on the created Jira issue\n\n## Technical Notes\nTouches `composer/Attachments.tsx` and `api/upload.ts`; follows the existing `useDropzone` pattern. Risk: object-storage signing for large files.\n\n## Out of Scope\nVideo attachments and clipboard paste — tracked separately.",
    bodyTypes: ["Story", "Epic", "Bug", "Task"],
    bodyByType: {
      Story: {
        template: "## User Story\n{As a [role], I want [capability], so that [benefit].}\n\n## Background\n{1–2 sentences on why this is needed; link the triggering conversation or evidence.}\n\n## Acceptance Criteria\n{Bulleted, testable conditions — each independently verifiable.}\n\n## Technical Notes\n{Affected components and files, patterns to follow, risks surfaced during investigation.}\n\n## Out of Scope\n{What this ticket explicitly does not cover.}",
        example: "## User Story\nAs a user, I want to attach screenshots and files to a message, so that the agent has visual context for my request.\n\n## Background\nRaised in the spec-review for **TEK-140**. Users paste image URLs by hand today, which the agent cannot read.\n\n## Acceptance Criteria\n- Drag-and-drop upload onto the composer\n- 50 MB per-file limit with an inline error when exceeded\n- Thumbnail preview before send\n- Attachments persist on the created Jira issue\n\n## Technical Notes\nTouches `composer/Attachments.tsx` and `api/upload.ts`; follows the existing `useDropzone` pattern. Risk: object-storage signing for large files.\n\n## Out of Scope\nVideo attachments and clipboard paste — tracked separately.",
      },
      Epic: {
        template: "## Epic Summary\n{One sentence describing the outcome this epic delivers.}\n\n## Goal & Success Metrics\n{What success looks like, with measurable targets.}\n\n## Scope\n{The major capabilities or child stories this epic groups together.}\n\n## Out of Scope\n{What this epic explicitly excludes.}\n\n## Dependencies\n{Other epics, teams, or systems this relies on.}",
        example: "## Epic Summary\nGive every conversation rich attachment support — images, files, and pasted media — so the agent can reason over visual context.\n\n## Goal & Success Metrics\n80% of spec-review threads include at least one attachment within two sprints; zero attachment-related data-loss reports.\n\n## Scope\n- Drag-and-drop and clipboard upload in the composer\n- Per-file size limits and inline error handling\n- Thumbnail previews and attachment persistence on Jira issues\n\n## Out of Scope\nVideo transcoding and external CDN delivery — tracked under the Media Pipeline epic.\n\n## Dependencies\nObject-storage signing service (Platform), Jira attachment API quota increase.",
      },
      Bug: {
        template: "## Summary\n{One sentence: what's broken and where.}\n\n## Steps to Reproduce\n{Numbered, deterministic steps that trigger the defect.}\n\n## Expected vs Actual\n{What should happen, and what actually happens.}\n\n## Environment\n{Browser/OS/version, release, feature flags in play.}\n\n## Technical Notes\n{Suspected root cause, affected components/files, related tickets.}",
        example: "## Summary\nLarge image uploads (>20 MB) silently fail in the composer — no error, no attachment.\n\n## Steps to Reproduce\n1. Open any conversation\n2. Drag a 30 MB PNG onto the composer\n3. Send the message\n\n## Expected vs Actual\nExpected: an inline \"file too large\" error at the 50 MB limit, or a successful upload.\nActual: the thumbnail spins, then disappears; the message sends with no attachment and no error.\n\n## Environment\nChrome 124 / macOS 14.4, release v3.4.0, `attachments` flag on.\n\n## Technical Notes\nObject-storage signing in `api/upload.ts` times out for payloads over ~20 MB; the rejection is swallowed in `composer/Attachments.tsx`. Related: TEK-140.",
      },
      Task: {
        template: "## Task\n{One sentence describing the unit of work.}\n\n## Details\n{What needs to be done, with enough context to start.}\n\n## Definition of Done\n{Checklist of conditions that mark this complete.}\n\n## Technical Notes\n{Affected components/files, patterns to follow.}",
        example: "## Task\nAdd a 50 MB per-file size guard to the upload endpoint with a typed error response.\n\n## Details\nEnforce the limit server-side in `api/upload.ts` before signing the storage URL, returning a structured `FILE_TOO_LARGE` error the composer can surface inline.\n\n## Definition of Done\n- Requests over 50 MB rejected with a 413 and `FILE_TOO_LARGE` code\n- Composer shows the inline error from the typed response\n- Unit test covers the boundary at exactly 50 MB\n\n## Technical Notes\nMirror the existing validation pattern in `api/validate.ts`; no client changes beyond reading the error code.",
      },
    },
    desc: "Draft Jira tickets grounded in codebase evidence. Use when the user asks to \"draft a ticket\", \"create a story\", \"write a Jira issue\", or \"make a task for X\"",
    instructions: "**Always check for duplicates first.** Run the core search tool with `sources: [\"atlassian\"]` and `entityTypes: [\"jira:issue\"]` using a broad query that covers the feature or bug. Scan for partial matches, not just exact ones. If a potential duplicate exists, surface it with the issue key, status, and assignee. Ask if the user wants to update the existing ticket instead.\n\n**Investigate when it adds value.** For most tickets, use start_agent with code-analysis to understand the implementation area (affected files, existing patterns, risks) and start_agent with find-experts to identify who has expertise. Then call list_jira_users to match the top expert to a Jira account. Skip these when the user provides very specific requirements and the area is already well-understood — use judgment.\n\n**Draft with context.** Jira ticket drafts must be created with `draft_jira_ticket`. Do not use `generate_ui`, `render_widget`, raw HTML, or widget JSON for Jira drafts; those are visual-only surfaces and cannot create Jira issues. When calling `draft_jira_ticket`:\n\n- Reference specific components and files in acceptance criteria\n- Set the matched expert as assignee (assigneeAccountId + assigneeDisplayName)\n- Include risk areas identified during investigation\n- Keep description focused on what and why, not implementation details",
    agents: ["code-analysis", "find-experts"],
    template: "Editable ticket draft (interactive): summary, description, acceptance criteria, issue type, story points, labels, assignee, sprint — with a submit action.",
    schema: `{\n  "summary": "string",\n  "description": "string",\n  "acceptanceCriteria": ["string"],\n  "issueType": "string",\n  "storyPoints": "number",\n  "labels": ["string"],\n  "assignee": "string",\n  "sprint": "string"\n}`,
    sample: `{\n  "summary": "Allow attachments in conversations",\n  "description": "As a user I want to attach screenshots and files to a message so the agent has visual context.",\n  "acceptanceCriteria": ["Drag-and-drop upload", "50 MB per-file limit", "Inline preview before send"],\n  "issueType": "Story",\n  "storyPoints": 8,\n  "labels": ["chat", "attachments"],\n  "assignee": "Maya Chen",\n  "sprint": "Sprint 42"\n}` },
  { id: "atlassian", name: "Jira & Confluence", lineage: "built-in", icon: "jira", enabled: true, output: "none", gatedBy: "atlassian",
    availability: "Requires an Atlassian connection AND a linked Jira project",
    desc: "Jira and Confluence integration — tickets, sprints, search, and documentation. Use when the user asks about Jira tickets, sprints, project tracking, or says \"check this ticket\", \"show me the sprint\", \"find open bugs\"",
    instructions: "**Ticket references:**\n\n- When users say \"this ticket\", \"this one\", \"the ticket\", \"this epic\", or similar without specifying a key, they ALWAYS mean the currently pinned ticket(s) in Active Context — not tickets mentioned earlier in conversation. Do not fetch unrelated tickets unless the user explicitly names them by key.\n- When users reference a specific ticket key (e.g. \"check TEK-70\") or need full Jira fields, use get_jira_issue. When they need the indexed entity shape, use get_entity with `sources: [\"atlassian\"]` and `entityTypes: [\"jira:issue\"]`.\n\n**Tool selection:**\n\n- For ticket refinement questions (is it well-defined? what are the requirements?), always fetch full details with get_jira_issue first — pinned context only shows a summary. Only use get_jira_issue when the user wants details OF the ticket itself (e.g. \"check this ticket\", \"what's the status of this epic?\").\n- CRITICAL: If the user asks what's UNDER a ticket (e.g. \"what are the stories under this epic?\", \"show sub-tasks\"), use list_related_entities with the ticket entity and `labels: [\"children\"]`. Do not use text search for hierarchy lookup.\n- Use the core search tool for general issue or sprint discovery (e.g. \"find open bugs\", \"show me auth stories\", \"list sprints\"). Set `sources: [\"atlassian\"]` and narrow with `entityTypes: [\"jira:issue\"]` or `entityTypes: [\"jira:sprint\"]`.\n- Use list_related_entities for graph lookups such as sprint issues (`labels: [\"issues\"]`), issue sprints (`labels: [\"sprints\"]`), and issue children (`labels: [\"children\"]`).\n- **Label/status/type/priority/assignee filters:** Use the typed search filter hints for available values. They are case-sensitive; copy exact values and do not invent fallback terms.\n- When users ask follow-up questions about previously shown results (\"which of those are not done?\", \"find the open ones\"), do NOT run a new search. Filter and reference the tickets already in the conversation.\n\n**Ticket management:**\n\n- You can update issue fields (summary, description, issue type, story points, labels, priority, parent) with update_jira_issue. This includes converting between types (e.g. Story → Epic) and setting the parent/epic of an issue (e.g. \"add TEK-161 to epic TEK-89\" → update TEK-161 with parentKey \"TEK-89\").\n- You can link two issues together with link_jira_issues (e.g. \"Relates\", \"Blocks\"). Use this when the user asks to connect stories to an epic or link related tickets.\n- You can change status with transition_jira_issue. Always check available transitions first.\n- You can add comments with add_jira_comment. To @mention someone, first call list_jira_users to get the team roster, then include @DisplayName in the comment body and pass the mentions array with their accountId and displayName.\n- Use list_jira_users to resolve a person's name to their Jira account ID before assigning tickets or @mentioning them. Call it once per conversation — the roster rarely changes.\n- Use assign_jira_issue to assign issues after looking up the account ID via list_jira_users.\n- When drafting a Jira ticket, call draft_jira_ticket. Do not use generate_ui, render_widget, raw HTML, or widget JSON for Jira drafts — those are visual-only surfaces and cannot create Jira issues.\n\n**CRITICAL:** Never execute write operations (update, transition, comment) without explicit user approval. Before the first write in a conversation, present what you plan to do and ask for permission. Once the user approves, you may proceed with related writes in that conversation without asking again for each one. But never assume permission — always get that first explicit \"yes\".",
    agents: [] },
  { id: "file-watcher", name: "Local Files", lineage: "built-in", icon: "folder", enabled: true, output: "none", gatedBy: "folder",
    availability: "Requires at least one watched folder",
    desc: "The user's private knowledge base — personal notes, client records, health data, design docs, and any markdown/text/PDF/Office files they've synced from their machine. Load this skill whenever the user asks about anything that isn't obviously code, a Jira ticket, or about the codebase. When in doubt, load it and search; the cost of missing private context is higher than the cost of an empty search.",
    instructions: "**Local files — the user's private knowledge base:**\n\nUse the unified `search` tool to search indexed local files. Limit to local file entities with `sources: [\"file-watcher\"]` or `entityTypes: [\"file-watcher:file\"]` when the user is specifically asking about synced documents.\n\nWhen to call it:\n- Any question mentioning a person, client, company, event, or domain term that is not obviously a code symbol or ticket ID.\n- When the user says \"my ...\" such as my notes, my clients, my lab results, my meeting prep, or my briefing.\n- When a factual question could plausibly be answered from private synced files.\n- When code/Jira/GitHub search returns nothing and the question could be about personal or project documents.\n\nHow to search:\n- Use broad semantic search terms first. Refine only if the first hit set is noisy.\n- Prefer `sources: [\"file-watcher\"]` for local-file-only questions.\n- Prefer `entityTypes: [\"file-watcher:file\"]` when the query should return file entities specifically.\n- Cite the file path from the result instead of summarizing vaguely.\n\nCombine with other sources by leaving `sources` unset when the question spans local files, Jira, GitHub, and other indexed entities.",
    agents: [] },
  { id: "release-note-generator", name: "Release Note Generator", lineage: "custom", icon: "book", enabled: true, output: "html",
    useTemplate: true, templateOverridden: false, createsArtifact: true, artifactType: "Release Note",
    desc: "Use when the user asks to generate release notes, a changelog, or a 'what shipped' summary for a release or date range.",
    instructions: "Use the Release Scanner to collect and classify everything that shipped in the window. Then fill the HTML template below: keep every entry user-facing (describe the value, not the commit message), group by category, and credit the author or ticket where useful. Omit a section that has no entries.",
    agents: ["release-scanner"],
    structure: "# Release {{version}} — {{date}}\n\nHeader band: the version, the date, and a one-line summary of the release theme.\n\n## New Features\nOne user-facing line per feature — what it does, with ticket and author.\n\n## Fixes\nOne line per fix — what was broken and is now resolved, with ticket.\n\n## Breaking Changes\nOne line per breaking change — what changed and the action required. (rendered with an amber heading)",
    template: `<article style="font-family:Poppins,system-ui;color:var(--text)">
  <header style="border-bottom:1px solid var(--border);padding-bottom:14px;margin-bottom:16px">
    <span style="display:inline-block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);background:var(--accent-tint);padding:2px 8px;border-radius:999px">Release</span>
    <h1 style="font-size:23px;font-weight:700;margin:10px 0 4px">{{version}} <span style="font-size:14px;font-weight:500;color:var(--text-faint)">· {{date}}</span></h1>
    <p style="font-size:13.5px;color:var(--text-3);margin:0">{{one-line summary of the release theme}}</p>
  </header>
  <section style="margin-bottom:16px">
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 8px">New Features</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">{{per feature: a user-facing line — what it does (ticket · author)}}</ul>
  </section>
  <section style="margin-bottom:16px">
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 8px">Fixes</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">{{per fix: what was broken and is now resolved (ticket)}}</ul>
  </section>
  <section>
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--amber-ink);margin:0 0 8px">Breaking Changes</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">{{per breaking change: what changed and the action required}}</ul>
  </section>
</article>`,
    example: `<article style="font-family:Poppins,system-ui;color:var(--text)">
  <header style="border-bottom:1px solid var(--border);padding-bottom:14px;margin-bottom:16px">
    <span style="display:inline-block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);background:var(--accent-tint);padding:2px 8px;border-radius:999px">Release</span>
    <h1 style="font-size:23px;font-weight:700;margin:10px 0 4px">v3.4.0 <span style="font-size:14px;font-weight:500;color:var(--text-faint)">· Apr 22, 2026</span></h1>
    <p style="font-size:13.5px;color:var(--text-3);margin:0">Faster chat streaming, a redesigned context rail, and safer pinned-context migration.</p>
  </header>
  <section style="margin-bottom:16px">
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 8px">New Features</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">
      <li>Streaming responses now render token-by-token for snappier replies. <span style="color:var(--text-faint2)">(TEK-412 · Maya Chen)</span></li>
      <li>Redesigned context rail with a pinned dock and breadcrumbs. <span style="color:var(--text-faint2)">(TEK-388 · Sol Park)</span></li>
    </ul>
  </section>
  <section style="margin-bottom:16px">
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 8px">Fixes</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">
      <li>Pinned context no longer disappears when reopening an in-flight chat. <span style="color:var(--text-faint2)">(TEK-401)</span></li>
      <li>Virtualized message list stops jumping on rapid updates. <span style="color:var(--text-faint2)">(TEK-377)</span></li>
    </ul>
  </section>
  <section>
    <h2 style="font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--amber-ink);margin:0 0 8px">Breaking Changes</h2>
    <ul style="margin:0;padding-left:18px;font-size:13px;color:var(--text-3);line-height:1.7">
      <li><code style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface-inset);padding:1px 5px;border-radius:4px">contextChips[]</code> is replaced by <code style="font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface-inset);padding:1px 5px;border-radius:4px">contextGraph</code> — run the migration before upgrading. <span style="color:var(--text-faint2)">(TEK-410)</span></li>
    </ul>
  </section>
</article>` },
  { id: "risk-analyzer", name: "Risk Analyzer", lineage: "custom", icon: "target", enabled: true, output: "widget",
    useTemplate: true, templateOverridden: false, comp: "RiskAnalyzer",
    desc: "Use when the user asks about the risk of a change, an area, or a release — delivery risk, technical risk, or what could go wrong.",
    instructions: "Use the Risk Assessor — and the Codebase Investigator when deeper grounding helps — to evaluate the area. Produce a risk widget: an overall score and headline, then each risk with severity, score, likelihood, impact, the evidence (file · lines · owner) behind it, and a recommended mitigation.",
    agents: ["risk-assessor", "code-analysis"],
    widgetHtml: `<!-- Risk report widget · bound to the schema below -->
<section class="rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5">
  <header class="flex items-start justify-between gap-4">
    <div>
      <h2 class="text-[18px] font-semibold tracking-tight">{{headline}}</h2>
      <p class="text-[13px] text-[var(--text-muted)] mt-1 leading-relaxed">{{summary}}</p>
      <div class="text-[12px] text-[var(--text-faint2)] mt-2">{{counts.total}} risks · {{counts.high}} high · {{counts.medium}} medium · {{counts.low}} low</div>
    </div>
    <div class="shrink-0 grid place-items-center w-16 h-16 rounded-full"
         style="background: conic-gradient(var(--red-ink) calc({{overall}}*1%), var(--surface-inset) 0)">
      <span class="w-12 h-12 rounded-full bg-[var(--card)] grid place-items-center text-[18px] font-bold">{{overall}}</span>
    </div>
  </header>

  <h3 class="text-[13px] font-semibold mt-5 mb-2">Top risks</h3>
  {{#each risks}}
  <article class="rounded-xl border border-[var(--border)] p-4 mb-2.5">
    <div class="flex items-center gap-2">
      <span class="badge badge-{{severity}}">{{severity}}</span>
      <span class="font-semibold text-[14px] flex-1">{{title}}</span>
      <span class="font-mono text-[12px] text-[var(--text-faint2)]">{{score}}/100</span>
    </div>
    <div class="font-mono text-[12px] text-[var(--text-muted)] mt-1">{{area}} · {{lines}} · Likelihood: {{likelihood}} · Impact: {{impact}}</div>
    <p class="text-[13px] mt-2 leading-relaxed">{{desc}}</p>
    <div class="rounded-lg bg-[var(--surface-2)] p-3 mt-2.5">
      <div class="text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--text-faint2)] mb-1">Mitigation</div>
      <p class="text-[13px]">{{mitigation}}</p>
    </div>
    <footer class="flex items-center justify-between mt-2.5 text-[12px] text-[var(--text-muted)]">
      <span>{{owner}} · Effort: {{effort}}</span>
      <a class="text-[var(--accent)] font-medium">Create ticket</a>
    </footer>
  </article>
  {{/each}}
</section>`,
    schema: `{\n  "overall": "number (0-100)",\n  "headline": "string",\n  "summary": "string",\n  "counts": { "total": "number", "high": "number", "medium": "number", "low": "number" },\n  "risks": [{\n    "severity": "high | medium | low",\n    "score": "number (0-100)",\n    "title": "string",\n    "area": "string (file path)",\n    "lines": "string",\n    "likelihood": "Likely | Possible | Unlikely",\n    "impact": "High | Medium | Low",\n    "desc": "string",\n    "mitigation": "string",\n    "owner": "string",\n    "effort": "S | M | L"\n  }]\n}`,
    sample: `{\n  "overall": 68,\n  "headline": "Implementation Risk",\n  "summary": "Branch features/new-chat-frontend diverges 38 commits from main. Risk is concentrated in two adjacent surfaces: streaming state and message virtualization. Migration of the pinned-context schema is the highest-blast-radius item if shipped without backfill.",\n  "counts": { "total": 5, "high": 2, "medium": 2, "low": 1 },\n  "risks": [\n    { "severity": "high", "score": 84, "title": "Race condition between streaming chunks and optimistic user message", "area": "src/chat/useChatStream.ts", "lines": "L42–L78", "likelihood": "Likely", "impact": "High", "desc": "Stream subscription opens before the optimistic insert resolves. On poor network, chunks may target a stale message id and silently drop.", "mitigation": "Defer stream subscription until the optimistic id is committed; introduce a pending-buffer keyed by clientMsgId and replay on bind.", "owner": "felix.h", "effort": "M" },\n    { "severity": "high", "score": 76, "title": "Virtualization breaks scroll-to-bottom on tool-call expansion", "area": "src/chat/MessageList.tsx", "lines": "L120–L165", "likelihood": "Possible", "impact": "High", "desc": "Variable-height tool cards aren't measured before next render. Auto-scroll lands above the latest message ~12% of the time in repro.", "mitigation": "Use ResizeObserver per row; gate auto-scroll on a 1-frame post-measure RAF. Add an at-bottom sentinel.", "owner": "anita.r", "effort": "S" },\n    { "severity": "medium", "score": 64, "title": "Pinned-context store migration has no backfill", "area": "src/state/contextGraph.ts", "lines": "—", "likelihood": "Likely", "impact": "Medium", "desc": "Schema changes contextChips[] → contextGraph. Existing users would lose pins on first load.", "mitigation": "Write one-shot migrator on hydrate; emit telemetry per migrated user; add rollback flag.", "owner": "lukas.b", "effort": "M" }\n  ]\n}` },
  { id: "component-analyzer", name: "Component Analyzer", lineage: "custom", icon: "hierarchy", enabled: true, output: "widget",
    desc: "Use when the user asks for an overview of the system's components or features, an architecture map, or how the parts fit together.",
    instructions: "Use the Component Mapper to cluster the codebase, then present an interactive component map: each component with its purpose, owners, and dependencies, plus the relationships between components.",
    agents: ["component-mapper"],
    template: "Component map. A node per component (name, purpose, owner count, file count) with edges for dependencies, plus a side list of components showing details on selection.",
    schema: `{\n  "components": [{ "name": "string", "purpose": "string", "owners": ["string"], "files": "number", "dependsOn": ["string"] }],\n  "edges": [{ "from": "string", "to": "string" }]\n}`,
    sample: `{\n  "components": [\n    { "name": "Search", "purpose": "Indexing & ranking", "owners": ["Maya Chen"], "files": 34, "dependsOn": ["Storage"] },\n    { "name": "Storage", "purpose": "Persistence layer", "owners": ["Tomas Vidal"], "files": 21, "dependsOn": [] }\n  ],\n  "edges": [{ "from": "Search", "to": "Storage" }]\n}` },
  { id: "idea-estimator", name: "Idea Estimator", lineage: "custom", icon: "lab", enabled: true, output: "widget",
    useTemplate: true, templateOverridden: false, comp: "IdeaEstimatorWidget",
    desc: "Use when the user floats a new idea, feature, or tool and wants a quick first-pass estimate — complexity, total risk, the top risks, who should build it, and a fuller breakdown — before committing.",
    instructions: "Use the Feature Estimator to size the idea against the real codebase, the Risk Assessor to surface delivery and technical risk, and the Expert Finder to recommend who should own it. Produce an estimate widget: a complexity and total-risk score, the top risks ranked by severity, recommended team members with why, and a collapsible full estimation analysis.",
    agents: ["estimate-feature", "risk-assessor", "find-experts"],
    widgetHtml: `<!-- Idea estimate widget · bound to the schema below -->
<section class="rounded-2xl border border-[var(--border)] bg-[var(--card)] p-5">
  <header class="flex items-start justify-between gap-4">
    <div>
      <h2 class="text-[18px] font-semibold tracking-tight">{{idea}}</h2>
      <p class="text-[13px] text-[var(--text-muted)]">{{subtitle}}</p>
    </div>
    <div class="flex gap-5">
      <div class="text-center"><div class="ring" data-value="{{complexity}}" data-color="accent">{{complexity}}%</div><div class="overline mt-1">Complexity</div></div>
      <div class="text-center"><div class="ring" data-value="{{totalRisk}}" data-color="amber">{{totalRisk}}%</div><div class="overline mt-1">Total risk</div></div>
    </div>
  </header>

  <h3 class="overline mt-5 mb-2">Top 3 risks</h3>
  {{#each risks}}
  <div class="row row-{{severity}}"><span>{{title}}</span><span class="sev">{{severityLabel}}</span></div>
  {{/each}}

  <h3 class="text-[14px] font-semibold mt-5 mb-2">Recommended team members</h3>
  <div class="grid grid-cols-2 gap-3">
  {{#each team}}
    <div class="member"><span class="avatar">{{initials}}</span><div><div class="font-semibold">{{name}}</div><div class="role">{{role}}</div><p class="why">{{why}}</p></div></div>
  {{/each}}
  </div>

  <details class="mt-4"><summary class="text-[14px] font-semibold">Full estimation analysis</summary>
    <div class="mt-2 text-[13px] text-[var(--text-muted)]">{{analysis}}</div>
  </details>
</section>`,
    schema: `{\n  "idea": "string",\n  "subtitle": "string",\n  "complexity": "number (0-100)",\n  "totalRisk": "number (0-100)",\n  "risks": [{ "title": "string", "severity": "high | medium | low", "severityLabel": "HIGH | MEDIUM | LOW" }],\n  "team": [{ "initials": "string", "name": "string", "role": "string", "why": "string" }],\n  "analysis": "string"\n}`,
    sample: `{\n  "idea": "AI Legal Summary Tool",\n  "subtitle": "Resource & Risk Assessment",\n  "complexity": 72,\n  "totalRisk": 55,\n  "risks": [\n    { "title": "Data Privacy Compliance (GDPR)", "severity": "high", "severityLabel": "HIGH" },\n    { "title": "Latency in AI Inference", "severity": "medium", "severityLabel": "MEDIUM" },\n    { "title": "Frontend State Complexity", "severity": "low", "severityLabel": "LOW" }\n  ],\n  "team": [\n    { "initials": "AR", "name": "Alex Rivera", "role": "Backend Architect", "why": "Recent commits in high-concurrency data pipelines and database scaling." },\n    { "initials": "JS", "name": "Jordan Smith", "role": "Security & DevOps", "why": "Handled the last 3 security audits and Auth0 migrations." }\n  ],\n  "analysis": "Net-new surface with a heavy compliance dimension. Inference latency and GDPR handling dominate the risk; frontend is well-understood. Recommend a spike on data residency before committing the estimate."\n}` },
  { id: "tech-debt-report", name: "Tech Debt Report", lineage: "custom", icon: "target", enabled: true, output: "markdown",
    useTemplate: true, templateOverridden: false,
    desc: "Use when the user asks about technical debt, hotspots, or where the codebase needs investment.",
    instructions: "Use the Risk Assessor, Component Mapper, and Dependency Auditor to find debt hotspots. Then fill the Markdown template: an overall debt summary with a score and the top three concerns, followed by a hotspot section per area ranked by severity — each with the evidence behind it and a suggested remediation with rough effort.",
    agents: ["risk-assessor", "component-mapper", "dependency-auditor"],
    template: "# Technical Debt Report — {{scope}}\n\n**Debt score: {{0-100}}** · {{one-line headline}}\n\nTop concerns:\n- {{concern 1}}\n- {{concern 2}}\n- {{concern 3}}\n\n## Hotspots\n\n### {{component}} · {{severity}}\n{{evidence: churn, coverage, ownership, outdated deps}}\n**Fix:** {{remediation}} ({{rough effort}})",
    example: "# Technical Debt Report — Chat & Context\n\n**Debt score: 64** · Concentrated in the streaming layer and the pinned-context store\n\nTop concerns:\n- Streaming state machine has high churn and no test coverage\n- Pinned-context store migration lacks a backfill path\n- Three core dependencies are two majors behind\n\n## Hotspots\n\n### Streaming layer · High\n12 commits in 30 days, 0% test coverage, single owner (bus factor 1).\n**Fix:** Extract a tested reducer and add contract tests (~3 days)\n\n### Pinned-context store · High\nSchema migration drops existing pins for in-flight chats; no backfill path.\n**Fix:** Add a migration with backfill behind a feature flag (~2 days)\n\n### Dependency freshness · Medium\nreact-flow, zod, and tiptap are two majors behind with open advisories.\n**Fix:** Stage upgrades behind CI smoke tests (~1.5 days)" },
  { id: "sprint-digest", name: "Sprint Digest", lineage: "custom", icon: "calendar", enabled: true, output: "widget",
    createsArtifact: true, artifactType: "Sprint Summary", comp: "SprintDigestWidget",
    desc: "Use when the user asks for a sprint status, a sprint summary, or 'where are we' on the current sprint.",
    instructions: "Use the Sprint Analyst to assess the sprint, then present a digest widget: completion, quality and test-coverage scores, story-points progress (completed vs planned), at-risk and off-spec items, mid-sprint bugs, and verbose notes per story.",
    agents: ["sprint-analyst"],
    template: "Sprint digest widget: completion / quality / test-coverage scores, a story-points progress bar (completed vs planned), and a verbose per-story breakdown flagging off-spec work, missing tests, scope creep and mid-sprint bugs.",
    schema: `{\n  "sprintName": "string",\n  "scores": { "completion": "number (0-100)", "quality": "number (0-100)", "testCoverage": "number (0-100)" },\n  "points": { "done": "number", "planned": "number" },\n  "stories": [{ "key": "string", "title": "string", "owner": "string", "points": "number", "flag": "done | off-spec | no-tests | scope-creep | bug", "note": "string" }],\n  "notes": "string"\n}`,
    sample: `{\n  "sprintName": "Sprint 42",\n  "scores": { "completion": 76, "quality": 58, "testCoverage": 41 },\n  "points": { "done": 34, "planned": 45 },\n  "stories": [\n    { "key": "TEK-388", "title": "Redesigned context rail", "owner": "anita.r", "points": 8, "flag": "off-spec", "note": "Shipped without design sign-off; dropped the agreed compact variant." },\n    { "key": "TEK-401", "title": "Pinned context survives reopen", "owner": "lukas.b", "points": 5, "flag": "no-tests", "note": "Data-loss fix landed with no regression test." },\n    { "key": "TEK-412", "title": "Token-by-token streaming", "owner": "felix.h", "points": 8, "flag": "scope-creep", "note": "Also swapped the markdown renderer and added code-splitting — ~11 unestimated pts." },\n    { "key": "TEK-431", "title": "Safari paste regression", "owner": "felix.h", "points": 2, "flag": "bug", "note": "Introduced day 3 by the dropzone change." }\n  ],\n  "notes": "Quality is the story of the sprint — merges are landing ahead of test and design sign-off."\n}` },
  { id: "prd-collector", name: "Product Requirements Collector", lineage: "custom", icon: "file", enabled: true, output: "markdown",
    useTemplate: true, templateOverridden: false, createsArtifact: true, artifactType: "PRD",
    desc: "Use when the user wants to capture or structure a product requirement — when they say \"write a PRD\", \"spec this out\", \"capture the requirements for X\", or describe a feature they want to formalise.",
    instructions: "Collect requirements through a short, focused conversation — don't dump a blank template on the user. Ask about the problem, the target user, and the success metric first; infer the rest from context (linked tickets, the codebase, prior conversations) before asking. Use the Codebase Investigator to ground feasibility and surface constraints. Then fill the Markdown template: lead with the problem and the user, make goals measurable, write requirements as testable statements (P0/P1/P2), and always include what's explicitly out of scope. Flag any section where you're guessing so the user can confirm.",
    agents: ["code-analysis"],
    template: "# {{feature name}}\n\n**Status:** {{Draft · Review · Approved}} · **Owner:** {{owner}} · **Updated:** {{date}}\n\n## Problem\n{{the user problem in 2–3 sentences — who hurts and why now}}\n\n## Target user\n{{the primary persona and the job they're trying to do}}\n\n## Goals & success metrics\n- {{measurable goal — the metric that proves this worked}}\n\n## Requirements\n- **P0** {{must-have, testable statement}}\n- **P1** {{should-have}}\n- **P2** {{nice-to-have}}\n\n## Out of scope\n- {{what this explicitly does not cover}}\n\n## Open questions\n- {{unresolved decision needing an owner}}",
    example: "# Inline attachments in conversations\n\n**Status:** Draft · **Owner:** Sam · **Updated:** Apr 22, 2026\n\n## Problem\nUsers can't give the agent visual context — screenshots, error states, design mockups — without pasting raw URLs it can't read. Support threads stall and tickets get drafted with missing detail. With attachments now expected in every modern tool, the gap is increasingly the reason teams bounce in the first session.\n\n## Target user\nProduct managers and support specialists drafting tickets mid-conversation, who already have the evidence on their screen but no way to hand it to the agent.\n\n## Goals & success metrics\n- 40% of ticket drafts include at least one attachment within two sprints of launch\n- Time-to-first-draft drops by 25% on conversations that start with an image\n- Zero increase in p95 message-send latency\n\n## Requirements\n- **P0** Drag-and-drop and click-to-upload on the composer, 50 MB per file\n- **P0** Inline thumbnail preview before send, with remove\n- **P0** Attachments persist onto the created Jira issue\n- **P1** Clipboard paste of images directly into the composer\n- **P2** Auto-extract text from uploaded screenshots for search\n\n## Out of scope\n- Video and audio attachments — tracked separately\n- Server-side virus scanning — handled by the existing upload pipeline\n\n## Open questions\n- Retention policy for attachments on deleted conversations — needs legal sign-off\n- Do we count an attachment against the user's storage quota? Owner: Billing" },
];

// ============================================================
//  Side-panel item lists  (rendered inside SettingsSidePanel)
// ============================================================
// ============================================================
//  Activation model — built-in skills are governed by their connection
//  (or are essential); only custom skills get a manual on/off switch.
// ============================================================
const SA_CONNECTIONS = {
  git:       { label: "Git repository", connected: true },
  atlassian: { label: "Atlassian", connected: true },
  folder:    { label: "watched folder", connected: true },
};
function skillActivation(s) {
  if (s.essential) return { mode: "always" };
  if (s.gatedBy) { const c = SA_CONNECTIONS[s.gatedBy] || { label: s.gatedBy, connected: false }; return { mode: "gated", conn: c, active: !!c.connected }; }
  return { mode: "toggle" };
}
function ActivationControl({ skill, onToggle, compact }) {
  const a = skillActivation(skill);
  if (a.mode === "toggle") return <SettingsToggle on={skill.enabled} onChange={onToggle} />;
  const base = "inline-flex items-center gap-1.5 rounded-full border font-medium " + (compact ? "px-2 py-0.5 text-[10.5px]" : "px-2.5 py-1 text-[11.5px]");
  if (a.mode === "always")
    return <span title="Essential skill — always on and cannot be disabled." className={`${base} border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-muted)]`}><Icon name="shield" size={compact ? 10 : 12} /> Always on</span>;
  // gated
  return a.active
    ? <span title={`Active — ${a.conn.label} connected. Governed by the connection, not a manual switch.`} className={`${base} border-[color-mix(in_srgb,var(--green-ink)_30%,transparent)] bg-[var(--green-bg)] text-[var(--green-ink)]`}><span className="w-1.5 h-1.5 rounded-full bg-[var(--green-ink)]" /> Active</span>
    : <span title={`Unavailable — connect ${a.conn.label} to enable.`} className={`${base} border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-faint)]`}><span className="w-1.5 h-1.5 rounded-full bg-[var(--bar)]" /> Unavailable</span>;
}

// Whether a skill can be modified in the current scope. Built-ins with no
// template / body and no widget to override are read-only.
function skillEditable(s, scope) {
  if (!saLockState(s, scope).locked) return true;
  if (s.templatableBody) return true;
  if ((s.output === "markdown" || s.output === "html") && s.useTemplate) return true;
  if (s.output === "widget" && !s.bespoke) return true;
  return false;
}

function SkillSideRow({ s, scope, active, onClick }) {
  const lock = saLockState(s, scope);
  return (
    <button onClick={onClick} className={`group flex items-center gap-2 pl-2.5 pr-2 py-1.5 rounded-md text-left text-[12.5px] ${active ? "bg-[var(--hover)] text-[var(--text)] font-medium" : "text-[var(--text-2)] hover:bg-[var(--hover)]"}`}>
      <Icon name={SKILL_ICON} size={15} className="shrink-0 text-[var(--text-muted)]" />
      <span className="truncate flex-1">{s.name}</span>
      {s.createsArtifact && <Icon name="file" size={12} className="shrink-0 text-[var(--violet-ink)]" title={`Creates artifact · ${s.artifactType}`} />}
      {lock.locked && <Icon name="shield" size={11} className="shrink-0" style={{ color: "var(--bar)" }} />}
    </button>
  );
}

function SkillItems({ ws }) {
  const { scope } = ws;
  const inScope = (s) => scope === "workspace" ? s.lineage !== "project" : true;
  const list = ws.skills.filter(inScope);
  const groups = scope === "workspace"
    ? [["Built-in", "ship with Teklens", list.filter((s) => s.lineage === "built-in")],
       ["Custom", "defined for this workspace", list.filter((s) => s.lineage === "custom")]]
    : [["Built-in", "ship with Teklens", list.filter((s) => s.lineage === "built-in")],
       ["Inherited", "from the workspace", list.filter((s) => s.lineage === "custom")],
       ["Project", "specific to this project", list.filter((s) => s.lineage === "project")]];
  return (
    <div>
      <SAItemsHeader label="Skills" onAdd={ws.addSkill} addTitle="New skill" onTitle={() => { ws.setSkillSel(null); ws.setAgentSel(null); ws.setSaMode("detail"); }} />
      {groups.map(([label, hint, rows], i) => (
        <div key={label} className={i ? "mt-3" : ""}>
          <SAGroupLabel label={label} hint={hint} />
          <div className="flex flex-col gap-0.5">
            {rows.length ? rows.map((s) => <SkillSideRow key={s.id} s={s} scope={scope} active={s.id === ws.skillSel && !ws.agentSel} onClick={() => { ws.setSkillSel(s.id); ws.setAgentSel(null); ws.setSaMode("detail"); }} />)
              : <div className="px-1.5 py-1 text-[11.5px] text-[var(--text-faint2)]">None yet — add one with +</div>}
          </div>
        </div>
      ))}
    </div>
  );
}

function AgentSideRow({ a, scope, active, onClick }) {
  const lock = saLockState(a, scope);
  return (
    <button onClick={onClick} className={`group flex items-center gap-2 pl-2.5 pr-2 py-1.5 rounded-md text-left text-[12.5px] ${active ? "bg-[var(--hover)] text-[var(--text)] font-medium" : "text-[var(--text-2)] hover:bg-[var(--hover)]"}`}>
      <Icon name={AGENT_ICON} size={15} className="shrink-0 text-[var(--text-muted)]" />
      <span className="truncate flex-1">{a.name}</span>
      {lock.locked && <Icon name="shield" size={11} className="shrink-0" style={{ color: "var(--bar)" }} />}
    </button>
  );
}

function AgentItems({ ws }) {
  const { scope } = ws;
  const list = ws.agents.filter((a) => scope === "workspace" ? a.lineage !== "project" : true);
  const groups = scope === "workspace"
    ? [["Built-in", "ship with Teklens", list.filter((a) => a.lineage === "built-in")],
       ["Custom", "defined for this workspace", list.filter((a) => a.lineage === "custom")]]
    : [["Built-in", "ship with Teklens", list.filter((a) => a.lineage === "built-in")],
       ["Inherited", "from the workspace", list.filter((a) => a.lineage === "custom")],
       ["Project", "specific to this project", list.filter((a) => a.lineage === "project")]];
  return (
    <div>
      <SAItemsHeader label="Sub-Agents" onAdd={ws.addAgent} addTitle="New sub-agent" onTitle={() => { ws.setAgentSel(null); ws.setSkillSel(null); ws.setSaMode("detail"); }} />
      {groups.map(([label, hint, rows], i) => (
        <div key={label} className={i ? "mt-3" : ""}>
          <SAGroupLabel label={label} hint={hint} />
          <div className="flex flex-col gap-0.5">
            {rows.length ? rows.map((a) => <AgentSideRow key={a.id} a={a} scope={scope} active={a.id === ws.agentSel} onClick={() => { ws.setAgentSel(a.id); ws.setSkillSel(null); ws.setSaMode("detail"); }} />)
              : <div className="px-1.5 py-1 text-[11.5px] text-[var(--text-faint2)]">None yet — add one with +</div>}
          </div>
        </div>
      ))}
    </div>
  );
}

function SAItemsHeader({ label, onAdd, addTitle, onTitle }) {
  return (
    <div className="flex items-center justify-between px-1.5 mb-2">
      {onTitle
        ? <button onClick={onTitle} title={`Show all ${label.toLowerCase()}`} className="group flex items-center gap-1 text-[10.5px] font-semibold tracking-[0.07em] text-[var(--text-faint2)] hover:text-[var(--accent)] uppercase">{label}</button>
        : <span className="text-[10.5px] font-semibold tracking-[0.07em] text-[var(--text-faint2)] uppercase">{label}</span>}
      <button onClick={onAdd} title={addTitle} className="w-6 h-6 rounded-md flex items-center justify-center text-[var(--accent)] hover:bg-[var(--accent)]/[0.1]"><Icon name="plus" size={16} /></button>
    </div>
  );
}
function SAGroupLabel({ label, hint }) {
  return <div className="flex items-baseline gap-1.5 px-1.5 mb-1.5"><span className="text-[11px] font-semibold tracking-[0.06em] text-[var(--text-muted)] uppercase">{label}</span><span className="text-[10.5px] text-[var(--text-faint2)]">{hint}</span></div>;
}
function SAScopePills({ scope, setScope }) {
  return (
    <div className="mx-1.5 mb-3 inline-flex items-center bg-[var(--border-soft)] rounded-lg p-0.5 w-[calc(100%-12px)]">
      {[["workspace", "building"], ["project", "grid"]].map(([k, ic]) => (
        <button key={k} onClick={() => setScope(k)} className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[12px] font-medium capitalize ${scope === k ? "bg-[var(--card)] shadow-sm text-[var(--text)]" : "text-[var(--text-muted)]"}`}>
          <Icon name={ic} size={12} /> {k}
        </button>
      ))}
    </div>
  );
}

// ============================================================
//  Shared editor chrome
// ============================================================
function SAHeader({ crumb, label, hint, right }) {
  return (
    <div className="shrink-0 bg-[var(--card)] border-b border-[var(--border)] px-6 py-3.5 flex items-center gap-2.5">
      <Icon name="settings" size={18} className="text-[var(--text-faint)]" />
      <h1 className="text-[18px] font-semibold text-[var(--text)]">Settings</h1>
      <Icon name="chevron-right" size={15} className="text-[var(--grey-light)]" />
      <span className="text-[14px] font-medium text-[var(--text-3)]">{label}</span>
      {hint && <span className="ml-2 text-[12px] text-[var(--text-faint2)] hidden md:block">{hint}</span>}
      {right && <div className="ml-auto">{right}</div>}
    </div>
  );
}

function SAScopeNote({ scope }) {
  return (
    <div className="flex items-center gap-2 text-[12px] text-[var(--text-faint)] bg-[var(--surface-inset)] border border-[#e8ddc2] rounded-lg px-3 py-2">
      <Icon name={scope === "workspace" ? "building" : "grid"} size={14} className="shrink-0" />
      {scope === "workspace"
        ? <span>Editing the <b>workspace</b> library. Changes apply to every project unless a project overrides them.</span>
        : <span>Editing this <b>project</b>. Workspace items are inherited and read-only here, except their template.</span>}
    </div>
  );
}

// ============================================================
//  Markdown / template preview (reuses .ctx-doc styles)
// ============================================================
function renderTemplatePreview(text) {
  const lines = (text || "").split("\n");
  const out = [];
  let list = null;
  const flush = () => { if (list) { out.push(React.createElement("ul", { key: "u" + out.length }, list)); list = null; } };
  const fmt = (s) => s.replace(/\{[^}]+\}/g, (m) => `\u2039${m.slice(1, -1)}\u203a`)
    .split(/(\*\*[^*]+\*\*)/g).map((part, i) => part.startsWith("**") ? <strong key={i}>{part.slice(2, -2)}</strong> : part);
  lines.forEach((ln, i) => {
    const t = ln.trim();
    if (/^###\s/.test(t)) { flush(); out.push(<h3 key={i}>{fmt(t.replace(/^###\s/, ""))}</h3>); }
    else if (/^##\s/.test(t)) { flush(); out.push(<h2 key={i}>{fmt(t.replace(/^##\s/, ""))}</h2>); }
    else if (/^#\s/.test(t)) { flush(); out.push(<h1 key={i}>{fmt(t.replace(/^#\s/, ""))}</h1>); }
    else if (/^[-*]\s/.test(t)) { (list = list || []).push(<li key={i}>{fmt(t.replace(/^[-*]\s\[.\]\s?|^[-*]\s/, ""))}</li>); }
    else if (t === "") { flush(); }
    else { flush(); out.push(<p key={i}>{fmt(t)}</p>); }
  });
  flush();
  return out;
}

function PreviewPane({ children, label = "Live preview" }) {
  return (
    <div className="rounded-xl border border-[var(--border)] bg-[var(--surface-2)] overflow-hidden">
      <div className="px-3 py-2 border-b border-[var(--border-soft)] bg-white/60 flex items-center gap-1.5 text-[11px] font-medium text-[var(--text-faint2)] uppercase tracking-[0.05em]"><Icon name="eye" size={12} /> {label}</div>
      <div className="p-4">{children}</div>
    </div>
  );
}

// ============================================================
//  MAIN ROUTER for the Skills / Agents sections
// ============================================================
function SkillsMain({ ws }) {
  const skill = ws.skills.find((s) => s.id === ws.skillSel);
  if (!skill || (ws.scope === "workspace" && skill.lineage === "project")) return <SkillsListView ws={ws} />;
  if (ws.saMode === "build") return <GuidedBuilder ws={ws} kind="skill" item={skill} onBack={() => ws.setSaMode("detail")} />;
  return <SkillDetailView ws={ws} skill={skill} />;
}

function AgentsMain({ ws }) {
  const agent = ws.agents.find((a) => a.id === ws.agentSel);
  if (!agent || (ws.scope === "workspace" && agent.lineage === "project")) return <AgentsListView ws={ws} />;
  if (ws.saMode === "build") return <GuidedBuilder ws={ws} kind="agent" item={agent} onBack={() => ws.setSaMode("detail")} />;
  return <AgentDetailView ws={ws} agent={agent} />;
}

// ============================================================
//  SCREEN 1 — Skills list
// ============================================================
function saListGroups(list, scope) {
  return scope === "workspace"
    ? [["Built-in", "ship with Teklens", list.filter((x) => x.lineage === "built-in")],
       ["Custom", "defined for this workspace", list.filter((x) => x.lineage === "custom")]]
    : [["Built-in", "ship with Teklens", list.filter((x) => x.lineage === "built-in")],
       ["Inherited", "from the workspace", list.filter((x) => x.lineage === "custom")],
       ["Project", "specific to this project", list.filter((x) => x.lineage === "project")]];
}

function SAGroupHeading({ label }) {
  return (
    <div className="mb-3 mt-1">
      <span className="text-[11px] font-semibold uppercase tracking-[0.07em] text-[var(--text-faint2)]">{label}</span>
    </div>
  );
}

function SkillsListView({ ws }) {
  const { scope } = ws;
  const list = ws.skills.filter((s) => scope === "workspace" ? s.lineage !== "project" : true);
  const groups = saListGroups(list, scope).filter(([, , rows]) => rows.length);
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[860px] mx-auto px-7 py-7 flex flex-col gap-5">
          <div className="flex items-start justify-between gap-4">
            <div>
              <h2 className="text-[22px] font-semibold tracking-tight text-[var(--text)]">Skills</h2>
              <p className="text-[13.5px] text-[var(--text-muted)] mt-1 max-w-[640px] leading-relaxed">A skill is a capability used to perform tasks. Each skill creates an output — markdown, HTML, or a fixed, configured widget. Skills may use sub-agents to perform their job.</p>
            </div>
            <button onClick={ws.addSkill} className="shrink-0 flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="plus" size={15} /> New Skill</button>
          </div>
          <SAScopeNote scope={scope} />
          {list.length === 0
            ? <SAEmptyState title="No skills in this project yet" body="Create a project-specific skill, or switch to the workspace to manage shared skills." onAction={ws.addSkill} actionLabel="New Skill" />
            : groups.map(([label, hint, rows], i) => (
                <div key={label} className={i ? "mt-3" : ""}>
                  <SAGroupHeading label={label} />
                  <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
                    {rows.map((s) => <SkillCard key={s.id} s={s} scope={scope} ws={ws} />)}
                  </div>
                </div>
              ))}
        </div>
      </div>
    </div>
  );
}

function SkillCard({ s, scope, ws }) {
  const lock = saLockState(s, scope);
  const update = (patch) => ws.setSkills((xs) => xs.map((x) => x.id === s.id ? { ...x, ...patch } : x));
  const open = () => { ws.setSkillSel(s.id); ws.setAgentSel(null); ws.setSaMode("detail"); };
  const act = skillActivation(s);
  const dim = act.mode === "toggle" ? !s.enabled : act.mode === "gated" ? !act.active : false;
  return (
    <div className={`group rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex items-start gap-3.5 hover:border-[var(--accent)]/40 transition-colors ${dim ? "opacity-70" : ""}`}>
      <button onClick={open} className="flex items-start gap-3.5 text-left min-w-0 flex-1">
        <span className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name={SKILL_ICON} size={18} /></span>
        <div className="min-w-0 flex-1">
          <div className="text-[15px] font-semibold text-[var(--text)]">{s.name}</div>
          <div className="mt-1.5"><OutputBadge output={s.output} /></div>
          <p className="text-[12.5px] text-[var(--text-muted)] mt-1.5 leading-snug max-w-[560px]">{s.desc}</p>
          {s.createsArtifact && (
            <div className="mt-2.5">
              <span className="inline-flex items-center gap-1.5 rounded-lg border border-[color-mix(in_srgb,var(--violet-ink)_28%,transparent)] bg-[var(--violet-tint)] pl-1.5 pr-2.5 py-1 text-[11.5px] font-medium text-[var(--violet-ink)]">
                <Icon name="file" size={13} /> {s.artifactType}
              </span>
            </div>
          )}
        </div>
      </button>
      <div className="flex flex-col items-end gap-2.5 shrink-0">
        <ActivationControl skill={s} onToggle={(v) => update({ enabled: v })} compact />
        {skillEditable(s, scope)
          ? <button onClick={open} title="Modify" className="px-2 py-1 rounded-md text-[12px] font-medium text-[var(--text-3)] hover:bg-[var(--surface)] flex items-center gap-1">
              <Icon name="edit" size={13} /> Modify
            </button>
          : <span className="inline-flex items-center gap-1 text-[11px] text-[var(--text-faint2)] px-1"><Icon name="shield" size={11} /> Read-only</span>}
      </div>
    </div>
  );
}

function SAEmptyState({ title, body, onAction, actionLabel }) {
  return (
    <div className="rounded-2xl border border-dashed border-[var(--line)] bg-white/50 px-8 py-14 flex flex-col items-center text-center">
      <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--surface-inset)] text-[var(--bar)] mb-4"><Icon name="puzzle" size={24} /></span>
      <div className="text-[15px] font-semibold text-[var(--text)]">{title}</div>
      <p className="text-[13px] text-[var(--text-faint)] mt-1.5 max-w-[380px] leading-relaxed">{body}</p>
      {onAction && <button onClick={onAction} className="mt-5 flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110"><Icon name="plus" size={15} /> {actionLabel}</button>}
    </div>
  );
}

// ============================================================
//  Detail / inspector — uniform read view for built-in AND custom.
//  The guided builder opens only on Modify / Override / New.
// ============================================================
function DetailSection({ label, locked, children }) {
  return (
    <div>
      <div className="flex items-center gap-2 mb-2">
        <span className="text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--text-faint2)]">{label}</span>
        {locked && <span className="inline-flex items-center gap-1 text-[10px] text-[var(--bar)]"><Icon name="shield" size={10} /> locked</span>}
      </div>
      {children}
    </div>
  );
}

function DetailBackBar({ onBack, kindPlural, right }) {
  return (
    <div className="flex items-center gap-3 -mb-1">
      <button onClick={onBack} className="flex items-center gap-1.5 text-[13px] text-[var(--text-muted)] hover:text-[var(--text)] w-fit"><Icon name="chevron-left" size={15} /> All {kindPlural}</button>
      {right && <div className="ml-auto flex items-center gap-2">{right}</div>}
    </div>
  );
}

function SkillAgentChips({ ws, ids }) {
  if (!ids || !ids.length) return null;
  return (
    <div className="flex flex-wrap gap-1.5">
      {ids.map((aid) => {
        const a = ws.agents.find((x) => x.id === aid);
        return (
          <button key={aid} onClick={() => { ws.setAgentSel(aid); ws.setSkillSel(null); ws.setSaMode("detail"); ws.gotoSaSection && ws.gotoSaSection("agents"); }}
            className="inline-flex items-center gap-1.5 text-[12.5px] text-[var(--text-2)] bg-[var(--card)] border border-[var(--border)] rounded-lg px-2.5 py-1.5 hover:border-[var(--accent)]/50 hover:text-[var(--accent)] transition-colors">
            <Icon name={AGENT_ICON} size={13} className="text-[var(--text-faint2)]" /> {a ? a.name : aid}
            <Icon name="chevron-right" size={12} className="text-[var(--text-faint2)]" />
          </button>
        );
      })}
    </div>
  );
}

function AvailabilityNote({ text }) {
  if (!text) return null;
  return (
    <div className="flex items-center gap-2 text-[12.5px] text-[var(--text-muted)]">
      <Icon name="link" size={14} className="text-[var(--text-faint2)] shrink-0" /> {text}
    </div>
  );
}

function SkillOutputPreview({ skill }) {
  if (skill.output === "none")
    return <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 flex items-center gap-3"><span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name="message" size={16} /></span><div><div className="text-[13px] font-medium text-[var(--text)]">Answers in chat</div><div className="text-[11.5px] text-[var(--text-faint2)]">No artefact — the skill replies inline from what it finds.</div></div></div>;
  if (skill.output === "widget") {
    if (skill.comp && typeof window !== "undefined" && window[skill.comp])
      return <PreviewPane label="Widget · live preview">{React.createElement(window[skill.comp])}</PreviewPane>;
    if (skill.templatableBody)
      return (
        <div className="flex flex-col gap-3">
          <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 flex items-center gap-3"><span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name="grid" size={16} /></span><div><div className="text-[13px] font-medium text-[var(--text)]">Interactive ticket widget</div><div className="text-[11.5px] text-[var(--text-faint2)]">Rendered by <code className="font-mono text-[11px]">{skill.widgetTool}</code>. The form chrome is fixed — the <b>ticket body</b> below follows an editable template.</div></div></div>
          <PreviewPane label="Ticket body · example"><div className="ctx-doc">{renderTemplatePreview(skill.bodyExample || skill.bodyTemplate)}</div></PreviewPane>
        </div>
      );
    if (skill.bespoke)
      return <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 flex items-center gap-3"><span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name="grid" size={16} /></span><div><div className="text-[13px] font-medium text-[var(--text)]">Built-in widget</div><div className="text-[11.5px] text-[var(--text-faint2)]">Rendered by <code className="font-mono text-[11px]">{skill.widgetTool}</code> — bespoke, not templatable.</div></div></div>;
    let data = null, ok = true; try { data = JSON.parse(skill.sample || "{}"); } catch (e) { ok = false; }
    return <PreviewPane label="Widget · sample data"><WidgetSamplePreview data={data} ok={ok} /></PreviewPane>;
  }
  if (skill.output === "html")
    return (skill.useTemplate && (skill.example || skill.template))
      ? <PreviewPane label="Example output"><div className="ctx-doc" dangerouslySetInnerHTML={{ __html: skill.example || skill.template || "" }} /></PreviewPane>
      : <FreeFormOutputNote kind="HTML" />;
  return (skill.useTemplate && (skill.example || skill.template))
    ? <PreviewPane label="Example output"><div className="ctx-doc">{renderTemplatePreview(skill.example || skill.template)}</div></PreviewPane>
    : <FreeFormOutputNote kind="Markdown" />;
}

function FreeFormOutputNote({ kind }) {
  return <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3 flex items-center gap-3"><span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name="message" size={16} /></span><div><div className="text-[13px] font-medium text-[var(--text)]">Free-form {kind}</div><div className="text-[11.5px] text-[var(--text-faint2)]">No fixed template — the skill writes its {kind} answer inline each run.</div></div></div>;
}

function SkillDetailView({ ws, skill }) {
  const lock = saLockState(skill, ws.scope);
  const lineage = saEffLineage(skill, ws.scope);
  const update = (patch) => ws.setSkills((xs) => xs.map((x) => x.id === skill.id ? { ...x, ...patch } : x));
  const onDelete = () => { ws.setSkills((xs) => xs.filter((x) => x.id !== skill.id)); ws.setSkillSel(null); };
  const build = () => ws.setSaMode("build");
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[820px] mx-auto px-7 py-7 flex flex-col gap-6">
          <DetailBackBar onBack={() => ws.setSkillSel(null)} kindPlural="skills" right={
            <>
              <ActivationControl skill={skill} onToggle={(v) => update({ enabled: v })} />
              {skillEditable(skill, ws.scope)
                ? <button onClick={build} className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="edit" size={15} /> Modify skill</button>
                : <span className="flex items-center gap-1.5 text-[12px] text-[var(--text-faint)] px-2"><Icon name="shield" size={14} /> Read-only built-in</span>}
            </>} />

          <div className="flex items-start gap-3.5">
            <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--card)] border border-[var(--border)] text-[var(--text-3)] shrink-0"><Icon name={SKILL_ICON} size={22} /></span>
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 flex-wrap">
                <span className="text-[21px] font-semibold tracking-tight text-[var(--text)]">{skill.name}</span>
                <OutputBadge output={skill.output} />
                <LineageBadge lineage={lineage} overridden={skill.templateOverridden} />
              </div>
            </div>
          </div>

          {lock.locked && (
            <div className="flex items-start gap-2.5 text-[12.5px] text-[var(--text-faint)] bg-[var(--surface-inset)] border border-[#e8ddc2] rounded-xl px-4 py-3">
              <Icon name="shield" size={16} className="shrink-0 mt-px" />
              <span>This skill is {lineage === "inherited" ? "inherited from the workspace" : "built-in"} and read-only.{((skill.output === "markdown" || skill.output === "html") && skill.useTemplate) ? <> You can modify its <b>template</b> for {ws.scope === "project" ? "this project" : "your workspace"}.</> : skill.templatableBody ? <> You can modify the <b>ticket body template</b> for {ws.scope === "project" ? "this project" : "your workspace"}.</> : " Its description, instructions and output are fixed."}</span>
            </div>
          )}

          <DetailSection label="When it runs">
            <p className="text-[13.5px] text-[var(--text-3)] leading-relaxed">{skill.desc || <span className="text-[var(--bar)]">No description yet.</span>}</p>
          </DetailSection>

          <DetailSection label="Instructions" locked={lock.locked}>
            <div className="ctx-doc rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3">{skill.instructions ? renderTemplatePreview(skill.instructions) : <span className="text-[var(--bar)] text-[13px]">No instructions yet.</span>}</div>
          </DetailSection>

          {skill.agents && skill.agents.length > 0 && false && (
            <DetailSection label="Agents used">
              <SkillAgentChips ws={ws} ids={skill.agents} />
            </DetailSection>
          )}

          <DetailSection label="Output">
            {skill.createsArtifact && (
              <div className="mb-3 flex items-center gap-3 rounded-xl border border-[color-mix(in_srgb,var(--violet-ink)_28%,transparent)] bg-[var(--violet-tint)] px-4 py-3">
                <span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--card)] text-[var(--violet-ink)]"><Icon name="file" size={16} /></span>
                <div className="min-w-0 flex-1">
                  <div className="text-[11px] font-semibold uppercase tracking-[0.05em] text-[var(--violet-ink)]">Creates artifact</div>
                  {lock.locked
                    ? <div className="text-[13.5px] font-medium text-[var(--text)] mt-0.5">{skill.artifactType}</div>
                    : <input value={skill.artifactType || ""} onChange={(e) => update({ artifactType: e.target.value })}
                        className="mt-0.5 w-full bg-transparent text-[13.5px] font-medium text-[var(--text)] outline-none border-b border-transparent hover:border-[var(--border)] focus:border-[var(--violet-ink)] transition-colors" />}
                  <div className="text-[11.5px] text-[var(--text-faint2)] mt-0.5">Stored in Teklens · the title is generated per run from the output.</div>
                </div>
              </div>
            )}
            <SkillOutputPreview skill={skill} />
          </DetailSection>

          {skill.availability && (
            <DetailSection label="Availability">
              <AvailabilityNote text={skill.availability} />
            </DetailSection>
          )}

          {!lock.locked && (
            <div className="flex items-center gap-2 pt-1 border-t border-[var(--border-soft)] mt-1">
              <button onClick={() => ws.duplicateSkill(skill.id)} className="flex items-center gap-1.5 px-3 py-2 mt-3 rounded-lg text-[13px] text-[var(--text-2)] bg-[var(--card)] border border-[var(--border)] hover:bg-[var(--surface-2)]"><Icon name="copy" size={14} /> Duplicate</button>
              <button onClick={onDelete} className="flex items-center gap-1.5 px-3 py-2 mt-3 rounded-lg text-[13px] text-[var(--amber-ink)] hover:bg-[var(--amber-ink)]/[0.07]"><Icon name="trash" size={14} /> Delete</button>
            </div>
          )}
          {lock.locked && (
            <div className="border-t border-[var(--border-soft)] mt-1 pt-3">
              <button onClick={() => ws.duplicateSkill(skill.id)} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--text-2)] bg-[var(--card)] border border-[var(--border)] hover:bg-[var(--surface-2)]"><Icon name="copy" size={14} /> Duplicate as editable copy</button>
            </div>
          )}
          <div className="h-2" />
        </div>
      </div>
    </div>
  );
}

function AgentDetailView({ ws, agent }) {
  const lock = saLockState(agent, ws.scope);
  const lineage = saEffLineage(agent, ws.scope);
  const update = (patch) => ws.setAgents((xs) => xs.map((x) => x.id === agent.id ? { ...x, ...patch } : x));
  const onDelete = () => { ws.setAgents((xs) => xs.filter((x) => x.id !== agent.id)); ws.setAgentSel(null); };
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[820px] mx-auto px-7 py-7 flex flex-col gap-6">
          <DetailBackBar onBack={() => ws.setAgentSel(null)} kindPlural="sub-agents" right={
            !lock.locked
              ? <button onClick={() => ws.setSaMode("build")} className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="edit" size={15} /> Modify sub-agent</button>
              : <button onClick={() => ws.duplicateAgent(agent.id)} className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-[var(--text-2)] text-[13px] font-medium hover:bg-[var(--surface-2)]"><Icon name="copy" size={15} /> Duplicate to edit</button>} />

          <div className="flex items-start gap-3.5">
            <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--card)] border border-[var(--border)] text-[var(--text-3)] shrink-0"><Icon name={AGENT_ICON} size={22} /></span>
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 flex-wrap">
                <span className="text-[21px] font-semibold tracking-tight text-[var(--text)]">{agent.name}</span>
                <LineageBadge lineage={lineage} />
                <span className="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-[0.04em] px-1.5 py-0.5 rounded text-[var(--text-muted)] bg-[var(--surface-inset)]"><Icon name="robot" size={9} /> Sub-Agent</span>
              </div>
            </div>
          </div>

          {lock.locked && (
            <div className="flex items-start gap-2.5 text-[12.5px] text-[var(--text-faint)] bg-[var(--surface-inset)] border border-[#e8ddc2] rounded-xl px-4 py-3">
              <Icon name="shield" size={16} className="shrink-0 mt-px" />
              <span>This sub-agent is {lineage === "inherited" ? "inherited from the workspace" : "built-in"} and read-only here. Duplicate it to make an editable copy.</span>
            </div>
          )}

          <DetailSection label="When it's picked">
            <p className="text-[13.5px] text-[var(--text-3)] leading-relaxed">{agent.desc || <span className="text-[var(--bar)]">No description yet.</span>}</p>
          </DetailSection>

          <DetailSection label="Instructions / prompt" locked={lock.locked}>
            <div className="ctx-doc rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-3">{agent.instructions ? renderTemplatePreview(agent.instructions) : <span className="text-[var(--bar)] text-[13px]">No prompt yet.</span>}</div>
          </DetailSection>

          <DetailSection label="Model">
            <div className="max-w-[420px]"><AgentModelPicker value={agent.model} onChange={(v) => ws.setAgents((xs) => xs.map((x) => x.id === agent.id ? { ...x, model: v } : x))} /></div>
            <p className="text-[11.5px] text-[var(--text-faint2)] mt-1.5">Select the model for this agent. Either a fixed model, or the same model the user selects in the conversation.<br />Default is the user-selected model.</p>
          </DetailSection>

          {!lock.locked && (
            <div className="flex items-center gap-2 border-t border-[var(--border-soft)] mt-1 pt-3">
              <button onClick={() => ws.duplicateAgent(agent.id)} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--text-2)] bg-[var(--card)] border border-[var(--border)] hover:bg-[var(--surface-2)]"><Icon name="copy" size={14} /> Duplicate</button>
              <button onClick={onDelete} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--amber-ink)] hover:bg-[var(--amber-ink)]/[0.07]"><Icon name="trash" size={14} /> Delete</button>
            </div>
          )}
          <div className="h-2" />
        </div>
      </div>
    </div>
  );
}

// ============================================================
//  SCREEN 2 — Skill editor
// ============================================================
function SkillEditor({ ws, skill }) {
  const update = (patch) => ws.setSkills((xs) => xs.map((x) => x.id === skill.id ? { ...x, ...patch } : x));
  const onDelete = () => { ws.setSkills((xs) => xs.filter((x) => x.id !== skill.id)); ws.setSkillSel(null); };
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <SAHeader label="Skills" hint="Author the routing, instructions, agents, and output." />
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[820px] mx-auto px-7 py-7 flex flex-col gap-7">
          <button onClick={() => ws.setSkillSel(null)} className="flex items-center gap-1.5 text-[13px] text-[var(--text-muted)] hover:text-[var(--text)] -mb-2 w-fit"><Icon name="chevron-left" size={15} /> All skills</button>

          {/* identity */}
          <div className="flex items-start gap-3.5">
            <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--card)] border border-[var(--border)] text-[var(--text-3)] shrink-0"><Icon name={SKILL_ICON} size={22} /></span>
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2">
                <input value={skill.name} onChange={(e) => update({ name: e.target.value })}
                  className="text-[20px] font-semibold text-[var(--text)] bg-transparent outline-none border-b border-transparent focus:border-[var(--accent)] min-w-0" />
                <LineageBadge lineage={saEffLineage(skill, ws.scope)} overridden={skill.templateOverridden} />
              </div>
            </div>
          </div>

          {/* basics: routing description */}
          <SAField label="Description" hint="this is matched against user requests to decide WHEN the skill runs">
            <textarea value={skill.desc} onChange={(e) => update({ desc: e.target.value })} rows={2}
              placeholder="e.g. Draft a Jira-ready user story from a request, grounded in real code…"
              className={SETTINGS_INPUT + " resize-none"} />
            <div className="flex items-start gap-1.5 mt-1.5 text-[11.5px] text-[var(--text-faint2)]">
              <Icon name="info" size={13} className="shrink-0 mt-px text-[var(--accent)]" />
              <span>Write it like a trigger: name the request shapes this skill should win. Teklens routes each message to the best-matching description.</span>
            </div>
          </SAField>

          {/* instructions */}
          <SAField label="Instructions" hint="how the skill works (markdown)">
            <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] overflow-hidden">
              <div className="px-3 py-2 border-b border-[var(--border-soft)] bg-[var(--surface-2)] flex items-center gap-2 text-[11.5px] text-[var(--text-faint2)]"><Icon name="file" size={12} /> instructions.md</div>
              <textarea value={skill.instructions} onChange={(e) => update({ instructions: e.target.value })} rows={7}
                placeholder="Describe the steps the skill follows, what good output looks like, and edge cases…"
                className="w-full resize-none px-3.5 py-3 text-[12.5px] font-mono text-[var(--text)] bg-transparent outline-none leading-relaxed placeholder:text-[var(--bar)]" />
            </div>
          </SAField>

          {/* agents */}
          <SAField label="Agents used" hint="reusable investigators this skill can call (0..n)">
            <AgentMultiSelect ws={ws} value={skill.agents || []} onChange={(agents) => update({ agents })} />
          </SAField>

          {/* output */}
          <SAField label="Output" hint="what the user receives when the skill runs">
            <OutputSelector value={skill.output} onChange={(output) => update({ output })} />
            <div className="mt-3">
              <OutputSubEditor skill={skill} update={update} />
            </div>
          </SAField>

          <div className="flex items-center gap-2 pt-1">
            <button className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="check" size={15} /> Save skill</button>
            <button onClick={onDelete} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--amber-ink)] hover:bg-[var(--amber-ink)]/[0.07]"><Icon name="trash" size={14} /> Delete</button>
          </div>
          <div className="h-2" />
        </div>
      </div>
    </div>
  );
}

function OutputSelector({ value, onChange }) {
  return (
    <div className="inline-flex items-center bg-[var(--border-soft)] rounded-lg p-0.5 flex-wrap">
      {Object.entries(SA_OUTPUT).map(([k, o]) => (
        <button key={k} onClick={() => onChange(k)} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[12.5px] font-medium ${value === k ? "bg-[var(--card)] shadow-sm text-[var(--text)]" : "text-[var(--text-muted)]"}`}>
          <Icon name={o.icon} size={13} /> {o.label}
        </button>
      ))}
    </div>
  );
}

function OutputSubEditor({ skill, update }) {
  if (skill.output === "none")
    return (
      <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex items-center gap-3">
        <span className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[#6b7280]"><Icon name="message" size={18} /></span>
        <div className="min-w-0"><div className="text-[13.5px] font-medium text-[var(--text)]">Replies in chat</div><div className="text-[11.5px] text-[var(--text-faint2)]">No artefact is produced — the skill answers inline as normal chat markdown.</div></div>
      </div>
    );
  if (skill.output === "widget") return <WidgetEditor skill={skill} update={update} />;
  // markdown / html
  return <DocOutputEditor skill={skill} update={update} />;
}

function DocOutputEditor({ skill, update }) {
  const lang = skill.output;
  const ext = lang === "html" ? "html" : "md";
  return (
    <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex flex-col gap-3.5">
      <div className="flex items-center gap-3">
        <div className="flex-1 min-w-0">
          <div className="text-[13px] font-medium text-[var(--text)]">Use a template</div>
          <div className="text-[11.5px] text-[var(--text-faint2)]">Pin a fixed structure with content-descriptions, so output always follows your story.</div>
        </div>
        <SettingsToggle on={!!skill.useTemplate} onChange={(v) => update({ useTemplate: v })} />
      </div>
      {skill.useTemplate && (
        <div className="border-t border-[var(--border-soft)] pt-3.5">
          <div className="flex items-center justify-between mb-2">
            <span className="text-[12px] font-medium text-[var(--text-muted)]">Template structure &amp; content-descriptions</span>
            <button onClick={() => update({ template: skill.template })} className="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[12px] font-medium text-[var(--text-muted)] bg-[var(--surface)] border border-[var(--border)] hover:bg-[var(--card)]"><Icon name="sparkles" size={13} /> Propose template</button>
          </div>
          <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
            <div className="rounded-lg border border-[var(--border)] overflow-hidden">
              <div className="px-3 py-2 border-b border-[var(--border-soft)] bg-[var(--surface-2)] flex items-center gap-2 text-[11.5px] text-[var(--text-faint2)]"><Icon name={lang === "html" ? "code" : "file"} size={12} /> template.{ext}</div>
              <textarea value={skill.template || ""} onChange={(e) => update({ template: e.target.value })} rows={10}
                placeholder={"# {Title}\n\n## Section\n{describe what goes here}"}
                className="w-full resize-none px-3 py-2.5 text-[12px] font-mono text-[var(--text)] bg-transparent outline-none leading-relaxed placeholder:text-[var(--bar)]" />
            </div>
            <PreviewPane>
              <div className="ctx-doc">{renderTemplatePreview(skill.template)}</div>
            </PreviewPane>
          </div>
          <p className="text-[11px] text-[var(--text-faint2)] mt-2">Use <code className="font-mono">{"{curly}"}</code> for content-descriptions — the skill fills them per run; everything outside stays fixed.</p>
        </div>
      )}
    </div>
  );
}

function WidgetEditor({ skill, update }) {
  const [tab, setTab] = useState("preview");
  let sampleObj = null, schemaOk = true;
  try { sampleObj = JSON.parse(skill.sample || "{}"); } catch (e) { schemaOk = false; }
  const tabs = [["preview", "Preview", "eye"], ["html", "Visual template", "code"], ["schema", "Data structure", "json"], ["sample", "Sample data", "entity"]];
  return (
    <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex flex-col gap-3.5">
      <div className="flex items-center gap-2">
        <span className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-2)] text-[var(--text-muted)]"><Icon name="grid" size={17} /></span>
        <div className="min-w-0 flex-1"><div className="text-[13px] font-medium text-[var(--text)]">Widget output</div><div className="text-[11.5px] text-[var(--text-faint2)]">An HTML visual template bound to a data structure, rendered with sample data.</div></div>
      </div>
      <div className="border-t border-[var(--border-soft)] pt-3.5 flex flex-col gap-3">
        <div className="inline-flex items-center bg-[var(--border-soft)] rounded-lg p-0.5 w-fit max-w-full overflow-x-auto">
          {tabs.map(([k, lbl, ic]) => (
            <button key={k} onClick={() => setTab(k)} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-[12px] font-medium whitespace-nowrap ${tab === k ? "bg-[var(--card)] shadow-sm text-[var(--text)]" : "text-[var(--text-muted)]"}`}><Icon name={ic} size={12} /> {lbl}</button>
          ))}
        </div>
        {tab === "preview" && <PreviewPane label="Live preview">{skill.comp && typeof window !== "undefined" && window[skill.comp] ? React.createElement(window[skill.comp]) : <WidgetSamplePreview data={sampleObj} ok={schemaOk} />}</PreviewPane>}
        {tab === "html" && <WidgetCode value={skill.widgetHtml} onChange={(v) => update({ widgetHtml: v })} file="widget.html" rows={16} />}
        {tab === "schema" && <WidgetCode value={skill.schema} onChange={(v) => update({ schema: v })} file="schema.json" rows={16} />}
        {tab === "sample" && <WidgetCode value={skill.sample} onChange={(v) => update({ sample: v })} file="sample.json" valid={schemaOk} rows={16} />}
      </div>
    </div>
  );
}

function WidgetCode({ value, onChange, file, valid = true, rows = 11 }) {
  return (
    <div className={`rounded-lg border overflow-hidden ${valid ? "border-[var(--border)]" : "border-[#e8b4a0]"}`}>
      <div className="px-3 py-2 border-b border-[var(--border-soft)] bg-[var(--surface-2)] flex items-center gap-2 text-[11.5px] text-[var(--text-faint2)]">
        <Icon name="code" size={12} /> {file}
        {!valid && <span className="ml-auto text-[var(--amber-ink)] flex items-center gap-1"><Icon name="info" size={11} /> invalid JSON</span>}
      </div>
      <textarea value={value || ""} onChange={(e) => onChange(e.target.value)} rows={rows}
        className="w-full resize-none px-3 py-2.5 text-[12px] font-mono text-[var(--text)] bg-transparent outline-none leading-relaxed placeholder:text-[var(--bar)]" />
    </div>
  );
}

function WidgetSamplePreview({ data, ok }) {
  if (!ok) return <div className="text-[12px] text-[var(--amber-ink)] flex items-center gap-1.5"><Icon name="info" size={14} /> Fix the sample JSON to see a preview.</div>;
  if (!data) return <div className="text-[12px] text-[var(--text-faint2)]">No sample data.</div>;
  const entries = Object.entries(data);
  return (
    <div className="flex flex-col gap-2.5">
      {entries.map(([k, v]) => (
        <div key={k}>
          <div className="text-[10.5px] font-semibold uppercase tracking-[0.05em] text-[var(--text-faint2)] mb-1">{k}</div>
          {Array.isArray(v)
            ? <div className="flex flex-col gap-1.5">{v.map((row, i) => (
                <div key={i} className="rounded-lg border border-[var(--border)] bg-[var(--card)] px-3 py-2 flex flex-wrap gap-x-3 gap-y-0.5">
                  {typeof row === "object" ? Object.entries(row).map(([rk, rv]) => (
                    <span key={rk} className="text-[12px]"><span className="text-[var(--text-faint2)]">{rk}:</span> <span className="text-[var(--text)] font-medium">{String(rv)}</span></span>
                  )) : <span className="text-[12px] text-[var(--text)]">{String(row)}</span>}
                </div>
              ))}</div>
            : <div className="text-[13px] text-[var(--text)] font-medium">{String(v)}</div>}
        </div>
      ))}
    </div>
  );
}

function AgentMultiSelect({ ws, value, onChange }) {
  const [open, setOpen] = useState(false);
  const scopeAgents = ws.agents.filter((a) => ws.scope === "workspace" ? a.lineage !== "project" : true);
  const candidates = scopeAgents.filter((a) => !value.includes(a.id));
  const remove = (id) => onChange(value.filter((x) => x !== id));
  const add = (id) => { onChange([...value, id]); setOpen(false); };
  return (
    <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-3 flex flex-wrap items-center gap-1.5">
      {value.length === 0 && <span className="text-[12px] text-[var(--bar)] px-1">No sub-agents — the skill works from chat context alone.</span>}
      {value.map((id) => {
        const a = ws.agents.find((x) => x.id === id);
        return (
          <span key={id} className="inline-flex items-center gap-1.5 pl-2 pr-1 py-1 rounded-md border border-[var(--border)] bg-[var(--surface-2)] text-[12.5px] text-[var(--text)]">
            <Icon name={AGENT_ICON} size={12} className="text-[var(--text-faint2)]" /> {a ? a.name : id}
            <button onClick={() => remove(id)} className="p-0.5 rounded hover:bg-[var(--hover)] text-[var(--text-faint2)]"><Icon name="x" size={11} /></button>
          </span>
        );
      })}
      <div className="relative">
        <button onClick={() => setOpen((o) => !o)} className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-dashed border-[var(--line)] text-[12px] text-[var(--text-muted)] hover:border-[var(--accent)]/50 hover:text-[var(--accent)]"><Icon name="plus" size={12} /> Add sub-agent</button>
        {open && (
          <>
            <div className="fixed inset-0 z-30" onClick={() => setOpen(false)} />
            <div className="absolute left-0 top-full mt-1 w-[260px] bg-[var(--card)] rounded-lg border border-[var(--border)] shadow-xl z-40 py-1">
              {candidates.map((a) => (
                <button key={a.id} onClick={() => add(a.id)} className="w-full flex items-start gap-2 px-3 py-2 hover:bg-[var(--hover)] text-left">
                  <Icon name={AGENT_ICON} size={14} className="text-[var(--text-faint2)] mt-0.5 shrink-0" />
                  <span className="min-w-0"><span className="block text-[12.5px] text-[var(--text)] font-medium truncate">{a.name}</span><span className="block text-[11px] text-[var(--text-faint2)] truncate">{a.desc}</span></span>
                </button>
              ))}
              <div className="my-1 h-px bg-[var(--border-soft)]" />
              <button onClick={() => { setOpen(false); ws.addAgent(); }} className="w-full flex items-center gap-2 px-3 py-2 text-[12.5px] text-[var(--accent)] font-medium hover:bg-[var(--accent)]/[0.06] text-left"><Icon name="plus" size={13} /> Create new sub-agent…</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function SAField({ label, hint, children }) {
  return (
    <div>
      <div className="flex items-baseline gap-2 mb-2">
        <span className="text-[13px] font-semibold text-[var(--text)]">{label}</span>
        {hint && <span className="text-[11.5px] text-[var(--text-faint2)]">{hint}</span>}
      </div>
      {children}
    </div>
  );
}

// ============================================================
//  SCREEN 3 — Agent editor & list
// ============================================================
// Model selector identical to the conversation composer's, with a "Current Conversation model"
// default that always tracks whatever model is active in the conversation.
const AGENT_MODEL_DEFAULT = "Current Conversation model";
function AgentModelPicker({ value, onChange }) {
  const [open, setOpen] = useState(false);
  const list = (typeof MODELS_LIST !== "undefined" && MODELS_LIST) || [];
  const isDefault = !value || value === AGENT_MODEL_DEFAULT;
  const cur = list.find((m) => m.label === value);
  return (
    <div className="relative">
      <button onClick={() => setOpen((o) => !o)} className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-[var(--border)] bg-[var(--card)] hover:border-[var(--accent)]/40 text-left">
        <span className={`relative inline-flex items-center justify-center w-5 h-5 rounded shrink-0 ${isDefault ? "bg-[var(--accent)]/[0.1] text-[var(--accent)]" : "bg-[var(--surface)] text-[var(--text-2)]"}`}><Icon name={isDefault ? "sparkles" : (cur ? cur.provider : "model")} size={13} /></span>
        <span className="flex-1 text-[13px] text-[var(--text)] truncate">{isDefault ? AGENT_MODEL_DEFAULT : value}</span>
        <Icon name="chevron-down" size={14} className="text-[var(--text-faint2)]" />
      </button>
      {open && (
        <>
          <div className="fixed inset-0 z-30" onClick={() => setOpen(false)} />
          <div className="absolute left-0 right-0 top-full mt-1 bg-[var(--card)] rounded-lg border border-[var(--border)] shadow-xl z-40 py-1 max-h-[320px] overflow-y-auto">
            <button onClick={() => { onChange(AGENT_MODEL_DEFAULT); setOpen(false); }} className="w-full flex items-center gap-2.5 px-2.5 py-2 hover:bg-[var(--hover)] text-left border-b border-[var(--border-soft)]">
              <span className="relative inline-flex items-center justify-center w-6 h-6 rounded bg-[var(--accent)]/[0.1] text-[var(--accent)] shrink-0"><Icon name="sparkles" size={13} /></span>
              <span className="min-w-0 flex-1"><span className="block text-[12.5px] font-medium text-[var(--text)]">Current Conversation model</span><span className="block text-[11px] text-[var(--text-faint2)]">Always uses the model selected in the conversation · default</span></span>
              {isDefault && <Icon name="check" size={13} className="text-[var(--accent)] shrink-0" />}
            </button>
            {list.map((m) => (
              <button key={m.id} onClick={() => { onChange(m.label); setOpen(false); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 hover:bg-[var(--hover)] text-left">
                <span className="relative inline-flex items-center justify-center w-6 h-6 rounded bg-[var(--surface)] text-[var(--text-2)] shrink-0"><Icon name={m.provider} size={13} /></span>
                <span className="min-w-0 flex-1"><span className="block text-[12.5px] text-[var(--text)] truncate">{m.label}</span><span className="block text-[11px] text-[var(--text-faint2)] truncate">{m.sub}</span></span>
                {m.anonymous && <span className="text-[9px] font-semibold tracking-wider uppercase px-1 py-[1px] rounded bg-[var(--ink)] text-white shrink-0">Anon</span>}
                {m.label === value && <Icon name="check" size={13} className="text-[var(--accent)] shrink-0" />}
              </button>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

function AgentsListView({ ws }) {
  const { scope } = ws;
  const list = ws.agents.filter((a) => scope === "workspace" ? a.lineage !== "project" : true);
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[860px] mx-auto px-7 py-7 flex flex-col gap-5">
          <div className="flex items-start justify-between gap-4">
            <div>
              <h2 className="text-[22px] font-semibold tracking-tight text-[var(--text)]">Sub-Agents</h2>
              <p className="text-[13.5px] text-[var(--text-muted)] mt-1 max-w-[640px] leading-relaxed">Sub-agents investigate sources and return <b>data</b> to the thread — they don't render user-facing output. They run in their own context and return only the result back to the main agent. Skills pick sub-agents by their description.</p>
            </div>
            <button onClick={ws.addAgent} className="shrink-0 flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="plus" size={15} /> New Sub-Agent</button>
          </div>
          <SAScopeNote scope={scope} />
          {list.length === 0
            ? <SAEmptyState title="No sub-agents in this project yet" body="Create a project-specific sub-agent, or switch to the workspace to manage shared sub-agents." onAction={ws.addAgent} actionLabel="New Sub-Agent" />
            : saListGroups(list, scope).filter(([, , rows]) => rows.length).map(([label, hint, rows], gi) => (
                <div key={label} className={gi ? "mt-3" : ""}>
                  <SAGroupHeading label={label} />
                  <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
                    {rows.map((a) => {
                      const lock = saLockState(a, scope);
                      return (
                        <button key={a.id} onClick={() => { ws.setAgentSel(a.id); ws.setSkillSel(null); ws.setSaMode("detail"); }} className="text-left rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex items-start gap-3 hover:border-[var(--accent)]/40 transition-colors">
                          <span className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name={AGENT_ICON} size={18} /></span>
                          <div className="min-w-0 flex-1">
                            <div className="flex items-center gap-2 flex-wrap">
                              <span className="text-[14px] font-semibold text-[var(--text)] truncate">{a.name}</span>
                            </div>
                            <p className="text-[12px] text-[var(--text-muted)] mt-1 leading-snug">{a.desc}</p>
                            <span className="flex items-center gap-1.5 mt-2 text-[11px] text-[var(--text-faint2)]"><Icon name="model" size={11} /> {a.model}{lock.locked && <><span className="text-[var(--line)]">·</span><Icon name="shield" size={11} /> read-only</>}</span>
                          </div>
                          <Icon name="chevron-right" size={15} className="text-[var(--text-faint2)] shrink-0 self-center" />
                        </button>
                      );
                    })}
                  </div>
                </div>
              ))}
        </div>
      </div>
    </div>
  );
}

// ============================================================
//  Agents (the "Agent Team") — settings overview, card grid.
//  Matches the Skills / Sub-Agents list concept (neutral icon, not the
//  personalized avatar). Drilling into a card opens RoleDetailPage.
// ============================================================
function RolesListView({ ws }) {
  const list = ws.roles || [];
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[860px] mx-auto px-7 py-7 flex flex-col gap-5">
          <div className="flex items-start justify-between gap-4">
            <div>
              <h2 className="text-[22px] font-semibold tracking-tight text-[var(--text)]">Agents</h2>
              <p className="text-[13.5px] text-[var(--text-muted)] mt-1 max-w-[640px] leading-relaxed">An agent owns a job. Each one bundles the skills, sub-agents, and sources needed to do that job, and the assistant adapts to it.</p>
            </div>
            <button onClick={ws.addRole} className="shrink-0 flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="plus" size={15} /> New Agent</button>
          </div>
          <SAScopeNote scope={ws.scope} />
          {list.length === 0
            ? <SAEmptyState title="No agents yet" body="Create an agent to bundle the skills, sub-agents, and sources for a job." onAction={ws.addRole} actionLabel="New Agent" />
            : <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
                {list.map((r) => (
                  <button key={r.id} onClick={() => ws.setRoleSel(r.id)} className="text-left rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex items-start gap-3 hover:border-[var(--accent)]/40 transition-colors">
                    <span className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name="users" size={18} /></span>
                    <div className="min-w-0 flex-1">
                      <div className="flex items-center gap-2 flex-wrap">
                        <span className="text-[14px] font-semibold text-[var(--text)] truncate">{r.title}</span>
                        {r.name && <span className="text-[12px] text-[var(--text-muted)] truncate">{r.name}</span>}
                      </div>
                      <p className="text-[12px] text-[var(--text-muted)] mt-1 leading-snug line-clamp-2">{r.description}</p>
                      <span className="flex items-center gap-2 mt-2 text-[11px] text-[var(--text-faint2)] flex-wrap">
                        <span className="inline-flex items-center gap-1"><Icon name="puzzle" size={11} /> {(r.skillIds || []).length} {(r.skillIds || []).length === 1 ? "skill" : "skills"}</span>
                      </span>
                    </div>
                    <Icon name="chevron-right" size={15} className="text-[var(--text-faint2)] shrink-0 self-center" />
                  </button>
                ))}
              </div>}
        </div>
      </div>
    </div>
  );
}

// Built-in skills that ARE assignable to an agent. All other built-ins
// (Code Analysis, Find Experts, Jira & Confluence, Local Files) are always-on
// tools available to every agent and are never assigned to one.
const ASSIGNABLE_BUILTIN = ["story-drafting", "estimate-feature"];
const isAssignableSkill = (s) => s.lineage === "custom" || ASSIGNABLE_BUILTIN.includes(s.id);

// Capabilities editor: chip list of assigned skills (artefact-parent style) +
// an add menu. Assigning a skill owned by another agent prompts to re-assign,
// since a skill belongs to exactly one agent.
function RoleCapabilities({ ws, role }) {
  const [open, setOpen] = useState(false);
  const [pending, setPending] = useState(null); // skill awaiting re-assign confirm
  const assigned = role.skillIds || [];
  const eligible = ws.skills.filter(isAssignableSkill);
  const ownerOf = (sid) => ws.roles.find((r) => (r.skillIds || []).includes(sid));
  const skillById = (sid) => ws.skills.find((s) => s.id === sid);

  const assign = (sid) => {
    ws.setRoles((rs) => rs.map((r) => {
      if (r.id === role.id) return { ...r, skillIds: [...(r.skillIds || []), sid] };
      return (r.skillIds || []).includes(sid) ? { ...r, skillIds: r.skillIds.filter((i) => i !== sid) } : r;
    }));
  };
  const remove = (sid) => ws.setRoles((rs) => rs.map((r) => r.id === role.id ? { ...r, skillIds: (r.skillIds || []).filter((i) => i !== sid) } : r));
  const pick = (sid) => {
    setOpen(false);
    const owner = ownerOf(sid);
    if (owner && owner.id !== role.id) { setPending(sid); return; }
    assign(sid);
  };

  const candidates = eligible.filter((s) => !assigned.includes(s.id));
  const pendingSkill = pending ? skillById(pending) : null;
  const pendingOwner = pending ? ownerOf(pending) : null;

  return (
    <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 flex flex-col gap-3">
      <div className="flex flex-wrap items-center gap-1.5">
        {assigned.length === 0 && <span className="text-[12px] text-[var(--text-faint2)]">No skills assigned yet.</span>}
        {assigned.map((sid) => {
          const s = skillById(sid);
          if (!s) return null;
          return (
            <span key={sid} className="inline-flex items-center gap-1.5 pl-2 pr-1 py-1 rounded-md bg-[var(--surface-inset)] border border-[var(--border)] text-[12.5px] text-[var(--text)]">
              <Icon name={s.icon || "puzzle"} size={13} className="text-[var(--text-muted)]" />
              {s.name}
              <button onClick={() => remove(sid)} title="Remove" className="w-4 h-4 rounded flex items-center justify-center text-[var(--text-faint2)] hover:text-[var(--text)] hover:bg-[var(--hover)]"><Icon name="x" size={11} /></button>
            </span>
          );
        })}
        {candidates.length > 0 && (
          <div className="relative">
            <button onClick={() => setOpen((o) => !o)} className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-dashed border-[var(--line)] text-[12px] text-[var(--text-muted)] hover:text-[var(--text)] hover:border-[var(--text-faint2)]"><Icon name="plus" size={12} /> Add skill</button>
            {open && (
              <>
                <div className="fixed inset-0 z-20" onClick={() => setOpen(false)} />
                <div className="absolute left-0 top-full mt-1 z-30 w-[320px] max-h-[300px] overflow-y-auto rounded-lg border border-[var(--border)] bg-[var(--card)] shadow-[0_8px_28px_rgba(0,0,0,0.14)] p-1">
                  {candidates.map((s) => {
                    const owner = ownerOf(s.id);
                    const otherOwner = owner && owner.id !== role.id ? owner : null;
                    return (
                      <button key={s.id} onClick={() => pick(s.id)} className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md text-left hover:bg-[var(--hover)]">
                        <span className="w-7 h-7 rounded-md flex items-center justify-center shrink-0 bg-[var(--surface-inset)] text-[var(--text-muted)]"><Icon name={s.icon || "puzzle"} size={14} /></span>
                        <div className="min-w-0 flex-1">
                          <div className="text-[13px] font-medium text-[var(--text)] truncate">{s.name}</div>
                          {otherOwner
                            ? <div className="text-[11px] text-[var(--amber-ink)] truncate flex items-center gap-1"><Icon name="info" size={10} /> Assigned to {otherOwner.title}</div>
                            : <div className="text-[11px] text-[var(--text-faint2)] truncate">{s.desc || ""}</div>}
                        </div>
                      </button>
                    );
                  })}
                </div>
              </>
            )}
          </div>
        )}
      </div>

      {pendingSkill && (
        <div className="rounded-lg border border-[var(--amber-ink)]/30 bg-[var(--amber-ink)]/[0.06] px-3.5 py-3 flex items-start gap-2.5">
          <Icon name="info" size={15} className="text-[var(--amber-ink)] shrink-0 mt-0.5" />
          <div className="min-w-0 flex-1">
            <div className="text-[12.5px] text-[var(--text)] leading-snug"><b>{pendingSkill.name}</b> is currently assigned to <b>{pendingOwner ? pendingOwner.title : "another agent"}</b>. A skill can belong to only one agent — re-assign it to {role.title || "this agent"}?</div>
            <div className="flex items-center gap-2 mt-2.5">
              <button onClick={() => { assign(pending); setPending(null); }} className="px-2.5 py-1.5 rounded-md bg-[var(--accent)] text-white text-[12px] font-medium hover:brightness-110">Re-assign</button>
              <button onClick={() => setPending(null)} className="px-2.5 py-1.5 rounded-md text-[12px] text-[var(--text-muted)] hover:bg-[var(--hover)]">Cancel</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================
//  Agent (role) settings editor — name, title, description, and the
//  uniquely-assigned skills ("Agent capabilities"). Same chrome as the
//  Skills / Sub-Agents editors. The rail "Agent Team" detail page is separate.
// ============================================================
function RoleEditor({ ws, role }) {
  const update = (patch) => ws.setRoles((rs) => rs.map((x) => x.id === role.id ? { ...x, ...patch } : x));
  const onDelete = () => { ws.setRoles((rs) => rs.filter((x) => x.id !== role.id)); ws.setRoleSel(null); };
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <SAHeader label="Agents" hint="Name, title, description and assigned skills." />
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[820px] mx-auto px-7 py-7 flex flex-col gap-7">
          <button onClick={() => ws.setRoleSel(null)} className="flex items-center gap-1.5 text-[13px] text-[var(--text-muted)] hover:text-[var(--text)] -mb-2 w-fit"><Icon name="chevron-left" size={15} /> All agents</button>

          <div className="flex items-start gap-3.5">
            <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--card)] border border-[var(--border)] text-[var(--text-3)] shrink-0"><Icon name="users" size={22} /></span>
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2">
                <input value={role.title || ""} onChange={(e) => update({ title: e.target.value })}
                  placeholder="Agent title"
                  className="text-[20px] font-semibold text-[var(--text)] bg-transparent outline-none border-b border-transparent focus:border-[var(--accent)] min-w-0 placeholder:text-[var(--bar)]" />
                <span className="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-[0.04em] px-1.5 py-0.5 rounded text-[var(--text-muted)] bg-[var(--surface-inset)] shrink-0"><Icon name="users" size={9} /> Agent</span>
              </div>
            </div>
          </div>

          <SAField label="Name" hint="the persona name shown when this agent completes a task (e.g. Sam)">
            <input value={role.name || ""} onChange={(e) => update({ name: e.target.value })} placeholder="e.g. Sam" className={SETTINGS_INPUT} />
          </SAField>

          <SAField label="Title" hint="the role this agent plays (e.g. Product Manager)">
            <input value={role.title || ""} onChange={(e) => update({ title: e.target.value })} placeholder="e.g. Product Manager" className={SETTINGS_INPUT} />
          </SAField>

          <SAField label="Description" hint="what this agent is responsible for">
            <textarea value={role.description || ""} onChange={(e) => update({ description: e.target.value })} rows={3}
              placeholder="Describe what this agent is responsible for." className={SETTINGS_INPUT + " resize-none"} />
          </SAField>

          <SAField label="Agent capabilities" hint="skills assigned uniquely to this agent — always-on tools (Code Analysis, Jira, Local Files…) are available to every agent and aren't assigned">
            <RoleCapabilities ws={ws} role={role} />
          </SAField>

          <div className="flex items-center gap-2 pt-1">
            <button className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="check" size={15} /> Save agent</button>
            <button onClick={onDelete} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--amber-ink)] hover:bg-[var(--amber-ink)]/[0.07]"><Icon name="trash" size={14} /> Delete</button>
          </div>
          <div className="h-2" />
        </div>
      </div>
    </div>
  );
}

function AgentEditor({ ws, agent }) {
  const lock = saLockState(agent, ws.scope);
  const ro = lock.locked;
  const update = (patch) => ws.setAgents((xs) => xs.map((x) => x.id === agent.id ? { ...x, ...patch } : x));
  const onDelete = () => { ws.setAgents((xs) => xs.filter((x) => x.id !== agent.id)); ws.setAgentSel(null); };
  return (
    <div className="flex-1 min-w-0 flex flex-col bg-[var(--surface)]">
      <SAHeader label="Sub-Agents" hint="Name, routing description, prompt and model." />
      <div className="flex-1 min-w-0 overflow-y-auto">
        <div className="max-w-[820px] mx-auto px-7 py-7 flex flex-col gap-7">
          <button onClick={() => ws.setAgentSel(null)} className="flex items-center gap-1.5 text-[13px] text-[var(--text-muted)] hover:text-[var(--text)] -mb-2 w-fit"><Icon name="chevron-left" size={15} /> All sub-agents</button>

          <div className="flex items-start gap-3.5">
            <span className="w-12 h-12 rounded-xl flex items-center justify-center bg-[var(--card)] border border-[var(--border)] text-[var(--text-3)] shrink-0"><Icon name={AGENT_ICON} size={22} /></span>
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2">
                <input value={agent.name} disabled={ro} onChange={(e) => update({ name: e.target.value })}
                  className="text-[20px] font-semibold text-[var(--text)] bg-transparent outline-none border-b border-transparent focus:border-[var(--accent)] disabled:cursor-default min-w-0" />
                <LineageBadge lineage={saEffLineage(agent, ws.scope)} />
                <span className="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-[0.04em] px-1.5 py-0.5 rounded text-[var(--text-muted)] bg-[var(--surface-inset)]"><Icon name="robot" size={9} /> Sub-Agent</span>
              </div>
            </div>
          </div>

          {ro && (
            <div className="flex items-center gap-2.5 text-[12.5px] text-[var(--text-faint)] bg-[var(--surface-inset)] border border-[#e8ddc2] rounded-xl px-4 py-3">
              <Icon name="shield" size={16} className="shrink-0" />
              <span>This sub-agent is {saEffLineage(agent, ws.scope) === "inherited" ? "inherited from the workspace" : "built-in"} and read-only here. Duplicate it to make an editable copy.</span>
              <button onClick={() => ws.duplicateAgent(agent.id)} className="ml-auto shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-[12px] font-medium text-[var(--text-2)] hover:bg-[var(--surface-2)]"><Icon name="copy" size={13} /> Duplicate</button>
            </div>
          )}

          <div className="flex items-start gap-2.5 text-[12.5px] text-[var(--text-2)] bg-[var(--accent-tint)] border border-[var(--accent)]/30 rounded-xl px-4 py-3">
            <Icon name="sparkles" size={16} className="shrink-0 mt-0.5 text-[var(--accent)]" />
            <span>Agents investigate and return findings to the main thread — they don't choose the user-facing format. The calling skill owns the output.</span>
          </div>

          <SAField label="Description" hint="used to decide when this agent is picked by a skill">
            <textarea value={agent.desc} disabled={ro} onChange={(e) => update({ desc: e.target.value })} rows={2}
              placeholder="e.g. Traces architecture, ownership, and dependencies across the connected repositories."
              className={SETTINGS_INPUT + " resize-none disabled:opacity-70"} />
          </SAField>

          <SAField label="Instructions / prompt" hint="how the agent investigates and what it returns">
            <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] overflow-hidden">
              <div className="px-3 py-2 border-b border-[var(--border-soft)] bg-[var(--surface-2)] flex items-center gap-2 text-[11.5px] text-[var(--text-faint2)]"><Icon name="file" size={12} /> prompt.md</div>
              <textarea value={agent.instructions} disabled={ro} onChange={(e) => update({ instructions: e.target.value })} rows={7}
                className="w-full resize-none px-3.5 py-3 text-[12.5px] font-mono text-[var(--text)] bg-transparent outline-none leading-relaxed disabled:opacity-70 placeholder:text-[var(--bar)]" />
            </div>
          </SAField>

          <SAField label="Model" hint="optional override">
            {ro ? <div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-3.5 py-2.5 text-[13px] text-[var(--text-3)] flex items-center gap-2"><Icon name="model" size={14} className="text-[var(--text-faint2)]" /> {agent.model}</div>
                : <AgentModelPicker value={agent.model} onChange={(v) => update({ model: v })} />}
          </SAField>

          {!ro && (
            <div className="flex items-center gap-2 pt-1">
              <button className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[var(--accent)] text-white text-[13px] font-medium hover:brightness-110 shadow-[0_1px_0_rgba(0,0,0,0.08)]"><Icon name="check" size={15} /> Save agent</button>
              <button onClick={onDelete} className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-[var(--amber-ink)] hover:bg-[var(--amber-ink)]/[0.07]"><Icon name="trash" size={14} /> Delete</button>
            </div>
          )}
          <div className="h-2" />
        </div>
      </div>
    </div>
  );
}

// ============================================================
//  SCREEN 4 — (removed) Template override view — editing now happens via the
//  normal Modify flow from the skill detail; no separate override screen.
// ============================================================
