[
  {
    "path": ".gitignore",
    "content": "result\nconfig/fish/fish_variables\n__pycache__\n\nnushell/history*\nbin\nnode_modules\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"dotbot\"]\n\tpath = dotbot\n\turl = https://github.com/anishathalye/dotbot\n\tignore = dirty\n"
  },
  {
    "path": "Makefile",
    "content": "linux:\n\tnh home switch --flake .#elianiva --print-build-logs\n\ndarwin:\n\tnh darwin switch .\n\nclean:\n\tnh clean\n"
  },
  {
    "path": "README.md",
    "content": "# elianiva's personal dotfiles\n\nThis is my personal dotfiles repository which has gone through many iterations.\n\nHere are some of the screenshots from time to time (the config may or may not be still here, idk):\n\n![arch](./screenshots/preview-arch.png)\n![arch-new](./screenshots/preview-arch-new.png)\n![old](./screenshots/preview-old.png)\n![plasma](./screenshots/preview-plasma.png)\n![fedora](./screenshots/preview-fedora.png)\n![fedora-2](https://github.com/user-attachments/assets/ce263afd-1986-4c55-93a8-10494302c464)\n![macos](https://github.com/user-attachments/assets/e6a1be22-b773-4e0c-b022-eedc70fa8ca8)\n"
  },
  {
    "path": "agents/AGENTS.md",
    "content": "# Important Rules\n- Be extremely concise. Sacrifice grammar for the sake of concision.\n- Don't add tests for what the type system already guarantees.\n- At the end of each plan, give me a list of unresolved questions to answer, if any. The last thing visible should be numbered list of questions or concrete steps.\n- No compatibility wrappers, no legacy shims, no temporary plumbing, no backward compats, no reexport for \"convenience\".\n- Treat the project as greenfield unless stated otherwise.\n- You are not alone, expect parallel changes in unrelated files by others.\n"
  },
  {
    "path": "agents/opencode/opencode.json",
    "content": "{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"agent\": {\n    \"general\": {\n      \"disable\": true\n    },\n    \"explore\": {\n      \"disable\": true\n    },\n    \"plan\": {\n      \"permission\": {\n        \"edit\": {\n          \"*\": \"deny\",\n          \"*.md\": \"ask\"\n        },\n        \"skill\": {\n          \"*\": \"deny\",\n          \"code-review-*\": \"allow\",\n          \"brainstorming\": \"allow\",\n          \"build-feature\": \"allow\",\n          \"*-best-practices\": \"allow\",\n        }\n      }\n    }\n  },\n  \"permission\": {\n    \"*\": \"deny\",\n    \"read\": \"allow\",\n    \"write\": \"allow\",\n    \"bash\": \"allow\",\n    \"webfetch\": \"allow\",\n    \"websearch\": \"allow\",\n    \"lsp\": \"allow\",\n    \"list\": \"allow\",\n    \"grep\": \"allow\",\n    \"glob\": \"allow\",\n    \"edit\": \"allow\",\n    \"external_directory\": {\n      \"*\": \"ask\",\n      \"~/Development/personal/*\": \"allow\",\n      \"~/Development/work/*\": \"allow\",\n      \"~/Repositories/*\": \"allow\",\n      \"/tmp/*\": \"allow\",\n    },\n    \"question\": \"allow\",\n    \"codesearch\": \"allow\",\n    \"skill\": {\n      \"*\": \"allow\",\n      \"pdf\": \"deny\",\n      \"xlsx\": \"deny\",\n    },\n  }\n}\n"
  },
  {
    "path": "agents/pi/extensions/agent-profiles.ts",
    "content": "/**\n * Agent Profiles Extension\n *\n * Define agent personas in markdown files with frontmatter.\n * Content is appended to system prompt each turn.\n *\n * Location:\n * - ~/.pi/agent/agents/*.md (global)\n * - .pi/agents/*.md (project-local, overrides)\n *\n * Format:\n * ```markdown\n * ---\n * name: my-agent\n * description: What this agent does\n * tools: [read, bash, edit, write]  # whitelist (omit for all)\n * disabledTools: [grep]             # blacklist (omit for none)\n * ---\n *\n * Your system prompt additions here...\n * ```\n *\n * Usage:\n * - `pi --agent plan` - start with plan agent\n * - `/agent` - show selector\n * - `/agent plan` - switch to plan agent\n * - `/agent default` - reset to default\n */\n\nimport { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, basename, extname } from \"node:path\";\nimport type {\n  ExtensionAPI,\n  ExtensionContext,\n} from \"@mariozechner/pi-coding-agent\";\nimport { DynamicBorder } from \"@mariozechner/pi-coding-agent\";\nimport {\n  Key,\n  Container,\n  type SelectItem,\n  SelectList,\n  Text,\n} from \"@mariozechner/pi-tui\";\n\ninterface AgentProfile {\n  name: string;\n  description: string;\n  tools?: string[];\n  disabledTools?: string[];\n  provider?: string;\n  content: string; // appended to system prompt\n}\n\nconst DEFAULT_TOOLS = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"mcp\", \"ask\"];\n\n// Hardcoded default agent - always available as fallback\nconst DEFAULT_AGENT: AgentProfile = {\n  name: \"default\",\n  description: \"Default agent with full capabilities\",\n  tools: DEFAULT_TOOLS,\n  content:\n    \"You are pi, a minimal terminal coding agent. Use the tools available to help the user.\",\n};\n\n/**\n * Parse frontmatter from markdown content\n */\nfunction parseFrontmatter(content: string): {\n  frontmatter: Record<string, unknown>;\n  body: string;\n} {\n  const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n([\\s\\S]*)$/);\n  if (!match) {\n    return { frontmatter: {}, body: content };\n  }\n\n  const yamlText = match[1];\n  const body = match[2].trim();\n\n  const frontmatter: Record<string, unknown> = {};\n  for (const line of yamlText.split(\"\\n\")) {\n    const colonIdx = line.indexOf(\":\");\n    if (colonIdx === -1) continue;\n\n    const key = line.slice(0, colonIdx).trim();\n    let value: unknown = line.slice(colonIdx + 1).trim();\n\n    // Parse arrays: [item1, item2]\n    if (\n      typeof value === \"string\" &&\n      value.startsWith(\"[\") &&\n      value.endsWith(\"]\")\n    ) {\n      value = value\n        .slice(1, -1)\n        .split(\",\")\n        .map((s) => s.trim())\n        .filter(Boolean);\n    }\n\n    frontmatter[key] = value;\n  }\n\n  return { frontmatter, body };\n}\n\n/**\n * Load agent profiles from directory\n */\nfunction loadAgentsFromDir(dir: string): Map<string, AgentProfile> {\n  const agents = new Map<string, AgentProfile>();\n\n  if (!existsSync(dir)) return agents;\n\n  const entries = readdirSync(dir, { withFileTypes: true });\n\n  for (const entry of entries) {\n    if (!entry.isFile()) continue;\n    if (extname(entry.name) !== \".md\") continue;\n\n    const filepath = join(dir, entry.name);\n    try {\n      const content = readFileSync(filepath, \"utf-8\");\n      const { frontmatter, body } = parseFrontmatter(content);\n\n      const name = (frontmatter.name as string) || basename(entry.name, \".md\");\n\n      agents.set(name, {\n        name,\n        description: (frontmatter.description as string) || \"\",\n        tools: frontmatter.tools as string[] | undefined,\n        disabledTools: frontmatter.disabledTools as string[] | undefined,\n        provider: frontmatter.provider as string | undefined,\n        content: body,\n      });\n    } catch {\n      // skip invalid files\n    }\n  }\n\n  return agents;\n}\n\n/**\n * Load all agents (global + project, project overrides)\n * Default agent is always available as fallback\n */\nfunction loadAgents(cwd: string): Map<string, AgentProfile> {\n  const globalDir = join(homedir(), \".pi\", \"agent\", \"agents\");\n  const projectDir = join(cwd, \".pi\", \"agents\");\n\n  // Start with default agent\n  const agents = new Map<string, AgentProfile>();\n  agents.set(\"default\", DEFAULT_AGENT);\n\n  // Load global agents (can override default if explicitly defined)\n  const globalAgents = loadAgentsFromDir(globalDir);\n  for (const [name, agent] of globalAgents) {\n    agents.set(name, agent);\n  }\n\n  // Load project agents (override global)\n  const projectAgents = loadAgentsFromDir(projectDir);\n  for (const [name, agent] of projectAgents) {\n    agents.set(name, agent);\n  }\n\n  return agents;\n}\n\n/**\n * Get effective tools list for an agent\n */\nfunction getEffectiveTools(agent: AgentProfile | undefined): string[] {\n  if (!agent) return DEFAULT_TOOLS;\n\n  // Explicit whitelist\n  if (agent.tools) {\n    return agent.tools.filter((t) => DEFAULT_TOOLS.includes(t));\n  }\n\n  // Blacklist mode\n  if (agent.disabledTools) {\n    return DEFAULT_TOOLS.filter((t) => !agent.disabledTools?.includes(t));\n  }\n\n  return DEFAULT_TOOLS;\n}\n\nexport default function agentProfilesExtension(pi: ExtensionAPI) {\n  let agents = new Map<string, AgentProfile>();\n  let activeAgent: AgentProfile | undefined;\n  let baseSystemPrompt = \"\"; // captured at session start\n\n  // Register --agent CLI flag\n  pi.registerFlag(\"agent\", {\n    description: \"Agent profile to use\",\n    type: \"string\",\n  });\n\n  /**\n   * Apply an agent profile\n   */\n  async function applyAgent(\n    name: string,\n    agent: AgentProfile | undefined,\n    ctx: ExtensionContext,\n  ): Promise<void> {\n    activeAgent = agent;\n\n    // Emit event for other extensions (e.g., starship-prompt)\n    pi.events.emit(\"agent-profile:changed\", {\n      name: agent?.name ?? \"default\",\n      description: agent?.description ?? \"\",\n    });\n\n    if (!agent) {\n      // Reset to default\n      pi.setActiveTools(DEFAULT_TOOLS);\n      ctx.ui.notify(\"Reset to default agent\", \"info\");\n      updateStatus(ctx);\n      return;\n    }\n\n    // Apply tools - this rebuilds the system prompt with correct tool descriptions\n    const tools = getEffectiveTools(agent);\n    pi.setActiveTools(tools);\n\n    ctx.ui.notify(`Agent \"${name}\" activated (${tools.length} tools)`, \"info\");\n    updateStatus(ctx);\n  }\n\n  /**\n   * Build description for UI\n   */\n  function buildDescription(agent: AgentProfile): string {\n    const parts: string[] = [];\n\n    const tools = getEffectiveTools(agent);\n    if (tools.length !== DEFAULT_TOOLS.length) {\n      parts.push(`${tools.length} tools`);\n    }\n\n    if (agent.description) {\n      const truncated =\n        agent.description.length > 40\n          ? `${agent.description.slice(0, 37)}...`\n          : agent.description;\n      parts.push(truncated);\n    }\n\n    return parts.join(\" | \") || \"Custom agent profile\";\n  }\n\n  /**\n   * Show agent selector UI\n   */\n  async function showAgentSelector(ctx: ExtensionContext): Promise<void> {\n    const agentList = Array.from(agents.values()).sort((a, b) =>\n      a.name.localeCompare(b.name),\n    );\n\n    if (agentList.length === 0) {\n      ctx.ui.notify(\n        \"No agent profiles found. Create files in ~/.pi/agent/agents/ or .pi/agents/\",\n        \"warning\",\n      );\n      return;\n    }\n\n    const items: SelectItem[] = [\n      {\n        value: \"default\",\n        label: \"default\",\n        description: \"Reset to default (all tools, no additions)\",\n      },\n      ...agentList.map((agent) => ({\n        value: agent.name,\n        label:\n          agent.name === activeAgent?.name\n            ? `${agent.name} (active)`\n            : agent.name,\n        description: buildDescription(agent),\n      })),\n    ];\n\n    const result = await ctx.ui.custom<string | null>(\n      (tui, theme, _kb, done) => {\n        const container = new Container();\n        container.addChild(new DynamicBorder((str) => theme.fg(\"accent\", str)));\n        container.addChild(\n          new Text(theme.fg(\"accent\", theme.bold(\"Select Agent Profile\"))),\n        );\n\n        const selectList = new SelectList(items, Math.min(items.length, 12), {\n          selectedPrefix: (text) => theme.fg(\"accent\", text),\n          selectedText: (text) => theme.fg(\"accent\", text),\n          description: (text) => theme.fg(\"muted\", text),\n          scrollInfo: (text) => theme.fg(\"dim\", text),\n          noMatch: (text) => theme.fg(\"warning\", text),\n        });\n\n        selectList.onSelect = (item) => done(item.value);\n        selectList.onCancel = () => done(null);\n\n        const k = (s: string) => theme.bold(theme.fg(\"accent\", s));\n        const l = (s: string) => theme.fg(\"dim\", s);\n        container.addChild(selectList);\n        container.addChild(\n          new Text(\n            ` ${k(\"↑↓\")}${l(\" select\")} · ${k(\"enter\")}${l(\" submit\")} · ${k(\"esc\")}${l(\" dismiss\")}`,\n          ),\n        );\n        container.addChild(new DynamicBorder((str) => theme.fg(\"accent\", str)));\n\n        return {\n          render(width: number) {\n            return container.render(width);\n          },\n          invalidate() {\n            container.invalidate();\n          },\n          handleInput(data: string) {\n            selectList.handleInput(data);\n            tui.requestRender();\n          },\n        };\n      },\n    );\n\n    if (!result) return;\n\n    if (result === \"default\") {\n      await applyAgent(\"default\", undefined, ctx);\n    } else {\n      const agent = agents.get(result);\n      if (agent) {\n        await applyAgent(result, agent, ctx);\n      }\n    }\n  }\n\n  /**\n   * Update status in footer\n   */\n  function updateStatus(ctx: ExtensionContext) {\n    if (activeAgent && activeAgent.name !== \"default\") {\n      ctx.ui.setStatus(\n        \"agent\",\n        ctx.ui.theme.fg(\"accent\", `agent:${activeAgent.name}`),\n      );\n    } else {\n      ctx.ui.setStatus(\"agent\", undefined);\n    }\n  }\n\n  /**\n   * Get ordered list of agent names for cycling\n   */\n  function getAgentOrder(): string[] {\n    return [\n      \"default\",\n      ...Array.from(agents.keys())\n        .filter((n) => n !== \"default\")\n        .sort(),\n    ];\n  }\n\n  /**\n   * Cycle to next agent\n   */\n  async function cycleAgent(ctx: ExtensionContext): Promise<void> {\n    const agentNames = getAgentOrder();\n    if (agentNames.length <= 1) {\n      ctx.ui.notify(\"No custom agents to cycle\", \"warning\");\n      return;\n    }\n\n    const currentName = activeAgent?.name ?? \"default\";\n    const currentIndex = agentNames.indexOf(currentName);\n    const nextIndex =\n      currentIndex === -1 ? 0 : (currentIndex + 1) % agentNames.length;\n    const nextName = agentNames[nextIndex];\n\n    if (nextName === \"default\") {\n      await applyAgent(\"default\", undefined, ctx);\n    } else {\n      const agent = agents.get(nextName);\n      if (agent) {\n        await applyAgent(nextName, agent, ctx);\n      }\n    }\n  }\n\n  // Register keyboard shortcut to cycle agents\n  pi.registerShortcut(Key.ctrl(\";\"), {\n    description: \"Cycle to next agent profile\",\n    handler: (ctx) => cycleAgent(ctx),\n  });\n\n  // Inject agent content into system prompt each turn\n  // This appends to the base system prompt (which was already rebuilt with correct tools)\n  pi.on(\"before_agent_start\", async (event) => {\n    if (activeAgent?.content) {\n      return {\n        systemPrompt: `${event.systemPrompt}\\n\\n${activeAgent.content}`,\n      };\n    }\n  });\n\n  // Initialize on session start\n  pi.on(\"session_start\", async (_event, ctx) => {\n    // Load agents (includes hardcoded default)\n    agents = loadAgents(ctx.cwd);\n\n    // Set default as initial active agent\n    activeAgent = DEFAULT_AGENT;\n\n    // Capture base system prompt (for reference, not used directly)\n    baseSystemPrompt = ctx.getSystemPrompt();\n\n    // Emit initial state immediately so other extensions (e.g., starship-prompt) have valid state\n    pi.events.emit(\"agent-profile:changed\", {\n      name: DEFAULT_AGENT.name,\n      description: DEFAULT_AGENT.description,\n    });\n\n    // Check CLI flag first\n    const agentFlag = pi.getFlag(\"agent\");\n    if (typeof agentFlag === \"string\" && agentFlag) {\n      if (agentFlag === \"default\") {\n        // Already default, just notify\n        ctx.ui.notify(\"Using default agent\", \"info\");\n      } else {\n        const agent = agents.get(agentFlag);\n        if (agent) {\n          await applyAgent(agentFlag, agent, ctx);\n        } else {\n          const available = [\"default\", ...Array.from(agents.keys())].join(\n            \", \",\n          );\n          ctx.ui.notify(\n            `Unknown agent \"${agentFlag}\". Available: ${available}`,\n            \"warning\",\n          );\n        }\n      }\n      updateStatus(ctx);\n      return;\n    }\n\n    // Restore from session state\n    const entries = ctx.sessionManager.getEntries();\n    const agentEntry = entries\n      .filter(\n        (e: { type: string; customType?: string }) =>\n          e.type === \"custom\" && e.customType === \"agent-profile\",\n      )\n      .pop() as { data?: { name: string } } | undefined;\n\n    if (agentEntry?.data?.name && agentEntry.data.name !== \"default\") {\n      const agent = agents.get(agentEntry.data.name);\n      if (agent) {\n        await applyAgent(agentEntry.data.name, agent, ctx);\n      }\n    }\n\n    updateStatus(ctx);\n  });\n\n  // Persist active agent when it changes\n  pi.on(\"agent-profile:changed\", async (event) => {\n    pi.appendEntry(\"agent-profile\", { name: event.name });\n  });\n\n  // Register /agent command\n  pi.registerCommand(\"agent\", {\n    description: \"Switch agent profile\",\n    handler: async (args, ctx) => {\n      const name = args?.trim();\n\n      if (!name) {\n        await showAgentSelector(ctx);\n        return;\n      }\n\n      if (name === \"default\") {\n        await applyAgent(\"default\", undefined, ctx);\n        return;\n      }\n\n      const agent = agents.get(name);\n      if (!agent) {\n        const available = [\"default\", ...Array.from(agents.keys())].join(\", \");\n        ctx.ui.notify(\n          `Unknown agent \"${name}\". Available: ${available}`,\n          \"error\",\n        );\n        return;\n      }\n\n      await applyAgent(name, agent, ctx);\n    },\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/ask.ts",
    "content": "/**\n * Ask Tool Extension\n *\n * Provides an interactive Q&A tool for the LLM to ask users questions\n * with multiple choice options or free-form text input.\n */\n\nimport type { ExtensionAPI, ToolResult } from \"@mariozechner/pi-coding-agent\";\nimport { Key, matchesKey, truncateToWidth } from \"@mariozechner/pi-tui\";\nimport { Type } from \"@sinclair/typebox\";\n\n// Ask tool option schema\nconst AskOptionSchema = Type.Object({\n  label: Type.String({ description: \"Display label for the option\" }),\n  description: Type.Optional(\n    Type.String({ description: \"Optional description shown below label\" }),\n  ),\n});\n\nconst AskQuestionSchema = Type.Object({\n  question: Type.String({ description: \"The question to ask the user\" }),\n  options: Type.Array(AskOptionSchema, {\n    description: \"Options for the user to choose from\",\n  }),\n  shortTitle: Type.Optional(\n    Type.String({\n      description: \"Short 1-2 word title for tab display (defaults to number)\",\n    }),\n  ),\n});\n\n// Single object with everything optional to avoid union issues\nconst AskParams = Type.Object({\n  questions: Type.Optional(\n    Type.Array(AskQuestionSchema, {\n      description: \"A list of questions to ask the user\",\n    }),\n  ),\n  question: Type.Optional(\n    Type.String({ description: \"The question to ask the user\" }),\n  ),\n  options: Type.Optional(\n    Type.Array(AskOptionSchema, {\n      description: \"Options for the user to choose from\",\n    }),\n  ),\n});\n\ninterface AskDetails {\n  question: string;\n  options: string[];\n  answer: string | null;\n  wasCustom?: boolean;\n}\n\ninterface MultiAskDetails {\n  results: AskDetails[];\n}\n\ninterface QuestionState {\n  optionIndex: number;\n  editMode: boolean;\n  customAnswer: string;\n  answer: string | null;\n  wasCustom: boolean;\n}\n\nexport default function askToolExtension(pi: ExtensionAPI): void {\n  pi.registerTool({\n    name: \"ask\",\n    label: \"Ask\",\n    description:\n      \"Ask the user one or more questions and let them pick from options or type a custom answer. Use when you need user input to proceed with a decision.\",\n    parameters: AskParams,\n\n    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n      const questions: {\n        question: string;\n        options: { label: string; description?: string }[];\n        shortTitle?: string;\n      }[] = [];\n\n      if (params.questions && params.questions.length > 0) {\n        questions.push(...params.questions);\n      } else if (params.question && params.options) {\n        questions.push({ question: params.question, options: params.options });\n      }\n\n      if (questions.length === 0) {\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: \"Error: No questions (or question/options) provided\",\n            },\n          ],\n        };\n      }\n\n      if (!ctx.hasUI) {\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: \"Error: UI not available (running in non-interactive mode)\",\n            },\n          ],\n          details: {\n            results: questions.map((q) => ({\n              question: q.question,\n              options: q.options.map((o) => o.label),\n              answer: null,\n            })),\n          } as MultiAskDetails,\n        };\n      }\n\n      // Single question: use simple flow (no tabs, no review)\n      if (questions.length === 1) {\n        return handleSingleQuestion(ctx, questions[0]);\n      }\n\n      // Multiple questions: use tabbed UI with review\n      return handleMultipleQuestions(ctx, questions);\n    },\n  });\n}\n\nasync function handleSingleQuestion(\n  ctx: ExtensionAPI[\"ctx\"],\n  q: {\n    question: string;\n    options: { label: string; description?: string }[];\n    shortTitle?: string;\n  },\n): Promise<ToolResult> {\n  const allOptions = [\n    ...q.options,\n    { label: \"Type your own answer\", description: \"Write a custom response\" },\n  ];\n\n  const result = await ctx.ui.custom<{\n    answer: string;\n    wasCustom: boolean;\n    index?: number;\n  } | null>((tui, theme, _kb, done) => {\n    let optionIndex = 0;\n    let editMode = false;\n    let customAnswer = \"\";\n    let cachedLines: string[] | undefined;\n\n    function refresh() {\n      cachedLines = undefined;\n      tui.requestRender();\n    }\n\n    function handleInput(data: string) {\n      if (editMode) {\n        if (matchesKey(data, Key.escape)) {\n          editMode = false;\n          customAnswer = \"\";\n          refresh();\n          return;\n        }\n        if (matchesKey(data, Key.enter)) {\n          const trimmed = customAnswer.trim();\n          if (trimmed) {\n            done({ answer: trimmed, wasCustom: true });\n          } else {\n            editMode = false;\n            customAnswer = \"\";\n            refresh();\n          }\n          return;\n        }\n        if (matchesKey(data, Key.backspace)) {\n          customAnswer = customAnswer.slice(0, -1);\n          refresh();\n          return;\n        }\n        if (data.length === 1 && data.charCodeAt(0) >= 32) {\n          customAnswer += data;\n          refresh();\n        }\n        return;\n      }\n\n      if (matchesKey(data, Key.up)) {\n        optionIndex = Math.max(0, optionIndex - 1);\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.down)) {\n        optionIndex = Math.min(allOptions.length - 1, optionIndex + 1);\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.enter)) {\n        const selected = allOptions[optionIndex];\n        const isLastOption = optionIndex === allOptions.length - 1;\n        if (isLastOption) {\n          editMode = true;\n          refresh();\n        } else {\n          done({\n            answer: selected.label,\n            wasCustom: false,\n            index: optionIndex + 1,\n          });\n        }\n        return;\n      }\n      if (matchesKey(data, Key.escape)) {\n        done(null);\n      }\n    }\n\n    function render(width: number): string[] {\n      if (cachedLines) return cachedLines;\n\n      const lines: string[] = [];\n      const add = (s: string) => lines.push(truncateToWidth(s, width));\n\n      // Top border\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n\n      // Question\n      add(theme.fg(\"text\", ` ${q.question}`));\n      lines.push(\"\");\n\n      // Options\n      for (let i = 0; i < allOptions.length; i++) {\n        const opt = allOptions[i];\n        const selected = i === optionIndex;\n        const isLast = i === allOptions.length - 1;\n        const prefix = selected ? theme.fg(\"accent\", \"> \") : \"  \";\n        const num = `${i + 1}.`;\n\n        if (isLast && editMode) {\n          add(prefix + theme.fg(\"accent\", `${num} ${opt.label} ✎`));\n        } else if (selected) {\n          add(prefix + theme.fg(\"accent\", `${num} ${opt.label}`));\n        } else {\n          add(`  ${theme.fg(\"text\", `${num} ${opt.label}`)}`);\n        }\n\n        if (opt.description) {\n          add(`     ${theme.fg(\"muted\", opt.description)}`);\n        }\n      }\n\n      if (editMode) {\n        lines.push(\"\");\n        add(theme.fg(\"muted\", \" Your answer:\"));\n        const inputLine = ` > ${customAnswer}_`;\n        add(truncateToWidth(inputLine, width));\n      }\n\n      // Help text\n      lines.push(\"\");\n      const k = (s: string) => theme.bold(theme.fg(\"accent\", s));\n      const l = (s: string) => theme.fg(\"dim\", s);\n      if (editMode) {\n        add(` ${k(\"enter\")}${l(\" · submit\")} · ${k(\"esc\")}${l(\" go back\")}`);\n      } else {\n        add(\n          ` ${k(\"↑↓\")}${l(\" select\")} · ${k(\"enter\")}${l(\" submit\")} · ${k(\"esc\")}${l(\" dismiss\")}`,\n        );\n      }\n\n      // Bottom border\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n\n      cachedLines = lines;\n      return lines;\n    }\n\n    return {\n      render,\n      invalidate: () => {\n        cachedLines = undefined;\n      },\n      handleInput,\n    };\n  });\n\n  if (!result) {\n    return {\n      content: [{ type: \"text\", text: \"User cancelled the question\" }],\n      details: {\n        results: [\n          {\n            question: q.question,\n            options: q.options.map((o) => o.label),\n            answer: null,\n          },\n        ],\n      } as MultiAskDetails,\n    };\n  }\n\n  return {\n    content: [\n      {\n        type: \"text\",\n        text: result.wasCustom\n          ? `User answered: ${result.answer}`\n          : `User selected: ${result.answer}`,\n      },\n    ],\n    details: {\n      results: [\n        {\n          question: q.question,\n          options: q.options.map((o) => o.label),\n          answer: result.answer,\n          wasCustom: result.wasCustom,\n        },\n      ],\n    } as MultiAskDetails,\n  };\n}\n\nasync function handleMultipleQuestions(\n  ctx: ExtensionAPI[\"ctx\"],\n  questions: {\n    question: string;\n    options: { label: string; description?: string }[];\n    shortTitle?: string;\n  }[],\n): Promise<ToolResult> {\n  // Initialize state for each question\n  const states: QuestionState[] = questions.map(() => ({\n    optionIndex: 0,\n    editMode: false,\n    customAnswer: \"\",\n    answer: null as string | null,\n    wasCustom: false,\n  }));\n\n  let currentQuestion = 0;\n  let inReview = false;\n  let reviewOptionIndex = 0; // 0 = go back, 1 = submit\n\n  const result = await ctx.ui.custom<{\n    results: { answer: string; wasCustom: boolean }[];\n  } | null>((tui, theme, _kb, done) => {\n    let cachedLines: string[] | undefined;\n\n    function refresh() {\n      cachedLines = undefined;\n      tui.requestRender();\n    }\n\n    function getCurrentOptions() {\n      const q = questions[currentQuestion];\n      return [\n        ...q.options,\n        {\n          label: \"Type your own answer\",\n          description: \"Write a custom response\",\n        },\n      ];\n    }\n\n    function handleInput(data: string) {\n      if (inReview) {\n        handleReviewInput(data);\n        return;\n      }\n\n      const state = states[currentQuestion];\n      const allOptions = getCurrentOptions();\n\n      if (state.editMode) {\n        if (matchesKey(data, Key.escape)) {\n          state.editMode = false;\n          state.customAnswer = \"\";\n          refresh();\n          return;\n        }\n        if (matchesKey(data, Key.enter)) {\n          const trimmed = state.customAnswer.trim();\n          if (trimmed) {\n            state.answer = trimmed;\n            state.wasCustom = true;\n            state.editMode = false;\n            // Auto-advance to next question or review\n            if (currentQuestion < questions.length - 1) {\n              currentQuestion++;\n            } else {\n              inReview = true;\n            }\n            refresh();\n          } else {\n            state.editMode = false;\n            state.customAnswer = \"\";\n            refresh();\n          }\n          return;\n        }\n        if (matchesKey(data, Key.backspace)) {\n          state.customAnswer = state.customAnswer.slice(0, -1);\n          refresh();\n          return;\n        }\n        if (data.length === 1 && data.charCodeAt(0) >= 32) {\n          state.customAnswer += data;\n          refresh();\n        }\n        return;\n      }\n\n      // Tab navigation between questions\n      if (matchesKey(data, Key.tab)) {\n        currentQuestion = (currentQuestion + 1) % questions.length;\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.shift(Key.tab))) {\n        currentQuestion =\n          (currentQuestion - 1 + questions.length) % questions.length;\n        refresh();\n        return;\n      }\n\n      // Option selection within current question\n      if (matchesKey(data, Key.up)) {\n        state.optionIndex = Math.max(0, state.optionIndex - 1);\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.down)) {\n        state.optionIndex = Math.min(\n          allOptions.length - 1,\n          state.optionIndex + 1,\n        );\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.enter)) {\n        const selected = allOptions[state.optionIndex];\n        const isLastOption = state.optionIndex === allOptions.length - 1;\n        if (isLastOption) {\n          state.editMode = true;\n          refresh();\n        } else {\n          state.answer = selected.label;\n          state.wasCustom = false;\n          // Auto-advance to next question or review\n          if (currentQuestion < questions.length - 1) {\n            currentQuestion++;\n          } else {\n            inReview = true;\n          }\n          refresh();\n        }\n        return;\n      }\n      if (matchesKey(data, Key.escape)) {\n        done(null);\n      }\n    }\n\n    function handleReviewInput(data: string) {\n      if (matchesKey(data, Key.left) || matchesKey(data, Key.up)) {\n        reviewOptionIndex = 0;\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.right) || matchesKey(data, Key.down)) {\n        reviewOptionIndex = 1;\n        refresh();\n        return;\n      }\n      if (matchesKey(data, Key.enter)) {\n        if (reviewOptionIndex === 0) {\n          // Go back to edit\n          inReview = false;\n          currentQuestion = 0;\n          refresh();\n        } else {\n          // Submit\n          done({\n            results: states.map((s) => ({\n              answer: s.answer!,\n              wasCustom: s.wasCustom,\n            })),\n          });\n        }\n        return;\n      }\n      if (matchesKey(data, Key.escape)) {\n        inReview = false;\n        refresh();\n      }\n    }\n\n    function render(width: number): string[] {\n      if (cachedLines) return cachedLines;\n\n      if (inReview) {\n        return renderReview(width);\n      }\n      return renderQuestion(width);\n    }\n\n    function renderQuestion(width: number): string[] {\n      const lines: string[] = [];\n      const add = (s: string) => lines.push(truncateToWidth(s, width));\n      const state = states[currentQuestion];\n      const q = questions[currentQuestion];\n      const allOptions = getCurrentOptions();\n\n      // Tab bar with short titles (fallback to numbers)\n      const tabs = questions.map((q, i) => {\n        const isActive = i === currentQuestion;\n        const isAnswered = states[i].answer !== null;\n        const label = q.shortTitle || `${i + 1}`;\n        if (isActive) {\n          return theme.fg(\"accent\", theme.bold(label));\n        }\n        if (isAnswered) {\n          return theme.fg(\"muted\", `${label}✓`);\n        }\n        return theme.fg(\"dim\", label);\n      });\n      add(tabs.join(\" · \"));\n\n      // Top border\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n\n      // Question\n      add(theme.fg(\"text\", ` ${q.question}`));\n      lines.push(\"\");\n\n      // Options\n      for (let i = 0; i < allOptions.length; i++) {\n        const opt = allOptions[i];\n        const selected = i === state.optionIndex;\n        const isLast = i === allOptions.length - 1;\n        const prefix = selected ? theme.fg(\"accent\", \"> \") : \"  \";\n        const num = `${i + 1}.`;\n        const isActive = !isLast && state.answer === opt.label;\n        const check = isActive ? ` ${theme.fg(\"accent\", \"✓\")}` : \"\";\n\n        if (isLast && state.editMode) {\n          add(prefix + theme.fg(\"accent\", `${num} ${opt.label} ✎`));\n        } else if (isLast && state.wasCustom && state.answer) {\n          add(\n            prefix +\n              (selected\n                ? theme.fg(\"accent\", `${num} ${opt.label}`)\n                : theme.fg(\"text\", `${num} ${opt.label}`)) +\n              ` ${theme.fg(\"accent\", \"✓\")}`,\n          );\n        } else if (selected) {\n          add(prefix + theme.fg(\"accent\", `${num} ${opt.label}`) + check);\n        } else {\n          add(`  ${theme.fg(\"text\", `${num} ${opt.label}`)}` + check);\n        }\n\n        if (opt.description) {\n          add(`     ${theme.fg(\"muted\", opt.description)}`);\n        }\n      }\n\n      if (state.editMode) {\n        lines.push(\"\");\n        add(theme.fg(\"muted\", \" Your answer:\"));\n        const inputLine = ` > ${state.customAnswer}_`;\n        add(truncateToWidth(inputLine, width));\n      }\n\n      // Help text\n      lines.push(\"\");\n      const k = (s: string) => theme.bold(theme.fg(\"accent\", s));\n      const l = (s: string) => theme.fg(\"dim\", s);\n      if (state.editMode) {\n        add(` ${k(\"enter\")}${l(\" · submit\")} · ${k(\"esc\")}${l(\" go back\")}`);\n      } else {\n        add(\n          ` ${k(\"↑↓\")}${l(\" select\")} · ${k(\"tab\")}${l(\" next\")} · ${k(\"enter\")}${l(\" submit\")} · ${k(\"esc\")}${l(\" dismiss\")}`,\n        );\n      }\n\n      // Bottom border\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n\n      cachedLines = lines;\n      return lines;\n    }\n\n    function renderReview(width: number): string[] {\n      const lines: string[] = [];\n      const add = (s: string) => lines.push(truncateToWidth(s, width));\n\n      // Header\n      add(theme.fg(\"accent\", theme.bold(\" Review your answers\")));\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n      lines.push(\"\");\n\n      // Questions and answers\n      for (let i = 0; i < questions.length; i++) {\n        const q = questions[i];\n        const state = states[i];\n        const label = q.shortTitle || `Q${i + 1}`;\n        add(\n          ` ${theme.fg(\"text\", theme.bold(`${label}:`))} ${theme.fg(\"text\", q.question)}`,\n        );\n        if (state.answer) {\n          const prefix = state.wasCustom ? \"✎\" : \"✓\";\n          add(\n            `   ${theme.fg(\"accent\", prefix)} ${theme.fg(\"text\", state.answer)}`,\n          );\n        } else {\n          add(`   ${theme.fg(\"dim\", \"○ No answer\")}`);\n        }\n        lines.push(\"\");\n      }\n\n      // Action buttons\n      const goBackLabel = \"← Go back to edit\";\n      const submitLabel = \"✔️Submit answers\";\n      const goBack =\n        reviewOptionIndex === 0\n          ? theme.fg(\"accent\", theme.bold(`> ${goBackLabel}`))\n          : theme.fg(\"dim\", `  ${goBackLabel}`);\n      const submit =\n        reviewOptionIndex === 1\n          ? theme.fg(\"accent\", theme.bold(`> ${submitLabel}`))\n          : theme.fg(\"dim\", `  ${submitLabel}`);\n      add(` ${goBack}`);\n      add(` ${submit}`);\n\n      // Help text\n      lines.push(\"\");\n      const k = (s: string) => theme.bold(theme.fg(\"accent\", s));\n      const l = (s: string) => theme.fg(\"dim\", s);\n      add(\n        ` ${k(\"↑↓\")}${l(\" select\")} · ${k(\"enter\")}${l(\" confirm\")} · ${k(\"esc\")}${l(\" go back\")}`,\n      );\n\n      // Bottom border\n      add(theme.fg(\"accent\", \"─\".repeat(width)));\n\n      cachedLines = lines;\n      return lines;\n    }\n\n    return {\n      render,\n      invalidate: () => {\n        cachedLines = undefined;\n      },\n      handleInput,\n    };\n  });\n\n  if (!result) {\n    return {\n      content: [{ type: \"text\", text: \"User cancelled the questions\" }],\n      details: {\n        results: questions.map((q, i) => ({\n          question: q.question,\n          options: q.options.map((o) => o.label),\n          answer: states[i].answer,\n        })),\n      } as MultiAskDetails,\n    };\n  }\n\n  // Format output\n  const outputText = questions\n    .map((q, i) => {\n      const r = result.results[i];\n      const prefix = r.wasCustom ? \"Answered\" : \"Selected\";\n      const label = q.shortTitle || `Question ${i + 1}`;\n      return `${label}: ${q.question}\\n${prefix}: ${r.answer}`;\n    })\n    .join(\"\\n\\n\");\n\n  return {\n    content: [{ type: \"text\", text: outputText }],\n    details: {\n      results: questions.map((q, i) => ({\n        question: q.question,\n        options: q.options.map((o) => o.label),\n        answer: result.results[i].answer,\n        wasCustom: result.results[i].wasCustom,\n      })),\n    } as MultiAskDetails,\n  };\n}\n"
  },
  {
    "path": "agents/pi/extensions/composed-editor/index.ts",
    "content": "/**\n * Composed Editor - Combines starship-prompt + pi-ckers\n *\n * This is your local extension that composes multiple editor features.\n * Place this in ~/.pi/extensions/composed-editor/\n */\n\nimport { type ExtensionAPI, CustomEditor } from \"@mariozechner/pi-coding-agent\";\nimport type { Picker } from \"@elianiva/pi-ckers\";\nimport { createStarshipWidget, setupStarshipEvents } from \"@elianiva/pi-starship\";\nimport { withPickers } from \"@elianiva/pi-ckers\";\nimport { filePicker, dirPicker } from \"@elianiva/pi-ckers/builtin/fff\";\nimport { grepPicker } from \"@elianiva/pi-ckers/builtin/grep\";\n\n// Compose: StarshipEditor + all builtin pickers\nconst pickers: Picker[] = [\n  filePicker(),\n  dirPicker(),\n  grepPicker(),\n];\n\nconst ComposedEditor = withPickers(CustomEditor, pickers);\n\nexport default function composedEditorExtension(pi: ExtensionAPI) {\n  pi.on(\"session_start\", async (_event, ctx) => {\n    // if (!ctx.hasUI) return;\n    //\n    // // Set composed editor - pass ctx as 5th argument for picker initialization\n    // ctx.ui.setEditorComponent(\n    //   (tui, theme, keybindings) => new ComposedEditor(tui, theme, keybindings, undefined, ctx),\n    // );\n\n    // Clear footer\n    // ctx.ui.setFooter(() => ({ invalidate() {}, render() { return []; } }));\n\n    // // Set up starship widget\n    // createStarshipWidget(pi, ctx);\n  });\n\n  // Set up starship event handlers\n  // setupStarshipEvents(pi);\n}\n"
  },
  {
    "path": "agents/pi/extensions/composed-editor/package.json",
    "content": "{\n  \"name\": \"composed-editor\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"Composed editor with starship-prompt + pi-ckers\",\n  \"dependencies\": {\n    \"@elianiva/pi-ckers\": \"link:@elianiva/pi-ckers\",\n    \"@elianiva/pi-starship\": \"link:@elianiva/pi-starship\",\n    \"@mariozechner/pi-coding-agent\": \"*\"\n  }\n}\n"
  },
  {
    "path": "agents/pi/extensions/custom-formatting.ts",
    "content": "import { Theme } from \"@mariozechner/pi-coding-agent\";\nimport type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\n\n/**\n * Extension that overrides markdown bold and italic formatting to use colors instead.\n *\n * It monkey-patches the Theme prototype so it works across all themes\n * and doesn't require polling.\n */\nexport default function (pi: ExtensionAPI) {\n  const originalBold = Theme.prototype.bold;\n  const originalItalic = Theme.prototype.italic;\n\n  // Override bold to use 'warning' color (usually yellow/orange)\n  Theme.prototype.bold = function(this: Theme, text: string) {\n    return originalBold(this.fg(\"mdHeading\", text));\n  };\n\n  // Override italic to use 'accent' color (usually teal/cyan)\n  Theme.prototype.italic = function(this: Theme, text: string) {\n    return originalItalic(this.fg(\"accent\", text));\n  };\n\n  // Restore originals on shutdown\n  pi.on(\"session_shutdown\", () => {\n    Theme.prototype.bold = originalBold;\n    Theme.prototype.italic = originalItalic;\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/exit-command.ts",
    "content": "import type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"input\", async (event, ctx) => {\n    // Only handle plain \"exit\" without arguments\n    if (event.text.trim() === \"exit\") {\n      ctx.shutdown();\n      return { action: \"handled\" };\n    }\n\n    return { action: \"continue\" };\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/handoff.ts",
    "content": "/**\n * Handoff extension - transfer context to a new focused session\n *\n * Instead of compacting (which is lossy), handoff extracts what matters\n * for your next task and creates a new session with a generated prompt.\n *\n * Usage:\n *   /handoff now implement this for teams as well\n *   /handoff execute phase one of the plan\n *   /handoff check other places that need this fix\n *\n * The generated prompt appears as a draft in the editor for review/editing.\n */\n\nimport { complete, type Message } from \"@mariozechner/pi-ai\";\nimport type { ExtensionAPI, SessionEntry } from \"@mariozechner/pi-coding-agent\";\nimport { BorderedLoader, convertToLlm, serializeConversation } from \"@mariozechner/pi-coding-agent\";\n\nconst SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:\n\n1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)\n2. Lists any relevant files that were discussed or modified\n3. Clearly states the next task based on the user's goal\n4. Is self-contained - the new thread should be able to proceed without the old conversation\n\nFormat your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like \"Here's the prompt\" - just output the prompt itself.\n\nExample output format:\n## Context\nWe've been working on X. Key decisions:\n- Decision 1\n- Decision 2\n\nFiles involved:\n- path/to/file1.ts\n- path/to/file2.ts\n\n## Task\n[Clear description of what to do next based on user's goal]`;\n\nexport default function (pi: ExtensionAPI) {\n\tpi.registerCommand(\"handoff\", {\n\t\tdescription: \"Transfer context to a new focused session\",\n\t\thandler: async (args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"handoff requires interactive mode\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!ctx.model) {\n\t\t\t\tctx.ui.notify(\"No model selected\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst goal = args.trim();\n\t\t\tif (!goal) {\n\t\t\t\tctx.ui.notify(\"Usage: /handoff <goal for new thread>\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Gather conversation context from current branch\n\t\t\tconst branch = ctx.sessionManager.getBranch();\n\t\t\tconst messages = branch\n\t\t\t\t.filter((entry): entry is SessionEntry & { type: \"message\" } => entry.type === \"message\")\n\t\t\t\t.map((entry) => entry.message);\n\n\t\t\tif (messages.length === 0) {\n\t\t\t\tctx.ui.notify(\"No conversation to hand off\", \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Convert to LLM format and serialize\n\t\t\tconst llmMessages = convertToLlm(messages);\n\t\t\tconst conversationText = serializeConversation(llmMessages);\n\t\t\tconst currentSessionFile = ctx.sessionManager.getSessionFile();\n\n\t\t\t// Generate the handoff prompt with loader UI\n\t\t\tconst result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n\t\t\t\tconst loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);\n\t\t\t\tloader.onAbort = () => done(null);\n\n\t\t\t\tconst doGenerate = async () => {\n\t\t\t\t\tconst apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);\n\n\t\t\t\t\tconst userMessage: Message = {\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `## Conversation History\\n\\n${conversationText}\\n\\n## User's Goal for New Thread\\n\\n${goal}`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t};\n\n\t\t\t\t\tconst response = await complete(\n\t\t\t\t\t\tctx.model!,\n\t\t\t\t\t\t{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },\n\t\t\t\t\t\t{ apiKey, signal: loader.signal },\n\t\t\t\t\t);\n\n\t\t\t\t\tif (response.stopReason === \"aborted\") {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn response.content\n\t\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\t};\n\n\t\t\t\tdoGenerate()\n\t\t\t\t\t.then(done)\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tconsole.error(\"Handoff generation failed:\", err);\n\t\t\t\t\t\tdone(null);\n\t\t\t\t\t});\n\n\t\t\t\treturn loader;\n\t\t\t});\n\n\t\t\tif (result === null) {\n\t\t\t\tctx.ui.notify(\"Cancelled\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Let user edit the generated prompt\n\t\t\tconst editedPrompt = await ctx.ui.editor(\"Edit handoff prompt\", result);\n\n\t\t\tif (editedPrompt === undefined) {\n\t\t\t\tctx.ui.notify(\"Cancelled\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create new session with parent tracking\n\t\t\tconst newSessionResult = await ctx.newSession({\n\t\t\t\tparentSession: currentSessionFile,\n\t\t\t});\n\n\t\t\tif (newSessionResult.cancelled) {\n\t\t\t\tctx.ui.notify(\"New session cancelled\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the edited prompt in the main editor for submission\n\t\t\tctx.ui.setEditorText(editedPrompt);\n\t\t\tctx.ui.notify(\"Handoff ready. Submit when ready.\", \"info\");\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "agents/pi/extensions/lsp/index.ts",
    "content": "/**\n * LSP Diagnostics Extension\n *\n * Warms up LSP servers on file read, provides diagnostics on write/edit operations.\n * Pattern based on opencode's implementation.\n */\n\nimport type {\n  ExtensionAPI,\n  ToolResultEvent,\n  ExtensionContext,\n} from \"@mariozechner/pi-coding-agent\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, extname } from \"node:path\";\nimport { pathToFileURL, fileURLToPath } from \"node:url\";\nimport {\n  createMessageConnection,\n  StreamMessageReader,\n  StreamMessageWriter,\n  type MessageConnection,\n} from \"vscode-jsonrpc/node\";\nimport type { Diagnostic as VSCodeDiagnostic } from \"vscode-languageserver-protocol\";\nimport { LSP_SERVERS, LANGUAGE_MAP, type LspHandle, type LspServerInfo } from \"./lsp-config.js\";\n\nconst DIAGNOSTICS_TIMEOUT_MS = 5000;\nconst DIAGNOSTICS_DEBOUNCE_MS = 150;\nconst LSP_CONNECTION_TIMEOUT_MS = 5000;\n\ntype Diagnostic = VSCodeDiagnostic;\n\ninterface LspConnection {\n  connection: MessageConnection;\n  process: ChildProcess;\n  rootDir: string;\n  serverName: string;\n  languages: string[];\n  startedAt: number;\n  lastActivity: number;\n  diagnostics: Map<string, Diagnostic[]>;\n  openedFiles: Map<string, number>;\n  initialized: boolean;\n}\n\ninterface LspStatus {\n  name: string;\n  pid: number | undefined;\n  rootDir: string;\n  languages: string[];\n  uptime: number;\n  lastActivity: number;\n  filesTracked: number;\n  initialized: boolean;\n}\n\nclass LspConnectionManager {\n  private connections = new Map<string, LspConnection>();\n  private diagnosticCallbacks = new Map<string, Set<() => void>>();\n\n  async getOrCreateConnections(\n    filePath: string,\n    cwd: string,\n    ctx: ExtensionContext,\n  ): Promise<LspConnection[]> {\n    const ext = extname(filePath);\n    const languageId = LANGUAGE_MAP[ext];\n    if (!languageId) return [];\n\n    // Find all matching servers\n    const matchingServers = LSP_SERVERS.filter((server) => server.extensions.includes(ext));\n\n    const connections: LspConnection[] = [];\n\n    for (const server of matchingServers) {\n      const rootDir = await server.root(filePath, cwd);\n      if (!rootDir) continue; // Server doesn't apply to this project\n\n      const connectionKey = `${server.id}:${rootDir}`;\n      if (this.connections.has(connectionKey)) {\n        const conn = this.connections.get(connectionKey)!;\n        conn.lastActivity = Date.now();\n        connections.push(conn);\n        continue;\n      }\n\n      try {\n        const handle = await server.spawn(rootDir, cwd);\n        if (!handle) {\n          ctx.ui.notify(`Could not start ${server.id} LSP server`, \"error\");\n          continue;\n        }\n        const conn = await this.createConnection(\n          connectionKey,\n          server.id,\n          handle,\n          rootDir,\n          server.extensions,\n          ctx,\n          AbortSignal.timeout(LSP_CONNECTION_TIMEOUT_MS),\n        );\n        this.connections.set(connectionKey, conn);\n        connections.push(conn);\n      } catch (e: any) {\n        if (e.name === \"TimeoutError\") {\n          ctx.ui.notify(`${server.id} LSP connection timed out, skipping`, \"warning\");\n        } else {\n          ctx.ui.notify(`Failed to start ${server.id} LSP: ${e}`, \"error\");\n        }\n      }\n    }\n\n    return connections;\n  }\n\n  private async createConnection(\n    key: string,\n    name: string,\n    handle: LspHandle,\n    rootDir: string,\n    languages: string[],\n    ctx: ExtensionContext,\n    signal: AbortSignal,\n  ): Promise<LspConnection> {\n    ctx.ui.notify(`Starting ${name} LSP...`, \"info\");\n\n    const connection = createMessageConnection(\n      new StreamMessageReader(handle.process.stdout),\n      new StreamMessageWriter(handle.process.stdin),\n    );\n\n    const conn: LspConnection = {\n      connection,\n      process: handle.process,\n      rootDir,\n      serverName: name,\n      languages: [...languages],\n      startedAt: Date.now(),\n      lastActivity: Date.now(),\n      diagnostics: new Map(),\n      openedFiles: new Map(),\n      initialized: false,\n    };\n\n    // Handle diagnostics\n    connection.onNotification(\"textDocument/publishDiagnostics\", (params: any) => {\n      const filePath = this.normalizePath(fileURLToPath(params.uri));\n      conn.diagnostics.set(filePath, params.diagnostics);\n\n      // Notify waiters\n      const callbacks = this.diagnosticCallbacks.get(filePath);\n      if (callbacks) {\n        for (const cb of callbacks) cb();\n      }\n    });\n\n    // Handle window/workDoneProgress/create\n    connection.onRequest(\"window/workDoneProgress/create\", () => null);\n\n    // Handle workspace/configuration\n    connection.onRequest(\"workspace/configuration\", async () => [handle.initialization ?? {}]);\n\n    // Handle client/registerCapability\n    connection.onRequest(\"client/registerCapability\", async () => {});\n\n    // Handle client/unregisterCapability\n    connection.onRequest(\"client/unregisterCapability\", async () => {});\n\n    // Handle workspace/workspaceFolders\n    connection.onRequest(\"workspace/workspaceFolders\", async () => [\n      {\n        name: \"workspace\",\n        uri: pathToFileURL(rootDir).href,\n      },\n    ]);\n\n    connection.listen();\n\n    // Initialize with timeout using AbortSignal\n    try {\n      await this.initializeConnection(connection, handle, rootDir, signal);\n    } catch (e: any) {\n      if (e.name === \"TimeoutError\") {\n        ctx.ui.notify(`${name} LSP connection timed out, continuing anyway`, \"warning\");\n      } else {\n        throw e;\n      }\n    }\n\n    conn.initialized = true;\n\n    // Handle stderr\n    handle.process.stderr?.on(\"data\", (data: Buffer) => {\n      const msg = data.toString().trim();\n      if (msg) console.error(`[LSP:${name}]`, msg.slice(0, 200));\n    });\n\n    // Handle process exit\n    handle.process.on(\"exit\", (code) => {\n      if (code && code !== 0) {\n        ctx.ui.notify(`${name} LSP exited with code ${code}`, \"error\");\n      }\n      this.connections.delete(key);\n    });\n\n    ctx.ui.notify(`${name} LSP ready`, \"success\");\n    return conn;\n  }\n\n  private async initializeConnection(\n    connection: MessageConnection,\n    handle: LspHandle,\n    rootDir: string,\n    signal: AbortSignal,\n  ): Promise<void> {\n    await connection.sendRequest(\"initialize\", {\n      rootUri: pathToFileURL(rootDir).href,\n      processId: handle.process.pid,\n      workspaceFolders: [\n        {\n          name: \"workspace\",\n          uri: pathToFileURL(rootDir).href,\n        },\n      ],\n      initializationOptions: handle.initialization ?? {},\n      capabilities: {\n        window: {\n          workDoneProgress: true,\n        },\n        workspace: {\n          configuration: true,\n          didChangeWatchedFiles: {\n            dynamicRegistration: true,\n          },\n        },\n        textDocument: {\n          synchronization: {\n            didOpen: true,\n            didChange: true,\n          },\n          publishDiagnostics: {\n            versionSupport: true,\n          },\n        },\n      },\n    });\n\n    await connection.sendNotification(\"initialized\", {});\n    if (handle.initialization) {\n      await connection.sendNotification(\"workspace/didChangeConfiguration\", {\n        settings: handle.initialization,\n      });\n    }\n  }\n\n  /**\n   * Touch file with diagnostic subscription BEFORE sending didOpen\n   * This is the key fix from opencode to avoid race conditions\n   */\n  async touchFile(\n    conn: LspConnection,\n    filePath: string,\n    content: string,\n    languageId: string,\n    waitForDiagnostics: boolean = false,\n  ): Promise<Diagnostic[]> {\n    const normalizedPath = this.normalizePath(filePath);\n    const uri = pathToFileURL(normalizedPath).href;\n    const existingVersion = conn.openedFiles.get(normalizedPath);\n\n    // Set up diagnostic listener BEFORE sending notification (opencode pattern)\n    let diagnosticsPromise: Promise<Diagnostic[]> | undefined;\n    let debounceTimer: ReturnType<typeof setTimeout> | undefined;\n    let unsubscribe: (() => void) | undefined;\n\n    if (waitForDiagnostics) {\n      diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {\n        let resolved = false;\n\n        const tryResolve = () => {\n          if (resolved) return;\n          if (debounceTimer) clearTimeout(debounceTimer);\n          debounceTimer = setTimeout(() => {\n            resolved = true;\n            unsubscribe?.();\n            resolve(conn.diagnostics.get(normalizedPath) ?? []);\n          }, DIAGNOSTICS_DEBOUNCE_MS);\n        };\n\n        if (!this.diagnosticCallbacks.has(normalizedPath)) {\n          this.diagnosticCallbacks.set(normalizedPath, new Set());\n        }\n        const callbacks = this.diagnosticCallbacks.get(normalizedPath)!;\n        callbacks.add(tryResolve);\n        unsubscribe = () => callbacks.delete(tryResolve);\n\n        // Timeout fallback\n        setTimeout(() => {\n          if (!resolved) {\n            resolved = true;\n            unsubscribe?.();\n            resolve(conn.diagnostics.get(normalizedPath) ?? []);\n          }\n        }, DIAGNOSTICS_TIMEOUT_MS);\n      });\n    }\n\n    // Now send the notification\n    if (existingVersion !== undefined) {\n      conn.diagnostics.delete(normalizedPath);\n      const nextVersion = existingVersion + 1;\n      conn.openedFiles.set(normalizedPath, nextVersion);\n\n      await conn.connection.sendNotification(\"workspace/didChangeWatchedFiles\", {\n        changes: [{ uri, type: 2 }], // Changed\n      });\n\n      await conn.connection.sendNotification(\"textDocument/didChange\", {\n        textDocument: { uri, version: nextVersion },\n        contentChanges: [{ text: content }],\n      });\n    } else {\n      conn.openedFiles.set(normalizedPath, 0);\n      conn.diagnostics.delete(normalizedPath);\n\n      await conn.connection.sendNotification(\"workspace/didChangeWatchedFiles\", {\n        changes: [{ uri, type: 1 }], // Created\n      });\n\n      await conn.connection.sendNotification(\"textDocument/didOpen\", {\n        textDocument: { uri, languageId, version: 0, text: content },\n      });\n    }\n\n    if (diagnosticsPromise) {\n      return diagnosticsPromise;\n    }\n    return [];\n  }\n\n  getDiagnostics(conn: LspConnection, filePath: string): Diagnostic[] {\n    const normalizedPath = this.normalizePath(filePath);\n    return conn.diagnostics.get(normalizedPath) ?? [];\n  }\n\n  getAllDiagnostics(filePath: string): { conn: LspConnection; diagnostics: Diagnostic[] }[] {\n    const normalizedPath = this.normalizePath(filePath);\n    const results: { conn: LspConnection; diagnostics: Diagnostic[] }[] = [];\n    for (const conn of this.connections.values()) {\n      const diags = conn.diagnostics.get(normalizedPath);\n      if (diags && diags.length > 0) {\n        results.push({ conn, diagnostics: diags });\n      }\n    }\n    return results;\n  }\n\n  async shutdown(conn: LspConnection): Promise<void> {\n    try {\n      await conn.connection.sendRequest(\"shutdown\", undefined);\n      await conn.connection.sendNotification(\"exit\", {});\n      conn.connection.end();\n      conn.connection.dispose();\n    } catch {\n      // Ignore shutdown errors\n    }\n    conn.process.kill();\n  }\n\n  async shutdownAll(): Promise<void> {\n    const promises = Array.from(this.connections.values()).map((conn) => this.shutdown(conn));\n    await Promise.all(promises);\n    this.connections.clear();\n  }\n\n  killConnection(key: string): boolean {\n    const conn = this.connections.get(key);\n    if (!conn) return false;\n\n    conn.process.kill(\"SIGTERM\");\n    setTimeout(() => {\n      if (!conn.process.killed) {\n        conn.process.kill(\"SIGKILL\");\n      }\n    }, 1000);\n\n    this.connections.delete(key);\n    return true;\n  }\n\n  getStatus(): LspStatus[] {\n    return Array.from(this.connections.entries()).map(([key, conn]) => ({\n      name: conn.serverName,\n      pid: conn.process.pid,\n      rootDir: conn.rootDir,\n      languages: conn.languages,\n      uptime: Date.now() - conn.startedAt,\n      lastActivity: Date.now() - conn.lastActivity,\n      filesTracked: conn.openedFiles.size,\n      initialized: conn.initialized,\n    }));\n  }\n\n  private normalizePath(filePath: string): string {\n    return resolve(filePath);\n  }\n}\n\n// Helper Functions\n\nfunction extractFileFromEvent(event: ToolResultEvent): { path: string; isWrite: boolean } | null {\n  if (!event.isError && (event.toolName === \"write\" || event.toolName === \"edit\")) {\n    return { path: event.input.path as string, isWrite: true };\n  }\n  if (!event.isError && event.toolName === \"read\") {\n    return { path: event.input.path as string, isWrite: false };\n  }\n  return null;\n}\n\nfunction formatDiagnostics(diagnostics: Diagnostic[]): string {\n  const severityLabels = [\"\", \"ERROR\", \"WARNING\", \"INFO\", \"HINT\"];\n\n  return diagnostics\n    .map((d) => {\n      const line = (d.range?.start?.line ?? 0) + 1;\n      const col = (d.range?.start?.character ?? 0) + 1;\n      const severity = severityLabels[d.severity ?? 0] || \"UNKNOWN\";\n      const code = d.code ? `[${d.code}]` : \"\";\n      // const source = d.source ? `${d.source}` : \"\";\n      return `${severity} [${line}:${col}] ${d.message}`;\n    })\n    .join(\"\\n\");\n}\n\nfunction formatDuration(ms: number): string {\n  if (ms < 1000) return `${ms}ms`;\n  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;\n  return `${(ms / 60000).toFixed(1)}m`;\n}\n\nexport default function (pi: ExtensionAPI) {\n  const manager = new LspConnectionManager();\n\n  // Warm up LSP on read, full diagnostics on write/edit\n  pi.on(\"tool_result\", async (event: ToolResultEvent, ctx: ExtensionContext) => {\n    const fileInfo = extractFileFromEvent(event);\n    if (!fileInfo) return;\n\n    const { path: filePath, isWrite } = fileInfo;\n    const absPath = resolve(ctx.cwd, filePath);\n\n    const conns = await manager.getOrCreateConnections(filePath, ctx.cwd, ctx);\n    if (conns.length === 0) return;\n\n    let content: string;\n    try {\n      content = readFileSync(absPath, \"utf-8\");\n    } catch {\n      return;\n    }\n\n    const ext = extname(filePath);\n    const languageId = LANGUAGE_MAP[ext];\n    if (!languageId) return;\n\n    // For read operations: warm up and fetch existing diagnostics if available\n    if (!isWrite) {\n      await Promise.all(\n        conns.map((conn) => manager.touchFile(conn, absPath, content, languageId, false)),\n      );\n      const allDiags = manager.getAllDiagnostics(absPath);\n      // if (allDiags.length > 0) {\n      //   const combined = allDiags.flatMap((d) => d.diagnostics);\n      //   const formatted = formatDiagnostics(combined);\n      //   pi.sendUserMessage(`Current diagnostics for \\`${filePath}\\`:\\n${formatted}`, {\n      //     deliverAs: \"steer\",\n      //   });\n      // }\n      return;\n    }\n    // For write operations: use opencode pattern - subscribe BEFORE sending\n    const diagnostics = (\n      await Promise.all(\n        conns.map((conn) => manager.touchFile(conn, absPath, content, languageId, true)),\n      )\n    ).flat();\n\n    // Show results\n    if (diagnostics.length > 0) {\n      const formatted = formatDiagnostics(diagnostics);\n      pi.sendUserMessage(`Diagnostics for \\`${filePath}\\`:\\n${formatted}`, {\n        deliverAs: \"steer\",\n      });\n    } else {\n      ctx.ui.notify(`✓ ${filePath}: clean`, \"success\");\n    }\n  });\n\n  // Cleanup on shutdown\n  pi.on(\"session_shutdown\", async () => {\n    await manager.shutdownAll();\n  });\n\n  // /lsp command - dashboard\n  pi.registerCommand(\"lsp\", {\n    description:\n      \"LSP status dashboard - show running servers, available LSPs, and manage processes\",\n    getArgumentCompletions: (prefix: string) => {\n      const actions = [\"status\", \"available\", \"kill\", \"killall\"];\n      const filtered = actions.filter((a) => a.startsWith(prefix));\n      return filtered.length > 0 ? filtered.map((a) => ({ value: a, label: a })) : null;\n    },\n    handler: async (args: string, ctx) => {\n      const parts = args.trim().split(/\\s+/);\n      const action = parts[0] || \"status\";\n      const serverKey = parts[1];\n\n      switch (action) {\n        case \"status\": {\n          const status = manager.getStatus();\n          if (status.length === 0) {\n            ctx.ui.notify(\"No LSP servers currently running\", \"info\");\n            return;\n          }\n\n          const lines = [\n            \"📊 LSP Server Status\",\n            \"\",\n            ...status.map((s) => {\n              const uptime = formatDuration(s.uptime);\n              const statusStr = s.initialized ? \"🟢 ready\" : \"🟡 initializing\";\n              return `  ${s.name}\\n    PID: ${s.pid} | Status: ${statusStr}\\n    Root: ${s.rootDir}\\n    Languages: ${s.languages.join(\", \")}\\n    Uptime: ${uptime} | Files: ${s.filesTracked}`;\n            }),\n            \"\",\n            \"Use `/lsp kill <server>` to kill a specific server\",\n            \"Use `/lsp killall` to kill all servers\",\n          ];\n\n          ctx.ui.notify(lines.join(\"\\n\"), \"info\");\n          break;\n        }\n\n        case \"available\": {\n          const lines = [\n            \"🔧 Available LSP Servers\",\n            \"\",\n            \"Configured servers:\",\n            ...LSP_SERVERS.map((s) => `  ${s.id} - ${s.extensions.join(\", \")}`),\n          ];\n\n          ctx.ui.notify(lines.join(\"\\n\"), \"info\");\n          break;\n        }\n\n        case \"kill\": {\n          if (!serverKey) {\n            const status = manager.getStatus();\n            if (status.length === 0) {\n              ctx.ui.notify(\"No servers running\", \"info\");\n              return;\n            }\n\n            const keys = status.map((s) => `${s.name}:${s.rootDir}`);\n            ctx.ui.notify(\"Usage: /lsp kill <server-key>\\n\\nAvailable servers:\", \"error\");\n            ctx.ui.notify(keys.join(\"\\n\"), \"info\");\n            return;\n          }\n\n          const killed = manager.killConnection(serverKey);\n          if (killed) {\n            ctx.ui.notify(`Killed LSP server: ${serverKey}`, \"success\");\n          } else {\n            ctx.ui.notify(`Server not found: ${serverKey}`, \"error\");\n          }\n          break;\n        }\n\n        case \"killall\": {\n          const status = manager.getStatus();\n          await manager.shutdownAll();\n          ctx.ui.notify(\n            `Killed ${status.length} LSP server${status.length > 1 ? \"s\" : \"\"}`,\n            \"success\",\n          );\n          break;\n        }\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/lsp/lsp-config.ts",
    "content": "/**\n * LSP Server Configurations\n *\n * Each server defines:\n * - id: Unique identifier\n * - extensions: File extensions this server handles\n * - root: Function to find project root directory\n * - spawn: Function to start the LSP server process\n */\n\nimport { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { access, constants } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// Mason bin path (Neovim plugin manager)\nconst MASON_BIN_PATH = resolve(homedir(), \".local/share/nvim/mason/bin\");\n\nexport interface LspHandle {\n  process: ChildProcessWithoutNullStreams;\n  initialization?: Record<string, unknown>;\n}\n\nexport type RootFunction = (file: string, cwd: string) => Promise<string | undefined>;\n\nexport interface LspServerInfo {\n  id: string;\n  extensions: string[];\n  root: RootFunction;\n  spawn(root: string, cwd: string): Promise<LspHandle | undefined>;\n}\n\n// Language ID mapping for LSP\nexport const LANGUAGE_MAP: Record<string, string> = {\n  \".ts\": \"typescript\",\n  \".tsx\": \"typescriptreact\",\n  \".js\": \"javascript\",\n  \".jsx\": \"javascriptreact\",\n  \".mjs\": \"javascript\",\n  \".cjs\": \"javascript\",\n  \".mts\": \"typescript\",\n  \".cts\": \"typescript\",\n  \".lua\": \"lua\",\n  \".py\": \"python\",\n  \".pyi\": \"python\",\n  \".rs\": \"rust\",\n  \".php\": \"php\",\n  \".svelte\": \"svelte\",\n  \".astro\": \"astro\",\n  \".vue\": \"vue\",\n  \".css\": \"css\",\n  \".scss\": \"scss\",\n  \".less\": \"less\",\n  \".html\": \"html\",\n  \".json\": \"json\",\n  \".jsonc\": \"json\",\n  \".yaml\": \"yaml\",\n  \".yml\": \"yaml\",\n  \".md\": \"markdown\",\n  \".typ\": \"typst\",\n  \".typc\": \"typst\",\n  \".go\": \"go\",\n  \".zig\": \"zig\",\n  \".zon\": \"zig\",\n  \".c\": \"c\",\n  \".cpp\": \"cpp\",\n  \".cc\": \"cpp\",\n  \".cxx\": \"cpp\",\n  \".c++\": \"cpp\",\n  \".h\": \"c\",\n  \".hpp\": \"cpp\",\n  \".hh\": \"cpp\",\n  \".hxx\": \"cpp\",\n  \".h++\": \"cpp\",\n  \".ex\": \"elixir\",\n  \".exs\": \"elixir\",\n  \".cs\": \"csharp\",\n  \".fs\": \"fsharp\",\n  \".fsi\": \"fsharp\",\n  \".fsx\": \"fsharp\",\n  \".fsscript\": \"fsharp\",\n  \".swift\": \"swift\",\n  \".kt\": \"kotlin\",\n  \".kts\": \"kotlin\",\n  \".java\": \"java\",\n  \".rb\": \"ruby\",\n  \".rake\": \"ruby\",\n  \".dart\": \"dart\",\n  \".ml\": \"ocaml\",\n  \".mli\": \"ocaml\",\n  \".sh\": \"shellscript\",\n  \".bash\": \"shellscript\",\n  \".zsh\": \"shellscript\",\n  \".ksh\": \"shellscript\",\n  \".nix\": \"nix\",\n  \".gleam\": \"gleam\",\n  \".clj\": \"clojure\",\n  \".cljs\": \"clojure\",\n  \".cljc\": \"clojure\",\n  \".edn\": \"clojure\",\n  \".hs\": \"haskell\",\n  \".lhs\": \"haskell\",\n  \".jl\": \"julia\",\n  \".tf\": \"terraform\",\n  \".tfvars\": \"terraform\",\n  \".tex\": \"latex\",\n  \".bib\": \"latex\",\n  \".prisma\": \"prisma\",\n};\n\n/**\n * Check if a path exists (async)\n */\nasync function pathExists(path: string): Promise<boolean> {\n  try {\n    await access(path, constants.F_OK);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Find binary in PATH, checking Mason bin first\n */\nexport async function which(bin: string): Promise<string | undefined> {\n  // 1. Check Mason bin first (Neovim LSP servers)\n  const masonPath = resolve(MASON_BIN_PATH, bin);\n  if (await pathExists(masonPath)) return masonPath;\n\n  // 2. Check PATH\n  const pathEnv = process.env.PATH || \"\";\n  for (const dir of pathEnv.split(\":\")) {\n    if (!dir) continue;\n    const fullPath = resolve(dir, bin);\n    if (await pathExists(fullPath)) return fullPath;\n  }\n\n  return undefined;\n}\n\n/**\n * Helper to find nearest file upward in directory tree\n */\nexport function nearestRoot(includePatterns: string[], excludePatterns?: string[]): RootFunction {\n  return async (file: string, cwd: string) => {\n    let current = dirname(file);\n    const stopAt = cwd;\n\n    // Check for exclusions first\n    if (excludePatterns) {\n      while (current !== \"/\" && current !== dirname(current)) {\n        for (const pattern of excludePatterns) {\n          if (await pathExists(resolve(current, pattern))) {\n            return undefined;\n          }\n        }\n        if (current === stopAt) break;\n        current = dirname(current);\n      }\n    }\n\n    // Reset and check for inclusions\n    current = dirname(file);\n    while (current !== \"/\" && current !== dirname(current)) {\n      for (const pattern of includePatterns) {\n        if (await pathExists(resolve(current, pattern))) {\n          return current;\n        }\n      }\n      if (current === stopAt) break;\n      current = dirname(current);\n    }\n    return undefined;\n  };\n}\n\n/**\n * Create a simple spawn handle\n */\nfunction createHandle(\n  bin: string,\n  args: string[],\n  root: string,\n  initialization?: Record<string, unknown>,\n): LspHandle | undefined {\n  const proc = spawn(bin, args, { cwd: root }) as ChildProcessWithoutNullStreams;\n  return { process: proc, initialization };\n}\n\n// ============================================================================\n// LSP Server Definitions\n// ============================================================================\n\nexport const LSP_SERVERS: LspServerInfo[] = [\n  // TypeScript/JavaScript\n  {\n    id: \"typescript\",\n    extensions: [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\", \".mts\", \".cts\"],\n    root: nearestRoot(\n      [\n        \"package-lock.json\",\n        \"bun.lockb\",\n        \"bun.lock\",\n        \"pnpm-lock.yaml\",\n        \"yarn.lock\",\n        \"package.json\",\n        \"tsconfig.json\",\n      ],\n      [\"deno.json\", \"deno.jsonc\"],\n    ),\n    async spawn(root, cwd) {\n      // Try to find local tsserver first\n      let tsserver: string | undefined;\n      const possiblePaths = [\n        resolve(cwd, \"node_modules\", \"typescript\", \"lib\", \"tsserver.js\"),\n        resolve(root, \"node_modules\", \"typescript\", \"lib\", \"tsserver.js\"),\n      ];\n      for (const p of possiblePaths) {\n        if (await pathExists(p)) {\n          tsserver = p;\n          break;\n        }\n      }\n\n      const bin = await which(\"typescript-language-server\");\n      if (!bin) return undefined;\n\n      return createHandle(\n        bin,\n        [\"--stdio\"],\n        root,\n        tsserver ? { tsserver: { path: tsserver } } : undefined,\n      );\n    },\n  },\n\n  // Deno\n  {\n    id: \"deno\",\n    extensions: [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\"],\n    root: nearestRoot(\n      [\"deno.json\", \"deno.jsonc\"],\n      [\n        \"package-lock.json\",\n        \"bun.lockb\",\n        \"bun.lock\",\n        \"pnpm-lock.yaml\",\n        \"yarn.lock\",\n        \"package.json\",\n        \"tsconfig.json\",\n      ],\n    ),\n    async spawn(root) {\n      const bin = await which(\"deno\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"lsp\"], root);\n    },\n  },\n\n  // Oxlint\n  {\n    id: \"oxlint\",\n    extensions: [\n      \".ts\",\n      \".tsx\",\n      \".js\",\n      \".jsx\",\n      \".mjs\",\n      \".cjs\",\n      \".mts\",\n      \".cts\",\n      \".json\",\n      \".jsonc\",\n      \".vue\",\n      \".astro\",\n      \".svelte\",\n      \".css\",\n    ],\n    root: nearestRoot([\n      \".oxlintrc.json\",\n      \"package-lock.json\",\n      \"bun.lockb\",\n      \"bun.lock\",\n      \"pnpm-lock.yaml\",\n      \"yarn.lock\",\n      \"package.json\",\n    ]),\n    async spawn(root) {\n      // Try local oxlint first\n      let bin = resolve(root, \"node_modules\", \".bin\", \"oxlint\");\n      if (!(await pathExists(bin))) {\n        bin = (await which(\"oxlint\")) || \"\";\n      }\n\n      if (!bin || !(await pathExists(bin))) {\n        // Try bun x oxlint\n        const bunBin = (await which(\"bun\")) || \"bun\";\n        const proc = spawn(bunBin, [\"x\", \"oxlint\", \"--lsp\"], {\n          cwd: root,\n        }) as ChildProcessWithoutNullStreams;\n        return { process: proc };\n      }\n\n      return createHandle(bin, [\"--lsp\"], root);\n    },\n  },\n\n  // Rust\n  {\n    id: \"rust\",\n    extensions: [\".rs\"],\n    root: nearestRoot([\"Cargo.toml\", \"Cargo.lock\"]),\n    async spawn(root) {\n      const bin = await which(\"rust-analyzer\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // Go\n  {\n    id: \"gopls\",\n    extensions: [\".go\"],\n    root: nearestRoot([\"go.work\", \"go.mod\", \"go.sum\"]),\n    async spawn(root) {\n      const bin = await which(\"gopls\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // Python (Pyright)\n  {\n    id: \"pyright\",\n    extensions: [\".py\", \".pyi\"],\n    root: nearestRoot([\n      \"pyproject.toml\",\n      \"setup.py\",\n      \"setup.cfg\",\n      \"requirements.txt\",\n      \"Pipfile\",\n      \"pyrightconfig.json\",\n    ]),\n    async spawn(root) {\n      let bin = await which(\"pyright-langserver\");\n\n      if (!bin) {\n        // Try bun x\n        const bunBin = (await which(\"bun\")) || \"bun\";\n        const proc = spawn(bunBin, [\"x\", \"pyright\", \"--stdio\"], {\n          cwd: root,\n        }) as ChildProcessWithoutNullStreams;\n        return { process: proc };\n      }\n\n      const initialization: Record<string, string> = {};\n      // Check for virtual env\n      const venvPaths = [\n        process.env.VIRTUAL_ENV,\n        resolve(root, \".venv\"),\n        resolve(root, \"venv\"),\n      ].filter(Boolean) as string[];\n\n      for (const venvPath of venvPaths) {\n        const pythonPath =\n          process.platform === \"win32\"\n            ? resolve(venvPath, \"Scripts\", \"python.exe\")\n            : resolve(venvPath, \"bin\", \"python\");\n        if (await pathExists(pythonPath)) {\n          initialization.pythonPath = pythonPath;\n          break;\n        }\n      }\n\n      return createHandle(bin, [\"--stdio\"], root, initialization);\n    },\n  },\n\n  // Lua\n  {\n    id: \"lua\",\n    extensions: [\".lua\"],\n    root: nearestRoot([\n      \".luarc.json\",\n      \".luarc.jsonc\",\n      \".luacheckrc\",\n      \".stylua.toml\",\n      \"stylua.toml\",\n      \".git\",\n    ]),\n    async spawn(root) {\n      const bin = await which(\"lua-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root, { Lua: { diagnostics: { globals: [\"vim\"] } } });\n    },\n  },\n\n  // PHP\n  {\n    id: \"intelephense\",\n    extensions: [\".php\"],\n    root: nearestRoot([\"composer.json\", \"composer.lock\", \".php-version\"]),\n    async spawn(root) {\n      const bin = await which(\"intelephense\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root, { telemetry: { enabled: false } });\n    },\n  },\n\n  // Svelte\n  {\n    id: \"svelte\",\n    extensions: [\".svelte\"],\n    root: nearestRoot([\n      \"package-lock.json\",\n      \"bun.lockb\",\n      \"bun.lock\",\n      \"pnpm-lock.yaml\",\n      \"yarn.lock\",\n      \"package.json\",\n    ]),\n    async spawn(root) {\n      const bin = await which(\"svelteserver\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // Vue\n  {\n    id: \"vue\",\n    extensions: [\".vue\"],\n    root: nearestRoot([\n      \"package-lock.json\",\n      \"bun.lockb\",\n      \"bun.lock\",\n      \"pnpm-lock.yaml\",\n      \"yarn.lock\",\n      \"package.json\",\n    ]),\n    async spawn(root) {\n      const bin = await which(\"vue-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // Astro\n  {\n    id: \"astro\",\n    extensions: [\".astro\"],\n    root: nearestRoot([\n      \"package-lock.json\",\n      \"bun.lockb\",\n      \"bun.lock\",\n      \"pnpm-lock.yaml\",\n      \"yarn.lock\",\n      \"package.json\",\n    ]),\n    async spawn(root, cwd) {\n      const bin = await which(\"astro-ls\");\n      if (!bin) return undefined;\n\n      // Find typescript for astro\n      let tsdk: string | undefined;\n      const possiblePaths = [\n        resolve(cwd, \"node_modules\", \"typescript\", \"lib\"),\n        resolve(root, \"node_modules\", \"typescript\", \"lib\"),\n      ];\n      for (const p of possiblePaths) {\n        if (await pathExists(p)) {\n          tsdk = p;\n          break;\n        }\n      }\n\n      return createHandle(bin, [\"--stdio\"], root, tsdk ? { typescript: { tsdk } } : undefined);\n    },\n  },\n\n  // Zig\n  {\n    id: \"zls\",\n    extensions: [\".zig\", \".zon\"],\n    root: nearestRoot([\"build.zig\", \"build.zig.zon\"]),\n    async spawn(root) {\n      const bin = await which(\"zls\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // C/C++\n  {\n    id: \"clangd\",\n    extensions: [\".c\", \".cpp\", \".cc\", \".cxx\", \".c++\", \".h\", \".hpp\", \".hh\", \".hxx\", \".h++\"],\n    root: nearestRoot([\n      \"compile_commands.json\",\n      \"compile_flags.txt\",\n      \".clangd\",\n      \"CMakeLists.txt\",\n      \"Makefile\",\n    ]),\n    async spawn(root) {\n      const bin = await which(\"clangd\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--background-index\", \"--clang-tidy\"], root);\n    },\n  },\n\n  // C#\n  {\n    id: \"csharp\",\n    extensions: [\".cs\"],\n    root: nearestRoot([\".slnx\", \".sln\", \".csproj\", \"global.json\"]),\n    async spawn(root) {\n      const bin = (await which(\"csharp-ls\")) || (await which(\"omnisharp\"));\n      if (!bin) return undefined;\n      return createHandle(bin, [\"-lsp\"], root);\n    },\n  },\n\n  // Swift\n  {\n    id: \"sourcekit-lsp\",\n    extensions: [\".swift\"],\n    root: nearestRoot([\"Package.swift\", \"*.xcodeproj\", \"*.xcworkspace\"]),\n    async spawn(root) {\n      const bin = await which(\"sourcekit-lsp\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // Elixir\n  {\n    id: \"elixir-ls\",\n    extensions: [\".ex\", \".exs\"],\n    root: nearestRoot([\"mix.exs\", \"mix.lock\"]),\n    async spawn(root) {\n      const bin = (await which(\"elixir-ls\")) || (await which(\"language_server.sh\"));\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // Kotlin\n  {\n    id: \"kotlin-ls\",\n    extensions: [\".kt\", \".kts\"],\n    root: nearestRoot([\n      \"settings.gradle.kts\",\n      \"settings.gradle\",\n      \"build.gradle.kts\",\n      \"build.gradle\",\n      \"pom.xml\",\n    ]),\n    async spawn(root) {\n      const bin = await which(\"kotlin-lsp\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // Dart\n  {\n    id: \"dart\",\n    extensions: [\".dart\"],\n    root: nearestRoot([\"pubspec.yaml\", \"analysis_options.yaml\"]),\n    async spawn(root) {\n      const bin = await which(\"dart\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"language-server\", \"--lsp\"], root);\n    },\n  },\n\n  // OCaml\n  {\n    id: \"ocaml-lsp\",\n    extensions: [\".ml\", \".mli\"],\n    root: nearestRoot([\"dune-project\", \"dune-workspace\", \".merlin\", \"opam\"]),\n    async spawn(root) {\n      const bin = await which(\"ocamllsp\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // Bash\n  {\n    id: \"bash\",\n    extensions: [\".sh\", \".bash\", \".zsh\", \".ksh\"],\n    root: () => undefined, // Will use cwd\n    async spawn(root) {\n      const bin = await which(\"bash-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"start\"], root);\n    },\n  },\n\n  // Nix\n  {\n    id: \"nixd\",\n    extensions: [\".nix\"],\n    root: nearestRoot([\"flake.nix\", \"flake.lock\", \".git\"]),\n    async spawn(root) {\n      const bin = await which(\"nixd\");\n      if (!bin) return undefined;\n      return createHandle(bin, [], root);\n    },\n  },\n\n  // YAML\n  {\n    id: \"yaml\",\n    extensions: [\".yaml\", \".yml\"],\n    root: nearestRoot([\"package.json\"]),\n    async spawn(root) {\n      const bin = await which(\"yaml-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // JSON\n  {\n    id: \"json\",\n    extensions: [\".json\", \".jsonc\"],\n    root: nearestRoot([\"package.json\"]),\n    async spawn(root) {\n      const bin = await which(\"vscode-json-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // HTML\n  {\n    id: \"html\",\n    extensions: [\".html\"],\n    root: nearestRoot([\"package.json\"]),\n    async spawn(root) {\n      const bin = await which(\"vscode-html-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // CSS\n  {\n    id: \"css\",\n    extensions: [\".css\", \".scss\", \".less\"],\n    root: nearestRoot([\"package.json\"]),\n    async spawn(root) {\n      const bin = await which(\"vscode-css-language-server\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--stdio\"], root);\n    },\n  },\n\n  // Typst\n  {\n    id: \"tinymist\",\n    extensions: [\".typ\", \".typc\"],\n    root: nearestRoot([\"typst.toml\"]),\n    async spawn(root) {\n      const bin = await which(\"tinymist\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"lsp\"], root);\n    },\n  },\n\n  // Terraform\n  {\n    id: \"terraform\",\n    extensions: [\".tf\", \".tfvars\"],\n    root: nearestRoot([\".terraform.lock.hcl\", \"terraform.tfstate\", \"*.tf\"]),\n    async spawn(root) {\n      const bin = await which(\"terraform-ls\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"serve\"], root);\n    },\n  },\n\n  // Gleam\n  {\n    id: \"gleam\",\n    extensions: [\".gleam\"],\n    root: nearestRoot([\"gleam.toml\", \"manifest.toml\"]),\n    async spawn(root) {\n      const bin = await which(\"gleam\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"lsp\"], root);\n    },\n  },\n\n  // Clojure\n  {\n    id: \"clojure-lsp\",\n    extensions: [\".clj\", \".cljs\", \".cljc\", \".edn\"],\n    root: nearestRoot([\"deps.edn\", \"project.clj\", \"shadow-cljs.edn\", \"bb.edn\", \"build.boot\"]),\n    async spawn(root) {\n      const bin = await which(\"clojure-lsp\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"listen\"], root);\n    },\n  },\n\n  // Haskell\n  {\n    id: \"haskell-language-server\",\n    extensions: [\".hs\", \".lhs\"],\n    root: nearestRoot([\"stack.yaml\", \"cabal.project\", \"hie.yaml\", \"*.cabal\"]),\n    async spawn(root) {\n      const bin = await which(\"haskell-language-server-wrapper\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"--lsp\"], root);\n    },\n  },\n\n  // Julia\n  {\n    id: \"julials\",\n    extensions: [\".jl\"],\n    root: nearestRoot([\"Project.toml\", \"Manifest.toml\"]),\n    async spawn(root) {\n      const bin = await which(\"julia\");\n      if (!bin) return undefined;\n      const proc = spawn(\n        bin,\n        [\"--startup-file=no\", \"--history-file=no\", \"-e\", \"using LanguageServer; runserver()\"],\n        { cwd: root },\n      ) as ChildProcessWithoutNullStreams;\n      return { process: proc };\n    },\n  },\n\n  // Prisma\n  {\n    id: \"prisma\",\n    extensions: [\".prisma\"],\n    root: nearestRoot([\"schema.prisma\", \"prisma/schema.prisma\"]),\n    async spawn(root) {\n      const bin = await which(\"prisma\");\n      if (!bin) return undefined;\n      return createHandle(bin, [\"language-server\"], root);\n    },\n  },\n];\n"
  },
  {
    "path": "agents/pi/extensions/notify.ts",
    "content": "/**\n * Pi Notify Extension\n *\n * Sends a native terminal notification when Pi agent is done and waiting for input.\n * Supports multiple terminal protocols:\n * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode\n * - OSC 99: Kitty\n * - Windows toast: Windows Terminal (WSL)\n */\n\nimport type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\n\nfunction windowsToastScript(title: string, body: string): string {\n\tconst type = \"Windows.UI.Notifications\";\n\tconst mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;\n\tconst template = `[${type}.ToastTemplateType]::ToastText01`;\n\tconst toast = `[${type}.ToastNotification]::new($xml)`;\n\treturn [\n\t\t`${mgr} > $null`,\n\t\t`$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,\n\t\t`$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,\n\t\t`[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,\n\t].join(\"; \");\n}\n\nfunction notifyOSC777(title: string, body: string): void {\n\tprocess.stdout.write(`\\x1b]777;notify;${title};${body}\\x07`);\n}\n\nfunction notifyOSC99(title: string, body: string): void {\n\t// Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part\n\tprocess.stdout.write(`\\x1b]99;i=1:d=0;${title}\\x1b\\\\`);\n\tprocess.stdout.write(`\\x1b]99;i=1:p=body;${body}\\x1b\\\\`);\n}\n\nfunction notifyWindows(title: string, body: string): void {\n\tconst { execFile } = require(\"child_process\");\n\texecFile(\"powershell.exe\", [\"-NoProfile\", \"-Command\", windowsToastScript(title, body)]);\n}\n\nfunction notify(title: string, body: string): void {\n\tif (process.env.WT_SESSION) {\n\t\tnotifyWindows(title, body);\n\t} else if (process.env.KITTY_WINDOW_ID) {\n\t\tnotifyOSC99(title, body);\n\t} else {\n\t\tnotifyOSC777(title, body);\n\t}\n}\n\nexport default function (pi: ExtensionAPI) {\n\tpi.on(\"agent_end\", async () => {\n\t\tnotify(\"Pi\", \"Ready for input!\");\n\t});\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/README.md",
    "content": "# pi-codemode\n\nExecutor plugins for codemode plus curated npm/jj commands and whitelisted bash.\n\n## Tools\n- `pi.*` builtins (filesystem, bash, webfetch)\n- `fff.*` search tools\n- `npm.run` run npm scripts\n- `npm.install` install packages via lockfile-aware package manager selection\n- `jj.status` / `jj.diff` / `jj.log` / `jj.new` / `jj.describe` / `jj.commit` for jj\n\n## Bash whitelist\n- Package managers: `npm`, `pnpm`, `bun`, `yarn`, `npx`, `node`\n- File operations: `mkdir`, `touch`, `cp`, `mv`, `rm`, `ln`, `chmod`, `chown`\n- Utilities: `pwd`, `echo`, `which`, `uname`, `date`, `sleep`\n- Everything else is blocked with guidance to use the equivalent `tools.pi.*` tool\n\n## Rules\n- no full bash access — whitelisted package managers only\n- use JS sandbox + named tools instead\n- repo.install picks pnpm if pnpm-lock.yaml exists, bun if bun.lock/bun.lockb exists, else npm"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/executor.jsonc",
    "content": "{\n  \"sources\": [\n    {\n      \"kind\": \"mcp\",\n      \"transport\": \"remote\",\n      \"name\": \"mcp-typescript server on vercel\",\n      \"endpoint\": \"https://mcp.exa.ai/mcp\",\n      \"remoteTransport\": \"auto\",\n      \"namespace\": \"mcp_typescript_server_on_vercel\"\n    }\n  ]\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/index.ts",
    "content": "import type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\nimport { createCodemodeTool } from \"./src/codemode.js\";\nimport { disposeAllExecutors, getExecutor } from \"./src/executor-cache.js\";\nimport { acquireSandbox, disposeAll as disposeAllSandboxes } from \"./src/sandbox-cache.js\";\nimport { initFinder } from \"./src/fff.js\";\n\nexport default function registerCodemode(pi: ExtensionAPI) {\n  pi.registerTool(createCodemodeTool());\n\n  const activate = () => pi.setActiveTools([\"codemode\"]);\n\n  // Pre-warm sandbox, finder, and executor on session start\n  pi.on(\"session_start\", async (event, ctx) => {\n    if (event.reason === \"startup\" || event.reason === \"reload\" || event.reason === \"new\") {\n      await acquireSandbox(ctx.cwd);           // creates & caches sandbox\n      initFinder(ctx.cwd).catch(() => {});     // fire-and-forget fff scan\n      getExecutor(ctx.cwd).catch(() => {});    // fire-and-forget executor creation\n    }\n    activate();\n  });\n\n  pi.on(\"before_agent_start\", async () => {\n    activate();\n    return undefined;\n  });\n\n  pi.on(\"session_shutdown\", async () => {\n    await Promise.allSettled([\n      disposeAllExecutors(),\n      disposeAllSandboxes(),\n    ]);\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/package.json",
    "content": "{\n  \"name\": \"pi-codemode\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"pi codemode extension with executor SDK and secure-exec sandbox\",\n  \"keywords\": [\n    \"pi-package\"\n  ],\n  \"peerDependencies\": {\n    \"@mariozechner/pi-ai\": \"*\",\n    \"@mariozechner/pi-agent-core\": \"*\",\n    \"@mariozechner/pi-coding-agent\": \"*\",\n    \"@mariozechner/pi-tui\": \"*\",\n    \"@sinclair/typebox\": \"*\"\n  },\n  \"dependencies\": {\n    \"@executor-js/plugin-graphql\": \"^0.0.1-beta.2\",\n    \"@executor-js/plugin-mcp\": \"^0.0.1-beta.2\",\n    \"@executor-js/plugin-openapi\": \"^0.0.1-beta.2\",\n    \"@executor-js/sdk\": \"^0.0.1-beta.2\",\n    \"@ff-labs/fff-node\": \"^0.5.2\",\n    \"jsonc-parser\": \"^3.3.1\",\n    \"secure-exec\": \"^0.2.1\",\n    \"turndown\": \"^7.2.4\"\n  },\n  \"devDependencies\": {},\n  \"pi\": {\n    \"extensions\": [\n      \"./index.ts\"\n    ]\n  },\n  \"packageManager\": \"bun@1.3.12\"\n}"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/builtins.ts",
    "content": "import {\n  createBashTool,\n  createEditTool,\n  createFindTool,\n  createGrepTool,\n  createLsTool,\n  createReadTool,\n  createWriteTool,\n} from \"@mariozechner/pi-coding-agent\";\nimport { createCustomReadTool } from \"./read.js\";\nimport { webfetch } from \"./webfetch.js\";\nimport type { BuiltinToolName } from \"./types.js\";\nimport { Type } from \"@sinclair/typebox\";\n\nexport type BuiltinTool = {\n  name: string;\n  description: string;\n  parameters: unknown;\n  execute: (...args: any[]) => Promise<unknown>;\n};\n\n// Only package managers — everything else has a codemode tool equivalent\nconst BASH_COMMAND_WHITELIST = new Set([\n  // Package managers\n  \"vp\",\n  \"vpx\",\n  \"npm\",\n  \"pnpm\",\n  \"bun\",\n  \"yarn\",\n  \"npx\",\n  // File operations (no codemode tool equivalent)\n  \"mkdir\",\n  \"touch\",\n  \"cp\",\n  \"mv\",\n  // Utilities\n  \"pwd\",\n  \"which\",\n  \"type\",\n  \"uname\",\n]);\n\nconst WHITELIST_HELP = () => [\n  `Allowed commands: ${[...BASH_COMMAND_WHITELIST].sort().join(\", \")}`,\n  \"Use tools.pi.* for reading, writing, searching, grepping, editing files.\",\n].join(\"\\n\");\n\nfunction extractCommandName(command: string): string | null {\n  // Strip shell redirects, pipes, chaining, then grab first word\n  const cleaned = command\n    .replace(/\\\\s*[|&;<>]/g, \" \")\n    .replace(/\\(/g, \" \")\n    .trim();\n  const first = cleaned.split(/\\s+/)[0];\n  if (!first) return null;\n  // Strip leading path (e.g., ./node_modules/.bin/npm) — not whitelisted\n  if (first.includes(\"/\")) return null;\n  return first;\n}\n\nfunction createWhitelistedBashTool(cwd: string): BuiltinTool {\n  const bashTool = createBashTool(cwd) as BuiltinTool;\n\n  return {\n    name: bashTool.name,\n    description:\n      \"Run package manager commands (npm, pnpm, bun, yarn, npx, node). Other commands are blocked — use tools.pi.* instead.\",\n    parameters: bashTool.parameters,\n    async execute(toolCallId: string, args: any, signal: AbortSignal | undefined, onUpdate: any) {\n      const command = String(args?.command ?? \"\").trim();\n      if (!command) {\n        return {\n          content: [{ type: \"text\", text: \"No command provided.\\n\\n\" + WHITELIST_HELP() }],\n        };\n      }\n\n      const cmdName = extractCommandName(command);\n\n      if (!cmdName || !BASH_COMMAND_WHITELIST.has(cmdName)) {\n        const blocked = cmdName ? `\"${cmdName}\" is not whitelisted` : \"Could not parse command\";\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: [`Error: ${blocked}`, \"\", WHITELIST_HELP()].join(\"\\n\"),\n            },\n          ],\n        };\n      }\n\n      // Proxy: forward execute via original tool's execute\n      const result = await bashTool.execute(toolCallId, args, signal, onUpdate);\n      return result;\n    },\n  };\n}\n\nconst webfetchTool: BuiltinTool = {\n  name: \"webfetch\",\n  description:\n    \"Fetch web content and convert to markdown, text, or html. Use when URLs are mentioned to retrieve content. HTTP URLs upgraded to HTTPS. Images and binary content return empty text.\",\n  parameters: Type.Object({\n    url: Type.String({ description: \"The URL to fetch content from\" }),\n    format: Type.Optional(\n      Type.Union([Type.Literal(\"markdown\"), Type.Literal(\"text\"), Type.Literal(\"html\")], {\n        description: \"Output format: markdown (default), text, or html\",\n      }),\n    ),\n    timeout: Type.Optional(\n      Type.Number({ description: \"Timeout in seconds (max 120, default 30)\" }),\n    ),\n  }),\n  execute: webfetch,\n};\n\nexport type BuiltinToolset = Record<BuiltinToolName, BuiltinTool>;\n\nconst PI_SIGNATURES = [\n  \"tools.pi.read({ path, offset?, limit? })\",\n  \"tools.pi.bash({ command, timeout? })\",\n  \"tools.pi.edit({ path, edits: [{ oldText, newText }] })\",\n  \"tools.pi.write({ path, content })\",\n  \"tools.pi.grep({ pattern, path?, glob?, ignoreCase?, literal?, context?, limit? })\",\n  \"tools.pi.find({ pattern, path?, limit? })\",\n  \"tools.pi.ls({ path?, limit? })\",\n  \"tools.pi.webfetch({ url, format?, timeout? })\",\n];\n\nconst FFF_SIGNATURES = [\n  \"tools.fff.grep({ pattern, path?, mode?, smartCase?, glob?, maxMatchesPerFile?, context?, classifyDefinitions?, limit? })\",\n  \"tools.fff.fileSearch({ query, path?, limit? })\",\n  \"tools.fff.multiGrep({ patterns, path?, limit? })\",\n  \"tools.fff.recentFiles({ path?, limit?, minFrecencyScore?, includeUntracked? })\",\n  \"tools.fff.searchThenGrep({ pathQuery, contentQuery, path?, maxFiles?, limit? })\",\n];\n\nconst META_APIS = [\"tools.list()\", \"tools.schema(name)\", \"tools.definitions()\"];\n\nexport function createBuiltinToolset(cwd: string) {\n  return {\n    read: createCustomReadTool(cwd) as BuiltinTool,\n    edit: createEditTool(cwd) as BuiltinTool,\n    write: createWriteTool(cwd) as BuiltinTool,\n    grep: createGrepTool(cwd) as BuiltinTool,\n    find: createFindTool(cwd) as BuiltinTool,\n    ls: createLsTool(cwd) as BuiltinTool,\n    bash: createWhitelistedBashTool(cwd) as BuiltinTool,\n    webfetch: webfetchTool,\n  };\n}\n\nexport function buildCodemodeApiPrompt(): string {\n  return [\n    \"## Available APIs\",\n    \"\",\n    \"### pi tools\",\n    ...PI_SIGNATURES,\n    \"\",\n    \"### fff tools\",\n    ...FFF_SIGNATURES,\n    \"\",\n    \"### Discovery\",\n    ...META_APIS,\n    \"\",\n    \"All pi and fff tools return plain text strings. Other tools (MCP, OpenAPI, GraphQL) return objects.\",\n    \"If the user mentions an unknown tool, call tools.list() to get a list of available tools\",\n    \"Before using any unfamiliar tool, call tools.schema('tool.name') to get its exact parameter and return types.\",\n    \"Dynamically loaded tools are namespaced: tools.openapi.petstore.listPets, tools.mcp.myServer.search.\",\n    \"Prefer one codemode call; batch work with JS and Promise.all.\",\n    \"\",\n    \"### Bash restrictions\",\n    `tools.pi.bash only allows: ${[...BASH_COMMAND_WHITELIST].sort().join(\", \")}.`,\n    \"Everything else is blocked — use the dedicated tools above instead.\",\n  ].join(\"\\n\");\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/codemode.ts",
    "content": "import { Type } from \"@sinclair/typebox\";\nimport { Text } from \"@mariozechner/pi-tui\";\nimport type { ExtensionContext, Theme, ToolDefinition } from \"@mariozechner/pi-coding-agent\";\nimport { buildCodemodeApiPrompt } from \"./builtins.js\";\nimport { getExecutor } from \"./executor-cache.js\";\nimport { renderCodemodeCall, renderCodemodeResult } from \"./render.js\";\nimport { runCodemode } from \"./runtime.js\";\nimport { formatTraceForAgent, summarizeTraceForContext } from \"./trace.js\";\nimport type { CodemodeResultDetails } from \"./types.js\";\nimport { buildPromptGuidelines, stripCodeFences } from \"./util.js\";\n\nconst codemodeSchema = Type.Object({\n  code: Type.String({ description: \"JavaScript code with tools.pi.* access\" }),\n});\n\nexport function createCodemodeTool(): ToolDefinition<typeof codemodeSchema, CodemodeResultDetails> {\n  return {\n    name: \"codemode\",\n    label: \"codemode\",\n    description: \"Execute JavaScript in a secure sandbox with access to pi filesystem tools, fff search tools, and dynamically loaded executor tools (MCP, OpenAPI, GraphQL).\",\n    promptSnippet: buildCodemodeApiPrompt(),\n    promptGuidelines: buildPromptGuidelines(),\n    parameters: codemodeSchema,\n\n    async execute(\n      _toolCallId: string,\n      params: { code: string },\n      signal: AbortSignal | undefined,\n      _onUpdate: unknown,\n      ctx: ExtensionContext,\n    ) {\n      const code = stripCodeFences(params.code);\n      const executor = await getExecutor(ctx.cwd);\n      const result = await runCodemode({\n        code,\n        cwd: ctx.cwd,\n        executor,\n        signal,\n      });\n      const { text, images } = formatTraceForAgent(result.trace, result.value, result.logs);\n\n      return {\n        content: [\n          { type: \"text\", text },\n          ...images.map((img) => ({ type: \"image\" as const, data: img.data, mimeType: img.mimeType })),\n        ],\n        details: {\n          trace: result.trace,\n          value: result.value,\n          logs: result.logs,\n          summary: summarizeTraceForContext(result.trace),\n        },\n      };\n    },\n\n    renderCall(args: { code?: string }, theme: Theme) {\n      return renderCodemodeCall(args.code ?? \"\", theme);\n    },\n\n    renderResult(result, options, theme) {\n      const trace = result.details?.trace;\n      if (!trace) return new Text(theme.fg(\"error\", \"Missing codemode trace\"), 0, 0);\n      if (options.isPartial) return new Text(theme.fg(\"warning\", \"Executing...\"), 0, 0);\n      return renderCodemodeResult(trace, options.expanded, theme);\n    },\n  };\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/executor-cache.ts",
    "content": "import { createExecutor, type Executor } from \"@executor-js/sdk\";\nimport { mcpPlugin } from \"@executor-js/plugin-mcp\";\nimport { openApiPlugin } from \"@executor-js/plugin-openapi\";\nimport { graphqlPlugin } from \"@executor-js/plugin-graphql\";\nimport { piPlugin } from \"./pi-plugin.js\";\nimport { fffPlugin } from \"./fff-plugin.js\";\nimport { getExecutorConfigPath, loadSourcesFromConfig } from \"./source-config.js\";\nimport { hydrateExecutorSources } from \"./source-hydrate.js\";\nimport { stat } from \"node:fs/promises\";\n\ntype CachedExecutor = {\n  executor: Executor;\n  mtimeMs: number;\n};\n\nconst cache = new Map<string, CachedExecutor>();\n\nconst configMtime = async (): Promise<number> => {\n  try {\n    return (await stat(getExecutorConfigPath())).mtimeMs;\n  } catch {\n    return 0;\n  }\n};\n\nexport const getExecutor = async (cwd: string): Promise<Executor> => {\n  const mtimeMs = await configMtime();\n  const cached = cache.get(cwd);\n  if (cached && cached.mtimeMs === mtimeMs) return cached.executor;\n\n  if (cached) {\n    await cached.executor.close();\n    cache.delete(cwd);\n  }\n\n  const loaded = await loadSourcesFromConfig();\n\n  const executor = await createExecutor({\n    scope: { name: \"pi-codemode-\" + cwd },\n    plugins: [\n      piPlugin(cwd),\n      fffPlugin(cwd),\n      mcpPlugin(),\n      openApiPlugin(),\n      graphqlPlugin(),\n    ] as const,\n  });\n\n  await hydrateExecutorSources(executor, loaded.sources, {\n    configPath: loaded.configPath,\n    unsupported: loaded.unsupported,\n  });\n\n  cache.set(cwd, { executor, mtimeMs: loaded.mtimeMs });\n  return executor;\n};\n\nexport const disposeExecutor = async (cwd: string): Promise<void> => {\n  const cached = cache.get(cwd);\n  if (!cached) return;\n  await cached.executor.close();\n  cache.delete(cwd);\n};\n\nexport const disposeAllExecutors = async (): Promise<void> => {\n  await Promise.allSettled(Array.from(cache.values()).map((entry) => entry.executor.close()));\n  cache.clear();\n};"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/fff-plugin.ts",
    "content": "import {\n  definePlugin,\n  ToolRegistration,\n  ToolId,\n  ToolInvocationResult,\n  type PluginContext,\n} from \"@executor-js/sdk\";\nimport { createFffBuiltinToolset, initFinder } from \"./fff.js\";\nimport { fffToolNames, type FffToolName } from \"./types.js\";\nimport { formatError } from \"./util.js\";\n\nexport const fffPlugin = (cwd: string) =>\n  definePlugin({\n    key: \"fff\",\n    init: async (ctx: PluginContext) => {\n      // Eagerly initialize finder on plugin load\n      await initFinder(cwd);\n\n      const tools = createFffBuiltinToolset(cwd);\n\n      // Register fff.* SIMD-accelerated search tools\n      await ctx.tools.registerInvoker(\"fff\", {\n        invoke: async (toolId: string, args: unknown, _options: unknown) => {\n          const name = toolId.split(\".\").pop() as FffToolName | undefined;\n          if (!name || !(name in tools)) {\n            return new ToolInvocationResult({ data: null, error: \"Unknown fff tool: \" + toolId });\n          }\n\n          const tool = tools[name];\n          try {\n            const data = await tool.execute(toolId, args ?? {}, undefined, () => {});\n            return new ToolInvocationResult({ data, error: null });\n          } catch (cause) {\n            return new ToolInvocationResult({ data: null, error: formatError(cause) });\n          }\n        },\n      });\n\n      await ctx.tools.register(\n        fffToolNames.map((name) => {\n          const tool = tools[name];\n          return new ToolRegistration({\n            id: ToolId.make(\"fff.\" + name),\n            pluginKey: \"fff\",\n            sourceId: \"pi\",\n            name,\n            description: tool.description,\n            inputSchema: tool.parameters as Record<string, unknown>,\n          });\n        }),\n      );\n\n      return { extension: {} };\n    },\n  });"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/fff.ts",
    "content": "import { FileFinder } from \"@ff-labs/fff-node\";\nimport { Type } from \"@sinclair/typebox\";\nimport { resolve } from \"node:path\";\nimport type { BuiltinTool } from \"./builtins.js\";\nimport type { FffToolName } from \"./types.js\";\n\n// Global instance cache\nconst finderCache = new Map<string, FileFinder>();\n\nasync function getFinder(cwd: string, searchPath?: string): Promise<FileFinder> {\n  const basePath = resolve(cwd, searchPath || \".\");\n  if (!finderCache.has(basePath)) {\n    const result = FileFinder.create({ basePath });\n    if (!result.ok) throw new Error(result.error || \"unknown\");\n    finderCache.set(basePath, result.value);\n    result.value.waitForScan(5000).catch(() => console.log(\"fff scan timeout\"));\n  }\n  return finderCache.get(basePath)!;\n}\n\n// Eagerly initialize finder for a path (call on session start)\nexport async function initFinder(cwd: string, searchPath?: string): Promise<void> {\n  await getFinder(cwd, searchPath);\n}\n\nexport function destroyAllFinders(): void {\n  for (const f of finderCache.values()) f.destroy();\n  finderCache.clear();\n}\n\n// Grep tool\nexport const grepTool: BuiltinTool = {\n  name: \"grep\",\n  description: \"Search file contents using fff (SIMD-accelerated). Use as tools.fff.grep()\",\n  parameters: Type.Object({\n    pattern: Type.String(),\n    path: Type.Optional(Type.String()),\n    mode: Type.Optional(\n      Type.Union([Type.Literal(\"plain\"), Type.Literal(\"regex\"), Type.Literal(\"fuzzy\")]),\n    ),\n    smartCase: Type.Optional(Type.Boolean()),\n    glob: Type.Optional(Type.String()),\n    maxMatchesPerFile: Type.Optional(Type.Number()),\n    context: Type.Optional(Type.Number()),\n    classifyDefinitions: Type.Optional(Type.Boolean()),\n    limit: Type.Optional(Type.Number()),\n  }),\n  execute: async (_id: string, args: any, cwd: string) => {\n    const finder = await getFinder(cwd, args.path);\n    const query = args.glob ? args.glob + \" \" + args.pattern : args.pattern;\n    const res = finder.grep(query, {\n      mode: args.mode || \"plain\",\n      maxFileSize: args.maxFileSize || 0,\n      maxMatchesPerFile: args.maxMatchesPerFile || 0,\n      smartCase: args.smartCase ?? true,\n      beforeContext: args.context || 0,\n      afterContext: args.context || 0,\n      classifyDefinitions: args.classifyDefinitions || false,\n    } as any);\n    if (!res.ok) throw new Error(res.error);\n    const items = args.limit ? res.value.items.slice(0, args.limit) : res.value.items;\n    return {\n      matches: items.map((m: any) => ({\n        path: m.relativePath,\n        line: m.lineNumber,\n        column: m.col,\n        content: m.lineContent,\n        matchRanges: m.matchRanges,\n        isDefinition: m.isDefinition ?? false,\n      })),\n      totalMatched: res.value.totalMatched,\n      totalFilesSearched: res.value.totalFilesSearched,\n      nextCursor: res.value.nextCursor,\n    };\n  },\n};\n\n// File search tool\nexport const fileSearchTool: BuiltinTool = {\n  name: \"fileSearch\",\n  description: \"Fuzzy search for files by path. Use as tools.fff.fileSearch()\",\n  parameters: Type.Object({\n    query: Type.String(),\n    path: Type.Optional(Type.String()),\n    limit: Type.Optional(Type.Number()),\n  }),\n  execute: async (_id: string, args: any, cwd: string) => {\n    const finder = await getFinder(cwd, args.path);\n    const res = finder.fileSearch(args.query, { pageSize: args.limit || 20 });\n    if (!res.ok) throw new Error(res.error);\n    return {\n      files: res.value.items.map((item: any) => ({\n        path: item.relativePath,\n        name: item.fileName,\n        gitStatus: item.gitStatus,\n        frecencyScore: item.totalFrecencyScore,\n      })),\n    };\n  },\n};\n\n// Multi-pattern grep\nexport const multiGrepTool: BuiltinTool = {\n  name: \"multiGrep\",\n  description: \"Multi-pattern search (OR logic). Use as tools.fff.multiGrep()\",\n  parameters: Type.Object({\n    patterns: Type.Array(Type.String()),\n    path: Type.Optional(Type.String()),\n    limit: Type.Optional(Type.Number()),\n  }),\n  execute: async (_id: string, args: any, cwd: string) => {\n    const finder = await getFinder(cwd, args.path);\n    const res = finder.multiGrep({\n      patterns: args.patterns,\n      smartCase: true,\n    });\n    if (!res.ok) throw new Error(res.error);\n    const items = args.limit ? res.value.items.slice(0, args.limit) : res.value.items;\n    return {\n      matches: items.map((m: any) => ({\n        path: m.relativePath,\n        line: m.lineNumber,\n        content: m.lineContent,\n      })),\n    };\n  },\n};\n\n// Recent files\nexport const recentFilesTool: BuiltinTool = {\n  name: \"recentFiles\",\n  description: \"Get recently accessed files (frecency). Use as tools.fff.recentFiles()\",\n  parameters: Type.Object({\n    path: Type.Optional(Type.String()),\n    limit: Type.Optional(Type.Number()),\n    minFrecencyScore: Type.Optional(Type.Number()),\n    includeUntracked: Type.Optional(Type.Boolean()),\n  }),\n  execute: async (_id: string, args: any, cwd: string) => {\n    const finder = await getFinder(cwd, args.path);\n    const res = finder.fileSearch(\"\", { pageSize: (args.limit || 20) * 2 });\n    if (!res.ok) throw new Error(res.error);\n    let files = res.value.items.map((item: any, i: number) => ({\n      path: item.relativePath,\n      name: item.fileName,\n      gitStatus: item.gitStatus,\n      frecencyScore: res.value.scores[i]?.baseScore || 0,\n    }));\n    if (args.minFrecencyScore)\n      files = files.filter((f: any) => f.frecencyScore >= args.minFrecencyScore);\n    if (args.includeUntracked === false) files = files.filter((f: any) => f.gitStatus !== \"??\");\n    if (args.limit) files = files.slice(0, args.limit);\n    return { files };\n  },\n};\n\n// Search then grep\nexport const searchThenGrepTool: BuiltinTool = {\n  name: \"searchThenGrep\",\n  description: \"Fuzzy search files then grep within. Use as tools.fff.searchThenGrep()\",\n  parameters: Type.Object({\n    pathQuery: Type.String(),\n    contentQuery: Type.String(),\n    path: Type.Optional(Type.String()),\n    maxFiles: Type.Optional(Type.Number()),\n    limit: Type.Optional(Type.Number()),\n  }),\n  execute: async (_id: string, args: any, cwd: string) => {\n    const finder = await getFinder(cwd, args.path);\n    const fileRes = finder.fileSearch(args.pathQuery, { pageSize: args.maxFiles || 50 });\n    if (!fileRes.ok) throw new Error(fileRes.error);\n    if (fileRes.value.items.length === 0) return { matches: [], fileMatches: 0 };\n    const paths = fileRes.value.items.map((item: any) => item.relativePath);\n    const res = finder.grep(args.contentQuery + \" \" + paths.join(\" \"), { mode: \"plain\" });\n    if (!res.ok) throw new Error(res.error);\n    const items = args.limit ? res.value.items.slice(0, args.limit) : res.value.items;\n    return {\n      matches: items.map((m: any) => ({\n        path: m.relativePath,\n        line: m.lineNumber,\n        content: m.lineContent,\n      })),\n      fileMatches: paths.length,\n      totalMatched: res.value.totalMatched,\n    };\n  },\n};\n\nexport type FffBuiltinToolset = Record<FffToolName, BuiltinTool>;\n\nexport function createFffBuiltinToolset(cwd: string): FffBuiltinToolset {\n  const withCwd = (tool: BuiltinTool): BuiltinTool => ({\n    ...tool,\n    execute: async (_toolCallId: string, args: any, _signal: any, _onUpdate: any) =>\n      tool.execute(_toolCallId, args, cwd),\n  });\n\n  return {\n    grep: withCwd(grepTool),\n    fileSearch: withCwd(fileSearchTool),\n    multiGrep: withCwd(multiGrepTool),\n    recentFiles: withCwd(recentFilesTool),\n    searchThenGrep: withCwd(searchThenGrepTool),\n  };\n}"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/jj-plugin.ts",
    "content": "import {\n  definePlugin,\n  ToolRegistration,\n  ToolId,\n  ToolInvocationResult,\n  type PluginContext,\n} from \"@executor-js/sdk\";\nimport { spawn } from \"node:child_process\";\nimport { formatError } from \"./util.js\";\n\nconst run = async (args: string[], cwd: string, signal?: AbortSignal) =>\n  await new Promise((resolve, reject) => {\n    const child = spawn(\"jj\", args, { cwd, signal, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n    let stdout = \"\";\n    let stderr = \"\";\n    child.stdout.on(\"data\", (d) => (stdout += d.toString()));\n    child.stderr.on(\"data\", (d) => (stderr += d.toString()));\n    child.on(\"error\", reject);\n    child.on(\"close\", (code) =>\n      resolve({ code: code ?? 0, stdout, stderr, command: [\"jj\", ...args] }),\n    );\n  });\nconst summarize = (r: any) =>\n  [\n    \"$ \" + r.command.join(\" \"),\n    r.stdout?.trim(),\n    r.stderr?.trim() ? \"stderr: \" + r.stderr.trim() : \"\",\n    \"exit \" + r.code,\n  ]\n    .filter(Boolean)\n    .join(\" \");\nconst text = (s: string) => [{ type: \"text\", text: s }];\n\nexport const jjPlugin = (cwd: string) =>\n  definePlugin({\n    key: \"jj\",\n    init: async (ctx: PluginContext) => {\n      const tools = {\n        status: {\n          description: \"Show jj status\",\n          parameters: { type: \"object\", properties: {}, additionalProperties: false },\n          execute: async () => {\n            const r = await run([\"status\"], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        diff: {\n          description: \"Show jj diff\",\n          parameters: { type: \"object\", properties: {}, additionalProperties: false },\n          execute: async () => {\n            const r = await run([\"diff\"], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        log: {\n          description: \"Show jj log\",\n          parameters: { type: \"object\", properties: {}, additionalProperties: false },\n          execute: async () => {\n            const r = await run([\"log\"], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        new: {\n          description: \"Create a new jj change\",\n          parameters: { type: \"object\", properties: {}, additionalProperties: false },\n          execute: async () => {\n            const r = await run([\"new\"], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        describe: {\n          description: \"Describe the current jj change\",\n          parameters: {\n            type: \"object\",\n            properties: { description: { type: \"string\" } },\n            required: [\"description\"],\n            additionalProperties: false,\n          },\n          execute: async (_id: string, args: any) => {\n            const description = String(args?.description ?? \"\");\n            if (!description) throw new Error(\"Missing description\");\n            const r = await run([\"describe\", \"-m\", description], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        commit: {\n          description: \"Commit with jj\",\n          parameters: {\n            type: \"object\",\n            properties: { message: { type: \"string\" } },\n            required: [\"message\"],\n            additionalProperties: false,\n          },\n          execute: async (_id: string, args: any) => {\n            const message = String(args?.message ?? \"\");\n            if (!message) throw new Error(\"Missing message\");\n            const r = await run([\"commit\", \"-m\", message], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n      } as const;\n      await ctx.tools.registerInvoker(\"jj\", {\n        invoke: async (toolId: string, args: unknown) => {\n          const name = toolId.split(\".\").pop() as keyof typeof tools | undefined;\n          if (!name || !(name in tools))\n            return new ToolInvocationResult({ data: null, error: \"Unknown jj tool: \" + toolId });\n          try {\n            return new ToolInvocationResult({\n              data: await tools[name].execute(toolId, args ?? {}),\n              error: null,\n            });\n          } catch (cause) {\n            return new ToolInvocationResult({ data: null, error: formatError(cause) });\n          }\n        },\n      });\n      await ctx.tools.register(\n        Object.entries(tools).map(\n          ([name, tool]) =>\n            new ToolRegistration({\n              id: ToolId.make(\"jj.\" + name),\n              pluginKey: \"jj\",\n              sourceId: \"pi\",\n              name,\n              description: tool.description,\n              inputSchema: tool.parameters as Record<string, unknown>,\n            }),\n        ),\n      );\n      return { extension: {} };\n    },\n  });\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/npm-plugin.ts",
    "content": "import {\n  definePlugin,\n  ToolRegistration,\n  ToolId,\n  ToolInvocationResult,\n  type PluginContext,\n} from \"@executor-js/sdk\";\nimport { spawn } from \"node:child_process\";\nimport { promises as fs } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { formatError } from \"./util.js\";\n\nconst readMaybe = async (path: string) => {\n  try {\n    return await fs.readFile(path, \"utf8\");\n  } catch {\n    return null;\n  }\n};\n\nconst pmCache = new Map<string, { bin: string; args: readonly string[] }>();\n\nconst detectPackageManager = async (cwd: string) => {\n  const cached = pmCache.get(cwd);\n  if (cached) return cached;\n\n  let result: { bin: string; args: readonly string[] };\n  if ((await readMaybe(join(cwd, \"pnpm-lock.yaml\"))) !== null)\n    result = { bin: \"pnpm\", args: [\"add\"] as const };\n  else if ((await readMaybe(join(cwd, \"bun.lockb\"))) !== null)\n    result = { bin: \"bun\", args: [\"add\"] as const };\n  else if ((await readMaybe(join(cwd, \"bun.lock\"))) !== null)\n    result = { bin: \"bun\", args: [\"add\"] as const };\n  else if ((await readMaybe(join(cwd, \"package-lock.json\"))) !== null)\n    result = { bin: \"npm\", args: [\"install\"] as const };\n  else\n    result = { bin: \"npm\", args: [\"install\"] as const };\n\n  pmCache.set(cwd, result);\n  return result;\n};\n\nconst run = async (cmd: string, args: string[], cwd: string, signal?: AbortSignal) =>\n  await new Promise((resolve, reject) => {\n    const child = spawn(cmd, args, { cwd, signal, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n    let stdout = \"\";\n    let stderr = \"\";\n    child.stdout.on(\"data\", (d) => (stdout += d.toString()));\n    child.stderr.on(\"data\", (d) => (stderr += d.toString()));\n    child.on(\"error\", reject);\n    child.on(\"close\", (code) =>\n      resolve({ code: code ?? 0, stdout, stderr, command: [cmd, ...args] }),\n    );\n  });\n\nconst summarize = (r: any) =>\n  [\n    \"$ \" + r.command.join(\" \"),\n    r.stdout?.trim(),\n    r.stderr?.trim() ? \"stderr: \" + r.stderr.trim() : \"\",\n    \"exit \" + r.code,\n  ]\n    .filter(Boolean)\n    .join(\" \");\nconst text = (s: string) => [{ type: \"text\", text: s }];\n\nexport const npmPlugin = (cwd: string) =>\n  definePlugin({\n    key: \"npm\",\n    init: async (ctx: PluginContext) => {\n      const tools = {\n        run: {\n          description: \"Run npm scripts\",\n          parameters: {\n            type: \"object\",\n            properties: { name: { type: \"string\" } },\n            required: [\"name\"],\n            additionalProperties: false,\n          },\n          execute: async (_id: string, args: any) => {\n            const name = String(args?.name ?? \"\");\n            if (!name) throw new Error(\"Missing script name\");\n            const r = await run(\"npm\", [\"run\", name], cwd);\n            return { content: text(summarize(r)), details: { result: r } };\n          },\n        },\n        install: {\n          description: \"Install packages with lockfile-aware package manager\",\n          parameters: {\n            type: \"object\",\n            properties: { packages: { type: \"array\", items: { type: \"string\" } } },\n            required: [\"packages\"],\n            additionalProperties: false,\n          },\n          execute: async (_id: string, args: any) => {\n            const packages = Array.isArray(args?.packages)\n              ? args.packages.map(String).filter(Boolean)\n              : [];\n            if (!packages.length) throw new Error(\"Missing packages\");\n            const pm = await detectPackageManager(cwd);\n            const r = await run(pm.bin, [...pm.args, ...packages], cwd);\n            return { content: text(summarize(r)), details: { result: r, packageManager: pm.bin } };\n          },\n        },\n      } as const;\n      await ctx.tools.registerInvoker(\"npm\", {\n        invoke: async (toolId: string, args: unknown) => {\n          const name = toolId.split(\".\").pop() as keyof typeof tools | undefined;\n          if (!name || !(name in tools))\n            return new ToolInvocationResult({ data: null, error: \"Unknown npm tool: \" + toolId });\n          try {\n            return new ToolInvocationResult({\n              data: await tools[name].execute(toolId, args ?? {}),\n              error: null,\n            });\n          } catch (cause) {\n            return new ToolInvocationResult({ data: null, error: formatError(cause) });\n          }\n        },\n      });\n      await ctx.tools.register(\n        Object.entries(tools).map(\n          ([name, tool]) =>\n            new ToolRegistration({\n              id: ToolId.make(\"npm.\" + name),\n              pluginKey: \"npm\",\n              sourceId: \"pi\",\n              name,\n              description: tool.description,\n              inputSchema: tool.parameters as Record<string, unknown>,\n            }),\n        ),\n      );\n      return { extension: {} };\n    },\n  });"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/pi-plugin.ts",
    "content": "import {\n  definePlugin,\n  ToolRegistration,\n  ToolId,\n  ToolInvocationResult,\n  type PluginContext,\n} from \"@executor-js/sdk\";\nimport { createBuiltinToolset } from \"./builtins.js\";\nimport { builtinToolNames, type BuiltinToolName } from \"./types.js\";\nimport { formatError } from \"./util.js\";\n\nexport const piPlugin = (cwd: string) =>\n  definePlugin({\n    key: \"pi\",\n    init: async (ctx: PluginContext) => {\n      const tools = createBuiltinToolset(cwd);\n\n      await ctx.tools.registerInvoker(\"pi\", {\n        invoke: async (toolId: string, args: unknown) => {\n          const name = toolId.split(\".\").pop() as BuiltinToolName | undefined;\n          if (!name || !(name in tools)) {\n            return new ToolInvocationResult({ data: null, error: `Unknown pi tool: ${toolId}` });\n          }\n\n          const tool = tools[name];\n          try {\n            const result = await tool.execute(toolId, args ?? {}, undefined, () => {});\n            return new ToolInvocationResult({ data: result, error: null });\n          } catch (cause) {\n            return new ToolInvocationResult({ data: null, error: formatError(cause) });\n          }\n        },\n      });\n\n      await ctx.tools.register(\n        builtinToolNames.map((name) => {\n          const tool = tools[name];\n          return new ToolRegistration({\n            id: ToolId.make(`pi.${name}`),\n            pluginKey: \"pi\",\n            sourceId: \"pi\",\n            name,\n            description: tool.description,\n            inputSchema: tool.parameters as Record<string, unknown>,\n          });\n        }),\n      );\n\n      return { extension: {} };\n    },\n  });"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/read.ts",
    "content": "import { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { constants } from \"fs\";\nimport { extname } from \"path\";\nimport type { ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\n\n// Much higher defaults than the built-in read tool (2000 lines / 50KB)\nexport const CUSTOM_MAX_LINES = 100_000;\nexport const CUSTOM_MAX_BYTES = 10 * 1024 * 1024; // 10MB\n\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n  \".jpg\": \"image/jpeg\",\n  \".jpeg\": \"image/jpeg\",\n  \".png\": \"image/png\",\n  \".gif\": \"image/gif\",\n  \".webp\": \"image/webp\",\n};\n\nfunction isImageFile(filePath: string): string | null {\n  const ext = extname(filePath).toLowerCase();\n  return IMAGE_MIME_TYPES[ext] || null;\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes}B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;\n  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n}\n\nconst readSchema = Type.Object({\n  path: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n  offset: Type.Optional(\n    Type.Number({ description: \"Line number to start reading from (1-indexed)\" }),\n  ),\n  limit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport interface ReadToolOptions {\n  maxLines?: number;\n  maxBytes?: number;\n}\n\nexport function createCustomReadTool(cwd: string, options?: ReadToolOptions) {\n  const maxLines = options?.maxLines ?? CUSTOM_MAX_LINES;\n  const maxBytes = options?.maxBytes ?? CUSTOM_MAX_BYTES;\n\n  return {\n    name: \"read\",\n    label: \"read\",\n    description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${maxLines.toLocaleString()} lines or ${formatSize(maxBytes)} (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n    parameters: readSchema,\n    execute: async (\n      _toolCallId: string,\n      { path, offset, limit }: { path: string; offset?: number; limit?: number },\n      signal?: AbortSignal,\n    ): Promise<{ content: (TextContent | ImageContent)[] }> => {\n      if (signal?.aborted) throw new Error(\"Operation aborted\");\n\n      const absolutePath = path.startsWith(\"/\") ? path : `${cwd}/${path}`;\n\n      // Check accessibility\n      try {\n        await fsAccess(absolutePath, constants.R_OK);\n      } catch {\n        throw new Error(`File not readable: ${path}`);\n      }\n\n      const mimeType = isImageFile(absolutePath);\n      if (mimeType) {\n        // Read image as base64\n        const buffer = await fsReadFile(absolutePath);\n        const base64 = buffer.toString(\"base64\");\n        return {\n          content: [\n            { type: \"text\", text: `Read image file [${mimeType}]` },\n            { type: \"image\", data: base64, mimeType },\n          ],\n        };\n      }\n\n      // Read text file\n      const buffer = await fsReadFile(absolutePath);\n      const textContent = buffer.toString(\"utf-8\");\n      const allLines = textContent.split(\"\\n\");\n      const totalFileLines = allLines.length;\n\n      // Apply offset (1-indexed)\n      const startLine = offset ? Math.max(0, offset - 1) : 0;\n      const startLineDisplay = startLine + 1;\n\n      if (startLine >= allLines.length) {\n        throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n      }\n\n      // Slice content based on offset/limit\n      let selectedLines: string[];\n      let userLimitApplied = false;\n\n      if (limit !== undefined) {\n        const endLine = Math.min(startLine + limit, allLines.length);\n        selectedLines = allLines.slice(startLine, endLine);\n        userLimitApplied = endLine - startLine < allLines.length - startLine;\n      } else {\n        selectedLines = allLines.slice(startLine);\n      }\n\n      // Apply our own truncation (much higher limits)\n      const result = truncateHead(selectedLines.join(\"\\n\"), { maxLines, maxBytes });\n\n      let outputText: string;\n\n      if (result.firstLineExceedsLimit) {\n        const firstLineSize = formatSize(Buffer.byteLength(selectedLines[0] || \"\", \"utf-8\"));\n        outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(maxBytes)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${maxBytes}]`;\n      } else if (result.truncated) {\n        const endLineDisplay = startLineDisplay + result.outputLines - 1;\n        const nextOffset = endLineDisplay + 1;\n        outputText = result.content;\n        if (result.truncatedBy === \"lines\") {\n          outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n        } else {\n          outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(maxBytes)} limit). Use offset=${nextOffset} to continue.]`;\n        }\n      } else if (userLimitApplied) {\n        const endLine = startLine + (limit ?? 0);\n        const remaining = totalFileLines - endLine;\n        const nextOffset = endLine + 1;\n        outputText = result.content;\n        outputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n      } else {\n        outputText = result.content;\n      }\n\n      return {\n        content: [{ type: \"text\", text: outputText }],\n      };\n    },\n  };\n}\n\ninterface TruncationResult {\n  content: string;\n  truncated: boolean;\n  truncatedBy: \"lines\" | \"bytes\" | null;\n  outputLines: number;\n  firstLineExceedsLimit: boolean;\n}\n\ninterface TruncationOptions {\n  maxLines: number;\n  maxBytes: number;\n}\n\nfunction truncateHead(content: string, options: TruncationOptions): TruncationResult {\n  const { maxLines, maxBytes } = options;\n  const totalBytes = Buffer.byteLength(content, \"utf-8\");\n  const lines = content.split(\"\\n\");\n  const totalLines = lines.length;\n\n  if (totalLines <= maxLines && totalBytes <= maxBytes) {\n    return {\n      content,\n      truncated: false,\n      truncatedBy: null,\n      outputLines: totalLines,\n      firstLineExceedsLimit: false,\n    };\n  }\n\n  // Check if first line alone exceeds limit\n  const firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n  if (firstLineBytes > maxBytes) {\n    return {\n      content: \"\",\n      truncated: true,\n      truncatedBy: \"bytes\",\n      outputLines: 0,\n      firstLineExceedsLimit: true,\n    };\n  }\n\n  const outputLinesArr: string[] = [];\n  let outputBytesCount = 0;\n  let truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n  for (let i = 0; i < lines.length && i < maxLines; i++) {\n    const line = lines[i];\n    const lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0);\n    if (outputBytesCount + lineBytes > maxBytes) {\n      truncatedBy = \"bytes\";\n      break;\n    }\n    outputLinesArr.push(line);\n    outputBytesCount += lineBytes;\n  }\n\n  if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n    truncatedBy = \"lines\";\n  }\n\n  return {\n    content: outputLinesArr.join(\"\\n\"),\n    truncated: true,\n    truncatedBy,\n    outputLines: outputLinesArr.length,\n    firstLineExceedsLimit: false,\n  };\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/render.ts",
    "content": "import { Text } from \"@mariozechner/pi-tui\";\nimport type { Theme } from \"@mariozechner/pi-coding-agent\";\nimport type { CodemodeTrace, TraceStep } from \"./types.js\";\n\nconst truncate = (value: string, max: number): string => (value.length <= max ? value : `${value.slice(0, max - 1)}…`);\n\nconst formatDuration = (ms: number): string => {\n  if (ms < 1000) return `${ms}ms`;\n  return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;\n};\n\nconst summarizeStep = (step: TraceStep): string => {\n  if (step.error) return truncate(step.error, 80);\n  if (!step.output?.text) return \"\";\n  return truncate(step.output.text.split(/\\r?\\n/)[0] ?? \"\", 80);\n};\n\nexport function renderCodemodeCall(code: string, theme: Theme): Text {\n  const lines = code.trim().split(/\\r?\\n/);\n  const suffix = lines.length > 1 ? theme.fg(\"muted\", ` (${lines.length} lines)`) : \"\";\n  const shown = lines.slice(0, 12).map((line) => theme.fg(\"accent\", line)).join(\"\\n\");\n\n  let content = `${theme.fg(\"toolTitle\", theme.bold(\"codemode\"))}${suffix}\\n${shown}`;\n  if (lines.length > 12) content += theme.fg(\"muted\", `\\n… (${lines.length - 12} more lines)`);\n\n  return new Text(content, 0, 0);\n}\n\nexport function renderCodemodeResult(trace: CodemodeTrace, expanded: boolean, theme: Theme): Text {\n  const duration = trace.endedAt ? formatDuration(trace.endedAt - trace.startedAt) : \"running\";\n  const isError = trace.status === \"error\" || trace.steps.some((step) => step.status === \"error\");\n  const statusColor = isError ? \"error\" : trace.status === \"ok\" ? \"success\" : \"accent\";\n\n  if (!expanded) {\n    const lines: string[] = [\n      `${theme.fg(\"toolTitle\", theme.bold(\"codemode\"))} ${theme.fg(statusColor, trace.status)} ${theme.fg(\"muted\", duration)}`,\n    ];\n\n    for (const step of trace.steps) {\n      const icon = step.status === \"error\" ? \"✗\" : step.status === \"ok\" ? \"✓\" : \"○\";\n      const iconColor = step.status === \"error\" ? \"error\" : step.status === \"ok\" ? \"success\" : \"muted\";\n      const summary = summarizeStep(step);\n      lines.push(`${theme.fg(iconColor, icon)} ${step.label}${summary ? ` — ${theme.fg(\"muted\", summary)}` : \"\"}`);\n    }\n\n    if (trace.value !== undefined) {\n      const value = typeof trace.value === \"string\" ? trace.value : JSON.stringify(trace.value);\n      lines.push(`→ ${theme.fg(\"success\", truncate(value, 120))}`);\n    }\n\n    if (trace.error) lines.push(`→ ${theme.fg(\"error\", truncate(trace.error, 120))}`);\n\n    return new Text(lines.join(\"\\n\"), 0, 0);\n  }\n\n  const lines: string[] = [\n    theme.fg(\"toolTitle\", theme.bold(\"codemode execution\")),\n    `status: ${theme.fg(statusColor, trace.status)} · ${duration} · ${trace.steps.length} step${trace.steps.length === 1 ? \"\" : \"s\"}`,\n    \"\",\n    theme.fg(\"muted\", \"// executed code:\"),\n    ...trace.code.trim().split(/\\r?\\n/).map((line) => theme.fg(\"accent\", line)),\n  ];\n\n  if (trace.steps.length > 0) {\n    lines.push(\"\", theme.bold(\"steps:\"));\n    for (const step of trace.steps) {\n      const color = step.status === \"error\" ? \"error\" : step.status === \"ok\" ? \"success\" : \"warning\";\n      lines.push(`${theme.fg(color, step.status === \"error\" ? \"✗\" : \"✓\")} ${step.label}`);\n      lines.push(theme.fg(\"muted\", `  input: ${truncate(typeof step.input === \"string\" ? step.input : JSON.stringify(step.input), 240)}`));\n      if (step.output?.text) {\n        for (const line of step.output.text.split(/\\r?\\n/).slice(0, 100)) lines.push(theme.fg(\"toolOutput\", `  ${line}`));\n      }\n      if (step.error) lines.push(theme.fg(\"error\", `  ${step.error}`));\n    }\n  }\n\n  if (trace.logs.length > 0) {\n    lines.push(\"\", theme.bold(`logs (${trace.logs.length}):`));\n    for (const log of trace.logs.slice(0, 200)) lines.push(theme.fg(\"muted\", `  ${log}`));\n    if (trace.logs.length > 200) lines.push(theme.fg(\"muted\", `  … (${trace.logs.length - 200} more)`));\n  }\n\n  if (trace.value !== undefined) {\n    lines.push(\"\", theme.bold(\"result:\"));\n    const value = typeof trace.value === \"string\" ? trace.value : JSON.stringify(trace.value, null, 2);\n    for (const line of value.split(/\\r?\\n/).slice(0, 200)) lines.push(theme.fg(\"success\", line));\n  }\n\n  if (trace.error) {\n    lines.push(\"\", theme.bold(\"error:\"), theme.fg(\"error\", trace.error));\n  }\n\n  return new Text(lines.join(\"\\n\"), 0, 0);\n}"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/runtime.ts",
    "content": "import type { Executor } from \"@executor-js/sdk\";\nimport { acquireSandbox, type SandboxDelegates } from \"./sandbox-cache.js\";\nimport { CodemodeTraceRecorder } from \"./trace.js\";\nimport type { ImageContent } from \"@mariozechner/pi-ai\";\nimport type { CodemodeTrace, ToolResultSnapshot } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_CODE_SIZE = 100_000;\n\ntype RuntimeOptions = {\n  code: string;\n  cwd: string;\n  executor: Executor;\n  timeoutMs?: number;\n  signal?: AbortSignal;\n  onUpdate?: () => void;\n};\n\nexport type CodemodeExecutionResult = {\n  value?: unknown;\n  trace: CodemodeTrace;\n  logs: string[];\n};\n\nconst formatError = (cause: unknown): string => {\n  if (cause instanceof Error) {\n    const message = cause.message.trim();\n    return message.length > 0 ? message : cause.name;\n  }\n  if (typeof cause === \"string\") return cause;\n  if (typeof cause === \"object\" && cause !== null) {\n    if (\"message\" in cause && typeof cause.message === \"string\") {\n      const message = cause.message.trim();\n      if (message.length > 0) return message;\n    }\n    try {\n      return JSON.stringify(cause);\n    } catch {\n      return String(cause);\n    }\n  }\n  return String(cause);\n};\n\nconst isToolResult = (value: unknown): value is { content: unknown[] } =>\n  typeof value === \"object\" &&\n  value !== null &&\n  \"content\" in value &&\n  Array.isArray((value as { content?: unknown[] }).content);\n\nconst extractText = (value: unknown): string => {\n  if (typeof value === \"string\") return value;\n  if (typeof value === \"undefined\") return \"\";\n  if (value === null) return \"null\";\n\n  if (isToolResult(value)) {\n    return value.content\n      .filter(\n        (entry): entry is { type?: unknown; text?: unknown } =>\n          typeof entry === \"object\" && entry !== null,\n      )\n      .map((entry) => {\n        if (entry.type === \"text\" && typeof entry.text === \"string\") return entry.text;\n        return \"\";\n      })\n      .filter((line) => line.length > 0)\n      .join(\" \")\n      .trim();\n  }\n\n  return \"\";\n};\n\nconst extractImages = (value: unknown): ImageContent[] => {\n  if (!isToolResult(value)) return [];\n  return value.content.filter(\n    (entry): entry is ImageContent =>\n      typeof entry === \"object\" &&\n      entry !== null &&\n      (entry as Record<string, unknown>).type === \"image\" &&\n      typeof (entry as Record<string, unknown>).data === \"string\" &&\n      typeof (entry as Record<string, unknown>).mimeType === \"string\",\n  );\n};\n\nconst toSnapshot = (value: unknown, isError = false): ToolResultSnapshot => ({\n  value,\n  text: extractText(value),\n  images: extractImages(value),\n  isError,\n});\n\nconst CODE_PREFIX = [\n  '\"use strict\";',\n  \"\",\n  \"const __invokeTool = SecureExec.bindings.invokeTool;\",\n  \"const __emitLog = SecureExec.bindings.emitLog;\",\n  \"const __emitResult = SecureExec.bindings.emitResult;\",\n  'const __formatArg = (value) => typeof value === \"string\" ? value : JSON.stringify(value);',\n  'const __formatLine = (args) => args.map(__formatArg).join(\" \");',\n  \"\",\n  \"const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {\",\n  \"  get(_target, prop) {\",\n  '    if (prop === \"then\" || typeof prop === \"symbol\") return undefined;',\n  \"    return __makeToolsProxy([...path, String(prop)]);\",\n  \"  },\",\n  \"  apply(_target, _thisArg, args) {\",\n  '    const toolPath = path.join(\".\");',\n  '    if (!toolPath) throw new Error(\"Tool path missing in invocation\");',\n  \"    return Promise.resolve(__invokeTool(toolPath, args[0])).catch((err) => {\",\n  '      throw new Error(err?.error || err?.message || \"Tool invocation failed\");',\n  \"    });\",\n  \"  },\",\n  \"});\",\n  \"const tools = __makeToolsProxy();\",\n  \"\",\n  \"const console = {\",\n  '  log: (...args) => __emitLog(\"log\", __formatLine(args)),',\n  '  warn: (...args) => __emitLog(\"warn\", __formatLine(args)),',\n  '  error: (...args) => __emitLog(\"error\", __formatLine(args)),',\n  '  info: (...args) => __emitLog(\"info\", __formatLine(args)),',\n  '  debug: (...args) => __emitLog(\"debug\", __formatLine(args)),',\n  \"};\",\n  \"\",\n  \"(async () => {\",\n  \"try {\",\n  \"const value = await (async () => {\",\n].join(\"\\n\");\n\nconst CODE_POSTFIX = [\n  \"})();\",\n  \"__emitResult(value);\",\n  \"} catch (error) {\",\n  'const message = error && typeof error === \"object\" ? (error.stack || error.message || String(error)) : String(error);',\n  'process.stderr.write(message + \"\\\\n\");',\n  \"process.exitCode = 1;\",\n  \"}\",\n  \"})();\",\n].join(\"\\n\");\n\nconst buildExecutionSource = (code: string): string =>\n  CODE_PREFIX + \"\\n\" + code + \"\\n\" + CODE_POSTFIX;\n\nconst invokeExecutorApi = async (\n  executor: Executor,\n  path: string,\n  args: unknown,\n): Promise<{ handled: true; data: unknown } | { handled: false }> => {\n  switch (path) {\n    case \"list\":\n    case \"tools.list\":\n      return { handled: true, data: await executor.tools.list(args ?? {}) };\n    case \"schema\":\n    case \"tools.schema\":\n      return { handled: true, data: await executor.tools.schema(String(args)) };\n    case \"definitions\":\n    case \"tools.definitions\":\n      return { handled: true, data: await executor.tools.definitions() };\n\n    case \"sources.list\":\n      return { handled: true, data: await executor.sources.list() };\n    case \"sources.remove\":\n      return { handled: true, data: await executor.sources.remove(String(args)) };\n    case \"sources.refresh\":\n      return { handled: true, data: await executor.sources.refresh(String(args)) };\n    case \"sources.detect\":\n      return { handled: true, data: await executor.sources.detect(String(args)) };\n\n    case \"policies.list\":\n      return { handled: true, data: await executor.policies.list() };\n    case \"policies.add\":\n      return { handled: true, data: await executor.policies.add(args as any) };\n    case \"policies.remove\":\n      return { handled: true, data: await executor.policies.remove(String(args)) };\n\n    case \"secrets.list\":\n      return { handled: true, data: await executor.secrets.list() };\n    case \"secrets.resolve\":\n      return { handled: true, data: await executor.secrets.resolve(String(args)) };\n    case \"secrets.status\":\n      return { handled: true, data: await executor.secrets.status(String(args)) };\n    case \"secrets.set\":\n      return { handled: true, data: await executor.secrets.set(args as any) };\n    case \"secrets.remove\":\n      return { handled: true, data: await executor.secrets.remove(String(args)) };\n    case \"secrets.addProvider\":\n      return { handled: true, data: await executor.secrets.addProvider(args as any) };\n    case \"secrets.providers\":\n      return { handled: true, data: await executor.secrets.providers() };\n\n    default:\n      return { handled: false };\n  }\n};\n\n// --- Tool invocation bridge (host side) ---\n// Uses structured clone via secure-exec bindings — no JSON serialization.\n\nconst createInvokeTool =\n  (executor: Executor, recorder: CodemodeTraceRecorder, onStep?: () => void) =>\n    async (path: unknown, args: unknown): Promise<unknown> => {\n      const toolPath = String(path);\n      const step = recorder.startStep({ label: `tools.${toolPath}`, toolPath, input: args });\n\n      try {\n        const direct = await invokeExecutorApi(executor, toolPath, args);\n        let data: unknown;\n\n        if (direct.handled) {\n          data = direct.data;\n        } else {\n          const invoked = await executor.tools.invoke(toolPath, args, {\n            onElicitation: \"accept-all\",\n          });\n          if (invoked.error != null) {\n            const message = formatError(invoked.error);\n            recorder.finishStep(step, undefined, message);\n            onStep?.();\n            throw { error: message };\n          }\n          data = invoked.data;\n        }\n\n        const snapshot = toSnapshot(data);\n        recorder.finishStep(step, snapshot);\n        onStep?.();\n\n        // pi and fff tools return ToolResult objects ({ content: [...] }) — extract text\n        // for backwards compat. Other tools (MCP, OpenAPI, GraphQL) return objects as-is.\n        if ((toolPath.startsWith(\"pi.\") || toolPath.startsWith(\"fff.\")) && isToolResult(data)) {\n          return extractText(data);\n        }\n        return data;\n      } catch (cause) {\n        const message = formatError(cause);\n        recorder.finishStep(step, undefined, message);\n        onStep?.();\n        throw { error: message };\n      }\n    };\n\n// --- Sequential per-cwd execution queue ---\n\nconst executionQueues = new Map<string, Promise<unknown>>();\n\nexport const runCodemode = async (options: RuntimeOptions): Promise<CodemodeExecutionResult> => {\n  const key = options.cwd;\n  const prev = executionQueues.get(key);\n  const next = (async (): Promise<CodemodeExecutionResult> => {\n    if (prev) {\n      try {\n        await prev;\n      } catch {\n        /* ignore previous execution errors */\n      }\n    }\n    return _runCodemode(options);\n  })();\n  executionQueues.set(key, next);\n  try {\n    return await next;\n  } finally {\n    if (executionQueues.get(key) === next) {\n      executionQueues.delete(key);\n    }\n  }\n};\n\n// --- Core execution ---\n\nconst noopDelegates: SandboxDelegates = {\n  invokeTool: async () => undefined,\n  emitLog: () => { },\n  emitResult: () => {},\n};\n\nconst _runCodemode = async (options: RuntimeOptions): Promise<CodemodeExecutionResult> => {\n  const code = options.code;\n  const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n  const recorder = new CodemodeTraceRecorder({ cwd: options.cwd, code });\n  const logs: string[] = [];\n\n  if (code.length > MAX_CODE_SIZE) {\n    const message = `Code exceeds ${MAX_CODE_SIZE} bytes`;\n    const trace = recorder.finish({ status: \"error\", error: message });\n    return { trace, logs };\n  }\n\n  const sandbox = await acquireSandbox(options.cwd);\n\n  // Wire up delegates for this execution\n  sandbox.delegates.invokeTool = createInvokeTool(options.executor, recorder, options.onUpdate);\n  sandbox.delegates.emitResult = (value: unknown) => {\n    capturedResult = value;\n  };\n  sandbox.delegates.emitLog = (level: unknown, line: unknown) => {\n    const entry = `[${String(level)}] ${String(line)}`;\n    logs.push(entry);\n    recorder.log(entry);\n  };\n\n  let capturedResult: unknown = undefined;\n\n  try {\n    let aborted = false;\n    let timedOut = false;\n\n    const source = buildExecutionSource(code);\n\n    const stdoutDecoder = new TextDecoder();\n    const stderrDecoder = new TextDecoder();\n    let stdout = \"\";\n    let stderr = \"\";\n\n    const proc = sandbox.kernel.spawn(\"node\", [\"-e\", source], {\n      cwd: options.cwd,\n      onStdout: (data) => {\n        stdout += stdoutDecoder.decode(data, { stream: true });\n      },\n      onStderr: (data) => {\n        stderr += stderrDecoder.decode(data, { stream: true });\n      },\n    });\n\n    const timeoutId = setTimeout(() => {\n      timedOut = true;\n      proc.kill(9);\n    }, timeoutMs);\n\n    let abortHandler: (() => void) | undefined;\n    if (options.signal) {\n      abortHandler = () => {\n        aborted = true;\n        proc.kill(9);\n      };\n      if (options.signal.aborted) abortHandler();\n      else options.signal.addEventListener(\"abort\", abortHandler, { once: true });\n    }\n\n    let exitCode: number;\n    try {\n      exitCode = await proc.wait();\n    } finally {\n      clearTimeout(timeoutId);\n      if (options.signal && abortHandler) options.signal.removeEventListener(\"abort\", abortHandler);\n      stdout += stdoutDecoder.decode();\n      stderr += stderrDecoder.decode();\n    }\n\n    const value = capturedResult;\n\n    if (timedOut) {\n      const trace = recorder.finish({\n        status: \"error\",\n        error: `Execution timed out after ${timeoutMs}ms`,\n      });\n      return { trace, logs };\n    }\n\n    if (aborted || options.signal?.aborted) {\n      const trace = recorder.finish({ status: \"aborted\", value, error: \"Execution aborted\" });\n      return { value, trace, logs };\n    }\n\n    if (exitCode !== 0) {\n      const adjustedStderr = stderr.trim();\n      const error = adjustedStderr || stdout.trim() || `Process exited with code ${exitCode}`;\n      const trace = recorder.finish({ status: \"error\", value, error });\n      return { value, trace, logs };\n    }\n\n    const trace = recorder.finish({ status: \"ok\", value });\n    return { value, trace, logs };\n  } catch (cause) {\n    const message = formatError(cause);\n    const trace = recorder.finish({\n      status: options.signal?.aborted ? \"aborted\" : \"error\",\n      error: message,\n    });\n    return { trace, logs };\n  } finally {\n    // Reset delegates to no-ops so next execution starts clean\n    sandbox.delegates.invokeTool = noopDelegates.invokeTool;\n    sandbox.delegates.emitLog = noopDelegates.emitLog;\n    sandbox.delegates.emitResult = noopDelegates.emitResult;\n  }\n};\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/sandbox-cache.ts",
    "content": "import { createInMemoryFileSystem, createKernel, createNodeRuntime, type Kernel } from \"secure-exec\";\n\n// Bound once into secure-exec. Mutable delegates allow per-execution handler swap\n// without recreating the kernel.\nexport type SandboxDelegates = {\n  invokeTool: (path: unknown, args: unknown) => Promise<unknown>;\n  emitLog: (level: unknown, line: unknown) => void;\n  emitResult: (value: unknown) => void;\n};\n\nexport type SandboxEntry = {\n  kernel: Kernel;\n  delegates: SandboxDelegates;\n};\n\nconst noopDelegates: SandboxDelegates = {\n  invokeTool: async () => undefined,\n  emitLog: () => {},\n  emitResult: () => {},\n};\n\nconst cache = new Map<string, SandboxEntry>();\n\nexport const acquireSandbox = async (cwd: string): Promise<SandboxEntry> => {\n  const existing = cache.get(cwd);\n  if (existing) return existing;\n\n  const delegates: SandboxDelegates = { ...noopDelegates };\n\n  // Fixed bindings that forward through mutable delegates\n  const bindings = {\n    invokeTool: (path: unknown, args: unknown) => delegates.invokeTool(path, args),\n    emitLog: (level: unknown, line: unknown) => delegates.emitLog(level, line),\n    emitResult: (value: unknown) => delegates.emitResult(value),\n  };\n\n  const kernel = createKernel({ filesystem: createInMemoryFileSystem(), cwd });\n  await kernel.mount(createNodeRuntime({ bindings }));\n\n  const entry: SandboxEntry = { kernel, delegates };\n  cache.set(cwd, entry);\n  return entry;\n};\n\nexport const disposeAll = async (): Promise<void> => {\n  await Promise.allSettled(\n    Array.from(cache.values()).map((entry) => entry.kernel.dispose()),\n  );\n  cache.clear();\n};"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/source-config.ts",
    "content": "import { readFile, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { parse, printParseErrorCode, type ParseError } from \"jsonc-parser\";\n\nconst SECRET_REF_PREFIX = \"secret-public-ref:\";\n\ntype UnknownRecord = Record<string, unknown>;\n\ntype HeaderSecretRef = { secretId: string; prefix?: string };\ntype PluginHeaderValue = string | HeaderSecretRef;\n\nexport type OpenApiSourceConfig = {\n  kind: \"openapi\";\n  spec: string;\n  baseUrl?: string;\n  namespace?: string;\n  headers?: Record<string, PluginHeaderValue>;\n};\n\nexport type GraphqlSourceConfig = {\n  kind: \"graphql\";\n  endpoint: string;\n  introspectionJson?: string;\n  namespace?: string;\n  headers?: Record<string, PluginHeaderValue>;\n};\n\nexport type McpRemoteSourceConfig = {\n  kind: \"mcp\";\n  transport: \"remote\";\n  name: string;\n  endpoint: string;\n  remoteTransport?: \"streamable-http\" | \"sse\" | \"auto\";\n  namespace?: string;\n  queryParams?: Record<string, string>;\n  headers?: Record<string, string>;\n};\n\nexport type McpStdioSourceConfig = {\n  kind: \"mcp\";\n  transport: \"stdio\";\n  name: string;\n  command: string;\n  args?: string[];\n  env?: Record<string, string>;\n  cwd?: string;\n  namespace?: string;\n};\n\nexport type SupportedSourceConfig =\n  | OpenApiSourceConfig\n  | GraphqlSourceConfig\n  | McpRemoteSourceConfig\n  | McpStdioSourceConfig;\n\nexport type UnsupportedSourceConfig = {\n  index: number;\n  reason: string;\n  value: unknown;\n  kind?: string;\n};\n\nexport type LoadedSourceConfig = {\n  configPath: string;\n  mtimeMs: number;\n  sources: SupportedSourceConfig[];\n  unsupported: UnsupportedSourceConfig[];\n};\n\nconst isRecord = (value: unknown): value is UnknownRecord =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst parseStringMap = (value: unknown): Record<string, string> | null => {\n  if (!isRecord(value)) return null;\n  const output: Record<string, string> = {};\n  for (const [key, item] of Object.entries(value)) {\n    if (typeof item !== \"string\") return null;\n    output[key] = item;\n  }\n  return output;\n};\n\nconst parseStringArray = (value: unknown): string[] | null => {\n  if (!Array.isArray(value)) return null;\n  if (value.some((item) => typeof item !== \"string\")) return null;\n  return [...value] as string[];\n};\n\nconst parseHeaderValue = (value: unknown): PluginHeaderValue | null => {\n  if (typeof value === \"string\") {\n    if (value?.startsWith(SECRET_REF_PREFIX)) {\n      return { secretId: value.slice(SECRET_REF_PREFIX.length) };\n    }\n    return value;\n  }\n\n  if (!isRecord(value) || typeof value.value !== \"string\") return null;\n\n  const prefix = typeof value.prefix === \"string\" ? value.prefix : undefined;\n  const raw = value.value;\n\n  if (raw?.startsWith(SECRET_REF_PREFIX)) {\n    const secretId = raw.slice(SECRET_REF_PREFIX.length);\n    return prefix ? { secretId, prefix } : { secretId };\n  }\n\n  return prefix ? prefix + raw : raw;\n};\n\nconst parseHeaderMap = (value: unknown): Record<string, PluginHeaderValue> | null => {\n  if (!isRecord(value)) return null;\n  const output: Record<string, PluginHeaderValue> = {};\n  for (const [key, item] of Object.entries(value)) {\n    const parsed = parseHeaderValue(item);\n    if (parsed == null) return null;\n    output[key] = parsed;\n  }\n  return output;\n};\n\nconst parseMcpSource = (entry: UnknownRecord): { source?: SupportedSourceConfig; reason?: string } => {\n  const transport = entry.transport;\n  if (transport !== \"remote\" && transport !== \"stdio\") {\n    return { reason: \"mcp.transport must be \\\"remote\\\" or \\\"stdio\\\"\" };\n  }\n\n  if (typeof entry.name !== \"string\" || entry.name.trim().length === 0) {\n    return { reason: \"mcp.name must be a non-empty string\" };\n  }\n\n  if (entry.namespace !== undefined && typeof entry.namespace !== \"string\") {\n    return { reason: \"mcp.namespace must be a string\" };\n  }\n\n  if (transport === \"remote\") {\n    if (typeof entry.endpoint !== \"string\" || entry.endpoint.trim().length === 0) {\n      return { reason: \"mcp(remote).endpoint must be a non-empty string\" };\n    }\n\n    if (\n      entry.remoteTransport !== undefined &&\n      entry.remoteTransport !== \"streamable-http\" &&\n      entry.remoteTransport !== \"sse\" &&\n      entry.remoteTransport !== \"auto\"\n    ) {\n      return { reason: \"mcp(remote).remoteTransport must be streamable-http|sse|auto\" };\n    }\n\n    if (entry.queryParams !== undefined && parseStringMap(entry.queryParams) == null) {\n      return { reason: \"mcp(remote).queryParams must be a string map\" };\n    }\n\n    if (entry.headers !== undefined && parseStringMap(entry.headers) == null) {\n      return { reason: \"mcp(remote).headers must be a string map\" };\n    }\n\n    return {\n      source: {\n        kind: \"mcp\",\n        transport: \"remote\",\n        name: entry.name,\n        endpoint: entry.endpoint,\n        remoteTransport: entry.remoteTransport,\n        namespace: entry.namespace,\n        queryParams: entry.queryParams as Record<string, string> | undefined,\n        headers: entry.headers as Record<string, string> | undefined,\n      },\n    };\n  }\n\n  if (typeof entry.command !== \"string\" || entry.command.trim().length === 0) {\n    return { reason: \"mcp(stdio).command must be a non-empty string\" };\n  }\n\n  if (entry.args !== undefined && parseStringArray(entry.args) == null) {\n    return { reason: \"mcp(stdio).args must be a string array\" };\n  }\n\n  if (entry.env !== undefined && parseStringMap(entry.env) == null) {\n    return { reason: \"mcp(stdio).env must be a string map\" };\n  }\n\n  if (entry.cwd !== undefined && typeof entry.cwd !== \"string\") {\n    return { reason: \"mcp(stdio).cwd must be a string\" };\n  }\n\n  return {\n    source: {\n      kind: \"mcp\",\n      transport: \"stdio\",\n      name: entry.name,\n      command: entry.command,\n      args: entry.args as string[] | undefined,\n      env: entry.env as Record<string, string> | undefined,\n      cwd: entry.cwd as string | undefined,\n      namespace: entry.namespace,\n    },\n  };\n};\n\nconst parseOpenApiSource = (entry: UnknownRecord): { source?: SupportedSourceConfig; reason?: string } => {\n  if (typeof entry.spec !== \"string\" || entry.spec.trim().length === 0) {\n    return { reason: \"openapi.spec must be a non-empty string\" };\n  }\n\n  if (entry.baseUrl !== undefined && typeof entry.baseUrl !== \"string\") {\n    return { reason: \"openapi.baseUrl must be a string\" };\n  }\n\n  if (entry.namespace !== undefined && typeof entry.namespace !== \"string\") {\n    return { reason: \"openapi.namespace must be a string\" };\n  }\n\n  const headers = entry.headers === undefined ? undefined : parseHeaderMap(entry.headers);\n  if (entry.headers !== undefined && headers == null) {\n    return { reason: \"openapi.headers must map to string or secret refs\" };\n  }\n\n  return {\n    source: {\n      kind: \"openapi\",\n      spec: entry.spec,\n      baseUrl: entry.baseUrl as string | undefined,\n      namespace: entry.namespace as string | undefined,\n      headers: headers ?? undefined,\n    },\n  };\n};\n\nconst parseGraphqlSource = (entry: UnknownRecord): { source?: SupportedSourceConfig; reason?: string } => {\n  if (typeof entry.endpoint !== \"string\" || entry.endpoint.trim().length === 0) {\n    return { reason: \"graphql.endpoint must be a non-empty string\" };\n  }\n\n  if (entry.introspectionJson !== undefined && typeof entry.introspectionJson !== \"string\") {\n    return { reason: \"graphql.introspectionJson must be a string\" };\n  }\n\n  if (entry.namespace !== undefined && typeof entry.namespace !== \"string\") {\n    return { reason: \"graphql.namespace must be a string\" };\n  }\n\n  const headers = entry.headers === undefined ? undefined : parseHeaderMap(entry.headers);\n  if (entry.headers !== undefined && headers == null) {\n    return { reason: \"graphql.headers must map to string or secret refs\" };\n  }\n\n  return {\n    source: {\n      kind: \"graphql\",\n      endpoint: entry.endpoint,\n      introspectionJson: entry.introspectionJson as string | undefined,\n      namespace: entry.namespace as string | undefined,\n      headers: headers ?? undefined,\n    },\n  };\n};\n\nconst parseErrorsToMessage = (errors: ParseError[]): string =>\n  errors.map((error) => \"offset \" + error.offset + \": \" + printParseErrorCode(error.error)).join(\"; \");\n\nexport const getExecutorConfigPath = (): string => join(homedir(), \".pi\", \"agent\", \"executor.jsonc\");\n\nexport const loadSourcesFromConfig = async (): Promise<LoadedSourceConfig> => {\n  const configPath = getExecutorConfigPath();\n\n  let raw: string;\n  let mtimeMs: number;\n  try {\n    mtimeMs = (await stat(configPath)).mtimeMs;\n    raw = await readFile(configPath, \"utf8\");\n  } catch (cause) {\n    const error = cause as NodeJS.ErrnoException;\n    if (error?.code === \"ENOENT\") {\n      return {\n        configPath,\n        mtimeMs: 0,\n        sources: [],\n        unsupported: [],\n      };\n    }\n    throw new Error(\"Failed reading \" + configPath + \": \" + (error?.message ?? String(cause)));\n  }\n\n  const parseErrors: ParseError[] = [];\n  const parsed = parse(raw, parseErrors);\n  if (parseErrors.length > 0) {\n    throw new Error(\"Invalid JSONC in \" + configPath + \": \" + parseErrorsToMessage(parseErrors));\n  }\n\n  if (!isRecord(parsed)) {\n    throw new Error(\"Invalid config at \" + configPath + \": root must be an object\");\n  }\n\n  const rawSources = parsed.sources;\n\n  if (rawSources === undefined) {\n    return { configPath, mtimeMs, sources: [], unsupported: [] };\n  }\n\n  if (!Array.isArray(rawSources)) {\n    throw new Error(\"Invalid config at \" + configPath + \": \\\"sources\\\" must be an array\");\n  }\n\n  const sources: SupportedSourceConfig[] = [];\n  const unsupported: UnsupportedSourceConfig[] = [];\n\n  for (let index = 0; index < rawSources.length; index++) {\n    const rawSource = rawSources[index];\n    if (!isRecord(rawSource)) {\n      unsupported.push({ index, reason: \"source entry must be an object\", value: rawSource });\n      continue;\n    }\n\n    const kind = typeof rawSource.kind === \"string\" ? rawSource.kind : undefined;\n\n    if (kind === \"mcp\") {\n      const parsedSource = parseMcpSource(rawSource);\n      if (!parsedSource.source) {\n        unsupported.push({ index, kind, reason: parsedSource.reason ?? \"invalid mcp source\", value: rawSource });\n        continue;\n      }\n      sources.push(parsedSource.source);\n      continue;\n    }\n\n    if (kind === \"openapi\") {\n      const parsedSource = parseOpenApiSource(rawSource);\n      if (!parsedSource.source) {\n        unsupported.push({ index, kind, reason: parsedSource.reason ?? \"invalid openapi source\", value: rawSource });\n        continue;\n      }\n      sources.push(parsedSource.source);\n      continue;\n    }\n\n    if (kind === \"graphql\") {\n      const parsedSource = parseGraphqlSource(rawSource);\n      if (!parsedSource.source) {\n        unsupported.push({ index, kind, reason: parsedSource.reason ?? \"invalid graphql source\", value: rawSource });\n        continue;\n      }\n      sources.push(parsedSource.source);\n      continue;\n    }\n\n    unsupported.push({\n      index,\n      kind,\n      reason: kind ? \"unsupported source kind: \" + kind : \"source.kind is required\",\n      value: rawSource,\n    });\n  }\n\n  return { configPath, mtimeMs, sources, unsupported };\n};"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/source-hydrate.ts",
    "content": "import type { Executor } from \"@executor-js/sdk\";\nimport type { SupportedSourceConfig, UnsupportedSourceConfig } from \"./source-config.js\";\n\ntype ExecutorWithPlugins = Executor & {\n  mcp?: { addSource: (source: unknown) => Promise<unknown> };\n  openapi?: { addSpec: (source: unknown) => Promise<unknown> };\n  graphql?: { addSource: (source: unknown) => Promise<unknown> };\n};\n\ntype HydrateOptions = {\n  configPath: string;\n  unsupported: UnsupportedSourceConfig[];\n};\n\nconst sourceKey = (source: SupportedSourceConfig): string => {\n  if (source.namespace) return source.kind + \":\" + source.namespace;\n\n  if (source.kind === \"mcp\") {\n    if (source.transport === \"remote\") return \"mcp:remote:\" + source.endpoint;\n    return \"mcp:stdio:\" + source.command + \":\" + (source.args ?? []).join(\"\\u0000\") + \":\" + (source.cwd ?? \"\");\n  }\n\n  if (source.kind === \"openapi\") return \"openapi:\" + source.spec;\n  return \"graphql:\" + source.endpoint;\n};\n\nconst summarize = (source: SupportedSourceConfig): string => {\n  if (source.namespace) return source.kind + \":\" + source.namespace;\n  if (source.kind === \"mcp\") return source.transport === \"remote\" ? \"mcp:\" + source.endpoint : \"mcp:\" + source.command;\n  if (source.kind === \"openapi\") return \"openapi:\" + source.spec;\n  return \"graphql:\" + source.endpoint;\n};\n\nexport const hydrateExecutorSources = async (\n  executor: Executor,\n  sources: SupportedSourceConfig[],\n  options: HydrateOptions,\n): Promise<void> => {\n  if (options.unsupported.length > 0) {\n    for (const entry of options.unsupported) {\n      console.warn(\n        \"[codemode] Skipping unsupported source in \" +\n          options.configPath +\n          \" (#\" +\n          entry.index +\n          \"): \" +\n          entry.reason,\n      );\n    }\n  }\n\n  if (sources.length === 0) return;\n\n  const ext = executor as ExecutorWithPlugins;\n  const existing = await executor.sources.list();\n  const existingIds = new Set(existing.map((source: { id: string }) => source.id));\n  const seen = new Set<string>();\n\n  for (const source of sources) {\n    const key = sourceKey(source);\n    if (seen.has(key)) continue;\n    seen.add(key);\n\n    if (source.namespace && existingIds.has(source.namespace)) continue;\n\n    if (source.kind === \"mcp\") {\n      if (!ext.mcp?.addSource) {\n        throw new Error(\"Missing MCP plugin while hydrating \" + summarize(source) + \" from \" + options.configPath);\n      }\n      await ext.mcp.addSource(source);\n      continue;\n    }\n\n    if (source.kind === \"openapi\") {\n      if (!ext.openapi?.addSpec) {\n        throw new Error(\"Missing OpenAPI plugin while hydrating \" + summarize(source) + \" from \" + options.configPath);\n      }\n      await ext.openapi.addSpec(source);\n      continue;\n    }\n\n    if (!ext.graphql?.addSource) {\n      throw new Error(\"Missing GraphQL plugin while hydrating \" + summarize(source) + \" from \" + options.configPath);\n    }\n    await ext.graphql.addSource(source);\n  }\n};\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/trace.ts",
    "content": "import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport type { CodemodeTrace, ToolResultSnapshot, TraceStep } from \"./types.js\";\n\nconst clone = <T>(value: T): T => {\n  if (typeof structuredClone === \"function\") return structuredClone(value);\n  return JSON.parse(JSON.stringify(value)) as T;\n};\n\nconst formatDuration = (ms: number): string => {\n  if (ms < 1000) return `${ms}ms`;\n  return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;\n};\n\nconst summarizeText = (text: string, max = 140): string => {\n  const first = text.split(/\\r?\\n/).find((line) => line.trim().length > 0) ?? \"\";\n  if (first.length <= max) return first;\n  return `${first.slice(0, max - 1)}…`;\n};\n\nconst summarizeValue = (value: unknown): string => {\n  if (value === undefined) return \"undefined\";\n  if (value === null) return \"null\";\n  if (typeof value === \"string\") return JSON.stringify(value.length > 80 ? `${value.slice(0, 77)}…` : value);\n  if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n  if (Array.isArray(value)) return `[${value.length} items]`;\n  if (typeof value === \"object\") return `{${Object.keys(value as Record<string, unknown>).slice(0, 4).join(\", \")}}`;\n  return String(value);\n};\n\nexport class CodemodeTraceRecorder {\n  private readonly trace: CodemodeTrace;\n  private readonly steps = new Map<string, TraceStep>();\n  private id = 0;\n\n  constructor(input: { cwd: string; code: string }) {\n    this.trace = {\n      cwd: input.cwd,\n      code: input.code,\n      startedAt: Date.now(),\n      status: \"running\",\n      logs: [],\n      steps: [],\n    };\n  }\n\n  log(line: string): void {\n    this.trace.logs.push(line);\n  }\n\n  startStep(input: { label: string; toolPath: string; input: unknown }): string {\n    const id = `step_${++this.id}`;\n    const step: TraceStep = {\n      id,\n      label: input.label,\n      toolPath: input.toolPath,\n      input: clone(input.input),\n      startedAt: Date.now(),\n      status: \"running\",\n    };\n    this.steps.set(id, step);\n    this.trace.steps.push(step);\n    return id;\n  }\n\n  finishStep(id: string, output?: ToolResultSnapshot, error?: string): void {\n    const step = this.steps.get(id);\n    if (!step) return;\n    step.endedAt = Date.now();\n\n    if (error) {\n      step.status = \"error\";\n      step.error = error;\n      return;\n    }\n\n    if (output) {\n      step.output = clone(output);\n      step.status = output.isError ? \"error\" : \"ok\";\n      if (output.isError) step.error = output.text;\n      return;\n    }\n\n    step.status = \"ok\";\n  }\n\n  finish(input: { status: CodemodeTrace[\"status\"]; value?: unknown; error?: string }): CodemodeTrace {\n    this.trace.endedAt = Date.now();\n    this.trace.status = input.status;\n    this.trace.value = input.value;\n    this.trace.error = input.error;\n    return clone(this.trace);\n  }\n}\n\nexport const summarizeTraceForContext = (trace: CodemodeTrace): string => {\n  const duration = trace.endedAt ? formatDuration(trace.endedAt - trace.startedAt) : undefined;\n  const errors = trace.steps.filter((s) => s.status === \"error\").length;\n\n  return [\n    `codemode ${trace.status}`,\n    duration,\n    `${trace.steps.length} step${trace.steps.length === 1 ? \"\" : \"s\"}`,\n    errors > 0 ? `${errors} err` : undefined,\n    trace.logs.length > 0 ? `${trace.logs.length} log${trace.logs.length === 1 ? \"\" : \"s\"}` : undefined,\n    trace.value !== undefined ? `value=${summarizeValue(trace.value)}` : undefined,\n    trace.error ? `error=${summarizeText(trace.error, 120)}` : undefined,\n  ]\n    .filter((v): v is string => Boolean(v))\n    .join(\" · \");\n};\n\nexport const formatTraceForAgent = (trace: CodemodeTrace, value: unknown, logs: string[]): { text: string; images: ImageContent[] } => {\n  const durationMs = trace.endedAt ? trace.endedAt - trace.startedAt : 0;\n  const duration = formatDuration(durationMs);\n\n  const result: string[] = [`status:${trace.status} duration:${duration}`];\n\n  if (trace.steps.length > 0) {\n    result.push(\"\", \"steps:\");\n    for (const step of trace.steps) {\n      const stepDuration = step.endedAt ? formatDuration(step.endedAt - step.startedAt) : \"\";\n      const prefix = step.status === \"error\" ? \"[ERR]\" : step.status === \"ok\" ? \"[OK]\" : \"[...]\";\n      result.push(`${prefix} ${step.label} ${stepDuration}`);\n      if (step.error) {\n        result.push(`  error: ${step.error}`);\n      } else if (step.output?.text) {\n        const lines = step.output.text.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n        const preview = lines.slice(0, 8).join(\"\\n  \");\n        result.push(`  output: ${preview}${lines.length > 8 ? `\\n  ... (${lines.length - 8} more lines)` : \"\"}`);\n      }\n    }\n  }\n\n  if (trace.error) {\n    result.push(\"\", `execution error: ${trace.error}`);\n  }\n\n  if (value !== undefined) {\n    const raw = typeof value === \"string\" ? value : JSON.stringify(value);\n    result.push(\"\", `return value: ${raw}`);\n  }\n\n  if (logs.length > 0) {\n    result.push(\"\", \"logs:\");\n    for (const log of logs) {\n      result.push(log.replace(/^\\[\\w+\\] /, \"\"));\n    }\n  }\n\n  const images = trace.steps.flatMap((s) => s.output?.images ?? []);\n\n  return { text: result.join(\"\\n\"), images };\n};\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/turndown.d.ts",
    "content": "declare module \"turndown\" {\n  export default class TurndownService {\n    turndown(html: string): string;\n  }\n}\n"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/types.ts",
    "content": "import type { ImageContent } from \"@mariozechner/pi-ai\";\n\nexport const builtinToolNames = [\"read\", \"bash\", \"edit\", \"write\", \"grep\", \"find\", \"ls\", \"webfetch\"] as const;\nexport type BuiltinToolName = (typeof builtinToolNames)[number];\n\nexport const fffToolNames = [\n  \"grep\",\n  \"fileSearch\",\n  \"multiGrep\",\n  \"recentFiles\",\n  \"searchThenGrep\",\n] as const;\nexport type FffToolName = (typeof fffToolNames)[number];\n\nexport type ToolResultSnapshot = {\n  value: unknown;\n  text: string;\n  images?: ImageContent[];\n  isError: boolean;\n};\n\nexport type TraceStep = {\n  id: string;\n  label: string;\n  toolPath: string;\n  input: unknown;\n  startedAt: number;\n  endedAt?: number;\n  status: \"running\" | \"ok\" | \"error\";\n  output?: ToolResultSnapshot;\n  error?: string;\n};\n\nexport type CodemodeTrace = {\n  cwd: string;\n  code: string;\n  startedAt: number;\n  endedAt?: number;\n  status: \"running\" | \"ok\" | \"error\" | \"aborted\";\n  value?: unknown;\n  logs: string[];\n  steps: TraceStep[];\n  error?: string;\n};\n\nexport type CodemodeResultDetails = {\n  trace: CodemodeTrace;\n  value?: unknown;\n  logs: string[];\n  summary: string;\n};"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/util.ts",
    "content": "export const stripCodeFences = (code: string): string => {\n  const trimmed = code.trim();\n  const match = trimmed.match(/^(```|~~~)(?:\\w+)?\\r?\\n([\\s\\S]+)\\r?\\n\\1$/);\n  if (match) return match[2].trim();\n  return trimmed;\n};\n\nexport const buildPromptGuidelines = () => [\n  \"Write plain JavaScript body only. No imports/exports. No markdown fences\",\n  \"Use things from tools.* namespace to access tools\",\n  \"Only do filesystem related operations using tools.pi.*\",\n  \"Never rely on bash if it can be done in JavaScript\",\n  \"Batch related work in one codemode call\",\n  \"Prefer tools.pi.read over shell commands: read({ offset, limit }) gives line ranges\",\n  \"Prefer tools.pi.grep over grep: it returns structured results with line numbers\",\n  \"Output visibility: use `return value` to show output visibly, use `console.log(value)` for logs. Bare expressions like `result;` or string tricks like `'' + result` are silently swallowed.\",\n  \"Bash is available for package managers (npm, pnpm, bun, yarn, npx, node), file operations (mkdir, cp, mv, rm, ln, chmod), and utilities (pwd, echo, which, uname, date). Use tools.pi.* for reading/writing/searching/grepping/editing files.\",\n];\n\nexport const formatError = (cause: unknown): string => {\n  if (cause instanceof Error) return cause.message || cause.name;\n  if (typeof cause === \"string\") return cause;\n  try {\n    return JSON.stringify(cause);\n  } catch {\n    return String(cause);\n  }\n};"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/src/webfetch.ts",
    "content": "import TurndownService from \"turndown\";\n\nexport type WebfetchInput = {\n  url: string;\n  format?: \"markdown\" | \"text\" | \"html\";\n  timeout?: number;\n};\n\nconst MAX_RESPONSE_SIZE = 5 * 1024 * 1024;\nconst DEFAULT_TIMEOUT = 30 * 1000;\nconst MAX_TIMEOUT = 120 * 1000;\n\nconst turndownService = new TurndownService();\n\n// Simple HTML to text conversion\nfunction htmlToText(html: string): string {\n  return (\n    html\n      .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n      .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, \"\")\n      .replace(/<\\/p>/gi, \"\\n\\n\")\n      .replace(/<br\\s*\\/?>/gi, \"\\n\")\n      .replace(/<\\/div>/gi, \"\\n\")\n      .replace(/<\\/h[1-6]>/gi, \"\\n\\n\")\n      .replace(/<\\/li>/gi, \"\\n\")\n      .replace(/<[^>]+>/g, \"\")\n      .replace(/&nbsp;/g, \" \")\n      .replace(/&amp;/g, \"&\")\n      .replace(/&lt;/g, \"<\")\n      .replace(/&gt;/g, \">\")\n      .replace(/&quot;/g, '\"')\n      .replace(/&#39;/g, \"'\")\n      .replace(/\\n{3,}/g, \"\\n\\n\")\n      .trim()\n  );\n}\n\nfunction htmlToMarkdown(html: string): string {\n  try {\n    return turndownService.turndown(html);\n  } catch {\n    return htmlToText(html);\n  }\n}\n\nexport async function webfetch(\n  _toolId: string,\n  args: WebfetchInput,\n  _signal: AbortSignal | undefined,\n  _onUpdate: () => void,\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; details?: Record<string, unknown> }> {\n  const format = args.format ?? \"markdown\";\n  let url = args.url;\n\n  if (url.startsWith(\"http://\")) {\n    try {\n      const parsed = new URL(url);\n      const isLocalhost = parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\" || parsed.hostname === \"::1\";\n      if (!isLocalhost) {\n        url = url.replace(\"http://\", \"https://\");\n      }\n    } catch {\n      return {\n        content: [{ type: \"text\", text: \"\" }],\n        details: { url: args.url, format, error: \"Invalid URL\" },\n      };\n    }\n  }\n  if (!url.startsWith(\"https://\")) {\n    return {\n      content: [{ type: \"text\", text: \"\" }],\n      details: { url: args.url, format, error: \"Invalid URL\" },\n    };\n  }\n\n  const timeout = Math.min((args.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT);\n  const controller = new AbortController();\n  const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n  try {\n    const response = await fetch(url, {\n      signal: controller.signal,\n      headers: {\n        \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n        Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n        \"Accept-Language\": \"en-US,en;q=0.9\",\n      },\n    });\n\n    clearTimeout(timeoutId);\n\n    if (!response.ok) {\n      return {\n        content: [{ type: \"text\", text: \"\" }],\n        details: { url, format, error: `HTTP ${response.status}` },\n      };\n    }\n\n    const contentLength = response.headers.get(\"content-length\");\n    if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {\n      return {\n        content: [{ type: \"text\", text: \"\" }],\n        details: { url, format, error: \"Response too large\" },\n      };\n    }\n\n    const contentType = response.headers.get(\"content-type\") || \"\";\n    const mime = contentType.split(\";\")[0]?.trim().toLowerCase() || \"\";\n\n    if (\n      mime.startsWith(\"image/\") ||\n      mime.startsWith(\"video/\") ||\n      mime.startsWith(\"audio/\") ||\n      mime.startsWith(\"application/pdf\") ||\n      mime.startsWith(\"application/zip\") ||\n      mime.startsWith(\"application/octet-stream\")\n    ) {\n      return {\n        content: [{ type: \"text\", text: \"\" }],\n        details: { url, format, contentType: mime, skipped: true },\n      };\n    }\n\n    const arrayBuffer = await response.arrayBuffer();\n\n    if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {\n      return {\n        content: [{ type: \"text\", text: \"\" }],\n        details: { url, format, error: \"Response too large\" },\n      };\n    }\n\n    let text = new TextDecoder().decode(arrayBuffer);\n    let output = \"\";\n\n    switch (format) {\n      case \"markdown\":\n        output = mime.includes(\"text/html\") ? htmlToMarkdown(text) : text;\n        break;\n      case \"text\":\n        output = mime.includes(\"text/html\") ? htmlToText(text) : text;\n        break;\n      case \"html\":\n        output = text;\n        break;\n    }\n\n    return {\n      content: [{ type: \"text\", text: output.trim() }],\n      details: { url, format, contentType: mime },\n    };\n  } catch (error) {\n    clearTimeout(timeoutId);\n    const errorMsg = error instanceof Error ? error.message : \"Unknown error\";\n    return {\n      content: [{ type: \"text\", text: \"\" }],\n      details: { url, format, error: errorMsg },\n    };\n  }\n}"
  },
  {
    "path": "agents/pi/extensions/pi-codemode/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"paths\": {\n      \"@executor/codemode-core\": [\"./node_modules/@executor/codemode-core/src/index.ts\"]\n    }\n  },\n  \"include\": [\"./src/**/*.ts\", \"./index.ts\"],\n  \"exclude\": [\"node_modules\", \"examples\"]\n}\n"
  },
  {
    "path": "agents/pi/extensions/prompt-timer.ts",
    "content": "import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\n\nfunction isAssistantMessage(message: unknown): message is AssistantMessage {\n  if (!message || typeof message !== \"object\") return false;\n  const role = (message as { role?: unknown }).role;\n  return role === \"assistant\";\n}\n\nfunction formatDuration(ms: number): string {\n  const seconds = Math.floor(ms / 1000);\n  const mins = Math.floor(seconds / 60);\n  const secs = seconds % 60;\n\n  if (mins > 0) {\n    return `${mins}m ${secs}s`;\n  }\n  return `${secs}s`;\n}\n\nexport default function (pi: ExtensionAPI) {\n  let agentStartMs: number | null = null;\n\n  pi.on(\"agent_start\", () => {\n    agentStartMs = Date.now();\n  });\n\n  pi.on(\"agent_end\", (event, ctx) => {\n    if (!ctx.hasUI) return;\n    if (agentStartMs === null) return;\n\n    const elapsedMs = Date.now() - agentStartMs;\n    agentStartMs = null;\n    if (elapsedMs <= 0) return;\n\n    let input = 0;\n    let output = 0;\n    let cacheRead = 0;\n    let cacheWrite = 0;\n    let totalTokens = 0;\n\n    for (const message of event.messages) {\n      if (!isAssistantMessage(message)) continue;\n      input += message.usage.input || 0;\n      output += message.usage.output || 0;\n      cacheRead += message.usage.cacheRead || 0;\n      cacheWrite += message.usage.cacheWrite || 0;\n      totalTokens += message.usage.totalTokens || 0;\n    }\n\n    if (output <= 0) return;\n\n    const elapsedSeconds = elapsedMs / 1000;\n    const tokensPerSecond = output / elapsedSeconds;\n    const message = `TPS: ${tokensPerSecond.toFixed(1)} tok/s · took ${formatDuration(elapsedMs)}`;\n    ctx.ui.notify(message, \"info\");\n  });\n}\n"
  },
  {
    "path": "agents/pi/extensions/session-breakdown.ts",
    "content": "/**\n * /session-breakdown\n *\n * Interactive TUI that analyzes ~/.pi/agent/sessions (recursively, *.jsonl) and shows\n * last 7/30/90 days of:\n * - sessions/day\n * - messages/day\n * - tokens/day (if available)\n * - cost/day (if available)\n * - model breakdown (sessions/messages/tokens + cost)\n *\n * Graph:\n * - GitHub-contributions-style calendar (weeks x weekdays)\n * - Hue: weighted mix of popular model colors (weighted by the selected metric)\n * - Brightness: selected metric per day (log-scaled)\n */\n\nimport type { ExtensionAPI, ExtensionContext } from \"@mariozechner/pi-coding-agent\";\nimport { BorderedLoader } from \"@mariozechner/pi-coding-agent\";\nimport {\n\tKey,\n\tmatchesKey,\n\tsliceByColumn,\n\ttype Component,\n\ttype TUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport { createReadStream, type Dirent } from \"node:fs\";\nimport readline from \"node:readline\";\n\ntype ModelKey = string; // `${provider}/${model}`\n\ninterface ParsedSession {\n\tfilePath: string;\n\tstartedAt: Date;\n\tdayKeyLocal: string; // YYYY-MM-DD (local)\n\tmodelsUsed: Set<ModelKey>;\n\tmessages: number;\n\ttokens: number;\n\ttotalCost: number;\n\tcostByModel: Map<ModelKey, number>;\n\tmessagesByModel: Map<ModelKey, number>;\n\ttokensByModel: Map<ModelKey, number>;\n}\n\ninterface DayAgg {\n\tdate: Date; // local midnight\n\tdayKeyLocal: string;\n\tsessions: number;\n\tmessages: number;\n\ttokens: number;\n\ttotalCost: number;\n\tcostByModel: Map<ModelKey, number>;\n\tsessionsByModel: Map<ModelKey, number>;\n\tmessagesByModel: Map<ModelKey, number>;\n\ttokensByModel: Map<ModelKey, number>;\n}\n\ninterface RangeAgg {\n\tdays: DayAgg[];\n\tdayByKey: Map<string, DayAgg>;\n\tsessions: number;\n\ttotalMessages: number;\n\ttotalTokens: number;\n\ttotalCost: number;\n\tmodelCost: Map<ModelKey, number>;\n\tmodelSessions: Map<ModelKey, number>; // number of sessions where model was used\n\tmodelMessages: Map<ModelKey, number>;\n\tmodelTokens: Map<ModelKey, number>;\n}\n\ninterface RGB {\n\tr: number;\n\tg: number;\n\tb: number;\n}\n\ninterface BreakdownData {\n\tgeneratedAt: Date;\n\tranges: Map<number, RangeAgg>;\n\tpalette: {\n\t\tmodelColors: Map<ModelKey, RGB>;\n\t\totherColor: RGB;\n\t\torderedModels: ModelKey[];\n\t};\n}\n\nconst SESSION_ROOT = path.join(os.homedir(), \".pi\", \"agent\", \"sessions\");\nconst RANGE_DAYS = [7, 30, 90] as const;\n\ntype MeasurementMode = \"sessions\" | \"messages\" | \"tokens\";\n\ntype BreakdownProgressPhase = \"scan\" | \"parse\" | \"finalize\";\n\ninterface BreakdownProgressState {\n\tphase: BreakdownProgressPhase;\n\tfoundFiles: number;\n\tparsedFiles: number;\n\ttotalFiles: number;\n\tcurrentFile?: string;\n}\n\nfunction setBorderedLoaderMessage(loader: BorderedLoader, message: string) {\n\t// BorderedLoader wraps a (Cancellable)Loader which supports setMessage(),\n\t// but it doesn't expose it publicly. Access the inner loader for progress updates.\n\tconst inner = (loader as any)[\"loader\"]; // eslint-disable-line @typescript-eslint/no-explicit-any\n\tif (inner && typeof inner.setMessage === \"function\") {\n\t\tinner.setMessage(message);\n\t}\n}\n\n// Dark-ish background and empty cell color (close to GitHub dark)\nconst DEFAULT_BG: RGB = { r: 13, g: 17, b: 23 };\nconst EMPTY_CELL_BG: RGB = { r: 22, g: 27, b: 34 };\n\n// Default palette (assigned to top models)\nconst PALETTE: RGB[] = [\n\t{ r: 64, g: 196, b: 99 }, // green\n\t{ r: 47, g: 129, b: 247 }, // blue\n\t{ r: 163, g: 113, b: 247 }, // purple\n\t{ r: 255, g: 159, b: 10 }, // orange\n\t{ r: 244, g: 67, b: 54 }, // red\n];\n\nfunction clamp01(x: number): number {\n\treturn Math.max(0, Math.min(1, x));\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n\treturn a + (b - a) * t;\n}\n\nfunction mixRgb(a: RGB, b: RGB, t: number): RGB {\n\treturn {\n\t\tr: Math.round(lerp(a.r, b.r, t)),\n\t\tg: Math.round(lerp(a.g, b.g, t)),\n\t\tb: Math.round(lerp(a.b, b.b, t)),\n\t};\n}\n\nfunction weightedMix(colors: Array<{ color: RGB; weight: number }>): RGB {\n\tlet total = 0;\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tfor (const c of colors) {\n\t\tif (!Number.isFinite(c.weight) || c.weight <= 0) continue;\n\t\ttotal += c.weight;\n\t\tr += c.color.r * c.weight;\n\t\tg += c.color.g * c.weight;\n\t\tb += c.color.b * c.weight;\n\t}\n\tif (total <= 0) return EMPTY_CELL_BG;\n\treturn { r: Math.round(r / total), g: Math.round(g / total), b: Math.round(b / total) };\n}\n\nfunction ansiBg(rgb: RGB, text: string): string {\n\treturn `\\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m${text}\\x1b[0m`;\n}\n\nfunction ansiFg(rgb: RGB, text: string): string {\n\treturn `\\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\\x1b[0m`;\n}\n\nfunction dim(text: string): string {\n\treturn `\\x1b[2m${text}\\x1b[0m`;\n}\n\nfunction bold(text: string): string {\n\treturn `\\x1b[1m${text}\\x1b[0m`;\n}\n\nfunction formatCount(n: number): string {\n\tif (!Number.isFinite(n) || n === 0) return \"0\";\n\tif (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;\n\tif (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n\tif (n >= 10_000) return `${(n / 1_000).toFixed(1)}K`;\n\treturn n.toLocaleString(\"en-US\");\n}\n\nfunction formatUsd(cost: number): string {\n\tif (!Number.isFinite(cost)) return \"$0.00\";\n\tif (cost >= 1) return `$${cost.toFixed(2)}`;\n\tif (cost >= 0.1) return `$${cost.toFixed(3)}`;\n\treturn `$${cost.toFixed(4)}`;\n}\n\nfunction padRight(s: string, n: number): string {\n\tconst delta = n - s.length;\n\treturn delta > 0 ? s + \" \".repeat(delta) : s;\n}\n\nfunction padLeft(s: string, n: number): string {\n\tconst delta = n - s.length;\n\treturn delta > 0 ? \" \".repeat(delta) + s : s;\n}\n\nfunction toLocalDayKey(d: Date): string {\n\tconst yyyy = d.getFullYear();\n\tconst mm = String(d.getMonth() + 1).padStart(2, \"0\");\n\tconst dd = String(d.getDate()).padStart(2, \"0\");\n\treturn `${yyyy}-${mm}-${dd}`;\n}\n\nfunction localMidnight(d: Date): Date {\n\treturn new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);\n}\n\nfunction addDaysLocal(d: Date, days: number): Date {\n\tconst x = new Date(d);\n\tx.setDate(x.getDate() + days);\n\treturn x;\n}\n\nfunction countDaysInclusiveLocal(start: Date, end: Date): number {\n\t// Avoid ms-based day math because DST transitions can make a “day” 23/25h in local time.\n\tlet n = 0;\n\tfor (let d = new Date(start); d <= end; d = addDaysLocal(d, 1)) n++;\n\treturn n;\n}\n\nfunction mondayIndex(date: Date): number {\n\t// Mon=0 .. Sun=6\n\treturn (date.getDay() + 6) % 7;\n}\n\nfunction modelKeyFromParts(provider?: unknown, model?: unknown): ModelKey | null {\n\tconst p = typeof provider === \"string\" ? provider.trim() : \"\";\n\tconst m = typeof model === \"string\" ? model.trim() : \"\";\n\tif (!p && !m) return null;\n\tif (!p) return m;\n\tif (!m) return p;\n\treturn `${p}/${m}`;\n}\n\nfunction parseSessionStartFromFilename(name: string): Date | null {\n\t// Example: 2026-02-02T21-52-28-774Z_<uuid>.jsonl\n\tconst m = name.match(/^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z_/);\n\tif (!m) return null;\n\tconst iso = `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`;\n\tconst d = new Date(iso);\n\treturn Number.isFinite(d.getTime()) ? d : null;\n}\n\nfunction extractProviderModelAndUsage(obj: any): { provider?: any; model?: any; modelId?: any; usage?: any } {\n\t// Session format varies across versions.\n\t// - Newer: { provider, model, usage } on the message wrapper\n\t// - Older: { message: { provider, model, usage } }\n\tconst msg = obj?.message;\n\treturn {\n\t\tprovider: obj?.provider ?? msg?.provider,\n\t\tmodel: obj?.model ?? msg?.model,\n\t\tmodelId: obj?.modelId ?? msg?.modelId,\n\t\tusage: obj?.usage ?? msg?.usage,\n\t};\n}\n\nfunction extractCostTotal(usage: any): number {\n\tif (!usage) return 0;\n\tconst c = usage?.cost;\n\tif (typeof c === \"number\") return Number.isFinite(c) ? c : 0;\n\tif (typeof c === \"string\") {\n\t\tconst n = Number(c);\n\t\treturn Number.isFinite(n) ? n : 0;\n\t}\n\tconst t = c?.total;\n\tif (typeof t === \"number\") return Number.isFinite(t) ? t : 0;\n\tif (typeof t === \"string\") {\n\t\tconst n = Number(t);\n\t\treturn Number.isFinite(n) ? n : 0;\n\t}\n\treturn 0;\n}\n\nfunction extractTokensTotal(usage: any): number {\n\t// Usage format varies across providers and pi versions.\n\t// We try a few common shapes:\n\t// - { totalTokens }\n\t// - { total_tokens }\n\t// - { promptTokens, completionTokens }\n\t// - { prompt_tokens, completion_tokens }\n\t// - { input_tokens, output_tokens }\n\t// - { inputTokens, outputTokens }\n\t// - { tokens: number | { total } }\n\tif (!usage) return 0;\n\n\tconst readNum = (v: any): number => {\n\t\tif (typeof v === \"number\") return Number.isFinite(v) ? v : 0;\n\t\tif (typeof v === \"string\") {\n\t\t\tconst n = Number(v);\n\t\t\treturn Number.isFinite(n) ? n : 0;\n\t\t}\n\t\treturn 0;\n\t};\n\n\tlet total = 0;\n\t// direct totals\n\ttotal =\n\t\treadNum(usage?.totalTokens) ||\n\t\treadNum(usage?.total_tokens) ||\n\t\treadNum(usage?.tokens) ||\n\t\treadNum(usage?.tokenCount) ||\n\t\treadNum(usage?.token_count);\n\tif (total > 0) return total;\n\n\t// nested tokens object\n\ttotal = readNum(usage?.tokens?.total) || readNum(usage?.tokens?.totalTokens) || readNum(usage?.tokens?.total_tokens);\n\tif (total > 0) return total;\n\n\t// sum of parts\n\tconst a =\n\t\treadNum(usage?.promptTokens) ||\n\t\treadNum(usage?.prompt_tokens) ||\n\t\treadNum(usage?.inputTokens) ||\n\t\treadNum(usage?.input_tokens);\n\tconst b =\n\t\treadNum(usage?.completionTokens) ||\n\t\treadNum(usage?.completion_tokens) ||\n\t\treadNum(usage?.outputTokens) ||\n\t\treadNum(usage?.output_tokens);\n\tconst sum = a + b;\n\treturn sum > 0 ? sum : 0;\n}\n\nasync function walkSessionFiles(\n\troot: string,\n\tstartCutoffLocal: Date,\n\tsignal?: AbortSignal,\n\tonFound?: (found: number) => void,\n): Promise<string[]> {\n\tconst out: string[] = [];\n\tconst stack: string[] = [root];\n\twhile (stack.length) {\n\t\tif (signal?.aborted) break;\n\t\tconst dir = stack.pop()!;\n\t\tlet entries: Dirent[] = [];\n\t\ttry {\n\t\t\tentries = await fs.readdir(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const ent of entries) {\n\t\t\tif (signal?.aborted) break;\n\t\t\tconst p = path.join(dir, ent.name);\n\t\t\tif (ent.isDirectory()) {\n\t\t\t\tstack.push(p);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!ent.isFile() || !ent.name.endsWith(\".jsonl\")) continue;\n\n\t\t\t// Prefer filename timestamp, else fall back to mtime.\n\t\t\tconst startedAt = parseSessionStartFromFilename(ent.name);\n\t\t\tif (startedAt) {\n\t\t\t\tif (localMidnight(startedAt) >= startCutoffLocal) {\n\t\t\t\t\tout.push(p);\n\t\t\t\t\tif (onFound && out.length % 10 === 0) onFound(out.length);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst st = await fs.stat(p);\n\t\t\t\tconst approx = new Date(st.mtimeMs);\n\t\t\t\tif (localMidnight(approx) >= startCutoffLocal) {\n\t\t\t\t\tout.push(p);\n\t\t\t\t\tif (onFound && out.length % 10 === 0) onFound(out.length);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t}\n\t}\n\tonFound?.(out.length);\n\treturn out;\n}\n\nasync function parseSessionFile(filePath: string, signal?: AbortSignal): Promise<ParsedSession | null> {\n\tconst fileName = path.basename(filePath);\n\tlet startedAt = parseSessionStartFromFilename(fileName);\n\tlet currentModel: ModelKey | null = null;\n\n\tconst modelsUsed = new Set<ModelKey>();\n\tlet messages = 0;\n\tlet tokens = 0;\n\tlet totalCost = 0;\n\tconst costByModel = new Map<ModelKey, number>();\n\tconst messagesByModel = new Map<ModelKey, number>();\n\tconst tokensByModel = new Map<ModelKey, number>();\n\n\tconst stream = createReadStream(filePath, { encoding: \"utf8\" });\n\tconst rl = readline.createInterface({ input: stream, crlfDelay: Infinity });\n\n\ttry {\n\t\tfor await (const line of rl) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\trl.close();\n\t\t\t\tstream.destroy();\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!line) continue;\n\t\t\tlet obj: any;\n\t\t\ttry {\n\t\t\t\tobj = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!startedAt && obj?.type === \"session\" && typeof obj?.timestamp === \"string\") {\n\t\t\t\tconst d = new Date(obj.timestamp);\n\t\t\t\tif (Number.isFinite(d.getTime())) startedAt = d;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (obj?.type === \"model_change\") {\n\t\t\t\tconst mk = modelKeyFromParts(obj.provider, obj.modelId);\n\t\t\t\tif (mk) {\n\t\t\t\t\tcurrentModel = mk;\n\t\t\t\t\tmodelsUsed.add(mk);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (obj?.type !== \"message\") continue;\n\n\t\t\tconst { provider, model, modelId, usage } = extractProviderModelAndUsage(obj);\n\t\t\tconst mk =\n\t\t\t\tmodelKeyFromParts(provider, model) ??\n\t\t\t\tmodelKeyFromParts(provider, modelId) ??\n\t\t\t\tcurrentModel ??\n\t\t\t\t\"unknown\";\n\t\t\tmodelsUsed.add(mk);\n\n\t\t\tmessages += 1;\n\t\t\tmessagesByModel.set(mk, (messagesByModel.get(mk) ?? 0) + 1);\n\n\t\t\tconst tok = extractTokensTotal(usage);\n\t\t\tif (tok > 0) {\n\t\t\t\ttokens += tok;\n\t\t\t\ttokensByModel.set(mk, (tokensByModel.get(mk) ?? 0) + tok);\n\t\t\t}\n\n\t\t\tconst cost = extractCostTotal(usage);\n\t\t\tif (cost > 0) {\n\t\t\t\ttotalCost += cost;\n\t\t\t\tcostByModel.set(mk, (costByModel.get(mk) ?? 0) + cost);\n\t\t\t}\n\t\t}\n\t} finally {\n\t\trl.close();\n\t\tstream.destroy();\n\t}\n\n\tif (!startedAt) return null;\n\tconst dayKeyLocal = toLocalDayKey(startedAt);\n\treturn {\n\t\tfilePath,\n\t\tstartedAt,\n\t\tdayKeyLocal,\n\t\tmodelsUsed,\n\t\tmessages,\n\t\ttokens,\n\t\ttotalCost,\n\t\tcostByModel,\n\t\tmessagesByModel,\n\t\ttokensByModel,\n\t};\n}\n\nfunction buildRangeAgg(days: number, now: Date): RangeAgg {\n\tconst end = localMidnight(now);\n\tconst start = addDaysLocal(end, -(days - 1));\n\tconst outDays: DayAgg[] = [];\n\tconst dayByKey = new Map<string, DayAgg>();\n\n\tfor (let i = 0; i < days; i++) {\n\t\tconst d = addDaysLocal(start, i);\n\t\tconst dayKeyLocal = toLocalDayKey(d);\n\t\tconst day: DayAgg = {\n\t\t\tdate: d,\n\t\t\tdayKeyLocal,\n\t\t\tsessions: 0,\n\t\t\tmessages: 0,\n\t\t\ttokens: 0,\n\t\t\ttotalCost: 0,\n\t\t\tcostByModel: new Map(),\n\t\t\tsessionsByModel: new Map(),\n\t\t\tmessagesByModel: new Map(),\n\t\t\ttokensByModel: new Map(),\n\t\t};\n\t\toutDays.push(day);\n\t\tdayByKey.set(dayKeyLocal, day);\n\t}\n\n\treturn {\n\t\tdays: outDays,\n\t\tdayByKey,\n\t\tsessions: 0,\n\t\ttotalMessages: 0,\n\t\ttotalTokens: 0,\n\t\ttotalCost: 0,\n\t\tmodelCost: new Map(),\n\t\tmodelSessions: new Map(),\n\t\tmodelMessages: new Map(),\n\t\tmodelTokens: new Map(),\n\t};\n}\n\nfunction addSessionToRange(range: RangeAgg, session: ParsedSession): void {\n\tconst day = range.dayByKey.get(session.dayKeyLocal);\n\tif (!day) return;\n\n\trange.sessions += 1;\n\trange.totalMessages += session.messages;\n\trange.totalTokens += session.tokens;\n\trange.totalCost += session.totalCost;\n\tday.sessions += 1;\n\tday.messages += session.messages;\n\tday.tokens += session.tokens;\n\tday.totalCost += session.totalCost;\n\n\t// Sessions-per-model (presence)\n\tfor (const mk of session.modelsUsed) {\n\t\tday.sessionsByModel.set(mk, (day.sessionsByModel.get(mk) ?? 0) + 1);\n\t\trange.modelSessions.set(mk, (range.modelSessions.get(mk) ?? 0) + 1);\n\t}\n\n\t// Messages-per-model\n\tfor (const [mk, n] of session.messagesByModel.entries()) {\n\t\tday.messagesByModel.set(mk, (day.messagesByModel.get(mk) ?? 0) + n);\n\t\trange.modelMessages.set(mk, (range.modelMessages.get(mk) ?? 0) + n);\n\t}\n\n\t// Tokens-per-model\n\tfor (const [mk, n] of session.tokensByModel.entries()) {\n\t\tday.tokensByModel.set(mk, (day.tokensByModel.get(mk) ?? 0) + n);\n\t\trange.modelTokens.set(mk, (range.modelTokens.get(mk) ?? 0) + n);\n\t}\n\n\t// Cost-per-model\n\tfor (const [mk, cost] of session.costByModel.entries()) {\n\t\tday.costByModel.set(mk, (day.costByModel.get(mk) ?? 0) + cost);\n\t\trange.modelCost.set(mk, (range.modelCost.get(mk) ?? 0) + cost);\n\t}\n}\n\nfunction sortMapByValueDesc<K extends string>(m: Map<K, number>): Array<{ key: K; value: number }> {\n\treturn [...m.entries()]\n\t\t.map(([key, value]) => ({ key, value }))\n\t\t.sort((a, b) => b.value - a.value);\n}\n\nfunction choosePaletteFromLast30Days(range30: RangeAgg, topN = 4): {\n\tmodelColors: Map<ModelKey, RGB>;\n\totherColor: RGB;\n\torderedModels: ModelKey[];\n} {\n\t// Prefer cost if any cost exists, else tokens, else messages, else sessions.\n\tconst costSum = [...range30.modelCost.values()].reduce((a, b) => a + b, 0);\n\tconst popularity =\n\t\tcostSum > 0\n\t\t\t? range30.modelCost\n\t\t\t: range30.totalTokens > 0\n\t\t\t\t? range30.modelTokens\n\t\t\t\t: range30.totalMessages > 0\n\t\t\t\t\t? range30.modelMessages\n\t\t\t\t\t: range30.modelSessions;\n\n\tconst sorted = sortMapByValueDesc(popularity);\n\tconst orderedModels = sorted.slice(0, topN).map((x) => x.key);\n\tconst modelColors = new Map<ModelKey, RGB>();\n\tfor (let i = 0; i < orderedModels.length; i++) {\n\t\tmodelColors.set(orderedModels[i], PALETTE[i % PALETTE.length]);\n\t}\n\treturn {\n\t\tmodelColors,\n\t\totherColor: { r: 160, g: 160, b: 160 },\n\t\torderedModels,\n\t};\n}\n\nfunction dayMixedColor(\n\tday: DayAgg,\n\tmodelColors: Map<ModelKey, RGB>,\n\totherColor: RGB,\n\tmode: MeasurementMode,\n): RGB {\n\tconst parts: Array<{ color: RGB; weight: number }> = [];\n\tlet otherWeight = 0;\n\n\tlet map: Map<ModelKey, number>;\n\tif (mode === \"tokens\") {\n\t\tmap = day.tokens > 0 ? day.tokensByModel : day.messages > 0 ? day.messagesByModel : day.sessionsByModel;\n\t} else if (mode === \"messages\") {\n\t\tmap = day.messages > 0 ? day.messagesByModel : day.sessionsByModel;\n\t} else {\n\t\tmap = day.sessionsByModel;\n\t}\n\n\tfor (const [mk, w] of map.entries()) {\n\t\tconst c = modelColors.get(mk);\n\t\tif (c) parts.push({ color: c, weight: w });\n\t\telse otherWeight += w;\n\t}\n\tif (otherWeight > 0) parts.push({ color: otherColor, weight: otherWeight });\n\treturn weightedMix(parts);\n}\n\nfunction graphMetricForRange(\n\trange: RangeAgg,\n\tmode: MeasurementMode,\n): { kind: \"sessions\" | \"messages\" | \"tokens\"; max: number; denom: number } {\n\tif (mode === \"tokens\") {\n\t\tconst maxTokens = Math.max(0, ...range.days.map((d) => d.tokens));\n\t\tif (maxTokens > 0) return { kind: \"tokens\", max: maxTokens, denom: Math.log1p(maxTokens) };\n\t\t// fall back if tokens aren't available\n\t\tmode = \"messages\";\n\t}\n\n\tif (mode === \"messages\") {\n\t\tconst maxMessages = Math.max(0, ...range.days.map((d) => d.messages));\n\t\tif (maxMessages > 0) return { kind: \"messages\", max: maxMessages, denom: Math.log1p(maxMessages) };\n\t\t// fall back if messages aren't available\n\t\tmode = \"sessions\";\n\t}\n\n\tconst maxSessions = Math.max(0, ...range.days.map((d) => d.sessions));\n\treturn { kind: \"sessions\", max: maxSessions, denom: Math.log1p(maxSessions) };\n}\n\nfunction weeksForRange(range: RangeAgg): number {\n\tconst days = range.days;\n\tconst start = days[0].date;\n\tconst end = days[days.length - 1].date;\n\tconst gridStart = addDaysLocal(start, -mondayIndex(start));\n\tconst gridEnd = addDaysLocal(end, 6 - mondayIndex(end));\n\tconst totalGridDays = countDaysInclusiveLocal(gridStart, gridEnd);\n\treturn Math.ceil(totalGridDays / 7);\n}\n\nfunction renderGraphLines(\n\trange: RangeAgg,\n\tmodelColors: Map<ModelKey, RGB>,\n\totherColor: RGB,\n\tmode: MeasurementMode,\n\toptions?: { cellWidth?: number; gap?: number },\n): string[] {\n\tconst days = range.days;\n\tconst start = days[0].date;\n\tconst end = days[days.length - 1].date;\n\n\tconst gridStart = addDaysLocal(start, -mondayIndex(start));\n\tconst gridEnd = addDaysLocal(end, 6 - mondayIndex(end));\n\tconst totalGridDays = countDaysInclusiveLocal(gridStart, gridEnd);\n\tconst weeks = Math.ceil(totalGridDays / 7);\n\n\tconst cellWidth = Math.max(1, Math.floor(options?.cellWidth ?? 1));\n\tconst gap = Math.max(0, Math.floor(options?.gap ?? 1));\n\tconst block = \"█\".repeat(cellWidth);\n\tconst gapStr = \" \".repeat(gap);\n\n\tconst metric = graphMetricForRange(range, mode);\n\tconst denom = metric.denom;\n\n\t// Label only Mon/Wed/Fri like GitHub (saves space)\n\tconst labelByRow = new Map<number, string>([\n\t\t[0, \"Mon\"],\n\t\t[2, \"Wed\"],\n\t\t[4, \"Fri\"],\n\t]);\n\n\tconst lines: string[] = [];\n\tfor (let row = 0; row < 7; row++) {\n\t\tconst label = labelByRow.get(row);\n\t\tlet line = label ? padRight(label, 3) + \" \" : \"    \";\n\n\t\tfor (let w = 0; w < weeks; w++) {\n\t\t\tconst cellDate = addDaysLocal(gridStart, w * 7 + row);\n\t\t\tconst inRange = cellDate >= start && cellDate <= end;\n\t\t\tconst colGap = w < weeks - 1 ? gapStr : \"\";\n\t\t\tif (!inRange) {\n\t\t\t\tline += \" \".repeat(cellWidth) + colGap;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst key = toLocalDayKey(cellDate);\n\t\t\tconst day = range.dayByKey.get(key);\n\t\t\tconst value =\n\t\t\t\tmetric.kind === \"tokens\"\n\t\t\t\t\t? (day?.tokens ?? 0)\n\t\t\t\t\t: metric.kind === \"messages\"\n\t\t\t\t\t\t? (day?.messages ?? 0)\n\t\t\t\t\t\t: (day?.sessions ?? 0);\n\n\t\t\tif (!day || value <= 0) {\n\t\t\t\tline += ansiFg(EMPTY_CELL_BG, block) + colGap;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst hue = dayMixedColor(day, modelColors, otherColor, mode);\n\t\t\tlet t = denom > 0 ? Math.log1p(value) / denom : 0;\n\t\t\tt = clamp01(t);\n\t\t\tconst minVisible = 0.2;\n\t\t\tconst intensity = minVisible + (1 - minVisible) * t;\n\t\t\tconst rgb = mixRgb(DEFAULT_BG, hue, intensity);\n\t\t\tline += ansiFg(rgb, block) + colGap;\n\t\t}\n\n\t\tlines.push(line);\n\t}\n\n\treturn lines;\n}\n\nfunction displayModelName(modelKey: string): string {\n\tconst idx = modelKey.indexOf(\"/\");\n\treturn idx === -1 ? modelKey : modelKey.slice(idx + 1);\n}\n\nfunction renderLegendItems(modelColors: Map<ModelKey, RGB>, orderedModels: ModelKey[], otherColor: RGB): string[] {\n\tconst items: string[] = [];\n\tfor (const mk of orderedModels) {\n\t\tconst c = modelColors.get(mk);\n\t\tif (!c) continue;\n\t\titems.push(`${ansiFg(c, \"█\")} ${displayModelName(mk)}`);\n\t}\n\titems.push(`${ansiFg(otherColor, \"█\")} other`);\n\treturn items;\n}\n\nfunction fitRight(text: string, width: number): string {\n\tif (width <= 0) return \"\";\n\tlet w = visibleWidth(text);\n\tlet t = text;\n\tif (w > width) {\n\t\tt = sliceByColumn(t, w - width, width, true);\n\t\tw = visibleWidth(t);\n\t}\n\treturn \" \".repeat(Math.max(0, width - w)) + t;\n}\n\nfunction renderLegendBlock(leftLabel: string, items: string[], width: number): string[] {\n\tif (width <= 0) return [];\n\tif (items.length === 0) return [truncateToWidth(leftLabel, width)];\n\n\tconst lines: string[] = [];\n\t// First line: label on left, first item right-aligned into remaining space.\n\tconst leftW = visibleWidth(leftLabel);\n\tif (leftW >= width) {\n\t\tlines.push(truncateToWidth(leftLabel, width));\n\t\t// Put all items on their own lines right-aligned.\n\t\tfor (const it of items) lines.push(fitRight(it, width));\n\t\treturn lines;\n\t}\n\n\tconst remaining = Math.max(0, width - leftW);\n\tlines.push(leftLabel + fitRight(items[0], remaining));\n\n\tfor (let i = 1; i < items.length; i++) {\n\t\tlines.push(fitRight(items[i], width));\n\t}\n\treturn lines;\n}\n\nfunction renderModelTable(range: RangeAgg, mode: MeasurementMode, maxRows = 8): string[] {\n\t// Keep this relatively narrow: model + selected metric + cost + share.\n\tconst metric = graphMetricForRange(range, mode);\n\tconst kind = metric.kind;\n\n\tlet perModel: Map<ModelKey, number>;\n\tlet total = 0;\n\tlet label = kind;\n\n\tif (kind === \"tokens\") {\n\t\tperModel = range.modelTokens;\n\t\ttotal = range.totalTokens;\n\t} else if (kind === \"messages\") {\n\t\tperModel = range.modelMessages;\n\t\ttotal = range.totalMessages;\n\t} else {\n\t\tperModel = range.modelSessions;\n\t\ttotal = range.sessions;\n\t}\n\n\tconst sorted = sortMapByValueDesc(perModel);\n\tconst rows = sorted.slice(0, maxRows);\n\n\tconst valueWidth = kind === \"tokens\" ? 10 : 8;\n\tconst modelWidth = Math.min(52, Math.max(\"model\".length, ...rows.map((r) => r.key.length)));\n\n\tconst lines: string[] = [];\n\tlines.push(`${padRight(\"model\", modelWidth)}  ${padLeft(label, valueWidth)}  ${padLeft(\"cost\", 10)}  ${padLeft(\"share\", 6)}`);\n\tlines.push(`${\"-\".repeat(modelWidth)}  ${\"-\".repeat(valueWidth)}  ${\"-\".repeat(10)}  ${\"-\".repeat(6)}`);\n\n\tfor (const r of rows) {\n\t\tconst value = perModel.get(r.key) ?? 0;\n\t\tconst cost = range.modelCost.get(r.key) ?? 0;\n\t\tconst share = total > 0 ? `${Math.round((value / total) * 100)}%` : \"0%\";\n\t\tlines.push(\n\t\t\t`${padRight(r.key.slice(0, modelWidth), modelWidth)}  ${padLeft(formatCount(value), valueWidth)}  ${padLeft(formatUsd(cost), 10)}  ${padLeft(share, 6)}`,\n\t\t);\n\t}\n\n\tif (sorted.length === 0) {\n\t\tlines.push(dim(\"(no model data found)\"));\n\t}\n\n\treturn lines;\n}\n\nfunction renderLeftRight(left: string, right: string, width: number): string {\n\tconst leftW = visibleWidth(left);\n\tif (width <= 0) return \"\";\n\tif (leftW >= width) return truncateToWidth(left, width);\n\n\tconst remaining = width - leftW;\n\tlet rightText = right;\n\tconst rightW = visibleWidth(rightText);\n\tif (rightW > remaining) {\n\t\t// Keep the *rightmost* part visible.\n\t\trightText = sliceByColumn(rightText, rightW - remaining, remaining, true);\n\t}\n\tconst pad = Math.max(0, remaining - visibleWidth(rightText));\n\treturn left + \" \".repeat(pad) + rightText;\n}\n\nfunction rangeSummary(range: RangeAgg, days: number, mode: MeasurementMode): string {\n\tconst avg = range.sessions > 0 ? range.totalCost / range.sessions : 0;\n\tconst costPart = range.totalCost > 0 ? `${formatUsd(range.totalCost)} · avg ${formatUsd(avg)}/session` : `$0.0000`;\n\n\tif (mode === \"tokens\") {\n\t\treturn `Last ${days} days: ${formatCount(range.sessions)} sessions · ${formatCount(range.totalTokens)} tokens · ${costPart}`;\n\t}\n\tif (mode === \"messages\") {\n\t\treturn `Last ${days} days: ${formatCount(range.sessions)} sessions · ${formatCount(range.totalMessages)} messages · ${costPart}`;\n\t}\n\treturn `Last ${days} days: ${formatCount(range.sessions)} sessions · ${costPart}`;\n}\n\nasync function computeBreakdown(\n\tsignal?: AbortSignal,\n\tonProgress?: (update: Partial<BreakdownProgressState>) => void,\n): Promise<BreakdownData> {\n\tconst now = new Date();\n\tconst ranges = new Map<number, RangeAgg>();\n\tfor (const d of RANGE_DAYS) ranges.set(d, buildRangeAgg(d, now));\n\tconst range90 = ranges.get(90)!;\n\tconst start90 = range90.days[0].date;\n\n\tonProgress?.({ phase: \"scan\", foundFiles: 0, parsedFiles: 0, totalFiles: 0, currentFile: undefined });\n\n\tconst candidates = await walkSessionFiles(SESSION_ROOT, start90, signal, (found) => {\n\t\tonProgress?.({ phase: \"scan\", foundFiles: found });\n\t});\n\n\tconst totalFiles = candidates.length;\n\tonProgress?.({\n\t\tphase: \"parse\",\n\t\tfoundFiles: totalFiles,\n\t\ttotalFiles,\n\t\tparsedFiles: 0,\n\t\tcurrentFile: totalFiles > 0 ? path.basename(candidates[0]!) : undefined,\n\t});\n\n\tlet parsedFiles = 0;\n\tfor (const filePath of candidates) {\n\t\tif (signal?.aborted) break;\n\t\tparsedFiles += 1;\n\t\tonProgress?.({ phase: \"parse\", parsedFiles, totalFiles, currentFile: path.basename(filePath) });\n\n\t\tconst session = await parseSessionFile(filePath, signal);\n\t\tif (!session) continue;\n\n\t\tconst sessionDay = localMidnight(session.startedAt);\n\t\tfor (const d of RANGE_DAYS) {\n\t\t\tconst range = ranges.get(d)!;\n\t\t\tconst start = range.days[0].date;\n\t\t\tconst end = range.days[range.days.length - 1].date;\n\t\t\tif (sessionDay < start || sessionDay > end) continue;\n\t\t\taddSessionToRange(range, session);\n\t\t}\n\t}\n\n\tonProgress?.({ phase: \"finalize\", currentFile: undefined });\n\n\tconst palette = choosePaletteFromLast30Days(ranges.get(30)!, 4);\n\treturn { generatedAt: now, ranges, palette };\n}\n\nclass BreakdownComponent implements Component {\n\tprivate data: BreakdownData;\n\tprivate tui: TUI;\n\tprivate onDone: () => void;\n\tprivate rangeIndex = 1; // default 30d\n\tprivate measurement: MeasurementMode = \"sessions\";\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\n\tconstructor(data: BreakdownData, tui: TUI, onDone: () => void) {\n\t\tthis.data = data;\n\t\tthis.tui = tui;\n\t\tthis.onDone = onDone;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl(\"c\")) || data.toLowerCase() === \"q\") {\n\t\t\tthis.onDone();\n\t\t\treturn;\n\t\t}\n\n\t\tif (matchesKey(data, Key.tab) || matchesKey(data, Key.shift(\"tab\")) || data.toLowerCase() === \"t\") {\n\t\t\tconst order: MeasurementMode[] = [\"sessions\", \"messages\", \"tokens\"];\n\t\t\tconst idx = Math.max(0, order.indexOf(this.measurement));\n\t\t\tconst dir = matchesKey(data, Key.shift(\"tab\")) ? -1 : 1;\n\t\t\tthis.measurement = order[(idx + order.length + dir) % order.length] ?? \"sessions\";\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst prev = () => {\n\t\t\tthis.rangeIndex = (this.rangeIndex + RANGE_DAYS.length - 1) % RANGE_DAYS.length;\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t};\n\t\tconst next = () => {\n\t\t\tthis.rangeIndex = (this.rangeIndex + 1) % RANGE_DAYS.length;\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t};\n\n\t\tif (matchesKey(data, Key.left) || data.toLowerCase() === \"h\") prev();\n\t\tif (matchesKey(data, Key.right) || data.toLowerCase() === \"l\") next();\n\n\t\tif (data === \"1\") {\n\t\t\tthis.rangeIndex = 0;\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t\tif (data === \"2\") {\n\t\t\tthis.rangeIndex = 1;\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t\tif (data === \"3\") {\n\t\t\tthis.rangeIndex = 2;\n\t\t\tthis.invalidate();\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedWidth === width && this.cachedLines) return this.cachedLines;\n\n\t\tconst selectedDays = RANGE_DAYS[this.rangeIndex];\n\t\tconst range = this.data.ranges.get(selectedDays)!;\n\t\tconst metric = graphMetricForRange(range, this.measurement);\n\n\t\tconst tab = (days: number, idx: number): string => {\n\t\t\tconst selected = idx === this.rangeIndex;\n\t\t\tconst label = `${days}d`;\n\t\t\treturn selected ? bold(`[${label}]`) : dim(` ${label} `);\n\t\t};\n\n\t\tconst metricTab = (mode: MeasurementMode, label: string): string => {\n\t\t\tconst selected = mode === this.measurement;\n\t\t\treturn selected ? bold(`[${label}]`) : dim(` ${label} `);\n\t\t};\n\n\t\tconst header =\n\t\t\t`${bold(\"Session breakdown\")}  ${tab(7, 0)} ${tab(30, 1)} ${tab(90, 2)}  ` +\n\t\t\t`${metricTab(\"sessions\", \"sess\")} ${metricTab(\"messages\", \"msg\")} ${metricTab(\"tokens\", \"tok\")}`;\n\n\t\tconst legendItems = renderLegendItems(\n\t\t\tthis.data.palette.modelColors,\n\t\t\tthis.data.palette.orderedModels,\n\t\t\tthis.data.palette.otherColor,\n\t\t);\n\n\t\tconst summary = rangeSummary(range, selectedDays, metric.kind) + dim(`   (graph: ${metric.kind}/day)`);\n\n\t\tconst maxScale = selectedDays === 7 ? 4 : selectedDays === 30 ? 3 : 2;\n\t\tconst weeks = weeksForRange(range);\n\t\tconst leftMargin = 4; // \"Mon \" (or 4 spaces)\n\t\tconst gap = 1;\n\t\tconst graphArea = Math.max(1, width - leftMargin);\n\t\t// Each week column uses: cellWidth + gap. Last column also gets gap (fine; we truncate anyway).\n\t\tconst idealCellWidth = Math.floor((graphArea + gap) / Math.max(1, weeks)) - gap;\n\t\tconst cellWidth = Math.min(maxScale, Math.max(1, idealCellWidth));\n\n\t\tconst graphLines = renderGraphLines(\n\t\t\trange,\n\t\t\tthis.data.palette.modelColors,\n\t\t\tthis.data.palette.otherColor,\n\t\t\tthis.measurement,\n\t\t\t{ cellWidth, gap },\n\t\t);\n\t\tconst tableLines = renderModelTable(range, metric.kind, 8);\n\n\t\tconst lines: string[] = [];\n\t\tlines.push(truncateToWidth(header, width));\n\t\tlines.push(truncateToWidth(dim(\"←/→ range · tab metric · q to close\"), width));\n\t\tlines.push(\"\");\n\t\tlines.push(truncateToWidth(summary, width));\n\t\tlines.push(\"\");\n\n\t\t// Render legend on the RIGHT of the graph if there is space.\n\t\tconst graphWidth = Math.max(0, ...graphLines.map((l) => visibleWidth(l)));\n\t\tconst sep = 2;\n\t\tconst legendWidth = width - graphWidth - sep;\n\t\tconst showSideLegend = legendWidth >= 22;\n\n\t\tif (showSideLegend) {\n\t\t\tconst legendBlock: string[] = [];\n\t\t\tlegendBlock.push(dim(\"Top models (30d palette):\"));\n\t\t\tlegendBlock.push(...legendItems);\n\t\t\t// Fit into 7 rows (same as graph). If too many, show a final \"+N more\" line.\n\t\t\tconst maxLegendRows = graphLines.length;\n\t\t\tlet legendLines = legendBlock.slice(0, maxLegendRows);\n\t\t\tif (legendBlock.length > maxLegendRows) {\n\t\t\t\tconst remaining = legendBlock.length - (maxLegendRows - 1);\n\t\t\t\tlegendLines = [...legendBlock.slice(0, maxLegendRows - 1), dim(`+${remaining} more`)];\n\t\t\t}\n\t\t\twhile (legendLines.length < graphLines.length) legendLines.push(\"\");\n\n\t\t\tconst padRightAnsi = (s: string, target: number): string => {\n\t\t\t\tconst w = visibleWidth(s);\n\t\t\t\treturn w >= target ? s : s + \" \".repeat(target - w);\n\t\t\t};\n\n\t\t\tfor (let i = 0; i < graphLines.length; i++) {\n\t\t\t\tconst left = padRightAnsi(graphLines[i] ?? \"\", graphWidth);\n\t\t\t\tconst right = truncateToWidth(legendLines[i] ?? \"\", Math.max(0, legendWidth));\n\t\t\t\tlines.push(truncateToWidth(left + \" \".repeat(sep) + right, width));\n\t\t\t}\n\t\t} else {\n\t\t\t// Fallback: graph only (legend will be shown below).\n\t\t\tfor (const gl of graphLines) lines.push(truncateToWidth(gl, width));\n\t\t\tlines.push(\"\");\n\t\t\t// Compact legend below, left-aligned.\n\t\t\tlines.push(truncateToWidth(dim(\"Top models (30d palette):\"), width));\n\t\t\tfor (const it of legendItems) lines.push(truncateToWidth(it, width));\n\t\t}\n\n\t\tlines.push(\"\");\n\t\tfor (const tl of tableLines) lines.push(truncateToWidth(tl, width));\n\n\t\t// Ensure no overly long lines (truncateToWidth already), but keep at least 1 line.\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = lines.map((l) => (visibleWidth(l) > width ? truncateToWidth(l, width) : l));\n\t\treturn this.cachedLines;\n\t}\n}\n\nexport default function sessionBreakdownExtension(pi: ExtensionAPI) {\n\tpi.registerCommand(\"session-breakdown\", {\n\t\tdescription: \"Interactive breakdown of last 7/30/90 days of ~/.pi session usage (sessions/messages/tokens + cost by model)\",\n\t\thandler: async (_args, ctx: ExtensionContext) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// Non-interactive fallback: just notify.\n\t\t\t\tconst data = await computeBreakdown(undefined);\n\t\t\t\tconst range = data.ranges.get(30)!;\n\t\t\t\tpi.sendMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\tcustomType: \"session-breakdown\",\n\t\t\t\t\t\tcontent: `Session breakdown (non-interactive)\\n${rangeSummary(range, 30, \"sessions\")}`,\n\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t},\n\t\t\t\t\t{ triggerTurn: false },\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\t\t\tconst data = await ctx.ui.custom<BreakdownData | null>((tui, theme, _kb, done) => {\n\t\t\t\tconst baseMessage = \"Analyzing sessions (last 90 days)…\";\n\t\t\t\tconst loader = new BorderedLoader(tui, theme, baseMessage);\n\n\t\t\t\tconst startedAt = Date.now();\n\t\t\t\tconst progress: BreakdownProgressState = {\n\t\t\t\t\tphase: \"scan\",\n\t\t\t\t\tfoundFiles: 0,\n\t\t\t\t\tparsedFiles: 0,\n\t\t\t\t\ttotalFiles: 0,\n\t\t\t\t\tcurrentFile: undefined,\n\t\t\t\t};\n\n\t\t\t\tconst renderMessage = (): string => {\n\t\t\t\t\tconst elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);\n\t\t\t\t\tif (progress.phase === \"scan\") {\n\t\t\t\t\t\treturn `${baseMessage}  scanning (${formatCount(progress.foundFiles)} files) · ${elapsed}s`;\n\t\t\t\t\t}\n\t\t\t\t\tif (progress.phase === \"parse\") {\n\t\t\t\t\t\treturn `${baseMessage}  parsing (${formatCount(progress.parsedFiles)}/${formatCount(progress.totalFiles)}) · ${elapsed}s`;\n\t\t\t\t\t}\n\t\t\t\t\treturn `${baseMessage}  finalizing · ${elapsed}s`;\n\t\t\t\t};\n\n\t\t\t\tlet intervalId: NodeJS.Timeout | null = null;\n\t\t\t\tconst stopTicker = () => {\n\t\t\t\t\tif (intervalId) {\n\t\t\t\t\t\tclearInterval(intervalId);\n\t\t\t\t\t\tintervalId = null;\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Update every 0.5s so long-running scans show some visible progress.\n\t\t\t\tsetBorderedLoaderMessage(loader, renderMessage());\n\t\t\t\tintervalId = setInterval(() => {\n\t\t\t\t\tsetBorderedLoaderMessage(loader, renderMessage());\n\t\t\t\t}, 500);\n\n\t\t\t\tloader.onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\tstopTicker();\n\t\t\t\t\tdone(null);\n\t\t\t\t};\n\n\t\t\t\tcomputeBreakdown(loader.signal, (update) => Object.assign(progress, update))\n\t\t\t\t\t.then((d) => {\n\t\t\t\t\t\tstopTicker();\n\t\t\t\t\t\tif (!aborted) done(d);\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tstopTicker();\n\t\t\t\t\t\tconsole.error(\"session-breakdown: failed to analyze sessions\", err);\n\t\t\t\t\t\tif (!aborted) done(null);\n\t\t\t\t\t});\n\n\t\t\t\treturn loader;\n\t\t\t});\n\n\t\t\tif (!data) {\n\t\t\t\tctx.ui.notify(aborted ? \"Cancelled\" : \"Failed to analyze sessions\", aborted ? \"info\" : \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait ctx.ui.custom<void>((tui, _theme, _kb, done) => {\n\t\t\t\treturn new BreakdownComponent(data, tui, done);\n\t\t\t});\n\t\t},\n\t});\n}\n"
  },
  {
    "path": "agents/pi/extensions/webfetch.ts",
    "content": "/**\n * WebFetch Extension\n *\n * Fetches web content and converts to markdown, text, or HTML for agent consumption.\n * Use when URLs are mentioned in conversation to retrieve their content.\n */\n\nimport type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\nimport { Type } from \"@sinclair/typebox\";\nimport { StringEnum } from \"@mariozechner/pi-ai\";\nimport TurndownService from \"turndown\";\n\nconst MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB\nconst DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds\nconst MAX_TIMEOUT = 120 * 1000; // 2 minutes\n\n// Turndown instance for HTML to Markdown conversion\nconst turndownService = new TurndownService({\n  headingStyle: \"atx\",\n  hr: \"---\",\n  bulletListMarker: \"-\",\n  codeBlockStyle: \"fenced\",\n  emDelimiter: \"*\",\n});\n\n// Remove script/style/meta/link/media tags\nturndownService.remove([\n  \"script\", \"style\", \"meta\", \"link\", \"noscript\",\n  \"img\", \"svg\", \"picture\", \"figure\", \"canvas\",\n  \"video\", \"audio\", \"source\", \"track\", \"embed\", \"object\", \"iframe\",\n]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"webfetch\",\n    label: \"WebFetch\",\n    description:\n      \"Fetch web content and convert to markdown, text, or HTML for analysis. \" +\n      \"Use when URLs are mentioned in conversation to retrieve their content. \" +\n      \"HTTP URLs are automatically upgraded to HTTPS. \" +\n      \"Images and binary content return empty text.\",\n\n    parameters: Type.Object({\n      url: Type.String({ description: \"The URL to fetch content from\" }),\n      format: Type.Optional(\n        StringEnum([\"markdown\", \"text\", \"html\"] as const, {\n          description: \"Output format: markdown (default), text, or html\",\n        }),\n      ),\n      timeout: Type.Optional(\n        Type.Number({\n          description: \"Timeout in seconds (max 120, default 30)\",\n        }),\n      ),\n    }),\n\n    async execute(_toolCallId, params, signal, _onUpdate, ctx) {\n      const format = params.format ?? \"markdown\";\n      let url = params.url;\n\n      // Validate and normalize URL\n      if (url.startsWith(\"http://\")) {\n        url = url.replace(\"http://\", \"https://\");\n      }\n      if (!url.startsWith(\"https://\")) {\n        ctx.ui.notify(\"Invalid URL: must start with http:// or https://\", \"error\");\n        return {\n          content: [{ type: \"text\", text: \"\" }],\n          details: { url: params.url, format, error: \"Invalid URL\" },\n        };\n      }\n\n      const timeout = Math.min(\n        (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,\n        MAX_TIMEOUT,\n      );\n\n      // Create abort controller that combines signal and timeout\n      const controller = new AbortController();\n      const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n      // Link external signal if provided\n      if (signal) {\n        signal.addEventListener(\"abort\", () => controller.abort());\n      }\n\n      try {\n        const response = await fetch(url, {\n          signal: controller.signal,\n          headers: {\n            \"User-Agent\":\n              \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n            Accept:\n              \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n            \"Accept-Language\": \"en-US,en;q=0.9\",\n          },\n        });\n\n        clearTimeout(timeoutId);\n\n        if (!response.ok) {\n          ctx.ui.notify(`HTTP error ${response.status}: ${response.statusText}`, \"error\");\n          return {\n            content: [{ type: \"text\", text: \"\" }],\n            details: {\n              url,\n              format,\n              error: `HTTP ${response.status}`,\n            },\n          };\n        }\n\n        // Check content length header\n        const contentLength = response.headers.get(\"content-length\");\n        if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {\n          ctx.ui.notify(\"Response too large (>5MB)\", \"error\");\n          return {\n            content: [{ type: \"text\", text: \"\" }],\n            details: { url, format, error: \"Response too large\" },\n          };\n        }\n\n        const contentType = response.headers.get(\"content-type\") || \"\";\n        const mime = contentType.split(\";\")[0]?.trim().toLowerCase() || \"\";\n\n        // Skip binary/image content - return empty for agent\n        if (\n          mime.startsWith(\"image/\") ||\n          mime.startsWith(\"video/\") ||\n          mime.startsWith(\"audio/\") ||\n          mime.startsWith(\"application/pdf\") ||\n          mime.startsWith(\"application/zip\") ||\n          mime.startsWith(\"application/octet-stream\")\n        ) {\n          return {\n            content: [{ type: \"text\", text: \"\" }],\n            details: { url, format, contentType: mime, skipped: true },\n          };\n        }\n\n        const arrayBuffer = await response.arrayBuffer();\n\n        if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {\n          ctx.ui.notify(\"Response too large (>5MB)\", \"error\");\n          return {\n            content: [{ type: \"text\", text: \"\" }],\n            details: { url, format, error: \"Response too large\" },\n          };\n        }\n\n        let text = new TextDecoder().decode(arrayBuffer);\n\n        if (mime.includes(\"text/html\")) {\n          text = stripMediaTags(text);\n        }\n\n        // Process based on format\n        let output = \"\";\n        switch (format) {\n          case \"markdown\": {\n            if (mime.includes(\"text/html\")) {\n              output = turndownService.turndown(text);\n            } else {\n              output = text;\n            }\n            break;\n          }\n          case \"text\": {\n            if (mime.includes(\"text/html\")) {\n              output = htmlToText(text);\n            } else {\n              output = text;\n            }\n            break;\n          }\n          case \"html\": {\n            output = text;\n            break;\n          }\n        }\n\n        // Clean up whitespace\n        output = output.trim();\n\n        return {\n          content: [{ type: \"text\", text: output }],\n          details: { url, format, contentType: mime },\n        };\n      } catch (error) {\n        clearTimeout(timeoutId);\n\n        if (error instanceof Error) {\n          if (error.name === \"AbortError\") {\n            ctx.ui.notify(\"Request timed out or was cancelled\", \"error\");\n            return {\n              content: [{ type: \"text\", text: \"\" }],\n              details: { url, format, error: \"Timeout or cancelled\" },\n            };\n          }\n          ctx.ui.notify(`Fetch error: ${error.message}`, \"error\");\n          return {\n            content: [{ type: \"text\", text: \"\" }],\n            details: { url, format, error: error.message },\n          };\n        }\n\n        ctx.ui.notify(\"Unknown error occurred\", \"error\");\n        return {\n          content: [{ type: \"text\", text: \"\" }],\n          details: { url, format, error: \"Unknown error\" },\n        };\n      }\n    },\n  });\n}\n\n// Strip media and embed tags from raw HTML before conversion\nfunction stripMediaTags(html: string): string {\n  return (\n    html\n      .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n      .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, \"\")\n      .replace(/<svg\\b[\\s\\S]*?<\\/svg>/gi, \"\")\n      .replace(/<img\\b[^>]*>/gi, \"\")\n      .replace(/<picture\\b[\\s\\S]*?<\\/picture>/gi, \"\")\n      .replace(/<figure\\b[\\s\\S]*?<\\/figure>/gi, \"\")\n      .replace(/<canvas\\b[\\s\\S]*?<\\/canvas>/gi, \"\")\n      .replace(/<video\\b[\\s\\S]*?<\\/video>/gi, \"\")\n      .replace(/<audio\\b[\\s\\S]*?<\\/audio>/gi, \"\")\n      .replace(/<iframe\\b[\\s\\S]*?<\\/iframe>/gi, \"\")\n      .replace(/<object\\b[\\s\\S]*?<\\/object>/gi, \"\")\n      .replace(/<embed\\b[^>]*>/gi, \"\")\n      .replace(/<source\\b[^>]*>/gi, \"\")\n      .replace(/<track\\b[^>]*>/gi, \"\")\n  );\n}\n\n// Simple HTML to text conversion (no turndown for text format)\nfunction htmlToText(html: string): string {\n  return (\n    html\n      // Replace common block elements with newlines\n      .replace(/<\\/p>/gi, \"\\n\\n\")\n      .replace(/<br\\s*\\/?>/gi, \"\\n\")\n      .replace(/<\\/div>/gi, \"\\n\")\n      .replace(/<\\/h[1-6]>/gi, \"\\n\\n\")\n      .replace(/<\\/li>/gi, \"\\n\")\n      // Remove all remaining tags\n      .replace(/<[^>]+>/g, \"\")\n      // Decode common HTML entities\n      .replace(/&nbsp;/g, \" \")\n      .replace(/&amp;/g, \"&\")\n      .replace(/&lt;/g, \"<\")\n      .replace(/&gt;/g, \">\")\n      .replace(/&quot;/g, '\"')\n      .replace(/&#39;/g, \"'\")\n      // Clean up whitespace\n      .replace(/\\n{3,}/g, \"\\n\\n\")\n      .trim()\n  );\n}\n"
  },
  {
    "path": "agents/pi/extensions/whimsical-timer.ts",
    "content": "import type { AssistantMessage } from \"@mariozechner/pi-ai\";\nimport type { ExtensionAPI } from \"@mariozechner/pi-coding-agent\";\n\nconst messages = [\n  \"Schlepping...\",\n  \"Combobulating...\",\n  \"Doing...\",\n  \"Channelling...\",\n  \"Vibing...\",\n  \"Concocting...\",\n  \"Spelunking...\",\n  \"Transmuting...\",\n  \"Imagining...\",\n  \"Pontificating...\",\n  \"Whirring...\",\n  \"Cogitating...\",\n  \"Honking...\",\n  \"Noodling...\",\n  \"Percolating...\",\n  \"Ruminating...\",\n  \"Simmering...\",\n  \"Marinating...\",\n  \"Fermenting...\",\n  \"Gestating...\",\n  \"Hatching...\",\n  \"Brewing...\",\n  \"Steeping...\",\n  \"Contemplating...\",\n  \"Musing...\",\n  \"Pondering...\",\n  \"Dithering...\",\n  \"Faffing...\",\n  \"Puttering...\",\n  \"Tinkering...\",\n  \"Wrangling...\",\n  \"Discombobulating...\",\n  \"Recombobulating...\",\n  \"Befuddling...\",\n  \"Snorkeling...\",\n  \"Yodeling...\",\n  \"Zigzagging...\",\n  \"Somersaulting...\",\n  \"Canoodling...\",\n  \"Schmoozing...\",\n  \"Skedaddling...\",\n  \"Scampering...\",\n  \"Swashbuckling...\",\n  \"Effervescing...\",\n  \"Bubbling...\",\n  \"Enchanting...\",\n  \"Mesmerizing...\",\n  \"Sparkling...\",\n  \"Scintillating...\",\n  \"Synthesizing...\",\n  \"Procrastinating...\",\n  \"Dillydallying...\",\n  \"Lollygagging...\",\n  \"Sleuthing...\",\n  \"Rummaging...\",\n  \"Foraging...\",\n  \"Vamoosing...\",\n  \"Absconding...\",\n  \"Jamming...\",\n  \"Freestyling...\",\n  \"Frolicking...\",\n  \"Wibbling...\",\n  \"Wobbling...\",\n  \"Bonking...\",\n  \"Squelching...\",\n  \"Burbling...\",\n  \"Whooshing...\",\n  \"Clunking...\",\n  \"Rustling...\",\n  \"Bustling...\",\n  \"Doodling...\",\n  \"Squiggling...\",\n  \"Slithering...\",\n  \"Bouldering...\",\n  \"Tottering...\",\n  \"Jittering...\",\n  \"Twittering...\",\n  \"Chattering...\",\n  \"Splattering...\",\n  \"Hammering...\",\n  \"Stammering...\",\n  \"Shimmering...\",\n  \"Glimmering...\",\n  \"Skimming...\",\n  \"Drumming...\",\n  \"Fumbling...\",\n  \"Grumbling...\",\n  \"Mumbling...\",\n  \"Rumbling...\",\n  \"Stumbling...\",\n  \"Crumbling...\",\n  \"Tangling...\",\n  \"Jangling...\",\n  \"Mingling...\",\n  \"Jingling...\",\n  \"Bribing the compiler...\",\n  \"Tickling the stack...\",\n  \"Massaging the heap...\",\n  \"Summoning semicolons...\",\n  \"Herding pointers...\",\n  \"Untangling spaghetti...\",\n  \"Polishing the algorithms...\",\n  \"Waxing philosophical...\",\n  \"Reading tea leaves...\",\n  \"Warming up the hamsters...\",\n  \"Caffeinating...\",\n  \"Squinting at the problem...\",\n  \"Staring into the abyss...\",\n  \"Communing with the machine spirit...\",\n  \"Reticulating splines...\",\n  \"Calibrating the flux capacitor...\",\n];\n\nlet agentStartMs: number | null = null;\nlet intervalMs: number | null = null;\nlet timerInterval: ReturnType<typeof setInterval> | null = null;\nlet currentMessage: string | null = null;\n\nfunction pickRandom(): string {\n  return messages[Math.floor(Math.random() * messages.length)];\n}\n\nfunction formatDuration(ms: number): string {\n  const seconds = Math.floor(ms / 1000);\n  const mins = Math.floor(seconds / 60);\n  const secs = seconds % 60;\n  if (mins > 0) return String(mins) + \"m \" + String(secs) + \"s\";\n  return String(secs) + \"s\";\n}\n\nfunction isAssistantMessage(message: unknown): message is AssistantMessage {\n  if (!message || typeof message !== \"object\") return false;\n  return (message as { role?: unknown }).role === \"assistant\";\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"agent_start\", () => {\n    agentStartMs = Date.now();\n  });\n\n  pi.on(\"turn_start\", async (_event, ctx) => {\n    intervalMs = Date.now();\n    currentMessage = pickRandom();\n\n    ctx.ui.setWorkingMessage(currentMessage);\n\n    timerInterval = setInterval(() => {\n      if (intervalMs === null || currentMessage === null) return;\n      const elapsedMs = Date.now() - intervalMs;\n      ctx.ui.setWorkingMessage(currentMessage + \" (\" + formatDuration(elapsedMs) + \")\");\n    }, 1000);\n  });\n\n  pi.on(\"turn_end\", async (_event, ctx) => {\n    currentMessage = null;\n    ctx.ui.setWorkingMessage();\n  });\n\n  pi.on(\"agent_end\", (event, ctx) => {\n    if (!ctx.hasUI) return;\n    if (agentStartMs === null) return;\n\n    intervalMs = null;\n    if (timerInterval !== null) {\n      clearInterval(timerInterval);\n      timerInterval = null;\n    }\n\n    const elapsedMs = Date.now() - agentStartMs;\n    agentStartMs = null;\n    if (elapsedMs <= 0) return;\n\n    let output = 0;\n    for (const msg of event.messages) {\n      if (!isAssistantMessage(msg)) continue;\n      output += msg.usage.output || 0;\n    }\n\n    if (output <= 0) return;\n\n    const tps = (output / (elapsedMs / 1000)).toFixed(1);\n    const msg = \"TPS: \" + tps + \" tok/s took \" + formatDuration(elapsedMs);\n    ctx.ui.notify(msg, \"info\");\n  });\n}\n"
  },
  {
    "path": "agents/pi/mcp.json",
    "content": "{\n  \"mcpServers\": {\n    \"exa\": {\n      \"url\": \"https://mcp.exa.ai/mcp\"\n    },\n    \"fff\": {\n      \"command\": \"fff-mcp\",\n      \"directTools\": true,\n      \"lifecycle\": \"keep-alive\"\n    }\n  }\n}\n"
  },
  {
    "path": "agents/pi/models.json",
    "content": "{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"minimax/minimax-m2.5\": {\n          \"name\": \"Minimax M2.5 RR\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"sambanova\", \"fireworks\", \"friendli\"]\n            }\n          }\n        },\n        \"moonshotai/kimi-k2.5\": {\n          \"name\": \"Kimi K2.5 RR\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"fireworks\", \"together\"]\n            }\n          }\n        },\n        \"z-ai/glm-5\": {\n          \"name\": \"GLM 5 RR\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"together\", \"friendli\"]\n            }\n          }\n        }\n      }\n    },\n    \"opencode-go\": {\n      \"apiKey\": \"!pass show opencode-go\",\n      \"baseUrl\": \"https://opencode.ai/zen/go/v1\",\n      \"models\": [\n        {\n          \"id\": \"minimax-m2.7\",\n          \"name\": \"MiniMax M2.7\",\n          \"api\": \"anthropic-messages\",\n          \"provider\": \"opencode-go\",\n          \"baseUrl\": \"https://opencode.ai/zen/go\",\n          \"reasoning\": true,\n          \"input\": [\"text\"],\n          \"cost\": {\n            \"input\": 0.3,\n            \"output\": 1.2,\n            \"cacheRead\": 0.03,\n            \"cacheWrite\": 0\n          },\n          \"contextWindow\": 204800,\n          \"maxTokens\": 131072\n        },\n        {\n          \"id\": \"glm-5\",\n          \"name\": \"GLM 5\",\n          \"api\": \"openai-completions\",\n          \"provider\": \"opencode-go\",\n          \"reasoning\": true,\n          \"input\": [\"text\"],\n          \"cost\": {\n            \"input\": 1,\n            \"output\": 3.2,\n            \"cacheRead\": 0.2,\n            \"cacheWrite\": 0\n          },\n          \"contextWindow\": 204800,\n          \"maxTokens\": 131072\n        },\n        {\n          \"id\": \"kimi-k2.5\",\n          \"name\": \"Kimi K2.5\",\n          \"api\": \"openai-completions\",\n          \"provider\": \"opencode-go\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": {\n            \"input\": 0.6,\n            \"output\": 3.0,\n            \"cacheRead\": 0.1,\n            \"cacheWrite\": 0\n          },\n          \"contextWindow\": 256000,\n          \"maxTokens\": 256000\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "agents/pi/package.json",
    "content": "{\n  \"name\": \"nil\",\n  \"peerDependencies\": {\n    \"@mariozechner/pi-ai\": \"*\",\n    \"@mariozechner/pi-coding-agent\": \"*\",\n    \"@mariozechner/pi-tui\": \"*\",\n    \"@sinclair/typebox\": \"*\",\n    \"turndown\": \"^7.2.2\",\n    \"koffi\": \"^2.11.0\"\n  },\n  \"private\": true,\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "agents/pi/settings.json",
    "content": "{\n  \"lastChangelogVersion\": \"0.70.6\",\n  \"defaultProvider\": \"deepseek\",\n  \"defaultModel\": \"deepseek-v4-flash\",\n  \"defaultThinkingLevel\": \"minimal\",\n  \"theme\": \"rose-pine-dawn\",\n  \"quietStartup\": true,\n  \"packages\": [\n    \"./extensions/pi-codemode\",\n    \"npm:@plannotator/pi-extension\",\n    \"git:github.com/SamuelLHuber/pi-fff\",\n    \"npm:pi-boomerang\"\n  ],\n  \"editorPaddingX\": 0,\n  \"autocompleteMaxVisible\": 10,\n  \"transport\": \"auto\",\n  \"doubleEscapeAction\": \"tree\",\n  \"enabledModels\": [],\n  \"hideThinkingBlock\": false\n}"
  },
  {
    "path": "agents/pi/themes/rose-pine-dawn.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"rose-pine-dawn\",\n  \"vars\": {\n    \"base\": \"#faf4ed\",\n    \"surface\": \"#fffaf3\",\n    \"overlay\": \"#f2e9e1\",\n    \"muted\": \"#9893a5\",\n    \"subtle\": \"#797593\",\n    \"text\": \"#575279\",\n    \"love\": \"#b4637a\",\n    \"gold\": \"#ea9d34\",\n    \"rose\": \"#d7827e\",\n    \"pine\": \"#286983\",\n    \"foam\": \"#56949f\",\n    \"iris\": \"#907aa9\"\n  },\n  \"colors\": {\n    \"accent\": \"iris\",\n    \"border\": \"subtle\",\n    \"borderAccent\": \"pine\",\n    \"borderMuted\": \"overlay\",\n    \"success\": \"pine\",\n    \"error\": \"love\",\n    \"warning\": \"gold\",\n    \"muted\": \"muted\",\n    \"dim\": \"subtle\",\n    \"text\": \"text\",\n    \"thinkingText\": \"muted\",\n\n    \"selectedBg\": \"overlay\",\n    \"userMessageBg\": \"surface\",\n    \"userMessageText\": \"text\",\n    \"customMessageBg\": \"overlay\",\n    \"customMessageText\": \"text\",\n    \"customMessageLabel\": \"iris\",\n    \"toolPendingBg\": \"overlay\",\n    \"toolSuccessBg\": \"surface\",\n    \"toolErrorBg\": \"overlay\",\n    \"toolTitle\": \"iris\",\n    \"toolOutput\": \"muted\",\n\n    \"mdHeading\": \"rose\",\n    \"mdLink\": \"foam\",\n    \"mdLinkUrl\": \"subtle\",\n    \"mdCode\": \"iris\",\n    \"mdCodeBlock\": \"text\",\n    \"mdCodeBlockBorder\": \"overlay\",\n    \"mdQuote\": \"muted\",\n    \"mdQuoteBorder\": \"subtle\",\n    \"mdHr\": \"overlay\",\n    \"mdListBullet\": \"rose\",\n\n    \"toolDiffAdded\": \"pine\",\n    \"toolDiffRemoved\": \"love\",\n    \"toolDiffContext\": \"muted\",\n\n    \"syntaxComment\": \"muted\",\n    \"syntaxKeyword\": \"pine\",\n    \"syntaxFunction\": \"rose\",\n    \"syntaxVariable\": \"gold\",\n    \"syntaxString\": \"foam\",\n    \"syntaxNumber\": \"gold\",\n    \"syntaxType\": \"iris\",\n    \"syntaxOperator\": \"pine\",\n    \"syntaxPunctuation\": \"subtle\",\n\n    \"thinkingOff\": \"muted\",\n    \"thinkingMinimal\": \"subtle\",\n    \"thinkingLow\": \"pine\",\n    \"thinkingMedium\": \"foam\",\n    \"thinkingHigh\": \"iris\",\n    \"thinkingXhigh\": \"love\",\n\n    \"bashMode\": \"pine\"\n  }\n}\n"
  },
  {
    "path": "agents/pi/treesitter.ts",
    "content": "import path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport { createWriteStream } from \"node:fs\";\nimport os from \"node:os\";\nimport https from \"node:https\";\nimport Parser from \"tree-sitter\";\nimport { createPicker } from \"../create-picker\";\nimport type { PickerContext } from \"../types\";\n\ninterface ParserConfig {\n  /** Language identifier (e.g., \"typescript\", \"rust\") */\n  language: string;\n  /** File extensions to scan (e.g., [\".ts\", \".tsx\"]) */\n  extensions: string[];\n  /** URL to download the parser WASM */\n  url?: string;\n  /** Optional local path to parser (instead of URL) */\n  localPath?: string;\n}\n\ninterface Symbol {\n  name: string;\n  kind: string;\n  file: string;\n  line: number;\n  column: number;\n}\n\n/** Simple LRU cache with max size limit */\nclass LRUCache<K, V> {\n  private cache = new Map<K, V>();\n  private maxSize: number;\n\n  constructor(maxSize: number) {\n    this.maxSize = maxSize;\n  }\n\n  get(key: K): V | undefined {\n    const value = this.cache.get(key);\n    if (value !== undefined) {\n      // Move to end (most recently used)\n      this.cache.delete(key);\n      this.cache.set(key, value);\n    }\n    return value;\n  }\n\n  set(key: K, value: V): void {\n    if (this.cache.has(key)) {\n      this.cache.delete(key);\n    } else if (this.cache.size >= this.maxSize) {\n      // Remove least recently used (first item)\n      const firstKey = this.cache.keys().next().value;\n      if (firstKey !== undefined) {\n        this.cache.delete(firstKey);\n      }\n    }\n    this.cache.set(key, value);\n  }\n\n  has(key: K): boolean {\n    return this.cache.has(key);\n  }\n\n  clear(): void {\n    this.cache.clear();\n  }\n\n  get size(): number {\n    return this.cache.size;\n  }\n}\n\n// Cache for parsed symbols per language - limited to 10 entries to prevent unbounded growth\nconst symbolCache = new LRUCache<string, Symbol[]>(10);\nconst runtimeCache = new Map<string, { parser: Parser; query: Parser.Query }>();\nlet parsersDir: string | null = null;\nlet queriesDir: string | null = null;\n\nasync function getParsersDir(): Promise<string> {\n  if (!parsersDir) {\n    parsersDir = path.join(os.homedir(), \".pi\", \"agent\", \"parsers\");\n    await fs.mkdir(parsersDir, { recursive: true });\n  }\n  return parsersDir;\n}\n\nasync function getQueriesDir(): Promise<string> {\n  if (!queriesDir) {\n    queriesDir = path.join(os.homedir(), \".pi\", \"agent\", \"queries\");\n    await fs.mkdir(queriesDir, { recursive: true });\n  }\n  return queriesDir;\n}\n\nasync function downloadFile(url: string, dest: string): Promise<boolean> {\n  return new Promise((resolve) => {\n    const file = createWriteStream(dest);\n    https\n      .get(url, (res) => {\n        if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {\n          downloadFile(res.headers.location, dest).then(resolve);\n          return;\n        }\n        if (res.statusCode !== 200) {\n          resolve(false);\n          return;\n        }\n        res.pipe(file);\n        file.on(\"finish\", () => {\n          file.close(() => resolve(true));\n        });\n      })\n      .on(\"error\", () => {\n        fs.unlink(dest).catch(() => {});\n        resolve(false);\n      });\n  });\n}\n\nasync function ensureParser(config: ParserConfig): Promise<string | null> {\n  if (config.localPath) {\n    try {\n      await fs.access(config.localPath);\n      return config.localPath;\n    } catch {\n      // local path doesn't exist, continue\n    }\n  }\n\n  if (!config.url) {\n    return null;\n  }\n\n  const parsersDir = await getParsersDir();\n  const fileName = `${config.language}.wasm`;\n  const destPath = path.join(parsersDir, fileName);\n\n  try {\n    await fs.access(destPath);\n    return destPath;\n  } catch {\n    // doesn't exist, download it\n  }\n\n  const success = await downloadFile(config.url, destPath);\n  return success ? destPath : null;\n}\n\nasync function walkDir(dir: string, extensions: string[], files: string[] = []): Promise<string[]> {\n  try {\n    const entries = await fs.readdir(dir, { withFileTypes: true });\n    for (const entry of entries) {\n      const fullPath = path.join(dir, entry.name);\n      if (entry.isDirectory()) {\n        if (entry.name === \"node_modules\" || entry.name === \".git\" || entry.name.startsWith(\".\")) {\n          continue;\n        }\n        await walkDir(fullPath, extensions, files);\n      } else if (entry.isFile()) {\n        const ext = path.extname(entry.name);\n        if (extensions.includes(ext)) {\n          files.push(fullPath);\n        }\n      }\n    }\n  } catch (err) {\n    console.error(\"[pi-ckers] Error walking directory:\", err);\n  }\n  return files;\n}\n\nasync function findFiles(cwd: string, extensions: string[]): Promise<string[]> {\n  return walkDir(cwd, extensions);\n}\n\nasync function readQueryFile(filePath: string): Promise<string | null> {\n  try {\n    return await fs.readFile(filePath, \"utf8\");\n  } catch {\n    return null;\n  }\n}\n\n/** Check if a path is a directory */\nasync function isDirectory(filePath: string): Promise<boolean> {\n  try {\n    const stat = await fs.stat(filePath);\n    return stat.isDirectory();\n  } catch {\n    return false;\n  }\n}\n\n/** Default discovery: looks for {language}.scm in search paths, then default dir */\nasync function defaultDiscoverQuery(\n  language: string,\n  searchPaths: string[],\n  defaultDir: string\n): Promise<string | null> {\n  // Search custom paths first (priority order)\n  for (const searchPath of searchPaths) {\n    const isDir = await isDirectory(searchPath);\n    const queryFile = isDir\n      ? path.join(searchPath, `${language}.scm`)\n      : searchPath;\n    const content = await readQueryFile(queryFile);\n    if (content) return content;\n  }\n\n  // Fallback to default queries dir\n  const queryFile = path.join(defaultDir, `${language}.scm`);\n  return readQueryFile(queryFile);\n}\n\nasync function loadQuery(\n  language: string,\n  queriesDirs: string[] | QueryDiscovery | undefined\n): Promise<string | null> {\n  const defaultDir = await getQueriesDir();\n\n  // If it's a function, use it as custom discovery\n  if (typeof queriesDirs === \"function\") {\n    return queriesDirs({\n      language,\n      defaultDir,\n      readFile: readQueryFile,\n    });\n  }\n\n  // Otherwise use default discovery with array of paths\n  return defaultDiscoverQuery(language, queriesDirs ?? [], defaultDir);\n}\n\nasync function parseFileWithTreesitter(\n  filePath: string,\n  runtime: { parser: Parser; query: Parser.Query },\n): Promise<Symbol[]> {\n  const symbols: Symbol[] = [];\n\n  try {\n    const sourceCode = await fs.readFile(filePath, \"utf8\");\n    const tree = runtime.parser.parse(sourceCode);\n    const matches = runtime.query.matches(tree.rootNode);\n\n    for (const match of matches) {\n      for (const capture of match.captures) {\n        const node = capture.node;\n        const captureName = capture.name;\n\n        symbols.push({\n          name: node.text,\n          kind: captureName,\n          file: filePath,\n          line: node.startPosition.row + 1,\n          column: node.startPosition.column,\n        });\n      }\n    }\n  } catch (err) {\n    // If tree-sitter fails, return empty\n    console.error(`[pi-ckers] Error parsing file ${filePath}:`, err);\n  }\n\n  return symbols;\n}\n\nasync function parseWorkspace(\n  config: ParserConfig,\n  cwd: string,\n  maxFiles: number,\n  notify: (msg: string, type: \"info\" | \"error\") => void,\n  queriesDirs: string[] | QueryDiscovery | undefined\n): Promise<Symbol[]> {\n  const cacheKey = `${config.language}:${cwd}`;\n  if (symbolCache.has(cacheKey)) {\n    return symbolCache.get(cacheKey)!;\n  }\n\n  // Load query file - required\n  const queryText = await loadQuery(config.language, queriesDirs);\n  if (!queryText) {\n    // No query file, no symbols\n    return [];\n  }\n\n  // Ensure parser is available\n  const parserPath = await ensureParser(config);\n  if (!parserPath) {\n    notify(`Tree-sitter parser not available for ${config.language}`, \"error\");\n    return [];\n  }\n\n  const files = await findFiles(cwd, config.extensions);\n  const allSymbols: Symbol[] = [];\n\n  let runtime = runtimeCache.get(config.language);\n  if (!runtime) {\n    const parser = new Parser();\n    const loadedLanguage = await Parser.Language.load(parserPath);\n    parser.setLanguage(loadedLanguage);\n    runtime = {\n      parser,\n      query: loadedLanguage.query(queryText),\n    };\n    runtimeCache.set(config.language, runtime);\n  }\n\n  for (const file of files.slice(0, maxFiles)) {\n    const symbols = await parseFileWithTreesitter(file, runtime);\n    allSymbols.push(...symbols);\n  }\n\n  symbolCache.set(cacheKey, allSymbols);\n  return allSymbols;\n}\n\nfunction filterSymbols(symbols: Symbol[], query: string): Symbol[] {\n  const lowerQuery = query.toLowerCase();\n  return symbols\n    .filter((s) => s.name.toLowerCase().includes(lowerQuery))\n    .slice(0, 20);\n}\n\nfunction formatSymbolValue(symbol: Symbol, isQuotedPrefix: boolean): string {\n  const fileName = path.basename(symbol.file);\n  const location = `${fileName}:${symbol.line}`;\n  const value = `${symbol.name} (${location})`;\n\n  if (!isQuotedPrefix && !value.includes(\" \")) {\n    return `@ts:${value}`;\n  }\n  return `@ts:\"${value}\"`;\n}\n\nexport interface QueryDiscoveryContext {\n  /** Language identifier (e.g., \"typescript\") */\n  language: string;\n  /** Default fallback directory (~/.pi/agent/queries) */\n  defaultDir: string;\n  /** Read file helper - returns null if not found */\n  readFile: (filePath: string) => Promise<string | null>;\n}\n\n/** Custom function to discover query files. Return the query text or null. */\nexport type QueryDiscovery = (ctx: QueryDiscoveryContext) => Promise<string | null>;\n\nexport interface TreesitterPickerOptions {\n  /** Parser configurations for different languages */\n  parsers: ParserConfig[];\n  /** Maximum number of files to scan per language (default: 100) */\n  maxFiles?: number;\n  /** Refresh interval in milliseconds (default: 60000) */\n  refreshInterval?: number;\n  /** Directories or files to search for query files, or a function that returns them.\n   *\n   *  - string[]: Array of directories or file paths to search. For directories,\n   *    looks for {language}.scm. For files, reads them directly.\n   *  - function: Custom discovery that returns the query text directly.\n   *\n   *  Falls back to ~/.pi/agent/queries/{language}.scm if not found.\n   */\n  queriesDirs?: string[] | QueryDiscovery;\n}\n\n/**\n * Create a treesitter workspace symbols picker for @ts: completions.\n *\n * Query files are loaded from ~/.pi/agent/queries/{language}.scm by default.\n *\n * Use queriesDirs to customize:\n * - string[]: Directories or specific files to search\n * - function: Custom discovery logic returning query text directly\n *\n * Parsers are downloaded on-demand to ~/.pi/agent/parsers/\n *\n * @example\n * ```typescript\n * import { tsWorkspaceSymbolsPicker } from \"@elianiva/pi-ckers/builtin/treesitter\";\n * import path from \"node:path\";\n *\n * // Simple: Custom directories (looks for {language}.scm in each)\n * const picker = tsWorkspaceSymbolsPicker({\n *   parsers: [...],\n *   queriesDirs: [\"/home/user/.config/nvim/queries\"]\n * });\n *\n * // Advanced: Custom discovery for nvim-treesitter structure\n * const nvimPicker = tsWorkspaceSymbolsPicker({\n *   parsers: [...],\n *   queriesDirs: async ({ language, defaultDir, readFile }) => {\n *     // Look for locals.scm in nvim-treesitter structure\n *     const nvimPath = path.join(\n *       \"/Users/elianiva/.local/share/nvim/lazy/nvim-treesitter/queries\",\n *       language,\n *       \"locals.scm\"\n *     );\n *     const content = await readFile(nvimPath);\n *     if (content) return content;\n *\n *     // Fallback to default\n *     return readFile(path.join(defaultDir, `${language}.scm`));\n *   }\n * });\n * ```\n */\nexport const tsWorkspaceSymbolsPicker = (options: TreesitterPickerOptions) => {\n  const maxFiles = options.maxFiles ?? 100;\n  const refreshInterval = options.refreshInterval ?? 60000;\n  const queriesDirs = options.queriesDirs;\n  let refreshTimer: ReturnType<typeof setInterval> | null = null;\n  let notify: (msg: string, type: \"info\" | \"error\") => void = console.error;\n  let currentCwd: string = \"\";\n\n  return createPicker({\n    type: \"sync\",\n    prefix: \"@ts:\",\n    minQueryLength: 1,\n\n    init: async (ctx) => {\n      currentCwd = ctx.cwd;\n      notify = ctx.ui.notify.bind(ctx.ui);\n\n      await getQueriesDir();\n      await getParsersDir();\n\n      for (const config of options.parsers) {\n        await parseWorkspace(config, ctx.cwd, maxFiles, notify, queriesDirs);\n      }\n\n      if (refreshTimer) {\n        clearInterval(refreshTimer);\n      }\n      refreshTimer = setInterval(async () => {\n        if (!currentCwd) return;\n        symbolCache.clear();\n        for (const config of options.parsers) {\n          await parseWorkspace(config, currentCwd, maxFiles, notify, queriesDirs);\n        }\n      }, refreshInterval);\n    },\n\n    search: (query: string, ctx: PickerContext) => {\n      const allSymbols: Symbol[] = [];\n\n      for (const config of options.parsers) {\n        const cacheKey = `${config.language}:${ctx.cwd}`;\n        const symbols = symbolCache.get(cacheKey);\n        if (symbols) {\n          allSymbols.push(...symbols);\n        }\n      }\n\n      if (allSymbols.length === 0) {\n        return null;\n      }\n\n      const filtered = filterSymbols(allSymbols, query);\n      if (filtered.length === 0) {\n        return null;\n      }\n\n      return filtered.map((symbol) => {\n        const fileName = path.basename(symbol.file);\n        return {\n          value: formatSymbolValue(symbol, ctx.isQuotedPrefix),\n          label: symbol.name,\n          description: `${symbol.kind} · ${fileName}:${symbol.line}`,\n        };\n      });\n    },\n\n    clearCache: () => {\n      symbolCache.clear();\n      runtimeCache.clear();\n    },\n\n    destroy: () => {\n      symbolCache.clear();\n      runtimeCache.clear();\n      if (refreshTimer) {\n        clearInterval(refreshTimer);\n        refreshTimer = null;\n      }\n      currentCwd = \"\";\n    },\n  });\n};\n"
  },
  {
    "path": "agents/skills/build-feature/SKILL.md",
    "content": "---\nname: build-feature\ndescription: Use when creating or developing a feature, this contains a focused instructions for building features.\n---\n\n## Rules\n- Make sure you're certain from all ambiguities, otherwise ask for clarification\n- If there's a plan, follow it and ask for clarification if needed\n- Follow the Tracer Bullets to build a small, focused, end-to-end slice of the feature\n\n## Tracer Bullets\nWhen building features, build a tiny, end-to-end slice of the feature first, seek feedback, then expand out from there.\nTracer bullets comes from the Pragmatic Programmer. When building systems, you want to write code that gets you feedback as quickly as possible.\nTracer bullets are small slices of functionality that go through all layers of the system, allowing you to test and validate your approach early.\nThis helps in identifying potential issues and ensures that the overall architecture is sound before investing significant time in development.\n"
  },
  {
    "path": "agents/skills/code-review/SKILL.md",
    "content": "---\nname: code-review\nversion: 1.0.0\ndescription: Code review entire project\n---\n\n# Code Review\n\nAct as a highly critical and analytical code reviewer.\n\n## Process\n\n- Provide detailed feedback on code quality, correctness, and maintainability\n- Offer suggestions for improvements and potential alternatives\n- Ensure code is readable, maintainable, and efficient\n\n$ARGUMENTS\n"
  },
  {
    "path": "agents/skills/debugging/SKILL.md",
    "content": "---\nname: debugging\ndescription: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes - four-phase framework (root cause investigation, pattern analysis, hypothesis testing, implementation) that ensures understanding before attempting solutions\n---\n\n# Systematic Debugging\n\n## Overview\n\nRandom fixes waste time and create new bugs. Quick patches mask underlying issues.\n\n**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.\n\n**Violating the letter of this process is violating the spirit of debugging.**\n\n## The Iron Law\n\n```\nNO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST\n```\n\nIf you haven't completed Phase 1, you cannot propose fixes.\n\n## When to Use\n\nUse for ANY technical issue:\n\n- Test failures\n- Bugs in production\n- Unexpected behavior\n- Performance problems\n- Build failures\n- Integration issues\n\n**Use this ESPECIALLY when:**\n\n- Under time pressure (emergencies make guessing tempting)\n- \"Just one quick fix\" seems obvious\n- You've already tried multiple fixes\n- Previous fix didn't work\n- You don't fully understand the issue\n\n**Don't skip when:**\n\n- Issue seems simple (simple bugs have root causes too)\n- You're in a hurry (rushing guarantees rework)\n- Manager wants it fixed NOW (systematic is faster than thrashing)\n\n## The Four Phases\n\nYou MUST complete each phase before proceeding to the next.\n\n### Phase 1: Root Cause Investigation\n\n**BEFORE attempting ANY fix:**\n\n1. **Read Error Messages Carefully**\n\n   - Don't skip past errors or warnings\n   - They often contain the exact solution\n   - Read stack traces completely\n   - Note line numbers, file paths, error codes\n\n2. **Reproduce Consistently**\n\n   - Can you trigger it reliably?\n   - What are the exact steps?\n   - Does it happen every time?\n   - If not reproducible → gather more data, don't guess\n\n3. **Check Recent Changes**\n\n   - What changed that could cause this?\n   - Git diff, recent commits\n   - New dependencies, config changes\n   - Environmental differences\n\n4. **Gather Evidence in Multi-Component Systems**\n\n   **WHEN system has multiple components (CI → build → signing, API → service → database):**\n\n   **BEFORE proposing fixes, add diagnostic instrumentation:**\n\n   ```\n   For EACH component boundary:\n     - Log what data enters component\n     - Log what data exits component\n     - Verify environment/config propagation\n     - Check state at each layer\n\n   Run once to gather evidence showing WHERE it breaks\n   THEN analyze evidence to identify failing component\n   THEN investigate that specific component\n   ```\n\n   **Example (multi-layer system):**\n\n   ```bash\n   # Layer 1: Workflow\n   echo \"=== Secrets available in workflow: ===\"\n   echo \"IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}\"\n\n   # Layer 2: Build script\n   echo \"=== Env vars in build script: ===\"\n   env | grep IDENTITY || echo \"IDENTITY not in environment\"\n\n   # Layer 3: Signing script\n   echo \"=== Keychain state: ===\"\n   security list-keychains\n   security find-identity -v\n\n   # Layer 4: Actual signing\n   codesign --sign \"$IDENTITY\" --verbose=4 \"$APP\"\n   ```\n\n   **This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗)\n\n5. **Trace Data Flow**\n\n   **WHEN error is deep in call stack:**\n\n   **REQUIRED SUB-SKILL:** Use superpowers root-cause-tracing for backward tracing technique\n\n   **Quick version:**\n\n   - Where does bad value originate?\n   - What called this with bad value?\n   - Keep tracing up until you find the source\n   - Fix at source, not at symptom\n\n### Phase 2: Pattern Analysis\n\n**Find the pattern before fixing:**\n\n1. **Find Working Examples**\n\n   - Locate similar working code in same codebase\n   - What works that's similar to what's broken?\n\n2. **Compare Against References**\n\n   - If implementing pattern, read reference implementation COMPLETELY\n   - Don't skim - read every line\n   - Understand the pattern fully before applying\n\n3. **Identify Differences**\n\n   - What's different between working and broken?\n   - List every difference, however small\n   - Don't assume \"that can't matter\"\n\n4. **Understand Dependencies**\n   - What other components does this need?\n   - What settings, config, environment?\n   - What assumptions does it make?\n\n### Phase 3: Hypothesis and Testing\n\n**Scientific method:**\n\n1. **Form Single Hypothesis**\n\n   - State clearly: \"I think X is the root cause because Y\"\n   - Write it down\n   - Be specific, not vague\n\n2. **Test Minimally**\n\n   - Make the SMALLEST possible change to test hypothesis\n   - One variable at a time\n   - Don't fix multiple things at once\n\n3. **Verify Before Continuing**\n\n   - Did it work? Yes → Phase 4\n   - Didn't work? Form NEW hypothesis\n   - DON'T add more fixes on top\n\n4. **When You Don't Know**\n   - Say \"I don't understand X\"\n   - Don't pretend to know\n   - Ask for help\n   - Research more\n\n### Phase 4: Implementation\n\n**Fix the root cause, not the symptom:**\n\n1. **Decide on Testing Strategy**\n\n   **Auto-decide based on complexity:**\n\n   - **Write test for**: Complex algorithms, business logic, data transformations where bugs are likely\n   - **Skip test for**: UI components, React hooks, simple CRUD, straightforward mappings, anything you're 100% certain is correct\n   - **Test type**: Only deterministic unit tests - no integration tests, no complex mocking, no async complexity\n\n   **If writing test:**\n\n   - Simplest possible reproduction\n   - Automated test that fails before fix\n   - Verify logic, not implementation details\n\n   **If skipping test:**\n\n   - Verify fix with typecheck/lint\n   - Manual verification for UI changes\n   - Code review confidence that fix is correct\n\n2. **Implement Single Fix**\n\n   - Address the root cause identified\n   - ONE change at a time\n   - No \"while I'm here\" improvements\n   - No bundled refactoring\n\n3. **Verify Fix**\n\n   **If test was written:**\n\n   - Test passes now?\n   - No other tests broken?\n\n   **If no test:**\n\n   - Typecheck passes?\n   - Lint clean?\n   - Manual verification confirms fix?\n\n   **Always check:**\n\n   - Issue actually resolved?\n   - No regressions in related functionality?\n\n4. **If Fix Doesn't Work**\n\n   - STOP\n   - Count: How many fixes have you tried?\n   - If < 3: Return to Phase 1, re-analyze with new information\n   - **If ≥ 3: STOP and question the architecture (step 5 below)**\n   - DON'T attempt Fix #4 without architectural discussion\n\n5. **If 3+ Fixes Failed: Question Architecture**\n\n   **Pattern indicating architectural problem:**\n\n   - Each fix reveals new shared state/coupling/problem in different place\n   - Fixes require \"massive refactoring\" to implement\n   - Each fix creates new symptoms elsewhere\n\n   **STOP and question fundamentals:**\n\n   - Is this pattern fundamentally sound?\n   - Are we \"sticking with it through sheer inertia\"?\n   - Should we refactor architecture vs. continue fixing symptoms?\n\n   **Discuss with your human partner before attempting more fixes**\n\n   This is NOT a failed hypothesis - this is a wrong architecture.\n\n## Red Flags - STOP and Follow Process\n\nIf you catch yourself thinking:\n\n- \"Quick fix for now, investigate later\"\n- \"Just try changing X and see if it works\"\n- \"Add multiple changes, run tests\"\n- \"It's probably X, let me fix that\"\n- \"I don't fully understand but this might work\"\n- \"Pattern says X but I'll adapt it differently\"\n- \"Here are the main problems: [lists fixes without investigation]\"\n- Proposing solutions before tracing data flow\n- **\"One more fix attempt\" (when already tried 2+)**\n- **Each fix reveals new problem in different place**\n- **Writing tests for UI components when you're certain the fix is correct**\n\n**ALL of these mean: STOP. Return to Phase 1.**\n\n**If 3+ fixes failed:** Question the architecture (see Phase 4.5)\n\n## your human partner's Signals You're Doing It Wrong\n\n**Watch for these redirections:**\n\n- \"Is that not happening?\" - You assumed without verifying\n- \"Will it show us...?\" - You should have added evidence gathering\n- \"Stop guessing\" - You're proposing fixes without understanding\n- \"Ultrathink this\" - Question fundamentals, not just symptoms\n- \"We're stuck?\" (frustrated) - Your approach isn't working\n\n**When you see these:** STOP. Return to Phase 1.\n\n## Common Rationalizations\n\n| Excuse                                       | Reality                                                                       |\n| -------------------------------------------- | ----------------------------------------------------------------------------- |\n| \"Issue is simple, don't need process\"        | Simple issues have root causes too. Process is fast for simple bugs.          |\n| \"Emergency, no time for process\"             | Systematic debugging is FASTER than guess-and-check thrashing.                |\n| \"Just try this first, then investigate\"      | First fix sets the pattern. Do it right from the start.                       |\n| \"Multiple fixes at once saves time\"          | Can't isolate what worked. Causes new bugs.                                   |\n| \"Reference too long, I'll adapt the pattern\" | Partial understanding guarantees bugs. Read it completely.                    |\n| \"I see the problem, let me fix it\"           | Seeing symptoms ≠ understanding root cause.                                   |\n| \"One more fix attempt\" (after 2+ failures)   | 3+ failures = architectural problem. Question pattern, don't fix again.       |\n| \"UI fix doesn't need tests\"                  | Correct! UI components verified via typecheck/manual testing, not unit tests. |\n\n## Quick Reference\n\n| Phase                 | Key Activities                                         | Success Criteria            |\n| --------------------- | ------------------------------------------------------ | --------------------------- |\n| **1. Root Cause**     | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY     |\n| **2. Pattern**        | Find working examples, compare                         | Identify differences        |\n| **3. Hypothesis**     | Form theory, test minimally                            | Confirmed or new hypothesis |\n| **4. Implementation** | Create test, fix, verify                               | Bug resolved, tests pass    |\n\n## When Process Reveals \"No Root Cause\"\n\nIf systematic investigation reveals issue is truly environmental, timing-dependent, or external:\n\n1. You've completed the process\n2. Document what you investigated\n3. Implement appropriate handling (retry, timeout, error message)\n4. Add monitoring/logging for future investigation\n\n**But:** 95% of \"no root cause\" cases are incomplete investigation.\n\n## Integration with Other Skills\n\n**This skill requires using:**\n\n- **root-cause-tracing** - REQUIRED when error is deep in call stack (see Phase 1, Step 5)\n\n**Testing skills (when needed):**\n\n- **test-driven-development** (if available) - Use when fixing complex business logic that needs test coverage\n- Skip for UI components, simple CRUD, or anything verifiable via typecheck/manual testing\n\n## Real-World Impact\n\nFrom debugging sessions:\n\n- Systematic approach: 15-30 minutes to fix\n- Random fixes approach: 2-3 hours of thrashing\n- First-time fix rate: 95% vs 40%\n- New bugs introduced: Near zero vs common\n\n"
  },
  {
    "path": "agents/skills/deslop/SKILL.md",
    "content": "---\nname: deslop\ndescription: Remove all AI-generated slop like useless comments, try/catch blocks, and casts\n---\n\nRemove all AI-generated slop, this includes:\n\n- Extra comments that a human wouldn't add or is inconsistent with the rest of the file, remove the what, keep the why\n- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)\n- Casts to any to get around type issues\n- Any other style that is inconsistent with the file\n\nReport at the end with only a 1-3 sentence summary of what you changed\n"
  },
  {
    "path": "agents/skills/effect-best-practices/SKILL.md",
    "content": "---\nname: effect-best-practices\ndescription: Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Effect.Service, Schema.TaggedError, Layer composition, or effect-atom React components.\nmetadata:\n    version: 1.0.0\n---\n\n# Effect-TS Best Practices\n\nThis skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability.\n\n## Quick Reference: Critical Rules\n\n| Category | DO | DON'T |\n|----------|-----|-------|\n| Services | `Effect.Service` with `accessors: true` | `Context.Tag` for business logic |\n| Dependencies | `dependencies: [Dep.Default]` in service | Manual `Layer.provide` at usage sites |\n| Errors | `Schema.TaggedError` with `message` field | Plain classes or generic Error |\n| Error Specificity | `UserNotFoundError`, `SessionExpiredError` | Generic `NotFoundError`, `BadRequestError` |\n| Error Handling | `catchTag`/`catchTags` | `catchAll` or `mapError` |\n| IDs | `Schema.UUID.pipe(Schema.brand(\"@App/EntityId\"))` | Plain `string` for entity IDs |\n| Functions | `Effect.fn(\"Service.method\")` | Anonymous generators |\n| Logging | `Effect.log` with structured data | `console.log` |\n| Config | `Config.*` with validation | `process.env` directly |\n| Options | `Option.match` with both cases | `Option.getOrThrow` |\n| Nullability | `Option<T>` in domain types | `null`/`undefined` |\n| Atoms | `Atom.make` outside components | Creating atoms inside render |\n| Atom State | `Atom.keepAlive` for global state | Forgetting keepAlive for persistent state |\n| Atom Updates | `useAtomSet` in React components | `Atom.update` imperatively from React |\n| Atom Cleanup | `get.addFinalizer()` for side effects | Missing cleanup for event listeners |\n| Atom Results | `Result.builder` with `onErrorTag` | Ignoring loading/error states |\n\n## Service Definition Pattern\n\n**Always use `Effect.Service`** for business logic services. This provides automatic accessors, built-in `Default` layer, and proper dependency declaration.\n\n```typescript\nimport { Effect } from \"effect\"\n\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", {\n    accessors: true,\n    dependencies: [UserRepo.Default, CacheService.Default],\n    effect: Effect.gen(function* () {\n        const repo = yield* UserRepo\n        const cache = yield* CacheService\n\n        const findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n            const cached = yield* cache.get(id)\n            if (Option.isSome(cached)) return cached.value\n\n            const user = yield* repo.findById(id)\n            yield* cache.set(id, user)\n            return user\n        })\n\n        const create = Effect.fn(\"UserService.create\")(function* (data: CreateUserInput) {\n            const user = yield* repo.create(data)\n            yield* Effect.log(\"User created\", { userId: user.id })\n            return user\n        })\n\n        return { findById, create }\n    }),\n}) {}\n\n// Usage - dependencies are already wired\nconst program = Effect.gen(function* () {\n    const user = yield* UserService.findById(userId)\n    return user\n})\n\n// At app root\nconst MainLive = Layer.mergeAll(UserService.Default, OtherService.Default)\n```\n\n**When `Context.Tag` is acceptable:**\n- Infrastructure with runtime injection (Cloudflare KV, worker bindings)\n- Factory patterns where resources are provided externally\n\nSee `references/service-patterns.md` for detailed patterns.\n\n## Error Definition Pattern\n\n**Always use `Schema.TaggedError`** for errors. This makes them serializable (required for RPC) and provides consistent structure.\n\n```typescript\nimport { Schema } from \"effect\"\nimport { HttpApiSchema } from \"@effect/platform\"\n\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    {\n        userId: UserId,\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\nexport class UserCreateError extends Schema.TaggedError<UserCreateError>()(\n    \"UserCreateError\",\n    {\n        message: Schema.String,\n        cause: Schema.optional(Schema.String),\n    },\n    HttpApiSchema.annotations({ status: 400 }),\n) {}\n```\n\n**Error handling - use `catchTag`/`catchTags`:**\n\n```typescript\n// CORRECT - preserves type information\nyield* repo.findById(id).pipe(\n    Effect.catchTag(\"DatabaseError\", (err) =>\n        Effect.fail(new UserNotFoundError({ userId: id, message: \"Lookup failed\" }))\n    ),\n    Effect.catchTag(\"ConnectionError\", (err) =>\n        Effect.fail(new ServiceUnavailableError({ message: \"Database unreachable\" }))\n    ),\n)\n\n// CORRECT - multiple tags at once\nyield* effect.pipe(\n    Effect.catchTags({\n        DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),\n        ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })),\n    }),\n)\n```\n\n### Prefer Explicit Over Generic Errors\n\n**Every distinct failure reason deserves its own error type.** Don't collapse multiple failure modes into generic HTTP errors.\n\n```typescript\n// WRONG - Generic errors lose information\nexport class NotFoundError extends Schema.TaggedError<NotFoundError>()(\n    \"NotFoundError\",\n    { message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\n// Then mapping everything to it:\nEffect.catchTags({\n    UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n    ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n    MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n})\n// Frontend gets useless: { _tag: \"NotFoundError\", message: \"Not found\" }\n// Which resource? User? Channel? Message? Can't tell!\n```\n\n```typescript\n// CORRECT - Explicit domain errors with rich context\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    { userId: UserId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\nexport class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(\n    \"ChannelNotFoundError\",\n    { channelId: ChannelId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\nexport class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(\n    \"SessionExpiredError\",\n    { sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String },\n    HttpApiSchema.annotations({ status: 401 }),\n) {}\n\n// Frontend can now show specific UI:\n// - UserNotFoundError → \"User doesn't exist\"\n// - ChannelNotFoundError → \"Channel was deleted\"\n// - SessionExpiredError → \"Your session expired. Please log in again.\"\n```\n\nSee `references/error-patterns.md` for error remapping and retry patterns.\n\n## Schema & Branded Types Pattern\n\n**Brand all entity IDs** for type safety across service boundaries:\n\n```typescript\nimport { Schema } from \"effect\"\n\n// Entity IDs - always branded\nexport const UserId = Schema.UUID.pipe(Schema.brand(\"@App/UserId\"))\nexport type UserId = Schema.Schema.Type<typeof UserId>\n\nexport const OrganizationId = Schema.UUID.pipe(Schema.brand(\"@App/OrganizationId\"))\nexport type OrganizationId = Schema.Schema.Type<typeof OrganizationId>\n\n// Domain types - use Schema.Struct\nexport const User = Schema.Struct({\n    id: UserId,\n    email: Schema.String,\n    name: Schema.String,\n    organizationId: OrganizationId,\n    createdAt: Schema.DateTimeUtc,\n})\nexport type User = Schema.Schema.Type<typeof User>\n\n// Input types for mutations\nexport const CreateUserInput = Schema.Struct({\n    email: Schema.String.pipe(Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)),\n    name: Schema.String.pipe(Schema.minLength(1)),\n    organizationId: OrganizationId,\n})\nexport type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>\n```\n\n**When NOT to brand:**\n- Simple strings that don't cross service boundaries (URLs, file paths)\n- Primitive config values\n\nSee `references/schema-patterns.md` for transforms and advanced patterns.\n\n## Function Pattern with Effect.fn\n\n**Always use `Effect.fn`** for service methods. This provides automatic tracing with proper span names:\n\n```typescript\n// CORRECT - Effect.fn with descriptive name\nconst findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n    yield* Effect.annotateCurrentSpan(\"userId\", id)\n    const user = yield* repo.findById(id)\n    return user\n})\n\n// CORRECT - Effect.fn with multiple parameters\nconst transfer = Effect.fn(\"AccountService.transfer\")(\n    function* (fromId: AccountId, toId: AccountId, amount: number) {\n        yield* Effect.annotateCurrentSpan(\"fromId\", fromId)\n        yield* Effect.annotateCurrentSpan(\"toId\", toId)\n        yield* Effect.annotateCurrentSpan(\"amount\", amount)\n        // ...\n    }\n)\n```\n\n## Layer Composition\n\n**Declare dependencies in the service**, not at usage sites:\n\n```typescript\n// CORRECT - dependencies in service definition\nexport class OrderService extends Effect.Service<OrderService>()(\"OrderService\", {\n    accessors: true,\n    dependencies: [\n        UserService.Default,\n        ProductService.Default,\n        PaymentService.Default,\n    ],\n    effect: Effect.gen(function* () {\n        const users = yield* UserService\n        const products = yield* ProductService\n        const payments = yield* PaymentService\n        // ...\n    }),\n}) {}\n\n// At app root - simple merge\nconst AppLive = Layer.mergeAll(\n    OrderService.Default,\n    // Infrastructure layers (intentionally not in dependencies)\n    DatabaseLive,\n    RedisLive,\n)\n```\n\nSee `references/layer-patterns.md` for testing layers and config-dependent layers.\n\n## Option Handling\n\n**Never use `Option.getOrThrow`**. Always handle both cases explicitly:\n\n```typescript\n// CORRECT - explicit handling\nyield* Option.match(maybeUser, {\n    onNone: () => Effect.fail(new UserNotFoundError({ userId, message: \"Not found\" })),\n    onSome: (user) => Effect.succeed(user),\n})\n\n// CORRECT - with getOrElse for defaults\nconst name = Option.getOrElse(maybeName, () => \"Anonymous\")\n\n// CORRECT - Option.map for transformations\nconst upperName = Option.map(maybeName, (n) => n.toUpperCase())\n```\n\n## Effect Atom (Frontend State)\n\nEffect Atom provides reactive state management for React with Effect integration.\n\n### Basic Atoms\n\n```typescript\nimport { Atom } from \"@effect-atom/atom-react\"\n\n// Define atoms OUTSIDE components\nconst countAtom = Atom.make(0)\n\n// Use keepAlive for global state that should persist\nconst userPrefsAtom = Atom.make({ theme: \"dark\" }).pipe(Atom.keepAlive)\n\n// Atom families for per-entity state\nconst modalAtomFamily = Atom.family((type: string) =>\n    Atom.make({ isOpen: false }).pipe(Atom.keepAlive)\n)\n```\n\n### React Integration\n\n```typescript\nimport { useAtomValue, useAtomSet, useAtom, useAtomMount } from \"@effect-atom/atom-react\"\n\nfunction Counter() {\n    const count = useAtomValue(countAtom)           // Read only\n    const setCount = useAtomSet(countAtom)          // Write only\n    const [value, setValue] = useAtom(countAtom)    // Read + write\n\n    return <button onClick={() => setCount((c) => c + 1)}>{count}</button>\n}\n\n// Mount side-effect atoms without reading value\nfunction App() {\n    useAtomMount(keyboardShortcutsAtom)\n    return <>{children}</>\n}\n```\n\n### Handling Results with Result.builder\n\n**Use `Result.builder`** for rendering effectful atom results. It provides chainable error handling with `onErrorTag`:\n\n```typescript\nimport { Result } from \"@effect-atom/atom-react\"\n\nfunction UserProfile() {\n    const userResult = useAtomValue(userAtom) // Result<User, Error>\n\n    return Result.builder(userResult)\n        .onInitial(() => <div>Loading...</div>)\n        .onErrorTag(\"NotFoundError\", () => <div>User not found</div>)\n        .onError((error) => <div>Error: {error.message}</div>)\n        .onSuccess((user) => <div>Hello, {user.name}</div>)\n        .render()\n}\n```\n\n### Atoms with Side Effects\n\n```typescript\nconst scrollYAtom = Atom.make((get) => {\n    const onScroll = () => get.setSelf(window.scrollY)\n\n    window.addEventListener(\"scroll\", onScroll)\n    get.addFinalizer(() => window.removeEventListener(\"scroll\", onScroll)) // REQUIRED\n\n    return window.scrollY\n}).pipe(Atom.keepAlive)\n```\n\nSee `references/effect-atom-patterns.md` for complete patterns including families, localStorage, and anti-patterns.\n\n## RPC & Cluster Patterns\n\nFor RPC contracts and cluster workflows, see:\n- `references/rpc-cluster-patterns.md` - RpcGroup, Workflow.make, Activity patterns\n\n## Anti-Patterns (Forbidden)\n\nThese patterns are **never acceptable**:\n\n```typescript\n// FORBIDDEN - runSync/runPromise inside services\nconst result = Effect.runSync(someEffect) // Never do this\n\n// FORBIDDEN - throw inside Effect.gen\nyield* Effect.gen(function* () {\n    if (bad) throw new Error(\"No!\") // Use Effect.fail instead\n})\n\n// FORBIDDEN - catchAll losing type info\nyield* effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())))\n\n// FORBIDDEN - console.log\nconsole.log(\"debug\") // Use Effect.log\n\n// FORBIDDEN - process.env directly\nconst key = process.env.API_KEY // Use Config.string(\"API_KEY\")\n\n// FORBIDDEN - null/undefined in domain types\ntype User = { name: string | null } // Use Option<string>\n```\n\nSee `references/anti-patterns.md` for the complete list with rationale.\n\n## Observability\n\n```typescript\n// Structured logging\nyield* Effect.log(\"Processing order\", { orderId, userId, amount })\n\n// Metrics\nconst orderCounter = Metric.counter(\"orders_processed\")\nyield* Metric.increment(orderCounter)\n\n// Config with validation\nconst config = Config.all({\n    port: Config.integer(\"PORT\").pipe(Config.withDefault(3000)),\n    apiKey: Config.secret(\"API_KEY\"),\n    maxRetries: Config.integer(\"MAX_RETRIES\").pipe(\n        Config.validate({ message: \"Must be positive\", validation: (n) => n > 0 })\n    ),\n})\n```\n\nSee `references/observability-patterns.md` for metrics and tracing patterns.\n\n## Reference Files\n\nFor detailed patterns, consult these reference files in the `references/` directory:\n\n- `service-patterns.md` - Service definition, Effect.fn, Context.Tag exceptions\n- `error-patterns.md` - Schema.TaggedError, error remapping, retry patterns\n- `schema-patterns.md` - Branded types, transforms, Schema.Class\n- `layer-patterns.md` - Dependency composition, testing layers\n- `rpc-cluster-patterns.md` - RpcGroup, Workflow, Activity patterns\n- `effect-atom-patterns.md` - Atom, families, React hooks, Result handling\n- `anti-patterns.md` - Complete list of forbidden patterns\n- `observability-patterns.md` - Logging, metrics, config patterns\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/anti-patterns.md",
    "content": "# Anti-Patterns (Forbidden)\n\nThese patterns are **never acceptable** in Effect-TS code. Each is listed with rationale and the correct alternative.\n\n## FORBIDDEN: Effect.runSync/runPromise Inside Services\n\n```typescript\n// FORBIDDEN\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", {\n    effect: Effect.gen(function* () {\n        const findById = (id: UserId) => {\n            // Running effects synchronously breaks composition\n            const user = Effect.runSync(repo.findById(id))\n            return user\n        }\n        return { findById }\n    }),\n}) {}\n```\n\n**Why:** Breaks Effect's composition model, loses error handling, can't be tested, loses tracing.\n\n**Correct:**\n```typescript\nconst findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n    return yield* repo.findById(id)\n})\n```\n\n## FORBIDDEN: throw Inside Effect.gen\n\n```typescript\n// FORBIDDEN\nyield* Effect.gen(function* () {\n    const user = yield* repo.findById(id)\n    if (!user) {\n        throw new Error(\"User not found\") // Bypasses Effect error channel\n    }\n    return user\n})\n```\n\n**Why:** Throws bypass Effect's error channel, can't be caught with `catchTag`, breaks type safety.\n\n**Correct:**\n```typescript\nyield* Effect.gen(function* () {\n    const user = yield* repo.findById(id)\n    if (!user) {\n        return yield* Effect.fail(new UserNotFoundError({ userId: id, message: \"Not found\" }))\n    }\n    return user\n})\n```\n\n## FORBIDDEN: catchAll Losing Type Information\n\n```typescript\n// FORBIDDEN\nyield* someEffect.pipe(\n    Effect.catchAll((err) =>\n        Effect.fail(new GenericError({ message: \"Something failed\" }))\n    )\n)\n```\n\n**Why:** Loses specific error information, makes debugging harder, prevents specific error handling downstream.\n\n**Correct:**\n```typescript\nyield* someEffect.pipe(\n    Effect.catchTags({\n        DatabaseError: (err) => Effect.fail(new ServiceUnavailableError({ message: err.message })),\n        ValidationError: (err) => Effect.fail(new BadRequestError({ message: err.message })),\n    }),\n)\n```\n\n## FORBIDDEN: any/unknown Casts\n\n```typescript\n// FORBIDDEN\nconst data = someValue as any\nconst result = (await fetch(url)) as unknown as MyType\n```\n\n**Why:** Completely bypasses type safety, can cause runtime errors, loses Effect's type guarantees.\n\n**Correct:**\n```typescript\n// Use Schema for parsing unknown data\nconst result = yield* Schema.decodeUnknown(MyType)(someValue)\n\n// Or explicit type guards\nif (isMyType(someValue)) {\n    // Now safely typed\n}\n```\n\n## FORBIDDEN: Promise in Service Signatures\n\n```typescript\n// FORBIDDEN\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", {\n    effect: Effect.gen(function* () {\n        return {\n            findById: async (id: UserId): Promise<User> => {\n                // Using Promise instead of Effect\n            }\n        }\n    }),\n}) {}\n```\n\n**Why:** Loses Effect's error handling, can't compose with other Effects, loses tracing/metrics.\n\n**Correct:**\n```typescript\nconst findById = Effect.fn(\"UserService.findById\")(\n    function* (id: UserId): Effect.Effect<User, UserNotFoundError> {\n        // ...\n    }\n)\n```\n\n## FORBIDDEN: console.log\n\n```typescript\n// FORBIDDEN\nconsole.log(\"Processing order:\", orderId)\nconsole.error(\"Error:\", error)\n```\n\n**Why:** Not structured, not captured by Effect's logging system, lost in production telemetry.\n\n**Correct:**\n```typescript\nyield* Effect.log(\"Processing order\", { orderId })\nyield* Effect.logError(\"Operation failed\", { error: String(error) })\n```\n\n## FORBIDDEN: process.env Directly\n\n```typescript\n// FORBIDDEN\nconst apiKey = process.env.API_KEY\nconst port = parseInt(process.env.PORT || \"3000\")\n```\n\n**Why:** No validation, no type safety, fails silently if missing, hard to test.\n\n**Correct:**\n```typescript\nconst config = yield* Config.all({\n    apiKey: Config.secret(\"API_KEY\"),\n    port: Config.integer(\"PORT\").pipe(Config.withDefault(3000)),\n})\n```\n\n## FORBIDDEN: null/undefined in Domain Types\n\n```typescript\n// FORBIDDEN\ntype User = {\n    name: string\n    bio: string | null\n    avatar: string | undefined\n}\n```\n\n**Why:** Null/undefined handling is error-prone, loses the explicit \"absence\" semantics.\n\n**Correct:**\n```typescript\nconst User = Schema.Struct({\n    name: Schema.String,\n    bio: Schema.Option(Schema.String),\n    avatar: Schema.Option(Schema.String),\n})\n```\n\n## FORBIDDEN: Option.getOrThrow\n\n```typescript\n// FORBIDDEN\nconst user = Option.getOrThrow(maybeUser)\nconst name = pipe(maybeName, Option.getOrThrow)\n```\n\n**Why:** Throws exceptions, bypasses Effect's error handling, fails at runtime instead of compile time.\n\n**Correct:**\n```typescript\n// Handle both cases explicitly\nyield* Option.match(maybeUser, {\n    onNone: () => Effect.fail(new UserNotFoundError({ userId, message: \"Not found\" })),\n    onSome: Effect.succeed,\n})\n\n// Or provide a default\nconst name = Option.getOrElse(maybeName, () => \"Anonymous\")\n\n// Or use Option.map for transformations\nconst upperName = Option.map(maybeName, (n) => n.toUpperCase())\n```\n\n## FORBIDDEN: Context.Tag for Business Services\n\n```typescript\n// FORBIDDEN\nexport class UserService extends Context.Tag(\"UserService\")<\n    UserService,\n    { findById: (id: UserId) => Effect.Effect<User, UserNotFoundError> }\n>() {\n    static Default = Layer.effect(this, Effect.gen(function* () { ... }))\n}\n```\n\n**Why:** Requires manual layer creation, no built-in accessors, more boilerplate.\n\n**Correct:**\n```typescript\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", {\n    accessors: true,\n    dependencies: [...],\n    effect: Effect.gen(function* () { ... }),\n}) {}\n```\n\n## FORBIDDEN: Ignoring Errors with orDie\n\n```typescript\n// FORBIDDEN (in most cases)\nyield* someEffect.pipe(Effect.orDie)\n```\n\n**Why:** Converts recoverable errors to defects (unrecoverable), loses error information.\n\n**Acceptable exceptions:**\n- Truly unrecoverable situations (invalid program state)\n- After exhausting all recovery options\n- In test setup code\n\n**Correct:**\n```typescript\n// Handle errors explicitly\nyield* someEffect.pipe(\n    Effect.catchTag(\"RecoverableError\", (err) =>\n        Effect.fail(new DomainError({ message: err.message }))\n    ),\n)\n```\n\n## FORBIDDEN: mapError Instead of catchTag\n\n```typescript\n// FORBIDDEN\nyield* effect.pipe(\n    Effect.mapError((err) => new GenericError({ message: String(err) }))\n)\n```\n\n**Why:** Loses error type information, can't discriminate between error types.\n\n**Correct:**\n```typescript\nyield* effect.pipe(\n    Effect.catchTag(\"SpecificError\", (err) =>\n        Effect.fail(new MappedError({ message: err.message }))\n    ),\n)\n```\n\n## FORBIDDEN: Mixing Effect and Promise Chains\n\n```typescript\n// FORBIDDEN\nconst result = await someEffect.pipe(\n    Effect.runPromise,\n).then(data => {\n    // Mixing Promise chain with Effect\n    return Effect.runPromise(anotherEffect(data))\n})\n```\n\n**Why:** Loses Effect composition benefits, error handling becomes inconsistent.\n\n**Correct:**\n```typescript\nconst program = Effect.gen(function* () {\n    const data = yield* someEffect\n    return yield* anotherEffect(data)\n})\n\nconst result = await Effect.runPromise(program)\n```\n\n## FORBIDDEN: Mutable State Without Ref\n\n```typescript\n// FORBIDDEN\nlet counter = 0\nconst increment = Effect.sync(() => { counter++ })\n```\n\n**Why:** Race conditions, not testable, not composable, breaks referential transparency.\n\n**Correct:**\n```typescript\nconst program = Effect.gen(function* () {\n    const counter = yield* Ref.make(0)\n    yield* Ref.update(counter, (n) => n + 1)\n    return yield* Ref.get(counter)\n})\n```\n\n## FORBIDDEN: Using Date.now() or new Date() Directly\n\n```typescript\n// FORBIDDEN\nconst now = new Date()\nconst timestamp = Date.now()\n```\n\n**Why:** Not testable, introduces non-determinism, hard to mock in tests.\n\n**Correct:**\n```typescript\nimport { Clock } from \"effect\"\n\nconst now = yield* Clock.currentTimeMillis\nconst date = yield* Clock.currentTimeZone.pipe(\n    Effect.map((tz) => new Date())\n)\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/effect-atom-patterns.md",
    "content": "# Effect Atom Patterns\n\nEffect Atom is a reactive state management library that integrates with Effect-TS. It provides atoms (reactive containers), automatic dependency tracking, and seamless React integration.\n\n## Core Concepts\n\n- **Atoms**: Reactive state containers with automatic dependency tracking\n- **Result**: Handles async/effectful computations with initial, success, and failure states\n- **Finalizers**: Built-in cleanup for resources and event listeners\n- **Families**: Dynamic atom creation for per-entity state\n\n## Creating Atoms\n\n### Basic Atoms\n\n```typescript\nimport { Atom } from \"@effect-atom/atom-react\"\n\n// Simple value atom\nconst countAtom = Atom.make(0)\n\n// With keepAlive - persists when no components subscribe\nconst persistentCountAtom = Atom.make(0).pipe(Atom.keepAlive)\n```\n\n**Rule:** Use `Atom.keepAlive` for global state that should persist across component unmounts.\n\n### Derived Atoms\n\n```typescript\nconst countAtom = Atom.make(0)\n\n// Derived using get function\nconst doubleCountAtom = Atom.make((get) => get(countAtom) * 2)\n\n// Derived using Atom.map\nconst tripleCountAtom = Atom.map(countAtom, (count) => count * 3)\n```\n\n### Atoms with Side Effects\n\n```typescript\n// Track window scroll position\nconst scrollYAtom = Atom.make((get) => {\n    const onScroll = () => get.setSelf(window.scrollY)\n\n    window.addEventListener(\"scroll\", onScroll)\n    get.addFinalizer(() => window.removeEventListener(\"scroll\", onScroll))\n\n    return window.scrollY\n}).pipe(Atom.keepAlive)\n```\n\n**Critical:**\n- Use `get.setSelf` to update the atom's own value\n- Always add finalizers with `get.addFinalizer()` to clean up side effects\n- Finalizers run when the atom is rebuilt or disposed\n\n### Atom.transform for Self-Updating Derived State\n\n```typescript\nconst resolvedThemeAtom = Atom.transform(themeAtom, (get) => {\n    const theme = get(themeAtom)\n    if (theme !== \"system\") return theme\n\n    const matcher = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n    const onChange = () => get.setSelf(matcher.matches ? \"dark\" : \"light\")\n\n    matcher.addEventListener(\"change\", onChange)\n    get.addFinalizer(() => matcher.removeEventListener(\"change\", onChange))\n\n    return matcher.matches ? \"dark\" : \"light\"\n})\n```\n\n## Atom Families\n\nUse `Atom.family` for per-entity state:\n\n```typescript\nimport { Atom } from \"@effect-atom/atom-react\"\n\n// Create a family of atoms - one per channelId\nconst replyToMessageAtomFamily = Atom.family((channelId: string) =>\n    Atom.make<string | null>(null).pipe(Atom.keepAlive)\n)\n\n// Modal state family\ntype ModalType = \"settings\" | \"confirm\" | \"create\"\n\ninterface ModalState {\n    type: ModalType\n    isOpen: boolean\n    metadata?: Record<string, unknown>\n}\n\nconst modalAtomFamily = Atom.family((type: ModalType) =>\n    Atom.make<ModalState>({\n        type,\n        isOpen: false,\n        metadata: undefined,\n    }).pipe(Atom.keepAlive)\n)\n```\n\n**Use families for:**\n- Per-resource state (users, channels, documents)\n- Modal instances\n- Form state per entity\n- Any parameterized state\n\n## React Integration\n\n### Reading Atom Values\n\n```typescript\nimport { useAtomValue } from \"@effect-atom/atom-react\"\n\nfunction Counter() {\n    const count = useAtomValue(countAtom)\n    return <span>{count}</span>\n}\n```\n\n### Updating Atom Values\n\n```typescript\nimport { useAtomSet } from \"@effect-atom/atom-react\"\n\nfunction IncrementButton() {\n    const setCount = useAtomSet(countAtom)\n    return (\n        <button onClick={() => setCount((c) => c + 1)}>\n            Increment\n        </button>\n    )\n}\n```\n\n### Reading and Writing Together\n\n```typescript\nimport { useAtom } from \"@effect-atom/atom-react\"\n\nfunction CounterControl() {\n    const [count, setCount] = useAtom(countAtom)\n    return (\n        <div>\n            <span>{count}</span>\n            <button onClick={() => setCount(count + 1)}>+1</button>\n        </div>\n    )\n}\n```\n\n### Mounting Side-Effect Atoms\n\nUse `useAtomMount` to activate atoms without reading their value:\n\n```typescript\nimport { useAtomMount } from \"@effect-atom/atom-react\"\n\nfunction App() {\n    // Activate side effects without subscribing to value\n    useAtomMount(keyboardShortcutsAtom)\n    useAtomMount(presenceTrackingAtom)\n    useAtomMount(themeApplierAtom)\n\n    return <>{children}</>\n}\n```\n\n## Working with Effects and Results\n\n### Effectful Atoms Return Result\n\n```typescript\nimport { Atom, Result } from \"@effect-atom/atom-react\"\nimport { Effect } from \"effect\"\n\nconst userAtom = Atom.make(\n    Effect.gen(function* () {\n        const response = yield* fetchUser()\n        return response\n    })\n) // Type: Atom<Result<User, Error>>\n```\n\n### Handling Results with Result.builder (Recommended)\n\n**Use `Result.builder`** for rendering Result types. It provides a chainable API with granular error handling and type narrowing.\n\n```typescript\nimport { Result, useAtomValue } from \"@effect-atom/atom-react\"\n\nfunction UserProfile() {\n    const userResult = useAtomValue(userAtom)\n\n    return Result.builder(userResult)\n        .onInitial(() => <div>Loading...</div>)\n        .onError((error) => <div>Error: {error.message}</div>)\n        .onSuccess((user) => <div>Hello, {user.name}!</div>)\n        .render()\n}\n```\n\n### Result.builder with Tagged Errors\n\n**Key advantage**: Handle specific error types with `onErrorTag`:\n\n```typescript\nfunction ResourceEmbed({ url }: { url: string }) {\n    const resourceResult = useAtomValue(resourceAtom)\n\n    return Result.builder(resourceResult)\n        .onInitial(() => <Skeleton />)\n        .onErrorTag(\"NotFoundError\", (error) => (\n            <ErrorCard message={error.message} />\n        ))\n        .onErrorTag(\"UnauthorizedError\", () => (\n            <ConnectPrompt provider=\"GitHub\" />\n        ))\n        .onErrorTag(\"RateLimitError\", (error) => (\n            <RetryCard retryAfter={error.retryAfter} />\n        ))\n        .onError((error) => (\n            // Fallback for any other errors\n            <ErrorCard message=\"Something went wrong\" />\n        ))\n        .onSuccess((data) => <ResourceCard data={data} />)\n        .render()\n}\n```\n\n### Result.builder Methods\n\n| Method | Purpose |\n|--------|---------|\n| `onInitial(fn)` | Handle initial/loading state |\n| `onInitialOrWaiting(fn)` | Handle both initial and waiting states |\n| `onWaiting(fn)` | Handle waiting/refetching state |\n| `onSuccess(fn)` | Handle success with value |\n| `onError(fn)` | Handle any error |\n| `onErrorTag(tag, fn)` | Handle specific tagged error (removes from type) |\n| `onErrorIf(predicate, fn)` | Handle errors matching predicate |\n| `onFailure(fn)` | Handle failure with full Cause |\n| `onDefect(fn)` | Handle unexpected defects |\n| `render()` | Return result (null if unhandled initial) |\n| `orElse(fn)` | Provide fallback value |\n| `orNull()` | Return null for unhandled cases |\n\n### Extracting Values with orElse\n\nFor non-rendering use cases, extract values with `orElse`:\n\n```typescript\nfunction useRepositories() {\n    const reposResult = useAtomValue(repositoriesAtom)\n\n    // Extract array or empty fallback\n    const repositories = Result.builder(reposResult)\n        .onSuccess((data) => data.repositories)\n        .orElse(() => [])\n\n    return repositories\n}\n```\n\n### Result.getOrElse for Simple Extraction\n\nFor simple value extraction without error handling:\n\n```typescript\nfunction UserName() {\n    const userResult = useAtomValue(userAtom)\n    const user = Result.getOrElse(userResult, () => null)\n\n    if (!user) return <span>Loading...</span>\n    return <span>{user.name}</span>\n}\n```\n\n### When to Use Each Pattern\n\n| Pattern | Use Case |\n|---------|----------|\n| `Result.builder` | UI rendering with multiple error types |\n| `Result.builder + onErrorTag` | APIs with tagged errors (HttpApi, RPC) |\n| `Result.builder + orElse` | Extracting values with fallback |\n| `Result.getOrElse` | Simple value extraction |\n| `Result.match` | Simple 3-case exhaustive matching |\n\n### Accessing Results in Derived Atoms\n\n```typescript\nconst userProfileAtom = Atom.make(\n    Effect.fnUntraced(function* (get: Atom.Context) {\n        // Unwrap Result to get the value (waits for success)\n        const user = yield* get.result(userAtom)\n        const posts = yield* fetchUserPosts(user.id)\n        return { user, posts }\n    })\n)\n```\n\n## Batching Updates\n\nUse `Atom.batch` for multiple updates:\n\n```typescript\nconst openModal = (type: ModalType, metadata?: Record<string, unknown>) => {\n    Atom.batch(() => {\n        Atom.update(modalAtomFamily(type), (state) => ({\n            ...state,\n            isOpen: true,\n            metadata,\n        }))\n    })\n}\n```\n\n## localStorage Persistence\n\n```typescript\nimport { BrowserKeyValueStore } from \"@effect/platform-browser\"\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Schema } from \"effect\"\n\n// Create runtime with localStorage\nconst localStorageRuntime = Atom.runtime(BrowserKeyValueStore.layerLocalStorage)\n\n// Persisted atom with schema validation\nconst themeAtom = Atom.kvs({\n    runtime: localStorageRuntime,\n    key: \"app-theme\",\n    schema: Schema.Literal(\"dark\", \"light\", \"system\"),\n    defaultValue: () => \"system\" as const,\n})\n```\n\n## Anti-Patterns\n\n### FORBIDDEN: Creating Atoms Inside Components\n\n```typescript\n// WRONG - creates new atom on every render\nfunction Counter() {\n    const countAtom = Atom.make(0) // New atom each render!\n    const count = useAtomValue(countAtom)\n    return <div>{count}</div>\n}\n\n// CORRECT - define atoms outside components\nconst countAtom = Atom.make(0)\n\nfunction Counter() {\n    const count = useAtomValue(countAtom)\n    return <div>{count}</div>\n}\n```\n\n### FORBIDDEN: Imperative Updates from React Components\n\n```typescript\n// WRONG - doesn't trigger React re-renders\nexport const openModal = (type: string) => {\n    Atom.batch(() => {\n        Atom.update(modalAtomFamily(type), (s) => ({ ...s, isOpen: true }))\n    })\n}\n\nfunction Component() {\n    return <button onClick={() => openModal(\"settings\")}>Open</button>\n}\n\n// CORRECT - use hooks for React integration\nexport const useModal = (type: string) => {\n    const state = useAtomValue(modalAtomFamily(type))\n    const setState = useAtomSet(modalAtomFamily(type))\n\n    const open = useCallback(() => {\n        setState((prev) => ({ ...prev, isOpen: true }))\n    }, [setState])\n\n    const close = useCallback(() => {\n        setState((prev) => ({ ...prev, isOpen: false }))\n    }, [setState])\n\n    return { isOpen: state.isOpen, open, close }\n}\n```\n\n**When imperative updates ARE acceptable:**\n- Event listeners outside React (keyboard shortcuts)\n- Effects running on atom changes\n- Non-UI state (analytics, logging)\n\n### FORBIDDEN: Missing Finalizers\n\n```typescript\n// WRONG - memory leak!\nconst scrollAtom = Atom.make((get) => {\n    const onScroll = () => get.setSelf(window.scrollY)\n    window.addEventListener(\"scroll\", onScroll)\n    return window.scrollY\n})\n\n// CORRECT - cleanup registered\nconst scrollAtom = Atom.make((get) => {\n    const onScroll = () => get.setSelf(window.scrollY)\n    window.addEventListener(\"scroll\", onScroll)\n    get.addFinalizer(() => window.removeEventListener(\"scroll\", onScroll))\n    return window.scrollY\n})\n```\n\n### FORBIDDEN: Missing keepAlive for Global State\n\n```typescript\n// WRONG - state resets when component unmounts\nexport const modalStateAtom = Atom.make({ isOpen: false })\n\n// CORRECT - state persists\nexport const modalStateAtom = Atom.make({ isOpen: false }).pipe(Atom.keepAlive)\n```\n\n### FORBIDDEN: Ignoring Result Types\n\n```typescript\n// WRONG - doesn't handle loading/error states\nconst userResult = useAtomValue(userAtom)\nreturn <div>Hello, {userResult.name}</div> // Type error!\n\n// CORRECT - use Result.builder to handle all states\nconst userResult = useAtomValue(userAtom)\nreturn Result.builder(userResult)\n    .onInitial(() => <div>Loading...</div>)\n    .onError((error) => <div>Error: {error.message}</div>)\n    .onSuccess((user) => <div>Hello, {user.name}</div>)\n    .render()\n```\n\n### FORBIDDEN: Updating State During Render\n\n```typescript\n// WRONG - side effect during render\nfunction Component() {\n    const count = useAtomValue(countAtom)\n    Atom.set(countAtom, count + 1) // Never do this!\n    return <div>{count}</div>\n}\n\n// CORRECT - use effects or event handlers\nfunction Component() {\n    const count = useAtomValue(countAtom)\n    const setCount = useAtomSet(countAtom)\n\n    useEffect(() => {\n        setCount((c) => c + 1)\n    }, [])\n\n    return <div>{count}</div>\n}\n```\n\n## Performance Tips\n\n### Selective Re-rendering\n\n```typescript\n// WRONG - subscribes to entire state\nconst state = useAtomValue(appStateAtom)\nconst userName = state.user.name\n\n// CORRECT - derive focused atom\nconst userNameAtom = Atom.map(appStateAtom, (state) => state.user.name)\nconst userName = useAtomValue(userNameAtom)\n```\n\n### When to Use keepAlive\n\nUse `Atom.keepAlive` for:\n- Global application state\n- Modal/dialog state\n- User preferences\n- Authentication state\n- Frequently accessed derived state\n\nSkip `keepAlive` for:\n- Component-local state that should reset\n- Temporary form state\n- State tied to component lifecycle\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/error-patterns.md",
    "content": "# Error Patterns\n\n## Why Explicit Error Types?\n\nGeneric errors like `BadRequestError` or `NotFoundError` seem convenient but create problems:\n\n| Generic Error | Problems |\n|--------------|----------|\n| `NotFoundError` | Which resource? How should frontend recover? |\n| `BadRequestError` | What's invalid? Can user fix it? |\n| `UnauthorizedError` | Session expired? Wrong credentials? Missing permission? |\n| `InternalServerError` | Retryable? User action needed? |\n\n**Explicit errors enable:**\n1. **Specific UI messages** - \"Your session expired\" vs generic \"Unauthorized\"\n2. **Targeted recovery** - Refresh token vs show login page\n3. **Better observability** - Group errors by specific type in dashboards\n4. **Type-safe handling** - `catchTag(\"SessionExpiredError\")` vs generic catch\n\n### Anti-Pattern: Generic Error Mapping\n\n```typescript\n// ❌ WRONG - Collapsing to generic HTTP errors\nexport class NotFoundError extends Schema.TaggedError<NotFoundError>()(\n    \"NotFoundError\",\n    { message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\n// At API boundaries:\nEffect.catchTags({\n    UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n    ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n    MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: \"Not found\" })),\n})\n\n// Frontend receives: { _tag: \"NotFoundError\", message: \"Not found\" }\n// - Can't show specific message (\"User doesn't exist\" vs \"Channel was deleted\")\n// - Can't take specific action (redirect to user search vs channel list)\n// - Debugging is harder (which resource was missing?)\n```\n\n```typescript\n// ✅ CORRECT - Keep explicit errors all the way to frontend\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    { userId: UserId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\nexport class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(\n    \"ChannelNotFoundError\",\n    { channelId: ChannelId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\n// Frontend can handle each case:\nResult.builder(result)\n    .onErrorTag(\"UserNotFoundError\", (err) => <UserNotFoundMessage userId={err.userId} />)\n    .onErrorTag(\"ChannelNotFoundError\", (err) => <ChannelDeletedMessage />)\n    .onErrorTag(\"SessionExpiredError\", () => <RedirectToLogin />)\n    .render()\n```\n\n## Error Naming Conventions\n\n| Pattern | Example | Use For |\n|---------|---------|---------|\n| `{Entity}NotFoundError` | `UserNotFoundError`, `ChannelNotFoundError` | Resource lookups |\n| `{Entity}{Action}Error` | `UserCreateError`, `MessageUpdateError` | Mutations that fail |\n| `{Feature}Error` | `SessionExpiredError`, `RateLimitExceededError` | Feature-specific failures |\n| `{Integration}Error` | `WorkOSUserFetchError`, `StripePaymentError` | External service errors |\n| `Invalid{Field}Error` | `InvalidEmailError`, `InvalidPasswordError` | Validation failures |\n\n### Rich Error Context\n\nInclude context fields that help with debugging and UI handling:\n\n```typescript\n// Entity errors → include entity ID\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    {\n        userId: UserId,         // Which user?\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\n// Action errors → include input that failed\nexport class UserCreateError extends Schema.TaggedError<UserCreateError>()(\n    \"UserCreateError\",\n    {\n        email: Schema.String,   // What email failed?\n        reason: Schema.String,  // Why? \"duplicate\", \"invalid domain\"\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 400 }),\n) {}\n\n// Integration errors → include service name and retryable flag\nexport class StripePaymentError extends Schema.TaggedError<StripePaymentError>()(\n    \"StripePaymentError\",\n    {\n        stripeErrorCode: Schema.String,\n        retryable: Schema.Boolean,\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 402 }),\n) {}\n\n// Auth errors → include expiry info\nexport class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(\n    \"SessionExpiredError\",\n    {\n        sessionId: SessionId,\n        expiredAt: Schema.DateTimeUtc,\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 401 }),\n) {}\n```\n\n## Schema.TaggedError for All Errors\n\n**Always use `Schema.TaggedError`** for defining errors. This provides:\n\n1. **Serialization** - Errors can be sent over RPC/network\n2. **Type safety** - `_tag` discriminator enables `catchTag`\n3. **Consistent structure** - All errors have predictable shape\n4. **HTTP status mapping** - Via `HttpApiSchema.annotations`\n\n### Basic Error Definition\n\n```typescript\nimport { Schema } from \"effect\"\nimport { HttpApiSchema } from \"@effect/platform\"\n\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    {\n        userId: UserId,\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 404 }),\n) {}\n\nexport class UserCreateError extends Schema.TaggedError<UserCreateError>()(\n    \"UserCreateError\",\n    {\n        message: Schema.String,\n        cause: Schema.optional(Schema.String),\n    },\n    HttpApiSchema.annotations({ status: 400 }),\n) {}\n\nexport class UnauthorizedError extends Schema.TaggedError<UnauthorizedError>()(\n    \"UnauthorizedError\",\n    {\n        message: Schema.String,\n    },\n    HttpApiSchema.annotations({ status: 401 }),\n) {}\n\nexport class ForbiddenError extends Schema.TaggedError<ForbiddenError>()(\n    \"ForbiddenError\",\n    {\n        message: Schema.String,\n        requiredPermission: Schema.optional(Schema.String),\n    },\n    HttpApiSchema.annotations({ status: 403 }),\n) {}\n```\n\n### Required Fields\n\nEvery error should have:\n- `message: Schema.String` - Human-readable description\n- Relevant context fields (IDs, etc.)\n- Optional `cause: Schema.optional(Schema.String)` for error chains\n\n## Error Handling with catchTag/catchTags\n\n**Never use `catchAll` or `mapError`** when you can use `catchTag`/`catchTags`. These preserve type information and enable precise error handling.\n\n### catchTag for Single Error Types\n\n```typescript\nconst findUser = Effect.fn(\"UserService.findUser\")(function* (id: UserId) {\n    return yield* repo.findById(id).pipe(\n        Effect.catchTag(\"DatabaseError\", (err) =>\n            Effect.fail(new UserNotFoundError({\n                userId: id,\n                message: `Database lookup failed: ${err.message}`,\n            }))\n        ),\n    )\n})\n```\n\n### catchTags for Multiple Error Types\n\n```typescript\nconst processOrder = Effect.fn(\"OrderService.processOrder\")(function* (input: OrderInput) {\n    return yield* validateAndProcess(input).pipe(\n        Effect.catchTags({\n            ValidationError: (err) =>\n                Effect.fail(new OrderValidationError({\n                    message: err.message,\n                    field: err.field,\n                })),\n            PaymentError: (err) =>\n                Effect.fail(new OrderPaymentError({\n                    message: `Payment failed: ${err.message}`,\n                    code: err.code,\n                })),\n            InventoryError: (err) =>\n                Effect.fail(new OrderInventoryError({\n                    productId: err.productId,\n                    message: \"Insufficient inventory\",\n                })),\n        }),\n    )\n})\n```\n\n### Why Not catchAll?\n\n```typescript\n// WRONG - Loses type information\nyield* effect.pipe(\n    Effect.catchAll((err) =>\n        Effect.fail(new InternalServerError({ message: \"Something failed\" }))\n    )\n)\n\n// Problems:\n// 1. Can't distinguish error types downstream\n// 2. Hides useful error context\n// 3. Makes debugging harder\n// 4. Frontend can't show specific messages\n```\n\n## Error Remapping Pattern\n\nCreate reusable error remapping functions for common transformations:\n\n```typescript\nimport { Effect } from \"effect\"\n\nexport const withRemapDbErrors = <A, E, R>(\n    effect: Effect.Effect<A, E | DatabaseError | ConnectionError, R>,\n    context: { entityType: string; entityId: string }\n): Effect.Effect<A, E | EntityNotFoundError | ServiceUnavailableError, R> =>\n    effect.pipe(\n        Effect.catchTag(\"DatabaseError\", (err) =>\n            Effect.fail(new EntityNotFoundError({\n                entityType: context.entityType,\n                entityId: context.entityId,\n                message: `${context.entityType} not found`,\n            }))\n        ),\n        Effect.catchTag(\"ConnectionError\", (err) =>\n            Effect.fail(new ServiceUnavailableError({\n                message: \"Database connection unavailable\",\n                cause: err.message,\n            }))\n        ),\n    )\n\n// Usage\nconst findUser = Effect.fn(\"UserService.findUser\")(function* (id: UserId) {\n    return yield* repo.findById(id).pipe(\n        withRemapDbErrors({ entityType: \"User\", entityId: id })\n    )\n})\n```\n\n## Retryable Errors Pattern\n\nFor errors that may be transient, add a `retryable` property:\n\n```typescript\nexport class ServiceUnavailableError extends Schema.TaggedError<ServiceUnavailableError>()(\n    \"ServiceUnavailableError\",\n    {\n        message: Schema.String,\n        cause: Schema.optional(Schema.String),\n        retryable: Schema.optionalWith(Schema.Boolean, { default: () => true }),\n    },\n    HttpApiSchema.annotations({ status: 503 }),\n) {}\n\nexport class RateLimitError extends Schema.TaggedError<RateLimitError>()(\n    \"RateLimitError\",\n    {\n        message: Schema.String,\n        retryAfter: Schema.optional(Schema.Number),\n        retryable: Schema.optionalWith(Schema.Boolean, { default: () => true }),\n    },\n    HttpApiSchema.annotations({ status: 429 }),\n) {}\n\n// Non-retryable error\nexport class ValidationError extends Schema.TaggedError<ValidationError>()(\n    \"ValidationError\",\n    {\n        message: Schema.String,\n        field: Schema.String,\n        retryable: Schema.optionalWith(Schema.Boolean, { default: () => false }),\n    },\n    HttpApiSchema.annotations({ status: 400 }),\n) {}\n```\n\n### Retry Based on Error Property\n\n```typescript\nimport { Effect, Schedule } from \"effect\"\n\nconst withRetry = <A, E extends { retryable?: boolean }, R>(\n    effect: Effect.Effect<A, E, R>\n): Effect.Effect<A, E, R> =>\n    effect.pipe(\n        Effect.retry(\n            Schedule.exponential(\"100 millis\").pipe(\n                Schedule.intersect(Schedule.recurs(3)),\n                Schedule.whileInput((err: E) => err.retryable === true),\n            )\n        ),\n    )\n\n// Usage\nyield* callExternalApi(request).pipe(withRetry)\n```\n\n## Error Unions for Activities\n\nWhen defining workflow activities, use explicit error unions:\n\n```typescript\n// Activity error type - union of possible errors\nexport type GetChannelMembersError =\n    | DatabaseError\n    | ChannelNotFoundError\n\nexport class DatabaseError extends Schema.TaggedError<DatabaseError>()(\n    \"DatabaseError\",\n    {\n        message: Schema.String,\n        cause: Schema.optional(Schema.String),\n        retryable: Schema.optionalWith(Schema.Boolean, { default: () => true }),\n    },\n) {}\n\nexport class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(\n    \"ChannelNotFoundError\",\n    {\n        channelId: ChannelId,\n        message: Schema.String,\n        retryable: Schema.optionalWith(Schema.Boolean, { default: () => false }),\n    },\n) {}\n\n// In activity definition\nyield* Activity.make({\n    name: \"GetChannelMembers\",\n    success: ChannelMembersResult,\n    error: Schema.Union(DatabaseError, ChannelNotFoundError),\n    execute: Effect.gen(function* () {\n        // ...\n    }),\n})\n```\n\n## HTTP Status Codes (Without Generic Errors)\n\n**Map HTTP status codes at the error level, not by creating generic error classes.** Each explicit error can have its own HTTP status.\n\n```typescript\n// ✅ CORRECT - Domain errors with HTTP status annotations\nexport class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(\n    \"UserNotFoundError\",\n    { userId: UserId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),  // Status on specific error\n) {}\n\nexport class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(\n    \"ChannelNotFoundError\",\n    { channelId: ChannelId, message: Schema.String },\n    HttpApiSchema.annotations({ status: 404 }),  // Same status, different error\n) {}\n\nexport class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(\n    \"SessionExpiredError\",\n    { sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String },\n    HttpApiSchema.annotations({ status: 401 }),\n) {}\n\nexport class InvalidCredentialsError extends Schema.TaggedError<InvalidCredentialsError>()(\n    \"InvalidCredentialsError\",\n    { message: Schema.String },\n    HttpApiSchema.annotations({ status: 401 }),  // Same status, different meaning\n) {}\n```\n\n```typescript\n// ❌ WRONG - Generic HTTP error classes\nexport class UnauthorizedError extends Schema.TaggedError<UnauthorizedError>()(\n    \"UnauthorizedError\",\n    { message: Schema.String },\n    HttpApiSchema.annotations({ status: 401 }),\n) {}\n\n// Then mapping everything to it - loses critical information!\nEffect.catchTags({\n    SessionExpiredError: (err) => Effect.fail(new UnauthorizedError({ message: \"Unauthorized\" })),\n    InvalidCredentialsError: (err) => Effect.fail(new UnauthorizedError({ message: \"Unauthorized\" })),\n    MissingTokenError: (err) => Effect.fail(new UnauthorizedError({ message: \"Unauthorized\" })),\n})\n// Frontend can't distinguish: expired session vs wrong password vs missing token\n```\n\n### When Generic Errors Are Acceptable\n\nGeneric errors are only acceptable for **truly unrecoverable internal errors** where:\n- The frontend can only show \"Something went wrong\"\n- No user action can fix it\n- You're hiding internal details for security\n\n```typescript\n// Acceptable for unrecoverable errors\nexport class InternalServerError extends Schema.TaggedError<InternalServerError>()(\n    \"InternalServerError\",\n    { message: Schema.String, requestId: Schema.optional(Schema.String) },\n    HttpApiSchema.annotations({ status: 500 }),\n) {}\n\n// Use sparingly - only for truly unexpected errors\nEffect.catchAll((unexpectedError) =>\n    Effect.fail(new InternalServerError({\n        message: \"An unexpected error occurred\",\n        requestId: context.requestId,\n    }))\n)\n```\n\n## Error Logging\n\nLog errors with structured context:\n\n```typescript\nconst processWithLogging = Effect.fn(\"OrderService.process\")(function* (orderId: OrderId) {\n    return yield* processOrder(orderId).pipe(\n        Effect.tapError((err) =>\n            Effect.log(\"Order processing failed\", {\n                orderId,\n                errorTag: err._tag,\n                errorMessage: err.message,\n            })\n        ),\n    )\n})\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/layer-patterns.md",
    "content": "# Layer Patterns\n\n## Dependencies in Effect.Service\n\n**Critical rule:** Always declare dependencies in the `dependencies` array of `Effect.Service`. This ensures proper composition and avoids \"leaked dependencies\" that require manual wiring at usage sites.\n\n### Correct Pattern\n\n```typescript\nexport class OrderService extends Effect.Service<OrderService>()(\"OrderService\", {\n    accessors: true,\n    dependencies: [\n        UserService.Default,\n        ProductService.Default,\n        InventoryService.Default,\n        PaymentService.Default,\n    ],\n    effect: Effect.gen(function* () {\n        const users = yield* UserService\n        const products = yield* ProductService\n        const inventory = yield* InventoryService\n        const payments = yield* PaymentService\n\n        // Service implementation...\n        return { /* methods */ }\n    }),\n}) {}\n\n// At app root - simple, flat composition\nconst AppLive = Layer.mergeAll(\n    OrderService.Default,\n    // Other top-level services\n    NotificationService.Default,\n    AnalyticsService.Default,\n)\n```\n\n### Wrong Pattern (Leaked Dependencies)\n\n```typescript\n// WRONG - Dependencies not declared\nexport class OrderService extends Effect.Service<OrderService>()(\"OrderService\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const users = yield* UserService // Not in dependencies!\n        // ...\n    }),\n}) {}\n\n// Now every usage requires manual wiring\nconst program = OrderService.create(input).pipe(\n    Effect.provide(\n        OrderService.Default.pipe(\n            Layer.provide(UserService.Default),\n            Layer.provide(ProductService.Default),\n            // Easy to forget one, causes runtime errors\n        )\n    ),\n)\n```\n\n## Infrastructure Layers\n\nInfrastructure layers (Database, Redis, HTTP clients) are **acceptable** to leave as \"leaked\" dependencies because:\n\n1. They're provided once at the application root\n2. They don't change between test/production (different implementations, same interface)\n3. They're true infrastructure, not business logic\n\n```typescript\n// Infrastructure can be provided at app root\nimport { PgClient } from \"@effect/sql-pg\"\n\nconst DatabaseLive = PgClient.layer({\n    host: Config.string(\"DB_HOST\"),\n    port: Config.integer(\"DB_PORT\"),\n    database: Config.string(\"DB_NAME\"),\n    username: Config.string(\"DB_USER\"),\n    password: Config.secret(\"DB_PASSWORD\"),\n})\n\n// Services use database but don't declare it in dependencies\nexport class UserRepo extends Effect.Service<UserRepo>()(\"UserRepo\", {\n    accessors: true,\n    // No dependencies array - PgClient provided at app root\n    effect: Effect.gen(function* () {\n        const sql = yield* PgClient.PgClient\n\n        const findById = Effect.fn(\"UserRepo.findById\")(function* (id: UserId) {\n            const rows = yield* sql`SELECT * FROM users WHERE id = ${id}`.pipe(Effect.orDie)\n            return rows[0] as User | undefined\n        })\n\n        return { findById }\n    }),\n}) {}\n\n// App root provides infrastructure once\nconst AppLive = Layer.mergeAll(\n    OrderService.Default,\n    UserService.Default,\n).pipe(\n    Layer.provide(DatabaseLive), // Infrastructure provided here\n    Layer.provide(RedisLive),\n)\n```\n\n## Layer.mergeAll Over Nested Provides\n\n**Use `Layer.mergeAll`** for composing layers at the same level:\n\n```typescript\n// CORRECT - Flat composition\nconst ServicesLive = Layer.mergeAll(\n    UserService.Default,\n    OrderService.Default,\n    ProductService.Default,\n    NotificationService.Default,\n)\n\nconst InfrastructureLive = Layer.mergeAll(\n    DatabaseLive,\n    RedisLive,\n    HttpClientLive,\n)\n\nconst AppLive = ServicesLive.pipe(\n    Layer.provide(InfrastructureLive),\n)\n```\n\n```typescript\n// WRONG - Deeply nested, hard to read\nconst AppLive = UserService.Default.pipe(\n    Layer.provide(\n        OrderService.Default.pipe(\n            Layer.provide(\n                ProductService.Default.pipe(\n                    Layer.provide(DatabaseLive),\n                ),\n            ),\n        ),\n    ),\n)\n```\n\n## Layer Naming Conventions\n\nUse suffixes to indicate layer type:\n\n- `ServiceLive` - Production implementation\n- `ServiceTest` - Test/mock implementation\n- `ServiceLayer` - Generic layer (rare)\n\n```typescript\n// Production\nexport const UserServiceLive = UserService.Default\n\n// Test with mocks\nexport const UserServiceTest = Layer.succeed(\n    UserService,\n    UserService.of({\n        findById: (id) => Effect.succeed(mockUser),\n        create: (input) => Effect.succeed({ id: UserId.make(\"test-id\"), ...input }),\n    })\n)\n\n// Test with in-memory state\nexport class UserServiceInMemory extends Effect.Service<UserService>()(\"UserService\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const store = new Map<string, User>()\n\n        return {\n            findById: Effect.fn(\"UserService.findById\")(function* (id) {\n                const user = store.get(id)\n                if (!user) return yield* Effect.fail(new UserNotFoundError({ userId: id }))\n                return user\n            }),\n            create: Effect.fn(\"UserService.create\")(function* (input) {\n                const user = { id: UserId.make(crypto.randomUUID()), ...input }\n                store.set(user.id, user)\n                return user\n            }),\n        }\n    }),\n}) {}\n```\n\n## Layer.unwrapEffect for Config-Dependent Layers\n\nWhen a layer needs async configuration:\n\n```typescript\nimport { Config, Effect, Layer } from \"effect\"\n\n// Layer that depends on config\nconst ApiClientLive = Layer.unwrapEffect(\n    Effect.gen(function* () {\n        const apiKey = yield* Config.string(\"API_KEY\")\n        const baseUrl = yield* Config.string(\"API_BASE_URL\")\n        const timeout = yield* Config.integer(\"API_TIMEOUT\").pipe(\n            Config.withDefault(5000)\n        )\n\n        return Layer.succeed(\n            ApiClient,\n            new ApiClientImpl({ apiKey, baseUrl, timeout })\n        )\n    })\n)\n\n// Layer that validates config\nconst ValidatedConfigLive = Layer.unwrapEffect(\n    Effect.gen(function* () {\n        const config = yield* Config.all({\n            dbUrl: Config.string(\"DATABASE_URL\"),\n            redisUrl: Config.string(\"REDIS_URL\"),\n            port: Config.integer(\"PORT\"),\n        })\n\n        // Validate config\n        if (!config.dbUrl.startsWith(\"postgresql://\")) {\n            return yield* Effect.fail(new ConfigError({ message: \"Invalid DATABASE_URL\" }))\n        }\n\n        return Layer.succeed(AppConfig, config)\n    })\n)\n```\n\n## Scoped Layers\n\nFor resources that need cleanup:\n\n```typescript\nimport { Effect, Layer, Scope } from \"effect\"\n\n// Resource that needs cleanup\nconst DatabaseConnectionLive = Layer.scoped(\n    DatabaseConnection,\n    Effect.acquireRelease(\n        Effect.gen(function* () {\n            const pool = yield* createPool(config)\n            yield* Effect.log(\"Database pool created\")\n            return pool\n        }),\n        (pool) =>\n            Effect.gen(function* () {\n                yield* pool.end()\n                yield* Effect.log(\"Database pool closed\")\n            }).pipe(Effect.orDie)\n    )\n)\n\n// Service using scoped resource\nexport class UserRepo extends Effect.Service<UserRepo>()(\"UserRepo\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const db = yield* DatabaseConnection\n\n        return {\n            findById: Effect.fn(\"UserRepo.findById\")(function* (id) {\n                return yield* db.query(\"SELECT * FROM users WHERE id = $1\", [id])\n            }),\n        }\n    }),\n}) {}\n```\n\n## Testing Layer Composition\n\n```typescript\n// test/setup.ts\nimport { Layer } from \"effect\"\n\nexport const TestLive = Layer.mergeAll(\n    UserServiceTest,\n    OrderServiceTest,\n    ProductServiceTest,\n).pipe(\n    Layer.provide(InMemoryDatabaseLive),\n)\n\n// test/user.test.ts\nimport { Effect } from \"effect\"\nimport { TestLive } from \"./setup\"\n\ndescribe(\"UserService\", () => {\n    it(\"creates users\", async () => {\n        const program = Effect.gen(function* () {\n            const user = yield* UserService.create({\n                email: \"test@example.com\",\n                name: \"Test User\",\n            })\n            expect(user.email).toBe(\"test@example.com\")\n        })\n\n        await Effect.runPromise(program.pipe(Effect.provide(TestLive)))\n    })\n})\n```\n\n## Layer.effect vs Layer.succeed\n\n```typescript\n// Layer.succeed - for static values (no effects)\nconst ConfigLive = Layer.succeed(AppConfig, {\n    port: 3000,\n    env: \"development\",\n})\n\n// Layer.effect - when construction needs effects\nconst LoggerLive = Layer.effect(\n    Logger,\n    Effect.gen(function* () {\n        const config = yield* AppConfig\n        const transport = config.env === \"production\"\n            ? createCloudTransport()\n            : createConsoleTransport()\n        return new LoggerImpl(transport)\n    })\n)\n```\n\n## Lazy Layers\n\nFor expensive initialization that should be deferred:\n\n```typescript\nconst ExpensiveServiceLive = Layer.lazy(() => {\n    // This code runs only when the layer is first used\n    return Layer.effect(\n        ExpensiveService,\n        Effect.gen(function* () {\n            yield* Effect.log(\"Initializing expensive service...\")\n            const client = yield* createExpensiveClient()\n            return new ExpensiveServiceImpl(client)\n        })\n    )\n})\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/observability-patterns.md",
    "content": "# Observability Patterns\n\n## Structured Logging with Effect.log\n\n**Always use Effect.log** instead of console.log. Effect.log provides:\n- Structured data\n- Log levels\n- Integration with telemetry systems\n- Testability\n\n### Basic Logging\n\n```typescript\n// Simple message\nyield* Effect.log(\"Processing started\")\n\n// With structured data\nyield* Effect.log(\"Processing order\", {\n    orderId,\n    userId,\n    amount,\n    currency,\n})\n\n// Different log levels\nyield* Effect.logDebug(\"Cache lookup\", { key, hit: true })\nyield* Effect.logInfo(\"User logged in\", { userId })\nyield* Effect.logWarning(\"Rate limit approaching\", { current: 95, limit: 100 })\nyield* Effect.logError(\"Payment failed\", { orderId, reason: error.message })\nyield* Effect.logFatal(\"Database connection lost\")\n```\n\n### Logging in Services\n\n```typescript\nconst processOrder = Effect.fn(\"OrderService.processOrder\")(function* (input: OrderInput) {\n    yield* Effect.log(\"Starting order processing\", { orderId: input.orderId })\n\n    const result = yield* validateAndProcess(input).pipe(\n        Effect.tap(() => Effect.log(\"Order processed successfully\")),\n        Effect.tapError((err) =>\n            Effect.logError(\"Order processing failed\", {\n                orderId: input.orderId,\n                error: err._tag,\n                message: err.message,\n            })\n        ),\n    )\n\n    return result\n})\n```\n\n## Effect.fn for Automatic Tracing\n\n**Always use Effect.fn** for service methods. This automatically creates spans with proper names:\n\n```typescript\n// Creates span: \"UserService.findById\"\nconst findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n    // Automatic span creation with:\n    // - Start/end timing\n    // - Error capture\n    // - Parameter tracking (if annotated)\n})\n\n// Creates span: \"PaymentService.processPayment\"\nconst processPayment = Effect.fn(\"PaymentService.processPayment\")(\n    function* (orderId: OrderId, amount: number) {\n        // ...\n    }\n)\n```\n\n### Naming Convention\n\nUse `ServiceName.methodName` format consistently:\n- `UserService.findById`\n- `OrderService.create`\n- `PaymentService.refund`\n- `NotificationService.sendEmail`\n\n## Span Annotations\n\nAdd important context to spans, but don't overdo it:\n\n```typescript\nconst processOrder = Effect.fn(\"OrderService.process\")(function* (orderId: OrderId) {\n    // GOOD - Important business identifiers\n    yield* Effect.annotateCurrentSpan(\"orderId\", orderId)\n    yield* Effect.annotateCurrentSpan(\"userId\", order.userId)\n    yield* Effect.annotateCurrentSpan(\"totalAmount\", order.total)\n\n    // BAD - Too much detail, creates noise\n    // yield* Effect.annotateCurrentSpan(\"step\", \"validating\")\n    // yield* Effect.annotateCurrentSpan(\"itemCount\", order.items.length)\n    // yield* Effect.annotateCurrentSpan(\"item0Name\", order.items[0].name)\n})\n```\n\n### What to Annotate\n\n**Do annotate:**\n- Entity IDs (orderId, userId, etc.)\n- Important business values (amounts, statuses)\n- Error context when failing\n\n**Don't annotate:**\n- Step-by-step progress\n- Individual item details\n- Internal implementation state\n- Sensitive data (PII, secrets)\n\n## Metrics\n\n### Counter\n\n```typescript\nimport { Metric } from \"effect\"\n\n// Define metrics at module level\nconst ordersProcessed = Metric.counter(\"orders_processed\", {\n    description: \"Total orders processed\",\n})\n\nconst ordersFailed = Metric.counter(\"orders_failed\", {\n    description: \"Total orders that failed processing\",\n})\n\n// Use in service\nconst processOrder = Effect.fn(\"OrderService.process\")(function* (input: OrderInput) {\n    return yield* process(input).pipe(\n        Effect.tap(() => Metric.increment(ordersProcessed)),\n        Effect.tapError(() => Metric.increment(ordersFailed)),\n    )\n})\n```\n\n### Counter with Tags\n\n```typescript\nconst httpRequests = Metric.counter(\"http_requests_total\", {\n    description: \"Total HTTP requests\",\n})\n\n// Tag with method and status\nyield* Metric.increment(httpRequests).pipe(\n    Metric.tagged(\"method\", request.method),\n    Metric.tagged(\"status\", String(response.status)),\n    Metric.tagged(\"path\", request.path),\n)\n```\n\n### Gauge\n\n```typescript\nconst activeConnections = Metric.gauge(\"active_connections\", {\n    description: \"Number of active connections\",\n})\n\n// Update gauge\nyield* Metric.set(activeConnections, connectionCount)\n\n// Or increment/decrement\nyield* Metric.increment(activeConnections)\nyield* Metric.decrement(activeConnections)\n```\n\n### Histogram\n\n```typescript\nconst requestDuration = Metric.histogram(\"request_duration_ms\", {\n    description: \"Request duration in milliseconds\",\n    boundaries: [10, 50, 100, 250, 500, 1000, 2500, 5000],\n})\n\n// Record value\nyield* Metric.update(requestDuration, durationMs)\n\n// Or use timer helper\nconst timedEffect = effect.pipe(\n    Metric.timerWithHistogram(requestDuration),\n)\n```\n\n## Configuration with Config\n\n**Always use Config** instead of process.env:\n\n### Basic Config\n\n```typescript\nimport { Config, Effect } from \"effect\"\n\nconst config = Config.all({\n    port: Config.integer(\"PORT\").pipe(Config.withDefault(3000)),\n    host: Config.string(\"HOST\").pipe(Config.withDefault(\"localhost\")),\n    env: Config.literal(\"development\", \"staging\", \"production\")(\"NODE_ENV\"),\n})\n\n// Use in layer\nconst ServerLive = Layer.unwrapEffect(\n    Effect.gen(function* () {\n        const { port, host, env } = yield* config\n        return Layer.succeed(ServerConfig, { port, host, env })\n    })\n)\n```\n\n### Config with Validation\n\n```typescript\nconst dbConfig = Config.all({\n    host: Config.string(\"DB_HOST\"),\n    port: Config.integer(\"DB_PORT\").pipe(\n        Config.validate({\n            message: \"Port must be between 1 and 65535\",\n            validation: (p) => p >= 1 && p <= 65535,\n        })\n    ),\n    database: Config.string(\"DB_NAME\"),\n    maxConnections: Config.integer(\"DB_MAX_CONNECTIONS\").pipe(\n        Config.withDefault(10),\n        Config.validate({\n            message: \"Max connections must be positive\",\n            validation: (n) => n > 0,\n        })\n    ),\n})\n```\n\n### Secret Config\n\n```typescript\n// For sensitive values that shouldn't be logged\nconst secretConfig = Config.all({\n    apiKey: Config.secret(\"API_KEY\"),           // Returns Secret<string>\n    dbPassword: Config.secret(\"DB_PASSWORD\"),\n})\n\n// Using secrets\nconst program = Effect.gen(function* () {\n    const { apiKey, dbPassword } = yield* secretConfig\n\n    // Secret values are wrapped - use Secret.value to unwrap\n    const key = Secret.value(apiKey)\n\n    // Logging a Secret shows \"[REDACTED]\"\n    yield* Effect.log(\"Config loaded\", { apiKey }) // Safe - shows [REDACTED]\n})\n```\n\n### Config with Nested Structure\n\n```typescript\nconst appConfig = Config.all({\n    server: Config.all({\n        port: Config.integer(\"SERVER_PORT\"),\n        host: Config.string(\"SERVER_HOST\"),\n    }),\n    database: Config.all({\n        url: Config.string(\"DATABASE_URL\"),\n        pool: Config.integer(\"DATABASE_POOL_SIZE\").pipe(Config.withDefault(10)),\n    }),\n    features: Config.all({\n        enableBeta: Config.boolean(\"ENABLE_BETA\").pipe(Config.withDefault(false)),\n        maxUploadSize: Config.integer(\"MAX_UPLOAD_SIZE\").pipe(Config.withDefault(10485760)),\n    }),\n})\n```\n\n## Log Level Configuration\n\n```typescript\nimport { Logger, LogLevel } from \"effect\"\n\n// Set log level via config\nconst LoggerLive = Layer.unwrapEffect(\n    Effect.gen(function* () {\n        const level = yield* Config.literal(\n            \"debug\", \"info\", \"warning\", \"error\"\n        )(\"LOG_LEVEL\").pipe(Config.withDefault(\"info\"))\n\n        const logLevel = {\n            debug: LogLevel.Debug,\n            info: LogLevel.Info,\n            warning: LogLevel.Warning,\n            error: LogLevel.Error,\n        }[level]\n\n        return Logger.minimumLogLevel(logLevel)\n    })\n)\n\n// Production: structured JSON logging\nconst JsonLoggerLive = Logger.json\n```\n\n## Combining Observability\n\n```typescript\nconst processOrder = Effect.fn(\"OrderService.process\")(function* (input: OrderInput) {\n    const startTime = yield* Effect.clockWith((clock) => clock.currentTimeMillis)\n\n    // Annotate span\n    yield* Effect.annotateCurrentSpan(\"orderId\", input.orderId)\n    yield* Effect.annotateCurrentSpan(\"userId\", input.userId)\n\n    // Log start\n    yield* Effect.log(\"Processing order\", { orderId: input.orderId })\n\n    const result = yield* process(input).pipe(\n        Effect.tap((order) =>\n            Effect.gen(function* () {\n                const endTime = yield* Effect.clockWith((c) => c.currentTimeMillis)\n                const duration = endTime - startTime\n\n                // Record metric\n                yield* Metric.update(orderProcessingDuration, duration)\n                yield* Metric.increment(ordersProcessed)\n\n                // Log completion\n                yield* Effect.log(\"Order processed\", {\n                    orderId: input.orderId,\n                    durationMs: duration,\n                })\n            })\n        ),\n        Effect.tapError((err) =>\n            Effect.gen(function* () {\n                yield* Metric.increment(ordersFailed)\n                yield* Effect.logError(\"Order processing failed\", {\n                    orderId: input.orderId,\n                    error: err._tag,\n                })\n            })\n        ),\n    )\n\n    return result\n})\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/rpc-cluster-patterns.md",
    "content": "# RPC & Cluster Patterns\n\n## RpcGroup for API Organization\n\n**Use `RpcGroup.make`** to organize related RPC endpoints:\n\n```typescript\nimport { Rpc, RpcGroup } from \"@effect/rpc\"\nimport { Schema } from \"effect\"\n\n// Group related operations\nexport const UserRpc = RpcGroup.make(\"User\", {\n    // Queries (read operations)\n    findById: Rpc.query({\n        input: UserId,\n        output: User,\n        error: UserNotFoundError,\n    }),\n\n    list: Rpc.query({\n        input: Schema.Struct({\n            organizationId: OrganizationId,\n            limit: Schema.optionalWith(Schema.Number, { default: () => 50 }),\n            offset: Schema.optionalWith(Schema.Number, { default: () => 0 }),\n        }),\n        output: Schema.Array(User),\n        error: Schema.Never,\n    }),\n\n    // Mutations (write operations)\n    create: Rpc.mutation({\n        input: CreateUserInput,\n        output: User,\n        error: Schema.Union(UserCreateError, ValidationError),\n    }),\n\n    update: Rpc.mutation({\n        input: Schema.Struct({\n            id: UserId,\n            data: UpdateUserInput,\n        }),\n        output: User,\n        error: Schema.Union(UserNotFoundError, ValidationError),\n    }),\n\n    delete: Rpc.mutation({\n        input: UserId,\n        output: Schema.Void,\n        error: UserNotFoundError,\n    }),\n})\n```\n\n### Query vs Mutation\n\n- **Rpc.query** - Read operations, idempotent, cacheable\n- **Rpc.mutation** - Write operations, may have side effects\n\n```typescript\n// Query - safe to retry, can be cached\nfindById: Rpc.query({ ... }),\nsearch: Rpc.query({ ... }),\nlist: Rpc.query({ ... }),\n\n// Mutation - may modify state\ncreate: Rpc.mutation({ ... }),\nupdate: Rpc.mutation({ ... }),\ndelete: Rpc.mutation({ ... }),\n```\n\n## Error Unions in RPC\n\n**Always use explicit error unions** for RPC error types:\n\n```typescript\n// Explicit union of possible errors\ncreate: Rpc.mutation({\n    input: CreateOrderInput,\n    output: Order,\n    error: Schema.Union(\n        ValidationError,\n        InsufficientInventoryError,\n        PaymentFailedError,\n        UserNotFoundError,\n    ),\n}),\n\n// NOT - generic error type\ncreate: Rpc.mutation({\n    input: CreateOrderInput,\n    output: Order,\n    error: GenericError, // WRONG - loses type information\n}),\n```\n\n## RPC Middleware for Authentication\n\n```typescript\nimport { RpcMiddleware, Rpc } from \"@effect/rpc\"\nimport { Effect, Layer } from \"effect\"\n\n// Context type for authenticated user\nexport class CurrentUser extends Context.Tag(\"CurrentUser\")<\n    CurrentUser,\n    { id: UserId; role: UserRole; organizationId: OrganizationId }\n>() {}\n\n// Auth middleware - extracts and validates auth\nexport class AuthMiddleware extends RpcMiddleware.Tag<AuthMiddleware>()(\n    \"AuthMiddleware\",\n    {\n        provides: CurrentUser,\n        failure: UnauthorizedError,\n    }\n) {}\n\n// Middleware implementation\nexport const AuthMiddlewareLive = Layer.effect(\n    AuthMiddleware,\n    Effect.gen(function* () {\n        const authService = yield* AuthService\n\n        return AuthMiddleware.of({\n            execute: (request) =>\n                Effect.gen(function* () {\n                    const token = request.headers.get(\"authorization\")?.replace(\"Bearer \", \"\")\n\n                    if (!token) {\n                        return yield* Effect.fail(new UnauthorizedError({ message: \"Missing token\" }))\n                    }\n\n                    const user = yield* authService.validateToken(token).pipe(\n                        Effect.catchTag(\"TokenExpiredError\", () =>\n                            Effect.fail(new UnauthorizedError({ message: \"Token expired\" }))\n                        ),\n                        Effect.catchTag(\"TokenInvalidError\", () =>\n                            Effect.fail(new UnauthorizedError({ message: \"Invalid token\" }))\n                        ),\n                    )\n\n                    return user\n                }),\n        })\n    })\n)\n\n// Protected RPC using middleware\nexport const ProtectedUserRpc = UserRpc.middleware(AuthMiddleware)\n```\n\n## Workflow Definition\n\n**Use `Workflow.make`** with explicit idempotency keys:\n\n```typescript\nimport { Workflow } from \"@effect/cluster\"\nimport { Schema } from \"effect\"\n\nexport const OrderFulfillmentWorkflow = Workflow.make({\n    name: \"OrderFulfillmentWorkflow\",\n    payload: {\n        id: OrderId,           // Execution ID\n        orderId: OrderId,\n        userId: UserId,\n        items: Schema.Array(OrderItem),\n        shippingAddress: ShippingAddress,\n    },\n    // Idempotency key prevents duplicate processing\n    idempotencyKey: ({ orderId }) => orderId,\n})\n\nexport const NotificationWorkflow = Workflow.make({\n    name: \"NotificationWorkflow\",\n    payload: {\n        id: Schema.String,     // Unique execution ID\n        messageId: MessageId,\n        channelId: ChannelId,\n        authorId: UserId,\n    },\n    idempotencyKey: ({ messageId }) => messageId,\n})\n```\n\n### Workflow Implementation\n\n```typescript\nimport { Activity } from \"@effect/workflow\"\nimport { Effect } from \"effect\"\n\nexport const OrderFulfillmentWorkflowLayer = OrderFulfillmentWorkflow.toLayer(\n    Effect.fn(\"OrderFulfillmentWorkflow\")(function* (payload) {\n        // Step 1: Reserve inventory\n        const reservation = yield* Activity.make({\n            name: \"ReserveInventory\",\n            success: InventoryReservation,\n            error: Schema.Union(InsufficientInventoryError, DatabaseError),\n            execute: Effect.gen(function* () {\n                const inventory = yield* InventoryService\n                return yield* inventory.reserve(payload.items)\n            }),\n        })\n\n        // Step 2: Process payment\n        const payment = yield* Activity.make({\n            name: \"ProcessPayment\",\n            success: PaymentResult,\n            error: Schema.Union(PaymentFailedError, PaymentTimeoutError),\n            execute: Effect.gen(function* () {\n                const payments = yield* PaymentService\n                return yield* payments.charge(payload.userId, payload.items)\n            }),\n        })\n\n        // Step 3: Create shipment\n        const shipment = yield* Activity.make({\n            name: \"CreateShipment\",\n            success: Shipment,\n            error: Schema.Union(ShippingError, AddressInvalidError),\n            execute: Effect.gen(function* () {\n                const shipping = yield* ShippingService\n                return yield* shipping.createShipment({\n                    items: payload.items,\n                    address: payload.shippingAddress,\n                    reservationId: reservation.id,\n                })\n            }),\n        })\n\n        // Step 4: Send confirmation\n        yield* Activity.make({\n            name: \"SendConfirmation\",\n            success: Schema.Void,\n            error: NotificationError,\n            execute: Effect.gen(function* () {\n                const notifications = yield* NotificationService\n                yield* notifications.sendOrderConfirmation({\n                    userId: payload.userId,\n                    orderId: payload.orderId,\n                    trackingNumber: shipment.trackingNumber,\n                })\n            }),\n        })\n\n        return { shipment, payment }\n    })\n)\n```\n\n## Activity Patterns\n\n**Always include `success` and `error` schemas** in Activity.make:\n\n```typescript\n// CORRECT - schemas specified\nyield* Activity.make({\n    name: \"SendEmail\",\n    success: EmailSentResult,\n    error: Schema.Union(EmailDeliveryError, EmailTemplateError),\n    execute: Effect.gen(function* () {\n        // Implementation\n        return { messageId: \"msg-123\", sentAt: new Date() }\n    }),\n})\n\n// WRONG - missing schemas\nyield* Activity.make({\n    name: \"SendEmail\",\n    execute: Effect.gen(function* () {\n        // This will not serialize properly across workflow restarts\n    }),\n})\n```\n\n### Activity Error Handling with Retryable\n\n```typescript\nexport class ExternalApiError extends Schema.TaggedError<ExternalApiError>()(\n    \"ExternalApiError\",\n    {\n        message: Schema.String,\n        statusCode: Schema.Number,\n        retryable: Schema.Boolean,\n    },\n) {\n    static fromResponse(response: Response): ExternalApiError {\n        return new ExternalApiError({\n            message: `API error: ${response.statusText}`,\n            statusCode: response.status,\n            retryable: response.status >= 500, // 5xx errors are retryable\n        })\n    }\n}\n\nyield* Activity.make({\n    name: \"CallExternalApi\",\n    success: ApiResponse,\n    error: ExternalApiError,\n    execute: Effect.gen(function* () {\n        const response = yield* fetch(url)\n        if (!response.ok) {\n            return yield* Effect.fail(ExternalApiError.fromResponse(response))\n        }\n        return yield* response.json()\n    }),\n})\n```\n\n## ClusterCron for Scheduled Jobs\n\n```typescript\nimport { ClusterCron } from \"@effect/cluster\"\n\nexport const DailyReportCron = ClusterCron.make({\n    name: \"DailyReportCron\",\n    // Cron expression: every day at 6 AM UTC\n    schedule: \"0 6 * * *\",\n})\n\n// Implementation\nexport const DailyReportCronLayer = DailyReportCron.toLayer(\n    Effect.fn(\"DailyReportCron\")(function* () {\n        yield* Effect.log(\"Starting daily report generation\")\n\n        const reports = yield* ReportService\n        yield* reports.generateDailyReport()\n\n        yield* Effect.log(\"Daily report generation complete\")\n    })\n)\n```\n\n## Triggering Workflows\n\n### From HTTP Handler\n\n```typescript\nimport { HttpApi, HttpApiEndpoint } from \"@effect/platform\"\n\nconst createOrder = HttpApiEndpoint.post(\"createOrder\", \"/orders\")\n    .setPayload(CreateOrderInput)\n    .addSuccess(Order)\n    .addError(ValidationError)\n\n// Handler triggers workflow\nconst createOrderHandler = Effect.gen(function* () {\n    const input = yield* HttpApi.payload\n    const workflowClient = yield* WorkflowClient\n\n    // Create order in database\n    const order = yield* OrderService.create(input)\n\n    // Trigger async fulfillment workflow\n    yield* workflowClient.workflows.OrderFulfillmentWorkflow.execute({\n        id: order.id,\n        orderId: order.id,\n        userId: input.userId,\n        items: input.items,\n        shippingAddress: input.shippingAddress,\n    })\n\n    return order\n})\n```\n\n### From Backend Service\n\n```typescript\nexport class MessageService extends Effect.Service<MessageService>()(\"MessageService\", {\n    accessors: true,\n    dependencies: [MessageRepo.Default, WorkflowClient.Default],\n    effect: Effect.gen(function* () {\n        const repo = yield* MessageRepo\n        const workflows = yield* WorkflowClient\n\n        const create = Effect.fn(\"MessageService.create\")(function* (input: CreateMessageInput) {\n            const message = yield* repo.create(input)\n\n            // Trigger notification workflow\n            yield* workflows.workflows.NotificationWorkflow.execute({\n                id: message.id,\n                messageId: message.id,\n                channelId: message.channelId,\n                authorId: message.authorId,\n            })\n\n            return message\n        })\n\n        return { create }\n    }),\n}) {}\n```\n\n## Workflow HTTP API\n\n```typescript\n// Expose workflow execution via HTTP\nconst executeWorkflow = HttpApiEndpoint.post(\"executeWorkflow\", \"/workflows/:name/execute\")\n    .setPath(Schema.Struct({ name: Schema.String }))\n    .setPayload(Schema.Unknown)\n    .addSuccess(Schema.Struct({ executionId: Schema.String }))\n    .addError(WorkflowNotFoundError)\n\n// Handler\nconst executeWorkflowHandler = Effect.gen(function* () {\n    const { name } = yield* HttpApi.path\n    const payload = yield* HttpApi.payload\n    const client = yield* WorkflowClient\n\n    const workflow = client.workflows[name]\n    if (!workflow) {\n        return yield* Effect.fail(new WorkflowNotFoundError({ name }))\n    }\n\n    const result = yield* workflow.execute(payload)\n    return { executionId: payload.id }\n})\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/schema-patterns.md",
    "content": "# Schema Patterns\n\n## Branded Types for IDs\n\n**Always brand entity IDs** to prevent accidentally passing the wrong ID type:\n\n```typescript\nimport { Schema } from \"effect\"\n\n// Entity IDs - always branded with namespace\nexport const UserId = Schema.UUID.pipe(Schema.brand(\"@App/UserId\"))\nexport type UserId = Schema.Schema.Type<typeof UserId>\n\nexport const OrganizationId = Schema.UUID.pipe(Schema.brand(\"@App/OrganizationId\"))\nexport type OrganizationId = Schema.Schema.Type<typeof OrganizationId>\n\nexport const OrderId = Schema.UUID.pipe(Schema.brand(\"@App/OrderId\"))\nexport type OrderId = Schema.Schema.Type<typeof OrderId>\n\nexport const ProductId = Schema.UUID.pipe(Schema.brand(\"@App/ProductId\"))\nexport type ProductId = Schema.Schema.Type<typeof ProductId>\n```\n\n### Branding Convention\n\nUse `@Namespace/EntityName` format:\n- `@App/UserId` - Main application entities\n- `@Billing/InvoiceId` - Billing domain entities\n- `@External/StripeCustomerId` - External system IDs\n\n### Creating Branded Values\n\n```typescript\n// From string (validates UUID format)\nconst userId = Schema.decodeSync(UserId)(\"123e4567-e89b-12d3-a456-426614174000\")\n\n// Generate new ID\nconst newUserId = UserId.make(crypto.randomUUID())\n\n// Type error - can't mix ID types\nconst order = yield* orderService.findById(userId) // Error: UserId is not OrderId\n```\n\n### When NOT to Brand\n\nDon't brand simple strings that don't need type safety:\n\n```typescript\n// NOT branded - acceptable\nexport const Url = Schema.String\nexport const FilePath = Schema.String\nexport const EmailAddress = Schema.String.pipe(Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/))\n\n// These don't need branding because:\n// 1. They don't cross service boundaries in ways that could be confused\n// 2. They're typically validated by format, not by type\n```\n\n## Schema.Struct for Domain Types\n\n**Prefer Schema.Struct** over TypeScript interfaces for domain types:\n\n```typescript\n// CORRECT - Schema.Struct\nexport const User = Schema.Struct({\n    id: UserId,\n    email: Schema.String,\n    name: Schema.String,\n    organizationId: OrganizationId,\n    role: Schema.Literal(\"admin\", \"member\", \"viewer\"),\n    createdAt: Schema.DateTimeUtc,\n    updatedAt: Schema.DateTimeUtc,\n})\nexport type User = Schema.Schema.Type<typeof User>\n\n// Can derive encoded type for database/API\nexport type UserEncoded = Schema.Schema.Encoded<typeof User>\n```\n\n### Input Types for Mutations\n\n```typescript\nexport const CreateUserInput = Schema.Struct({\n    email: Schema.String.pipe(\n        Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/),\n        Schema.annotations({ description: \"Valid email address\" }),\n    ),\n    name: Schema.String.pipe(\n        Schema.minLength(1),\n        Schema.maxLength(100),\n    ),\n    organizationId: OrganizationId,\n    role: Schema.optionalWith(\n        Schema.Literal(\"admin\", \"member\", \"viewer\"),\n        { default: () => \"member\" as const }\n    ),\n})\nexport type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>\n\nexport const UpdateUserInput = Schema.Struct({\n    name: Schema.optional(Schema.String.pipe(Schema.minLength(1))),\n    role: Schema.optional(Schema.Literal(\"admin\", \"member\", \"viewer\")),\n})\nexport type UpdateUserInput = Schema.Schema.Type<typeof UpdateUserInput>\n```\n\n## Schema.transform and transformOrFail\n\n**Use transforms** instead of manual parsing:\n\n```typescript\n// Transform string to Date\nexport const DateFromString = Schema.transform(\n    Schema.String,\n    Schema.DateTimeUtc,\n    {\n        decode: (s) => new Date(s),\n        encode: (d) => d.toISOString(),\n    }\n)\n\n// Transform with validation (can fail)\nexport const PositiveNumber = Schema.transformOrFail(\n    Schema.Number,\n    Schema.Number.pipe(Schema.brand(\"PositiveNumber\")),\n    {\n        decode: (n, _, ast) =>\n            n > 0\n                ? ParseResult.succeed(n as Schema.Schema.Type<typeof PositiveNumber>)\n                : ParseResult.fail(new ParseResult.Type(ast, n, \"Must be positive\")),\n        encode: ParseResult.succeed,\n    }\n)\n```\n\n### Common Transforms\n\n```typescript\n// JSON string to object\nexport const JsonFromString = <A, I>(schema: Schema.Schema<A, I>) =>\n    Schema.transform(\n        Schema.String,\n        schema,\n        {\n            decode: (s) => JSON.parse(s),\n            encode: (a) => JSON.stringify(a),\n        }\n    )\n\n// Comma-separated string to array\nexport const CommaSeparatedList = Schema.transform(\n    Schema.String,\n    Schema.Array(Schema.String),\n    {\n        decode: (s) => s.split(\",\").map((x) => x.trim()).filter(Boolean),\n        encode: (arr) => arr.join(\",\"),\n    }\n)\n\n// Cents to dollars\nexport const DollarsFromCents = Schema.transform(\n    Schema.Number.pipe(Schema.int()),\n    Schema.Number,\n    {\n        decode: (cents) => cents / 100,\n        encode: (dollars) => Math.round(dollars * 100),\n    }\n)\n```\n\n## Schema.Class for Entities with Methods\n\nUse `Schema.Class` when entities need methods:\n\n```typescript\nexport class User extends Schema.Class<User>(\"User\")({\n    id: UserId,\n    email: Schema.String,\n    name: Schema.String,\n    role: Schema.Literal(\"admin\", \"member\", \"viewer\"),\n    createdAt: Schema.DateTimeUtc,\n}) {\n    get isAdmin(): boolean {\n        return this.role === \"admin\"\n    }\n\n    get displayName(): string {\n        return this.name || this.email.split(\"@\")[0]\n    }\n\n    canAccessResource(resource: Resource): boolean {\n        if (this.isAdmin) return true\n        return resource.ownerId === this.id\n    }\n}\n\n// Usage\nconst user = new User({\n    id: UserId.make(crypto.randomUUID()),\n    email: \"alice@example.com\",\n    name: \"Alice\",\n    role: \"member\",\n    createdAt: new Date(),\n})\n\nconsole.log(user.displayName) // \"Alice\"\nconsole.log(user.isAdmin) // false\n```\n\n## Schema.annotations\n\nAdd annotations for documentation and validation messages:\n\n```typescript\nexport const CreateOrderInput = Schema.Struct({\n    productId: ProductId.pipe(\n        Schema.annotations({ description: \"The product to order\" }),\n    ),\n    quantity: Schema.Number.pipe(\n        Schema.int(),\n        Schema.positive(),\n        Schema.annotations({\n            description: \"Number of items to order\",\n            examples: [1, 5, 10],\n        }),\n    ),\n    shippingAddress: Schema.Struct({\n        line1: Schema.String.pipe(Schema.annotations({ description: \"Street address\" })),\n        line2: Schema.optional(Schema.String),\n        city: Schema.String,\n        state: Schema.String.pipe(Schema.length(2)),\n        zip: Schema.String.pipe(Schema.pattern(/^\\d{5}(-\\d{4})?$/)),\n    }).pipe(Schema.annotations({ description: \"Shipping destination\" })),\n}).pipe(\n    Schema.annotations({\n        title: \"Create Order Input\",\n        description: \"Input for creating a new order\",\n    }),\n)\n```\n\n## Optional Fields\n\nUse `Schema.optional` and `Schema.optionalWith`:\n\n```typescript\nexport const UserPreferences = Schema.Struct({\n    // Optional, undefined if not provided\n    theme: Schema.optional(Schema.Literal(\"light\", \"dark\")),\n\n    // Optional with default value\n    language: Schema.optionalWith(Schema.String, { default: () => \"en\" }),\n\n    // Optional with null support (for database compatibility)\n    bio: Schema.NullOr(Schema.String),\n\n    // Optional but must be present if set (no undefined)\n    timezone: Schema.optional(Schema.String, { exact: true }),\n})\n```\n\n## Union Types and Discriminated Unions\n\n```typescript\n// Simple union\nexport const PaymentMethod = Schema.Union(\n    Schema.Literal(\"card\"),\n    Schema.Literal(\"bank_transfer\"),\n    Schema.Literal(\"crypto\"),\n)\n\n// Discriminated union (tagged)\nexport const PaymentDetails = Schema.Union(\n    Schema.Struct({\n        _tag: Schema.Literal(\"Card\"),\n        cardNumber: Schema.String,\n        expiry: Schema.String,\n        cvv: Schema.String,\n    }),\n    Schema.Struct({\n        _tag: Schema.Literal(\"BankTransfer\"),\n        accountNumber: Schema.String,\n        routingNumber: Schema.String,\n    }),\n    Schema.Struct({\n        _tag: Schema.Literal(\"Crypto\"),\n        walletAddress: Schema.String,\n        network: Schema.Literal(\"ethereum\", \"bitcoin\", \"solana\"),\n    }),\n)\nexport type PaymentDetails = Schema.Schema.Type<typeof PaymentDetails>\n\n// Usage with match\nconst processPayment = (details: PaymentDetails) => {\n    switch (details._tag) {\n        case \"Card\":\n            return processCard(details.cardNumber, details.expiry, details.cvv)\n        case \"BankTransfer\":\n            return processBankTransfer(details.accountNumber, details.routingNumber)\n        case \"Crypto\":\n            return processCrypto(details.walletAddress, details.network)\n    }\n}\n```\n\n## Enums and Literals\n\n```typescript\n// Use Literal for small, fixed sets\nexport const UserRole = Schema.Literal(\"admin\", \"member\", \"viewer\")\nexport type UserRole = Schema.Schema.Type<typeof UserRole>\n\n// Use Enums for larger sets or when you need runtime values\nexport const OrderStatus = Schema.Enums({\n    Pending: \"pending\",\n    Processing: \"processing\",\n    Shipped: \"shipped\",\n    Delivered: \"delivered\",\n    Cancelled: \"cancelled\",\n} as const)\nexport type OrderStatus = Schema.Schema.Type<typeof OrderStatus>\n```\n\n## Recursive Schemas\n\n```typescript\ninterface Category {\n    id: string\n    name: string\n    children: readonly Category[]\n}\n\nexport const Category: Schema.Schema<Category> = Schema.Struct({\n    id: Schema.String,\n    name: Schema.String,\n    children: Schema.Array(Schema.suspend(() => Category)),\n})\n```\n\n## Decoding and Encoding\n\n```typescript\n// Decode (parse) - use in services\nconst parseUser = Schema.decodeUnknown(User)\nconst result = yield* parseUser(rawData) // Effect<User, ParseError>\n\n// Decode sync - only in controlled contexts\nconst user = Schema.decodeUnknownSync(User)(rawData)\n\n// Encode - for serialization\nconst encodeUser = Schema.encode(User)\nconst encoded = yield* encodeUser(user) // Effect<UserEncoded, ParseError>\n```\n"
  },
  {
    "path": "agents/skills/effect-best-practices/references/service-patterns.md",
    "content": "# Service Patterns\n\n## Effect.Service Over Context.Tag\n\n**Always prefer `Effect.Service`** for defining business logic services. This is the modern, recommended approach that provides:\n\n1. **Built-in `Default` layer** - No manual layer creation needed\n2. **Automatic accessors** - Direct method calls via `ServiceName.method()`\n3. **Proper dependency declaration** - Dependencies are explicit and type-checked\n4. **Consistent structure** - All services follow the same pattern\n\n### Basic Service Definition\n\n```typescript\nimport { Effect, Layer } from \"effect\"\n\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n            // Implementation\n        })\n\n        const findByEmail = Effect.fn(\"UserService.findByEmail\")(function* (email: string) {\n            // Implementation\n        })\n\n        const create = Effect.fn(\"UserService.create\")(function* (input: CreateUserInput) {\n            // Implementation\n        })\n\n        return { findById, findByEmail, create }\n    }),\n}) {}\n```\n\n### Service with Dependencies\n\n**Critical:** Always declare dependencies using the `dependencies` array. This ensures:\n- Dependencies are automatically provided when using `ServiceName.Default`\n- Type errors if dependencies are missing\n- No manual `Layer.provide` at usage sites\n\n```typescript\nexport class OrderService extends Effect.Service<OrderService>()(\"OrderService\", {\n    accessors: true,\n    dependencies: [\n        UserService.Default,\n        ProductService.Default,\n        InventoryService.Default,\n    ],\n    effect: Effect.gen(function* () {\n        // Dependencies are automatically available\n        const users = yield* UserService\n        const products = yield* ProductService\n        const inventory = yield* InventoryService\n\n        const create = Effect.fn(\"OrderService.create\")(function* (input: CreateOrderInput) {\n            // Validate user exists\n            const user = yield* users.findById(input.userId)\n\n            // Check product availability\n            const product = yield* products.findById(input.productId)\n            const available = yield* inventory.checkAvailability(input.productId, input.quantity)\n\n            if (!available) {\n                return yield* Effect.fail(new InsufficientInventoryError({\n                    productId: input.productId,\n                    message: \"Not enough inventory\",\n                }))\n            }\n\n            // Create order...\n        })\n\n        return { create }\n    }),\n}) {}\n```\n\n### Wrong: Leaking Dependencies\n\n```typescript\n// WRONG - Dependencies not declared, must be provided manually\nexport class OrderService extends Effect.Service<OrderService>()(\"OrderService\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const users = yield* UserService  // Dependency not in `dependencies` array!\n        // ...\n    }),\n}) {}\n\n// Now every usage site must do this:\nconst program = OrderService.create(input).pipe(\n    Effect.provide(UserService.Default),  // Annoying and error-prone\n)\n```\n\n## Effect.fn for Tracing\n\n**Always wrap service methods with `Effect.fn`**. This provides automatic tracing with meaningful span names.\n\n### Naming Convention\n\nUse `ServiceName.methodName` format for span names:\n\n```typescript\nconst findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n    yield* Effect.annotateCurrentSpan(\"userId\", id)\n    // Implementation\n})\n\nconst processPayment = Effect.fn(\"PaymentService.processPayment\")(\n    function* (orderId: OrderId, amount: number, currency: string) {\n        yield* Effect.annotateCurrentSpan(\"orderId\", orderId)\n        yield* Effect.annotateCurrentSpan(\"amount\", amount)\n        yield* Effect.annotateCurrentSpan(\"currency\", currency)\n        // Implementation\n    }\n)\n```\n\n### Annotating Spans\n\nAdd important context to spans, but don't overdo it:\n\n```typescript\n// CORRECT - Important business identifiers\nyield* Effect.annotateCurrentSpan(\"userId\", userId)\nyield* Effect.annotateCurrentSpan(\"orderId\", orderId)\nyield* Effect.annotateCurrentSpan(\"amount\", amount)\n\n// WRONG - Too much detail, noise in traces\nyield* Effect.annotateCurrentSpan(\"userEmail\", user.email)\nyield* Effect.annotateCurrentSpan(\"userName\", user.name)\nyield* Effect.annotateCurrentSpan(\"userCreatedAt\", user.createdAt)\nyield* Effect.annotateCurrentSpan(\"step\", \"validating\")\nyield* Effect.annotateCurrentSpan(\"step\", \"processing\")\nyield* Effect.annotateCurrentSpan(\"step\", \"completing\")\n```\n\n## When Context.Tag is Acceptable\n\n`Context.Tag` is appropriate **only** for infrastructure that's injected at runtime:\n\n### Cloudflare Worker Bindings\n\n```typescript\nimport { Context } from \"effect\"\n\n// These are provided by the runtime, not created by our code\nexport class KVNamespace extends Context.Tag(\"KVNamespace\")<\n    KVNamespace,\n    CloudflareKVNamespace\n>() {}\n\nexport class R2Bucket extends Context.Tag(\"R2Bucket\")<\n    R2Bucket,\n    CloudflareR2Bucket\n>() {}\n\n// In the worker entry point\nconst handler = {\n    fetch(request: Request, env: Env) {\n        return program.pipe(\n            Effect.provideService(KVNamespace, env.MY_KV),\n            Effect.provideService(R2Bucket, env.MY_BUCKET),\n            Effect.runPromise,\n        )\n    }\n}\n```\n\n### Database/Redis Clients (Infrastructure)\n\n```typescript\n// Infrastructure provided at app root - acceptable as Context.Tag\n// But prefer using @effect/sql or similar typed clients\n\nimport { PgClient } from \"@effect/sql-pg\"\n\n// PgClient is already a Context.Tag from the library\n// Just provide it at the app root\nconst DatabaseLive = PgClient.layer({\n    host: Config.string(\"DB_HOST\"),\n    port: Config.integer(\"DB_PORT\"),\n    database: Config.string(\"DB_NAME\"),\n    // ...\n})\n```\n\n## Single Responsibility\n\nEach service should have a focused responsibility:\n\n```typescript\n// CORRECT - Focused services\nexport class UserService extends Effect.Service<UserService>()(\"UserService\", { /* user operations */ }) {}\nexport class AuthService extends Effect.Service<AuthService>()(\"AuthService\", { /* auth operations */ }) {}\nexport class NotificationService extends Effect.Service<NotificationService>()(\"NotificationService\", { /* notifications */ }) {}\n\n// WRONG - God service doing everything\nexport class AppService extends Effect.Service<AppService>()(\"AppService\", {\n    effect: Effect.gen(function* () {\n        return {\n            createUser,\n            deleteUser,\n            login,\n            logout,\n            sendEmail,\n            sendPush,\n            processPayment,\n            // ... 50 more methods\n        }\n    }),\n}) {}\n```\n\n## Service Interface Patterns\n\n### Return Types\n\nServices should return `Effect` types, never `Promise`:\n\n```typescript\n// CORRECT\nconst findById = Effect.fn(\"UserService.findById\")(\n    function* (id: UserId): Effect.Effect<User, UserNotFoundError> {\n        // ...\n    }\n)\n\n// WRONG - Promise in service interface\nconst findById = async (id: UserId): Promise<User> => {\n    // ...\n}\n```\n\n### Use Option for Nullable Results\n\n```typescript\n// CORRECT - findById can fail, findByIdOption returns Option\nconst findById = Effect.fn(\"UserService.findById\")(\n    function* (id: UserId): Effect.Effect<User, UserNotFoundError> {\n        const maybeUser = yield* repo.findById(id)\n        return yield* Option.match(maybeUser, {\n            onNone: () => Effect.fail(new UserNotFoundError({ userId: id, message: \"Not found\" })),\n            onSome: Effect.succeed,\n        })\n    }\n)\n\nconst findByIdOption = Effect.fn(\"UserService.findByIdOption\")(\n    function* (id: UserId): Effect.Effect<Option<User>> {\n        return yield* repo.findById(id)\n    }\n)\n```\n\n## Testing Services\n\nCreate test implementations using the same pattern:\n\n```typescript\n// Test implementation\nexport const UserServiceTest = Layer.succeed(\n    UserService,\n    UserService.of({\n        findById: (id) => Effect.succeed(mockUser),\n        create: (input) => Effect.succeed({ ...mockUser, ...input }),\n    })\n)\n\n// Or with Effect.Service for stateful mocks\nexport class UserServiceTest extends Effect.Service<UserService>()(\"UserService\", {\n    accessors: true,\n    effect: Effect.gen(function* () {\n        const users = new Map<string, User>()\n\n        const findById = Effect.fn(\"UserService.findById\")(function* (id: UserId) {\n            const user = users.get(id)\n            if (!user) return yield* Effect.fail(new UserNotFoundError({ userId: id, message: \"Not found\" }))\n            return user\n        })\n\n        const create = Effect.fn(\"UserService.create\")(function* (input: CreateUserInput) {\n            const user = { id: UserId.make(crypto.randomUUID()), ...input }\n            users.set(user.id, user)\n            return user\n        })\n\n        return { findById, create }\n    }),\n}) {}\n```\n"
  },
  {
    "path": "agents/skills/emil-design-eng/SKILL.md",
    "content": "---\nname: emil-design-eng\ndescription: This skill encodes Emil Kowalski's philosophy on UI polish, component design, animation decisions, and the invisible details that make software feel great.\n---\n\n# Design Engineering\n\nYou are a design engineer with the craft sensibility. You build interfaces where every detail compounds into something that feels right. You understand that in a world where everyone's software is good enough, taste is the differentiator.\n\n## Core Philosophy\n\n### Taste is trained, not innate\n\nGood taste is not personal preference. It is a trained instinct: the ability to see beyond the obvious and recognize what elevates. You develop it by surrounding yourself with great work, thinking deeply about why something feels good, and practicing relentlessly.\n\nWhen building UI, don't just make it work. Study why the best interfaces feel the way they do. Reverse engineer animations. Inspect interactions. Be curious.\n\n### Unseen details compound\n\nMost details users never consciously notice. That is the point. When a feature functions exactly as someone assumes it should, they proceed without giving it a second thought. That is the goal.\n\n> \"All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune.\" - Paul Graham\n\nEvery decision below exists because the aggregate of invisible correctness creates interfaces people love without knowing why.\n\n### Beauty is leverage\n\nPeople select tools based on the overall experience, not just functionality. Good defaults and good animations are real differentiators. Beauty is underutilized in software. Use it as leverage to stand out.\n\n## Review Format (Required)\n\nWhen reviewing UI code, you MUST use a markdown table with Before/After columns. Do NOT use a list with \"Before:\" and \"After:\" on separate lines. Always output an actual markdown table like this:\n\n| Before | After | Why |\n| --- | --- | --- |\n| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; avoid `all` |\n| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing |\n| `ease-in` on dropdown | `ease-out` with custom curve | `ease-in` feels sluggish; `ease-out` gives instant feedback |\n| No `:active` state on button | `transform: scale(0.97)` on `:active` | Buttons must feel responsive to press |\n| `transform-origin: center` on popover | `transform-origin: var(--radix-popover-content-transform-origin)` | Popovers should scale from their trigger (not modals — modals stay centered) |\n\nWrong format (never do this):\n\n```\nBefore: transition: all 300ms\nAfter: transition: transform 200ms ease-out\n────────────────────────────\nBefore: scale(0)\nAfter: scale(0.95)\n```\n\nCorrect format: A single markdown table with | Before | After | Why | columns, one row per issue found. The \"Why\" column briefly explains the reasoning.\n\n## The Animation Decision Framework\n\nBefore writing any animation code, answer these questions in order:\n\n### 1. Should this animate at all?\n\n**Ask:** How often will users see this animation?\n\n| Frequency                                                   | Decision                     |\n| ----------------------------------------------------------- | ---------------------------- |\n| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever.          |\n| Tens of times/day (hover effects, list navigation)          | Remove or drastically reduce |\n| Occasional (modals, drawers, toasts)                        | Standard animation           |\n| Rare/first-time (onboarding, feedback forms, celebrations)  | Can add delight              |\n\n**Never animate keyboard-initiated actions.** These actions are repeated hundreds of times daily. Animation makes them feel slow, delayed, and disconnected from the user's actions.\n\nRaycast has no open/close animation. That is the optimal experience for something used hundreds of times a day.\n\n### 2. What is the purpose?\n\nEvery animation must have a clear answer to \"why does this animate?\"\n\nValid purposes:\n\n- **Spatial consistency**: toast enters and exits from the same direction, making swipe-to-dismiss feel intuitive\n- **State indication**: a morphing feedback button shows the state change\n- **Explanation**: a marketing animation that shows how a feature works\n- **Feedback**: a button scales down on press, confirming the interface heard the user\n- **Preventing jarring changes**: elements appearing or disappearing without transition feel broken\n\nIf the purpose is just \"it looks cool\" and the user will see it often, don't animate.\n\n### 3. What easing should it use?\n\nIs the element entering or exiting?\n  Yes → ease-out (starts fast, feels responsive)\n  No →\n    Is it moving/morphing on screen?\n      Yes → ease-in-out (natural acceleration/deceleration)\n    Is it a hover/color change?\n      Yes → ease\n    Is it constant motion (marquee, progress bar)?\n      Yes → linear\n    Default → ease-out\n\n**Critical: use custom easing curves.** The built-in CSS easings are too weak. They lack the punch that makes animations feel intentional.\n\n```css\n/* Strong ease-out for UI interactions */\n--ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n/* Strong ease-in-out for on-screen movement */\n--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n\n/* iOS-like drawer curve (from Ionic Framework) */\n--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n```\n\n**Never use ease-in for UI animations.** It starts slow, which makes the interface feel sluggish and unresponsive. A dropdown with `ease-in` at 300ms _feels_ slower than `ease-out` at the same 300ms, because ease-in delays the initial movement — the exact moment the user is watching most closely.\n\n**Easing curve resources:** Don't create curves from scratch. Use [easing.dev](https://easing.dev/) or [easings.co](https://easings.co/) to find stronger custom variants of standard easings.\n\n### 4. How fast should it be?\n\n| Element                  | Duration      |\n| ------------------------ | ------------- |\n| Button press feedback    | 100-160ms     |\n| Tooltips, small popovers | 125-200ms     |\n| Dropdowns, selects       | 150-250ms     |\n| Modals, drawers          | 200-500ms     |\n| Marketing/explanatory    | Can be longer |\n\n**Rule: UI animations should stay under 300ms.** A 180ms dropdown feels more responsive than a 400ms one. A faster-spinning spinner makes the app feel like it loads faster, even when the load time is identical.\n\n### Perceived performance\n\nSpeed in animation is not just about feeling snappy — it directly affects how users perceive your app's performance:\n\n- A **fast-spinning spinner** makes loading feel faster (same load time, different perception)\n- A **180ms select** animation feels more responsive than a **400ms** one\n- **Instant tooltips** after the first one is open (skip delay + skip animation) make the whole toolbar feel faster\n\nThe perception of speed matters as much as actual speed. Easing amplifies this: `ease-out` at 200ms _feels_ faster than `ease-in` at 200ms because the user sees immediate movement.\n\n## Spring Animations\n\nSprings feel more natural than duration-based animations because they simulate real physics. They don't have fixed durations — they settle based on physical parameters.\n\n### When to use springs\n\n- Drag interactions with momentum\n- Elements that should feel \"alive\" (like Apple's Dynamic Island)\n- Gestures that can be interrupted mid-animation\n- Decorative mouse-tracking interactions\n\n### Spring-based mouse interactions\n\nTying visual changes directly to mouse position feels artificial because it lacks motion. Use `useSpring` from Motion (formerly Framer Motion) to interpolate value changes with spring-like behavior instead of updating immediately.\n\n```jsx\nimport { useSpring } from 'framer-motion';\n\n// Without spring: feels artificial, instant\nconst rotation = mouseX * 0.1;\n\n// With spring: feels natural, has momentum\nconst springRotation = useSpring(mouseX * 0.1, {\n  stiffness: 100,\n  damping: 10,\n});\n```\n\nThis works because the animation is **decorative** — it doesn't serve a function. If this were a functional graph in a banking app, no animation would be better. Know when decoration helps and when it hinders.\n\n### Spring configuration\n\n**Apple's approach (recommended — easier to reason about):**\n\n```js\n{ type: \"spring\", duration: 0.5, bounce: 0.2 }\n```\n\n**Traditional physics (more control):**\n\n```js\n{ type: \"spring\", mass: 1, stiffness: 100, damping: 10 }\n```\n\nKeep bounce subtle (0.1-0.3) when used. Avoid bounce in most UI contexts. Use it for drag-to-dismiss and playful interactions.\n\n### Interruptibility advantage\n\nSprings maintain velocity when interrupted — CSS animations and keyframes restart from zero. This makes springs ideal for gestures users might change mid-motion. When you click an expanded item and quickly press Escape, a spring-based animation smoothly reverses from its current position.\n\n## Component Building Principles\n\n### Buttons must feel responsive\n\nAdd `transform: scale(0.97)` on `:active`. This gives instant feedback, making the UI feel like it is truly listening to the user.\n\n```css\n.button {\n  transition: transform 160ms ease-out;\n}\n\n.button:active {\n  transform: scale(0.97);\n}\n```\n\nThis applies to any pressable element. The scale should be subtle (0.95-0.98).\n\n### Never animate from scale(0)\n\nNothing in the real world disappears and reappears completely. Elements animating from `scale(0)` look like they come out of nowhere.\n\nStart from `scale(0.9)` or higher, combined with opacity. Even a barely-visible initial scale makes the entrance feel more natural, like a balloon that has a visible shape even when deflated.\n\n```css\n/* Bad */\n.entering {\n  transform: scale(0);\n}\n\n/* Good */\n.entering {\n  transform: scale(0.95);\n  opacity: 0;\n}\n```\n\n### Make popovers origin-aware\n\nPopovers should scale in from their trigger, not from center. The default `transform-origin: center` is wrong for almost every popover. **Exception: modals.** Modals should keep `transform-origin: center` because they are not anchored to a specific trigger — they appear centered in the viewport.\n\n```css\n/* Radix UI */\n.popover {\n  transform-origin: var(--radix-popover-content-transform-origin);\n}\n\n/* Base UI */\n.popover {\n  transform-origin: var(--transform-origin);\n}\n```\n\nWhether the user notices the difference individually does not matter. In the aggregate, unseen details become visible. They compound.\n\n### Tooltips: skip delay on subsequent hovers\n\nTooltips should delay before appearing to prevent accidental activation. But once one tooltip is open, hovering over adjacent tooltips should open them instantly with no animation. This feels faster without defeating the purpose of the initial delay.\n\n```css\n.tooltip {\n  transition: transform 125ms ease-out, opacity 125ms ease-out;\n  transform-origin: var(--transform-origin);\n}\n\n.tooltip[data-starting-style],\n.tooltip[data-ending-style] {\n  opacity: 0;\n  transform: scale(0.97);\n}\n\n/* Skip animation on subsequent tooltips */\n.tooltip[data-instant] {\n  transition-duration: 0ms;\n}\n```\n\n### Use CSS transitions over keyframes for interruptible UI\n\nCSS transitions can be interrupted and retargeted mid-animation. Keyframes restart from zero. For any interaction that can be triggered rapidly (adding toasts, toggling states), transitions produce smoother results.\n\n```css\n/* Interruptible - good for UI */\n.toast {\n  transition: transform 400ms ease;\n}\n\n/* Not interruptible - avoid for dynamic UI */\n@keyframes slideIn {\n  from {\n    transform: translateY(100%);\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n```\n\n### Use blur to mask imperfect transitions\n\nWhen a crossfade between two states feels off despite trying different easings and durations, add subtle `filter: blur(2px)` during the transition.\n\n**Why blur works:** Without blur, you see two distinct objects during a crossfade — the old state and the new state overlapping. This looks unnatural. Blur bridges the visual gap by blending the two states together, tricking the eye into perceiving a single smooth transformation instead of two objects swapping.\n\nCombine blur with scale-on-press (`scale(0.97)`) for a polished button state transition:\n\n```css\n.button {\n  transition: transform 160ms ease-out;\n}\n\n.button:active {\n  transform: scale(0.97);\n}\n\n.button-content {\n  transition: filter 200ms ease, opacity 200ms ease;\n}\n\n.button-content.transitioning {\n  filter: blur(2px);\n  opacity: 0.7;\n}\n```\n\nKeep blur under 20px. Heavy blur is expensive, especially in Safari.\n\n### Animate enter states with @starting-style\n\nThe modern CSS way to animate element entry without JavaScript:\n\n```css\n.toast {\n  opacity: 1;\n  transform: translateY(0);\n  transition: opacity 400ms ease, transform 400ms ease;\n\n  @starting-style {\n    opacity: 0;\n    transform: translateY(100%);\n  }\n}\n```\n\nThis replaces the common React pattern of using `useEffect` to set `mounted: true` after initial render. Use `@starting-style` when browser support allows; fall back to the `data-mounted` attribute pattern otherwise.\n\n```jsx\n// Legacy pattern (still works everywhere)\nuseEffect(() => {\n  setMounted(true);\n}, []);\n// <div data-mounted={mounted}>\n```\n\n## CSS Transform Mastery\n\n### translateY with percentages\n\nPercentage values in `translate()` are relative to the element's own size. Use `translateY(100%)` to move an element by its own height, regardless of actual dimensions. This is how Sonner positions toasts and how Vaul hides the drawer before animating in.\n\n```css\n/* Works regardless of drawer height */\n.drawer-hidden {\n  transform: translateY(100%);\n}\n\n/* Works regardless of toast height */\n.toast-enter {\n  transform: translateY(-100%);\n}\n```\n\nPrefer percentages over hardcoded pixel values. They are less error-prone and adapt to content.\n\n### scale() scales children too\n\nUnlike `width`/`height`, `scale()` also scales an element's children. When scaling a button on press, the font size, icons, and content scale proportionally. This is a feature, not a bug.\n\n### 3D transforms for depth\n\n`rotateX()`, `rotateY()` with `transform-style: preserve-3d` create real 3D effects in CSS. Orbiting animations, coin flips, and depth effects are all possible without JavaScript.\n\n```css\n.wrapper {\n  transform-style: preserve-3d;\n}\n\n@keyframes orbit {\n  from {\n    transform: translate(-50%, -50%) rotateY(0deg) translateZ(72px) rotateY(360deg);\n  }\n  to {\n    transform: translate(-50%, -50%) rotateY(360deg) translateZ(72px) rotateY(0deg);\n  }\n}\n```\n\n### transform-origin\n\nEvery element has an anchor point from which transforms execute. The default is center. Set it to match where the trigger lives for origin-aware interactions.\n\n## clip-path for Animation\n\n`clip-path` is not just for shapes. It is one of the most powerful animation tools in CSS.\n\n### The inset shape\n\n`clip-path: inset(top right bottom left)` defines a rectangular clipping region. Each value \"eats\" into the element from that side.\n\n```css\n/* Fully hidden from right */\n.hidden {\n  clip-path: inset(0 100% 0 0);\n}\n\n/* Fully visible */\n.visible {\n  clip-path: inset(0 0 0 0);\n}\n\n/* Reveal from left to right */\n.overlay {\n  clip-path: inset(0 100% 0 0);\n  transition: clip-path 200ms ease-out;\n}\n.button:active .overlay {\n  clip-path: inset(0 0 0 0);\n  transition: clip-path 2s linear;\n}\n```\n\n### Tabs with perfect color transitions\n\nDuplicate the tab list. Style the copy as \"active\" (different background, different text color). Clip the copy so only the active tab is visible. Animate the clip on tab change. This creates a seamless color transition that timing individual color transitions can never achieve.\n\n### Hold-to-delete pattern\n\nUse `clip-path: inset(0 100% 0 0)` on a colored overlay. On `:active`, transition to `inset(0 0 0 0)` over 2s with linear timing. On release, snap back with 200ms ease-out. Add `scale(0.97)` on the button for press feedback.\n\n### Image reveals on scroll\n\nStart with `clip-path: inset(0 0 100% 0)` (hidden from bottom). Animate to `inset(0 0 0 0)` when the element enters the viewport. Use `IntersectionObserver` or Framer Motion's `useInView` with `{ once: true, margin: \"-100px\" }`.\n\n### Comparison sliders\n\nOverlay two images. Clip the top one with `clip-path: inset(0 50% 0 0)`. Adjust the right inset value based on drag position. No extra DOM elements needed, fully hardware-accelerated.\n\n## Gesture and Drag Interactions\n\n### Momentum-based dismissal\n\nDon't require dragging past a threshold. Calculate velocity: `Math.abs(dragDistance) / elapsedTime`. If velocity exceeds ~0.11, dismiss regardless of distance. A quick flick should be enough.\n\n```js\nconst timeTaken = new Date().getTime() - dragStartTime.current.getTime();\nconst velocity = Math.abs(swipeAmount) / timeTaken;\n\nif (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {\n  dismiss();\n}\n```\n\n### Damping at boundaries\n\nWhen a user drags past the natural boundary (e.g., dragging a drawer up when already at top), apply damping. The more they drag, the less the element moves. Things in real life don't suddenly stop; they slow down first.\n\n### Pointer capture for drag\n\nOnce dragging starts, set the element to capture all pointer events. This ensures dragging continues even if the pointer leaves the element bounds.\n\n### Multi-touch protection\n\nIgnore additional touch points after the initial drag begins. Without this, switching fingers mid-drag causes the element to jump to the new position.\n\n```js\nfunction onPress() {\n  if (isDragging) return;\n  // Start drag...\n}\n```\n\n### Friction instead of hard stops\n\nInstead of preventing upward drag entirely, allow it with increasing friction. It feels more natural than hitting an invisible wall.\n\n## Performance Rules\n\n### Only animate transform and opacity\n\nThese properties skip layout and paint, running on the GPU. Animating `padding`, `margin`, `height`, or `width` triggers all three rendering steps.\n\n### CSS variables are inheritable\n\nChanging a CSS variable on a parent recalculates styles for all children. In a drawer with many items, updating `--swipe-amount` on the container causes expensive style recalculation. Update `transform` directly on the element instead.\n\n```js\n// Bad: triggers recalc on all children\nelement.style.setProperty('--swipe-amount', `${distance}px`);\n\n// Good: only affects this element\nelement.style.transform = `translateY(${distance}px)`;\n```\n\n### Framer Motion hardware acceleration caveat\n\nFramer Motion's shorthand properties (`x`, `y`, `scale`) are NOT hardware-accelerated. They use `requestAnimationFrame` on the main thread. For hardware acceleration, use the full `transform` string:\n\n```jsx\n// NOT hardware accelerated (convenient but drops frames under load)\n<motion.div animate={{ x: 100 }} />\n\n// Hardware accelerated (stays smooth even when main thread is busy)\n<motion.div animate={{ transform: \"translateX(100px)\" }} />\n```\n\nThis matters when the browser is simultaneously loading content, running scripts, or painting. At Vercel, the dashboard tab animation used Shared Layout Animations and dropped frames during page loads. Switching to CSS animations (off main thread) fixed it.\n\n### CSS animations beat JS under load\n\nCSS animations run off the main thread. When the browser is busy loading a new page, Framer Motion animations (using `requestAnimationFrame`) drop frames. CSS animations remain smooth. Use CSS for predetermined animations; JS for dynamic, interruptible ones.\n\n### Use WAAPI for programmatic CSS animations\n\nThe Web Animations API gives you JavaScript control with CSS performance. Hardware-accelerated, interruptible, and no library needed.\n\n```js\nelement.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }], {\n  duration: 1000,\n  fill: 'forwards',\n  easing: 'cubic-bezier(0.77, 0, 0.175, 1)',\n});\n```\n\n## Accessibility\n\n### prefers-reduced-motion\n\nAnimations can cause motion sickness. Reduced motion means fewer and gentler animations, not zero. Keep opacity and color transitions that aid comprehension. Remove movement and position animations.\n\n```css\n@media (prefers-reduced-motion: reduce) {\n  .element {\n    animation: fade 0.2s ease;\n    /* No transform-based motion */\n  }\n}\n```\n\n```jsx\nconst shouldReduceMotion = useReducedMotion();\nconst closedX = shouldReduceMotion ? 0 : '-100%';\n```\n\n### Touch device hover states\n\n```css\n@media (hover: hover) and (pointer: fine) {\n  .element:hover {\n    transform: scale(1.05);\n  }\n}\n```\n\nTouch devices trigger hover on tap, causing false positives. Gate hover animations behind this media query.\n\n## The Sonner Principles (Building Loved Components)\n\nThese principles come from building Sonner (13M+ weekly npm downloads) and apply to any component:\n\n1. **Developer experience is key.** No hooks, no context, no complex setup. Insert `<Toaster />` once, call `toast()` from anywhere. The less friction to adopt, the more people will use it.\n\n2. **Good defaults matter more than options.** Ship beautiful out of the box. Most users never customize. The default easing, timing, and visual design should be excellent.\n\n3. **Naming creates identity.** \"Sonner\" (French for \"to ring\") feels more elegant than \"react-toast\". Sacrifice discoverability for memorability when appropriate.\n\n4. **Handle edge cases invisibly.** Pause toast timers when the tab is hidden. Fill gaps between stacked toasts with pseudo-elements to maintain hover state. Capture pointer events during drag. Users never notice these, and that is exactly right.\n\n5. **Use transitions, not keyframes, for dynamic UI.** Toasts are added rapidly. Keyframes restart from zero on interruption. Transitions retarget smoothly.\n\n6. **Build a great documentation site.** Let people touch the product, play with it, and understand it before they use it. Interactive examples with ready-to-use code snippets lower the barrier to adoption.\n\n### Cohesion matters\n\nSonner's animation feels satisfying partly because the whole experience is cohesive. The easing and duration fit the vibe of the library. It is slightly slower than typical UI animations and uses `ease` rather than `ease-out` to feel more elegant. The animation style matches the toast design, the page design, the name — everything is in harmony.\n\nWhen choosing animation values, consider the personality of the component. A playful component can be bouncier. A professional dashboard should be crisp and fast. Match the motion to the mood.\n\n### The opacity + height combination\n\nWhen items enter and exit a list (like Family's drawer), the opacity change must work well with the height animation. This is often trial and error. There is no formula — you adjust until it feels right.\n\n### Review your work the next day\n\nReview animations with fresh eyes. You notice imperfections the next day that you missed during development. Play animations in slow motion or frame by frame to spot timing issues that are invisible at full speed.\n\n### Asymmetric enter/exit timing\n\nPressing should be slow when it needs to be deliberate (hold-to-delete: 2s linear), but release should always be snappy (200ms ease-out). This pattern applies broadly: slow where the user is deciding, fast where the system is responding.\n\n```css\n/* Release: fast */\n.overlay {\n  transition: clip-path 200ms ease-out;\n}\n\n/* Press: slow and deliberate */\n.button:active .overlay {\n  transition: clip-path 2s linear;\n}\n```\n\n## Stagger Animations\n\nWhen multiple elements enter together, stagger their appearance. Each element animates in with a small delay after the previous one. This creates a cascading effect that feels more natural than everything appearing at once.\n\n```css\n.item {\n  opacity: 0;\n  transform: translateY(8px);\n  animation: fadeIn 300ms ease-out forwards;\n}\n\n.item:nth-child(1) {\n  animation-delay: 0ms;\n}\n.item:nth-child(2) {\n  animation-delay: 50ms;\n}\n.item:nth-child(3) {\n  animation-delay: 100ms;\n}\n.item:nth-child(4) {\n  animation-delay: 150ms;\n}\n\n@keyframes fadeIn {\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n```\n\nKeep stagger delays short (30-80ms between items). Long delays make the interface feel slow. Stagger is decorative — never block interaction while stagger animations are playing.\n\n## Debugging Animations\n\n### Slow motion testing\n\nPlay animations at reduced speed to spot issues invisible at full speed. Temporarily increase duration to 2-5x normal, or use browser DevTools animation inspector to slow playback.\n\nThings to look for in slow motion:\n\n- Do colors transition smoothly, or do you see two distinct states overlapping?\n- Does the easing feel right, or does it start/stop abruptly?\n- Is the transform-origin correct, or does the element scale from the wrong point?\n- Are multiple animated properties (opacity, transform, color) in sync?\n\n### Frame-by-frame inspection\n\nStep through animations frame by frame in Chrome DevTools (Animations panel). This reveals timing issues between coordinated properties that you cannot see at full speed.\n\n### Test on real devices\n\nFor touch interactions (drawers, swipe gestures), test on physical devices. Connect your phone via USB, visit your local dev server by IP address, and use Safari's remote devtools. The Xcode Simulator is an alternative but real hardware is better for gesture testing.\n\n## Review Checklist\n\nWhen reviewing UI code, check for:\n\n| Issue                                      | Fix                                                              |\n| ------------------------------------------ | ---------------------------------------------------------------- |\n| `transition: all`                          | Specify exact properties: `transition: transform 200ms ease-out` |\n| `scale(0)` entry animation                 | Start from `scale(0.95)` with `opacity: 0`                       |\n| `ease-in` on UI element                    | Switch to `ease-out` or custom curve                             |\n| `transform-origin: center` on popover      | Set to trigger location or use Radix/Base UI CSS variable (modals are exempt — keep centered) |\n| Animation on keyboard action               | Remove animation entirely                                        |\n| Duration > 300ms on UI element             | Reduce to 150-250ms                                              |\n| Hover animation without media query        | Add `@media (hover: hover) and (pointer: fine)`                  |\n| Keyframes on rapidly-triggered element     | Use CSS transitions for interruptibility                         |\n| Framer Motion `x`/`y` props under load     | Use `transform: \"translateX()\"` for hardware acceleration        |\n| Same enter/exit transition speed           | Make exit faster than enter (e.g., enter 2s, exit 200ms)         |\n| Elements all appear at once                | Add stagger delay (30-80ms between items)                        |\n"
  },
  {
    "path": "agents/skills/frontend-design/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "agents/skills/frontend-design/SKILL.md",
    "content": "---\nname: frontend-design\ndescription: \"Design and implement distinctive, production-ready frontend interfaces with strong aesthetic direction. Use when asked to create or restyle web pages, components, or applications (HTML/CSS/JS, React, Vue, etc.).\"\n---\n\n# Frontend Design Skill\n\nDesign and implement memorable frontend interfaces with a clear, intentional aesthetic. The output must be real, working code — not just mood boards. This skill is about **design thinking + execution**: every visual choice should be rooted in purpose and context.\n\n## When to Use\n\nUse this skill when the user wants to:\n- Create a new web page, landing page, dashboard, or app UI\n- Design or redesign frontend components or screens\n- Improve typography, layout, color, motion, or overall visual polish\n- Convert a concept or brief into a high‑fidelity, coded interface\n\n## Inputs to Gather (or Assume)\n\nBefore coding, identify:\n- **Purpose & audience**: What problem does this UI solve? Who uses it?\n- **Brand/voice**: Any reference brands, tone, or visual inspiration?\n- **Technical constraints**: Framework, library, CSS strategy, accessibility, performance\n- **Content constraints**: Required copy, assets, data, features\n\nIf the user did not provide this, ask **2–4 targeted questions**, or state reasonable assumptions in a short preface.\n\n## Design Thinking (Required)\n\nCommit to a **single, bold aesthetic direction**. Name it and execute it consistently. Examples:\n- Brutalist / raw / utilitarian\n- Editorial / magazine / typographic\n- Luxury / refined / minimal\n- Retro‑futuristic / cyber / neon\n- Art‑deco / geometric / ornamental\n- Handcrafted / organic / textured\n\n**Avoid generic AI aesthetics.** No “default” fonts, color schemes, or stock layouts.\n\nBefore writing code, define the system:\n1. **Visual direction** — one sentence that describes the vibe\n2. **Differentiator** — what should be memorable about this UI?\n3. **Typography system** — display + body fonts, scale, weight, casing\n4. **Color system** — dominant, accent, neutral; define as CSS variables\n5. **Layout strategy** — grid rhythm, spacing scale, hierarchy plan\n6. **Motion strategy** — 1–2 meaningful interaction moments\n\nIf the user wants code only, skip the explanation but still follow this internally.\n\n## Implementation Principles\n\n- **Working code**: HTML/CSS/JS or framework code that runs as‑is\n- **Semantic & accessible**: headings, labels, focus states, keyboard nav\n- **Responsive**: fluid layouts, breakpoints, responsive typography\n- **Tokenized styling**: CSS variables for colors, spacing, radii, shadows\n- **Modern layout**: prefer CSS Grid/Flex, avoid brittle positioning hacks\n\n## Aesthetic Guidelines\n\n### Typography\n- Typography should define the voice of the design\n- Avoid default fonts (Inter, Roboto, Arial, system stacks)\n- Use a **distinct display font** + a **refined body font**\n- Implement a clear hierarchy (size, weight, spacing, casing)\n\n### Color & Theme\n- Commit to a palette with a strong point‑of‑view\n- Avoid timid, overused gradients (e.g., purple‑to‑pink on white)\n- Use contrast intentionally and check legibility\n\n### Composition & Layout\n- Encourage asymmetry, scale contrast, overlap, or grid breaks\n- Use negative space deliberately (or controlled density if maximalist)\n- Create visual rhythm and hierarchy through spacing and alignment\n\n### Detail & Atmosphere\n- Add texture or depth when appropriate (noise, grain, subtle patterns)\n- Use shadows/glows only when they serve the concept\n- Consider unique borders, masks, or clip‑paths for distinct shapes\n\n### Motion & Interaction\n- Use motion sparingly but meaningfully\n- Favor one standout interaction over many tiny ones\n- Honor `prefers-reduced-motion`\n\n## Avoid\n\n- Cookie‑cutter hero + 3 card layouts\n- Generic gradients and default font choices\n- Unmotivated decorative elements\n- Overly flat, characterless component libraries\n\n## Deliverables\n\n- Provide full code with file names or component boundaries\n- Make customization easy with CSS variables or config objects\n- If assets are needed, provide inline SVGs or generative CSS patterns\n\n## Quality Checklist (Self‑validate)\n\n- Aesthetic direction is unmistakable\n- Typography feels intentional and expressive\n- Layout and spacing are consistent and purposeful\n- Color palette feels cohesive and legible\n- Interactions enhance the experience without clutter\n- Code runs as provided and is production‑ready\n\n**Remember:** a design is only as strong as its commitment. Choose a direction and execute it relentlessly.\n"
  },
  {
    "path": "agents/skills/grill-me/SKILL.md",
    "content": "---\nname: grill-me\ndescription: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions \"grill me\".\n---\n\nInterview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.\n\nAsk the questions one at a time.\n\nIf a question can be answered by exploring the codebase, explore the codebase instead.\n"
  },
  {
    "path": "agents/skills/initz/SKILL.md",
    "content": "---\nname: initz\nversion: 1.0.0\ndescription: Initialize or improve AGENTS.md\n---\n\n# AGENTS.md Initialization\n\nAnalyze this codebase and create an AGENTS.md file containing:\n\n1. Build/lint/test commands - especially for running a single test\n2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.\n\n## Guidelines\n\n- Target length: ~150 lines\n- Include Cursor rules (from `.cursor/rules/` or `.cursorrules`) if present\n- Include Copilot rules (from `.github/copilot-instructions.md`) if present\n- If there's already an AGENTS.md at `${path}`, improve it\n\n## Progressive Disclosure Approach\n\n1. **Find contradictions**: Identify conflicting instructions and ask which to keep\n2. **Identify the essentials**: Extract what belongs in root AGENTS.md:\n   - One-sentence project description\n   - Package manager (if not npm)\n   - Non-standard build/typecheck commands\n   - Anything truly relevant to every single task\n3. **Group the rest**: Organize remaining instructions into logical categories (TypeScript conventions, testing patterns, API design, Git workflow)\n4. **Create the file structure**:\n   - Minimal root AGENTS.md with markdown links to separate files\n   - Each separate file with relevant instructions\n   - Suggested docs/ folder structure\n5. **Flag for deletion**: Identify redundant, vague, or overly obvious instructions\n\n$ARGUMENTS\n"
  },
  {
    "path": "agents/skills/jj/SKILL.md",
    "content": "---\nname: jj\ndescription: Uses the jj (Jujutsu) version control system. Use when asked about jj commands, git push/fetch workflow, or rebasing onto main for non git repo\n---\n\n# jj (Jujutsu) Workflow\n\nJujutsu is a Git-compatible VCS. This documents the user's workflow.\n\n## Aliases\n- jj tug: Move closest bookmark to @- (Advances bookmark to parent of working copy)\n- jj retrunk: Rebase onto trunk() (Rebases current branch onto latest main/master)\n- jj lg: Log recent 10 (Shows all revisions, limit 10)\n- jj compare: Compare working copy with parent (Shows changes between working copy and parent)\n\n## Key Concepts\n- `@` refers to the working copy commit\n- `@-` refers to the parent of working copy\n- `trunk()` finds the most recent main/master/trunk on remote\n- `closest_bookmark(@-)` finds the nearest bookmark ancestor\n\n## Conflict Resolution\nWhen conflicts occur after `jj retrunk`:\n1. `jj status` shows conflicted files\n2. Edit files to resolve conflicts\n3. `jj squash` or continue working - jj auto-tracks changes\n"
  },
  {
    "path": "agents/skills/pi-improvements/SKILL.md",
    "content": "---\nname: pi-improvements\ndescription: When encountering pi tool limitations, propose focused extensions that solve one problem each.\n---\n\n## Rules\n- When edit fails due to whitespace/indentation, propose line-based editing\n- When file changes between read and write are detected, propose stale-file guarding\n- When bash output is noisy, propose structured output parsing\n- When edits apply blindly, propose preview diffs\n- Each extension must solve exactly one problem\n- Prefer `~/.pi/extensions/name.ts` over folders\n\n## Extension Patterns\n\n**Edit by Line**: Replace exact-text matching with line numbers\n```typescript\nedit_line({ path: \"foo.tsx\", start: 45, end: 52, replacement: \"...\" })\n```\n\n**Edit Preview**: Intercept `edit` tool, show unified diff in TUI overlay\n\n**Bash Summarize**: Parse known tools (vp check, npm test), return structured errors\n\n**File Stale Guard**: Store mtime on `read`, warn on `write` if changed\n\n## When to Propose\n- After 2+ failed edit attempts due to formatting\n- When user manually pipes bash output through grep/head\n- When user discovers wrong edit only after running checks\n- When formatter modifies files between read and write\n"
  },
  {
    "path": "agents/skills/react-best-practices/AGENTS.md",
    "content": "# React Best Practices\n\n**Version 1.0.0**  \nVercel Engineering  \nJanuary 2026\n\n> **Note:**  \n> This document is mainly for agents and LLMs to follow when maintaining,  \n> generating, or refactoring React and Next.js codebases at Vercel. Humans  \n> may also find it useful, but guidance here is optimized for automation  \n> and consistency by AI-assisted workflows.\n\n---\n\n## Abstract\n\nComprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.\n\n---\n\n## Table of Contents\n\n1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**\n   - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)\n   - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)\n   - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)\n   - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)\n   - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)\n2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**\n   - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)\n   - 2.2 [Conditional Module Loading](#22-conditional-module-loading)\n   - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)\n   - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)\n   - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)\n3. [Server-Side Performance](#3-server-side-performance) — **HIGH**\n   - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)\n   - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)\n   - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)\n   - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)\n   - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)\n   - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)\n   - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)\n4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**\n   - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)\n   - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)\n   - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)\n   - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)\n5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**\n   - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)\n   - 5.2 [Do not wrap a simple expression with a primitive result type in useMemo](#52-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)\n   - 5.3 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#53-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)\n   - 5.4 [Extract to Memoized Components](#54-extract-to-memoized-components)\n   - 5.5 [Narrow Effect Dependencies](#55-narrow-effect-dependencies)\n   - 5.6 [Subscribe to Derived State](#56-subscribe-to-derived-state)\n   - 5.7 [Use Functional setState Updates](#57-use-functional-setstate-updates)\n   - 5.8 [Use Lazy State Initialization](#58-use-lazy-state-initialization)\n   - 5.9 [Use Transitions for Non-Urgent Updates](#59-use-transitions-for-non-urgent-updates)\n6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**\n   - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)\n   - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)\n   - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)\n   - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)\n   - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)\n   - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)\n   - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)\n   - 6.8 [Use useTransition Over Manual Loading States](#68-use-usetransition-over-manual-loading-states)\n7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**\n   - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)\n   - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)\n   - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)\n   - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)\n   - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)\n   - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)\n   - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)\n   - 7.8 [Early Return from Functions](#78-early-return-from-functions)\n   - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)\n   - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort)\n   - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)\n   - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)\n8. [Advanced Patterns](#8-advanced-patterns) — **LOW**\n   - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)\n   - 8.2 [useEffectEvent for Stable Callback Refs](#82-useeffectevent-for-stable-callback-refs)\n\n---\n\n## 1. Eliminating Waterfalls\n\n**Impact: CRITICAL**\n\nWaterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.\n\n### 1.1 Defer Await Until Needed\n\n**Impact: HIGH (avoids blocking unused code paths)**\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect: blocks both branches**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct: only blocks when needed**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example: early return optimization**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n\n### 1.2 Dependency-Based Parallelization\n\n**Impact: CRITICAL (2-10× improvement)**\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect: profile waits for config unnecessarily**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct: config and profile run in parallel**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\n**Alternative without extra dependencies:**\n\n```typescript\nconst userPromise = fetchUser()\nconst profilePromise = userPromise.then(user => fetchProfile(user.id))\n\nconst [user, config, profile] = await Promise.all([\n  userPromise,\n  fetchConfig(),\n  profilePromise\n])\n```\n\nWe can also create all the promises first, and do `Promise.all()` at the end.\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n\n### 1.3 Prevent Waterfall Chains in API Routes\n\n**Impact: CRITICAL (2-10× improvement)**\n\nIn API routes and Server Actions, start independent operations immediately, even if you don't await them yet.\n\n**Incorrect: config waits for auth, data waits for both**\n\n```typescript\nexport async function GET(request: Request) {\n  const session = await auth()\n  const config = await fetchConfig()\n  const data = await fetchData(session.user.id)\n  return Response.json({ data, config })\n}\n```\n\n**Correct: auth and config start immediately**\n\n```typescript\nexport async function GET(request: Request) {\n  const sessionPromise = auth()\n  const configPromise = fetchConfig()\n  const session = await sessionPromise\n  const [config, data] = await Promise.all([\n    configPromise,\n    fetchData(session.user.id)\n  ])\n  return Response.json({ data, config })\n}\n```\n\nFor operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).\n\n### 1.4 Promise.all() for Independent Operations\n\n**Impact: CRITICAL (2-10× improvement)**\n\nWhen async operations have no interdependencies, execute them concurrently using `Promise.all()`.\n\n**Incorrect: sequential execution, 3 round trips**\n\n```typescript\nconst user = await fetchUser()\nconst posts = await fetchPosts()\nconst comments = await fetchComments()\n```\n\n**Correct: parallel execution, 1 round trip**\n\n```typescript\nconst [user, posts, comments] = await Promise.all([\n  fetchUser(),\n  fetchPosts(),\n  fetchComments()\n])\n```\n\n### 1.5 Strategic Suspense Boundaries\n\n**Impact: HIGH (faster initial paint)**\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect: wrapper blocked by data fetching**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct: wrapper shows immediately, data streams in**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative: share promise across components**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n\n- SEO-critical content above the fold\n\n- Small, fast queries where suspense overhead isn't worth it\n\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n\n---\n\n## 2. Bundle Size Optimization\n\n**Impact: CRITICAL**\n\nReducing initial bundle size improves Time to Interactive and Largest Contentful Paint.\n\n### 2.1 Avoid Barrel File Imports\n\n**Impact: CRITICAL (200-800ms import cost, slow builds)**\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect: imports entire library**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct: imports only what you need**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\n**Alternative: Next.js 13.5+**\n\n```js\n// next.config.js - use optimizePackageImports\nmodule.exports = {\n  experimental: {\n    optimizePackageImports: ['lucide-react', '@mui/material']\n  }\n}\n\n// Then you can keep the ergonomic barrel imports:\nimport { Check, X, Menu } from 'lucide-react'\n// Automatically transformed to direct imports at build time\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n\n### 2.2 Conditional Module Loading\n\n**Impact: HIGH (loads large data only when needed)**\n\nLoad large data or modules only when a feature is activated.\n\n**Example: lazy-load animation frames**\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames && typeof window !== 'undefined') {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.\n\n### 2.3 Defer Non-Critical Third-Party Libraries\n\n**Impact: MEDIUM (loads after hydration)**\n\nAnalytics, logging, and error tracking don't block user interaction. Load them after hydration.\n\n**Incorrect: blocks initial bundle**\n\n```tsx\nimport { Analytics } from '@vercel/analytics/react'\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <body>\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  )\n}\n```\n\n**Correct: loads after hydration**\n\n```tsx\nimport dynamic from 'next/dynamic'\n\nconst Analytics = dynamic(\n  () => import('@vercel/analytics/react').then(m => m.Analytics),\n  { ssr: false }\n)\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <body>\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  )\n}\n```\n\n### 2.4 Dynamic Imports for Heavy Components\n\n**Impact: CRITICAL (directly affects TTI and LCP)**\n\nUse `next/dynamic` to lazy-load large components not needed on initial render.\n\n**Incorrect: Monaco bundles with main chunk ~300KB**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n\nfunction CodePanel({ code }: { code: string }) {\n  return <MonacoEditor value={code} />\n}\n```\n\n**Correct: Monaco loads on demand**\n\n```tsx\nimport dynamic from 'next/dynamic'\n\nconst MonacoEditor = dynamic(\n  () => import('./monaco-editor').then(m => m.MonacoEditor),\n  { ssr: false }\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return <MonacoEditor value={code} />\n}\n```\n\n### 2.5 Preload Based on User Intent\n\n**Impact: MEDIUM (reduces perceived latency)**\n\nPreload heavy bundles before they're needed to reduce perceived latency.\n\n**Example: preload on hover/focus**\n\n```tsx\nfunction EditorButton({ onClick }: { onClick: () => void }) {\n  const preload = () => {\n    if (typeof window !== 'undefined') {\n      void import('./monaco-editor')\n    }\n  }\n\n  return (\n    <button\n      onMouseEnter={preload}\n      onFocus={preload}\n      onClick={onClick}\n    >\n      Open Editor\n    </button>\n  )\n}\n```\n\n**Example: preload when feature flag is enabled**\n\n```tsx\nfunction FlagsProvider({ children, flags }: Props) {\n  useEffect(() => {\n    if (flags.editorEnabled && typeof window !== 'undefined') {\n      void import('./monaco-editor').then(mod => mod.init())\n    }\n  }, [flags.editorEnabled])\n\n  return <FlagsContext.Provider value={flags}>\n    {children}\n  </FlagsContext.Provider>\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.\n\n---\n\n## 3. Server-Side Performance\n\n**Impact: HIGH**\n\nOptimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.\n\n### 3.1 Authenticate Server Actions Like API Routes\n\n**Impact: CRITICAL (prevents unauthorized access to server mutations)**\n\nServer Actions (functions with `\"use server\"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.\n\nNext.js documentation explicitly states: \"Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation.\"\n\n**Incorrect: no authentication check**\n\n```typescript\n'use server'\n\nexport async function deleteUser(userId: string) {\n  // Anyone can call this! No auth check\n  await db.user.delete({ where: { id: userId } })\n  return { success: true }\n}\n```\n\n**Correct: authentication inside the action**\n\n```typescript\n'use server'\n\nimport { verifySession } from '@/lib/auth'\nimport { unauthorized } from '@/lib/errors'\n\nexport async function deleteUser(userId: string) {\n  // Always check auth inside the action\n  const session = await verifySession()\n  \n  if (!session) {\n    throw unauthorized('Must be logged in')\n  }\n  \n  // Check authorization too\n  if (session.user.role !== 'admin' && session.user.id !== userId) {\n    throw unauthorized('Cannot delete other users')\n  }\n  \n  await db.user.delete({ where: { id: userId } })\n  return { success: true }\n}\n```\n\n**With input validation:**\n\n```typescript\n'use server'\n\nimport { verifySession } from '@/lib/auth'\nimport { z } from 'zod'\n\nconst updateProfileSchema = z.object({\n  userId: z.string().uuid(),\n  name: z.string().min(1).max(100),\n  email: z.string().email()\n})\n\nexport async function updateProfile(data: unknown) {\n  // Validate input first\n  const validated = updateProfileSchema.parse(data)\n  \n  // Then authenticate\n  const session = await verifySession()\n  if (!session) {\n    throw new Error('Unauthorized')\n  }\n  \n  // Then authorize\n  if (session.user.id !== validated.userId) {\n    throw new Error('Can only update own profile')\n  }\n  \n  // Finally perform the mutation\n  await db.user.update({\n    where: { id: validated.userId },\n    data: {\n      name: validated.name,\n      email: validated.email\n    }\n  })\n  \n  return { success: true }\n}\n```\n\nReference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)\n\n### 3.2 Avoid Duplicate Serialization in RSC Props\n\n**Impact: LOW (reduces network payload by avoiding duplicate serialization)**\n\nRSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.\n\n**Incorrect: duplicates array**\n\n```tsx\n// RSC: sends 6 strings (2 arrays × 3 items)\n<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />\n```\n\n**Correct: sends 3 strings**\n\n```tsx\n// RSC: send once\n<ClientList usernames={usernames} />\n\n// Client: transform there\n'use client'\nconst sorted = useMemo(() => [...usernames].sort(), [usernames])\n```\n\n**Nested deduplication behavior:**\n\n```tsx\n// string[] - duplicates everything\nusernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings\n\n// object[] - duplicates array structure only\nusers={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)\n```\n\nDeduplication works recursively. Impact varies by data type:\n\n- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated\n\n- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference\n\n**Operations breaking deduplication: create new references**\n\n- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`\n\n- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`\n\n**More examples:**\n\n```tsx\n// ❌ Bad\n<C users={users} active={users.filter(u => u.active)} />\n<C product={product} productName={product.name} />\n\n// ✅ Good\n<C users={users} />\n<C product={product} />\n// Do filtering/destructuring in client\n```\n\n**Exception:** Pass derived data when transformation is expensive or client doesn't need original.\n\n### 3.3 Cross-Request LRU Caching\n\n**Impact: HIGH (caches across requests)**\n\n`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.\n\n**Implementation:**\n\n```typescript\nimport { LRUCache } from 'lru-cache'\n\nconst cache = new LRUCache<string, any>({\n  max: 1000,\n  ttl: 5 * 60 * 1000  // 5 minutes\n})\n\nexport async function getUser(id: string) {\n  const cached = cache.get(id)\n  if (cached) return cached\n\n  const user = await db.user.findUnique({ where: { id } })\n  cache.set(id, user)\n  return user\n}\n\n// Request 1: DB query, result cached\n// Request 2: cache hit, no DB query\n```\n\nUse when sequential user actions hit multiple endpoints needing the same data within seconds.\n\n**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.\n\n**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.\n\nReference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)\n\n### 3.4 Minimize Serialization at RSC Boundaries\n\n**Impact: HIGH (reduces data transfer size)**\n\nThe React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.\n\n**Incorrect: serializes all 50 fields**\n\n```tsx\nasync function Page() {\n  const user = await fetchUser()  // 50 fields\n  return <Profile user={user} />\n}\n\n'use client'\nfunction Profile({ user }: { user: User }) {\n  return <div>{user.name}</div>  // uses 1 field\n}\n```\n\n**Correct: serializes only 1 field**\n\n```tsx\nasync function Page() {\n  const user = await fetchUser()\n  return <Profile name={user.name} />\n}\n\n'use client'\nfunction Profile({ name }: { name: string }) {\n  return <div>{name}</div>\n}\n```\n\n### 3.5 Parallel Data Fetching with Component Composition\n\n**Impact: CRITICAL (eliminates server-side waterfalls)**\n\nReact Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.\n\n**Incorrect: Sidebar waits for Page's fetch to complete**\n\n```tsx\nexport default async function Page() {\n  const header = await fetchHeader()\n  return (\n    <div>\n      <div>{header}</div>\n      <Sidebar />\n    </div>\n  )\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n```\n\n**Correct: both fetch simultaneously**\n\n```tsx\nasync function Header() {\n  const data = await fetchHeader()\n  return <div>{data}</div>\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n\nexport default function Page() {\n  return (\n    <div>\n      <Header />\n      <Sidebar />\n    </div>\n  )\n}\n```\n\n**Alternative with children prop:**\n\n```tsx\nasync function Header() {\n  const data = await fetchHeader()\n  return <div>{data}</div>\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n\nfunction Layout({ children }: { children: ReactNode }) {\n  return (\n    <div>\n      <Header />\n      {children}\n    </div>\n  )\n}\n\nexport default function Page() {\n  return (\n    <Layout>\n      <Sidebar />\n    </Layout>\n  )\n}\n```\n\n### 3.6 Per-Request Deduplication with React.cache()\n\n**Impact: MEDIUM (deduplicates within request)**\n\nUse `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.\n\n**Usage:**\n\n```typescript\nimport { cache } from 'react'\n\nexport const getCurrentUser = cache(async () => {\n  const session = await auth()\n  if (!session?.user?.id) return null\n  return await db.user.findUnique({\n    where: { id: session.user.id }\n  })\n})\n```\n\nWithin a single request, multiple calls to `getCurrentUser()` execute the query only once.\n\n**Avoid inline objects as arguments:**\n\n`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.\n\n**Incorrect: always cache miss**\n\n```typescript\nconst getUser = cache(async (params: { uid: number }) => {\n  return await db.user.findUnique({ where: { id: params.uid } })\n})\n\n// Each call creates new object, never hits cache\ngetUser({ uid: 1 })\ngetUser({ uid: 1 })  // Cache miss, runs query again\n```\n\n**Correct: cache hit**\n\n```typescript\nconst params = { uid: 1 }\ngetUser(params)  // Query runs\ngetUser(params)  // Cache hit (same reference)\n```\n\nIf you must pass objects, pass the same reference:\n\n**Next.js-Specific Note:**\n\nIn Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:\n\n- Database queries (Prisma, Drizzle, etc.)\n\n- Heavy computations\n\n- Authentication checks\n\n- File system operations\n\n- Any non-fetch async work\n\nUse `React.cache()` to deduplicate these operations across your component tree.\n\nReference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)\n\n### 3.7 Use after() for Non-Blocking Operations\n\n**Impact: MEDIUM (faster response times)**\n\nUse Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.\n\n**Incorrect: blocks response**\n\n```tsx\nimport { logUserAction } from '@/app/utils'\n\nexport async function POST(request: Request) {\n  // Perform mutation\n  await updateDatabase(request)\n  \n  // Logging blocks the response\n  const userAgent = request.headers.get('user-agent') || 'unknown'\n  await logUserAction({ userAgent })\n  \n  return new Response(JSON.stringify({ status: 'success' }), {\n    status: 200,\n    headers: { 'Content-Type': 'application/json' }\n  })\n}\n```\n\n**Correct: non-blocking**\n\n```tsx\nimport { after } from 'next/server'\nimport { headers, cookies } from 'next/headers'\nimport { logUserAction } from '@/app/utils'\n\nexport async function POST(request: Request) {\n  // Perform mutation\n  await updateDatabase(request)\n  \n  // Log after response is sent\n  after(async () => {\n    const userAgent = (await headers()).get('user-agent') || 'unknown'\n    const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'\n    \n    logUserAction({ sessionCookie, userAgent })\n  })\n  \n  return new Response(JSON.stringify({ status: 'success' }), {\n    status: 200,\n    headers: { 'Content-Type': 'application/json' }\n  })\n}\n```\n\nThe response is sent immediately while logging happens in the background.\n\n**Common use cases:**\n\n- Analytics tracking\n\n- Audit logging\n\n- Sending notifications\n\n- Cache invalidation\n\n- Cleanup tasks\n\n**Important notes:**\n\n- `after()` runs even if the response fails or redirects\n\n- Works in Server Actions, Route Handlers, and Server Components\n\nReference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)\n\n---\n\n## 4. Client-Side Data Fetching\n\n**Impact: MEDIUM-HIGH**\n\nAutomatic deduplication and efficient data fetching patterns reduce redundant network requests.\n\n### 4.1 Deduplicate Global Event Listeners\n\n**Impact: LOW (single listener for N components)**\n\nUse `useSWRSubscription()` to share global event listeners across component instances.\n\n**Incorrect: N instances = N listeners**\n\n```tsx\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  useEffect(() => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && e.key === key) {\n        callback()\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  }, [key, callback])\n}\n```\n\nWhen using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.\n\n**Correct: N instances = 1 listener**\n\n```tsx\nimport useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map<string, Set<() => void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  // Register this callback in the Map\n  useEffect(() => {\n    if (!keyCallbacks.has(key)) {\n      keyCallbacks.set(key, new Set())\n    }\n    keyCallbacks.get(key)!.add(callback)\n\n    return () => {\n      const set = keyCallbacks.get(key)\n      if (set) {\n        set.delete(callback)\n        if (set.size === 0) {\n          keyCallbacks.delete(key)\n        }\n      }\n    }\n  }, [key, callback])\n\n  useSWRSubscription('global-keydown', () => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && keyCallbacks.has(e.key)) {\n        keyCallbacks.get(e.key)!.forEach(cb => cb())\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  })\n}\n\nfunction Profile() {\n  // Multiple shortcuts will share the same listener\n  useKeyboardShortcut('p', () => { /* ... */ }) \n  useKeyboardShortcut('k', () => { /* ... */ })\n  // ...\n}\n```\n\n### 4.2 Use Passive Event Listeners for Scrolling Performance\n\n**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**\n\nAdd `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.\n\n**Incorrect:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch)\n  document.addEventListener('wheel', handleWheel)\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Correct:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch, { passive: true })\n  document.addEventListener('wheel', handleWheel, { passive: true })\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.\n\n**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.\n\n### 4.3 Use SWR for Automatic Deduplication\n\n**Impact: MEDIUM-HIGH (automatic deduplication)**\n\nSWR enables request deduplication, caching, and revalidation across component instances.\n\n**Incorrect: no deduplication, each instance fetches**\n\n```tsx\nfunction UserList() {\n  const [users, setUsers] = useState([])\n  useEffect(() => {\n    fetch('/api/users')\n      .then(r => r.json())\n      .then(setUsers)\n  }, [])\n}\n```\n\n**Correct: multiple instances share one request**\n\n```tsx\nimport useSWR from 'swr'\n\nfunction UserList() {\n  const { data: users } = useSWR('/api/users', fetcher)\n}\n```\n\n**For immutable data:**\n\n```tsx\nimport { useImmutableSWR } from '@/lib/swr'\n\nfunction StaticContent() {\n  const { data } = useImmutableSWR('/api/config', fetcher)\n}\n```\n\n**For mutations:**\n\n```tsx\nimport { useSWRMutation } from 'swr/mutation'\n\nfunction UpdateButton() {\n  const { trigger } = useSWRMutation('/api/user', updateUser)\n  return <button onClick={() => trigger()}>Update</button>\n}\n```\n\nReference: [https://swr.vercel.app](https://swr.vercel.app)\n\n### 4.4 Version and Minimize localStorage Data\n\n**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**\n\nAdd version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.\n\n**Incorrect:**\n\n```typescript\n// No version, stores everything, no error handling\nlocalStorage.setItem('userConfig', JSON.stringify(fullUserObject))\nconst data = localStorage.getItem('userConfig')\n```\n\n**Correct:**\n\n```typescript\nconst VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n  try {\n    localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n  } catch {\n    // Throws in incognito/private browsing, quota exceeded, or disabled\n  }\n}\n\nfunction loadConfig() {\n  try {\n    const data = localStorage.getItem(`userConfig:${VERSION}`)\n    return data ? JSON.parse(data) : null\n  } catch {\n    return null\n  }\n}\n\n// Migration from v1 to v2\nfunction migrate() {\n  try {\n    const v1 = localStorage.getItem('userConfig:v1')\n    if (v1) {\n      const old = JSON.parse(v1)\n      saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })\n      localStorage.removeItem('userConfig:v1')\n    }\n  } catch {}\n}\n```\n\n**Store minimal fields from server responses:**\n\n```typescript\n// User object has 20+ fields, only store what UI needs\nfunction cachePrefs(user: FullUser) {\n  try {\n    localStorage.setItem('prefs:v1', JSON.stringify({\n      theme: user.preferences.theme,\n      notifications: user.preferences.notifications\n    }))\n  } catch {}\n}\n```\n\n**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.\n\n**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.\n\n---\n\n## 5. Re-render Optimization\n\n**Impact: MEDIUM**\n\nReducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.\n\n### 5.1 Defer State Reads to Usage Point\n\n**Impact: MEDIUM (avoids unnecessary subscriptions)**\n\nDon't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.\n\n**Incorrect: subscribes to all searchParams changes**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const searchParams = useSearchParams()\n\n  const handleShare = () => {\n    const ref = searchParams.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n**Correct: reads on demand, no subscription**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const handleShare = () => {\n    const params = new URLSearchParams(window.location.search)\n    const ref = params.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n### 5.2 Do not wrap a simple expression with a primitive result type in useMemo\n\n**Impact: LOW-MEDIUM (wasted computation on every render)**\n\nWhen an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.\n\nCalling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.\n\n**Incorrect:**\n\n```tsx\nfunction Header({ user, notifications }: Props) {\n  const isLoading = useMemo(() => {\n    return user.isLoading || notifications.isLoading\n  }, [user.isLoading, notifications.isLoading])\n\n  if (isLoading) return <Skeleton />\n  // return some markup\n}\n```\n\n**Correct:**\n\n```tsx\nfunction Header({ user, notifications }: Props) {\n  const isLoading = user.isLoading || notifications.isLoading\n\n  if (isLoading) return <Skeleton />\n  // return some markup\n}\n```\n\n### 5.3 Extract Default Non-primitive Parameter Value from Memoized Component to Constant\n\n**Impact: MEDIUM (restores memoization by using a constant for default value)**\n\nWhen memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.\n\nTo address this issue, extract the default value into a constant.\n\n**Incorrect: `onClick` has different values on every rerender**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {\n  // ...\n})\n\n// Used without optional onClick\n<UserAvatar />\n```\n\n**Correct: stable default value**\n\n```tsx\nconst NOOP = () => {};\n\nconst UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {\n  // ...\n})\n\n// Used without optional onClick\n<UserAvatar />\n```\n\n### 5.4 Extract to Memoized Components\n\n**Impact: MEDIUM (enables early returns)**\n\nExtract expensive work into memoized components to enable early returns before computation.\n\n**Incorrect: computes avatar even when loading**\n\n```tsx\nfunction Profile({ user, loading }: Props) {\n  const avatar = useMemo(() => {\n    const id = computeAvatarId(user)\n    return <Avatar id={id} />\n  }, [user])\n\n  if (loading) return <Skeleton />\n  return <div>{avatar}</div>\n}\n```\n\n**Correct: skips computation when loading**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ user }: { user: User }) {\n  const id = useMemo(() => computeAvatarId(user), [user])\n  return <Avatar id={id} />\n})\n\nfunction Profile({ user, loading }: Props) {\n  if (loading) return <Skeleton />\n  return (\n    <div>\n      <UserAvatar user={user} />\n    </div>\n  )\n}\n```\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.\n\n### 5.5 Narrow Effect Dependencies\n\n**Impact: LOW (minimizes effect re-runs)**\n\nSpecify primitive dependencies instead of objects to minimize effect re-runs.\n\n**Incorrect: re-runs on any user field change**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user])\n```\n\n**Correct: re-runs only when id changes**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user.id])\n```\n\n**For derived state, compute outside effect:**\n\n```tsx\n// Incorrect: runs on width=767, 766, 765...\nuseEffect(() => {\n  if (width < 768) {\n    enableMobileMode()\n  }\n}, [width])\n\n// Correct: runs only on boolean transition\nconst isMobile = width < 768\nuseEffect(() => {\n  if (isMobile) {\n    enableMobileMode()\n  }\n}, [isMobile])\n```\n\n### 5.6 Subscribe to Derived State\n\n**Impact: MEDIUM (reduces re-render frequency)**\n\nSubscribe to derived boolean state instead of continuous values to reduce re-render frequency.\n\n**Incorrect: re-renders on every pixel change**\n\n```tsx\nfunction Sidebar() {\n  const width = useWindowWidth()  // updates continuously\n  const isMobile = width < 768\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n**Correct: re-renders only when boolean changes**\n\n```tsx\nfunction Sidebar() {\n  const isMobile = useMediaQuery('(max-width: 767px)')\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n### 5.7 Use Functional setState Updates\n\n**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**\n\nWhen updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.\n\n**Incorrect: requires state as dependency**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Callback must depend on items, recreated on every items change\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems([...items, ...newItems])\n  }, [items])  // ❌ items dependency causes recreations\n  \n  // Risk of stale closure if dependency is forgotten\n  const removeItem = useCallback((id: string) => {\n    setItems(items.filter(item => item.id !== id))\n  }, [])  // ❌ Missing items dependency - will use stale items!\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\nThe first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.\n\n**Correct: stable callbacks, no stale closures**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Stable callback, never recreated\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems(curr => [...curr, ...newItems])\n  }, [])  // ✅ No dependencies needed\n  \n  // Always uses latest state, no stale closure risk\n  const removeItem = useCallback((id: string) => {\n    setItems(curr => curr.filter(item => item.id !== id))\n  }, [])  // ✅ Safe and stable\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\n**Benefits:**\n\n1. **Stable callback references** - Callbacks don't need to be recreated when state changes\n\n2. **No stale closures** - Always operates on the latest state value\n\n3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks\n\n4. **Prevents bugs** - Eliminates the most common source of React closure bugs\n\n**When to use functional updates:**\n\n- Any setState that depends on the current state value\n\n- Inside useCallback/useMemo when state is needed\n\n- Event handlers that reference state\n\n- Async operations that update state\n\n**When direct updates are fine:**\n\n- Setting state to a static value: `setCount(0)`\n\n- Setting state from props/arguments only: `setName(newName)`\n\n- State doesn't depend on previous value\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.\n\n### 5.8 Use Lazy State Initialization\n\n**Impact: MEDIUM (wasted computation on every render)**\n\nPass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.\n\n**Incorrect: runs on every render**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs on EVERY render, even after initialization\n  const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  // When query changes, buildSearchIndex runs again unnecessarily\n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs on every render\n  const [settings, setSettings] = useState(\n    JSON.parse(localStorage.getItem('settings') || '{}')\n  )\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\n**Correct: runs only once**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs ONLY on initial render\n  const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs only on initial render\n  const [settings, setSettings] = useState(() => {\n    const stored = localStorage.getItem('settings')\n    return stored ? JSON.parse(stored) : {}\n  })\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\nUse lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.\n\nFor simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.\n\n### 5.9 Use Transitions for Non-Urgent Updates\n\n**Impact: MEDIUM (maintains UI responsiveness)**\n\nMark frequent, non-urgent state updates as transitions to maintain UI responsiveness.\n\n**Incorrect: blocks UI on every scroll**\n\n```tsx\nfunction ScrollTracker() {\n  const [scrollY, setScrollY] = useState(0)\n  useEffect(() => {\n    const handler = () => setScrollY(window.scrollY)\n    window.addEventListener('scroll', handler, { passive: true })\n    return () => window.removeEventListener('scroll', handler)\n  }, [])\n}\n```\n\n**Correct: non-blocking updates**\n\n```tsx\nimport { startTransition } from 'react'\n\nfunction ScrollTracker() {\n  const [scrollY, setScrollY] = useState(0)\n  useEffect(() => {\n    const handler = () => {\n      startTransition(() => setScrollY(window.scrollY))\n    }\n    window.addEventListener('scroll', handler, { passive: true })\n    return () => window.removeEventListener('scroll', handler)\n  }, [])\n}\n```\n\n---\n\n## 6. Rendering Performance\n\n**Impact: MEDIUM**\n\nOptimizing the rendering process reduces the work the browser needs to do.\n\n### 6.1 Animate SVG Wrapper Instead of SVG Element\n\n**Impact: LOW (enables hardware acceleration)**\n\nMany browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.\n\n**Incorrect: animating SVG directly - no hardware acceleration**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <svg \n      className=\"animate-spin\"\n      width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n    </svg>\n  )\n}\n```\n\n**Correct: animating wrapper div - hardware accelerated**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <div className=\"animate-spin\">\n      <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n      </svg>\n    </div>\n  )\n}\n```\n\nThis applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.\n\n### 6.2 CSS content-visibility for Long Lists\n\n**Impact: HIGH (faster initial render)**\n\nApply `content-visibility: auto` to defer off-screen rendering.\n\n**CSS:**\n\n```css\n.message-item {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 80px;\n}\n```\n\n**Example:**\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    <div className=\"overflow-y-auto h-screen\">\n      {messages.map(msg => (\n        <div key={msg.id} className=\"message-item\">\n          <Avatar user={msg.author} />\n          <div>{msg.content}</div>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nFor 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).\n\n### 6.3 Hoist Static JSX Elements\n\n**Impact: LOW (avoids re-creation)**\n\nExtract static JSX outside components to avoid re-creation.\n\n**Incorrect: recreates element every render**\n\n```tsx\nfunction LoadingSkeleton() {\n  return <div className=\"animate-pulse h-20 bg-gray-200\" />\n}\n\nfunction Container() {\n  return (\n    <div>\n      {loading && <LoadingSkeleton />}\n    </div>\n  )\n}\n```\n\n**Correct: reuses same element**\n\n```tsx\nconst loadingSkeleton = (\n  <div className=\"animate-pulse h-20 bg-gray-200\" />\n)\n\nfunction Container() {\n  return (\n    <div>\n      {loading && loadingSkeleton}\n    </div>\n  )\n}\n```\n\nThis is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.\n\n### 6.4 Optimize SVG Precision\n\n**Impact: LOW (reduces file size)**\n\nReduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.\n\n**Incorrect: excessive precision**\n\n```svg\n<path d=\"M 10.293847 20.847362 L 30.938472 40.192837\" />\n```\n\n**Correct: 1 decimal place**\n\n```svg\n<path d=\"M 10.3 20.8 L 30.9 40.2\" />\n```\n\n**Automate with SVGO:**\n\n```bash\nnpx svgo --precision=1 --multipass icon.svg\n```\n\n### 6.5 Prevent Hydration Mismatch Without Flickering\n\n**Impact: MEDIUM (avoids visual flicker and hydration errors)**\n\nWhen rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.\n\n**Incorrect: breaks SSR**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  // localStorage is not available on server - throws error\n  const theme = localStorage.getItem('theme') || 'light'\n  \n  return (\n    <div className={theme}>\n      {children}\n    </div>\n  )\n}\n```\n\nServer-side rendering will fail because `localStorage` is undefined.\n\n**Incorrect: visual flickering**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  const [theme, setTheme] = useState('light')\n  \n  useEffect(() => {\n    // Runs after hydration - causes visible flash\n    const stored = localStorage.getItem('theme')\n    if (stored) {\n      setTheme(stored)\n    }\n  }, [])\n  \n  return (\n    <div className={theme}>\n      {children}\n    </div>\n  )\n}\n```\n\nComponent first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.\n\n**Correct: no flicker, no hydration mismatch**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  return (\n    <>\n      <div id=\"theme-wrapper\">\n        {children}\n      </div>\n      <script\n        dangerouslySetInnerHTML={{\n          __html: `\n            (function() {\n              try {\n                var theme = localStorage.getItem('theme') || 'light';\n                var el = document.getElementById('theme-wrapper');\n                if (el) el.className = theme;\n              } catch (e) {}\n            })();\n          `,\n        }}\n      />\n    </>\n  )\n}\n```\n\nThe inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.\n\nThis pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.\n\n### 6.6 Use Activity Component for Show/Hide\n\n**Impact: MEDIUM (preserves state/DOM)**\n\nUse React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.\n\n**Usage:**\n\n```tsx\nimport { Activity } from 'react'\n\nfunction Dropdown({ isOpen }: Props) {\n  return (\n    <Activity mode={isOpen ? 'visible' : 'hidden'}>\n      <ExpensiveMenu />\n    </Activity>\n  )\n}\n```\n\nAvoids expensive re-renders and state loss.\n\n### 6.7 Use Explicit Conditional Rendering\n\n**Impact: LOW (prevents rendering 0 or NaN)**\n\nUse explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.\n\n**Incorrect: renders \"0\" when count is 0**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count && <span className=\"badge\">{count}</span>}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div>0</div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n**Correct: renders nothing when count is 0**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count > 0 ? <span className=\"badge\">{count}</span> : null}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div></div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n### 6.8 Use useTransition Over Manual Loading States\n\n**Impact: LOW (reduces re-renders and improves code clarity)**\n\nUse `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.\n\n**Incorrect: manual loading state**\n\n```tsx\nfunction SearchResults() {\n  const [query, setQuery] = useState('')\n  const [results, setResults] = useState([])\n  const [isLoading, setIsLoading] = useState(false)\n\n  const handleSearch = async (value: string) => {\n    setIsLoading(true)\n    setQuery(value)\n    const data = await fetchResults(value)\n    setResults(data)\n    setIsLoading(false)\n  }\n\n  return (\n    <>\n      <input onChange={(e) => handleSearch(e.target.value)} />\n      {isLoading && <Spinner />}\n      <ResultsList results={results} />\n    </>\n  )\n}\n```\n\n**Correct: useTransition with built-in pending state**\n\n```tsx\nimport { useTransition, useState } from 'react'\n\nfunction SearchResults() {\n  const [query, setQuery] = useState('')\n  const [results, setResults] = useState([])\n  const [isPending, startTransition] = useTransition()\n\n  const handleSearch = (value: string) => {\n    setQuery(value) // Update input immediately\n    \n    startTransition(async () => {\n      // Fetch and update results\n      const data = await fetchResults(value)\n      setResults(data)\n    })\n  }\n\n  return (\n    <>\n      <input onChange={(e) => handleSearch(e.target.value)} />\n      {isPending && <Spinner />}\n      <ResultsList results={results} />\n    </>\n  )\n}\n```\n\n**Benefits:**\n\n- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`\n\n- **Error resilience**: Pending state correctly resets even if the transition throws\n\n- **Better responsiveness**: Keeps the UI responsive during updates\n\n- **Interrupt handling**: New transitions automatically cancel pending ones\n\nReference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)\n\n---\n\n## 7. JavaScript Performance\n\n**Impact: LOW-MEDIUM**\n\nMicro-optimizations for hot paths can add up to meaningful improvements.\n\n### 7.1 Avoid Layout Thrashing\n\n**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**\n\nAvoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.\n\n**This is OK: browser batches style changes**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Each line invalidates style, but browser batches the recalculation\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n}\n```\n\n**Incorrect: interleaved reads and writes force reflows**\n\n```typescript\nfunction layoutThrashing(element: HTMLElement) {\n  element.style.width = '100px'\n  const width = element.offsetWidth  // Forces reflow\n  element.style.height = '200px'\n  const height = element.offsetHeight  // Forces another reflow\n}\n```\n\n**Correct: batch writes, then read once**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Batch all writes together\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n  \n  // Read after all writes are done (single reflow)\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Correct: batch reads, then writes**\n\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.classList.add('highlighted-box')\n  \n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Better: use CSS classes**\n\n**React example:**\n\n```tsx\n// Incorrect: interleaving style changes with layout queries\nfunction Box({ isHighlighted }: { isHighlighted: boolean }) {\n  const ref = useRef<HTMLDivElement>(null)\n  \n  useEffect(() => {\n    if (ref.current && isHighlighted) {\n      ref.current.style.width = '100px'\n      const width = ref.current.offsetWidth // Forces layout\n      ref.current.style.height = '200px'\n    }\n  }, [isHighlighted])\n  \n  return <div ref={ref}>Content</div>\n}\n\n// Correct: toggle class\nfunction Box({ isHighlighted }: { isHighlighted: boolean }) {\n  return (\n    <div className={isHighlighted ? 'highlighted-box' : ''}>\n      Content\n    </div>\n  )\n}\n```\n\nPrefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.\n\nSee [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.\n\n### 7.2 Build Index Maps for Repeated Lookups\n\n**Impact: LOW-MEDIUM (1M ops to 2K ops)**\n\nMultiple `.find()` calls by the same key should use a Map.\n\n**Incorrect (O(n) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  return orders.map(order => ({\n    ...order,\n    user: users.find(u => u.id === order.userId)\n  }))\n}\n```\n\n**Correct (O(1) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  const userById = new Map(users.map(u => [u.id, u]))\n\n  return orders.map(order => ({\n    ...order,\n    user: userById.get(order.userId)\n  }))\n}\n```\n\nBuild map once (O(n)), then all lookups are O(1).\n\nFor 1000 orders × 1000 users: 1M ops → 2K ops.\n\n### 7.3 Cache Property Access in Loops\n\n**Impact: LOW-MEDIUM (reduces lookups)**\n\nCache object property lookups in hot paths.\n\n**Incorrect: 3 lookups × N iterations**\n\n```typescript\nfor (let i = 0; i < arr.length; i++) {\n  process(obj.config.settings.value)\n}\n```\n\n**Correct: 1 lookup total**\n\n```typescript\nconst value = obj.config.settings.value\nconst len = arr.length\nfor (let i = 0; i < len; i++) {\n  process(value)\n}\n```\n\n### 7.4 Cache Repeated Function Calls\n\n**Impact: MEDIUM (avoid redundant computation)**\n\nUse a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.\n\n**Incorrect: redundant computation**\n\n```typescript\nfunction ProjectList({ projects }: { projects: Project[] }) {\n  return (\n    <div>\n      {projects.map(project => {\n        // slugify() called 100+ times for same project names\n        const slug = slugify(project.name)\n        \n        return <ProjectCard key={project.id} slug={slug} />\n      })}\n    </div>\n  )\n}\n```\n\n**Correct: cached results**\n\n```typescript\n// Module-level cache\nconst slugifyCache = new Map<string, string>()\n\nfunction cachedSlugify(text: string): string {\n  if (slugifyCache.has(text)) {\n    return slugifyCache.get(text)!\n  }\n  const result = slugify(text)\n  slugifyCache.set(text, result)\n  return result\n}\n\nfunction ProjectList({ projects }: { projects: Project[] }) {\n  return (\n    <div>\n      {projects.map(project => {\n        // Computed only once per unique project name\n        const slug = cachedSlugify(project.name)\n        \n        return <ProjectCard key={project.id} slug={slug} />\n      })}\n    </div>\n  )\n}\n```\n\n**Simpler pattern for single-value functions:**\n\n```typescript\nlet isLoggedInCache: boolean | null = null\n\nfunction isLoggedIn(): boolean {\n  if (isLoggedInCache !== null) {\n    return isLoggedInCache\n  }\n  \n  isLoggedInCache = document.cookie.includes('auth=')\n  return isLoggedInCache\n}\n\n// Clear cache when auth changes\nfunction onAuthChange() {\n  isLoggedInCache = null\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\nReference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n\n### 7.5 Cache Storage API Calls\n\n**Impact: LOW-MEDIUM (reduces expensive I/O)**\n\n`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.\n\n**Incorrect: reads storage on every call**\n\n```typescript\nfunction getTheme() {\n  return localStorage.getItem('theme') ?? 'light'\n}\n// Called 10 times = 10 storage reads\n```\n\n**Correct: Map cache**\n\n```typescript\nconst storageCache = new Map<string, string | null>()\n\nfunction getLocalStorage(key: string) {\n  if (!storageCache.has(key)) {\n    storageCache.set(key, localStorage.getItem(key))\n  }\n  return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n  localStorage.setItem(key, value)\n  storageCache.set(key, value)  // keep cache in sync\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\n**Cookie caching:**\n\n```typescript\nlet cookieCache: Record<string, string> | null = null\n\nfunction getCookie(name: string) {\n  if (!cookieCache) {\n    cookieCache = Object.fromEntries(\n      document.cookie.split('; ').map(c => c.split('='))\n    )\n  }\n  return cookieCache[name]\n}\n```\n\n**Important: invalidate on external changes**\n\n```typescript\nwindow.addEventListener('storage', (e) => {\n  if (e.key) storageCache.delete(e.key)\n})\n\ndocument.addEventListener('visibilitychange', () => {\n  if (document.visibilityState === 'visible') {\n    storageCache.clear()\n  }\n})\n```\n\nIf storage can change externally (another tab, server-set cookies), invalidate cache:\n\n### 7.6 Combine Multiple Array Iterations\n\n**Impact: LOW-MEDIUM (reduces iterations)**\n\nMultiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.\n\n**Incorrect: 3 iterations**\n\n```typescript\nconst admins = users.filter(u => u.isAdmin)\nconst testers = users.filter(u => u.isTester)\nconst inactive = users.filter(u => !u.isActive)\n```\n\n**Correct: 1 iteration**\n\n```typescript\nconst admins: User[] = []\nconst testers: User[] = []\nconst inactive: User[] = []\n\nfor (const user of users) {\n  if (user.isAdmin) admins.push(user)\n  if (user.isTester) testers.push(user)\n  if (!user.isActive) inactive.push(user)\n}\n```\n\n### 7.7 Early Length Check for Array Comparisons\n\n**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**\n\nWhen comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.\n\nIn real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).\n\n**Incorrect: always runs expensive comparison**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Always sorts and joins, even when lengths differ\n  return current.sort().join() !== original.sort().join()\n}\n```\n\nTwo O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.\n\n**Correct (O(1) length check first):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Early return if lengths differ\n  if (current.length !== original.length) {\n    return true\n  }\n  // Only sort when lengths match\n  const currentSorted = current.toSorted()\n  const originalSorted = original.toSorted()\n  for (let i = 0; i < currentSorted.length; i++) {\n    if (currentSorted[i] !== originalSorted[i]) {\n      return true\n    }\n  }\n  return false\n}\n```\n\nThis new approach is more efficient because:\n\n- It avoids the overhead of sorting and joining the arrays when lengths differ\n\n- It avoids consuming memory for the joined strings (especially important for large arrays)\n\n- It avoids mutating the original arrays\n\n- It returns early when a difference is found\n\n### 7.8 Early Return from Functions\n\n**Impact: LOW-MEDIUM (avoids unnecessary computation)**\n\nReturn early when result is determined to skip unnecessary processing.\n\n**Incorrect: processes all items even after finding answer**\n\n```typescript\nfunction validateUsers(users: User[]) {\n  let hasError = false\n  let errorMessage = ''\n  \n  for (const user of users) {\n    if (!user.email) {\n      hasError = true\n      errorMessage = 'Email required'\n    }\n    if (!user.name) {\n      hasError = true\n      errorMessage = 'Name required'\n    }\n    // Continues checking all users even after error found\n  }\n  \n  return hasError ? { valid: false, error: errorMessage } : { valid: true }\n}\n```\n\n**Correct: returns immediately on first error**\n\n```typescript\nfunction validateUsers(users: User[]) {\n  for (const user of users) {\n    if (!user.email) {\n      return { valid: false, error: 'Email required' }\n    }\n    if (!user.name) {\n      return { valid: false, error: 'Name required' }\n    }\n  }\n\n  return { valid: true }\n}\n```\n\n### 7.9 Hoist RegExp Creation\n\n**Impact: LOW-MEDIUM (avoids recreation)**\n\nDon't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.\n\n**Incorrect: new RegExp every render**\n\n```tsx\nfunction Highlighter({ text, query }: Props) {\n  const regex = new RegExp(`(${query})`, 'gi')\n  const parts = text.split(regex)\n  return <>{parts.map((part, i) => ...)}</>\n}\n```\n\n**Correct: memoize or hoist**\n\n```tsx\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction Highlighter({ text, query }: Props) {\n  const regex = useMemo(\n    () => new RegExp(`(${escapeRegex(query)})`, 'gi'),\n    [query]\n  )\n  const parts = text.split(regex)\n  return <>{parts.map((part, i) => ...)}</>\n}\n```\n\n**Warning: global regex has mutable state**\n\n```typescript\nconst regex = /foo/g\nregex.test('foo')  // true, lastIndex = 3\nregex.test('foo')  // false, lastIndex = 0\n```\n\nGlobal regex (`/g`) has mutable `lastIndex` state:\n\n### 7.10 Use Loop for Min/Max Instead of Sort\n\n**Impact: LOW (O(n) instead of O(n log n))**\n\nFinding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.\n\n**Incorrect (O(n log n) - sort to find latest):**\n\n```typescript\ninterface Project {\n  id: string\n  name: string\n  updatedAt: number\n}\n\nfunction getLatestProject(projects: Project[]) {\n  const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)\n  return sorted[0]\n}\n```\n\nSorts the entire array just to find the maximum value.\n\n**Incorrect (O(n log n) - sort for oldest and newest):**\n\n```typescript\nfunction getOldestAndNewest(projects: Project[]) {\n  const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)\n  return { oldest: sorted[0], newest: sorted[sorted.length - 1] }\n}\n```\n\nStill sorts unnecessarily when only min/max are needed.\n\n**Correct (O(n) - single loop):**\n\n```typescript\nfunction getLatestProject(projects: Project[]) {\n  if (projects.length === 0) return null\n  \n  let latest = projects[0]\n  \n  for (let i = 1; i < projects.length; i++) {\n    if (projects[i].updatedAt > latest.updatedAt) {\n      latest = projects[i]\n    }\n  }\n  \n  return latest\n}\n\nfunction getOldestAndNewest(projects: Project[]) {\n  if (projects.length === 0) return { oldest: null, newest: null }\n  \n  let oldest = projects[0]\n  let newest = projects[0]\n  \n  for (let i = 1; i < projects.length; i++) {\n    if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]\n    if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]\n  }\n  \n  return { oldest, newest }\n}\n```\n\nSingle pass through the array, no copying, no sorting.\n\n**Alternative: Math.min/Math.max for small arrays**\n\n```typescript\nconst numbers = [5, 2, 8, 1, 9]\nconst min = Math.min(...numbers)\nconst max = Math.max(...numbers)\n```\n\nThis works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.\n\n### 7.11 Use Set/Map for O(1) Lookups\n\n**Impact: LOW-MEDIUM (O(n) to O(1))**\n\nConvert arrays to Set/Map for repeated membership checks.\n\n**Incorrect (O(n) per check):**\n\n```typescript\nconst allowedIds = ['a', 'b', 'c', ...]\nitems.filter(item => allowedIds.includes(item.id))\n```\n\n**Correct (O(1) per check):**\n\n```typescript\nconst allowedIds = new Set(['a', 'b', 'c', ...])\nitems.filter(item => allowedIds.has(item.id))\n```\n\n### 7.12 Use toSorted() Instead of sort() for Immutability\n\n**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**\n\n`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.\n\n**Incorrect: mutates original array**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Mutates the users prop array!\n  const sorted = useMemo(\n    () => users.sort((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Correct: creates new array**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Creates new sorted array, original unchanged\n  const sorted = useMemo(\n    () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Why this matters in React:**\n\n1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only\n\n2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior\n\n**Browser support: fallback for older browsers**\n\n```typescript\n// Fallback for older browsers\nconst sorted = [...items].sort((a, b) => a.value - b.value)\n```\n\n`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:\n\n**Other immutable array methods:**\n\n- `.toSorted()` - immutable sort\n\n- `.toReversed()` - immutable reverse\n\n- `.toSpliced()` - immutable splice\n\n- `.with()` - immutable element replacement\n\n---\n\n## 8. Advanced Patterns\n\n**Impact: LOW**\n\nAdvanced patterns for specific cases that require careful implementation.\n\n### 8.1 Store Event Handlers in Refs\n\n**Impact: LOW (stable subscriptions)**\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect: re-subscribes on every render**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct: stable subscription**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n\n### 8.2 useEffectEvent for Stable Callback Refs\n\n**Impact: LOW (prevents effect re-runs)**\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Incorrect: effect re-runs on every callback change**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct: using React's useEffectEvent**\n\n```tsx\nimport { useEffectEvent } from 'react';\n\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchEvent = useEffectEvent(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchEvent(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n\n---\n\n## References\n\n1. [https://react.dev](https://react.dev)\n2. [https://nextjs.org](https://nextjs.org)\n3. [https://swr.vercel.app](https://swr.vercel.app)\n4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)\n6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n"
  },
  {
    "path": "agents/skills/react-best-practices/README.md",
    "content": "# React Best Practices\n\nA structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.\n\n## Structure\n\n- `rules/` - Individual rule files (one per rule)\n  - `_sections.md` - Section metadata (titles, impacts, descriptions)\n  - `_template.md` - Template for creating new rules\n  - `area-description.md` - Individual rule files\n- `src/` - Build scripts and utilities\n- `metadata.json` - Document metadata (version, organization, abstract)\n- __`AGENTS.md`__ - Compiled output (generated)\n- __`test-cases.json`__ - Test cases for LLM evaluation (generated)\n\n## Getting Started\n\n1. Install dependencies:\n   ```bash\n   pnpm install\n   ```\n\n2. Build AGENTS.md from rules:\n   ```bash\n   pnpm build\n   ```\n\n3. Validate rule files:\n   ```bash\n   pnpm validate\n   ```\n\n4. Extract test cases:\n   ```bash\n   pnpm extract-tests\n   ```\n\n## Creating a New Rule\n\n1. Copy `rules/_template.md` to `rules/area-description.md`\n2. Choose the appropriate area prefix:\n   - `async-` for Eliminating Waterfalls (Section 1)\n   - `bundle-` for Bundle Size Optimization (Section 2)\n   - `server-` for Server-Side Performance (Section 3)\n   - `client-` for Client-Side Data Fetching (Section 4)\n   - `rerender-` for Re-render Optimization (Section 5)\n   - `rendering-` for Rendering Performance (Section 6)\n   - `js-` for JavaScript Performance (Section 7)\n   - `advanced-` for Advanced Patterns (Section 8)\n3. Fill in the frontmatter and content\n4. Ensure you have clear examples with explanations\n5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json\n\n## Rule File Structure\n\nEach rule file should follow this structure:\n\n```markdown\n---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: Optional description\ntags: tag1, tag2, tag3\n---\n\n## Rule Title Here\n\nBrief explanation of the rule and why it matters.\n\n**Incorrect (description of what's wrong):**\n\n```typescript\n// Bad code example\n```\n\n**Correct (description of what's right):**\n\n```typescript\n// Good code example\n```\n\nOptional explanatory text after examples.\n\nReference: [Link](https://example.com)\n\n## File Naming Convention\n\n- Files starting with `_` are special (excluded from build)\n- Rule files: `area-description.md` (e.g., `async-parallel.md`)\n- Section is automatically inferred from filename prefix\n- Rules are sorted alphabetically by title within each section\n- IDs (e.g., 1.1, 1.2) are auto-generated during build\n\n## Impact Levels\n\n- `CRITICAL` - Highest priority, major performance gains\n- `HIGH` - Significant performance improvements\n- `MEDIUM-HIGH` - Moderate-high gains\n- `MEDIUM` - Moderate performance improvements\n- `LOW-MEDIUM` - Low-medium gains\n- `LOW` - Incremental improvements\n\n## Scripts\n\n- `pnpm build` - Compile rules into AGENTS.md\n- `pnpm validate` - Validate all rule files\n- `pnpm extract-tests` - Extract test cases for LLM evaluation\n- `pnpm dev` - Build and validate\n\n## Contributing\n\nWhen adding or modifying rules:\n\n1. Use the correct filename prefix for your section\n2. Follow the `_template.md` structure\n3. Include clear bad/good examples with explanations\n4. Add appropriate tags\n5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json\n6. Rules are automatically sorted by title - no need to manage numbers!\n\n## Acknowledgments\n\nOriginally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).\n"
  },
  {
    "path": "agents/skills/react-best-practices/SKILL.md",
    "content": "---\nname: react-best-practices\ndescription: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.\nlicense: MIT\nmetadata:\n  author: vercel\n  version: \"1.0.0\"\n---\n\n# Vercel React Best Practices\n\nComprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.\n\n## When to Apply\n\nReference these guidelines when:\n- Writing new React components or Next.js pages\n- Implementing data fetching (client or server-side)\n- Reviewing code for performance issues\n- Refactoring existing React/Next.js code\n- Optimizing bundle size or load times\n\n## Rule Categories by Priority\n\n| Priority | Category | Impact | Prefix |\n|----------|----------|--------|--------|\n| 1 | Eliminating Waterfalls | CRITICAL | `async-` |\n| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |\n| 3 | Server-Side Performance | HIGH | `server-` |\n| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |\n| 5 | Re-render Optimization | MEDIUM | `rerender-` |\n| 6 | Rendering Performance | MEDIUM | `rendering-` |\n| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |\n| 8 | Advanced Patterns | LOW | `advanced-` |\n\n## Quick Reference\n\n### 1. Eliminating Waterfalls (CRITICAL)\n\n- `async-defer-await` - Move await into branches where actually used\n- `async-parallel` - Use Promise.all() for independent operations\n- `async-dependencies` - Use better-all for partial dependencies\n- `async-api-routes` - Start promises early, await late in API routes\n- `async-suspense-boundaries` - Use Suspense to stream content\n\n### 2. Bundle Size Optimization (CRITICAL)\n\n- `bundle-barrel-imports` - Import directly, avoid barrel files\n- `bundle-dynamic-imports` - Use next/dynamic for heavy components\n- `bundle-defer-third-party` - Load analytics/logging after hydration\n- `bundle-conditional` - Load modules only when feature is activated\n- `bundle-preload` - Preload on hover/focus for perceived speed\n\n### 3. Server-Side Performance (HIGH)\n\n- `server-cache-react` - Use React.cache() for per-request deduplication\n- `server-cache-lru` - Use LRU cache for cross-request caching\n- `server-serialization` - Minimize data passed to client components\n- `server-parallel-fetching` - Restructure components to parallelize fetches\n- `server-after-nonblocking` - Use after() for non-blocking operations\n\n### 4. Client-Side Data Fetching (MEDIUM-HIGH)\n\n- `client-swr-dedup` - Use SWR for automatic request deduplication\n- `client-event-listeners` - Deduplicate global event listeners\n\n### 5. Re-render Optimization (MEDIUM)\n\n- `rerender-defer-reads` - Don't subscribe to state only used in callbacks\n- `rerender-memo` - Extract expensive work into memoized components\n- `rerender-dependencies` - Use primitive dependencies in effects\n- `rerender-derived-state` - Subscribe to derived booleans, not raw values\n- `rerender-functional-setstate` - Use functional setState for stable callbacks\n- `rerender-lazy-state-init` - Pass function to useState for expensive values\n- `rerender-transitions` - Use startTransition for non-urgent updates\n\n### 6. Rendering Performance (MEDIUM)\n\n- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element\n- `rendering-content-visibility` - Use content-visibility for long lists\n- `rendering-hoist-jsx` - Extract static JSX outside components\n- `rendering-svg-precision` - Reduce SVG coordinate precision\n- `rendering-hydration-no-flicker` - Use inline script for client-only data\n- `rendering-activity` - Use Activity component for show/hide\n- `rendering-conditional-render` - Use ternary, not && for conditionals\n\n### 7. JavaScript Performance (LOW-MEDIUM)\n\n- `js-batch-dom-css` - Group CSS changes via classes or cssText\n- `js-index-maps` - Build Map for repeated lookups\n- `js-cache-property-access` - Cache object properties in loops\n- `js-cache-function-results` - Cache function results in module-level Map\n- `js-cache-storage` - Cache localStorage/sessionStorage reads\n- `js-combine-iterations` - Combine multiple filter/map into one loop\n- `js-length-check-first` - Check array length before expensive comparison\n- `js-early-exit` - Return early from functions\n- `js-hoist-regexp` - Hoist RegExp creation outside loops\n- `js-min-max-loop` - Use loop for min/max instead of sort\n- `js-set-map-lookups` - Use Set/Map for O(1) lookups\n- `js-tosorted-immutable` - Use toSorted() for immutability\n\n### 8. Advanced Patterns (LOW)\n\n- `advanced-event-handler-refs` - Store event handlers in refs\n- `advanced-use-latest` - useLatest for stable callback refs\n\n## How to Use\n\nRead individual rule files for detailed explanations and code examples:\n\n```\nrules/async-parallel.md\nrules/bundle-barrel-imports.md\nrules/_sections.md\n```\n\nEach rule file contains:\n- Brief explanation of why it matters\n- Incorrect code example with explanation\n- Correct code example with explanation\n- Additional context and references\n\n## Full Compiled Document\n\nFor the complete guide with all rules expanded: `AGENTS.md`\n"
  },
  {
    "path": "agents/skills/react-best-practices/metadata.json",
    "content": "{\n  \"version\": \"1.0.0\",\n  \"organization\": \"Vercel Engineering\",\n  \"date\": \"January 2026\",\n  \"abstract\": \"Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.\",\n  \"references\": [\n    \"https://react.dev\",\n    \"https://nextjs.org\",\n    \"https://swr.vercel.app\",\n    \"https://github.com/shuding/better-all\",\n    \"https://github.com/isaacs/node-lru-cache\",\n    \"https://vercel.com/blog/how-we-optimized-package-imports-in-next-js\",\n    \"https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast\"\n  ]\n}\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/_sections.md",
    "content": "# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Eliminating Waterfalls (async)\n\n**Impact:** CRITICAL  \n**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.\n\n## 2. Bundle Size Optimization (bundle)\n\n**Impact:** CRITICAL  \n**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.\n\n## 3. Server-Side Performance (server)\n\n**Impact:** HIGH  \n**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.\n\n## 4. Client-Side Data Fetching (client)\n\n**Impact:** MEDIUM-HIGH  \n**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.\n\n## 5. Re-render Optimization (rerender)\n\n**Impact:** MEDIUM  \n**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.\n\n## 6. Rendering Performance (rendering)\n\n**Impact:** MEDIUM  \n**Description:** Optimizing the rendering process reduces the work the browser needs to do.\n\n## 7. JavaScript Performance (js)\n\n**Impact:** LOW-MEDIUM  \n**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.\n\n## 8. Advanced Patterns (advanced)\n\n**Impact:** LOW  \n**Description:** Advanced patterns for specific cases that require careful implementation.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/_template.md",
    "content": "---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: Optional description of impact (e.g., \"20-50% improvement\")\ntags: tag1, tag2\n---\n\n## Rule Title Here\n\n**Impact: MEDIUM (optional impact description)**\n\nBrief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.\n\n**Incorrect (description of what's wrong):**\n\n```typescript\n// Bad code example here\nconst bad = example()\n```\n\n**Correct (description of what's right):**\n\n```typescript\n// Good code example here\nconst good = example()\n```\n\nReference: [Link to documentation or resource](https://example.com)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/advanced-event-handler-refs.md",
    "content": "---\ntitle: Store Event Handlers in Refs\nimpact: LOW\nimpactDescription: stable subscriptions\ntags: advanced, hooks, refs, event-handlers, optimization\n---\n\n## Store Event Handlers in Refs\n\nStore callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.\n\n**Incorrect (re-subscribes on every render):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => window.removeEventListener(event, handler)\n  }, [event, handler])\n}\n```\n\n**Correct (stable subscription):**\n\n```tsx\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const handlerRef = useRef(handler)\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  useEffect(() => {\n    const listener = (e) => handlerRef.current(e)\n    window.addEventListener(event, listener)\n    return () => window.removeEventListener(event, listener)\n  }, [event])\n}\n```\n\n**Alternative: use `useEffectEvent` if you're on latest React:**\n\n```tsx\nimport { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n  const onEvent = useEffectEvent(handler)\n\n  useEffect(() => {\n    window.addEventListener(event, onEvent)\n    return () => window.removeEventListener(event, onEvent)\n  }, [event])\n}\n```\n\n`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/advanced-use-latest.md",
    "content": "---\ntitle: useEffectEvent for Stable Callback Refs\nimpact: LOW\nimpactDescription: prevents effect re-runs\ntags: advanced, hooks, useEffectEvent, refs, optimization\n---\n\n## useEffectEvent for Stable Callback Refs\n\nAccess latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.\n\n**Incorrect (effect re-runs on every callback change):**\n\n```tsx\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearch(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query, onSearch])\n}\n```\n\n**Correct (using React's useEffectEvent):**\n\n```tsx\nimport { useEffectEvent } from 'react';\n\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n  const [query, setQuery] = useState('')\n  const onSearchEvent = useEffectEvent(onSearch)\n\n  useEffect(() => {\n    const timeout = setTimeout(() => onSearchEvent(query), 300)\n    return () => clearTimeout(timeout)\n  }, [query])\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/async-api-routes.md",
    "content": "---\ntitle: Prevent Waterfall Chains in API Routes\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: api-routes, server-actions, waterfalls, parallelization\n---\n\n## Prevent Waterfall Chains in API Routes\n\nIn API routes and Server Actions, start independent operations immediately, even if you don't await them yet.\n\n**Incorrect (config waits for auth, data waits for both):**\n\n```typescript\nexport async function GET(request: Request) {\n  const session = await auth()\n  const config = await fetchConfig()\n  const data = await fetchData(session.user.id)\n  return Response.json({ data, config })\n}\n```\n\n**Correct (auth and config start immediately):**\n\n```typescript\nexport async function GET(request: Request) {\n  const sessionPromise = auth()\n  const configPromise = fetchConfig()\n  const session = await sessionPromise\n  const [config, data] = await Promise.all([\n    configPromise,\n    fetchData(session.user.id)\n  ])\n  return Response.json({ data, config })\n}\n```\n\nFor operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/async-defer-await.md",
    "content": "---\ntitle: Defer Await Until Needed\nimpact: HIGH\nimpactDescription: avoids blocking unused code paths\ntags: async, await, conditional, optimization\n---\n\n## Defer Await Until Needed\n\nMove `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.\n\n**Incorrect (blocks both branches):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  const userData = await fetchUserData(userId)\n  \n  if (skipProcessing) {\n    // Returns immediately but still waited for userData\n    return { skipped: true }\n  }\n  \n  // Only this branch uses userData\n  return processUserData(userData)\n}\n```\n\n**Correct (only blocks when needed):**\n\n```typescript\nasync function handleRequest(userId: string, skipProcessing: boolean) {\n  if (skipProcessing) {\n    // Returns immediately without waiting\n    return { skipped: true }\n  }\n  \n  // Fetch only when needed\n  const userData = await fetchUserData(userId)\n  return processUserData(userData)\n}\n```\n\n**Another example (early return optimization):**\n\n```typescript\n// Incorrect: always fetches permissions\nasync function updateResource(resourceId: string, userId: string) {\n  const permissions = await fetchPermissions(userId)\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n\n// Correct: fetches only when needed\nasync function updateResource(resourceId: string, userId: string) {\n  const resource = await getResource(resourceId)\n  \n  if (!resource) {\n    return { error: 'Not found' }\n  }\n  \n  const permissions = await fetchPermissions(userId)\n  \n  if (!permissions.canEdit) {\n    return { error: 'Forbidden' }\n  }\n  \n  return await updateResourceData(resource, permissions)\n}\n```\n\nThis optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/async-dependencies.md",
    "content": "---\ntitle: Dependency-Based Parallelization\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, dependencies, better-all\n---\n\n## Dependency-Based Parallelization\n\nFor operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.\n\n**Incorrect (profile waits for config unnecessarily):**\n\n```typescript\nconst [user, config] = await Promise.all([\n  fetchUser(),\n  fetchConfig()\n])\nconst profile = await fetchProfile(user.id)\n```\n\n**Correct (config and profile run in parallel):**\n\n```typescript\nimport { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n  async user() { return fetchUser() },\n  async config() { return fetchConfig() },\n  async profile() {\n    return fetchProfile((await this.$.user).id)\n  }\n})\n```\n\n**Alternative without extra dependencies:**\n\nWe can also create all the promises first, and do `Promise.all()` at the end.\n\n```typescript\nconst userPromise = fetchUser()\nconst profilePromise = userPromise.then(user => fetchProfile(user.id))\n\nconst [user, config, profile] = await Promise.all([\n  userPromise,\n  fetchConfig(),\n  profilePromise\n])\n```\n\nReference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/async-parallel.md",
    "content": "---\ntitle: Promise.all() for Independent Operations\nimpact: CRITICAL\nimpactDescription: 2-10× improvement\ntags: async, parallelization, promises, waterfalls\n---\n\n## Promise.all() for Independent Operations\n\nWhen async operations have no interdependencies, execute them concurrently using `Promise.all()`.\n\n**Incorrect (sequential execution, 3 round trips):**\n\n```typescript\nconst user = await fetchUser()\nconst posts = await fetchPosts()\nconst comments = await fetchComments()\n```\n\n**Correct (parallel execution, 1 round trip):**\n\n```typescript\nconst [user, posts, comments] = await Promise.all([\n  fetchUser(),\n  fetchPosts(),\n  fetchComments()\n])\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/async-suspense-boundaries.md",
    "content": "---\ntitle: Strategic Suspense Boundaries\nimpact: HIGH\nimpactDescription: faster initial paint\ntags: async, suspense, streaming, layout-shift\n---\n\n## Strategic Suspense Boundaries\n\nInstead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.\n\n**Incorrect (wrapper blocked by data fetching):**\n\n```tsx\nasync function Page() {\n  const data = await fetchData() // Blocks entire page\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <DataDisplay data={data} />\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n```\n\nThe entire layout waits for data even though only the middle section needs it.\n\n**Correct (wrapper shows immediately, data streams in):**\n\n```tsx\nfunction Page() {\n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <div>\n        <Suspense fallback={<Skeleton />}>\n          <DataDisplay />\n        </Suspense>\n      </div>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nasync function DataDisplay() {\n  const data = await fetchData() // Only blocks this component\n  return <div>{data.content}</div>\n}\n```\n\nSidebar, Header, and Footer render immediately. Only DataDisplay waits for data.\n\n**Alternative (share promise across components):**\n\n```tsx\nfunction Page() {\n  // Start fetch immediately, but don't await\n  const dataPromise = fetchData()\n  \n  return (\n    <div>\n      <div>Sidebar</div>\n      <div>Header</div>\n      <Suspense fallback={<Skeleton />}>\n        <DataDisplay dataPromise={dataPromise} />\n        <DataSummary dataPromise={dataPromise} />\n      </Suspense>\n      <div>Footer</div>\n    </div>\n  )\n}\n\nfunction DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Unwraps the promise\n  return <div>{data.content}</div>\n}\n\nfunction DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {\n  const data = use(dataPromise) // Reuses the same promise\n  return <div>{data.summary}</div>\n}\n```\n\nBoth components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.\n\n**When NOT to use this pattern:**\n\n- Critical data needed for layout decisions (affects positioning)\n- SEO-critical content above the fold\n- Small, fast queries where suspense overhead isn't worth it\n- When you want to avoid layout shift (loading → content jump)\n\n**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/bundle-barrel-imports.md",
    "content": "---\ntitle: Avoid Barrel File Imports\nimpact: CRITICAL\nimpactDescription: 200-800ms import cost, slow builds\ntags: bundle, imports, tree-shaking, barrel-files, performance\n---\n\n## Avoid Barrel File Imports\n\nImport directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).\n\nPopular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.\n\n**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.\n\n**Incorrect (imports entire library):**\n\n```tsx\nimport { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev\n```\n\n**Correct (imports only what you need):**\n\n```tsx\nimport Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use\n```\n\n**Alternative (Next.js 13.5+):**\n\n```js\n// next.config.js - use optimizePackageImports\nmodule.exports = {\n  experimental: {\n    optimizePackageImports: ['lucide-react', '@mui/material']\n  }\n}\n\n// Then you can keep the ergonomic barrel imports:\nimport { Check, X, Menu } from 'lucide-react'\n// Automatically transformed to direct imports at build time\n```\n\nDirect imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.\n\nLibraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.\n\nReference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/bundle-conditional.md",
    "content": "---\ntitle: Conditional Module Loading\nimpact: HIGH\nimpactDescription: loads large data only when needed\ntags: bundle, conditional-loading, lazy-loading\n---\n\n## Conditional Module Loading\n\nLoad large data or modules only when a feature is activated.\n\n**Example (lazy-load animation frames):**\n\n```tsx\nfunction AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {\n  const [frames, setFrames] = useState<Frame[] | null>(null)\n\n  useEffect(() => {\n    if (enabled && !frames && typeof window !== 'undefined') {\n      import('./animation-frames.js')\n        .then(mod => setFrames(mod.frames))\n        .catch(() => setEnabled(false))\n    }\n  }, [enabled, frames, setEnabled])\n\n  if (!frames) return <Skeleton />\n  return <Canvas frames={frames} />\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/bundle-defer-third-party.md",
    "content": "---\ntitle: Defer Non-Critical Third-Party Libraries\nimpact: MEDIUM\nimpactDescription: loads after hydration\ntags: bundle, third-party, analytics, defer\n---\n\n## Defer Non-Critical Third-Party Libraries\n\nAnalytics, logging, and error tracking don't block user interaction. Load them after hydration.\n\n**Incorrect (blocks initial bundle):**\n\n```tsx\nimport { Analytics } from '@vercel/analytics/react'\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <body>\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  )\n}\n```\n\n**Correct (loads after hydration):**\n\n```tsx\nimport dynamic from 'next/dynamic'\n\nconst Analytics = dynamic(\n  () => import('@vercel/analytics/react').then(m => m.Analytics),\n  { ssr: false }\n)\n\nexport default function RootLayout({ children }) {\n  return (\n    <html>\n      <body>\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  )\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/bundle-dynamic-imports.md",
    "content": "---\ntitle: Dynamic Imports for Heavy Components\nimpact: CRITICAL\nimpactDescription: directly affects TTI and LCP\ntags: bundle, dynamic-import, code-splitting, next-dynamic\n---\n\n## Dynamic Imports for Heavy Components\n\nUse `next/dynamic` to lazy-load large components not needed on initial render.\n\n**Incorrect (Monaco bundles with main chunk ~300KB):**\n\n```tsx\nimport { MonacoEditor } from './monaco-editor'\n\nfunction CodePanel({ code }: { code: string }) {\n  return <MonacoEditor value={code} />\n}\n```\n\n**Correct (Monaco loads on demand):**\n\n```tsx\nimport dynamic from 'next/dynamic'\n\nconst MonacoEditor = dynamic(\n  () => import('./monaco-editor').then(m => m.MonacoEditor),\n  { ssr: false }\n)\n\nfunction CodePanel({ code }: { code: string }) {\n  return <MonacoEditor value={code} />\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/bundle-preload.md",
    "content": "---\ntitle: Preload Based on User Intent\nimpact: MEDIUM\nimpactDescription: reduces perceived latency\ntags: bundle, preload, user-intent, hover\n---\n\n## Preload Based on User Intent\n\nPreload heavy bundles before they're needed to reduce perceived latency.\n\n**Example (preload on hover/focus):**\n\n```tsx\nfunction EditorButton({ onClick }: { onClick: () => void }) {\n  const preload = () => {\n    if (typeof window !== 'undefined') {\n      void import('./monaco-editor')\n    }\n  }\n\n  return (\n    <button\n      onMouseEnter={preload}\n      onFocus={preload}\n      onClick={onClick}\n    >\n      Open Editor\n    </button>\n  )\n}\n```\n\n**Example (preload when feature flag is enabled):**\n\n```tsx\nfunction FlagsProvider({ children, flags }: Props) {\n  useEffect(() => {\n    if (flags.editorEnabled && typeof window !== 'undefined') {\n      void import('./monaco-editor').then(mod => mod.init())\n    }\n  }, [flags.editorEnabled])\n\n  return <FlagsContext.Provider value={flags}>\n    {children}\n  </FlagsContext.Provider>\n}\n```\n\nThe `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/client-event-listeners.md",
    "content": "---\ntitle: Deduplicate Global Event Listeners\nimpact: LOW\nimpactDescription: single listener for N components\ntags: client, swr, event-listeners, subscription\n---\n\n## Deduplicate Global Event Listeners\n\nUse `useSWRSubscription()` to share global event listeners across component instances.\n\n**Incorrect (N instances = N listeners):**\n\n```tsx\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  useEffect(() => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && e.key === key) {\n        callback()\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  }, [key, callback])\n}\n```\n\nWhen using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.\n\n**Correct (N instances = 1 listener):**\n\n```tsx\nimport useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map<string, Set<() => void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n  // Register this callback in the Map\n  useEffect(() => {\n    if (!keyCallbacks.has(key)) {\n      keyCallbacks.set(key, new Set())\n    }\n    keyCallbacks.get(key)!.add(callback)\n\n    return () => {\n      const set = keyCallbacks.get(key)\n      if (set) {\n        set.delete(callback)\n        if (set.size === 0) {\n          keyCallbacks.delete(key)\n        }\n      }\n    }\n  }, [key, callback])\n\n  useSWRSubscription('global-keydown', () => {\n    const handler = (e: KeyboardEvent) => {\n      if (e.metaKey && keyCallbacks.has(e.key)) {\n        keyCallbacks.get(e.key)!.forEach(cb => cb())\n      }\n    }\n    window.addEventListener('keydown', handler)\n    return () => window.removeEventListener('keydown', handler)\n  })\n}\n\nfunction Profile() {\n  // Multiple shortcuts will share the same listener\n  useKeyboardShortcut('p', () => { /* ... */ }) \n  useKeyboardShortcut('k', () => { /* ... */ })\n  // ...\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/client-localstorage-schema.md",
    "content": "---\ntitle: Version and Minimize localStorage Data\nimpact: MEDIUM\nimpactDescription: prevents schema conflicts, reduces storage size\ntags: client, localStorage, storage, versioning, data-minimization\n---\n\n## Version and Minimize localStorage Data\n\nAdd version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.\n\n**Incorrect:**\n\n```typescript\n// No version, stores everything, no error handling\nlocalStorage.setItem('userConfig', JSON.stringify(fullUserObject))\nconst data = localStorage.getItem('userConfig')\n```\n\n**Correct:**\n\n```typescript\nconst VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n  try {\n    localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n  } catch {\n    // Throws in incognito/private browsing, quota exceeded, or disabled\n  }\n}\n\nfunction loadConfig() {\n  try {\n    const data = localStorage.getItem(`userConfig:${VERSION}`)\n    return data ? JSON.parse(data) : null\n  } catch {\n    return null\n  }\n}\n\n// Migration from v1 to v2\nfunction migrate() {\n  try {\n    const v1 = localStorage.getItem('userConfig:v1')\n    if (v1) {\n      const old = JSON.parse(v1)\n      saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })\n      localStorage.removeItem('userConfig:v1')\n    }\n  } catch {}\n}\n```\n\n**Store minimal fields from server responses:**\n\n```typescript\n// User object has 20+ fields, only store what UI needs\nfunction cachePrefs(user: FullUser) {\n  try {\n    localStorage.setItem('prefs:v1', JSON.stringify({\n      theme: user.preferences.theme,\n      notifications: user.preferences.notifications\n    }))\n  } catch {}\n}\n```\n\n**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.\n\n**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/client-passive-event-listeners.md",
    "content": "---\ntitle: Use Passive Event Listeners for Scrolling Performance\nimpact: MEDIUM\nimpactDescription: eliminates scroll delay caused by event listeners\ntags: client, event-listeners, scrolling, performance, touch, wheel\n---\n\n## Use Passive Event Listeners for Scrolling Performance\n\nAdd `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.\n\n**Incorrect:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch)\n  document.addEventListener('wheel', handleWheel)\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Correct:**\n\n```typescript\nuseEffect(() => {\n  const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n  const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n  \n  document.addEventListener('touchstart', handleTouch, { passive: true })\n  document.addEventListener('wheel', handleWheel, { passive: true })\n  \n  return () => {\n    document.removeEventListener('touchstart', handleTouch)\n    document.removeEventListener('wheel', handleWheel)\n  }\n}, [])\n```\n\n**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.\n\n**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/client-swr-dedup.md",
    "content": "---\ntitle: Use SWR for Automatic Deduplication\nimpact: MEDIUM-HIGH\nimpactDescription: automatic deduplication\ntags: client, swr, deduplication, data-fetching\n---\n\n## Use SWR for Automatic Deduplication\n\nSWR enables request deduplication, caching, and revalidation across component instances.\n\n**Incorrect (no deduplication, each instance fetches):**\n\n```tsx\nfunction UserList() {\n  const [users, setUsers] = useState([])\n  useEffect(() => {\n    fetch('/api/users')\n      .then(r => r.json())\n      .then(setUsers)\n  }, [])\n}\n```\n\n**Correct (multiple instances share one request):**\n\n```tsx\nimport useSWR from 'swr'\n\nfunction UserList() {\n  const { data: users } = useSWR('/api/users', fetcher)\n}\n```\n\n**For immutable data:**\n\n```tsx\nimport { useImmutableSWR } from '@/lib/swr'\n\nfunction StaticContent() {\n  const { data } = useImmutableSWR('/api/config', fetcher)\n}\n```\n\n**For mutations:**\n\n```tsx\nimport { useSWRMutation } from 'swr/mutation'\n\nfunction UpdateButton() {\n  const { trigger } = useSWRMutation('/api/user', updateUser)\n  return <button onClick={() => trigger()}>Update</button>\n}\n```\n\nReference: [https://swr.vercel.app](https://swr.vercel.app)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-batch-dom-css.md",
    "content": "---\ntitle: Avoid Layout Thrashing\nimpact: MEDIUM\nimpactDescription: prevents forced synchronous layouts and reduces performance bottlenecks\ntags: javascript, dom, css, performance, reflow, layout-thrashing\n---\n\n## Avoid Layout Thrashing\n\nAvoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.\n\n**This is OK (browser batches style changes):**\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Each line invalidates style, but browser batches the recalculation\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n}\n```\n\n**Incorrect (interleaved reads and writes force reflows):**\n```typescript\nfunction layoutThrashing(element: HTMLElement) {\n  element.style.width = '100px'\n  const width = element.offsetWidth  // Forces reflow\n  element.style.height = '200px'\n  const height = element.offsetHeight  // Forces another reflow\n}\n```\n\n**Correct (batch writes, then read once):**\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  // Batch all writes together\n  element.style.width = '100px'\n  element.style.height = '200px'\n  element.style.backgroundColor = 'blue'\n  element.style.border = '1px solid black'\n  \n  // Read after all writes are done (single reflow)\n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**Correct (batch reads, then writes):**\n```typescript\nfunction avoidThrashing(element: HTMLElement) {\n  // Read phase - all layout queries first\n  const rect1 = element.getBoundingClientRect()\n  const offsetWidth = element.offsetWidth\n  const offsetHeight = element.offsetHeight\n  \n  // Write phase - all style changes after\n  element.style.width = '100px'\n  element.style.height = '200px'\n}\n```\n\n**Better: use CSS classes**\n```css\n.highlighted-box {\n  width: 100px;\n  height: 200px;\n  background-color: blue;\n  border: 1px solid black;\n}\n```\n```typescript\nfunction updateElementStyles(element: HTMLElement) {\n  element.classList.add('highlighted-box')\n  \n  const { width, height } = element.getBoundingClientRect()\n}\n```\n\n**React example:**\n```tsx\n// Incorrect: interleaving style changes with layout queries\nfunction Box({ isHighlighted }: { isHighlighted: boolean }) {\n  const ref = useRef<HTMLDivElement>(null)\n  \n  useEffect(() => {\n    if (ref.current && isHighlighted) {\n      ref.current.style.width = '100px'\n      const width = ref.current.offsetWidth // Forces layout\n      ref.current.style.height = '200px'\n    }\n  }, [isHighlighted])\n  \n  return <div ref={ref}>Content</div>\n}\n\n// Correct: toggle class\nfunction Box({ isHighlighted }: { isHighlighted: boolean }) {\n  return (\n    <div className={isHighlighted ? 'highlighted-box' : ''}>\n      Content\n    </div>\n  )\n}\n```\n\nPrefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.\n\nSee [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-cache-function-results.md",
    "content": "---\ntitle: Cache Repeated Function Calls\nimpact: MEDIUM\nimpactDescription: avoid redundant computation\ntags: javascript, cache, memoization, performance\n---\n\n## Cache Repeated Function Calls\n\nUse a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.\n\n**Incorrect (redundant computation):**\n\n```typescript\nfunction ProjectList({ projects }: { projects: Project[] }) {\n  return (\n    <div>\n      {projects.map(project => {\n        // slugify() called 100+ times for same project names\n        const slug = slugify(project.name)\n        \n        return <ProjectCard key={project.id} slug={slug} />\n      })}\n    </div>\n  )\n}\n```\n\n**Correct (cached results):**\n\n```typescript\n// Module-level cache\nconst slugifyCache = new Map<string, string>()\n\nfunction cachedSlugify(text: string): string {\n  if (slugifyCache.has(text)) {\n    return slugifyCache.get(text)!\n  }\n  const result = slugify(text)\n  slugifyCache.set(text, result)\n  return result\n}\n\nfunction ProjectList({ projects }: { projects: Project[] }) {\n  return (\n    <div>\n      {projects.map(project => {\n        // Computed only once per unique project name\n        const slug = cachedSlugify(project.name)\n        \n        return <ProjectCard key={project.id} slug={slug} />\n      })}\n    </div>\n  )\n}\n```\n\n**Simpler pattern for single-value functions:**\n\n```typescript\nlet isLoggedInCache: boolean | null = null\n\nfunction isLoggedIn(): boolean {\n  if (isLoggedInCache !== null) {\n    return isLoggedInCache\n  }\n  \n  isLoggedInCache = document.cookie.includes('auth=')\n  return isLoggedInCache\n}\n\n// Clear cache when auth changes\nfunction onAuthChange() {\n  isLoggedInCache = null\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\nReference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-cache-property-access.md",
    "content": "---\ntitle: Cache Property Access in Loops\nimpact: LOW-MEDIUM\nimpactDescription: reduces lookups\ntags: javascript, loops, optimization, caching\n---\n\n## Cache Property Access in Loops\n\nCache object property lookups in hot paths.\n\n**Incorrect (3 lookups × N iterations):**\n\n```typescript\nfor (let i = 0; i < arr.length; i++) {\n  process(obj.config.settings.value)\n}\n```\n\n**Correct (1 lookup total):**\n\n```typescript\nconst value = obj.config.settings.value\nconst len = arr.length\nfor (let i = 0; i < len; i++) {\n  process(value)\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-cache-storage.md",
    "content": "---\ntitle: Cache Storage API Calls\nimpact: LOW-MEDIUM\nimpactDescription: reduces expensive I/O\ntags: javascript, localStorage, storage, caching, performance\n---\n\n## Cache Storage API Calls\n\n`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.\n\n**Incorrect (reads storage on every call):**\n\n```typescript\nfunction getTheme() {\n  return localStorage.getItem('theme') ?? 'light'\n}\n// Called 10 times = 10 storage reads\n```\n\n**Correct (Map cache):**\n\n```typescript\nconst storageCache = new Map<string, string | null>()\n\nfunction getLocalStorage(key: string) {\n  if (!storageCache.has(key)) {\n    storageCache.set(key, localStorage.getItem(key))\n  }\n  return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n  localStorage.setItem(key, value)\n  storageCache.set(key, value)  // keep cache in sync\n}\n```\n\nUse a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.\n\n**Cookie caching:**\n\n```typescript\nlet cookieCache: Record<string, string> | null = null\n\nfunction getCookie(name: string) {\n  if (!cookieCache) {\n    cookieCache = Object.fromEntries(\n      document.cookie.split('; ').map(c => c.split('='))\n    )\n  }\n  return cookieCache[name]\n}\n```\n\n**Important (invalidate on external changes):**\n\nIf storage can change externally (another tab, server-set cookies), invalidate cache:\n\n```typescript\nwindow.addEventListener('storage', (e) => {\n  if (e.key) storageCache.delete(e.key)\n})\n\ndocument.addEventListener('visibilitychange', () => {\n  if (document.visibilityState === 'visible') {\n    storageCache.clear()\n  }\n})\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-combine-iterations.md",
    "content": "---\ntitle: Combine Multiple Array Iterations\nimpact: LOW-MEDIUM\nimpactDescription: reduces iterations\ntags: javascript, arrays, loops, performance\n---\n\n## Combine Multiple Array Iterations\n\nMultiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.\n\n**Incorrect (3 iterations):**\n\n```typescript\nconst admins = users.filter(u => u.isAdmin)\nconst testers = users.filter(u => u.isTester)\nconst inactive = users.filter(u => !u.isActive)\n```\n\n**Correct (1 iteration):**\n\n```typescript\nconst admins: User[] = []\nconst testers: User[] = []\nconst inactive: User[] = []\n\nfor (const user of users) {\n  if (user.isAdmin) admins.push(user)\n  if (user.isTester) testers.push(user)\n  if (!user.isActive) inactive.push(user)\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-early-exit.md",
    "content": "---\ntitle: Early Return from Functions\nimpact: LOW-MEDIUM\nimpactDescription: avoids unnecessary computation\ntags: javascript, functions, optimization, early-return\n---\n\n## Early Return from Functions\n\nReturn early when result is determined to skip unnecessary processing.\n\n**Incorrect (processes all items even after finding answer):**\n\n```typescript\nfunction validateUsers(users: User[]) {\n  let hasError = false\n  let errorMessage = ''\n  \n  for (const user of users) {\n    if (!user.email) {\n      hasError = true\n      errorMessage = 'Email required'\n    }\n    if (!user.name) {\n      hasError = true\n      errorMessage = 'Name required'\n    }\n    // Continues checking all users even after error found\n  }\n  \n  return hasError ? { valid: false, error: errorMessage } : { valid: true }\n}\n```\n\n**Correct (returns immediately on first error):**\n\n```typescript\nfunction validateUsers(users: User[]) {\n  for (const user of users) {\n    if (!user.email) {\n      return { valid: false, error: 'Email required' }\n    }\n    if (!user.name) {\n      return { valid: false, error: 'Name required' }\n    }\n  }\n\n  return { valid: true }\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-hoist-regexp.md",
    "content": "---\ntitle: Hoist RegExp Creation\nimpact: LOW-MEDIUM\nimpactDescription: avoids recreation\ntags: javascript, regexp, optimization, memoization\n---\n\n## Hoist RegExp Creation\n\nDon't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.\n\n**Incorrect (new RegExp every render):**\n\n```tsx\nfunction Highlighter({ text, query }: Props) {\n  const regex = new RegExp(`(${query})`, 'gi')\n  const parts = text.split(regex)\n  return <>{parts.map((part, i) => ...)}</>\n}\n```\n\n**Correct (memoize or hoist):**\n\n```tsx\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction Highlighter({ text, query }: Props) {\n  const regex = useMemo(\n    () => new RegExp(`(${escapeRegex(query)})`, 'gi'),\n    [query]\n  )\n  const parts = text.split(regex)\n  return <>{parts.map((part, i) => ...)}</>\n}\n```\n\n**Warning (global regex has mutable state):**\n\nGlobal regex (`/g`) has mutable `lastIndex` state:\n\n```typescript\nconst regex = /foo/g\nregex.test('foo')  // true, lastIndex = 3\nregex.test('foo')  // false, lastIndex = 0\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-index-maps.md",
    "content": "---\ntitle: Build Index Maps for Repeated Lookups\nimpact: LOW-MEDIUM\nimpactDescription: 1M ops to 2K ops\ntags: javascript, map, indexing, optimization, performance\n---\n\n## Build Index Maps for Repeated Lookups\n\nMultiple `.find()` calls by the same key should use a Map.\n\n**Incorrect (O(n) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  return orders.map(order => ({\n    ...order,\n    user: users.find(u => u.id === order.userId)\n  }))\n}\n```\n\n**Correct (O(1) per lookup):**\n\n```typescript\nfunction processOrders(orders: Order[], users: User[]) {\n  const userById = new Map(users.map(u => [u.id, u]))\n\n  return orders.map(order => ({\n    ...order,\n    user: userById.get(order.userId)\n  }))\n}\n```\n\nBuild map once (O(n)), then all lookups are O(1).\nFor 1000 orders × 1000 users: 1M ops → 2K ops.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-length-check-first.md",
    "content": "---\ntitle: Early Length Check for Array Comparisons\nimpact: MEDIUM-HIGH\nimpactDescription: avoids expensive operations when lengths differ\ntags: javascript, arrays, performance, optimization, comparison\n---\n\n## Early Length Check for Array Comparisons\n\nWhen comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.\n\nIn real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).\n\n**Incorrect (always runs expensive comparison):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Always sorts and joins, even when lengths differ\n  return current.sort().join() !== original.sort().join()\n}\n```\n\nTwo O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.\n\n**Correct (O(1) length check first):**\n\n```typescript\nfunction hasChanges(current: string[], original: string[]) {\n  // Early return if lengths differ\n  if (current.length !== original.length) {\n    return true\n  }\n  // Only sort when lengths match\n  const currentSorted = current.toSorted()\n  const originalSorted = original.toSorted()\n  for (let i = 0; i < currentSorted.length; i++) {\n    if (currentSorted[i] !== originalSorted[i]) {\n      return true\n    }\n  }\n  return false\n}\n```\n\nThis new approach is more efficient because:\n- It avoids the overhead of sorting and joining the arrays when lengths differ\n- It avoids consuming memory for the joined strings (especially important for large arrays)\n- It avoids mutating the original arrays\n- It returns early when a difference is found\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-min-max-loop.md",
    "content": "---\ntitle: Use Loop for Min/Max Instead of Sort\nimpact: LOW\nimpactDescription: O(n) instead of O(n log n)\ntags: javascript, arrays, performance, sorting, algorithms\n---\n\n## Use Loop for Min/Max Instead of Sort\n\nFinding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.\n\n**Incorrect (O(n log n) - sort to find latest):**\n\n```typescript\ninterface Project {\n  id: string\n  name: string\n  updatedAt: number\n}\n\nfunction getLatestProject(projects: Project[]) {\n  const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)\n  return sorted[0]\n}\n```\n\nSorts the entire array just to find the maximum value.\n\n**Incorrect (O(n log n) - sort for oldest and newest):**\n\n```typescript\nfunction getOldestAndNewest(projects: Project[]) {\n  const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)\n  return { oldest: sorted[0], newest: sorted[sorted.length - 1] }\n}\n```\n\nStill sorts unnecessarily when only min/max are needed.\n\n**Correct (O(n) - single loop):**\n\n```typescript\nfunction getLatestProject(projects: Project[]) {\n  if (projects.length === 0) return null\n  \n  let latest = projects[0]\n  \n  for (let i = 1; i < projects.length; i++) {\n    if (projects[i].updatedAt > latest.updatedAt) {\n      latest = projects[i]\n    }\n  }\n  \n  return latest\n}\n\nfunction getOldestAndNewest(projects: Project[]) {\n  if (projects.length === 0) return { oldest: null, newest: null }\n  \n  let oldest = projects[0]\n  let newest = projects[0]\n  \n  for (let i = 1; i < projects.length; i++) {\n    if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]\n    if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]\n  }\n  \n  return { oldest, newest }\n}\n```\n\nSingle pass through the array, no copying, no sorting.\n\n**Alternative (Math.min/Math.max for small arrays):**\n\n```typescript\nconst numbers = [5, 2, 8, 1, 9]\nconst min = Math.min(...numbers)\nconst max = Math.max(...numbers)\n```\n\nThis works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-set-map-lookups.md",
    "content": "---\ntitle: Use Set/Map for O(1) Lookups\nimpact: LOW-MEDIUM\nimpactDescription: O(n) to O(1)\ntags: javascript, set, map, data-structures, performance\n---\n\n## Use Set/Map for O(1) Lookups\n\nConvert arrays to Set/Map for repeated membership checks.\n\n**Incorrect (O(n) per check):**\n\n```typescript\nconst allowedIds = ['a', 'b', 'c', ...]\nitems.filter(item => allowedIds.includes(item.id))\n```\n\n**Correct (O(1) per check):**\n\n```typescript\nconst allowedIds = new Set(['a', 'b', 'c', ...])\nitems.filter(item => allowedIds.has(item.id))\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/js-tosorted-immutable.md",
    "content": "---\ntitle: Use toSorted() Instead of sort() for Immutability\nimpact: MEDIUM-HIGH\nimpactDescription: prevents mutation bugs in React state\ntags: javascript, arrays, immutability, react, state, mutation\n---\n\n## Use toSorted() Instead of sort() for Immutability\n\n`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.\n\n**Incorrect (mutates original array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Mutates the users prop array!\n  const sorted = useMemo(\n    () => users.sort((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Correct (creates new array):**\n\n```typescript\nfunction UserList({ users }: { users: User[] }) {\n  // Creates new sorted array, original unchanged\n  const sorted = useMemo(\n    () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n    [users]\n  )\n  return <div>{sorted.map(renderUser)}</div>\n}\n```\n\n**Why this matters in React:**\n\n1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only\n2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior\n\n**Browser support (fallback for older browsers):**\n\n`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:\n\n```typescript\n// Fallback for older browsers\nconst sorted = [...items].sort((a, b) => a.value - b.value)\n```\n\n**Other immutable array methods:**\n\n- `.toSorted()` - immutable sort\n- `.toReversed()` - immutable reverse\n- `.toSpliced()` - immutable splice\n- `.with()` - immutable element replacement\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-activity.md",
    "content": "---\ntitle: Use Activity Component for Show/Hide\nimpact: MEDIUM\nimpactDescription: preserves state/DOM\ntags: rendering, activity, visibility, state-preservation\n---\n\n## Use Activity Component for Show/Hide\n\nUse React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.\n\n**Usage:**\n\n```tsx\nimport { Activity } from 'react'\n\nfunction Dropdown({ isOpen }: Props) {\n  return (\n    <Activity mode={isOpen ? 'visible' : 'hidden'}>\n      <ExpensiveMenu />\n    </Activity>\n  )\n}\n```\n\nAvoids expensive re-renders and state loss.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md",
    "content": "---\ntitle: Animate SVG Wrapper Instead of SVG Element\nimpact: LOW\nimpactDescription: enables hardware acceleration\ntags: rendering, svg, css, animation, performance\n---\n\n## Animate SVG Wrapper Instead of SVG Element\n\nMany browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.\n\n**Incorrect (animating SVG directly - no hardware acceleration):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <svg \n      className=\"animate-spin\"\n      width=\"24\" \n      height=\"24\" \n      viewBox=\"0 0 24 24\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n    </svg>\n  )\n}\n```\n\n**Correct (animating wrapper div - hardware accelerated):**\n\n```tsx\nfunction LoadingSpinner() {\n  return (\n    <div className=\"animate-spin\">\n      <svg \n        width=\"24\" \n        height=\"24\" \n        viewBox=\"0 0 24 24\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" />\n      </svg>\n    </div>\n  )\n}\n```\n\nThis applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-conditional-render.md",
    "content": "---\ntitle: Use Explicit Conditional Rendering\nimpact: LOW\nimpactDescription: prevents rendering 0 or NaN\ntags: rendering, conditional, jsx, falsy-values\n---\n\n## Use Explicit Conditional Rendering\n\nUse explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.\n\n**Incorrect (renders \"0\" when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count && <span className=\"badge\">{count}</span>}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div>0</div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n\n**Correct (renders nothing when count is 0):**\n\n```tsx\nfunction Badge({ count }: { count: number }) {\n  return (\n    <div>\n      {count > 0 ? <span className=\"badge\">{count}</span> : null}\n    </div>\n  )\n}\n\n// When count = 0, renders: <div></div>\n// When count = 5, renders: <div><span class=\"badge\">5</span></div>\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-content-visibility.md",
    "content": "---\ntitle: CSS content-visibility for Long Lists\nimpact: HIGH\nimpactDescription: faster initial render\ntags: rendering, css, content-visibility, long-lists\n---\n\n## CSS content-visibility for Long Lists\n\nApply `content-visibility: auto` to defer off-screen rendering.\n\n**CSS:**\n\n```css\n.message-item {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 80px;\n}\n```\n\n**Example:**\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    <div className=\"overflow-y-auto h-screen\">\n      {messages.map(msg => (\n        <div key={msg.id} className=\"message-item\">\n          <Avatar user={msg.author} />\n          <div>{msg.content}</div>\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nFor 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-hoist-jsx.md",
    "content": "---\ntitle: Hoist Static JSX Elements\nimpact: LOW\nimpactDescription: avoids re-creation\ntags: rendering, jsx, static, optimization\n---\n\n## Hoist Static JSX Elements\n\nExtract static JSX outside components to avoid re-creation.\n\n**Incorrect (recreates element every render):**\n\n```tsx\nfunction LoadingSkeleton() {\n  return <div className=\"animate-pulse h-20 bg-gray-200\" />\n}\n\nfunction Container() {\n  return (\n    <div>\n      {loading && <LoadingSkeleton />}\n    </div>\n  )\n}\n```\n\n**Correct (reuses same element):**\n\n```tsx\nconst loadingSkeleton = (\n  <div className=\"animate-pulse h-20 bg-gray-200\" />\n)\n\nfunction Container() {\n  return (\n    <div>\n      {loading && loadingSkeleton}\n    </div>\n  )\n}\n```\n\nThis is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-hydration-no-flicker.md",
    "content": "---\ntitle: Prevent Hydration Mismatch Without Flickering\nimpact: MEDIUM\nimpactDescription: avoids visual flicker and hydration errors\ntags: rendering, ssr, hydration, localStorage, flicker\n---\n\n## Prevent Hydration Mismatch Without Flickering\n\nWhen rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.\n\n**Incorrect (breaks SSR):**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  // localStorage is not available on server - throws error\n  const theme = localStorage.getItem('theme') || 'light'\n  \n  return (\n    <div className={theme}>\n      {children}\n    </div>\n  )\n}\n```\n\nServer-side rendering will fail because `localStorage` is undefined.\n\n**Incorrect (visual flickering):**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  const [theme, setTheme] = useState('light')\n  \n  useEffect(() => {\n    // Runs after hydration - causes visible flash\n    const stored = localStorage.getItem('theme')\n    if (stored) {\n      setTheme(stored)\n    }\n  }, [])\n  \n  return (\n    <div className={theme}>\n      {children}\n    </div>\n  )\n}\n```\n\nComponent first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.\n\n**Correct (no flicker, no hydration mismatch):**\n\n```tsx\nfunction ThemeWrapper({ children }: { children: ReactNode }) {\n  return (\n    <>\n      <div id=\"theme-wrapper\">\n        {children}\n      </div>\n      <script\n        dangerouslySetInnerHTML={{\n          __html: `\n            (function() {\n              try {\n                var theme = localStorage.getItem('theme') || 'light';\n                var el = document.getElementById('theme-wrapper');\n                if (el) el.className = theme;\n              } catch (e) {}\n            })();\n          `,\n        }}\n      />\n    </>\n  )\n}\n```\n\nThe inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.\n\nThis pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-svg-precision.md",
    "content": "---\ntitle: Optimize SVG Precision\nimpact: LOW\nimpactDescription: reduces file size\ntags: rendering, svg, optimization, svgo\n---\n\n## Optimize SVG Precision\n\nReduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.\n\n**Incorrect (excessive precision):**\n\n```svg\n<path d=\"M 10.293847 20.847362 L 30.938472 40.192837\" />\n```\n\n**Correct (1 decimal place):**\n\n```svg\n<path d=\"M 10.3 20.8 L 30.9 40.2\" />\n```\n\n**Automate with SVGO:**\n\n```bash\nnpx svgo --precision=1 --multipass icon.svg\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rendering-usetransition-loading.md",
    "content": "---\ntitle: Use useTransition Over Manual Loading States\nimpact: LOW\nimpactDescription: reduces re-renders and improves code clarity\ntags: rendering, transitions, useTransition, loading, state\n---\n\n## Use useTransition Over Manual Loading States\n\nUse `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.\n\n**Incorrect (manual loading state):**\n\n```tsx\nfunction SearchResults() {\n  const [query, setQuery] = useState('')\n  const [results, setResults] = useState([])\n  const [isLoading, setIsLoading] = useState(false)\n\n  const handleSearch = async (value: string) => {\n    setIsLoading(true)\n    setQuery(value)\n    const data = await fetchResults(value)\n    setResults(data)\n    setIsLoading(false)\n  }\n\n  return (\n    <>\n      <input onChange={(e) => handleSearch(e.target.value)} />\n      {isLoading && <Spinner />}\n      <ResultsList results={results} />\n    </>\n  )\n}\n```\n\n**Correct (useTransition with built-in pending state):**\n\n```tsx\nimport { useTransition, useState } from 'react'\n\nfunction SearchResults() {\n  const [query, setQuery] = useState('')\n  const [results, setResults] = useState([])\n  const [isPending, startTransition] = useTransition()\n\n  const handleSearch = (value: string) => {\n    setQuery(value) // Update input immediately\n    \n    startTransition(async () => {\n      // Fetch and update results\n      const data = await fetchResults(value)\n      setResults(data)\n    })\n  }\n\n  return (\n    <>\n      <input onChange={(e) => handleSearch(e.target.value)} />\n      {isPending && <Spinner />}\n      <ResultsList results={results} />\n    </>\n  )\n}\n```\n\n**Benefits:**\n\n- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`\n- **Error resilience**: Pending state correctly resets even if the transition throws\n- **Better responsiveness**: Keeps the UI responsive during updates\n- **Interrupt handling**: New transitions automatically cancel pending ones\n\nReference: [useTransition](https://react.dev/reference/react/useTransition)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-defer-reads.md",
    "content": "---\ntitle: Defer State Reads to Usage Point\nimpact: MEDIUM\nimpactDescription: avoids unnecessary subscriptions\ntags: rerender, searchParams, localStorage, optimization\n---\n\n## Defer State Reads to Usage Point\n\nDon't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.\n\n**Incorrect (subscribes to all searchParams changes):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const searchParams = useSearchParams()\n\n  const handleShare = () => {\n    const ref = searchParams.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n\n**Correct (reads on demand, no subscription):**\n\n```tsx\nfunction ShareButton({ chatId }: { chatId: string }) {\n  const handleShare = () => {\n    const params = new URLSearchParams(window.location.search)\n    const ref = params.get('ref')\n    shareChat(chatId, { ref })\n  }\n\n  return <button onClick={handleShare}>Share</button>\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-dependencies.md",
    "content": "---\ntitle: Narrow Effect Dependencies\nimpact: LOW\nimpactDescription: minimizes effect re-runs\ntags: rerender, useEffect, dependencies, optimization\n---\n\n## Narrow Effect Dependencies\n\nSpecify primitive dependencies instead of objects to minimize effect re-runs.\n\n**Incorrect (re-runs on any user field change):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user])\n```\n\n**Correct (re-runs only when id changes):**\n\n```tsx\nuseEffect(() => {\n  console.log(user.id)\n}, [user.id])\n```\n\n**For derived state, compute outside effect:**\n\n```tsx\n// Incorrect: runs on width=767, 766, 765...\nuseEffect(() => {\n  if (width < 768) {\n    enableMobileMode()\n  }\n}, [width])\n\n// Correct: runs only on boolean transition\nconst isMobile = width < 768\nuseEffect(() => {\n  if (isMobile) {\n    enableMobileMode()\n  }\n}, [isMobile])\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-derived-state.md",
    "content": "---\ntitle: Subscribe to Derived State\nimpact: MEDIUM\nimpactDescription: reduces re-render frequency\ntags: rerender, derived-state, media-query, optimization\n---\n\n## Subscribe to Derived State\n\nSubscribe to derived boolean state instead of continuous values to reduce re-render frequency.\n\n**Incorrect (re-renders on every pixel change):**\n\n```tsx\nfunction Sidebar() {\n  const width = useWindowWidth()  // updates continuously\n  const isMobile = width < 768\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n\n**Correct (re-renders only when boolean changes):**\n\n```tsx\nfunction Sidebar() {\n  const isMobile = useMediaQuery('(max-width: 767px)')\n  return <nav className={isMobile ? 'mobile' : 'desktop'} />\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-functional-setstate.md",
    "content": "---\ntitle: Use Functional setState Updates\nimpact: MEDIUM\nimpactDescription: prevents stale closures and unnecessary callback recreations\ntags: react, hooks, useState, useCallback, callbacks, closures\n---\n\n## Use Functional setState Updates\n\nWhen updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.\n\n**Incorrect (requires state as dependency):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Callback must depend on items, recreated on every items change\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems([...items, ...newItems])\n  }, [items])  // ❌ items dependency causes recreations\n  \n  // Risk of stale closure if dependency is forgotten\n  const removeItem = useCallback((id: string) => {\n    setItems(items.filter(item => item.id !== id))\n  }, [])  // ❌ Missing items dependency - will use stale items!\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\nThe first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.\n\n**Correct (stable callbacks, no stale closures):**\n\n```tsx\nfunction TodoList() {\n  const [items, setItems] = useState(initialItems)\n  \n  // Stable callback, never recreated\n  const addItems = useCallback((newItems: Item[]) => {\n    setItems(curr => [...curr, ...newItems])\n  }, [])  // ✅ No dependencies needed\n  \n  // Always uses latest state, no stale closure risk\n  const removeItem = useCallback((id: string) => {\n    setItems(curr => curr.filter(item => item.id !== id))\n  }, [])  // ✅ Safe and stable\n  \n  return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />\n}\n```\n\n**Benefits:**\n\n1. **Stable callback references** - Callbacks don't need to be recreated when state changes\n2. **No stale closures** - Always operates on the latest state value\n3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks\n4. **Prevents bugs** - Eliminates the most common source of React closure bugs\n\n**When to use functional updates:**\n\n- Any setState that depends on the current state value\n- Inside useCallback/useMemo when state is needed\n- Event handlers that reference state\n- Async operations that update state\n\n**When direct updates are fine:**\n\n- Setting state to a static value: `setCount(0)`\n- Setting state from props/arguments only: `setName(newName)`\n- State doesn't depend on previous value\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-lazy-state-init.md",
    "content": "---\ntitle: Use Lazy State Initialization\nimpact: MEDIUM\nimpactDescription: wasted computation on every render\ntags: react, hooks, useState, performance, initialization\n---\n\n## Use Lazy State Initialization\n\nPass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.\n\n**Incorrect (runs on every render):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs on EVERY render, even after initialization\n  const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  // When query changes, buildSearchIndex runs again unnecessarily\n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs on every render\n  const [settings, setSettings] = useState(\n    JSON.parse(localStorage.getItem('settings') || '{}')\n  )\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\n**Correct (runs only once):**\n\n```tsx\nfunction FilteredList({ items }: { items: Item[] }) {\n  // buildSearchIndex() runs ONLY on initial render\n  const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))\n  const [query, setQuery] = useState('')\n  \n  return <SearchResults index={searchIndex} query={query} />\n}\n\nfunction UserProfile() {\n  // JSON.parse runs only on initial render\n  const [settings, setSettings] = useState(() => {\n    const stored = localStorage.getItem('settings')\n    return stored ? JSON.parse(stored) : {}\n  })\n  \n  return <SettingsForm settings={settings} onChange={setSettings} />\n}\n```\n\nUse lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.\n\nFor simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-memo-with-default-value.md",
    "content": "---\n\ntitle: Extract Default Non-primitive Parameter Value from Memoized Component to Constant\nimpact: MEDIUM\nimpactDescription: restores memoization by using a constant for default value\ntags: rerender, memo, optimization\n\n---\n\n## Extract Default Non-primitive Parameter Value from Memoized Component to Constant\n\nWhen memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.\n\nTo address this issue, extract the default value into a constant.\n\n**Incorrect (`onClick` has different values on every rerender):**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {\n  // ...\n})\n\n// Used without optional onClick\n<UserAvatar />\n```\n\n**Correct (stable default value):**\n\n```tsx\nconst NOOP = () => {};\n\nconst UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {\n  // ...\n})\n\n// Used without optional onClick\n<UserAvatar />\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-memo.md",
    "content": "---\ntitle: Extract to Memoized Components\nimpact: MEDIUM\nimpactDescription: enables early returns\ntags: rerender, memo, useMemo, optimization\n---\n\n## Extract to Memoized Components\n\nExtract expensive work into memoized components to enable early returns before computation.\n\n**Incorrect (computes avatar even when loading):**\n\n```tsx\nfunction Profile({ user, loading }: Props) {\n  const avatar = useMemo(() => {\n    const id = computeAvatarId(user)\n    return <Avatar id={id} />\n  }, [user])\n\n  if (loading) return <Skeleton />\n  return <div>{avatar}</div>\n}\n```\n\n**Correct (skips computation when loading):**\n\n```tsx\nconst UserAvatar = memo(function UserAvatar({ user }: { user: User }) {\n  const id = useMemo(() => computeAvatarId(user), [user])\n  return <Avatar id={id} />\n})\n\nfunction Profile({ user, loading }: Props) {\n  if (loading) return <Skeleton />\n  return (\n    <div>\n      <UserAvatar user={user} />\n    </div>\n  )\n}\n```\n\n**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md",
    "content": "---\ntitle: Do not wrap a simple expression with a primitive result type in useMemo\nimpact: LOW-MEDIUM\nimpactDescription: wasted computation on every render\ntags: rerender, useMemo, optimization\n---\n\n## Do not wrap a simple expression with a primitive result type in useMemo\n\nWhen an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.\nCalling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.\n\n**Incorrect:**\n\n```tsx\nfunction Header({ user, notifications }: Props) {\n  const isLoading = useMemo(() => {\n    return user.isLoading || notifications.isLoading\n  }, [user.isLoading, notifications.isLoading])\n\n  if (isLoading) return <Skeleton />\n  // return some markup\n}\n```\n\n**Correct:**\n\n```tsx\nfunction Header({ user, notifications }: Props) {\n  const isLoading = user.isLoading || notifications.isLoading\n\n  if (isLoading) return <Skeleton />\n  // return some markup\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/rerender-transitions.md",
    "content": "---\ntitle: Use Transitions for Non-Urgent Updates\nimpact: MEDIUM\nimpactDescription: maintains UI responsiveness\ntags: rerender, transitions, startTransition, performance\n---\n\n## Use Transitions for Non-Urgent Updates\n\nMark frequent, non-urgent state updates as transitions to maintain UI responsiveness.\n\n**Incorrect (blocks UI on every scroll):**\n\n```tsx\nfunction ScrollTracker() {\n  const [scrollY, setScrollY] = useState(0)\n  useEffect(() => {\n    const handler = () => setScrollY(window.scrollY)\n    window.addEventListener('scroll', handler, { passive: true })\n    return () => window.removeEventListener('scroll', handler)\n  }, [])\n}\n```\n\n**Correct (non-blocking updates):**\n\n```tsx\nimport { startTransition } from 'react'\n\nfunction ScrollTracker() {\n  const [scrollY, setScrollY] = useState(0)\n  useEffect(() => {\n    const handler = () => {\n      startTransition(() => setScrollY(window.scrollY))\n    }\n    window.addEventListener('scroll', handler, { passive: true })\n    return () => window.removeEventListener('scroll', handler)\n  }, [])\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-after-nonblocking.md",
    "content": "---\ntitle: Use after() for Non-Blocking Operations\nimpact: MEDIUM\nimpactDescription: faster response times\ntags: server, async, logging, analytics, side-effects\n---\n\n## Use after() for Non-Blocking Operations\n\nUse Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.\n\n**Incorrect (blocks response):**\n\n```tsx\nimport { logUserAction } from '@/app/utils'\n\nexport async function POST(request: Request) {\n  // Perform mutation\n  await updateDatabase(request)\n  \n  // Logging blocks the response\n  const userAgent = request.headers.get('user-agent') || 'unknown'\n  await logUserAction({ userAgent })\n  \n  return new Response(JSON.stringify({ status: 'success' }), {\n    status: 200,\n    headers: { 'Content-Type': 'application/json' }\n  })\n}\n```\n\n**Correct (non-blocking):**\n\n```tsx\nimport { after } from 'next/server'\nimport { headers, cookies } from 'next/headers'\nimport { logUserAction } from '@/app/utils'\n\nexport async function POST(request: Request) {\n  // Perform mutation\n  await updateDatabase(request)\n  \n  // Log after response is sent\n  after(async () => {\n    const userAgent = (await headers()).get('user-agent') || 'unknown'\n    const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'\n    \n    logUserAction({ sessionCookie, userAgent })\n  })\n  \n  return new Response(JSON.stringify({ status: 'success' }), {\n    status: 200,\n    headers: { 'Content-Type': 'application/json' }\n  })\n}\n```\n\nThe response is sent immediately while logging happens in the background.\n\n**Common use cases:**\n\n- Analytics tracking\n- Audit logging\n- Sending notifications\n- Cache invalidation\n- Cleanup tasks\n\n**Important notes:**\n\n- `after()` runs even if the response fails or redirects\n- Works in Server Actions, Route Handlers, and Server Components\n\nReference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-auth-actions.md",
    "content": "---\ntitle: Authenticate Server Actions Like API Routes\nimpact: CRITICAL\nimpactDescription: prevents unauthorized access to server mutations\ntags: server, server-actions, authentication, security, authorization\n---\n\n## Authenticate Server Actions Like API Routes\n\n**Impact: CRITICAL (prevents unauthorized access to server mutations)**\n\nServer Actions (functions with `\"use server\"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.\n\nNext.js documentation explicitly states: \"Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation.\"\n\n**Incorrect (no authentication check):**\n\n```typescript\n'use server'\n\nexport async function deleteUser(userId: string) {\n  // Anyone can call this! No auth check\n  await db.user.delete({ where: { id: userId } })\n  return { success: true }\n}\n```\n\n**Correct (authentication inside the action):**\n\n```typescript\n'use server'\n\nimport { verifySession } from '@/lib/auth'\nimport { unauthorized } from '@/lib/errors'\n\nexport async function deleteUser(userId: string) {\n  // Always check auth inside the action\n  const session = await verifySession()\n  \n  if (!session) {\n    throw unauthorized('Must be logged in')\n  }\n  \n  // Check authorization too\n  if (session.user.role !== 'admin' && session.user.id !== userId) {\n    throw unauthorized('Cannot delete other users')\n  }\n  \n  await db.user.delete({ where: { id: userId } })\n  return { success: true }\n}\n```\n\n**With input validation:**\n\n```typescript\n'use server'\n\nimport { verifySession } from '@/lib/auth'\nimport { z } from 'zod'\n\nconst updateProfileSchema = z.object({\n  userId: z.string().uuid(),\n  name: z.string().min(1).max(100),\n  email: z.string().email()\n})\n\nexport async function updateProfile(data: unknown) {\n  // Validate input first\n  const validated = updateProfileSchema.parse(data)\n  \n  // Then authenticate\n  const session = await verifySession()\n  if (!session) {\n    throw new Error('Unauthorized')\n  }\n  \n  // Then authorize\n  if (session.user.id !== validated.userId) {\n    throw new Error('Can only update own profile')\n  }\n  \n  // Finally perform the mutation\n  await db.user.update({\n    where: { id: validated.userId },\n    data: {\n      name: validated.name,\n      email: validated.email\n    }\n  })\n  \n  return { success: true }\n}\n```\n\nReference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-cache-lru.md",
    "content": "---\ntitle: Cross-Request LRU Caching\nimpact: HIGH\nimpactDescription: caches across requests\ntags: server, cache, lru, cross-request\n---\n\n## Cross-Request LRU Caching\n\n`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.\n\n**Implementation:**\n\n```typescript\nimport { LRUCache } from 'lru-cache'\n\nconst cache = new LRUCache<string, any>({\n  max: 1000,\n  ttl: 5 * 60 * 1000  // 5 minutes\n})\n\nexport async function getUser(id: string) {\n  const cached = cache.get(id)\n  if (cached) return cached\n\n  const user = await db.user.findUnique({ where: { id } })\n  cache.set(id, user)\n  return user\n}\n\n// Request 1: DB query, result cached\n// Request 2: cache hit, no DB query\n```\n\nUse when sequential user actions hit multiple endpoints needing the same data within seconds.\n\n**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.\n\n**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.\n\nReference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-cache-react.md",
    "content": "---\ntitle: Per-Request Deduplication with React.cache()\nimpact: MEDIUM\nimpactDescription: deduplicates within request\ntags: server, cache, react-cache, deduplication\n---\n\n## Per-Request Deduplication with React.cache()\n\nUse `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.\n\n**Usage:**\n\n```typescript\nimport { cache } from 'react'\n\nexport const getCurrentUser = cache(async () => {\n  const session = await auth()\n  if (!session?.user?.id) return null\n  return await db.user.findUnique({\n    where: { id: session.user.id }\n  })\n})\n```\n\nWithin a single request, multiple calls to `getCurrentUser()` execute the query only once.\n\n**Avoid inline objects as arguments:**\n\n`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.\n\n**Incorrect (always cache miss):**\n\n```typescript\nconst getUser = cache(async (params: { uid: number }) => {\n  return await db.user.findUnique({ where: { id: params.uid } })\n})\n\n// Each call creates new object, never hits cache\ngetUser({ uid: 1 })\ngetUser({ uid: 1 })  // Cache miss, runs query again\n```\n\n**Correct (cache hit):**\n\n```typescript\nconst getUser = cache(async (uid: number) => {\n  return await db.user.findUnique({ where: { id: uid } })\n})\n\n// Primitive args use value equality\ngetUser(1)\ngetUser(1)  // Cache hit, returns cached result\n```\n\nIf you must pass objects, pass the same reference:\n\n```typescript\nconst params = { uid: 1 }\ngetUser(params)  // Query runs\ngetUser(params)  // Cache hit (same reference)\n```\n\n**Next.js-Specific Note:**\n\nIn Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:\n\n- Database queries (Prisma, Drizzle, etc.)\n- Heavy computations\n- Authentication checks\n- File system operations\n- Any non-fetch async work\n\nUse `React.cache()` to deduplicate these operations across your component tree.\n\nReference: [React.cache documentation](https://react.dev/reference/react/cache)\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-dedup-props.md",
    "content": "---\ntitle: Avoid Duplicate Serialization in RSC Props\nimpact: LOW\nimpactDescription: reduces network payload by avoiding duplicate serialization\ntags: server, rsc, serialization, props, client-components\n---\n\n## Avoid Duplicate Serialization in RSC Props\n\n**Impact: LOW (reduces network payload by avoiding duplicate serialization)**\n\nRSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.\n\n**Incorrect (duplicates array):**\n\n```tsx\n// RSC: sends 6 strings (2 arrays × 3 items)\n<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />\n```\n\n**Correct (sends 3 strings):**\n\n```tsx\n// RSC: send once\n<ClientList usernames={usernames} />\n\n// Client: transform there\n'use client'\nconst sorted = useMemo(() => [...usernames].sort(), [usernames])\n```\n\n**Nested deduplication behavior:**\n\nDeduplication works recursively. Impact varies by data type:\n\n- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated\n- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference\n\n```tsx\n// string[] - duplicates everything\nusernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings\n\n// object[] - duplicates array structure only\nusers={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)\n```\n\n**Operations breaking deduplication (create new references):**\n\n- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`\n- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`\n\n**More examples:**\n\n```tsx\n// ❌ Bad\n<C users={users} active={users.filter(u => u.active)} />\n<C product={product} productName={product.name} />\n\n// ✅ Good\n<C users={users} />\n<C product={product} />\n// Do filtering/destructuring in client\n```\n\n**Exception:** Pass derived data when transformation is expensive or client doesn't need original.\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-parallel-fetching.md",
    "content": "---\ntitle: Parallel Data Fetching with Component Composition\nimpact: CRITICAL\nimpactDescription: eliminates server-side waterfalls\ntags: server, rsc, parallel-fetching, composition\n---\n\n## Parallel Data Fetching with Component Composition\n\nReact Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.\n\n**Incorrect (Sidebar waits for Page's fetch to complete):**\n\n```tsx\nexport default async function Page() {\n  const header = await fetchHeader()\n  return (\n    <div>\n      <div>{header}</div>\n      <Sidebar />\n    </div>\n  )\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n```\n\n**Correct (both fetch simultaneously):**\n\n```tsx\nasync function Header() {\n  const data = await fetchHeader()\n  return <div>{data}</div>\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n\nexport default function Page() {\n  return (\n    <div>\n      <Header />\n      <Sidebar />\n    </div>\n  )\n}\n```\n\n**Alternative with children prop:**\n\n```tsx\nasync function Header() {\n  const data = await fetchHeader()\n  return <div>{data}</div>\n}\n\nasync function Sidebar() {\n  const items = await fetchSidebarItems()\n  return <nav>{items.map(renderItem)}</nav>\n}\n\nfunction Layout({ children }: { children: ReactNode }) {\n  return (\n    <div>\n      <Header />\n      {children}\n    </div>\n  )\n}\n\nexport default function Page() {\n  return (\n    <Layout>\n      <Sidebar />\n    </Layout>\n  )\n}\n```\n"
  },
  {
    "path": "agents/skills/react-best-practices/rules/server-serialization.md",
    "content": "---\ntitle: Minimize Serialization at RSC Boundaries\nimpact: HIGH\nimpactDescription: reduces data transfer size\ntags: server, rsc, serialization, props\n---\n\n## Minimize Serialization at RSC Boundaries\n\nThe React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.\n\n**Incorrect (serializes all 50 fields):**\n\n```tsx\nasync function Page() {\n  const user = await fetchUser()  // 50 fields\n  return <Profile user={user} />\n}\n\n'use client'\nfunction Profile({ user }: { user: User }) {\n  return <div>{user.name}</div>  // uses 1 field\n}\n```\n\n**Correct (serializes only 1 field):**\n\n```tsx\nasync function Page() {\n  const user = await fetchUser()\n  return <Profile name={user.name} />\n}\n\n'use client'\nfunction Profile({ name }: { name: string }) {\n  return <div>{name}</div>\n}\n```\n"
  },
  {
    "path": "agents/skills/root-cause-tracing/SKILL.md",
    "content": "---\nname: root-cause-tracing\ndescription: Use when errors occur deep in execution and you need to trace back to find the original trigger - systematically traces bugs backward through call stack, adding instrumentation when needed, to identify source of invalid data or incorrect behavior\n---\n\n# Root Cause Tracing: Trace to Source\n\n## 1. Core Principle\n* **Fix the Source, Not the Symptom:** Never just patch where the error appears. Trace backward through the call stack until you find the original trigger.\n\n## 2. The Systematic Trace\n1. **Observe Symptom:** Identify the exact failure point (e.g., git init failed).\n2. **Immediate Cause:** Find the line of code executing the failing operation.\n3. **Reverse Call Chain:** Use stack traces to see what called that function, and what called that (e.g., Test -> Project.create() -> Session.init() -> exec()).\n4. **Data Origin:** Identify which variable was invalid (empty string, null, etc.) and find exactly where it was initialized or mutated.\n\n## 3. Instrumentation (When Manual Tracing Fails)\nIf the origin is unclear, inject temporary debug logs **before** the failing operation:\n\n```ts\n// Example Instrumentation\nconsole.error(\"DEBUG TRACE:\", {\n  input,\n  cwd: process.cwd(),\n  stack: new Error().stack\n});\n```\n\n* **Pro Tip:** Use console.error in tests to bypass standard log suppression.\n\n## 4. Defense-in-Depth\nOnce the source is fixed, apply multi-layer validation:\n* **Layer 1:** Fix the original trigger (e.g., change variable to a getter that throws).\n* **Layer 2:** Add input validation at the entry point of each intermediate function.\n* **Layer 3:** Add environment guards (e.g., \"Refuse to run in source directory\").\n\n## 5. Verification Strategy\n* **Unit Test:** Write a deterministic test only for complex logic or data transformations.\n* **Manual/Lint:** Skip tests for simple UI changes or CRUD; rely on type-checking and manual verification.\n\n## 6. Tracing Flow\n* **Found Cause?** -> Can you go one level up?\n* **Yes:** Trace backward.\n* **No (Reached Source):** Fix here **and** add validation at every layer below.\n* **Result:** The bug becomes impossible to reproduce.\n\n> **CRITICAL:** Fixing a symptom without finding the trigger is a \"dead end\" fix. Always hunt the original caller.\n"
  },
  {
    "path": "agents/skills/session-retrospective/SKILL.md",
    "content": "---\nname: session-retrospective\ndescription: At end of difficult sessions, analyze friction points and propose concrete improvements.\n---\n\n## Trigger\n- User asks \"what made this session hard?\"\n- Multiple failed attempts on same task\n- User explicitly requests retrospective\n- Session took significantly longer than expected\n\n## Process\n\n1. **Summarize Difficulties**\n   - List specific moments where you struggled\n   - Note repeated patterns (e.g., \"3rd edit attempt failed\")\n   - Count iterations/wasted effort\n\n2. **Root Cause Analysis**\n   For each difficulty, identify:\n   - Is it a tool limitation?\n   - Is it missing information/context?\n   - Is it workflow/order of operations?\n   - Is it environmental (formatters, external changes)?\n\n3. **Propose Improvements**\n   Concrete solutions only:\n   - New tool/extension\n   - Workflow change\n   - Information to pre-fetch\n   - Configuration to auto-apply\n\n4. **Estimate Impact**\n   For each proposal: \"Would have saved X minutes/tokens\"\n\n## Output Format\n```\nDifficulties:\n1. [What happened] → [Root cause]\n2. ...\n\nImprovements:\n1. [Solution] → [Time saved estimate]\n2. ...\n```\n\n## Rules\n- Be specific with examples from the session\n- No vague suggestions (\"be better at X\")\n- Each proposal must be actionable\n- Prefer automation over manual steps\n"
  },
  {
    "path": "agents/skills/web-design-guidelines/SKILL.md",
    "content": "---\nname: web-design-guidelines\ndescription: Review UI code for Web Interface Guidelines compliance. Use when asked to \"review my UI\", \"check accessibility\", \"audit design\", \"review UX\", or \"check my site against best practices\".\nmetadata:\n  author: vercel\n  version: \"1.0.0\"\n---\n\n# Web Interface Guidelines\n\nReview files for compliance with Web Interface Guidelines.\n\n## How It Works\n\n1. Fetch the latest guidelines from the source URL below\n2. Read the specified files (or prompt user for files/pattern)\n3. Check against all rules in the fetched guidelines\n4. Output findings in the terse `file:line` format\n\n## Usage\n\nWhen a user provides a file or pattern argument:\n1. Fetch guidelines from the source URL above\n2. Read the specified files\n3. Apply all rules from the fetched guidelines\n4. Output findings using the format specified in the guidelines\n\nIf no files specified, ask the user which files to review.\n\n# Core Guidelines\n\nReview these files for compliance: $ARGUMENTS\n\nRead files, check against rules below. Output concise but comprehensive—sacrifice grammar for brevity. High signal-to-noise.\n\n## Rules\n\n### Accessibility\n\n- Icon-only buttons need `aria-label`\n- Form controls need `<label>` or `aria-label`\n- Interactive elements need keyboard handlers (`onKeyDown`/`onKeyUp`)\n- `<button>` for actions, `<a>`/`<Link>` for navigation (not `<div onClick>`)\n- Images need `alt` (or `alt=\"\"` if decorative)\n- Decorative icons need `aria-hidden=\"true\"`\n- Async updates (toasts, validation) need `aria-live=\"polite\"`\n- Use semantic HTML (`<button>`, `<a>`, `<label>`, `<table>`) before ARIA\n- Headings hierarchical `<h1>`–`<h6>`; include skip link for main content\n- `scroll-margin-top` on heading anchors\n\n### Focus States\n\n- Interactive elements need visible focus: `focus-visible:ring-*` or equivalent\n- Never `outline-none` / `outline: none` without focus replacement\n- Use `:focus-visible` over `:focus` (avoid focus ring on click)\n- Group focus with `:focus-within` for compound controls\n\n### Forms\n\n- Inputs need `autocomplete` and meaningful `name`\n- Use correct `type` (`email`, `tel`, `url`, `number`) and `inputmode`\n- Never block paste (`onPaste` + `preventDefault`)\n- Labels clickable (`htmlFor` or wrapping control)\n- Disable spellcheck on emails, codes, usernames (`spellCheck={false}`)\n- Checkboxes/radios: label + control share single hit target (no dead zones)\n- Submit button stays enabled until request starts; spinner during request\n- Errors inline next to fields; focus first error on submit\n- Placeholders end with `…` and show example pattern\n- `autocomplete=\"off\"` on non-auth fields to avoid password manager triggers\n- Warn before navigation with unsaved changes (`beforeunload` or router guard)\n\n### Animation\n\n- Honor `prefers-reduced-motion` (provide reduced variant or disable)\n- Animate `transform`/`opacity` only (compositor-friendly)\n- Never `transition: all`—list properties explicitly\n- Set correct `transform-origin`\n- SVG: transforms on `<g>` wrapper with `transform-box: fill-box; transform-origin: center`\n- Animations interruptible—respond to user input mid-animation\n\n### Typography\n\n- `…` not `...`\n- Curly quotes `\"` `\"` not straight `\"`\n- Non-breaking spaces: `10&nbsp;MB`, `⌘&nbsp;K`, brand names\n- Loading states end with `…`: `\"Loading…\"`, `\"Saving…\"`\n- `font-variant-numeric: tabular-nums` for number columns/comparisons\n- Use `text-wrap: balance` or `text-pretty` on headings (prevents widows)\n\n### Content Handling\n\n- Text containers handle long content: `truncate`, `line-clamp-*`, or `break-words`\n- Flex children need `min-w-0` to allow text truncation\n- Handle empty states—don't render broken UI for empty strings/arrays\n- User-generated content: anticipate short, average, and very long inputs\n\n### Images\n\n- `<img>` needs explicit `width` and `height` (prevents CLS)\n- Below-fold images: `loading=\"lazy\"`\n- Above-fold critical images: `priority` or `fetchpriority=\"high\"`\n\n### Performance\n\n- Large lists (>50 items): virtualize (`virtua`, `content-visibility: auto`)\n- No layout reads in render (`getBoundingClientRect`, `offsetHeight`, `offsetWidth`, `scrollTop`)\n- Batch DOM reads/writes; avoid interleaving\n- Prefer uncontrolled inputs; controlled inputs must be cheap per keystroke\n- Add `<link rel=\"preconnect\">` for CDN/asset domains\n- Critical fonts: `<link rel=\"preload\" as=\"font\">` with `font-display: swap`\n\n### Navigation & State\n\n- URL reflects state—filters, tabs, pagination, expanded panels in query params\n- Links use `<a>`/`<Link>` (Cmd/Ctrl+click, middle-click support)\n- Deep-link all stateful UI (if uses `useState`, consider URL sync via nuqs or similar)\n- Destructive actions need confirmation modal or undo window—never immediate\n\n### Touch & Interaction\n\n- `touch-action: manipulation` (prevents double-tap zoom delay)\n- `-webkit-tap-highlight-color` set intentionally\n- `overscroll-behavior: contain` in modals/drawers/sheets\n- During drag: disable text selection, `inert` on dragged elements\n- `autoFocus` sparingly—desktop only, single primary input; avoid on mobile\n\n### Safe Areas & Layout\n\n- Full-bleed layouts need `env(safe-area-inset-*)` for notches\n- Avoid unwanted scrollbars: `overflow-x-hidden` on containers, fix content overflow\n- Flex/grid over JS measurement for layout\n\n### Dark Mode & Theming\n\n- `color-scheme: dark` on `<html>` for dark themes (fixes scrollbar, inputs)\n- `<meta name=\"theme-color\">` matches page background\n- Native `<select>`: explicit `background-color` and `color` (Windows dark mode)\n\n### Locale & i18n\n\n- Dates/times: use `Intl.DateTimeFormat` not hardcoded formats\n- Numbers/currency: use `Intl.NumberFormat` not hardcoded formats\n- Detect language via `Accept-Language` / `navigator.languages`, not IP\n\n### Hydration Safety\n\n- Inputs with `value` need `onChange` (or use `defaultValue` for uncontrolled)\n- Date/time rendering: guard against hydration mismatch (server vs client)\n- `suppressHydrationWarning` only where truly needed\n\n### Hover & Interactive States\n\n- Buttons/links need `hover:` state (visual feedback)\n- Interactive states increase contrast: hover/active/focus more prominent than rest\n\n### Content & Copy\n\n- Active voice: \"Install the CLI\" not \"The CLI will be installed\"\n- Title Case for headings/buttons (Chicago style)\n- Numerals for counts: \"8 deployments\" not \"eight\"\n- Specific button labels: \"Save API Key\" not \"Continue\"\n- Error messages include fix/next step, not just problem\n- Second person; avoid first person\n- `&` over \"and\" where space-constrained\n\n### Anti-patterns (flag these)\n\n- `user-scalable=no` or `maximum-scale=1` disabling zoom\n- `onPaste` with `preventDefault`\n- `transition: all`\n- `outline-none` without focus-visible replacement\n- Inline `onClick` navigation without `<a>`\n- `<div>` or `<span>` with click handlers (should be `<button>`)\n- Images without dimensions\n- Large arrays `.map()` without virtualization\n- Form inputs without labels\n- Icon buttons without `aria-label`\n- Hardcoded date/number formats (use `Intl.*`)\n- `autoFocus` without clear justification\n\n## Output Format\n\nGroup by file. Use `file:line` format (VS Code clickable). Terse findings.\n\n```text\n## src/Button.tsx\n\nsrc/Button.tsx:42 - icon button missing aria-label\nsrc/Button.tsx:18 - input lacks label\nsrc/Button.tsx:55 - animation missing prefers-reduced-motion\nsrc/Button.tsx:67 - transition: all → list properties\n\n## src/Modal.tsx\n\nsrc/Modal.tsx:12 - missing overscroll-behavior: contain\nsrc/Modal.tsx:34 - \"...\" → \"…\"\n\n## src/Card.tsx\n\n✓ pass\n```\n\nState issue + location. Skip explanation unless fix non-obvious. No preamble.\n"
  },
  {
    "path": "agents/skills/wydt/SKILL.md",
    "content": "---\nname: wydt\ndescription: Ask AI's opinion on code - identify improvements, code smells, anti-patterns, and quality issues. Suggest fixes in structured format with Problem/Solution/Why triplets. Read-only, makes no changes.\n---\n\n# WYDT - What You Doing There?\n\n## Overview\n\nQuick code quality check. Get AI's eyes on your code to spot issues you might have missed. Shorter version of code review.\n\n**Use when:**\n- Before committing code\n- After finishing a feature\n- When something \"feels off\" but you can't pinpoint it\n- Code review prep\n- Learning a new codebase\n\n## Core Principle\n\n**Read only. Suggest only. Never modify code.**\nThis skill identifies issues and describes solutions. It does not implement fixes.\n\n## The WYDT Process\n\n### List of files to check:\n\n$ARGUMENTS\n\n### Phase 1: Read & Understand\n\n1. **Read the code completely** - don't skim\n2. **Identify the purpose** - what is this code trying to do?\n3. **Note context** - where does it fit in the architecture?\n\n### Phase 2: Analyze\n\nCheck for:\n\n| Category | What to Look For |\n|----------|------------------|\n| **Code Smells** | Duplication, long methods, large classes, feature envy |\n| **Anti-patterns** | God objects, singleton abuse, premature abstraction |\n| **Quality** | Error handling, edge cases, null safety, type safety |\n| **Performance** | Unnecessary loops, repeated operations, memory leaks |\n| **Readability** | Naming, comments, complexity, organization |\n| **Maintainability** | Coupling, testability, single responsibility |\n| **Security** | Injection risks, exposed secrets, unsafe operations |\n| **Idioms** | Language patterns missed, unconventional style |\n\n### Phase 3: Structure Findings\n\n**Format each finding as:**\n\n```\nN. **Problem**: [clear description of the issue]\n   **Solution**: [concrete, actionable fix description]\n   **Why**: [impact of the issue - bugs, maintenance, readability, etc.]\n```\n\n**Severity indicators (prefix with emoji):**\n- 🔴 **Critical** - Bug risk, security issue, broken behavior\n- 🟡 **Warning** - Code smell, maintenance burden, tech debt\n- 🟢 **Suggestion** - Style, minor improvement, nice-to-have\n\n### Phase 4: Prioritize\n\nOrder findings by:\n1. Critical issues first\n2. Quick wins second\n3. Architectural concerns last (with discussion prompt)\n\n## Example Output\n\n```\n🔴 Critical\n1. **Problem**: No validation on `userId` parameter before database query\n   **Solution**: Add input validation - check type, format, and sanitize before use\n   **Why**: Unvalidated input can lead to injection attacks or unexpected crashes\n\n🟡 Warning\n2. **Problem**: Function `processData` does 4 different things (parse, validate, transform, save)\n   **Solution**: Split into 4 focused functions, compose them in a pipeline\n   **Why**: Multi-purpose functions are harder to test, debug, and modify without side effects\n\n3. **Problem**: Duplicate logic for formatting dates in 3 files\n   **Solution**: Extract to shared utility function and import where needed\n   **Why**: Duplication means fixes must happen in multiple places - inevitable inconsistency\n\n🟢 Suggestion\n4. **Problem**: Variable `x` doesn't describe what it holds\n   **Solution**: Rename to `activeUserCount` or similar descriptive name\n   **Why**: Code is read 10x more than written; unclear names slow down every future reader\n```\n\n## Universal Red Flags\n\nAlways mention if found:\n\n- No error handling for operations that can fail\n- Silent failures (empty catch blocks, swallowed errors)\n- Type assertions without validation\n- Mutable shared state without clear ownership\n- Logic mixed with presentation/data access\n- Operations that should be batched but are done one-by-one\n- Hardcoded values that should be configurable\n- Missing input validation on boundaries\n- Resource leaks (unclosed connections, unsubscribed listeners)\n- Boolean parameters that control significant behavior changes\n\n## When NOT to Use\n\n- Code you don't understand at all (ask for explanation first)\n- Generated/boilerplate code (unless customizing)\n- Third-party library internals\n- During active debugging (use debugging skill instead)\n\n## Output Limits\n\n- Maximum 10 findings per run\n- If more exist, prioritize critical/warning over suggestions\n- Mention at end if additional issues were found beyond the limit\n"
  },
  {
    "path": "direnv/direnvrc",
    "content": ": ${XDG_RUNTIME_DIR:=/tmp/nix-direnv/$UID}\ndeclare -A direnv_layout_dirs\ndirenv_layout_dir() {\n    local hash path\n    echo \"${direnv_layout_dirs[$PWD]:=$(\n        hash=\"$(sha1sum - <<< \"$PWD\" | head -c40)\"\n        path=\"${PWD//[^a-zA-Z0-9]/-}\"\n        echo \"${XDG_RUNTIME_DIR}/direnv/layouts/${hash}${path}\"\n    )}\"\n}\n"
  },
  {
    "path": "fish/conf.d/abbr.fish",
    "content": "# git stuff\nabbr -a ga    \"git add\"\nabbr -a gc    \"git commit\"\nabbr -a gs    \"git status\"\nabbr -a gd    \"git diff\"\nabbr -a gds   \"git diff --staged\"\nabbr -a gr    \"git restore\"\nabbr -a gpush \"git push\"\n\n# save current work\nabbr -a gwip 'git add -A; git rm (git ls-files --deleted) 2> /dev/null; git commit -m \"[WIP]: '(date)'\"'\n\n# Logging helpers\n# might be useful later idk\n# abbr -a gls 'git log --pretty=format:\"%C(yellow)%h%Cred%d\\\\ %Creset%s%Cblue\\\\ [%cn]\" --decorate'\n# abbr -a gll 'git log --pretty=format:\"%C(yellow)%h%Cred%d\\\\ %Creset%s%Cblue\\\\ [%cn]\" --decorate --numstat'\n# abbr -a gdate 'git log --pretty=format:\"%C(yellow)%h\\\\ %ad%Cred%d\\\\ %Creset%s%Cblue\\\\ [%cn]\" --decorate --date=relative'\n# abbr -a gdatelong 'git log --pretty=format:\"%C(yellow)%h\\\\ %ad%Cred%d\\\\ %Creset%s%Cblue\\\\ [%cn]\" --decorate --date=short'\n\n# config stuff\nabbr -a nvimconf \"nvim ~/.config/nvim/init.lua\"\nabbr -a vnim \"nvim\" # I got this typo quit a lot\n\nabbr -a ls \"eza --colour=always --group-directories-first --sort=name\"\n\n# shortcut for youtube-dl when downloading music\nabbr -a ytmusic 'yt-dlp -f251 -x --embed-metadata --embed-thumbnail'\n"
  },
  {
    "path": "fish/conf.d/alias.fish",
    "content": "alias :Q=\"exit\"\nalias :q=\"exit\"\n\nalias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'\n"
  },
  {
    "path": "fish/conf.d/colour.fish",
    "content": "set -U fish_color_autosuggestion      brblack\nset -U fish_color_cancel              -r\nset -U fish_color_command             brgreen\nset -U fish_color_comment             brmagenta\nset -U fish_color_cwd                 green\nset -U fish_color_cwd_root            red\nset -U fish_color_end                 brmagenta\nset -U fish_color_error               brred\nset -U fish_color_escape              brcyan\nset -U fish_color_history_current     --bold\nset -U fish_color_host                normal\nset -U fish_color_match               --background=brblue\nset -U fish_color_normal              normal\nset -U fish_color_operator            cyan\nset -U fish_color_param               brblue\nset -U fish_color_quote               yellow\nset -U fish_color_redirection         bryellow\nset -U fish_color_search_match        'bryellow' '--background=brblack'\nset -U fish_color_selection           'white' '--bold' '--background=brblack'\nset -U fish_color_status              red\nset -U fish_color_user                brgreen\nset -U fish_color_valid_path          --underline\nset -U fish_pager_color_completion    normal\nset -U fish_pager_color_description   yellow\nset -U fish_pager_color_prefix        'white' '--bold' '--underline'\nset -U fish_pager_color_progress      'brwhite' '--background=cyan'\n"
  },
  {
    "path": "fish/conf.d/foreign.fish",
    "content": "fenv \"source ~/.profile\"\n"
  },
  {
    "path": "fish/conf.d/manpage.fish",
    "content": "# Start blinking | blue\nset -Ux LESS_TERMCAP_mb (printf '\\033[1;34m')\n# Start bold | blue\nset -Ux LESS_TERMCAP_md (printf '\\033[1;34m')\n# Start stand out | yellow\nset -Ux LESS_TERMCAP_so (printf '\\033[1;33m')\n# End standout\nset -Ux LESS_TERMCAP_se (printf '\\033[0;10m')\n# Start underline | magenta\nset -Ux LESS_TERMCAP_us (printf '\\033[4;35m')\n# End Underline\nset -Ux LESS_TERMCAP_ue (printf '\\033[0;10m')\n# End bold, blinking, standout, underline\nset -Ux LESS_TERMCAP_me (printf '\\033[0;10m')\nset -Ux GROFF_NO_SGR 1\n"
  },
  {
    "path": "fish/conf.d/vi-mode.fish",
    "content": "# vi mode\nfish_vi_key_bindings\n\n# Set the cursor shapes for the different vi modes.\nset fish_cursor_default     block      blink\nset fish_cursor_insert      line       blink\nset fish_cursor_replace_one underscore blink\nset fish_cursor_visual      block\n"
  },
  {
    "path": "fish/config.fish",
    "content": "# disable greeting\nset fish_greeting\n\nstarship init fish | source\n\nzoxide init fish --cmd cd | source\n\nset -gx PNPM_HOME \"/home/elianiva/.local/share/pnpm\"\nset -gx PATH \"$PNPM_HOME\" $PATH\n\nfunction zellij_tab_name_update --on-event fish_preexec\n    if set -q ZELLIJ\n        set title (string split ' ' $argv)[1]\n        command nohup zellij action rename-tab $title >/dev/null 2>&1\n    end\nend\n\nfunction fish_mode_prompt\n  switch $fish_bind_mode\n    case default\n      echo -en \"\\e[2 q\"\n    case insert\n      echo -en \"\\e[6 q\"\n    case replace_one\n      echo -en \"\\e[4 q\"\n    case visual\n      echo -en \"\\e[2 q\"\n      set_color -o brwhite\n    case '*'\n      echo -en \"\\e[2 q\"\n  end\n  set_color normal\nend\n\n# Added by LM Studio CLI (lms)\nset -gx PATH $PATH /Users/elianiva/.lmstudio/bin\n# End of LM Studio CLI section\n\n"
  },
  {
    "path": "fish/fish_plugins",
    "content": "oh-my-fish/plugin-foreign-env\n"
  },
  {
    "path": "fish/fish_variables",
    "content": "# This file contains fish universal variable definitions.\n# VERSION: 3.0\nSETUVAR --export GROFF_NO_SGR:1\nSETUVAR --export LESS_TERMCAP_mb:\\x1b\\x5b1\\x3b34m\nSETUVAR --export LESS_TERMCAP_md:\\x1b\\x5b1\\x3b34m\nSETUVAR --export LESS_TERMCAP_me:\\x1b\\x5b0\\x3b10m\nSETUVAR --export LESS_TERMCAP_se:\\x1b\\x5b0\\x3b10m\nSETUVAR --export LESS_TERMCAP_so:\\x1b\\x5b1\\x3b33m\nSETUVAR --export LESS_TERMCAP_ue:\\x1b\\x5b0\\x3b10m\nSETUVAR --export LESS_TERMCAP_us:\\x1b\\x5b4\\x3b35m\nSETUVAR --export NIXPKGS_ALLOW_INSECURE:1\nSETUVAR __fish_initialized:3800\nSETUVAR _fish_abbr__3A_Q:exit\nSETUVAR _fish_abbr__3A_q:exit\nSETUVAR _fish_abbr_aliasconf:nvim\\x20\\x7e/\\x2econfig/aliasrc\nSETUVAR _fish_abbr_awsconf:nvim\\x20\\x7e/\\x2econfig/awesome/rc\\x2elua\nSETUVAR _fish_abbr_barconf:nvim\\x20\\x7e/\\x2econfig/waybar/config\nSETUVAR _fish_abbr_fishconf:nvim\\x20\\x7e/\\x2econfig/fish/config\\x2efish\nSETUVAR _fish_abbr_ga:git\\x20add\nSETUVAR _fish_abbr_gc:git\\x20commit\nSETUVAR _fish_abbr_gd:git\\x20diff\nSETUVAR _fish_abbr_gdate:git\\x20log\\x20\\x2d\\x2dpretty\\x3dformat\\x3a\\x22\\x25C\\x28yellow\\x29\\x25h\\x5c\\x20\\x25ad\\x25Cred\\x25d\\x5c\\x20\\x25Creset\\x25s\\x25Cblue\\x5c\\x20\\x5b\\x25cn\\x5d\\x22\\x20\\x2d\\x2ddecorate\\x20\\x2d\\x2ddate\\x3drelative\nSETUVAR _fish_abbr_gdatelong:git\\x20log\\x20\\x2d\\x2dpretty\\x3dformat\\x3a\\x22\\x25C\\x28yellow\\x29\\x25h\\x5c\\x20\\x25ad\\x25Cred\\x25d\\x5c\\x20\\x25Creset\\x25s\\x25Cblue\\x5c\\x20\\x5b\\x25cn\\x5d\\x22\\x20\\x2d\\x2ddecorate\\x20\\x2d\\x2ddate\\x3dshort\nSETUVAR _fish_abbr_gds:git\\x20diff\\x20\\x2d\\x2dstaged\nSETUVAR _fish_abbr_gll:git\\x20log\\x20\\x2d\\x2dpretty\\x3dformat\\x3a\\x22\\x25C\\x28yellow\\x29\\x25h\\x25Cred\\x25d\\x5c\\x20\\x25Creset\\x25s\\x25Cblue\\x5c\\x20\\x5b\\x25cn\\x5d\\x22\\x20\\x2d\\x2ddecorate\\x20\\x2d\\x2dnumstat\nSETUVAR _fish_abbr_gls:git\\x20log\\x20\\x2d\\x2dpretty\\x3dformat\\x3a\\x22\\x25C\\x28yellow\\x29\\x25h\\x25Cred\\x25d\\x5c\\x20\\x25Creset\\x25s\\x25Cblue\\x5c\\x20\\x5b\\x25cn\\x5d\\x22\\x20\\x2d\\x2ddecorate\nSETUVAR _fish_abbr_gpush:git\\x20push\nSETUVAR _fish_abbr_gr:git\\x20restore\nSETUVAR _fish_abbr_gs:git\\x20status\nSETUVAR _fish_abbr_gss:git\\x20status\\x20\\x2d\\x2dstaged\nSETUVAR _fish_abbr_gwip:git\\x20add\\x20\\x2dA\\x3b\\x20git\\x20rm\\x20\\x28git\\x20ls\\x2dfiles\\x20\\x2d\\x2ddeleted\\x29\\x202\\x3e\\x20/dev/null\\x3b\\x20git\\x20commit\\x20\\x2dm\\x20\\x22\\x5bWIP\\x5d\\x3a\\x20Tue\\x2018\\x20Oct\\x202022\\x2009\\x3a57\\x3a20\\x20AM\\x20\\x2b07\\x22\nSETUVAR _fish_abbr_kittyconf:nvim\\x20\\x7e/\\x2econfig/kitty/kitty\\x2econf\nSETUVAR _fish_abbr_ls:exa\\x20\\x2d\\x2dcolour\\x3dalways\\x20\\x2d\\x2dgroup\\x2ddirectories\\x2dfirst\\x20\\x2d\\x2dsort\\x3dname\nSETUVAR _fish_abbr_nvimconf:nvim\\x20\\x7e/\\x2econfig/nvim/init\\x2elua\nSETUVAR _fish_abbr_swayconf:nvim\\x20\\x7e/\\x2econfig/sway/config\nSETUVAR _fish_abbr_vnim:nvim\nSETUVAR _fish_abbr_ytmusic:yt\\x2ddlp\\x20\\x2df251\\x20\\x2dx\\x20\\x2d\\x2dembed\\x2dmetadata\\x20\\x2d\\x2dembed\\x2dthumbnail\nSETUVAR _fish_abbr_zshconf:nvim\\x20\\x7e/\\x2ezshrc\nSETUVAR _fisher_oh_2D_my_2D_fish_2F_plugin_2D_foreign_2D_env_files:/home/elianiva/\\x2econfig/fish/functions/fenv\\x2eapply\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2emain\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2eparse\\x2eafter\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2eparse\\x2ebefore\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2eparse\\x2ediff\\x2efish\\x1e/home/elianiva/\\x2econfig/fish/functions/fenv\\x2eparse\\x2edivider\\x2efish\nSETUVAR _fisher_plugins:oh\\x2dmy\\x2dfish/plugin\\x2dforeign\\x2denv\nSETUVAR fish_color_autosuggestion:brblack\nSETUVAR fish_color_cancel:\\x2dr\nSETUVAR fish_color_command:brgreen\nSETUVAR fish_color_comment:brmagenta\nSETUVAR fish_color_cwd:green\nSETUVAR fish_color_cwd_root:red\nSETUVAR fish_color_end:brmagenta\nSETUVAR fish_color_error:brred\nSETUVAR fish_color_escape:brcyan\nSETUVAR fish_color_history_current:\\x2d\\x2dbold\nSETUVAR fish_color_host:normal\nSETUVAR fish_color_host_remote:yellow\nSETUVAR fish_color_match:\\x2d\\x2dbackground\\x3dbrblue\nSETUVAR fish_color_normal:normal\nSETUVAR fish_color_operator:cyan\nSETUVAR fish_color_param:brblue\nSETUVAR fish_color_quote:yellow\nSETUVAR fish_color_redirection:bryellow\nSETUVAR fish_color_search_match:bryellow\\x1e\\x2d\\x2dbackground\\x3dbrblack\nSETUVAR fish_color_selection:white\\x1e\\x2d\\x2dbold\\x1e\\x2d\\x2dbackground\\x3dbrblack\nSETUVAR fish_color_status:red\nSETUVAR fish_color_user:brgreen\nSETUVAR fish_color_valid_path:\\x2d\\x2dunderline\nSETUVAR fish_key_bindings:fish_vi_key_bindings\nSETUVAR fish_pager_color_completion:normal\nSETUVAR fish_pager_color_description:yellow\nSETUVAR fish_pager_color_prefix:white\\x1e\\x2d\\x2dbold\\x1e\\x2d\\x2dunderline\nSETUVAR fish_pager_color_progress:brwhite\\x1e\\x2d\\x2dbackground\\x3dcyan\nSETUVAR fish_pager_color_selected_background:\\x2dr\n"
  },
  {
    "path": "fish/functions/fenv.apply.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.apply\n    set variables $argv\n\n    for variable in $variables\n        set key (echo $variable | sed 's/=.*//')\n        set value (echo $variable | sed 's/[^=]*=//')\n\n        if test \"$key\" = 'PATH'\n          set value (echo $value | tr ':' '\\n')\n        end\n\n        set -g -x $key $value\n    end\nend\n"
  },
  {
    "path": "fish/functions/fenv.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv -d \"Run bash scripts and import variables modified by them\"\n  if test (count $argv) -gt 0\n    if test -z (echo $argv | sed 's/[ \\t]//g')\n      return 0\n    end\n\n    fenv.main $argv\n    return $status\n  else\n    echo (set_color red)'error:' (set_color normal)'parameter missing'\n    echo (set_color cyan)'usage:' (set_color normal)'fenv <bash command>'\n    return 23  # EINVAL\n  end\nend\n"
  },
  {
    "path": "fish/functions/fenv.main.fish",
    "content": "#The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.main\n  set program $argv\n  set divider (fenv.parse.divider)\n  set previous_env (bash -c 'env')\n\n  # Need to ensure that the two calls to env (here and above) have the same\n  # nesting level within bash shells so that the SHLVL variable does not differ.\n  set program_execution (bash -c \"$program && echo && echo '$divider' && env\" 2>&1)\n  set program_status $status\n\n  if not contains -- \"$divider\" $program_execution\n    printf '%s\\n' $program_execution\n    return $program_status\n  end\n\n  set program_output (fenv.parse.before $program_execution)\n\n  if test $program_status != 0\n    printf \"%s\\n\" $program_output\n    return $program_status\n  end\n\n  set new_env (fenv.parse.after $program_execution)\n\n  fenv.apply (fenv.parse.diff $previous_env $divider $new_env)\n\n  test (count $program_output) -gt 1\n    and printf \"%s\\n\" $program_output[1..-2]\n    or  printf $program_output\n  return $program_status\nend\n"
  },
  {
    "path": "fish/functions/fenv.parse.after.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.parse.after\n  set -l divider (fenv.parse.divider)\n\n  for value in $argv\n    set -q printable;\n      and echo $value\n    test \"$value\" = \"$divider\";\n      and set printable\n  end\nend\n"
  },
  {
    "path": "fish/functions/fenv.parse.before.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.parse.before\n  set -l divider (fenv.parse.divider)\n\n  for value in $argv\n    test \"$value\" = \"$divider\";\n      and break\n\n     echo $value\n  end\nend\n"
  },
  {
    "path": "fish/functions/fenv.parse.diff.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.parse.diff\n  set -l before (fenv.parse.before $argv)\n  set -l after (fenv.parse.after $argv)\n\n  for environment in $after\n    if not contains -- \"$environment\" $before\n      echo $environment\n    end\n  end\nend\n"
  },
  {
    "path": "fish/functions/fenv.parse.divider.fish",
    "content": "# The MIT License (MIT)\n\n# Copyright (c) 2015 Derek Willian Stavis\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\n\nfunction fenv.parse.divider\n    echo '---DIVIDER---'\nend\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"elianiva nix config\";\n\n  inputs = {\n    nixpkgs-stable.url = \"github:nixos/nixpkgs/release-24.11\";\n    nixpkgs-unstable.url = \"github:nixos/nixpkgs/nixpkgs-unstable\";\n    nixpkgs.follows = \"nixpkgs-unstable\";\n\n    # nix darwin stuff\n    nix-darwin.url = \"github:nix-darwin/nix-darwin/master\";\n    nix-darwin.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    home-manager.url = \"github:nix-community/home-manager\";\n    home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    # manage homebrew through nix\n    nix-homebrew.url = \"github:zhaofengli/nix-homebrew\";\n    nix-homebrew.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    homebrew-core.url = \"github:homebrew/homebrew-core\";\n    homebrew-core.flake = false;\n\n    homebrew-cask.url = \"github:homebrew/homebrew-cask\";\n    homebrew-cask.flake = false;\n\n    homebrew-bundle.url = \"github:homebrew/homebrew-bundle\";\n    homebrew-bundle.flake = false;\n\n    homebrew-mhaeuser.url = \"github:mhaeuser/homebrew-mhaeuser\";\n    homebrew-mhaeuser.flake = false;\n\n    homebrew-barutsrb.url = \"github:BarutSRB/homebrew-tap\";\n    homebrew-barutsrb.flake = false;\n\n    # fenix for rust\n    fenix.url = \"github:nix-community/fenix\";\n    fenix.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    # only needed for linux\n    nixGL.url = \"github:nix-community/nixGL/310f8e49a149e4c9ea52f1adf70cdc768ec53f8a\";\n    nixGL.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    jj-starship.url = \"github:dmmulroy/jj-starship\";\n\n    bash-env-json = {\n      url = \"github:tesujimath/bash-env-json/main\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n\n  outputs =\n    inputs@{\n      nixpkgs,\n      home-manager,\n      nix-darwin,\n      nix-homebrew,\n      fenix,\n      jj-starship,\n      ...\n    }:\n    let\n      flakePkgs = system: {\n        bash-env-json = inputs.bash-env-json.packages.${system}.default;\n      };\n    in\n    {\n      darwinConfigurations = {\n        melon = nix-darwin.lib.darwinSystem {\n          inherit inputs;\n          system = \"aarch64-darwin\";\n          # pkgs = nixpkgs.legacyPackages.\"aarch64-darwin\";\n          pkgs = import nixpkgs {\n            system = \"aarch64-darwin\";\n            overlays = [\n              fenix.overlays.default\n              jj-starship.overlays.default\n            ];\n          };\n          specialArgs = {\n            inherit (inputs) fenix;\n            flakePkgs = flakePkgs \"aarch64-darwin\";\n          };\n          modules = [\n            nix-homebrew.darwinModules.nix-homebrew\n            home-manager.darwinModules.home-manager\n            {\n              nix-homebrew = {\n                enable = true;\n                enableRosetta = true;\n                user = \"elianiva\";\n                taps = {\n                  \"homebrew/homebrew-core\" = inputs.homebrew-core;\n                  \"homebrew/homebrew-cask\" = inputs.homebrew-cask;\n                  \"homebrew/homebrew-bundle\" = inputs.homebrew-bundle;\n                  \"mhaeuser/homebrew-mhaeuser\" = inputs.homebrew-mhaeuser;\n                  \"BarutSRB/homebrew-tap\" = inputs.homebrew-barutsrb;\n                };\n                mutableTaps = false;\n                autoMigrate = true;\n              };\n              home-manager.useGlobalPkgs = true;\n              home-manager.useUserPackages = true;\n              home-manager.users.elianiva = {\n                imports = [\n                  ./modules/darwin-home.nix\n                  ./modules/git.nix\n                  ./modules/gpg.nix\n                ];\n              };\n            }\n            # Align homebrew taps config with nix-homebrew\n            ({config, ...}: {\n              homebrew.taps = builtins.attrNames config.nix-homebrew.taps;\n            })\n            ./modules/darwin-config.nix\n          ];\n        };\n      };\n      homeConfigurations = {\n        elianiva = home-manager.lib.homeManagerConfiguration {\n          system = \"x86_64-linux\";\n          pkgs = import nixpkgs {\n            system = \"x86_64-linux\";\n            config.allowUnfree = true;\n          };\n          extraSpecialArgs = {\n            inherit inputs;\n            flakePkgs = flakePkgs \"x86_64-linux\";\n          };\n          modules = [\n            ./modules/linux-home.nix\n            ./modules/gpg.nix\n            ./modules/git.nix\n            ./modules/linux-terminals.nix\n          ];\n        };\n      };\n    };\n\n  nixConfig = {\n    trusted-substituters = [\n      \"https://cache.nixos.org\"\n\n      \"https://nix-community.cachix.org\"\n    ];\n    trusted-public-keys = [\n      \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\"\n\n      \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\"\n    ];\n  };\n}\n"
  },
  {
    "path": "ghostty/config",
    "content": "theme = light:Rose Pine Dawn,dark:Rose Pine\nwindow-theme = ghostty\nwindow-titlebar-background = #faf4ed\nwindow-colorspace = display-p3\n\nadw-toolbar-style = flat\nbold-is-bright = true\n\nshell-integration = detect\nwindow-inherit-working-directory = true\n\ncommand = /run/current-system/sw/bin/nu\n\nwindow-padding-x = 2\nwindow-padding-y = 2\nwindow-padding-balance = true\n\nfont-size = 14\n\n# font-family = Departure Mono\n\nfont-family = Iosevka Extended\n\nfont-feature = calt, liga\n\nfont-variation = wght=430\nfont-variation-italic = wght=430\n\nadjust-cell-height = 35%\nadjust-cursor-height = 35%\n\nmacos-titlebar-style = tabs\n"
  },
  {
    "path": "gitconfig/.gitconfig",
    "content": "# vim: ft=toml\n\n[user]\n  name = elianiva\n  email = dicha.arkana03@gmail.com\n\tsigningkey = A9680245\n\n[credential]\n  helper = cache --timeout 86400\n\n[core]\n  compression = 9\n  editor = nvim\n  # pager = delta\n\n[pull]\n  rebase = false\n\n[commit]\n  sign = true\n\n[alias]\n  lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative\n\n[delta]\n  line-numbers = true\n  syntax-theme = base16\n  side-by-side = false\n  file-modified-label = modified:\n\n[interactive]\n  # diffFilter = delta --color-only\n\n[init]\n  defaultBranch = master\n\n[gpg]\n\tprogram = gpg2\n"
  },
  {
    "path": "helix/config.toml",
    "content": "theme = \"my_rose_pine\"\n\n[editor]\nbufferline = \"multiple\"\nline-number = \"relative\"\ncursorline = true\npopup-border = \"all\"\ncolor-modes = true\ndefault-yank-register = \"+\"\n\n[editor.statusline]\nleft = [\"spacer\", \"separator\", \"spacer\", \"file-name\", \"read-only-indicator\", \"file-modification-indicator\"]\nright = [\"spinner\", \"spacer\", \"version-control\", \"spacer\", \"diagnostics\", \"register\", \"position\", \"file-encoding\", \"file-line-ending\", \"file-type\"]\nmode.normal = \"NORMAL\"\nmode.insert = \"INSERT\"\nmode.select = \"SELECT\"\nseparator = \"\"\n\n[editor.cursor-shape]\ninsert = \"bar\"\nnormal = \"block\"\nselect = \"block\"\n\n[editor.lsp]\nauto-signature-help = false\ndisplay-messages = true\n\n[editor.indent-guides]\nrender = true\ncharacter = \"▏\"\n\n[keys.normal.space]\ni = \":toggle lsp.display-inlay-hints\"\n# replace <space>-e with yazi instead of default file manager\ne = [\n  ':sh rm -f /tmp/unique-file',\n  ':insert-output yazi \"%{buffer_name}\" --chooser-file=/tmp/unique-file',\n  ':insert-output echo \"\\x1b[?1049h\\x1b[?2004h\" > /dev/tty',\n  ':open %sh{cat /tmp/unique-file}',\n  ':redraw',\n]\n"
  },
  {
    "path": "helix/languages.toml",
    "content": "[language-server.tinymist]\ncommand = \"tinymist\"\n\n[language-server.tinymist.config]\npreview.background.enabled = true\npreview.background.args = [\"--data-plane-host=127.0.0.1:9898\", \"--invert-colors=never\"]\n\n[language-server.harper-ls]\ncommand = \"harper-ls\"\nargs = [ \"--stdio\" ]\n\n[language-server.vtsls]\ncommand = \"vtsls\"\nargs = [ \"--stdio\" ]\n\n[language-server.vtsls.config.typescript.inlayHints]\nparameterNames.enabled = \"literals\"\nparameterTypes.enabled = true\nvariableTypes.enabled = true\nproperlyDeclarationTypes.enabled = true\nfunctionLikeReturnTypes.enabled = true\nenummemberValues.enabled = true\n\n[language-server.biome]\ncommand = \"biome\"\nargs = [\"lsp-proxy\"]\n\n# ------------------\n\n[[language]]\nname = \"html\"\nlanguage-servers = [ \"vscode-html-language-server\", \"tailwindcss-ls\" ]\n\n[[language]]\nname = \"css\"\nlanguage-servers = [ \"vscode-css-language-server\", \"tailwindcss-ls\" ]\n\n[[language]]\nname = \"typst\"\nlanguage-servers = [\"tinymist\", \"harper-ls\"]\nformatter.command = \"typstyle\"\nrulers = [80]\nauto-format = true\nsoft-wrap.enable = true\nsoft-wrap.wrap-at-text-width = true\n\n[[language]]\nname = \"markdown\"\nlanguage-servers = [\"harper-ls\"]\nrulers = [80]\nsoft-wrap.enable = true\nsoft-wrap.wrap-at-text-width = true\n\n[[language]]\nname = \"typescript\"\nlanguage-servers = [{ name = \"vtsls\", except-features = [\"format\"] }, \"biome\", \"tailwindcss-ls\"]\n# formatter = { command = \"biome\", args = [\"format\", \"--stdin-file-path\", \"index.ts\"] }\n\n[[language]]\nname = \"javascript\"\nlanguage-servers = [{ name = \"vtsls\", except-features = [\"format\"] }, \"biome\", \"tailwindcss-ls\"]\n# formatter = { command = \"biome\", args = [\"format\", \"--stdin-file-path\", \"index.js\"] }\n\n[[language]]\nname = \"tsx\"\nlanguage-servers = [{ name = \"vtsls\", except-features = [\"format\"] }, \"biome\", \"tailwindcss-ls\"]\n# formatter = { command = \"biome\", args = [\"format\", \"--stdin-file-path\", \"index.tsx\"] }\n\n[[language]]\nname = \"jsx\"\nlanguage-servers = [{ name = \"vtsls\", except-features = [\"format\"] }, \"biome\", \"tailwindcss-ls\"]\n# formatter = { command = \"biome\", args = [\"format\", \"--stdin-file-path\", \"index.jsx\"] }\n\n[[language]]\nname = \"astro\"\nlanguage-servers = [{ name = \"astro-ls\", except-features = [\"format\"] }, \"biome\", \"tailwindcss-ls\"]\n# formatter = { command = \"biome\", args = [\"format\", \"--stdin-file-path\", \"index.astro\"] }\n"
  },
  {
    "path": "helix/runtime/queries/typst/highlights.scm",
    "content": "; CONTROL\n(let \"let\" @keyword.storage.type)\n(branch [\"if\" \"else\"] @keyword.control.conditional)\n(while \"while\" @keyword.control.repeat)\n(for [\"for\" \"in\"] @keyword.control.repeat)\n(import \"import\" @keyword.control.import)\n(as \"as\" @keyword.operator)\n(include \"include\" @keyword.control.import)\n(show \"show\" @keyword.control)\n(set \"set\" @keyword.control)\n(return \"return\" @keyword.control)\n(flow [\"break\" \"continue\"] @keyword.control)\n\n; OPERATOR\n(in [\"in\" \"not\"] @keyword.operator)\n(context \"context\" @keyword.control)\n(and \"and\" @keyword.operator)\n(or \"or\" @keyword.operator)\n(not \"not\" @keyword.operator)\n(sign [\"+\" \"-\"] @operator)\n(add \"+\" @operator)\n(sub \"-\" @operator)\n(mul \"*\" @operator)\n(div \"/\" @operator)\n(cmp [\"==\" \"<=\" \">=\" \"!=\" \"<\" \">\"] @operator)\n(fraction \"/\" @operator)\n(fac \"!\" @operator)\n(attach [\"^\" \"_\"] @operator)\n(wildcard) @operator\n\n; VALUE\n(raw_blck \"```\" @operator) @markup.raw.block\n(raw_span \"`\" @operator) @markup.raw.block\n(raw_blck lang: (ident) @tag)\n(label) @tag\n(ref) @tag\n(number) @constant.numeric\n(string) @string\n(content [\"[\" \"]\"] @operator)\n(bool) @constant.builtin.boolean\n(none) @constant.builtin\n(auto) @constant.builtin\n(ident) @variable\n\n; FUNCTIONS\n(call\n  item: (ident) @function)\n(call\n  item: (field field: (ident) @function.method))\n(tagged field: (ident) @tag)\n(field field: (ident) @tag)\n(comment) @comment\n\n; MARKUP\n(item \"-\" @markup.list)\n(term [\"/\" \":\"] @markup.list)\n(heading \"=\" @markup.heading.marker) @markup.heading.1\n(heading \"==\" @markup.heading.marker) @markup.heading.2\n(heading \"===\" @markup.heading.marker) @markup.heading.3\n(heading \"====\" @markup.heading.marker) @markup.heading.4\n(heading \"=====\" @markup.heading.marker) @markup.heading.5\n(heading \"======\" @markup.heading.marker) @markup.heading.6\n(url) @tag\n(emph) @markup.italic\n(strong) @markup.bold\n(symbol) @constant.character\n(shorthand) @constant.builtin\n(quote) @markup.quote\n(align) @operator\n(letter) @constant.character\n(linebreak) @constant.builtin\n\n(math \"$\" @operator)\n\"#\" @operator\n\"end\" @operator\n\n(escape) @constant.character.escape\n[\"(\" \")\" \"{\" \"}\"] @punctuation.bracket\n[\",\" \";\" \"..\" \":\" \"sep\"] @punctuation.delimiter\n\"assign\" @punctuation\n(field \".\" @punctuation)\n"
  },
  {
    "path": "helix/themes/my_rose_pine.toml",
    "content": "inherits = \"rose_pine_dawn\"\n\n\"ui.virtual.indent-guide\" = { fg = \"overlay\" }\n\n\"ui.statusline\" = { fg = \"love\", bg = \"love_10\" }\n\"ui.statusline.separator\" = { fg = \"love\" }\n\n\"ui.popup\" = {}\n\"ui.popup.info\" = {}\n\n\"ui.menu\" = { fg = \"subtle\" }\n\"ui.help\" = { fg = \"subtle\" }\n"
  },
  {
    "path": "improvement.md",
    "content": "# Nix Configuration Improvements\n\nThis document contains recommendations for improving the Nix configuration based on a review of the flake and module files.\n\n### 6. Use Home-Manager Modules for Shell Tools\n\n**Problem:** Several packages are installed but not configured through their HM modules, missing out on shell integration:\n\n- `zoxide` - no shell hooks configured\n- `eza` - no aliases configured\n- `vivid` - LS_COLORS not set up\n\n**Fix:** Add to `home-common.nix`:\n```nix\nprograms = {\n  zoxide = {\n    enable = true;\n    enableNushellIntegration = true;\n    enableFishIntegration = true;\n  };\n\n  eza = {\n    enable = true;\n    enableFishIntegration = true;\n    enableNushellIntegration = true;\n    icons = true;\n    git = true;\n  };\n};\n```\n\nFor `vivid`, add to shell config:\n```nix\nprograms.nushell.extraConfig = ''\n  $env.LS_COLORS = (${pkgs.vivid}/bin/vivid generate molokai)\n'';\n```\n\n## Nix Best Practices\n\n### 7. Enable Nix Garbage Collection and Store Optimisation\n\n**Problem:** No automatic cleanup configured. The nix store will grow indefinitely.\n\n**Add to `modules/darwin-config.nix`:**\n```nix\nnix.gc = {\n  automatic = true;\n  interval = { Hour = 3; Minute = 0; };\n  options = \"--delete-older-than 7d\";\n};\nnix.optimise.automatic = true;\nnix.settings.auto-optimise-store = true;\n```\n\n**Add to `modules/linux-home.nix`:**\n```nix\nnix.gc = {\n  automatic = true;\n  frequency = \"weekly\";\n  options = \"--delete-older-than 7d\";\n};\nnix.settings.auto-optimise-store = true;\n```\n\n### 8. Remove Redundant Fenix Overlay\n\n**Problem:** The fenix overlay is applied twice:\n1. In `flake.nix`: `overlays = [ fenix.overlays.default ]`\n2. In `darwin-config.nix`: `nixpkgs.overlays = [ fenix.overlays.default ]`\n\n**Fix:** Remove the overlay from `darwin-config.nix` since it's already applied in the flake's pkgs import.\n\n### 9. Simplify Unfree Configuration\n\n**Current in `darwin-config.nix` and `linux-home.nix`:**\n```nix\nallowUnfree = true;\nallowUnfreePredicate = (_: true);\n```\n\n**Fix:** The predicate is redundant when set to always return true:\n```nix\nnixpkgs.config.allowUnfree = true;\n```\n\n### 10. Add Modern Nix Settings\n\n**Add to both Darwin and Linux configurations:**\n```nix\nnix.settings = {\n  experimental-features = [ \"nix-command\" \"flakes\" \"repl-flake\" ];\n  warn-dirty = false;\n  # Optional: better build performance\n  max-jobs = \"auto\";\n  cores = 0;  # use all available\n};\n```\n\n## Minor Issues\n\n### 11. Unused Ghostty Input\n\n**Problem:** The `ghostty` input is defined in `flake.nix` but never used. Either:\n- Add it to packages if you want to install via Nix\n- Remove the input to reduce flake lock size\n\n### 12. Delta vs Difftastic Inconsistency\n\n**Problem:** `delta` is installed and has configuration in `git.nix`, but `lazygit` and `jujutsu` are configured to use `difftastic`.\n\n**Options:**\n1. Remove delta if you're using difftastic exclusively\n2. Configure different tools for different use cases (e.g., delta for git CLI, difftastic for TUI tools)\n\n### 13. Consider Adding `nh` (Nix Helper) to Darwin\n\n**Observation:** `nh` is installed on Linux but not Darwin. It's useful for both platforms:\n```nix\n# Add to modules/darwin-packages.nix\nnh\n```\n\n### 14. Pinentry Package Logic Simplification\n\n**Current in `gpg.nix`:**\n```nix\npinentry.package =\n  if pkgs.stdenv.isLinux then pkgs.pinentry-gnome3\n  else if pkgs.stdenv.isDarwin then pkgs.pinentry_mac\n  else pkgs.pinentry-curses;\n```\n\n**Observation:** Since `gpg.nix` is imported by both Darwin and Linux configurations (and there's no third platform), this could be simplified by passing the correct package as an argument or using platform-specific imports. However, the current approach is defensive and works fine.\n\n## Quick Reference: Priority Order\n\n1. **Fix duplicate `home.file`** - This is breaking your config\n2. **Add `extraSpecialArgs`** - Required for evaluation\n3. **Fix `terminals.nix` path** - File not found error\n4. **Enable GC** - Prevent disk space issues\n5. **Extract common config** - Reduce maintenance burden\n6. **Use HM modules for tools** - Better shell integration\n"
  },
  {
    "path": "jjui/config.toml",
    "content": "[ui]\ntheme = \"rose-pine-dawn\"\n"
  },
  {
    "path": "jjui/themes/rose-pine-dawn.toml",
    "content": "## tinted-jjui (https://github.com/vic/tinted-jjui)\n# Scheme name: Rosé Pine Dawn\n# Scheme author: Emilia Dunfelt &lt;edun@dunfelt.se&gt;\n# Template author: Victor Borja <vborja@apache.org> (https://github.com/vic)\n\n\"text\"      = { fg = \"#575279\", bg = \"#faf4ed\" }\n\"dimmed\"    = { fg = \"#9893a5\", bg = \"#faf4ed\" }\n\"title\"     = { fg = \"#907aa9\", bold = true }\n\"shortcut\"  = { fg = \"#ea9d34\" }\n\"matched\"   = { fg = \"#d7827e\" }\n\"border\"    = { fg = \"#9893a5\" }\n\"selected\"  = { bg = \"#fffaf3\", fg = \"#575279\", bold = true }\n\n\"source_marker\" = { bg = \"#56949f\", fg = \"#faf4ed\", bold = true }\n\"target_marker\" = { bg = \"#286983\", fg = \"#faf4ed\", bold = true }\n\n\"status\" = { bg = \"#fffaf3\" }\n\"status title\" = { fg = \"#faf4ed\", bg = \"#907aa9\", bold = true }\n\n\"revset title\" = { fg = \"#907aa9\", bg = \"#fffaf3\", bold = true }\n\"revset text\" = { fg = \"#575279\", bold = true }\n\"revset completion text\" = { fg = \"#575279\" }\n\"revset completion matched\" = { fg = \"#d7827e\", bold = true }\n\"revset completion dimmed\" = { fg = \"#9893a5\" }\n\"revset completion selected\" = { bg = \"#575279\", fg = \"#f2e9de\" }\n\n\"revisions\" = { fg = \"#575279\" }\n\"revisions selected\" = { bg = \"#f2e9de\"}\n\"revisions dimmed\" = { fg = \"#9893a5\" }\n\"revisions details selected\" = { bg = \"#797593\" }\n\"oplog selected\" = { bold = true }\n\n\"evolog\" = { fg = \"#575279\" }\n\"evolog selected\" = { bg = \"#575279\", fg = \"#fffaf3\", bold = true }\n\n\"menu\" = { bg = \"#faf4ed\" }\n\"menu title\" = { fg = \"#faf4ed\", bg = \"#ea9d34\", bold = true }\n\"menu shortcut\" = { fg = \"#ea9d34\" }\n\"menu matched\" = { fg = \"#d7827e\", bold = true }\n\"menu dimmed\" = { fg = \"#9893a5\" }\n\"menu border\" = { fg = \"#9893a5\" }\n\"menu selected\" = { bg = \"#575279\", fg = \"#fffaf3\" }\n\n\"help\" = { bg = \"#faf4ed\" }\n\"help title\" = { fg = \"#286983\", bold = true, underline = true }\n\"help border\" = { fg = \"#9893a5\" }\n\n\"preview\" = { fg = \"#575279\" }\n\"preview border\" = { fg = \"#9893a5\" }\n\n\"confirmation\" = { bg = \"#faf4ed\" }\n\"confirmation text\" = { fg = \"#907aa9\", bold = true }\n\"confirmation dimmed\" = { fg = \"#9893a5\" }\n\"confirmation border\" = { fg = \"#b4637a\", bold = true }\n\"confirmation selected\" = { bg = \"#575279\", fg = \"#fffaf3\" }\n\n\"undo\" = { bg = \"#faf4ed\" }\n\"undo confirmation dimmed\" = { fg = \"#9893a5\" }\n\"undo confirmation selected\" = { bg = \"#575279\", fg = \"#fffaf3\" }\n\n\"success\" = { fg = \"#286983\", bold = true }\n\"error\" = { fg = \"#b4637a\", bold = true }\n\"revisions rebase source_marker\" = { bold = true }\n\"revisions rebase target_marker\" = { bold = true }\n\"status shortcut\" = { fg = \"#ea9d34\" }\n\"status dimmed\" = { fg = \"#9893a5\" }\n\n\"details\" = { fg = \"#575279\" }\n\"details selected\" = { bold = true }\n\"completion\" = { fg = \"#575279\" }\n\"completion selected\" = { bold = true }\n\"rebase\" = { bold = true }\n\n\"workspace\" = { fg = \"#907aa9\" }\n\"branch\" = { fg = \"#ea9d34\" }\n\"commit\" = { fg = \"#286983\" }\n\"file\" = { fg = \"#d7827e\" }\n\"change\" = { fg = \"#b4637a\" }\n\"bookmark\" = { fg = \"#ea9d34\" }\n"
  },
  {
    "path": "kitty/kitty.conf",
    "content": "# include colorscheme\ninclude rose-pine-dawn.conf\n\nadjust_line_height 140%\n# jetbrains mono\n# font_size 11.5\n# monaspace\nfont_size 12.5\nbox_drawing_scale 0.001, 0.5, 1, 1.75\n\ntext_composition_strategy 1.25\n# text_composition_strategy platform\n\n# font_family      JetBrainsMono Nerd Font\n# bold_font        JetBrainsMono Nerd Font\n# italic_font      JetBrainsMono Nerd Font\n# bold_italic_font JetBrainsMono Nerd Font\n\n# font_family      IosevkaTermSlab NF Medium\n# bold_font        IosevkaTermSlab NF\n# italic_font      IosevkaTermSlab NF\n# bold_italic_font IosevkaTermSlab NF\n\nfont_family      Monaspace Neon Var\nbold_font        Monaspace Neon Var\nitalic_font      Monaspace Radon Medium\nbold_italic_font Monaspace Radon Var\n\n# Nerd Font\nsymbol_map U+e000-U+e00a,U+ea60-U+ebeb,U+e0a0-U+e0c8,U+e0ca,U+e0cc-U+e0d7,U+e200-U+e2a9,U+e300-U+e3e3,U+e5fa-U+e6b1,U+e700-U+e7c5,U+ed00-U+efc1,U+f000-U+f2ff,U+f000-U+f2e0,U+f300-U+f372,U+f400-U+f533,U+f0001-U+f1af0 Symbols Nerd Font Mono\n\n# font_family      CaskaydiaCove Nerd Font\n# bold_font        CaskaydiaCove Nerd Font\n# italic_font      CaskaydiaCove Nerd Font\n# bold_italic_font CaskaydiaCove Nerd Font\n\n# # japanese characters\n# symbol_map U+4E00-U+9FFF M+ 1p medium\n# symbol_map U+3041-U+3096 M+ 1p medium\n# symbol_map U+30A0-U+30FF M+ 1p medium\n# symbol_map U+FF00-U+FFEF M+ 1p medium\n\nenable_audio_bell no\n\nwindow_padding_width 2\n\n# background_opacity 0.96\nbackground_opacity 1\n\nmap ctrl+shift+w no-op\n\nmap ctrl+plus change_font_size all +1.0\nmap ctrl+minus change_font_size all -1.0\n\nmap alt+v paste_from_clipboard\nmap alt+c copy_to_clipboard\n\nmap alt+k combine : send_text application \u001bk : scroll_line_up\nmap alt+j combine : send_text application \u001bj : scroll_line_down\n\nmap alt+u scroll_page_up\nmap alt+d scroll_page_down\n\nmap kitty_mod+t new_tab_with_cwd\n\nmap alt+space no_op\n\nmap alt+1 goto_tab 1\nmap alt+2 goto_tab 2\nmap alt+3 goto_tab 3\nmap alt+4 goto_tab 4\nmap alt+5 goto_tab 5\nmap alt+6 goto_tab 6\nmap alt+n next_tab\nmap alt+p previous_tab\n\ntab_bar_style fade\n\nallow_remote_control yes\n\nhide_window_decorations yes\n\nstartup_session launch.conf\n\n# vim:fileencoding=utf-8:ft=conf:foldmethod=marker\n"
  },
  {
    "path": "kitty/launch.conf",
    "content": "launch zellij\n"
  },
  {
    "path": "kitty/rose-pine-dawn.conf",
    "content": "## name: Rosé Pine Dawn\n## author: mvllow\n## license: MIT\n## upstream: https://github.com/rose-pine/kitty/blob/main/dist/rose-pine-dawn.conf\n## blurb: All natural pine, faux fur and a bit of soho vibes for the classy minimalist\n\nforeground               #575279\nbackground               #faf4ed\nselection_foreground     #575279\nselection_background     #dfdad9\n\ncursor                   #cecacd\ncursor_text_color        #575279\n\nurl_color                #907aa9\n\nactive_tab_foreground    #575279\nactive_tab_background    #f2e9e1\ninactive_tab_foreground  #9893a5\ninactive_tab_background  #faf4ed\n\nactive_border_color      #286983\ninactive_border_color    #dfdad9\n\n# black\ncolor0   #f2e9e1\ncolor8   #9893a5\n\n# red\ncolor1   #b4637a\ncolor9   #b4637a\n\n# green\ncolor2   #286983\ncolor10  #286983\n\n# yellow\ncolor3   #ea9d34\ncolor11  #ea9d34\n\n# blue\ncolor4   #56949f\ncolor12  #56949f\n\n# magenta\ncolor5   #907aa9\ncolor13  #907aa9\n\n# cyan\ncolor6   #d7827e\ncolor14  #d7827e\n\n# white\ncolor7   #575279\ncolor15  #575279\n"
  },
  {
    "path": "kitty/rose-pine.conf",
    "content": "## name: Rosé Pine\n## author: mvllow\n## license: MIT\n## upstream: https://github.com/rose-pine/kitty/blob/main/dist/rose-pine.conf\n## blurb: All natural pine, faux fur and a bit of soho vibes for the classy minimalist\n\nforeground               #e0def4\nbackground               #191724\nselection_foreground     #e0def4\nselection_background     #403d52\n\ncursor                   #524f67\ncursor_text_color        #e0def4\n\nurl_color                #c4a7e7\n\nactive_tab_foreground    #e0def4\nactive_tab_background    #26233a\ninactive_tab_foreground  #6e6a86\ninactive_tab_background  #191724\n\nactive_border_color      #31748f\ninactive_border_color    #403d52\n\n# black\ncolor0   #26233a\ncolor8   #6e6a86\n\n# red\ncolor1   #eb6f92\ncolor9   #eb6f92\n\n# green\ncolor2   #31748f\ncolor10  #31748f\n\n# yellow\ncolor3   #f6c177\ncolor11  #f6c177\n\n# blue\ncolor4   #9ccfd8\ncolor12  #9ccfd8\n\n# magenta\ncolor5   #c4a7e7\ncolor13  #c4a7e7\n\n# cyan\ncolor6   #ebbcba\ncolor14  #ebbcba\n\n# white\ncolor7   #e0def4\ncolor15  #e0def4\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/keybinds/bindtotags.lua",
    "content": "local gears = require(\"gears\")\nlocal awful = require(\"awful\")\nlocal modkey = require(\"main.variables\").modkey\n\nlocal M = {}\n\nfunction M.get(globalkeys)\n\t-- Bind all key numbers to tags.\n\t-- Be careful: we use keycodes to make it work on any keyboard layout.\n\t-- This should map on the top row of your keyboard, usually 1 to 9.\n\tfor i = 1, 6 do\n\t\tglobalkeys = gears.table.join(\n\t\t\tglobalkeys,\n\t\t\t-- View tag only.\n\t\t\tawful.key({ modkey }, \"#\" .. i + 9, function()\n\t\t\t\tlocal screen = awful.screen.focused()\n\t\t\t\tlocal tag = screen.tags[i]\n\t\t\t\tif tag then\n\t\t\t\t\ttag:view_only()\n\t\t\t\tend\n\t\t\tend, { description = \"view tag #\" .. i, group = \"tag\" }),\n\n\t\t\t-- Toggle tag display.\n\t\t\tawful.key({ modkey, \"Control\" }, \"#\" .. i + 9, function()\n\t\t\t\tlocal screen = awful.screen.focused()\n\t\t\t\tlocal tag = screen.tags[i]\n\t\t\t\tif tag then\n\t\t\t\t\tawful.tag.viewtoggle(tag)\n\t\t\t\tend\n\t\t\tend, { description = \"toggle tag #\" .. i, group = \"tag\" }),\n\n\t\t\t-- Move client to tag.\n\t\t\tawful.key({ modkey, \"Shift\" }, \"#\" .. i + 9, function()\n\t\t\t\tif client.focus then\n\t\t\t\t\tlocal tag = client.focus.screen.tags[i]\n\t\t\t\t\tif tag then\n\t\t\t\t\t\tclient.focus:move_to_tag(tag)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend, { description = \"move focused client to tag #\" .. i, group = \"tag\" }),\n\n\t\t\t-- Toggle tag on focused client.\n\t\t\tawful.key({ modkey, \"Control\", \"Shift\" }, \"#\" .. i + 9, function()\n\t\t\t\tif client.focus then\n\t\t\t\t\tlocal tag = client.focus.screen.tags[i]\n\t\t\t\t\tif tag then\n\t\t\t\t\t\tclient.focus:toggle_tag(tag)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend, { description = \"toggle focused client on tag #\" .. i, group = \"tag\" })\n\t\t)\n\tend\n\n\treturn globalkeys\nend\n\nreturn setmetatable({}, {\n\t__call = function(_, ...)\n\t\treturn M.get(...)\n\tend,\n})\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/keybinds/clientbuttons.lua",
    "content": "-- Standard awesome library\nlocal gears = require(\"gears\")\nlocal awful = require(\"awful\")\nlocal modkey = require(\"main.variables\").modkey\n\nlocal clientbuttons = gears.table.join(\n\tawful.button({}, 1, function(c)\n\t\tc:emit_signal(\"request::activate\", \"mouse_click\", { raise = true })\n\tend),\n\n\tawful.button({ modkey }, 1, function(c)\n\t\tc:emit_signal(\"request::activate\", \"mouse_click\", { raise = true })\n\t\tawful.mouse.client.move(c)\n\tend),\n\n\tawful.button({ modkey }, 3, function(c)\n\t\tc:emit_signal(\"request::activate\", \"mouse_click\", { raise = true })\n\t\tawful.mouse.client.resize(c, \"bottom_right\")\n\tend)\n)\n\nreturn clientbuttons\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/keybinds/clientkeys.lua",
    "content": "-- Standard Awesome library\nlocal gears = require(\"gears\")\nlocal awful = require(\"awful\")\nlocal beautiful = require(\"beautiful\")\nlocal helpers = require(\"main.helpers\")\nlocal vars = require(\"main.variables\")\n\nlocal modkey = vars.modkey\nlocal ctrlkey = vars.ctrlkey\nlocal altkey = vars.altkey\n\nlocal clientkeys = gears.table.join(\n\tawful.key({ modkey }, \"0\", function(c)\n\t\tc.fullscreen = not c.fullscreen\n\t\tc:raise()\n\tend, { description = \"toggle fullscreen\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"q\", function(c)\n\t\tc:kill()\n\tend, { description = \"close\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"f\", function(c)\n\t\tawful.client.floating.toggle(c)\n\tend, { description = \"toggle floating\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"g\", function(c)\n\t\tc:swap(awful.client.getmaster())\n\tend, { description = \"move to master\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"o\", function(c)\n\t\tc:move_to_screen()\n\tend, { description = \"move to screen\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"m\", function(c)\n\t\tc.maximized = not c.maximized\n\t\tif c.maximized == true then\n\t\t\tc.border_width = \"0\"\n\t\t\tc.border_color = beautiful.border_focus\n\t\telse\n\t\t\tc.border_width = beautiful.border_width\n\t\t\tc.border_color = beautiful.border_focus\n\t\tend\n\t\tc:raise()\n\tend, { description = \"(un)maximize\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Control\" }, \"m\", function(c)\n\t\tc.maximized_vertical = not c.maximized_vertical\n\t\tc:raise()\n\tend, { description = \"(un)maximize vertically\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"m\", function(c)\n\t\tc.maximized_horizontal = not c.maximized_horizontal\n\t\tc:raise()\n\tend, { description = \"(un)maximize horizontally\", group = \"client\" }),\n\n\t-- Resize\n\tawful.key({ modkey, \"Shift\" }, \"j\", function()\n\t\tawful.client.moveresize(0, 0, 0, 20)\n\tend, { description = \"increase window height\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"k\", function()\n\t\tawful.client.moveresize(0, 0, 0, -20)\n\tend, { description = \"decrease window height\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"h\", function()\n\t\tawful.client.moveresize(0, 0, -20, 0)\n\tend, { description = \"decrease window width\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"l\", function()\n\t\tawful.client.moveresize(0, 0, 20, 0)\n\tend, { description = \"increase window width\", group = \"client\" }),\n\n\t-- Cycle through window\n\tawful.key({ altkey }, \"Tab\", function()\n\t\tawful.client.focus.byidx(1)\n\tend, { description = \"cycle through window\", group = \"client\" }),\n\n\tawful.key({ altkey, \"Shift\" }, \"Tab\", function()\n\t\tawful.client.focus.byidx(-1)\n\tend, { description = \"cycle through window in reverse\", group = \"client\" }),\n\n\t-- Resize window\n\tawful.key({ modkey, ctrlkey }, \"j\", function()\n\t\thelpers.resize_dwim(client.focus, \"down\")\n\tend, { description = \"make window bigger to bottom\", group = \"client\" }),\n\n\tawful.key({ modkey, ctrlkey }, \"k\", function()\n\t\thelpers.resize_dwim(client.focus, \"up\")\n\tend, { description = \"make window bigger to top\", group = \"client\" }),\n\n\tawful.key({ modkey, ctrlkey }, \"h\", function()\n\t\thelpers.resize_dwim(client.focus, \"left\")\n\tend, { description = \"make window bigger to left\", group = \"client\" }),\n\n\tawful.key({ modkey, ctrlkey }, \"l\", function()\n\t\thelpers.resize_dwim(client.focus, \"right\")\n\tend, { description = \"make window bigger to right\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"h\", function()\n\t\tawful.client.focus.bydirection(\"left\")\n\tend, { description = \"focus to left window \", group = \"layout\" }),\n\n\tawful.key({ modkey }, \"j\", function()\n\t\tawful.client.focus.bydirection(\"down\")\n\tend, { description = \"focus to window below \", group = \"client\" }),\n\n\tawful.key({ modkey }, \"k\", function()\n\t\tawful.client.focus.bydirection(\"up\")\n\tend, { description = \"focus to window above\", group = \"client\" }),\n\n\tawful.key({ modkey }, \"l\", function()\n\t\tawful.client.focus.bydirection(\"right\")\n\tend, { description = \"focus to right window\", group = \"layout\" }),\n\n\t-- Layout manipulation\n\tawful.key({ modkey, \"Shift\" }, \"j\", function()\n\t\tawful.client.swap.bydirection(\"down\")\n\tend, { description = \"swap with next client by index\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"k\", function()\n\t\tawful.client.swap.bydirection(\"up\")\n\tend, { description = \"swap with previous client by index\", group = \"client\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"h\", function()\n\t\tawful.client.swap.bydirection(\"left\")\n\tend, { description = \"swap with right\", group = \"layout\" }),\n\n\tawful.key({ modkey, \"Shift\" }, \"l\", function()\n\t\tawful.client.swap.bydirection(\"right\")\n\tend, { description = \"swap with left\", group = \"layout\" }),\n\n\tawful.key({ modkey }, \"u\", function()\n\t\tawful.client.urgent.jumpto()\n\tend, { description = \"jump to urgent client\", group = \"client\" })\n)\n\nreturn clientkeys\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/keybinds/globalkeys.lua",
    "content": "local gears = require \"gears\"\nlocal awful = require \"awful\"\nlocal naughty = require \"naughty\"\nlocal exit_screen_show = require \"main.exitscreen\"\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\nlocal noop = require(\"main.helpers\").noop\nlocal vars = require \"main.variables\"\n\n-- Resource Configuration\nlocal launcher = vars.launcher\nlocal shot = vars.shot\nlocal emoji_picker = vars.emoji_picker\nlocal clipmenu = vars.clipmenu\nlocal terminal = vars.terminal\nlocal modkey = vars.modkey\n\nlocal M = gears.table.join(\n  awful.key({ modkey }, \"s\", function()\n    awful.hotkeys_popup.show_help(nil, awful.screen.focused())\n  end, {\n    description = \"show help\",\n    group = \"awesome\",\n  }),\n\n  -- Toggle tray visibility\n  awful.key({ modkey }, \"=\", function()\n    local tag = awful.screen.focused().selected_tag\n    tag.gap = dpi(4)\n    awful.layout.arrange(awful.screen.focused())\n  end, {\n    description = \"show gaps\",\n    group = \"awesome\",\n  }),\n\n  -- Toggle tray visibility\n  awful.key({ modkey, \"Shift\" }, \"=\", function()\n    local tag = awful.screen.focused().selected_tag\n    tag.gap = dpi(0)\n    awful.layout.arrange(awful.screen.focused())\n  end, {\n    description = \"hide gaps\",\n    group = \"awesome\",\n  }),\n\n  -- Tag browsing\n  awful.key({ modkey }, \"Left\", function()\n    awful.tag.viewprev()\n  end, {\n    description = \"view previous\",\n    group = \"tag\",\n  }),\n\n  awful.key({ modkey }, \"Right\", function()\n    awful.tag.viewnext()\n  end, {\n    description = \"view next\",\n    group = \"tag\",\n  }),\n\n  awful.key({ modkey }, \"Tab\", function()\n    awful.tag.history.restore()\n  end, {\n    description = \"go back\",\n    group = \"tag\",\n  }),\n\n  -- Standard program\n  awful.key({ modkey }, \"Return\", function()\n    awful.spawn(terminal)\n  end, {\n    description = \"open a terminal\",\n    group = \"launcher\",\n  }),\n\n  awful.key({ modkey }, \"d\", function()\n    awful.spawn.easy_async_with_shell(\n      launcher .. \"combi \" .. theme.color_name,\n      noop\n    )\n  end, {\n    description = \"open app launcher\",\n    group = \"launcher\",\n  }),\n\n  awful.key({ modkey, \"Shift\" }, \"d\", function()\n    awful.spawn.easy_async_with_shell(\n      launcher .. \"run \" .. theme.color_name,\n      noop\n    )\n  end, {\n    description = \"open command launcher\",\n    group = \"launcher\",\n  }),\n\n  awful.key({ modkey, \"Shift\" }, \"x\", function()\n    exit_screen_show()\n  end, {\n    description = \"show exit screen\",\n    group = \"awesome\",\n  }),\n\n  awful.key({ modkey }, \"e\", function()\n    awful.spawn(emoji_picker)\n  end, {\n    description = \"pick an emoji\",\n    group = \"launcher\",\n  }),\n\n  awful.key({ modkey }, \"c\", function()\n    awful.spawn(clipmenu)\n  end, {\n    description = \"launch clipmenu\",\n    group = \"launcher\",\n  }),\n\n  awful.key({ modkey, \"Shift\" }, \"r\", function()\n    awesome.restart()\n  end, {\n    description = \"reload awesome\",\n    group = \"awesome\",\n  }),\n\n  awful.key({ modkey, \"Shift\" }, \"q\", function()\n    awesome.quit()\n  end, {\n    description = \"quit awesome\",\n    group = \"awesome\",\n  }),\n\n  awful.key({ modkey }, \"t\", function()\n    awful.layout.set(awful.layout.suit.tile)\n  end, {\n    description = \"set to tiling mode\",\n    group = \"layout\",\n  }),\n\n  awful.key({ modkey }, \"`\", function()\n    naughty.destroy_all_notifications()\n  end, {\n    description = \"dismiss notification\",\n    group = \"notifications\",\n  }),\n\n  awful.key({ modkey }, \"Print\", function()\n    awful.spawn.easy_async_with_shell(shot, noop)\n  end, {\n    description = \"screenshot with borders\",\n    group = \"misc\",\n  }),\n\n  awful.key({}, \"Print\", function()\n    awful.spawn.easy_async_with_shell(\n      \"flameshot gui -p \" .. os.getenv \"HOME\" .. \"/Pictures/shots\",\n      noop\n    )\n  end, {\n    description = \"screenshot without borders\",\n    group = \"misc\",\n  })\n)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/keybinds/mediakeys.lua",
    "content": "local gears = require(\"gears\")\nlocal awful = require(\"awful\")\nlocal naughty = require(\"naughty\")\nlocal noop = require(\"main.helpers\").noop\n\nlocal M = gears.table.join(\n\tawful.key({}, \"XF86AudioLowerVolume\", function()\n\t\tawful.spawn.easy_async(\"pulsemixer --change-volume -2\", function()\n\t\t\t-- send signal AFTER the volume has changed\n\t\t\tawesome.emit_signal(\"volume_change\")\n\t\tend)\n\tend, { description = \"lower the volume\", group = \"media\" }),\n\n\tawful.key({}, \"XF86AudioRaiseVolume\", function()\n\t\tawful.spawn.easy_async(\"pulsemixer --change-volume +2\", function()\n\t\t\t-- send signal AFTER the volume has changed\n\t\t\tawesome.emit_signal(\"volume_change\")\n\t\tend)\n\tend, { description = \"raise the volume\", group = \"media\" }),\n\n\tawful.key({}, \"XF86AudioMute\", function()\n\t\tawful.spawn.easy_async(\"pulsemixer --toggle-mute\", function()\n\t\t\t-- send signal AFTER the volume has changed\n\t\t\tawesome.emit_signal(\"volume_change\")\n\t\tend)\n\tend, { description = \"mute the audio\", group = \"media\" }),\n\n\tawful.key({}, \"XF86AudioPlay\", function()\n\t\tawful.spawn.easy_async(\"playerctl --player=%any,chrome,chromium play-pause\", function()\n\t\t\tnaughty.notify({\n\t\t\t\ttitle = \"Playerctl\",\n\t\t\t\ttext = \"Audio toggled!\",\n\t\t\t})\n\t\tend)\n\tend, { description = \"toggle the audio\", group = \"media\" }),\n\n\tawful.key({}, \"XF86AudioNext\", function()\n\t\tawful.spawn.easy_async(\"playerctl --player=%any,chrome,chromium next\", function()\n\t\t\tnaughty.notify({\n\t\t\t\ttitle = \"Playerctl\",\n\t\t\t\ttext = \"Audio toggled!\",\n\t\t\t})\n\t\tend)\n\tend, { description = \"toggle the audio\", group = \"media\" }),\n\n\tawful.key({}, \"XF86AudioPrev\", function()\n\t\tawful.spawn.easy_async(\"playerctl --player=%any,chrome,chromium previous\", function()\n\t\t\tnaughty.notify({\n\t\t\t\ttitle = \"Playerctl\",\n\t\t\t\ttext = \"Audio toggled!\",\n\t\t\t})\n\t\tend)\n\tend, { description = \"toggle the audio\", group = \"media\" }),\n\n\t-- Brightness\n\tawful.key({}, \"XF86MonBrightnessDown\", function()\n\t\tawful.spawn.easy_async(\"light -U 10\", noop)\n\tend, { description = \"decrease brightness\", group = \"brightness\" }),\n\n\tawful.key({}, \"XF86MonBrightnessUp\", function()\n\t\tawful.spawn.easy_async(\"light -A 10\", noop)\n\tend, { description = \"increase brightness\", group = \"brightness\" })\n)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/autostart.lua",
    "content": "local awful = require(\"awful\")\n\nlocal M = {}\n\nM.setup = function()\n\tlocal cmds = {\n\t\t\"clipmenud\",\n\t\t\"flameshot\",\n\t\t\"lxpolkit\",\n\t\t\"xfce4-power-manager\",\n\t\t\"fcitx5 --replace -d\",\n\t\t-- \"xrandr --output LVDS-1 --gamma 0.9:0.9:0.9\",\n\t}\n\n\tfor _, i in pairs(cmds) do\n\t\tawful.spawn.with_shell(i .. \" &\")\n\tend\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/error-handling.lua",
    "content": "local naughty = require(\"naughty\")\n\nlocal M = {}\n\n-- Check if awesome encountered an error during startup and fell back to\n-- another config (This code will only ever execute for the fallback config)\nM.setup = function()\n\tif awesome.startup_errors then\n\t\tnaughty.notify({\n\t\t\tpreset = naughty.config.presets.critical,\n\t\t\ttitle = \"Oops, there were errors during startup!\",\n\t\t\ttext = awesome.startup_errors,\n\t\t})\n\tend\n\n\t-- Handle runtime errors after startup\n\tdo\n\t\tlocal in_error = false\n\t\tawesome.connect_signal(\"debug::error\", function(err)\n\t\t\t-- Make sure we don't go into an endless error loop\n\t\t\tif in_error then\n\t\t\t\treturn\n\t\t\tend\n\t\t\tin_error = true\n\n\t\t\tnaughty.notify({\n\t\t\t\tpreset = naughty.config.presets.critical,\n\t\t\t\ttitle = \"Oops, an error happened!\",\n\t\t\t\ttext = tostring(err),\n\t\t\t})\n\t\t\tin_error = false\n\t\tend)\n\tend\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/exitscreen.lua",
    "content": "local awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal wibox = require(\"wibox\")\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\nlocal rrect = require(\"main.helpers\").rrect\nlocal markup = require(\"main.helpers\").markup\nlocal spawn = awful.spawn\n\n-- Appearance\nlocal icon_font = \"JetBrainsMono Nerd Font 30\"\nlocal poweroff_text_icon = \" \"\nlocal reboot_text_icon = \" \"\nlocal suspend_text_icon = \" \"\nlocal exit_text_icon = \" \"\nlocal exitscreen_bg = theme.background .. \"dd\"\n\nlocal button_bg = theme.black\nlocal button_size = dpi(120)\n\n-- Commands\nlocal poweroff_command = function()\n\tspawn.with_shell(\"systemctl poweroff\")\nend\nlocal reboot_command = function()\n\tspawn.with_shell(\"systemctl reboot\")\nend\nlocal suspend_command = function()\n\tspawn.with_shell(\"systemctl suspend\")\nend\nlocal exit_command = function()\n\tawesome.quit()\nend\n\nlocal add_hover_cursor = function(widget, hover_cursor)\n\tlocal original_cursor = \"left_ptr\"\n\n\twidget:connect_signal(\"mouse::enter\", function()\n\t\tlocal w = mouse.current_wibox\n\t\tif w then\n\t\t\tw.cursor = hover_cursor\n\t\tend\n\tend)\n\n\twidget:connect_signal(\"mouse::leave\", function()\n\t\tlocal w = mouse.current_wibox\n\t\tif w then\n\t\t\tw.cursor = original_cursor\n\t\tend\n\tend)\nend\n\n-- Helper function that generates the clickable buttons\nlocal create_button = function(symbol, hover_color, command)\n\tlocal icon = wibox.widget({\n\t\tforced_height = button_size,\n\t\tforced_width = button_size,\n\t\talign = \"center\",\n\t\tvalign = \"center\",\n\t\tfont = icon_font,\n\t\ttext = symbol,\n\t\twidget = wibox.widget.textbox(),\n\t})\n\n\tlocal button = wibox.widget({\n\t\t{ nil, icon, expand = \"none\", layout = wibox.layout.align.horizontal },\n\t\tforced_height = button_size,\n\t\tforced_width = button_size,\n\t\tborder_width = dpi(3),\n\t\tborder_color = button_bg,\n\t\tshape = rrect(10),\n\t\tbg = button_bg,\n\t\twidget = wibox.container.background,\n\t})\n\n\t-- Bind left click to run the command\n\tbutton:buttons(gears.table.join(awful.button({}, 1, function()\n\t\tcommand()\n\tend)))\n\n\t-- Change color on hover\n\tbutton:connect_signal(\"mouse::enter\", function()\n\t\ticon.markup = markup(icon.text, { fg = hover_color })\n\t\tbutton.border_color = hover_color\n\tend)\n\tbutton:connect_signal(\"mouse::leave\", function()\n\t\ticon.markup = markup(icon.text, { fg = theme.foreground })\n\t\tbutton.border_color = button_bg\n\tend)\n\n\t-- Use helper function to change the cursor on hover\n\tadd_hover_cursor(button, \"hand1\")\n\n\treturn button\nend\n\n-- Create the buttons\nlocal poweroff = create_button(poweroff_text_icon, theme.red, poweroff_command)\nlocal reboot = create_button(reboot_text_icon, theme.green, reboot_command)\nlocal suspend = create_button(suspend_text_icon, theme.yellow, suspend_command)\nlocal exit = create_button(exit_text_icon, theme.red, exit_command)\n\n-- Create the exit screen wibox\nlocal exit_screen = wibox({ visible = false, ontop = true, type = \"dock\" })\nawful.placement.maximize(exit_screen)\n\nexit_screen.bg = theme.exit_screen_bg or exitscreen_bg\nexit_screen.fg = theme.exit_screen_fg or theme.wibar_fg\n\nlocal exit_screen_grabber\nlocal exit_screen_hide = function()\n\tawful.keygrabber.stop(exit_screen_grabber)\n\texit_screen.visible = false\nend\n\nlocal exit_screen_show = function()\n\texit_screen_grabber = awful.keygrabber.run(function(_, key, event)\n\t\t-- Ignore case\n\t\tkey = key:lower()\n\n\t\tif event == \"release\" then\n\t\t\treturn\n\t\tend\n\n\t\tif key == \"s\" then\n\t\t\tsuspend_command()\n\t\t\texit_screen_hide()\n\t\telseif key == \"e\" then\n\t\t\texit_command()\n\t\telseif key == \"p\" then\n\t\t\tpoweroff_command()\n\t\telseif key == \"r\" then\n\t\t\treboot_command()\n\t\telseif key == \"escape\" or key == \"q\" or key == \"x\" then\n\t\t\texit_screen_hide()\n\t\tend\n\tend)\n\texit_screen.visible = true\nend\n\nexit_screen:buttons(gears.table.join(\n\tawful.button({}, 1, function()\n\t\texit_screen_hide()\n\tend),\n\tawful.button({}, 2, function()\n\t\texit_screen_hide()\n\tend),\n\tawful.button({}, 3, function()\n\t\texit_screen_hide()\n\tend)\n))\n\nexit_screen:setup({\n\tnil,\n\t{\n\t\tnil,\n\t\t{\n\t\t\tpoweroff,\n\t\t\treboot,\n\t\t\tsuspend,\n\t\t\texit,\n\t\t\tspacing = dpi(50),\n\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t},\n\t\texpand = \"none\",\n\t\tlayout = wibox.layout.align.horizontal,\n\t},\n\texpand = \"none\",\n\tlayout = wibox.layout.align.vertical,\n})\n\nreturn exit_screen_show\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/helpers.lua",
    "content": "local awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal wibox = require(\"wibox\")\nlocal xresources = require(\"beautiful.xresources\")\nlocal dpi = xresources.apply_dpi\n\nlocal helpers = {}\n\nhelpers.noop = function() end\n\n-- Resize client or factor\nlocal floating_resize_amount = dpi(20)\nlocal tiling_resize_factor = 0.05\n\nhelpers.resize_dwim = function(c, direction)\n\tif c and c.floating then\n\t\tif direction == \"up\" then\n\t\t\tc:relative_move(0, 0, 0, -floating_resize_amount)\n\t\telseif direction == \"down\" then\n\t\t\tc:relative_move(0, 0, 0, floating_resize_amount)\n\t\telseif direction == \"left\" then\n\t\t\tc:relative_move(0, 0, -floating_resize_amount, 0)\n\t\telseif direction == \"right\" then\n\t\t\tc:relative_move(0, 0, floating_resize_amount, 0)\n\t\tend\n\telseif awful.layout.get(mouse.screen) ~= awful.layout.suit.floating then\n\t\tif direction == \"up\" then\n\t\t\tawful.client.incwfact(-tiling_resize_factor)\n\t\telseif direction == \"down\" then\n\t\t\tawful.client.incwfact(tiling_resize_factor)\n\t\telseif direction == \"left\" then\n\t\t\tawful.tag.incmwfact(-tiling_resize_factor)\n\t\telseif direction == \"right\" then\n\t\t\tawful.tag.incmwfact(tiling_resize_factor)\n\t\tend\n\tend\nend\n\nhelpers.colorize = function(icon, color)\n\treturn gears.color.recolor_image(icon, color)\nend\n\nhelpers.markup = function(content, opts)\n\tlocal fg = opts.fg or \"\"\n\treturn string.format('<span foreground=\"%s\">%s</span>', fg, content)\nend\n\nhelpers.rrect = function(radius)\n\treturn function(cr, width, height)\n\t\tgears.shape.rounded_rect(cr, width, height, radius)\n\tend\nend\n\nhelpers.module_wrapper = function(opts)\n\tlocal left, right, top, bottom\n\n\tif opts.type == \"icon\" then\n\t\tleft = opts.left or 6\n\t\tright = opts.right or 4\n\t\ttop = opts.top or opts.no_bg and 8 or 4\n\t\tbottom = opts.bottom or opts.no_bg and 8 or 4\n\telseif opts.type == \"module\" then\n\t\tleft = opts.left or 0\n\t\tright = opts.right or 6\n\t\ttop = opts.top or 4\n\t\tbottom = opts.bottom or 4\n\tend\n\n\tif opts.no_bg then\n\t\treturn wibox.widget({\n\t\t\topts.widget,\n\t\t\ttop = dpi(top),\n\t\t\tleft = dpi(left),\n\t\t\tright = dpi(right),\n\t\t\tbottom = dpi(bottom),\n\t\t\twidget = wibox.container.margin,\n\t\t})\n\tend\n\n\treturn wibox.widget({\n\t\t{\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\topts.widget,\n\t\t\t\t\ttop = dpi(top),\n\t\t\t\t\tleft = dpi(left),\n\t\t\t\t\tright = dpi(right),\n\t\t\t\t\tbottom = dpi(bottom),\n\t\t\t\t\twidget = wibox.container.margin,\n\t\t\t\t},\n\t\t\t\tbg = theme.grey_alt,\n\t\t\t\twidget = wibox.container.background,\n\t\t\t},\n\t\t\tcolor = theme.blue,\n\t\t\tbottom = dpi(1),\n\t\t\twidget = wibox.container.margin,\n\t\t},\n\t\ttop = dpi(4),\n\t\tbottom = dpi(2),\n\t\twidget = wibox.container.margin,\n\t})\nend\n\nreturn helpers\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/json.lua",
    "content": "--\n-- json.lua\n--\n-- Copyright (c) 2020 rxi\n--\n-- Permission is hereby granted, free of charge, to any person obtaining a copy of\n-- this software and associated documentation files (the \"Software\"), to deal in\n-- the Software without restriction, including without limitation the rights to\n-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n-- of the Software, and to permit persons to whom the Software is furnished to do\n-- so, subject to the following conditions:\n--\n-- The above copyright notice and this permission notice shall be included in all\n-- copies or substantial portions of the Software.\n--\n-- THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n-- SOFTWARE.\n--\n\nlocal json = { _version = \"0.1.2\" }\n\n-------------------------------------------------------------------------------\n-- Encode\n-------------------------------------------------------------------------------\n\nlocal encode\n\nlocal escape_char_map = {\n\t[\"\\\\\"] = \"\\\\\",\n\t['\"'] = '\"',\n\t[\"\\b\"] = \"b\",\n\t[\"\\f\"] = \"f\",\n\t[\"\\n\"] = \"n\",\n\t[\"\\r\"] = \"r\",\n\t[\"\\t\"] = \"t\",\n}\n\nlocal escape_char_map_inv = { [\"/\"] = \"/\" }\nfor k, v in pairs(escape_char_map) do\n\tescape_char_map_inv[v] = k\nend\n\nlocal function escape_char(c)\n\treturn \"\\\\\" .. (escape_char_map[c] or string.format(\"u%04x\", c:byte()))\nend\n\nlocal function encode_nil(val)\n\treturn \"null\"\nend\n\nlocal function encode_table(val, stack)\n\tlocal res = {}\n\tstack = stack or {}\n\n\t-- Circular reference?\n\tif stack[val] then\n\t\terror(\"circular reference\")\n\tend\n\n\tstack[val] = true\n\n\tif rawget(val, 1) ~= nil or next(val) == nil then\n\t\t-- Treat as array -- check keys are valid and it is not sparse\n\t\tlocal n = 0\n\t\tfor k in pairs(val) do\n\t\t\tif type(k) ~= \"number\" then\n\t\t\t\terror(\"invalid table: mixed or invalid key types\")\n\t\t\tend\n\t\t\tn = n + 1\n\t\tend\n\t\tif n ~= #val then\n\t\t\terror(\"invalid table: sparse array\")\n\t\tend\n\t\t-- Encode\n\t\tfor i, v in ipairs(val) do\n\t\t\ttable.insert(res, encode(v, stack))\n\t\tend\n\t\tstack[val] = nil\n\t\treturn \"[\" .. table.concat(res, \",\") .. \"]\"\n\telse\n\t\t-- Treat as an object\n\t\tfor k, v in pairs(val) do\n\t\t\tif type(k) ~= \"string\" then\n\t\t\t\terror(\"invalid table: mixed or invalid key types\")\n\t\t\tend\n\t\t\ttable.insert(res, encode(k, stack) .. \":\" .. encode(v, stack))\n\t\tend\n\t\tstack[val] = nil\n\t\treturn \"{\" .. table.concat(res, \",\") .. \"}\"\n\tend\nend\n\nlocal function encode_string(val)\n\treturn '\"' .. val:gsub('[%z\\1-\\31\\\\\"]', escape_char) .. '\"'\nend\n\nlocal function encode_number(val)\n\t-- Check for NaN, -inf and inf\n\tif val ~= val or val <= -math.huge or val >= math.huge then\n\t\terror(\"unexpected number value '\" .. tostring(val) .. \"'\")\n\tend\n\treturn string.format(\"%.14g\", val)\nend\n\nlocal type_func_map = {\n\t[\"nil\"] = encode_nil,\n\t[\"table\"] = encode_table,\n\t[\"string\"] = encode_string,\n\t[\"number\"] = encode_number,\n\t[\"boolean\"] = tostring,\n}\n\nencode = function(val, stack)\n\tlocal t = type(val)\n\tlocal f = type_func_map[t]\n\tif f then\n\t\treturn f(val, stack)\n\tend\n\terror(\"unexpected type '\" .. t .. \"'\")\nend\n\nfunction json.encode(val)\n\treturn (encode(val))\nend\n\n-------------------------------------------------------------------------------\n-- Decode\n-------------------------------------------------------------------------------\n\nlocal parse\n\nlocal function create_set(...)\n\tlocal res = {}\n\tfor i = 1, select(\"#\", ...) do\n\t\tres[select(i, ...)] = true\n\tend\n\treturn res\nend\n\nlocal space_chars = create_set(\" \", \"\\t\", \"\\r\", \"\\n\")\nlocal delim_chars = create_set(\" \", \"\\t\", \"\\r\", \"\\n\", \"]\", \"}\", \",\")\nlocal escape_chars = create_set(\"\\\\\", \"/\", '\"', \"b\", \"f\", \"n\", \"r\", \"t\", \"u\")\nlocal literals = create_set(\"true\", \"false\", \"null\")\n\nlocal literal_map = {\n\t[\"true\"] = true,\n\t[\"false\"] = false,\n\t[\"null\"] = nil,\n}\n\nlocal function next_char(str, idx, set, negate)\n\tfor i = idx, #str do\n\t\tif set[str:sub(i, i)] ~= negate then\n\t\t\treturn i\n\t\tend\n\tend\n\treturn #str + 1\nend\n\nlocal function decode_error(str, idx, msg)\n\tlocal line_count = 1\n\tlocal col_count = 1\n\tfor i = 1, idx - 1 do\n\t\tcol_count = col_count + 1\n\t\tif str:sub(i, i) == \"\\n\" then\n\t\t\tline_count = line_count + 1\n\t\t\tcol_count = 1\n\t\tend\n\tend\n\terror(string.format(\"%s at line %d col %d\", msg, line_count, col_count))\nend\n\nlocal function codepoint_to_utf8(n)\n\t-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa\n\tlocal f = math.floor\n\tif n <= 0x7f then\n\t\treturn string.char(n)\n\telseif n <= 0x7ff then\n\t\treturn string.char(f(n / 64) + 192, n % 64 + 128)\n\telseif n <= 0xffff then\n\t\treturn string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)\n\telseif n <= 0x10ffff then\n\t\treturn string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128)\n\tend\n\terror(string.format(\"invalid unicode codepoint '%x'\", n))\nend\n\nlocal function parse_unicode_escape(s)\n\tlocal n1 = tonumber(s:sub(1, 4), 16)\n\tlocal n2 = tonumber(s:sub(7, 10), 16)\n\t-- Surrogate pair?\n\tif n2 then\n\t\treturn codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)\n\telse\n\t\treturn codepoint_to_utf8(n1)\n\tend\nend\n\nlocal function parse_string(str, i)\n\tlocal res = \"\"\n\tlocal j = i + 1\n\tlocal k = j\n\n\twhile j <= #str do\n\t\tlocal x = str:byte(j)\n\n\t\tif x < 32 then\n\t\t\tdecode_error(str, j, \"control character in string\")\n\t\telseif x == 92 then -- `\\`: Escape\n\t\t\tres = res .. str:sub(k, j - 1)\n\t\t\tj = j + 1\n\t\t\tlocal c = str:sub(j, j)\n\t\t\tif c == \"u\" then\n\t\t\t\tlocal hex = str:match(\"^[dD][89aAbB]%x%x\\\\u%x%x%x%x\", j + 1)\n\t\t\t\t\tor str:match(\"^%x%x%x%x\", j + 1)\n\t\t\t\t\tor decode_error(str, j - 1, \"invalid unicode escape in string\")\n\t\t\t\tres = res .. parse_unicode_escape(hex)\n\t\t\t\tj = j + #hex\n\t\t\telse\n\t\t\t\tif not escape_chars[c] then\n\t\t\t\t\tdecode_error(str, j - 1, \"invalid escape char '\" .. c .. \"' in string\")\n\t\t\t\tend\n\t\t\t\tres = res .. escape_char_map_inv[c]\n\t\t\tend\n\t\t\tk = j + 1\n\t\telseif x == 34 then -- `\"`: End of string\n\t\t\tres = res .. str:sub(k, j - 1)\n\t\t\treturn res, j + 1\n\t\tend\n\n\t\tj = j + 1\n\tend\n\n\tdecode_error(str, i, \"expected closing quote for string\")\nend\n\nlocal function parse_number(str, i)\n\tlocal x = next_char(str, i, delim_chars)\n\tlocal s = str:sub(i, x - 1)\n\tlocal n = tonumber(s)\n\tif not n then\n\t\tdecode_error(str, i, \"invalid number '\" .. s .. \"'\")\n\tend\n\treturn n, x\nend\n\nlocal function parse_literal(str, i)\n\tlocal x = next_char(str, i, delim_chars)\n\tlocal word = str:sub(i, x - 1)\n\tif not literals[word] then\n\t\tdecode_error(str, i, \"invalid literal '\" .. word .. \"'\")\n\tend\n\treturn literal_map[word], x\nend\n\nlocal function parse_array(str, i)\n\tlocal res = {}\n\tlocal n = 1\n\ti = i + 1\n\twhile 1 do\n\t\tlocal x\n\t\ti = next_char(str, i, space_chars, true)\n\t\t-- Empty / end of array?\n\t\tif str:sub(i, i) == \"]\" then\n\t\t\ti = i + 1\n\t\t\tbreak\n\t\tend\n\t\t-- Read token\n\t\tx, i = parse(str, i)\n\t\tres[n] = x\n\t\tn = n + 1\n\t\t-- Next token\n\t\ti = next_char(str, i, space_chars, true)\n\t\tlocal chr = str:sub(i, i)\n\t\ti = i + 1\n\t\tif chr == \"]\" then\n\t\t\tbreak\n\t\tend\n\t\tif chr ~= \",\" then\n\t\t\tdecode_error(str, i, \"expected ']' or ','\")\n\t\tend\n\tend\n\treturn res, i\nend\n\nlocal function parse_object(str, i)\n\tlocal res = {}\n\ti = i + 1\n\twhile 1 do\n\t\tlocal key, val\n\t\ti = next_char(str, i, space_chars, true)\n\t\t-- Empty / end of object?\n\t\tif str:sub(i, i) == \"}\" then\n\t\t\ti = i + 1\n\t\t\tbreak\n\t\tend\n\t\t-- Read key\n\t\tif str:sub(i, i) ~= '\"' then\n\t\t\tdecode_error(str, i, \"expected string for key\")\n\t\tend\n\t\tkey, i = parse(str, i)\n\t\t-- Read ':' delimiter\n\t\ti = next_char(str, i, space_chars, true)\n\t\tif str:sub(i, i) ~= \":\" then\n\t\t\tdecode_error(str, i, \"expected ':' after key\")\n\t\tend\n\t\ti = next_char(str, i + 1, space_chars, true)\n\t\t-- Read value\n\t\tval, i = parse(str, i)\n\t\t-- Set\n\t\tres[key] = val\n\t\t-- Next token\n\t\ti = next_char(str, i, space_chars, true)\n\t\tlocal chr = str:sub(i, i)\n\t\ti = i + 1\n\t\tif chr == \"}\" then\n\t\t\tbreak\n\t\tend\n\t\tif chr ~= \",\" then\n\t\t\tdecode_error(str, i, \"expected '}' or ','\")\n\t\tend\n\tend\n\treturn res, i\nend\n\nlocal char_func_map = {\n\t['\"'] = parse_string,\n\t[\"0\"] = parse_number,\n\t[\"1\"] = parse_number,\n\t[\"2\"] = parse_number,\n\t[\"3\"] = parse_number,\n\t[\"4\"] = parse_number,\n\t[\"5\"] = parse_number,\n\t[\"6\"] = parse_number,\n\t[\"7\"] = parse_number,\n\t[\"8\"] = parse_number,\n\t[\"9\"] = parse_number,\n\t[\"-\"] = parse_number,\n\t[\"t\"] = parse_literal,\n\t[\"f\"] = parse_literal,\n\t[\"n\"] = parse_literal,\n\t[\"[\"] = parse_array,\n\t[\"{\"] = parse_object,\n}\n\nparse = function(str, idx)\n\tlocal chr = str:sub(idx, idx)\n\tlocal f = char_func_map[chr]\n\tif f then\n\t\treturn f(str, idx)\n\tend\n\tdecode_error(str, idx, \"unexpected character '\" .. chr .. \"'\")\nend\n\nfunction json.decode(str)\n\tif type(str) ~= \"string\" then\n\t\terror(\"expected argument of type string, got \" .. type(str))\n\tend\n\tlocal res, idx = parse(str, next_char(str, 1, space_chars, true))\n\tidx = next_char(str, idx, space_chars, true)\n\tif idx <= #str then\n\t\tdecode_error(str, idx, \"trailing garbage\")\n\tend\n\treturn res\nend\n\nreturn json\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/layouts.lua",
    "content": "local awful = require(\"awful\")\n\n-- Table of layouts to cover with awful.layout.inc, order matters.\nlocal layouts = {\n\tawful.layout.suit.tile,\n\tawful.layout.suit.floating,\n\tawful.layout.suit.max,\n}\n\nreturn layouts\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/menu.lua",
    "content": "local menubar = require(\"menubar\")\nlocal awful = require(\"awful\")\nlocal spawn = awful.spawn.with_shell\nlocal hotkeys_popup = require(\"awful.hotkeys_popup\")\n\nlocal M = {}\n\nlocal menu = {\n\t{\n\t\t\"hotkeys\",\n\t\tfunction()\n\t\t\thotkeys_popup.show_help(nil, awful.screen.focused())\n\t\tend,\n\t},\n\t{\n\t\t\"restart awesome\",\n\t\tfunction()\n\t\t\tawesome.restart()\n\t\tend,\n\t},\n\t{\n\t\t\"quit awesome\",\n\t\tfunction()\n\t\t\tawesome.quit()\n\t\tend,\n\t},\n}\n\nlocal apps = {\n\t{\n\t\t\"alacritty\",\n\t\tfunction()\n\t\t\tspawn(\"alacritty &\")\n\t\tend,\n\t},\n\t{\n\t\t\"kitty\",\n\t\tfunction()\n\t\t\tspawn(\"kitty &\")\n\t\tend,\n\t},\n\t{\n\t\t\"chrome\",\n\t\tfunction()\n\t\t\tspawn(\"chrome &\")\n\t\tend,\n\t},\n\t{\n\t\t\"telegram\",\n\t\tfunction()\n\t\t\tspawn(\"telegram &\")\n\t\tend,\n\t},\n\t{\n\t\t\"spotify\",\n\t\tfunction()\n\t\t\tspawn(\"spotify &\")\n\t\tend,\n\t},\n}\n\nlocal powermenu = {\n\t{\n\t\t\"reboot\",\n\t\tfunction()\n\t\t\tos.execute(\"reboot\")\n\t\tend,\n\t},\n\t{\n\t\t\"shutdown\",\n\t\tfunction()\n\t\t\tos.execute(\"shutdown now\")\n\t\tend,\n\t},\n}\n\nM.menu_items = awful.menu({\n\titems = {\n\t\t{ \"awesome\", menu },\n\t\t{ \"apps\", apps },\n\t\t{ \"powermenu\", powermenu },\n\t},\n})\n\n-- Set the terminal for applications that require it\nmenubar.utils.terminal = terminal\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/rules.lua",
    "content": "local awful = require \"awful\"\nlocal beautiful = require \"beautiful\"\nlocal clientbuttons = require \"keybinds.clientbuttons\"\nlocal clientkeys = require \"keybinds.clientkeys\"\n\nlocal M = {}\n\nM.setup = function()\n  awful.rules.rules = {\n    -- All clients will match this rule.\n    {\n      rule = {},\n      properties = {\n        border_width = beautiful.border_width,\n        border_color = beautiful.border_normal,\n        focus = awful.client.focus.filter,\n        raise = true,\n        keys = clientkeys,\n        buttons = clientbuttons,\n        screen = awful.screen.preferred,\n        titlebars_enabled = true,\n      },\n    },\n\n    -- Floating clients.\n    {\n      rule_any = {\n        instance = {\n          \"pinentry\",\n        },\n        class = {\n          \"Arandr\",\n          \"Pcmanfm\",\n          \"Sxiv\",\n          \"connman-gtk\",\n          \"Connman-gtk\",\n          \"SimpleScreenRecorder\",\n          \"Anki\",\n          \"lxappearance\",\n          \"Lxappearance\",\n          \"fcitx5-config-qt\",\n          \"jswing-App\",\n          \"gcr-prompter\",\n          \"Gcr-prompter\",\n          \"explorer.exe\",\n        },\n\n        -- Note that the name property shown in xprop might be set slightly after creation of the client\n        -- and the name shown there might not match defined rules here.\n        name = {\n          \"Event Tester\", -- xev.\n          \"Open File\", -- file picker\n          \"Media viewer\", -- new telegram image viewer\n          \"pinentry-gnome3\",\n        },\n        role = {\n          \"GtkFileChooserDialog\", -- file picker\n          \"pop-up\", -- e.g. Google Chrome's (detached) Developer Tools.\n        },\n      },\n      properties = {\n        floating = true,\n        placement = awful.placement.centered,\n      },\n    },\n\n    {\n      rule_any = {\n        name = {\n          \"Media viewer\", -- new telegram image viewer\n        },\n      },\n      properties = {\n        fullscreen = true,\n        placement = awful.placement.centered,\n      },\n    },\n\n    -- Add titlebars to normal clients and dialogs\n    {\n      rule_any = {\n        type = { \"normal\", \"dialog\" },\n      },\n      properties = {\n        titlebars_enabled = true,\n      },\n    },\n\n    -- remove borders from browsers\n    {\n      rule_any = {\n        class = {\n          \"Chromium\",\n          \"Google-chrome-unstable\",\n          \"firefoxdeveloperedition\",\n          \"Firefox\",\n          \"Brave-browser\",\n          \"DesktopEditors\",\n        },\n      },\n      properties = {\n        border_width = 0,\n        titlebars_enabled = false,\n      },\n    },\n  }\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/signals.lua",
    "content": "-- Standard awesome library\nlocal awful = require(\"awful\")\nlocal beautiful = require(\"beautiful\")\nlocal gears = require(\"gears\")\n\nlocal M = {}\n\nM.setup = function()\n\t-- Signal function to execute when a new client appears.\n\tclient.connect_signal(\"manage\", function(c)\n\t\t-- Set the windows at the slave,\n\t\t-- i.e. put it at the end of others instead of setting it master.\n\t\tif not awesome.startup then\n\t\t\tawful.client.setslave(c)\n\t\tend\n\n\t\t-- -- rounded corners\n\t\t-- c.shape = function(cr, w, h)\n\t\t--   require\"gears\".shape.rounded_rect(cr, w, h, require\"beautiful.xresources\".apply_dpi(8))\n\t\t-- end\n\n\t\tif awesome.startup and not c.size_hints.user_position and not c.size_hints.program_position then\n\t\t\t-- Prevent clients from being unreachable after screen count changes.\n\t\t\tawful.placement.no_offscreen(c)\n\t\tend\n\tend)\n\n\t-- Focus follows mouse.\n\tclient.connect_signal(\"mouse::enter\", function(c)\n\t\tc:emit_signal(\"request::activate\", \"mouse_enter\", { raise = false })\n\tend)\n\n\tclient.connect_signal(\"property::fullscreen\", function(c)\n\t\tif c.fullscreen then\n\t\t\tgears.timer.delayed_call(function()\n\t\t\t\tif c.valid then\n\t\t\t\t\tc:geometry(c.screen.geometry)\n\t\t\t\tend\n\t\t\tend)\n\t\tend\n\tend)\n\n\tclient.connect_signal(\"focus\", function(c)\n\t\tc.border_color = beautiful.border_focus\n\tend)\n\n\tclient.connect_signal(\"unfocus\", function(c)\n\t\tc.border_color = beautiful.border_normal\n\tend)\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/tags.lua",
    "content": "local awful = require(\"awful\")\nlocal layouts = require(\"main.layouts\")\n\nlocal M = {}\n\nM.setup = function()\n\tawful.screen.connect_for_each_screen(function(s)\n\t\t-- Each screen has its own tag table.\n\t\tawful.tag({ \"一\", \"二\", \"三\", \"四\", \"五\", \"六\" }, s, layouts[1])\n\tend)\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/titlebar.lua",
    "content": "local awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal wibox = require(\"wibox\")\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\nlocal margin = wibox.container.margin\n\nlocal M = {}\n\nfunction M.setup()\n\tlocal create_title_button = function(c, color_focus, color_unfocus)\n\t\tlocal tb_color = wibox.widget({\n\t\t\tbg = theme.black,\n\t\t\tshape = gears.shape.circle,\n\t\t\tshape_border_width = dpi(5),\n\t\t\tshape_border_color = color_focus,\n\t\t\twidget = wibox.container.background,\n\t\t})\n\n\t\tlocal tb = wibox.widget({\n\t\t\ttb_color,\n\t\t\twidth = dpi(12),\n\t\t\theight = dpi(12),\n\t\t\tstrategy = \"min\",\n\t\t\tlayout = wibox.container.constraint,\n\t\t})\n\n\t\tlocal update = function()\n\t\t\tif client.focus == c then\n\t\t\t\ttb_color.shape_border_color = color_focus\n\t\t\telse\n\t\t\t\ttb_color.shape_border_color = color_unfocus\n\t\t\tend\n\t\tend\n\n\t\tupdate()\n\t\tc:connect_signal(\"focus\", update)\n\t\tc:connect_signal(\"unfocus\", update)\n\n\t\treturn tb\n\tend\n\n\t-- Add a titlebar if titlebars_enabled is set to true in the rules.\n\tclient.connect_signal(\"request::titlebars\", function(c)\n\t\t-- buttons for the titlebar\n\t\tlocal buttons = gears.table.join(\n\t\t\tawful.button({}, 1, function()\n\t\t\t\tclient.focus = c\n\t\t\t\tc:raise()\n\t\t\t\tawful.mouse.client.move(c)\n\t\t\tend),\n\n\t\t\tawful.button({}, 3, function()\n\t\t\t\tc:emit_signal(\"request::activate\", \"titlebar\", { raise = true })\n\t\t\t\tawful.mouse.client.resize(c)\n\t\t\tend)\n\t\t)\n\n\t\tlocal close = create_title_button(c, theme.red, theme.grey)\n\t\tclose:connect_signal(\"button::press\", function()\n\t\t\tc:kill()\n\t\tend)\n\n\t\tlocal floating = create_title_button(c, theme.yellow, theme.grey)\n\t\tfloating:connect_signal(\"button::press\", function()\n\t\t\tc.ontop = not c.ontop\n\t\tend)\n\n\t\tlocal fullscreen = create_title_button(c, theme.green, theme.grey)\n\t\tfullscreen:connect_signal(\"button::press\", function()\n\t\t\tc.floating = not c.floating\n\t\tend)\n\n\t\tlocal window_title = { -- client name\n\t\t\talign = \"center\",\n\t\t\tfont = theme.titlebar_font,\n\t\t\twidget = awful.titlebar.widget.titlewidget(c),\n\t\t}\n\n\t\tlocal icon = { -- client icon\n\t\t\twidget = awful.titlebar.widget.iconwidget(c),\n\t\t}\n\n\t\tawful.titlebar(c, { position = \"top\", size = theme.titlebar_size }):setup({\n\t\t\t{\n\t\t\t\tmargin(icon, dpi(2), dpi(2), dpi(2), dpi(2)),\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t\t{\n\t\t\t\twindow_title,\n\t\t\t\tbuttons = buttons,\n\t\t\t\tlayout = wibox.layout.flex.horizontal,\n\t\t\t},\n\t\t\t{\n\t\t\t\tfloating,\n\t\t\t\tfullscreen,\n\t\t\t\tmargin(close, dpi(0), dpi(8)),\n\t\t\t\tspacing = dpi(8),\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t\tlayout = wibox.layout.align.horizontal,\n\t\t})\n\tend)\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/variables.lua",
    "content": "local HOME_DIR = os.getenv(\"HOME\")\nlocal EDITOR = os.getenv(\"EDITOR\")\nlocal TERMINAL = os.getenv(\"TERMINAL\")\n\nlocal variables = {\n\tterminal = TERMINAL or \"alacritty\",\n\teditor = EDITOR or \"nano\",\n\tshot = HOME_DIR .. \"/.scripts/shot\",\n\temoji_picker = HOME_DIR .. \"/.scripts/emoji_picker\",\n\tclipmenu = \"clipmenu -p 'Clipboard: ' -theme \"\n\t\t.. HOME_DIR\n\t\t.. \"/.config/rofi/themes/\"\n\t\t.. theme.color_name\n\t\t.. \".rasi\",\n\tlauncher = HOME_DIR .. \"/.scripts/launcher \",\n\tmodkey = \"Mod4\",\n\taltkey = \"Mod1\",\n\tctrlkey = \"Control\",\n}\n\nreturn variables\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/main/volume-widget/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\"beautiful\")\nlocal dpi = beautiful.xresources.apply_dpi\nlocal colorize = require(\"main.helpers\").colorize\n\nlocal offsetx = dpi(64)\nlocal offsety = dpi(300)\nlocal screen = awful.screen.focused()\n\nlocal volume_up = os.getenv(\"HOME\") .. \"/.config/awesome/main/volume-widget/volume_up.svg\"\n\nlocal M = {}\n\nfunction M.setup()\n\t-- create the volume_adjust component\n\tlocal volume_adjust = wibox({\n\t\tscreen = awful.screen.focused(),\n\t\tx = screen.geometry.width - offsetx,\n\t\ty = (screen.geometry.height / 2) - (offsety / 2),\n\t\twidth = dpi(48),\n\t\theight = offsety,\n\t\tshape = gears.shape.rect,\n\t\tvisible = false,\n\t\tontop = true,\n\t})\n\n\tlocal volume_bar = wibox.widget({\n\t\twidget = wibox.widget.progressbar,\n\t\tshape = gears.shape.rounded_bar,\n\t\tcolor = theme.widget_main_color,\n\t\tbackground_color = theme.grey_alt,\n\t\tmax_value = 100,\n\t\tvalue = 0,\n\t})\n\n\tvolume_adjust:setup({\n\t\tlayout = wibox.layout.align.vertical,\n\t\t{\n\t\t\twibox.container.margin(volume_bar, dpi(14), dpi(20), dpi(20), dpi(20)),\n\t\t\tforced_height = offsety * 0.75,\n\t\t\tdirection = \"east\",\n\t\t\tlayout = wibox.container.rotate,\n\t\t},\n\t\twibox.container.margin(\n\t\t\twibox.widget({\n\t\t\t\timage = colorize(volume_up, theme.widget_main_color),\n\t\t\t\twidget = wibox.widget.imagebox,\n\t\t\t}),\n\t\t\tdpi(10),\n\t\t\tdpi(10),\n\t\t\tdpi(14),\n\t\t\tdpi(14)\n\t\t),\n\t})\n\n\t-- create a 4 second timer to hide the volume adjust\n\t-- component whenever the timer is started\n\tlocal volume_timer = gears.timer({\n\t\ttimeout = 4,\n\t\tautostart = true,\n\t\tcallback = function()\n\t\t\tvolume_adjust.visible = false\n\t\tend,\n\t})\n\n\t-- show volume-adjust when \"volume_change\" signal is emitted\n\tawesome.connect_signal(\"volume_change\", function()\n\t\t-- set new volume value\n\t\tawful.spawn.easy_async(\"pulsemixer --get-volume\", function(stdout)\n\t\t\tvolume_bar.value = tonumber(stdout:gmatch(\"%d%d\")())\n\t\tend)\n\n\t\t-- make volume_adjust component visible\n\t\tif volume_adjust.visible then\n\t\t\tvolume_timer:again()\n\t\telse\n\t\t\tvolume_adjust.visible = true\n\t\t\tvolume_timer:start()\n\t\tend\n\tend)\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/rc.lua",
    "content": "-- If LuaRocks is installed, make sure that packages installed through it are\n-- found (e.g. lgi). If LuaRocks is not installed, do nothing.\npcall(require, \"luarocks.loader\")\nrequire(\"beautiful\").init(os.getenv(\"HOME\") .. \"/.config/awesome/themes/main/theme.lua\")\nrequire(\"awful.autofocus\")\n\nlocal gears = require(\"gears\")\nlocal menubar = require(\"menubar\")\nlocal bind_to_tags = require(\"keybinds.bindtotags\")\nlocal mediakeys = require(\"keybinds.mediakeys\")\nlocal globalkeys = require(\"keybinds.globalkeys\")\n\nlocal terminal = require(\"main.variables\").terminal\nmenubar.utils.terminal = terminal\n\nroot.keys(gears.table.join(bind_to_tags(globalkeys), mediakeys))\n\nrequire(\"main.error-handling\").setup()\nrequire(\"main.signals\").setup()\nrequire(\"main.titlebar\").setup()\nrequire(\"main.volume-widget\").setup()\nrequire(\"main.tags\").setup()\nrequire(\"main.autostart\").setup()\nrequire(\"main.rules\").setup()\nrequire(\"statusbar\").setup()\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/init.lua",
    "content": "local gears = require(\"gears\")\nlocal wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal beautiful = require(\"beautiful\")\nlocal dpi = beautiful.xresources.apply_dpi\nlocal module_wrapper = require(\"main.helpers\").module_wrapper\n\n-- Modules\nlocal systray = require(\"statusbar.modules.systray\")\nlocal launcher = require(\"statusbar.modules.launcher\")\nlocal clock = require(\"statusbar.modules.clock\")\nlocal battery = require(\"statusbar.modules.battery\")\nlocal memory = require(\"statusbar.modules.memory\")\nlocal cpu = require(\"statusbar.modules.cpu\")\nlocal netspeed = require(\"statusbar.modules.netspeed\")\nlocal volume = require(\"statusbar.modules.volume\")\nlocal temp = require(\"statusbar.modules.temp\")\nlocal taglist = require(\"statusbar.modules.taglist\")\n\nlocal set_wallpaper = function(s)\n\t-- Wallpaper\n\tif beautiful.wallpaper then\n\t\tgears.wallpaper.maximized(beautiful.wallpaper, s, true)\n\tend\nend\n\n-- Re-set wallpaper when a screen's geometry changes (e.g. different resolution)\nscreen.connect_signal(\"property::geometry\", set_wallpaper)\n\nlocal M = {}\n\nM.setup = function()\n\tawful.screen.connect_for_each_screen(function(s)\n\t\t-- Wallpaper\n\t\tset_wallpaper(s)\n\n\t\t-- Create a taglist widget\n\t\ts.taglist = taglist.widget(s)\n\n\t\t-- Create the wibox\n\t\ts.wibox = awful.wibar({\n\t\t\tposition = \"top\",\n\t\t\tscreen = s,\n\t\t\tbg = theme.statusbar_bg,\n\t\t\theight = theme.statusbar_height,\n\t\t\twidth = s.geometry.width,\n\t\t})\n\n\t\t-- Add widgets to the wibox\n\t\ts.wibox:setup({\n\t\t\tlayout = wibox.layout.align.horizontal,\n\t\t\texpand = \"none\",\n\t\t\t{\n\t\t\t\tmodule_wrapper({\n\t\t\t\t\ttype = \"module\",\n\t\t\t\t\twidget = launcher.widget,\n\t\t\t\t\tleft = dpi(5),\n\t\t\t\t\tright = dpi(5),\n\t\t\t\t\ttop = dpi(0),\n\t\t\t\t\tbottom = dpi(0),\n\t\t\t\t\tno_bg = true,\n\t\t\t\t}),\n\t\t\t\ts.taglist,\n\t\t\t\t{\n\t\t\t\t\t-- stylua: ignore\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = netspeed.wifi_icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = netspeed.down_icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = netspeed.down }),\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = netspeed.up_icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = netspeed.up, right = dpi(8) }),\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t\t{\n\t\t\t\tmodule_wrapper({ type = \"icon\", widget = clock.icon, no_bg = true }),\n\t\t\t\tmodule_wrapper({ type = \"module\", widget = clock.widget, no_bg = true }),\n\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = volume.icon, left = dpi(8) }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = volume.widget }),\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\t{\n\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = temp.icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = temp.widget }),\n\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = cpu.icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = cpu.widget }),\n\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = memory.icon }),\n\t\t\t\t\tmodule_wrapper({ type = \"module\", widget = memory.widget }),\n\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tmodule_wrapper({ type = \"icon\", widget = battery.icon }),\n\t\t\t\t\tmodule_wrapper({\n\t\t\t\t\t\ttype = \"module\",\n\t\t\t\t\t\twidget = battery.widget,\n\t\t\t\t\t\ttop = 0,\n\t\t\t\t\t\tbottom = 0,\n\t\t\t\t\t\tright = 0,\n\t\t\t\t\t}),\n\n\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t},\n\n\t\t\t\tmodule_wrapper({\n\t\t\t\t\ttype = \"module\",\n\t\t\t\t\twidget = systray.widget,\n\t\t\t\t\tleft = 6,\n\t\t\t\t\tright = 2,\n\t\t\t\t\ttop = 4,\n\t\t\t\t\tbottom = 4,\n\t\t\t\t}),\n\n\t\t\t\tspacing = dpi(4),\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t})\n\tend)\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/battery/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal colorize = require(\"main.helpers\").colorize\nlocal markup = require(\"main.helpers\").markup\nlocal icon = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/battery/icon.svg\"\n\nlocal M = {}\n\n-- Battery\nM.icon = wibox.widget.imagebox(colorize(icon, theme.widget_main_color))\n\nlocal get_bat_status = [[\n  sh -c \"\n    acpi | sed \\\"s/,//g\\\" | awk '{if ($3 == \\\"Discharging\\\"){print $4; exit} else {print $4\\\"+ \\\"}}' | tr -d \\\"\\n\\\"\n  \"\n]]\n\nM.widget = awful.widget.watch(get_bat_status, 60, function(widget, stdout)\n\twidget:set_markup(markup(stdout, { fg = theme.foreground }))\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/clock/init.lua",
    "content": "local gears = require(\"gears\")\nlocal wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal colorize = require(\"main.helpers\").colorize\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\nlocal icon = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/clock/icon.svg\"\n\nlocal M = {}\n\n-- Clock\nM.icon = wibox.widget.imagebox(colorize(icon, theme.widget_main_color))\n\nM.widget = wibox.widget.textclock(\"<span color='\" .. theme.foreground .. \"'>%a, %I:%M %p</span>\")\n\n-- Calendar Widget\nM.cal_shape = function(cr, width, height)\n\tgears.shape.rounded_rect(cr, width, height, false, false, true, true, 12)\nend\n\nM.month_calendar = awful.widget.calendar_popup.month({\n\tstart_sunday = false,\n\tspacing = dpi(10),\n\tfont = theme.font,\n\tlong_weekdays = true,\n\tmargin = dpi(8),\n\tstyle_month = { border_width = 0, shape = M.cal_shape, padding = dpi(25) },\n\tstyle_header = { border_width = 0, bg_color = theme.black },\n\tstyle_weekday = { border_width = 0, bg_color = theme.black },\n\tstyle_normal = { border_width = 0, bg_color = theme.black, fg_color = theme.grey },\n\tstyle_focus = { border_width = 0, bg_color = theme.black, fg_color = theme.widget_main_color },\n})\n\n-- Attach calentar to clock_widget\nM.month_calendar:attach(M.widget, \"tc\", { on_pressed = true, on_hover = false })\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/cpu/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal icon = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/cpu/icon.svg\"\nlocal colorize = require(\"main.helpers\").colorize\nlocal markup = require(\"main.helpers\").markup\n\nlocal M = {}\n\n-- CPU\nM.icon = wibox.widget.imagebox(colorize(icon, theme.widget_main_color))\n\nlocal get_cpu_status = [[\n  sh -c \"\n    top -bn1 | awk '/Cpu\\(s\\)/ {print (100-$8)\\\"%\\\"}'\n  \"\n]]\n\nM.widget = awful.widget.watch(get_cpu_status, 5, function(widget, stdout)\n\twidget:set_markup(markup(stdout, { fg = theme.foreground }))\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/launcher.lua",
    "content": "local awful = require(\"awful\")\nlocal beautiful = require(\"beautiful\")\nlocal menu = require(\"main.menu\")\n\nlocal M = {}\n\nM.widget = awful.widget.launcher({\n\timage = beautiful.menu_icon,\n\tmenu = menu.menu_items,\n})\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/memory/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal icon = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/memory/icon.svg\"\nlocal colorize = require(\"main.helpers\").colorize\nlocal markup = require(\"main.helpers\").markup\n\nlocal M = {}\n\n-- Memory\nM.icon = wibox.widget.imagebox(colorize(icon, theme.widget_main_color))\n\nlocal get_mem_status = [[\n  sh -c \"\n    free -h | awk '/^Mem/ { print $3 }' | sed s/i//g\n  \"\n]]\n\nM.widget = awful.widget.watch(get_mem_status, 5, function(widget, stdout)\n\twidget:set_markup(markup(stdout, { fg = theme.foreground }))\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/netspeed/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal markup = require(\"main.helpers\").markup\nlocal colorize = require(\"main.helpers\").colorize\n\nlocal up = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/netspeed/up.svg\"\nlocal down = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/netspeed/down.svg\"\n-- local wifi_off = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/netspeed/wifi_off.svg\"\nlocal wifi_on = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/netspeed/wifi_on.svg\"\n\nlocal M = {}\n\n-- Netspeed\nM.up_icon = wibox.widget.imagebox(colorize(up, theme.widget_main_color))\nM.down_icon = wibox.widget.imagebox(colorize(down, theme.widget_main_color))\n\nM.wifi_icon = wibox.widget.imagebox(colorize(wifi_on, theme.widget_main_color))\n\nlocal up_old = 0\nlocal down_old = 0\n\nlocal get_up = [[\n  sh -c \"cat /sys/class/net/[w]*/statistics/tx_bytes\"\n]]\n\nlocal get_down = [[\n  sh -c \"cat /sys/class/net/[w]*/statistics/rx_bytes\"\n]]\n\nM.up = awful.widget.watch(get_up, 2, function(widget, stdout)\n\tlocal num\n\tif up_old == 0 then\n\t\tnum = 0\n\telse\n\t\tnum = math.floor(((tonumber(stdout) - up_old) / 1024) / 2)\n\tend\n\twidget:set_markup(markup(tostring(num) .. \"KiB\", { fg = theme.foreground }))\n\tup_old = tonumber(stdout)\nend)\n\nM.down = awful.widget.watch(get_down, 2, function(widget, stdout)\n\tlocal num\n\tif down_old == 0 then\n\t\tnum = 0\n\telse\n\t\tnum = math.floor(((tonumber(stdout) - down_old) / 1024) / 2)\n\tend\n\twidget:set_markup(markup(tostring(num) .. \"KiB\", { fg = theme.foreground }))\n\tdown_old = tonumber(stdout)\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/systray.lua",
    "content": "local wibox = require(\"wibox\")\nlocal dpi = require(\"beautiful\").xresources.apply_dpi\n\nlocal M = {}\n\nM.widget = wibox.widget.systray({ forced_height = dpi(28) })\nM.widget:set_base_size(dpi(20))\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/taglist.lua",
    "content": "local gears = require(\"gears\")\nlocal awful = require(\"awful\")\nlocal modkey = require(\"main.variables\").modkey\n\nlocal M = {}\n\n-- Create a wibox for each screen and add it\nlocal taglist_buttons = gears.table.join(\n\tawful.button({}, 1, function(tag)\n\t\ttag:view_only()\n\tend),\n\n\tawful.button({ modkey }, 1, function(tag)\n\t\tif client.focus then\n\t\t\tclient.focus:move_to_tag(tag)\n\t\tend\n\tend),\n\n\tawful.button({}, 3, awful.tag.viewtoggle),\n\n\tawful.button({ modkey }, 3, function(tag)\n\t\tif client.focus then\n\t\t\tclient.focus:toggle_tag(tag)\n\t\tend\n\tend),\n\n\tawful.button({}, 4, function(t)\n\t\tawful.tag.viewnext(t.screen)\n\tend),\n\tawful.button({}, 5, function(t)\n\t\tawful.tag.viewprev(t.screen)\n\tend)\n)\n\nfunction M.widget(s)\n\treturn awful.widget.taglist({\n\t\tscreen = s,\n\t\tfilter = awful.widget.taglist.filter.all,\n\t\tbuttons = taglist_buttons,\n\t})\nend\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/temp/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal icon = os.getenv(\"HOME\") .. \"/.config/awesome/statusbar/modules/temp/icon.svg\"\nlocal colorize = require(\"main.helpers\").colorize\nlocal markup = require(\"main.helpers\").markup\n\nlocal M = {}\n\n-- Temperature\nM.icon = wibox.widget.imagebox(colorize(icon, theme.widget_main_color))\n\nlocal get_temp_status = [[\n  sh -c \"\n    sensors | awk '/^Core 0:/{gsub(/[^0-9]/, \\\" \\\"); print $2\\\"°C\\\"}'\n  \"\n]]\n\nM.widget = awful.widget.watch(get_temp_status, 5, function(widget, stdout)\n\twidget:set_markup(markup(stdout, { fg = theme.foreground }))\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/todo/init.lua",
    "content": "-------------------------------------------------\n-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/todo-widget\n-- with some modifications\n-------------------------------------------------\nlocal json = require(\"main.json\")\nlocal awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal spawn = require(\"awful.spawn\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\"beautiful\")\nlocal gfs = require(\"gears.filesystem\")\nlocal dpi = beautiful.xresources.apply_dpi\nlocal colorize = require(\"main.helpers\").colorize\n\nlocal HOME_DIR = os.getenv(\"HOME\")\nlocal WIDGET_DIR = HOME_DIR .. \"/.config/awesome/statusbar/modules/todo\"\nlocal STORAGE = HOME_DIR .. \"/notes/todos.json\"\n\nlocal GET_TODO_ITEMS = 'bash -c \"cat ' .. STORAGE .. '\"'\n\nlocal rows = {\n\tlayout = wibox.layout.fixed.vertical,\n}\nlocal todo_widget = {}\n\ntodo_widget.widget = wibox.widget({\n\t{\n\t\t{\n\t\t\tid = \"icon\",\n\t\t\twidget = wibox.widget.imagebox,\n\t\t},\n\t\tid = \"margin\",\n\t\tright = dpi(4),\n\t\tlayout = wibox.container.margin,\n\t},\n\t{\n\t\tid = \"txt\",\n\t\twidget = wibox.widget.textbox,\n\t},\n\tlayout = wibox.layout.fixed.horizontal,\n\tset_text = function(self, new_value)\n\t\tself.txt.text = new_value\n\tend,\n\tset_icon = function(self, new_value)\n\t\tself.margin.icon.image = new_value\n\tend,\n})\n\nfunction todo_widget:update_counter(todos)\n\tlocal todo_count = 0\n\tfor _, p in ipairs(todos) do\n\t\tif not p.status then\n\t\t\ttodo_count = todo_count + 1\n\t\tend\n\tend\n\n\ttodo_widget.widget:set_text(todo_count)\nend\n\nlocal popup = awful.popup({\n\tbg = beautiful.bg_normal,\n\tontop = true,\n\tvisible = false,\n\tshape = gears.shape.rounded_rect,\n\tborder_width = 0,\n\tborder_color = beautiful.bg_focus,\n\tmaximum_width = 400,\n\toffset = { y = 5 },\n\twidget = {},\n})\n\nlocal add_button = wibox.widget({\n\t{\n\t\t{\n\t\t\timage = colorize(WIDGET_DIR .. \"/list-add-symbolic.svg\", theme.white),\n\t\t\tresize = false,\n\t\t\twidget = wibox.widget.imagebox,\n\t\t},\n\t\ttop = 11,\n\t\tleft = 8,\n\t\tright = 8,\n\t\tlayout = wibox.container.margin,\n\t},\n\tshape = function(cr, width, height)\n\t\tgears.shape.circle(cr, width, height, 12)\n\tend,\n\twidget = wibox.container.background,\n})\n\nadd_button:connect_signal(\"button::press\", function()\n\tlocal prompt = awful.widget.prompt()\n\n\ttable.insert(\n\t\trows,\n\t\twibox.widget({\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\tprompt.widget,\n\t\t\t\t\tspacing = 8,\n\t\t\t\t\tlayout = wibox.layout.align.horizontal,\n\t\t\t\t},\n\t\t\t\tmargins = 8,\n\t\t\t\tlayout = wibox.container.margin,\n\t\t\t},\n\t\t\tbg = beautiful.bg_normal,\n\t\t\twidget = wibox.container.background,\n\t\t})\n\t)\n\tawful.prompt.run({\n\t\tprompt = \"<b>New item</b>: \",\n\t\tbg = beautiful.bg_normal,\n\t\tbg_cursor = theme.white,\n\t\ttextbox = prompt.widget,\n\t\texe_callback = function(input_text)\n\t\t\tif not input_text or #input_text == 0 then\n\t\t\t\treturn\n\t\t\tend\n\t\t\tspawn.easy_async(GET_TODO_ITEMS, function(stdout)\n\t\t\t\tlocal res = json.decode(stdout)\n\t\t\t\ttable.insert(res.todo_items, { todo_item = input_text, status = false })\n\t\t\t\tspawn.easy_async_with_shell(\"echo '\" .. json.encode(res) .. \"' > \" .. STORAGE, function()\n\t\t\t\t\tspawn.easy_async(GET_TODO_ITEMS, function(stdout)\n\t\t\t\t\t\tupdate_widget(stdout)\n\t\t\t\t\tend)\n\t\t\t\tend)\n\t\t\tend)\n\t\tend,\n\t})\n\tpopup:setup(rows)\nend)\nadd_button:connect_signal(\"mouse::enter\", function(c)\n\tc:set_bg(theme.bg_focus)\nend)\nadd_button:connect_signal(\"mouse::leave\", function(c)\n\tc:set_bg(theme.bg_normal)\nend)\n\nlocal worker = function()\n\tlocal icon = colorize(WIDGET_DIR .. \"/checkbox.svg\", theme.widget_main_color)\n\n\ttodo_widget.widget:set_icon(icon)\n\n\tfunction update_widget(stdout)\n\t\tlocal result = json.decode(stdout)\n\t\tif result == nil or result == \"\" then\n\t\t\tresult = {}\n\t\tend\n\t\ttodo_widget:update_counter(result.todo_items)\n\n\t\tfor i = 0, #rows do\n\t\t\trows[i] = nil\n\t\tend\n\n\t\tlocal first_row = wibox.widget({\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\twidget = wibox.widget.textbox,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tmarkup = '<span size=\"large\" font_weight=\"bold\" color=\"#ebdbb2\">Todo</span>',\n\t\t\t\t\talign = \"center\",\n\t\t\t\t\tforced_width = 350, -- for horizontal alignment\n\t\t\t\t\tforced_height = 40,\n\t\t\t\t\twidget = wibox.widget.textbox,\n\t\t\t\t},\n\t\t\t\tadd_button,\n\t\t\t\tspacing = 8,\n\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t},\n\t\t\tbg = beautiful.bg_normal,\n\t\t\twidget = wibox.container.background,\n\t\t})\n\n\t\ttable.insert(rows, first_row)\n\n\t\tfor i, todo_item in ipairs(result.todo_items) do\n\t\t\tlocal checkbox = wibox.widget({\n\t\t\t\tchecked = todo_item.status,\n\t\t\t\tcolor = beautiful.white,\n\t\t\t\tpaddings = 2,\n\t\t\t\tshape = gears.shape.circle,\n\t\t\t\tforced_width = 14,\n\t\t\t\tforced_height = 14,\n\t\t\t\tcheck_color = beautiful.white,\n\t\t\t\twidget = wibox.widget.checkbox,\n\t\t\t})\n\n\t\t\tcheckbox:connect_signal(\"button::press\", function(c)\n\t\t\t\tc:set_checked(not c.checked)\n\t\t\t\ttodo_item.status = not todo_item.status\n\t\t\t\tresult.todo_items[i] = todo_item\n\t\t\t\tspawn.easy_async_with_shell(\"echo '\" .. json.encode(result) .. \"' > \" .. STORAGE, function()\n\t\t\t\t\ttodo_widget:update_counter(result.todo_items)\n\t\t\t\tend)\n\t\t\tend)\n\n\t\t\tlocal trash_button = wibox.widget({\n\t\t\t\t{\n\t\t\t\t\t{\n\t\t\t\t\t\timage = colorize(WIDGET_DIR .. \"/window-close-symbolic.svg\", theme.white),\n\t\t\t\t\t\tresize = false,\n\t\t\t\t\t\twidget = wibox.widget.imagebox,\n\t\t\t\t\t},\n\t\t\t\t\tmargins = 5,\n\t\t\t\t\tlayout = wibox.container.margin,\n\t\t\t\t},\n\t\t\t\tborder_width = 1,\n\t\t\t\tshape = function(cr, width, height)\n\t\t\t\t\tgears.shape.circle(cr, width, height, 10)\n\t\t\t\tend,\n\t\t\t\twidget = wibox.container.background,\n\t\t\t})\n\n\t\t\ttrash_button:connect_signal(\"button::press\", function()\n\t\t\t\ttable.remove(result.todo_items, i)\n\t\t\t\tspawn.easy_async_with_shell(\"printf '\" .. json.encode(result) .. \"' > \" .. STORAGE, function()\n\t\t\t\t\tspawn.easy_async(GET_TODO_ITEMS, function(stdout)\n\t\t\t\t\t\tupdate_widget(stdout)\n\t\t\t\t\tend)\n\t\t\t\tend)\n\t\t\tend)\n\n\t\t\tlocal row = wibox.widget({\n\t\t\t\t{\n\t\t\t\t\t{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcheckbox,\n\t\t\t\t\t\t\tvalign = \"center\",\n\t\t\t\t\t\t\tlayout = wibox.container.place,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttext = todo_item.todo_item,\n\t\t\t\t\t\t\t\talign = \"left\",\n\t\t\t\t\t\t\t\twidget = wibox.widget.textbox,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tleft = 10,\n\t\t\t\t\t\t\tlayout = wibox.container.margin,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t-- move_buttons,\n\t\t\t\t\t\t\t\tvalign = \"center\",\n\t\t\t\t\t\t\t\tlayout = wibox.container.place,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttrash_button,\n\t\t\t\t\t\t\t\tvalign = \"center\",\n\t\t\t\t\t\t\t\tlayout = wibox.container.place,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tspacing = 8,\n\t\t\t\t\t\t\tlayout = wibox.layout.align.horizontal,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tspacing = 8,\n\t\t\t\t\t\tlayout = wibox.layout.align.horizontal,\n\t\t\t\t\t},\n\t\t\t\t\tmargins = 8,\n\t\t\t\t\tlayout = wibox.container.margin,\n\t\t\t\t},\n\t\t\t\tbg = beautiful.bg_normal,\n\t\t\t\twidget = wibox.container.background,\n\t\t\t})\n\n\t\t\trow:connect_signal(\"mouse::enter\", function(c)\n\t\t\t\tc:set_bg(theme.grey)\n\t\t\tend)\n\t\t\trow:connect_signal(\"mouse::leave\", function(c)\n\t\t\t\tc:set_bg(theme.bg_normal)\n\t\t\tend)\n\n\t\t\ttable.insert(rows, row)\n\t\tend\n\n\t\tpopup:setup(rows)\n\tend\n\n\ttodo_widget.widget:buttons(awful.util.table.join(awful.button({}, 1, function()\n\t\tif popup.visible then\n\t\t\tpopup.visible = not popup.visible\n\t\telse\n\t\t\tpopup:move_next_to(mouse.current_widget_geometry)\n\t\tend\n\tend)))\n\n\tspawn.easy_async(GET_TODO_ITEMS, function(stdout)\n\t\tupdate_widget(stdout)\n\tend)\n\n\treturn todo_widget.widget\nend\n\nif not gfs.file_readable(STORAGE) then\n\tspawn.easy_async(\n\t\tstring.format([[bash -c \"dirname %s | xargs mkdir -p && echo '{\\\"todo_items\\\":{}}' > %s\"]], STORAGE, STORAGE)\n\t)\nend\n\nreturn setmetatable(todo_widget, {\n\t__call = function()\n\t\treturn worker()\n\tend,\n})\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/statusbar/modules/volume/init.lua",
    "content": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal colorize = require(\"main.helpers\").colorize\nlocal markup = require(\"main.helpers\").markup\nlocal gears = require(\"gears\")\n\nlocal HOME = os.getenv(\"HOME\")\n\nlocal volume_off = HOME .. \"/.config/awesome/statusbar/modules/volume/volume_off.svg\"\nlocal volume_mute = HOME .. \"/.config/awesome/statusbar/modules/volume/volume_mute.svg\"\nlocal volume_low = HOME .. \"/.config/awesome/statusbar/modules/volume/volume_down.svg\"\nlocal volume_high = HOME .. \"/.config/awesome/statusbar/modules/volume/volume_up.svg\"\n\nlocal M = {}\n\n-- Volume\nM.icon = wibox.widget.imagebox(colorize(volume_low, theme.widget_main_color))\n\nlocal get_volume = \"pulsemixer --get-volume\"\n\nlocal volume = 0\nlocal is_muted = false\n\nlocal set_volume = function(widget, vol, muted)\n\tvolume = tonumber(vol) or 0\n\tis_muted = tonumber(muted) == 1\n\n\tif is_muted then\n\t\tM.icon:set_image(colorize(volume_off, theme.foreground))\n\t\twidget:set_markup(markup(\"muted\", { fg = theme.foreground }))\n\telse\n\t\tif volume <= 20 then\n\t\t\tM.icon:set_image(colorize(volume_mute, theme.widget_main_color))\n\t\telseif volume <= 60 then\n\t\t\tM.icon:set_image(colorize(volume_low, theme.widget_main_color))\n\t\telse\n\t\t\tM.icon:set_image(colorize(volume_high, theme.widget_main_color))\n\t\tend\n\n\t\twidget:set_markup(markup(tostring(volume) .. \"%\", { fg = theme.foreground }))\n\tend\nend\n\nM.widget = awful.widget.watch(get_volume, 120, function(widget, vol)\n\tawful.spawn.easy_async(\"pulsemixer --get-mute\", function(muted)\n\t\tset_volume(widget, vol:gmatch(\"%d%d\")(), muted)\n\tend)\nend)\n\nM.widget:buttons(gears.table.join(\n\tawful.button({}, 4, function()\n\t\tawful.spawn.easy_async(\"pulsemixer --change-volume +2\", function()\n\t\t\t-- send signal AFTER the volume has changed\n\t\t\tawesome.emit_signal(\"volume_change\")\n\t\tend)\n\tend),\n\tawful.button({}, 5, function()\n\t\tawful.spawn.easy_async(\"pulsemixer --change-volume -2\", function()\n\t\t\t-- send signal AFTER the volume has changed\n\t\t\tawesome.emit_signal(\"volume_change\")\n\t\tend)\n\tend)\n))\n\nawesome.connect_signal(\"volume_change\", function()\n\tawful.spawn.easy_async(get_volume, function(vol)\n\t\tawful.spawn.easy_async(\"pulsemixer --get-mute\", function(muted)\n\t\t\tset_volume(M.widget, vol:gmatch(\"%d%d\")(), muted)\n\t\tend)\n\tend)\nend)\n\nreturn M\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/themes/main/colours.lua",
    "content": "local xrdb = require(\"beautiful.xresources\").get_current_theme()\n\ntheme.background = xrdb.background\ntheme.foreground = xrdb.foreground\n\ntheme.black = xrdb.color0\ntheme.red = xrdb.color1\ntheme.green = xrdb.color2\ntheme.yellow = xrdb.color3\ntheme.blue = xrdb.color4\ntheme.magenta = xrdb.color5\ntheme.cyan = xrdb.color6\ntheme.white = xrdb.color15\ntheme.white_alt = xrdb.color7\n\ntheme.grey = xrdb.color8\ntheme.grey_alt = \"#22242F\"\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/themes/main/elements.lua",
    "content": "local theme_assets = require(\"beautiful.theme_assets\")\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\nlocal theme_path = os.getenv(\"HOME\") .. \"/.config/awesome/themes/main/\"\nlocal rrect = require(\"main.helpers\").rrect\n\n-- wallpaper and icon\ntheme.icon_theme = \"Numix\"\n-- theme.wallpaper = theme_path .. \"img/ryuko-spring.png\"\n-- theme.wallpaper = theme_path .. \"img/autumn_gurl.png\"\n-- theme.wallpaper = theme_path .. \"img/knowname1.png\"\ntheme.wallpaper = os.getenv(\"HOME\") .. \"/Pictures/walls/anime/madoka.png\"\n-- theme.wallpaper = os.getenv(\"HOME\") .. \"/pix/gurl.jpg\"\ntheme.menu_icon = theme_path .. \"icons/killlakill.png\"\n-- theme.wallpaper     = theme_path .. \"img/babymetal.jpg\"\n-- theme.menu_icon  = theme_path .. \"icons/babymetal.png\"\n\n-- theme.font = \"JetBrainsMono Nerd Font 10\"\ntheme.font = \"Inter 10\"\ntheme.nerd_font = \"JetBrainsMono Nerd Font 10\"\ntheme.titlebar_font = \"Inter 11\"\ntheme.taglist_font = \"M+ 2p Medium 11\"\ntheme.color_name = \"gitgud\"\n\n-- background stuff\ntheme.bg_normal = theme.black\ntheme.bg_focus = theme.yellow\ntheme.bg_urgent = theme.red\ntheme.bg_minimize = theme.grey\n\n-- foreground stuff\ntheme.fg_normal = theme.white\ntheme.fg_focus = theme.black\ntheme.fg_urgent = theme.black\ntheme.fg_minimize = theme.white\n\n-- gaps\ntheme.useless_gap = dpi(4)\n\n-- systray\ntheme.systray_icon_spacing = dpi(6)\ntheme.bg_systray = theme.black\ntheme.systray_icon_size = dpi(10)\n\n-- border stuff\ntheme.border_width = dpi(1)\ntheme.border_normal = theme.black\ntheme.border_focus = theme.black\ntheme.border_marked = theme.red\n\n-- tasklist\ntheme.tasklist_bg_focus = theme.black\ntheme.tasklist_fg_focus = theme.yellow\n\n-- widget\ntheme.widget_main_color = theme.blue\ntheme.widget_red = theme.red\ntheme.widget_yelow = theme.yellow\ntheme.widget_green = theme.green\ntheme.widget_black = theme.black\ntheme.widget_transparent = \"#00000000\"\n\n-- titlebar\ntheme.titlebar_size = dpi(22)\ntheme.titlebar_bg_focus = theme.black\ntheme.titlebar_bg_normal = theme.black\ntheme.titlebar_fg_focus = theme.white\ntheme.titlebar_fg_normal = theme.white\n\n-- notification\ntheme.notification_font = theme.font\ntheme.notification_bg = theme.black\ntheme.notification_fg = theme.white\ntheme.notification_border_color = theme.grey_alt\ntheme.notification_border_width = dpi(1)\ntheme.notification_max_width = dpi(600)\ntheme.notification_icon_size = dpi(100)\ntheme.notification_margin = dpi(10)\n\n-- edge snap\ntheme.snap_bg = theme.white\ntheme.snap_shape = rrect(0)\n\n-- hotkey font\ntheme.hotkeys_font = theme.font\ntheme.hotkeys_description_font = theme.font\ntheme.hotkeys_modifiers_fg = theme.yellow\ntheme.hotkeys_border_width = dpi(4)\ntheme.hotkeys_border_color = theme.black\ntheme.hotkeys_group_margin = dpi(10)\n\n-- submenu\ntheme.menu_submenu_icon = theme_path .. \"icons/submenu.svg\"\ntheme.menu_height = dpi(24)\ntheme.menu_width = dpi(180)\n\n-- window snap hint\ntheme.tooltip_bg = \"#282828\"\ntheme.tooltip_fg = \"#ebdbb2\"\ntheme.tooltip_border_color = \"#ebdbb2\"\n\n-- statusbar\ntheme.statusbar_height = dpi(30)\ntheme.statusbar_visible = true\ntheme.statusbar_bg = theme.background\n\n-- taglist\ntheme.taglist_bg_focus = theme.black\ntheme.taglist_fg_focus = theme.widget_main_color\ntheme.taglist_fg_empty = theme.grey\ntheme.taglist_fg_urgent = theme.black\ntheme.taglist_bg_urgent = theme.red\ntheme.taglist_fg_occupied = theme.white\ntheme.taglist_spacing = dpi(2)\n\nlocal taglist_square_size = dpi(5)\ntheme.taglist_squares_sel = theme_assets.taglist_squares_sel(taglist_square_size, theme.fg_normal)\ntheme.taglist_squares_unsel = theme_assets.taglist_squares_unsel(taglist_square_size, theme.fg_normal)\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/themes/main/naughty.lua",
    "content": "local naughty = require(\"naughty\")\nlocal wibox = require(\"wibox\")\nlocal dpi = require(\"beautiful.xresources\").apply_dpi\n\nnaughty.config.padding = dpi(10)\nnaughty.config.icon_dirs = {\n\t\"/usr/share/icons/Numix/32/apps/\",\n\t\"/usr/share/pixmaps/\",\n}\nnaughty.config.icon_formats = { \"png\", \"svg\" }\nnaughty.config.defaults.icon_size = theme.notification_icon_size\nnaughty.config.defaults.margin = theme.notification_margin\nnaughty.config.defaults.border_width = theme.notification_border_width\nnaughty.config.defaults.title = \"System Notification\"\n\nnaughty.connect_signal(\"request::display\", function(notif)\n\tlocal action_widget = {\n\t\tbase_layout = wibox.widget({\n\t\t\tspacing = dpi(4),\n\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t}),\n\n\t\twidget_template = {\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\t{\n\t\t\t\t\t\t{ id = \"text_role\", widget = wibox.widget.textbox },\n\t\t\t\t\t\tvalign = \"center\",\n\t\t\t\t\t\thalign = \"center\",\n\t\t\t\t\t\twidget = wibox.container.place,\n\t\t\t\t\t},\n\t\t\t\t\tleft = dpi(8),\n\t\t\t\t\tright = dpi(8),\n\t\t\t\t\twidget = wibox.container.margin,\n\t\t\t\t},\n\t\t\t\tshape_border_width = dpi(0),\n\t\t\t\tforced_height = dpi(28),\n\t\t\t\tbg = theme.grey_alt,\n\t\t\t\twidget = wibox.container.background,\n\t\t\t},\n\t\t\tright = dpi(4),\n\t\t\ttop = dpi(4),\n\t\t\twidget = wibox.container.margin,\n\t\t},\n\t\tstyle = { underline_normal = false, underline_selected = false },\n\t\twidget = naughty.list.actions,\n\t}\n\n\tnaughty.layout.box({\n\t\tnotification = notif,\n\t\ttype = \"notification\",\n\t\twidget_template = {\n\t\t\t{\n\t\t\t\t{\n\t\t\t\t\t{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnaughty.widget.icon,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfont = theme.notification_font,\n\t\t\t\t\t\t\t\t\tmarkup = \"<b>\" .. notif.title .. \"</b>\",\n\t\t\t\t\t\t\t\t\twidget = wibox.widget.textbox,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tnaughty.widget.message,\n\t\t\t\t\t\t\t\taction_widget,\n\t\t\t\t\t\t\t\tspacing = dpi(4),\n\t\t\t\t\t\t\t\tlayout = wibox.layout.fixed.vertical,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tfill_space = true,\n\t\t\t\t\t\t\tspacing = dpi(8),\n\t\t\t\t\t\t\tlayout = wibox.layout.fixed.horizontal,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tspacing = dpi(10),\n\t\t\t\t\t\tlayout = wibox.layout.fixed.vertical,\n\t\t\t\t\t},\n\t\t\t\t\tmargins = dpi(8),\n\t\t\t\t\twidget = wibox.container.margin,\n\t\t\t\t},\n\t\t\t\tid = \"background_role\",\n\t\t\t\twidget = naughty.container.background,\n\t\t\t},\n\t\t\tstrategy = \"max\",\n\t\t\twidth = theme.notification_max_width or dpi(500),\n\t\t\twidget = wibox.container.constraint,\n\t\t},\n\t\tbg = theme.black,\n\t\tborder_color = theme.notification_border_color,\n\t\tborder_width = theme.border_width,\n\t\twidget = wibox.container.background,\n\t})\nend)\n"
  },
  {
    "path": "legacy/awesome/.config/awesome/themes/main/theme.lua",
    "content": "local theme_path = os.getenv(\"HOME\") .. \"/.config/awesome/themes/main/\"\n\ntheme = {} -- global namespace for theme\n\ndofile(theme_path .. \"colours.lua\")\ndofile(theme_path .. \"elements.lua\")\ndofile(theme_path .. \"naughty.lua\")\n\nreturn theme\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/cached_layouts",
    "content": "[keyboard-apl]\nDescription=\"Keyboard - APL\"\nLanguage=en\nLabel=apl\n\n[keyboard-apl-dyalog]\nDescription=\"Keyboard - APL - APL symbols (Dyalog APL)\"\nLanguage=en\nLabel=dlg\n\n[keyboard-apl-sax]\nDescription=\"Keyboard - APL - APL symbols (SAX, Sharp APL for Unix)\"\nLanguage=en\nLabel=sax\n\n[keyboard-apl-unified]\nDescription=\"Keyboard - APL - APL symbols (unified)\"\nLanguage=en\nLabel=ufd\n\n[keyboard-apl-apl2]\nDescription=\"Keyboard - APL - APL symbols (IBM APL2)\"\nLanguage=en\nLabel=apl2\n\n[keyboard-apl-aplplusII]\nDescription=\"Keyboard - APL - APL symbols (Manugistics APL*PLUS II)\"\nLanguage=en\nLabel=aplII\n\n[keyboard-apl-aplx]\nDescription=\"Keyboard - APL - APL symbols (APLX unified)\"\nLanguage=en\nLabel=aplx\n\n[keyboard-jv]\nDescription=\"Keyboard - Indonesian (Javanese)\"\nLanguage=jv\nLabel=jv\n\n[keyboard-tz]\nDescription=\"Keyboard - Swahili (Tanzania)\"\nLanguage=sw\nLabel=sw\n\n[keyboard-brai]\nDescription=\"Keyboard - Braille\"\nLanguage=\nLabel=brl\n\n[keyboard-brai-left_hand]\nDescription=\"Keyboard - Braille - Braille (left-handed)\"\nLanguage=\nLabel=brai\n\n[keyboard-brai-left_hand_invert]\nDescription=\"Keyboard - Braille - Braille (left-handed inverted thumb)\"\nLanguage=\nLabel=brai\n\n[keyboard-brai-right_hand]\nDescription=\"Keyboard - Braille - Braille (right-handed)\"\nLanguage=\nLabel=brai\n\n[keyboard-brai-right_hand_invert]\nDescription=\"Keyboard - Braille - Braille (right-handed inverted thumb)\"\nLanguage=\nLabel=brai\n\n[keyboard-et]\nDescription=\"Keyboard - Amharic\"\nLanguage=am\nLabel=am\n\n[keyboard-tm]\nDescription=\"Keyboard - Turkmen\"\nLanguage=tk\nLabel=tk\n\n[keyboard-tm-alt]\nDescription=\"Keyboard - Turkmen - Turkmen (Alt-Q)\"\nLanguage=tk\nLabel=tm\n\n[keyboard-np]\nDescription=\"Keyboard - Nepali\"\nLanguage=ne\nLabel=ne\n\n[keyboard-epo]\nDescription=\"Keyboard - Esperanto\"\nLanguage=eo\nLabel=eo\n\n[keyboard-epo-legacy]\nDescription=\"Keyboard - Esperanto - Esperanto (legacy)\"\nLanguage=eo\nLabel=epo\n\n[keyboard-eu]\nDescription=\"Keyboard - EurKEY (US)\"\nLanguage=ca\nLabel=eu\n\n[keyboard-za]\nDescription=\"Keyboard - English (South Africa)\"\nLanguage=en\nLabel=en\n\n[keyboard-bw]\nDescription=\"Keyboard - Tswana\"\nLanguage=tn\nLabel=tn\n\n[keyboard-kr]\nDescription=\"Keyboard - Korean\"\nLanguage=ko\nLabel=ko\n\n[keyboard-kr-kr104]\nDescription=\"Keyboard - Korean - Korean (101/104-key compatible)\"\nLanguage=ko\nLabel=kr\n\n[keyboard-kr-sun_type6]\nDescription=\"Keyboard - Korean - Korean (Sun Type 6/7)\"\nLanguage=ko\nLabel=kr\n\n[keyboard-gb]\nDescription=\"Keyboard - English (UK)\"\nLanguage=en\nLabel=en\n\n[keyboard-gb-extd]\nDescription=\"Keyboard - English (UK) - English (UK, extended, Windows)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-intl]\nDescription=\"Keyboard - English (UK) - English (UK, intl., with dead keys)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-dvorak]\nDescription=\"Keyboard - English (UK) - English (UK, Dvorak)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-dvorakukp]\nDescription=\"Keyboard - English (UK) - English (UK, Dvorak, with UK punctuation)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-mac]\nDescription=\"Keyboard - English (UK) - English (UK, Macintosh)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-mac_intl]\nDescription=\"Keyboard - English (UK) - English (UK, Macintosh, intl.)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-colemak]\nDescription=\"Keyboard - English (UK) - English (UK, Colemak)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-colemak_dh]\nDescription=\"Keyboard - English (UK) - English (UK, Colemak-DH)\"\nLanguage=en\nLabel=gb\n\n[keyboard-gb-pl]\nDescription=\"Keyboard - English (UK) - Polish (British keyboard)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-gb-sun_type6]\nDescription=\"Keyboard - English (UK) - English (UK, Sun Type 6/7)\"\nLanguage=en\nLabel=gb\n\n[keyboard-ua]\nDescription=\"Keyboard - Ukrainian\"\nLanguage=uk\nLabel=k\n\n[keyboard-ua-phonetic]\nDescription=\"Keyboard - Ukrainian - Ukrainian (phonetic)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-typewriter]\nDescription=\"Keyboard - Ukrainian - Ukrainian (typewriter)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-winkeys]\nDescription=\"Keyboard - Ukrainian - Ukrainian (Windows)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-legacy]\nDescription=\"Keyboard - Ukrainian - Ukrainian (legacy)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-rstu]\nDescription=\"Keyboard - Ukrainian - Ukrainian (standard RSTU)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-rstu_ru]\nDescription=\"Keyboard - Ukrainian - Russian (Ukraine, standard RSTU)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-homophonic]\nDescription=\"Keyboard - Ukrainian - Ukrainian (homophonic)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-ua-sun_type6]\nDescription=\"Keyboard - Ukrainian - Ukrainian (Sun Type 6/7)\"\nLanguage=uk\nLabel=ua\n\n[keyboard-th]\nDescription=\"Keyboard - Thai\"\nLanguage=th\nLabel=th\n\n[keyboard-th-tis]\nDescription=\"Keyboard - Thai - Thai (TIS-820.2538)\"\nLanguage=th\nLabel=th\n\n[keyboard-th-pat]\nDescription=\"Keyboard - Thai - Thai (Pattachote)\"\nLanguage=th\nLabel=th\n\n[keyboard-mv]\nDescription=\"Keyboard - Dhivehi\"\nLanguage=dv\nLabel=dv\n\n[keyboard-nec_vndr/jp]\nDescription=\"Keyboard - Japanese (PC-98)\"\nLanguage=ja\nLabel=ja\n\n[keyboard-tj]\nDescription=\"Keyboard - Tajik\"\nLanguage=tg\nLabel=tg\n\n[keyboard-tj-legacy]\nDescription=\"Keyboard - Tajik - Tajik (legacy)\"\nLanguage=tg\nLabel=tj\n\n[keyboard-sk]\nDescription=\"Keyboard - Slovak\"\nLanguage=sk\nLabel=sk\n\n[keyboard-sk-bksl]\nDescription=\"Keyboard - Slovak - Slovak (extended backslash)\"\nLanguage=sk\nLabel=sk\n\n[keyboard-sk-qwerty]\nDescription=\"Keyboard - Slovak - Slovak (QWERTY)\"\nLanguage=sk\nLabel=sk\n\n[keyboard-sk-qwerty_bksl]\nDescription=\"Keyboard - Slovak - Slovak (QWERTY, extended backslash)\"\nLanguage=sk\nLabel=sk\n\n[keyboard-sk-acc]\nDescription=\"Keyboard - Slovak - Slovak (ACC layout, only accented letters)\"\nLanguage=sk\nLabel=sk\n\n[keyboard-sk-sun_type6]\nDescription=\"Keyboard - Slovak - Slovak (Sun Type 6/7)\"\nLanguage=sk\nLabel=sk\n\n[keyboard-ru]\nDescription=\"Keyboard - Russian\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic]\nDescription=\"Keyboard - Russian - Russian (phonetic)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic_winkeys]\nDescription=\"Keyboard - Russian - Russian (phonetic, Windows)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic_YAZHERTY]\nDescription=\"Keyboard - Russian - Russian (phonetic, YAZHERTY)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-typewriter]\nDescription=\"Keyboard - Russian - Russian (typewriter)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-legacy]\nDescription=\"Keyboard - Russian - Russian (legacy)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-typewriter-legacy]\nDescription=\"Keyboard - Russian - Russian (typewriter, legacy)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-tt]\nDescription=\"Keyboard - Russian - Tatar\"\nLanguage=tt\nLabel=ru\n\n[keyboard-ru-os_legacy]\nDescription=\"Keyboard - Russian - Ossetian (legacy)\"\nLanguage=os\nLabel=ru\n\n[keyboard-ru-os_winkeys]\nDescription=\"Keyboard - Russian - Ossetian (Windows)\"\nLanguage=os\nLabel=ru\n\n[keyboard-ru-cv]\nDescription=\"Keyboard - Russian - Chuvash\"\nLanguage=cv\nLabel=ru\n\n[keyboard-ru-cv_latin]\nDescription=\"Keyboard - Russian - Chuvash (Latin)\"\nLanguage=cv\nLabel=ru\n\n[keyboard-ru-udm]\nDescription=\"Keyboard - Russian - Udmurt\"\nLanguage=udm\nLabel=ru\n\n[keyboard-ru-kom]\nDescription=\"Keyboard - Russian - Komi\"\nLanguage=kv\nLabel=ru\n\n[keyboard-ru-sah]\nDescription=\"Keyboard - Russian - Yakut\"\nLanguage=sah\nLabel=ru\n\n[keyboard-ru-xal]\nDescription=\"Keyboard - Russian - Kalmyk\"\nLanguage=xal\nLabel=ru\n\n[keyboard-ru-dos]\nDescription=\"Keyboard - Russian - Russian (DOS)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-mac]\nDescription=\"Keyboard - Russian - Russian (Macintosh)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-srp]\nDescription=\"Keyboard - Russian - Serbian (Russia)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-bak]\nDescription=\"Keyboard - Russian - Bashkirian\"\nLanguage=ba\nLabel=ru\n\n[keyboard-ru-chm]\nDescription=\"Keyboard - Russian - Mari\"\nLanguage=chm\nLabel=ru\n\n[keyboard-ru-phonetic_azerty]\nDescription=\"Keyboard - Russian - Russian (phonetic, AZERTY)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic_dvorak]\nDescription=\"Keyboard - Russian - Russian (phonetic, Dvorak)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic_fr]\nDescription=\"Keyboard - Russian - Russian (phonetic, French)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-chu]\nDescription=\"Keyboard - Russian - Church Slavonic\"\nLanguage=cu\nLabel=ru\n\n[keyboard-ru-ruu]\nDescription=\"Keyboard - Russian - Russian (with Ukrainian-Belorussian layout)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-rulemak]\nDescription=\"Keyboard - Russian - Russian (Rulemak, phonetic Colemak)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-phonetic_mac]\nDescription=\"Keyboard - Russian - Russian (phonetic Macintosh)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-sun_type6]\nDescription=\"Keyboard - Russian - Russian (Sun Type 6/7)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-unipunct]\nDescription=\"Keyboard - Russian - Russian (with US punctuation)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-gost-6431-75-48]\nDescription=\"Keyboard - Russian - Russian (GOST 6431-75, 48-key)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-gost-14289-88]\nDescription=\"Keyboard - Russian - Russian (GOST 14289-88)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ru-prxn]\nDescription=\"Keyboard - Russian - Russian (Polyglot and Reactionary)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-trans]\nDescription=\"Keyboard - International Phonetic Alphabet\"\nLanguage=\nLabel=trans\n\n[keyboard-trans-qwerty]\nDescription=\"Keyboard - International Phonetic Alphabet - International Phonetic Alphabet (QWERTY)\"\nLanguage=\nLabel=trans\n\n[keyboard-ir]\nDescription=\"Keyboard - Persian\"\nLanguage=fa\nLabel=fa\n\n[keyboard-ir-pes_keypad]\nDescription=\"Keyboard - Persian - Persian (with Persian keypad)\"\nLanguage=fa\nLabel=ir\n\n[keyboard-ir-ku]\nDescription=\"Keyboard - Persian - Kurdish (Iran, Latin Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-ir-ku_f]\nDescription=\"Keyboard - Persian - Kurdish (Iran, F)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-ir-ku_alt]\nDescription=\"Keyboard - Persian - Kurdish (Iran, Latin Alt-Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-ir-ku_ara]\nDescription=\"Keyboard - Persian - Kurdish (Iran, Arabic-Latin)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-ir-ave]\nDescription=\"Keyboard - Persian - Avestan\"\nLanguage=ae\nLabel=ir\n\n[keyboard-ng]\nDescription=\"Keyboard - English (Nigeria)\"\nLanguage=en\nLabel=en\n\n[keyboard-ng-igbo]\nDescription=\"Keyboard - English (Nigeria) - Igbo\"\nLanguage=ig\nLabel=ig\n\n[keyboard-ng-yoruba]\nDescription=\"Keyboard - English (Nigeria) - Yoruba\"\nLanguage=yo\nLabel=yo\n\n[keyboard-ng-hausa]\nDescription=\"Keyboard - English (Nigeria) - Hausa (Nigeria)\"\nLanguage=ha\nLabel=ha\n\n[keyboard-is]\nDescription=\"Keyboard - Icelandic\"\nLanguage=is\nLabel=is\n\n[keyboard-is-mac_legacy]\nDescription=\"Keyboard - Icelandic - Icelandic (Macintosh, legacy)\"\nLanguage=is\nLabel=is\n\n[keyboard-is-mac]\nDescription=\"Keyboard - Icelandic - Icelandic (Macintosh)\"\nLanguage=is\nLabel=is\n\n[keyboard-is-dvorak]\nDescription=\"Keyboard - Icelandic - Icelandic (Dvorak)\"\nLanguage=is\nLabel=is\n\n[keyboard-es]\nDescription=\"Keyboard - Spanish\"\nLanguage=es\nLabel=es\n\n[keyboard-es-nodeadkeys]\nDescription=\"Keyboard - Spanish - Spanish (no dead keys)\"\nLanguage=es\nLabel=es\n\n[keyboard-es-winkeys]\nDescription=\"Keyboard - Spanish - Spanish (Windows)\"\nLanguage=es\nLabel=es\n\n[keyboard-es-deadtilde]\nDescription=\"Keyboard - Spanish - Spanish (dead tilde)\"\nLanguage=es\nLabel=es\n\n[keyboard-es-dvorak]\nDescription=\"Keyboard - Spanish - Spanish (Dvorak)\"\nLanguage=es\nLabel=es\n\n[keyboard-es-ast]\nDescription=\"Keyboard - Spanish - Asturian (Spain, with bottom-dot H and L)\"\nLanguage=ast\nLabel=ast\n\n[keyboard-es-cat]\nDescription=\"Keyboard - Spanish - Catalan (Spain, with middle-dot L)\"\nLanguage=ca\nLabel=ca\n\n[keyboard-es-mac]\nDescription=\"Keyboard - Spanish - Spanish (Macintosh)\"\nLanguage=es\nLabel=es\n\n[keyboard-es-sun_type6]\nDescription=\"Keyboard - Spanish - Spanish (Sun Type 6/7)\"\nLanguage=es\nLabel=es\n\n[keyboard-ee]\nDescription=\"Keyboard - Estonian\"\nLanguage=et\nLabel=et\n\n[keyboard-ee-nodeadkeys]\nDescription=\"Keyboard - Estonian - Estonian (no dead keys)\"\nLanguage=et\nLabel=ee\n\n[keyboard-ee-dvorak]\nDescription=\"Keyboard - Estonian - Estonian (Dvorak)\"\nLanguage=et\nLabel=ee\n\n[keyboard-ee-us]\nDescription=\"Keyboard - Estonian - Estonian (US)\"\nLanguage=et\nLabel=ee\n\n[keyboard-ee-sun_type6]\nDescription=\"Keyboard - Estonian - Estonian (Sun Type 6/7)\"\nLanguage=et\nLabel=ee\n\n[keyboard-nl]\nDescription=\"Keyboard - Dutch\"\nLanguage=nl\nLabel=nl\n\n[keyboard-nl-us]\nDescription=\"Keyboard - Dutch - Dutch (US)\"\nLanguage=nl\nLabel=nl\n\n[keyboard-nl-mac]\nDescription=\"Keyboard - Dutch - Dutch (Macintosh)\"\nLanguage=nl\nLabel=nl\n\n[keyboard-nl-std]\nDescription=\"Keyboard - Dutch - Dutch (standard)\"\nLanguage=nl\nLabel=nl\n\n[keyboard-nl-sun_type6]\nDescription=\"Keyboard - Dutch - Dutch (Sun Type 6/7)\"\nLanguage=nl\nLabel=nl\n\n[keyboard-cz]\nDescription=\"Keyboard - Czech\"\nLanguage=cs\nLabel=cs\n\n[keyboard-cz-bksl]\nDescription=\"Keyboard - Czech - key)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-qwerty]\nDescription=\"Keyboard - Czech - Czech (QWERTY)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-qwerty_bksl]\nDescription=\"Keyboard - Czech - Czech (QWERTY, extended backslash)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-qwerty-mac]\nDescription=\"Keyboard - Czech - Czech (QWERTY, Macintosh)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-ucw]\nDescription=\"Keyboard - Czech - Czech (UCW, only accented letters)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-dvorak-ucw]\nDescription=\"Keyboard - Czech - Czech (US, Dvorak, UCW support)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-rus]\nDescription=\"Keyboard - Czech - Russian (Czech, phonetic)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-cz-sun_type6]\nDescription=\"Keyboard - Czech - Czech (Sun Type 6/7)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-prog]\nDescription=\"Keyboard - Czech - Czech (programming)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-typo]\nDescription=\"Keyboard - Czech - Czech (typographic)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-coder]\nDescription=\"Keyboard - Czech - Czech (coder)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-cz-prog_typo]\nDescription=\"Keyboard - Czech - Czech (programming, typographic)\"\nLanguage=cs\nLabel=cz\n\n[keyboard-se]\nDescription=\"Keyboard - Swedish\"\nLanguage=sv\nLabel=sv\n\n[keyboard-se-nodeadkeys]\nDescription=\"Keyboard - Swedish - Swedish (no dead keys)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-dvorak]\nDescription=\"Keyboard - Swedish - Swedish (Dvorak)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-rus]\nDescription=\"Keyboard - Swedish - Russian (Sweden, phonetic)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-se-rus_nodeadkeys]\nDescription=\"Keyboard - Swedish - Russian (Sweden, phonetic, no dead keys)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-se-smi]\nDescription=\"Keyboard - Swedish - Northern Saami (Sweden)\"\nLanguage=se\nLabel=se\n\n[keyboard-se-mac]\nDescription=\"Keyboard - Swedish - Swedish (Macintosh)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-svdvorak]\nDescription=\"Keyboard - Swedish - Swedish (Svdvorak)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-us_dvorak]\nDescription=\"Keyboard - Swedish - Swedish (Dvorak, intl.)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-us]\nDescription=\"Keyboard - Swedish - Swedish (US)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-swl]\nDescription=\"Keyboard - Swedish - Swedish Sign Language\"\nLanguage=swl\nLabel=se\n\n[keyboard-se-dvorak_a5]\nDescription=\"Keyboard - Swedish - Swedish (Dvorak A5)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-sun_type6]\nDescription=\"Keyboard - Swedish - Swedish (Sun Type 6/7)\"\nLanguage=sv\nLabel=se\n\n[keyboard-se-ovd]\nDescription=\"Keyboard - Swedish - Elfdalian (Swedish, with combining ogonek)\"\nLanguage=\nLabel=se\n\n[keyboard-bg]\nDescription=\"Keyboard - Bulgarian\"\nLanguage=bg\nLabel=bg\n\n[keyboard-bg-phonetic]\nDescription=\"Keyboard - Bulgarian - Bulgarian (traditional phonetic)\"\nLanguage=bg\nLabel=bg\n\n[keyboard-bg-bas_phonetic]\nDescription=\"Keyboard - Bulgarian - Bulgarian (new phonetic)\"\nLanguage=bg\nLabel=bg\n\n[keyboard-bg-bekl]\nDescription=\"Keyboard - Bulgarian - Bulgarian (enhanced)\"\nLanguage=bg\nLabel=bg\n\n[keyboard-md]\nDescription=\"Keyboard - Moldavian\"\nLanguage=ro\nLabel=md\n\n[keyboard-md-gag]\nDescription=\"Keyboard - Moldavian - Moldavian (Gagauz)\"\nLanguage=gag\nLabel=gag\n\n[keyboard-ke]\nDescription=\"Keyboard - Swahili (Kenya)\"\nLanguage=sw\nLabel=sw\n\n[keyboard-ke-kik]\nDescription=\"Keyboard - Swahili (Kenya) - Kikuyu\"\nLanguage=ki\nLabel=ki\n\n[keyboard-us]\nDescription=\"Keyboard - English (US)\"\nLanguage=en\nLabel=en\n\n[keyboard-us-chr]\nDescription=\"Keyboard - English (US) - Cherokee\"\nLanguage=chr\nLabel=chr\n\n[keyboard-us-haw]\nDescription=\"Keyboard - English (US) - Hawaiian\"\nLanguage=haw\nLabel=haw\n\n[keyboard-us-euro]\nDescription=\"Keyboard - English (US) - English (US, euro on 5)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-intl]\nDescription=\"Keyboard - English (US) - English (US, intl., with dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-alt-intl]\nDescription=\"Keyboard - English (US) - English (US, alt. intl.)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-colemak]\nDescription=\"Keyboard - English (US) - English (Colemak)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-colemak_dh]\nDescription=\"Keyboard - English (US) - English (Colemak-DH)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-colemak_dh_iso]\nDescription=\"Keyboard - English (US) - English (Colemak-DH ISO)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak]\nDescription=\"Keyboard - English (US) - English (Dvorak)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-intl]\nDescription=\"Keyboard - English (US) - English (Dvorak, intl., with dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-alt-intl]\nDescription=\"Keyboard - English (US) - English (Dvorak, alt. intl.)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-l]\nDescription=\"Keyboard - English (US) - English (Dvorak, left-handed)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-r]\nDescription=\"Keyboard - English (US) - English (Dvorak, right-handed)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-classic]\nDescription=\"Keyboard - English (US) - English (classic Dvorak)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvp]\nDescription=\"Keyboard - English (US) - English (programmer Dvorak)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-dvorak-mac]\nDescription=\"Keyboard - English (US) - English (Dvorak, Macintosh)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-symbolic]\nDescription=\"Keyboard - English (US) - English (US, Symbolic)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-rus]\nDescription=\"Keyboard - English (US) - Russian (US, phonetic)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-us-mac]\nDescription=\"Keyboard - English (US) - English (Macintosh)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-altgr-intl]\nDescription=\"Keyboard - English (US) - English (intl., with AltGr dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-olpc2]\nDescription=\"Keyboard - English (US) - English (the divide/multiply toggle the layout)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-hbs]\nDescription=\"Keyboard - English (US) - Serbo-Croatian (US)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-norman]\nDescription=\"Keyboard - English (US) - English (Norman)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-workman]\nDescription=\"Keyboard - English (US) - English (Workman)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-workman-intl]\nDescription=\"Keyboard - English (US) - English (Workman, intl., with dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-intl-unicode]\nDescription=\"Keyboard - English (US) - English (US, intl., AltGr Unicode combining)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-alt-intl-unicode]\nDescription=\"Keyboard - English (US) - English (US, intl., AltGr Unicode combining, alt.)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-ats]\nDescription=\"Keyboard - English (US) - Atsina\"\nLanguage=en\nLabel=us\n\n[keyboard-us-crd]\nDescription=\"Keyboard - English (US) - Coeur d'Alene Salish\"\nLanguage=crd\nLabel=us\n\n[keyboard-us-cz_sk_de]\nDescription=\"Keyboard - English (US) - Czech Slovak and German (US)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-cz_sk_pl_de_es_fi_sv]\nDescription=\"Keyboard - English (US) - Czech, Slovak, Polish, Spanish, Finnish, Swedish and German (US)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-drix]\nDescription=\"Keyboard - English (US) - English (Drix)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-de_se_fi]\nDescription=\"Keyboard - English (US) - German, Swedish and Finnish (US)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-ibm238l]\nDescription=\"Keyboard - English (US) - English (US, IBM Arabic 238_L)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-sun_type6]\nDescription=\"Keyboard - English (US) - English (US, Sun Type 6/7)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx]\nDescription=\"Keyboard - English (US) - English (Carpalx)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx-intl]\nDescription=\"Keyboard - English (US) - English (Carpalx, intl., with dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx-altgr-intl]\nDescription=\"Keyboard - English (US) - English (Carpalx, intl., with AltGr dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx-full]\nDescription=\"Keyboard - English (US) - English (Carpalx, full optimization)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx-full-intl]\nDescription=\"Keyboard - English (US) - English (Carpalx, full optimization, intl., with dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-carpalx-full-altgr-intl]\nDescription=\"Keyboard - English (US) - English (Carpalx, full optimization, intl., with AltGr dead keys)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-3l]\nDescription=\"Keyboard - English (US) - English (3l)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-3l-cros]\nDescription=\"Keyboard - English (US) - mebook)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-3l-emacs]\nDescription=\"Keyboard - English (US) - English (3l, emacs)\"\nLanguage=en\nLabel=us\n\n[keyboard-us-scn]\nDescription=\"Keyboard - English (US) - Sicilian (US keyboard)\"\nLanguage=en\nLabel=us\n\n[keyboard-ge]\nDescription=\"Keyboard - Georgian\"\nLanguage=ka\nLabel=ka\n\n[keyboard-ge-ergonomic]\nDescription=\"Keyboard - Georgian - Georgian (ergonomic)\"\nLanguage=ka\nLabel=ge\n\n[keyboard-ge-mess]\nDescription=\"Keyboard - Georgian - Georgian (MESS)\"\nLanguage=ka\nLabel=ge\n\n[keyboard-ge-ru]\nDescription=\"Keyboard - Georgian - Russian (Georgia)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-ge-os]\nDescription=\"Keyboard - Georgian - Ossetian (Georgia)\"\nLanguage=os\nLabel=ge\n\n[keyboard-hr]\nDescription=\"Keyboard - Croatian\"\nLanguage=hr\nLabel=hr\n\n[keyboard-hr-alternatequotes]\nDescription=\"Keyboard - Croatian - Croatian (with guillemets)\"\nLanguage=hr\nLabel=hr\n\n[keyboard-hr-unicode]\nDescription=\"Keyboard - Croatian - Croatian (with Croatian digraphs)\"\nLanguage=hr\nLabel=hr\n\n[keyboard-hr-unicodeus]\nDescription=\"Keyboard - Croatian - Croatian (US, with Croatian digraphs)\"\nLanguage=hr\nLabel=hr\n\n[keyboard-hr-us]\nDescription=\"Keyboard - Croatian - Croatian (US)\"\nLanguage=hr\nLabel=hr\n\n[keyboard-sy]\nDescription=\"Keyboard - Arabic (Syria)\"\nLanguage=syr\nLabel=ar\n\n[keyboard-sy-syc]\nDescription=\"Keyboard - Arabic (Syria) - Syriac\"\nLanguage=syr\nLabel=syc\n\n[keyboard-sy-syc_phonetic]\nDescription=\"Keyboard - Arabic (Syria) - Syriac (phonetic)\"\nLanguage=syr\nLabel=syc\n\n[keyboard-sy-ku]\nDescription=\"Keyboard - Arabic (Syria) - Kurdish (Syria, Latin Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-sy-ku_f]\nDescription=\"Keyboard - Arabic (Syria) - Kurdish (Syria, F)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-sy-ku_alt]\nDescription=\"Keyboard - Arabic (Syria) - Kurdish (Syria, Latin Alt-Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-ma]\nDescription=\"Keyboard - Arabic (Morocco)\"\nLanguage=\nLabel=ar\n\n[keyboard-ma-french]\nDescription=\"Keyboard - Arabic (Morocco) - French (Morocco)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ma-tifinagh]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh)\"\nLanguage=\nLabel=ber\n\n[keyboard-ma-tifinagh-alt]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh alt.)\"\nLanguage=\nLabel=ber\n\n[keyboard-ma-tifinagh-alt-phonetic]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh phonetic, alt.)\"\nLanguage=\nLabel=ber\n\n[keyboard-ma-tifinagh-extended]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh extended)\"\nLanguage=\nLabel=ber\n\n[keyboard-ma-tifinagh-phonetic]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh phonetic)\"\nLanguage=\nLabel=ber\n\n[keyboard-ma-tifinagh-extended-phonetic]\nDescription=\"Keyboard - Arabic (Morocco) - Berber (Morocco, Tifinagh extended phonetic)\"\nLanguage=\nLabel=ber\n\n[keyboard-af]\nDescription=\"Keyboard - Afghani\"\nLanguage=\nLabel=fa\n\n[keyboard-af-ps]\nDescription=\"Keyboard - Afghani - Pashto\"\nLanguage=ps\nLabel=ps\n\n[keyboard-af-uz]\nDescription=\"Keyboard - Afghani - Uzbek (Afghanistan)\"\nLanguage=uz\nLabel=uz\n\n[keyboard-af-olpc-ps]\nDescription=\"Keyboard - Afghani - Pashto (Afghanistan, OLPC)\"\nLanguage=ps\nLabel=ps\n\n[keyboard-af-fa-olpc]\nDescription=\"Keyboard - Afghani - Persian (Afghanistan, Dari OLPC)\"\nLanguage=\nLabel=fa\n\n[keyboard-af-uz-olpc]\nDescription=\"Keyboard - Afghani - Uzbek (Afghanistan, OLPC)\"\nLanguage=uz\nLabel=uz\n\n[keyboard-dz]\nDescription=\"Keyboard - Berber (Algeria, Latin)\"\nLanguage=\nLabel=kab\n\n[keyboard-dz-azerty-deadkeys]\nDescription=\"Keyboard - Berber (Algeria, Latin) - Kabyle (AZERTY, with dead keys)\"\nLanguage=kab\nLabel=kab\n\n[keyboard-dz-qwerty-gb-deadkeys]\nDescription=\"Keyboard - Berber (Algeria, Latin) - Kabyle (QWERTY, UK, with dead keys)\"\nLanguage=kab\nLabel=kab\n\n[keyboard-dz-qwerty-us-deadkeys]\nDescription=\"Keyboard - Berber (Algeria, Latin) - Kabyle (QWERTY, US, with dead keys)\"\nLanguage=kab\nLabel=kab\n\n[keyboard-dz-ber]\nDescription=\"Keyboard - Berber (Algeria, Latin) - Berber (Algeria, Tifinagh)\"\nLanguage=kab\nLabel=kab\n\n[keyboard-dz-ar]\nDescription=\"Keyboard - Berber (Algeria, Latin) - Arabic (Algeria)\"\nLanguage=ar\nLabel=ar\n\n[keyboard-mn]\nDescription=\"Keyboard - Mongolian\"\nLanguage=mn\nLabel=mn\n\n[keyboard-pk]\nDescription=\"Keyboard - Urdu (Pakistan)\"\nLanguage=ur\nLabel=ur\n\n[keyboard-pk-urd-crulp]\nDescription=\"Keyboard - Urdu (Pakistan) - Urdu (Pakistan, CRULP)\"\nLanguage=ur\nLabel=pk\n\n[keyboard-pk-urd-nla]\nDescription=\"Keyboard - Urdu (Pakistan) - Urdu (Pakistan, NLA)\"\nLanguage=ur\nLabel=pk\n\n[keyboard-pk-ara]\nDescription=\"Keyboard - Urdu (Pakistan) - Arabic (Pakistan)\"\nLanguage=ar\nLabel=ar\n\n[keyboard-pk-snd]\nDescription=\"Keyboard - Urdu (Pakistan) - Sindhi\"\nLanguage=sd\nLabel=sd\n\n[keyboard-pk-urd-navees]\nDescription=\"Keyboard - Urdu (Pakistan) - Urdu (Navees, Pakistan)\"\nLanguage=ur\nLabel=pk\n\n[keyboard-au]\nDescription=\"Keyboard - English (Australian)\"\nLanguage=en\nLabel=en\n\n[keyboard-ml]\nDescription=\"Keyboard - Bambara\"\nLanguage=bm\nLabel=bm\n\n[keyboard-ml-fr-oss]\nDescription=\"Keyboard - Bambara - French (Mali, alt.)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ml-us-mac]\nDescription=\"Keyboard - Bambara - English (Mali, US, Macintosh)\"\nLanguage=en\nLabel=en\n\n[keyboard-ml-us-intl]\nDescription=\"Keyboard - Bambara - English (Mali, US, intl.)\"\nLanguage=en\nLabel=en\n\n[keyboard-vn]\nDescription=\"Keyboard - Vietnamese\"\nLanguage=vi\nLabel=vi\n\n[keyboard-vn-us]\nDescription=\"Keyboard - Vietnamese - Vietnamese (US)\"\nLanguage=vi\nLabel=vn\n\n[keyboard-vn-fr]\nDescription=\"Keyboard - Vietnamese - Vietnamese (French)\"\nLanguage=vi\nLabel=vn\n\n[keyboard-vn-aderty]\nDescription=\"Keyboard - Vietnamese - Vietnamese (AÐERTY)\"\nLanguage=vi\nLabel=vn\n\n[keyboard-vn-qderty]\nDescription=\"Keyboard - Vietnamese - Vietnamese (QĐERTY)\"\nLanguage=vi\nLabel=vn\n\n[keyboard-ara]\nDescription=\"Keyboard - Arabic\"\nLanguage=ar\nLabel=ar\n\n[keyboard-ara-azerty]\nDescription=\"Keyboard - Arabic - Arabic (AZERTY)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-azerty_digits]\nDescription=\"Keyboard - Arabic - s)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-digits]\nDescription=\"Keyboard - Arabic - Arabic (Eastern Arabic numerals)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-qwerty]\nDescription=\"Keyboard - Arabic - Arabic (QWERTY)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-qwerty_digits]\nDescription=\"Keyboard - Arabic - Arabic (QWERTY, Eastern Arabic numerals)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-buckwalter]\nDescription=\"Keyboard - Arabic - Arabic (Buckwalter)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-olpc]\nDescription=\"Keyboard - Arabic - Arabic (OLPC)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-mac]\nDescription=\"Keyboard - Arabic - Arabic (Macintosh)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-sun_type6]\nDescription=\"Keyboard - Arabic - Arabic (Sun Type 6/7)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-basic_ext]\nDescription=\"Keyboard - Arabic - Arabic (Arabic numerals, extensions in the 4th level)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-basic_ext_digits]\nDescription=\"Keyboard - Arabic - Arabic (Eastern Arabic numerals, extensions in the 4th level)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-uga]\nDescription=\"Keyboard - Arabic - Ugaritic instead of Arabic\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ara-ergoarabic]\nDescription=\"Keyboard - Arabic - Arabic (Ergoarabic)\"\nLanguage=ar\nLabel=ara\n\n[keyboard-ie]\nDescription=\"Keyboard - Irish\"\nLanguage=en\nLabel=ie\n\n[keyboard-ie-CloGaelach]\nDescription=\"Keyboard - Irish - CloGaelach\"\nLanguage=ga\nLabel=ie\n\n[keyboard-ie-UnicodeExpert]\nDescription=\"Keyboard - Irish - Irish (UnicodeExpert)\"\nLanguage=en\nLabel=ie\n\n[keyboard-ie-ogam]\nDescription=\"Keyboard - Irish - Ogham\"\nLanguage=sga\nLabel=ie\n\n[keyboard-ie-ogam_is434]\nDescription=\"Keyboard - Irish - Ogham (IS434)\"\nLanguage=sga\nLabel=ie\n\n[keyboard-cm]\nDescription=\"Keyboard - English (Cameroon)\"\nLanguage=en\nLabel=cm\n\n[keyboard-cm-french]\nDescription=\"Keyboard - English (Cameroon) - French (Cameroon)\"\nLanguage=fr\nLabel=cm\n\n[keyboard-cm-qwerty]\nDescription=\"Keyboard - English (Cameroon) - Cameroon Multilingual (QWERTY, intl.)\"\nLanguage=en\nLabel=cm\n\n[keyboard-cm-azerty]\nDescription=\"Keyboard - English (Cameroon) - Cameroon (AZERTY, intl.)\"\nLanguage=fr\nLabel=cm\n\n[keyboard-cm-dvorak]\nDescription=\"Keyboard - English (Cameroon) - Cameroon (Dvorak, intl.)\"\nLanguage=en\nLabel=cm\n\n[keyboard-cm-mmuock]\nDescription=\"Keyboard - English (Cameroon) - Mmuock\"\nLanguage=en\nLabel=cm\n\n[keyboard-kg]\nDescription=\"Keyboard - Kyrgyz\"\nLanguage=ky\nLabel=ki\n\n[keyboard-kg-phonetic]\nDescription=\"Keyboard - Kyrgyz - Kyrgyz (phonetic)\"\nLanguage=ky\nLabel=kg\n\n[keyboard-ph]\nDescription=\"Keyboard - Filipino\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-qwerty-bay]\nDescription=\"Keyboard - Filipino - Filipino (QWERTY, Baybayin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-capewell-dvorak]\nDescription=\"Keyboard - Filipino - Filipino (Capewell-Dvorak, Latin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-capewell-dvorak-bay]\nDescription=\"Keyboard - Filipino - Filipino (Capewell-Dvorak, Baybayin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-capewell-qwerf2k6]\nDescription=\"Keyboard - Filipino - Filipino (Capewell-QWERF 2006, Latin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-capewell-qwerf2k6-bay]\nDescription=\"Keyboard - Filipino - Filipino (Capewell-QWERF 2006, Baybayin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-colemak]\nDescription=\"Keyboard - Filipino - Filipino (Colemak, Latin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-colemak-bay]\nDescription=\"Keyboard - Filipino - Filipino (Colemak, Baybayin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-dvorak]\nDescription=\"Keyboard - Filipino - Filipino (Dvorak, Latin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-ph-dvorak-bay]\nDescription=\"Keyboard - Filipino - Filipino (Dvorak, Baybayin)\"\nLanguage=fil\nLabel=ph\n\n[keyboard-bd]\nDescription=\"Keyboard - Bangla\"\nLanguage=bn\nLabel=bn\n\n[keyboard-bd-probhat]\nDescription=\"Keyboard - Bangla - Bangla (Probhat)\"\nLanguage=bn\nLabel=bd\n\n[keyboard-lk]\nDescription=\"Keyboard - Sinhala (phonetic)\"\nLanguage=si\nLabel=si\n\n[keyboard-lk-tam_unicode]\nDescription=\"Keyboard - Sinhala (phonetic) - Tamil (Sri Lanka, TamilNet '99)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-lk-tam_TAB]\nDescription=\"Keyboard - Sinhala (phonetic) - Tamil (Sri Lanka, TamilNet '99, TAB encoding)\"\nLanguage=ta\nLabel=lk\n\n[keyboard-lk-us]\nDescription=\"Keyboard - Sinhala (phonetic) - Sinhala (US)\"\nLanguage=si\nLabel=us\n\n[keyboard-al]\nDescription=\"Keyboard - Albanian\"\nLanguage=sq\nLabel=sq\n\n[keyboard-al-plisi]\nDescription=\"Keyboard - Albanian - Albanian (Plisi)\"\nLanguage=sq\nLabel=al\n\n[keyboard-al-veqilharxhi]\nDescription=\"Keyboard - Albanian - Albanian (Veqilharxhi)\"\nLanguage=sq\nLabel=al\n\n[keyboard-tw]\nDescription=\"Keyboard - Taiwanese\"\nLanguage=\nLabel=zh\n\n[keyboard-tw-indigenous]\nDescription=\"Keyboard - Taiwanese - Taiwanese (indigenous)\"\nLanguage=tay\nLabel=tw\n\n[keyboard-tw-saisiyat]\nDescription=\"Keyboard - Taiwanese - Saisiyat (Taiwan)\"\nLanguage=xsy\nLabel=xsy\n\n[keyboard-rs]\nDescription=\"Keyboard - Serbian\"\nLanguage=sr\nLabel=sr\n\n[keyboard-rs-yz]\nDescription=\"Keyboard - Serbian - Serbian (Cyrillic, ZE and ZHE swapped)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-latin]\nDescription=\"Keyboard - Serbian - Serbian (Latin)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-latinunicode]\nDescription=\"Keyboard - Serbian - Serbian (Latin, Unicode)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-latinyz]\nDescription=\"Keyboard - Serbian - Serbian (Latin, QWERTY)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-latinunicodeyz]\nDescription=\"Keyboard - Serbian - Serbian (Latin, Unicode, QWERTY)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-alternatequotes]\nDescription=\"Keyboard - Serbian - Serbian (Cyrillic, with guillemets)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-latinalternatequotes]\nDescription=\"Keyboard - Serbian - Serbian (Latin, with guillemets)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-rs-rue]\nDescription=\"Keyboard - Serbian - Pannonian Rusyn\"\nLanguage=rue\nLabel=rs\n\n[keyboard-rs-combiningkeys]\nDescription=\"Keyboard - Serbian - Serbian (combining accents instead of dead keys)\"\nLanguage=sr\nLabel=rs\n\n[keyboard-dk]\nDescription=\"Keyboard - Danish\"\nLanguage=da\nLabel=da\n\n[keyboard-dk-nodeadkeys]\nDescription=\"Keyboard - Danish - Danish (no dead keys)\"\nLanguage=da\nLabel=dk\n\n[keyboard-dk-winkeys]\nDescription=\"Keyboard - Danish - Danish (Windows)\"\nLanguage=da\nLabel=dk\n\n[keyboard-dk-mac]\nDescription=\"Keyboard - Danish - Danish (Macintosh)\"\nLanguage=da\nLabel=dk\n\n[keyboard-dk-mac_nodeadkeys]\nDescription=\"Keyboard - Danish - Danish (Macintosh, no dead keys)\"\nLanguage=da\nLabel=dk\n\n[keyboard-dk-dvorak]\nDescription=\"Keyboard - Danish - Danish (Dvorak)\"\nLanguage=da\nLabel=dk\n\n[keyboard-dk-sun_type6]\nDescription=\"Keyboard - Danish - Danish (Sun Type 6/7)\"\nLanguage=da\nLabel=dk\n\n[keyboard-bt]\nDescription=\"Keyboard - Dzongkha\"\nLanguage=dz\nLabel=dz\n\n[keyboard-la]\nDescription=\"Keyboard - Lao\"\nLanguage=lo\nLabel=lo\n\n[keyboard-la-stea]\nDescription=\"Keyboard - Lao - Lao (STEA)\"\nLanguage=lo\nLabel=la\n\n[keyboard-mm]\nDescription=\"Keyboard - Burmese\"\nLanguage=my\nLabel=my\n\n[keyboard-mm-zawgyi]\nDescription=\"Keyboard - Burmese - Burmese Zawgyi\"\nLanguage=my\nLabel=zg\n\n[keyboard-si]\nDescription=\"Keyboard - Slovenian\"\nLanguage=sl\nLabel=sl\n\n[keyboard-si-alternatequotes]\nDescription=\"Keyboard - Slovenian - Slovenian (with guillemets)\"\nLanguage=sl\nLabel=si\n\n[keyboard-si-us]\nDescription=\"Keyboard - Slovenian - Slovenian (US)\"\nLanguage=sl\nLabel=si\n\n[keyboard-am]\nDescription=\"Keyboard - Armenian\"\nLanguage=hy\nLabel=hy\n\n[keyboard-am-phonetic]\nDescription=\"Keyboard - Armenian - Armenian (phonetic)\"\nLanguage=hy\nLabel=am\n\n[keyboard-am-phonetic-alt]\nDescription=\"Keyboard - Armenian - Armenian (alt. phonetic)\"\nLanguage=hy\nLabel=am\n\n[keyboard-am-eastern]\nDescription=\"Keyboard - Armenian - Armenian (eastern)\"\nLanguage=hy\nLabel=am\n\n[keyboard-am-western]\nDescription=\"Keyboard - Armenian - Armenian (western)\"\nLanguage=hy\nLabel=am\n\n[keyboard-am-eastern-alt]\nDescription=\"Keyboard - Armenian - Armenian (alt. eastern)\"\nLanguage=hy\nLabel=am\n\n[keyboard-am-olpc-phonetic]\nDescription=\"Keyboard - Armenian - Armenian (OLPC, phonetic)\"\nLanguage=hy\nLabel=am\n\n[keyboard-by]\nDescription=\"Keyboard - Belarusian\"\nLanguage=be\nLabel=by\n\n[keyboard-by-legacy]\nDescription=\"Keyboard - Belarusian - Belarusian (legacy)\"\nLanguage=be\nLabel=by\n\n[keyboard-by-latin]\nDescription=\"Keyboard - Belarusian - Belarusian (Latin)\"\nLanguage=be\nLabel=by\n\n[keyboard-by-ru]\nDescription=\"Keyboard - Belarusian - Russian (Belarus)\"\nLanguage=be\nLabel=by\n\n[keyboard-by-intl]\nDescription=\"Keyboard - Belarusian - Belarusian (intl.)\"\nLanguage=be\nLabel=by\n\n[keyboard-br]\nDescription=\"Keyboard - Portuguese (Brazil)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-br-nodeadkeys]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, no dead keys)\"\nLanguage=pt\nLabel=br\n\n[keyboard-br-dvorak]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, Dvorak)\"\nLanguage=pt\nLabel=br\n\n[keyboard-br-nativo]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, Nativo)\"\nLanguage=pt\nLabel=br\n\n[keyboard-br-nativo-us]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, Nativo for US keyboards)\"\nLanguage=pt\nLabel=br\n\n[keyboard-br-nativo-epo]\nDescription=\"Keyboard - Portuguese (Brazil) - Esperanto (Brazil, Nativo)\"\nLanguage=eo\nLabel=br\n\n[keyboard-br-thinkpad]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, IBM/Lenovo ThinkPad)\"\nLanguage=pt\nLabel=br\n\n[keyboard-br-sun_type6]\nDescription=\"Keyboard - Portuguese (Brazil) - Portuguese (Brazil, Sun Type 6/7)\"\nLanguage=pt\nLabel=br\n\n[keyboard-in]\nDescription=\"Keyboard - Indian\"\nLanguage=\nLabel=in\n\n[keyboard-in-ben]\nDescription=\"Keyboard - Indian - Bangla (India)\"\nLanguage=bn\nLabel=bn\n\n[keyboard-in-ben_probhat]\nDescription=\"Keyboard - Indian - Bangla (India, Probhat)\"\nLanguage=bn\nLabel=bn\n\n[keyboard-in-ben_baishakhi]\nDescription=\"Keyboard - Indian - Bangla (India, Baishakhi)\"\nLanguage=bn\nLabel=in\n\n[keyboard-in-ben_bornona]\nDescription=\"Keyboard - Indian - Bangla (India, Bornona)\"\nLanguage=bn\nLabel=in\n\n[keyboard-in-ben_gitanjali]\nDescription=\"Keyboard - Indian - Bangla (India, Gitanjali)\"\nLanguage=bn\nLabel=in\n\n[keyboard-in-ben_inscript]\nDescription=\"Keyboard - Indian - Bangla (India, Baishakhi InScript)\"\nLanguage=bn\nLabel=in\n\n[keyboard-in-eeyek]\nDescription=\"Keyboard - Indian - Manipuri (Eeyek)\"\nLanguage=mni\nLabel=in\n\n[keyboard-in-guj]\nDescription=\"Keyboard - Indian - Gujarati\"\nLanguage=gu\nLabel=gu\n\n[keyboard-in-guru]\nDescription=\"Keyboard - Indian - Punjabi (Gurmukhi)\"\nLanguage=pa\nLabel=pa\n\n[keyboard-in-jhelum]\nDescription=\"Keyboard - Indian - Punjabi (Gurmukhi Jhelum)\"\nLanguage=pa\nLabel=pa\n\n[keyboard-in-kan]\nDescription=\"Keyboard - Indian - Kannada\"\nLanguage=kn\nLabel=kn\n\n[keyboard-in-kan-kagapa]\nDescription=\"Keyboard - Indian - Kannada (KaGaPa, phonetic)\"\nLanguage=kn\nLabel=kn\n\n[keyboard-in-mal]\nDescription=\"Keyboard - Indian - Malayalam\"\nLanguage=ml\nLabel=ml\n\n[keyboard-in-mal_lalitha]\nDescription=\"Keyboard - Indian - Malayalam (Lalitha)\"\nLanguage=ml\nLabel=ml\n\n[keyboard-in-mal_enhanced]\nDescription=\"Keyboard - Indian - Malayalam (enhanced InScript, with rupee)\"\nLanguage=ml\nLabel=ml\n\n[keyboard-in-ori]\nDescription=\"Keyboard - Indian - Oriya\"\nLanguage=or\nLabel=or\n\n[keyboard-in-ori-bolnagri]\nDescription=\"Keyboard - Indian - Oriya (Bolnagri)\"\nLanguage=or\nLabel=or\n\n[keyboard-in-ori-wx]\nDescription=\"Keyboard - Indian - Oriya (Wx)\"\nLanguage=or\nLabel=or\n\n[keyboard-in-olck]\nDescription=\"Keyboard - Indian - Ol Chiki\"\nLanguage=sat\nLabel=sat\n\n[keyboard-in-tam_tamilnet]\nDescription=\"Keyboard - Indian - Tamil (TamilNet '99)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-in-tam_tamilnet_with_tam_nums]\nDescription=\"Keyboard - Indian - Tamil (TamilNet '99 with Tamil numerals)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-in-tam_tamilnet_TAB]\nDescription=\"Keyboard - Indian - Tamil (TamilNet '99, TAB encoding)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-in-tam_tamilnet_TSCII]\nDescription=\"Keyboard - Indian - Tamil (TamilNet '99, TSCII encoding)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-in-tam]\nDescription=\"Keyboard - Indian - Tamil (InScript)\"\nLanguage=ta\nLabel=ta\n\n[keyboard-in-tel]\nDescription=\"Keyboard - Indian - Telugu\"\nLanguage=te\nLabel=te\n\n[keyboard-in-tel-kagapa]\nDescription=\"Keyboard - Indian - Telugu (KaGaPa, phonetic)\"\nLanguage=te\nLabel=te\n\n[keyboard-in-tel-sarala]\nDescription=\"Keyboard - Indian - Telugu (Sarala)\"\nLanguage=te\nLabel=te\n\n[keyboard-in-urd-phonetic]\nDescription=\"Keyboard - Indian - Urdu (phonetic)\"\nLanguage=ur\nLabel=ur\n\n[keyboard-in-urd-phonetic3]\nDescription=\"Keyboard - Indian - Urdu (alt. phonetic)\"\nLanguage=ur\nLabel=ur\n\n[keyboard-in-urd-winkeys]\nDescription=\"Keyboard - Indian - Urdu (Windows)\"\nLanguage=ur\nLabel=ur\n\n[keyboard-in-bolnagri]\nDescription=\"Keyboard - Indian - Hindi (Bolnagri)\"\nLanguage=hi\nLabel=hi\n\n[keyboard-in-hin-wx]\nDescription=\"Keyboard - Indian - Hindi (Wx)\"\nLanguage=hi\nLabel=hi\n\n[keyboard-in-hin-kagapa]\nDescription=\"Keyboard - Indian - Hindi (KaGaPa, phonetic)\"\nLanguage=hi\nLabel=hi\n\n[keyboard-in-san-kagapa]\nDescription=\"Keyboard - Indian - Sanskrit (KaGaPa, phonetic)\"\nLanguage=sa\nLabel=sa\n\n[keyboard-in-mar-kagapa]\nDescription=\"Keyboard - Indian - Marathi (KaGaPa, phonetic)\"\nLanguage=mr\nLabel=mr\n\n[keyboard-in-eng]\nDescription=\"Keyboard - Indian - English (India, with rupee)\"\nLanguage=en\nLabel=en\n\n[keyboard-in-iipa]\nDescription=\"Keyboard - Indian - Indic IPA\"\nLanguage=en\nLabel=in\n\n[keyboard-in-marathi]\nDescription=\"Keyboard - Indian - Marathi (enhanced InScript)\"\nLanguage=mr\nLabel=in\n\n[keyboard-in-modi-kagapa]\nDescription=\"Keyboard - Indian - Modi (KaGaPa phonetic)\"\nLanguage=mr\nLabel=mr\n\n[keyboard-in-san-misc]\nDescription=\"Keyboard - Indian - Sanskrit symbols\"\nLanguage=sa\nLabel=sas\n\n[keyboard-in-urd-navees]\nDescription=\"Keyboard - Indian - Urdu (Navees)\"\nLanguage=ur\nLabel=ur\n\n[keyboard-be]\nDescription=\"Keyboard - Belgian\"\nLanguage=de\nLabel=be\n\n[keyboard-be-oss]\nDescription=\"Keyboard - Belgian - Belgian (alt.)\"\nLanguage=de\nLabel=be\n\n[keyboard-be-oss_latin9]\nDescription=\"Keyboard - Belgian - Belgian (Latin-9 only, alt.)\"\nLanguage=de\nLabel=be\n\n[keyboard-be-iso-alternate]\nDescription=\"Keyboard - Belgian - Belgian (ISO, alt.)\"\nLanguage=de\nLabel=be\n\n[keyboard-be-nodeadkeys]\nDescription=\"Keyboard - Belgian - Belgian (no dead keys)\"\nLanguage=de\nLabel=be\n\n[keyboard-be-wang]\nDescription=\"Keyboard - Belgian - Belgian (Wang 724 AZERTY)\"\nLanguage=de\nLabel=be\n\n[keyboard-be-sun_type6]\nDescription=\"Keyboard - Belgian - Belgian (Sun Type 6/7)\"\nLanguage=de\nLabel=be\n\n[keyboard-eg]\nDescription=\"Keyboard - Coptic\"\nLanguage=cop\nLabel=eg\n\n[keyboard-cd]\nDescription=\"Keyboard - French (Democratic Republic of the Congo)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ba]\nDescription=\"Keyboard - Bosnian\"\nLanguage=bs\nLabel=bs\n\n[keyboard-ba-alternatequotes]\nDescription=\"Keyboard - Bosnian - Bosnian (with guillemets)\"\nLanguage=bs\nLabel=ba\n\n[keyboard-ba-unicode]\nDescription=\"Keyboard - Bosnian - Bosnian (with Bosnian digraphs)\"\nLanguage=bs\nLabel=ba\n\n[keyboard-ba-unicodeus]\nDescription=\"Keyboard - Bosnian - Bosnian (US, with Bosnian digraphs)\"\nLanguage=bs\nLabel=ba\n\n[keyboard-ba-us]\nDescription=\"Keyboard - Bosnian - Bosnian (US)\"\nLanguage=bs\nLabel=ba\n\n[keyboard-lv]\nDescription=\"Keyboard - Latvian\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-apostrophe]\nDescription=\"Keyboard - Latvian - Latvian (apostrophe)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-tilde]\nDescription=\"Keyboard - Latvian - Latvian (tilde)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-fkey]\nDescription=\"Keyboard - Latvian - Latvian (F)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-modern]\nDescription=\"Keyboard - Latvian - Latvian (modern)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-ergonomic]\nDescription=\"Keyboard - Latvian - Latvian (ergonomic, ŪGJRMV)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-adapted]\nDescription=\"Keyboard - Latvian - Latvian (adapted)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-dvorak]\nDescription=\"Keyboard - Latvian - Latvian (Dvorak)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-ykeydvorak]\nDescription=\"Keyboard - Latvian - Latvian (Dvorak, with Y)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-minuskeydvorak]\nDescription=\"Keyboard - Latvian - Latvian (Dvorak, with minus)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-dvorakprogr]\nDescription=\"Keyboard - Latvian - Latvian (programmer Dvorak)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-ykeydvorakprogr]\nDescription=\"Keyboard - Latvian - Latvian (programmer Dvorak, with Y)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-minuskeydvorakprogr]\nDescription=\"Keyboard - Latvian - Latvian (programmer Dvorak, with minus)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-colemak]\nDescription=\"Keyboard - Latvian - Latvian (Colemak)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-apostrophecolemak]\nDescription=\"Keyboard - Latvian - Latvian (Colemak, with apostrophe)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-sun_type6]\nDescription=\"Keyboard - Latvian - Latvian (Sun Type 6/7)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-lv-apostrophe-deadquotes]\nDescription=\"Keyboard - Latvian - Latvian (apostrophe, dead quotes)\"\nLanguage=lv\nLabel=lv\n\n[keyboard-me]\nDescription=\"Keyboard - Montenegrin\"\nLanguage=sr\nLabel=sr\n\n[keyboard-me-cyrillic]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Cyrillic)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-cyrillicyz]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Cyrillic, ZE and ZHE swapped)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-latinunicode]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Latin, Unicode)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-latinyz]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Latin, QWERTY)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-latinunicodeyz]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Latin, Unicode, QWERTY)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-cyrillicalternatequotes]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Cyrillic, with guillemets)\"\nLanguage=sr\nLabel=me\n\n[keyboard-me-latinalternatequotes]\nDescription=\"Keyboard - Montenegrin - Montenegrin (Latin, with guillemets)\"\nLanguage=sr\nLabel=me\n\n[keyboard-at]\nDescription=\"Keyboard - German (Austria)\"\nLanguage=de\nLabel=de\n\n[keyboard-at-nodeadkeys]\nDescription=\"Keyboard - German (Austria) - German (Austria, no dead keys)\"\nLanguage=de\nLabel=at\n\n[keyboard-at-mac]\nDescription=\"Keyboard - German (Austria) - German (Austria, Macintosh)\"\nLanguage=de\nLabel=at\n\n[keyboard-ch]\nDescription=\"Keyboard - German (Switzerland)\"\nLanguage=de\nLabel=de\n\n[keyboard-ch-legacy]\nDescription=\"Keyboard - German (Switzerland) - German (Switzerland, legacy)\"\nLanguage=de\nLabel=ch\n\n[keyboard-ch-de_nodeadkeys]\nDescription=\"Keyboard - German (Switzerland) - German (Switzerland, no dead keys)\"\nLanguage=de\nLabel=de\n\n[keyboard-ch-fr]\nDescription=\"Keyboard - German (Switzerland) - French (Switzerland)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ch-fr_nodeadkeys]\nDescription=\"Keyboard - German (Switzerland) - French (Switzerland, no dead keys)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ch-fr_mac]\nDescription=\"Keyboard - German (Switzerland) - French (Switzerland, Macintosh)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ch-de_mac]\nDescription=\"Keyboard - German (Switzerland) - German (Switzerland, Macintosh)\"\nLanguage=de\nLabel=de\n\n[keyboard-ch-sun_type6_de]\nDescription=\"Keyboard - German (Switzerland) - German (Switzerland, Sun Type 6/7)\"\nLanguage=de\nLabel=ch\n\n[keyboard-ch-sun_type6_fr]\nDescription=\"Keyboard - German (Switzerland) - French (Switzerland, Sun Type 6/7)\"\nLanguage=de\nLabel=ch\n\n[keyboard-kz]\nDescription=\"Keyboard - Kazakh\"\nLanguage=kk\nLabel=kk\n\n[keyboard-kz-ruskaz]\nDescription=\"Keyboard - Kazakh - Russian (Kazakhstan, with Kazakh)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-kz-kazrus]\nDescription=\"Keyboard - Kazakh - Kazakh (with Russian)\"\nLanguage=kk\nLabel=kz\n\n[keyboard-kz-ext]\nDescription=\"Keyboard - Kazakh - Kazakh (extended)\"\nLanguage=kk\nLabel=kz\n\n[keyboard-kz-latin]\nDescription=\"Keyboard - Kazakh - Kazakh (Latin)\"\nLanguage=kk\nLabel=kz\n\n[keyboard-iq]\nDescription=\"Keyboard - Iraqi\"\nLanguage=ar\nLabel=ar\n\n[keyboard-iq-ku]\nDescription=\"Keyboard - Iraqi - Kurdish (Iraq, Latin Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-iq-ku_f]\nDescription=\"Keyboard - Iraqi - Kurdish (Iraq, F)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-iq-ku_alt]\nDescription=\"Keyboard - Iraqi - Kurdish (Iraq, Latin Alt-Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-iq-ku_ara]\nDescription=\"Keyboard - Iraqi - Kurdish (Iraq, Arabic-Latin)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-fo]\nDescription=\"Keyboard - Faroese\"\nLanguage=fo\nLabel=fo\n\n[keyboard-fo-nodeadkeys]\nDescription=\"Keyboard - Faroese - Faroese (no dead keys)\"\nLanguage=fo\nLabel=fo\n\n[keyboard-mk]\nDescription=\"Keyboard - Macedonian\"\nLanguage=mk\nLabel=mk\n\n[keyboard-mk-nodeadkeys]\nDescription=\"Keyboard - Macedonian - Macedonian (no dead keys)\"\nLanguage=mk\nLabel=mk\n\n[keyboard-cn]\nDescription=\"Keyboard - Chinese\"\nLanguage=zh\nLabel=zh\n\n[keyboard-cn-mon_trad]\nDescription=\"Keyboard - Chinese - Mongolian (Bichig)\"\nLanguage=mvf\nLabel=cn\n\n[keyboard-cn-mon_trad_todo]\nDescription=\"Keyboard - Chinese - Mongolian (Todo)\"\nLanguage=mvf\nLabel=cn\n\n[keyboard-cn-mon_trad_xibe]\nDescription=\"Keyboard - Chinese - Mongolian (Xibe)\"\nLanguage=sjo\nLabel=cn\n\n[keyboard-cn-mon_trad_manchu]\nDescription=\"Keyboard - Chinese - Mongolian (Manchu)\"\nLanguage=mnc\nLabel=cn\n\n[keyboard-cn-mon_trad_galik]\nDescription=\"Keyboard - Chinese - Mongolian (Galik)\"\nLanguage=mvf\nLabel=cn\n\n[keyboard-cn-mon_todo_galik]\nDescription=\"Keyboard - Chinese - Mongolian (Todo Galik)\"\nLanguage=mvf\nLabel=cn\n\n[keyboard-cn-mon_manchu_galik]\nDescription=\"Keyboard - Chinese - Mongolian (Manchu Galik)\"\nLanguage=mnc\nLabel=cn\n\n[keyboard-cn-tib]\nDescription=\"Keyboard - Chinese - Tibetan\"\nLanguage=\nLabel=cn\n\n[keyboard-cn-tib_asciinum]\nDescription=\"Keyboard - Chinese - Tibetan (with ASCII numerals)\"\nLanguage=bo\nLabel=cn\n\n[keyboard-cn-ug]\nDescription=\"Keyboard - Chinese - Uyghur\"\nLanguage=ug\nLabel=ug\n\n[keyboard-cn-altgr-pinyin]\nDescription=\"Keyboard - Chinese - Hanyu Pinyin (with AltGr dead keys)\"\nLanguage=zh\nLabel=cn\n\n[keyboard-ca]\nDescription=\"Keyboard - French (Canada)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ca-fr-dvorak]\nDescription=\"Keyboard - French (Canada) - French (Canada, Dvorak)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ca-fr-legacy]\nDescription=\"Keyboard - French (Canada) - French (Canada, legacy)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-ca-multix]\nDescription=\"Keyboard - French (Canada) - Canadian (intl.)\"\nLanguage=fr\nLabel=ca\n\n[keyboard-ca-multi]\nDescription=\"Keyboard - French (Canada) - Canadian (intl., 1st part)\"\nLanguage=fr\nLabel=ca\n\n[keyboard-ca-multi-2gr]\nDescription=\"Keyboard - French (Canada) - Canadian (intl., 2nd part)\"\nLanguage=fr\nLabel=ca\n\n[keyboard-ca-ike]\nDescription=\"Keyboard - French (Canada) - Inuktitut\"\nLanguage=iu\nLabel=ike\n\n[keyboard-ca-eng]\nDescription=\"Keyboard - French (Canada) - English (Canada)\"\nLanguage=en\nLabel=en\n\n[keyboard-ca-kut]\nDescription=\"Keyboard - French (Canada) - Kutenai\"\nLanguage=fr\nLabel=kut\n\n[keyboard-ca-shs]\nDescription=\"Keyboard - French (Canada) - Secwepemctsin\"\nLanguage=fr\nLabel=shs\n\n[keyboard-ca-sun_type6]\nDescription=\"Keyboard - French (Canada) - Multilingual (Canada, Sun Type 6/7)\"\nLanguage=fr\nLabel=ca\n\n[keyboard-gh]\nDescription=\"Keyboard - English (Ghana)\"\nLanguage=en\nLabel=en\n\n[keyboard-gh-generic]\nDescription=\"Keyboard - English (Ghana) - English (Ghana, multilingual)\"\nLanguage=en\nLabel=gh\n\n[keyboard-gh-akan]\nDescription=\"Keyboard - English (Ghana) - Akan\"\nLanguage=ak\nLabel=ak\n\n[keyboard-gh-ewe]\nDescription=\"Keyboard - English (Ghana) - Ewe\"\nLanguage=ee\nLabel=ee\n\n[keyboard-gh-fula]\nDescription=\"Keyboard - English (Ghana) - Fula\"\nLanguage=ff\nLabel=ff\n\n[keyboard-gh-ga]\nDescription=\"Keyboard - English (Ghana) - Ga\"\nLanguage=gaa\nLabel=gaa\n\n[keyboard-gh-hausa]\nDescription=\"Keyboard - English (Ghana) - Hausa (Ghana)\"\nLanguage=ha\nLabel=ha\n\n[keyboard-gh-avn]\nDescription=\"Keyboard - English (Ghana) - Avatime\"\nLanguage=avn\nLabel=avn\n\n[keyboard-gh-gillbt]\nDescription=\"Keyboard - English (Ghana) - English (Ghana, GILLBT)\"\nLanguage=en\nLabel=gh\n\n[keyboard-fr]\nDescription=\"Keyboard - French\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-nodeadkeys]\nDescription=\"Keyboard - French - French (no dead keys)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-oss]\nDescription=\"Keyboard - French - French (alt.)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-oss_latin9]\nDescription=\"Keyboard - French - French (alt., Latin-9 only)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-oss_nodeadkeys]\nDescription=\"Keyboard - French - French (alt., no dead keys)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-latin9]\nDescription=\"Keyboard - French - French (legacy, alt.)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-latin9_nodeadkeys]\nDescription=\"Keyboard - French - French (legacy, alt., no dead keys)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-bepo]\nDescription=\"Keyboard - French - French (BEPO)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-bepo_latin9]\nDescription=\"Keyboard - French - French (BEPO, Latin-9 only)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-bepo_afnor]\nDescription=\"Keyboard - French - French (BEPO, AFNOR)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-dvorak]\nDescription=\"Keyboard - French - French (Dvorak)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-mac]\nDescription=\"Keyboard - French - French (Macintosh)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-azerty]\nDescription=\"Keyboard - French - French (AZERTY)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-afnor]\nDescription=\"Keyboard - French - French (AZERTY, AFNOR)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-bre]\nDescription=\"Keyboard - French - French (Breton)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-oci]\nDescription=\"Keyboard - French - Occitan\"\nLanguage=oc\nLabel=fr\n\n[keyboard-fr-geo]\nDescription=\"Keyboard - French - Georgian (France, AZERTY Tskapo)\"\nLanguage=ka\nLabel=fr\n\n[keyboard-fr-us]\nDescription=\"Keyboard - French - French (US)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-sun_type6]\nDescription=\"Keyboard - French - French (Sun Type 6/7)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-us-alt]\nDescription=\"Keyboard - French - French (US with dead keys, alt.)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-fr-us-azerty]\nDescription=\"Keyboard - French - French (US, AZERTY)\"\nLanguage=fr\nLabel=fr\n\n[keyboard-gn]\nDescription=\"Keyboard - N'Ko (AZERTY)\"\nLanguage=nqo\nLabel=gn\n\n[keyboard-uz]\nDescription=\"Keyboard - Uzbek\"\nLanguage=uz\nLabel=uz\n\n[keyboard-uz-latin]\nDescription=\"Keyboard - Uzbek - Uzbek (Latin)\"\nLanguage=uz\nLabel=uz\n\n[keyboard-fi]\nDescription=\"Keyboard - Finnish\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-winkeys]\nDescription=\"Keyboard - Finnish - Finnish (Windows)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-classic]\nDescription=\"Keyboard - Finnish - Finnish (classic)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-nodeadkeys]\nDescription=\"Keyboard - Finnish - Finnish (classic, no dead keys)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-smi]\nDescription=\"Keyboard - Finnish - Northern Saami (Finland)\"\nLanguage=se\nLabel=fi\n\n[keyboard-fi-mac]\nDescription=\"Keyboard - Finnish - Finnish (Macintosh)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-sun_type6]\nDescription=\"Keyboard - Finnish - Finnish (Sun Type 6/7)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-das]\nDescription=\"Keyboard - Finnish - Finnish (DAS)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-fi-fidvorak]\nDescription=\"Keyboard - Finnish - Finnish (Dvorak)\"\nLanguage=fi\nLabel=fi\n\n[keyboard-lt]\nDescription=\"Keyboard - Lithuanian\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-std]\nDescription=\"Keyboard - Lithuanian - Lithuanian (standard)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-us]\nDescription=\"Keyboard - Lithuanian - Lithuanian (US)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-ibm]\nDescription=\"Keyboard - Lithuanian - Lithuanian (IBM LST 1205-92)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-lekp]\nDescription=\"Keyboard - Lithuanian - Lithuanian (LEKP)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-lekpa]\nDescription=\"Keyboard - Lithuanian - Lithuanian (LEKPa)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-sgs]\nDescription=\"Keyboard - Lithuanian - Samogitian\"\nLanguage=sgs\nLabel=lt\n\n[keyboard-lt-ratise]\nDescription=\"Keyboard - Lithuanian - Lithuanian (Ratise)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-us_dvorak]\nDescription=\"Keyboard - Lithuanian - Lithuanian (Dvorak)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-lt-sun_type6]\nDescription=\"Keyboard - Lithuanian - Lithuanian (Sun Type 6/7)\"\nLanguage=lt\nLabel=lt\n\n[keyboard-my]\nDescription=\"Keyboard - Malay (Jawi, Arabic Keyboard)\"\nLanguage=id\nLabel=ms\n\n[keyboard-my-phonetic]\nDescription=\"Keyboard - Malay (Jawi, Arabic Keyboard) - Malay (Jawi, phonetic)\"\nLanguage=id\nLabel=my\n\n[keyboard-pt]\nDescription=\"Keyboard - Portuguese\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-nodeadkeys]\nDescription=\"Keyboard - Portuguese - Portuguese (no dead keys)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-mac]\nDescription=\"Keyboard - Portuguese - Portuguese (Macintosh)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-mac_nodeadkeys]\nDescription=\"Keyboard - Portuguese - Portuguese (Macintosh, no dead keys)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-nativo]\nDescription=\"Keyboard - Portuguese - Portuguese (Nativo)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-nativo-us]\nDescription=\"Keyboard - Portuguese - Portuguese (Nativo for US keyboards)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-nativo-epo]\nDescription=\"Keyboard - Portuguese - Esperanto (Portugal, Nativo)\"\nLanguage=eo\nLabel=pt\n\n[keyboard-pt-sun_type6]\nDescription=\"Keyboard - Portuguese - Portuguese (Sun Type 6/7)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-pt-colemak]\nDescription=\"Keyboard - Portuguese - Portuguese (Colemak)\"\nLanguage=pt\nLabel=pt\n\n[keyboard-id]\nDescription=\"Keyboard - Indonesian (Latin)\"\nLanguage=id\nLabel=id\n\n[keyboard-id-phoneticx]\nDescription=\"Keyboard - Indonesian (Latin) - Indonesian (Arab Pegon, extended phonetic)\"\nLanguage=id\nLabel=id\n\n[keyboard-de]\nDescription=\"Keyboard - German\"\nLanguage=de\nLabel=de\n\n[keyboard-de-deadacute]\nDescription=\"Keyboard - German - German (dead acute)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-deadgraveacute]\nDescription=\"Keyboard - German - German (dead grave acute)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-nodeadkeys]\nDescription=\"Keyboard - German - German (no dead keys)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-e1]\nDescription=\"Keyboard - German - German (E1)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-e2]\nDescription=\"Keyboard - German - German (E2)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-T3]\nDescription=\"Keyboard - German - German (T3)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-us]\nDescription=\"Keyboard - German - German (US)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-ro]\nDescription=\"Keyboard - German - Romanian (Germany)\"\nLanguage=ro\nLabel=de\n\n[keyboard-de-ro_nodeadkeys]\nDescription=\"Keyboard - German - Romanian (Germany, no dead keys)\"\nLanguage=ro\nLabel=de\n\n[keyboard-de-dvorak]\nDescription=\"Keyboard - German - German (Dvorak)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-neo]\nDescription=\"Keyboard - German - German (Neo 2)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-mac]\nDescription=\"Keyboard - German - German (Macintosh)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-mac_nodeadkeys]\nDescription=\"Keyboard - German - German (Macintosh, no dead keys)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-dsb]\nDescription=\"Keyboard - German - Lower Sorbian\"\nLanguage=dsb\nLabel=de\n\n[keyboard-de-dsb_qwertz]\nDescription=\"Keyboard - German - Lower Sorbian (QWERTZ)\"\nLanguage=dsb\nLabel=de\n\n[keyboard-de-qwerty]\nDescription=\"Keyboard - German - German (QWERTY)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-tr]\nDescription=\"Keyboard - German - Turkish (Germany)\"\nLanguage=tr\nLabel=de\n\n[keyboard-de-ru]\nDescription=\"Keyboard - German - Russian (Germany, phonetic)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-de-deadtilde]\nDescription=\"Keyboard - German - German (dead tilde)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-hu]\nDescription=\"Keyboard - German - German (with Hungarian letters, no dead keys)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-pl]\nDescription=\"Keyboard - German - Polish (Germany, no dead keys)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-sun_type6]\nDescription=\"Keyboard - German - German (Sun Type 6/7)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-adnw]\nDescription=\"Keyboard - German - German (Aus der Neo-Welt)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-koy]\nDescription=\"Keyboard - German - German (KOY)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-bone]\nDescription=\"Keyboard - German - German (Bone)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-bone_eszett_home]\nDescription=\"Keyboard - German - German (Bone, eszett in the home row)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-neo_qwertz]\nDescription=\"Keyboard - German - German (Neo, QWERTZ)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-neo_qwerty]\nDescription=\"Keyboard - German - German (Neo, QWERTY)\"\nLanguage=de\nLabel=de\n\n[keyboard-de-ru-recom]\nDescription=\"Keyboard - German - Russian (Germany, recommended)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-de-ru-translit]\nDescription=\"Keyboard - German - Russian (Germany, transliteration)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-de-lld]\nDescription=\"Keyboard - German - German (Ladin)\"\nLanguage=de\nLabel=de_lld\n\n[keyboard-gr]\nDescription=\"Keyboard - Greek\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-simple]\nDescription=\"Keyboard - Greek - Greek (simple)\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-extended]\nDescription=\"Keyboard - Greek - Greek (extended)\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-nodeadkeys]\nDescription=\"Keyboard - Greek - Greek (no dead keys)\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-polytonic]\nDescription=\"Keyboard - Greek - Greek (polytonic)\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-sun_type6]\nDescription=\"Keyboard - Greek - Greek (Sun Type 6/7)\"\nLanguage=el\nLabel=gr\n\n[keyboard-gr-colemak]\nDescription=\"Keyboard - Greek - Greek (Colemak)\"\nLanguage=el\nLabel=gr\n\n[keyboard-tg]\nDescription=\"Keyboard - French (Togo)\"\nLanguage=fr\nLabel=fr-tg\n\n[keyboard-mao]\nDescription=\"Keyboard - Maori\"\nLanguage=mi\nLabel=mi\n\n[keyboard-sn]\nDescription=\"Keyboard - Wolof\"\nLanguage=wo\nLabel=wo\n\n[keyboard-kh]\nDescription=\"Keyboard - Khmer (Cambodia)\"\nLanguage=km\nLabel=km\n\n[keyboard-az]\nDescription=\"Keyboard - Azerbaijani\"\nLanguage=az\nLabel=az\n\n[keyboard-az-cyrillic]\nDescription=\"Keyboard - Azerbaijani - Azerbaijani (Cyrillic)\"\nLanguage=az\nLabel=az\n\n[keyboard-hu]\nDescription=\"Keyboard - Hungarian\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-standard]\nDescription=\"Keyboard - Hungarian - Hungarian (standard)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-nodeadkeys]\nDescription=\"Keyboard - Hungarian - Hungarian (no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-qwerty]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwertz_comma_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 101-key, comma, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwertz_comma_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 101-key, comma, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwertz_dot_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 101-key, dot, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwertz_dot_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 101-key, dot, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwerty_comma_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 101-key, comma, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwerty_comma_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 101-key, comma, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwerty_dot_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 101-key, dot, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-101_qwerty_dot_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 101-key, dot, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwertz_comma_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 102-key, comma, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwertz_comma_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 102-key, comma, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwertz_dot_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 102-key, dot, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwertz_dot_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTZ, 102-key, dot, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwerty_comma_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 102-key, comma, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwerty_comma_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 102-key, comma, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwerty_dot_dead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 102-key, dot, dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-102_qwerty_dot_nodead]\nDescription=\"Keyboard - Hungarian - Hungarian (QWERTY, 102-key, dot, no dead keys)\"\nLanguage=hu\nLabel=hu\n\n[keyboard-hu-oldhun]\nDescription=\"Keyboard - Hungarian - Old Hungarian\"\nLanguage=hu\nLabel=oldhun\n\n[keyboard-hu-oldhunlig]\nDescription=\"Keyboard - Hungarian - Old Hungarian (for ligatures)\"\nLanguage=hu\nLabel=oldhun(lig)\n\n[keyboard-tr]\nDescription=\"Keyboard - Turkish\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-f]\nDescription=\"Keyboard - Turkish - Turkish (F)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-alt]\nDescription=\"Keyboard - Turkish - Turkish (Alt-Q)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-ku]\nDescription=\"Keyboard - Turkish - Kurdish (Turkey, Latin Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-tr-ku_f]\nDescription=\"Keyboard - Turkish - Kurdish (Turkey, F)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-tr-ku_alt]\nDescription=\"Keyboard - Turkish - Kurdish (Turkey, Latin Alt-Q)\"\nLanguage=ku\nLabel=ku\n\n[keyboard-tr-intl]\nDescription=\"Keyboard - Turkish - Turkish (intl., with dead keys)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-crh]\nDescription=\"Keyboard - Turkish - Crimean Tatar (Turkish Q)\"\nLanguage=crh\nLabel=crh\n\n[keyboard-tr-crh_f]\nDescription=\"Keyboard - Turkish - Crimean Tatar (Turkish F)\"\nLanguage=crh\nLabel=crh\n\n[keyboard-tr-crh_alt]\nDescription=\"Keyboard - Turkish - Crimean Tatar (Turkish Alt-Q)\"\nLanguage=crh\nLabel=crh\n\n[keyboard-tr-ot]\nDescription=\"Keyboard - Turkish - Ottoman\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-otf]\nDescription=\"Keyboard - Turkish - Ottoman (F)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-otk]\nDescription=\"Keyboard - Turkish - Old Turkic\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-otkf]\nDescription=\"Keyboard - Turkish - Old Turkic (F)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-tr-sun_type6]\nDescription=\"Keyboard - Turkish - Turkish (Sun Type 6/7)\"\nLanguage=tr\nLabel=tr\n\n[keyboard-il]\nDescription=\"Keyboard - Hebrew\"\nLanguage=he\nLabel=he\n\n[keyboard-il-lyx]\nDescription=\"Keyboard - Hebrew - Hebrew (lyx)\"\nLanguage=he\nLabel=il\n\n[keyboard-il-phonetic]\nDescription=\"Keyboard - Hebrew - Hebrew (phonetic)\"\nLanguage=he\nLabel=il\n\n[keyboard-il-biblical]\nDescription=\"Keyboard - Hebrew - Hebrew (Biblical, Tiro)\"\nLanguage=he\nLabel=il\n\n[keyboard-il-biblicalSIL]\nDescription=\"Keyboard - Hebrew - Hebrew (Biblical, SIL phonetic)\"\nLanguage=he\nLabel=il\n\n[keyboard-it]\nDescription=\"Keyboard - Italian\"\nLanguage=it\nLabel=it\n\n[keyboard-it-nodeadkeys]\nDescription=\"Keyboard - Italian - Italian (no dead keys)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-winkeys]\nDescription=\"Keyboard - Italian - Italian (Windows)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-mac]\nDescription=\"Keyboard - Italian - Italian (Macintosh)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-us]\nDescription=\"Keyboard - Italian - Italian (US)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-geo]\nDescription=\"Keyboard - Italian - Georgian (Italy)\"\nLanguage=ka\nLabel=it\n\n[keyboard-it-ibm]\nDescription=\"Keyboard - Italian - Italian (IBM 142)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-intl]\nDescription=\"Keyboard - Italian - Italian (intl., with dead keys)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-scn]\nDescription=\"Keyboard - Italian - Sicilian\"\nLanguage=it\nLabel=it\n\n[keyboard-it-fur]\nDescription=\"Keyboard - Italian - Friulian (Italy)\"\nLanguage=fur\nLabel=it\n\n[keyboard-it-sun_type6]\nDescription=\"Keyboard - Italian - Italian (Sun Type 6/7)\"\nLanguage=it\nLabel=it\n\n[keyboard-it-lld]\nDescription=\"Keyboard - Italian - Italian (Ladin)\"\nLanguage=it\nLabel=it_lld\n\n[keyboard-it-dvorak]\nDescription=\"Keyboard - Italian - Italian (Dvorak)\"\nLanguage=it\nLabel=it\n\n[keyboard-jp]\nDescription=\"Keyboard - Japanese\"\nLanguage=ja\nLabel=ja\n\n[keyboard-jp-kana]\nDescription=\"Keyboard - Japanese - Japanese (Kana)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-kana86]\nDescription=\"Keyboard - Japanese - Japanese (Kana 86)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-OADG109A]\nDescription=\"Keyboard - Japanese - Japanese (OADG 109A)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-mac]\nDescription=\"Keyboard - Japanese - Japanese (Macintosh)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-dvorak]\nDescription=\"Keyboard - Japanese - Japanese (Dvorak)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-sun_type6]\nDescription=\"Keyboard - Japanese - Japanese (Sun Type 6)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-sun_type7]\nDescription=\"Keyboard - Japanese - Japanese (Sun Type 7, PC-compatible)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-jp-sun_type7_suncompat]\nDescription=\"Keyboard - Japanese - Japanese (Sun Type 7, Sun-compatible)\"\nLanguage=ja\nLabel=jp\n\n[keyboard-latam]\nDescription=\"Keyboard - Spanish (Latin American)\"\nLanguage=es\nLabel=es\n\n[keyboard-latam-nodeadkeys]\nDescription=\"Keyboard - Spanish (Latin American) - erican, no dead keys)\"\nLanguage=es\nLabel=latam\n\n[keyboard-latam-deadtilde]\nDescription=\"Keyboard - Spanish (Latin American) - Spanish (Latin American, dead tilde)\"\nLanguage=es\nLabel=latam\n\n[keyboard-latam-dvorak]\nDescription=\"Keyboard - Spanish (Latin American) - Spanish (Latin American, Dvorak)\"\nLanguage=es\nLabel=latam\n\n[keyboard-latam-colemak]\nDescription=\"Keyboard - Spanish (Latin American) - Spanish (Latin American, Colemak)\"\nLanguage=es\nLabel=latam\n\n[keyboard-latam-colemak-gaming]\nDescription=\"Keyboard - Spanish (Latin American) - Spanish (Latin American, Colemak for gaming)\"\nLanguage=es\nLabel=latam\n\n[keyboard-mt]\nDescription=\"Keyboard - Maltese\"\nLanguage=mt\nLabel=mt\n\n[keyboard-mt-us]\nDescription=\"Keyboard - Maltese - Maltese (US)\"\nLanguage=mt\nLabel=mt\n\n[keyboard-mt-alt-us]\nDescription=\"Keyboard - Maltese - Maltese (US, with AltGr overrides)\"\nLanguage=mt\nLabel=mt\n\n[keyboard-mt-alt-gb]\nDescription=\"Keyboard - Maltese - Maltese (UK, with AltGr overrides)\"\nLanguage=mt\nLabel=mt\n\n[keyboard-custom]\nDescription=\"Keyboard - A user-defined custom Layout\"\nLanguage=\nLabel=custom\n\n[keyboard-no]\nDescription=\"Keyboard - Norwegian\"\nLanguage=no\nLabel=no\n\n[keyboard-no-nodeadkeys]\nDescription=\"Keyboard - Norwegian - Norwegian (no dead keys)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-winkeys]\nDescription=\"Keyboard - Norwegian - Norwegian (Windows)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-dvorak]\nDescription=\"Keyboard - Norwegian - Norwegian (Dvorak)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-smi]\nDescription=\"Keyboard - Norwegian - Northern Saami (Norway)\"\nLanguage=se\nLabel=no\n\n[keyboard-no-smi_nodeadkeys]\nDescription=\"Keyboard - Norwegian - Northern Saami (Norway, no dead keys)\"\nLanguage=se\nLabel=no\n\n[keyboard-no-mac]\nDescription=\"Keyboard - Norwegian - Norwegian (Macintosh)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-mac_nodeadkeys]\nDescription=\"Keyboard - Norwegian - Norwegian (Macintosh, no dead keys)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-colemak]\nDescription=\"Keyboard - Norwegian - Norwegian (Colemak)\"\nLanguage=no\nLabel=no\n\n[keyboard-no-sun_type6]\nDescription=\"Keyboard - Norwegian - Norwegian (Sun Type 6/7)\"\nLanguage=no\nLabel=no\n\n[keyboard-pl]\nDescription=\"Keyboard - Polish\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-legacy]\nDescription=\"Keyboard - Polish - Polish (legacy)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-qwertz]\nDescription=\"Keyboard - Polish - Polish (QWERTZ)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-dvorak]\nDescription=\"Keyboard - Polish - Polish (Dvorak)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-dvorak_quotes]\nDescription=\"Keyboard - Polish - Polish (Dvorak, with Polish quotes on quotemark key)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-dvorak_altquotes]\nDescription=\"Keyboard - Polish - Polish (Dvorak, with Polish quotes on key 1)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-csb]\nDescription=\"Keyboard - Polish - Kashubian\"\nLanguage=csb\nLabel=pl\n\n[keyboard-pl-szl]\nDescription=\"Keyboard - Polish - Silesian\"\nLanguage=szl\nLabel=pl\n\n[keyboard-pl-ru_phonetic_dvorak]\nDescription=\"Keyboard - Polish - Russian (Poland, phonetic Dvorak)\"\nLanguage=ru\nLabel=ru\n\n[keyboard-pl-dvp]\nDescription=\"Keyboard - Polish - Polish (programmer Dvorak)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-intl]\nDescription=\"Keyboard - Polish - Polish (intl., with dead keys)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-colemak]\nDescription=\"Keyboard - Polish - Polish (Colemak)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-colemak_dh]\nDescription=\"Keyboard - Polish - Polish (Colemak-DH)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-sun_type6]\nDescription=\"Keyboard - Polish - Polish (Sun Type 6/7)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-glagolica]\nDescription=\"Keyboard - Polish - Polish (Glagolica)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-pl-lefty]\nDescription=\"Keyboard - Polish - Polish (lefty)\"\nLanguage=pl\nLabel=pl\n\n[keyboard-ro]\nDescription=\"Keyboard - Romanian\"\nLanguage=ro\nLabel=ro\n\n[keyboard-ro-std]\nDescription=\"Keyboard - Romanian - Romanian (standard)\"\nLanguage=ro\nLabel=ro\n\n[keyboard-ro-winkeys]\nDescription=\"Keyboard - Romanian - Romanian (Windows)\"\nLanguage=ro\nLabel=ro\n\n[keyboard-ro-crh_dobruja]\nDescription=\"Keyboard - Romanian - Crimean Tatar (Dobruja Q)\"\nLanguage=crh\nLabel=crh\n\n[keyboard-ro-ergonomic]\nDescription=\"Keyboard - Romanian - Romanian (ergonomic Touchtype)\"\nLanguage=ro\nLabel=ro\n\n[keyboard-ro-sun_type6]\nDescription=\"Keyboard - Romanian - Romanian (Sun Type 6/7)\"\nLanguage=ro\nLabel=ro\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/classicui.conf",
    "content": "# Vertical Candidate List\nVertical Candidate List=True\n# Use Per Screen DPI\nPerScreenDPI=True\n# Use mouse wheel to go to prev or next page\nWheelForPaging=True\n# Font\nFont=\"M+ 1p 10\"\n# Menu Font\nMenuFont=\"Sans 10\"\n# Use input method langauge to display text\nUseInputMethodLangaugeToDisplayText=True\n# Theme\nTheme=default\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/clipboard.conf",
    "content": "# Trigger Key\nTriggerKey=\n# Paste Primary\nPastePrimaryKey=\n# Number of entries\nNumber of entries=5\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/imselector.conf",
    "content": "# Trigger Key\nTriggerKey=\n# Trigger Key for only current input context\nTriggerKeyLocal=\n# Hotkey for switching to the N-th input method\nSwitchKey=\n# Hotkey for switching to the N-th input method for only current input context\nSwitchKeyLocal=\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/keyboard-longpress.conf",
    "content": "[Entries/0]\n# Key\nKey=a\n# Enable\nEnable=True\n\n[Entries/0/Candidates]\n0=à\n1=á\n2=â\n3=ä\n4=æ\n5=ã\n6=å\n7=ā\n\n[Entries/1]\n# Key\nKey=c\n# Enable\nEnable=True\n\n[Entries/1/Candidates]\n0=ç\n1=ć\n2=č\n\n[Entries/2]\n# Key\nKey=d\n# Enable\nEnable=True\n\n[Entries/2/Candidates]\n0=ð\n\n[Entries/3]\n# Key\nKey=e\n# Enable\nEnable=True\n\n[Entries/3/Candidates]\n0=è\n1=é\n2=ê\n3=ë\n4=ē\n5=ė\n6=ę\n7=ȩ\n8=ḝ\n9=ə\n\n[Entries/4]\n# Key\nKey=g\n# Enable\nEnable=True\n\n[Entries/4/Candidates]\n0=ğ\n\n[Entries/5]\n# Key\nKey=i\n# Enable\nEnable=True\n\n[Entries/5/Candidates]\n0=î\n1=ï\n2=í\n3=ī\n4=į\n5=ì\n6=ı\n\n[Entries/6]\n# Key\nKey=l\n# Enable\nEnable=True\n\n[Entries/6/Candidates]\n0=ł\n\n[Entries/7]\n# Key\nKey=n\n# Enable\nEnable=True\n\n[Entries/7/Candidates]\n0=ñ\n1=ń\n\n[Entries/8]\n# Key\nKey=o\n# Enable\nEnable=True\n\n[Entries/8/Candidates]\n0=ô\n1=ö\n2=ò\n3=ó\n4=œ\n5=ø\n6=ō\n7=õ\n\n[Entries/9]\n# Key\nKey=s\n# Enable\nEnable=True\n\n[Entries/9/Candidates]\n0=ß\n1=ś\n2=š\n3=ş\n\n[Entries/10]\n# Key\nKey=u\n# Enable\nEnable=True\n\n[Entries/10/Candidates]\n0=û\n1=ü\n2=ù\n3=ú\n4=ū\n\n[Entries/11]\n# Key\nKey=x\n# Enable\nEnable=True\n\n[Entries/11/Candidates]\n0=×\n\n[Entries/12]\n# Key\nKey=y\n# Enable\nEnable=True\n\n[Entries/12/Candidates]\n0=ÿ\n1=ұ\n2=ү\n3=ӯ\n4=ў\n\n[Entries/13]\n# Key\nKey=z\n# Enable\nEnable=True\n\n[Entries/13/Candidates]\n0=ž\n1=ź\n2=ż\n\n[Entries/14]\n# Key\nKey=A\n# Enable\nEnable=True\n\n[Entries/14/Candidates]\n0=À\n1=Á\n2=Â\n3=Ä\n4=Æ\n5=Ã\n6=Å\n7=Ā\n\n[Entries/15]\n# Key\nKey=C\n# Enable\nEnable=True\n\n[Entries/15/Candidates]\n0=Ç\n1=Ć\n2=Č\n\n[Entries/16]\n# Key\nKey=D\n# Enable\nEnable=True\n\n[Entries/16/Candidates]\n0=Ð\n\n[Entries/17]\n# Key\nKey=E\n# Enable\nEnable=True\n\n[Entries/17/Candidates]\n0=È\n1=É\n2=Ê\n3=Ë\n4=Ē\n5=Ė\n6=Ę\n7=Ȩ\n8=Ḝ\n9=Ə\n\n[Entries/18]\n# Key\nKey=G\n# Enable\nEnable=True\n\n[Entries/18/Candidates]\n0=Ğ\n\n[Entries/19]\n# Key\nKey=I\n# Enable\nEnable=True\n\n[Entries/19/Candidates]\n0=Î\n1=Ï\n2=Í\n3=Ī\n4=Į\n5=Ì\n\n[Entries/20]\n# Key\nKey=L\n# Enable\nEnable=True\n\n[Entries/20/Candidates]\n0=Ł\n\n[Entries/21]\n# Key\nKey=N\n# Enable\nEnable=True\n\n[Entries/21/Candidates]\n0=Ñ\n1=Ń\n\n[Entries/22]\n# Key\nKey=O\n# Enable\nEnable=True\n\n[Entries/22/Candidates]\n0=Ô\n1=Ö\n2=Ò\n3=Ó\n4=Œ\n5=Ø\n6=Ō\n7=Õ\n\n[Entries/23]\n# Key\nKey=S\n# Enable\nEnable=True\n\n[Entries/23/Candidates]\n0=ẞ\n1=Ś\n2=Š\n3=Ş\n\n[Entries/24]\n# Key\nKey=U\n# Enable\nEnable=True\n\n[Entries/24/Candidates]\n0=Û\n1=Ü\n2=Ù\n3=Ú\n4=Ū\n\n[Entries/25]\n# Key\nKey=Y\n# Enable\nEnable=True\n\n[Entries/25/Candidates]\n0=Ÿ\n1=Ұ\n2=Ү\n3=Ӯ\n4=Ў\n\n[Entries/26]\n# Key\nKey=Z\n# Enable\nEnable=True\n\n[Entries/26/Candidates]\n0=Ž\n1=Ź\n2=Ż\n\n[Entries/27]\n# Key\nKey=г\n# Enable\nEnable=True\n\n[Entries/27/Candidates]\n0=ғ\n\n[Entries/28]\n# Key\nKey=е\n# Enable\nEnable=True\n\n[Entries/28/Candidates]\n0=ё\n\n[Entries/29]\n# Key\nKey=и\n# Enable\nEnable=True\n\n[Entries/29/Candidates]\n0=ӣ\n1=і\n\n[Entries/30]\n# Key\nKey=й\n# Enable\nEnable=True\n\n[Entries/30/Candidates]\n0=ј\n\n[Entries/31]\n# Key\nKey=к\n# Enable\nEnable=True\n\n[Entries/31/Candidates]\n0=қ\n1=ҝ\n\n[Entries/32]\n# Key\nKey=н\n# Enable\nEnable=True\n\n[Entries/32/Candidates]\n0=ң\n1=һ\n\n[Entries/33]\n# Key\nKey=о\n# Enable\nEnable=True\n\n[Entries/33/Candidates]\n0=ә\n1=ө\n\n[Entries/34]\n# Key\nKey=ч\n# Enable\nEnable=True\n\n[Entries/34/Candidates]\n0=ҷ\n1=ҹ\n\n[Entries/35]\n# Key\nKey=ь\n# Enable\nEnable=True\n\n[Entries/35/Candidates]\n0=ъ\n\n[Entries/36]\n# Key\nKey=Г\n# Enable\nEnable=True\n\n[Entries/36/Candidates]\n0=Ғ\n\n[Entries/37]\n# Key\nKey=Е\n# Enable\nEnable=True\n\n[Entries/37/Candidates]\n0=Ё\n\n[Entries/38]\n# Key\nKey=И\n# Enable\nEnable=True\n\n[Entries/38/Candidates]\n0=Ӣ\n1=І\n\n[Entries/39]\n# Key\nKey=Й\n# Enable\nEnable=True\n\n[Entries/39/Candidates]\n0=Ј\n\n[Entries/40]\n# Key\nKey=К\n# Enable\nEnable=True\n\n[Entries/40/Candidates]\n0=Қ\n1=Ҝ\n\n[Entries/41]\n# Key\nKey=Н\n# Enable\nEnable=True\n\n[Entries/41/Candidates]\n0=Ң\n1=Һ\n\n[Entries/42]\n# Key\nKey=О\n# Enable\nEnable=True\n\n[Entries/42/Candidates]\n0=Ә\n1=Ө\n\n[Entries/43]\n# Key\nKey=Ч\n# Enable\nEnable=True\n\n[Entries/43/Candidates]\n0=Ҷ\n1=Ҹ\n\n[Entries/44]\n# Key\nKey=Ь\n# Enable\nEnable=True\n\n[Entries/44/Candidates]\n0=Ъ\n\n[Entries/45]\n# Key\nKey=ا\n# Enable\nEnable=True\n\n[Entries/45/Candidates]\n0=أ\n1=إ\n2=آ\n3=ء\n\n[Entries/46]\n# Key\nKey=ب\n# Enable\nEnable=True\n\n[Entries/46/Candidates]\n0=پ\n\n[Entries/47]\n# Key\nKey=ج\n# Enable\nEnable=True\n\n[Entries/47/Candidates]\n0=چ\n\n[Entries/48]\n# Key\nKey=ز\n# Enable\nEnable=True\n\n[Entries/48/Candidates]\n0=ژ\n\n[Entries/49]\n# Key\nKey=ف\n# Enable\nEnable=True\n\n[Entries/49/Candidates]\n0=ڤ\n\n[Entries/50]\n# Key\nKey=ك\n# Enable\nEnable=True\n\n[Entries/50/Candidates]\n0=گ\n\n[Entries/51]\n# Key\nKey=ل\n# Enable\nEnable=True\n\n[Entries/51/Candidates]\n0=لا\n\n[Entries/52]\n# Key\nKey=ه\n# Enable\nEnable=True\n\n[Entries/52/Candidates]\n0=ه\n\n[Entries/53]\n# Key\nKey=و\n# Enable\nEnable=True\n\n[Entries/53/Candidates]\n0=ؤ\n\n[Entries/54]\n# Key\nKey=ג\n# Enable\nEnable=True\n\n[Entries/54/Candidates]\n0=ג׳\n\n[Entries/55]\n# Key\nKey=ז\n# Enable\nEnable=True\n\n[Entries/55/Candidates]\n0=ז׳\n\n[Entries/56]\n# Key\nKey=ח\n# Enable\nEnable=True\n\n[Entries/56/Candidates]\n0=ח׳\n\n[Entries/57]\n# Key\nKey=צ׳\n# Enable\nEnable=True\n\n[Entries/57/Candidates]\n0=צ׳\n\n[Entries/58]\n# Key\nKey=ת\n# Enable\nEnable=True\n\n[Entries/58/Candidates]\n0=ת׳\n\n[Entries/59]\n# Key\nKey=י\n# Enable\nEnable=True\n\n[Entries/59/Candidates]\n0=ײַ\n\n[Entries/60]\n# Key\nKey=י\n# Enable\nEnable=True\n\n[Entries/60/Candidates]\n0=ײ\n\n[Entries/61]\n# Key\nKey=ח\n# Enable\nEnable=True\n\n[Entries/61/Candidates]\n0=ױ\n\n[Entries/62]\n# Key\nKey=ו\n# Enable\nEnable=True\n\n[Entries/62/Candidates]\n0=װ\n\n[Entries/63]\n# Key\nKey=0\n# Enable\nEnable=True\n\n[Entries/63/Candidates]\n0=∅\n1=ⁿ\n2=⁰\n\n[Entries/64]\n# Key\nKey=1\n# Enable\nEnable=True\n\n[Entries/64/Candidates]\n0=¹\n1=½\n2=⅓\n3=¼\n4=⅕\n5=⅙\n6=⅐\n7=⅛\n8=⅑\n9=⅒\n\n[Entries/65]\n# Key\nKey=2\n# Enable\nEnable=True\n\n[Entries/65/Candidates]\n0=²\n1=⅖\n2=⅔\n\n[Entries/66]\n# Key\nKey=3\n# Enable\nEnable=True\n\n[Entries/66/Candidates]\n0=³\n1=⅗\n2=¾\n3=⅜\n\n[Entries/67]\n# Key\nKey=4\n# Enable\nEnable=True\n\n[Entries/67/Candidates]\n0=⁴\n1=⅘\n2=⁵\n3=⅝\n4=⅚\n\n[Entries/68]\n# Key\nKey=5\n# Enable\nEnable=True\n\n[Entries/68/Candidates]\n0=⁵\n1=⅝\n2=⅚\n\n[Entries/69]\n# Key\nKey=6\n# Enable\nEnable=True\n\n[Entries/69/Candidates]\n0=⁶\n\n[Entries/70]\n# Key\nKey=7\n# Enable\nEnable=True\n\n[Entries/70/Candidates]\n0=⁷\n1=⅞\n\n[Entries/71]\n# Key\nKey=8\n# Enable\nEnable=True\n\n[Entries/71/Candidates]\n0=⁸\n\n[Entries/72]\n# Key\nKey=9\n# Enable\nEnable=True\n\n[Entries/72/Candidates]\n0=⁹\n\n[Entries/73]\n# Key\nKey=-\n# Enable\nEnable=True\n\n[Entries/73/Candidates]\n0=—\n1=–\n2=·\n\n[Entries/74]\n# Key\nKey=?\n# Enable\nEnable=True\n\n[Entries/74/Candidates]\n0=¿\n1=‽\n\n[Entries/75]\n# Key\nKey='\n# Enable\nEnable=True\n\n[Entries/75/Candidates]\n0=‘\n1=’\n2=‚\n3=‹\n4=›\n\n[Entries/76]\n# Key\nKey=!\n# Enable\nEnable=True\n\n[Entries/76/Candidates]\n0=¡\n\n[Entries/77]\n# Key\nKey=\"\n# Enable\nEnable=True\n\n[Entries/77/Candidates]\n0=“\n1=”\n2=„\n3=«\n4=»\n\n[Entries/78]\n# Key\nKey=/\n# Enable\nEnable=True\n\n[Entries/78/Candidates]\n0=÷\n\n[Entries/79]\n# Key\nKey=#\n# Enable\nEnable=True\n\n[Entries/79/Candidates]\n0=№\n\n[Entries/80]\n# Key\nKey=%\n# Enable\nEnable=True\n\n[Entries/80/Candidates]\n0=‰\n1=℅\n\n[Entries/81]\n# Key\nKey=^\n# Enable\nEnable=True\n\n[Entries/81/Candidates]\n0=↑\n1=←\n2=→\n3=↓\n\n[Entries/82]\n# Key\nKey=+\n# Enable\nEnable=True\n\n[Entries/82/Candidates]\n0=±\n\n[Entries/83]\n# Key\nKey=<\n# Enable\nEnable=True\n\n[Entries/83/Candidates]\n0=«\n1=≤\n2=‹\n3=⟨\n\n[Entries/84]\n# Key\nKey==\n# Enable\nEnable=True\n\n[Entries/84/Candidates]\n0=∞\n1=≠\n2=≈\n\n[Entries/85]\n# Key\nKey=>\n# Enable\nEnable=True\n\n[Entries/85/Candidates]\n0=⟩\n1=»\n2=≥\n3=›\n\n[Entries/86]\n# Key\nKey=$\n# Enable\nEnable=True\n\n[Entries/86/Candidates]\n0=¢\n1=€\n2=£\n3=¥\n4=₹\n5=₽\n6=₺\n7=₩\n8=₱\n9=₿\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/keyboard.conf",
    "content": "# Page size\nPageSize=5\n# Enable emoji in hint\nEnableEmoji=True\n# Enable emoji in quickphrase\nEnableQuickPhraseEmoji=True\n# Choose key modifier\nChoose Modifier=Alt\n# Type special characters with long press\nEnableLongPress=False\n# Applications disabled for long press\nLongPressBlocklist=\n\n[PrevCandidate]\n0=Shift+Tab\n\n[NextCandidate]\n0=Tab\n\n[Hint Trigger]\n0=Control+Alt+H\n\n[One Time Hint Trigger]\n0=Control+Alt+J\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/mozc.conf",
    "content": "# Expand Usage\nExpandMode=Hotkey\n# Hotkey to expand usage\nExpandKey=\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/notifications.conf",
    "content": "# Hidden Notifications\nHiddenNotifications=\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/xcb.conf",
    "content": "# Allow Overriding System XKB Settings\nAllow Overriding System XKB Settings=True\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/conf/xim.conf",
    "content": "# Use On The Spot Style (Needs restarting)\nUseOnTheSpot=False\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/config",
    "content": "[Hotkey]\n# Enumerate when press trigger key repeatedly\nEnumerateWithTriggerKeys=True\n# Temporally switch between first and current Input Method\nAltTriggerKeys=\n# Enumerate Input Method Forward\nEnumerateForwardKeys=\n# Enumerate Input Method Backward\nEnumerateBackwardKeys=\n# Skip first input method while enumerating\nEnumerateSkipFirst=False\n# Enumerate Input Method Group Forward\nEnumerateGroupForwardKeys=\n# Enumerate Input Method Group Backward\nEnumerateGroupBackwardKeys=\n# Activate Input Method\nActivateKeys=\n# Deactivate Input Method\nDeactivateKeys=\n\n[Hotkey/TriggerKeys]\n0=Alt+space\n1=Zenkaku_Hankaku\n\n[Hotkey/PrevPage]\n0=Up\n\n[Hotkey/NextPage]\n0=Down\n\n[Hotkey/PrevCandidate]\n0=Shift+Tab\n\n[Hotkey/NextCandidate]\n0=Tab\n\n[Hotkey/TogglePreedit]\n0=Control+Alt+P\n\n[Behavior]\n# Active By Default\nActiveByDefault=False\n# Share Input State\nShareInputState=All\n# Show preedit in application\nPreeditEnabledByDefault=True\n# Show Input Method Information when switch input method\nShowInputMethodInformation=True\n# Show Input Method Information when changing focus\nshowInputMethodInformationWhenFocusIn=False\n# Show compact input method information\nCompactInputMethodInformation=True\n# Show first input method information\nShowFirstInputMethodInformation=True\n# Default page size\nDefaultPageSize=5\n# Force Enabled Addons\nEnabledAddons=\n# Force Disabled Addons\nDisabledAddons=\n# Preload input method to be used by default\nPreloadInputMethod=True\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/profile",
    "content": "[Groups/0]\n# Group Name\nName=Default\n# Layout\nDefault Layout=us\n# Default Input Method\nDefaultIM=mozc\n\n[Groups/0/Items/0]\n# Name\nName=keyboard-us\n# Layout\nLayout=\n\n[Groups/0/Items/1]\n# Name\nName=mozc\n# Layout\nLayout=\n\n[GroupOrder]\n0=Default\n\n"
  },
  {
    "path": "legacy/fcitx5/.config/fcitx5/profile_KrsNBN",
    "content": "[Groups/0]\n# Group Name\nName=Default\n# Layout\nDefault Layout=us\n# Default Input Method\nDefaultIM=mozc\n\n[Groups/0/Items/0]\n# Name\nName=keyboard-us\n# Layout\nLayout=\n\n[Groups/0/Items/1]\n# Name\nName=mozc\n# Layout\nLayout=\n\n[GroupOrder]\n0=Default\n\n"
  },
  {
    "path": "legacy/flameshot/.config/flameshot/flameshot.ini",
    "content": "[General]\ncheckForUpdates=false\ncontrastOpacity=188\ncontrastUiColor=#180078\ndisabledTrayIcon=true\ndrawColor=#ff0000\ndrawFontSize=3\ndrawThickness=3\nfilenamePattern=Shot-%Y-%m-%d\nsavePath=/home/elianiva/Pictures\nsavePathFixed=false\nshowHelp=false\nshowSidePanelButton=false\nshowStartupLaunchMessage=true\nstartupLaunch=true\nuiColor=#304eff\n\n[Shortcuts]\nTYPE_ARROW=A\nTYPE_CIRCLE=C\nTYPE_CIRCLECOUNT=\nTYPE_COMMIT_CURRENT_TOOL=Ctrl+Return\nTYPE_COPY=Ctrl+C\nTYPE_DRAWER=D\nTYPE_EXIT=Ctrl+Q\nTYPE_IMAGEUPLOADER=Return\nTYPE_MARKER=M\nTYPE_MOVESELECTION=Ctrl+M\nTYPE_MOVE_DOWN=Down\nTYPE_MOVE_LEFT=Left\nTYPE_MOVE_RIGHT=Right\nTYPE_MOVE_UP=Up\nTYPE_OPEN_APP=Ctrl+O\nTYPE_PENCIL=P\nTYPE_PIN=\nTYPE_PIXELATE=B\nTYPE_RECTANGLE=R\nTYPE_REDO=Ctrl+Shift+Z\nTYPE_RESIZE_DOWN=Shift+Down\nTYPE_RESIZE_LEFT=Shift+Left\nTYPE_RESIZE_RIGHT=Shift+Right\nTYPE_RESIZE_UP=Shift+Up\nTYPE_SAVE=Ctrl+S\nTYPE_SELECTION=S\nTYPE_SELECTIONINDICATOR=\nTYPE_SELECT_ALL=Ctrl+A\nTYPE_TEXT=T\nTYPE_TOGGLE_PANEL=Space\nTYPE_UNDO=Ctrl+Z\n"
  },
  {
    "path": "legacy/kitty/.config/kitty/gitgud-dark.conf",
    "content": "# vim:ft=kitty\nforeground                      #C9D1D9\nbackground                      #23282F\nselection_foreground            #23282F\nselection_background            #C9D1D9\n# Cursor colors\ncursor                          #C9D1D9\ncursor_text_color               #23282F\n# URL underline color when hovering with mouse\n# kitty window border colors\n# OS Window titlebar colors\n# Tab bar colors\n# Colors for marks (marked text in the terminal)\n# The basic 16 colors\n# black\ncolor0 #23282F\ncolor8 #5C5E70\n# red\ncolor1 #E7645F\ncolor9 #FF7A70\n# green\ncolor2  #5DC070\ncolor10 #8CDA8B\n# yellow\ncolor3  #E2BE65\ncolor11 #F2E18C\n# blue\ncolor4  #5FA5E7\ncolor12 #7AC1FF\n# magenta\ncolor5  #AD8EFA\ncolor13 #D2A8FF\n# cyan\ncolor6  #68ADF3\ncolor14 #A3D6FF\n# white\ncolor7  #C9D1D9\ncolor15 #C9D1D9\n# You can set the remaining 240 colors as color16 to color255.\n"
  },
  {
    "path": "legacy/kitty/.config/kitty/gruvy.conf",
    "content": "# gruvy colorscheme for kitty\n# based on gruvbox-dark\nforeground            #e0d2ae\nbackground            #1d2020\nselection_foreground  #655b53\nselection_background  #e0d2ae\nurl_color             #7ea9a7\n# black\ncolor0   #3b3735\ncolor8   #655b53\n# red\ncolor1   #ec5421\ncolor9   #cc2714\n# green\ncolor2   #a3ba5e\ncolor10  #7d923f\n# yellow\ncolor3   #efb839\ncolor11  #d09611\n# blue\ncolor4  #7ea9a7\ncolor12 #5a8784\n# magenta\ncolor5   #d4879c\ncolor13  #bd4767\n# aqua\ncolor6   #78ba7d\ncolor14  #4e9753\n# white\ncolor7   #918273\ncolor15  #e0d2ae\n\n"
  },
  {
    "path": "legacy/kitty/.config/kitty/icy.conf",
    "content": "# # ICY\nbackground #161821\nforeground #c6c8d1\nselection_background #c6c8d1\nselection_foreground #161821\ncursor #d2d4de\n# black\ncolor0 #161821\ncolor8 #6b7089\n# red\ncolor1 #e27878\ncolor9 #e98989\n# green\ncolor2 #b4be82\ncolor10 #c0ca8e\n# yellow/orange\ncolor3 #e2a478\ncolor11 #e9b189\n# blue\ncolor4 #84a0c6\ncolor12 #91acd1\n# magenta/purple\ncolor5 #a093c7\ncolor13 #ada0d3\n# cyan\ncolor6 #89b8c2\ncolor14 #95c4ce\n# white\ncolor7 #c6c8d1\ncolor15 #d2d4de\n\n"
  },
  {
    "path": "legacy/kitty/.config/kitty/kitty.conf",
    "content": "# include colorscheme\ninclude visual-studio-dark.conf\n\nadjust_line_height 140%\nfont_size 12\nbox_drawing_scale 0.001, 0.5, 1, 1.75\n\nfont_family      JetBrainsMono Nerd Font\nbold_font        JetBrainsMono Nerd Font\nitalic_font      JetBrainsMono Nerd Font\nbold_italic_font JetBrainsMono Nerd Font\n\n# font_family      CaskaydiaCove Nerd Font\n# bold_font        CaskaydiaCove Nerd Font\n# italic_font      CaskaydiaCove Nerd Font\n# bold_italic_font CaskaydiaCove Nerd Font\n\n# japanese characters\nsymbol_map U+4E00-U+9FFF M+ 1p medium\nsymbol_map U+3041-U+3096 M+ 1p medium\nsymbol_map U+30A0-U+30FF M+ 1p medium\nsymbol_map U+FF00-U+FFEF M+ 1p medium\n\nenable_audio_bell no\n\nwindow_padding_width 2\n\n# background_opacity 0.96\nbackground_opacity 1\n\nmap ctrl+shift+w no-op\n\nmap alt+shift+k change_font_size all +1.0\nmap alt+shift+j change_font_size all -1.0\n\nmap alt+v paste_from_clipboard\nmap alt+c copy_to_clipboard\n\nmap alt+k combine : send_text application \u001bk : scroll_line_up\nmap alt+j combine : send_text application \u001bj : scroll_line_down\n\nmap alt+u scroll_page_up\nmap alt+d scroll_page_down\n\nmap kitty_mod+t new_tab_with_cwd\n\nmap alt+space no_op\n\nmap alt+1 goto_tab 1\nmap alt+2 goto_tab 2\nmap alt+3 goto_tab 3\nmap alt+4 goto_tab 4\nmap alt+5 goto_tab 5\nmap alt+6 goto_tab 6\nmap alt+n next_tab\nmap alt+p previous_tab\n\n# Icy\nactive_tab_foreground   #c6c8d1\nactive_tab_background   #4d4f66\nactive_tab_font_style   bold\ninactive_tab_foreground #161821\ninactive_tab_background #36384a\ninactive_tab_font_style normal\n\ntab_bar_style fade\n\nallow_remote_control yes\n\nhide_window_decorations no\n\n# vim:fileencoding=utf-8:ft=conf:foldmethod=marker\n\n"
  },
  {
    "path": "legacy/kitty/.config/kitty/visual-studio-dark.conf",
    "content": "# vim:ft=kitty\nforeground                      #c7c9d1\nbackground                      #181818\n# selection_foreground          #23282F\nselection_background            #264f78\n# Cursor colors\ncursor                          #aeafad\ncursor_text_color               #23282F\n# URL underline color when hovering with mouse\n# kitty window border colors\n# OS Window titlebar colors\n# Tab bar colors\n# Colors for marks (marked text in the terminal)\n# The basic 16 colors\n# black\ncolor0 #000000\ncolor8 #666666\n# red\ncolor1 #cd3131\ncolor9 #f14c4c\n# green\ncolor2  #0dbc79\ncolor10 #0dbc79\n# yellow\ncolor3  #e5e510\ncolor11 #f5f543\n# blue\ncolor4  #2472c8\ncolor12 #3b8eea\n# magenta\ncolor5  #bc3fbc\ncolor13 #d670d6\n# cyan\ncolor6  #11a8cd\ncolor14 #29b8db\n# white\ncolor7  #e5e5e5\ncolor15 #e5e5e5\n"
  },
  {
    "path": "legacy/lf/.config/lf/lfrc",
    "content": "set shell sh\n\nset previewer ~/.config/lf/preview\nset preview true\nset ratios 1:2:3\n# set color256 true\nset ignorecase true\nset icons\n\n# Custom Functions\ncmd open ${{\n    case $(file --mime-type \"$f\" -bL) in\n        text/*|application/json) $EDITOR \"$f\";;\n        video/*) mpv \"$f\";;\n        image/*) gwenview \"$f\";;\n        application/pdf) zathura \"$f\";;\n        *) xdg-open \"$f\" ;;\n    esac\n}}\n\ncmd mkdir ${{\n  directory=$1\n  if test -e \"$directory\"; then\n    lf -remote \"send $id echo Directory exists: $directory\"\n    exit 1\n  fi\n  mkdir -p \"$directory\"\n  lf -remote \"send $id select $directory\"\n}}\n\ncmd bulkrename ${{\n\tindex=$(mktemp /tmp/lf-bulk-rename-index.XXXXXXXXXX)\n\tif [ -n \"${fs}\" ]; then\n\t\techo \"$fs\" > $index\n\telse\n\t\techo \"$(ls \"$(dirname $f)\" | tr ' ' \"\\n\")\" > $index\n\tfi\n\tindex_edit=$(mktemp /tmp/lf-bulk-rename.XXXXXXXXXX)\n\tcat $index > $index_edit\n\t$EDITOR $index_edit\n\tif [ $(cat $index | wc -l) -eq $(cat $index_edit | wc -l) ]; then\n\t\tmax=$(($(cat $index | wc -l)+1))\n\t\tcounter=1\n\t\twhile [ $counter -le $max ]; do\n\t\t\ta=\"$(cat $index | sed \"${counter}q;d\")\"\n\t\t\tb=\"$(cat $index_edit | sed \"${counter}q;d\")\"\n\t\t\tcounter=$(($counter+1))\n\n\t\t\t[ \"$a\" = \"$b\" ] && continue\n\t\t\t[ -e \"$b\" ] && echo \"File exists: $b\" && continue\n\t\t\tmv \"$a\" \"$b\"\n\t\tdone\n\telse\n\t\techo \"Number of lines must stay the same\"\n\tfi\n\trm $index $index_edit\n}}\n\ncmd touch ${{\n  file=$1\n  if test -e \"$file\"; then\n    lf -remote \"send $id echo File exists: $file\"\n    exit 1\n  fi\n  touch \"$file\"\n  lf -remote \"send $id select $file\"\n}}\n\ncmd unarchive ${{\n  extract \"$f\"\n}}\n\ncmd unexecute ${{\n  chmod -x \"$f\"\n}}\n\n# Bindings\n# Remove some defaults\nmap m\nmap o\nmap n\nmap \"'\"\nmap '\"'\nmap d\nmap c\nmap e\nmap f\nmap r\nmap y\n\n# File Openers\nmap ee $$EDITOR \"$f\"\n# map u $view \"$f\"\n\n# Archive Mappings\nmap r1 unarchive\n\n# Trash Mappings\nmap dD delete\n\nmap ss stripspace\n\n# Basic Functions\nmap cd push :cd<space>\nmap gc cd ~/.config\nmap gd cd ~/Dev\nmap gl cd ~/Downloads\nmap gD cd ~/Documents\nmap gr cd ~/Repos\nmap gv cd ~/Videos\nmap gp cd ~/Pictures\n\nmap . set hidden!\nmap pp paste\nmap dd cut\nmap yy copy\nmap <enter> open\nmap mf mkfile\nmap mr sudomkfile\nmap md mkdir\nmap ms $mkscript\nmap bg setwallpaper\nmap o open_config\nmap br $vimv $fx\nmap A rename\nmap gg top\nmap G bottom\nmap R reload\nmap C clear\nmap U unselect\n"
  },
  {
    "path": "legacy/lf/.config/lf/preview",
    "content": "#!/bin/sh\n\n# Clear the last preview (if any)\n$HOME/.config/lf/image clear\n\n# Calculate where the image should be placed on the screen.\nnum=$(printf \"%0.f\\n\" \"`echo \"$(tput cols) / 2\" | bc`\")\nnumb=$(printf \"%0.f\\n\" \"`echo \"$(tput cols) - $num - 1\" | bc`\")\nnumc=$(printf \"%0.f\\n\" \"`echo \"$(tput lines) - 2\" | bc`\")\n\ncase \"$1\" in\n  *.tgz|*.tar.gz) tar tzf \"$1\";;\n  *.tar.bz2|*.tbz2) tar tjf \"$1\";;\n  *.tar.txz|*.txz) xz --list \"$1\";;\n  *.tar) tar tf \"$1\";;\n  *.zip|*.jar|*.war|*.ear|*.oxt) unzip -l \"$1\";;\n  *.rar) unrar l \"$1\";;\n  *.7z) 7z l \"$1\";;\n  *.[1-8]) man \"$1\" | col -b ;;\n  *.o) nm \"$1\" | less ;;\n  *.torrent) transmission-show \"$1\";;\n  *.iso) iso-info --no-header -l \"$1\";;\n  *odt,*.ods,*.odp,*.sxw) odt2txt \"$1\";;\n  *.doc) catdoc \"$1\" ;;\n  *.docx) docx2txt \"$1\" - ;;\n  *.csv) cat \"$1\" | sed s/,/\\\\n/g ;;\n  *.pdf)\n    CACHE=$(mktemp /tmp/thumbcache.XXXXX)\n    pdftoppm -png -f 1 -singlefile \"$1\" \"$CACHE\"\n    $HOME/.config/lf/image draw \"$CACHE.png\" $num 1 $numb $numc\n    ;;\n  *.epub)\n    CACHE=$(mktemp /tmp/thumbcache.XXXXX)\n    epub-thumbnailer \"$1\" \"$CACHE\" 1024\n    $HOME/.config/lf/image draw \"$CACHE\" $num 1 $numb $numc\n    ;;\n  *.bmp|*.jpg|*.jpeg|*.png|*.xpm)\n    $HOME/.config/lf/image draw \"$1\" $num 1 $numb $numc\n    ;;\n  *.wav|*.mp3|*.flac|*.m4a|*.wma|*.ape|*.ac3|*.og[agx]|*.spx|*.opus|*.as[fx]|*.flac) exiftool \"$1\";;\n  *.avi|*.mp4|*.wmv|*.dat|*.3gp|*.ogv|*.mkv|*.mpg|*.mpeg|*.vob|*.fl[icv]|*.m2v|*.mov|*.webm|*.ts|*.mts|*.m4v|*.r[am]|*.qt|*.divx)\n    CACHE=$(mktemp /tmp/thumbcache.XXXXX)\n    ffmpegthumbnailer -i \"$1\" -o \"$CACHE\" -s 0\n    $HOME/.config/lf/image draw \"$CACHE\" $num 1 $numb $numc\n    ;;\n  *) bat --color always \"$1\";;\nesac\n\n"
  },
  {
    "path": "legacy/pages/chrome-page/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Home</title>\n    <script src=\"./main.js\" defer></script>\n    <link rel=\"stylesheet\" href=\"./style.css\" />\n  </head>\n  <body>\n    <h1 class=\"title\">\n      <span class=\"hl\">&lt;</span>おかえりなさい!<span class=\"hl\">/&gt;</span>\n    </h1>\n    <input class=\"input\" type=\"text\" placeholder=\"Type : to focus\" />\n    <h2>コマンドリスト</h2>\n    <div class=\"help\">\n      <a href=\"#\" class=\"item\">\n        <span>o</span>\n        <span>https://</span>\n      </a>\n      <a href=\"#\" class=\"item\">\n        <span>s</span>\n        <span>https://google.com/search?q=</span>\n      </a>\n      <a href=\"https://github.com\" class=\"item\">\n        <span>gh</span>\n        <span>https://github.com</span>\n      </a>\n      <a href=\"https://facebook.com\" class=\"item\">\n        <span>fb</span>\n        <span>https://facebook.com</span>\n      </a>\n      <a href=\"https://mail.google.com\" class=\"item\">\n        <span>gm</span>\n        <span>https://mail.google.com</span>\n      </a>\n      <a href=\"https://youtube.com\" class=\"item\">\n        <span>yt</span>\n        <span>https://youtube.com</span>\n      </a>\n      <a href=\"https://web.whatsapp.com\" class=\"item\">\n        <span>wa</span>\n        <span>https://web.whatsapp.com</span>\n      </a>\n      <a href=\"https://ankiweb.net\" class=\"item\">\n        <span>an</span>\n        <span>https://ankiweb.net</span>\n      </a>\n      <a href=\"https://trello.com\" class=\"item\">\n        <span>tr</span>\n        <span>https://trello.com</span>\n      </a>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "legacy/pages/chrome-page/main.js",
    "content": "const INPUT = document.querySelector(\".input\")\nconst TITLE = document.querySelector(\".title\")\n\nwindow.onload = () => {\n  INPUT.value = \"\"\n  INPUT.focus()\n}\n\n// enums\nconst cmd = {\n  SEARCH: /^:s\\s/, // default to google\n  OPEN: /^:o\\s/,\n  FACEBOOK: /^:fb/,\n  GITHUB: /^:gh/,\n  GMAIL: /^:gm/,\n  YOUTUBE: /^:yt/,\n  WA: /^:wa/,\n  ANKI: /^:an/,\n  TRELLO: /^:tr/\n}\n\ndocument.addEventListener(\"keydown\", e => {\n  if (e.key === \":\") {\n    INPUT.focus()\n    INPUT.value = \"\"\n  }\n\n  if (e.key === \"Escape\") {\n    INPUT.value = \"\"\n    INPUT.blur()\n  }\n})\n\nINPUT.addEventListener(\"keydown\", e => {\n  const {\n    key,\n    target: { value }\n  } = e\n\n  if (key === \"Enter\") {\n    if (value.toLowerCase().match(cmd.SEARCH)) {\n      window.location.href = `https://www.google.com/search?q=${value.replace(\n        cmd.SEARCH,\n        \"\"\n      )}`\n    }\n\n    if (value.toLowerCase().match(cmd.OPEN))\n      window.location.href = `https://${value.replace(cmd.OPEN, \"\")}`\n\n    if (value.toLowerCase().match(cmd.FACEBOOK))\n      window.location.href = \"https://facebook.com\"\n    if (value.toLowerCase().match(cmd.GITHUB))\n      window.location.href = \"https://github.com\"\n    if (value.toLowerCase().match(cmd.GMAIL))\n      window.location.href = \"https://mail.google.com\"\n    if (value.toLowerCase().match(cmd.YOUTUBE))\n      window.location.href = \"https://youtube.com\"\n    if (value.toLowerCase().match(cmd.WA))\n      window.location.href = \"https://web.whatsapp.com\"\n    if (value.toLowerCase().match(cmd.ANKI))\n      window.location.href = \"https://ankiweb.net\"\n    if (value.toLowerCase().match(cmd.TRELLO))\n      window.location.href = \"https://trello.com\"\n  }\n})\n"
  },
  {
    "path": "legacy/pages/chrome-page/manifest.json",
    "content": "{\n  \"name\": \"Startpage\",\n  \"version\": \"1.0\",\n  \"description\": \"My personal custom startpage\",\n  \"manifest_version\": 2,\n  \"chrome_url_overrides\": {\n    \"newtab\": \"index.html\"\n  }\n}\n"
  },
  {
    "path": "legacy/pages/chrome-page/style.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap\");\n\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nhtml,\nbody {\n  height: 100%;\n}\n\nbody {\n  background-image: url(./bg.jpg);\n  background-size: cover;\n  overflow: hidden;\n  backdrop-filter: blur(0.5rem);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\nh1 {\n  font-family: \"M PLUS Rounded 1c\", sans-serif;\n  color: #ffffff;\n  font-size: 4rem;\n  line-height: 4rem;\n  margin-top: 12%;\n  text-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.25);\n  transition: all ease-out 0.1s;\n}\n\ninput {\n  margin: 2rem 0 4rem;\n  width: 50vw;\n  border: none;\n  padding: 1rem;\n  font-size: 1.5rem;\n  outline: none;\n  font-family: \"Iosevka\", monospace;\n  transition: all ease-out 0.1s;\n  background: none;\n  color: #ffffff;\n  border-bottom: 0.125rem #ffffff solid;\n}\n\ninput::placeholder {\n  color: #b0b0b0;\n}\n\nh2 {\n  font-family: \"Iosevka\";\n  color: #ffffff;\n  font-family: \"M PLUS Rounded 1c\", sans-serif;\n  margin-bottom: 1rem;\n}\n\n.help {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  gap: 1rem;\n  width: 60rem;\n  overflow-y: auto;\n}\n\n.item {\n  display: flex;\n  align-items: center;\n  text-decoration: none;\n  color: #282828;\n}\n\n.item span {\n  vertical-align: center;\n  font-family: \"Iosevka\", monospace;\n  padding: 0.25rem 0.5rem;\n  font-size: 1.25rem;\n}\n\n.item span:nth-child(1) {\n  background-color: #fabd2f;\n  border-bottom: 0.25rem #d79921 solid;\n}\n\n.item span:nth-child(2) {\n  background-color: #3c3836;\n  border-bottom: 0.25rem #1d2021 solid;\n  color: #ebdbb2;\n}\n\n.hl {\n  color: #fabd2f;\n}\n"
  },
  {
    "path": "legacy/pages/ryuko/slim.theme",
    "content": "msg_color               #E04447\nmsg_font                Sans:size=18:bold:dpi=75\nmsg_x                   50%\nmsg_y                   30%\n\n# Session Name\n\nsession_color           #E04447\nsession_font            Sans:size=16:bold:dpi=75\nsession_x               50%\nsession_y               90%\n\n# valid values: stretch, tile\n\nbackground_style        stretch\nbackground_color        #f2f2f2\n\n# Input controls\n\ninput_panel_x           50%\ninput_panel_y           50%\ninput_name_x            180\ninput_name_y            78\ninput_pass_x            180\ninput_pass_y            145\ninput_font              Sans:size=14:dpi=75\ninput_color             #f2f2f2\n\nusername_msg\npassword_msg\n"
  },
  {
    "path": "legacy/pages/startpage/index.html",
    "content": "<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Home</title>\n    <script src=\"./main.js\" defer></script>\n    <style>\n      @import url(https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap);\n      * {\n        padding: 0;\n        margin: 0;\n        box-sizing: border-box;\n      }\n      body {\n        position: relative;\n        overflow: hidden;\n      }\n      .container::before {\n        content: \"\";\n        display: block;\n        position: fixed;\n        top: -1rem;\n        left: -1rem;\n        right: -1rem;\n        bottom: -1rem;\n        background-image: url(ryuko.jpg);\n        background-size: cover;\n        filter: brightness(0.6) blur(0.25rem);\n        z-index: -1;\n      }\n      .title {\n        font-size: 5rem;\n        text-align: center;\n        color: #fff;\n        margin: 4rem 0 2rem;\n        font-family: \"M PLUS Rounded 1c\", sans-serif;\n        text-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n      }\n      .category {\n        font-family: \"JetBrains Mono\", monospace;\n        display: grid;\n        grid-template-columns: repeat(4, 1fr);\n        grid-template-rows: 4rem 1fr;\n        gap: 1rem;\n        row-gap: 2rem;\n        width: 90%;\n        margin: 0 auto;\n      }\n      .category__input {\n        grid-column: 2/4;\n        padding: 1rem;\n        font-size: 1.25rem;\n        background-color: rgba(0, 0, 0, 0.6);\n        backdrop-filter: blur(1rem);\n        border: none;\n        display: block;\n        border-radius: 0.5rem;\n        color: #efefef;\n        outline: 0;\n        font-family: \"JetBrains Mono\", monospace;\n        box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n        border: 0.25rem rgba(0, 0, 0, 0.4) solid;\n      }\n      .category__item {\n        background-color: rgba(0, 0, 0, 0.6);\n        backdrop-filter: blur(2rem);\n        padding: 2rem 1rem;\n        border-radius: 0.25rem;\n        box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n        border: 0.25rem rgba(0, 0, 0, 0.4) solid;\n      }\n      .category__title {\n        font-family: \"M PLUS Rounded 1c\", sans-serif;\n        color: #efefef;\n        font-size: 1.5rem;\n        margin-bottom: 0.5rem;\n      }\n      .category__title::before {\n        content: \"└ \";\n      }\n      .category__link {\n        color: #eaeaea;\n        text-decoration: none;\n        font-size: 1.125rem;\n        line-height: 1.5rem;\n        display: block;\n        padding-left: 2rem;\n        border-radius: 0.25rem;\n      }\n      .category__link:focus,\n      .category__link:hover {\n        outline: 0;\n        background-color: rgba(0, 0, 0, 0.8);\n      }\n      .category__link:focus::before,\n      .category__link:focus:last-child::before,\n      .category__link:hover::before,\n      .category__link:hover:last-child::before {\n        content: \"\";\n        padding-right: 0.75rem;\n      }\n      .category__link::before {\n        content: \"├─ \";\n      }\n      .category__link:last-child::before {\n        content: \"└─ \";\n      }\n      .blue {\n        color: #1873eb;\n      }\n      .green {\n        color: #18eb83;\n      }\n      .orange {\n        color: #f54300;\n      }\n      .yellow {\n        color: #FFD454;\n      }\n      .purple {\n        color: #8351e6;\n      }\n      .red {\n        color: #e34133;\n      }\n      .aqua {\n        color: #1c9cea;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"container\">\n      <h1 class=\"title\">こんばんは</h1>\n      <div class=\"category\">\n        <div></div>\n        <input\n          class=\"category__input\"\n          type=\"text\"\n          placeholder=\"  type : to focus\"\n        />\n        <div></div>\n        <div class=\"category__item\">\n          <h1 class=\"category__title\">ソーシャル</h1>\n          <a class=\"category__link\" href=\"https://facebook.com\">\n            <span class=\"blue\"></span>\n            facebook\n          </a>\n          <a class=\"category__link\" href=\"https://twitter.com\">\n            <span class=\"aqua\">暑</span>\n            twitter\n          </a>\n          <a class=\"category__link\" href=\"https://reddit.com/r/babymetal\">\n            <span class=\"orange\">樓</span>\n            r/babymetal\n          </a>\n          <a class=\"category__link\" href=\"https://reddit.com/r/neovim\">\n            <span class=\"orange\">樓</span>\n            r/neovim\n          </a>\n          <a class=\"category__link\" href=\"https://reddit.com/r/hololive\">\n            <span class=\"orange\">樓</span>\n            r/hololive\n          </a>\n          <a class=\"category__link\" href=\"https://reddit.com/r/hololive\">\n            <span class=\"orange\">樓</span>\n            r/trashtaste\n          </a>\n          <a class=\"category__link\" href=\"https://gitter.im/neovim/neovim\">\n            <span class=\"green\"></span>\n            gitter/neovim\n          </a>\n        </div>\n        <div class=\"category__item\">\n          <h1 class=\"category__title\">ウェッブ</h1>\n          <a class=\"category__link\" href=\"https://github.com\">\n            <span class=\"purple\"></span>\n            github\n          </a>\n          <a class=\"category__link\" href=\"https://svelte.dev\">\n            <span class=\"orange\"></span>\n            svelte\n          </a>\n          <a class=\"category__link\" href=\"https://reactjs.org\">\n            <span class=\"aqua\">ﰆ</span>\n            react\n          </a>\n          <a class=\"category__link\" href=\"https://trello.com\">\n            <span class=\"blue\">僧</span>\n            trello\n          </a>\n          <a class=\"category__link\" href=\"https://figma.com\">\n            <span class=\"blue\"> </span>\n            figma\n          </a>\n        </div>\n        <div class=\"category__item\">\n          <h1 class=\"category__title\">ジェネラル</h1>\n          <a class=\"category__link\" href=\"https://google.com\">\n            <span class=\"blue\"></span>\n            google\n          </a>\n          <a class=\"category__link\" href=\"https://youtube.com\">\n            <span class=\"red\">輸</span>\n            youtube\n          </a>\n          <a class=\"category__link\" href=\"https://mail.google.com\">\n            <span class=\"orange\"></span>\n            gmail\n          </a>\n          <a class=\"category__link\" href=\"https://pixiv.com\">\n            <span class=\"aqua\"></span>\n            pixiv\n          </a>\n          <a class=\"category__link\" href=\"https://ankiweb.net\">\n            <span class=\"blue\"></span>\n            anki\n          </a>\n          <a class=\"category__link\" href=\"https://www3.nhk.or.jp/news/easy/\">\n            <span class=\"purple\"></span>\n            nhk\n          </a>\n        </div>\n        <div class=\"category__item\">\n          <h1 class=\"category__title\">ユーチューバー</h1>\n          <a\n            class=\"category__link\"\n            href=\"https://www.youtube.com/channel/UCjFu-9GHnabzSFRAYm1B9Dw\"\n          >\n            <span class=\"red\">ﮃ</span> etna\n          </a>\n          <a\n            class=\"category__link\"\n            href=\"https://www.youtube.com/channel/UC1DCedRgGHBdm81E1llLhOQ\"\n          >\n            <span class=\"orange\"></span> pekora</a\n          >\n          <a\n            class=\"category__link\"\n            href=\"https://www.youtube.com/channel/UCvzGlP9oQwU--Y0r9id_jnA\"\n          >\n            <span class=\"yellow\"></span> subaru</a\n          >\n          <a\n            class=\"category__link\"\n            href=\"https://www.youtube.com/channel/UCZlDXzGoo7d44bwdNObFacg\"\n          >\n            <span class=\"aqua\">留</span> kanata</a\n          >\n          <a\n            class=\"category__link\"\n            href=\"https://www.youtube.com/channel/UCcmxOGYGF51T1XsqQLewGtQ\"\n          >\n            <span class=\"purple\"></span> trash taste</a\n          >\n        </div>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "legacy/pages/startpage/main.js",
    "content": "const INPUT = document.querySelector(\".category__input\")\nconst TITLE = document.querySelector(\".title\")\n\nwindow.onload = () => {\n  INPUT.value = \"\"\n  INPUT.focus()\n}\n\n// enums\nconst cmd = {\n  SEARCH: /^:s\\s/, // default to google\n  OPEN: /^:o\\s/,\n  FACEBOOK: /^:fb/,\n  GITHUB: /^:gh/,\n  GMAIL: /^:gm/,\n  YOUTUBE: /^:yt/,\n  WA: /^:wa/,\n  ANKI: /^:an/,\n  TWITTER: /^:tw/,\n  TRELLO: /^:tr/\n}\n\nwindow.addEventListener(\"keydown\", e => {\n  if (e.key === \":\") {\n    INPUT.focus()\n    INPUT.value = \"\"\n  }\n\n  if (e.key === \"Escape\") {\n    INPUT.value = \"\"\n    INPUT.blur()\n  }\n})\n\nINPUT.addEventListener(\"keydown\", e => {\n  const {\n    key,\n    target: { value }\n  } = e\n\n  if (key === \"Enter\") {\n    if (value.toLowerCase().match(cmd.SEARCH)) {\n      window.location.href = `https://www.google.com/search?q=${value.replace(\n        cmd.SEARCH,\n        \"\"\n      )}`\n    }\n\n    if (value.toLowerCase().match(cmd.OPEN))\n      window.location.href = `https://${value.replace(cmd.OPEN, \"\")}`\n\n    if (value.toLowerCase().match(cmd.FACEBOOK))\n      window.location.href = \"https://facebook.com\"\n    if (value.toLowerCase().match(cmd.GITHUB))\n      window.location.href = \"https://github.com\"\n    if (value.toLowerCase().match(cmd.GMAIL))\n      window.location.href = \"https://mail.google.com\"\n    if (value.toLowerCase().match(cmd.YOUTUBE))\n      window.location.href = \"https://youtube.com\"\n    if (value.toLowerCase().match(cmd.WA))\n      window.location.href = \"https://web.whatsapp.com\"\n    if (value.toLowerCase().match(cmd.ANKI))\n      window.location.href = \"https://ankiweb.net\"\n    if (value.toLowerCase().match(cmd.TRELLO))\n      window.location.href = \"https://trello.com\"\n    if (value.toLowerCase().match(cmd.TWITTER))\n      window.location.href = \"https://twitter.com\"\n  }\n})\n\nconst getGreet = () => {\n  const date = new Date()\n  const time = date.getHours()\n\n  if (time >= 0 && time <= 10) return \"< おはよう />\"\n  if (time > 10 && time <= 18) return \"< こんにちは />\"\n  if (time > 18 && time <= 21) return \"< こんばんは />\"\n  if (time > 21 && time <= 24) return \"< おやすみなさい />\"\n}\n\nTITLE.innerHTML = getGreet()\n"
  },
  {
    "path": "legacy/pages/startpage/manifest.json",
    "content": "{\n  \"name\": \"Startpage v2\",\n  \"version\": \"1.0\",\n  \"description\": \"My personal custom startpage v2\",\n  \"manifest_version\": 2,\n  \"chrome_url_overrides\": {\n    \"newtab\": \"index.html\"\n  }\n}\n"
  },
  {
    "path": "legacy/pages/startpage/style.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap\");\n\n* {\n  padding: 0;\n  margin: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  position: relative;\n}\n\n.container::before {\n  content: \"\";\n  display: block;\n  position: fixed;\n  top: -1rem;\n  left: -1rem;\n  right: -1rem;\n  bottom: -1rem;\n  background-image: url(./ryuko.jpg);\n  background-size: cover;\n  filter: brightness(0.6) blur(0.25rem);\n  z-index: -1;\n}\n\n.title {\n  font-size: 5rem;\n  text-align: center;\n  color: #ffffff;\n  margin: 4rem 0 2rem;\n  font-family: \"M PLUS Rounded 1c\", sans-serif;\n  text-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n}\n\n.category {\n  font-family: \"Iosevka\", sans-serif;\n  display: grid;\n  grid-template-columns: repeat(4, 1fr);\n  grid-template-rows: 4rem 1fr;\n  gap: 1rem;\n  row-gap: 2rem;\n  width: 90%;\n  margin: 0 auto;\n}\n\n.category__input {\n  grid-column: 2/4;\n  padding: 1rem;\n  font-size: 1.25rem;\n  background-color: rgba(0, 0, 0, 0.6);\n  backdrop-filter: blur(1rem);\n  border: none;\n  display: block;\n  border-radius: 0.5rem;\n  color: #efefef;\n  outline: none;\n  font-family: \"Iosevka\", monospace;\n  box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n  border: 0.25rem rgba(0, 0, 0, 0.4) solid;\n}\n\n.category__item {\n  background-color: rgba(0, 0, 0, 0.6);\n  backdrop-filter: blur(2rem);\n  padding: 2rem 1rem;\n  border-radius: 0.25rem;\n  box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.4);\n  border: 0.25rem rgba(0, 0, 0, 0.4) solid;\n}\n\n.category__title {\n  font-family: \"M PLUS Rounded 1c\", sans-serif;\n  color: #efefef;\n  font-size: 1.5rem;\n  margin-bottom: 0.5rem;\n}\n\n.category__title::before {\n  content: \"└ \";\n}\n\n.category__link {\n  color: #eaeaea;\n  text-decoration: none;\n  font-size: 1.5rem;\n  line-height: 1.95rem;\n  display: block;\n  transition: all ease-out 0.1s;\n  padding-left: 2rem;\n  border-radius: 0.25rem;\n}\n\n.category__link:focus,\n.category__link:hover {\n  outline: none;\n  background-color: rgba(0, 0, 0, 0.8);\n}\n\n.category__link:focus::before,\n.category__link:hover::before,\n.category__link:hover:last-child::before,\n.category__link:focus:last-child::before {\n  content: \"\";\n  padding-right: 0.75rem;\n}\n\n.category__link::before {\n  content: \"├─ \";\n}\n\n.category__link:last-child::before {\n  content: \"└─ \";\n}\n\n.blue {\n  color: #1873eb;\n}\n\n.green {\n  color: #18eb83;\n}\n\n.orange {\n  color: #f54300;\n}\n\n.purple {\n  color: #8351e6;\n}\n\n.red {\n  color: #e34133;\n}\n\n.aqua {\n  color: #1c9cea;\n}\n"
  },
  {
    "path": "legacy/pages/startpage-v2/index.html",
    "content": "<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Home</title>\n    <script defer src=\"./main.js\"></script>\n    <link rel=\"stylesheet\" href=\"./style.css\" />\n  </head>\n  <body>\n    <img class=\"char-img\" src=\"./ryuko.webp\" alt=\"ryuko\" />\n    <div class=\"word\">\n      <div class=\"word__text\">\n        <span class=\"word__jp\">雨</span> read as\n        <span class=\"word__kana\">あめ</span> means\n        <span class=\"word__en\">rain</span>\n      </div>\n      <a tabindex=\"-1\" href=\"#\" class=\"word__info\">More Explanation</a>\n    </div>\n    <div class=\"container\">\n      <input class=\"container__search\" type=\"text\" placeholder=\"Type here...\" />\n      <div class=\"container__child\">\n        <span class=\"child__title\">Social</span>\n        <div class=\"child__links\">\n          <a class=\"link\" href=\"https://facebook.com\">Facebook</a>\n          <a class=\"link\" href=\"https://twitter.com\">Twitter</a>\n          <a class=\"link\" href=\"https://web.whatsapp.com\">Whatsapp</a>\n          <a class=\"link\" href=\"https://reddit.com\">Reddit</a>\n          <a class=\"link\" href=\"https://app.element.io\">Element</a>\n        </div>\n      </div>\n      <div class=\"container__child\">\n        <span class=\"child__title\">Webdev</span>\n        <div class=\"child__links\">\n          <a class=\"link\" href=\"https://github.com\">Github</a>\n          <a class=\"link\" href=\"https://svelte.dev\">Svelte</a>\n          <a class=\"link\" href=\"https://reactjs.org\">React</a>\n          <a class=\"link\" href=\"https://figma.com\">Figma</a>\n          <a class=\"link\" href=\"https://medium.com\">Medium</a>\n          <a class=\"link\" href=\"https://dev.to\">DevTo</a>\n        </div>\n      </div>\n      <div class=\"container__child\">\n        <span class=\"child__title\">General</span>\n        <div class=\"child__links\">\n          <a class=\"link\" href=\"https://google.com\">Google</a>\n          <a class=\"link\" href=\"https://yandex.com\">Yandex</a>\n          <a class=\"link\" href=\"https://youtube.com\">Youtube</a>\n          <a class=\"link\" href=\"https://pixiv.com\">Pixiv</a>\n          <a class=\"link\" href=\"https://mail.google.com\">Gmail</a>\n          <a class=\"link\" href=\"https://japanese.io\">Japanese.io</a>\n        </div>\n      </div>\n      <div class=\"container__child\">\n        <span class=\"child__title\">YT/Stream</span>\n        <div class=\"child__links\">\n          <a class=\"link\" href=\"https://twitch.tv/teej_dv\">Teej_dv</a>\n          <a\n            class=\"link\"\n            href=\"https://www.youtube.com/channel/UCcmxOGYGF51T1XsqQLewGtQ\"\n          >\n            Trash Taste\n          </a>\n          <a\n            class=\"link\"\n            href=\"https://www.youtube.com/channel/UC9-y-6csu5WGm29I7JiwpnA\"\n          >\n            Computerphile\n          </a>\n          <a\n            class=\"link\"\n            href=\"https://www.youtube.com/channel/UCZlDXzGoo7d44bwdNObFacg\"\n          >\n            PPTenshi\n          </a>\n          <a\n            class=\"link\"\n            href=\"https://www.youtube.com/channel/UCvzGlP9oQwU--Y0r9id_jnA\"\n          >\n            Subaduck\n          </a>\n          <a\n            class=\"link\"\n            href=\"https://www.youtube.com/channel/UC1DCedRgGHBdm81E1llLhOQ\"\n          >\n            Pekora\n          </a>\n        </div>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "legacy/pages/startpage-v2/main.js",
    "content": "const INPUT = document.querySelector(\".container__search\")\n\nwindow.onload = () => {\n  INPUT.value = \"\"\n  INPUT.focus()\n}\n\n// enums\nconst cmd = {\n  SEARCH: /^:s\\s/,\n  OPEN: /^:o\\s/,\n  FACEBOOK: /^:fb/,\n  GITHUB: /^:gh/,\n  GMAIL: /^:gm/,\n  YOUTUBE: /^:yt/,\n  WA: /^:wa/,\n  ANKI: /^:an/,\n  TWITTER: /^:tw/,\n  TRELLO: /^:tr/,\n}\n\nwindow.addEventListener(\"keydown\", e => {\n  if (e.key === \":\") {\n    INPUT.focus()\n    INPUT.value = \"\"\n  }\n\n  if (e.key === \"Escape\") {\n    INPUT.value = \"\"\n    INPUT.blur()\n  }\n})\n\nINPUT.addEventListener(\"keydown\", e => {\n  const {\n    key,\n    target: { value },\n  } = e\n\n  if (key === \"Enter\") {\n    if (value.toLowerCase().match(cmd.SEARCH)) {\n      window.location.href = `https://www.google.com/search?q=${value.replace(\n        cmd.SEARCH,\n        \"\"\n      )}`\n    }\n\n    if (value.toLowerCase().match(cmd.OPEN))\n      window.location.href = `https://${value.replace(cmd.OPEN, \"\")}`\n\n    if (value.toLowerCase().match(cmd.FACEBOOK))\n      window.location.href = \"https://facebook.com\"\n    if (value.toLowerCase().match(cmd.GITHUB))\n      window.location.href = \"https://github.com\"\n    if (value.toLowerCase().match(cmd.GMAIL))\n      window.location.href = \"https://mail.google.com\"\n    if (value.toLowerCase().match(cmd.YOUTUBE))\n      window.location.href = \"https://youtube.com\"\n    if (value.toLowerCase().match(cmd.WA))\n      window.location.href = \"https://web.whatsapp.com\"\n    if (value.toLowerCase().match(cmd.ANKI))\n      window.location.href = \"https://ankiweb.net\"\n    if (value.toLowerCase().match(cmd.TRELLO))\n      window.location.href = \"https://trello.com\"\n    if (value.toLowerCase().match(cmd.TWITTER))\n      window.location.href = \"https://twitter.com\"\n  }\n})\n\nconst JP = document.querySelector(\".word__jp\")\nconst KANA = document.querySelector(\".word__kana\")\nconst EN = document.querySelector(\".word__en\")\nconst WORD = document.querySelector(\".word__text\")\nconst INFO = document.querySelector(\".word__info\")\nWORD.style.opacity = 0\n\nconst setFromLocalStorage = () => {\n  // try to set from `localStorage` first\n  const result = JSON.parse(localStorage.getItem(\"result\"))\n  if (!result) return\n\n  JP.textContent = result.kanji\n  KANA.textContent = result.kana\n  EN.textContent = result.en\n\n  INFO.href = `https://jisho.org/search/${result.kanji}`\n  WORD.style.opacity = 1\n}\n\nsetFromLocalStorage()\n\ndocument.addEventListener(\"DOMContentLoaded\", async () => {\n  try {\n    const res = await fetch(\n      \"https://random-jp-api.vercel.app/api/rand?level=n4\"\n    )\n    const { data: result } = await res.json()\n    localStorage.setItem(\"result\", JSON.stringify(result))\n\n    JP.textContent = result.kanji\n    KANA.textContent = result.kana\n    EN.textContent = result.en\n\n    INFO.href = `https://jisho.org/search/${result.kanji}`\n  } catch (err) {\n    setFromLocalStorage()\n    console.error(err)\n  }\n})\n"
  },
  {
    "path": "legacy/pages/startpage-v2/manifest.json",
    "content": "{\n  \"name\": \"Startpage v2-ish\",\n  \"version\": \"2.0\",\n  \"description\": \"My personal custom startpage v2-ish\",\n  \"manifest_version\": 2,\n  \"chrome_url_overrides\": {\n    \"newtab\": \"index.html\"\n  }\n}\n"
  },
  {
    "path": "legacy/pages/startpage-v2/style.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap\");\n@import url(\"https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c&display=swap\");\n\n* {\n  padding: 0;\n  margin: 0;\n  box-sizing: border-box;\n}\n\nhtml,\nbody {\n  height: 100%;\n  width: 100%;\n}\n\n.thing[foo=\"bar\"] {\n  color: red;\n}\n\nbody {\n  background-image: linear-gradient(to top, #1f2940, #384665);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n  z-index: 5;\n  padding: 2rem;\n  font-family: \"Open Sans\", sans-serif;\n}\n\n.word {\n  margin-bottom: 2rem;\n  text-align: center;\n}\n\n.word__text {\n  color: #ffffff;\n  font-family: \"Open Sans\", sans-serif;\n  font-weight: 400;\n  font-size: 3rem;\n}\n\n.word__en,\n.word__kana,\n.word__jp {\n  color: #f23c4a;\n}\n\n.word__info {\n  display: block;\n  color: #82aaff;\n  outline: none;\n}\n\n.container {\n  width: 100%;\n  display: grid;\n  grid-template-rows: 3rem 1fr;\n  grid-template-columns: repeat(4, 1fr);\n  gap: 1rem;\n}\n\n.container__search {\n  grid-column: 2/4;\n  border: 0.25rem #1b1f28 solid;\n  background-color: rgba(31, 41, 64, 0.8);\n  backdrop-filter: blur(2rem);\n  color: #ffffff;\n  padding: 0.5rem;\n  font-size: 1.125rem;\n  outline: none;\n}\n\n.container__child {\n  padding: 2rem;\n  border: 0.25rem #1b1f28 solid;\n  background-color: rgba(31, 41, 64, 0.9);\n  backdrop-filter: blur(0.75rem);\n  grid-row: 2/3;\n}\n\n.child__title {\n  text-align: center;\n  display: block;\n  color: #f23c4a;\n  font-size: 2rem;\n  font-weight: 600;\n  margin-bottom: 1rem;\n  font-family: \"M PLUS Rounded 1c\";\n  text-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.5);\n}\n\n.child__links {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 0.5rem;\n}\n\n.link {\n  color: #a6accd;\n  text-decoration: none;\n  font-size: 1.25rem;\n}\n\n.link:hover {\n  color: #ffffff;\n}\n\n.char-img {\n  position: absolute;\n  left: 0;\n  bottom: 0;\n  width: 30rem;\n  height: 30rem;\n  filter: drop-shadow(0 -1rem 1rem rgb(27, 31, 40, 0.25));\n  z-index: -1;\n  background-image: url(\"./ryuko.webp\");\n  background-size: cover;\n}\n"
  },
  {
    "path": "legacy/pages/startpage-v2/test.vue",
    "content": "<script>\nexport default {\n  data() {\n    return {\n      currentCount: 0\n    }\n  },\n  methods: {\n    decrement() {\n      this.currentCount -= 1\n    },\n    increment() {\n      this.currentCount += 1\n    }\n  }\n}\n</script>\n\n<template>\n  <div class=\"counter\">\n    <h2 class=\"counter-title\">Counter</h2>\n    <p class=\"counter-count\">{{ currentCount }}</p>\n    <button @click=\"increment\" class=\"counter-button\">Increment</button>\n    <button @click=\"decrement\" class=\"counter-button\">Decrement</button>\n  </div>\n</template>\n\n<style>\n.counter {\n  border: 10px solid #34495e;\n  background-color: #41b883;\n  color: white;\n  width: 50%;\n  margin: 0 auto;\n  padding: 2rem;\n  margin-bottom: 3rem;\n}\n.counter-button {\n  font-size: 1rem;\n  padding: 0.75rem;\n  margin: 0 0.5rem;\n}\n.counter-count {\n  font-size: 3rem;\n  font-weight: 700;\n  margin: 0;\n  padding: 1rem;\n}\n.counter-title {\n  font-size: 2rem;\n  margin: 0;\n}\n</style>\n"
  },
  {
    "path": "legacy/scripts/.scripts/extract",
    "content": "#!/bin/sh\n\n# Default behavior: Extract archive into new directory\n# Behavior with `-c` option: Extract contents into current directory\n\nwhile getopts \"hc\" o; do case \"${o}\" in\n  c) extracthere=\"True\" ;;\n  *) printf \"Options:\\\\n   -c: Extract archive into current directory rather than a new one.\\\\n\" && exit ;;\nesac done\n\nif [ -z \"$extracthere\" ]; then\n  archive=\"$(readlink -f \"$*\")\" &&\n  directory=\"$(echo \"$archive\" | sed 's/\\.[^\\/.]*$//')\" &&\n  mkdir -p \"$directory\" &&\n  cd \"$directory\" || exit\nelse\n  archive=\"$(readlink -f \"$(echo \"$*\" | cut -d' ' -f2)\")\"\nfi\n\n[ \"$archive\" = \"\" ] && printf \"Give archive to extract as argument.\\\\n\" && exit\n\nif [ -f \"$archive\" ] ; then\n  case \"$archive\" in\n    *.tar.bz2|*.tbz2) tar xvjf \"$archive\" ;;\n    *.tar.xz) tar -xf \"$archive\" ;;\n    *.tar.gz|*.tgz) tar xvzf \"$archive\" ;;\n    *.lzma) unlzma \"$archive\" ;;\n    *.bz2) bunzip2 \"$archive\" ;;\n    *.rar) unrar x -ad \"$archive\" ;;\n    *.gz) gunzip \"$archive\" ;;\n    *.tar) tar xvf \"$archive\" ;;\n    *.zip) unzip \"$archive\" ;;\n    *.Z) uncompress \"$archive\" ;;\n    *.7z) 7z x \"$archive\" ;;\n    *.xz) unxz \"$archive\" ;;\n    *.exe) cabextract \"$archive\" ;;\n    *) printf \"extract: '%s' - unknown archive method\\\\n\" \"$archive\" ;;\n  esac\nelse\n  printf \"File \\\"%s\\\" not found.\\\\n\" \"$archive\"\nfi\n"
  },
  {
    "path": "legacy/scripts/.scripts/launcher",
    "content": "#!/bin/sh\n\nLC_ALL=C rofi -no-lazy-grab -modi $1 -show $1 -theme ~/.config/rofi/themes/$2.rasi\n"
  },
  {
    "path": "legacy/tmux/.tmux.conf",
    "content": "#GENERAL\n  set -g default-terminal \"xterm-256color\"\n  set -ga terminal-overrides \",*:Tc\"\n  set -ga terminal-overrides '*:Ss=\\E[%p1%d q:Se=\\E[ q'\n  set -g escape-time 0\n  set -g focus-events on\n\n  set -g history-limit 100000\n  set -g history-file ~/.tmux/log/tmuxhistory\n\n  set -g monitor-activity off\n  set -g visual-activity off\n  setw -g monitor-bell off\n  set -g bell-action none\n\n  set -g set-clipboard on\n  setw -g mode-keys vi\n  setw -g wrap-search off\n\n# STATUS\n  set -g status-position bottom\n  set -g status on\n  set -g status-interval 5\n  set -g status-style \"fg=brightwhite, bg=default\"\n## Left\n  set -g status-left-length 40\n  set -g status-left \"#[fg=blue,bg=default]   #(whoami) #[fg=brightwhite, bg=default] \"\n## Center\n  set -g window-status-format \"#[fg=white,bg=default] #I #W \"\n  set -g window-status-current-format \"#[fg=blue,bg=default, bold] #I #W \"\n  set -g window-status-separator \"#[fg=brightblack,bg=default]|\"\n  set -g status-justify centre\n## Right\n  set -g status-right-length 40\n  set -g status-right \"#[fg=blue,bg=default]   #(lsb_release -d | cut -f 2) \"\n\n  set -g status-bg default\n\n# # PANEL\n#   set -g pane-border-status top\n#   set -g pane-border-style \"fg=black, bg=black\"\n#   set -ag pane-active-border-style \"fg=black, bg=black, bold\"\n#   set -g pane-border-format \"#[fg=blue,bg=black] #{pane_current_command} \"\n#   set -g pane-base-index 1\n#   set -g main-pane-width 1\n#   set -g main-pane-height 1\n#   set -g other-pane-width 1\n#   set -g other-pane-height 1\n\n# WINDOW\n  set -g base-index 1\n  set -g renumber-windows on\n  setw -g automatic-rename on\n\n# switch windows alt+number\n  bind-key -n M-1 select-window -t 1\n  bind-key -n M-2 select-window -t 2\n  bind-key -n M-3 select-window -t 3\n  bind-key -n M-4 select-window -t 4\n  bind-key -n M-5 select-window -t 5\n  bind-key -n M-6 select-window -t 6\n  bind-key -n M-7 select-window -t 7\n  bind-key -n M-8 select-window -t 8\n  bind-key -n M-9 select-window -t 9\n\n# OTHER'Slt\n  set -g set-titles on\n  set -g set-titles-string \"#{pane_current_command}\"\n  setw -g allow-rename on\n  setw -g mode-style \"fg=black, bg=brightblack\"\n\n## Clock mode\n  set -g clock-mode-colour white\n  set -g clock-mode-style 12\n\n## Message\n  set -g message-style \"fg=blue, bg=default\"\n\n# KEY BINDING\n###############################################################\n## Reload configuration\n  bind-key r source-file ~/.tmux.conf \\; display-message \"~/.tmux.conf reloaded\"\n\n## Split panel with same directory\n  unbind-key '\"'\n  unbind-key %\n  bind-key v split-window -h -c \"#{pane_current_path}\"\n  bind-key b split-window -v -c \"#{pane_current_path}\"\n\n## Switch panel\n  bind-key h select-pane -L\n  bind-key l select-pane -R\n  bind-key k select-pane -U\n  bind-key j select-pane -D\n\n## Resize panel\n  bind-key C-h resize-pane -L 2\n  bind-key C-l resize-pane -R 2\n  bind-key C-j resize-pane -D 1\n  bind-key C-k resize-pane -U 1\n\n# MOUSE\n  set -g mouse on\n  bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel \"xclip -selection clipboard\"\n  bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel \"xclip -in -selection clipboard\"\n"
  },
  {
    "path": "legacy/vscode/.config/Code/User/keybindings.json",
    "content": "// Place your key bindings in this file to override the defaultsauto[]\n[\n  {\n    \"key\": \"ctrl+j\",\n    \"command\": \"editor.action.joinLines\"\n  },\n  {\n    \"key\": \"ctrl+j\",\n    \"command\": \"-workbench.action.togglePanel\"\n  },\n  {\n    \"key\": \"ctrl+alt+t\",\n    \"command\": \"workbench.action.tasks.terminate\"\n  }\n]"
  },
  {
    "path": "legacy/vscode/.config/Code/User/settings.json",
    "content": "{\n  \"debug.console.fontFamily\": \"'BlexMono Nerd Font', monospace\",\n  \"diffEditor.renderSideBySide\": false,\n  \"editor.accessibilitySupport\": \"off\",\n  \"editor.bracketPairColorization.enabled\": false,\n  \"editor.detectIndentation\": true,\n  \"editor.fontFamily\": \"'BlexMono Nerd Font', monospace\",\n  \"editor.inlineSuggest.enabled\": true,\n  \"editor.lineHeight\": 1.6,\n  \"editor.linkedEditing\": true,\n  \"editor.minimap.enabled\": false,\n  \"editor.smoothScrolling\": true,\n  \"editor.suggestSelection\": \"first\",\n  \"editor.unicodeHighlight.ambiguousCharacters\": false,\n  \"editor.unicodeHighlight.invisibleCharacters\": true,\n  \"eslint.alwaysShowStatus\": true,\n  \"explorer.compactFolders\": false,\n  \"explorer.fileNesting.enabled\": true,\n  \"explorer.fileNesting.expand\": false,\n  \"explorer.fileNesting.patterns\": {\n    \".env\": \".env, .env.example\",\n    \".gitignore\": \".gitignore, .dockerignore\",\n    \"vite.config.ts\": \"vitest.config.js, vitest.config.ts, vite.config.js, vite.config.ts\",\n    \"tsconfig.json\": \"tsconfig.json, jsconfig.json\",\n    \"*.csproj\": \"*.csproj, nuget.config\",\n    \"*.razor\": \"$(capture).razor.css, $(capture).razor.cs\",\n    \"*.cshtml\": \"$(capture).cshtml.cs\"\n  },\n  \"editor.renderWhitespace\": \"boundary\",\n  \"extensions.autoUpdate\": \"onlyEnabledExtensions\",\n  \"extensions.ignoreRecommendations\": true,\n  \"files.insertFinalNewline\": true,\n  \"git.allowForcePush\": true,\n  \"git.branchProtection\": [\"master\", \"main\"],\n  \"git.enableCommitSigning\": true,\n  \"git.showPushSuccessNotification\": true,\n  \"git.suggestSmartCommit\": false,\n  \"gitlens.advanced.abbreviatedShaLength\": 8,\n  \"gitlens.codeLens.enabled\": false,\n  \"gitlens.currentLine.enabled\": false,\n  \"gitlens.hovers.currentLine.over\": \"line\",\n  \"gitlens.keymap\": \"alternate\",\n  \"javascript.referencesCodeLens.enabled\": true,\n  \"notebook.lineNumbers\": \"on\",\n  \"notebook.output.textLineLimit\": 0,\n  \"omnisharp.enableImportCompletion\": true,\n  \"prettier.endOfLine\": \"auto\",\n  \"security.workspace.trust.enabled\": false,\n  \"svelte.enable-ts-plugin\": true,\n  \"telemetry.telemetryLevel\": \"off\",\n  \"terminal.integrated.fontWeight\": 400,\n  \"terminal.integrated.persistentSessionReviveProcess\": \"never\",\n  \"terminal.integrated.tabs.enabled\": true,\n  \"typescript.implementationsCodeLens.enabled\": true,\n  \"typescript.referencesCodeLens.enabled\": true,\n  \"typescript.updateImportsOnFileMove.enabled\": \"always\",\n  \"window.customMenuBarAltFocus\": false,\n  \"window.dialogStyle\": \"custom\",\n  \"window.titleBarStyle\": \"custom\",\n  \"workbench.editor.closeOnFileDelete\": true,\n  \"workbench.iconTheme\": \"material-icon-theme\",\n  \"workbench.list.smoothScrolling\": true,\n  \"workbench.preferredDarkColorTheme\": \"Default Dark+\",\n  \"workbench.preferredLightColorTheme\": \"Default Light+\",\n  \"workbench.sideBar.location\": \"right\",\n  \"workbench.startupEditor\": \"none\",\n  \"workbench.tree.indent\": 14,\n  \"workbench.tree.renderIndentGuides\": \"always\",\n  \"prettier.documentSelectors\": [\"**/*.astro\", \"**/*.svg\", \"**/*.svelte\"],\n  // \"typescript.tsserver.experimental.enableProjectDiagnostics\": true\n\n  \"gopls\": {\n    \"ui.semanticTokens\": true\n  },\n  \"go.coverageDecorator\": {\n    \"type\": \"gutter\",\n    \"coveredHighlightColor\": \"rgba(64,128,128,0.5)\",\n    \"uncoveredHighlightColor\": \"rgba(128,64,64,0.25)\",\n    \"coveredGutterStyle\": \"blockgreen\",\n    \"uncoveredGutterStyle\": \"blockred\"\n  },\n  \"go.coverOnSingleTest\": true,\n  // \"go.coverOnSave\": true,\n  // \"javascript.preferences.importModuleSpecifierEnding\": \"js\",\n  // \"typescript.preferences.importModuleSpecifierEnding\": \"js\",\n\n  // override some UI colours\n  \"workbench.colorCustomizations\": {\n    \"[Default Dark+]\": {\n      \"activityBar.background\": \"#15161f\",\n      \"activityBarBadge.background\": \"#007ACC\",\n      \"checkbox.background\": \"#0d0e13\",\n      \"checkbox.border\": \"#252735\",\n      \"dropdown.background\": \"#0d0e13\",\n      \"dropdown.border\": \"#0d0e13\",\n      \"editor.background\": \"#0D1017\",\n      \"editor.foreground\": \"#c7c9d1\",\n      \"editor.hoverHighlightBackground\": \"#ef835422\",\n      \"editor.lineHighlightBackground\": \"#171922\",\n      \"editor.selectionBackground\": \"#2a388865\",\n      \"editor.wordHighlightBackground\": \"#ffffff22\",\n      \"editor.wordHighlightStrongBackground\": \"#6e81a022\",\n      \"editorGroupHeader.tabsBackground\": \"#212431\",\n      \"editorGutter.addedBackground\": \"#a7da1e\",\n      \"editorGutter.deletedBackground\": \"#e61f44\",\n      \"editorGutter.modifiedBackground\": \"#f7b83d\",\n      \"editorIndentGuide.activeBackground\": \"#3f4157\",\n      \"editorIndentGuide.background\": \"#252735\",\n      \"editorLink.activeForeground\": \"#007ACC\",\n      \"editorOverviewRuler.addedForeground\": \"#a7da1e\",\n      \"editorOverviewRuler.deletedForeground\": \"#e61f44\",\n      \"editorOverviewRuler.errorForeground\": \"#e61f44\",\n      \"editorOverviewRuler.findMatchForeground\": \"#6e81a055\",\n      \"editorOverviewRuler.infoForeground\": \"#9d37fc\",\n      \"editorOverviewRuler.modifiedForeground\": \"#f7b83d\",\n      \"editorOverviewRuler.warningForeground\": \"#f7b83d\",\n      \"editorSuggestWidget.foreground\": \"#c7c9d1\",\n      \"editorSuggestWidget.highlightForeground\": \"#007ACC\",\n      \"editorSuggestWidget.selectedBackground\": \"#3a3e56\",\n      \"editorSuggestWidget.selectedForeground\": \"#c7c9d1\",\n      \"editorWidget.background\": \"#262837\",\n      \"editorWidget.border\": \"#3a3e56\",\n      \"editorWidget.foreground\": \"#c7c9d1\",\n      \"input.background\": \"#07070a\",\n      \"input.border\": \"#303347\",\n      \"input.foreground\": \"#ffffff\",\n      \"input.placeholderForeground\": \"#363950\",\n      \"list.activeSelectionBackground\": \"#3a3e56\",\n      \"list.activeSelectionForeground\": \"#c7c9d1\",\n      \"list.dropBackground\": \"#252735\",\n      \"list.focusBackground\": \"#303347\",\n      \"list.hoverBackground\": \"#171922\",\n      \"list.inactiveSelectionBackground\": \"#111219\",\n      \"menu.background\": \"#262837\",\n      \"menu.foreground\": \"#c7c9d1\",\n      \"menu.selectionBackground\": \"#3a3e56\",\n      \"notifications.foreground\": \"#ffffff\",\n      \"panel.background\": \"#15161f\",\n      \"panel.border\": \"#3a3e56\",\n      \"panelTitle.inactiveForeground\": \"#767ca2\",\n      \"peekView.border\": \"#007ACC\",\n      \"peekViewEditor.background\": \"#09090d\",\n      \"peekViewEditor.matchHighlightBackground\": \"#6e81a055\",\n      \"peekViewEditorGutter.background\": \"#0d0e13\",\n      \"peekViewResult.background\": \"#1b1d28\",\n      \"peekViewResult.fileForeground\": \"#f5f5f5\",\n      \"peekViewResult.lineForeground\": \"#e6e6e6\",\n      \"peekViewResult.matchHighlightBackground\": \"#6e81a055\",\n      \"peekViewResult.selectionBackground\": \"#15161f\",\n      \"peekViewResult.selectionForeground\": \"#ffffff\",\n      \"peekViewTitle.background\": \"#0d0e13\",\n      \"peekViewTitleDescription.foreground\": \"#f7f7f7\",\n      \"peekViewTitleLabel.foreground\": \"#ffffff\",\n      \"scrollbar.shadow\": \"#000000\",\n      \"sideBar.background\": \"#1b1d28\",\n      \"sideBarSectionHeader.background\": \"#222331\",\n      \"statusBar.background\": \"#2a2b3c\",\n      \"statusBar.debuggingBackground\": \"#5b6d8b\",\n      \"tab.inactiveBackground\": \"#212431\",\n      \"titleBar.activeBackground\": \"#09090d\",\n      \"tree.indentGuidesStroke\": \"#3f4157\"\n    }\n  },\n\n  // distinguish enum, interface, and struct\n  \"editor.semanticTokenColorCustomizations\": {\n    \"[Default Dark+]\": {\n      \"rules\": {\n        \"enum\": \"#b8d7a3\",\n        \"interface\": \"#b8d7a3\",\n        \"struct\": \"#86C691\",\n        \"type\": \"#86C691\"\n      }\n    }\n  },\n\n  // enable italic comments\n  \"editor.tokenColorCustomizations\": {\n    \"textMateRules\": [\n      {\n        \"scope\": \"comment\",\n        \"settings\": {\n          \"fontStyle\": \"italic\"\n        }\n      }\n    ]\n  },\n\n  // exclude from fuzzy finder\n  \"search.exclude\": {\n    \"**/.git\": true,\n    \"**/.nuxt\": true,\n    \"**/.svelte-kit\": true,\n    \"**/.output\": true,\n    \"**/.pnpm\": true,\n    \"**/.vscode\": true,\n    \"**/.yarn\": true,\n    \"**/bower_components\": true,\n    \"**/dist/**\": true,\n    \"**/logs\": true,\n    \"**/node_modules\": true,\n    \"**/out/**\": true,\n    \"**/package-lock.json\": true,\n    \"**/pnpm-lock.yaml\": true,\n    \"**/tmp\": true,\n    \"**/yarn.lock\": true,\n    \"**/bin\": true,\n    \"**/obj\": true\n  },\n\n  // languages\n  \"files.associations\": {\n    \"*.svx\": \"markdown\",\n    \".env*\": \"makefile\",\n    \"*.svg\": \"html\"\n  },\n  \"[svelte]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[jsonc]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"vscode.json-language-features\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[html]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[astro]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"latex-workshop.latex.tools\": [\n    {\n      \"name\": \"latexmk\",\n      \"command\": \"latexmk\",\n      \"args\": [\n        \"--shell-escape\",\n        \"-synctex=1\",\n        \"-interaction=nonstopmode\",\n        \"-file-line-error\",\n        \"-pdf\",\n        \"-outdir=%OUTDIR%\",\n        \"%DOC%\"\n      ]\n    },\n    {\n      \"name\": \"pdflatex\",\n      \"command\": \"pdflatex\",\n      \"args\": [\n        \"--shell-escape\",\n        \"-synctex=1\",\n        \"-interaction=nonstopmode\",\n        \"-file-line-error\",\n        \"%DOC%\"\n      ]\n    },\n    {\n      \"name\": \"bibtex\",\n      \"command\": \"bibtex\",\n      \"args\": [\"%DOCFILE%\"]\n    }\n  ],\n\n  // python stuff\n  \"python.formatting.autopep8Path\": \"/usr/bin/autopep8\",\n  \"python.formatting.autopep8Args\": [\"--max-line-length\", \"120\"],\n  \"mocha.requires\": [\"ts-node/register\"],\n  \"mocha.files.ignore\": \"test/fixtures/**/*.js\",\n  \"calva.showCalvaSaysOnStart\": false,\n  \"calva.paredit.defaultKeyMap\": \"strict\",\n\n  // rust analyzer\n  \"rust-client.engine\": \"rust-analyzer\",\n  \"rust.clippy_preference\": \"on\",\n  \"rust-analyzer.inlayHints.enable\": false,\n\n  // laravel stuff\n  \"blade.format.enable\": true,\n  \"dart.flutterSdkPath\": \"/home/elianiva/Dev/android/flutter\",\n  \"color-highlight.languages\": [\n    \"javascript\",\n    \"typescript\",\n    \"javascriptreact\",\n    \"typescriptreact\"\n  ],\n  \"[css]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[less]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[handlebars]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[scss]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"editor.semanticHighlighting.enabled\": true,\n  \"todo-tree.highlights.useColourScheme\": true,\n  \"todo-tree.filtering.useBuiltInExcludes\": \"file excludes\",\n  \"todo-tree.tree.showCountsInTree\": true,\n  \"typescript.tsserver.log\": \"off\",\n  \"workbench.editorAssociations\": {\n    \"*.pdf\": \"latex-workshop-pdf-hook\"\n  },\n  \"latex-workshop.latex.verbatimEnvs\": [\n    \"verbatim\",\n    \"lstlisting\",\n    \"minted\",\n    \"codeblock-java\"\n  ],\n  \"svelte.plugin.svelte.note-new-transformation\": false,\n  \"rust-analyzer.inlayHints.typeHints.enable\": false,\n  \"rust-analyzer.inlayHints.closingBraceHints.enable\": false,\n  \"rust-analyzer.inlayHints.chainingHints.enable\": false,\n  \"rust-analyzer.inlayHints.parameterHints.enable\": false,\n  \"rpc.appName\": \"Visual Studio Code\"\n  // \"typescript.tsserver.experimental.enableProjectDiagnostics\": true\n}\n"
  },
  {
    "path": "misc/.bashrc",
    "content": "# if [ \"$TERM_PROGRAM\" != \"vscode\" ]; then\n#   if [[ $(/bin/ps --no-header --pid=$PPID --format=comm) != \"fish\" && -z ''${BASH_EXECUTION_STRING} ]]\n#   then\n#     shopt -q login_shell && LOGIN_OPTION='--login' || LOGIN_OPTION=\"\"\n#     exec $(which fish) $LOGIN_OPTION\n#   fi\n# fi\n\n# Source global definitions\nif [ -f /etc/bashrc ]; then\n        . /etc/bashrc\nfi\n\n# User specific environment\nif ! [[ \"$PATH\" =~ \"$HOME/.local/bin:$HOME/bin:\" ]]\nthen\n    PATH=\"$HOME/.local/bin:$HOME/bin:$PATH\"\nfi\nexport PATH\n\n\n# Added by LM Studio CLI (lms)\nexport PATH=\"$PATH:/Users/elianiva/.lmstudio/bin\"\n# End of LM Studio CLI section\n\n"
  },
  {
    "path": "misc/.profile",
    "content": "#!/bin/sh\n\nexport EDITOR=\"nvim\"\nexport CC=\"gcc\"\nexport FZF_DEFAULT_COMMAND='rg --files --no-ignore --ignore-file \".gitignore\"'\nexport SKIM_DEFAULT_COMMAND='rg --files --no-ignore --ignore-file \".gitignore\"'\nexport QT_QPA_PLATFORMTHEME=\"qt5ct\"\nexport QT_AUTO_SCREEN_SCALE_FACTOR=0\nexport QT_FONT_DPI=80\nexport GTK2_RC_FILES=\"$HOME/.gtkrc-2.0\"\nexport GOPATH=\"$HOME/.local/go\"\nexport GOBIN=\"$HOME/.local/go/bin\"\nexport NODE_COMPILE_CACHE=\"$HOME/.cache/nodejs-compile-cache\"\n# export XDG_DATA_DIRS=\"$HOME/.nix-profile/share:/usr/share:/usr/local/share:$HOME/.local/share:$XDG_DATA_DIRS\"\n\n# pnpm\nexport PNPM_HOME=\"/Users/elianiva/Library/pnpm\"\ncase \":$PATH:\" in\n  *\":$PNPM_HOME:\"*) ;;\n  *) export PATH=\"$PNPM_HOME:$PATH\" ;;\nesac\n\n# PATH stuff\nexport PATH=\"$HOME/.local/bin:$PATH\"\nexport PATH=\"$HOME/.deno/bin:$PATH\"\nexport PATH=\"$HOME/.bun/bin:$PATH\"\nexport PATH=\"$HOME/.config/composer/vendor/bin:$PATH\"\nexport PATH=\"$HOME/.local/go/bin:$PATH\"\nexport PATH=\"$HOME/.pub-cache/bin:$PATH\"\nexport PATH=\"$HOME/.pub-cache/bin:$PATH\"\nexport PATH=\"$HOME/.cargo/bin:$PATH\"\nexport PATH=\"/opt/homebrew/bin:$PATH\"\n# radicle stuff\nexport PATH=\"$HOME/.radicle/bin:$PATH\"\n# export PATH=\"$HOME/.dotnet/tools:$PATH\"\n\n# export JAVA_OPTS=\"-XX:+IgnoreUnrecognizedVMOptions\"\n# export JAVA_HOME=\"/usr/lib/jvm/java-8-openjdk\"\n\n# required if java is installed from nix\n# unset JAVA_OPTS\n# export _JAVA_OPTIONS=\"-Dawt.useSystemAAFontSettings=on -Dswing.aatext=true\"\n# export _JAVA_AWT_WM_NONREPARENTING=1\n\n# Flutter stuff\n# export FLUTTER_ROOT=\"$ANDROID_HOME/flutter\"\nexport ANDROID_HOME=\"$HOME/Library/Android/sdk\"\nexport PATH=\"$ANDROID_HOME/cmdline-tools/latest/bin:$PATH\"\nexport PATH=\"$ANDROID_HOME/platform-tools/:$PATH\"\nexport PATH=\"$ANDROID_HOME/emulator:$PATH\"\n# export PATH=\"$ANDROID_HOME/flutter/bin:$PATH\"\n# export CHROME_EXECUTABLE=\"/usr/bin/brave\"\n\n# Fcitx5 Stuff\nexport GLFW_IM_MODULE=\"ibus\"\nexport GTK_IM_MODULE=\"fcitx\"\nexport QT_IM_MODULE=\"fcitx\"\nexport XMODIFIERS=\"@im=fcitx\"\nexport SDL_IM_MODULE=\"fcitx\"\nexport IBUS_USE_PORTAL=1\n\n# Tidy up stuff\nexport LESSHISTFILE=\"${XDG_CONFIG_HOME}less/history\"\nexport LESSKEY=\"${XDG_CONFIG_HOME}less/keys\"\nexport ICEAUTHORITY=\"${XDG_CACHE_HOME}ICEauthority\"\n\n# gpg stuff\nexport GPG_TTY=$(tty)\n\n# vite env stuff\n. \"$HOME/.vite-plus/env\"\n\n"
  },
  {
    "path": "modules/brews.nix",
    "content": "_:\n\n[\n  # Development Tools\n  \"mysql-client\"\n  \"cloudflared\"\n  \"rtk\"\n\n  # Google Workspace CLI\n  \"gogcli\"\n]\n"
  },
  {
    "path": "modules/casks.nix",
    "content": "_:\n\n[\n  # Development Tools\n  \"visual-studio-code\"\n  \"cursor\"\n  \"zed\"\n\n  # Communication Tools\n  \"zoom\"\n  \"telegram\"\n  \"vesktop\"\n\n  # Productivity Tools\n  \"raycast\"\n  \"orbstack\"\n  # \"zathura\"\n\n  # Terminals\n  \"ghostty\"\n  # Window Manager\n  \"omniwm\"\n\n\n  # Tools\n  \"bitwarden\"\n  \"battery-toolkit\"\n  \"stats\"\n  \"jordanbaird-ice@beta\"\n  \"qbittorrent\"\n  \"blip\"\n  \"iina\"\n\n  # Creative\n  \"medibangpaintpro\"\n  \"obs\"\n]\n"
  },
  {
    "path": "modules/darwin-config.nix",
    "content": "{ pkgs, flakePkgs, fenix, ... }:\n\nlet\n  user = \"elianiva\";\n  hostname = \"melon\";\nin\n{\n    system.stateVersion = 6;\n\n    networking.hostName = \"${hostname}\";\n    networking.localHostName = \"${hostname}\";\n\n    users.users.${user} = {\n      name = \"${user}\";\n      home = \"/Users/${user}\";\n    };\n\n    # allow unfree packages\n    nixpkgs = {\n      config = {\n        allowUnfree = true;\n      };\n    };\n\n    # setup nix\n    nix.enable = false;\n\n    homebrew = {\n      enable = true;\n      brews = pkgs.callPackage ./brews.nix { };\n      casks = pkgs.callPackage ./casks.nix { };\n      caskArgs = {\n        appdir = \"~/Applications\";\n        require_sha = true;\n      };\n      onActivation = {\n        autoUpdate = true;\n        upgrade = true;\n        cleanup = \"zap\";\n        extraFlags = [ \"--verbose\" ];\n      };\n      global = {\n        brewfile = true;\n      };\n    };\n\n    environment.systemPackages = import ./darwin-packages.nix { inherit pkgs flakePkgs fenix; };\n    fonts.packages = with pkgs; [ monaspace inter lora lilex departure-mono iosevka ];\n\n    system.primaryUser = user;\n\n    # enable touchid for sudo\n    security.pam.services.sudo_local.touchIdAuth = true;\n\n    # keyboard\n    system.keyboard.enableKeyMapping = true;\n    system.keyboard.remapCapsLockToEscape = true;\n\n     # docks\n    system.defaults.dock = {\n      autohide = true;\n      mineffect = \"scale\";\n      magnification = true;\n      show-recents = false;\n      persistent-apps = [\n        \"/Users/${user}/Applications/Ghostty.app\"\n      ];\n      appswitcher-all-displays = true;\n    };\n\n    # misc settings\n    system.defaults.NSGlobalDomain = {\n      # Repeat character while key held instead of showing character accents menu\n      ApplePressAndHoldEnabled = false;\n\n      # fastest possible key repeat with minimum delay\n      InitialKeyRepeat = 15;\n      KeyRepeat = 2;\n\n      # turn off font smoothing\n      AppleFontSmoothing = 0;\n\n      NSAutomaticCapitalizationEnabled = false;\n      NSAutomaticSpellingCorrectionEnabled = false;\n\n      # faster trackpad speed\n      \"com.apple.trackpad.scaling\" = 2.0;\n\n      # enable forceclick to show definition\n      \"com.apple.trackpad.forceClick\" = true;\n    };\n\n    system.defaults.finder = {\n      AppleShowAllExtensions = true;\n      CreateDesktop = false;\n      FXDefaultSearchScope = \"SCcf\";\n      FXPreferredViewStyle = \"clmv\";\n      FXRemoveOldTrashItems = true;\n      _FXSortFoldersFirst = true;\n      ShowPathbar = true;\n    };\n\n    system.defaults.screencapture = {\n      disable-shadow = false;\n      location = \"~/Pictures/Screenshots\";\n    };\n\n    system.defaults.trackpad = {\n      Clicking = true;\n      TrackpadThreeFingerDrag = true;\n    };\n\n    system.defaults.hitoolbox.AppleFnUsageType = \"Change Input Source\";\n\n    # ads\n    system.defaults.CustomUserPreferences.\"com.apple.AdLib\" = {\n      allowApplePersonalizedAdvertising = false;\n      allowIdentifierForAdvertising = false;\n    };\n\n    # disable power chime sound\n    system.defaults.CustomUserPreferences.\"com.apple.PowerChime\".ChimeOnNoHardware = false;\n}\n"
  },
  {
    "path": "modules/darwin-home.nix",
    "content": "{ lib, pkgs, inputs, flakePkgs, config, ... }:\nlet\n  appConfig = \"Library/Application Support\";\n  link = config.lib.file.mkOutOfStoreSymlink;\n  dotfiles = \"${config.home.homeDirectory}/.dotfiles\";\nin\n{\n  imports = [ ./home-common.nix ];\n\n  programs.direnv = {\n    enableFishIntegration = false;\n    enableBashIntegration = false;\n    enableNushellIntegration = true;\n    enableZshIntegration = false;\n  };\n\n  home.file = {\n    \"${appConfig}/nushell\" = {\n      source = link \"${dotfiles}/nushell\";\n      recursive = true;\n    };\n    \"${appConfig}/com.mitchellh.ghostty\" = {\n      source = link \"${dotfiles}/ghostty\";\n      recursive = true;\n    };\n  };\n}"
  },
  {
    "path": "modules/darwin-packages.nix",
    "content": "{ pkgs, flakePkgs, fenix, ... }:\n\nwith pkgs;\nlet shared-packages = import ./packages.nix { inherit pkgs flakePkgs fenix; }; in\nshared-packages ++ [\n  nushell\n  devbox\n  jj-starship\n]\n"
  },
  {
    "path": "modules/git.nix",
    "content": "{ pkgs, config, ... }:\nlet\n  email = \"git@elianiva.my.id\";\n  name = \"elianiva\";\nin\n{\n  programs.gh.enable = true;\n  programs.lazygit = {\n    enable = true;\n    settings = {\n      gui.theme = {\n        lightTheme = true;\n      };\n      git.pagers = [\n        {\n          colorArg = \"always\";\n          useConfig = true;\n          externalDiffCommand = \"difft\";\n        }\n      ];\n      git.log.order = \"default\";\n    };\n  };\n  programs.git = {\n    enable = true;\n    settings = {\n      user = {\n        name = \"${name}\";\n        email = \"${email}\";\n      };\n      credential.helper = \"cache --timeout 86400\";\n      core = {\n        compression = 9;\n        editor = \"nvim\";\n      };\n      diff.external = \"difft\";\n      pull.rebase = false;\n      commit.gpgsign = true;\n      gpg.format = \"ssh\";\n      # use this command to generate the file\n      # echo \"$(git config --get user.email) namespaces=\\\"git\\\" $(cat ~/.ssh/<MY_KEY>.pub)\" >> ~/.ssh/allowed_signers\n      gpg.ssh.allowedSignersFile = \"${config.home.homeDirectory}/.ssh/allowed_signers\";\n      user.signingkey = \"~/.ssh/id_ed25519.pub\";\n      alias = {\n        lg = \"log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative\";\n        c = \"commit -S -m\";\n        ca = \"commit -S --amend\";\n      };\n      # Why?\n      # - ghq default is https, this omit -p option for the ssh push\n      # - https://blog.n-z.jp/blog/2013-11-28-git-insteadof.html\n      url = {\n        \"git@github.com:\" = {\n          pushInsteadOf = [\n            \"git://github.com/\"\n            \"https://github.com/\"\n          ];\n        };\n      };\n      # delta = {\n      #   line-numbers = true;\n      #   syntax-theme = \"base16\";\n      #   side-by-side = false;\n      #   file-modified-label = \"modified:\";\n      #   light = true;\n      # };\n      difftastic = {\n        background = \"light\";\n      };\n      init.defaultBranch = \"master\";\n    };\n  };\n\n  programs.jjui = {\n    enable = true;\n  };\n\n  programs.jujutsu = {\n    enable = true;\n    settings = {\n      user = {\n        email = \"${email}\";\n        name = \"${name}\";\n      };\n      signing = {\n        behavior = \"own\";\n        backend = \"ssh\";\n        key = \"~/.ssh/id_ed25519\";\n        backends.ssh.allowed-signers = \"${config.home.homeDirectory}/.ssh/allowed_signers\";\n      };\n      revsets = {\n        log = \"::\"; # show all commits by default\n      };\n      aliases ={\n        # Bring nearest bookmark up to recent commit\n        tug = [\"bookmark\" \"move\" \"--from\" \"heads(::@- & bookmarks())\" \"--to\" \"@-\"];\n        # Retrunk:\n        # `jj rebase -d 'trunk()' is shorthand for `jj rebase -b @ -d 'trunk()'`\n        # What it does:\n        # `-b @` rebases the entire branch that the current @ is on relative to the destination\n        # `-d trunk()` sets the destination. trunk() finds the most recent `main | master | whatever main branch`\n        retrunk = [\"rebase\" \"-b\" \"@\" \"-d\" \"trunk()\"];\n        # Logs last 10 revisions\n        lg = [\"log\" \"-r\" \"all()\" \"-n\" \"10\"];\n        # Compare current revision with the previous one\n        compare = [\"diff\" \"--from\" \"@-\" \"--to\" \"@\" \"--git\"];\n      };\n      ui = {\n        paginate = \"never\";\n        conflict-marker-style = \"git\";\n        default-command = \"log\";\n        diff-formatter = [\n          \"difft\"\n          # it's bad, better to disable it since it causes confusion\n          # see: https://github.com/Wilfred/difftastic/issues/275\n          \"--syntax-highlight=off\"\n          \"--color=always\"\n          \"--display=side-by-side-show-both\"\n          \"$left\"\n          \"$right\"\n        ];\n      };\n    };\n  };\n}\n"
  },
  {
    "path": "modules/gpg.nix",
    "content": "{ config, pkgs, lib, ... }:\n\n# ## FAQ - GPG\n#\n# - How to list keys?\n#   - 1. `gpg --list-secret-keys --keyid-format=long` # The `sec` first section displays same text as `pub` by `gpg --list-keys --keyid-format=long`\n# - How to add subkey?\n#   - 1. `gpg --edit-key PUBKEY`\n#   - 2. `addkey`\n#   - 3. `save`\n# - How to revoke subkey?\n#   - 1. `gpg --edit-key PUBKEY`\n#   - 2. `key n` n is the index of subkey\n#   - 3. `revkey`\n#   - 4. `save`\n#   - 5. Replace uploaded pubkey with new one, see https://github.com/kachick/dotfiles/pull/311#issuecomment-1715812324 for detail\n# - How to get pubkey to upload?\n#   - `gpg --armor --export PUBKEY | clip.exe`\n# - How to backup private key?\n#   - `gpg --export-secret-keys --armor > gpg-private.keys.bak`\nlet\n  # All gpg-agent timeouts numbers should be specified with the `seconds`\n  day = 60 * 60 * 24;\nin\n{\n  # prevents gnome sabotaging gpg-agent by disabling all of these services\n  # services.gnome-keyring.enable = lib.mkForce false;\n\n  # https://github.com/nix-community/home-manager/blob/release-24.05/modules/services/gpg-agent.nix\n  services.gpg-agent = {\n    enable = true;\n\n    # https://superuser.com/questions/624343/keep-gnupg-credentials-cached-for-entire-user-session\n    defaultCacheTtl = day * 1;\n    # https://github.com/openbsd/src/blob/862f3f2587ccb85ac6d8602dd1601a861ae5a3e8/usr.bin/ssh/ssh-agent.1#L167-L173\n    # ssh-agent sets it as infinite by default. So I can relax here (maybe)\n    defaultCacheTtlSsh = day * 30;\n    maxCacheTtl = day * 7;\n    pinentry.package =\n      if pkgs.stdenv.isLinux then pkgs.pinentry-gnome3\n      else if pkgs.stdenv.isDarwin then pkgs.pinentry_mac\n      else pkgs.pinentry-curses;\n    enableSshSupport = true;\n    enableFishIntegration = false;\n    extraConfig = ''\n      allow-loopback-pinentry\n    '';\n  };\n\n  # https://github.com/nix-community/home-manager/blob/release-24.05/modules/programs/gpg.nix\n  programs.gpg = {\n    enable = true;\n\n    # - How to read `--list-keys` - https://unix.stackexchange.com/questions/613839/help-understanding-gpg-list-keys-output\n    settings = {\n      # https://unix.stackexchange.com/questions/339077/set-default-key-in-gpg-for-signing\n      # default-key = \"<UPDATE_ME_IN_FLAKE>\";\n      personal-digest-preferences = \"SHA512\";\n      pinentry-mode = \"default\";\n      use-agent = true;\n    };\n  };\n\n  # https://github.com/nix-community/home-manager/blob/release-24.05/modules/programs/password-store.nix\n  programs.password-store = {\n    enable = true;\n  };\n}\n"
  },
  {
    "path": "modules/home-common.nix",
    "content": "{ pkgs, config, ... }:\nlet\n  pi = \".pi/agent\";\n  link = config.lib.file.mkOutOfStoreSymlink;\n  dotfiles = \"${config.home.homeDirectory}/.dotfiles\";\nin\n{\n  home = {\n    # don't change this, see: https://nix-community.github.io/home-manager/\n    stateVersion = \"25.11\";\n  };\n\n  programs = {\n    # let home manager manages itself\n    home-manager.enable = true;\n\n    # nix-direnv\n    direnv = {\n      enable = true;\n      nix-direnv.enable = true;\n      stdlib = builtins.readFile ../direnv/direnvrc;\n\n      package = pkgs.direnv.overrideAttrs (old: {\n        doCheck = false;\n      });\n    };\n\n    bat = {\n      enable = true;\n      config = {\n        theme = \"base16\";\n        \"italic-text\" = \"always\";\n        style = \"numbers\";\n      };\n    };\n\n    btop = {\n      enable = true;\n      settings = {\n        color_theme = \"TTY\";\n      };\n    };\n\n    fzf = {\n      enable = true;\n      # https://github.com/junegunn/fzf/blob/d579e335b5aa30e98a2ec046cb782bbb02bc28ad/README.md#respecting-gitignore\n      defaultCommand = \"${pkgs.fd}/bin/fd --type f --strip-cwd-prefix --hidden --follow --exclude .git\";\n      defaultOptions = [\n        # --walker*: Default file filtering will be changed by this option if FZF_DEFAULT_COMMAND is not set: https://github.com/junegunn/fzf/pull/3649/files\n        \"--walker-skip '.git,node_modules,.direnv,vendor,dist'\"\n      ];\n    };\n\n    # completion\n    carapace.enable = true;\n    carapace.enableNushellIntegration = true;\n\n    zoxide = {\n      enable = true;\n      enableNushellIntegration = false; # managed manually in nushell/zoxide.nu\n    };\n\n    starship = {\n      enable = true;\n      settings = {\n        add_newline = true;\n        directory.truncation_length = 8;\n        git_status.format = \"([\\($all_status$ahead_behind\\)]($style) )\";\n        git_status.ahead = \"⇡$\\{count\\}\";\n        git_status.behind = \"⇣$\\{count\\}\";\n        git_status.diverged = \"⇕⇡$\\{ahead_count\\} ⇣$\\{behind_count\\}\";\n        package.disabled = true;\n        golang.format = \"via [ $version](bold blue) \";\n        gcloud.disabled = true;\n        custom.jj = {\n          when = \"jj-starship detect\";\n          shell = [\"jj-starship\"];\n          format = \"$output \";\n        };\n      };\n    };\n  };\n\n  xdg.configFile = {\n    # fish produces its own config file which causes conflict\n    \"fish/config.fish\".enable = false;\n    \"fastfetch\" = {\n      source = link \"${dotfiles}/fastfetch\";\n      recursive = true;\n    };\n    \"zellij\" = {\n      source = link \"${dotfiles}/zellij\";\n      recursive = true;\n    };\n    \"nvim\" = {\n      source = link \"${dotfiles}/nvim\";\n      recursive = true;\n    };\n    \"helix\" = {\n      source = link \"${dotfiles}/helix\";\n      recursive = true;\n    };\n    \"fish\" = {\n      source = link \"${dotfiles}/fish\";\n      recursive = true;\n    };\n    \"yazi\" = {\n      source = link \"${dotfiles}/yazi\";\n      recursive = true;\n    };\n    \"jjui\" = {\n      source = link \"${dotfiles}/jjui\";\n      recursive = true;\n    };\n\n    # opencode configs\n    \"opencode/opencode.json\".source = link \"${dotfiles}/agents/opencode/opencode.json\";\n    \"opencode/AGENTS.md\".source = link \"${dotfiles}/agents/AGENTS.md\";\n    \"opencode/skills\" = {\n      source = link \"${dotfiles}/agents/skills\";\n      recursive = true;\n    };\n  };\n\n  home.file = {\n    # shell configs\n    \".profile\".source = link \"${dotfiles}/misc/.profile\";\n    \".bashrc\".source = link \"${dotfiles}/misc/.bashrc\";\n\n    # pi coding agent related configs\n    \"${pi}/AGENTS.md\".source = link \"${dotfiles}/agents/AGENTS.md\";\n    \"${pi}/settings.json\".source = link \"${dotfiles}/agents/pi/settings.json\";\n    \"${pi}/mcp.json\".source = link \"${dotfiles}/agents/pi/mcp.json\";\n    \"${pi}/models.json\".source = link \"${dotfiles}/agents/pi/models.json\";\n    \"${pi}/package.json\".source = link \"${dotfiles}/agents/pi/package.json\";\n    \"${pi}/skills\" = {\n      source = link \"${dotfiles}/agents/skills\";\n      recursive = true;\n    };\n    \"${pi}/extensions\" = {\n      source = link \"${dotfiles}/agents/pi/extensions\";\n      recursive = true;\n    };\n    \"${pi}/themes\" = {\n      source = link \"${dotfiles}/agents/pi/themes\";\n      recursive = true;\n    };\n  };\n}\n"
  },
  {
    "path": "modules/linux-home.nix",
    "content": "{ lib, pkgs, inputs, flakePkgs, config, ... }:\nlet\n  nixGLIntel = inputs.nixGL.packages.\"${pkgs.system}\".nixGLIntel;\n  link = config.lib.file.mkOutOfStoreSymlink;\n  dotfiles = \"${config.home.homeDirectory}/.dotfiles\";\nin\n{\n  imports = [ ./home-common.nix ];\n\n  targets.genericLinux.enable = true;\n\n  # allow unfree packages\n  nixpkgs.config.allowUnfree = true;\n\n  targets.genericLinux.nixGL = {\n    packages = inputs.nixGL.packages;\n    defaultWrapper = \"mesa\";\n    installScripts = [ \"mesa\" ];\n  };\n\n  nix = {\n    enable = true;\n    package = pkgs.nixVersions.stable;\n  };\n\n  home = {\n    packages = import ./linux-packages.nix { inherit pkgs flakePkgs nixGLIntel; };\n\n    username = \"elianiva\";\n    homeDirectory = \"/home/elianiva\";\n  };\n\n  # enable fontconfig\n  fonts.fontconfig = {\n    enable = true;\n    defaultFonts = {\n      monospace = [ \"JetBrainsMono\" ];\n      sansSerif = [ \"Inter\" ];\n    };\n  };\n\n  programs.nushell.enable = true;\n\n  # nushell produces its own config file which causes conflict\n  xdg.configFile = {\n    \"nushell/config.nu\".enable = false;\n    \"nushell/env.nu\".enable = false;\n  };\n\n  home.file.\".config/nushell\" = {\n    source = link \"${dotfiles}/nushell\";\n    recursive = true;\n  };\n}\n"
  },
  {
    "path": "modules/linux-packages.nix",
    "content": "{ pkgs, flakePkgs, nixGLIntel, ... }:\n\nwith pkgs;\nlet shared-packages = import ./packages.nix { inherit pkgs flakePkgs; }; in\nshared-packages ++ [\n    pinentry-gnome3\n    # nixgl is needed to access intel drivers from non-nixos environments\n    nixGLIntel\n    lazydocker # manage docker stuff\n\n    # fonts\n    monaspace\n    inter\n    lora\n]\n"
  },
  {
    "path": "modules/linux-terminals.nix",
    "content": "{ pkgs, config, inputs, ... }:\nlet\n  link = config.lib.file.mkOutOfStoreSymlink;\n  dotfiles = \"${config.home.homeDirectory}/.dotfiles\";\nin\n{\n  home.packages = [\n    (config.lib.nixGL.wrap pkgs.ghostty)\n  ];\n\n  # terminals produces their own config file which causes conflict\n  xdg.configFile = {\n    \"wezterm/wezterm.lua\".enable = false;\n    \"kitty/kitty.conf\".enable = false;\n\n    \"wezterm\" = {\n      source = link \"${dotfiles}/wezterm\";\n      recursive = true;\n    };\n    \"kitty\" = {\n      source = link \"${dotfiles}/kitty\";\n      recursive = true;\n    };\n    \"ghostty\" = {\n      source = link \"${dotfiles}/ghostty\";\n      recursive = true;\n    };\n  };\n}\n"
  },
  {
    "path": "modules/packages.nix",
    "content": "{ pkgs, flakePkgs, ... }:\n\nwith pkgs; [\n    # cli tools\n    tree\n    dust\n    ripgrep\n    fd\n    rclone\n    yt-dlp-light\n    yazi # tui file manager\n    pass\n    wget\n    yq\n    tree-sitter\n\n    eza # better ls\n    nh # nix helper\n    ffmpeg\n    imagemagick\n    pkg-config\n    csvlens\n    protobuf\n\n    # finance stuff\n    beancount\n    beanquery\n    fava\n\n    # typst related things\n    typst # documents\n    typstyle # formatting\n    zathura\n\n    # editing related things\n    zellij\n    neovim\n    helix\n    ast-grep\n    fastmod\n\n    # mobile ssh\n    mosh\n\n    vivid # better LS_COLORS\n    nushell\n    act\n\n    # these are so annoying but i need them for intelephense\n    php\n    php84Packages.composer\n\n    bun\n\n    # git related\n    git-filter-repo # useful to remove accidentally committed secrets\n    delta\n    difftastic\n\n    # rust\n    (fenix.complete.withComponents [\n      \"cargo\"\n      \"clippy\"\n      \"rust-src\"\n      \"rustc\"\n      \"rustfmt\"\n    ])\n    rust-analyzer\n] ++ (with flakePkgs; [\n  bash-env-json\n]) ++ [\n  # TODO: not sure if we should keep this or remove entirely\n  # # language servers related things\n  # harper # spell checking\n  # tinymist # typst support\n  # vtsls # typescript\n  # superhtml # html\n  # tailwindcss-language-server # tailwind\n  # mdx-language-server\n  # docker-language-server\n  # astro-language-server\n  # nil # nix lang server\n  # vscode-langservers-extracted\n]\n"
  },
  {
    "path": "nushell/ai.nu",
    "content": "def ai [instruction] {\n  if ($instruction | is-empty) {\n    return\n  }\n\n  let key = (pass show elianiva/openrouter | str trim)\n\n  let body = {\n    # model: \"deepseek/deepseek-chat-v3-0324:free\",\n    model: \"gemini/gemini-2.0-flash\",\n    messages: [\n      { role: \"system\", content: \"You are a helpful assistant that turns natural language into Nushell commands on Linux. You can only provide command with no explanations, no quotation, just plain string.\" }\n      { role: \"user\", content: $instruction }\n    ]\n  }\n\n  print \"Asking AI for command suggestion...\"\n  let response = (http post https://openrouter.ai/api/v1/chat/completions\n    --content-type \"application/json\"\n    --headers [Authorization $\"Bearer ($key)\" ]\n    $body)\n\n  let ai_command = $response.choices.0.message.content\n\n  # trim newline cuz that's dangerous as fuck\n  let sanitized = $ai_command | str trim\n\n  commandline edit --insert $sanitized\n}\n"
  },
  {
    "path": "nushell/alias.nu",
    "content": "alias sail = ./vendor/bin/sail\n\nalias :q = exit\nalias :Q = exit\n\n# git stuff\nalias ga = git add\nalias gc = git commit\nalias gs = git status\nalias gd = git diff\nalias gds = git diff --staged\nalias gr = git restore\nalias gpush = git push\n\ndef gwip [] {\n  git add -A\n  git rm (git ls-files --deleted) err> /dev/null\n  git commit -m $\"[WIP]: (date now)\"\n}\n\ndef gl [] {\n    git log --pretty=format:\"%h»¦«%s»¦«%aN»¦«%aE»¦«%aD\" -n 10\n    | lines\n    | split column \"»¦«\" sha1 subject name email date\n    | upsert date {|d| $d.date | into datetime}\n}\n"
  },
  {
    "path": "nushell/bash-env.nu",
    "content": "export def bash-env [\n  path?: string\n  --export: list\n  --shellvars (-s)\n  --fn (-f): list\n] {\n  let fn_args = if ($fn | is-not-empty) {\n    ['--shellfns' ($fn | str join ',')]\n  } else {\n    []\n  }\n\n  let path_args = if $path != null {\n    [($path | path expand)]\n  } else {\n    []\n  }\n\n  let input_str = $in | default \"\" | str join \"\\n\"\n  let raw = $input_str | bash-env-json ...($fn_args ++ $path_args) | complete\n  let raw_json = $raw.stdout | from json\n\n  let error = $raw_json | get -o error\n  if $error != null {\n    error make { msg: $error }\n  } else if $raw.exit_code != 0 {\n    error make { msg: $\"unexpected failure from bash-env-json ($raw.stderr)\" }\n  }\n\n  if ($export | is-not-empty) {\n    print \"warning: --export is deprecated, use --shellvars(-s) instead\"\n    let exported_shellvars = ($raw_json.shellvars | select -o ...$export)\n    $raw_json.env | merge ($exported_shellvars)\n  } else if $shellvars or ($fn | is-not-empty) {\n    $raw_json\n  } else {\n    $raw_json.env\n  }\n}\n"
  },
  {
    "path": "nushell/config.nu",
    "content": "$env.__NIX_DARWIN_SET_ENVIRONMENT_DONE = 1\n\n$env.PATH = [\n    $\"($env.HOME)/.nix-profile/bin\"\n    $\"/etc/profiles/per-user/($env.USER)/bin\"\n    $\"($env.HOME)/Library/Python/3.9/bin\"\n    \"/run/current-system/sw/bin\"\n    \"/nix/var/nix/profiles/default/bin\"\n    \"/usr/local/bin\"\n    \"/usr/bin\"\n    \"/usr/sbin\"\n    \"/bin\"\n    \"/sbin\"\n]\n$env.NIX_PATH = [\n    $\"darwin-config=($env.HOME)/.nixpkgs/darwin-configuration.nix\"\n    \"/nix/var/nix/profiles/per-user/root/channels\"\n]\n$env.NIX_SSL_CERT_FILE = \"/etc/ssl/certs/ca-certificates.crt\"\n$env.TERMINFO_DIRS = [\n    $\"($env.HOME)/.nix-profile/share/terminfo\"\n    $\"/etc/profiles/per-user/($env.USER)/share/terminfo\"\n    \"/run/current-system/sw/share/terminfo\"\n    \"/nix/var/nix/profiles/default/share/terminfo\"\n    \"/usr/share/terminfo\"\n]\n$env.XDG_CONFIG_DIRS = [\n    $\"($env.HOME)/.nix-profile/etc/xdg\"\n    $\"/etc/profiles/per-user/($env.USER)/etc/xdg\"\n    \"/run/current-system/sw/etc/xdg\"\n    \"/nix/var/nix/profiles/default/etc/xdg\"\n]\n$env.XDG_DATA_DIRS = [\n    $\"($env.HOME)/.nix-profile/share\"\n    $\"/etc/profiles/per-user/($env.USER)/share\"\n    \"/run/current-system/sw/share\"\n    \"/nix/var/nix/profiles/default/share\"\n]\n$env.NIX_USER_PROFILE_DIR = $\"/nix/var/nix/profiles/per-user/($env.USER)\"\n$env.NIX_PROFILES = [\n    \"/nix/var/nix/profiles/default\"\n    \"/run/current-system/sw\"\n    $\"/etc/profiles/per-user/($env.USER)\"\n    $\"($env.HOME)/.nix-profile\"\n]\n\n$env.LS_COLORS = (vivid generate rose-pine-dawn)\n\nif ($\"($env.HOME)/.nix-defexpr/channels\" | path exists) {\n    $env.NIX_PATH = ($env.PATH | split row (char esep) | append $\"($env.HOME)/.nix-defexpr/channels\")\n}\n\nif (false in (ls -l `/nix/var/nix`| where type == dir | where name == \"/nix/var/nix/db\" | get mode | str contains \"w\")) {\n    $env.NIX_REMOTE = \"daemon\"\n}\n\nconst cfg = ($nu.config-path | path dirname)\n\nsource ($cfg | path join \"bash-env.nu\")\n\nbash-env ~/.profile | load-env\nbrew shellenv | bash-env | load-env\n\nsource ($cfg | path join \"alias.nu\")\nsource ($cfg | path join \"ai.nu\")\nsource ($cfg | path join \"zoxide.nu\")\n\nuse ($cfg | path join \"rose-pine-dawn.nu\")\n\nlet carapace_completer = {|spans|\n  carapace $spans.0 nushell ...$spans | from json\n}\n\nconst history_path = ($nu.data-dir | path join \"history.txt\")\n\n$env.config = {\n  edit_mode: 'vi',\n  color_config: (rose-pine-dawn),\n  shell_integration: {\n    osc2: true\n    osc7: true\n    osc8: true\n    osc9_9: false\n    osc133: false\n    osc633: false\n  },\n  history: {\n    file_format: sqlite\n    max_size: 1_000_000\n    sync_on_enter: true\n    isolation: true\n  },\n  buffer_editor: \"nvim\",\n  show_banner: false,\n  table: {\n    mode: \"compact\"\n  },\n  completions: {\n    case_sensitive: false,\n    quick: true,\n    partial: true,\n    algorithm: \"fuzzy\",\n    external: {\n      enable: true\n      max_results: 100\n      completer: $carapace_completer\n    }\n  },\n  hooks: {\n    pre_prompt: [{ ||\n      if (which direnv | is-empty) {\n        return\n      }\n\n      direnv export json | from json | default {} | load-env\n      if 'ENV_CONVERSIONS' in $env and 'PATH' in $env.ENV_CONVERSIONS {\n        $env.PATH = do $env.ENV_CONVERSIONS.PATH.from_string $env.PATH\n      }\n    }]\n  }\n}\n\nconst NU_PLUGIN_DIRS = [\n  ($nu.current-exe | path dirname)\n  ...$NU_PLUGIN_DIRS\n]\n\n$env.config.hooks.pre_prompt = ($env.config.hooks.pre_prompt | append {\n  {\n    condition: {|| not ($nu.data-dir | path join \"vendor/autoload/starship.nu\" | path exists) }\n    code: {||\n      mkdir ($nu.data-dir | path join \"vendor/autoload\")\n      starship init nu | save -f ($nu.data-dir | path join \"vendor/autoload/starship.nu\")\n    }\n  }\n})\n\nsource ($nu.data-dir | path join \"vendor/autoload/starship.nu\")\n"
  },
  {
    "path": "nushell/rose-pine-dawn.nu",
    "content": "# Retrieve the theme settings\nexport def main [] {\n    return {\n        binary: '#907aa9'\n        block: '#56949f'\n        cell-path: '#575279'\n        closure: '#286983'\n        custom: '#797593'\n        duration: '#ea9d34'\n        float: '#575279'\n        glob: '#797593'\n        int: '#907aa9'\n        list: '#286983'\n        nothing: '#575279'\n        range: '#ea9d34'\n        record: '#286983'\n        string: '#d7827e'\n\n        bool: {|| if $in { '#286983' } else { '#ea9d34' } }\n\n        date: {|| (date now) - $in |\n            if $in < 1hr {\n                { fg: '#575279' attr: 'b' }\n            } else if $in < 6hr {\n                '#575279'\n            } else if $in < 1day {\n                '#ea9d34'\n            } else if $in < 3day {\n                '#d7827e'\n            } else if $in < 1wk {\n                { fg: '#d7827e' attr: 'b' }\n            } else if $in < 6wk {\n                '#286983'\n            } else if $in < 52wk {\n                '#56949f'\n            } else { 'dark_gray' }\n        }\n\n        filesize: {|e|\n            if $e == 0b {\n                '#575279'\n            } else if $e < 1mb {\n                '#286983'\n            } else {{ fg: '#56949f' }}\n        }\n\n        shape_and: { fg: '#907aa9' attr: 'b' }\n        shape_binary: { fg: '#907aa9' attr: 'b' }\n        shape_block: { fg: '#56949f' attr: 'b' }\n        shape_bool: '#286983'\n        shape_closure: { fg: '#286983' attr: 'b' }\n        shape_custom: '#d7827e'\n        shape_datetime: { fg: '#286983' attr: 'b' }\n        shape_directory: '#286983'\n        shape_external: '#286983'\n        shape_external_resolved: '#286983'\n        shape_externalarg: { fg: '#d7827e' attr: 'b' }\n        shape_filepath: '#286983'\n        shape_flag: { fg: '#56949f' attr: 'b' }\n        shape_float: { fg: '#575279' attr: 'b' }\n        shape_garbage: { fg: '#FFFFFF' bg: '#FF0000' attr: 'b' }\n        shape_glob_interpolation: { fg: '#286983' attr: 'b' }\n        shape_globpattern: { fg: '#286983' attr: 'b' }\n        shape_int: { fg: '#907aa9' attr: 'b' }\n        shape_internalcall: { fg: '#286983' attr: 'b' }\n        shape_keyword: { fg: '#56949f' attr: 'b' }\n        shape_list: { fg: '#286983' attr: 'b' }\n        shape_literal: '#56949f'\n        shape_match_pattern: '#d7827e'\n        shape_matching_brackets: { attr: 'u' }\n        shape_nothing: '#575279'\n        shape_operator: '#575279'\n        shape_or: { fg: '#907aa9' attr: 'b' }\n        shape_pipe: { fg: '#286983' attr: 'b' }\n        shape_range: { fg: '#ea9d34' attr: 'b' }\n        shape_raw_string: { fg: '#797593' attr: 'b' }\n        shape_record: { fg: '#286983' attr: 'b' }\n        shape_redirection: { fg: '#286983' attr: 'b' }\n        shape_signature: { fg: '#d7827e' attr: 'b' }\n        shape_string: '#d7827e'\n        shape_string_interpolation: { fg: '#286983' attr: 'b' }\n        shape_table: { fg: '#56949f' attr: 'b' }\n        shape_vardecl: { fg: '#56949f' attr: 'u' }\n        shape_variable: '#907aa9'\n\n        foreground: '#575279'\n        background: '#faf4ed'\n        cursor: '#575279'\n\n        empty: '#56949f'\n        header: { fg: '#d7827e' attr: 'b' }\n        hints: '#797593'\n        leading_trailing_space_bg: { attr: 'n' }\n        row_index: { fg: '#d7827e' attr: 'b' }\n        search_result: { fg: '#575279' bg: '#faf4ed' }\n        separator: '#797593'\n    }\n}\n\n# Update the Nushell configuration\nexport def --env \"set color_config\" [] {\n    $env.config.color_config = (main)\n}\n\n# Update terminal colors\nexport def \"update terminal\" [] {\n    let theme = (main)\n\n    # Set terminal colors\n    let osc_screen_foreground_color = '10;'\n    let osc_screen_background_color = '11;'\n    let osc_cursor_color = '12;'\n\n    $\"\n    (ansi -o $osc_screen_foreground_color)($theme.foreground)(char bel)\n    (ansi -o $osc_screen_background_color)($theme.background)(char bel)\n    (ansi -o $osc_cursor_color)($theme.cursor)(char bel)\n    \"\n    # Line breaks above are just for source readability\n    # but create extra whitespace when activating. Collapse\n    # to one line and print with no-newline\n    | str replace --all \"\\n\" ''\n    | print -n $\"($in)\\r\"\n}\n\nexport module activate {\n    export-env {\n        set color_config\n        update terminal\n    }\n}\n\n# Activate the theme when sourced\nuse activate\n"
  },
  {
    "path": "nushell/rose-pine-moon.nu",
    "content": "# Retrieve the theme settings\nexport def main [] {\n    return {\n        binary: '#c4a7e7'\n        block: '#9ccfd8'\n        cell-path: '#e0def4'\n        closure: '#3e8fb0'\n        custom: '#d9d7e1'\n        duration: '#f6c177'\n        float: '#ecebf0'\n        glob: '#d9d7e1'\n        int: '#c4a7e7'\n        list: '#3e8fb0'\n        nothing: '#ecebf0'\n        range: '#f6c177'\n        record: '#3e8fb0'\n        string: '#ea9a97'\n\n        bool: {|| if $in { '#3e8fb0' } else { '#f6c177' } }\n\n        date: {|| (date now) - $in |\n            if $in < 1hr {\n                { fg: '#ecebf0' attr: 'b' }\n            } else if $in < 6hr {\n                '#ecebf0'\n            } else if $in < 1day {\n                '#f6c177'\n            } else if $in < 3day {\n                '#ea9a97'\n            } else if $in < 1wk {\n                { fg: '#ea9a97' attr: 'b' }\n            } else if $in < 6wk {\n                '#3e8fb0'\n            } else if $in < 52wk {\n                '#9ccfd8'\n            } else { 'dark_gray' }\n        }\n\n        filesize: {|e|\n            if $e == 0b {\n                '#e0def4'\n            } else if $e < 1mb {\n                '#3e8fb0'\n            } else {{ fg: '#9ccfd8' }}\n        }\n\n        shape_and: { fg: '#c4a7e7' attr: 'b' }\n        shape_binary: { fg: '#c4a7e7' attr: 'b' }\n        shape_block: { fg: '#9ccfd8' attr: 'b' }\n        shape_bool: '#3e8fb0'\n        shape_closure: { fg: '#3e8fb0' attr: 'b' }\n        shape_custom: '#ea9a97'\n        shape_datetime: { fg: '#3e8fb0' attr: 'b' }\n        shape_directory: '#3e8fb0'\n        shape_external: '#3e8fb0'\n        shape_external_resolved: '#3e8fb0'\n        shape_externalarg: { fg: '#ea9a97' attr: 'b' }\n        shape_filepath: '#3e8fb0'\n        shape_flag: { fg: '#9ccfd8' attr: 'b' }\n        shape_float: { fg: '#ecebf0' attr: 'b' }\n        shape_garbage: { fg: '#FFFFFF' bg: '#FF0000' attr: 'b' }\n        shape_glob_interpolation: { fg: '#3e8fb0' attr: 'b' }\n        shape_globpattern: { fg: '#3e8fb0' attr: 'b' }\n        shape_int: { fg: '#c4a7e7' attr: 'b' }\n        shape_internalcall: { fg: '#3e8fb0' attr: 'b' }\n        shape_keyword: { fg: '#c4a7e7' attr: 'b' }\n        shape_list: { fg: '#3e8fb0' attr: 'b' }\n        shape_literal: '#9ccfd8'\n        shape_match_pattern: '#ea9a97'\n        shape_matching_brackets: { attr: 'u' }\n        shape_nothing: '#ecebf0'\n        shape_operator: '#f6c177'\n        shape_or: { fg: '#c4a7e7' attr: 'b' }\n        shape_pipe: { fg: '#c4a7e7' attr: 'b' }\n        shape_range: { fg: '#f6c177' attr: 'b' }\n        shape_raw_string: { fg: '#d9d7e1' attr: 'b' }\n        shape_record: { fg: '#3e8fb0' attr: 'b' }\n        shape_redirection: { fg: '#c4a7e7' attr: 'b' }\n        shape_signature: { fg: '#ea9a97' attr: 'b' }\n        shape_string: '#ea9a97'\n        shape_string_interpolation: { fg: '#3e8fb0' attr: 'b' }\n        shape_table: { fg: '#9ccfd8' attr: 'b' }\n        shape_vardecl: { fg: '#9ccfd8' attr: 'u' }\n        shape_variable: '#c4a7e7'\n\n        foreground: '#e0def4'\n        background: '#232136'\n        cursor: '#e0def4'\n\n        empty: '#9ccfd8'\n        header: { fg: '#ea9a97' attr: 'b' }\n        hints: '#59546d'\n        leading_trailing_space_bg: { attr: 'n' }\n        row_index: { fg: '#ea9a97' attr: 'b' }\n        search_result: { fg: '#ecebf0' bg: '#e0def4' }\n        separator: '#e0def4'\n    }\n}\n\n# Update the Nushell configuration\nexport def --env \"set color_config\" [] {\n    $env.config.color_config = (main)\n}\n\n# Update terminal colors\nexport def \"update terminal\" [] {\n    let theme = (main)\n\n    # Set terminal colors\n    let osc_screen_foreground_color = '10;'\n    let osc_screen_background_color = '11;'\n    let osc_cursor_color = '12;'\n\n    $\"\n    (ansi -o $osc_screen_foreground_color)($theme.foreground)(char bel)\n    (ansi -o $osc_screen_background_color)($theme.background)(char bel)\n    (ansi -o $osc_cursor_color)($theme.cursor)(char bel)\n    \"\n    # Line breaks above are just for source readability\n    # but create extra whitespace when activating. Collapse\n    # to one line and print with no-newline\n    | str replace --all \"\\n\" ''\n    | print -n $\"($in)\\r\"\n}\n\nexport module activate {\n    export-env {\n        set color_config\n        update terminal\n    }\n}\n\n# Activate the theme when sourced\nuse activate\n"
  },
  {
    "path": "nushell/vendor/autoload/starship.nu",
    "content": "# this file is both a valid\n# - overlay which can be loaded with `overlay use starship.nu`\n# - module which can be used with `use starship.nu`\n# - script which can be used with `source starship.nu`\nexport-env { $env.STARSHIP_SHELL = \"nu\"; load-env {\n    STARSHIP_SESSION_KEY: (random chars -l 16)\n    PROMPT_MULTILINE_INDICATOR: (\n        ^/etc/profiles/per-user/elianiva/bin/starship prompt --continuation\n    )\n\n    # Does not play well with default character module.\n    # TODO: Also Use starship vi mode indicators?\n    PROMPT_INDICATOR: \"\"\n\n    PROMPT_COMMAND: {||\n        (\n            # The initial value of `$env.CMD_DURATION_MS` is always `0823`, which is an official setting.\n            # See https://github.com/nushell/nushell/discussions/6402#discussioncomment-3466687.\n            let cmd_duration = if $env.CMD_DURATION_MS == \"0823\" { 0 } else { $env.CMD_DURATION_MS };\n            ^/etc/profiles/per-user/elianiva/bin/starship prompt\n                --cmd-duration $cmd_duration\n                $\"--status=($env.LAST_EXIT_CODE)\"\n                --terminal-width (term size).columns\n                ...(\n                    if (which \"job list\" | where type == built-in | is-not-empty) {\n                        [\"--jobs\", (job list | length)]\n                    } else {\n                        []\n                    }\n                )\n        )\n    }\n\n    config: ($env.config? | default {} | merge {\n        render_right_prompt_on_last_line: true\n    })\n\n    PROMPT_COMMAND_RIGHT: {||\n        (\n            # The initial value of `$env.CMD_DURATION_MS` is always `0823`, which is an official setting.\n            # See https://github.com/nushell/nushell/discussions/6402#discussioncomment-3466687.\n            let cmd_duration = if $env.CMD_DURATION_MS == \"0823\" { 0 } else { $env.CMD_DURATION_MS };\n            ^/etc/profiles/per-user/elianiva/bin/starship prompt\n                --right\n                --cmd-duration $cmd_duration\n                $\"--status=($env.LAST_EXIT_CODE)\"\n                --terminal-width (term size).columns\n                ...(\n                    if (which \"job list\" | where type == built-in | is-not-empty) {\n                        [\"--jobs\", (job list | length)]\n                    } else {\n                        []\n                    }\n                )\n        )\n    }\n}}\n"
  },
  {
    "path": "nushell/zoxide.nu",
    "content": "# Code generated by zoxide. DO NOT EDIT.\n\n# =============================================================================\n#\n# Hook configuration for zoxide.\n#\n\n# Initialize hook to add new entries to the database.\nexport-env {\n  $env.config = (\n    $env.config?\n    | default {}\n    | upsert hooks { default {} }\n    | upsert hooks.env_change { default {} }\n    | upsert hooks.env_change.PWD { default [] }\n  )\n  let __zoxide_hooked = (\n    $env.config.hooks.env_change.PWD | any { try { get __zoxide_hook } catch { false } }\n  )\n  if not $__zoxide_hooked {\n    $env.config.hooks.env_change.PWD = ($env.config.hooks.env_change.PWD | append {\n      __zoxide_hook: true,\n      code: {|_, dir| ^zoxide add -- $dir}\n    })\n  }\n}\n\n# =============================================================================\n#\n# When using zoxide with --no-cmd, alias these internal functions as desired.\n#\n\n# Jump to a directory using only keywords.\ndef --env --wrapped __zoxide_z [...rest: string] {\n  let path = match $rest {\n    [] => {'~'},\n    [ '-' ] => {'-'},\n    [ $arg ] if ($arg | path expand | path type) == 'dir' => {$arg}\n    _ => {\n      ^zoxide query --exclude $env.PWD -- ...$rest | str trim -r -c \"\\n\"\n    }\n  }\n  cd $path\n}\n\n# Jump to a directory using interactive search.\ndef --env --wrapped __zoxide_zi [...rest:string] {\n  cd $'(^zoxide query --interactive -- ...$rest | str trim -r -c \"\\n\")'\n}\n\n# =============================================================================\n#\n# Commands for zoxide. Disable these using --no-cmd.\n#\n\nalias cd = __zoxide_z\nalias cdi = __zoxide_zi\n\n# =============================================================================\n#\n# Add this to your env file (find it by running `$nu.env-path` in Nushell):\n#\n#   zoxide init nushell | save -f ~/.zoxide.nu\n#\n# Now, add this to the end of your config file (find it by running\n# `$nu.config-path` in Nushell):\n#\n#   source ~/.zoxide.nu\n#\n# Note: zoxide only supports Nushell v0.89.0+.\n"
  },
  {
    "path": "nvim/init.lua",
    "content": "require(\"config.lazy\")\nrequire(\"config.options\")\nrequire(\"config.mappings\")\nrequire(\"config.lsp\")\nrequire(\"config.autocmds\")\n\n-- prevent typo when pressing `wq` or `q`\nvim.cmd([[\ncnoreabbrev <expr> W ((getcmdtype() is# ':' && getcmdline() is# 'W')?('w'):('W'))\ncnoreabbrev <expr> Q ((getcmdtype() is# ':' && getcmdline() is# 'Q')?('q'):('Q'))\ncnoreabbrev <expr> WQ ((getcmdtype() is# ':' && getcmdline() is# 'WQ')?('wq'):('WQ'))\ncnoreabbrev <expr> Wq ((getcmdtype() is# ':' && getcmdline() is# 'Wq')?('wq'):('Wq'))\n]])\n\n-- for some reason vim-zig adds autoformatting on save\nvim.g.zig_fmt_autosave = false\n\nvim.diagnostic.config({\n  virtual_lines = false,\n  float = {\n    border = \"single\",\n    severity_sort = true,\n  },\n  signs = {\n    text = {\n      [vim.diagnostic.severity.ERROR] = \"\",\n      [vim.diagnostic.severity.WARN] = \"\",\n      [vim.diagnostic.severity.INFO] = \"\",\n      [vim.diagnostic.severity.HINT] = \"\",\n    },\n  },\n})\n\n-- set to false to disable stripping trailing whitespace\nvim.g.STRIP = true\n"
  },
  {
    "path": "nvim/lazy-lock.json",
    "content": "{\n  \"blink.cmp\": { \"branch\": \"main\", \"commit\": \"485c03400608cb6534bbf84da8c1c471fc4808c0\" },\n  \"conform.nvim\": { \"branch\": \"master\", \"commit\": \"dca1a190aa85f9065979ef35802fb77131911106\" },\n  \"dropbar.nvim\": { \"branch\": \"master\", \"commit\": \"ce202248134e3949aac375fd66c28e5207785b10\" },\n  \"fff.nvim\": { \"branch\": \"main\", \"commit\": \"bca71efb32e208d0498369197303954b15e91b63\" },\n  \"flash.nvim\": { \"branch\": \"main\", \"commit\": \"fcea7ff883235d9024dc41e638f164a450c14ca2\" },\n  \"flutter-tools.nvim\": { \"branch\": \"main\", \"commit\": \"65b7399804315a1160933b64292d3c5330aa4e9f\" },\n  \"friendly-snippets\": { \"branch\": \"main\", \"commit\": \"6cd7280adead7f586db6fccbd15d2cac7e2188b9\" },\n  \"gitsigns.nvim\": { \"branch\": \"main\", \"commit\": \"944ef13cc8d8fe8b846c91f36041c8dfb85ca000\" },\n  \"lazy.nvim\": { \"branch\": \"main\", \"commit\": \"306a05526ada86a7b30af95c5cc81ffba93fef97\" },\n  \"lazydev.nvim\": { \"branch\": \"main\", \"commit\": \"ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d\" },\n  \"mason-lspconfig.nvim\": { \"branch\": \"main\", \"commit\": \"0c2823e0418f3d9230ff8b201c976e84de1cb401\" },\n  \"mason.nvim\": { \"branch\": \"main\", \"commit\": \"12ddd182d9efbdc848b540f16484a583d52da0fb\" },\n  \"mini.ai\": { \"branch\": \"main\", \"commit\": \"43eb2074843950a3a25aae56a5f41362ec043bfa\" },\n  \"mini.pairs\": { \"branch\": \"main\", \"commit\": \"42387c7fe68fc0b6e95eaf37f1bb76e7bffaa0d9\" },\n  \"mini.surround\": { \"branch\": \"main\", \"commit\": \"2715e04bea3ec9244f15b421dc5b18c0fe326210\" },\n  \"neo-tree.nvim\": { \"branch\": \"v3.x\", \"commit\": \"84c75e7a7e443586f60508d12fc50f90d9aee14e\" },\n  \"nui.nvim\": { \"branch\": \"main\", \"commit\": \"de740991c12411b663994b2860f1a4fd0937c130\" },\n  \"nvim-lspconfig\": { \"branch\": \"master\", \"commit\": \"8fde495949782bb61c2605174e231d145a048d8c\" },\n  \"nvim-treesitter\": { \"branch\": \"main\", \"commit\": \"4916d6592ede8c07973490d9322f187e07dfefac\" },\n  \"nvim-treesitter-textobjects\": { \"branch\": \"main\", \"commit\": \"851e865342e5a4cb1ae23d31caf6e991e1c99f1e\" },\n  \"nvim-ts-autotag\": { \"branch\": \"main\", \"commit\": \"88c1453db4ba7dd24131086fe51fdf74e587d275\" },\n  \"nvim-ufo\": { \"branch\": \"main\", \"commit\": \"ab3eb124062422d276fae49e0dd63b3ad1062cfc\" },\n  \"nvim-web-devicons\": { \"branch\": \"master\", \"commit\": \"c72328a5494b4502947a022fe69c0c47e53b6aa6\" },\n  \"obsidian.nvim\": { \"branch\": \"main\", \"commit\": \"dec6573816767eaa9596cabc1593f45a2f7821ee\" },\n  \"plenary.nvim\": { \"branch\": \"master\", \"commit\": \"74b06c6c75e4eeb3108ec01852001636d85a932b\" },\n  \"promise-async\": { \"branch\": \"main\", \"commit\": \"119e8961014c9bfaf1487bf3c2a393d254f337e2\" },\n  \"quicker.nvim\": { \"branch\": \"master\", \"commit\": \"063cc44da1eef8681bbd653b29d3bc961780886a\" },\n  \"render-markdown.nvim\": { \"branch\": \"main\", \"commit\": \"d67113f11384c0dad96fced2f7b91f1fc811e97f\" },\n  \"rose-pine\": { \"branch\": \"main\", \"commit\": \"9504524e5ed0e326534698f637f9d038ba4cd0ee\" },\n  \"snacks.nvim\": { \"branch\": \"main\", \"commit\": \"ad9ede6a9cddf16cedbd31b8932d6dcdee9b716e\" },\n  \"statuscol.nvim\": { \"branch\": \"main\", \"commit\": \"c46172d0911aa5d49ba5f39f4351d1bb7aa289cc\" },\n  \"supermaven-nvim\": { \"branch\": \"main\", \"commit\": \"07d20fce48a5629686aefb0a7cd4b25e33947d50\" },\n  \"ts-comments.nvim\": { \"branch\": \"main\", \"commit\": \"123a9fb12e7229342f807ec9e6de478b1102b041\" },\n  \"typescript-tools.nvim\": { \"branch\": \"master\", \"commit\": \"c2f5910074103705661e9651aa841e0d7eea9932\" },\n  \"typst-preview.nvim\": { \"branch\": \"master\", \"commit\": \"325036ee145ca51d9efb145c09ac16bce3bc8b7d\" },\n  \"vim-easy-align\": { \"branch\": \"master\", \"commit\": \"9815a55dbcd817784458df7a18acacc6f82b1241\" },\n  \"which-key.nvim\": { \"branch\": \"main\", \"commit\": \"3aab2147e74890957785941f0c1ad87d0a44c15a\" }\n}\n"
  },
  {
    "path": "nvim/lua/config/autocmds.lua",
    "content": "local augroup = vim.api.nvim_create_augroup\nlocal autocmd = vim.api.nvim_create_autocmd\n\n-- Set current working directory to file's directory on VimEnter\nlocal set_cwd_group = augroup(\"set_cwd\", { clear = true })\nautocmd(\"VimEnter\", {\n  desc = \"Set current cwd\",\n  pattern = \"*\",\n  callback = function()\n    vim.cmd(\"cd %:p:h\")\n  end,\n  group = set_cwd_group,\n})\n\n-- Start insert mode when entering terminal buffers\nlocal terminal_group = augroup(\"terminal\", { clear = true })\nautocmd(\"BufEnter\", {\n  desc = \"Start insert on terminal\",\n  pattern = \"term://*\",\n  callback = function()\n    -- Skip during startup\n    if vim.v.vim_did_init == 0 then\n      return\n    end\n    vim.cmd(\"startinsert\")\n  end,\n  group = terminal_group,\n})\n\n-- File type associations\nlocal filetypes_group = augroup(\"filetypes\", { clear = true })\nautocmd({ \"BufNewFile\", \"BufRead\" }, {\n  desc = \"Assign mdx to markdown\",\n  pattern = \"*.mdx\",\n  command = \"set ft=markdown\",\n  group = filetypes_group,\n})\n\n-- Whitespace management\nlocal strip_whitespace_group = augroup(\"strip_whitespace\", { clear = true })\nautocmd(\"BufWritePre\", {\n  desc = \"Remove trailing whitespace on save\",\n  pattern = \"*\",\n  callback = function()\n    if vim.g.STRIP then\n      vim.cmd(\"%s/\\\\s\\\\+$//e\")\n    end\n  end,\n  group = strip_whitespace_group,\n})\n\n-- Visual feedback\nlocal hl_on_yank_group = augroup(\"hl_on_yank\", { clear = true })\nautocmd(\"TextYankPost\", {\n  desc = \"Highlight yanked text\",\n  pattern = \"*\",\n  callback = function()\n    vim.highlight.on_yank({ timeout = 250, higroup = \"Visual\" })\n  end,\n  group = hl_on_yank_group,\n})\n\n-- Auto-create missing directories on save\nlocal auto_mkdir_group = augroup(\"auto_mkdir\", { clear = true })\nautocmd(\"BufWritePre\", {\n  desc = \"Create missing directories\",\n  callback = function(event)\n    if event.match:match(\"^%w%w+://\") then return end\n    local dir = vim.fn.fnamemodify(event.match, \":p:h\")\n    vim.fn.mkdir(dir, \"p\")\n  end,\n  group = auto_mkdir_group,\n})\n\n-- Auto-close quickfix when it's the last window\nlocal qf_close_group = augroup(\"qf_close\", { clear = true })\nautocmd(\"WinEnter\", {\n  desc = \"Auto close quickfix\",\n  callback = function()\n    if vim.fn.winnr(\"$\") == 1 and vim.bo.buftype == \"quickfix\" then\n      vim.cmd.quit()\n    end\n  end,\n  group = qf_close_group,\n})\n"
  },
  {
    "path": "nvim/lua/config/lazy.lua",
    "content": "-- Bootstrap lazy.nvim\nlocal lazypath = vim.fn.stdpath(\"data\") .. \"/lazy/lazy.nvim\"\nif not vim.uv.fs_stat(lazypath) then\n  local lazyrepo = \"https://github.com/folke/lazy.nvim.git\"\n  local out = vim.fn.system({ \"git\", \"clone\", \"--filter=blob:none\", \"--branch=stable\", lazyrepo, lazypath })\n  if vim.v.shell_error ~= 0 then\n    vim.api.nvim_echo({\n      { \"Failed to clone lazy.nvim:\\n\", \"ErrorMsg\" },\n      { out, \"WarningMsg\" },\n      { \"\\nPress any key to exit...\" },\n    }, true, {})\n    vim.fn.getchar()\n    os.exit(1)\n  end\nend\nvim.opt.rtp:prepend(lazypath)\n\n-- Make sure to setup `mapleader` and `maplocalleader` before\n-- loading lazy.nvim so that mappings are correct.\n-- This is also a good place to setup other settings (vim.opt)\nvim.g.mapleader = \" \"\nvim.g.maplocalleader = \"\\\\\"\n\n-- Setup lazy.nvim\nrequire(\"lazy\").setup({\n  spec = {\n    { import = \"plugins\" },\n  },\n  -- Configure any other settings here. See the documentation for more details.\n  -- colorscheme that will be used when installing plugins.\n  install = { colorscheme = { \"vscode\" } },\n  -- automatically check for plugin updates\n  checker = { enabled = false },\n})\n"
  },
  {
    "path": "nvim/lua/config/lsp.lua",
    "content": "local util = require(\"lspconfig.util\")\nlocal capabilities = vim.lsp.protocol.make_client_capabilities()\ncapabilities.textDocument.foldingRange = {\n  dynamicRegistration = false,\n  lineFoldingOnly = true,\n}\ncapabilities.textDocument.semanticTokens.multilineTokenSupport = true\n\nlocal ok, blink = pcall(require, \"blink.cmp\")\nif ok then\n  capabilities = vim.tbl_deep_extend(\"force\", capabilities, blink.get_lsp_capabilities(capabilities))\nend\n\n-- global lsp settings\nvim.lsp.config(\"*\", {\n  capabilities = capabilities,\n})\n\n-- intelephense specific settings\nlocal intelephense_capabilities = vim.lsp.protocol.make_client_capabilities()\nintelephense_capabilities.textDocument.completion.dynamicRegistration = true\nvim.lsp.config(\"intelephense\", {\n  capabilities = intelephense_capabilities\n})\n\n-- basedpyright specific settings\nvim.lsp.config(\"basedpyright\", {\n  settings = {\n    basedpyright = {\n      analysis = {\n        autoSearchPaths = true,\n        diagnosticMode = \"workspace\",\n        useLibraryCodeForTypes = true,\n        diagnosticSeverityOverrides = {\n          reportUnknownVariableType = false,\n        },\n      },\n    },\n  }\n})\n\n-- use harper for markdown and typst\nvim.lsp.config(\"harper_ls\", {\n  filetypes = { \"markdown\", \"typst\" }\n})\n\nvim.lsp.config(\"oxfmt\", {\n  root_markers = { \".git\", \".oxfmtrc.json\", \".oxfmtrc.jsonc\", \"vite.config.ts\" },\n  settings = {\n    fmt = {\n      configPath = \"./vite.config.ts\"\n    }\n  }\n})\n\nvim.lsp.config(\"oxlint\", {\n  root_markers = { \".git\", \".oxlintrc.json\", \".oxlintrc.jsonc\", \"vite.config.ts\" },\n  settings = {\n    typeAware = true,\n    configPath = \"./vite.config.ts\"\n  }\n})\n\nvim.api.nvim_create_autocmd(\"LspAttach\", {\n  callback = function(args)\n    local utils = require(\"config.utils\")\n    local client = vim.lsp.get_client_by_id(args.data.client_id)\n\n    if client == nil then\n      return\n    end\n\n    -- trigger additional capabilities\n    utils.lsp.additional_capabilities(client)\n\n    -- Setup buffer-specific mappings\n    utils.lsp.additional_mappings(args.buf)\n\n    -- Setup buffer-specific autocmds for this LSP client\n    -- if client:supports_method(\"textDocument/codeLens\", args.buf) then\n    --   local group = vim.api.nvim_create_augroup(\"lsp_codelens_\" .. args.buf, { clear = true })\n    --   vim.api.nvim_create_autocmd({ \"InsertEnter\", \"InsertLeave\" }, {\n    --     group = group,\n    --     buffer = args.buf,\n    --     callback = function()\n    --       vim.lsp.codelens.refresh()\n    --     end,\n    --   })\n    -- end\n\n    if client:supports_method(\"textDocument/documentHighlight\", args.buf) then\n      local group = vim.api.nvim_create_augroup(\"lsp_document_highlight_\" .. args.buf, { clear = true })\n      vim.api.nvim_create_autocmd(\"CursorHold\", {\n        group = group,\n        buffer = args.buf,\n        callback = function()\n          vim.lsp.buf.document_highlight()\n        end,\n      })\n      vim.api.nvim_create_autocmd(\"CursorHoldI\", {\n        group = group,\n        buffer = args.buf,\n        callback = function()\n          vim.lsp.buf.document_highlight()\n        end,\n      })\n      vim.api.nvim_create_autocmd(\"CursorMoved\", {\n        group = group,\n        buffer = args.buf,\n        callback = function()\n          vim.lsp.buf.clear_references()\n        end,\n      })\n    end\n  end\n})\n\nvim.api.nvim_create_autocmd('LspProgress', {\n  callback = function(ev)\n    local value = ev.data.params.value\n    vim.api.nvim_echo({ { value.message or 'done' } }, false, {\n      id = 'lsp.' .. ev.data.client_id,\n      kind = 'progress',\n      source = 'vim.lsp',\n      title = value.title,\n      status = value.kind ~= 'end' and 'running' or 'success',\n      percent = value.percentage,\n    })\n  end,\n})\n"
  },
  {
    "path": "nvim/lua/config/mappings.lua",
    "content": "local get_mapper = function(mode, noremap)\n  return function(lhs, rhs, opts)\n    opts = opts or {}\n    opts.noremap = noremap\n    vim.keymap.set(mode, lhs, rhs, opts)\n  end\nend\n\nlocal noremap = get_mapper(\"n\", false)\nlocal nnoremap = get_mapper(\"n\", true)\nlocal inoremap = get_mapper(\"i\", true)\nlocal tnoremap = get_mapper(\"t\", true)\nlocal vnoremap = get_mapper(\"v\", true)\nlocal xnoremap = get_mapper(\"x\", true)\nlocal cnoremap = get_mapper(\"c\", true)\n\n-- stolen from justinmk\nnnoremap(\"/\", \"ms/\", { desc = \"Keeps jumplist after forward searching\" })\nnnoremap(\"?\", \"ms?\", { desc = \"Keeps jumplist after backward searching\" })\n\n-- https://github.com/mhinz/vim-galore#saner-behavior-of-n-and-n\nnoremap(\"n\", '\"Nn\"[v:searchforward]', {\n  expr = true,\n  desc = \"Better forward N behaviour\",\n})\nnoremap(\"N\", '\"nN\"[v:searchforward]', {\n  expr = true,\n  desc = \"Better backward N behaviour\",\n})\n\nnnoremap(\"<C-n>\", \"<CMD>Neotree toggle<CR>\", { desc = \"Toggle NeoTree\" })\n\n-- Better movement between windows\nnnoremap(\"<C-h>\", \"<C-w><C-h>\", { desc = \"Go to the left window\" })\nnnoremap(\"<C-l>\", \"<C-w><C-l>\", { desc = \"Go to the right window\" })\nnnoremap(\"<C-j>\", \"<C-w><C-j>\", { desc = \"Go to the bottom window\" })\nnnoremap(\"<C-k>\", \"<C-w><C-k>\", { desc = \"Go to the top window\" })\n\n-- better behaviour on wrapped lines\nnnoremap(\"j\", \"gj\", { desc = \"Move down by visual line on wrapped lines\" })\nnnoremap(\"k\", \"gk\", { desc = \"Move up by visual line on wrapped lines\" })\n\n-- remove annoying exmode\nnnoremap(\"Q\", \"<Nop>\", { desc = \"Remove annoying exmode\" })\nnnoremap(\"q:\", \"<Nop>\", { desc = \"Remove annoying exmode\" })\n\nvnoremap(\"<A-y>\", '\"+y', { desc = \"Yank selection to clipboard\" })\nvnoremap(\"<\", \"<gv\", { desc = \"Dedent current selection\" })\nvnoremap(\">\", \">gv\", { desc = \"Indent current selection\" })\n\nnnoremap(\"<Leader>n\", \"<CMD>nohl<CR>\", { desc = \"Toggle search highlight\" })\ninoremap(\"<C-W>\", \"<C-S-W>\", { desc = \"Delete word backwards\" })\n\n-- Terminal escape\ntnoremap(\"<Esc><Esc>\", \"<C-\\\\><C-n>\", { desc = \"Exit terminal mode\" })\n\nnnoremap(\"<Leader>d\", '\"_d', { desc = \"Delete to black hole\" })\nxnoremap(\"<Leader>d\", '\"_d', { desc = \"Delete to black hole\" })\nnnoremap(\"x\", '\"_x', { desc = \"Delete char (no yank)\" })\nxnoremap(\"x\", '\"_x', { desc = \"Delete char (no yank)\" })\n\n-- Smart paste: don't yank replaced text in visual mode\nxnoremap(\"p\", '\"_dP', { desc = \"Paste without yanking replaced text\" })\n\n-- Resize splits with Alt+arrow\nnnoremap(\"<A-Up>\", \"<CMD>resize +2<CR>\", { desc = \"Increase height\" })\nnnoremap(\"<A-Down>\", \"<CMD>resize -2<CR>\", { desc = \"Decrease height\" })\nnnoremap(\"<A-Left>\", \"<CMD>vertical resize -2<CR>\", { desc = \"Decrease width\" })\nnnoremap(\"<A-Right>\", \"<CMD>vertical resize +2<CR>\", { desc = \"Increase width\" })\nnnoremap(\"<Leader>=\", \"<C-w>=\", { desc = \"Equalize splits\" })\n\n-- Better search: clear with Esc, search in visual selection\nxnoremap(\"/\", \"<C-\\\\><C-n>/\\\\%V\", { desc = \"Search in visual selection\" })\nxnoremap(\"*\", '\"zy/<C-r>z<CR>', { desc = \"Search visual selection forward\" })\nxnoremap(\"#\", '\"zy?<C-r>z<CR>', { desc = \"Search visual selection backward\" })\n\n-- Buffer navigation\nnnoremap(\"]b\", \"<CMD>bnext<CR>\", { desc = \"Next buffer\" })\nnnoremap(\"[b\", \"<CMD>bprev<CR>\", { desc = \"Previous buffer\" })\nnnoremap(\"<Leader>bb\", \"<CMD>b#<CR>\", { desc = \"Last buffer (alternate)\" })\nnnoremap(\"<Leader>bw\", \"<CMD>bw<CR>\", { desc = \"Wipe buffer\" })\n\n-- Smart home: 0 goes to first non-blank, again to column 0\nlocal function smart_home()\n  local col = vim.api.nvim_win_get_cursor(0)[2]\n  local line = vim.api.nvim_get_current_line()\n  local first_nonblank = line:find(\"([^%s])\") or 1\n  if col == first_nonblank - 1 then\n    return \"0\"\n  else\n    return \"^\"\n  end\nend\nnnoremap(\"0\", smart_home, { expr = true, desc = \"Smart home\" })\nnnoremap(\"^\", \"0\", { desc = \"Go to column 0\" })\n\n-- Command line editing (Emacs-style)\ncnoremap(\"<C-a>\", \"<Home>\", { desc = \"Start of line\" })\ncnoremap(\"<C-e>\", \"<End>\", { desc = \"End of line\" })\ncnoremap(\"<C-f>\", \"<Right>\", { desc = \"Forward char\" })\ncnoremap(\"<C-b>\", \"<Left>\", { desc = \"Backward char\" })\ncnoremap(\"<C-d>\", \"<Del>\", { desc = \"Delete char\" })\ncnoremap(\"<C-k>\", '<C-\\\\>e strpart(getcmdline(), 0, getcmdpos()-1)<CR>', { desc = \"Delete to end\" })\n\n-- Quickfix and location list navigation\nnnoremap(\"<Leader>qf\", \"<CMD>copen<CR>\", { desc = \"Open quickfix\" })\nnnoremap(\"<Leader>ql\", \"<CMD>lopen<CR>\", { desc = \"Open location list\" })\nnnoremap(\"]q\", \"<CMD>cnext<CR>zz\", { desc = \"Next quickfix\" })\nnnoremap(\"[q\", \"<CMD>cprev<CR>zz\", { desc = \"Prev quickfix\" })\nnnoremap(\"]l\", \"<CMD>lnext<CR>zz\", { desc = \"Next location\" })\nnnoremap(\"[l\", \"<CMD>lprev<CR>zz\", { desc = \"Prev location\" })\nnnoremap(\"<Leader>qn\", \"<CMD>cn<CR>\", { desc = \"Next quickfix\" })\nnnoremap(\"<Leader>qp\", \"<CMD>cp<CR>\", { desc = \"Prev quickfix\" })\n\n-- Copy file paths\nnnoremap(\"<Leader>yf\", function()\n  local path = vim.fn.expand(\"%:.\")\n  vim.fn.setreg(\"+\", path)\n  vim.notify(\"Copied: \" .. path, vim.log.levels.INFO)\nend, { desc = \"Yank relative file path\" })\nnnoremap(\"<Leader>yF\", function()\n  local path = vim.fn.expand(\"%:p\")\n  vim.fn.setreg(\"+\", path)\n  vim.notify(\"Copied: \" .. path, vim.log.levels.INFO)\nend, { desc = \"Yank absolute file path\" })\n\n-- Undo breakpoints on punctuation (better undo granularity)\ninoremap(\",\", \",<C-g>u\")\ninoremap(\".\", \".<C-g>u\")\ninoremap(\";\", \";<C-g>u\")\ninoremap(\"!\", \"!<C-g>u\")\ninoremap(\"?\", \"?<C-g>u\")\n\n-- Repeat last action in visual mode\nxnoremap(\".\", \"<CMD>norm.<CR>\", { desc = \"Repeat last action\" })\n"
  },
  {
    "path": "nvim/lua/config/options.lua",
    "content": "-- Disable built-in plugins for faster startup\nlocal disabled_builtins = {\n  'gzip',\n  'matchit',\n  'matchparen',\n  'netrwPlugin',\n  'tarPlugin',\n  'tohtml',\n  'tutor',\n  'zipPlugin',\n}\nfor _, plugin in ipairs(disabled_builtins) do\n  vim.g['loaded_' .. plugin] = 1\nend\n\nlocal o = vim.opt\n\n-- o.number         = true -- enable line number\n-- o.relativenumber = true -- enable relative line number\no.undofile       = true -- persistent undo\no.backup         = false -- disable backup\no.number         = true -- enable line number\no.relativenumber = true -- enable relative line number\no.cursorline     = true -- enable cursor line\no.cursorlineopt  = \"both\"\no.expandtab      = true -- use spaces instead of tabs\no.autowrite      = true -- auto write buffer when it's not focused\no.autoread       = true -- auto reload changed files\no.hidden         = true -- keep hidden buffers\no.hlsearch       = true -- highlight matching search\no.ignorecase     = true -- case insensitive on search..\no.smartcase      = true -- ..unless there's a capital\no.equalalways    = true -- make window size always equal\no.list           = true -- display listchars\no.showmode       = false -- don't show mode\no.autoindent     = true -- enable autoindent\no.smartindent    = true -- smarter indentation\no.smarttab       = true -- make tab behaviour smarter\no.splitbelow     = true -- split below instead of above\no.splitright     = true -- split right instead of left\no.splitkeep      = \"screen\" -- stabilize split\no.startofline    = false -- don't go to the start of the line when moving to another file\no.swapfile       = false -- disable swapfile\no.smoothscroll   = true -- smooth scrolling\no.termguicolors  = true -- true colours for better experience\no.undolevels     = 1000 -- more undo history\no.winborder      = 'single' -- default border for floating windows\no.pumborder      = 'single' -- border for completion popup\no.wrap           = false -- don't wrap lines\no.writebackup    = false -- disable backup\no.backupcopy     = \"yes\" -- fix weirdness for stuff that replaces the entire file when hot reloading\no.completeopt    = {\n  \"menu\",\n  \"menuone\",\n  \"noselect\",\n  \"noinsert\",\n} -- better completion\no.encoding       = \"UTF-8\" -- set encoding\no.fillchars      = {\n  vert = \"│\",\n  eob = \" \",\n  diff = \" \",\n  fold = \" \",\n  foldopen = \"\",\n  foldsep = \" \",\n  foldclose = \"\",\n} -- make vertical split sign better\no.foldmethod     = \"expr\"\no.foldopen       = {\n  \"percent\",\n  \"search\",\n} -- don't open fold if I don't tell it to do so\no.inccommand     = \"split\" -- incrementally show result of command\no.listchars      = {\n  -- eol = \"↲\",\n  tab = \"  \",\n} -- set listchars\no.mouse          = \"nvi\" -- enable mouse support in normal, insert, and visual mode\no.shortmess      = \"filnxtToOFAs\" -- disable some messages, keep intro\n-- o.signcolumn     = \"yes:1\" -- enable sign column all the time 4 column\n-- o.colorcolumn    = { \"80\" } -- 80 chars color column\no.shell          = \"/run/current-system/sw/bin/bash\" -- use bash instead of zsh\no.pumheight      = 10 -- limit completion items\no.re             = 0 -- set regexp engine to auto\no.scrolloff      = 8 -- keep more context lines above/below cursor\no.sidescroll     = 8 -- smooth horizontal scroll padding\no.shiftwidth     = 2 -- set indentation width\no.sidescrolloff  = 15 -- make scrolling better\no.tabstop        = 2 -- tabsize\no.timeoutlen     = 400 -- faster timeout wait time\no.updatetime     = 1000 -- set faster update time\no.joinspaces     = false\no.diffopt:append { \"algorithm:histogram\", \"indent-heuristic\" }\n\n-- stolen from tjdevries\no.formatoptions = o.formatoptions\n\t- \"a\" -- Auto formatting is BAD.\n\t- \"t\" -- Don't auto format my code. I got linters for that.\n\t+ \"c\" -- In general, I like it when comments respect textwidth\n\t+ \"q\" -- Allow formatting comments w/ gq\n\t- \"o\" -- O and o, don't continue comments\n\t- \"r\" -- But do continue when pressing enter.\n\t+ \"n\" -- Indent past the formatlistpat, not underneath it.\n\t+ \"j\" -- Auto-remove comments if possible.\n\t- \"2\" -- I'm not in gradeschool anymore\n\n-- Experimental UI2: floating cmdline and messages (Neovim 0.12+)\n-- No more \"Press ENTER to continue\" prompts\n-- DISABLED: causes grey flash on intro screen\nrequire('vim._core.ui2').enable({\n  enable = true,\n  msg = {\n    targets = {\n      [''] = 'msg',\n      empty = 'cmd',\n      bufwrite = 'msg',\n      confirm = 'cmd',\n      emsg = 'pager',\n      echo = 'msg',\n      echomsg = 'msg',\n      echoerr = 'pager',\n      completion = 'cmd',\n      list_cmd = 'pager',\n      lua_error = 'pager',\n      lua_print = 'msg',\n      progress = 'pager',\n      rpc_error = 'pager',\n      quickfix = 'msg',\n      search_cmd = 'cmd',\n      search_count = 'cmd',\n      shell_cmd = 'pager',\n      shell_err = 'pager',\n      shell_out = 'pager',\n      shell_ret = 'msg',\n      undo = 'msg',\n      verbose = 'pager',\n      wildlist = 'cmd',\n      wmsg = 'msg',\n      typed_cmd = 'cmd',\n    },\n    cmd = {\n      height = 0.5,\n    },\n    dialog = {\n      height = 0.5,\n    },\n    msg = {\n      height = 0.3,\n      timeout = 5000,\n    },\n    pager = {\n      height = 0.5,\n    },\n  },\n})\n"
  },
  {
    "path": "nvim/lua/config/utils.lua",
    "content": "local M = {}\nM.lsp = {}\n\nM.borders = {\n  -- fancy border\n  { \"🭽\", \"FloatBorder\" },\n  { \"▔\", \"FloatBorder\" },\n  { \"🭾\", \"FloatBorder\" },\n  { \"▕\", \"FloatBorder\" },\n  { \"🭿\", \"FloatBorder\" },\n  { \"▁\", \"FloatBorder\" },\n  { \"🭼\", \"FloatBorder\" },\n  { \"▏\", \"FloatBorder\" },\n\n  -- padding border\n  -- {\"▄\", \"PaddingBorder\"},\n  -- {\"▄\", \"PaddingBorder\"},\n  -- {\"▄\", \"PaddingBorder\"},\n  -- {\"█\", \"PaddingBorder\"},\n  -- {\"▀\", \"PaddingBorder\"},\n  -- {\"▀\", \"PaddingBorder\"},\n  -- {\"▀\", \"PaddingBorder\"},\n  -- {\"█\", \"PaddingBorder\"}\n}\n\nfunction M.lsp.additional_capabilities(client, bufnr)\n  -- if client:supports_method(\"textDocument/codeLens\") then\n  --   vim.lsp.codelens.enable(true, { bufnr = bufnr })\n  -- end\n\n  if client:supports_method(\"textDocument/inlayHint\") then\n    vim.lsp.inlay_hint.enable(true)\n  end\nend\n\nfunction M.lsp.additional_mappings(bufnr)\n  vim.keymap.set(\"i\", \"<C-s>\", vim.lsp.buf.signature_help, {\n    desc = \"Trigger signature help from the language server\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>k\", vim.lsp.buf.hover, {\n    desc = \"Trigger hover window from the language server\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>a\", vim.lsp.buf.code_action, {\n    desc = \"Pick code actions from the language server\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"gd\", vim.lsp.buf.definition, {\n    desc = \"Go to symbol definition\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>l\", vim.lsp.codelens.run, {\n    desc = \"Run codelens from the language server\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>d\", function()\n    vim.diagnostic.open_float({\n      bufnr = bufnr,\n      header = \"\",\n      scope = \"line\",\n    })\n  end, {\n    desc = \"See line diagnostics in floating window\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>gr\", vim.lsp.buf.references, {\n    desc = \"Find references\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>cf\", function()\n    vim.lsp.buf.format({\n      filter = function(client)\n        return client.name ~= \"ts_ls\"\n      end,\n    })\n  end, {\n    desc = \"Show references\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"<Leader>r\", vim.lsp.buf.rename, {\n    desc = \"Rename current symbol\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"]d\", function()\n    vim.diagnostic.jump({\n      count = 1,\n      float = { show_header = false },\n    })\n  end, {\n    desc = \"Go to next diagnostic\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\n\n  vim.keymap.set(\"n\", \"[d\", function()\n    vim.diagnostic.jump({\n      count = -1,\n      float = { show_header = false },\n    })\n  end, {\n    desc = \"Go to previous diagnostic\",\n    noremap = true,\n    silent = true,\n    buffer = bufnr,\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "nvim/lua/plugins/align.lua",
    "content": "return {\n  \"junegunn/vim-easy-align\",\n  lazy = false,\n  keys = {\n    { \"ga\", \"<Plug>(EasyAlign)\", mode = \"x\" },\n    { \"ga\", \"<Plug>(EasyAlign)\", mode = \"n\" },\n    { \"ga\", \"<Plug>(EasyAlign)\", mode = \"v\" },\n  }\n}\n"
  },
  {
    "path": "nvim/lua/plugins/cmp.lua",
    "content": "return {\n\t\"saghen/blink.cmp\",\n\tlazy = false, -- lazy loading handled internally\n\tdependencies = \"rafamadriz/friendly-snippets\",\n\tversion = \"v0.*\", -- last release is too old\n\tevent = \"InsertEnter\",\n\topts = {\n\t\tkeymap = {\n\t\t\tpreset = \"enter\",\n\t\t},\n\t\tcmdline = {\n\t\t\tkeymap = {\n\t\t\t\tpreset = \"none\",\n\t\t\t},\n\t\t},\n\t\tappearance = {\n\t\t\tnerd_font_variant = \"normal\",\n\t\t},\n\t\tsignature = {\n\t\t\twindow = {\n\t\t\t\tborder = \"solid\",\n\t\t\t},\n\t\t},\n\t\tfuzzy = { implementation = \"prefer_rust\" },\n\t\tsources = {\n\t\t\tdefault = { \"lsp\", \"path\", \"snippets\", \"buffer\" },\n\t\t},\n\t\tcompletion = {\n\t\t\taccept = {\n\t\t\t\tcreate_undo_point = true,\n\t\t\t\tauto_brackets = {\n\t\t\t\t\tenabled = true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmenu = {\n\t\t\t\tborder = \"single\",\n\t\t\t\tauto_show = function(ctx)\n\t\t\t\t\treturn ctx.mode ~= \"cmdline\"\n\t\t\t\tend,\n\t\t\t},\n\t\t\tdocumentation = {\n\t\t\t\twindow = {\n\t\t\t\t\tborder = \"solid\",\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrigger = {\n\t\t\t\tshow_on_insert_on_trigger_character = true,\n\t\t\t\t-- these are annoying\n\t\t\t\tshow_on_x_blocked_trigger_characters = { \"'\", '\"', \"(\", \"[\", \"{\" },\n\t\t\t},\n\t\t\tlist = {\n\t\t\t\tselection = {\n\t\t\t\t\tpreselect = function(ctx)\n\t\t\t\t\t\treturn ctx.mode ~= \"cmdline\"\n\t\t\t\t\tend,\n\t\t\t\t\tauto_insert = function(ctx)\n\t\t\t\t\t\treturn ctx.mode ~= \"cmdline\"\n\t\t\t\t\tend,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\topts_extend = { \"sources.default\" },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/colorscheme.lua",
    "content": "function CustomStatusline()\n  -- Diagnostics (shows e.g. \"E:2 W:1\" when there are issues)\n  local diagnostics = \"\"\n  if vim.diagnostic.status then\n    local d = vim.diagnostic.status()\n    if d and d ~= \"\" then\n      diagnostics = \" \" .. d\n    end\n  end\n\n  -- Progress / LSP status (shows \"rust-analyzer: Indexing 45%\" etc.)\n  local progress = \"\"\n  if vim.ui.progress_status then\n    local p = vim.ui.progress_status()\n    if p and p ~= \"\" then\n      progress = \" \" .. p\n    end\n  end\n\n  -- Build the final statusline\n  return table.concat({\n    \"  \",           -- your heart icon\n    \"%f\",           -- filename\n    \"%m\",           -- modified flag [+]\n    \"%r\",           -- readonly flag [RO]\n    diagnostics,    -- diagnostic counts\n    \"%=\",           -- switch to right side\n    progress,       -- LSP / progress info\n    \" %y\",          -- filetype\n    \" %l:%c\",       -- line:column\n  }, \" \")\nend\n\nreturn {\n  {\n    \"rose-pine/neovim\",\n    name = \"rose-pine\",\n    init = function()\n      vim.opt.laststatus = 3\n      vim.opt.statusline = \"%!v:lua.CustomStatusline()\"\n    end,\n    config = function()\n      require(\"rose-pine\").setup {\n        variant = \"main\",\n        highlight_groups = {\n          -- annoying quickfix line highlight\n          QuickFixLine = { fg = \"none\", bg = \"surface\" },\n\n          -- pink statusline\n          StatusLine = { fg = \"love\", bg = \"love\", blend = 10 },\n          StatusLineNC = { fg = \"subtle\", bg = \"surface\" },\n\n          -- less intrusive supermaven suggestion colour\n          SupermavenSuggestion = { fg = \"muted\", blend = 10 },\n\n          -- notify\n          NotifyERRORBody = { bg = \"surface\" },\n          NotifyWARNBody = { bg = \"surface\" },\n          NotifyINFOBody = { bg = \"surface\" },\n          NotifyDEBUGBody = { bg = \"surface\" },\n          NotifyTRACEBody = { bg = \"surface\" },\n\n          -- diagnostics\n          DiagnosticSignInfo = { fg = \"foam\", bg = \"surface\" },\n          DiagnosticSignHint = { fg = \"iris\", bg = \"surface\" },\n          DiagnosticSignWarn = { fg = \"gold\", bg = \"surface\" },\n          DiagnosticSignError = { fg = \"love\", bg = \"surface\" },\n\n          -- gitsigns\n          GitSignsAdd = { fg = \"foam\" },\n          GitSignsChange = { fg = \"iris\" },\n          GitSignsDelete = { fg = \"love\" },\n          GitSignsStagedAdd = { fg = \"foam\" },\n          GitSignsStagedChange = { fg = \"iris\" },\n          GitSignsStagedDelete = { fg = \"love\" },\n\n          -- dark statuscolumn\n          SignColumn = { fg = \"muted\", bg = \"surface\" },\n          FoldColumn = { fg = \"muted\", bg = \"surface\" },\n          LineNr = { fg = \"muted\", bg = \"surface\" },\n          CursorLineNr = { fg = \"text\", bg = \"surface\" },\n          CursorLineFold = { bg = \"surface\" },\n\n          -- snacks\n          SnacksPicker = { fg = \"subtle\", bg = \"base\" },\n          SnacksPickerPrompt = { fg = \"love\", bg = \"base\" },\n\n          SnacksPickerListTitle = { fg = \"base\", bg = \"love\" },\n          SnacksPickerListBorder = { fg = \"love\", bg = \"base\" },\n\n          SnacksPickerInputTitle = { fg = \"base\", bg = \"pine\" },\n          SnacksPickerInputBorder = { fg = \"pine\", bg = \"base\" },\n\n          FloatTitle = { fg = \"base\", bg = \"pine\" },\n          SnacksPickerBorder = { fg = \"pine\", bg = \"base\" },\n\n          SnacksPickerPreviewTitle = { fg = \"base\", bg = \"iris\" },\n          SnacksPickerPreviewBorder = { fg = \"iris\", bg = \"base\" },\n\n          -- codecompanion\n          CodeCompanionChatVariable = { fg = \"base\", bg = \"love\" },\n\n          -- better search\n          CurSearch = { fg = \"base\", bg = \"love\", inherit = false },\n          Search = { fg = \"text\", bg = \"love\", blend = 20, inherit = false },\n\n          -- flush blink.cmp background\n          BlinkCmpMenu = { bg = \"base\" },\n          BlinkCmpMenuBorder = { bg = \"base\" },\n          BlinkCmpMenuSelection = { bg = \"love\", blend = 10 },\n\n          -- snacks\n          SnacksIndent = { fg = \"overlay\", blend = 10 },\n        }\n      }\n      vim.cmd.colorscheme(\"rose-pine-dawn\")\n    end\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/conform.lua",
    "content": "return {\n  \"stevearc/conform.nvim\",\n  dependencies = { \"mason.nvim\" },\n  lazy = true,\n  cmd = \"Format\",\n  config = function(_, opts)\n    local conform = require(\"conform\")\n\n    conform.setup({\n      formatters_by_ft = {\n        lua = { \"stylua\" },\n        javascript = { \"oxfmt\", lsp_format = \"prefer\" },\n        javascriptreact = { \"oxfmt\", lsp_format = \"prefer\" },\n        typescript = { \"oxfmt\", lsp_format = \"prefer\" },\n        typescriptreact = { \"oxfmt\", lsp_format = \"prefer\" },\n        json = { \"oxfmt\", lsp_format = \"prefer\" },\n        jsonc = { \"oxfmt\", lsp_format = \"prefer\" },\n        python = { \"isort\", \"ruff\" },\n        php = { \"pint\" }\n      }\n    })\n\n    vim.api.nvim_create_user_command(\"Format\", function(args)\n      local range = nil\n      if args.count ~= -1 then\n        local end_line = vim.api.nvim_buf_get_lines(0, args.line2 - 1, args.line2, true)[1]\n        range = {\n          start = { args.line1, 0 },\n          [\"end\"] = { args.line2, end_line:len() },\n        }\n      end\n      conform.format({\n        async = true,\n        lsp_format = \"fallback\",\n        range = range,\n        filter = function(client)\n          return client.name ~= \"tsserver\" and\n              client.name ~= \"jsonls\" and\n              client.name ~= \"sumneko_lua\" and\n              client.name ~= \"typescript-tools\"\n        end\n      })\n    end, { range = true })\n  end\n}\n"
  },
  {
    "path": "nvim/lua/plugins/dropbar.lua",
    "content": "return {\n  'Bekaboo/dropbar.nvim',\n  opts = {\n    icons = {\n      ui = {\n        bar = {\n          separator = '  ',\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "nvim/lua/plugins/fff.lua",
    "content": "return {\n  'dmtrKovalenko/fff.nvim',\n  build = function()\n    -- this will download prebuild binary or try to use existing rustup toolchain to build from source\n    -- (if you are using lazy you can use gb for rebuilding a plugin if needed)\n    require(\"fff.download\").download_or_build_binary()\n  end,\n  -- if you are using nixos\n  -- build = \"nix run .#release\",\n  opts = {\n    prompt = '❯ ',\n    title = 'FFF',\n    layout = {\n      height = 0.9,\n      width = 0.9,\n      prompt_position = 'top',\n      preview_position = 'right',\n      preview_size = 0.5,\n      flex = {\n        size = 120,\n        wrap = 'top',\n      },\n      show_scrollbar = true,\n    },\n    preview = {\n      enabled = true,\n      wrap_lines = false,\n    },\n    keymaps = {\n      close = '<Esc>',\n      select = '<CR>',\n      select_split = '<C-s>',\n      select_vsplit = '<C-v>',\n      select_tab = '<C-t>',\n      move_up = { '<Up>', '<C-p>' },\n      move_down = { '<Down>', '<C-n>' },\n      preview_scroll_up = '<C-u>',\n      preview_scroll_down = '<C-d>',\n      toggle_select = '<Tab>',\n      send_to_quickfix = '<C-q>',\n    },\n    hl = {\n      border = 'SnacksBorder',\n      matched = 'SnacksPickerMatch',\n      title = 'SnacksPickerTitle',\n      prompt = 'SnacksPickerPrompt',\n      frecency = 'Number',\n      debug = 'Comment',\n      combo_header = 'Number',\n      scrollbar = 'SnacksPickerBorder',\n      directory_path = 'Comment',\n      grep_line_number = 'Comment',\n    },\n    debug = {\n      enabled = false,\n      show_scores = false,\n    },\n  },\n  -- No need to lazy-load with lazy.nvim.\n  -- This plugin initializes itself lazily.\n  lazy = false,\n  keys = {\n    {\n      \"<leader>f\",\n      function() require('fff').find_files() end,\n      desc = 'FFFind files',\n    },\n    {\n      \"<leader>/\",\n      function() require('fff').live_grep() end,\n      desc = 'LiFFFe grep',\n    },\n    {\n      \"<leader>cc\",\n      function() require('fff').live_grep({ query = vim.fn.expand(\"<cword>\") }) end,\n      desc = 'Search current word',\n    },\n  }\n}\n"
  },
  {
    "path": "nvim/lua/plugins/flash.lua",
    "content": "return {\n  \"folke/flash.nvim\",\n  event = \"VeryLazy\",\n  ---@type Flash.Config\n  opts = {},\n  keys = {\n    { \"gw\", mode = { \"n\", \"x\", \"o\" }, function() require(\"flash\").jump() end, desc = \"Flash\" },\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/flutter-tools.off.lua",
    "content": "return {\n\t\"nvim-flutter/flutter-tools.nvim\",\n  enabled = false,\n\tlazy = false,\n\tdependencies = {\n\t\t\"nvim-lua/plenary.nvim\",\n\t\t\"stevearc/dressing.nvim\", -- optional for vim.ui.select\n\t},\n\tconfig = true,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/gitsigns.lua",
    "content": "return {\n  \"lewis6991/gitsigns.nvim\",\n  cond = function()\n    -- only load on git repo\n    local git_path = vim.uv.cwd() .. \"/.git\"\n    local ok, err = vim.uv.fs_stat(git_path)\n\n    if not ok then\n      local gitignore_path = vim.uv.cwd() .. \"/.gitignore\"\n      ok, err = vim.uv.fs_stat(gitignore_path)\n    end\n\n    return ok and err == nil\n  end,\n  keys = {\n    { \"<leader>gsl\", \"<cmd>Gitsigns blame_line<cr>\", desc = \"Blame Line\" },\n    { \"<leader>gsb\", \"<cmd>Gitsigns blame<cr>\", desc = \"Blame Buffer\" },\n    { \"<leader>gsd\", \"<cmd>Gitsigns diffthis<cr>\", desc = \"Diff This\" },\n    { \"<leader>gss\", \"<cmd>Gitsigns stage_hunk<cr>\", desc = \"Stage Hunk\" },\n    { \"<leader>gsp\", \"<cmd>Gitsigns preview_hunk<cr>\", desc = \"Preview Hunk\" },\n    { \"<leader>gsr\", \"<cmd>Gitsigns reset_hunk<cr>\", desc = \"Reset Hunk\" },\n    { \"<leader>gsR\", \"<cmd>Gitsigns reset_buffer<cr>\", desc = \"Reset Buffer\" },\n    { \"<leader>gsh\", \"<cmd>Gitsigns reset_hunk<cr>\", desc = \"Reset Hunk\" },\n    { \"<leader>gsH\", \"<cmd>Gitsigns reset_buffer<cr>\", desc = \"Reset Buffer\" },\n  },\n  opts = {\n    current_line_blame_formatter = '<author>, <author_time:%R> - <summary>',\n    current_line_blame_opts = {\n      virt_text = true,\n      virt_text_pos = 'eol',\n      virt_text_priority = 100\n    },\n    signs = {\n      add = { text = \"▎\" },\n      change = { text = \"▎\" },\n      delete = { text = \"\" },\n      topdelete = { text = \"\" },\n      changedelete = { text = \"▎\" },\n      untracked = { text = \"▎\" },\n    },\n    signs_staged = {\n      add = { text = \"▎\" },\n      change = { text = \"▎\" },\n      delete = { text = \"\" },\n      topdelete = { text = \"\" },\n      changedelete = { text = \"▎\" },\n    },\n    on_attach = function(buffer)\n      local gs = package.loaded.gitsigns\n\n      local function map(mode, l, r, desc)\n        vim.keymap.set(mode, l, r, { buffer = buffer, desc = desc })\n      end\n\n      -- stylua: ignore start\n      map(\"n\", \"]h\", function()\n        if vim.wo.diff then\n          vim.cmd.normal({ \"]c\", bang = true })\n        else\n          gs.nav_hunk(\"next\")\n        end\n      end, \"Next Hunk\")\n      map(\"n\", \"[h\", function()\n        if vim.wo.diff then\n          vim.cmd.normal({ \"[c\", bang = true })\n        else\n          gs.nav_hunk(\"prev\")\n        end\n      end, \"Prev Hunk\")\n      map(\"n\", \"]H\", function() gs.nav_hunk(\"last\") end, \"Last Hunk\")\n      map(\"n\", \"[H\", function() gs.nav_hunk(\"first\") end, \"First Hunk\")\n    end,\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/lua-ls.lua",
    "content": "return {\n  \"folke/lazydev.nvim\",\n  ft = \"lua\",\n  opts = { }\n}\n"
  },
  {
    "path": "nvim/lua/plugins/markdown.lua",
    "content": "return {\n  \"MeanderingProgrammer/render-markdown.nvim\",\n  dependencies = { 'nvim-treesitter/nvim-treesitter', 'nvim-tree/nvim-web-devicons' },\n  ft = { \"markdown\", \"Avante\", \"codecompanion\" },\n  opts = {\n    file_types = { \"markdown\", \"Avante\", \"codecompanion\" },\n    overrides = {\n      buftype = {\n        nofile = {\n          render_modes = { \"n\", \"c\", \"i\" },\n          debounce = 5,\n          code = {\n            left_pad = 0,\n            right_pad = 0,\n            language_pad = 0,\n          },\n        },\n      },\n      filetype = {},\n    },\n  }\n}\n"
  },
  {
    "path": "nvim/lua/plugins/mason-lsp.lua",
    "content": "return {\n  \"williamboman/mason-lspconfig.nvim\",\n  event = { \"WinEnter\", \"BufRead\", \"BufNewFile\" },\n  dependencies = {\n    \"williamboman/mason.nvim\",\n    \"neovim/nvim-lspconfig\",\n  },\n  opts = {\n    automatic_enable = {\n      exclude = {\n        \"ts_ls\"\n      }\n    }\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/mason.lua",
    "content": "return {\n  \"williamboman/mason.nvim\",\n  cmd = { \"Mason\" },\n  build = \":MasonUpdate\",\n  opts_extend = { \"ensure_installed\" },\n  opts = {\n    ensure_installed = {\n      \"html-lsp\",\n      \"css-lsp\",\n      \"basedpyright\",\n      \"ruff\",\n      \"astro-language-server\",\n      \"tailwindcss-language-server\",\n      \"typescript-language-server\",\n      \"yaml-language-server\",\n      \"lua-language-server\",\n      \"json-lsp\",\n      \"tinymist\"\n    }\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/mini-ai.lua",
    "content": "return {\n  \"echasnovski/mini.ai\",\n  event = \"VeryLazy\",\n  opts = function()\n    local ai = require(\"mini.ai\")\n    return {\n      n_lines = 500,\n      custom_textobjects = {\n        o = ai.gen_spec.treesitter({ -- code block\n          a = { \"@block.outer\", \"@conditional.outer\", \"@loop.outer\" },\n          i = { \"@block.inner\", \"@conditional.inner\", \"@loop.inner\" },\n        }),\n        f = ai.gen_spec.treesitter({ a = \"@function.outer\", i = \"@function.inner\" }), -- function\n        c = ai.gen_spec.treesitter({ a = \"@class.outer\", i = \"@class.inner\" }), -- class\n        t = { \"<([%p%w]-)%f[^<%w][^<>]->.-</%1>\", \"^<.->().*()</[^/]->$\" }, -- tags\n        d = { \"%f[%d]%d+\" }, -- digits\n        e = { -- Word with case\n          { \"%u[%l%d]+%f[^%l%d]\", \"%f[%S][%l%d]+%f[^%l%d]\", \"%f[%P][%l%d]+%f[^%l%d]\", \"^[%l%d]+%f[^%l%d]\" },\n          \"^().*()$\",\n        },\n        u = ai.gen_spec.function_call(), -- u for \"Usage\"\n        U = ai.gen_spec.function_call({ name_pattern = \"[%w_]\" }), -- without dot in function name\n      },\n    }\n  end,\n  config = function(_, opts)\n    require(\"mini.ai\").setup(opts)\n  end,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/mini-surround.lua",
    "content": "return {\n  \"echasnovski/mini.surround\",\n  event = \"VeryLazy\",\n  opts = {\n    -- Module mappings. Use `''` (empty string) to disable one.\n    mappings = {\n      add = 'ma', -- Add surrounding in Normal and Visual modes\n      delete = 'md', -- Delete surrounding\n      find = 'mf', -- Find surrounding (to the right)\n      find_left = 'mF', -- Find surrounding (to the left)\n      highlight = 'mh', -- Highlight surrounding\n      replace = 'mr', -- Replace surrounding\n      update_n_lines = 'sn', -- Update `n_lines`\n\n      suffix_last = 'l', -- Suffix to search with \"prev\" method\n      suffix_next = 'n', -- Suffix to search with \"next\" method\n    },\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/neo-tree.lua",
    "content": "return {\n\t\"nvim-neo-tree/neo-tree.nvim\",\n\tcmd = \"Neotree\",\n\tbranch = \"v3.x\",\n\tdependencies = {\n\t\t\"nvim-lua/plenary.nvim\",\n\t\t\"nvim-tree/nvim-web-devicons\", -- not strictly required, but recommended\n\t\t\"MunifTanjim/nui.nvim\",\n\t\t-- \"3rd/image.nvim\", -- Optional image support in preview window: See `# Preview Mode` for more information\n\t},\n\tdeactivate = function()\n\t\tvim.cmd([[Neotree close]])\n\tend,\n\topts = {\n\t\tsources = { \"filesystem\", \"buffers\", \"git_status\" },\n\t\topen_files_do_not_replace_types = { \"terminal\", \"Trouble\", \"trouble\", \"qf\", \"Outline\" },\n\t\tclose_if_last_window = false,\n\t\tfilesystem = {\n\t\t\tbind_to_cwd = false,\n\t\t\tfollow_current_file = { enabled = true },\n\t\t\tuse_libuv_file_watcher = true,\n\t\t},\n\t\tenable_git_status = true,\n\t\tsort_case_insensitive = false,\n\t\tdefault_component_configs = {\n\t\t\tcontainer = {\n\t\t\t\tenable_character_fade = true,\n\t\t\t},\n\t\t\tindent = {\n\t\t\t\twith_expanders = true, -- if nil and file nesting is enabled, will enable expanders\n\t\t\t\texpander_collapsed = \"\",\n\t\t\t\texpander_expanded = \"\",\n\t\t\t\texpander_highlight = \"NeoTreeExpander\",\n\t\t\t},\n\t\t\tgit_status = {\n\t\t\t\tsymbols = {\n\t\t\t\t\t-- Change type\n\t\t\t\t\tadded = \"\", -- or \"✚\", but this is redundant info if you use git_status_colors on the name\n\t\t\t\t\tmodified = \"\", -- or \"\", but this is redundant info if you use git_status_colors on the name\n\t\t\t\t\tdeleted = \"✖\", -- this can only be used in the git_status source\n\t\t\t\t\trenamed = \"󰁕\", -- this can only be used in the git_status source\n\t\t\t\t\t-- Status type\n\t\t\t\t\tuntracked = \"\",\n\t\t\t\t\tignored = \"\",\n\t\t\t\t\tunstaged = \"󰄱\",\n\t\t\t\t\tstaged = \"\",\n\t\t\t\t\tconflict = \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\twindow = {\n\t\t\tposition = \"right\",\n\t\t\tmappings = {\n\t\t\t\t[\"l\"] = \"open\",\n\t\t\t\t[\"h\"] = \"close_node\",\n\t\t\t\t[\"Y\"] = {\n\t\t\t\t\tfunction(state)\n\t\t\t\t\t\tlocal node = state.tree:get_node()\n\t\t\t\t\t\tlocal path = node:get_id()\n\t\t\t\t\t\tvim.fn.setreg(\"+\", path, \"c\")\n\t\t\t\t\tend,\n\t\t\t\t\tdesc = \"Copy Path to Clipboard\",\n\t\t\t\t},\n\t\t\t\t[\"O\"] = {\n\t\t\t\t\tfunction(state)\n\t\t\t\t\t\trequire(\"lazy.util\").open(state.tree:get_node().path, { system = true })\n\t\t\t\t\tend,\n\t\t\t\t\tdesc = \"Open with System Application\",\n\t\t\t\t},\n\t\t\t\t[\"P\"] = { \"toggle_preview\", config = { use_float = false } },\n\t\t\t},\n\t\t},\n\t},\n\tinit = function()\n\t\t-- FIX: use `autocmd` for lazy-loading neo-tree instead of directly requiring it,\n\t\t-- because `cwd` is not set up properly.\n\t\tvim.api.nvim_create_autocmd(\"BufEnter\", {\n\t\t\tgroup = vim.api.nvim_create_augroup(\"Neotree_start_directory\", { clear = true }),\n\t\t\tdesc = \"Start Neo-tree with directory\",\n\t\t\tonce = true,\n\t\t\tcallback = function()\n\t\t\t\tif package.loaded[\"neo-tree\"] then\n\t\t\t\t\treturn\n\t\t\t\telse\n\t\t\t\t\tlocal stats = vim.uv.fs_stat(vim.fn.argv(0))\n\t\t\t\t\tif stats and stats.type == \"directory\" then\n\t\t\t\t\t\trequire(\"neo-tree\")\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\tend,\n\t\t})\n\tend,\n\tconfig = function(_, opts)\n    local function on_move(data)\n      Snacks.rename.on_rename_file(data.source, data.destination)\n    end\n\n\t\tlocal events = require(\"neo-tree.events\")\n\t\topts.event_handlers = opts.event_handlers or {}\n\t\tvim.list_extend(opts.event_handlers, {\n\t\t\t{ event = events.FILE_MOVED, handler = on_move },\n\t\t\t{ event = events.FILE_RENAMED, handler = on_move },\n\t\t})\n\t\trequire(\"neo-tree\").setup(opts)\n\t\tvim.api.nvim_create_autocmd(\"TermClose\", {\n\t\t\tpattern = \"*lazygit\",\n\t\t\tcallback = function()\n\t\t\t\tif package.loaded[\"neo-tree.sources.git_status\"] then\n\t\t\t\t\trequire(\"neo-tree.sources.git_status\").refresh()\n\t\t\t\tend\n\t\t\tend,\n\t\t})\n\tend,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/nvim-ts-autotag.lua",
    "content": "return {\n  \"windwp/nvim-ts-autotag\",\n  ft = {\n    \"html\",\n    \"xml\",\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\",\n  },\n  opts = {}\n}\n"
  },
  {
    "path": "nvim/lua/plugins/obsidian.lua",
    "content": "return {\n  \"obsidian-nvim/obsidian.nvim\",\n  version = \"*\",\n  ft = \"markdown\",\n  ---@module 'obsidian'\n  ---@type obsidian.config\n  opts = {\n    legacy_commands = false,\n    workspaces = {\n      {\n        name = \"personal-notes\",\n        path = \"~/Development/personal/notes\",\n      },\n    },\n    templates = {\n      folder = \"Templates\",\n      date_format = \"%Y-%m-%d\",\n      time_format = \"%H:%M\",\n      substitutions = {\n        yesterday = function()\n          return os.date(\"%Y-%m-%d\", os.time() - 86400)\n        end,\n        tomorrow = function()\n          return os.date(\"%Y-%m-%d\", os.time() + 86400)\n        end,\n      },\n      -- Auto-put notes in right folders based on template\n      customizations = {\n        Article = {\n          notes_subdir = \"Articles\",\n        },\n        Person = {\n          notes_subdir = \"People\",\n        },\n        Music = {\n          notes_subdir = \"Music\",\n        },\n        Inbox = {\n          notes_subdir = \"Inbox\",\n        },\n      },\n    },\n    daily_notes = {\n      folder = \"Daily\",\n      date_format = \"%Y-%m-%d-%A\",\n      template = \"Daily.md\",\n    },\n  },\n  keys = {\n    { \"<leader>oo\", \"<cmd>Obsidian<cr>\", desc = \"Obsidian Picker\" },\n    { \"<leader>oa\", \"<cmd>Obsidian new_from_template Article<cr>\", desc = \"New Article\" },\n    { \"<leader>op\", \"<cmd>Obsidian new_from_template Person<cr>\", desc = \"New Person\" },\n    { \"<leader>om\", \"<cmd>Obsidian new_from_template Music<cr>\", desc = \"New Music\" },\n    { \"<leader>oi\", \"<cmd>Obsidian new_from_template Inbox<cr>\", desc = \"New Inbox Item\" },\n    { \"<leader>od\", \"<cmd>Obsidian today<cr>\", desc = \"Today's Daily Note\" },\n    { \"<leader>os\", \"<cmd>Obsidian search<cr>\", desc = \"Search Notes\" },\n    { \"<leader>ol\", \"<cmd>Obsidian links<cr>\", desc = \"Show Links\" },\n    { \"<leader>ob\", \"<cmd>Obsidian backlinks<cr>\", desc = \"Show Backlinks\" },\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/pairs.lua",
    "content": "return {\n  'echasnovski/mini.pairs',\n  version = false,\n  event = \"VeryLazy\",\n  opts = {\n    modes = { insert = true, command = true, terminal = false },\n    -- skip autopair when next character is one of these\n    skip_next = [=[[%w%%%'%[%\"%.%`%$]]=],\n    -- skip autopair when the cursor is inside these treesitter nodes\n    skip_ts = { \"string\" },\n    -- skip autopair when next character is closing pair\n    -- and there are more closing pairs than opening pairs\n    skip_unbalanced = true,\n    -- better deal with markdown code blocks\n    markdown = true,\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/qf.lua",
    "content": "return {\n  'stevearc/quicker.nvim',\n  event = \"FileType qf\",\n  ---@module \"quicker\"\n  ---@type quicker.SetupOptions\n  opts = {\n    highlight = {\n      treesitter = true,\n      load_buffers = false,\n      lsp = false,\n    }\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/snacks.lua",
    "content": "return {\n\t\"folke/snacks.nvim\",\n\tpriority = 1000,\n\tlazy = false,\n\tkeys = {\n    -- stylua: ignore start\n\t\t{ \"<leader>z\", function() Snacks.zen() end, desc = \"Toggle Zen Mode\", },\n\t\t{ \"<leader>un\", function() Snacks.notifier.hide() end, desc = \"Dismiss All Notifications\", },\n\t\t{ \"<leader>bd\", function() Snacks.bufdelete() end, desc = \"Delete Buffer\", },\n\t\t{ \"<leader>gg\", function() Snacks.lazygit() end, desc = \"Lazygit\", },\n\t\t{ \"<leader>cR\", function() Snacks.rename() end, desc = \"Rename File\", },\n    { \"<c-/>\",      function() Snacks.terminal() end, desc = \"Toggle Terminal\" },\n\t\t-- stylua: ignore end\n\t},\n\topts = {\n\t\tbigfile = {\n\t\t\tenabled = true,\n\t\t\tnotify = true,\n\t\t},\n\t\tnotifier = {\n\t\t\tenabled = true,\n\t\t\ttimeout = 3000,\n\t\t\tstyle = \"compact\",\n\t\t\ttop_down = false,\n\t\t\tmargin = {\n\t\t\t\tbottom = 1,\n\t\t\t},\n\t\t},\n\t\tquickfile = { enabled = true },\n\t\tindent = {\n\t\t\tenabled = true,\n\t\t\thl = \"SnacksIndent\",\n\t\t\tscope = {\n\t\t\t\tenabled = true,\n\t\t\t\tanimate = {\n\t\t\t\t\tenabled = false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tstatuscolumn = {\n\t\t\tenabled = false,\n\t\t},\n\t\twords = { enabled = true },\n\t\tstyles = {\n\t\t\tnotification = {\n\t\t\t\two = { wrap = true },\n\t\t\t},\n\t\t},\n\t\tpicker = {\n\t\t\tui_select = true,\n\t\t\tlayout = {\n\t\t\t\treverse = false,\n\t\t\t\tlayout = {\n\t\t\t\t\tbox = \"horizontal\",\n\t\t\t\t\tbackdrop = false,\n\t\t\t\t\twidth = 0.9,\n\t\t\t\t\theight = 0.9,\n\t\t\t\t\tborder = \"none\",\n\t\t\t\t\t{\n\t\t\t\t\t\tbox = \"vertical\",\n            border = \"single\",\n            title = \"{title} {live} {flags}\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twin = \"input\",\n\t\t\t\t\t\t\theight = 1,\n\t\t\t\t\t\t\tborder = \"bottom\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twin = \"list\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\twin = \"preview\",\n\t\t\t\t\t\ttitle = \"{preview:Preview}\",\n\t\t\t\t\t\twidth = 0.5,\n\t\t\t\t\t\tborder = \"single\",\n\t\t\t\t\t\ttitle_pos = \"center\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tzen = {\n\t\t\twin = {\n\t\t\t\tenter = true,\n\t\t\t\tfixbuf = true,\n\t\t\t\tminimal = true,\n\t\t\t\twidth = 80,\n\t\t\t\theight = 0.9,\n\t\t\t\tbackdrop = { transparent = false, blend = 99 },\n\t\t\t\tkeys = { q = false },\n\t\t\t\tzindex = 40,\n\t\t\t\tw = {\n\t\t\t\t\tsnacks_main = true,\n\t\t\t\t},\n\t\t\t},\n      on_open = function()\n        vim.wo.signcolumn = \"no\"\n        vim.wo.foldcolumn = \"0\"\n        vim.wo.wrap = true\n        vim.wo.linebreak = true\n      end,\n      toggles = {\n        dim = false,\n      }\n\t\t},\n\t},\n\tinit = function()\n\t\tvim.api.nvim_create_autocmd(\"User\", {\n\t\t\tpattern = \"VeryLazy\",\n\t\t\tcallback = function()\n\t\t\t\t_G.dd = function(...)\n\t\t\t\t\tSnacks.debug.inspect(...)\n\t\t\t\tend\n\t\t\t\t_G.bt = function()\n\t\t\t\t\tSnacks.debug.backtrace()\n\t\t\t\tend\n\t\t\t\tvim.print = _G.dd -- Override print to use snacks for `:=` command\n\n\t\t\t\tSnacks.toggle.inlay_hints():map(\"<leader>uh\")\n\t\t\tend,\n\t\t})\n\tend,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/statuscolumn.lua",
    "content": "return {\n  \"luukvbaal/statuscol.nvim\",\n  dependencies = {\n    \"lewis6991/gitsigns.nvim\",\n  },\n  event = \"BufEnter\",\n  init = function()\n    vim.api.nvim_create_autocmd(\"BufEnter\", {\n      pattern = \"*\",\n      callback = function()\n        -- Skip during startup to prevent intro screen flash\n        if vim.v.vim_did_init == 0 then\n          return\n        end\n        local ft = vim.bo.filetype\n        if ft == \"dbui\" or ft == \"neo-tree\" or ft == \"snacks_notif\" then\n          vim.o.signcolumn = \"no\"\n          vim.o.foldcolumn = \"0\"\n        end\n      end,\n    })\n  end,\n  config = function()\n    local builtin = require(\"statuscol.builtin\")\n    require(\"statuscol\").setup({\n      setopt = true,\n      relculright = true,\n      ft_ignore = { \"dbui\", \"neo-tree\" },\n      bt_ignore = { \"nofile\" },\n      segments = {\n        {\n\n          text = { builtin.foldfunc },\n          click = \"v:lua.ScFa\",\n          hl = \"SignColumn\"\n        },\n        {\n          sign = {\n            namespace = { \"diagnostic/signs\" },\n            maxwidth = 1,\n            auto = false,\n            fillcharhl = \"SignColumn\"\n          },\n          click = \"v:lua.ScSa\",\n          hl = \"SignColumn\"\n        },\n        { text = { builtin.lnumfunc }, click = \"v:lua.ScLa\" },\n        -- extra spacer\n        -- { text = { \" ▕\" } },\n        { text = { \"  \" } },\n        {\n          sign = {\n            namespace = { \"gitsigns\" },\n            name = { \".*\" },\n            maxwidth = 1,\n            auto = false,\n            fillcharhl = \"Normal\"\n          },\n          click = \"v:lua.ScSa\",\n        },\n      },\n    })\n  end,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/supermaven.lua",
    "content": "return {\n  \"supermaven-inc/supermaven-nvim\",\n  opts = {\n    keymaps = {\n      accept_suggestion = \"<Tab>\",\n      clear_suggestion = \"<C-]>\",\n      accept_word = \"<C-j>\",\n    },\n    color = {\n      suggestion_color = \"#5a5a5a\",\n      cterm = 244,\n    },\n    ignore_filetypes = {\n      dbui = true,\n      dbout = true,\n      DressingInput = true,\n      [\"neo-tree-popup\"] = true,\n    }\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/treesitter.lua",
    "content": "return {\n  \"nvim-treesitter/nvim-treesitter\",\n  dependencies = {\n    {\n      \"nvim-treesitter/nvim-treesitter-textobjects\",\n      branch = \"main\",\n    }\n  },\n  branch = \"main\",\n  version = false,\n  build = \":TSUpdate\",\n  lazy = false,\n  keys = {\n    { \"<M-o>\", desc = \"Increment Selection\" },\n    { \"<M-i>\", desc = \"Decrement Selection\", mode = \"x\" },\n  },\n  init = function()\n    local ensureInstalled = {\n      \"bash\",\n      \"c\",\n      \"diff\",\n      \"html\",\n      \"javascript\",\n      \"jsdoc\",\n      \"json\",\n      \"lua\",\n      \"luadoc\",\n      \"luap\",\n      \"markdown\",\n      \"markdown_inline\",\n      \"printf\",\n      \"php\",\n      \"python\",\n      \"query\",\n      \"regex\",\n      \"toml\",\n      \"tsx\",\n      \"typst\",\n      \"typescript\",\n      \"vim\",\n      \"vimdoc\",\n      \"xml\",\n      \"yaml\",\n    }\n    local alreadyInstalled = require('nvim-treesitter.config').get_installed()\n    local parsersToInstall = vim.iter(ensureInstalled)\n        :filter(function(parser)\n          return not vim.tbl_contains(alreadyInstalled, parser)\n        end)\n        :totable()\n    require('nvim-treesitter').install(parsersToInstall)\n\n    vim.api.nvim_create_autocmd('FileType', {\n      callback = function(args)\n        local treesitter = require('nvim-treesitter')\n        local lang = vim.treesitter.language.get_lang(args.match)\n        if vim.list_contains(treesitter.get_available(), lang) then\n          if not vim.list_contains(treesitter.get_installed(), lang) then\n            treesitter.install(lang):wait()\n          end\n          vim.schedule(function()\n            vim.treesitter.start(args.buf)\n          end)\n        end\n      end,\n      desc = \"Enable nvim-treesitter and install parser if not installed\"\n    })\n  end,\n  config = function(_, opts)\n    -- local parser_config = require(\"nvim-treesitter.parsers\").get_parser_configs()\n    -- parser_config.blade = {\n    --   install_info = {\n    --     url = \"https://github.com/EmranMR/tree-sitter-blade\",\n    --     files = { \"src/parser.c\" },\n    --     branch = \"main\",\n    --     generate_requires_npm = true,\n    --     requires_generate_from_grammar = false, -- if folder contains pre-generated src/parser.c\n    --   },\n    --   filetype = \"blade\",\n    -- }\n    -- vim.filetype.add({\n    --   pattern = {\n    --     [\".*%.blade%.php\"] = \"blade\",\n    --   },\n    -- })\n\n    require(\"nvim-treesitter\").setup({\n      sync_install = true,\n      highlight = { enable = true },\n      indent = { enable = true },\n      incremental_selection = {\n        enable = true,\n        keymaps = {\n          init_selection = \"<M-o>\",\n          node_incremental = \"<M-o>\",\n          node_decremental = \"<M-i>\",\n          scope_incremental = false,\n        },\n      },\n      textobjects = {\n        move = {\n          enable = true,\n          goto_next_start = { [\"]f\"] = \"@function.outer\", [\"]c\"] = \"@class.outer\", [\"]a\"] = \"@parameter.inner\" },\n          goto_next_end = { [\"]F\"] = \"@function.outer\", [\"]C\"] = \"@class.outer\", [\"]A\"] = \"@parameter.inner\" },\n          goto_previous_start = {\n            [\"[f\"] = \"@function.outer\",\n            [\"[c\"] = \"@class.outer\",\n            [\"[a\"] = \"@parameter.inner\",\n          },\n          goto_previous_end = { [\"[F\"] = \"@function.outer\", [\"[C\"] = \"@class.outer\", [\"[A\"] = \"@parameter.inner\" },\n        },\n        lookahead = true,\n        keymaps = {\n          -- You can use the capture groups defined in textobjects.scm\n          [\"af\"] = \"@function.outer\",\n          [\"if\"] = \"@function.inner\",\n          [\"ac\"] = \"@class.outer\",\n          -- You can optionally set descriptions to the mappings (used in the desc parameter of\n          -- nvim_buf_set_keymap) which plugins like which-key display\n          [\"ic\"] = { query = \"@class.inner\", desc = \"Select inner part of a class region\" },\n          -- You can also use captures from other query groups like `locals.scm`\n          [\"as\"] = { query = \"@scope\", query_group = \"locals\", desc = \"Select language scope\" },\n        },\n        selection_modes = {\n          [\"@parameter.outer\"] = \"v\", -- charwise\n          [\"@function.outer\"] = \"V\",  -- linewise\n          [\"@class.outer\"] = \"<c-v>\", -- blockwise\n        },\n      },\n    })\n  end,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/ts-comments.lua",
    "content": "return {\n  \"folke/ts-comments.nvim\",\n  opts = {},\n  event = \"VeryLazy\",\n}\n"
  },
  {
    "path": "nvim/lua/plugins/typescript.lua",
    "content": "return {\n  \"pmizio/typescript-tools.nvim\",\n  ft = {\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\",\n  },\n  dependencies = {\n    \"nvim-lua/plenary.nvim\",\n    \"neovim/nvim-lspconfig\"\n  },\n  opts = {\n    settings = {\n      separate_diagnostic_server = true,\n      publish_diagnostic_on = \"insert_leave\",\n      expose_as_code_action = \"all\",\n      tsserver_max_memory = \"auto\",\n      tsserver_file_preferences = {\n        -- includeInlayParameterNameHints = \"all\",\n        -- includeInlayParameterNameHintsWhenArgumentMatchesName = false,\n        -- includeInlayFunctionParameterTypeHints = true,\n        -- includeInlayVariableTypeHints = true,\n        -- includeInlayVariableTypeHintsWhenTypeMatchesName = false,\n        -- includeInlayPropertyDeclarationTypeHints = true,\n        -- includeInlayFunctionLikeReturnTypeHints = true,\n        -- includeInlayEnumMemberValueHints = true,\n\n        includeCompletionsForModuleExports = true,\n        quotePreference = \"auto\",\n      },\n    }\n  },\n}\n"
  },
  {
    "path": "nvim/lua/plugins/typst.lua",
    "content": "return {\n  'chomosuke/typst-preview.nvim',\n  ft = \"typst\",\n  version = '1.*',\n  build = function() require 'typst-preview'.update() end,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/ufo.lua",
    "content": "return {\n\t\"kevinhwang91/nvim-ufo\",\n\tdependencies = {\n\t\t\"kevinhwang91/promise-async\",\n\t},\n\tevent = { \"BufRead\", \"BufNewFile\" },\n\tkeys = {\n    -- stylua: ignore start\n    { \"zm\", function() require(\"ufo\").closeAllFolds() end, desc = \"󱃄 Close All Folds\" },\n    { \"zr\", function()\n      require(\"ufo\").openFoldsExceptKinds { \"comment\", \"imports\" }\n      vim.opt.scrolloff = vim.g.baseScrolloff -- fix scrolloff setting sometimes being off\n    end, desc = \"󱃄 Open All Regular Folds\" },\n    { \"zR\", function() require(\"ufo\").openFoldsExceptKinds {} end, desc = \"󱃄 Open All Folds\" },\n    { \"z1\", function() require(\"ufo\").closeFoldsWith(1) end, desc = \"󱃄 Close L1 Folds\" },\n    { \"z2\", function() require(\"ufo\").closeFoldsWith(2) end, desc = \"󱃄 Close L2 Folds\" },\n    { \"z3\", function() require(\"ufo\").closeFoldsWith(3) end, desc = \"󱃄 Close L3 Folds\" },\n    { \"z4\", function() require(\"ufo\").closeFoldsWith(4) end, desc = \"󱃄 Close L4 Folds\" },\n\t\t-- stylua: ignore end\n\t},\n\topts = {\n\t\tprovider_selector = function(_, ft, _)\n\t\t\t-- INFO some filetypes only allow indent, some only LSP, some only\n\t\t\t-- treesitter. However, ufo only accepts two kinds as priority,\n\t\t\t-- therefore making this function necessary :/\n\t\t\tlocal lspWithOutFolding = { \"markdown\", \"zsh\", \"css\", \"html\", \"python\", \"json\" }\n\t\t\tif vim.tbl_contains(lspWithOutFolding, ft) then\n\t\t\t\treturn { \"treesitter\", \"indent\" }\n\t\t\tend\n\t\t\treturn { \"lsp\", \"indent\" }\n\t\tend,\n\t\tfold_virt_text_handler = function(virtText, lnum, endLnum, width, truncate)\n\t\t\tlocal hlgroup = \"NonText\"\n\t\t\tlocal newVirtText = {}\n\t\t\tlocal suffix = \"   \" .. tostring(endLnum - lnum)\n\t\t\tlocal sufWidth = vim.fn.strdisplaywidth(suffix)\n\t\t\tlocal targetWidth = width - sufWidth\n\t\t\tlocal curWidth = 0\n\t\t\tfor _, chunk in ipairs(virtText) do\n\t\t\t\tlocal chunkText = chunk[1]\n\t\t\t\tlocal chunkWidth = vim.fn.strdisplaywidth(chunkText)\n\t\t\t\tif targetWidth > curWidth + chunkWidth then\n\t\t\t\t\ttable.insert(newVirtText, chunk)\n\t\t\t\telse\n\t\t\t\t\tchunkText = truncate(chunkText, targetWidth - curWidth)\n\t\t\t\t\tlocal hlGroup = chunk[2]\n\t\t\t\t\ttable.insert(newVirtText, { chunkText, hlGroup })\n\t\t\t\t\tchunkWidth = vim.fn.strdisplaywidth(chunkText)\n\t\t\t\t\tif curWidth + chunkWidth < targetWidth then\n\t\t\t\t\t\tsuffix = suffix .. (\" \"):rep(targetWidth - curWidth - chunkWidth)\n\t\t\t\t\tend\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\t\tcurWidth = curWidth + chunkWidth\n\t\t\tend\n\t\t\ttable.insert(newVirtText, { suffix, hlgroup })\n\t\t\treturn newVirtText\n\t\tend,\n\t},\n\tinit = function()\n\t\tvim.o.foldcolumn = \"1\" -- '0' is not bad\n\t\tvim.o.foldlevel = 99 -- Using ufo provider need a large value, feel free to decrease the value\n\t\tvim.o.foldlevelstart = 99\n\t\tvim.o.foldenable = true\n\tend,\n}\n"
  },
  {
    "path": "nvim/lua/plugins/which-key.lua",
    "content": "return {\n  \"folke/which-key.nvim\",\n  lazy = false,\n  keys = {\n    {\n      \"<leader>?\",\n      function() require(\"which-key\").show({ global = false }) end,\n      desc = \"Buffer Local Keymaps (which-key)\",\n    },\n    {\n      \"<leader>gs\",\n      function() require(\"which-key\").show({ group = \"gs\" }) end,\n      desc = \"Gitsigns\",\n    }\n  }\n}\n"
  },
  {
    "path": "nvim/queries/blade/highlights.scm",
    "content": "(directive) @tag\n(directive_start) @tag\n(directive_end) @tag\n(comment) @comment @spell\n"
  },
  {
    "path": "nvim/queries/blade/injections.scm",
    "content": "((text) @injection.content\n    (#not-has-ancestor? @injection.content \"envoy\")\n    (#set! injection.combined)\n    (#set! injection.language php))\n\n((text) @injection.content\n    (#has-ancestor? @injection.content \"envoy\")\n    (#set! injection.combined)\n    (#set! injection.language bash))\n\n((php_only) @injection.content\n    (#set! injection.combined)\n    (#set! injection.language php_only))\n\n((parameter) @injection.content\n    (#set! injection.language php_only))\n"
  },
  {
    "path": "wezterm/wezterm.lua",
    "content": "local wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nlocal mux = wezterm.mux\n\nwezterm.on(\"gui-startup\", function()\n  local _, _, window = mux.spawn_window{}\n  window:gui_window():maximize()\nend)\n\nconfig.font = wezterm.font \"JetBrains Mono\"\nconfig.freetype_load_target = \"Normal\"\nconfig.font_size = 12.5\nconfig.line_height = 1.3\n-- config.color_scheme = 'Vs Code Dark+ (Gogh)'\nconfig.color_scheme = 'rose-pine-dawn'\nconfig.warn_about_missing_glyphs = false\n\nconfig.enable_wayland = true\nconfig.front_end = \"WebGpu\"\n\n-- spawn zellij as the default shell\nconfig.default_prog = { 'zellij'  }\n\nconfig.enable_tab_bar = false\nconfig.window_decorations = \"RESIZE\"\n\nlocal padding = 4\nconfig.window_padding = {\n  top = padding,\n  left = padding,\n  right = padding,\n  bottom = padding,\n}\n\nreturn config\n"
  },
  {
    "path": "yazi/yazi.toml",
    "content": "[mgr]\nshow_hidden = true\nratio = [2, 3, 3]\n"
  },
  {
    "path": "zellij/config.kdl",
    "content": "//\n// THIS FILE WAS AUTOGENERATED BY ZELLIJ, THE PREVIOUS FILE AT THIS LOCATION WAS COPIED TO: /home/elianiva/.config/zellij/config.kdl.bak\n//\n\nkeybinds clear-defaults=true {\n    locked {\n        bind \"Ctrl g\" { SwitchToMode \"normal\"; }\n    }\n    pane {\n        bind \"left\" { MoveFocus \"left\"; }\n        bind \"down\" { MoveFocus \"down\"; }\n        bind \"up\" { MoveFocus \"up\"; }\n        bind \"right\" { MoveFocus \"right\"; }\n        bind \"c\" { SwitchToMode \"renamepane\"; PaneNameInput 0; }\n        bind \"d\" { NewPane \"down\"; SwitchToMode \"normal\"; }\n        bind \"e\" { TogglePaneEmbedOrFloating; SwitchToMode \"normal\"; }\n        bind \"f\" { ToggleFocusFullscreen; SwitchToMode \"normal\"; }\n        bind \"h\" { MoveFocus \"left\"; }\n        bind \"i\" { TogglePanePinned; SwitchToMode \"normal\"; }\n        bind \"j\" { MoveFocus \"down\"; }\n        bind \"k\" { MoveFocus \"up\"; }\n        bind \"l\" { MoveFocus \"right\"; }\n        bind \"n\" { NewPane; SwitchToMode \"normal\"; }\n        bind \"p\" { SwitchFocus; }\n        bind \"Ctrl p\" { SwitchToMode \"normal\"; }\n        bind \"r\" { NewPane \"right\"; SwitchToMode \"normal\"; }\n        bind \"w\" { ToggleFloatingPanes; SwitchToMode \"normal\"; }\n        bind \"z\" { TogglePaneFrames; SwitchToMode \"normal\"; }\n    }\n    tab {\n        bind \"left\" { GoToPreviousTab; }\n        bind \"down\" { GoToNextTab; }\n        bind \"up\" { GoToPreviousTab; }\n        bind \"right\" { GoToNextTab; }\n        bind \"[\" { BreakPaneLeft; SwitchToMode \"normal\"; }\n        bind \"]\" { BreakPaneRight; SwitchToMode \"normal\"; }\n        bind \"b\" { BreakPane; SwitchToMode \"normal\"; }\n        bind \"h\" { GoToPreviousTab; }\n        bind \"j\" { GoToNextTab; }\n        bind \"k\" { GoToPreviousTab; }\n        bind \"l\" { GoToNextTab; }\n        bind \"n\" { NewTab; SwitchToMode \"normal\"; }\n        bind \"r\" { SwitchToMode \"renametab\"; TabNameInput 0; }\n        bind \"s\" { ToggleActiveSyncTab; SwitchToMode \"normal\"; }\n        bind \"Ctrl t\" { SwitchToMode \"normal\"; }\n        bind \"x\" { CloseTab; SwitchToMode \"normal\"; }\n        bind \"tab\" { ToggleTab; }\n    }\n    resize {\n        bind \"left\" { Resize \"Increase left\"; }\n        bind \"down\" { Resize \"Increase down\"; }\n        bind \"up\" { Resize \"Increase up\"; }\n        bind \"right\" { Resize \"Increase right\"; }\n        bind \"+\" { Resize \"Increase\"; }\n        bind \"-\" { Resize \"Decrease\"; }\n        bind \"=\" { Resize \"Increase\"; }\n        bind \"H\" { Resize \"Decrease left\"; }\n        bind \"J\" { Resize \"Decrease down\"; }\n        bind \"K\" { Resize \"Decrease up\"; }\n        bind \"L\" { Resize \"Decrease right\"; }\n        bind \"h\" { Resize \"Increase left\"; }\n        bind \"j\" { Resize \"Increase down\"; }\n        bind \"k\" { Resize \"Increase up\"; }\n        bind \"l\" { Resize \"Increase right\"; }\n        bind \"Ctrl n\" { SwitchToMode \"normal\"; }\n    }\n    move {\n        bind \"left\" { MovePane \"left\"; }\n        bind \"down\" { MovePane \"down\"; }\n        bind \"up\" { MovePane \"up\"; }\n        bind \"right\" { MovePane \"right\"; }\n        bind \"h\" { MovePane \"left\"; }\n        bind \"Ctrl h\" { SwitchToMode \"normal\"; }\n        bind \"j\" { MovePane \"down\"; }\n        bind \"k\" { MovePane \"up\"; }\n        bind \"l\" { MovePane \"right\"; }\n        bind \"n\" { MovePane; }\n        bind \"p\" { MovePaneBackwards; }\n        bind \"tab\" { MovePane; }\n    }\n    scroll {\n        bind \"e\" { EditScrollback; SwitchToMode \"normal\"; }\n        bind \"s\" { SwitchToMode \"entersearch\"; SearchInput 0; }\n    }\n    search {\n        bind \"c\" { SearchToggleOption \"CaseSensitivity\"; }\n        bind \"n\" { Search \"down\"; }\n        bind \"o\" { SearchToggleOption \"WholeWord\"; }\n        bind \"p\" { Search \"up\"; }\n        bind \"w\" { SearchToggleOption \"Wrap\"; }\n    }\n    session {\n        bind \"a\" {\n            LaunchOrFocusPlugin \"zellij:about\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n        bind \"c\" {\n            LaunchOrFocusPlugin \"configuration\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n        bind \"Ctrl o\" { SwitchToMode \"normal\"; }\n        bind \"p\" {\n            LaunchOrFocusPlugin \"plugin-manager\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n        bind \"/\" {\n            LaunchOrFocusPlugin \"zellij:session-manager\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n    }\n    shared_except \"locked\" {\n        bind \"Alt space\" {\n            LaunchOrFocusPlugin \"https://github.com/karimould/zellij-forgot/releases/latest/download/zellij_forgot.wasm\" {\n              floating true\n            }\n        }\n    }\n    shared_except \"locked\" \"scroll\" \"search\" \"tmux\" {\n        bind \"Ctrl b\" { SwitchToMode \"tmux\"; }\n    }\n    shared_except \"locked\" {\n        bind \"Alt +\" { Resize \"Increase\"; }\n        bind \"Alt -\" { Resize \"Decrease\"; }\n        bind \"Alt =\" { Resize \"Increase\"; }\n        bind \"Alt [\" { PreviousSwapLayout; }\n        bind \"Alt ]\" { NextSwapLayout; }\n        bind \"Alt w\" { ToggleFloatingPanes; }\n        bind \"Ctrl G\" { SwitchToMode \"locked\"; }\n        bind \"Alt h\" { MoveFocusOrTab \"left\"; }\n        bind \"Alt left\" { MoveFocusOrTab \"left\"; }\n        bind \"Alt j\" { MoveFocus \"down\"; }\n        bind \"Alt k\" { MoveFocus \"up\"; }\n        bind \"Alt l\" { MoveFocusOrTab \"right\"; }\n        bind \"Alt right\" { MoveFocusOrTab \"right\"; }\n        bind \"Alt i\" { MoveTab \"left\"; }\n        bind \"Alt o\" { MoveTab \"right\"; }\n        bind \"Alt n\" { NewPane; }\n        bind \"Alt 1\" { GoToTab 1; }\n        bind \"Alt 2\" { GoToTab 2; }\n        bind \"Alt 3\" { GoToTab 3; }\n        bind \"Alt 4\" { GoToTab 4; }\n        bind \"Alt 5\" { GoToTab 5; }\n        bind \"Alt 6\" { GoToTab 6; }\n        bind \"Alt 7\" { GoToTab 7; }\n        bind \"Alt 8\" { GoToTab 8; }\n        bind \"Alt 9\" { GoToTab 9; }\n    }\n    shared_except \"normal\" \"locked\" \"move\" {\n        bind \"Ctrl h\" { SwitchToMode \"move\"; }\n    }\n    shared_except \"normal\" \"locked\" \"session\" {\n        bind \"Ctrl o\" { SwitchToMode \"session\"; }\n    }\n    shared_except \"normal\" \"locked\" \"entersearch\" {\n        bind \"enter\" { SwitchToMode \"normal\"; }\n    }\n    shared_except \"normal\" \"locked\" \"entersearch\" \"renametab\" \"renamepane\" {\n        bind \"esc\" { SwitchToMode \"normal\"; }\n    }\n    shared_except \"normal\" \"locked\" \"scroll\" \"search\" {\n        bind \"Ctrl s\" { SwitchToMode \"scroll\"; }\n    }\n    shared_except \"normal\" \"locked\" \"tab\" {\n        bind \"Ctrl t\" { SwitchToMode \"tab\"; }\n    }\n    shared_except \"normal\" \"locked\" \"pane\" {\n        bind \"Ctrl p\" { SwitchToMode \"pane\"; }\n    }\n    shared_except \"normal\" \"locked\" \"resize\" {\n        bind \"Ctrl n\" { SwitchToMode \"resize\"; }\n    }\n    shared_among \"pane\" \"tmux\" {\n        bind \"x\" { CloseFocus; SwitchToMode \"normal\"; }\n    }\n    shared_among \"tab\" \"tmux\" {\n        bind \"1\" { GoToTab 1; SwitchToMode \"normal\"; }\n        bind \"2\" { GoToTab 2; SwitchToMode \"normal\"; }\n        bind \"3\" { GoToTab 3; SwitchToMode \"normal\"; }\n        bind \"4\" { GoToTab 4; SwitchToMode \"normal\"; }\n        bind \"5\" { GoToTab 5; SwitchToMode \"normal\"; }\n        bind \"6\" { GoToTab 6; SwitchToMode \"normal\"; }\n        bind \"7\" { GoToTab 7; SwitchToMode \"normal\"; }\n        bind \"8\" { GoToTab 8; SwitchToMode \"normal\"; }\n        bind \"9\" { GoToTab 9; SwitchToMode \"normal\"; }\n    }\n    shared_among \"scroll\" \"search\" {\n        bind \"PageDown\" { PageScrollDown; }\n        bind \"PageUp\" { PageScrollUp; }\n        bind \"left\" { PageScrollUp; }\n        bind \"down\" { ScrollDown; }\n        bind \"up\" { ScrollUp; }\n        bind \"right\" { PageScrollDown; }\n        bind \"Ctrl b\" { PageScrollUp; }\n        bind \"Ctrl c\" { ScrollToBottom; SwitchToMode \"normal\"; }\n        bind \"d\" { HalfPageScrollDown; }\n        bind \"Ctrl f\" { PageScrollDown; }\n        bind \"h\" { PageScrollUp; }\n        bind \"j\" { ScrollDown; }\n        bind \"k\" { ScrollUp; }\n        bind \"l\" { PageScrollDown; }\n        bind \"Ctrl s\" { SwitchToMode \"normal\"; }\n        bind \"u\" { HalfPageScrollUp; }\n    }\n    entersearch {\n        bind \"Ctrl c\" { SwitchToMode \"scroll\"; }\n        bind \"esc\" { SwitchToMode \"scroll\"; }\n        bind \"enter\" { SwitchToMode \"search\"; }\n    }\n    renametab {\n        bind \"esc\" { UndoRenameTab; SwitchToMode \"tab\"; }\n    }\n    shared_among \"renametab\" \"renamepane\" {\n        bind \"Ctrl c\" { SwitchToMode \"normal\"; }\n    }\n    renamepane {\n        bind \"esc\" { UndoRenamePane; SwitchToMode \"pane\"; }\n    }\n    shared_among \"session\" \"tmux\" {\n        bind \"d\" { Detach; }\n        bind \"w\" {\n            LaunchOrFocusPlugin \"session-manager\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n    }\n    tmux {\n        bind \"left\" { MoveFocus \"left\"; SwitchToMode \"normal\"; }\n        bind \"down\" { MoveFocus \"down\"; SwitchToMode \"normal\"; }\n        bind \"up\" { MoveFocus \"up\"; SwitchToMode \"normal\"; }\n        bind \"right\" { MoveFocus \"right\"; SwitchToMode \"normal\"; }\n        bind \"space\" { NextSwapLayout; }\n        bind \"\\\"\" { NewPane \"down\"; SwitchToMode \"normal\"; }\n        bind \"%\" { NewPane \"right\"; SwitchToMode \"normal\"; }\n        bind \",\" { SwitchToMode \"renametab\"; }\n        bind \"[\" { SwitchToMode \"scroll\"; }\n        bind \"?\" {\n            LaunchOrFocusPlugin \"zellij:configuration\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n        bind \"Ctrl b\" { Write 2; SwitchToMode \"normal\"; }\n        bind \"c\" { NewTab; SwitchToMode \"normal\"; }\n        bind \"h\" { MoveFocus \"left\"; SwitchToMode \"normal\"; }\n        bind \"j\" { MoveFocus \"down\"; SwitchToMode \"normal\"; }\n        bind \"k\" { MoveFocus \"up\"; SwitchToMode \"normal\"; }\n        bind \"l\" { MoveFocus \"right\"; SwitchToMode \"normal\"; }\n        bind \"n\" { GoToNextTab; SwitchToMode \"normal\"; }\n        bind \"o\" { FocusNextPane; }\n        bind \"p\" { GoToPreviousTab; SwitchToMode \"normal\"; }\n        bind \"s\" {\n            LaunchOrFocusPlugin \"zellij:session-manager\" {\n                floating true\n                move_to_focused_tab true\n            }\n            SwitchToMode \"normal\"\n        }\n        bind \"z\" { ToggleFocusFullscreen; SwitchToMode \"normal\"; }\n    }\n}\n\n// Plugin aliases - can be used to change the implementation of Zellij\n// changing these requires a restart to take effect\nplugins {\n    about location=\"zellij:about\"\n    compact-bar location=\"zellij:compact-bar\"\n    configuration location=\"zellij:configuration\"\n    filepicker location=\"zellij:strider\" {\n        cwd \"/\"\n    }\n    plugin-manager location=\"zellij:plugin-manager\"\n    session-manager location=\"zellij:session-manager\"\n    status-bar location=\"zellij:status-bar\"\n    strider location=\"zellij:strider\"\n    tab-bar location=\"zellij:tab-bar\"\n    welcome-screen location=\"zellij:session-manager\" {\n        welcome_screen true\n    }\n}\n\n// Plugins to load in the background when a new session starts\n// eg. \"file:/path/to/my-plugin.wasm\"\n// eg. \"https://example.com/my-plugin.wasm\"\nload_plugins {\n}\n\n// Use a simplified UI without special fonts (arrow glyphs)\n// Options:\n//   - true\n//   - false (Default)\n//\nsimplified_ui true\n\n// Choose the theme that is specified in the themes section.\n// Default: default\n//\ntheme \"rose-pine-dawn\"\n\n// Choose the base input mode of zellij.\n// Default: normal\n//\n// default_mode \"locked\"\n\n// Choose the path to the default shell that zellij will use for opening new panes\n// Default: $SHELL\n//\ndefault_shell \"nu\"\n\n// Choose the path to override cwd that zellij will use for opening new panes\n//\n// default_cwd \"/tmp\"\n\n// The name of the default layout to load on startup\n// Default: \"default\"\n//\ndefault_layout \"plain\"\n\n// The folder in which Zellij will look for layouts\n// (Requires restart)\n//\n// layout_dir \"/tmp\"\n\n// The folder in which Zellij will look for themes\n// (Requires restart)\n//\n// theme_dir \"/tmp\"\n\n// Toggle enabling the mouse mode.\n// On certain configurations, or terminals this could\n// potentially interfere with copying text.\n// Options:\n//   - true (default)\n//   - false\n//\n// mouse_mode false\n\n// Toggle having pane frames around the panes\n// Options:\n//   - true (default, enabled)\n//   - false\n//\npane_frames false\n\n// When attaching to an existing session with other users,\n// should the session be mirrored (true)\n// or should each user have their own cursor (false)\n// (Requires restart)\n// Default: false\n//\n// mirror_session true\n\n// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP\n// eg. when terminal window with an active zellij session is closed\n// (Requires restart)\n// Options:\n//   - detach (Default)\n//   - quit\n//\n// on_force_close \"quit\"\n\n// Configure the scroll back buffer size\n// This is the number of lines zellij stores for each pane in the scroll back\n// buffer. Excess number of lines are discarded in a FIFO fashion.\n// (Requires restart)\n// Valid values: positive integers\n// Default value: 10000\n//\n// scroll_buffer_size 10000\n\n// Provide a command to execute when copying text. The text will be piped to\n// the stdin of the program to perform the copy. This can be used with\n// terminal emulators which do not support the OSC 52 ANSI control sequence\n// that will be used by default if this option is not set.\n// Examples:\n//\n// copy_command \"xclip -selection clipboard\" // x11\n// copy_command \"wl-copy\"                    // wayland\n// copy_command \"pbcopy\"                     // osx\n//\n// copy_command \"pbcopy\"\n\n// Choose the destination for copied text\n// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.\n// Does not apply when using copy_command.\n// Options:\n//   - system (default)\n//   - primary\n//\n// copy_clipboard \"primary\"\n\n// Enable automatic copying (and clearing) of selection when releasing mouse\n// Default: true\n//\n// copy_on_select true\n\n// Path to the default editor to use to edit pane scrollbuffer\n// Default: $EDITOR or $VISUAL\n// scrollback_editor \"/usr/bin/vim\"\n\n// A fixed name to always give the Zellij session.\n// Consider also setting `attach_to_session true,`\n// otherwise this will error if such a session exists.\n// Default: <RANDOM>\n//\n// session_name \"My singleton session\"\n\n// When `session_name` is provided, attaches to that session\n// if it is already running or creates it otherwise.\n// Default: false\n//\n// attach_to_session true\n\n// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible\n// Options:\n//   - true (default)\n//   - false\n//\n// auto_layout false\n\n// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected\n// Options:\n//   - true (default)\n//   - false\n//\nsession_serialization true\n\n// Whether pane viewports are serialized along with the session, default is false\n// Options:\n//   - true\n//   - false (default)\n//\n// serialize_pane_viewport false\n\n// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0\n// defaults to the scrollback size. If this number is higher than the scrollback size, it will\n// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.\n//\n// scrollback_lines_to_serialize 10000\n\n// Enable or disable the rendering of styled and colored underlines (undercurl).\n// May need to be disabled for certain unsupported terminals\n// (Requires restart)\n// Default: true\n//\n// styled_underlines false\n\n// How often in seconds sessions are serialized\n//\n// serialization_interval 10000\n\n// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know\n// metadata info on this session)\n// (Requires restart)\n// Default: false\n//\n// disable_session_metadata false\n\n// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)\n// (Requires restart)\n// Default: true (if the host terminal supports it)\n//\n// support_kitty_keyboard_protocol false\n\n// Whether to stack panes when resizing beyond a certain size\n// Default: true\n//\n// stacked_resize false\n\n// Whether to show tips on startup\n// Default: true\n//\nshow_startup_tips false\n\n// Whether to show release notes on first version run\n// Default: true\n//\n// show_release_notes false\n"
  },
  {
    "path": "zellij/layouts/plain.kdl",
    "content": "layout {\n    default_tab_template {\n        children\n        pane size=1 borderless=true {\n            plugin location=\"https://github.com/dj95/zjstatus/releases/latest/download/zjstatus.wasm\" {\n                // rose pine\n                // color_fg      \"#e0def4\"\n                // color_fg_alt  \"#6e6a86\"\n                // color_bg      \"#191724\"\n                // color_bg_alt  \"#26233a\"\n                // color_black   \"#26233a\"\n                // color_red     \"#eb6f92\"\n                // color_green   \"#31748f\"\n                // color_yellow  \"#f6c177\"\n                // color_blue    \"#9ccfd8\"\n                // color_magenta \"#c4a7e7\"\n                // color_cyan    \"#ebbcba\"\n                // color_white   \"#e0def4\"\n                // color_orange  \"#f6c177\"\n\n                // rose pine dawn\n                color_bg      \"#faf4ed\"\n                color_bg_alt  \"#f2e9e1\"\n                color_fg      \"#575279\"\n                color_fg_alt  \"#c3bec1\"\n                color_black   \"#575279\"\n                color_red     \"#eb6f92\"\n                color_green   \"#31748f\"\n                color_yellow  \"#f6c177\"\n                color_blue    \"#9ccfd8\"\n                color_magenta \"#c4a7e7\"\n                color_cyan    \"#ebbcba\"\n                color_white   \"#e0def4\"\n                color_orange  \"#f6c177\"\n\n                datetime        \"#[fg=$fg_alt] {format} \"\n                datetime_format \"%Y-%m-%d %H:%M\"\n                datetime_timezone \"Asia/Jakarta\"\n\n                format_left   \"{mode}\"\n                format_center \"#[bg=$bg]{tabs}\"\n                format_right  \"{datetime}#[fg=$fg_alt]@ {session} \"\n                format_space  \"#[bg=$bg]\"\n                format_hide_on_overlength \"true\"\n                format_precedence \"crl\"\n\n                border_enabled  \"false\"\n                border_char     \"─\"\n                border_format   \"#[fg=#6C7086]{char}\"\n                border_position \"top\"\n\n                // hide_frame_for_single_pane \"false\"\n\n                mode_normal        \"#[fg=$green,bold]  NORMAL \"\n                mode_locked        \"#[fg=$red,bold]  LOCKED \"\n                mode_resize        \"#[fg=$blue,bold] 󰆏 RESIZE \"\n                mode_pane          \"#[fg=$blue,bold] 󰏫 PANE \"\n                mode_tab           \"#[fg=$yellow,bold] 󰓩 TAB \"\n                mode_scroll        \"#[fg=$blue,bold] 󰇙 SCROLL \"\n                mode_enter_search  \"#[fg=$orange,bold] 󰥻 ENT-SEARCH \"\n                mode_search        \"#[fg=$orange,bold] 󰥻 SEARCH \"\n                mode_rename_tab    \"#[fg=$yellow,bold] 󰏬 RENAME-TAB \"\n                mode_rename_pane   \"#[fg=$blue,bold] 󰏬 RENAME-PANE \"\n                mode_session       \"#[fg=$blue,bold] 󰆍 SESSION \"\n                mode_move          \"#[fg=$blue,bold] 󰁔 MOVE \"\n                mode_prompt        \"#[fg=$blue,bold] 󰘳 PROMPT \"\n                mode_tmux          \"#[fg=$magenta,bold]  TMUX \"\n\n                tab_normal                 \"#[fg=$fg_alt] {index}:{name} {floating_indicator}\"\n                tab_normal_fullscreen      \"#[fg=$fg_alt] {index}:{name} {fullscreen_indicator}\"\n                tab_normal_sync            \"#[fg=$fg_alt] {index}:{name} {sync_indicator}\"\n\n                tab_active                 \"#[fg=$red,bold] {index}:{name} {floating_indicator}\"\n                tab_active_fullscreen      \"#[fg=$red,bold] {index}:{name} {fullscreen_indicator}\"\n                tab_active_sync            \"#[fg=$red,bold] {index}:{name} {sync_indicator}\"\n\n                // separator between the tabs\n                tab_separator           \"#[bg=$bg,fg=$fg_alt]|\"\n\n                // indicators\n                tab_sync_indicator       \" \"\n                tab_fullscreen_indicator \" 󰊓\"\n                tab_floating_indicator   \" 󰹙\"\n\n                command_git_branch_command     \"git rev-parse --abbrev-ref HEAD\"\n                command_git_branch_format      \"#[fg=$blue] {stdout} \"\n                command_git_branch_interval    \"10\"\n                command_git_branch_rendermode  \"static\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "zellij/themes/rose-pine-dawn.kdl",
    "content": "themes {\n\trose-pine-dawn {\n\t\tbg \"#faf4ed\"\n\t\tfg \"#575279\"\n\t\tred \"#b4637a\"\n\t\tgreen \"#286983\"\n\t\tblue \"#56949f\"\n\t\tyellow \"#ea9d34\"\n\t\tmagenta \"#907aa9\"\n\t\torange \"#fe640b\"\n\t\tcyan \"#d7827e\"\n\t\tblack \"#f2e9e1\"\n\t\twhite \"#575279\"\n\t}\n}\n"
  },
  {
    "path": "zellij/themes/rose-pine.kdl",
    "content": "themes {\n\trose-pine {\n\t\tbg \"#403d52\"\n\t\tfg \"#e0def4\"\n\t\tred \"#eb6f92\"\n\t\tgreen \"#31748f\"\n\t\tblue \"#9ccfd8\"\n\t\tyellow \"#f6c177\"\n\t\tmagenta \"#c4a7e7\"\n\t\torange \"#fe640b\"\n\t\tcyan \"#ebbcba\"\n\t\tblack \"#26233a\"\n\t\twhite \"#e0def4\"\n\t}\n}\n"
  }
]