Full Code of YishenTu/claudian for AI

main d171f1781ee6 cached
375 files
3.7 MB
985.8k tokens
2313 symbols
2 requests
Download .txt
Showing preview only (3,936K chars total). Download the full file or copy to clipboard to get everything.
Repository: YishenTu/claudian
Branch: main
Commit: d171f1781ee6
Files: 375
Total size: 3.7 MB

Directory structure:
gitextract_g0fawvw1/

├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── claude-code-review.yml
│       ├── claude.yml
│       ├── duplicate-issues.yml
│       ├── release.yml
│       └── stale.yml
├── .gitignore
├── .npmrc
├── AGENTS.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── scripts/
│   ├── build-css.mjs
│   ├── build.mjs
│   ├── postinstall.mjs
│   ├── run-jest.js
│   └── sync-version.js
├── src/
│   ├── core/
│   │   ├── CLAUDE.md
│   │   ├── agent/
│   │   │   ├── ClaudianService.ts
│   │   │   ├── MessageChannel.ts
│   │   │   ├── QueryOptionsBuilder.ts
│   │   │   ├── SessionManager.ts
│   │   │   ├── customSpawn.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── agents/
│   │   │   ├── AgentManager.ts
│   │   │   ├── AgentStorage.ts
│   │   │   └── index.ts
│   │   ├── commands/
│   │   │   ├── builtInCommands.ts
│   │   │   └── index.ts
│   │   ├── hooks/
│   │   │   ├── SecurityHooks.ts
│   │   │   ├── SubagentHooks.ts
│   │   │   └── index.ts
│   │   ├── mcp/
│   │   │   ├── McpServerManager.ts
│   │   │   ├── McpTester.ts
│   │   │   └── index.ts
│   │   ├── plugins/
│   │   │   ├── PluginManager.ts
│   │   │   └── index.ts
│   │   ├── prompts/
│   │   │   ├── inlineEdit.ts
│   │   │   ├── instructionRefine.ts
│   │   │   ├── mainAgent.ts
│   │   │   └── titleGeneration.ts
│   │   ├── sdk/
│   │   │   ├── index.ts
│   │   │   ├── toolResultContent.ts
│   │   │   ├── transformSDKMessage.ts
│   │   │   ├── typeGuards.ts
│   │   │   └── types.ts
│   │   ├── security/
│   │   │   ├── ApprovalManager.ts
│   │   │   ├── BashPathValidator.ts
│   │   │   ├── BlocklistChecker.ts
│   │   │   └── index.ts
│   │   ├── storage/
│   │   │   ├── AgentVaultStorage.ts
│   │   │   ├── CCSettingsStorage.ts
│   │   │   ├── ClaudianSettingsStorage.ts
│   │   │   ├── McpStorage.ts
│   │   │   ├── SessionStorage.ts
│   │   │   ├── SkillStorage.ts
│   │   │   ├── SlashCommandStorage.ts
│   │   │   ├── StorageService.ts
│   │   │   ├── VaultFileAdapter.ts
│   │   │   ├── index.ts
│   │   │   └── migrationConstants.ts
│   │   ├── tools/
│   │   │   ├── index.ts
│   │   │   ├── todo.ts
│   │   │   ├── toolIcons.ts
│   │   │   ├── toolInput.ts
│   │   │   └── toolNames.ts
│   │   └── types/
│   │       ├── agent.ts
│   │       ├── chat.ts
│   │       ├── diff.ts
│   │       ├── index.ts
│   │       ├── mcp.ts
│   │       ├── models.ts
│   │       ├── plugins.ts
│   │       ├── sdk.ts
│   │       ├── settings.ts
│   │       └── tools.ts
│   ├── features/
│   │   ├── chat/
│   │   │   ├── CLAUDE.md
│   │   │   ├── ClaudianView.ts
│   │   │   ├── constants.ts
│   │   │   ├── controllers/
│   │   │   │   ├── BrowserSelectionController.ts
│   │   │   │   ├── CanvasSelectionController.ts
│   │   │   │   ├── ConversationController.ts
│   │   │   │   ├── InputController.ts
│   │   │   │   ├── NavigationController.ts
│   │   │   │   ├── SelectionController.ts
│   │   │   │   ├── StreamController.ts
│   │   │   │   ├── contextRowVisibility.ts
│   │   │   │   └── index.ts
│   │   │   ├── rendering/
│   │   │   │   ├── DiffRenderer.ts
│   │   │   │   ├── InlineAskUserQuestion.ts
│   │   │   │   ├── InlineExitPlanMode.ts
│   │   │   │   ├── MessageRenderer.ts
│   │   │   │   ├── SubagentRenderer.ts
│   │   │   │   ├── ThinkingBlockRenderer.ts
│   │   │   │   ├── TodoListRenderer.ts
│   │   │   │   ├── ToolCallRenderer.ts
│   │   │   │   ├── WriteEditRenderer.ts
│   │   │   │   ├── collapsible.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── todoUtils.ts
│   │   │   ├── rewind.ts
│   │   │   ├── services/
│   │   │   │   ├── BangBashService.ts
│   │   │   │   ├── InstructionRefineService.ts
│   │   │   │   ├── SubagentManager.ts
│   │   │   │   └── TitleGenerationService.ts
│   │   │   ├── state/
│   │   │   │   ├── ChatState.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── tabs/
│   │   │   │   ├── Tab.ts
│   │   │   │   ├── TabBar.ts
│   │   │   │   ├── TabManager.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   └── ui/
│   │   │       ├── BangBashModeManager.ts
│   │   │       ├── FileContext.ts
│   │   │       ├── ImageContext.ts
│   │   │       ├── InputToolbar.ts
│   │   │       ├── InstructionModeManager.ts
│   │   │       ├── NavigationSidebar.ts
│   │   │       ├── StatusPanel.ts
│   │   │       ├── file-context/
│   │   │       │   ├── state/
│   │   │       │   │   └── FileContextState.ts
│   │   │       │   └── view/
│   │   │       │       └── FileChipsView.ts
│   │   │       └── index.ts
│   │   ├── inline-edit/
│   │   │   ├── InlineEditService.ts
│   │   │   └── ui/
│   │   │       └── InlineEditModal.ts
│   │   └── settings/
│   │       ├── ClaudianSettings.ts
│   │       ├── keyboardNavigation.ts
│   │       └── ui/
│   │           ├── AgentSettings.ts
│   │           ├── EnvSnippetManager.ts
│   │           ├── McpServerModal.ts
│   │           ├── McpSettingsManager.ts
│   │           ├── McpTestModal.ts
│   │           ├── PluginSettingsManager.ts
│   │           └── SlashCommandSettings.ts
│   ├── i18n/
│   │   ├── constants.ts
│   │   ├── i18n.ts
│   │   ├── index.ts
│   │   ├── locales/
│   │   │   ├── de.json
│   │   │   ├── en.json
│   │   │   ├── es.json
│   │   │   ├── fr.json
│   │   │   ├── ja.json
│   │   │   ├── ko.json
│   │   │   ├── pt.json
│   │   │   ├── ru.json
│   │   │   ├── zh-CN.json
│   │   │   └── zh-TW.json
│   │   └── types.ts
│   ├── main.ts
│   ├── shared/
│   │   ├── components/
│   │   │   ├── ResumeSessionDropdown.ts
│   │   │   ├── SelectableDropdown.ts
│   │   │   ├── SelectionHighlight.ts
│   │   │   └── SlashCommandDropdown.ts
│   │   ├── icons.ts
│   │   ├── index.ts
│   │   ├── mention/
│   │   │   ├── MentionDropdownController.ts
│   │   │   ├── VaultMentionCache.ts
│   │   │   ├── VaultMentionDataProvider.ts
│   │   │   └── types.ts
│   │   └── modals/
│   │       ├── ConfirmModal.ts
│   │       ├── ForkTargetModal.ts
│   │       └── InstructionConfirmModal.ts
│   ├── style/
│   │   ├── CLAUDE.md
│   │   ├── accessibility.css
│   │   ├── base/
│   │   │   ├── animations.css
│   │   │   ├── container.css
│   │   │   └── variables.css
│   │   ├── components/
│   │   │   ├── code.css
│   │   │   ├── context-footer.css
│   │   │   ├── header.css
│   │   │   ├── history.css
│   │   │   ├── input.css
│   │   │   ├── messages.css
│   │   │   ├── nav-sidebar.css
│   │   │   ├── status-panel.css
│   │   │   ├── subagent.css
│   │   │   ├── tabs.css
│   │   │   ├── thinking.css
│   │   │   └── toolcalls.css
│   │   ├── features/
│   │   │   ├── ask-user-question.css
│   │   │   ├── diff.css
│   │   │   ├── file-context.css
│   │   │   ├── file-link.css
│   │   │   ├── image-context.css
│   │   │   ├── image-embed.css
│   │   │   ├── image-modal.css
│   │   │   ├── inline-edit.css
│   │   │   ├── plan-mode.css
│   │   │   ├── resume-session.css
│   │   │   └── slash-commands.css
│   │   ├── index.css
│   │   ├── modals/
│   │   │   ├── fork-target.css
│   │   │   ├── instruction.css
│   │   │   └── mcp-modal.css
│   │   ├── settings/
│   │   │   ├── agent-settings.css
│   │   │   ├── base.css
│   │   │   ├── env-snippets.css
│   │   │   ├── mcp-settings.css
│   │   │   ├── plugin-settings.css
│   │   │   └── slash-settings.css
│   │   └── toolbar/
│   │       ├── external-context.css
│   │       ├── mcp-selector.css
│   │       ├── model-selector.css
│   │       ├── permission-toggle.css
│   │       └── thinking-selector.css
│   └── utils/
│       ├── agent.ts
│       ├── browser.ts
│       ├── canvas.ts
│       ├── claudeCli.ts
│       ├── context.ts
│       ├── contextMentionResolver.ts
│       ├── date.ts
│       ├── diff.ts
│       ├── editor.ts
│       ├── env.ts
│       ├── externalContext.ts
│       ├── externalContextScanner.ts
│       ├── fileLink.ts
│       ├── frontmatter.ts
│       ├── imageEmbed.ts
│       ├── inlineEdit.ts
│       ├── interrupt.ts
│       ├── markdown.ts
│       ├── mcp.ts
│       ├── path.ts
│       ├── sdkSession.ts
│       ├── session.ts
│       ├── slashCommand.ts
│       └── subagentJsonl.ts
├── tests/
│   ├── __mocks__/
│   │   ├── claude-agent-sdk.ts
│   │   └── obsidian.ts
│   ├── helpers/
│   │   ├── mockElement.ts
│   │   └── sdkMessages.ts
│   ├── integration/
│   │   ├── core/
│   │   │   ├── agent/
│   │   │   │   └── ClaudianService.test.ts
│   │   │   └── mcp/
│   │   │       └── mcp.test.ts
│   │   ├── features/
│   │   │   └── chat/
│   │   │       └── imagePersistence.test.ts
│   │   └── main.test.ts
│   ├── tsconfig.json
│   └── unit/
│       ├── core/
│       │   ├── agent/
│       │   │   ├── ClaudianService.test.ts
│       │   │   ├── MessageChannel.test.ts
│       │   │   ├── QueryOptionsBuilder.test.ts
│       │   │   ├── SessionManager.test.ts
│       │   │   ├── customSpawn.test.ts
│       │   │   ├── index.test.ts
│       │   │   └── types.test.ts
│       │   ├── agents/
│       │   │   ├── AgentManager.test.ts
│       │   │   ├── AgentStorage.test.ts
│       │   │   └── index.test.ts
│       │   ├── commands/
│       │   │   └── builtInCommands.test.ts
│       │   ├── hooks/
│       │   │   ├── SecurityHooks.test.ts
│       │   │   └── SubagentHooks.test.ts
│       │   ├── mcp/
│       │   │   ├── McpServerManager.test.ts
│       │   │   ├── McpTester.test.ts
│       │   │   └── createNodeFetch.test.ts
│       │   ├── plugins/
│       │   │   ├── PluginManager.test.ts
│       │   │   └── index.test.ts
│       │   ├── prompts/
│       │   │   ├── instructionRefine.test.ts
│       │   │   ├── systemPrompt.test.ts
│       │   │   └── titleGeneration.test.ts
│       │   ├── sdk/
│       │   │   ├── transformSDKMessage.test.ts
│       │   │   └── typeGuards.test.ts
│       │   ├── security/
│       │   │   ├── ApprovalManager.test.ts
│       │   │   ├── BashPathValidator.test.ts
│       │   │   └── BlocklistChecker.test.ts
│       │   ├── storage/
│       │   │   ├── AgentVaultStorage.test.ts
│       │   │   ├── CCSettingsStorage.test.ts
│       │   │   ├── ClaudianSettingsStorage.test.ts
│       │   │   ├── McpStorage.test.ts
│       │   │   ├── SessionStorage.test.ts
│       │   │   ├── SkillStorage.test.ts
│       │   │   ├── SlashCommandStorage.test.ts
│       │   │   ├── VaultFileAdapter.test.ts
│       │   │   ├── migrationConstants.test.ts
│       │   │   ├── storage.test.ts
│       │   │   ├── storageService.convenience.test.ts
│       │   │   └── storageService.migration.test.ts
│       │   ├── tools/
│       │   │   ├── todo.test.ts
│       │   │   ├── toolIcons.test.ts
│       │   │   ├── toolInput.test.ts
│       │   │   └── toolNames.test.ts
│       │   └── types/
│       │       ├── mcp.test.ts
│       │       └── types.test.ts
│       ├── features/
│       │   ├── chat/
│       │   │   ├── controllers/
│       │   │   │   ├── BrowserSelectionController.test.ts
│       │   │   │   ├── CanvasSelectionController.test.ts
│       │   │   │   ├── ConversationController.test.ts
│       │   │   │   ├── InputController.test.ts
│       │   │   │   ├── NavigationController.test.ts
│       │   │   │   ├── SelectionController.test.ts
│       │   │   │   ├── StreamController.test.ts
│       │   │   │   ├── contextRowVisibility.test.ts
│       │   │   │   └── index.test.ts
│       │   │   ├── rendering/
│       │   │   │   ├── DiffRenderer.test.ts
│       │   │   │   ├── InlineAskUserQuestion.test.ts
│       │   │   │   ├── InlineExitPlanMode.test.ts
│       │   │   │   ├── MessageRenderer.test.ts
│       │   │   │   ├── SubagentRenderer.test.ts
│       │   │   │   ├── ThinkingBlockRenderer.test.ts
│       │   │   │   ├── TodoListRenderer.test.ts
│       │   │   │   ├── ToolCallRenderer.test.ts
│       │   │   │   ├── WriteEditRenderer.test.ts
│       │   │   │   ├── collapsible.test.ts
│       │   │   │   └── todoUtils.test.ts
│       │   │   ├── rewind.test.ts
│       │   │   ├── services/
│       │   │   │   ├── BangBashService.test.ts
│       │   │   │   ├── InstructionRefineService.test.ts
│       │   │   │   ├── SubagentManager.test.ts
│       │   │   │   └── TitleGenerationService.test.ts
│       │   │   ├── state/
│       │   │   │   └── ChatState.test.ts
│       │   │   ├── tabs/
│       │   │   │   ├── Tab.test.ts
│       │   │   │   ├── TabBar.test.ts
│       │   │   │   ├── TabManager.test.ts
│       │   │   │   └── index.test.ts
│       │   │   └── ui/
│       │   │       ├── BangBashModeManager.test.ts
│       │   │       ├── ExternalContextSelector.test.ts
│       │   │       ├── FileContextManager.test.ts
│       │   │       ├── ImageContext.test.ts
│       │   │       ├── InputToolbar.test.ts
│       │   │       ├── InstructionModeManager.test.ts
│       │   │       ├── NavigationSidebar.test.ts
│       │   │       ├── StatusPanel.test.ts
│       │   │       └── file-context/
│       │   │           └── state/
│       │   │               └── FileContextState.test.ts
│       │   ├── inline-edit/
│       │   │   ├── InlineEditService.test.ts
│       │   │   └── ui/
│       │   │       ├── InlineEditModal.openAndWait.test.ts
│       │   │       └── InlineEditModal.test.ts
│       │   └── settings/
│       │       ├── AgentSettings.test.ts
│       │       └── keyboardNavigation.test.ts
│       ├── i18n/
│       │   ├── constants.test.ts
│       │   ├── i18n.test.ts
│       │   └── locales.test.ts
│       ├── shared/
│       │   ├── components/
│       │   │   ├── ResumeSessionDropdown.test.ts
│       │   │   ├── SelectableDropdown.test.ts
│       │   │   └── SlashCommandDropdown.test.ts
│       │   ├── index.test.ts
│       │   ├── mention/
│       │   │   ├── MentionDropdownController.test.ts
│       │   │   ├── VaultFileCache.test.ts
│       │   │   ├── VaultFolderCache.test.ts
│       │   │   └── VaultMentionDataProvider.test.ts
│       │   └── modals/
│       │       ├── ConfirmModal.test.ts
│       │       ├── ForkTargetModal.test.ts
│       │       └── InstructionConfirmModal.test.ts
│       └── utils/
│           ├── agent.test.ts
│           ├── browser.test.ts
│           ├── canvas.test.ts
│           ├── claudeCli.test.ts
│           ├── context.test.ts
│           ├── contextMentionResolver.test.ts
│           ├── date.test.ts
│           ├── diff.test.ts
│           ├── editor.test.ts
│           ├── env.test.ts
│           ├── externalContext.test.ts
│           ├── externalContextScanner.test.ts
│           ├── fileLink.dom.test.ts
│           ├── fileLink.handler.test.ts
│           ├── fileLink.test.ts
│           ├── frontmatter.test.ts
│           ├── imageEmbed.test.ts
│           ├── inlineEdit.test.ts
│           ├── interrupt.test.ts
│           ├── markdown.test.ts
│           ├── mcp.test.ts
│           ├── path.test.ts
│           ├── sdkSession.test.ts
│           ├── session.test.ts
│           ├── slashCommand.test.ts
│           └── utils.test.ts
├── tsconfig.jest.json
├── tsconfig.json
└── versions.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.cjs
================================================
/** @type {import('eslint').Linter.Config} */
module.exports = {
  root: true,
  ignorePatterns: ['dist/', 'node_modules/', 'coverage/', 'main.js'],
  env: {
    browser: true,
    node: true,
    es2021: true,
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'jest', 'simple-import-sort'],
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  rules: {
    '@typescript-eslint/consistent-type-imports': [
      'error',
      { prefer: 'type-imports', fixStyle: 'separate-type-imports' },
    ],
    '@typescript-eslint/no-unused-vars': [
      'error',
      { args: 'none', ignoreRestSiblings: true },
    ],
    '@typescript-eslint/no-explicit-any': 'off',
    'simple-import-sort/imports': 'error',
    'simple-import-sort/exports': 'error',
  },
  overrides: [
    {
      files: [
        'src/ClaudianService.ts',
        'src/InlineEditService.ts',
        'src/InstructionRefineService.ts',
        'src/images/**/*.ts',
        'src/prompt/**/*.ts',
        'src/sdk/**/*.ts',
        'src/security/**/*.ts',
        'src/tools/**/*.ts',
      ],
      rules: {
        'no-restricted-imports': [
          'error',
          {
            patterns: [
              {
                group: ['./ui', './ui/*', '../ui', '../ui/*'],
                message: 'Service and shared modules must not import UI modules.',
              },
              {
                group: ['./ClaudianView', '../ClaudianView'],
                message: 'Service and shared modules must not import the view.',
              },
            ],
          },
        ],
      },
    },
    {
      files: ['tests/**/*.ts'],
      env: { jest: true },
      extends: ['plugin:jest/recommended'],
      rules: {
        '@typescript-eslint/no-explicit-any': 'off',
      },
    },
  ],
};


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node 22
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node 22
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Typecheck
        run: npm run typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node 22
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Test
        run: npm run test


================================================
FILE: .github/workflows/claude-code-review.yml
================================================
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize]
    # Optional: Only run on specific file changes
    # paths:
    #   - "src/**/*.ts"
    #   - "src/**/*.tsx"
    #   - "src/**/*.js"
    #   - "src/**/*.jsx"

jobs:
  claude-review:
    # Optional: Filter by PR author
    # if: |
    #   github.event.pull_request.user.login == 'external-contributor' ||
    #   github.event.pull_request.user.login == 'new-developer' ||
    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code Review
        id: claude-review
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ github.event.pull_request.number }}

            Please review this pull request and provide feedback on:
            - Code quality and best practices
            - Potential bugs or issues
            - Performance considerations
            - Security concerns
            - Test coverage

            Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

            Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'



================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
          # prompt: 'Update the pull request description to include a summary of changes.'

          # Optional: Add claude_args to customize behavior and configuration
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          # claude_args: '--allowed-tools Bash(gh pr:*)'



================================================
FILE: .github/workflows/duplicate-issues.yml
================================================
name: Potential Duplicates
on:
  issues:
    types: [opened, edited]

jobs:
  check-duplicates:
    runs-on: ubuntu-latest
    steps:
      - uses: wow-actions/potential-duplicates@v1
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          threshold: 0.6
          state: all
          label: possible-duplicate
          comment: >
            Possible duplicates found:
            {{#issues}}
            - #{{number}} ({{similarity}}% similar): {{title}}
            {{/issues}}

            If one of these matches your issue, please add your details there instead.


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - '*'

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for changelog generation

      - name: Setup Node 20
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Get previous tag
        id: prev_tag
        run: |
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
          echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT

      - name: Generate changelog
        id: changelog
        run: |
          if [ -n "${{ steps.prev_tag.outputs.tag }}" ]; then
            CHANGELOG=$(git log ${{ steps.prev_tag.outputs.tag }}..HEAD --pretty=format:"- %s" --no-merges | grep -v "^- ${{ github.ref_name }}$" || true)
          else
            CHANGELOG=$(git log --pretty=format:"- %s" --no-merges | head -20)
          fi
          # Handle multiline output
          echo "notes<<EOF" >> $GITHUB_OUTPUT
          echo "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          body: |
            ## What's Changed
            ${{ steps.changelog.outputs.notes }}

            **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ github.ref_name }}
          files: |
            main.js
            manifest.json
            styles.css


================================================
FILE: .github/workflows/stale.yml
================================================
name: Close stale issues

on:
  schedule:
    - cron: '0 0 * * *' # Runs daily at midnight UTC
  workflow_dispatch: # Allow manual trigger

jobs:
  stale:
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - uses: actions/stale@v9
        with:
          days-before-stale: 7
          days-before-close: 7
          stale-issue-message: 'This issue has been inactive for 7 days and will be closed in 7 days if no further activity occurs.'
          close-issue-message: 'Closed due to 14 days of inactivity.'
          stale-issue-label: 'stale'
          exempt-issue-labels: 'pinned,security,bug'


================================================
FILE: .gitignore
================================================
# Build output
main.js
styles.css
*.js.map
dist/
build/

# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# npm/yarn
npm-debug.log*
yarn-error.log*
.yarn-integrity

# TypeScript
*.tsbuildinfo

# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Output of 'npm pack'
*.tgz

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# Temporary folders
tmp/
temp/

# Obsidian specific
*.obsidian
data.json

# Test artifacts
test-results/

# Development files
sandbox/
dev

# Lock files (uncomment based on package manager)
# yarn.lock
# package-lock.json

.claude/
.codex/

================================================
FILE: .npmrc
================================================
tag-version-prefix=""
loglevel=silent


================================================
FILE: AGENTS.md
================================================
## Agents

Read CLAUDE.md for the agent overview and instructions.

================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

## Project Overview

Claudian - An Obsidian plugin that embeds Claude Code as a sidebar chat interface. The vault directory becomes Claude's working directory, giving it full agentic capabilities: file read/write, bash commands, and multi-step workflows.

## Commands

```bash
npm run dev        # Development (watch mode)
npm run build      # Production build
npm run typecheck  # Type check
npm run lint       # Lint code
npm run lint:fix   # Lint and auto-fix
npm run test       # Run tests
npm run test:watch # Run tests in watch mode
```

## Architecture

| Layer | Purpose | Details |
|-------|---------|---------|
| **core** | Infrastructure (no feature deps) | See [`src/core/CLAUDE.md`](src/core/CLAUDE.md) |
| **features/chat** | Main sidebar interface | See [`src/features/chat/CLAUDE.md`](src/features/chat/CLAUDE.md) |
| **features/inline-edit** | Inline edit modal | `InlineEditService`, read-only tools |
| **features/settings** | Settings tab | UI components for all settings |
| **shared** | Reusable UI | Dropdowns, instruction modal, fork target modal, @-mention, icons |
| **i18n** | Internationalization | 10 locales |
| **utils** | Utility functions | date, path, env, editor, session, markdown, diff, context, sdkSession, frontmatter, slashCommand, mcp, claudeCli, externalContext, externalContextScanner, fileLink, imageEmbed, inlineEdit |
| **style** | Modular CSS | See [`src/style/CLAUDE.md`](src/style/CLAUDE.md) |

## Tests

```bash
npm run test -- --selectProjects unit        # Run unit tests
npm run test -- --selectProjects integration # Run integration tests
npm run test:coverage -- --selectProjects unit # Unit coverage
```

Tests mirror `src/` structure in `tests/unit/` and `tests/integration/`.

## Storage

| File | Contents |
|------|----------|
| `.claude/settings.json` | CC-compatible: permissions, env, enabledPlugins |
| `.claude/claudian-settings.json` | Claudian-specific settings (model, UI, etc.) |
| `.claude/settings.local.json` | Local overrides (gitignored) |
| `.claude/mcp.json` | MCP server configs |
| `.claude/commands/*.md` | Slash commands (YAML frontmatter) |
| `.claude/agents/*.md` | Custom agents (YAML frontmatter) |
| `.claude/skills/*/SKILL.md` | Skill definitions |
| `.claude/sessions/*.meta.json` | Session metadata |
| `~/.claude/projects/{vault}/*.jsonl` | SDK-native session messages |

## Development Notes

- **SDK-first**: Proactively use native Claude SDK features over custom implementations. If the SDK provides a capability, use it — do not reinvent it. This ensures compatibility with Claude Code.
- **SDK exploration**: When developing SDK-related features, write a throwaway test script (e.g., in `dev/`) that calls the real SDK to observe actual response shapes, event sequences, and edge cases. Real output lands in `~/.claude/` or `{vault}/.claude/` — inspect those files to understand patterns and formats. Run this before writing implementation or tests — real output beats guessing at types and formats. This is the default first step for any SDK integration work.
- **Comments**: Only comment WHY, not WHAT. No JSDoc that restates the function name (`/** Get servers. */` on `getServers()`), no narrating inline comments (`// Create the channel` before `new Channel()`), no module-level docs on barrel `index.ts` files. Keep JSDoc only when it adds non-obvious context (edge cases, constraints, surprising behavior).
- **TDD workflow**: For new functions/modules and bug fixes, follow red-green-refactor:
  1. Write a failing test first in the mirrored path under `tests/unit/` (or `tests/integration/`)
  2. Run it with `npm run test -- --selectProjects unit --testPathPattern <pattern>` to confirm it fails
  3. Write the minimal implementation to make it pass
  4. Refactor, keeping tests green
  - For bug fixes, write a test that reproduces the bug before fixing it
  - Test behavior and public API, not internal implementation details
  - Skip TDD for trivial changes (renaming, moving files, config tweaks) — but still verify existing tests pass
- Run `npm run typecheck && npm run lint && npm run test && npm run build` after editing
- No `console.*` in production code 
  - use Obsidian's notification system if user should be notified
  - use `console.log` for debugging, but remove it before committing
- Generated docs/test scripts go in `dev/`.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Claudian

![GitHub stars](https://img.shields.io/github/stars/YishenTu/claudian?style=social)
![GitHub release](https://img.shields.io/github/v/release/YishenTu/claudian)
![License](https://img.shields.io/github/license/YishenTu/claudian)

![Preview](Preview.png)

An Obsidian plugin that embeds Claude Code as an AI collaborator in your vault. Your vault becomes Claude's working directory, giving it full agentic capabilities: file read/write, search, bash commands, and multi-step workflows.

## Features

- **Full Agentic Capabilities**: Leverage Claude Code's power to read, write, and edit files, search, and execute bash commands, all within your Obsidian vault.
- **Context-Aware**: Automatically attach the focused note, mention files with `@`, exclude notes by tag, include editor selection (Highlight), and access external directories for additional context.
- **Vision Support**: Analyze images by sending them via drag-and-drop, paste, or file path.
- **Inline Edit**: Edit selected text or insert content at cursor position directly in notes with word-level diff preview and read-only tool access for context.
- **Instruction Mode (`#`)**: Add refined custom instructions to your system prompt directly from the chat input, with review/edit in a modal.
- **Slash Commands**: Create reusable prompt templates triggered by `/command`, with argument placeholders, `@file` references, and optional inline bash substitutions.
- **Skills**: Extend Claudian with reusable capability modules that are automatically invoked based on context, compatible with Claude Code's skill format.
- **Custom Agents**: Define custom subagents that Claude can invoke, with support for tool restrictions and model overrides.
- **Claude Code Plugins**: Enable Claude Code plugins installed via the CLI, with automatic discovery from `~/.claude/plugins` and per-vault configuration. Plugin skills, agents, and slash commands integrate seamlessly.
- **MCP Support**: Connect external tools and data sources via Model Context Protocol servers (stdio, SSE, HTTP) with context-saving mode and `@`-mention activation.
- **Advanced Model Control**: Select between Haiku, Sonnet, and Opus, configure custom models via environment variables, fine-tune thinking budget, and enable Opus and Sonnet with 1M context window (requires Max subscription or extra usage).
- **Plan Mode**: Toggle plan mode via Shift+Tab in the chat input. Claudian explores and designs before implementing, presenting a plan for approval with options to approve in a new session, continue in the current session, or provide feedback.
- **Security**: Permission modes (YOLO/Safe/Plan), safety blocklist, and vault confinement with symlink-safe checks.
- **Claude in Chrome**: Allow Claude to interact with Chrome through the `claude-in-chrome` extension.

## Requirements

- [Claude Code CLI](https://code.claude.com/docs/en/overview) installed (strongly recommend install Claude Code via Native Install)
- Obsidian v1.8.9+
- Claude subscription/API or Custom model provider that supports Anthropic API format ([Openrouter](https://openrouter.ai/docs/guides/guides/claude-code-integration), [Kimi](https://platform.moonshot.ai/docs/guide/agent-support), [GLM](https://docs.z.ai/devpack/tool/claude), [DeepSeek](https://api-docs.deepseek.com/guides/anthropic_api), etc.)
- Desktop only (macOS, Linux, Windows)

## Installation

### From GitHub Release (recommended)

1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/YishenTu/claudian/releases/latest)
2. Create a folder called `claudian` in your vault's plugins folder:
   ```
   /path/to/vault/.obsidian/plugins/claudian/
   ```
3. Copy the downloaded files into the `claudian` folder
4. Enable the plugin in Obsidian:
   - Settings → Community plugins → Enable "Claudian"

### Using BRAT

[BRAT](https://github.com/TfTHacker/obsidian42-brat) (Beta Reviewers Auto-update Tester) allows you to install and automatically update plugins directly from GitHub.

1. Install the BRAT plugin from Obsidian Community Plugins
2. Enable BRAT in Settings → Community plugins
3. Open BRAT settings and click "Add Beta plugin"
4. Enter the repository URL: `https://github.com/YishenTu/claudian`
5. Click "Add Plugin" and BRAT will install Claudian automatically
6. Enable Claudian in Settings → Community plugins

> **Tip**: BRAT will automatically check for updates and notify you when a new version is available.

### From source (development)

1. Clone this repository into your vault's plugins folder:
   ```bash
   cd /path/to/vault/.obsidian/plugins
   git clone https://github.com/YishenTu/claudian.git
   cd claudian
   ```

2. Install dependencies and build:
   ```bash
   npm install
   npm run build
   ```

3. Enable the plugin in Obsidian:
   - Settings → Community plugins → Enable "Claudian"

### Development

```bash
# Watch mode
npm run dev

# Production build
npm run build
```

> **Tip**: Copy `.env.local.example` to `.env.local` or `npm install` and setup your vault path to auto-copy files during development.

## Usage

**Two modes:**
1. Click the bot icon in ribbon or use command palette to open chat
2. Select text + hotkey for inline edit

Use it like Claude Code—read, write, edit, search files in your vault.

### Context

- **File**: Auto-attaches focused note; type `@` to attach other files
- **@-mention dropdown**: Type `@` to see MCP servers, agents, external contexts, and vault files
  - `@Agents/` shows custom agents for selection
  - `@mcp-server` enables context-saving MCP servers
  - `@folder/` filters to files from that external context (e.g., `@workspace/`)
  - Vault files shown by default
- **Selection**: Select text in editor, or elements in canvas, then chat—selection included automatically
- **Images**: Drag-drop, paste, or type path; configure media folder for `![[image]]` embeds
- **External contexts**: Click folder icon in toolbar for access to directories outside vault

### Features

- **Inline Edit**: Select text + hotkey to edit directly in notes with word-level diff preview
- **Instruction Mode**: Type `#` to add refined instructions to system prompt
- **Slash Commands**: Type `/` for custom prompt templates or skills
- **Skills**: Add `skill/SKILL.md` files to `~/.claude/skills/` or `{vault}/.claude/skills/`, recommended to use Claude Code to manage skills
- **Custom Agents**: Add `agent.md` files to `~/.claude/agents/` (global) or `{vault}/.claude/agents/` (vault-specific); select via `@Agents/` in chat, or prompt Claudian to invoke agents
- **Claude Code Plugins**: Enable plugins via Settings → Claude Code Plugins, recommended to use Claude Code to manage plugins
- **MCP**: Add external tools via Settings → MCP Servers; use `@mcp-server` in chat to activate

## Configuration

### Settings

**Customization**
- **User name**: Your name for personalized greetings
- **Excluded tags**: Tags that prevent notes from auto-loading (e.g., `sensitive`, `private`)
- **Media folder**: Configure where vault stores attachments for embedded image support (e.g., `attachments`)
- **Custom system prompt**: Additional instructions appended to the default system prompt (Instruction Mode `#` saves here)
- **Enable auto-scroll**: Toggle automatic scrolling to bottom during streaming (default: on)
- **Auto-generate conversation titles**: Toggle AI-powered title generation after the first user message is sent
- **Title generation model**: Model used for auto-generating conversation titles (default: Auto/Haiku)
- **Vim-style navigation mappings**: Configure key bindings with lines like `map w scrollUp`, `map s scrollDown`, `map i focusInput`

**Hotkeys**
- **Inline edit hotkey**: Hotkey to trigger inline edit on selected text
- **Open chat hotkey**: Hotkey to open the chat sidebar

**Slash Commands**
- Create/edit/import/export custom `/commands` (optionally override model and allowed tools)

**MCP Servers**
- Add/edit/verify/delete MCP server configurations with context-saving mode

**Claude Code Plugins**
- Enable/disable Claude Code plugins discovered from `~/.claude/plugins`
- User-scoped plugins available in all vaults; project-scoped plugins only in matching vault

**Safety**
- **Load user Claude settings**: Load `~/.claude/settings.json` (user's Claude Code permission rules may bypass Safe mode)
- **Enable command blocklist**: Block dangerous bash commands (default: on)
- **Blocked commands**: Patterns to block (supports regex, platform-specific)
- **Allowed export paths**: Paths outside the vault where files can be exported (default: `~/Desktop`, `~/Downloads`). Supports `~`, `$VAR`, `${VAR}`, and `%VAR%` (Windows).

**Environment**
- **Custom variables**: Environment variables for Claude SDK (KEY=VALUE format, supports `export ` prefix)
- **Environment snippets**: Save and restore environment variable configurations

**Advanced**
- **Claude CLI path**: Custom path to Claude Code CLI (leave empty for auto-detection)

## Safety and Permissions

| Scope | Access |
|-------|--------|
| **Vault** | Full read/write (symlink-safe via `realpath`) |
| **Export paths** | Write-only (e.g., `~/Desktop`, `~/Downloads`) |
| **External contexts** | Full read/write (session-only, added via folder icon) |

- **YOLO mode**: No approval prompts; all tool calls execute automatically (default)
- **Safe mode**: Approval prompt per tool call; Bash requires exact match, file tools allow prefix match
- **Plan mode**: Explores and designs a plan before implementing. Toggle via Shift+Tab in the chat input

## Privacy & Data Use

- **Sent to API**: Your input, attached files, images, and tool call outputs. Default: Anthropic; custom endpoint via `ANTHROPIC_BASE_URL`.
- **Local storage**: Settings, session metadata, and commands stored in `vault/.claude/`; session messages in `~/.claude/projects/` (SDK-native); legacy sessions in `vault/.claude/sessions/`.
- **No telemetry**: No tracking beyond your configured API provider.

## Troubleshooting

### Claude CLI not found

If you encounter `spawn claude ENOENT` or `Claude CLI not found`, the plugin can't auto-detect your Claude installation. Common with Node version managers (nvm, fnm, volta).

**Solution**: Find your CLI path and set it in Settings → Advanced → Claude CLI path.

| Platform | Command | Example Path |
|----------|---------|--------------|
| macOS/Linux | `which claude` | `/Users/you/.volta/bin/claude` |
| Windows (native) | `where.exe claude` | `C:\Users\you\AppData\Local\Claude\claude.exe` |
| Windows (npm) | `npm root -g` | `{root}\@anthropic-ai\claude-code\cli.js` |

> **Note**: On Windows, avoid `.cmd` wrappers. Use `claude.exe` or `cli.js`.

**Alternative**: Add your Node.js bin directory to PATH in Settings → Environment → Custom variables.

### npm CLI and Node.js not in same directory

If using npm-installed CLI, check if `claude` and `node` are in the same directory:
```bash
dirname $(which claude)
dirname $(which node)
```

If different, GUI apps like Obsidian may not find Node.js.

**Solutions**:
1. Install native binary (recommended)
2. Add Node.js path to Settings → Environment: `PATH=/path/to/node/bin`

**Still having issues?** [Open a GitHub issue](https://github.com/YishenTu/claudian/issues) with your platform, CLI path, and error message.

## Architecture

```
src/
├── main.ts                      # Plugin entry point
├── core/                        # Core infrastructure
│   ├── agent/                   # Claude Agent SDK wrapper (ClaudianService)
│   ├── agents/                  # Custom agent management (AgentManager)
│   ├── commands/                # Slash command management (SlashCommandManager)
│   ├── hooks/                   # PreToolUse/PostToolUse hooks
│   ├── images/                  # Image caching and loading
│   ├── mcp/                     # MCP server config, service, and testing
│   ├── plugins/                 # Claude Code plugin discovery and management
│   ├── prompts/                 # System prompts for agents
│   ├── sdk/                     # SDK message transformation
│   ├── security/                # Approval, blocklist, path validation
│   ├── storage/                 # Distributed storage system
│   ├── tools/                   # Tool constants and utilities
│   └── types/                   # Type definitions
├── features/                    # Feature modules
│   ├── chat/                    # Main chat view + UI, rendering, controllers, tabs
│   ├── inline-edit/             # Inline edit service + UI
│   └── settings/                # Settings tab UI
├── shared/                      # Shared UI components and modals
│   ├── components/              # Input toolbar bits, dropdowns, selection highlight
│   ├── mention/                 # @-mention dropdown controller
│   ├── modals/                  # Instruction modal
│   └── icons.ts                 # Shared SVG icons
├── i18n/                        # Internationalization (10 locales)
├── utils/                       # Modular utility functions
└── style/                       # Modular CSS (→ styles.css)
```

## Roadmap

- [x] Claude Code Plugin support
- [x] Custom agent (subagent) support
- [x] Claude in Chrome support
- [x] `/compact` command
- [x] Plan mode
- [x] `rewind` and `fork` support (including `/fork` command)
- [x] `!command` support
- [x] Tool renderers refinement
- [x] 1M Opus and Sonnet models
- [ ] Codex SDK integration
- [ ] Hooks and other advanced features
- [ ] More to come!

## License

Licensed under the [MIT License](LICENSE).

## Star History

<a href="https://www.star-history.com/?repos=YishenTu%2Fclaudian&type=date&legend=top-left">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=YishenTu/claudian&type=date&legend=top-left&theme=dark" />
    <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=YishenTu/claudian&type=date&legend=top-left" />
    <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=YishenTu/claudian&type=date&legend=top-left" />
  </picture>
</a>

## Acknowledgments

- [Obsidian](https://obsidian.md) for the plugin API
- [Anthropic](https://anthropic.com) for Claude and the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview)


================================================
FILE: esbuild.config.mjs
================================================
import esbuild from 'esbuild';
import path from 'path';
import process from 'process';
import builtins from 'builtin-modules';
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'fs';

// Load .env.local if it exists
if (existsSync('.env.local')) {
  const envContent = readFileSync('.env.local', 'utf-8');
  for (const line of envContent.split('\n')) {
    const match = line.match(/^([^=]+)=["']?(.+?)["']?$/);
    if (match && !process.env[match[1]]) {
      process.env[match[1]] = match[2];
    }
  }
}

const prod = process.argv[2] === 'production';

// Obsidian plugin folder path (set via OBSIDIAN_VAULT env var or .env.local)
const OBSIDIAN_VAULT = process.env.OBSIDIAN_VAULT;
const OBSIDIAN_PLUGIN_PATH = OBSIDIAN_VAULT && existsSync(OBSIDIAN_VAULT)
  ? path.join(OBSIDIAN_VAULT, '.obsidian', 'plugins', 'claudian')
  : null;

// Plugin to copy built files to Obsidian plugin folder
const copyToObsidian = {
  name: 'copy-to-obsidian',
  setup(build) {
    build.onEnd((result) => {
      if (result.errors.length > 0 || !OBSIDIAN_PLUGIN_PATH) return;

      if (!existsSync(OBSIDIAN_PLUGIN_PATH)) {
        mkdirSync(OBSIDIAN_PLUGIN_PATH, { recursive: true });
      }

      const files = ['main.js', 'manifest.json', 'styles.css'];
      for (const file of files) {
        if (existsSync(file)) {
          copyFileSync(file, path.join(OBSIDIAN_PLUGIN_PATH, file));
          console.log(`Copied ${file} to Obsidian plugin folder`);
        }
      }
    });
  }
};

const context = await esbuild.context({
  entryPoints: ['src/main.ts'],
  bundle: true,
  plugins: [copyToObsidian],
  external: [
    'obsidian',
    'electron',
    '@codemirror/autocomplete',
    '@codemirror/collab',
    '@codemirror/commands',
    '@codemirror/language',
    '@codemirror/lint',
    '@codemirror/search',
    '@codemirror/state',
    '@codemirror/view',
    '@lezer/common',
    '@lezer/highlight',
    '@lezer/lr',
    ...builtins,
    ...builtins.map(m => `node:${m}`),
  ],
  format: 'cjs',
  target: 'es2018',
  logLevel: 'info',
  sourcemap: prod ? false : 'inline',
  treeShaking: true,
  outfile: 'main.js',
});

if (prod) {
  await context.rebuild();
  process.exit(0);
} else {
  await context.watch();
}


================================================
FILE: jest.config.js
================================================
/** @type {import('ts-jest').JestConfigWithTsJest} */
const baseConfig = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@test/(.*)$': '<rootDir>/tests/$1',
    '^@anthropic-ai/claude-agent-sdk$': '<rootDir>/tests/__mocks__/claude-agent-sdk.ts',
    '^obsidian$': '<rootDir>/tests/__mocks__/obsidian.ts',
    '^@modelcontextprotocol/sdk/(.*)$': '<rootDir>/node_modules/@modelcontextprotocol/sdk/dist/cjs/$1',
  },
  transformIgnorePatterns: [
    'node_modules/(?!(@anthropic-ai/claude-agent-sdk)/)',
  ],
};

module.exports = {
  projects: [
    {
      ...baseConfig,
      displayName: 'unit',
      testMatch: ['<rootDir>/tests/unit/**/*.test.ts'],
    },
    {
      ...baseConfig,
      displayName: 'integration',
      testMatch: ['<rootDir>/tests/integration/**/*.test.ts'],
    },
  ],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
  ],
  coverageDirectory: 'coverage',
};


================================================
FILE: manifest.json
================================================
{
  "id": "claudian",
  "name": "Claudian",
  "version": "1.3.70",
  "minAppVersion": "1.4.5",
  "description": "Embeds Claude Code as an AI collaborator in your vault. Your vault becomes Claude's working directory, giving it full agentic capabilities: file read/write, search, bash commands, and multi-step workflows.",
  "author": "Yishen Tu",
  "authorUrl": "https://github.com/YishenTu",
  "isDesktopOnly": true
}


================================================
FILE: package.json
================================================
{
  "name": "claudian",
  "version": "1.3.70",
  "description": "Claudian - Claude Code embedded in Obsidian sidebar",
  "main": "main.js",
  "scripts": {
    "postinstall": "node scripts/postinstall.mjs",
    "build:css": "node scripts/build-css.mjs",
    "dev": "npm run build:css && node esbuild.config.mjs",
    "build": "node scripts/build.mjs production",
    "typecheck": "tsc --noEmit",
    "lint": "eslint \"{src,tests}/**/*.ts\"",
    "lint:fix": "npm run lint -- --fix",
    "test": "node scripts/run-jest.js",
    "test:watch": "node scripts/run-jest.js --watch",
    "test:coverage": "node scripts/run-jest.js --coverage",
    "version": "node scripts/sync-version.js && git add manifest.json"
  },
  "keywords": [
    "claude-code",
    "obsidian",
    "obsidian-plugin",
    "productivity",
    "ide"
  ],
  "author": "Yishen Tu",
  "license": "MIT",
  "devDependencies": {
    "@types/jest": "^30.0.0",
    "@types/node": "^20.0.0",
    "@typescript-eslint/eslint-plugin": "^8.30.0",
    "@typescript-eslint/parser": "^8.30.0",
    "builtin-modules": "^3.3.0",
    "esbuild": "^0.27.1",
    "eslint": "^8.57.0",
    "eslint-plugin-jest": "^28.11.0",
    "eslint-plugin-simple-import-sort": "^12.1.1",
    "jest": "^30.2.0",
    "jest-environment-jsdom": "^30.2.0",
    "obsidian": "latest",
    "ts-jest": "^29.4.6",
    "typescript": "^5.0.0"
  },
  "dependencies": {
    "@anthropic-ai/claude-agent-sdk": "^0.2.76",
    "@modelcontextprotocol/sdk": "~1.25.3",
    "tslib": "^2.8.1"
  }
}


================================================
FILE: scripts/build-css.mjs
================================================
#!/usr/bin/env node
/**
 * CSS Build Script
 * Concatenates modular CSS files from src/style/ into root styles.css
 */

import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
import { join, dirname, resolve, relative } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const STYLE_DIR = join(ROOT, 'src', 'style');
const OUTPUT = join(ROOT, 'styles.css');
const INDEX_FILE = join(STYLE_DIR, 'index.css');

const IMPORT_PATTERN = /^\s*@import\s+(?:url\()?['"]([^'"]+)['"]\)?\s*;/gm;

function getModuleOrder() {
  if (!existsSync(INDEX_FILE)) {
    console.error('Missing src/style/index.css');
    process.exit(1);
  }

  const content = readFileSync(INDEX_FILE, 'utf-8');
  const matches = [...content.matchAll(IMPORT_PATTERN)];

  if (matches.length === 0) {
    console.error('No @import entries found in src/style/index.css');
    process.exit(1);
  }

  return matches.map((match) => match[1]);
}

function listCssFiles(dir, baseDir = dir) {
  const entries = readdirSync(dir, { withFileTypes: true });
  const files = [];

  for (const entry of entries) {
    const entryPath = join(dir, entry.name);

    if (entry.isDirectory()) {
      files.push(...listCssFiles(entryPath, baseDir));
      continue;
    }

    if (entry.isFile() && entry.name.endsWith('.css')) {
      const relativePath = relative(baseDir, entryPath).split('\\').join('/');
      files.push(relativePath);
    }
  }

  return files;
}

function build() {
  const moduleOrder = getModuleOrder();
  const parts = ['/* Claudian Plugin Styles */\n/* Built from src/style/ modules */\n'];
  const missingFiles = [];
  const invalidImports = [];
  const normalizedImports = [];

  for (const modulePath of moduleOrder) {
    const resolvedPath = resolve(STYLE_DIR, modulePath);
    const relativePath = relative(STYLE_DIR, resolvedPath);

    if (relativePath.startsWith('..') || !relativePath.endsWith('.css')) {
      invalidImports.push(modulePath);
      continue;
    }

    const normalizedPath = relativePath.split('\\').join('/');
    normalizedImports.push(normalizedPath);

    if (!existsSync(resolvedPath)) {
      missingFiles.push(normalizedPath);
      continue;
    }

    const content = readFileSync(resolvedPath, 'utf-8');
    const header = `\n/* ============================================\n   ${normalizedPath}\n   ============================================ */\n`;
    parts.push(header + content);
  }

  let hasErrors = false;

  if (invalidImports.length > 0) {
    console.error('Invalid @import entries in src/style/index.css:');
    invalidImports.forEach((modulePath) => console.error(`  - ${modulePath}`));
    hasErrors = true;
  }

  if (missingFiles.length > 0) {
    console.error('Missing CSS files:');
    missingFiles.forEach((f) => console.error(`  - ${f}`));
    hasErrors = true;
  }

  const allCssFiles = listCssFiles(STYLE_DIR).filter((file) => file !== 'index.css');
  const importedSet = new Set(normalizedImports);
  const unlistedFiles = allCssFiles.filter((file) => !importedSet.has(file));

  if (unlistedFiles.length > 0) {
    console.error('Unlisted CSS files (not imported in src/style/index.css):');
    unlistedFiles.forEach((file) => console.error(`  - ${file}`));
    hasErrors = true;
  }

  if (hasErrors) {
    process.exit(1);
  }

  const output = parts.join('\n');
  writeFileSync(OUTPUT, output);
  console.log(`Built styles.css (${(output.length / 1024).toFixed(1)} KB)`);
}

build();


================================================
FILE: scripts/build.mjs
================================================
#!/usr/bin/env node
/**
 * Combined build script - runs CSS build then esbuild
 * Avoids npm echoing commands
 */

import { execSync } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');

// Run CSS build silently
execSync('node scripts/build-css.mjs', { cwd: ROOT, stdio: 'inherit' });

// Run esbuild with args passed through
const args = process.argv.slice(2).join(' ');
execSync(`node esbuild.config.mjs ${args}`, { cwd: ROOT, stdio: 'inherit' });


================================================
FILE: scripts/postinstall.mjs
================================================
#!/usr/bin/env node
/**
 * Post-install script - copies .env.local.example to .env.local if it doesn't exist
 */

import { copyFileSync, existsSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

// Skip in CI environments
if (process.env.CI) {
  process.exit(0);
}

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');

const example = join(ROOT, '.env.local.example');
const target = join(ROOT, '.env.local');

if (existsSync(example) && !existsSync(target)) {
  copyFileSync(example, target);
  console.log('Created .env.local from .env.local.example');
  console.log('Edit it to set your OBSIDIAN_VAULT path for auto-copy during development.');
}


================================================
FILE: scripts/run-jest.js
================================================
const { spawnSync } = require('child_process');
const os = require('os');
const path = require('path');

const jestPath = require.resolve('jest/bin/jest');
const localStorageFile = path.join(os.tmpdir(), 'claudian-localstorage');

const result = spawnSync(
  process.execPath,
  [`--localstorage-file=${localStorageFile}`, jestPath, ...process.argv.slice(2)],
  { stdio: 'inherit' }
);

if (result.error) {
  console.error(result.error);
  process.exit(1);
}

process.exit(result.status ?? 1);


================================================
FILE: scripts/sync-version.js
================================================
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const packagePath = path.join(__dirname, '..', 'package.json');
const manifestPath = path.join(__dirname, '..', 'manifest.json');

const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const manifestJson = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));

manifestJson.version = packageJson.version;

fs.writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2) + '\n');

console.log(`Synced version to ${packageJson.version}`);


================================================
FILE: src/core/CLAUDE.md
================================================
# Core Infrastructure

Core modules have **no feature dependencies**. Features depend on core, never the reverse.

## Modules

| Module | Purpose | Key Files |
|--------|---------|-----------|
| `agent/` | Claude Agent SDK wrapper | `ClaudianService` (incl. fork session tracking), `SessionManager`, `QueryOptionsBuilder` (incl. `resumeSessionAt`), `MessageChannel`, `customSpawn` |
| `agents/` | Custom agent discovery | `AgentManager`, `AgentStorage` |
| `commands/` | Built-in command actions | `builtInCommands` |
| `hooks/` | Security hooks | `SecurityHooks` |
| `images/` | Image caching | SHA-256 dedup, base64 encoding |
| `mcp/` | Model Context Protocol | `McpServerManager`, `McpTester` |
| `plugins/` | Claude Code plugins | `PluginManager` |
| `prompts/` | System prompts | `mainAgent`, `inlineEdit`, `instructionRefine`, `titleGeneration` |
| `sdk/` | SDK message transform | `transformSDKMessage`, `typeGuards`, `types` |
| `security/` | Access control | `ApprovalManager` (permission utilities), `BashPathValidator`, `BlocklistChecker` |
| `storage/` | Persistence layer | `StorageService`, `SessionStorage`, `CCSettingsStorage`, `ClaudianSettingsStorage`, `McpStorage`, `SkillStorage`, `SlashCommandStorage`, `VaultFileAdapter` |
| `tools/` | Tool utilities | `toolNames` (incl. plan mode tools), `toolIcons`, `toolInput`, `todo` |
| `types/` | Type definitions | `settings`, `agent`, `mcp`, `chat` (incl. `forkSource?: { sessionId, resumeAt }`), `tools`, `models`, `sdk`, `plugins`, `diff` |

## Dependency Rules

```
types/ ← (all modules can import)
storage/ ← security/, agent/, mcp/
security/ ← agent/
sdk/ ← agent/
hooks/ ← agent/
prompts/ ← agent/
```

## Key Patterns

### ClaudianService
```typescript
// One instance per tab (lazy init on first query)
const service = new ClaudianService(plugin, vaultPath);
await service.query(prompt, options);  // Returns async iterator
service.abort();  // Cancel streaming
```

### QueryOptionsBuilder
```typescript
// Builds SDK Options from settings
const builder = new QueryOptionsBuilder(plugin, settings);
const options = builder.build({ sessionId, maxThinkingTokens });
```

### Storage (Claude Code pattern)
```typescript
// Settings in vault/.claude/settings.json
await CCSettingsStorage.load(vaultPath);
await CCSettingsStorage.save(vaultPath, settings);

// Sessions: SDK-native (~/.claude/projects/) + metadata overlay (.meta.json)
await SessionStorage.loadSession(vaultPath, sessionId);
```

### Security
- `BashPathValidator`: Vault-only by default, symlink-safe via `realpath`
- `ApprovalManager`: Permission utility functions (`buildPermissionUpdates`, `matchesRulePattern`, etc.)
- `BlocklistChecker`: Platform-specific dangerous commands

## Gotchas

- `ClaudianService` must be disposed on tab close (abort + cleanup)
- `SessionManager` handles SDK session resume via `sessionId`
- Fork uses `pendingForkSession` + `pendingResumeAt` on `ClaudianService` to pass `resumeSessionAt` to SDK; these are one-shot flags consumed on the next query
- Storage paths are encoded: non-alphanumeric → `-`
- `customSpawn` handles cross-platform process spawning
- Plan mode uses dedicated callbacks (`exitPlanModeCallback`, `permissionModeSyncCallback`) that bypass normal approval flow in `canUseTool`. `EnterPlanMode` is auto-approved by the SDK; the stream event is detected to sync UI state.


================================================
FILE: src/core/agent/ClaudianService.ts
================================================
/**
 * Claudian - Claude Agent SDK wrapper
 *
 * Handles communication with Claude via the Agent SDK. Manages streaming,
 * session persistence, permission modes, and security hooks.
 *
 * Architecture:
 * - Persistent query for active chat conversation (eliminates cold-start latency)
 * - Cold-start queries for inline edit, title generation
 * - MessageChannel for message queueing and turn management
 * - Dynamic updates (model, thinking tokens, permission mode, MCP servers)
 */

import type {
  CanUseTool,
  McpServerConfig,
  Options,
  PermissionMode as SDKPermissionMode,
  PermissionResult,
  Query,
  RewindFilesResult,
  SDKMessage,
  SDKUserMessage,
  SlashCommand as SDKSlashCommand,
} from '@anthropic-ai/claude-agent-sdk';
import { query as agentQuery } from '@anthropic-ai/claude-agent-sdk';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import { Notice } from 'obsidian';
import * as os from 'os';
import * as path from 'path';

import type ClaudianPlugin from '../../main';
import { stripCurrentNoteContext } from '../../utils/context';
import { getEnhancedPath, getMissingNodeError, parseEnvironmentVariables } from '../../utils/env';
import { getPathAccessType, getVaultPath } from '../../utils/path';
import {
  buildContextFromHistory,
  buildPromptWithHistoryContext,
  getLastUserMessage,
  isSessionExpiredError,
} from '../../utils/session';
import {
  createBlocklistHook,
  createStopSubagentHook,
  createVaultRestrictionHook,
  type SubagentHookState,
} from '../hooks';
import type { McpServerManager } from '../mcp';
import { isSessionInitEvent, isStreamChunk, transformSDKMessage } from '../sdk';
import {
  buildPermissionUpdates,
  getActionDescription,
} from '../security';
import { TOOL_ASK_USER_QUESTION, TOOL_ENTER_PLAN_MODE, TOOL_EXIT_PLAN_MODE, TOOL_SKILL } from '../tools/toolNames';
import type {
  ApprovalDecision,
  ChatMessage,
  Conversation,
  ExitPlanModeCallback,
  ExitPlanModeDecision,
  ImageAttachment,
  PermissionMode,
  SlashCommand,
  StreamChunk,
} from '../types';
import { isAdaptiveThinkingModel, THINKING_BUDGETS } from '../types';
import { MessageChannel } from './MessageChannel';
import {
  type ColdStartQueryContext,
  type PersistentQueryContext,
  QueryOptionsBuilder,
  type QueryOptionsContext,
} from './QueryOptionsBuilder';
import { SessionManager } from './SessionManager';
import {
  type ClosePersistentQueryOptions,
  createResponseHandler,
  isTurnCompleteMessage,
  type PersistentQueryConfig,
  type ResponseHandler,
  type UserContentBlock,
} from './types';

export type { ApprovalDecision };

export interface ApprovalCallbackOptions {
  decisionReason?: string;
  blockedPath?: string;
  agentID?: string;
}

export type ApprovalCallback = (
  toolName: string,
  input: Record<string, unknown>,
  description: string,
  options?: ApprovalCallbackOptions,
) => Promise<ApprovalDecision>;

export type AskUserQuestionCallback = (
  input: Record<string, unknown>,
  signal?: AbortSignal,
) => Promise<Record<string, string> | null>;

export interface QueryOptions {
  allowedTools?: string[];
  model?: string;
  /** MCP servers @-mentioned in the prompt. */
  mcpMentions?: Set<string>;
  /** MCP servers enabled via UI selector (in addition to @-mentioned servers). */
  enabledMcpServers?: Set<string>;
  /** Force cold-start query (bypass persistent query). */
  forceColdStart?: boolean;
  /** Session-specific external context paths (directories with full access). */
  externalContextPaths?: string[];
}

export interface EnsureReadyOptions {
  /** Session ID to resume. Auto-resolved from sessionManager if not provided. */
  sessionId?: string;
  /** External context paths to include. */
  externalContextPaths?: string[];
  /** Force restart even if query is running (for session switch, crash recovery). */
  force?: boolean;
  /** Preserve response handlers across restart (for mid-turn crash recovery). */
  preserveHandlers?: boolean;
}

export class ClaudianService {
  private plugin: ClaudianPlugin;
  private abortController: AbortController | null = null;
  private approvalCallback: ApprovalCallback | null = null;
  private approvalDismisser: (() => void) | null = null;
  private askUserQuestionCallback: AskUserQuestionCallback | null = null;
  private exitPlanModeCallback: ExitPlanModeCallback | null = null;
  private permissionModeSyncCallback: ((sdkMode: string) => void) | null = null;
  private vaultPath: string | null = null;
  private currentExternalContextPaths: string[] = [];
  private readyStateListeners = new Set<(ready: boolean) => void>();

  // Modular components
  private sessionManager = new SessionManager();
  private mcpManager: McpServerManager;

  private persistentQuery: Query | null = null;
  private messageChannel: MessageChannel | null = null;
  private queryAbortController: AbortController | null = null;
  private responseHandlers: ResponseHandler[] = [];
  private responseConsumerRunning = false;
  private responseConsumerPromise: Promise<void> | null = null;
  private shuttingDown = false;

  // Tracked configuration for detecting changes that require restart
  private currentConfig: PersistentQueryConfig | null = null;

  // Current allowed tools for canUseTool enforcement (null = no restriction)
  private currentAllowedTools: string[] | null = null;

  private pendingResumeAt?: string;
  private pendingForkSession = false;

  // Last sent message for crash recovery (Phase 1.3)
  private lastSentMessage: SDKUserMessage | null = null;
  private lastSentQueryOptions: QueryOptions | null = null;
  private crashRecoveryAttempted = false;
  private coldStartInProgress = false;  // Prevent consumer error restarts during cold-start

  // Subagent hook state provider (set from feature layer to avoid core→feature dependency)
  private _subagentStateProvider: (() => SubagentHookState) | null = null;

  // Auto-triggered turn handling (e.g., task-notification delivery by the SDK)
  private _autoTurnBuffer: StreamChunk[] = [];
  private _autoTurnSawStreamText = false;
  private _autoTurnCallback: ((chunks: StreamChunk[]) => void) | null = null;

  constructor(plugin: ClaudianPlugin, mcpManager: McpServerManager) {
    this.plugin = plugin;
    this.mcpManager = mcpManager;
  }

  onReadyStateChange(listener: (ready: boolean) => void): () => void {
    this.readyStateListeners.add(listener);
    try {
      listener(this.isReady());
    } catch {
      // Ignore listener errors
    }
    return () => {
      this.readyStateListeners.delete(listener);
    };
  }

  private notifyReadyStateChange(): void {
    if (this.readyStateListeners.size === 0) {
      return;
    }

    const isReady = this.isReady();
    for (const listener of this.readyStateListeners) {
      try {
        listener(isReady);
      } catch {
        // Ignore listener errors
      }
    }
  }

  setPendingResumeAt(uuid: string | undefined): void {
    this.pendingResumeAt = uuid;
  }

  /** One-shot: consumed on the next query, then cleared by routeMessage on session init. */
  applyForkState(conv: Pick<Conversation, 'sessionId' | 'sdkSessionId' | 'forkSource'>): string | null {
    const isPending = !conv.sessionId && !conv.sdkSessionId && !!conv.forkSource;
    this.pendingForkSession = isPending;
    if (isPending) {
      this.pendingResumeAt = conv.forkSource!.resumeAt;
    } else {
      this.pendingResumeAt = undefined;
    }
    return conv.sessionId ?? conv.forkSource?.sessionId ?? null;
  }

  async reloadMcpServers(): Promise<void> {
    await this.mcpManager.loadServers();
  }

  /**
   * Ensures the persistent query is running with current configuration.
   * Unified API that replaces preWarm() and restartPersistentQuery().
   *
   * Behavior:
   * - If not running → start (if paths available)
   * - If running and force=true → close and restart
   * - If running and config changed → close and restart
   * - If running and config unchanged → no-op
   *
   * Note: When restart is needed, the query is closed BEFORE checking if we can
   * start a new one. This ensures fallback to cold-start if CLI becomes unavailable.
   *
   * @returns true if the query was (re)started, false otherwise
   */
  async ensureReady(options?: EnsureReadyOptions): Promise<boolean> {
    const vaultPath = getVaultPath(this.plugin.app);

    // Track external context paths for dynamic updates (empty list clears)
    if (options && options.externalContextPaths !== undefined) {
      this.currentExternalContextPaths = options.externalContextPaths;
    }

    // Auto-resolve session ID from sessionManager if not explicitly provided
    const effectiveSessionId = options?.sessionId ?? this.sessionManager.getSessionId() ?? undefined;
    const externalContextPaths = options?.externalContextPaths ?? this.currentExternalContextPaths;

    // Case 1: Not running → try to start
    if (!this.persistentQuery) {
      if (!vaultPath) return false;
      const cliPath = this.plugin.getResolvedClaudeCliPath();
      if (!cliPath) return false;
      await this.startPersistentQuery(vaultPath, cliPath, effectiveSessionId, externalContextPaths);
      return true;
    }

    // Case 2: Force restart (session switch, crash recovery)
    // Close FIRST, then try to start new one (allows fallback if CLI unavailable)
    if (options?.force) {
      this.closePersistentQuery('forced restart', { preserveHandlers: options.preserveHandlers });
      if (!vaultPath) return false;
      const cliPath = this.plugin.getResolvedClaudeCliPath();
      if (!cliPath) return false;
      await this.startPersistentQuery(vaultPath, cliPath, effectiveSessionId, externalContextPaths);
      return true;
    }

    // Case 3: Check if config changed → restart if needed
    // We need vaultPath and cliPath to build config for comparison
    if (!vaultPath) return false;
    const cliPath = this.plugin.getResolvedClaudeCliPath();
    if (!cliPath) return false;

    const newConfig = this.buildPersistentQueryConfig(vaultPath, cliPath, externalContextPaths);
    if (this.needsRestart(newConfig)) {
      // Close FIRST, then try to start new one (allows fallback if CLI unavailable)
      this.closePersistentQuery('config changed', { preserveHandlers: options?.preserveHandlers });
      // Re-check CLI path as it might have changed during close
      const cliPathAfterClose = this.plugin.getResolvedClaudeCliPath();
      if (cliPathAfterClose) {
        await this.startPersistentQuery(vaultPath, cliPathAfterClose, effectiveSessionId, externalContextPaths);
        return true;
      }
      // CLI unavailable after close - query is closed, will fallback to cold-start
      return false;
    }

    // Case 4: Running and config unchanged → no-op
    return false;
  }

  /**
   * Starts the persistent query for the active chat conversation.
   */
  private async startPersistentQuery(
    vaultPath: string,
    cliPath: string,
    resumeSessionId?: string,
    externalContextPaths?: string[]
  ): Promise<void> {
    if (this.persistentQuery) {
      return;
    }

    this.shuttingDown = false;
    this.vaultPath = vaultPath;

    this.messageChannel = new MessageChannel();

    if (resumeSessionId) {
      this.messageChannel.setSessionId(resumeSessionId);
      this.sessionManager.setSessionId(resumeSessionId, this.plugin.settings.model);
    }

    this.queryAbortController = new AbortController();

    const config = this.buildPersistentQueryConfig(vaultPath, cliPath, externalContextPaths);
    this.currentConfig = config;

    // await is intentional: yields to microtask queue so fire-and-forget callers
    // (e.g. setSessionId → ensureReady) don't synchronously set persistentQuery
    const resumeSessionAt = this.pendingResumeAt;
    const options = await this.buildPersistentQueryOptions(
      vaultPath,
      cliPath,
      resumeSessionId,
      resumeSessionAt,
      externalContextPaths
    );

    this.persistentQuery = agentQuery({
      prompt: this.messageChannel,
      options,
    });

    if (this.pendingResumeAt === resumeSessionAt) {
      this.pendingResumeAt = undefined;
    }
    this.attachPersistentQueryStdinErrorHandler(this.persistentQuery);

    this.startResponseConsumer();
    this.notifyReadyStateChange();
  }

  private attachPersistentQueryStdinErrorHandler(query: Query): void {
    const stdin = (query as { transport?: { processStdin?: NodeJS.WritableStream } }).transport?.processStdin;
    if (!stdin || typeof stdin.on !== 'function' || typeof stdin.once !== 'function') {
      return;
    }

    const handler = (error: NodeJS.ErrnoException) => {
      if (this.shuttingDown || this.isPipeError(error)) {
        return;
      }
      this.closePersistentQuery('stdin error');
    };

    stdin.on('error', handler);
    stdin.once('close', () => {
      stdin.removeListener('error', handler);
    });
  }

  private isPipeError(error: unknown): boolean {
    if (!error || typeof error !== 'object') return false;
    const e = error as { code?: string; message?: string };
    return e.code === 'EPIPE' || (typeof e.message === 'string' && e.message.includes('EPIPE'));
  }

  /**
   * Closes the persistent query and cleans up resources.
   */
  closePersistentQuery(_reason?: string, options?: ClosePersistentQueryOptions): void {
    if (!this.persistentQuery) {
      return;
    }

    const preserveHandlers = options?.preserveHandlers ?? false;

    this.shuttingDown = true;

    // Close the message channel (ends the async iterable)
    this.messageChannel?.close();

    // Interrupt the query
    void this.persistentQuery.interrupt().catch(() => {
      // Silence abort/interrupt errors during shutdown
    });

    // Abort as backup
    this.queryAbortController?.abort();

    if (!preserveHandlers) {
      // Notify all handlers before clearing so generators don't hang forever.
      // This ensures queryViaPersistent() exits its while(!state.done) loop.
      for (const handler of this.responseHandlers) {
        handler.onDone();
      }
    }

    // Reset shuttingDown synchronously. The consumer loop sees shuttingDown=true
    // on its next iteration check (line 549) and breaks. The messageChannel.close()
    // above also terminates the for-await loop. Resetting here allows new queries
    // to proceed immediately without waiting for consumer loop teardown.
    this.shuttingDown = false;
    this.notifyReadyStateChange();

    // Clear state
    this.persistentQuery = null;
    this.messageChannel = null;
    this.queryAbortController = null;
    this.responseConsumerRunning = false;
    this.responseConsumerPromise = null;
    this.currentConfig = null;
    this._autoTurnBuffer = [];
    this._autoTurnSawStreamText = false;
    if (!preserveHandlers) {
      this.responseHandlers = [];
      this.currentAllowedTools = null;
    }

    // NOTE: Do NOT reset crashRecoveryAttempted here.
    // It's reset in queryViaPersistent after a successful message send,
    // or in resetSession/setSessionId when switching sessions.
    // Resetting it here would cause infinite restart loops on persistent errors.
  }

  /**
   * Checks if the persistent query needs to be restarted based on configuration changes.
   */
  private needsRestart(newConfig: PersistentQueryConfig): boolean {
    return QueryOptionsBuilder.needsRestart(this.currentConfig, newConfig);
  }

  /**
   * Builds configuration object for tracking changes.
   */
  private buildPersistentQueryConfig(
    vaultPath: string,
    cliPath: string,
    externalContextPaths?: string[]
  ): PersistentQueryConfig {
    return QueryOptionsBuilder.buildPersistentQueryConfig(
      this.buildQueryOptionsContext(vaultPath, cliPath),
      externalContextPaths
    );
  }

  /**
   * Builds the base query options context from current state.
   */
  private buildQueryOptionsContext(vaultPath: string, cliPath: string): QueryOptionsContext {
    const customEnv = parseEnvironmentVariables(this.plugin.getActiveEnvironmentVariables());
    const enhancedPath = getEnhancedPath(customEnv.PATH, cliPath);

    return {
      vaultPath,
      cliPath,
      settings: this.plugin.settings,
      customEnv,
      enhancedPath,
      mcpManager: this.mcpManager,
      pluginManager: this.plugin.pluginManager,
    };
  }

  /**
   * Builds SDK options for the persistent query.
   */
  private buildPersistentQueryOptions(
    vaultPath: string,
    cliPath: string,
    resumeSessionId?: string,
    resumeSessionAt?: string,
    externalContextPaths?: string[]
  ): Options {
    const baseContext = this.buildQueryOptionsContext(vaultPath, cliPath);
    const hooks = this.buildHooks();

    const ctx: PersistentQueryContext = {
      ...baseContext,
      abortController: this.queryAbortController ?? undefined,
      resume: resumeSessionId
        ? { sessionId: resumeSessionId, sessionAt: resumeSessionAt, fork: this.pendingForkSession || undefined }
        : undefined,
      canUseTool: this.createApprovalCallback(),
      hooks,
      externalContextPaths,
    };

    return QueryOptionsBuilder.buildPersistentQueryOptions(ctx);
  }

  /**
   * Builds the hooks for SDK options.
   * Hooks need access to `this` for dynamic settings, so they're built here.
   *
   * @param externalContextPaths - Optional external context paths for cold-start queries.
   *        If not provided, the closure reads this.currentExternalContextPaths at execution
   *        time (for persistent queries where the value may change dynamically).
   */
  private buildHooks(externalContextPaths?: string[]) {
    const blocklistHook = createBlocklistHook(() => ({
      blockedCommands: this.plugin.settings.blockedCommands,
      enableBlocklist: this.plugin.settings.enableBlocklist,
    }));

    const hooks: Options['hooks'] = {};

    if (this.plugin.settings.allowExternalAccess) {
      hooks.PreToolUse = [blocklistHook];
    } else {
      const vaultRestrictionHook = createVaultRestrictionHook({
        getPathAccessType: (p) => {
          if (!this.vaultPath) return 'vault';
          // For cold-start queries, use the passed externalContextPaths.
          // For persistent queries, read this.currentExternalContextPaths at execution time
          // so dynamic updates are reflected.
          const paths = externalContextPaths ?? this.currentExternalContextPaths;
          return getPathAccessType(
            p,
            paths,
            this.plugin.settings.allowedExportPaths,
            this.vaultPath
          );
        },
      });
      hooks.PreToolUse = [blocklistHook, vaultRestrictionHook];
    }

    // Always register subagent hooks — closures resolve provider at execution time
    // so hooks work even when provider is set after the persistent query starts.
    hooks.Stop = [createStopSubagentHook(
      () => this._subagentStateProvider?.() ?? { hasRunning: false }
    )];

    return hooks;
  }

  /**
   * Starts the background consumer loop that routes chunks to handlers.
   */
  private startResponseConsumer(): void {
    if (this.responseConsumerRunning) {
      return;
    }

    this.responseConsumerRunning = true;

    // Track which query this consumer is for, to detect if we were replaced
    const queryForThisConsumer = this.persistentQuery;

    this.responseConsumerPromise = (async () => {
      if (!this.persistentQuery) return;

      try {
        for await (const message of this.persistentQuery) {
          if (this.shuttingDown) break;

          await this.routeMessage(message);
        }
      } catch (error) {
        // Skip error handling if this consumer was replaced by a new one.
        // This prevents race conditions where the OLD consumer's error handler
        // interferes with the NEW handler after a restart (e.g., from applyDynamicUpdates).
        if (this.persistentQuery !== queryForThisConsumer && this.persistentQuery !== null) {
          return;
        }

        // Skip restart if cold-start is in progress (it will handle session capture)
        if (!this.shuttingDown && !this.coldStartInProgress) {
          const handler = this.responseHandlers[this.responseHandlers.length - 1];
          const errorInstance = error instanceof Error ? error : new Error(String(error));
          const messageToReplay = this.lastSentMessage;

          if (!this.crashRecoveryAttempted && messageToReplay && handler && !handler.sawAnyChunk) {
            this.crashRecoveryAttempted = true;
            try {
              await this.ensureReady({ force: true, preserveHandlers: true });
              if (!this.messageChannel) {
                throw new Error('Persistent query restart did not create message channel');
              }
              await this.applyDynamicUpdates(this.lastSentQueryOptions ?? undefined, { preserveHandlers: true });
              this.messageChannel.enqueue(messageToReplay);
              return;
            } catch (restartError) {
              // If restart failed due to session expiration, invalidate session
              // so next query triggers noSessionButHasHistory → history rebuild
              if (isSessionExpiredError(restartError)) {
                this.sessionManager.invalidateSession();
              }
              handler.onError(errorInstance);
              return;
            }
          }

          // Notify active handler of error
          if (handler) {
            handler.onError(errorInstance);
          }

          // Crash recovery: restart persistent query to prepare for next user message.
          if (!this.crashRecoveryAttempted) {
            this.crashRecoveryAttempted = true;
            try {
              await this.ensureReady({ force: true });
            } catch (restartError) {
              // If restart failed due to session expiration, invalidate session
              // so next query triggers noSessionButHasHistory → history rebuild
              if (isSessionExpiredError(restartError)) {
                this.sessionManager.invalidateSession();
              }
              // Restart failed - next query will start fresh.
            }
          }
        }
      } finally {
        // Only clear the flag if this consumer wasn't replaced by a new one (e.g., after restart)
        // If ensureReady() restarted, it starts a new consumer which sets the flag true,
        // so we shouldn't clear it here.
        if (this.persistentQuery === queryForThisConsumer || this.persistentQuery === null) {
          this.responseConsumerRunning = false;
        }
      }
    })();
  }

  /** @param modelOverride - Optional model override for cold-start queries */
  private getTransformOptions(modelOverride?: string) {
    return {
      intendedModel: modelOverride ?? this.plugin.settings.model,
      customContextLimits: this.plugin.settings.customContextLimits,
    };
  }

  /**
   * Routes an SDK message to the active response handler.
   *
   * Design: Only one handler exists at a time because MessageChannel enforces
   * single-turn processing. When a turn is active, new messages are queued/merged.
   * The next message only dequeues after onTurnComplete(), which calls onDone()
   * on the current handler. A new handler is registered only when the next query starts.
   */
  private async routeMessage(message: SDKMessage): Promise<void> {
    // Note: Session expiration errors are handled in catch blocks (queryViaSDK, handleAbort)
    // The SDK throws errors as exceptions, not as message types

    // Safe to use last handler - design guarantees single handler at a time
    const handler = this.responseHandlers[this.responseHandlers.length - 1];
    if (this.isStreamTextEvent(message)) {
      if (handler) {
        handler.markStreamTextSeen();
      } else {
        this._autoTurnSawStreamText = true;
      }
    }

    // Transform SDK message to StreamChunks
    for (const event of transformSDKMessage(message, this.getTransformOptions())) {
      if (isSessionInitEvent(event)) {
        // Fork: suppress needsHistoryRebuild since SDK returns a different session ID by design
        const wasFork = this.pendingForkSession;
        this.sessionManager.captureSession(event.sessionId);
        if (wasFork) {
          this.sessionManager.clearHistoryRebuild();
          this.pendingForkSession = false;
        }
        this.messageChannel?.setSessionId(event.sessionId);
        if (event.agents) {
          try { this.plugin.agentManager.setBuiltinAgentNames(event.agents); } catch { /* non-critical */ }
        }
        if (event.permissionMode && this.permissionModeSyncCallback) {
          try { this.permissionModeSyncCallback(event.permissionMode); } catch { /* non-critical */ }
        }
      } else if (isStreamChunk(event)) {
        // Dedup: SDK delivers text via stream_events (incremental) AND the assistant message
        // (complete). Skip the assistant message text if stream text was already seen.
        if (message.type === 'assistant' && event.type === 'text') {
          if (handler?.sawStreamText || (!handler && this._autoTurnSawStreamText)) {
            continue;
          }
        }

        // SDK auto-approves EnterPlanMode (checkPermissions → allow),
        // so canUseTool is never called. Detect the tool_use in the stream
        // and fire the sync callback to update the UI.
        if (event.type === 'tool_use' && event.name === TOOL_ENTER_PLAN_MODE) {
          if (this.currentConfig) {
            this.currentConfig.permissionMode = 'plan';
          }
          if (this.permissionModeSyncCallback) {
            try { this.permissionModeSyncCallback('plan'); } catch { /* non-critical */ }
          }
        }

        if (handler) {
          // Add sessionId to usage chunks (consistent with cold-start path)
          if (event.type === 'usage') {
            handler.onChunk({ ...event, sessionId: this.sessionManager.getSessionId() });
          } else {
            handler.onChunk(event);
          }
        } else {
          // No handler — buffer for auto-triggered turn (e.g., task-notification delivery)
          this._autoTurnBuffer.push(event);
        }
      }
    }

    if (message.type === 'assistant' && message.uuid) {
      const uuidChunk: StreamChunk = { type: 'sdk_assistant_uuid', uuid: message.uuid };
      if (handler) {
        handler.onChunk(uuidChunk);
      } else {
        this._autoTurnBuffer.push(uuidChunk);
      }
    }

    // Check for turn completion
    if (isTurnCompleteMessage(message)) {
      // Signal turn complete to message channel
      this.messageChannel?.onTurnComplete();

      // Notify handler
      if (handler) {
        handler.resetStreamText();
        handler.onDone();
      } else {
        this._autoTurnSawStreamText = false;
        if (this._autoTurnBuffer.length === 0) {
          return;
        }

        // Flush buffered chunks from auto-triggered turn (no handler was registered)
        const chunks = [...this._autoTurnBuffer];
        this._autoTurnBuffer = [];
        try {
          this._autoTurnCallback?.(chunks);
        } catch {
          new Notice('Background task completed, but the result could not be rendered.');
        }
      }
    }
  }

  private registerResponseHandler(handler: ResponseHandler): void {
    this.responseHandlers.push(handler);
  }

  private unregisterResponseHandler(handlerId: string): void {
    const idx = this.responseHandlers.findIndex(h => h.id === handlerId);
    if (idx >= 0) {
      this.responseHandlers.splice(idx, 1);
    }
  }

  isPersistentQueryActive(): boolean {
    return this.persistentQuery !== null && !this.shuttingDown;
  }

  /**
   * Sends a query to Claude and streams the response.
   *
   * Query selection:
   * - Persistent query: default chat conversation
   * - Cold-start query: only when forceColdStart is set
   */
  async *query(
    prompt: string,
    images?: ImageAttachment[],
    conversationHistory?: ChatMessage[],
    queryOptions?: QueryOptions
  ): AsyncGenerator<StreamChunk> {
    const vaultPath = getVaultPath(this.plugin.app);
    if (!vaultPath) {
      yield { type: 'error', content: 'Could not determine vault path' };
      return;
    }

    const resolvedClaudePath = this.plugin.getResolvedClaudeCliPath();
    if (!resolvedClaudePath) {
      yield { type: 'error', content: 'Claude CLI not found. Please install Claude Code CLI.' };
      return;
    }

    const customEnv = parseEnvironmentVariables(this.plugin.getActiveEnvironmentVariables());
    const enhancedPath = getEnhancedPath(customEnv.PATH, resolvedClaudePath);
    const missingNodeError = getMissingNodeError(resolvedClaudePath, enhancedPath);
    if (missingNodeError) {
      yield { type: 'error', content: missingNodeError };
      return;
    }

    // Rebuild history if needed before choosing persistent vs cold-start
    let promptToSend = prompt;
    let forceColdStart = false;

    // Clear interrupted flag - persistent query handles interruption gracefully,
    // no need to force cold-start just because user cancelled previous response
    if (this.sessionManager.wasInterrupted()) {
      this.sessionManager.clearInterrupted();
    }

    // Session mismatch recovery: SDK returned a different session ID (context lost)
    // Inject history to restore context without forcing cold-start
    if (this.sessionManager.needsHistoryRebuild() && conversationHistory && conversationHistory.length > 0) {
      const historyContext = buildContextFromHistory(conversationHistory);
      const actualPrompt = stripCurrentNoteContext(prompt);
      promptToSend = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory);
      this.sessionManager.clearHistoryRebuild();
    }

    const noSessionButHasHistory = !this.sessionManager.getSessionId() &&
      conversationHistory && conversationHistory.length > 0;

    if (noSessionButHasHistory) {
      const historyContext = buildContextFromHistory(conversationHistory!);
      const actualPrompt = stripCurrentNoteContext(prompt);
      promptToSend = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory!);

      // Note: Do NOT call invalidateSession() here. The cold-start will capture
      // a new session ID anyway, and invalidating would break any persistent query
      // restart that happens during the cold-start (causing SESSION MISMATCH).
      forceColdStart = true;
    }

    const effectiveQueryOptions = forceColdStart
      ? { ...queryOptions, forceColdStart: true }
      : queryOptions;

    if (forceColdStart) {
      // Set flag BEFORE closing to prevent consumer error from triggering restart
      this.coldStartInProgress = true;
      this.closePersistentQuery('session invalidated');
    }

    // Determine query path: persistent vs cold-start
    const shouldUsePersistent = !effectiveQueryOptions?.forceColdStart;

    if (shouldUsePersistent) {
      // Start persistent query if not running
      if (!this.persistentQuery && !this.shuttingDown) {
        await this.startPersistentQuery(
          vaultPath,
          resolvedClaudePath,
          this.sessionManager.getSessionId() ?? undefined
        );
      }

      if (this.persistentQuery && !this.shuttingDown) {
        // Use persistent query path
        try {
          yield* this.queryViaPersistent(promptToSend, images, vaultPath, resolvedClaudePath, effectiveQueryOptions);
          return;
        } catch (error) {
          if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
            this.sessionManager.invalidateSession();
            const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);

            this.coldStartInProgress = true;
            this.abortController = new AbortController();

            try {
              yield* this.queryViaSDK(
                retryRequest.prompt,
                vaultPath,
                resolvedClaudePath,
                // Use current message's images, fallback to history images
                images ?? retryRequest.images,
                effectiveQueryOptions
              );
            } catch (retryError) {
              const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
              yield { type: 'error', content: msg };
            } finally {
              this.coldStartInProgress = false;
              this.abortController = null;
            }
            return;
          }

          throw error;
        }
      }
    }

    // Cold-start path (existing logic)
    // Set flag to prevent consumer error restarts from interfering
    this.coldStartInProgress = true;
    this.abortController = new AbortController();

    try {
      yield* this.queryViaSDK(promptToSend, vaultPath, resolvedClaudePath, images, effectiveQueryOptions);
    } catch (error) {
      if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
        this.sessionManager.invalidateSession();
        const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);

        try {
          yield* this.queryViaSDK(
            retryRequest.prompt,
            vaultPath,
            resolvedClaudePath,
            // Use current message's images, fallback to history images
            images ?? retryRequest.images,
            effectiveQueryOptions
          );
        } catch (retryError) {
          const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
          yield { type: 'error', content: msg };
        }
        return;
      }

      const msg = error instanceof Error ? error.message : 'Unknown error';
      yield { type: 'error', content: msg };
    } finally {
      this.coldStartInProgress = false;
      this.abortController = null;
    }
  }

  private buildHistoryRebuildRequest(
    prompt: string,
    conversationHistory: ChatMessage[]
  ): { prompt: string; images?: ImageAttachment[] } {
    const historyContext = buildContextFromHistory(conversationHistory);
    const actualPrompt = stripCurrentNoteContext(prompt);
    const fullPrompt = buildPromptWithHistoryContext(historyContext, prompt, actualPrompt, conversationHistory);
    const lastUserMessage = getLastUserMessage(conversationHistory);

    return {
      prompt: fullPrompt,
      images: lastUserMessage?.images,
    };
  }

  /**
   * Query via persistent query (Phase 1.5).
   * Uses the message channel to send messages without cold-start latency.
   */
  private async *queryViaPersistent(
    prompt: string,
    images: ImageAttachment[] | undefined,
    vaultPath: string,
    cliPath: string,
    queryOptions?: QueryOptions
  ): AsyncGenerator<StreamChunk> {
    if (!this.persistentQuery || !this.messageChannel) {
      // Fallback to cold-start if persistent query not available
      yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
      return;
    }

    // Set allowed tools for canUseTool enforcement
    // undefined = no restriction, [] = no tools, [...] = restricted
    if (queryOptions?.allowedTools !== undefined) {
      this.currentAllowedTools = queryOptions.allowedTools.length > 0
        ? [...queryOptions.allowedTools, TOOL_SKILL]
        : [];
    } else {
      this.currentAllowedTools = null;
    }

    // Save allowedTools before applyDynamicUpdates - restart would clear it
    const savedAllowedTools = this.currentAllowedTools;

    // Apply dynamic updates before sending (Phase 1.6)
    await this.applyDynamicUpdates(queryOptions);

    // Restore allowedTools in case restart cleared it
    this.currentAllowedTools = savedAllowedTools;

    // Check if applyDynamicUpdates triggered a restart that failed
    // (e.g., CLI path not found, vault path missing)
    if (!this.persistentQuery || !this.messageChannel) {
      yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
      return;
    }
    if (!this.responseConsumerRunning) {
      yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
      return;
    }

    const message = this.buildSDKUserMessage(prompt, images);

    yield { type: 'sdk_user_uuid', uuid: message.uuid! };

    // Create a promise-based handler to yield chunks
    // Use a mutable state object to work around TypeScript's control flow analysis
    const state = {
      chunks: [] as StreamChunk[],
      resolveChunk: null as ((chunk: StreamChunk | null) => void) | null,
      done: false,
      error: null as Error | null,
    };

    const handlerId = `handler-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    const handler = createResponseHandler({
      id: handlerId,
      onChunk: (chunk) => {
        handler.markChunkSeen();
        if (state.resolveChunk) {
          state.resolveChunk(chunk);
          state.resolveChunk = null;
        } else {
          state.chunks.push(chunk);
        }
      },
      onDone: () => {
        state.done = true;
        if (state.resolveChunk) {
          state.resolveChunk(null);
          state.resolveChunk = null;
        }
      },
      onError: (err) => {
        state.error = err;
        state.done = true;
        if (state.resolveChunk) {
          state.resolveChunk(null);
          state.resolveChunk = null;
        }
      },
    });

    this.registerResponseHandler(handler);

    try {
      // Track message for crash recovery (Phase 1.3)
      this.lastSentMessage = message;
      this.lastSentQueryOptions = queryOptions ?? null;
      this.crashRecoveryAttempted = false;

      // Enqueue the message with race condition protection
      // The channel could close between our null check above and this call
      try {
        this.messageChannel.enqueue(message);
      } catch (error) {
        if (error instanceof Error && error.message.includes('closed')) {
          yield* this.queryViaSDK(prompt, vaultPath, cliPath, images, queryOptions);
          return;
        }
        throw error;
      }

      yield { type: 'sdk_user_sent', uuid: message.uuid! };

      // Yield chunks as they arrive
      while (!state.done) {
        if (state.chunks.length > 0) {
          yield state.chunks.shift()!;
        } else {
          const chunk = await new Promise<StreamChunk | null>((resolve) => {
            state.resolveChunk = resolve;
          });
          if (chunk) {
            yield chunk;
          }
        }
      }

      // Yield any remaining chunks
      while (state.chunks.length > 0) {
        yield state.chunks.shift()!;
      }

      // Check if an error occurred (assigned in onError callback)
      if (state.error) {
        // Re-throw session expired errors for outer retry logic to handle
        if (isSessionExpiredError(state.error)) {
          throw state.error;
        }
        yield { type: 'error', content: state.error.message };
      }

      // Clear message tracking after completion
      this.lastSentMessage = null;
      this.lastSentQueryOptions = null;

      yield { type: 'done' };
    } finally {
      this.unregisterResponseHandler(handlerId);
      this.currentAllowedTools = null;
    }
  }

  private buildSDKUserMessage(prompt: string, images?: ImageAttachment[]): SDKUserMessage {
    const sessionId = this.sessionManager.getSessionId() || '';

    if (!images || images.length === 0) {
      return {
        type: 'user',
        message: {
          role: 'user',
          content: prompt,
        },
        parent_tool_use_id: null,
        session_id: sessionId,
        uuid: randomUUID(),
      };
    }

    const content: UserContentBlock[] = [];

    for (const image of images) {
      content.push({
        type: 'image',
        source: {
          type: 'base64',
          media_type: image.mediaType,
          data: image.data,
        },
      });
    }

    if (prompt.trim()) {
      content.push({
        type: 'text',
        text: prompt,
      });
    }

    return {
      type: 'user',
      message: {
        role: 'user',
        content,
      },
      parent_tool_use_id: null,
      session_id: sessionId,
      uuid: randomUUID(),
    };
  }

  /**
   * Apply dynamic updates to the persistent query before sending a message (Phase 1.6).
   */
  private async applyDynamicUpdates(
    queryOptions?: QueryOptions,
    restartOptions?: ClosePersistentQueryOptions,
    allowRestart = true
  ): Promise<void> {
    if (!this.persistentQuery) return;

    if (!this.vaultPath) {
      return;
    }
    const cliPath = this.plugin.getResolvedClaudeCliPath();
    if (!cliPath) {
      return;
    }

    const selectedModel = queryOptions?.model || this.plugin.settings.model;
    const permissionMode = this.plugin.settings.permissionMode;

    // Model can always be updated dynamically
    if (this.currentConfig && selectedModel !== this.currentConfig.model) {
      try {
        await this.persistentQuery.setModel(selectedModel);
        this.currentConfig.model = selectedModel;
      } catch {
        new Notice('Failed to update model');
      }
    }

    // Update thinking tokens for custom models (adaptive models don't need dynamic updates)
    if (!isAdaptiveThinkingModel(selectedModel)) {
      const budgetConfig = THINKING_BUDGETS.find(b => b.value === this.plugin.settings.thinkingBudget);
      const thinkingTokens = budgetConfig?.tokens ?? null;
      const currentThinking = this.currentConfig?.thinkingTokens ?? null;
      if (thinkingTokens !== currentThinking) {
        try {
          await this.persistentQuery.setMaxThinkingTokens(thinkingTokens);
          if (this.currentConfig) {
            this.currentConfig.thinkingTokens = thinkingTokens;
          }
        } catch {
          new Notice('Failed to update thinking budget');
        }
      }
    }

    // Update permission mode if changed
    // Since we always start with allowDangerouslySkipPermissions: true,
    // we can dynamically switch between modes without restarting
    if (this.currentConfig && permissionMode !== this.currentConfig.permissionMode) {
      const sdkMode = this.mapToSDKPermissionMode(permissionMode);
      try {
        await this.persistentQuery.setPermissionMode(sdkMode);
        this.currentConfig.permissionMode = permissionMode;
      } catch {
        new Notice('Failed to update permission mode');
      }
    }

    // Update MCP servers if changed
    const mcpMentions = queryOptions?.mcpMentions || new Set<string>();
    const uiEnabledServers = queryOptions?.enabledMcpServers || new Set<string>();
    const combinedMentions = new Set([...mcpMentions, ...uiEnabledServers]);
    const mcpServers = this.mcpManager.getActiveServers(combinedMentions);
    // Include full config in key so config changes (not just name changes) trigger update
    const mcpServersKey = JSON.stringify(mcpServers);

    if (this.currentConfig && mcpServersKey !== this.currentConfig.mcpServersKey) {
      // Convert to McpServerConfig format
      const serverConfigs: Record<string, McpServerConfig> = {};
      for (const [name, config] of Object.entries(mcpServers)) {
        serverConfigs[name] = config as McpServerConfig;
      }
      try {
        await this.persistentQuery.setMcpServers(serverConfigs);
        this.currentConfig.mcpServersKey = mcpServersKey;
      } catch {
        new Notice('Failed to update MCP servers');
      }
    }

    // Track external context paths (used by hooks and for restart detection)
    const newExternalContextPaths = queryOptions?.externalContextPaths || [];
    this.currentExternalContextPaths = newExternalContextPaths;

    // Check for config changes that require restart
    if (!allowRestart) {
      return;
    }

    // Check if restart is needed using the valid cliPath we already have
    const newConfig = this.buildPersistentQueryConfig(this.vaultPath, cliPath, newExternalContextPaths);
    if (!this.needsRestart(newConfig)) {
      return;
    }

    // Restart is needed - use force to ensure query is closed even if CLI becomes unavailable
    const restarted = await this.ensureReady({
      externalContextPaths: newExternalContextPaths,
      preserveHandlers: restartOptions?.preserveHandlers,
      force: true,
    });

    // After restart, apply dynamic updates to the new process
    if (restarted && this.persistentQuery) {
      await this.applyDynamicUpdates(queryOptions, restartOptions, false);
    }
  }

  private isStreamTextEvent(message: SDKMessage): boolean {
    if (message.type !== 'stream_event') return false;
    const event = message.event;
    if (!event) return false;
    if (event.type === 'content_block_start') {
      return event.content_block?.type === 'text';
    }
    if (event.type === 'content_block_delta') {
      return event.delta?.type === 'text_delta';
    }
    return false;
  }

  private buildPromptWithImages(prompt: string, images?: ImageAttachment[]): string | AsyncGenerator<any> {
    if (!images || images.length === 0) {
      return prompt;
    }

    const content: UserContentBlock[] = [];

    // Images before text (Claude recommendation for best quality)
    for (const image of images) {
      content.push({
        type: 'image',
        source: {
          type: 'base64',
          media_type: image.mediaType,
          data: image.data,
        },
      });
    }

    if (prompt.trim()) {
      content.push({
        type: 'text',
        text: prompt,
      });
    }

    async function* messageGenerator() {
      yield {
        type: 'user',
        message: {
          role: 'user',
          content,
        },
      };
    }

    return messageGenerator();
  }

  private async *queryViaSDK(
    prompt: string,
    cwd: string,
    cliPath: string,
    images?: ImageAttachment[],
    queryOptions?: QueryOptions
  ): AsyncGenerator<StreamChunk> {
    const selectedModel = queryOptions?.model || this.plugin.settings.model;

    this.sessionManager.setPendingModel(selectedModel);
    this.vaultPath = cwd;

    const queryPrompt = this.buildPromptWithImages(prompt, images);
    const baseContext = this.buildQueryOptionsContext(cwd, cliPath);
    const externalContextPaths = queryOptions?.externalContextPaths || [];
    const hooks = this.buildHooks(externalContextPaths);
    const hasEditorContext = prompt.includes('<editor_selection');

    let allowedTools: string[] | undefined;
    if (queryOptions?.allowedTools !== undefined && queryOptions.allowedTools.length > 0) {
      const toolSet = new Set([...queryOptions.allowedTools, TOOL_SKILL]);
      allowedTools = [...toolSet];
    }

    const ctx: ColdStartQueryContext = {
      ...baseContext,
      abortController: this.abortController ?? undefined,
      sessionId: this.sessionManager.getSessionId() ?? undefined,
      modelOverride: queryOptions?.model,
      canUseTool: this.createApprovalCallback(),
      hooks,
      mcpMentions: queryOptions?.mcpMentions,
      enabledMcpServers: queryOptions?.enabledMcpServers,
      allowedTools,
      hasEditorContext,
      externalContextPaths,
    };

    const options = QueryOptionsBuilder.buildColdStartQueryOptions(ctx);

    let sawStreamText = false;
    try {
      const response = agentQuery({ prompt: queryPrompt, options });
      let streamSessionId: string | null = this.sessionManager.getSessionId();

      for await (const message of response) {
        if (this.isStreamTextEvent(message)) {
          sawStreamText = true;
        }
        if (this.abortController?.signal.aborted) {
          await response.interrupt();
          break;
        }

        for (const event of transformSDKMessage(message, this.getTransformOptions(selectedModel))) {
          if (isSessionInitEvent(event)) {
            this.sessionManager.captureSession(event.sessionId);
            streamSessionId = event.sessionId;
          } else if (isStreamChunk(event)) {
            if (message.type === 'assistant' && sawStreamText && event.type === 'text') {
              continue;
            }
            if (event.type === 'usage') {
              yield { ...event, sessionId: streamSessionId };
            } else {
              yield event;
            }
          }
        }

        if (message.type === 'result') {
          sawStreamText = false;
        }
      }
    } catch (error) {
      // Re-throw session expired errors for outer retry logic to handle
      if (isSessionExpiredError(error)) {
        throw error;
      }
      const msg = error instanceof Error ? error.message : 'Unknown error';
      yield { type: 'error', content: msg };
    } finally {
      this.sessionManager.clearPendingModel();
      this.currentAllowedTools = null; // Clear tool restriction after query
    }

    yield { type: 'done' };
  }

  cancel() {
    this.approvalDismisser?.();

    if (this.abortController) {
      this.abortController.abort();
      this.sessionManager.markInterrupted();
    }

    // Interrupt persistent query (Phase 1.9)
    if (this.persistentQuery && !this.shuttingDown) {
      void this.persistentQuery.interrupt().catch(() => {
        // Silence abort/interrupt errors
      });
    }
  }

  /**
   * Reset the conversation session.
   * Closes the persistent query since session is changing.
   */
  resetSession() {
    // Close persistent query (new session will use cold-start resume)
    this.closePersistentQuery('session reset');

    // Reset crash recovery for fresh start
    this.crashRecoveryAttempted = false;

    this.sessionManager.reset();
  }

  getSessionId(): string | null {
    return this.sessionManager.getSessionId();
  }

  /** Consume session invalidation flag for persistence updates. */
  consumeSessionInvalidation(): boolean {
    return this.sessionManager.consumeInvalidation();
  }

  /**
   * Check if the service is ready (persistent query is active).
   * Used to determine if SDK skills are available.
   */
  isReady(): boolean {
    return this.isPersistentQueryActive();
  }

  /**
   * Get supported commands (SDK skills) from the persistent query.
   * Returns an empty array if the query is not ready.
   */
  async getSupportedCommands(): Promise<SlashCommand[]> {
    if (!this.persistentQuery) {
      return [];
    }

    try {
      const sdkCommands: SDKSlashCommand[] = await this.persistentQuery.supportedCommands();
      return sdkCommands.map((cmd) => ({
        id: `sdk:${cmd.name}`,
        name: cmd.name,
        description: cmd.description,
        argumentHint: cmd.argumentHint,
        content: '', // SDK skills don't need content - they're handled by the SDK
        source: 'sdk' as const,
      }));
    } catch {
      // Empty array on error is intentional: callers (SlashCommandDropdown) keep
      // sdkSkillsFetched=false on empty results, allowing automatic retry.
      return [];
    }
  }

  /**
   * Set the session ID (for restoring from saved conversation).
   * Closes persistent query synchronously if session is changing, then ensures query is ready.
   *
   * @param id - Session ID to restore, or null for new session
   * @param externalContextPaths - External context paths for the session (prevents stale contexts)
   */
  setSessionId(id: string | null, externalContextPaths?: string[]): void {
    const currentId = this.sessionManager.getSessionId();
    const sessionChanged = currentId !== id;

    // Close synchronously when session changes (maintains backwards compatibility)
    if (sessionChanged) {
      this.closePersistentQuery('session switch');
      this.crashRecoveryAttempted = false;
    }

    this.sessionManager.setSessionId(id, this.plugin.settings.model);

    // Ensure query is ready with the new session ID and external contexts
    // Passing external contexts here prevents stale contexts from previous session
    this.ensureReady({
      sessionId: id ?? undefined,
      externalContextPaths,
    }).catch(() => {
      // Best-effort, ignore failures
    });
  }

  /**
   * Cleanup resources (Phase 5).
   * Called on plugin unload to close persistent query and abort any cold-start query.
   */
  cleanup() {
    // Close persistent query
    this.closePersistentQuery('plugin cleanup');

    // Cancel any in-flight cold-start query
    this.cancel();
    this.resetSession();
  }

  async rewindFiles(sdkUserUuid: string, dryRun?: boolean): Promise<RewindFilesResult> {
    if (!this.persistentQuery) throw new Error('No active query');
    if (this.shuttingDown) throw new Error('Service is shutting down');
    return this.persistentQuery.rewindFiles(sdkUserUuid, { dryRun });
  }

  private resolveRewindFilePath(filePath: string): string {
    if (path.isAbsolute(filePath)) {
      return filePath;
    }
    if (this.vaultPath) {
      return path.join(this.vaultPath, filePath);
    }
    return filePath;
  }

  private async createRewindBackup(filesChanged: string[] | undefined): Promise<{
    restore: () => Promise<void>;
    cleanup: () => Promise<void>;
  } | null> {
    if (!filesChanged || filesChanged.length === 0) {
      return null;
    }

    const backupRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'claudian-rewind-'));

    type BackupEntry =
      | { originalPath: string; existedBefore: false }
      | { originalPath: string; existedBefore: true; kind: 'file' | 'dir'; backupPath: string }
      | { originalPath: string; existedBefore: true; kind: 'symlink'; symlinkTarget: string };

    const entries: BackupEntry[] = [];

    const copyDir = async (from: string, to: string): Promise<void> => {
      await fs.promises.mkdir(to, { recursive: true });
      const dirents = await fs.promises.readdir(from, { withFileTypes: true });
      for (const dirent of dirents) {
        const srcPath = path.join(from, dirent.name);
        const destPath = path.join(to, dirent.name);

        if (dirent.isDirectory()) {
          await copyDir(srcPath, destPath);
          continue;
        }

        if (dirent.isSymbolicLink()) {
          const target = await fs.promises.readlink(srcPath);
          await fs.promises.symlink(target, destPath);
          continue;
        }

        if (dirent.isFile()) {
          await fs.promises.copyFile(srcPath, destPath);
        }
      }
    };

    const backupPathForIndex = (i: number) => path.join(backupRoot, String(i));

    for (let i = 0; i < filesChanged.length; i++) {
      const originalPath = this.resolveRewindFilePath(filesChanged[i]);

      try {
        const stats = await fs.promises.lstat(originalPath);

        if (stats.isSymbolicLink()) {
          const target = await fs.promises.readlink(originalPath);
          entries.push({ originalPath, existedBefore: true, kind: 'symlink', symlinkTarget: target });
          continue;
        }

        const backupPath = backupPathForIndex(i);

        if (stats.isDirectory()) {
          await copyDir(originalPath, backupPath);
          entries.push({ originalPath, existedBefore: true, kind: 'dir', backupPath });
          continue;
        }

        if (stats.isFile()) {
          await fs.promises.copyFile(originalPath, backupPath);
          entries.push({ originalPath, existedBefore: true, kind: 'file', backupPath });
          continue;
        }

        // Unsupported file type; treat as non-existent for rollback purposes.
        entries.push({ originalPath, existedBefore: false });
      } catch (error) {
        const err = error as NodeJS.ErrnoException;
        if (err && err.code === 'ENOENT') {
          entries.push({ originalPath, existedBefore: false });
          continue;
        }

        await fs.promises.rm(backupRoot, { recursive: true, force: true });
        throw error;
      }
    }

    const restore = async () => {
      const errors: unknown[] = [];

      for (const entry of entries) {
        try {
          if (!entry.existedBefore) {
            await fs.promises.rm(entry.originalPath, { recursive: true, force: true });
            continue;
          }

          await fs.promises.rm(entry.originalPath, { recursive: true, force: true });
          await fs.promises.mkdir(path.dirname(entry.originalPath), { recursive: true });

          if (entry.kind === 'symlink') {
            await fs.promises.symlink(entry.symlinkTarget, entry.originalPath);
            continue;
          }

          if (entry.kind === 'dir') {
            await copyDir(entry.backupPath, entry.originalPath);
            continue;
          }

          await fs.promises.copyFile(entry.backupPath, entry.originalPath);
        } catch (error) {
          errors.push(error);
        }
      }

      if (errors.length > 0) {
        throw new Error(`Failed to restore ${errors.length} file(s) after rewind failure.`);
      }
    };

    const cleanup = async () => {
      await fs.promises.rm(backupRoot, { recursive: true, force: true });
    };

    return { restore, cleanup };
  }

  async rewind(sdkUserUuid: string, sdkAssistantUuid: string): Promise<RewindFilesResult> {
    // SDK only returns filesChanged/insertions/deletions on dry runs
    const preview = await this.rewindFiles(sdkUserUuid, true);
    if (!preview.canRewind) return preview;

    const backup = await this.createRewindBackup(preview.filesChanged);

    try {
      const result = await this.rewindFiles(sdkUserUuid);
      if (!result.canRewind) {
        await backup?.restore();
        this.closePersistentQuery('rewind failed');
        return result;
      }

      this.pendingResumeAt = sdkAssistantUuid;
      this.closePersistentQuery('rewind');
      return {
        ...result,
        filesChanged: preview.filesChanged,
        insertions: preview.insertions,
        deletions: preview.deletions,
      };
    } catch (error) {
      try {
        await backup?.restore();
      } catch (rollbackError) {
        this.closePersistentQuery('rewind failed');
        throw new Error(
          `Rewind failed and files could not be fully restored: ${rollbackError instanceof Error ? rollbackError.message : 'Unknown error'}`
        );
      }

      this.closePersistentQuery('rewind failed');
      throw new Error(`Rewind failed but files were restored: ${error instanceof Error ? error.message : 'Unknown error'}`);
    } finally {
      await backup?.cleanup();
    }
  }

  setApprovalCallback(callback: ApprovalCallback | null) {
    this.approvalCallback = callback;
  }

  setApprovalDismisser(dismisser: (() => void) | null) {
    this.approvalDismisser = dismisser;
  }

  setAskUserQuestionCallback(callback: AskUserQuestionCallback | null) {
    this.askUserQuestionCallback = callback;
  }

  setExitPlanModeCallback(callback: ExitPlanModeCallback | null): void {
    this.exitPlanModeCallback = callback;
  }

  setPermissionModeSyncCallback(callback: ((sdkMode: string) => void) | null): void {
    this.permissionModeSyncCallback = callback;
  }

  setSubagentHookProvider(getState: () => SubagentHookState): void {
    this._subagentStateProvider = getState;
  }

  setAutoTurnCallback(callback: ((chunks: StreamChunk[]) => void) | null): void {
    this._autoTurnCallback = callback;
  }

  private createApprovalCallback(): CanUseTool {
    return async (toolName, input, options): Promise<PermissionResult> => {
      if (this.currentAllowedTools !== null) {
        if (!this.currentAllowedTools.includes(toolName) && toolName !== TOOL_SKILL) {
          const allowedList = this.currentAllowedTools.length > 0
            ? ` Allowed tools: ${this.currentAllowedTools.join(', ')}.`
            : ' No tools are allowed for this query type.';
          return {
            behavior: 'deny',
            message: `Tool "${toolName}" is not allowed for this query.${allowedList}`,
          };
        }
      }

      // ExitPlanMode uses a dedicated callback — bypasses normal approval flow
      if (toolName === TOOL_EXIT_PLAN_MODE && this.exitPlanModeCallback) {
        try {
          const decision: ExitPlanModeDecision | null = await this.exitPlanModeCallback(input, options.signal);
          if (decision === null) {
            return { behavior: 'deny', message: 'User cancelled.', interrupt: true };
          }
          if (decision.type === 'feedback') {
            return { behavior: 'deny', message: decision.text, interrupt: false };
          }
          // Callback already restored plugin.settings.permissionMode
          const sdkMode = this.mapToSDKPermissionMode(this.plugin.settings.permissionMode);
          // Sync config so applyDynamicUpdates doesn't re-send
          if (this.currentConfig) {
            this.currentConfig.permissionMode = this.plugin.settings.permissionMode;
          }
          return {
            behavior: 'allow',
            updatedInput: input,
            updatedPermissions: [
              { type: 'setMode', mode: sdkMode, destination: 'session' },
            ],
          };
        } catch (error) {
          return {
            behavior: 'deny',
            message: `Failed to handle plan mode exit: ${error instanceof Error ? error.message : 'Unknown error'}`,
            interrupt: true,
          };
        }
      }

      // AskUserQuestion uses a dedicated callback — bypasses normal approval flow
      if (toolName === TOOL_ASK_USER_QUESTION && this.askUserQuestionCallback) {
        try {
          const answers = await this.askUserQuestionCallback(input, options.signal);
          if (answers === null) {
            return { behavior: 'deny', message: 'User declined to answer.', interrupt: true };
          }
          return { behavior: 'allow', updatedInput: { ...input, answers } };
        } catch (error) {
          return {
            behavior: 'deny',
            message: `Failed to get user answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
            interrupt: true,
          };
        }
      }

      // No pre-check — SDK already checked permanent rules before calling canUseTool
      if (!this.approvalCallback) {
        return { behavior: 'deny', message: 'No approval handler available.' };
      }

      try {
        const { decisionReason, blockedPath, agentID } = options;
        const description = getActionDescription(toolName, input);
        const decision = await this.approvalCallback(
          toolName, input, description,
          { decisionReason, blockedPath, agentID }
        );

        if (decision === 'cancel') {
          return { behavior: 'deny', message: 'User interrupted.', interrupt: true };
        }

        if (decision === 'allow' || decision === 'allow-always') {
          const updatedPermissions = buildPermissionUpdates(
            toolName, input, decision, options.suggestions
          );
          return { behavior: 'allow', updatedInput: input, updatedPermissions };
        }

        return { behavior: 'deny', message: 'User denied this action.', interrupt: false };
      } catch (error) {
        // Don't interrupt session — the deny message is sufficient for Claude
        // to try an alternative approach or ask the user.
        return {
          behavior: 'deny',
          message: `Approval request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
          interrupt: false,
        };
      }
    };
  }

  private mapToSDKPermissionMode(mode: PermissionMode): SDKPermissionMode {
    if (mode === 'yolo') return 'bypassPermissions';
    if (mode === 'plan') return 'plan';
    return 'acceptEdits';
  }
}


================================================
FILE: src/core/agent/MessageChannel.ts
================================================
/**
 * Message Channel
 *
 * Queue-based async iterable for persistent queries.
 * Handles message queuing, turn management, and text merging.
 */

import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';

import {
  MESSAGE_CHANNEL_CONFIG,
  type PendingMessage,
  type PendingTextMessage,
} from './types';

/**
 * MessageChannel - Queue-based async iterable for persistent queries.
 *
 * Rules:
 * - Single in-flight turn at a time
 * - Text-only messages merge with \n\n while a turn is active
 * - Attachment messages (with images) queue one at a time; newer replaces older while turn is active
 * - Overflow policy: drop newest and warn
 */
export class MessageChannel implements AsyncIterable<SDKUserMessage> {
  private queue: PendingMessage[] = [];
  private turnActive = false;
  private closed = false;
  private resolveNext: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
  private currentSessionId: string | null = null;
  private onWarning: (message: string) => void;

  constructor(onWarning: (message: string) => void = () => {}) {
    this.onWarning = onWarning;
  }

  setSessionId(sessionId: string): void {
    this.currentSessionId = sessionId;
  }

  isTurnActive(): boolean {
    return this.turnActive;
  }

  isClosed(): boolean {
    return this.closed;
  }

  /**
   * Enqueue a message. If a turn is active:
   * - Text-only: merge with queued text (up to MAX_MERGED_CHARS)
   * - With attachments: replace any existing queued attachment (one at a time)
   */
  enqueue(message: SDKUserMessage): void {
    if (this.closed) {
      throw new Error('MessageChannel is closed');
    }

    const hasAttachments = this.messageHasAttachments(message);

    if (!this.turnActive) {
      if (this.resolveNext) {
        // Consumer is waiting - deliver immediately and mark turn active
        this.turnActive = true;
        const resolve = this.resolveNext;
        this.resolveNext = null;
        resolve({ value: message, done: false });
      } else {
        // No consumer waiting yet - queue for later pickup by next()
        // Don't set turnActive here; next() will set it when it dequeues
        if (this.queue.length >= MESSAGE_CHANNEL_CONFIG.MAX_QUEUED_MESSAGES) {
          this.onWarning(`[MessageChannel] Queue full (${MESSAGE_CHANNEL_CONFIG.MAX_QUEUED_MESSAGES}), dropping newest`);
          return;
        }
        if (hasAttachments) {
          this.queue.push({ type: 'attachment', message });
        } else {
          this.queue.push({ type: 'text', content: this.extractTextContent(message) });
        }
      }
      return;
    }

    // Turn is active - queue the message
    if (hasAttachments) {
      // Non-text messages are deferred as-is (one at a time)
      // Find existing attachment message or add new one
      const existingIdx = this.queue.findIndex(m => m.type === 'attachment');
      if (existingIdx >= 0) {
        // Replace existing (newer takes precedence for attachments)
        this.queue[existingIdx] = { type: 'attachment', message };
        this.onWarning('[MessageChannel] Attachment message replaced (only one can be queued)');
      } else {
        this.queue.push({ type: 'attachment', message });
      }
      return;
    }

    // Text-only - merge with existing text in queue
    const textContent = this.extractTextContent(message);
    const existingTextIdx = this.queue.findIndex(m => m.type === 'text');

    if (existingTextIdx >= 0) {
      const existing = this.queue[existingTextIdx] as PendingTextMessage;
      const mergedContent = existing.content + '\n\n' + textContent;

      // Check merged size
      if (mergedContent.length > MESSAGE_CHANNEL_CONFIG.MAX_MERGED_CHARS) {
        this.onWarning(`[MessageChannel] Merged content exceeds ${MESSAGE_CHANNEL_CONFIG.MAX_MERGED_CHARS} chars, dropping newest`);
        return;
      }

      existing.content = mergedContent;
    } else {
      // No existing text - add new
      if (this.queue.length >= MESSAGE_CHANNEL_CONFIG.MAX_QUEUED_MESSAGES) {
        this.onWarning(`[MessageChannel] Queue full (${MESSAGE_CHANNEL_CONFIG.MAX_QUEUED_MESSAGES}), dropping newest`);
        return;
      }
      this.queue.push({ type: 'text', content: textContent });
    }
  }

  onTurnComplete(): void {
    this.turnActive = false;

    if (this.queue.length > 0 && this.resolveNext) {
      const pending = this.queue.shift()!;
      this.turnActive = true;
      const resolve = this.resolveNext;
      this.resolveNext = null;
      resolve({ value: this.pendingToMessage(pending), done: false });
    }
  }

  close(): void {
    this.closed = true;
    this.queue = [];
    if (this.resolveNext) {
      const resolve = this.resolveNext;
      this.resolveNext = null;
      resolve({ value: undefined, done: true } as IteratorResult<SDKUserMessage>);
    }
  }

  reset(): void {
    this.queue = [];
    this.turnActive = false;
    this.closed = false;
    this.resolveNext = null;
  }

  getQueueLength(): number {
    return this.queue.length;
  }

  [Symbol.asyncIterator](): AsyncIterator<SDKUserMessage> {
    return {
      next: (): Promise<IteratorResult<SDKUserMessage>> => {
        if (this.closed) {
          return Promise.resolve({ value: undefined, done: true } as IteratorResult<SDKUserMessage>);
        }

        // If there's a queued message and no active turn, return it
        if (this.queue.length > 0 && !this.turnActive) {
          const pending = this.queue.shift()!;
          this.turnActive = true;
          return Promise.resolve({ value: this.pendingToMessage(pending), done: false });
        }

        // Wait for next message
        return new Promise((resolve) => {
          this.resolveNext = resolve;
        });
      },
    };
  }

  private messageHasAttachments(message: SDKUserMessage): boolean {
    if (!message.message?.content) return false;
    if (typeof message.message.content === 'string') return false;
    return message.message.content.some((block: { type: string }) => block.type === 'image');
  }

  private extractTextContent(message: SDKUserMessage): string {
    if (!message.message?.content) return '';
    if (typeof message.message.content === 'string') return message.message.content;
    return message.message.content
      .filter((block: { type: string }): block is { type: 'text'; text: string } => block.type === 'text')
      .map((block: { type: 'text'; text: string }) => block.text)
      .join('\n\n');
  }

  private pendingToMessage(pending: PendingMessage): SDKUserMessage {
    if (pending.type === 'attachment') {
      return pending.message;
    }

    return {
      type: 'user',
      message: {
        role: 'user',
        content: pending.content,
      },
      parent_tool_use_id: null,
      session_id: this.currentSessionId || '',
    };
  }
}


================================================
FILE: src/core/agent/QueryOptionsBuilder.ts
================================================
/**
 * QueryOptionsBuilder - SDK Options Construction
 *
 * Extracts options-building logic from ClaudianService for:
 * - Persistent query options (warm path)
 * - Cold-start query options
 * - Configuration change detection
 *
 * Design: Static builder methods that take a context object containing
 * all required dependencies (settings, managers, paths).
 */

import type {
  CanUseTool,
  Options,
} from '@anthropic-ai/claude-agent-sdk';

import type { McpServerManager } from '../mcp';
import type { PluginManager } from '../plugins';
import { buildSystemPrompt, type SystemPromptSettings } from '../prompts/mainAgent';
import type { ClaudianSettings, PermissionMode } from '../types';
import { isAdaptiveThinkingModel, THINKING_BUDGETS } from '../types';
import { createCustomSpawnFunction } from './customSpawn';
import {
  computeSystemPromptKey,
  DISABLED_BUILTIN_SUBAGENTS,
  type PersistentQueryConfig,
  UNSUPPORTED_SDK_TOOLS,
} from './types';

/**
 * Context required for building SDK options.
 * Passed to builder methods to avoid direct dependencies on ClaudianService.
 */
export interface QueryOptionsContext {
  /** Absolute path to the vault root. */
  vaultPath: string;
  /** Path to the Claude CLI executable. */
  cliPath: string;
  /** Current plugin settings. */
  settings: ClaudianSettings;
  /** Parsed environment variables (from settings). */
  customEnv: Record<string, string>;
  /** Enhanced PATH with CLI directories. */
  enhancedPath: string;
  /** MCP server manager for server configuration. */
  mcpManager: McpServerManager;
  /** Plugin manager for Claude Code plugins. */
  pluginManager: PluginManager;
}

/**
 * Additional context for persistent query options.
 */
export interface PersistentQueryContext extends QueryOptionsContext {
  /** AbortController for the query. */
  abortController?: AbortController;
  /** Session resume state (sessionId required; sessionAt and fork only meaningful with a session). */
  resume?: {
    sessionId: string;
    /** Assistant UUID for resumeSessionAt after rewind. */
    sessionAt?: string;
    /** Fork the session (non-destructive branch). */
    fork?: boolean;
  };
  /** Approval callback for normal mode. */
  canUseTool?: CanUseTool;
  /** Pre-built hooks array. */
  hooks: Options['hooks'];
  /** External context paths for additionalDirectories SDK option. */
  externalContextPaths?: string[];
}

/**
 * Additional context for cold-start query options.
 */
export interface ColdStartQueryContext extends QueryOptionsContext {
  /** AbortController for the query. */
  abortController?: AbortController;
  /** Session ID for resuming a conversation. */
  sessionId?: string;
  /** Optional model override for cold-start queries. */
  modelOverride?: string;
  /** Approval callback for normal mode. */
  canUseTool?: CanUseTool;
  /** Pre-built hooks array. */
  hooks: Options['hooks'];
  /** MCP server @-mentions from the query. */
  mcpMentions?: Set<string>;
  /** MCP servers enabled via UI selector. */
  enabledMcpServers?: Set<string>;
  /** Allowed tools restriction (undefined = no restriction). */
  allowedTools?: string[];
  /** Whether the query has editor context. */
  hasEditorContext: boolean;
  /** External context paths for additionalDirectories SDK option. */
  externalContextPaths?: string[];
}

/** Static builder for SDK Options and configuration objects. */
export class QueryOptionsBuilder {
  /**
   * Some changes (model, thinking tokens) can be updated dynamically; others require restart.
   */
  static needsRestart(
    currentConfig: PersistentQueryConfig | null,
    newConfig: PersistentQueryConfig
  ): boolean {
    if (!currentConfig) return true;

    // These require restart (cannot be updated dynamically)
    if (currentConfig.systemPromptKey !== newConfig.systemPromptKey) return true;
    if (currentConfig.disallowedToolsKey !== newConfig.disallowedToolsKey) return true;
    if (currentConfig.pluginsKey !== newConfig.pluginsKey) return true;
    if (currentConfig.settingSources !== newConfig.settingSources) return true;
    if (currentConfig.claudeCliPath !== newConfig.claudeCliPath) return true;

    // Note: Permission mode is handled dynamically via setPermissionMode() in ClaudianService.
    // Since allowDangerouslySkipPermissions is always true, both directions work without restart.

    if (currentConfig.enableChrome !== newConfig.enableChrome) return true;

    // Effort level requires restart (no setEffort() on persistent query)
    if (currentConfig.effortLevel !== newConfig.effortLevel) return true;

    // Export paths affect system prompt
    if (QueryOptionsBuilder.pathsChanged(currentConfig.allowedExportPaths, newConfig.allowedExportPaths)) {
      return true;
    }

    // External context paths require restart (additionalDirectories can't be updated dynamically)
    if (QueryOptionsBuilder.pathsChanged(currentConfig.externalContextPaths, newConfig.externalContextPaths)) {
      return true;
    }

    return false;
  }

  /** Builds configuration snapshot for restart detection. */
  static buildPersistentQueryConfig(
    ctx: QueryOptionsContext,
    externalContextPaths?: string[]
  ): PersistentQueryConfig {
    const systemPromptSettings: SystemPromptSettings = {
      mediaFolder: ctx.settings.mediaFolder,
      customPrompt: ctx.settings.systemPrompt,
      allowedExportPaths: ctx.settings.allowedExportPaths,
      allowExternalAccess: ctx.settings.allowExternalAccess,
      vaultPath: ctx.vaultPath,
      userName: ctx.settings.userName,
    };

    const budgetSetting = ctx.settings.thinkingBudget;
    const budgetConfig = THINKING_BUDGETS.find(b => b.value === budgetSetting);
    const thinkingTokens = budgetConfig?.tokens ?? null;

    // Compute disallowedToolsKey from all disabled MCP tools (pre-registered upfront)
    const allDisallowedTools = ctx.mcpManager.getAllDisallowedMcpTools();
    const disallowedToolsKey = allDisallowedTools.join('|');

    // Compute pluginsKey from active plugins
    const pluginsKey = ctx.pluginManager.getPluginsKey();

    return {
      model: ctx.settings.model,
      thinkingTokens: thinkingTokens && thinkingTokens > 0 ? thinkingTokens : null,
      effortLevel: isAdaptiveThinkingModel(ctx.settings.model) ? ctx.settings.effortLevel : null,
      permissionMode: ctx.settings.permissionMode,
      systemPromptKey: computeSystemPromptKey(systemPromptSettings),
      disallowedToolsKey,
      mcpServersKey: '', // Dynamic via setMcpServers, not tracked for restart
      pluginsKey,
      externalContextPaths: externalContextPaths || [],
      allowedExportPaths: ctx.settings.allowedExportPaths,
      settingSources: ctx.settings.loadUserClaudeSettings ? 'user,project' : 'project',
      claudeCliPath: ctx.cliPath,
      enableChrome: ctx.settings.enableChrome,
    };
  }

  /** Builds SDK options for the persistent query. */
  static buildPersistentQueryOptions(ctx: PersistentQueryContext): Options {
    const permissionMode = ctx.settings.permissionMode;

    const systemPrompt = buildSystemPrompt({
      mediaFolder: ctx.settings.mediaFolder,
      customPrompt: ctx.settings.systemPrompt,
      allowedExportPaths: ctx.settings.allowedExportPaths,
      allowExternalAccess: ctx.settings.allowExternalAccess,
      vaultPath: ctx.vaultPath,
      userName: ctx.settings.userName,
    });

    const options: Options = {
      cwd: ctx.vaultPath,
      systemPrompt,
      model: ctx.settings.model,
      abortController: ctx.abortController,
      pathToClaudeCodeExecutable: ctx.cliPath,
      settingSources: ctx.settings.loadUserClaudeSettings
        ? ['user', 'project']
        : ['project'],
      env: {
        ...process.env,
        ...ctx.customEnv,
        PATH: ctx.enhancedPath,
      },
      includePartialMessages: true,
    };

    QueryOptionsBuilder.applyExtraArgs(options, ctx.settings);

    options.disallowedTools = [
      ...ctx.mcpManager.getAllDisallowedMcpTools(),
      ...UNSUPPORTED_SDK_TOOLS,
      ...DISABLED_BUILTIN_SUBAGENTS,
    ];

    QueryOptionsBuilder.applyPermissionMode(options, permissionMode, ctx.canUseTool);
    QueryOptionsBuilder.applyThinking(options, ctx.settings, ctx.settings.model);
    options.hooks = ctx.hooks;

    options.enableFileCheckpointing = true;

    if (ctx.resume) {
      options.resume = ctx.resume.sessionId;
      if (ctx.resume.sessionAt) {
        options.resumeSessionAt = ctx.resume.sessionAt;
      }
      if (ctx.resume.fork) {
        options.forkSession = true;
      }
    }

    if (ctx.externalContextPaths && ctx.externalContextPaths.length > 0) {
      options.additionalDirectories = ctx.externalContextPaths;
    }

    options.spawnClaudeCodeProcess = createCustomSpawnFunction(ctx.enhancedPath);

    return options;
  }

  /** Builds SDK options for a cold-start query. */
  static buildColdStartQueryOptions(ctx: ColdStartQueryContext): Options {
    const permissionMode = ctx.settings.permissionMode;

    const selectedModel = ctx.modelOverride ?? ctx.settings.model;
    const systemPrompt = buildSystemPrompt({
      mediaFolder: ctx.settings.mediaFolder,
      customPrompt: ctx.settings.systemPrompt,
      allowedExportPaths: ctx.settings.allowedExportPaths,
      allowExternalAccess: ctx.settings.allowExternalAccess,
      vaultPath: ctx.vaultPath,
      userName: ctx.settings.userName,
    });

    const options: Options = {
      cwd: ctx.vaultPath,
      systemPrompt,
      model: selectedModel,
      abortController: ctx.abortController,
      pathToClaudeCodeExecutable: ctx.cliPath,
      // User settings may contain permission rules that bypass Claudian's permission system
      settingSources: ctx.settings.loadUserClaudeSettings
        ? ['user', 'project']
        : ['project'],
      env: {
        ...process.env,
        ...ctx.customEnv,
        PATH: ctx.enhancedPath,
      },
      includePartialMessages: true,
    };

    QueryOptionsBuilder.applyExtraArgs(options, ctx.settings);

    const mcpMentions = ctx.mcpMentions || new Set<string>();
    const uiEnabledServers = ctx.enabledMcpServers || new Set<string>();
    const combinedMentions = new Set([...mcpMentions, ...uiEnabledServers]);
    const mcpServers = ctx.mcpManager.getActiveServers(combinedMentions);

    if (Object.keys(mcpServers).length > 0) {
      options.mcpServers = mcpServers;
    }

    const disallowedMcpTools = ctx.mcpManager.getDisallowedMcpTools(combinedMentions);
    options.disallowedTools = [
      ...disallowedMcpTools,
      ...UNSUPPORTED_SDK_TOOLS,
      ...DISABLED_BUILTIN_SUBAGENTS,
    ];

    QueryOptionsBuilder.applyPermissionMode(options, permissionMode, ctx.canUseTool);
    options.hooks = ctx.hooks;
    QueryOptionsBuilder.applyThinking(options, ctx.settings, ctx.modelOverride ?? ctx.settings.model);

    if (ctx.allowedTools !== undefined && ctx.allowedTools.length > 0) {
      options.tools = ctx.allowedTools;
    }

    if (ctx.sessionId) {
      options.resume = ctx.sessionId;
    }

    if (ctx.externalContextPaths && ctx.externalContextPaths.length > 0) {
      options.additionalDirectories = ctx.externalContextPaths;
    }

    options.spawnClaudeCodeProcess = createCustomSpawnFunction(ctx.enhancedPath);

    return options;
  }

  /**
   * Always sets allowDangerouslySkipPermissions: true to enable dynamic
   * switching between permission modes without requiring a process restart.
   */
  private static applyPermissionMode(
    options: Options,
    permissionMode: PermissionMode,
    canUseTool?: CanUseTool
  ): void {
    options.allowDangerouslySkipPermissions = true;

    if (canUseTool) {
      options.canUseTool = canUseTool;
    }

    if (permissionMode === 'yolo') {
      options.permissionMode = 'bypassPermissions';
    } else if (permissionMode === 'plan') {
      options.permissionMode = 'plan';
    } else {
      options.permissionMode = 'acceptEdits';
    }
  }

  private static applyExtraArgs(options: Options, settings: ClaudianSettings): void {
    if (settings.enableChrome) {
      options.extraArgs = { ...options.extraArgs, chrome: null };
    }
  }

  private static applyThinking(
    options: Options,
    settings: ClaudianSettings,
    model: string
  ): void {
    if (isAdaptiveThinkingModel(model)) {
      options.thinking = { type: 'adaptive' };
      options.effort = settings.effortLevel;
    } else {
      const budgetConfig = THINKING_BUDGETS.find(b => b.value === settings.thinkingBudget);
      if (budgetConfig && budgetConfig.tokens > 0) {
        options.maxThinkingTokens = budgetConfig.tokens;
      }
    }
  }

  private static pathsChanged(a?: string[], b?: string[]): boolean {
    const aKey = [...(a || [])].sort().join('|');
    const bKey = [...(b || [])].sort().join('|');
    return aKey !== bKey;
  }

}


================================================
FILE: src/core/agent/SessionManager.ts
================================================
/**
 * Session Manager
 *
 * Manages SDK session state including session ID, model tracking,
 * and interruption state.
 */

import type { ClaudeModel } from '../types';
import type { SessionState } from './types';

/**
 * Manages session state for the Claude Agent SDK.
 *
 * Tracks:
 * - Session ID: Unique identifier for the conversation
 * - Session model: The model used for this session
 * - Pending model: Model to use when session is captured
 * - Interrupted state: Whether the session was interrupted
 *
 * Typical flow:
 * 1. setPendingModel() - before starting a query
 * 2. captureSession() - when session_id received from SDK
 * 3. invalidateSession() - when session expires or errors occur
 */
export class SessionManager {
  private state: SessionState = {
    sessionId: null,
    sessionModel: null,
    pendingSessionModel: null,
    wasInterrupted: false,
    needsHistoryRebuild: false,
    sessionInvalidated: false,
  };

  getSessionId(): string | null {
    return this.state.sessionId;
  }

  setSessionId(id: string | null, defaultModel?: ClaudeModel): void {
    this.state.sessionId = id;
    this.state.sessionModel = id ? (defaultModel ?? null) : null;
    // Clear rebuild flag when switching sessions to prevent carrying over to different conversation
    this.state.needsHistoryRebuild = false;
    // Clear invalidation flag when explicitly setting session
    this.state.sessionInvalidated = false;
  }

  wasInterrupted(): boolean {
    return this.state.wasInterrupted;
  }

  markInterrupted(): void {
    this.state.wasInterrupted = true;
  }

  clearInterrupted(): void {
    this.state.wasInterrupted = false;
  }

  setPendingModel(model: ClaudeModel): void {
    this.state.pendingSessionModel = model;
  }

  clearPendingModel(): void {
    this.state.pendingSessionModel = null;
  }

  /**
   * Captures a session ID from SDK response.
   * Detects mismatch if we had a different session ID before (context lost).
   */
  captureSession(sessionId: string): void {
    // Detect mismatch: we had a session, but SDK gave us a different one
    const hadSession = this.state.sessionId !== null;
    const isDifferent = this.state.sessionId !== sessionId;
    if (hadSession && isDifferent) {
      // SDK lost our session context - need to rebuild history on next message
      this.state.needsHistoryRebuild = true;
    }

    this.state.sessionId = sessionId;
    this.state.sessionModel = this.state.pendingSessionModel;
    this.state.pendingSessionModel = null;
    this.state.sessionInvalidated = false;
  }

  needsHistoryRebuild(): boolean {
    return this.state.needsHistoryRebuild;
  }

  clearHistoryRebuild(): void {
    this.state.needsHistoryRebuild = false;
  }

  invalidateSession(): void {
    this.state.sessionId = null;
    this.state.sessionModel = null;
    this.state.sessionInvalidated = true;
  }

  /** Consume the invalidation flag (returns true once). */
  consumeInvalidation(): boolean {
    const wasInvalidated = this.state.sessionInvalidated;
    this.state.sessionInvalidated = false;
    return wasInvalidated;
  }

  reset(): void {
    this.state = {
      sessionId: null,
      sessionModel: null,
      pendingSessionModel: null,
      wasInterrupted: false,
      needsHistoryRebuild: false,
      sessionInvalidated: false,
    };
  }
}


================================================
FILE: src/core/agent/customSpawn.ts
================================================
/**
 * Custom spawn logic for Claude Agent SDK.
 *
 * Provides a custom spawn function that resolves the full path to Node.js
 * instead of relying on PATH lookup. This fixes issues in GUI apps (like Obsidian)
 * where the minimal PATH doesn't include Node.js.
 */

import type { SpawnedProcess, SpawnOptions } from '@anthropic-ai/claude-agent-sdk';
import { spawn } from 'child_process';

import { findNodeExecutable } from '../../utils/env';

export function createCustomSpawnFunction(
  enhancedPath: string
): (options: SpawnOptions) => SpawnedProcess {
  return (options: SpawnOptions): SpawnedProcess => {
    let { command } = options;
    const { args, cwd, env, signal } = options;
    const shouldPipeStderr = !!env?.DEBUG_CLAUDE_AGENT_SDK;

    // Resolve full path to avoid PATH lookup issues in GUI apps
    if (command === 'node') {
      const nodeFullPath = findNodeExecutable(enhancedPath);
      if (nodeFullPath) {
        command = nodeFullPath;
      }
    }

    // Do not pass `signal` directly to spawn() — Obsidian's Electron runtime
    // uses a different realm for AbortSignal, causing `instanceof EventTarget`
    // checks inside Node's internals to fail. Handle abort manually instead.
    const child = spawn(command, args, {
      cwd,
      env: env as NodeJS.ProcessEnv,
      stdio: ['pipe', 'pipe', shouldPipeStderr ? 'pipe' : 'ignore'],
      windowsHide: true,
    });

    if (signal) {
      if (signal.aborted) {
        child.kill();
      } else {
        signal.addEventListener('abort', () => child.kill(), { once: true });
      }
    }

    if (shouldPipeStderr && child.stderr && typeof child.stderr.on === 'function') {
      child.stderr.on('data', () => {});
    }

    if (!child.stdin || !child.stdout) {
      throw new Error('Failed to create process streams');
    }

    return child as unknown as SpawnedProcess;
  };
}


================================================
FILE: src/core/agent/index.ts
================================================
export { type ApprovalCallback, type ApprovalCallbackOptions, ClaudianService, type QueryOptions } from './ClaudianService';
export { MessageChannel } from './MessageChannel';
export {
  type ColdStartQueryContext,
  type PersistentQueryContext,
  QueryOptionsBuilder,
  type QueryOptionsContext,
} from './QueryOptionsBuilder';
export { SessionManager } from './SessionManager';
export type {
  ClosePersistentQueryOptions,
  PersistentQueryConfig,
  ResponseHandler,
  SessionState,
  UserContentBlock,
} from './types';


================================================
FILE: src/core/agent/types.ts
================================================
/**
 * Types and constants for the ClaudianService module.
 */

import type { SDKMessage, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';

import type { SystemPromptSettings } from '../prompts/mainAgent';
import type { ClaudeModel, EffortLevel, PermissionMode, StreamChunk } from '../types';

export interface TextContentBlock {
  type: 'text';
  text: string;
}

export interface ImageContentBlock {
  type: 'image';
  source: {
    type: 'base64';
    media_type: string;
    data: string;
  };
}

export type UserContentBlock = TextContentBlock | ImageContentBlock;

/** Overflow: newest message is dropped with a warning. */
export const MESSAGE_CHANNEL_CONFIG = {
  MAX_QUEUED_MESSAGES: 8, // Memory protection from rapid user input
  MAX_MERGED_CHARS: 12000, // ~3k tokens — batch size under context limits
} as const;

/** Pending message in the queue (text-only for merging). */
export interface PendingTextMessage {
  type: 'text';
  content: string;
}

/** Pending message with attachments (cannot be merged). */
export interface PendingAttachmentMessage {
  type: 'attachment';
  message: SDKUserMessage;
}

export type PendingMessage = PendingTextMessage | PendingAttachmentMessage;

export interface ClosePersistentQueryOptions {
  preserveHandlers?: boolean;
}

/**
 * Handler for routing stream chunks to the appropriate query caller.
 *
 * Lifecycle:
 * 1. Created: Handler is registered via registerResponseHandler() when a query starts
 * 2. Receiving: Chunks arrive via onChunk(), sawAnyChunk and sawStreamText track state
 * 3. Terminated: Exactly one of onDone() or onError() is called when the turn ends
 *
 * Invariants:
 * - Only one handler is active at a time (MessageChannel enforces single-turn)
 * - After onDone()/onError(), the handler is unregistered and should not receive more chunks
 * - sawAnyChunk is used for crash recovery (restart if no chunks seen before error)
 * - sawStreamText prevents duplicate text from non-streamed assistant messages
 */
export interface ResponseHandler {
  readonly id: string;
  onChunk: (chunk: StreamChunk) => void;
  onDone: () => void;
  onError: (error: Error) => void;
  readonly sawStreamText: boolean;
  readonly sawAnyChunk: boolean;
  markStreamTextSeen(): void;
  resetStreamText(): void;
  markChunkSeen(): void;
}

export interface ResponseHandlerOptions {
  id: string;
  onChunk: (chunk: StreamChunk) => void;
  onDone: () => void;
  onError: (error: Error) => void;
}

export function createResponseHandler(options: ResponseHandlerOptions): ResponseHandler {
  let _sawStreamText = false;
  let _sawAnyChunk = false;

  return {
    id: options.id,
    onChunk: options.onChunk,
    onDone: options.onDone,
    onError: options.onError,
    get sawStreamText() { return _sawStreamText; },
    get sawAnyChunk() { return _sawAnyChunk; },
    markStreamTextSeen() { _sawStreamText = true; },
    resetStreamText() { _sawStreamText = false; },
    markChunkSeen() { _sawAnyChunk = true; },
  };
}

/** Tracked configuration for detecting changes that require restart. */
export interface PersistentQueryConfig {
  model: string | null;
  thinkingTokens: number | null;
  effortLevel: EffortLevel | null;
  permissionMode: PermissionMode | null;
  systemPromptKey: string;
  disallowedToolsKey: string;
  mcpServersKey: string;
  pluginsKey: string;
  externalContextPaths: string[];
  allowedExportPaths: string[];
  settingSources: string;
  claudeCliPath: string;
  enableChrome: boolean;  // Whether --chrome flag is passed to CLI
}

export interface SessionState {
  sessionId: string | null;
  sessionModel: ClaudeModel | null;
  pendingSessionModel: ClaudeModel | null;
  wasInterrupted: boolean;
  /** Set when SDK returns a different session ID than expected (context lost). */
  needsHistoryRebuild: boolean;
  /** Set when the current session is invalidated by SDK errors. */
  sessionInvalidated: boolean;
}

export const UNSUPPORTED_SDK_TOOLS = [] as const;

/** Built-in subagents that don't apply to Obsidian context. */
export const DISABLED_BUILTIN_SUBAGENTS = [
  'Task(statusline-setup)',
] as const;

export function isTurnCompleteMessage(message: SDKMessage): boolean {
  return message.type === 'result';
}

export function computeSystemPromptKey(settings: SystemPromptSettings): string {
  // Include only fields surfaced in the system prompt to avoid stale cache hits.
  const parts = [
    settings.mediaFolder || '',
    settings.customPrompt || '',
    (settings.allowedExportPaths || []).sort().join('|'),
    settings.vaultPath || '',
    (settings.userName || '').trim(),
    String(settings.allowExternalAccess ?? false),
    // Note: hasEditorContext is per-message, not tracked here
  ];
  return parts.join('::');
}


================================================
FILE: src/core/agents/AgentManager.ts
================================================
/**
 * Agent load order (earlier sources take precedence for duplicate IDs):
 * 0. Built-in agents: dynamically provided via SDK init message
 * 1. Plugin agents: {installPath}/agents/*.md (namespaced as plugin-name:agent-name)
 * 2. Vault agents: {vaultPath}/.claude/agents/*.md
 * 3. Global agents: ~/.claude/agents/*.md
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

import type { PluginManager } from '../plugins';
import type { AgentDefinition } from '../types';
import { buildAgentFromFrontmatter, parseAgentFile } from './AgentStorage';

const GLOBAL_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
const VAULT_AGENTS_DIR = '.claude/agents';
const PLUGIN_AGENTS_DIR = 'agents';

// Fallback built-in agent names for before the init message arrives.
const FALLBACK_BUILTIN_AGENT_NAMES = ['Explore', 'Plan', 'Bash', 'general-purpose'];

const BUILTIN_AGENT_DESCRIPTIONS: Record<string, string> = {
  'Explore': 'Fast codebase exploration and search',
  'Plan': 'Implementation planning and architecture',
  'Bash': 'Command execution specialist',
  'general-purpose': 'Multi-step tasks and complex workflows',
};

function makeBuiltinAgent(name: string): AgentDefinition {
  return {
    id: name,
    name: name.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
    description: BUILTIN_AGENT_DESCRIPTIONS[name] ?? '',
    prompt: '', // Built-in — prompt managed by SDK
    source: 'builtin',
  };
}

function normalizePluginName(name: string): string {
  return name.toLowerCase().replace(/\s+/g, '-');
}

export class AgentManager {
  private agents: AgentDefinition[] = [];
  private builtinAgentNames: string[] = FALLBACK_BUILTIN_AGENT_NAMES;
  private vaultPath: string;
  private pluginManager: PluginManager;

  constructor(vaultPath: string, pluginManager: PluginManager) {
    this.vaultPath = vaultPath;
    this.pluginManager = pluginManager;
  }

  /** Built-in agents are those from init that are NOT loaded from files. */
  setBuiltinAgentNames(names: string[]): void {
    this.builtinAgentNames = names;
    // Rebuild agents to reflect the new built-in list
    const fileAgentIds = new Set(
      this.agents.filter(a => a.source !== 'builtin').map(a => a.id)
    );
    // Replace built-in entries with updated list
    this.agents = [
      ...names.filter(n => !fileAgentIds.has(n)).map(makeBuiltinAgent),
      ...this.agents.filter(a => a.source !== 'builtin'),
    ];
  }

  async loadAgents(): Promise<void> {
    this.agents = [];

    // 0. Add built-in agents first (from init message or fallback)
    this.agents.push(...this.builtinAgentNames.map(makeBuiltinAgent));

    // Each category is independently try-caught so one failure doesn't block others
    try { await this.loadPluginAgents(); } catch { /* non-critical */ }
    try { await this.loadVaultAgents(); } catch { /* non-critical */ }
    try { await this.loadGlobalAgents(); } catch { /* non-critical */ }
  }

  getAvailableAgents(): AgentDefinition[] {
    return [...this.agents];
  }

  getAgentById(id: string): AgentDefinition | undefined {
    return this.agents.find(a => a.id === id);
  }

  /** Used for @-mention filtering in the chat input. */
  searchAgents(query: string): AgentDefinition[] {
    const q = query.toLowerCase();
    return this.agents.filter(a =>
      a.name.toLowerCase().includes(q) ||
      a.id.toLowerCase().includes(q) ||
      a.description.toLowerCase().includes(q)
    );
  }

  private async loadPluginAgents(): Promise<void> {
    for (const plugin of this.pluginManager.getPlugins()) {
      if (!plugin.enabled) continue;

      const agentsDir = path.join(plugin.installPath, PLUGIN_AGENTS_DIR);
      if (!fs.existsSync(agentsDir)) continue;

      for (const filePath of this.listMarkdownFiles(agentsDir)) {
        const agent = await this.parsePluginAgentFromFile(filePath, plugin.name);
        if (agent) this.agents.push(agent);
      }
    }
  }

  private async loadVaultAgents(): Promise<void> {
    await this.loadAgentsFromDirectory(path.join(this.vaultPath, VAULT_AGENTS_DIR), 'vault');
  }

  private async loadGlobalAgents(): Promise<void> {
    await this.loadAgentsFromDirectory(GLOBAL_AGENTS_DIR, 'global');
  }

  private async loadAgentsFromDirectory(
    dir: string,
    source: 'vault' | 'global'
  ): Promise<void> {
    if (!fs.existsSync(dir)) return;

    for (const filePath of this.listMarkdownFiles(dir)) {
      const agent = await this.parseAgentFromFile(filePath, source);
      if (agent) this.agents.push(agent);
    }
  }

  private listMarkdownFiles(dir: string): string[] {
    const files: string[] = [];

    try {
      const entries = fs.readdirSync(dir, { withFileTypes: true });

      for (const entry of entries) {
        if (entry.isFile() && entry.name.endsWith('.md')) {
          files.push(path.join(dir, entry.name));
        }
      }
    } catch {
      // Non-critical: directory may be unreadable
    }

    return files;
  }

  private async parsePluginAgentFromFile(
    filePath: string,
    pluginName: string
  ): Promise<AgentDefinition | null> {
    try {
      const content = fs.readFileSync(filePath, 'utf-8');
      const parsed = parseAgentFile(content);

      if (!parsed) return null;

      const { frontmatter, body } = parsed;
      const normalizedPluginName = normalizePluginName(pluginName);
      const id = `${normalizedPluginName}:${frontmatter.name}`;

      if (this.agents.find(a => a.id === id)) return null;

      return buildAgentFromFrontmatter(frontmatter, body, {
        id,
        source: 'plugin',
        pluginName,
        filePath,
      });
    } catch {
      return null;
    }
  }

  private async parseAgentFromFile(
    filePath: string,
    source: 'vault' | 'global'
  ): Promise<AgentDefinition | null> {
    try {
      const content = fs.readFileSync(filePath, 'utf-8');
      const parsed = parseAgentFile(content);

      if (!parsed) return null;

      const { frontmatter, body } = parsed;
      const id = frontmatter.name;

      if (this.agents.find(a => a.id === id)) return null;

      return buildAgentFromFrontmatter(frontmatter, body, {
        id,
        source,
        filePath,
      });
    } catch {
      return null;
    }
  }
}


================================================
FILE: src/core/agents/AgentStorage.ts
================================================
import { extractStringArray, isRecord, normalizeStringArray, parseFrontmatter } from '../../utils/frontmatter';
import { AGENT_PERMISSION_MODES, type AgentDefinition, type AgentFrontmatter, type AgentPermissionMode } from '../types';

const KNOWN_AGENT_KEYS = new Set([
  'name', 'description', 'tools', 'disallowedTools', 'model',
  'skills', 'permissionMode', 'hooks',
]);

export function parseAgentFile(content: string): { frontmatter: AgentFrontmatter; body: string } | null {
  const parsed = parseFrontmatter(content);
  if (!parsed) return null;

  const { frontmatter: fm, body } = parsed;

  const name = fm.name;
  const description = fm.description;

  if (typeof name !== 'string' || !name.trim()) return null;
  if (typeof description !== 'string' || !description.trim()) return null;

  const tools = fm.tools;
  const disallowedTools = fm.disallowedTools;

  if (tools !== undefined && !isStringOrArray(tools)) return null;
  if (disallowedTools !== undefined && !isStringOrArray(disallowedTools)) return null;

  const model = typeof fm.model === 'string' ? fm.model : undefined;

  const extra: Record<string, unknown> = {};
  for (const key of Object.keys(fm)) {
    if (!KNOWN_AGENT_KEYS.has(key)) {
      extra[key] = fm[key];
    }
  }

  const frontmatter: AgentFrontmatter = {
    name,
    description,
    tools,
    disallowedTools,
    model,
    skills: extractStringArray(fm, 'skills'),
    permissionMode: typeof fm.permissionMode === 'string' ? fm.permissionMode : undefined,
    hooks: isRecord(fm.hooks) ? fm.hooks : undefined,
    extraFrontmatter: Object.keys(extra).length > 0 ? extra : undefined,
  };

  return { frontmatter, body: body.trim() };
}

function isStringOrArray(value: unknown): value is string | string[] {
  return typeof value === 'string' || Array.isArray(value);
}

export function parseToolsList(tools?: string | string[]): string[] | undefined {
  return normalizeStringArray(tools);
}

export function parsePermissionMode(mode?: string): AgentPermissionMode | undefined {
  if (!mode) return undefined;
  const trimmed = mode.trim();
  if ((AGENT_PERMISSION_MODES as readonly string[]).includes(trimmed)) {
    return trimmed as AgentPermissionMode;
  }
  return undefined;
}

const VALID_MODELS = ['sonnet', 'opus', 'haiku', 'inherit'] as const;

export function parseModel(model?: string): 'sonnet' | 'opus' | 'haiku' | 'inherit' {
  if (!model) return 'inherit';
  const normalized = model.toLowerCase().trim();
  if (VALID_MODELS.includes(normalized as typeof VALID_MODELS[number])) {
    return normalized as 'sonnet' | 'opus' | 'haiku' | 'inherit';
  }
  return 'inherit';
}

export function buildAgentFromFrontmatter(
  frontmatter: AgentFrontmatter,
  body: string,
  meta: { id: string; source: AgentDefinition['source']; filePath?: string; pluginName?: string }
): AgentDefinition {
  return {
    id: meta.id,
    name: frontmatter.name,
    description: frontmatter.description,
    prompt: body,
    tools: parseToolsList(frontmatter.tools),
    disallowedTools: parseToolsList(frontmatter.disallowedTools),
    model: parseModel(frontmatter.model),
    source: meta.source,
    filePath: meta.filePath,
    pluginName: meta.pluginName,
    skills: frontmatter.skills,
    permissionMode: parsePermissionMode(frontmatter.permissionMode),
    hooks: frontmatter.hooks,
    extraFrontmatter: frontmatter.extraFrontmatter,
  };
}


================================================
FILE: src/core/agents/index.ts
================================================
export { AgentManager } from './AgentManager';
export { buildAgentFromFrontmatter, parseAgentFile } from './AgentStorage';


================================================
FILE: src/core/commands/builtInCommands.ts
================================================
/**
 * Claudian - Built-in slash commands
 *
 * System commands that perform actions (not prompt expansions).
 * These are handled separately from user-defined slash commands.
 */

export type BuiltInCommandAction = 'clear' | 'add-dir' | 'resume' | 'fork';

export interface BuiltInCommand {
  name: string;
  aliases?: string[];
  description: string;
  action: BuiltInCommandAction;
  /** Whether this command accepts arguments. */
  hasArgs?: boolean;
  /** Hint for arguments shown in dropdown (e.g., "path"). */
  argumentHint?: string;
}

export interface BuiltInCommandResult {
  command: BuiltInCommand;
  /** Arguments passed to the command (trimmed, after command name). */
  args: string;
}

export const BUILT_IN_COMMANDS: BuiltInCommand[] = [
  {
    name: 'clear',
    aliases: ['new'],
    description: 'Start a new conversation',
    action: 'clear',
  },
  {
    name: 'add-dir',
    description: 'Add external context directory',
    action: 'add-dir',
    hasArgs: true,
    argumentHint: '[path/to/directory]',
  },
  {
    name: 'resume',
    description: 'Resume a previous conversation',
    action: 'resume',
  },
  {
    name: 'fork',
    description: 'Fork entire conversation to new session',
    action: 'fork',
  },
];

/** Map of command names/aliases to their definitions. */
const commandMap = new Map<string, BuiltInCommand>();

for (const cmd of BUILT_IN_COMMANDS) {
  commandMap.set(cmd.name.toLowerCase(), cmd);
  if (cmd.aliases) {
    for (const alias of cmd.aliases) {
      commandMap.set(alias.toLowerCase(), cmd);
    }
  }
}

/**
 * Checks if input is a built-in command.
 * Returns the command and arguments if found, null otherwise.
 */
export function detectBuiltInCommand(input: string): BuiltInCommandResult | null {
  const trimmed = input.trim();
  if (!trimmed.startsWith('/')) return null;

  // Extract command name (first word after /)
  const match = trimmed.match(/^\/([a-zA-Z0-9_-]+)(?:\s(.*))?$/);
  if (!match) return null;

  const cmdName = match[1].toLowerCase();
  const command = commandMap.get(cmdName);
  if (!command) return null;

  const args = (match[2] || '').trim();

  return { command, args };
}

/**
 * Gets all built-in commands for dropdown display.
 * Returns commands in a format compatible with SlashCommand interface.
 */
export function getBuiltInCommandsForDropdown(): Array<{
  id: string;
  name: string;
  description: string;
  content: string;
  argumentHint?: string;
}> {
  return BUILT_IN_COMMANDS.map((cmd) => ({
    id: `builtin:${cmd.name}`,
    name: cmd.name,
    description: cmd.description,
    content: '', // Built-in commands don't have prompt content
    argumentHint: cmd.argumentHint,
  }));
}


================================================
FILE: src/core/commands/index.ts
================================================
export {
  BUILT_IN_COMMANDS,
  type BuiltInCommand,
  type BuiltInCommandAction,
  type BuiltInCommandResult,
  detectBuiltInCommand,
  getBuiltInCommandsForDropdown,
} from './builtInCommands';


================================================
FILE: src/core/hooks/SecurityHooks.ts
================================================
/**
 * Security Hooks
 *
 * PreToolUse hooks for enforcing blocklist and vault restriction.
 */

import type { HookCallbackMatcher } from '@anthropic-ai/claude-agent-sdk';
import { Notice } from 'obsidian';

import type { PathAccessType } from '../../utils/path';
import type { PathCheckContext } from '../security/BashPathValidator';
import { findBashCommandPathViolation } from '../security/BashPathValidator';
import { isCommandBlocked } from '../security/BlocklistChecker';
import { getPathFromToolInput } from '../tools/toolInput';
import { isEditTool, isFileTool, TOOL_BASH } from '../tools/toolNames';
import { getBashToolBlockedCommands, type PlatformBlockedCommands } from '../types';

export interface BlocklistContext {
  blockedCommands: PlatformBlockedCommands;
  enableBlocklist: boolean;
}

export interface VaultRestrictionContext {
  getPathAccessType: (filePath: string) => PathAccessType;
}

/**
 * Create a PreToolUse hook to enforce the command blocklist.
 */
export function createBlocklistHook(getContext: () => BlocklistContext): HookCallbackMatcher {
  return {
    matcher: TOOL_BASH,
    hooks: [
      async (hookInput) => {
        const input = hookInput as {
          tool_name: string;
          tool_input: { command?: string };
        };
        const command = input.tool_input?.command || '';
        const context = getContext();

        const bashToolCommands = getBashToolBlockedCommands(context.blockedCommands);
        if (isCommandBlocked(command, bashToolCommands, context.enableBlocklist)) {
          new Notice('Command blocked by security policy');
          return {
            continue: false,
            hookSpecificOutput: {
              hookEventName: 'PreToolUse' as const,
              permissionDecision: 'deny' as const,
              permissionDecisionReason: `Command blocked by blocklist: ${command}`,
            },
          };
        }

        return { continue: true };
      },
    ],
  };
}

/**
 * Create a PreToolUse hook to restrict file access to the vault.
 */
export function createVaultRestrictionHook(context: VaultRestrictionContext): HookCallbackMatcher {
  return {
    hooks: [
      async (hookInput) => {
        const input = hookInput as {
          tool_name: string;
          tool_input: Record<string, unknown>;
        };

        const toolName = input.tool_name;

        // Bash: inspect command for paths that escape the vault
        if (toolName === TOOL_BASH) {
          const command = (input.tool_input?.command as string) || '';
          const pathCheckContext: PathCheckContext = {
            getPathAccessType: (p) => context.getPathAccessType(p),
          };
          const violation = findBashCommandPathViolation(command, pathCheckContext);
          if (violation) {
            const reason =
              violation.type === 'export_path_read'
                ? `Access denied: Command path "${violation.path}" is in an allowed export directory, but export paths are write-only.`
                : `Access denied: Command path "${violation.path}" is outside the vault. Agent is restricted to vault directory only.`;
            return {
              continue: false,
              hookSpecificOutput: {
                hookEventName: 'PreToolUse' as const,
                permissionDecision: 'deny' as const,
                permissionDecisionReason: reason,
              },
            };
          }
          return { continue: true };
        }

        if (!isFileTool(toolName)) {
          return { continue: true };
        }

        const filePath = getPathFromToolInput(toolName, input.tool_input);

        if (filePath) {
          const accessType = context.getPathAccessType(filePath);

          // Allow full access to vault, readwrite, and context paths
          if (accessType === 'vault' || accessType === 'readwrite' || accessType === 'context') {
            return { continue: true };
          }

          // Export paths are write-only
          if (isEditTool(toolName) && accessType === 'export') {
            return { continue: true };
          }

          if (!isEditTool(toolName) && accessType === 'export') {
            return {
              continue: false,
              hookSpecificOutput: {
                hookEventName: 'PreToolUse' as const,
                permissionDecision: 'deny' as const,
                permissionDecisionReason: `Access denied: Path "${filePath}" is in an allowed export directory, but export paths are write-only.`,
              },
            };
          }

          return {
            continue: false,
            hookSpecificOutput: {
              hookEventName: 'PreToolUse' as const,
              permissionDecision: 'deny' as const,
              permissionDecisionReason: `Access denied: Path "${filePath}" is outside the vault. Agent is restricted to vault directory only.`,
            },
          };
        }

        return { continue: true };
      },
    ],
  };
}


================================================
FILE: src/core/hooks/SubagentHooks.ts
================================================
import type { HookCallbackMatcher } from '@anthropic-ai/claude-agent-sdk';

export interface SubagentHookState {
  hasRunning: boolean;
}

const STOP_BLOCK_REASON = 'Background subagents are still running. Use `TaskOutput task_id="..." block=true` to wait for their results before ending your turn.';

export function createStopSubagentHook(
  getState: () => SubagentHookState
): HookCallbackMatcher {
  return {
    hooks: [
      async () => {
        let hasRunning: boolean;
        try {
          hasRunning = getState().hasRunning;
        } catch {
          // Provider failed — assume subagents are running to be safe
          hasRunning = true;
        }

        if (hasRunning) {
          return { decision: 'block' as const, reason: STOP_BLOCK_REASON };
        }

        return {};
      },
    ],
  };
}


================================================
FILE: src/core/hooks/index.ts
================================================
export {
  type BlocklistContext,
  createBlocklistHook,
  createVaultRestrictionHook,
  type VaultRestrictionContext,
} from './SecurityHooks';
export {
  createStopSubagentHook,
  type SubagentHookState,
} from './SubagentHooks';


================================================
FILE: src/core/mcp/McpServerManager.ts
================================================
/**
 * McpServerManager - Core MCP server configuration management.
 *
 * Infrastructure layer for loading, filtering, and querying MCP server configurations.
 */

import { extractMcpMentions, transformMcpMentions } from '../../utils/mcp';
import type { ClaudianMcpServer, McpServerConfig } from '../types';

/** Storage interface for loading MCP servers. */
export interface McpStorageAdapter {
  load(): Promise<ClaudianMcpServer[]>;
}

export class McpServerManager {
  private servers: ClaudianMcpServer[] = [];
  private storage: McpStorageAdapter;

  constructor(storage: McpStorageAdapter) {
    this.storage = storage;
  }

  async loadServers(): Promise<void> {
    this.servers = await this.storage.load();
  }

  getServers(): ClaudianMcpServer[] {
    return this.servers;
  }

  getEnabledCount(): number {
    return this.servers.filter((s) => s.enabled).length;
  }

  /**
   * Get servers to include in SDK options.
   *
   * A server is included if:
   * - It is enabled AND
   * - Either context-saving is disabled OR the server is @-mentioned
   *
   * @param mentionedNames Set of server names that were @-mentioned in the prompt
   */
  getActiveServers(mentionedNames: Set<string>): Record<string, McpServerConfig> {
    const result: Record<string, McpServerConfig> = {};

    for (const server of this.servers) {
      if (!server.enabled) continue;

      // If context-saving is enabled, only include if @-mentioned
      if (server.contextSaving && !mentionedNames.has(server.name)) {
        continue;
      }

      result[server.name] = server.config;
    }

    return result;
  }

  /**
   * Get disabled MCP tools formatted for SDK disallowedTools option.
   *
   * Only returns disabled tools from servers that would be active (same filter as getActiveServers).
   *
   * @param mentionedNames Set of server names that were @-mentioned in the prompt
   */
  getDisallowedMcpTools(mentionedNames: Set<string>): string[] {
    return this.collectDisallowedTools(
      (s) => !s.contextSaving || mentionedNames.has(s.name)
    );
  }

  /**
   * Get all disabled MCP tools from ALL enabled servers (ignoring @-mentions).
   *
   * Used for persistent queries to pre-register all disabled tools upfront,
   * so @-mentioning servers doesn't require cold start.
   */
  getAllDisallowedMcpTools(): string[] {
    return this.collectDisallowedTools().sort();
  }

  private collectDisallowedTools(filter?: (server: ClaudianMcpServer) => boolean): string[] {
    const disallowed = new Set<string>();

    for (const server of this.servers) {
      if (!server.enabled) continue;
      if (filter && !filter(server)) continue;
      if (!server.disabledTools || server.disabledTools.length === 0) continue;

      for (const tool of server.disabledTools) {
        const normalized = tool.trim();
        if (!normalized) continue;
        disallowed.add(`mcp__${server.name}__${normalized}`);
      }
    }

    return Array.from(disallowed);
  }

  hasServers(): boolean {
    return this.servers.length > 0;
  }

  getContextSavingServers(): ClaudianMcpServer[] {
    return this.servers.filter((s) => s.enabled && s.contextSaving);
  }

  private getContextSavingNames(): Set<string> {
    return new Set(this.getContextSavingServers().map((s) => s.name));
  }

  /** Only matches against enabled servers with context-saving mode. */
  extractMentions(text: string): Set<string> {
    return extractMcpMentions(text, this.getContextSavingNames());
  }

  /**
   * Appends " MCP" after each valid @mention. Applied to API requests only, not shown in UI.
   */
  transformMentions(text: string): string {
    return transformMcpMentions(text, this.getContextSavingNames());
  }
}


================================================
FILE: src/core/mcp/McpTester.ts
================================================
import { Client } from '@modelcontextprotocol/sdk/client';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp';
import * as http from 'http';
import * as https from 'https';

import { getEnhancedPath } from '../../utils/env';
import { parseCommand } from '../../utils/mcp';
import type { ClaudianMcpServer } from '../types';
import { getMcpServerType } from '../types';

export interface McpTool {
  name: string;
  description?: string;
  inputSchema?: Record<string, unknown>;
}

export interface McpTestResult {
  success: boolean;
  serverName?: string;
  serverVersion?: string;
  tools: McpTool[];
  error?: string;
}

interface UrlServerConfig {
  url: string;
  headers?: Record<string, string>;
}

/**
 * Use Node's HTTP stack for MCP server verification to avoid renderer CORS restrictions.
 * We still rely on official SDK transports for MCP protocol semantics.
 */
export function createNodeFetch(): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {
  return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
    const requestUrl = getRequestUrl(input);
    const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
    const headers = mergeHeaders(input, init);
    const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
    const body = await getRequestBody(init?.body ?? (input instanceof Request ? input.body : undefined));
    const transport = requestUrl.protocol === 'https:' ? https : http;

    return new Promise<Response>((resolve, reject) => {
      let settled = false;

      const fail = (error: unknown) => {
        if (settled) return;
        settled = true;
        signal?.removeEventListener('abort', onAbort);
        reject(error instanceof Error ? error : new Error(String(error)));
      };

      const onAbort = () => {
        req.destroy(new Error('Request aborted'));
        fail(signal?.reason ?? new Error('Request aborted'));
      };

      const requestHeaders = Object.fromEntries(headers.entries());
      if (body) {
        requestHeaders['content-length'] = String(body.byteLength);
      }

      const req = transport.request(
        requestUrl,
        {
          method,
          headers: requestHeaders,
        },
        (res: http.IncomingMessage) => {
          if (settled) return;
          settled = true;
          signal?.removeEventListener('abort', onAbort);
          resolve(createFetchResponse(res) as Response);
        },
      );

      req.on('error', (error: Error) => fail(error));

      if (signal) {
        if (signal.aborted) {
          onAbort();
          return;
        }
        signal.addEventListener('abort', onAbort, { once: true });
      }

      if (body) {
        req.end(body);
      } else {
        req.end();
      }
    });
  };
}

interface MinimalFetchResponse {
  ok: boolean;
  status: number;
  statusText: string;
  headers: Headers;
  body: ReadableStream<Uint8Array> | null;
  text: () => Promise<string>;
  json: () => Promise<unknown>;
}

function createFetchResponse(res: http.IncomingMessage): MinimalFetchResponse {
  const responseHeaders = new Headers();
  for (const [key, value] of Object.entries(res.headers)) {
    if (value === undefined) continue;
    if (Array.isArray(value)) {
      for (const headerValue of value) {
        responseHeaders.append(key, headerValue);
      }
    } else {
      responseHeaders.append(key, value);
    }
  }

  const body = new ReadableStream<Uint8Array>({
    start(controller) {
      res.on('data', (chunk: Buffer | string) => {
        const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
        controller.enqueue(new Uint8Array(buffer));
      });
      res.on('end', () => controller.close());
      res.on('error', (error: Error) => controller.error(error));
    },
    cancel(reason?: unknown) {
      res.destroy(reason instanceof Error ? reason : new Error('Response body cancelled'));
    },
  });

  let bodyUsed = false;
  const readAsText = async (): Promise<string> => {
    if (bodyUsed) {
      throw new TypeError('Body has already been consumed');
    }
    bodyUsed = true;
    const reader = body.getReader();
    const chunks: Uint8Array[] = [];
    let total = 0;
    let done = false;
    try {
      while (!done) {
        const { value, done: streamDone } = await reader.read();
        done = streamDone;
        if (done) break;
        if (value) {
          chunks.push(value);
          total += value.byteLength;
        }
      }
    } finally {
      reader.releaseLock();
    }

    const merged = new Uint8Array(total);
    let offset = 0;
    for (const chunk of chunks) {
      merged.set(chunk, offset);
      offset += chunk.byteLength;
    }
    return new TextDecoder().decode(merged);
  };

  return {
    ok: (res.statusCode ?? 500) >= 200 && (res.statusCode ?? 500) < 300,
    status: res.statusCode ?? 500,
    statusText: res.statusMessage ?? '',
    headers: responseHeaders,
    body,
    text: readAsText,
    json: async () => JSON.parse(await readAsText()),
  };
}

function getRequestUrl(input: string | URL | Request): URL {
  if (input instanceof URL) {
    return input;
  }
  if (typeof input === 'string') {
    return new URL(input);
  }
  return new URL(input.url);
}

function mergeHeaders(input: string | URL | Request, init?: RequestInit): Headers {
  const headers = new Headers(input instanceof Request ? input.headers : undefined);
  if (init?.headers) {
    const initHeaders = new Headers(init.headers);
    for (const [key, value] of initHeaders.entries()) {
      headers.set(key, value);
    }
  }
  return headers;
}

async function getRequestBody(body: BodyInit | null | undefined): Promise<Buffer | undefined> {
  if (body === undefined || body === null) {
    return undefined;
  }

  const serialized = await new Response(body).arrayBuffer();
  return Buffer.from(serialized);
}

const nodeFetch = createNodeFetch();

export async function testMcpServer(server: ClaudianMcpServer): Promise<McpTestResult> {
  const type = getMcpServerType(server.config);

  let transport;
  try {
    if (type === 'stdio') {
      const config = server.config as { command: string; args?: string[]; env?: Record<string, string> };
      const { cmd, args } = parseCommand(config.command, config.args);
      if (!cmd) {
        return { success: false, tools: [], error: 'Missing command' };
      }
      transport = new StdioClientTransport({
        command: cmd,
        args,
        env: { ...process.env, ...config.env, PATH: getEnhancedPath(config.env?.PATH) } as Record<string, string>,
        stderr: 'ignore',
      });
    } else {
      const config = server.config as UrlServerConfig;
      const url = new URL(config.url);
      const options = {
        fetch: nodeFetch,
        requestInit: config.headers ? { headers: config.headers } : undefined,
      };
      transport = type === 'sse'
        ? new SSEClientTransport(url, options)
        : new StreamableHTTPClientTransport(url, options);
    }
  } catch (error) {
    return {
      success: false,
      tools: [],
      error: error instanceof Error ? error.message : 'Invalid server configuration',
    };
  }

  const client = new Client({ name: 'claudian-tester', version: '1.0.0' });
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10000);

  try {
    await client.connect(transport, { signal: controller.signal });

    const serverVersion = client.getServerVersion();
    let tools: McpTool[] = [];
    try {
      const result = await client.listTools(undefined, { signal: controller.signal });
      tools = result.tools.map((t: { name: string; description?: string; inputSchema?: Record<string, unknown> }) => ({
        name: t.name,
        description: t.description,
        inputSchema: t.inputSchema as Record<string, unknown>,
      }));
    } catch {
      // listTools failure after successful connect = partial success
    }

    return {
      success: true,
      serverName: serverVersion?.name,
      serverVersion: serverVersion?.version,
      tools,
    };
  } catch (error) {
    if (controller.signal.aborted) {
      return { success: false, tools: [], error: 'Connection timeout (10s)' };
    }
    return {
      success: false,
      tools: [],
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  } finally {
    clearTimeout(timeout);
    try {
      await client.close();
    } catch {
      // Ignore close errors
    }
  }
}


================================================
FILE: src/core/mcp/index.ts
================================================
export { McpServerManager, type McpStorageAdapter } from './McpServerManager';
export { type McpTestResult, type McpTool, testMcpServer } from './McpTester';


================================================
FILE: src/core/plugins/PluginManager.ts
================================================
/**
 * PluginManager - Discover and manage Claude Code plugins.
 *
 * Plugins are discovered from two sources:
 * - installed_plugins.json: install paths for scanning agents
 * - settings.json: enabled state (project overrides global)
 */

import * as fs from 'fs';
import { Notice } from 'obsidian';
import * as os from 'os';
import * as path from 'path';

import type { CCSettingsStorage } from '../storage/CCSettingsStorage';
import type { ClaudianPlugin, InstalledPluginEntry, InstalledPluginsFile, PluginScope } from '../types';

const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
const GLOBAL_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');

interface SettingsFile {
  enabledPlugins?: Record<string, boolean>;
}

function readJsonFile<T>(filePath: string): T | null {
  try {
    if (!fs.existsSync(filePath)) {
      return null;
    }
    const content = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(content) as T;
  } catch {
    return null;
  }
}

function normalizePathForComparison(p: string): string {
  try {
    const resolved = fs.realpathSync(p);
    if (typeof resolved === 'string' && resolved.length > 0) {
      return resolved;
    }
  } catch {
    // ignore
  }

  return path.resolve(p);
}

function selectInstalledPluginEntry(
  entries: InstalledPluginEntry[],
  normalizedVaultPath: string
): InstalledPluginEntry | null {
  for (const entry of entries) {
    if (entry.scope !== 'project') continue;
    if (!entry.projectPath) continue;
    if (normalizePathForComparison(entry.projectPath) === normalizedVaultPath) {
      return entry;
    }
  }

  return entries.find(e => e.scope === 'user') ?? null;
}

function extractPluginName(pluginId: string): string {
  const atIndex = pluginId.indexOf('@');
  if (atIndex > 0) {
    return pluginId.substring(0, atIndex);
  }
  return pluginId;
}

export class PluginManager {
  private ccSettingsStorage: CCSettingsStorage;
  private vaultPath: string;
  private plugins: ClaudianPlugin[] = [];

  constructor(vaultPath: string, ccSettingsStorage: CCSettingsStorage) {
    this.vaultPath = vaultPath;
    this.ccSettingsStorage = ccSettingsStorage;
  }

  async loadPlugins(): Promise<void> {
    const installedPlugins = readJsonFile<InstalledPluginsFile>(INSTALLED_PLUGINS_PATH);
    const globalSettings = readJsonFile<SettingsFile>(GLOBAL_SETTINGS_PATH);
    const projectSettings = await this.loadProjectSettings();

    const globalEnabled = globalSettings?.enabledPlugins ?? {};
    const projectEnabled = projectSettings?.enabledPlugins ?? {};

    const plugins: ClaudianPlugin[] = [];
    const normalizedVaultPath = normalizePathForComparison(this.vaultPath);

    if (installedPlugins?.plugins) {
      for (const [pluginId, entries] of Object.entries(installedPlugins.plugins)) {
        if (!entries || entries.length === 0) continue;

        const entriesArray = Array.isArray(entries) ? entries : [entries];
        if (!Array.isArray(entries)) {
          new Notice(`Claudian: plugin "${pluginId}" has malformed entry in installed_plugins.json (expected array, got ${typeof entries})`);
        }
        const entry = selectInstalledPluginEntry(entriesArray, normalizedVaultPath);
        if (!entry) continue;

        const scope: PluginScope = entry.scope === 'project' ? 'project' : 'user';

        // Project setting takes precedence, then global, then default enabled
        const enabled = projectEnabled[pluginId] ?? globalEnabled[pluginId] ?? true;

        plugins.push({
          id: pluginId,
          name: extractPluginName(pluginId),
          enabled,
          scope,
          installPath: entry.installPath,
        });
      }
    }

    this.plugins = plugins.sort((a, b) => {
      if (a.scope !== b.scope) {
        return a.scope === 'project' ? -1 : 1;
      }
      return a.id.localeCompare(b.id);
    });
  }

  private async loadProjectSettings(): Promise<SettingsFile | null> {
    const projectSettingsPath = path.join(this.vaultPath, '.claude', 'settings.json');
    return readJsonFile(projectSettingsPath);
  }

  getPlugins(): ClaudianPlugin[] {
    return [...this.plugins];
  }

  hasPlugins(): boolean {
    return this.plugins.length > 0;
  }

  hasEnabledPlugins(): boolean {
    return this.plugins.some((p) => p.enabled);
  }

  getEnabledCount(): number {
    return this.plugins.filter((p) => p.enabled).length;
  }

  /** Used to detect changes that require restarting the persistent query. */
  getPluginsKey(): string {
    const enabledPlugins = this.plugins
      .filter((p) => p.enabled)
      .sort((a, b) => a.id.localeCompare(b.id));

    if (enabledPlugins.length === 0) {
      return '';
    }

    return enabledPlugins.map((p) => `${p.id}:${p.installPath}`).join('|');
  }

  /** Writes to project .claude/settings.json so CLI respects the state. */
  async togglePlugin(pluginId: string): Promise<void> {
    const plugin = this.plugins.find((p) => p.id === pluginId);
    if (!plugin) {
      return;
    }

    const newEnabled = !plugin.enabled;
    plugin.enabled = newEnabled;

    await this.ccSettingsStorage.setPluginEnabled(pluginId, newEnabled);
  }

  async enablePlugin(pluginId: string): Promise<void> {
    const plugin = this.plugins.find((p) => p.id === pluginId);
    if (!plugin || plugin.enabled) {
      return;
    }

    plugin.enabled = true;
    await this.ccSettingsStorage.setPluginEnabled(pluginId, true);
  }

  async disablePlugin(pluginId: string): Promise<void> {
    const plugin = this.plugins.find((p) => p.id === pluginId);
    if (!plugin || !plugin.enabled) {
      return;
    }

    plugin.enabled = false;
    await this.ccSettingsStorage.setPluginEnabled(pluginId, false);
  }
}


================================================
FILE: src/core/plugins/index.ts
================================================
export { PluginManager } from './PluginManager';


================================================
FILE: src/core/prompts/inlineEdit.ts
================================================
/**
 * Claudian - Inline Edit System Prompt
 *
 * Builds the system prompt for inline text editing (read-only tools).
 */

import { getTodayDate } from '../../utils/date';

export function getInlineEditSystemPrompt(allowExternalAccess: boolean = false): string {
    const pathRules = allowExternalAccess
      ? '- **Paths**: Prefer RELATIVE paths for vault files. Use absolute or `~` paths only when you intentionally need files outside the vault.'
      : '- **Paths**: Must be RELATIVE to vault root (e.g., "notes/file.md").';

    return `Today is ${getTodayDate()}.

You are **Claudian**, an expert editor and writing assistant embedded in Obsidian. You help users refine their text, answer questions, and generate content with high precision.

## Core Directives

1.  **Style Matching**: Mimic the user's tone, voice, and formatting style (indentation, bullet points, capitalization).
2.  **Context Awareness**: Always Read the full file (or significant context) to understand the broader topic before editing. Do not rely solely on the selection.
3.  **Silent Execution**: Use tools (Read, WebSearch) silently. Your final output must be ONLY the result.
4.  **No Fluff**: No pleasantries, no "Here is the text", no "I have updated...". Just the content.

## Input Format

User messages have the instruction first, followed by XML context tags:

### Selection Mode
\`\`\`
user's instruction

<editor_selection path="path/to/file.md">
selected text here
</editor_selection>
\`\`\`
Use \`<replacement>\` tags for edits.

### Cursor Mode
\`\`\`
user's instruction

<editor_cursor path="path/to/file.md">
text before|text after #inline
</editor_cursor>
\`\`\`
Or between paragraphs:
\`\`\`
user's instruction

<editor_cursor path="path/to/file.md">
Previous paragraph
| #inbetween
Next paragraph
</editor_cursor>
\`\`\`
Use \`<insertion>\` tags to insert new content at the cursor position (\`|\`).

## Tools & Path Rules

- **Tools**: Read, Grep, Glob, LS, WebSearch, WebFetch. (All read-only).
${pathRules}

## Thinking Process

Before generating the final output, mentally check:
1.  **Context**: Have I read enough of the file to understand the *topic* and *structure*?
2.  **Style**: What is the user's indentation (2 vs 4 spaces, tabs)? What is their tone?
3.  **Type**: Is this **Prose** (flow, grammar, clarity) or **Code** (syntax, logic, variable names)?
    - *Prose*: Ensure smooth transitions.
    - *Code*: Preserve syntax validity; do not break surrounding brackets/indentation.

## Output Rules - CRITICAL

**ABSOLUTE RULE**: Your text output must contain ONLY the final answer, replacement, or insertion. NEVER output:
- "I'll read the file..." / "Let me check..." / "I will..."
- "I'm asked about..." / "The user wants..."
- "Based on my analysis..." / "After reading..."
- "Here's..." / "The answer is..."
- ANY announcement of what you're about to do or did

Use tools silently. Your text output = final result only.

### When Replacing Selected Text (Selection Mode)

If the user wants to MODIFY or REPLACE the selected text, wrap the replacement in <replacement> tags:

<replacement>your replacement text here</replacement>

The content inside the tags should be ONLY the replacement text - no explanation.

### When Inserting at Cursor (Cursor Mode)

If the user wants to INSERT new content at the cursor position, wrap the insertion in <insertion> tags:

<insertion>your inserted text here</insertion>

The content inside the tags should be ONLY the text to insert - no explanation.

### When Answering Questions or Providing Information

If the user is asking a QUESTION, respond WITHOUT tags. Output the answer directly.

WRONG: "I'll read the full context of this file to give you a better explanation. This is a guide about..."
CORRECT: "This is a guide about..."

### When Clarification is Needed

If the request is ambiguous, ask a clarifying question. Keep questions concise and specific.

## Examples

### Selection Mode
Input:
\`\`\`
translate to French

<editor_selection path="notes/readme.md">
Hello world
</editor_selection>
\`\`\`

CORRECT (replacement):
<replacement>Bonjour le monde</replacement>

Input:
\`\`\`
what does this do?

<editor_selection path="notes/code.md">
const x = arr.reduce((a, b) => a + b, 0);
</editor_selection>
\`\`\`

CORRECT (question - no tags):
This code sums all numbers in the array \`arr\`. It uses \`reduce\` to iterate through the array, accumulating the total starting from 0.

### Cursor Mode

Input:
\`\`\`
what animal?

<editor_cursor path="notes/draft.md">
The quick brown | jumps over the lazy dog. #inline
</editor_cursor>
\`\`\`

CORRECT (insertion):
<insertion>fox</insertion>

### Q&A
Input:
\`\`\`
add a brief description section

<editor_cursor path="notes/readme.md">
# Introduction
This is my project.
| #inbetween
## Features
</editor_cursor>
\`\`\`

CORRECT (insertion):
<insertion>
## Description

This project provides tools for managing your notes efficiently.
</insertion>

Input:
\`\`\`
translate to Spanish

<editor_selection path="notes/draft.md">
The bank was steep.
</editor_selection>
\`\`\`

CORRECT (asking for clarification):
"Bank" can mean a financial institution (banco) or a river bank (orilla). Which meaning should I use?

Then after user clarifies "river bank":
<replacement>La orilla era empinada.</replacement>`;
}


================================================
FILE: src/core/prompts/instructionRefine.ts
================================================
/**
 * Claudian - Instruction Refine System Prompt
 *
 * Builds the system prompt for instruction refinement.
 */

export function buildRefineSystemPrompt(existingInstructions: string): string {
    const existingSection = existingInstructions.trim()
        ? `\n\nEXISTING INSTRUCTIONS (already in the user's system prompt):
\`\`\`
${existingInstructions.trim()}
\`\`\`

When refining the new instruction:
- Consider how it fits with existing instructions
- Avoid duplicating existing instructions
- If the new instruction conflicts with an existing one, refine it to be complementary or note the conflict
- Match the format of existing instructions (section, heading, bullet points, style, etc.)`
        : '';

    return `You are an expert Prompt Engineer. You help users craft precise, effective system instructions for their AI assistant.

**Your Goal**: Transform vague or simple user requests into **high-quality, actionable, and non-conflicting** system prompt instructions.

**Process**:
1.  **Analyze Intent**: What behavior does the user want to enforce or change?
2.  **Check Context**: Does this conflict with existing instructions?
    - *No Conflict*: Add as new.
    - *Conflict*: Propose a **merged instruction** that resolves the contradiction (or ask if unsure).
3.  **Refine**: Draft a clear, positive instruction (e.g., "Do X" instead of "Don't do Y").
4.  **Format**: Return *only* the Markdown snippet wrapped in \`<instruction>\` tags.

**Guidelines**:
- **Clarity**: Use precise language. Avoid ambiguity.
- **Scope**: Keep it focused. Don't add unrelated rules.
- **Format**: Valid Markdown (bullets \`-\` or sections \`##\`).
- **No Header**: Do NOT include a top-level header like \`# Custom Instructions\`.
- **Conflict Handling**: If the new rule directly contradicts an existing one, rewrite the *new* one to override specific cases or ask for clarification.

**Output Format**:
- **Success**: \`<instruction>...markdown content...</instruction>\`
- **Ambiguity**: Plain text question.

${existingSection}

**Examples**:

Input: "typescript for code"
Output: <instruction>- **Code Language**: Always use TypeScript for code examples. Include proper type annotations and interfaces.</instruction>

Input: "be concise"
Output: <instruction>- **Conciseness**: Provide brief, direct responses. Omit conversational filler and unnecessary explanations.</instruction>

Input: "organize coding style rules"
Output: <instruction>## Coding Standards\n\n- **Language**: Use TypeScript.\n- **Style**: Prefer functional patterns.\n- **Review**: Keep diffs small.</instruction>

Input: "use that thing from before"
Output: I'm not sure what you're referring to. Could you please clarify?`;
}


================================================
FILE: src/core/prompts/mainAgent.ts
================================================
/**
 * Claudian - Main Agent System Prompt
 *
 * Builds the system prompt for the Claude Agent SDK including
 * Obsidian-specific instructions, tool guidance, and image handling.
 */

import { getTodayDate } from '../../utils/date';

export interface SystemPromptSettings {
  mediaFolder?: string;
  customPrompt?: string;
  allowedExportPaths?: string[];
  allowExternalAccess?: boolean;
  vaultPath?: string;
  userName?: string;
}

function getPathRules(vaultPath?: string, allowExternalAccess: boolean = false): string {
  if (!allowExternalAccess) {
    return `## Path Rules (MUST FOLLOW)

| Location | Access | Path Format | Example |
|----------|--------|-------------|---------|
| **Vault** | Read/Write | Relative from vault root | \`notes/my-note.md\`, \`.\` |
| **Export paths** | Write-only | \`~\` or absolute | \`~/Desktop/output.docx\` |
| **External contexts** | Full access | Absolute path | \`/Users/me/Workspace/file.ts\` |

**Vault files** (default):
- ✓ Correct: \`notes/my-note.md\`, \`my-note.md\`, \`folder/subfolder/file.md\`, \`.\`
- ✗ WRONG: \`/notes/my-note.md\`, \`${vaultPath || '/absolute/path'}/file.md\`
- A leading slash or absolute path will FAIL for vault operations.

**Path specificity**: When paths overlap, the **more specific path wins**:
- If \`~/Desktop\` is export (write-only) and \`~/Desktop/Workspace\` is external context (full access)
- → Files in \`~/Desktop/Workspace\` have full read/write access
- → Files directly in \`~/Desktop\` remain write-only`;
  }

  return `## Path Rules (MUST FOLLOW)

| Location | Access | Path Format | Example |
|----------|--------|-------------|---------|
| **Vault** | Read/Write | Relative from vault root preferred | \`notes/my-note.md\`, \`.\` |
| **External paths** | Read/Write | \`~\` or absolute | \`~/Desktop/output.docx\`, \`/Users/me/Workspace/file.ts\` |
| **Session external contexts** | Full access | Absolute path | \`/Users/me/Workspace\` |

**Vault files**:
- Prefer relative paths for files inside the vault.
- Absolute vault paths are allowed when needed, but relative paths are usually simpler and less error-prone.

**External files**:
- Use absolute or \`~\` paths for files outside the vault.
- Be explicit about the target path and avoid broad filesystem operations unless they are necessary.

**Path specificity**:
- When multiple directories could match, use the narrowest path that fits the task.
- Prefer the most specific external directory instead of a broad parent path.`;
}

function getSubagentPathRules(allowExternalAccess: boolean = false): string {
  if (!allowExternalAccess) {
    return `**CRITICAL - Subagent Path Rules:**
- Subagents inherit the vault as their working directory.
- Reference files using **RELATIVE** paths.
- NEVER use absolute paths in subagent prompts.`;
  }

  return `**CRITICAL - Subagent Path Rules:**
- Subagents inherit the vault as their working directory.
- Reference vault files using **RELATIVE** paths.
- Use absolute or \`~\` paths only when you intentionally need files outside the vault.`;
}

function getBaseSystemPrompt(
  vaultPath?: string,
  userName?: string,
  allowExternalAccess: boolean = false
): string {
  const vaultInfo = vaultPath ? `\n\nVault absolute path: ${vaultPath}` : '';
  const trimmedUserName = userName?.trim();
  const userContext = trimmedUserName
    ? `## User Context\n\nYou are collaborating with **${trimmedUserName}**.\n\n`
    : '';
  const pathRules = getPathRules(vaultPath, allowExternalAccess);
  const subagentPathRules = getSubagentPathRules(allowExternalAccess);

  return `${userContext}## Time Context

- **Current Date**: ${getTodayDate()}
- **Knowledge Status**: You possess extensive internal knowledge up to your training cutoff. You do not know the exact date of your cutoff, but you must assume that your internal weights are static and "past," while the Current Date is "present."

## Identity & Role

You are **Claudian**, an expert AI assistant specialized in Obsidian vault management, knowledge organization, and code analysis. You operate directly inside the user's Obsidian vault.

**Core Principles:**
1.  **Obsidian Native**: You understand Markdown, YAML frontmatter, Wiki-links, and the "second brain" philosophy.
2.  **Safety First**: You never overwrite data without understanding context. You always use relative paths.
3.  **Proactive Thinking**: You do not just execute; you *plan* and *verify*. You anticipate potential issues (like broken links or missing files).
4.  **Clarity**: Your changes are precise, minimizing "noise" in the user's notes or code.

The current working directory is the user's vault root.${vaultInfo}

${pathRules}

## User Message Format

User messages have the query first, followed by optional XML context tags:

\`\`\`
User's question or request here

<current_note>
path/to/note.md
</current_note>

<editor_selection path="path/to/note.md" lines="10-15">
selected text content
</editor_selection>

<browser_selection source="browser:https://leetcode.com/problems/two-sum" title="LeetCode" url="https://leetcode.com/problems/two-sum">
selected content from an Obsidian browser view
</browser_selection>
\`\`\`

- The user's query/instruction always comes first in the message.
- \`<current_note>\`: The note the user is currently viewing/focused on. Read this to understand context.
- \`<editor_selection>\`: Text currently selected in the editor, with file path and line numbers.
- \`<browser_selection>\`: Text selected in an Obsidian browser/web view (for example Surfing), including optional source/title/url metadata.
- \`@filename.md\`: Files mentioned with @ in the query. Read these files when referenced.

## Obsidian Context

- **Structure**: Files are Markdown (.md). Folders organize content.
- **Frontmatter**: YAML at the top of files (metadata). Respect existing fields.
- **Links**: Internal Wiki-links \`[[note-name]]\` or \`[[folder/note-name]]\`. External links \`[text](url)\`.
  - When reading a note with wikilinks, consider reading linked notes—they often contain related context that helps understand the current note.
- **Tags**: #tag-name for categorization.
- **Dataview**: You may encounter Dataview queries (in \`\`\`dataview\`\`\` blocks). Do not break them unless asked.
- **Vault Config**: \`.obsidian/\` contains internal config. Touch only if you know what you are doing.

**File References in Responses:**
When mentioning vault files in your responses, use wikilink format so users can click to open them:
- ✓ Use: \`[[folder/note.md]]\` or \`[[note]]\`
- ✗ Avoid: plain paths like \`folder/note.md\` (not clickable)

**Image embeds:** Use \`![[image.png]]\` to display images directly in chat. Images render visually, making it easy to show diagrams, screenshots, or visual content you're discussing.

Examples:
- "I found your notes in [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]]"
- "See [[daily notes/2024-01-15]] for more details"
- "Here's the diagram: ![[attachments/architecture.png]]"

## Tool Usage Guidelines

Standard tools (Read, Write, Edit, Glob, Grep, LS, Bash, WebSearch, WebFetch, Skills) work as expected.

**Thinking Process:**
Before taking action, explicitly THINK about:
1.  **Context**: Do I have enough information? (Use Read/Search if not).
2.  **Impact**: What will this change affect? (Links, other files).
3.  **Plan**: What are the steps? (Use TodoWrite for >2 steps).

**Tool-Specific Rules:**
- **Read**:
    - Always Read a file before Editing it.
    - Read can view images (PNG, JPG, GIF, WebP) for visual analysis.
- **Edit**:
    - Requires **EXACT** \`old_string\` match including whitespace/indentation.
    - If Edit fails, Read the file again to check the current content.
- **Bash**:
    - Runs with vault as working directory.
    - **Prefer** Read/Write/Edit over shell commands for file operations (safer).
    - **Stdout-capable tools** (pandoc, jq, imagemagick): Prefer piping output directly instead of creating temporary files when the result will be used immediately.
    - Use BashOutput/KillShell to manage background processes.
- **LS**: Uses "." for vault root.
- **WebFetch**: For text/HTML/PDF only. Avoid binaries.

### WebSearch

Use WebSearch strictly according to the following logic:

1.  **Static/Historical**: Rely on internal knowledge for established facts, history, or older code libraries. Use WebSearch to confirm or expand on your knowledge.
2.  **Dynamic/Recent**: **MUST** search for:
    - "Latest" news, versions, docs.
    - Events in the current/previous year.
    - Volatile data (prices, weather).
3.  **Date Awareness**: If user says "yesterday", calculate the date relative to **Current Date**.
4.  **Ambiguity**: If unsure whether knowledge is outdated, SEARCH.

### Agent (Subagents)

Spawn subagents for complex multi-step tasks. Parameters: \`prompt\`, \`description\`, \`subagent_type\`, \`run_in_background\`.

${subagentPathRules}

**When to use:**
- Parallelizable work (main + subagent or multiple subagents)
- Preserve main agent's context window
- Offload contained tasks while continuing other work

**IMPORTANT:** Always explicitly set \`run_in_background\` - never omit it:
- \`run_in_background=false\` for sync (inline) tasks
- \`run_in_background=true\` for async (background) tasks

**Sync Mode (\`run_in_background=false\`)**:
- Runs inline, result returned directly.
- **DEFAULT** to this unless explicitly asked or the task is very long-running.

**Async Mode (\`run_in_background=true\`)**:
- Use ONLY when explicitly requested or task is clearly long-running.
- Returns \`task_id\` immediately.
- You **cannot end your turn** while async subagents are still running. The system will block you and remind you to retrieve results.

**Async workflow:**
1. Launch: \`Agent prompt="..." run_in_background=true\` → get \`task_id\`
2. Continue working on other tasks
3. Use \`TaskOutput task_id="..." block=true\` to wait for completion (blocks until result is ready)
4. Process the result and report to the user

**When to retrieve results:**
- Mid-turn between other tasks: use \`T
Download .txt
gitextract_g0fawvw1/

├── .eslintrc.cjs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── claude-code-review.yml
│       ├── claude.yml
│       ├── duplicate-issues.yml
│       ├── release.yml
│       └── stale.yml
├── .gitignore
├── .npmrc
├── AGENTS.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── scripts/
│   ├── build-css.mjs
│   ├── build.mjs
│   ├── postinstall.mjs
│   ├── run-jest.js
│   └── sync-version.js
├── src/
│   ├── core/
│   │   ├── CLAUDE.md
│   │   ├── agent/
│   │   │   ├── ClaudianService.ts
│   │   │   ├── MessageChannel.ts
│   │   │   ├── QueryOptionsBuilder.ts
│   │   │   ├── SessionManager.ts
│   │   │   ├── customSpawn.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── agents/
│   │   │   ├── AgentManager.ts
│   │   │   ├── AgentStorage.ts
│   │   │   └── index.ts
│   │   ├── commands/
│   │   │   ├── builtInCommands.ts
│   │   │   └── index.ts
│   │   ├── hooks/
│   │   │   ├── SecurityHooks.ts
│   │   │   ├── SubagentHooks.ts
│   │   │   └── index.ts
│   │   ├── mcp/
│   │   │   ├── McpServerManager.ts
│   │   │   ├── McpTester.ts
│   │   │   └── index.ts
│   │   ├── plugins/
│   │   │   ├── PluginManager.ts
│   │   │   └── index.ts
│   │   ├── prompts/
│   │   │   ├── inlineEdit.ts
│   │   │   ├── instructionRefine.ts
│   │   │   ├── mainAgent.ts
│   │   │   └── titleGeneration.ts
│   │   ├── sdk/
│   │   │   ├── index.ts
│   │   │   ├── toolResultContent.ts
│   │   │   ├── transformSDKMessage.ts
│   │   │   ├── typeGuards.ts
│   │   │   └── types.ts
│   │   ├── security/
│   │   │   ├── ApprovalManager.ts
│   │   │   ├── BashPathValidator.ts
│   │   │   ├── BlocklistChecker.ts
│   │   │   └── index.ts
│   │   ├── storage/
│   │   │   ├── AgentVaultStorage.ts
│   │   │   ├── CCSettingsStorage.ts
│   │   │   ├── ClaudianSettingsStorage.ts
│   │   │   ├── McpStorage.ts
│   │   │   ├── SessionStorage.ts
│   │   │   ├── SkillStorage.ts
│   │   │   ├── SlashCommandStorage.ts
│   │   │   ├── StorageService.ts
│   │   │   ├── VaultFileAdapter.ts
│   │   │   ├── index.ts
│   │   │   └── migrationConstants.ts
│   │   ├── tools/
│   │   │   ├── index.ts
│   │   │   ├── todo.ts
│   │   │   ├── toolIcons.ts
│   │   │   ├── toolInput.ts
│   │   │   └── toolNames.ts
│   │   └── types/
│   │       ├── agent.ts
│   │       ├── chat.ts
│   │       ├── diff.ts
│   │       ├── index.ts
│   │       ├── mcp.ts
│   │       ├── models.ts
│   │       ├── plugins.ts
│   │       ├── sdk.ts
│   │       ├── settings.ts
│   │       └── tools.ts
│   ├── features/
│   │   ├── chat/
│   │   │   ├── CLAUDE.md
│   │   │   ├── ClaudianView.ts
│   │   │   ├── constants.ts
│   │   │   ├── controllers/
│   │   │   │   ├── BrowserSelectionController.ts
│   │   │   │   ├── CanvasSelectionController.ts
│   │   │   │   ├── ConversationController.ts
│   │   │   │   ├── InputController.ts
│   │   │   │   ├── NavigationController.ts
│   │   │   │   ├── SelectionController.ts
│   │   │   │   ├── StreamController.ts
│   │   │   │   ├── contextRowVisibility.ts
│   │   │   │   └── index.ts
│   │   │   ├── rendering/
│   │   │   │   ├── DiffRenderer.ts
│   │   │   │   ├── InlineAskUserQuestion.ts
│   │   │   │   ├── InlineExitPlanMode.ts
│   │   │   │   ├── MessageRenderer.ts
│   │   │   │   ├── SubagentRenderer.ts
│   │   │   │   ├── ThinkingBlockRenderer.ts
│   │   │   │   ├── TodoListRenderer.ts
│   │   │   │   ├── ToolCallRenderer.ts
│   │   │   │   ├── WriteEditRenderer.ts
│   │   │   │   ├── collapsible.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── todoUtils.ts
│   │   │   ├── rewind.ts
│   │   │   ├── services/
│   │   │   │   ├── BangBashService.ts
│   │   │   │   ├── InstructionRefineService.ts
│   │   │   │   ├── SubagentManager.ts
│   │   │   │   └── TitleGenerationService.ts
│   │   │   ├── state/
│   │   │   │   ├── ChatState.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── tabs/
│   │   │   │   ├── Tab.ts
│   │   │   │   ├── TabBar.ts
│   │   │   │   ├── TabManager.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   └── ui/
│   │   │       ├── BangBashModeManager.ts
│   │   │       ├── FileContext.ts
│   │   │       ├── ImageContext.ts
│   │   │       ├── InputToolbar.ts
│   │   │       ├── InstructionModeManager.ts
│   │   │       ├── NavigationSidebar.ts
│   │   │       ├── StatusPanel.ts
│   │   │       ├── file-context/
│   │   │       │   ├── state/
│   │   │       │   │   └── FileContextState.ts
│   │   │       │   └── view/
│   │   │       │       └── FileChipsView.ts
│   │   │       └── index.ts
│   │   ├── inline-edit/
│   │   │   ├── InlineEditService.ts
│   │   │   └── ui/
│   │   │       └── InlineEditModal.ts
│   │   └── settings/
│   │       ├── ClaudianSettings.ts
│   │       ├── keyboardNavigation.ts
│   │       └── ui/
│   │           ├── AgentSettings.ts
│   │           ├── EnvSnippetManager.ts
│   │           ├── McpServerModal.ts
│   │           ├── McpSettingsManager.ts
│   │           ├── McpTestModal.ts
│   │           ├── PluginSettingsManager.ts
│   │           └── SlashCommandSettings.ts
│   ├── i18n/
│   │   ├── constants.ts
│   │   ├── i18n.ts
│   │   ├── index.ts
│   │   ├── locales/
│   │   │   ├── de.json
│   │   │   ├── en.json
│   │   │   ├── es.json
│   │   │   ├── fr.json
│   │   │   ├── ja.json
│   │   │   ├── ko.json
│   │   │   ├── pt.json
│   │   │   ├── ru.json
│   │   │   ├── zh-CN.json
│   │   │   └── zh-TW.json
│   │   └── types.ts
│   ├── main.ts
│   ├── shared/
│   │   ├── components/
│   │   │   ├── ResumeSessionDropdown.ts
│   │   │   ├── SelectableDropdown.ts
│   │   │   ├── SelectionHighlight.ts
│   │   │   └── SlashCommandDropdown.ts
│   │   ├── icons.ts
│   │   ├── index.ts
│   │   ├── mention/
│   │   │   ├── MentionDropdownController.ts
│   │   │   ├── VaultMentionCache.ts
│   │   │   ├── VaultMentionDataProvider.ts
│   │   │   └── types.ts
│   │   └── modals/
│   │       ├── ConfirmModal.ts
│   │       ├── ForkTargetModal.ts
│   │       └── InstructionConfirmModal.ts
│   ├── style/
│   │   ├── CLAUDE.md
│   │   ├── accessibility.css
│   │   ├── base/
│   │   │   ├── animations.css
│   │   │   ├── container.css
│   │   │   └── variables.css
│   │   ├── components/
│   │   │   ├── code.css
│   │   │   ├── context-footer.css
│   │   │   ├── header.css
│   │   │   ├── history.css
│   │   │   ├── input.css
│   │   │   ├── messages.css
│   │   │   ├── nav-sidebar.css
│   │   │   ├── status-panel.css
│   │   │   ├── subagent.css
│   │   │   ├── tabs.css
│   │   │   ├── thinking.css
│   │   │   └── toolcalls.css
│   │   ├── features/
│   │   │   ├── ask-user-question.css
│   │   │   ├── diff.css
│   │   │   ├── file-context.css
│   │   │   ├── file-link.css
│   │   │   ├── image-context.css
│   │   │   ├── image-embed.css
│   │   │   ├── image-modal.css
│   │   │   ├── inline-edit.css
│   │   │   ├── plan-mode.css
│   │   │   ├── resume-session.css
│   │   │   └── slash-commands.css
│   │   ├── index.css
│   │   ├── modals/
│   │   │   ├── fork-target.css
│   │   │   ├── instruction.css
│   │   │   └── mcp-modal.css
│   │   ├── settings/
│   │   │   ├── agent-settings.css
│   │   │   ├── base.css
│   │   │   ├── env-snippets.css
│   │   │   ├── mcp-settings.css
│   │   │   ├── plugin-settings.css
│   │   │   └── slash-settings.css
│   │   └── toolbar/
│   │       ├── external-context.css
│   │       ├── mcp-selector.css
│   │       ├── model-selector.css
│   │       ├── permission-toggle.css
│   │       └── thinking-selector.css
│   └── utils/
│       ├── agent.ts
│       ├── browser.ts
│       ├── canvas.ts
│       ├── claudeCli.ts
│       ├── context.ts
│       ├── contextMentionResolver.ts
│       ├── date.ts
│       ├── diff.ts
│       ├── editor.ts
│       ├── env.ts
│       ├── externalContext.ts
│       ├── externalContextScanner.ts
│       ├── fileLink.ts
│       ├── frontmatter.ts
│       ├── imageEmbed.ts
│       ├── inlineEdit.ts
│       ├── interrupt.ts
│       ├── markdown.ts
│       ├── mcp.ts
│       ├── path.ts
│       ├── sdkSession.ts
│       ├── session.ts
│       ├── slashCommand.ts
│       └── subagentJsonl.ts
├── tests/
│   ├── __mocks__/
│   │   ├── claude-agent-sdk.ts
│   │   └── obsidian.ts
│   ├── helpers/
│   │   ├── mockElement.ts
│   │   └── sdkMessages.ts
│   ├── integration/
│   │   ├── core/
│   │   │   ├── agent/
│   │   │   │   └── ClaudianService.test.ts
│   │   │   └── mcp/
│   │   │       └── mcp.test.ts
│   │   ├── features/
│   │   │   └── chat/
│   │   │       └── imagePersistence.test.ts
│   │   └── main.test.ts
│   ├── tsconfig.json
│   └── unit/
│       ├── core/
│       │   ├── agent/
│       │   │   ├── ClaudianService.test.ts
│       │   │   ├── MessageChannel.test.ts
│       │   │   ├── QueryOptionsBuilder.test.ts
│       │   │   ├── SessionManager.test.ts
│       │   │   ├── customSpawn.test.ts
│       │   │   ├── index.test.ts
│       │   │   └── types.test.ts
│       │   ├── agents/
│       │   │   ├── AgentManager.test.ts
│       │   │   ├── AgentStorage.test.ts
│       │   │   └── index.test.ts
│       │   ├── commands/
│       │   │   └── builtInCommands.test.ts
│       │   ├── hooks/
│       │   │   ├── SecurityHooks.test.ts
│       │   │   └── SubagentHooks.test.ts
│       │   ├── mcp/
│       │   │   ├── McpServerManager.test.ts
│       │   │   ├── McpTester.test.ts
│       │   │   └── createNodeFetch.test.ts
│       │   ├── plugins/
│       │   │   ├── PluginManager.test.ts
│       │   │   └── index.test.ts
│       │   ├── prompts/
│       │   │   ├── instructionRefine.test.ts
│       │   │   ├── systemPrompt.test.ts
│       │   │   └── titleGeneration.test.ts
│       │   ├── sdk/
│       │   │   ├── transformSDKMessage.test.ts
│       │   │   └── typeGuards.test.ts
│       │   ├── security/
│       │   │   ├── ApprovalManager.test.ts
│       │   │   ├── BashPathValidator.test.ts
│       │   │   └── BlocklistChecker.test.ts
│       │   ├── storage/
│       │   │   ├── AgentVaultStorage.test.ts
│       │   │   ├── CCSettingsStorage.test.ts
│       │   │   ├── ClaudianSettingsStorage.test.ts
│       │   │   ├── McpStorage.test.ts
│       │   │   ├── SessionStorage.test.ts
│       │   │   ├── SkillStorage.test.ts
│       │   │   ├── SlashCommandStorage.test.ts
│       │   │   ├── VaultFileAdapter.test.ts
│       │   │   ├── migrationConstants.test.ts
│       │   │   ├── storage.test.ts
│       │   │   ├── storageService.convenience.test.ts
│       │   │   └── storageService.migration.test.ts
│       │   ├── tools/
│       │   │   ├── todo.test.ts
│       │   │   ├── toolIcons.test.ts
│       │   │   ├── toolInput.test.ts
│       │   │   └── toolNames.test.ts
│       │   └── types/
│       │       ├── mcp.test.ts
│       │       └── types.test.ts
│       ├── features/
│       │   ├── chat/
│       │   │   ├── controllers/
│       │   │   │   ├── BrowserSelectionController.test.ts
│       │   │   │   ├── CanvasSelectionController.test.ts
│       │   │   │   ├── ConversationController.test.ts
│       │   │   │   ├── InputController.test.ts
│       │   │   │   ├── NavigationController.test.ts
│       │   │   │   ├── SelectionController.test.ts
│       │   │   │   ├── StreamController.test.ts
│       │   │   │   ├── contextRowVisibility.test.ts
│       │   │   │   └── index.test.ts
│       │   │   ├── rendering/
│       │   │   │   ├── DiffRenderer.test.ts
│       │   │   │   ├── InlineAskUserQuestion.test.ts
│       │   │   │   ├── InlineExitPlanMode.test.ts
│       │   │   │   ├── MessageRenderer.test.ts
│       │   │   │   ├── SubagentRenderer.test.ts
│       │   │   │   ├── ThinkingBlockRenderer.test.ts
│       │   │   │   ├── TodoListRenderer.test.ts
│       │   │   │   ├── ToolCallRenderer.test.ts
│       │   │   │   ├── WriteEditRenderer.test.ts
│       │   │   │   ├── collapsible.test.ts
│       │   │   │   └── todoUtils.test.ts
│       │   │   ├── rewind.test.ts
│       │   │   ├── services/
│       │   │   │   ├── BangBashService.test.ts
│       │   │   │   ├── InstructionRefineService.test.ts
│       │   │   │   ├── SubagentManager.test.ts
│       │   │   │   └── TitleGenerationService.test.ts
│       │   │   ├── state/
│       │   │   │   └── ChatState.test.ts
│       │   │   ├── tabs/
│       │   │   │   ├── Tab.test.ts
│       │   │   │   ├── TabBar.test.ts
│       │   │   │   ├── TabManager.test.ts
│       │   │   │   └── index.test.ts
│       │   │   └── ui/
│       │   │       ├── BangBashModeManager.test.ts
│       │   │       ├── ExternalContextSelector.test.ts
│       │   │       ├── FileContextManager.test.ts
│       │   │       ├── ImageContext.test.ts
│       │   │       ├── InputToolbar.test.ts
│       │   │       ├── InstructionModeManager.test.ts
│       │   │       ├── NavigationSidebar.test.ts
│       │   │       ├── StatusPanel.test.ts
│       │   │       └── file-context/
│       │   │           └── state/
│       │   │               └── FileContextState.test.ts
│       │   ├── inline-edit/
│       │   │   ├── InlineEditService.test.ts
│       │   │   └── ui/
│       │   │       ├── InlineEditModal.openAndWait.test.ts
│       │   │       └── InlineEditModal.test.ts
│       │   └── settings/
│       │       ├── AgentSettings.test.ts
│       │       └── keyboardNavigation.test.ts
│       ├── i18n/
│       │   ├── constants.test.ts
│       │   ├── i18n.test.ts
│       │   └── locales.test.ts
│       ├── shared/
│       │   ├── components/
│       │   │   ├── ResumeSessionDropdown.test.ts
│       │   │   ├── SelectableDropdown.test.ts
│       │   │   └── SlashCommandDropdown.test.ts
│       │   ├── index.test.ts
│       │   ├── mention/
│       │   │   ├── MentionDropdownController.test.ts
│       │   │   ├── VaultFileCache.test.ts
│       │   │   ├── VaultFolderCache.test.ts
│       │   │   └── VaultMentionDataProvider.test.ts
│       │   └── modals/
│       │       ├── ConfirmModal.test.ts
│       │       ├── ForkTargetModal.test.ts
│       │       └── InstructionConfirmModal.test.ts
│       └── utils/
│           ├── agent.test.ts
│           ├── browser.test.ts
│           ├── canvas.test.ts
│           ├── claudeCli.test.ts
│           ├── context.test.ts
│           ├── contextMentionResolver.test.ts
│           ├── date.test.ts
│           ├── diff.test.ts
│           ├── editor.test.ts
│           ├── env.test.ts
│           ├── externalContext.test.ts
│           ├── externalContextScanner.test.ts
│           ├── fileLink.dom.test.ts
│           ├── fileLink.handler.test.ts
│           ├── fileLink.test.ts
│           ├── frontmatter.test.ts
│           ├── imageEmbed.test.ts
│           ├── inlineEdit.test.ts
│           ├── interrupt.test.ts
│           ├── markdown.test.ts
│           ├── mcp.test.ts
│           ├── path.test.ts
│           ├── sdkSession.test.ts
│           ├── session.test.ts
│           ├── slashCommand.test.ts
│           └── utils.test.ts
├── tsconfig.jest.json
├── tsconfig.json
└── versions.json
Download .txt
Showing preview only (208K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2313 symbols across 208 files)

FILE: esbuild.config.mjs
  constant OBSIDIAN_VAULT (line 21) | const OBSIDIAN_VAULT = process.env.OBSIDIAN_VAULT;
  constant OBSIDIAN_PLUGIN_PATH (line 22) | const OBSIDIAN_PLUGIN_PATH = OBSIDIAN_VAULT && existsSync(OBSIDIAN_VAULT)
  method setup (line 29) | setup(build) {

FILE: scripts/build-css.mjs
  constant ROOT (line 12) | const ROOT = join(__dirname, '..');
  constant STYLE_DIR (line 13) | const STYLE_DIR = join(ROOT, 'src', 'style');
  constant OUTPUT (line 14) | const OUTPUT = join(ROOT, 'styles.css');
  constant INDEX_FILE (line 15) | const INDEX_FILE = join(STYLE_DIR, 'index.css');
  constant IMPORT_PATTERN (line 17) | const IMPORT_PATTERN = /^\s*@import\s+(?:url\()?['"]([^'"]+)['"]\)?\s*;/gm;
  function getModuleOrder (line 19) | function getModuleOrder() {
  function listCssFiles (line 36) | function listCssFiles(dir, baseDir = dir) {
  function build (line 57) | function build() {

FILE: scripts/build.mjs
  constant ROOT (line 12) | const ROOT = join(__dirname, '..');

FILE: scripts/postinstall.mjs
  constant ROOT (line 16) | const ROOT = join(__dirname, '..');

FILE: src/core/agent/ClaudianService.ts
  type ApprovalCallbackOptions (line 87) | interface ApprovalCallbackOptions {
  type ApprovalCallback (line 93) | type ApprovalCallback = (
  type AskUserQuestionCallback (line 100) | type AskUserQuestionCallback = (
  type QueryOptions (line 105) | interface QueryOptions {
  type EnsureReadyOptions (line 118) | interface EnsureReadyOptions {
  class ClaudianService (line 129) | class ClaudianService {
    method constructor (line 176) | constructor(plugin: ClaudianPlugin, mcpManager: McpServerManager) {
    method onReadyStateChange (line 181) | onReadyStateChange(listener: (ready: boolean) => void): () => void {
    method notifyReadyStateChange (line 193) | private notifyReadyStateChange(): void {
    method setPendingResumeAt (line 208) | setPendingResumeAt(uuid: string | undefined): void {
    method applyForkState (line 213) | applyForkState(conv: Pick<Conversation, 'sessionId' | 'sdkSessionId' |...
    method reloadMcpServers (line 224) | async reloadMcpServers(): Promise<void> {
    method ensureReady (line 243) | async ensureReady(options?: EnsureReadyOptions): Promise<boolean> {
    method startPersistentQuery (line 302) | private async startPersistentQuery(
    method attachPersistentQueryStdinErrorHandler (line 352) | private attachPersistentQueryStdinErrorHandler(query: Query): void {
    method isPipeError (line 371) | private isPipeError(error: unknown): boolean {
    method closePersistentQuery (line 380) | closePersistentQuery(_reason?: string, options?: ClosePersistentQueryO...
    method needsRestart (line 438) | private needsRestart(newConfig: PersistentQueryConfig): boolean {
    method buildPersistentQueryConfig (line 445) | private buildPersistentQueryConfig(
    method buildQueryOptionsContext (line 459) | private buildQueryOptionsContext(vaultPath: string, cliPath: string): ...
    method buildPersistentQueryOptions (line 477) | private buildPersistentQueryOptions(
    method buildHooks (line 509) | private buildHooks(externalContextPaths?: string[]) {
    method startResponseConsumer (line 550) | private startResponseConsumer(): void {
    method getTransformOptions (line 636) | private getTransformOptions(modelOverride?: string) {
    method routeMessage (line 651) | private async routeMessage(message: SDKMessage): Promise<void> {
    method registerResponseHandler (line 753) | private registerResponseHandler(handler: ResponseHandler): void {
    method unregisterResponseHandler (line 757) | private unregisterResponseHandler(handlerId: string): void {
    method isPersistentQueryActive (line 764) | isPersistentQueryActive(): boolean {
    method query (line 775) | async *query(
    method buildHistoryRebuildRequest (line 930) | private buildHistoryRebuildRequest(
    method queryViaPersistent (line 949) | private async *queryViaPersistent(
    method buildSDKUserMessage (line 1095) | private buildSDKUserMessage(prompt: string, images?: ImageAttachment[]...
    method applyDynamicUpdates (line 1146) | private async applyDynamicUpdates(
    method isStreamTextEvent (line 1254) | private isStreamTextEvent(message: SDKMessage): boolean {
    method buildPromptWithImages (line 1267) | private buildPromptWithImages(prompt: string, images?: ImageAttachment...
    method queryViaSDK (line 1306) | private async *queryViaSDK(
    method cancel (line 1395) | cancel() {
    method resetSession (line 1415) | resetSession() {
    method getSessionId (line 1425) | getSessionId(): string | null {
    method consumeSessionInvalidation (line 1430) | consumeSessionInvalidation(): boolean {
    method isReady (line 1438) | isReady(): boolean {
    method getSupportedCommands (line 1446) | async getSupportedCommands(): Promise<SlashCommand[]> {
    method setSessionId (line 1475) | setSessionId(id: string | null, externalContextPaths?: string[]): void {
    method cleanup (line 1501) | cleanup() {
    method rewindFiles (line 1510) | async rewindFiles(sdkUserUuid: string, dryRun?: boolean): Promise<Rewi...
    method resolveRewindFilePath (line 1516) | private resolveRewindFilePath(filePath: string): string {
    method createRewindBackup (line 1526) | private async createRewindBackup(filesChanged: string[] | undefined): ...
    method rewind (line 1650) | async rewind(sdkUserUuid: string, sdkAssistantUuid: string): Promise<R...
    method setApprovalCallback (line 1690) | setApprovalCallback(callback: ApprovalCallback | null) {
    method setApprovalDismisser (line 1694) | setApprovalDismisser(dismisser: (() => void) | null) {
    method setAskUserQuestionCallback (line 1698) | setAskUserQuestionCallback(callback: AskUserQuestionCallback | null) {
    method setExitPlanModeCallback (line 1702) | setExitPlanModeCallback(callback: ExitPlanModeCallback | null): void {
    method setPermissionModeSyncCallback (line 1706) | setPermissionModeSyncCallback(callback: ((sdkMode: string) => void) | ...
    method setSubagentHookProvider (line 1710) | setSubagentHookProvider(getState: () => SubagentHookState): void {
    method setAutoTurnCallback (line 1714) | setAutoTurnCallback(callback: ((chunks: StreamChunk[]) => void) | null...
    method createApprovalCallback (line 1718) | private createApprovalCallback(): CanUseTool {
    method mapToSDKPermissionMode (line 1818) | private mapToSDKPermissionMode(mode: PermissionMode): SDKPermissionMode {

FILE: src/core/agent/MessageChannel.ts
  class MessageChannel (line 25) | class MessageChannel implements AsyncIterable<SDKUserMessage> {
    method constructor (line 33) | constructor(onWarning: (message: string) => void = () => {}) {
    method setSessionId (line 37) | setSessionId(sessionId: string): void {
    method isTurnActive (line 41) | isTurnActive(): boolean {
    method isClosed (line 45) | isClosed(): boolean {
    method enqueue (line 54) | enqueue(message: SDKUserMessage): void {
    method onTurnComplete (line 124) | onTurnComplete(): void {
    method close (line 136) | close(): void {
    method reset (line 146) | reset(): void {
    method getQueueLength (line 153) | getQueueLength(): number {
    method messageHasAttachments (line 179) | private messageHasAttachments(message: SDKUserMessage): boolean {
    method extractTextContent (line 185) | private extractTextContent(message: SDKUserMessage): string {
    method pendingToMessage (line 194) | private pendingToMessage(pending: PendingMessage): SDKUserMessage {
  method [Symbol.asyncIterator] (line 157) | [Symbol.asyncIterator](): AsyncIterator<SDKUserMessage> {

FILE: src/core/agent/QueryOptionsBuilder.ts
  type QueryOptionsContext (line 35) | interface QueryOptionsContext {
  type PersistentQueryContext (line 55) | interface PersistentQueryContext extends QueryOptionsContext {
  type ColdStartQueryContext (line 77) | interface ColdStartQueryContext extends QueryOptionsContext {
  class QueryOptionsBuilder (line 101) | class QueryOptionsBuilder {
    method needsRestart (line 105) | static needsRestart(
    method buildPersistentQueryConfig (line 140) | static buildPersistentQueryConfig(
    method buildPersistentQueryOptions (line 182) | static buildPersistentQueryOptions(ctx: PersistentQueryContext): Optio...
    method buildColdStartQueryOptions (line 245) | static buildColdStartQueryOptions(ctx: ColdStartQueryContext): Options {
    method applyPermissionMode (line 319) | private static applyPermissionMode(
    method applyExtraArgs (line 339) | private static applyExtraArgs(options: Options, settings: ClaudianSett...
    method applyThinking (line 345) | private static applyThinking(
    method pathsChanged (line 361) | private static pathsChanged(a?: string[], b?: string[]): boolean {

FILE: src/core/agent/SessionManager.ts
  class SessionManager (line 25) | class SessionManager {
    method getSessionId (line 35) | getSessionId(): string | null {
    method setSessionId (line 39) | setSessionId(id: string | null, defaultModel?: ClaudeModel): void {
    method wasInterrupted (line 48) | wasInterrupted(): boolean {
    method markInterrupted (line 52) | markInterrupted(): void {
    method clearInterrupted (line 56) | clearInterrupted(): void {
    method setPendingModel (line 60) | setPendingModel(model: ClaudeModel): void {
    method clearPendingModel (line 64) | clearPendingModel(): void {
    method captureSession (line 72) | captureSession(sessionId: string): void {
    method needsHistoryRebuild (line 87) | needsHistoryRebuild(): boolean {
    method clearHistoryRebuild (line 91) | clearHistoryRebuild(): void {
    method invalidateSession (line 95) | invalidateSession(): void {
    method consumeInvalidation (line 102) | consumeInvalidation(): boolean {
    method reset (line 108) | reset(): void {

FILE: src/core/agent/customSpawn.ts
  function createCustomSpawnFunction (line 14) | function createCustomSpawnFunction(

FILE: src/core/agent/types.ts
  type TextContentBlock (line 10) | interface TextContentBlock {
  type ImageContentBlock (line 15) | interface ImageContentBlock {
  type UserContentBlock (line 24) | type UserContentBlock = TextContentBlock | ImageContentBlock;
  constant MESSAGE_CHANNEL_CONFIG (line 27) | const MESSAGE_CHANNEL_CONFIG = {
  type PendingTextMessage (line 33) | interface PendingTextMessage {
  type PendingAttachmentMessage (line 39) | interface PendingAttachmentMessage {
  type PendingMessage (line 44) | type PendingMessage = PendingTextMessage | PendingAttachmentMessage;
  type ClosePersistentQueryOptions (line 46) | interface ClosePersistentQueryOptions {
  type ResponseHandler (line 64) | interface ResponseHandler {
  type ResponseHandlerOptions (line 76) | interface ResponseHandlerOptions {
  function createResponseHandler (line 83) | function createResponseHandler(options: ResponseHandlerOptions): Respons...
  type PersistentQueryConfig (line 101) | interface PersistentQueryConfig {
  type SessionState (line 117) | interface SessionState {
  constant UNSUPPORTED_SDK_TOOLS (line 128) | const UNSUPPORTED_SDK_TOOLS = [] as const;
  constant DISABLED_BUILTIN_SUBAGENTS (line 131) | const DISABLED_BUILTIN_SUBAGENTS = [
  function isTurnCompleteMessage (line 135) | function isTurnCompleteMessage(message: SDKMessage): boolean {
  function computeSystemPromptKey (line 139) | function computeSystemPromptKey(settings: SystemPromptSettings): string {

FILE: src/core/agents/AgentManager.ts
  constant GLOBAL_AGENTS_DIR (line 17) | const GLOBAL_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
  constant VAULT_AGENTS_DIR (line 18) | const VAULT_AGENTS_DIR = '.claude/agents';
  constant PLUGIN_AGENTS_DIR (line 19) | const PLUGIN_AGENTS_DIR = 'agents';
  constant FALLBACK_BUILTIN_AGENT_NAMES (line 22) | const FALLBACK_BUILTIN_AGENT_NAMES = ['Explore', 'Plan', 'Bash', 'genera...
  constant BUILTIN_AGENT_DESCRIPTIONS (line 24) | const BUILTIN_AGENT_DESCRIPTIONS: Record<string, string> = {
  function makeBuiltinAgent (line 31) | function makeBuiltinAgent(name: string): AgentDefinition {
  function normalizePluginName (line 41) | function normalizePluginName(name: string): string {
  class AgentManager (line 45) | class AgentManager {
    method constructor (line 51) | constructor(vaultPath: string, pluginManager: PluginManager) {
    method setBuiltinAgentNames (line 57) | setBuiltinAgentNames(names: string[]): void {
    method loadAgents (line 70) | async loadAgents(): Promise<void> {
    method getAvailableAgents (line 82) | getAvailableAgents(): AgentDefinition[] {
    method getAgentById (line 86) | getAgentById(id: string): AgentDefinition | undefined {
    method searchAgents (line 91) | searchAgents(query: string): AgentDefinition[] {
    method loadPluginAgents (line 100) | private async loadPluginAgents(): Promise<void> {
    method loadVaultAgents (line 114) | private async loadVaultAgents(): Promise<void> {
    method loadGlobalAgents (line 118) | private async loadGlobalAgents(): Promise<void> {
    method loadAgentsFromDirectory (line 122) | private async loadAgentsFromDirectory(
    method listMarkdownFiles (line 134) | private listMarkdownFiles(dir: string): string[] {
    method parsePluginAgentFromFile (line 152) | private async parsePluginAgentFromFile(
    method parseAgentFromFile (line 179) | private async parseAgentFromFile(

FILE: src/core/agents/AgentStorage.ts
  constant KNOWN_AGENT_KEYS (line 4) | const KNOWN_AGENT_KEYS = new Set([
  function parseAgentFile (line 9) | function parseAgentFile(content: string): { frontmatter: AgentFrontmatte...
  function isStringOrArray (line 51) | function isStringOrArray(value: unknown): value is string | string[] {
  function parseToolsList (line 55) | function parseToolsList(tools?: string | string[]): string[] | undefined {
  function parsePermissionMode (line 59) | function parsePermissionMode(mode?: string): AgentPermissionMode | undef...
  constant VALID_MODELS (line 68) | const VALID_MODELS = ['sonnet', 'opus', 'haiku', 'inherit'] as const;
  function parseModel (line 70) | function parseModel(model?: string): 'sonnet' | 'opus' | 'haiku' | 'inhe...
  function buildAgentFromFrontmatter (line 79) | function buildAgentFromFrontmatter(

FILE: src/core/commands/builtInCommands.ts
  type BuiltInCommandAction (line 8) | type BuiltInCommandAction = 'clear' | 'add-dir' | 'resume' | 'fork';
  type BuiltInCommand (line 10) | interface BuiltInCommand {
  type BuiltInCommandResult (line 21) | interface BuiltInCommandResult {
  constant BUILT_IN_COMMANDS (line 27) | const BUILT_IN_COMMANDS: BuiltInCommand[] = [
  function detectBuiltInCommand (line 69) | function detectBuiltInCommand(input: string): BuiltInCommandResult | null {
  function getBuiltInCommandsForDropdown (line 90) | function getBuiltInCommandsForDropdown(): Array<{

FILE: src/core/hooks/SecurityHooks.ts
  type BlocklistContext (line 18) | interface BlocklistContext {
  type VaultRestrictionContext (line 23) | interface VaultRestrictionContext {
  function createBlocklistHook (line 30) | function createBlocklistHook(getContext: () => BlocklistContext): HookCa...
  function createVaultRestrictionHook (line 64) | function createVaultRestrictionHook(context: VaultRestrictionContext): H...

FILE: src/core/hooks/SubagentHooks.ts
  type SubagentHookState (line 3) | interface SubagentHookState {
  constant STOP_BLOCK_REASON (line 7) | const STOP_BLOCK_REASON = 'Background subagents are still running. Use `...
  function createStopSubagentHook (line 9) | function createStopSubagentHook(

FILE: src/core/mcp/McpServerManager.ts
  type McpStorageAdapter (line 11) | interface McpStorageAdapter {
  class McpServerManager (line 15) | class McpServerManager {
    method constructor (line 19) | constructor(storage: McpStorageAdapter) {
    method loadServers (line 23) | async loadServers(): Promise<void> {
    method getServers (line 27) | getServers(): ClaudianMcpServer[] {
    method getEnabledCount (line 31) | getEnabledCount(): number {
    method getActiveServers (line 44) | getActiveServers(mentionedNames: Set<string>): Record<string, McpServe...
    method getDisallowedMcpTools (line 68) | getDisallowedMcpTools(mentionedNames: Set<string>): string[] {
    method getAllDisallowedMcpTools (line 80) | getAllDisallowedMcpTools(): string[] {
    method collectDisallowedTools (line 84) | private collectDisallowedTools(filter?: (server: ClaudianMcpServer) =>...
    method hasServers (line 102) | hasServers(): boolean {
    method getContextSavingServers (line 106) | getContextSavingServers(): ClaudianMcpServer[] {
    method getContextSavingNames (line 110) | private getContextSavingNames(): Set<string> {
    method extractMentions (line 115) | extractMentions(text: string): Set<string> {
    method transformMentions (line 122) | transformMentions(text: string): string {

FILE: src/core/mcp/McpTester.ts
  type McpTool (line 13) | interface McpTool {
  type McpTestResult (line 19) | interface McpTestResult {
  type UrlServerConfig (line 27) | interface UrlServerConfig {
  function createNodeFetch (line 36) | function createNodeFetch(): (input: string | URL | Request, init?: Reque...
  type MinimalFetchResponse (line 98) | interface MinimalFetchResponse {
  function createFetchResponse (line 108) | function createFetchResponse(res: http.IncomingMessage): MinimalFetchRes...
  function getRequestUrl (line 179) | function getRequestUrl(input: string | URL | Request): URL {
  function mergeHeaders (line 189) | function mergeHeaders(input: string | URL | Request, init?: RequestInit)...
  function getRequestBody (line 200) | async function getRequestBody(body: BodyInit | null | undefined): Promis...
  function testMcpServer (line 211) | async function testMcpServer(server: ClaudianMcpServer): Promise<McpTest...

FILE: src/core/plugins/PluginManager.ts
  constant INSTALLED_PLUGINS_PATH (line 17) | const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugi...
  constant GLOBAL_SETTINGS_PATH (line 18) | const GLOBAL_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'setting...
  type SettingsFile (line 20) | interface SettingsFile {
  function readJsonFile (line 24) | function readJsonFile<T>(filePath: string): T | null {
  function normalizePathForComparison (line 36) | function normalizePathForComparison(p: string): string {
  function selectInstalledPluginEntry (line 49) | function selectInstalledPluginEntry(
  function extractPluginName (line 64) | function extractPluginName(pluginId: string): string {
  class PluginManager (line 72) | class PluginManager {
    method constructor (line 77) | constructor(vaultPath: string, ccSettingsStorage: CCSettingsStorage) {
    method loadPlugins (line 82) | async loadPlugins(): Promise<void> {
    method loadProjectSettings (line 127) | private async loadProjectSettings(): Promise<SettingsFile | null> {
    method getPlugins (line 132) | getPlugins(): ClaudianPlugin[] {
    method hasPlugins (line 136) | hasPlugins(): boolean {
    method hasEnabledPlugins (line 140) | hasEnabledPlugins(): boolean {
    method getEnabledCount (line 144) | getEnabledCount(): number {
    method getPluginsKey (line 149) | getPluginsKey(): string {
    method togglePlugin (line 162) | async togglePlugin(pluginId: string): Promise<void> {
    method enablePlugin (line 174) | async enablePlugin(pluginId: string): Promise<void> {
    method disablePlugin (line 184) | async disablePlugin(pluginId: string): Promise<void> {

FILE: src/core/prompts/inlineEdit.ts
  function getInlineEditSystemPrompt (line 9) | function getInlineEditSystemPrompt(allowExternalAccess: boolean = false)...

FILE: src/core/prompts/instructionRefine.ts
  function buildRefineSystemPrompt (line 7) | function buildRefineSystemPrompt(existingInstructions: string): string {

FILE: src/core/prompts/mainAgent.ts
  type SystemPromptSettings (line 10) | interface SystemPromptSettings {
  function getPathRules (line 19) | function getPathRules(vaultPath?: string, allowExternalAccess: boolean =...
  function getSubagentPathRules (line 61) | function getSubagentPathRules(allowExternalAccess: boolean = false): str...
  function getBaseSystemPrompt (line 75) | function getBaseSystemPrompt(
  function getImageInstructions (line 276) | function getImageInstructions(mediaFolder: string): string {
  function getExportInstructions (line 308) | function getExportInstructions(
  function buildSystemPrompt (line 344) | function buildSystemPrompt(settings: SystemPromptSettings = {}): string {

FILE: src/core/prompts/titleGeneration.ts
  constant TITLE_GENERATION_SYSTEM_PROMPT (line 7) | const TITLE_GENERATION_SYSTEM_PROMPT = `You are a specialist in summariz...

FILE: src/core/sdk/toolResultContent.ts
  type ToolResultContentOptions (line 1) | interface ToolResultContentOptions {
  function extractToolResultContent (line 9) | function extractToolResultContent(
  function isTextBlock (line 24) | function isTextBlock(block: unknown): block is { type: 'text'; text: str...

FILE: src/core/sdk/transformSDKMessage.ts
  type TransformOptions (line 9) | interface TransformOptions {
  type MessageUsage (line 16) | interface MessageUsage {
  type ContextWindowEntry (line 22) | interface ContextWindowEntry {
  function isResultError (line 27) | function isResultError(message: { type: 'result'; subtype: string }): me...
  function getBuiltInModelSignature (line 31) | function getBuiltInModelSignature(model: string): { family: 'haiku' | 's...
  function getModelUsageSignature (line 45) | function getModelUsageSignature(model: string): { family: 'haiku' | 'son...
  function selectContextWindowEntry (line 59) | function selectContextWindowEntry(

FILE: src/core/sdk/typeGuards.ts
  function isSessionInitEvent (line 4) | function isSessionInitEvent(event: TransformEvent): event is SessionInit...
  function isStreamChunk (line 8) | function isStreamChunk(event: TransformEvent): event is StreamChunk {

FILE: src/core/sdk/types.ts
  type SessionInitEvent (line 3) | interface SessionInitEvent {
  type TransformEvent (line 10) | type TransformEvent = StreamChunk | SessionInitEvent;

FILE: src/core/security/ApprovalManager.ts
  function getActionPattern (line 15) | function getActionPattern(toolName: string, input: Record<string, unknow...
  function getActionDescription (line 37) | function getActionDescription(toolName: string, input: Record<string, un...
  function matchesRulePattern (line 62) | function matchesRulePattern(
  function isPathPrefixMatch (line 118) | function isPathPrefixMatch(actionPath: string, approvedPath: string): bo...
  function matchesBashPrefix (line 134) | function matchesBashPrefix(action: string, prefix: string): boolean {
  function buildPermissionUpdates (line 159) | function buildPermissionUpdates(

FILE: src/core/security/BashPathValidator.ts
  type PathViolation (line 12) | type PathViolation =
  type PathCheckContext (line 17) | interface PathCheckContext {
  function tokenizeBashCommand (line 25) | function tokenizeBashCommand(command: string): string[] {
  function splitBashTokensIntoSegments (line 46) | function splitBashTokensIntoSegments(tokens: string[]): string[][] {
  function getBashSegmentCommandName (line 69) | function getBashSegmentCommandName(segment: string[]): { cmdName: string...
  constant OUTPUT_REDIRECT_OPS (line 90) | const OUTPUT_REDIRECT_OPS = new Set(['>', '>>', '1>', '1>>', '2>', '2>>'...
  constant INPUT_REDIRECT_OPS (line 91) | const INPUT_REDIRECT_OPS = new Set(['<', '<<', '0<', '0<<']);
  constant OUTPUT_OPTION_FLAGS (line 92) | const OUTPUT_OPTION_FLAGS = new Set(['-o', '--output', '--out', '--outfi...
  function isBashOutputRedirectOperator (line 94) | function isBashOutputRedirectOperator(token: string): boolean {
  function isBashInputRedirectOperator (line 98) | function isBashInputRedirectOperator(token: string): boolean {
  function isBashOutputOptionExpectingValue (line 102) | function isBashOutputOptionExpectingValue(token: string): boolean {
  function cleanPathToken (line 107) | function cleanPathToken(raw: string): string | null {
  constant QUOTE_CHARS (line 137) | const QUOTE_CHARS = new Set(["'", '"', '`']);
  function stripQuoteChars (line 139) | function stripQuoteChars(token: string): string {
  function isPathLikeToken (line 158) | function isPathLikeToken(token: string): boolean {
  function checkBashPathAccess (line 191) | function checkBashPathAccess(
  function findBashPathViolationInSegment (line 220) | function findBashPathViolationInSegment(
  function extractSubshellCommands (line 339) | function extractSubshellCommands(command: string): string[] {
  function findBashCommandPathViolation (line 382) | function findBashCommandPathViolation(

FILE: src/core/security/BlocklistChecker.ts
  constant MAX_PATTERN_LENGTH (line 8) | const MAX_PATTERN_LENGTH = 500;
  function isCommandBlocked (line 10) | function isCommandBlocked(

FILE: src/core/storage/AgentVaultStorage.ts
  constant AGENTS_PATH (line 6) | const AGENTS_PATH = '.claude/agents';
  class AgentVaultStorage (line 8) | class AgentVaultStorage {
    method constructor (line 9) | constructor(private adapter: VaultFileAdapter) {}
    method loadAll (line 11) | async loadAll(): Promise<AgentDefinition[]> {
    method load (line 39) | async load(agent: AgentDefinition): Promise<AgentDefinition | null> {
    method save (line 59) | async save(agent: AgentDefinition): Promise<void> {
    method delete (line 63) | async delete(agent: AgentDefinition): Promise<void> {
    method resolvePath (line 67) | private resolvePath(agent: AgentDefinition): string {
    method isFileNotFoundError (line 80) | private isFileNotFoundError(error: unknown): boolean {

FILE: src/core/storage/CCSettingsStorage.ts
  constant CC_SETTINGS_PATH (line 30) | const CC_SETTINGS_PATH = '.claude/settings.json';
  constant CC_SETTINGS_SCHEMA (line 33) | const CC_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-set...
  function hasClaudianOnlyFields (line 35) | function hasClaudianOnlyFields(data: Record<string, unknown>): boolean {
  function isLegacyPermissionsFormat (line 43) | function isLegacyPermissionsFormat(data: unknown): data is { permissions...
  function normalizeRuleList (line 60) | function normalizeRuleList(value: unknown): PermissionRule[] {
  function normalizePermissions (line 65) | function normalizePermissions(permissions: unknown): CCPermissions {
  class CCSettingsStorage (line 89) | class CCSettingsStorage {
    method constructor (line 90) | constructor(private adapter: VaultFileAdapter) { }
    method load (line 97) | async load(): Promise<CCSettings> {
    method save (line 131) | async save(settings: CCSettings, stripClaudianFields: boolean = false)...
    method exists (line 174) | async exists(): Promise<boolean> {
    method getPermissions (line 178) | async getPermissions(): Promise<CCPermissions> {
    method updatePermissions (line 183) | async updatePermissions(permissions: CCPermissions): Promise<void> {
    method addAllowRule (line 189) | async addAllowRule(rule: PermissionRule): Promise<void> {
    method addDenyRule (line 197) | async addDenyRule(rule: PermissionRule): Promise<void> {
    method addAskRule (line 205) | async addAskRule(rule: PermissionRule): Promise<void> {
    method removeRule (line 216) | async removeRule(rule: PermissionRule): Promise<void> {
    method getEnabledPlugins (line 228) | async getEnabledPlugins(): Promise<Record<string, boolean>> {
    method setPluginEnabled (line 240) | async setPluginEnabled(pluginId: string, enabled: boolean): Promise<vo...
    method getExplicitlyEnabledPluginIds (line 254) | async getExplicitlyEnabledPluginIds(): Promise<string[]> {
    method isPluginDisabled (line 266) | async isPluginDisabled(pluginId: string): Promise<boolean> {

FILE: src/core/storage/ClaudianSettingsStorage.ts
  constant CLAUDIAN_SETTINGS_PATH (line 23) | const CLAUDIAN_SETTINGS_PATH = '.claude/claudian-settings.json';
  type SeparatelyLoadedFields (line 26) | type SeparatelyLoadedFields = 'slashCommands';
  type StoredClaudianSettings (line 29) | type StoredClaudianSettings = Omit<ClaudianSettings, SeparatelyLoadedFie...
  function normalizeCommandList (line 31) | function normalizeCommandList(value: unknown, fallback: string[]): strin...
  function normalizeBlockedCommands (line 42) | function normalizeBlockedCommands(value: unknown): PlatformBlockedComman...
  function normalizeHostnameCliPaths (line 64) | function normalizeHostnameCliPaths(value: unknown): Record<string, strin...
  class ClaudianSettingsStorage (line 78) | class ClaudianSettingsStorage {
    method constructor (line 79) | constructor(private adapter: VaultFileAdapter) { }
    method load (line 86) | async load(): Promise<StoredClaudianSettings> {
    method save (line 113) | async save(settings: StoredClaudianSettings): Promise<void> {
    method exists (line 118) | async exists(): Promise<boolean> {
    method update (line 122) | async update(updates: Partial<StoredClaudianSettings>): Promise<void> {
    method getLegacyActiveConversationId (line 131) | async getLegacyActiveConversationId(): Promise<string | null> {
    method clearLegacyActiveConversationId (line 150) | async clearLegacyActiveConversationId(): Promise<void> {
    method setLastModel (line 167) | async setLastModel(model: ClaudeModel, isCustom: boolean): Promise<voi...
    method setLastEnvHash (line 175) | async setLastEnvHash(hash: string): Promise<void> {
    method getDefaults (line 182) | private getDefaults(): StoredClaudianSettings {

FILE: src/core/storage/McpStorage.ts
  constant MCP_CONFIG_PATH (line 30) | const MCP_CONFIG_PATH = '.claude/mcp.json';
  class McpStorage (line 32) | class McpStorage {
    method constructor (line 33) | constructor(private adapter: VaultFileAdapter) {}
    method load (line 35) | async load(): Promise<ClaudianMcpServer[]> {
    method save (line 79) | async save(servers: ClaudianMcpServer[]): Promise<void> {
    method exists (line 156) | async exists(): Promise<boolean> {
    method parseClipboardConfig (line 168) | static parseClipboardConfig(json: string): ParsedMcpConfig {
    method tryParseClipboardConfig (line 242) | static tryParseClipboardConfig(text: string): ParsedMcpConfig | null {

FILE: src/core/storage/SessionStorage.ts
  constant SESSIONS_PATH (line 27) | const SESSIONS_PATH = '.claude/sessions';
  type SessionMetaRecord (line 30) | interface SessionMetaRecord {
  type SessionMessageRecord (line 44) | interface SessionMessageRecord {
  type SessionRecord (line 50) | type SessionRecord = SessionMetaRecord | SessionMessageRecord;
  class SessionStorage (line 52) | class SessionStorage {
    method constructor (line 53) | constructor(private adapter: VaultFileAdapter) { }
    method loadConversation (line 55) | async loadConversation(id: string): Promise<Conversation | null> {
    method saveConversation (line 70) | async saveConversation(conversation: Conversation): Promise<void> {
    method deleteConversation (line 76) | async deleteConversation(id: string): Promise<void> {
    method listConversations (line 82) | async listConversations(): Promise<ConversationMeta[]> {
    method loadAllConversations (line 110) | async loadAllConversations(): Promise<{ conversations: Conversation[];...
    method hasSessions (line 141) | async hasSessions(): Promise<boolean> {
    method getFilePath (line 146) | getFilePath(id: string): string {
    method loadMetaOnly (line 150) | private async loadMetaOnly(filePath: string): Promise<ConversationMeta...
    method parseJSONL (line 195) | private parseJSONL(content: string): Conversation | null {
    method serializeToJSONL (line 233) | private serializeToJSONL(conversation: Conversation): string {
    method isNativeSession (line 270) | async isNativeSession(id: string): Promise<boolean> {
    method getMetadataPath (line 277) | getMetadataPath(id: string): string {
    method saveMetadata (line 281) | async saveMetadata(metadata: SessionMetadata): Promise<void> {
    method loadMetadata (line 287) | async loadMetadata(id: string): Promise<SessionMetadata | null> {
    method deleteMetadata (line 302) | async deleteMetadata(id: string): Promise<void> {
    method listNativeMetadata (line 308) | async listNativeMetadata(): Promise<SessionMetadata[]> {
    method listAllConversations (line 349) | async listAllConversations(): Promise<ConversationMeta[]> {
    method toSessionMetadata (line 383) | toSessionMetadata(conversation: Conversation): SessionMetadata {
    method extractSubagentData (line 411) | private extractSubagentData(messages: ChatMessage[]): Record<string, S...

FILE: src/core/storage/SkillStorage.ts
  constant SKILLS_PATH (line 5) | const SKILLS_PATH = '.claude/skills';
  class SkillStorage (line 7) | class SkillStorage {
    method constructor (line 8) | constructor(private adapter: VaultFileAdapter) {}
    method loadAll (line 10) | async loadAll(): Promise<SlashCommand[]> {
    method save (line 42) | async save(skill: SlashCommand): Promise<void> {
    method delete (line 51) | async delete(skillId: string): Promise<void> {

FILE: src/core/storage/SlashCommandStorage.ts
  constant COMMANDS_PATH (line 5) | const COMMANDS_PATH = '.claude/commands';
  class SlashCommandStorage (line 7) | class SlashCommandStorage {
    method constructor (line 8) | constructor(private adapter: VaultFileAdapter) {}
    method loadAll (line 10) | async loadAll(): Promise<SlashCommand[]> {
    method loadFromFile (line 35) | private async loadFromFile(filePath: string): Promise<SlashCommand | n...
    method save (line 40) | async save(command: SlashCommand): Promise<void> {
    method delete (line 45) | async delete(commandId: string): Promise<void> {
    method getFilePath (line 59) | getFilePath(command: SlashCommand): string {
    method parseFile (line 64) | private parseFile(content: string, filePath: string): SlashCommand {
    method filePathToId (line 72) | private filePathToId(filePath: string): string {
    method filePathToName (line 88) | private filePathToName(filePath: string): string {

FILE: src/core/storage/StorageService.ts
  constant CLAUDE_PATH (line 53) | const CLAUDE_PATH = '.claude';
  constant SETTINGS_PATH (line 56) | const SETTINGS_PATH = CC_SETTINGS_PATH;
  type CombinedSettings (line 62) | interface CombinedSettings {
  type LegacySettingsJson (line 70) | interface LegacySettingsJson {
  type LegacyDataJson (line 100) | interface LegacyDataJson {
  class StorageService (line 114) | class StorageService {
    method constructor (line 127) | constructor(plugin: Plugin) {
    method initialize (line 140) | async initialize(): Promise<CombinedSettings> {
    method runMigrations (line 150) | private async runMigrations(): Promise<void> {
    method hasStateToMigrate (line 183) | private hasStateToMigrate(data: LegacyDataJson): boolean {
    method hasLegacyContentToMigrate (line 191) | private hasLegacyContentToMigrate(data: LegacyDataJson): boolean {
    method migrateFromOldSettingsJson (line 207) | private async migrateFromOldSettingsJson(): Promise<void> {
    method migrateFromDataJson (line 291) | private async migrateFromDataJson(dataJson: LegacyDataJson): Promise<v...
    method migrateLegacyDataJsonContent (line 308) | private async migrateLegacyDataJsonContent(dataJson: LegacyDataJson): ...
    method clearLegacyDataJson (line 342) | private async clearLegacyDataJson(): Promise<void> {
    method loadDataJson (line 364) | private async loadDataJson(): Promise<LegacyDataJson | null> {
    method ensureDirectories (line 374) | async ensureDirectories(): Promise<void> {
    method loadAllSlashCommands (line 382) | async loadAllSlashCommands(): Promise<SlashCommand[]> {
    method getAdapter (line 388) | getAdapter(): VaultFileAdapter {
    method getPermissions (line 392) | async getPermissions(): Promise<CCPermissions> {
    method updatePermissions (line 396) | async updatePermissions(permissions: CCPermissions): Promise<void> {
    method addAllowRule (line 400) | async addAllowRule(rule: string): Promise<void> {
    method addDenyRule (line 404) | async addDenyRule(rule: string): Promise<void> {
    method removePermissionRule (line 411) | async removePermissionRule(rule: string): Promise<void> {
    method updateClaudianSettings (line 415) | async updateClaudianSettings(updates: Partial<StoredClaudianSettings>)...
    method saveClaudianSettings (line 419) | async saveClaudianSettings(settings: StoredClaudianSettings): Promise<...
    method loadClaudianSettings (line 423) | async loadClaudianSettings(): Promise<StoredClaudianSettings> {
    method getLegacyActiveConversationId (line 430) | async getLegacyActiveConversationId(): Promise<string | null> {
    method clearLegacyActiveConversationId (line 447) | async clearLegacyActiveConversationId(): Promise<void> {
    method getTabManagerState (line 463) | async getTabManagerState(): Promise<TabManagerPersistedState | null> {
    method validateTabManagerState (line 479) | private validateTabManagerState(data: unknown): TabManagerPersistedSta...
    method setTabManagerState (line 515) | async setTabManagerState(state: TabManagerPersistedState): Promise<voi...
  type TabManagerPersistedState (line 530) | interface TabManagerPersistedState {

FILE: src/core/storage/VaultFileAdapter.ts
  class VaultFileAdapter (line 10) | class VaultFileAdapter {
    method constructor (line 13) | constructor(private app: App) {}
    method exists (line 15) | async exists(path: string): Promise<boolean> {
    method read (line 19) | async read(path: string): Promise<string> {
    method write (line 23) | async write(path: string, content: string): Promise<void> {
    method append (line 28) | async append(path: string, content: string): Promise<void> {
    method delete (line 43) | async delete(path: string): Promise<void> {
    method deleteFolder (line 50) | async deleteFolder(path: string): Promise<void> {
    method listFiles (line 60) | async listFiles(folder: string): Promise<string[]> {
    method listFolders (line 69) | async listFolders(folder: string): Promise<string[]> {
    method listFilesRecursive (line 78) | async listFilesRecursive(folder: string): Promise<string[]> {
    method ensureParentFolder (line 96) | private async ensureParentFolder(filePath: string): Promise<void> {
    method ensureFolder (line 104) | async ensureFolder(path: string): Promise<void> {
    method rename (line 119) | async rename(oldPath: string, newPath: string): Promise<void> {
    method stat (line 123) | async stat(path: string): Promise<{ mtime: number; size: number } | nu...

FILE: src/core/storage/migrationConstants.ts
  constant CLAUDIAN_ONLY_FIELDS (line 14) | const CLAUDIAN_ONLY_FIELDS = new Set([
  constant MIGRATABLE_CLAUDIAN_FIELDS (line 61) | const MIGRATABLE_CLAUDIAN_FIELDS = new Set([
  constant DEPRECATED_FIELDS (line 90) | const DEPRECATED_FIELDS = new Set([
  function convertEnvObjectToString (line 103) | function convertEnvObjectToString(env: Record<string, string> | undefine...
  function mergeEnvironmentVariables (line 118) | function mergeEnvironmentVariables(existing: string, additional: string)...

FILE: src/core/tools/todo.ts
  type TodoItem (line 9) | interface TodoItem {
  function isValidTodoItem (line 17) | function isValidTodoItem(item: unknown): item is TodoItem {
  function parseTodoInput (line 30) | function parseTodoInput(input: Record<string, unknown>): TodoItem[] | nu...
  function extractLastTodosFromMessages (line 49) | function extractLastTodosFromMessages(

FILE: src/core/tools/toolIcons.ts
  constant TOOL_ICONS (line 28) | const TOOL_ICONS: Record<string, string> = {
  constant MCP_ICON_MARKER (line 56) | const MCP_ICON_MARKER = '__mcp_icon__';
  function getToolIcon (line 58) | function getToolIcon(toolName: string): string {

FILE: src/core/tools/toolInput.ts
  function extractResolvedAnswers (line 18) | function extractResolvedAnswers(toolUseResult: unknown): AskUserAnswers ...
  function normalizeAnswerValue (line 24) | function normalizeAnswerValue(value: unknown): string | undefined {
  function normalizeAnswersObject (line 37) | function normalizeAnswersObject(value: unknown): AskUserAnswers | undefi...
  function parseAnswersFromJsonObject (line 51) | function parseAnswersFromJsonObject(resultText: string): AskUserAnswers ...
  function parseAnswersFromQuotedPairs (line 64) | function parseAnswersFromQuotedPairs(resultText: string): AskUserAnswers...
  function extractResolvedAnswersFromResultText (line 81) | function extractResolvedAnswersFromResultText(result: unknown): AskUserA...
  function getPathFromToolInput (line 89) | function getPathFromToolInput(

FILE: src/core/tools/toolNames.ts
  constant TOOL_AGENT_OUTPUT (line 1) | const TOOL_AGENT_OUTPUT = 'TaskOutput' as const;
  constant TOOL_ASK_USER_QUESTION (line 2) | const TOOL_ASK_USER_QUESTION = 'AskUserQuestion' as const;
  constant TOOL_BASH (line 3) | const TOOL_BASH = 'Bash' as const;
  constant TOOL_BASH_OUTPUT (line 4) | const TOOL_BASH_OUTPUT = 'BashOutput' as const;
  constant TOOL_EDIT (line 5) | const TOOL_EDIT = 'Edit' as const;
  constant TOOL_GLOB (line 6) | const TOOL_GLOB = 'Glob' as const;
  constant TOOL_GREP (line 7) | const TOOL_GREP = 'Grep' as const;
  constant TOOL_KILL_SHELL (line 8) | const TOOL_KILL_SHELL = 'KillShell' as const;
  constant TOOL_LS (line 9) | const TOOL_LS = 'LS' as const;
  constant TOOL_LIST_MCP_RESOURCES (line 10) | const TOOL_LIST_MCP_RESOURCES = 'ListMcpResources' as const;
  constant TOOL_MCP (line 11) | const TOOL_MCP = 'Mcp' as const;
  constant TOOL_NOTEBOOK_EDIT (line 12) | const TOOL_NOTEBOOK_EDIT = 'NotebookEdit' as const;
  constant TOOL_READ (line 13) | const TOOL_READ = 'Read' as const;
  constant TOOL_READ_MCP_RESOURCE (line 14) | const TOOL_READ_MCP_RESOURCE = 'ReadMcpResource' as const;
  constant TOOL_SKILL (line 15) | const TOOL_SKILL = 'Skill' as const;
  constant TOOL_SUBAGENT (line 16) | const TOOL_SUBAGENT = 'Agent' as const;
  constant TOOL_SUBAGENT_LEGACY (line 17) | const TOOL_SUBAGENT_LEGACY = 'Task' as const;
  constant TOOL_TASK (line 19) | const TOOL_TASK = TOOL_SUBAGENT;
  constant TOOL_TODO_WRITE (line 20) | const TOOL_TODO_WRITE = 'TodoWrite' as const;
  constant TOOL_TOOL_SEARCH (line 21) | const TOOL_TOOL_SEARCH = 'ToolSearch' as const;
  constant TOOL_WEB_FETCH (line 22) | const TOOL_WEB_FETCH = 'WebFetch' as const;
  constant TOOL_WEB_SEARCH (line 23) | const TOOL_WEB_SEARCH = 'WebSearch' as const;
  constant TOOL_WRITE (line 24) | const TOOL_WRITE = 'Write' as const;
  constant TOOL_ENTER_PLAN_MODE (line 26) | const TOOL_ENTER_PLAN_MODE = 'EnterPlanMode' as const;
  constant TOOL_EXIT_PLAN_MODE (line 27) | const TOOL_EXIT_PLAN_MODE = 'ExitPlanMode' as const;
  constant TOOLS_SKIP_BLOCKED_DETECTION (line 31) | const TOOLS_SKIP_BLOCKED_DETECTION = [
  constant SUBAGENT_TOOL_NAMES (line 37) | const SUBAGENT_TOOL_NAMES = [
  type SubagentToolName (line 41) | type SubagentToolName = (typeof SUBAGENT_TOOL_NAMES)[number];
  function skipsBlockedDetection (line 43) | function skipsBlockedDetection(name: string): boolean {
  function isSubagentToolName (line 47) | function isSubagentToolName(name: string): name is SubagentToolName {
  constant EDIT_TOOLS (line 51) | const EDIT_TOOLS = [TOOL_WRITE, TOOL_EDIT, TOOL_NOTEBOOK_EDIT] as const;
  type EditToolName (line 52) | type EditToolName = (typeof EDIT_TOOLS)[number];
  constant WRITE_EDIT_TOOLS (line 54) | const WRITE_EDIT_TOOLS = [TOOL_WRITE, TOOL_EDIT] as const;
  type WriteEditToolName (line 55) | type WriteEditToolName = (typeof WRITE_EDIT_TOOLS)[number];
  constant BASH_TOOLS (line 57) | const BASH_TOOLS = [TOOL_BASH, TOOL_BASH_OUTPUT, TOOL_KILL_SHELL] as const;
  type BashToolName (line 58) | type BashToolName = (typeof BASH_TOOLS)[number];
  constant FILE_TOOLS (line 60) | const FILE_TOOLS = [
  type FileToolName (line 70) | type FileToolName = (typeof FILE_TOOLS)[number];
  constant MCP_TOOLS (line 72) | const MCP_TOOLS = [
  type McpToolName (line 77) | type McpToolName = (typeof MCP_TOOLS)[number];
  constant READ_ONLY_TOOLS (line 79) | const READ_ONLY_TOOLS = [
  type ReadOnlyToolName (line 87) | type ReadOnlyToolName = (typeof READ_ONLY_TOOLS)[number];
  function isEditTool (line 89) | function isEditTool(toolName: string): toolName is EditToolName {
  function isWriteEditTool (line 93) | function isWriteEditTool(toolName: string): toolName is WriteEditToolName {
  function isFileTool (line 97) | function isFileTool(toolName: string): toolName is FileToolName {
  function isBashTool (line 101) | function isBashTool(toolName: string): toolName is BashToolName {
  function isMcpTool (line 105) | function isMcpTool(toolName: string): toolName is McpToolName {
  function isReadOnlyTool (line 109) | function isReadOnlyTool(toolName: string): toolName is ReadOnlyToolName {

FILE: src/core/types/agent.ts
  constant AGENT_PERMISSION_MODES (line 1) | const AGENT_PERMISSION_MODES = ['default', 'acceptEdits', 'dontAsk', 'by...
  type AgentPermissionMode (line 2) | type AgentPermissionMode = typeof AGENT_PERMISSION_MODES[number];
  type AgentDefinition (line 8) | interface AgentDefinition {
  type AgentFrontmatter (line 49) | interface AgentFrontmatter {

FILE: src/core/types/chat.ts
  type ForkSource (line 9) | interface ForkSource {
  constant VIEW_TYPE_CLAUDIAN (line 15) | const VIEW_TYPE_CLAUDIAN = 'claudian-view';
  type ImageMediaType (line 18) | type ImageMediaType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/...
  type ImageAttachment (line 21) | interface ImageAttachment {
  type ContentBlock (line 34) | type ContentBlock =
  type ChatMessage (line 42) | interface ChatMessage {
  type Conversation (line 68) | interface Conversation {
  type ConversationMeta (line 116) | interface ConversationMeta {
  type SessionMetadata (line 136) | interface SessionMetadata {
  type StreamChunk (line 175) | type StreamChunk =
  type UsageInfo (line 191) | interface UsageInfo {

FILE: src/core/types/diff.ts
  type DiffLine (line 5) | interface DiffLine {
  type DiffStats (line 12) | interface DiffStats {
  type StructuredPatchHunk (line 18) | interface StructuredPatchHunk {
  type SDKToolUseResult (line 27) | interface SDKToolUseResult {

FILE: src/core/types/mcp.ts
  type McpStdioServerConfig (line 8) | interface McpStdioServerConfig {
  type McpSSEServerConfig (line 16) | interface McpSSEServerConfig {
  type McpHttpServerConfig (line 23) | interface McpHttpServerConfig {
  type McpServerConfig (line 30) | type McpServerConfig =
  type McpServerType (line 36) | type McpServerType = 'stdio' | 'sse' | 'http';
  type ClaudianMcpServer (line 39) | interface ClaudianMcpServer {
  type McpConfigFile (line 52) | interface McpConfigFile {
  type ClaudianMcpConfigFile (line 57) | interface ClaudianMcpConfigFile extends McpConfigFile {
  type ParsedMcpConfig (line 73) | interface ParsedMcpConfig {
  function getMcpServerType (line 78) | function getMcpServerType(config: McpServerConfig): McpServerType {
  function isValidMcpServerConfig (line 85) | function isValidMcpServerConfig(obj: unknown): obj is McpServerConfig {
  constant DEFAULT_MCP_SERVER (line 98) | const DEFAULT_MCP_SERVER: Omit<ClaudianMcpServer, 'name' | 'config'> = {

FILE: src/core/types/models.ts
  type ClaudeModel (line 6) | type ClaudeModel = string;
  constant DEFAULT_CLAUDE_MODELS (line 8) | const DEFAULT_CLAUDE_MODELS: { value: ClaudeModel; label: string; descri...
  type ThinkingBudget (line 16) | type ThinkingBudget = 'off' | 'low' | 'medium' | 'high' | 'xhigh';
  constant THINKING_BUDGETS (line 18) | const THINKING_BUDGETS: { value: ThinkingBudget; label: string; tokens: ...
  type EffortLevel (line 27) | type EffortLevel = 'low' | 'medium' | 'high' | 'max';
  constant EFFORT_LEVELS (line 29) | const EFFORT_LEVELS: { value: EffortLevel; label: string }[] = [
  constant DEFAULT_EFFORT_LEVEL (line 37) | const DEFAULT_EFFORT_LEVEL: Record<string, EffortLevel> = {
  constant DEFAULT_THINKING_BUDGET (line 46) | const DEFAULT_THINKING_BUDGET: Record<string, ThinkingBudget> = {
  constant DEFAULT_MODEL_VALUES (line 54) | const DEFAULT_MODEL_VALUES = new Set(DEFAULT_CLAUDE_MODELS.map(m => m.va...
  function isAdaptiveThinkingModel (line 57) | function isAdaptiveThinkingModel(model: string): boolean {
  constant CONTEXT_WINDOW_STANDARD (line 62) | const CONTEXT_WINDOW_STANDARD = 200_000;
  constant CONTEXT_WINDOW_1M (line 63) | const CONTEXT_WINDOW_1M = 1_000_000;
  function filterVisibleModelOptions (line 65) | function filterVisibleModelOptions<T extends { value: string }>(
  function normalizeVisibleModelVariant (line 83) | function normalizeVisibleModelVariant(
  function getContextWindowSize (line 99) | function getContextWindowSize(

FILE: src/core/types/plugins.ts
  type PluginScope (line 1) | type PluginScope = 'user' | 'project';
  type ClaudianPlugin (line 3) | interface ClaudianPlugin {
  type InstalledPluginEntry (line 12) | interface InstalledPluginEntry {
  type InstalledPluginsFile (line 22) | interface InstalledPluginsFile {

FILE: src/core/types/sdk.ts
  type BlockedUserMessage (line 6) | type BlockedUserMessage = SDKUserMessage & {
  function isBlockedMessage (line 11) | function isBlockedMessage(message: { type: string }): message is Blocked...

FILE: src/core/types/settings.ts
  constant UNIX_BLOCKED_COMMANDS (line 8) | const UNIX_BLOCKED_COMMANDS = [
  constant WINDOWS_BLOCKED_COMMANDS (line 15) | const WINDOWS_BLOCKED_COMMANDS = [
  type PlatformBlockedCommands (line 51) | interface PlatformBlockedCommands {
  function getDefaultBlockedCommands (line 56) | function getDefaultBlockedCommands(): PlatformBlockedCommands {
  function getCurrentPlatformKey (line 63) | function getCurrentPlatformKey(): keyof PlatformBlockedCommands {
  function getCurrentPlatformBlockedCommands (line 67) | function getCurrentPlatformBlockedCommands(commands: PlatformBlockedComm...
  function getBashToolBlockedCommands (line 78) | function getBashToolBlockedCommands(commands: PlatformBlockedCommands): ...
  type PlatformCliPaths (line 89) | interface PlatformCliPaths {
  type CliPlatformKey (line 96) | type CliPlatformKey = keyof PlatformCliPaths;
  function getCliPlatformKey (line 102) | function getCliPlatformKey(): CliPlatformKey {
  type HostnameCliPaths (line 118) | type HostnameCliPaths = Record<string, string>;
  type PermissionMode (line 121) | type PermissionMode = 'yolo' | 'plan' | 'normal';
  type ApprovalDecision (line 124) | type ApprovalDecision = 'allow' | 'allow-always' | 'deny' | 'cancel';
  type LegacyPermission (line 130) | interface LegacyPermission {
  type PermissionRule (line 142) | type PermissionRule = string & { readonly __brand: 'PermissionRule' };
  function createPermissionRule (line 148) | function createPermissionRule(rule: string): PermissionRule {
  type CCPermissions (line 156) | interface CCPermissions {
  type CCSettings (line 173) | interface CCSettings {
  type EnvSnippet (line 193) | interface EnvSnippet {
  type SlashCommandSource (line 202) | type SlashCommandSource = 'builtin' | 'user' | 'plugin' | 'sdk';
  type SlashCommand (line 205) | interface SlashCommand {
  type KeyboardNavigationSettings (line 223) | interface KeyboardNavigationSettings {
  type TabBarPosition (line 230) | type TabBarPosition = 'input' | 'header';
  type ClaudianSettings (line 236) | interface ClaudianSettings {
  constant DEFAULT_SETTINGS (line 305) | const DEFAULT_SETTINGS: ClaudianSettings = {
  constant DEFAULT_CC_SETTINGS (line 371) | const DEFAULT_CC_SETTINGS: CCSettings = {
  constant DEFAULT_CC_PERMISSIONS (line 381) | const DEFAULT_CC_PERMISSIONS: CCPermissions = {
  type InstructionRefineResult (line 388) | interface InstructionRefineResult {
  function legacyPermissionToCCRule (line 402) | function legacyPermissionToCCRule(legacy: LegacyPermission): PermissionR...
  function legacyPermissionsToCCPermissions (line 417) | function legacyPermissionsToCCPermissions(
  function parseCCPermissionRule (line 442) | function parseCCPermissionRule(rule: PermissionRule): {

FILE: src/core/types/tools.ts
  type ToolDiffData (line 8) | interface ToolDiffData {
  type AskUserQuestionOption (line 15) | interface AskUserQuestionOption {
  type AskUserQuestionItem (line 21) | interface AskUserQuestionItem {
  type AskUserAnswers (line 29) | type AskUserAnswers = Record<string, string>;
  type ToolCallInfo (line 32) | interface ToolCallInfo {
  type ExitPlanModeDecision (line 44) | type ExitPlanModeDecision =
  type ExitPlanModeCallback (line 49) | type ExitPlanModeCallback = (
  type SubagentMode (line 55) | type SubagentMode = 'sync' | 'async';
  type AsyncSubagentStatus (line 58) | type AsyncSubagentStatus =
  type SubagentInfo (line 66) | interface SubagentInfo {

FILE: src/features/chat/ClaudianView.ts
  class ClaudianView (line 10) | class ClaudianView extends ItemView {
    method constructor (line 41) | constructor(leaf: WorkspaceLeaf, plugin: ClaudianPlugin) {
    method getViewType (line 68) | getViewType(): string {
    method getDisplayText (line 72) | getDisplayText(): string {
    method getIcon (line 76) | getIcon(): string {
    method refreshModelSelector (line 81) | refreshModelSelector(): void {
    method updateHiddenSlashCommands (line 97) | updateHiddenSlashCommands(): void {
    method onOpen (line 106) | async onOpen() {
    method onClose (line 179) | async onClose() {
    method buildHeader (line 208) | private buildHeader(header: HTMLElement) {
    method buildNavRowContent (line 239) | private buildNavRowContent(): HTMLElement {
    method updateNavRowLocation (line 301) | private updateNavRowLocation(): void {
    method updateLayoutForPosition (line 335) | updateLayoutForPosition(): void {
    method handleTabClick (line 354) | private handleTabClick(tabId: TabId): void {
    method handleTabClose (line 358) | private async handleTabClose(tabId: TabId): Promise<void> {
    method handleNewTab (line 366) | private async handleNewTab(): Promise<void> {
    method updateTabBar (line 376) | private updateTabBar(): void {
    method updateTabBarVisibility (line 394) | private updateTabBarVisibility(): void {
    method toggleHistoryDropdown (line 419) | private toggleHistoryDropdown(): void {
    method updateHistoryDropdown (line 431) | private updateHistoryDropdown(): void {
    method findTabWithConversation (line 468) | private findTabWithConversation(conversationId: string): TabData | null {
    method wireEventHandlers (line 477) | private wireEventHandlers(): void {
    method restoreOrCreateTabs (line 552) | private async restoreOrCreateTabs(): Promise<void> {
    method persistTabState (line 581) | private persistTabState(): void {
    method persistTabStateImmediate (line 597) | private async persistTabStateImmediate(): Promise<void> {
    method getActiveTab (line 613) | getActiveTab(): TabData | null {
    method getTabManager (line 618) | getTabManager(): TabManager | null {

FILE: src/features/chat/constants.ts
  constant LOGO_SVG (line 1) | const LOGO_SVG = {
  constant COMPLETION_FLAVOR_WORDS (line 10) | const COMPLETION_FLAVOR_WORDS = [
  constant FLAVOR_TEXTS (line 36) | const FLAVOR_TEXTS = [

FILE: src/features/chat/controllers/BrowserSelectionController.ts
  constant BROWSER_SELECTION_POLL_INTERVAL (line 6) | const BROWSER_SELECTION_POLL_INTERVAL = 250;
  type BrowserLikeWebview (line 8) | type BrowserLikeWebview = HTMLElement & {
  class BrowserSelectionController (line 12) | class BrowserSelectionController {
    method constructor (line 22) | constructor(
    method start (line 36) | start(): void {
    method stop (line 43) | stop(): void {
    method poll (line 51) | private async poll(): Promise<void> {
    method getActiveBrowserView (line 78) | private getActiveBrowserView(): { view: ItemView; viewType: string; co...
    method isBrowserLikeView (line 90) | private isBrowserLikeView(viewType: string, containerEl: HTMLElement):...
    method extractSelectedText (line 103) | private async extractSelectedText(containerEl: HTMLElement): Promise<s...
    method extractSelectionFromDocument (line 114) | private extractSelectionFromDocument(doc: Document, scopeEl: HTMLEleme...
    method extractSelectionFromActiveInput (line 128) | private extractSelectionFromActiveInput(doc: Document, scopeEl: HTMLEl...
    method extractSelectionFromIframes (line 141) | private extractSelectionFromIframes(containerEl: HTMLElement): string ...
    method extractSelectionFromWebviews (line 157) | private async extractSelectionFromWebviews(containerEl: HTMLElement): ...
    method buildContext (line 176) | private buildContext(
    method extractViewTitle (line 194) | private extractViewTitle(view: ItemView): string | undefined {
    method extractViewUrl (line 202) | private extractViewUrl(view: ItemView, containerEl: HTMLElement): stri...
    method isSameSelection (line 226) | private isSameSelection(
    method clearWhenInputIsNotFocused (line 237) | private clearWhenInputIsNotFocused(): void {
    method updateIndicator (line 245) | private updateIndicator(): void {
    method buildIndicatorTitle (line 262) | private buildIndicatorTitle(): string {
    method updateContextRowVisibility (line 277) | updateContextRowVisibility(): void {
    method getContext (line 283) | getContext(): BrowserSelectionContext | null {
    method hasSelection (line 287) | hasSelection(): boolean {
    method clear (line 291) | clear(): void {

FILE: src/features/chat/controllers/CanvasSelectionController.ts
  constant CANVAS_POLL_INTERVAL (line 6) | const CANVAS_POLL_INTERVAL = 250;
  class CanvasSelectionController (line 8) | class CanvasSelectionController {
    method constructor (line 17) | constructor(
    method start (line 31) | start(): void {
    method stop (line 36) | stop(): void {
    method poll (line 44) | private poll(): void {
    method getCanvasView (line 75) | private getCanvasView(): ItemView | null {
    method updateIndicator (line 88) | private updateIndicator(): void {
    method updateContextRowVisibility (line 103) | updateContextRowVisibility(): void {
    method getContext (line 109) | getContext(): CanvasSelectionContext | null {
    method hasSelection (line 117) | hasSelection(): boolean {
    method clear (line 121) | clear(): void {

FILE: src/features/chat/controllers/ConversationController.ts
  type ConversationCallbacks (line 16) | interface ConversationCallbacks {
  type ConversationControllerDeps (line 22) | interface ConversationControllerDeps {
  type SaveOptions (line 42) | type SaveOptions = {
  class ConversationController (line 46) | class ConversationController {
    method constructor (line 50) | constructor(deps: ConversationControllerDeps, callbacks: ConversationC...
    method getAgentService (line 55) | private getAgentService(): ClaudianService | null {
    method createNew (line 69) | async createNew(options: { force?: boolean } = {}): Promise<void> {
    method loadActive (line 158) | async loadActive(): Promise<void> {
    method switchTo (line 255) | async switchTo(id: string): Promise<void> {
    method rewind (line 338) | async rewind(userMessageId: string): Promise<void> {
    method save (line 430) | async save(updateLastResponse = false, options?: SaveOptions): Promise...
    method restoreExternalContextPaths (line 530) | private restoreExternalContextPaths(
    method toggleHistoryDropdown (line 555) | toggleHistoryDropdown(): void {
    method updateHistoryDropdown (line 568) | updateHistoryDropdown(): void {
    method renderHistoryItems (line 582) | private renderHistoryItems(
    method showRenameInput (line 687) | private showRenameInput(item: HTMLElement, convId: string, currentTitl...
    method getGreeting (line 727) | getGreeting(): string {
    method updateWelcomeVisibility (line 783) | updateWelcomeVisibility(): void {
    method initializeWelcome (line 798) | initializeWelcome(): void {
    method generateFallbackTitle (line 820) | generateFallbackTitle(firstMessage: string): string {
    method regenerateTitle (line 828) | async regenerateTitle(conversationId: string): Promise<void> {
    method formatDate (line 879) | formatDate(timestamp: number): string {
    method renderHistoryDropdown (line 897) | renderHistoryDropdown(

FILE: src/features/chat/controllers/InputController.ts
  constant APPROVAL_OPTION_MAP (line 33) | const APPROVAL_OPTION_MAP: Record<string, ApprovalDecision> = {
  type InputControllerDeps (line 39) | interface InputControllerDeps {
  class InputController (line 73) | class InputController {
    method constructor (line 81) | constructor(deps: InputControllerDeps) {
    method getAgentService (line 85) | private getAgentService(): ClaudianService | null {
    method isResumeSessionAtStillNeeded (line 89) | private isResumeSessionAtStillNeeded(resumeUuid: string, previousMessa...
    method sendMessage (line 103) | async sendMessage(options?: {
    method updateQueueIndicator (line 486) | updateQueueIndicator(): void {
    method clearQueuedMessage (line 509) | clearQueuedMessage(): void {
    method restoreQueuedMessageToInput (line 515) | private restoreQueuedMessageToInput(): void {
    method processQueuedMessage (line 530) | private processQueuedMessage(): void {
    method triggerTitleGeneration (line 562) | private async triggerTitleGeneration(): Promise<void> {
    method cancelStreaming (line 638) | cancelStreaming(): void {
    method syncScrollToBottomAfterRenderUpdates (line 648) | private syncScrollToBottomAfterRenderUpdates(): void {
    method handleInstructionSubmit (line 666) | async handleInstructionSubmit(rawInstruction: string): Promise<void> {
    method handleApprovalRequest (line 763) | async handleApprovalRequest(
    method handleAskUserQuestion (line 820) | async handleAskUserQuestion(
    method showInlineQuestion (line 839) | private showInlineQuestion(
    method handleExitPlanMode (line 873) | async handleExitPlanMode(
    method dismissPendingApproval (line 917) | dismissPendingApproval(): void {
    method hideInputContainer (line 933) | private hideInputContainer(inputContainerEl: HTMLElement): void {
    method restoreInputContainer (line 938) | private restoreInputContainer(inputContainerEl: HTMLElement): void {
    method resetInputContainerVisibility (line 946) | private resetInputContainerVisibility(): void {
    method executeBuiltInCommand (line 957) | private async executeBuiltInCommand(action: string, args: string): Pro...
    method handleResumeKeydown (line 999) | handleResumeKeydown(e: KeyboardEvent): boolean {
    method isResumeDropdownVisible (line 1004) | isResumeDropdownVisible(): boolean {
    method destroyResumeDropdown (line 1008) | destroyResumeDropdown(): void {
    method showResumeDropdown (line 1015) | private showResumeDropdown(): void {

FILE: src/features/chat/controllers/NavigationController.ts
  constant SCROLL_SPEED (line 4) | const SCROLL_SPEED = 8;
  type NavigationControllerDeps (line 6) | interface NavigationControllerDeps {
  class NavigationController (line 15) | class NavigationController {
    method constructor (line 27) | constructor(deps: NavigationControllerDeps) {
    method initialize (line 34) | initialize(): void {
    method dispose (line 58) | dispose(): void {
    method handleMessagesKeydown (line 80) | private handleMessagesKeydown(e: KeyboardEvent): void {
    method handleKeyup (line 109) | private handleKeyup(e: KeyboardEvent): void {
    method handleInputKeydown (line 126) | private handleInputKeydown(e: KeyboardEvent): void {
    method startScrolling (line 152) | private startScrolling(direction: 'up' | 'down'): void {
    method stopScrolling (line 161) | private stopScrolling(): void {
    method focusMessages (line 190) | focusMessages(): void {
    method focusInput (line 195) | focusInput(): void {

FILE: src/features/chat/controllers/SelectionController.ts
  constant SELECTION_POLL_INTERVAL (line 10) | const SELECTION_POLL_INTERVAL = 250;
  constant INPUT_HANDOFF_GRACE_MS (line 12) | const INPUT_HANDOFF_GRACE_MS = 1500;
  class SelectionController (line 14) | class SelectionController {
    method constructor (line 28) | constructor(
    method start (line 42) | start(): void {
    method stop (line 48) | stop(): void {
    method dispose (line 57) | dispose(): void {
    method poll (line 65) | private poll(): void {
    method pollReadingMode (line 121) | private pollReadingMode(view: MarkdownView): void {
    method handleDeselection (line 162) | private handleDeselection(): void {
    method clearWhenMarkdownIsNotActive (line 179) | private clearWhenMarkdownIsNotActive(): void {
    method showHighlight (line 193) | showHighlight(): void {
    method clearHighlight (line 199) | private clearHighlight(): void {
    method updateIndicator (line 208) | private updateIndicator(): void {
    method updateContextRowVisibility (line 221) | updateContextRowVisibility(): void {
    method getContext (line 231) | getContext(): EditorSelectionContext | null {
    method hasSelection (line 242) | hasSelection(): boolean {
    method clear (line 250) | clear(): void {

FILE: src/features/chat/controllers/StreamController.ts
  type StreamControllerDeps (line 42) | interface StreamControllerDeps {
  class StreamController (line 54) | class StreamController {
    method constructor (line 59) | constructor(deps: StreamControllerDeps) {
    method handleStreamChunk (line 67) | async handleStreamChunk(chunk: StreamChunk, msg: ChatMessage): Promise...
    method handleRegularToolUse (line 204) | private handleRegularToolUse(
    method capturePlanFilePath (line 287) | private capturePlanFilePath(input: Record<string, unknown>): void {
    method flushPendingTools (line 298) | private flushPendingTools(): void {
    method renderPendingTool (line 316) | private renderPendingTool(toolId: string): void {
    method handleToolResult (line 333) | private async handleToolResult(
    method appendText (line 419) | async appendText(text: string): Promise<void> {
    method finalizeCurrentTextBlock (line 434) | finalizeCurrentTextBlock(msg?: ChatMessage): void {
    method appendThinking (line 452) | async appendThinking(content: string): Promise<void> {
    method finalizeCurrentThinkingBlock (line 467) | finalizeCurrentThinkingBlock(msg?: ChatMessage): void {
    method handleTaskToolUseViaManager (line 490) | private handleTaskToolUseViaManager(
    method renderPendingTaskViaManager (line 517) | private renderPendingTaskViaManager(toolId: string, msg: ChatMessage):...
    method renderPendingTaskFromTaskResultViaManager (line 529) | private renderPendingTaskFromTaskResultViaManager(
    method recordSubagentInMessage (line 549) | private recordSubagentInMessage(
    method handleSubagentChunk (line 572) | private async handleSubagentChunk(chunk: StreamChunk, msg: ChatMessage...
    method finalizeSubagent (line 622) | private finalizeSubagent(
    method handleAgentOutputToolUse (line 653) | private handleAgentOutputToolUse(
    method handleAsyncTaskToolResult (line 671) | private handleAsyncTaskToolResult(
    method handleAgentOutputToolResult (line 684) | private async handleAgentOutputToolResult(
    method hydrateAsyncSubagentToolCalls (line 702) | private async hydrateAsyncSubagentToolCalls(subagent: SubagentInfo | u...
    method tryHydrateAsyncSubagent (line 732) | private async tryHydrateAsyncSubagent(
    method scheduleAsyncSubagentResultRetry (line 772) | private scheduleAsyncSubagentResultRetry(
    method retryAsyncSubagentResult (line 787) | private async retryAsyncSubagentResult(
    method onAsyncSubagentStateChange (line 813) | onAsyncSubagentStateChange(subagent: SubagentInfo): void {
    method updateSubagentInMessages (line 818) | private updateSubagentInMessages(subagent: SubagentInfo): void {
    method ensureTaskToolCall (line 829) | private ensureTaskToolCall(
    method applySubagentToTaskToolCall (line 856) | private applySubagentToTaskToolCall(taskToolCall: ToolCallInfo, subage...
    method linkTaskToolCallToSubagent (line 866) | private linkTaskToolCallToSubagent(msg: ChatMessage, subagent: Subagen...
    method showThinkingIndicator (line 888) | showThinkingIndicator(overrideText?: string, overrideCls?: string): vo...
    method hideThinkingIndicator (line 955) | hideThinkingIndicator(): void {
    method renderCompactBoundary (line 978) | private renderCompactBoundary(): void {
    method notifyVaultFileChange (line 995) | private notifyVaultFileChange(input: Record<string, unknown>): void {
    method scrollToBottom (line 1018) | private scrollToBottom(): void {
    method resetStreamingState (line 1027) | resetStreamingState(): void {

FILE: src/features/chat/controllers/contextRowVisibility.ts
  function updateContextRowHasContent (line 1) | function updateContextRowHasContent(contextRowEl: HTMLElement): void {

FILE: src/features/chat/rendering/DiffRenderer.ts
  type DiffHunk (line 3) | interface DiffHunk {
  function splitIntoHunks (line 9) | function splitIntoHunks(diffLines: DiffLine[], contextLines = 3): DiffHu...
  constant NEW_FILE_DISPLAY_CAP (line 62) | const NEW_FILE_DISPLAY_CAP = 20;
  function renderDiffContent (line 64) | function renderDiffContent(

FILE: src/features/chat/rendering/InlineAskUserQuestion.ts
  constant HINTS_TEXT (line 3) | const HINTS_TEXT = 'Enter to select \u00B7 Tab/Arrow keys to navigate \u...
  constant HINTS_TEXT_IMMEDIATE (line 4) | const HINTS_TEXT_IMMEDIATE = 'Enter to select \u00B7 Arrow keys to navig...
  type InlineAskQuestionConfig (line 6) | interface InlineAskQuestionConfig {
  class InlineAskUserQuestion (line 13) | class InlineAskUserQuestion {
    method constructor (line 37) | constructor(
    method render (line 57) | render(): void {
    method destroy (line 105) | destroy(): void {
    method parseQuestions (line 109) | private parseQuestions(): AskUserQuestionItem[] {
    method coerceOption (line 130) | private coerceOption(opt: unknown): AskUserQuestionOption {
    method deduplicateOptions (line 140) | private deduplicateOptions(options: AskUserQuestionOption[]): AskUserQ...
    method extractLabel (line 149) | private extractLabel(obj: Record<string, unknown>): string {
    method renderTabBar (line 157) | private renderTabBar(): void {
    method isQuestionAnswered (line 183) | private isQuestionAnswered(idx: number): boolean {
    method switchTab (line 187) | private switchTab(index: number): void {
    method renderTabContent (line 200) | private renderTabContent(): void {
    method renderQuestionTab (line 211) | private renderQuestionTab(idx: number): void {
    method renderSubmitTab (line 307) | private renderSubmitTab(): void {
    method getAnswerText (line 368) | private getAnswerText(idx: number): string {
    method selectOption (line 377) | private selectOption(qIdx: number, label: string): void {
    method renderMultiSelectCheckbox (line 410) | private renderMultiSelectCheckbox(parent: HTMLElement, checked: boolea...
    method updateOptionVisuals (line 417) | private updateOptionVisuals(qIdx: number): void {
    method updateFocusIndicator (line 446) | private updateFocusIndicator(): void {
    method updateTabIndicators (line 477) | private updateTabIndicators(): void {
    method handleNavigationKey (line 493) | private handleNavigationKey(e: KeyboardEvent, maxFocusIndex: number): ...
    method handleKeyDown (line 533) | private handleKeyDown(e: KeyboardEvent): void {
    method handleSubmit (line 613) | private handleSubmit(): void {
    method handleResolve (line 624) | private handleResolve(result: Record<string, string> | null): void {

FILE: src/features/chat/rendering/InlineExitPlanMode.ts
  constant HINTS_TEXT (line 6) | const HINTS_TEXT = 'Arrow keys to navigate \u00B7 Enter to select \u00B7...
  class InlineExitPlanMode (line 8) | class InlineExitPlanMode {
    method constructor (line 26) | constructor(
    method render (line 41) | render(): void {
    method destroy (line 132) | destroy(): void {
    method readPlanContent (line 136) | private readPlanContent(): string | null {
    method extractPlanContent (line 157) | private extractPlanContent(): string {
    method handleKeyDown (line 164) | private handleKeyDown(e: KeyboardEvent): void {
    method updateFocus (line 218) | private updateFocus(): void {
    method handleResolve (line 249) | private handleResolve(decision: ExitPlanModeDecision | null): void {

FILE: src/features/chat/rendering/MessageRenderer.ts
  type RenderContentFn (line 20) | type RenderContentFn = (el: HTMLElement, markdown: string) => Promise<vo...
  class MessageRenderer (line 22) | class MessageRenderer {
    method constructor (line 35) | constructor(
    method setMessagesEl (line 54) | setMessagesEl(el: HTMLElement): void {
    method addMessage (line 66) | addMessage(msg: ChatMessage): HTMLElement {
    method renderMessages (line 118) | renderMessages(
    method renderStoredMessage (line 137) | renderStoredMessage(msg: ChatMessage, allMessages?: ChatMessage[], ind...
    method isRewindEligible (line 193) | private isRewindEligible(allMessages?: ChatMessage[], index?: number):...
    method renderInterruptMessage (line 203) | private renderInterruptMessage(): void {
    method renderAssistantContent (line 213) | private renderAssistantContent(msg: ChatMessage, contentEl: HTMLElemen...
    method renderToolCall (line 290) | private renderToolCall(contentEl: HTMLElement, toolCall: ToolCallInfo)...
    method renderTaskSubagent (line 304) | private renderTaskSubagent(
    method resolveTaskSubagent (line 317) | private resolveTaskSubagent(toolCall: ToolCallInfo, modeHint?: 'sync' ...
    method mapToolStatusToSubagentStatus (line 358) | private mapToolStatusToSubagentStatus(
    method inferAsyncStatusFromTaskTool (line 372) | private inferAsyncStatusFromTaskTool(toolCall: ToolCallInfo): 'running...
    method renderMessageImages (line 398) | renderMessageImages(containerEl: HTMLElement, images: ImageAttachment[...
    method showFullImage (line 421) | showFullImage(image: ImageAttachment): void {
    method setImageSrc (line 458) | setImageSrc(imgEl: HTMLImageElement, image: ImageAttachment): void {
    method renderContent (line 470) | async renderContent(el: HTMLElement, markdown: string): Promise<void> {
    method addTextCopyButton (line 545) | addTextCopyButton(textEl: HTMLElement, markdown: string): void {
    method refreshActionButtons (line 579) | refreshActionButtons(msg: ChatMessage, allMessages?: ChatMessage[], in...
    method cleanupLiveMessageEl (line 594) | private cleanupLiveMessageEl(msgId: string, msgEl: HTMLElement): void {
    method getOrCreateActionsToolbar (line 602) | private getOrCreateActionsToolbar(msgEl: HTMLElement): HTMLElement {
    method addUserCopyButton (line 608) | private addUserCopyButton(msgEl: HTMLElement, content: string): void {
    method addRewindButton (line 635) | private addRewindButton(msgEl: HTMLElement, messageId: string): void {
    method addForkButton (line 651) | private addForkButton(msgEl: HTMLElement, messageId: string): void {
    method scrollToBottom (line 672) | scrollToBottom(): void {
    method scrollToBottomIfNeeded (line 677) | scrollToBottomIfNeeded(threshold = 100): void {

FILE: src/features/chat/rendering/SubagentRenderer.ts
  type SubagentToolView (line 14) | interface SubagentToolView {
  type SubagentSection (line 22) | interface SubagentSection {
  type SubagentState (line 27) | interface SubagentState {
  constant SUBAGENT_TOOL_STATUS_ICONS (line 43) | const SUBAGENT_TOOL_STATUS_ICONS: Partial<Record<ToolCallInfo['status'],...
  function extractTaskDescription (line 49) | function extractTaskDescription(input: Record<string, unknown>): string {
  function extractTaskPrompt (line 53) | function extractTaskPrompt(input: Record<string, unknown>): string {
  function truncateDescription (line 57) | function truncateDescription(description: string, maxLength = 40): string {
  function createSection (line 62) | function createSection(parentEl: HTMLElement, title: string, bodyClass?:...
  function setPromptText (line 83) | function setPromptText(promptBodyEl: HTMLElement, prompt: string): void {
  function updateSyncHeaderAria (line 89) | function updateSyncHeaderAria(state: SubagentState): void {
  function renderSubagentToolContent (line 98) | function renderSubagentToolContent(contentEl: HTMLElement, toolCall: Too...
  function setSubagentToolStatus (line 110) | function setSubagentToolStatus(view: SubagentToolView, status: ToolCallI...
  function updateSubagentToolView (line 122) | function updateSubagentToolView(view: SubagentToolView, toolCall: ToolCa...
  function createSubagentToolView (line 130) | function createSubagentToolView(parentEl: HTMLElement, toolCall: ToolCal...
  function ensureResultSection (line 171) | function ensureResultSection(state: SubagentState): SubagentSection {
  function setResultText (line 183) | function setResultText(state: SubagentState, text: string): void {
  function hydrateSyncSubagentStateFromStored (line 190) | function hydrateSyncSubagentStateFromStored(state: SubagentState, subage...
  function createSubagentBlock (line 221) | function createSubagentBlock(
  function addSubagentToolCall (line 288) | function addSubagentToolCall(
  function updateSubagentToolResult (line 303) | function updateSubagentToolResult(
  function finalizeSubagentBlock (line 321) | function finalizeSubagentBlock(
  function renderStoredSubagent (line 351) | function renderStoredSubagent(
  type AsyncSubagentState (line 364) | interface AsyncSubagentState {
  function setAsyncWrapperStatus (line 374) | function setAsyncWrapperStatus(wrapperEl: HTMLElement, status: string): ...
  function getAsyncDisplayStatus (line 381) | function getAsyncDisplayStatus(asyncStatus: string | undefined): 'runnin...
  function getAsyncStatusText (line 390) | function getAsyncStatusText(asyncStatus: string | undefined): string {
  function getAsyncStatusAriaLabel (line 400) | function getAsyncStatusAriaLabel(asyncStatus: string | undefined): string {
  function updateAsyncLabel (line 410) | function updateAsyncLabel(state: AsyncSubagentState): void {
  function renderAsyncContentLikeSync (line 420) | function renderAsyncContentLikeSync(
  function createAsyncSubagentBlock (line 462) | function createAsyncSubagentBlock(
  function updateAsyncSubagentRunning (line 520) | function updateAsyncSubagentRunning(
  function finalizeAsyncSubagent (line 535) | function finalizeAsyncSubagent(
  function markAsyncSubagentOrphaned (line 567) | function markAsyncSubagentOrphaned(state: AsyncSubagentState): void {
  function renderStoredAsyncSubagent (line 591) | function renderStoredAsyncSubagent(

FILE: src/features/chat/rendering/ThinkingBlockRenderer.ts
  type RenderContentFn (line 3) | type RenderContentFn = (el: HTMLElement, markdown: string) => Promise<vo...
  type ThinkingBlockState (line 5) | interface ThinkingBlockState {
  function createThinkingBlock (line 15) | function createThinkingBlock(
  function appendThinkingContent (line 59) | async function appendThinkingContent(
  function finalizeThinkingBlock (line 68) | function finalizeThinkingBlock(state: ThinkingBlockState): number {
  function cleanupThinkingBlock (line 90) | function cleanupThinkingBlock(state: ThinkingBlockState | null) {
  function renderStoredThinkingBlock (line 96) | function renderStoredThinkingBlock(

FILE: src/features/chat/rendering/ToolCallRenderer.ts
  function setToolIcon (line 27) | function setToolIcon(el: HTMLElement, name: string): void {
  function getToolName (line 36) | function getToolName(name: string, input: Record<string, unknown>): stri...
  function getToolSummary (line 55) | function getToolSummary(name: string, input: Record<string, unknown>): s...
  function getToolLabel (line 88) | function getToolLabel(name: string, input: Record<string, unknown>): str...
  function fileNameOnly (line 139) | function fileNameOnly(filePath: string): string {
  function shortenPath (line 145) | function shortenPath(filePath: string | undefined): string {
  function truncateText (line 153) | function truncateText(text: string, maxLength: number): string {
  function parseToolSearchQuery (line 158) | function parseToolSearchQuery(query: string | undefined): string {
  type WebSearchLink (line 165) | interface WebSearchLink {
  function parseWebSearchResult (line 170) | function parseWebSearchResult(result: string): { links: WebSearchLink[];...
  function renderWebSearchExpanded (line 186) | function renderWebSearchExpanded(container: HTMLElement, result: string)...
  function renderFileSearchExpanded (line 212) | function renderFileSearchExpanded(container: HTMLElement, result: string...
  function renderLinesExpanded (line 221) | function renderLinesExpanded(
  function renderToolSearchExpanded (line 247) | function renderToolSearchExpanded(container: HTMLElement, result: string...
  function renderWebFetchExpanded (line 273) | function renderWebFetchExpanded(container: HTMLElement, result: string):...
  function renderExpandedContent (line 291) | function renderExpandedContent(container: HTMLElement, toolName: string,...
  function getTodos (line 324) | function getTodos(input: Record<string, unknown>): TodoItem[] | undefined {
  function getCurrentTask (line 330) | function getCurrentTask(input: Record<string, unknown>): TodoItem | unde...
  function areAllTodosCompleted (line 336) | function areAllTodosCompleted(input: Record<string, unknown>): boolean {
  function resetStatusElement (line 342) | function resetStatusElement(statusEl: HTMLElement, statusClass: string, ...
  constant STATUS_ICONS (line 349) | const STATUS_ICONS: Record<string, string> = {
  function setTodoWriteStatus (line 355) | function setTodoWriteStatus(statusEl: HTMLElement, input: Record<string,...
  function setToolStatus (line 363) | function setToolStatus(statusEl: HTMLElement, status: ToolCallInfo['stat...
  function renderTodoWriteResult (line 369) | function renderTodoWriteResult(
  function isBlockedToolResult (line 387) | function isBlockedToolResult(content: string, isError?: boolean): boolean {
  type ToolElementStructure (line 398) | interface ToolElementStructure {
  function createToolElementStructure (line 409) | function createToolElementStructure(
  function formatAnswer (line 440) | function formatAnswer(raw: unknown): string {
  function resolveAskUserAnswers (line 446) | function resolveAskUserAnswers(toolCall: ToolCallInfo): Record<string, u...
  function renderAskUserQuestionResult (line 458) | function renderAskUserQuestionResult(container: HTMLElement, toolCall: T...
  function renderAskUserQuestionFallback (line 481) | function renderAskUserQuestionFallback(container: HTMLElement, toolCall:...
  function contentFallback (line 485) | function contentFallback(container: HTMLElement, text: string): void {
  function createCurrentTaskPreview (line 491) | function createCurrentTaskPreview(
  function createTodoToggleHandler (line 503) | function createTodoToggleHandler(
  function renderToolContent (line 519) | function renderToolContent(
  function renderToolCall (line 541) | function renderToolCall(
  function updateToolCallResult (line 571) | function updateToolCallResult(
  function renderStoredToolCall (line 624) | function renderStoredToolCall(

FILE: src/features/chat/rendering/WriteEditRenderer.ts
  type WriteEditState (line 10) | interface WriteEditState {
  function shortenPath (line 23) | function shortenPath(filePath: string, maxLength = 40): string {
  function renderDiffStats (line 46) | function renderDiffStats(statsEl: HTMLElement, stats: DiffStats): void {
  function createWriteEditBlock (line 60) | function createWriteEditBlock(
  function updateWriteEditWithDiff (line 119) | function updateWriteEditWithDiff(state: WriteEditState, diffData: ToolDi...
  function finalizeWriteEditBlock (line 135) | function finalizeWriteEditBlock(state: WriteEditState, isError: boolean)...
  function renderStoredWriteEdit (line 168) | function renderStoredWriteEdit(parentEl: HTMLElement, toolCall: ToolCall...

FILE: src/features/chat/rendering/collapsible.ts
  type CollapsibleState (line 1) | interface CollapsibleState {
  type CollapsibleOptions (line 5) | interface CollapsibleOptions {
  function setupCollapsible (line 30) | function setupCollapsible(
  function collapseElement (line 91) | function collapseElement(

FILE: src/features/chat/rendering/todoUtils.ts
  function getTodoStatusIcon (line 5) | function getTodoStatusIcon(status: TodoItem['status']): string {
  function getTodoDisplayText (line 9) | function getTodoDisplayText(todo: TodoItem): string {
  function renderTodoItems (line 13) | function renderTodoItems(

FILE: src/features/chat/rewind.ts
  type RewindContext (line 3) | interface RewindContext {
  function findRewindContext (line 12) | function findRewindContext(messages: ChatMessage[], userIndex: number): ...

FILE: src/features/chat/services/BangBashService.ts
  type BangBashResult (line 3) | interface BangBashResult {
  constant TIMEOUT_MS (line 11) | const TIMEOUT_MS = 30_000;
  constant MAX_BUFFER (line 12) | const MAX_BUFFER = 1024 * 1024;
  class BangBashService (line 14) | class BangBashService {
    method constructor (line 18) | constructor(cwd: string, enhancedPath: string) {
    method execute (line 23) | execute(command: string): Promise<BangBashResult> {

FILE: src/features/chat/services/InstructionRefineService.ts
  type RefineProgressCallback (line 10) | type RefineProgressCallback = (update: InstructionRefineResult) => void;
  class InstructionRefineService (line 12) | class InstructionRefineService {
    method constructor (line 18) | constructor(plugin: ClaudianPlugin) {
    method resetConversation (line 23) | resetConversation(): void {
    method refineInstruction (line 28) | async refineInstruction(
    method continueConversation (line 40) | async continueConversation(
    method cancel (line 51) | cancel(): void {
    method sendMessage (line 58) | private async sendMessage(
    method parseResponse (line 150) | private parseResponse(responseText: string): InstructionRefineResult {
    method extractTextFromMessage (line 166) | private extractTextFromMessage(message: { type: string; message?: { co...

FILE: src/features/chat/services/SubagentManager.ts
  type SubagentStateChangeCallback (line 31) | type SubagentStateChangeCallback = (subagent: SubagentInfo) => void;
  type HandleTaskResult (line 33) | type HandleTaskResult =
  type RenderPendingResult (line 39) | type RenderPendingResult =
  class SubagentManager (line 43) | class SubagentManager {
    method constructor (line 59) | constructor(onStateChange: SubagentStateChangeCallback) {
    method setCallback (line 63) | public setCallback(callback: SubagentStateChangeCallback): void {
    method handleTaskToolUse (line 75) | public handleTaskToolUse(
    method hasPendingTask (line 161) | public hasPendingTask(toolId: string): boolean {
    method renderPendingTask (line 170) | public renderPendingTask(
    method renderPendingTaskFromTaskResult (line 209) | public renderPendingTaskFromTaskResult(
    method getSyncSubagent (line 254) | public getSyncSubagent(toolId: string): SubagentState | undefined {
    method addSyncToolCall (line 258) | public addSyncToolCall(parentToolUseId: string, toolCall: ToolCallInfo...
    method updateSyncToolResult (line 264) | public updateSyncToolResult(
    method finalizeSyncSubagent (line 274) | public finalizeSyncSubagent(
    method handleTaskToolResult (line 294) | public handleTaskToolResult(
    method handleAgentOutputToolUse (line 328) | public handleAgentOutputToolUse(toolCall: ToolCallInfo): void {
    method handleAgentOutputToolResult (line 339) | public handleAgentOutputToolResult(
    method isPendingAsyncTask (line 397) | public isPendingAsyncTask(taskToolId: string): boolean {
    method isLinkedAgentOutputTool (line 401) | public isLinkedAgentOutputTool(toolId: string): boolean {
    method getByTaskId (line 405) | public getByTaskId(taskToolId: string): SubagentInfo | undefined {
    method refreshAsyncSubagent (line 421) | public refreshAsyncSubagent(subagent: SubagentInfo): void {
    method hasRunningSubagents (line 430) | public hasRunningSubagents(): boolean {
    method subagentsSpawnedThisStream (line 439) | public get subagentsSpawnedThisStream(): number {
    method resetSpawnedCount (line 443) | public resetSpawnedCount(): void {
    method resetStreamingState (line 447) | public resetStreamingState(): void {
    method orphanAllActive (line 452) | public orphanAllActive(): SubagentInfo[] {
    method clear (line 475) | public clear(): void {
    method markOrphaned (line 489) | private markOrphaned(subagent: SubagentInfo): void {
    method transitionToError (line 498) | private transitionToError(subagent: SubagentInfo, taskToolId: string, ...
    method createSyncTask (line 512) | private createSyncTask(
    method createAsyncTask (line 522) | private createAsyncTask(
    method updateSubagentLabel (line 553) | private updateSubagentLabel(
    method resolveTaskMode (line 578) | private resolveTaskMode(taskInput: Record<string, unknown>): 'sync' | ...
    method inferModeFromTaskResult (line 591) | private inferModeFromTaskResult(
    method parseAgentIdStrict (line 606) | private parseAgentIdStrict(result: string): string | null {
    method extractAgentIdFromString (line 637) | private extractAgentIdFromString(value: string): string | null {
    method hasAsyncMarkerInToolUseResult (line 655) | private hasAsyncMarkerInToolUseResult(taskToolUseResult?: unknown): bo...
    method updateAsyncDomState (line 711) | private updateAsyncDomState(subagent: SubagentInfo): void {
    method isStillRunningResult (line 747) | private isStillRunningResult(result: string, isError: boolean): boolean {
    method extractAgentResult (line 798) | private extractAgentResult(result: string, agentId: string, toolUseRes...
    method extractResultFromToolUseResult (line 865) | private extractResultFromToolUseResult(toolUseResult: unknown): string...
    method extractResultFromTaskObject (line 895) | private extractResultFromTaskObject(task: unknown): string | null {
    method extractResultFromCandidateString (line 904) | private extractResultFromCandidateString(candidate: unknown): string |...
    method parseAgentId (line 927) | private parseAgentId(result: string): string | null {
    method extractAgentIdFromTaskToolUseResult (line 965) | private extractAgentIdFromTaskToolUseResult(toolUseResult: unknown): s...
    method inferAgentIdFromResult (line 998) | private inferAgentIdFromResult(result: string): string | null {
    method unwrapTextPayload (line 1013) | private unwrapTextPayload(raw: string): string {
    method extractResultFromTaggedPayload (line 1028) | private extractResultFromTaggedPayload(payload: string): string | null {
    method extractResultFromOutputJsonl (line 1045) | private extractResultFromOutputJsonl(outputContent: string): string | ...
    method extractFullOutputPath (line 1064) | private extractFullOutputPath(content: string): string | null {
    method readFullOutputFile (line 1075) | private readFullOutputFile(fullOutputPath: string): string | null {
    method extractAgentIdFromInput (line 1093) | private extractAgentIdFromInput(input: Record<string, unknown>): strin...
    method resolveTrustedTmpRoots (line 1098) | private static resolveTrustedTmpRoots(): string[] {
    method isTrustedOutputPath (line 1111) | private isTrustedOutputPath(fullOutputPath: string): boolean {

FILE: src/features/chat/services/TitleGenerationService.ts
  type TitleGenerationResult (line 9) | type TitleGenerationResult =
  type TitleGenerationCallback (line 13) | type TitleGenerationCallback = (
  class TitleGenerationService (line 18) | class TitleGenerationService {
    method constructor (line 22) | constructor(plugin: ClaudianPlugin) {
    method generateTitle (line 30) | async generateTitle(
    method cancel (line 153) | cancel(): void {
    method truncateText (line 161) | private truncateText(text: string, maxLength: number): string {
    method extractTextFromMessage (line 167) | private extractTextFromMessage(
    method parseTitle (line 183) | private parseTitle(responseText: string): string | null {
    method safeCallback (line 208) | private async safeCallback(

FILE: src/features/chat/state/ChatState.ts
  function createInitialState (line 14) | function createInitialState(): ChatStateData {
  class ChatState (line 47) | class ChatState {
    method constructor (line 51) | constructor(callbacks: ChatStateCallbacks = {}) {
    method callbacks (line 56) | get callbacks(): ChatStateCallbacks {
    method callbacks (line 60) | set callbacks(value: ChatStateCallbacks) {
    method messages (line 68) | get messages(): ChatMessage[] {
    method messages (line 72) | set messages(value: ChatMessage[]) {
    method addMessage (line 77) | addMessage(msg: ChatMessage): void {
    method clearMessages (line 82) | clearMessages(): void {
    method truncateAt (line 87) | truncateAt(messageId: string): number {
    method isStreaming (line 100) | get isStreaming(): boolean {
    method isStreaming (line 104) | set isStreaming(value: boolean) {
    method cancelRequested (line 109) | get cancelRequested(): boolean {
    method cancelRequested (line 113) | set cancelRequested(value: boolean) {
    method streamGeneration (line 117) | get streamGeneration(): number {
    method bumpStreamGeneration (line 121) | bumpStreamGeneration(): number {
    method isCreatingConversation (line 126) | get isCreatingConversation(): boolean {
    method isCreatingConversation (line 130) | set isCreatingConversation(value: boolean) {
    method isSwitchingConversation (line 134) | get isSwitchingConversation(): boolean {
    method isSwitchingConversation (line 138) | set isSwitchingConversation(value: boolean) {
    method currentConversationId (line 146) | get currentConversationId(): string | null {
    method currentConversationId (line 150) | set currentConversationId(value: string | null) {
    method queuedMessage (line 159) | get queuedMessage(): QueuedMessage | null {
    method queuedMessage (line 163) | set queuedMessage(value: QueuedMessage | null) {
    method currentContentEl (line 171) | get currentContentEl(): HTMLElement | null {
    method currentContentEl (line 175) | set currentContentEl(value: HTMLElement | null) {
    method currentTextEl (line 179) | get currentTextEl(): HTMLElement | null {
    method currentTextEl (line 183) | set currentTextEl(value: HTMLElement | null) {
    method currentTextContent (line 187) | get currentTextContent(): string {
    method currentTextContent (line 191) | set currentTextContent(value: string) {
    method currentThinkingState (line 195) | get currentThinkingState(): ThinkingBlockState | null {
    method currentThinkingState (line 199) | set currentThinkingState(value: ThinkingBlockState | null) {
    method thinkingEl (line 203) | get thinkingEl(): HTMLElement | null {
    method thinkingEl (line 207) | set thinkingEl(value: HTMLElement | null) {
    method queueIndicatorEl (line 211) | get queueIndicatorEl(): HTMLElement | null {
    method queueIndicatorEl (line 215) | set queueIndicatorEl(value: HTMLElement | null) {
    method thinkingIndicatorTimeout (line 219) | get thinkingIndicatorTimeout(): ReturnType<typeof setTimeout> | null {
    method thinkingIndicatorTimeout (line 223) | set thinkingIndicatorTimeout(value: ReturnType<typeof setTimeout> | nu...
    method toolCallElements (line 231) | get toolCallElements(): Map<string, HTMLElement> {
    method writeEditStates (line 235) | get writeEditStates(): Map<string, WriteEditState> {
    method pendingTools (line 239) | get pendingTools(): Map<string, PendingToolCall> {
    method usage (line 247) | get usage(): UsageInfo | null {
    method usage (line 251) | set usage(value: UsageInfo | null) {
    method ignoreUsageUpdates (line 256) | get ignoreUsageUpdates(): boolean {
    method ignoreUsageUpdates (line 260) | set ignoreUsageUpdates(value: boolean) {
    method currentTodos (line 268) | get currentTodos(): TodoItem[] | null {
    method currentTodos (line 272) | set currentTodos(value: TodoItem[] | null) {
    method needsAttention (line 283) | get needsAttention(): boolean {
    method needsAttention (line 287) | set needsAttention(value: boolean) {
    method autoScrollEnabled (line 296) | get autoScrollEnabled(): boolean {
    method autoScrollEnabled (line 300) | set autoScrollEnabled(value: boolean) {
    method responseStartTime (line 312) | get responseStartTime(): number | null {
    method responseStartTime (line 316) | set responseStartTime(value: number | null) {
    method flavorTimerInterval (line 320) | get flavorTimerInterval(): ReturnType<typeof setInterval> | null {
    method flavorTimerInterval (line 324) | set flavorTimerInterval(value: ReturnType<typeof setInterval> | null) {
    method pendingNewSessionPlan (line 328) | get pendingNewSessionPlan(): string | null {
    method pendingNewSessionPlan (line 332) | set pendingNewSessionPlan(value: string | null) {
    method planFilePath (line 336) | get planFilePath(): string | null {
    method planFilePath (line 340) | set planFilePath(value: string | null) {
    method prePlanPermissionMode (line 344) | get prePlanPermissionMode(): PermissionMode | null {
    method prePlanPermissionMode (line 348) | set prePlanPermissionMode(value: PermissionMode | null) {
    method clearFlavorTimerInterval (line 356) | clearFlavorTimerInterval(): void {
    method resetStreamingState (line 363) | resetStreamingState(): void {
    method clearMaps (line 380) | clearMaps(): void {
    method resetForNewConversation (line 386) | resetForNewConversation(): void {
    method getPersistedMessages (line 396) | getPersistedMessages(): ChatMessage[] {

FILE: src/features/chat/state/types.ts
  type QueuedMessage (line 21) | interface QueuedMessage {
  type PendingToolCall (line 30) | interface PendingToolCall {
  type StoredSelection (line 36) | interface StoredSelection {
  type ChatStateData (line 47) | interface ChatStateData {
  type ChatStateCallbacks (line 111) | interface ChatStateCallbacks {
  type QueryOptions (line 122) | interface QueryOptions {

FILE: src/features/chat/tabs/Tab.ts
  type TabCreateOptions (line 41) | interface TabCreateOptions {
  function createTab (line 57) | function createTab(options: TabCreateOptions): TabData {
  function autoResizeTextarea (line 147) | function autoResizeTextarea(textarea: HTMLTextAreaElement): void {
  function buildTabDOM (line 174) | function buildTabDOM(contentEl: HTMLElement): TabDOMElements {
  function initializeTabService (line 238) | async function initializeTabService(
  function initializeContextManagers (line 303) | function initializeContextManagers(tab: TabData, plugin: ClaudianPlugin)...
  function initializeSlashCommands (line 350) | function initializeSlashCommands(
  function initializeInstructionAndTodo (line 374) | function initializeInstructionAndTodo(tab: TabData, plugin: ClaudianPlug...
  function initializeInputToolbar (line 424) | function initializeInputToolbar(tab: TabData, plugin: ClaudianPlugin): v...
  type InitializeTabUIOptions (line 516) | interface InitializeTabUIOptions {
  function initializeTabUI (line 524) | function initializeTabUI(
  type ForkContext (line 583) | interface ForkContext {
  function deepCloneMessages (line 593) | function deepCloneMessages(messages: ChatMessage[]): ChatMessage[] {
  function countUserMessagesForForkTitle (line 601) | function countUserMessagesForForkTitle(messages: ChatMessage[]): number {
  type ForkSource (line 606) | interface ForkSource {
  function resolveForkSource (line 617) | function resolveForkSource(tab: TabData, plugin: ClaudianPlugin): ForkSo...
  function handleForkRequest (line 641) | async function handleForkRequest(
  function handleForkAll (line 685) | async function handleForkAll(
  function initializeTabControllers (line 729) | function initializeTabControllers(
  function wireTabInputEvents (line 903) | function wireTabInputEvents(tab: TabData, plugin: ClaudianPlugin): void {
  function activateTab (line 1041) | function activateTab(tab: TabData): void {
  function deactivateTab (line 1053) | function deactivateTab(tab: TabData): void {
  function destroyTab (line 1064) | async function destroyTab(tab: TabData): Promise<void> {
  function getTabTitle (line 1123) | function getTabTitle(tab: TabData, plugin: ClaudianPlugin): string {
  function setupServiceCallbacks (line 1134) | function setupServiceCallbacks(tab: TabData, plugin: ClaudianPlugin): vo...
  function generateMessageId (line 1193) | function generateMessageId(): string {
  function renderAutoTriggeredTurn (line 1201) | function renderAutoTriggeredTurn(tab: TabData, chunks: StreamChunk[]): v...
  function updatePlanModeUI (line 1238) | function updatePlanModeUI(tab: TabData, plugin: ClaudianPlugin, mode: Pe...

FILE: src/features/chat/tabs/TabBar.ts
  type TabBarCallbacks (line 4) | interface TabBarCallbacks {
  class TabBar (line 18) | class TabBar {
    method constructor (line 22) | constructor(containerEl: HTMLElement, callbacks: TabBarCallbacks) {
    method build (line 29) | private build(): void {
    method update (line 37) | update(items: TabBarItem[]): void {
    method renderBadge (line 48) | private renderBadge(item: TabBarItem): void {
    method destroy (line 83) | destroy(): void {

FILE: src/features/chat/tabs/TabManager.ts
  class TabManager (line 39) | class TabManager implements TabManagerInterface {
    method getMaxTabs (line 56) | private getMaxTabs(): number {
    method constructor (line 61) | constructor(
    method createTab (line 85) | async createTab(conversationId?: string | null, tabId?: TabId): Promis...
    method switchToTab (line 148) | async switchToTab(tabId: TabId): Promise<void> {
    method closeTab (line 211) | async closeTab(tabId: TabId, force = false): Promise<boolean> {
    method getActiveTab (line 276) | getActiveTab(): TabData | null {
    method getActiveTabId (line 281) | getActiveTabId(): TabId | null {
    method getTab (line 286) | getTab(tabId: TabId): TabData | null {
    method getAllTabs (line 291) | getAllTabs(): TabData[] {
    method getTabCount (line 296) | getTabCount(): number {
    method canCreateTab (line 301) | canCreateTab(): boolean {
    method getTabBarItems (line 310) | getTabBarItems(): TabBarItem[] {
    method openConversation (line 338) | async openConversation(conversationId: string, preferNewTab = false): ...
    method createNewConversation (line 376) | async createNewConversation(): Promise<void> {
    method handleForkRequest (line 389) | private async handleForkRequest(context: ForkContext): Promise<void> {
    method forkToNewTab (line 411) | async forkToNewTab(context: ForkContext): Promise<TabData | null> {
    method forkInCurrentTab (line 426) | async forkInCurrentTab(context: ForkContext): Promise<boolean> {
    method createForkConversation (line 440) | private async createForkConversation(context: ForkContext): Promise<st...
    method buildForkTitle (line 460) | private buildForkTitle(sourceTitle: string, forkAtUserMessage?: number...
    method getPersistedState (line 485) | getPersistedState(): PersistedTabManagerState {
    method restoreState (line 502) | async restoreState(state: PersistedTabManagerState): Promise<void> {
    method initializeActiveTabService (line 535) | private async initializeActiveTabService(): Promise<void> {
    method getSdkCommands (line 559) | async getSdkCommands(): Promise<SlashCommand[]> {
    method broadcastToAllTabs (line 578) | async broadcastToAllTabs(fn: (service: ClaudianService) => Promise<voi...
    method destroy (line 599) | async destroy(): Promise<void> {

FILE: src/features/chat/tabs/types.ts
  constant DEFAULT_MAX_TABS (line 42) | const DEFAULT_MAX_TABS = 3;
  constant MIN_TABS (line 47) | const MIN_TABS = 3;
  constant MAX_TABS (line 53) | const MAX_TABS = 10;
  constant TEXTAREA_MIN_MAX_HEIGHT (line 59) | const TEXTAREA_MIN_MAX_HEIGHT = 150;
  constant TEXTAREA_MAX_HEIGHT_PERCENT (line 65) | const TEXTAREA_MAX_HEIGHT_PERCENT = 0.55;
  type TabManagerViewHost (line 72) | interface TabManagerViewHost extends Component {
  type TabManagerInterface (line 84) | interface TabManagerInterface {
  type TabId (line 93) | type TabId = string;
  function generateTabId (line 96) | function generateTabId(): TabId {
  type TabControllers (line 104) | interface TabControllers {
  type TabServices (line 117) | interface TabServices {
  type TabUIComponents (line 126) | interface TabUIComponents {
  type TabDOMElements (line 145) | interface TabDOMElements {
  type TabData (line 175) | interface TabData {
  type PersistedTabState (line 210) | interface PersistedTabState {
  type PersistedTabManagerState (line 218) | interface PersistedTabManagerState {
  type TabManagerCallbacks (line 226) | interface TabManagerCallbacks {
  type TabBarItem (line 252) | interface TabBarItem {

FILE: src/features/chat/ui/BangBashModeManager.ts
  type BangBashModeCallbacks (line 5) | interface BangBashModeCallbacks {
  type BangBashModeState (line 11) | interface BangBashModeState {
  class BangBashModeManager (line 16) | class BangBashModeManager {
    method constructor (line 23) | constructor(
    method handleTriggerKey (line 32) | handleTriggerKey(e: KeyboardEvent): boolean {
    method handleInputChange (line 42) | handleInputChange(): void {
    method enterMode (line 47) | private enterMode(): boolean {
    method exitMode (line 57) | private exitMode(): void {
    method handleKeydown (line 66) | handleKeydown(e: KeyboardEvent): boolean {
    method isActive (line 86) | isActive(): boolean {
    method getRawCommand (line 90) | getRawCommand(): string {
    method submit (line 94) | private async submit(): Promise<void> {
    method clear (line 112) | clear(): void {
    method destroy (line 118) | destroy(): void {

FILE: src/features/chat/ui/FileContext.ts
  type FileContextCallbacks (line 19) | interface FileContextCallbacks {
  class FileContextManager (line 27) | class FileContextManager {
    method constructor (line 46) | constructor(
    method getCurrentNotePath (line 112) | getCurrentNotePath(): string | null {
    method getAttachedFiles (line 116) | getAttachedFiles(): Set<string> {
    method shouldSendCurrentNote (line 121) | shouldSendCurrentNote(notePath?: string | null): boolean {
    method markCurrentNoteSent (line 127) | markCurrentNoteSent() {
    method isSessionStarted (line 131) | isSessionStarted(): boolean {
    method startSession (line 135) | startSession() {
    method resetForNewConversation (line 140) | resetForNewConversation() {
    method resetForLoadedConversation (line 147) | resetForLoadedConversation(hasMessages: boolean) {
    method setCurrentNote (line 154) | setCurrentNote(notePath: string | null) {
    method autoAttachActiveFile (line 163) | autoAttachActiveFile() {
    method handleFileOpen (line 176) | handleFileOpen(file: TFile) {
    method markFileCacheDirty (line 192) | markFileCacheDirty() {
    method markFolderCacheDirty (line 196) | markFolderCacheDirty() {
    method handleInputChange (line 201) | handleInputChange() {
    method handleMentionKeydown (line 206) | handleMentionKeydown(e: KeyboardEvent): boolean {
    method isMentionDropdownVisible (line 210) | isMentionDropdownVisible(): boolean {
    method hideMentionDropdown (line 214) | hideMentionDropdown() {
    method containsElement (line 218) | containsElement(el: Node): boolean {
    method transformContextMentions (line 222) | transformContextMentions(text: string): string {
    method destroy (line 255) | destroy() {
    method normalizePathForVault (line 263) | normalizePathForVault(rawPath: string | undefined | null): string | nu...
    method refreshCurrentNoteChip (line 268) | private refreshCurrentNoteChip(): void {
    method handleFileRenamed (line 273) | private handleFileRenamed(oldPath: string, newPath: string) {
    method handleFileDeleted (line 300) | private handleFileDeleted(deletedPath: string): void {
    method setMcpManager (line 327) | setMcpManager(manager: McpServerManager | null): void {
    method setAgentService (line 331) | setAgentService(agentManager: AgentManager | null): void {
    method setOnMcpMentionChange (line 336) | setOnMcpMentionChange(callback: (servers: Set<string>) => void): void {
    method preScanExternalContexts (line 344) | preScanExternalContexts(): void {
    method getMentionedMcpServers (line 348) | getMentionedMcpServers(): Set<string> {
    method clearMcpMentions (line 352) | clearMcpMentions(): void {
    method updateMcpMentionsFromText (line 356) | updateMcpMentionsFromText(text: string): void {
    method hasExcludedTag (line 360) | private hasExcludedTag(file: TFile): boolean {

FILE: src/features/chat/ui/ImageContext.ts
  constant MAX_IMAGE_SIZE (line 6) | const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
  constant IMAGE_EXTENSIONS (line 8) | const IMAGE_EXTENSIONS: Record<string, ImageMediaType> = {
  type ImageContextCallbacks (line 16) | interface ImageContextCallbacks {
  class ImageContextManager (line 20) | class ImageContextManager {
    method constructor (line 29) | constructor(
    method getAttachedImages (line 51) | getAttachedImages(): ImageAttachment[] {
    method hasImages (line 55) | hasImages(): boolean {
    method clearImages (line 59) | clearImages() {
    method setImages (line 66) | setImages(images: ImageAttachment[]) {
    method setupDragAndDrop (line 75) | private setupDragAndDrop() {
    method handleDragEnter (line 111) | private handleDragEnter(e: DragEvent) {
    method handleDragOver (line 120) | private handleDragOver(e: DragEvent) {
    method handleDragLeave (line 125) | private handleDragLeave(e: DragEvent) {
    method handleDrop (line 146) | private async handleDrop(e: DragEvent) {
    method setupPasteHandler (line 162) | private setupPasteHandler() {
    method isImageFile (line 181) | private isImageFile(file: File): boolean {
    method getMediaType (line 185) | private getMediaType(filename: string): ImageMediaType | null {
    method addImageFromFile (line 190) | private async addImageFromFile(file: File, source: 'paste' | 'drop'): ...
    method fileToBase64 (line 224) | private async fileToBase64(file: File): Promise<string> {
    method updateImagePreview (line 234) | private updateImagePreview() {
    method renderImagePreview (line 249) | private renderImagePreview(id: string, image: ImageAttachment) {
    method showFullImage (line 284) | private showFullImage(image: ImageAttachment) {
    method generateId (line 316) | private generateId(): string {
    method truncateName (line 320) | private truncateName(name: string, maxLen: number): string {
    method formatSize (line 328) | private formatSize(bytes: number): string {
    method notifyImageError (line 334) | private notifyImageError(message: string, error?: unknown) {

FILE: src/features/chat/ui/InputToolbar.ts
  type ToolbarSettings (line 25) | interface ToolbarSettings {
  type ToolbarCallbacks (line 34) | interface ToolbarCallbacks {
  class ModelSelector (line 43) | class ModelSelector {
    method constructor (line 50) | constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
    method getAvailableModels (line 56) | private getAvailableModels() {
    method render (line 72) | private render() {
    method updateDisplay (line 83) | updateDisplay() {
    method setReady (line 97) | setReady(ready: boolean) {
    method renderOptions (line 102) | renderOptions() {
  class ThinkingBudgetSelector (line 130) | class ThinkingBudgetSelector {
    method constructor (line 138) | constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
    method render (line 144) | private render() {
    method renderEffortGears (line 162) | private renderEffortGears() {
    method renderBudgetGears (line 190) | private renderBudgetGears() {
    method updateDisplay (line 219) | updateDisplay() {
  class PermissionToggle (line 238) | class PermissionToggle {
    method constructor (line 244) | constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
    method render (line 250) | private render() {
    method updateDisplay (line 261) | updateDisplay() {
    method toggle (line 283) | private async toggle() {
  type AddExternalContextResult (line 291) | type AddExternalContextResult =
  class ExternalContextSelector (line 295) | class ExternalContextSelector {
    method constructor (line 313) | constructor(parentEl: HTMLElement, callbacks: ToolbarCallbacks) {
    method setOnChange (line 319) | setOnChange(callback: (paths: string[]) => void): void {
    method setOnPersistenceChange (line 323) | setOnPersistenceChange(callback: (paths: string[]) => void): void {
    method getExternalContexts (line 327) | getExternalContexts(): string[] {
    method getPersistentPaths (line 331) | getPersistentPaths(): string[] {
    method setPersistentPaths (line 335) | setPersistentPaths(paths: string[]): void {
    method togglePersistence (line 354) | togglePersistence(path: string): void {
    method mergePersistentPaths (line 369) | private mergePersistentPaths(): void {
    method setExternalContexts (line 382) | setExternalContexts(paths: string[]): void {
    method removePath (line 392) | removePath(pathStr: string): void {
    method addExternalContext (line 410) | addExternalContext(pathInput: string): AddExternalContextResult {
    method clearExternalContexts (line 462) | clearExternalContexts(persistentPathsFromSettings?: string[]): void {
    method render (line 474) | private render() {
    method openFolderPicker (line 496) | private async openFolderPicker() {
    method formatConflictMessage (line 533) | private formatConflictMessage(newPath: string, conflict: { path: strin...
    method renderDropdown (line 541) | private renderDropdown() {
    method shortenPath (line 591) | private shortenPath(fullPath: string): string {
    method updateDisplay (line 616) | updateDisplay() {
  class McpServerSelector (line 640) | class McpServerSelector {
    method constructor (line 649) | constructor(parentEl: HTMLElement) {
    method setMcpManager (line 654) | setMcpManager(manager: McpServerManager | null): void {
    method setOnChange (line 661) | setOnChange(callback: (enabled: Set<string>) => void): void {
    method getEnabledServers (line 665) | getEnabledServers(): Set<string> {
    method addMentionedServers (line 669) | addMentionedServers(names: Set<string>): void {
    method clearEnabled (line 683) | clearEnabled(): void {
    method setEnabledServers (line 689) | setEnabledServers(names: string[]): void {
    method pruneEnabledServers (line 696) | private pruneEnabledServers(): void {
    method render (line 711) | private render() {
    method renderDropdown (line 732) | private renderDropdown() {
    method renderServerItem (line 758) | private renderServerItem(listEl: HTMLElement, server: ClaudianMcpServe...
    method toggleServer (line 794) | private toggleServer(name: string, itemEl: HTMLElement) {
    method updateDisplay (line 817) | updateDisplay() {
  class ContextUsageMeter (line 850) | class ContextUsageMeter {
    method constructor (line 856) | constructor(parentEl: HTMLElement) {
    method render (line 863) | private render() {
    method update (line 901) | update(usage: UsageInfo | null): void {
    method formatTokens (line 931) | private formatTokens(tokens: number): string {
  function createInputToolbar (line 939) | function createInputToolbar(

FILE: src/features/chat/ui/InstructionModeManager.ts
  type InstructionModeCallbacks (line 1) | interface InstructionModeCallbacks {
  type InstructionModeState (line 7) | interface InstructionModeState {
  constant INSTRUCTION_MODE_PLACEHOLDER (line 12) | const INSTRUCTION_MODE_PLACEHOLDER = '# Save in custom system prompt';
  class InstructionModeManager (line 14) | class InstructionModeManager {
    method constructor (line 21) | constructor(
    method handleTriggerKey (line 34) | handleTriggerKey(e: KeyboardEvent): boolean {
    method handleInputChange (line 46) | handleInputChange(): void {
    method enterMode (line 62) | private enterMode(): boolean {
    method exitMode (line 74) | private exitMode(): void {
    method handleKeydown (line 84) | handleKeydown(e: KeyboardEvent): boolean {
    method isActive (line 110) | isActive(): boolean {
    method getRawInstruction (line 115) | getRawInstruction(): string {
    method submit (line 120) | private async submit(): Promise<void> {
    method cancel (line 136) | private cancel(): void {
    method clear (line 143) | clear(): void {
    method destroy (line 150) | destroy(): void {

FILE: src/features/chat/ui/NavigationSidebar.ts
  class NavigationSidebar (line 7) | class NavigationSidebar {
    method constructor (line 15) | constructor(
    method createButton (line 31) | private createButton(cls: string, icon: string, label: string): HTMLEl...
    method setupEventListeners (line 38) | private setupEventListeners(): void {
    method updateVisibility (line 60) | updateVisibility(): void {
    method scrollToMessage (line 69) | private scrollToMessage(direction: 'prev' | 'next'): void {
    method destroy (line 100) | destroy(): void {

FILE: src/features/chat/ui/StatusPanel.ts
  type PanelBashOutput (line 8) | interface PanelBashOutput {
  constant MAX_BASH_OUTPUTS (line 16) | const MAX_BASH_OUTPUTS = 50;
  class StatusPanel (line 21) | class StatusPanel {
    method mount (line 50) | mount(containerEl: HTMLElement): void {
    method remount (line 59) | remount(): void {
    method createPanel (line 112) | private createPanel(): void {
    method updateTodos (line 186) | updateTodos(todos: TodoItem[] | null): void {
    method renderTodoHeader (line 224) | private renderTodoHeader(completedCount: number, totalCount: number, c...
    method renderTodoContent (line 264) | private renderTodoContent(todos: TodoItem[]): void {
    method toggleTodos (line 272) | private toggleTodos(): void {
    method updateTodoDisplay (line 280) | private updateTodoDisplay(): void {
    method updateTodoAriaLabel (line 301) | private updateTodoAriaLabel(completedCount: number, totalCount: number...
    method scrollToBottom (line 315) | private scrollToBottom(): void {
    method truncateDescription (line 325) | private truncateDescription(description: string, maxLength = 50): stri...
    method addBashOutput (line 330) | addBashOutput(info: PanelBashOutput): void {
    method updateBashOutput (line 341) | updateBashOutput(id: string, updates: Partial<Omit<PanelBashOutput, 'i...
    method clearBashOutputs (line 348) | clearBashOutputs(): void {
    method renderBashOutputs (line 354) | private renderBashOutputs(options: { scroll?: boolean } = {}): void {
    method renderBashEntry (line 429) | private renderBashEntry(info: PanelBashOutput): HTMLElement {
    method copyLatestBashOutput (line 495) | private async copyLatestBashOutput(): Promise<void> {
    method appendActionButton (line 508) | private appendActionButton(
    method toggleBashSection (line 535) | private toggleBashSection(): void {
    method destroy (line 547) | destroy(): void {

FILE: src/features/chat/ui/file-context/state/FileContextState.ts
  class FileContextState (line 1) | class FileContextState {
    method getAttachedFiles (line 7) | getAttachedFiles(): Set<string> {
    method hasSentCurrentNote (line 11) | hasSentCurrentNote(): boolean {
    method markCurrentNoteSent (line 15) | markCurrentNoteSent(): void {
    method isSessionStarted (line 19) | isSessionStarted(): boolean {
    method startSession (line 23) | startSession(): void {
    method resetForNewConversation (line 27) | resetForNewConversation(): void {
    method resetForLoadedConversation (line 34) | resetForLoadedConversation(hasMessages: boolean): void {
    method setAttachedFiles (line 41) | setAttachedFiles(files: string[]): void {
    method attachFile (line 48) | attachFile(path: string): void {
    method detachFile (line 52) | detachFile(path: string): void {
    method clearAttachments (line 56) | clearAttachments(): void {
    method getMentionedMcpServers (line 60) | getMentionedMcpServers(): Set<string> {
    method clearMcpMentions (line 64) | clearMcpMentions(): void {
    method setMentionedMcpServers (line 68) | setMentionedMcpServers(mentions: Set<string>): boolean {
    method addMentionedMcpServer (line 80) | addMentionedMcpServer(name: string): void {

FILE: src/features/chat/ui/file-context/view/FileChipsView.ts
  type FileChipsViewCallbacks (line 3) | interface FileChipsViewCallbacks {
  class FileChipsView (line 8) | class FileChipsView {
    method constructor (line 13) | constructor(containerEl: HTMLElement, callbacks: FileChipsViewCallback...
    method destroy (line 24) | destroy(): void {
    method renderCurrentNote (line 28) | renderCurrentNote(filePath: string | null): void {
    method renderFileChip (line 42) | private renderFileChip(filePath: string, onRemove: () => void): void {

FILE: src/features/inline-edit/InlineEditService.ts
  type InlineEditMode (line 21) | type InlineEditMode = 'selection' | 'cursor';
  type InlineEditSelectionRequest (line 23) | interface InlineEditSelectionRequest {
  type InlineEditCursorRequest (line 33) | interface InlineEditCursorRequest {
  type InlineEditRequest (line 41) | type InlineEditRequest = InlineEditSelectionRequest | InlineEditCursorRe...
  type InlineEditResult (line 43) | interface InlineEditResult {
  function parseInlineEditResponse (line 52) | function parseInlineEditResponse(responseText: string): InlineEditResult {
  function buildCursorPrompt (line 71) | function buildCursorPrompt(request: InlineEditCursorRequest): string {
  function buildInlineEditPrompt (line 95) | function buildInlineEditPrompt(request: InlineEditRequest): string {
  function createReadOnlyHook (line 122) | function createReadOnlyHook(): HookCallbackMatcher {
  function createVaultRestrictionHook (line 149) | function createVaultRestrictionHook(vaultPath: string): HookCallbackMatc...
  function extractTextFromSdkMessage (line 211) | function extractTextFromSdkMessage(message: any): string | null {
  class InlineEditService (line 233) | class InlineEditService {
    method constructor (line 238) | constructor(plugin: ClaudianPlugin) {
    method resetConversation (line 242) | resetConversation(): void {
    method editText (line 246) | async editText(request: InlineEditRequest): Promise<InlineEditResult> {
    method continueConversation (line 252) | async continueConversation(message: string, contextFiles?: string[]): ...
    method sendMessage (line 264) | private async sendMessage(prompt: string): Promise<InlineEditResult> {
    method cancel (line 351) | cancel(): void {

FILE: src/features/inline-edit/ui/InlineEditModal.ts
  type InlineEditContext (line 27) | type InlineEditContext =
  class DiffWidget (line 53) | class DiffWidget extends WidgetType {
    method constructor (line 54) | constructor(private diffHtml: string, private controller: InlineEditCo...
    method toDOM (line 57) | toDOM(): HTMLElement {
    method eq (line 83) | eq(other: DiffWidget): boolean {
    method ignoreEvent (line 86) | ignoreEvent(): boolean {
  class InputWidget (line 91) | class InputWidget extends WidgetType {
    method constructor (line 92) | constructor(private controller: InlineEditController) {
    method toDOM (line 95) | toDOM(): HTMLElement {
    method eq (line 98) | eq(): boolean {
    method ignoreEvent (line 101) | ignoreEvent(): boolean {
  type DiffOp (line 145) | interface DiffOp { type: 'equal' | 'insert' | 'delete'; text: string; }
  function computeDiff (line 147) | function computeDiff(oldText: string, newText: string): DiffOp[] {
  function diffToHtml (line 189) | function diffToHtml(ops: DiffOp[]): string {
  type InlineEditDecision (line 200) | type InlineEditDecision = 'accept' | 'edit' | 'reject';
  class InlineEditModal (line 202) | class InlineEditModal {
    method constructor (line 205) | constructor(
    method openAndWait (line 215) | async openAndWait(): Promise<{ decision: InlineEditDecision; editedTex...
  class InlineEditController (line 254) | class InlineEditController {
    method constructor (line 275) | constructor(
    method updatePositionsFromEditor (line 303) | private updatePositionsFromEditor() {
    method show (line 323) | show() {
    method updateHighlight (line 346) | private updateHighlight() {
    method updateSelectionHighlight (line 363) | private updateSelectionHighlight(): void {
    method attachSelectionListeners (line 371) | private attachSelectionListeners() {
    method createInputDOM (line 392) | createInputDOM(): HTMLElement {
    method generate (line 457) | private async generate() {
    method showAgentReply (line 521) | private showAgentReply(message: string) {
    method handleError (line 528) | private handleError(errorMessage: string) {
    method showDiffInPlace (line 538) | private showDiffInPlace() {
    method showInsertionInPlace (line 558) | private showInsertionInPlace() {
    method installAcceptRejectHandler (line 580) | private installAcceptRejectHandler() {
    method accept (line 594) | accept() {
    method reject (line 613) | reject() {
    method removeSelectionListeners (line 619) | private removeSelectionListeners() {
    method cleanup (line 627) | private cleanup(options?: { keepSelectionHighlight?: boolean }) {
    method restoreSelectionHighlight (line 652) | private restoreSelectionHighlight(): void {
    method handleKeydown (line 659) | private handleKeydown(e: KeyboardEvent) {
    method normalizePathForVault (line 674) | private normalizePathForVault(rawPath: string | undefined | null): str...
    method resolveContextFilesFromMessage (line 684) | private resolveContextFilesFromMessage(message: string): string[] {

FILE: src/features/settings/ClaudianSettings.ts
  function formatHotkey (line 20) | function formatHotkey(hotkey: { modifiers: string[]; key: string }): str...
  function openHotkeySettings (line 32) | function openHotkeySettings(app: App): void {
  function getHotkeyForCommand (line 49) | function getHotkeyForCommand(app: App, commandId: string): string | null {
  function addHotkeySettingRow (line 62) | function addHotkeySettingRow(
  class ClaudianSettingTab (line 77) | class ClaudianSettingTab extends PluginSettingTab {
    method constructor (line 81) | constructor(app: App, plugin: ClaudianPlugin) {
    method normalizeModelVariantSettings (line 86) | private normalizeModelVariantSettings(): void {
    method display (line 90) | display(): void {
    method renderContextLimitsSection (line 728) | private renderContextLimitsSection(): void {
    method restartServiceForPromptChange (line 800) | private async restartServiceForPromptChange(): Promise<void> {

FILE: src/features/settings/keyboardNavigation.ts
  constant NAV_ACTIONS (line 3) | const NAV_ACTIONS = ['scrollUp', 'scrollDown', 'focusInput'] as const;
  type NavAction (line 4) | type NavAction = (typeof NAV_ACTIONS)[number];

FILE: src/features/settings/ui/AgentSettings.ts
  constant MODEL_OPTIONS (line 10) | const MODEL_OPTIONS = [
  class AgentModal (line 17) | class AgentModal extends Modal {
    method constructor (line 22) | constructor(
    method onOpen (line 34) | onOpen() {
    method onClose (line 205) | onClose() {
  class AgentSettings (line 210) | class AgentSettings {
    method constructor (line 214) | constructor(containerEl: HTMLElement, plugin: ClaudianPlugin) {
    method render (line 220) | private render(): void {
    method renderAgentItem (line 258) | private renderAgentItem(listEl: HTMLElement, agent: AgentDefinition): ...
    method refreshAgents (line 302) | private async refreshAgents(): Promise<void> {
    method openAgentModal (line 312) | private async openAgentModal(existingAgent: AgentDefinition | null): P...
    method saveAgent (line 334) | private async saveAgent(agent: AgentDefinition, existing: AgentDefinit...
    method deleteAgent (line 360) | private async deleteAgent(agent: AgentDefinition): Promise<void> {

FILE: src/features/settings/ui/EnvSnippetManager.ts
  class EnvSnippetModal (line 10) | class EnvSnippetModal extends Modal {
    method constructor (line 15) | constructor(app: App, plugin: ClaudianPlugin, snippet: EnvSnippet | nu...
    method onOpen (line 22) | onOpen() {
    method onClose (line 168) | onClose() {
  class EnvSnippetManager (line 174) | class EnvSnippetManager {
    method constructor (line 179) | constructor(containerEl: HTMLElement, plugin: ClaudianPlugin, onContex...
    method render (line 186) | private render() {
    method saveCurrentEnv (line 263) | private async saveCurrentEnv() {
    method insertSnippet (line 278) | private async insertSnippet(snippet: EnvSnippet) {
    method editSnippet (line 303) | private editSnippet(snippet: EnvSnippet) {
    method deleteSnippet (line 321) | private async deleteSnippet(snippet: EnvSnippet) {
    method refresh (line 328) | public refresh() {

FILE: src/features/settings/ui/McpServerModal.ts
  class McpServerModal (line 16) | class McpServerModal extends Modal {
    method constructor (line 32) | constructor(
    method initFromConfig (line 60) | private initFromConfig(config: McpServerConfig) {
    method onOpen (line 77) | onOpen() {
    method renderTypeFields (line 148) | private renderTypeFields() {
    method renderStdioFields (line 159) | private renderStdioFields() {
    method renderUrlFields (line 193) | private renderUrlFields() {
    method handleKeyDown (line 224) | private handleKeyDown(e: KeyboardEvent) {
    method save (line 235) | private save() {
    method parseEnvString (line 307) | private parseEnvString(envStr: string): Record<string, string> {
    method envRecordToString (line 329) | private envRecordToString(env: Record<string, string> | undefined): st...
    method onClose (line 336) | onClose() {

FILE: src/features/settings/ui/McpSettingsManager.ts
  class McpSettingsManager (line 11) | class McpSettingsManager {
    method broadcastMcpReloadToAllViews (line 20) | private async broadcastMcpReloadToAllViews(): Promise<void> {
    method constructor (line 29) | constructor(containerEl: HTMLElement, plugin: ClaudianPlugin) {
    method loadAndRender (line 35) | private async loadAndRender() {
    method render (line 40) | private render() {
    method renderServerItem (line 100) | private renderServerItem(listEl: HTMLElement, server: ClaudianMcpServe...
    method testServer (line 166) | private async testServer(server: ClaudianMcpServer) {
    method updateServerDisabledTools (line 189) | private async updateServerDisabledTools(
    method updateDisabledTool (line 211) | private async updateDisabledTool(
    method updateAllDisabledTools (line 228) | private async updateAllDisabledTools(server: ClaudianMcpServer, disabl...
    method getServerPreview (line 235) | private getServerPreview(server: ClaudianMcpServer, type: McpServerTyp...
    method openModal (line 246) | private openModal(existing: ClaudianMcpServer | null, initialType?: Mc...
    method importFromClipboard (line 259) | private async importFromClipboard() {
    method saveServer (line 299) | private async saveServer(server: ClaudianMcpServer, existing: Claudian...
    method importServers (line 327) | private async importServers(servers: Array<{ name: string; config: Mcp...
    method toggleServer (line 369) | private async toggleServer(server: ClaudianMcpServer) {
    method deleteServer (line 377) | private async deleteServer(server: ClaudianMcpServer) {
    method refresh (line 390) | public refresh() {

FILE: src/features/settings/ui/McpTestModal.ts
  function formatToggleError (line 6) | function formatToggleError(error: unknown): string {
  class McpTestModal (line 22) | class McpTestModal extends Modal {
    method constructor (line 36) | constructor(
    method onOpen (line 54) | onOpen() {
    method setResult (line 61) | setResult(result: McpTestResult) {
    method setError (line 67) | setError(error: string) {
    method renderLoading (line 73) | private renderLoading() {
    method render (line 87) | private render() {
    method renderTool (line 162) | private renderTool(container: HTMLElement, tool: McpTool) {
    method handleToolToggle (line 207) | private async handleToolToggle(
    method updateToolState (line 248) | private updateToolState(toolEl: HTMLElement, enabled: boolean) {
    method updateToggleAllButton (line 252) | private updateToggleAllButton() {
    method handleToggleAll (line 267) | private async handleToggleAll() {
    method onClose (line 324) | onClose() {

FILE: src/features/settings/ui/PluginSettingsManager.ts
  class PluginSettingsManager (line 6) | class PluginSettingsManager {
    method constructor (line 10) | constructor(containerEl: HTMLElement, plugin: ClaudianPlugin) {
    method render (line 16) | private render() {
    method renderPluginItem (line 61) | private renderPluginItem(listEl: HTMLElement, plugin: ClaudianPluginTy...
    method togglePlugin (line 91) | private async togglePlugin(pluginId: string) {
    method refreshPlugins (line 121) | private async refreshPlugins() {
    method refresh (line 135) | public refresh() {

FILE: src/features/settings/ui/SlashCommandSettings.ts
  function resolveAllowedTools (line 9) | function resolveAllowedTools(inputValue: string, parsedTools?: string[])...
  class SlashCommandModal (line 20) | class SlashCommandModal extends Modal {
    method constructor (line 25) | constructor(
    method onOpen (line 37) | onOpen() {
    method onClose (line 277) | onClose() {
  class SlashCommandSettings (line 282) | class SlashCommandSettings {
    method constructor (line 286) | constructor(containerEl: HTMLElement, plugin: ClaudianPlugin) {
    method render (line 292) | private render(): void {
    method renderCommandItem (line 322) | private renderCommandItem(listEl: HTMLElement, cmd: SlashCommand): void {
    method openCommandModal (line 385) | private openCommandModal(existingCmd: SlashCommand | null): void {
    method storageFor (line 397) | private storageFor(cmd: SlashCommand) {
    method saveCommand (line 401) | private async saveCommand(cmd: SlashCommand, existing: SlashCommand | ...
    method deleteCommand (line 417) | private async deleteCommand(cmd: SlashCommand): Promise<void> {
    method transformToSkill (line 427) | private async transformToSkill(cmd: SlashCommand): Promise<void> {
    method reloadCommands (line 456) | private async reloadCommands(): Promise<void> {
    method refresh (line 460) | public refresh(): void {

FILE: src/i18n/constants.ts
  type LocaleInfo (line 12) | interface LocaleInfo {
  constant SUPPORTED_LOCALES (line 22) | const SUPPORTED_LOCALES: LocaleInfo[] = [
  constant DEFAULT_LOCALE (line 38) | const DEFAULT_LOCALE: Locale = 'en';
  function getLocaleInfo (line 43) | function getLocaleInfo(code: Locale): LocaleInfo | undefined {
  function getLocaleDisplayString (line 50) | function getLocaleDisplayString(code: Locale, includeFlag = true): string {

FILE: src/i18n/i18n.ts
  constant DEFAULT_LOCALE (line 33) | const DEFAULT_LOCALE: Locale = 'en';
  function t (line 39) | function t(key: TranslationKey, params?: Record<string, string | number>...
  function tFallback (line 69) | function tFallback(key: TranslationKey, params?: Record<string, string |...
  function setLocale (line 99) | function setLocale(locale: Locale): boolean {
  function getLocale (line 110) | function getLocale(): Locale {
  function getAvailableLocales (line 117) | function getAvailableLocales(): Locale[] {
  function getLocaleDisplayName (line 124) | function getLocaleDisplayName(locale: Locale): string {

FILE: src/i18n/types.ts
  type Locale (line 5) | type Locale = 'en' | 'zh-CN' | 'zh-TW' | 'ja' | 'ko' | 'de' | 'fr' | 'es...
  type TranslationKey (line 11) | type TranslationKey =

FILE: src/main.ts
  function chooseRicherResult (line 54) | function chooseRicherResult(sdkResult?: string, cachedResult?: string): ...
  function chooseRicherToolCalls (line 65) | function chooseRicherToolCalls(
  function normalizeAsyncStatus (line 73) | function normalizeAsyncStatus(
  function isTerminalAsyncStatus (line 85) | function isTerminalAsyncStatus(status: AsyncSubagentStatus | undefined):...
  function mergeSubagentInfo (line 89) | function mergeSubagentInfo(
  function ensureTaskToolCall (line 138) | function ensureTaskToolCall(
  class ClaudianPlugin (line 185) | class ClaudianPlugin extends Plugin {
    method onload (line 195) | async onload() {
    method onunload (line 336) | async onunload() {
    method activateView (line 347) | async activateView() {
    method loadSettings (line 370) | async loadSettings() {
    method backfillConversationResponseTimestamps (line 511) | private backfillConversationResponseTimestamps(): Conversation[] {
    method normalizeModelVariantSettings (line 529) | normalizeModelVariantSettings(): boolean {
    method saveSettings (line 560) | async saveSettings() {
    method applyEnvironmentVariables (line 571) | async applyEnvironmentVariables(envText: string): Promise<void> {
    method getActiveEnvironmentVariables (line 647) | getActiveEnvironmentVariables(): string {
    method getResolvedClaudeCliPath (line 651) | getResolvedClaudeCliPath(): string | null {
    method getDefaultModelValues (line 659) | private getDefaultModelValues(): string[] {
    method getPreferredCustomModel (line 663) | private getPreferredCustomModel(envVars: Record<string, string>, custo...
    method computeEnvHash (line 672) | private computeEnvHash(envText: string): string {
    method reconcileModelWithEnvironment (line 698) | private reconcileModelWithEnvironment(envText: string): {
    method generateConversationId (line 735) | private generateConversationId(): string {
    method generateDefaultTitle (line 739) | private generateDefaultTitle(): string {
    method getConversationPreview (line 749) | private getConversationPreview(conv: Conversation): string {
    method isPendingFork (line 760) | private isPendingFork(conversation: Conversation): boolean {
    method loadSdkMessagesForConversation (line 766) | private async loadSdkMessagesForConversation(conversation: Conversatio...
    method enrichAsyncSubagentToolCalls (line 857) | private async enrichAsyncSubagentToolCalls(
    method applySubagentData (line 898) | private applySubagentData(messages: ChatMessage[], subagentData: Recor...
    method dedupeMessages (line 981) | private dedupeMessages(messages: ChatMessage[]): ChatMessage[] {
    method createConversation (line 1002) | async createConversation(sessionId?: string): Promise<Conversation> {
    method switchConversation (line 1029) | async switchConversation(id: string): Promise<Conversation | null> {
    method deleteConversation (line 1044) | async deleteConversation(id: string): Promise<void> {
    method renameConversation (line 1080) | async renameConversation(id: string, title: string): Promise<void> {
    method updateConversation (line 1107) | async updateConversation(id: string, updates: Partial<Conversation>): ...
    method getConversationById (line 1141) | async getConversationById(id: string): Promise<Conversation | null> {
    method getConversationSync (line 1155) | getConversationSync(id: string): Conversation | null {
    method findEmptyConversation (line 1160) | findEmptyConversation(): Conversation | null {
    method getConversationList (line 1165) | getConversationList(): ConversationMeta[] {
    method getView (line 1180) | getView(): ClaudianView | null {
    method getAllViews (line 1189) | getAllViews(): ClaudianView[] {
    method findConversationAcrossViews (line 1198) | findConversationAcrossViews(conversationId: string): { view: ClaudianV...
    method getSdkCommands (line 1218) | async getSdkCommands(): Promise<SlashCommand[]> {

FILE: src/shared/components/ResumeSessionDropdown.ts
  type ResumeSessionDropdownCallbacks (line 12) | interface ResumeSessionDropdownCallbacks {
  class ResumeSessionDropdown (line 17) | class ResumeSessionDropdown {
    method constructor (line 27) | constructor(
    method handleKeydown (line 49) | handleKeydown(e: KeyboardEvent): boolean {
    method isVisible (line 77) | isVisible(): boolean {
    method destroy (line 81) | destroy(): void {
    method dismiss (line 86) | private dismiss(): void {
    method selectItem (line 91) | private selectItem(): void {
    method navigate (line 105) | private navigate(direction: number): void {
    method updateSelection (line 111) | private updateSelection(): void {
    method sortConversations (line 123) | private sortConversations(conversations: ConversationMeta[]): Conversa...
    method render (line 129) | private render(): void {
    method formatDate (line 176) | private formatDate(timestamp: number): string {

FILE: src/shared/components/SelectableDropdown.ts
  type SelectableDropdownOptions (line 1) | interface SelectableDropdownOptions {
  type SelectableDropdownRenderOptions (line 9) | interface SelectableDropdownRenderOptions<T> {
  class SelectableDropdown (line 19) | class SelectableDropdown<T> {
    method constructor (line 27) | constructor(containerEl: HTMLElement, options: SelectableDropdownOptio...
    method isVisible (line 32) | isVisible(): boolean {
    method getElement (line 36) | getElement(): HTMLElement | null {
    method getSelectedIndex (line 40) | getSelectedIndex(): number {
    method getSelectedItem (line 44) | getSelectedItem(): T | null {
    method getItems (line 48) | getItems(): T[] {
    method hide (line 52) | hide(): void {
    method destroy (line 58) | destroy(): void {
    method render (line 65) | render(options: SelectableDropdownRenderOptions<T>): void {
    method updateSelection (line 116) | updateSelection(): void {
    method moveSelection (line 127) | moveSelection(delta: number): void {
    method createDropdownElement (line 133) | private createDropdownElement(): HTMLElement {

FILE: src/shared/components/SelectionHighlight.ts
  type SelectionHighlighter (line 12) | interface SelectionHighlighter {
  function createSelectionHighlighter (line 17) | function createSelectionHighlighter(): SelectionHighlighter {
  function showSelectionHighlight (line 71) | function showSelectionHighlight(editorView: EditorView, from: number, to...
  function hideSelectionHighlight (line 75) | function hideSelectionHighlight(editorView: EditorView): void {

FILE: src/shared/components/SlashCommandDropdown.ts
  constant FILTERED_SDK_COMMANDS (line 16) | const FILTERED_SDK_COMMANDS = new Set([
  type SlashCommandDropdownCallbacks (line 25) | interface SlashCommandDropdownCallbacks {
  type SlashCommandDropdownOptions (line 36) | interface SlashCommandDropdownOptions {
  class SlashCommandDropdown (line 41) | class SlashCommandDropdown {
    method constructor (line 61) | constructor(
    method setEnabled (line 77) | setEnabled(enabled: boolean): void {
    method setHiddenCommands (line 84) | setHiddenCommands(commands: Set<string>): void {
    method handleInputChange (line 88) | handleInputChange(): void {
    method handleKeydown (line 115) | handleKeydown(e: KeyboardEvent): boolean {
    method isVisible (line 143) | isVisible(): boolean {
    method hide (line 147) | hide(): void {
    method destroy (line 155) | destroy(): void {
    method resetSdkSkillsCache (line 167) | resetSdkSkillsCache(): void {
    method getInputValue (line 173) | private getInputValue(): string {
    method getCursorPosition (line 177) | private getCursorPosition(): number {
    method setInputValue (line 181) | private setInputValue(value: string): void {
    method setCursorPosition (line 185) | private setCursorPosition(pos: number): void {
    method showDropdown (line 190) | private async showDropdown(searchText: string): Promise<void> {
    method buildCommandList (line 242) | private buildCommandList(builtInCommands: SlashCommand[]): SlashComman...
    method render (line 272) | private render(): void {
    method createDropdownElement (line 324) | private createDropdownElement(): HTMLElement {
    method positionFixed (line 337) | private positionFixed(): void {
    method navigate (line 349) | private navigate(direction: number): void {
    method updateSelection (line 355) | private updateSelection(): void {
    method selectItem (line 367) | private selectItem(): void {

FILE: src/shared/icons.ts
  constant MCP_ICON_SVG (line 1) | const MCP_ICON_SVG = `<svg fill="currentColor" fill-rule="evenodd" heigh...
  constant CHECK_ICON_SVG (line 3) | const CHECK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1...

FILE: src/shared/mention/MentionDropdownController.ts
  type MentionDropdownOptions (line 17) | interface MentionDropdownOptions {
  type MentionDropdownCallbacks (line 21) | interface MentionDropdownCallbacks {
  type McpMentionProvider (line 34) | interface McpMentionProvider {
  class MentionDropdownController (line 38) | class MentionDropdownController {
    method constructor (line 54) | constructor(
    method setMcpManager (line 74) | setMcpManager(manager: McpMentionProvider | null): void {
    method setAgentService (line 78) | setAgentService(service: AgentMentionProvider | null): void {
    method preScanExternalContexts (line 82) | preScanExternalContexts(): void {
    method isVisible (line 95) | isVisible(): boolean {
    method hide (line 99) | hide(): void {
    method containsElement (line 104) | containsElement(el: Node): boolean {
    method destroy (line 108) | destroy(): void {
    method updateMcpMentionsFromText (line 115) | updateMcpMentionsFromText(text: string): void {
    method handleInputChange (line 130) | handleInputChange(): void {
    method handleKeydown (line 166) | handleKeydown(e: KeyboardEvent): boolean {
    method showMentionDropdown (line 201) | private showMentionDropdown(searchText: string): void {
    method appendVaultItems (line 342) | private appendVaultItems(searchLower: string): number {
    method renderMentionDropdown (line 416) | private renderMentionDropdown(): void {
    method positionFixed (line 512) | private positionFixed(): void {
    method insertReplacement (line 525) | private insertReplacement(beforeAt: string, replacement: string, after...
    method returnToFirstLevel (line 530) | private returnToFirstLevel(): void {
    method selectMentionItem (line 545) | private selectMentionItem(): void {

FILE: src/shared/mention/VaultMentionCache.ts
  type VaultFileCacheOptions (line 4) | interface VaultFileCacheOptions {
  class VaultFileCache (line 8) | class VaultFileCache {
    method constructor (line 13) | constructor(
    method initializeInBackground (line 18) | initializeInBackground(): void {
    method markDirty (line 26) | markDirty(): void {
    method getFiles (line 30) | getFiles(): TFile[] {
    method tryRefreshFiles (line 37) | private tryRefreshFiles(): void {
  function isVisibleFolder (line 53) | function isVisibleFolder(folder: TFolder): boolean {
  class VaultFolderCache (line 61) | class VaultFolderCache {
    method constructor (line 66) | constructor(private app: App) {}
    method initializeInBackground (line 68) | initializeInBackground(): void {
    method markDirty (line 76) | markDirty(): void {
    method getFolders (line 80) | getFolders(): TFolder[] {
    method tryRefreshFolders (line 87) | private tryRefreshFolders(): void {
    method loadFolders (line 101) | private loadFolders(): TFolder[] {

FILE: src/shared/mention/VaultMentionDataProvider.ts
  type VaultMentionDataProviderOptions (line 5) | interface VaultMentionDataProviderOptions {
  class VaultMentionDataProvider (line 9) | class VaultMentionDataProvider {
    method constructor (line 14) | constructor(
    method initializeInBackground (line 28) | initializeInBackground(): void {
    method markFilesDirty (line 33) | markFilesDirty(): void {
    method markFoldersDirty (line 37) | markFoldersDirty(): void {
    method getCachedVaultFiles (line 41) | getCachedVaultFiles(): TFile[] {
    method getCachedVaultFolders (line 45) | getCachedVaultFolders(): Array<{ name: string; path: string }> {

FILE: src/shared/mention/types.ts
  type FileMentionItem (line 3) | interface FileMentionItem {
  type FolderMentionItem (line 10) | interface FolderMentionItem {
  type McpServerMentionItem (line 16) | interface McpServerMentionItem {
  type ContextFileMentionItem (line 21) | interface ContextFileMentionItem {
  type ContextFolderMentionItem (line 29) | interface ContextFolderMentionItem {
  type AgentMentionItem (line 36) | interface AgentMentionItem {
  type AgentFolderMentionItem (line 48) | interface AgentFolderMentionItem {
  type AgentMentionProvider (line 53) | interface AgentMentionProvider {
  type MentionItem (line 62) | type MentionItem =

FILE: src/shared/modals/ConfirmModal.ts
  function confirmDelete (line 5) | function confirmDelete(app: App, message: string): Promise<boolean> {
  function confirm (line 11) | function confirm(app: App, message: string, confirmText: string): Promis...
  class ConfirmModal (line 17) | class ConfirmModal extends Modal {
    method constructor (line 23) | constructor(app: App, message: string, resolve: (confirmed: boolean) =...
    method onOpen (line 30) | onOpen() {
    method onClose (line 54) | onClose() {

FILE: src/shared/modals/ForkTargetModal.ts
  type ForkTarget (line 5) | type ForkTarget = 'new-tab' | 'current-tab';
  function chooseForkTarget (line 7) | function chooseForkTarget(app: App): Promise<ForkTarget | null> {
  class ForkTargetModal (line 13) | class ForkTargetModal extends Modal {
    method constructor (line 17) | constructor(app: App, resolve: (target: ForkTarget | null) => void) {
    method onOpen (line 22) | onOpen() {
    method createOption (line 32) | private createOption(container: HTMLElement, target: ForkTarget, label...
    method onClose (line 41) | onClose() {

FILE: src/shared/modals/InstructionConfirmModal.ts
  type InstructionDecision (line 13) | type InstructionDecision = 'accept' | 'reject';
  type ModalState (line 15) | type ModalState = 'loading' | 'clarification' | 'confirmation';
  type InstructionModalCallbacks (line 17) | interface InstructionModalCallbacks {
  class InstructionModal (line 23) | class InstructionModal extends Modal {
    method constructor (line 49) | constructor(
    method onOpen (line 59) | onOpen() {
    method showClarification (line 125) | showClarification(clarification: string) {
    method showConfirmation (line 137) | showConfirmation(refinedInstruction: string) {
    method showError (line 150) | showError(error: string) {
    method showClarificationLoading (line 156) | showClarificationLoading() {
    method showState (line 166) | private showState(state: ModalState) {
    method updateButtons (line 182) | private updateButtons() {
    method submitClarification (line 218) | private async submitClarification() {
    method toggleEdit (line 233) | private toggleEdit() {
    method handleAccept (line 253) | private handleAccept() {
    method handleReject (line 265) | private handleReject() {
    method onClose (line 272) | onClose() {

FILE: src/utils/agent.ts
  function validateAgentName (line 5) | function validateAgentName(name: string): string | null {
  function pushYamlList (line 9) | function pushYamlList(lines: string[], key: string, items?: string[]): v...
  function serializeAgent (line 17) | function serializeAgent(agent: AgentDefinition): string {

FILE: src/utils/browser.ts
  type BrowserSelectionContext (line 1) | interface BrowserSelectionContext {
  function escapeXmlAttribute (line 8) | function escapeXmlAttribute(value: string): string {
  function buildAttributeList (line 16) | function buildAttributeList(context: BrowserSelectionContext): string {
  function escapeXmlBody (line 32) | function escapeXmlBody(text: string): string {
  function formatBrowserContext (line 36) | function formatBrowserContext(context: BrowserSelectionContext): string {
  function appendBrowserContext (line 43) | function appendBrowserContext(prompt: string, context: BrowserSelectionC...

FILE: src/utils/canvas.ts
  type CanvasSelectionContext (line 1) | interface CanvasSelectionContext {
  function formatCanvasContext (line 6) | function formatCanvasContext(context: CanvasSelectionContext): string {
  function appendCanvasContext (line 11) | function appendCanvasContext(prompt: string, context: CanvasSelectionCon...

FILE: src/utils/claudeCli.ts
  class ClaudeCliResolver (line 13) | class ClaudeCliResolver {
    method resolve (line 27) | resolve(
    method reset (line 55) | reset(): void {
  function resolveClaudeCliPath (line 69) | function resolveClaudeCliPath(

FILE: src/utils/context.ts
  constant CURRENT_NOTE_PREFIX_REGEX (line 8) | const CURRENT_NOTE_PREFIX_REGEX = /^<current_note>\n[\s\S]*?<\/current_n...
  constant CURRENT_NOTE_SUFFIX_REGEX (line 10) | const CURRENT_NOTE_SUFFIX_REGEX = /\n\n<current_note>\n[\s\S]*?<\/curren...
  constant XML_CONTEXT_PATTERN (line 18) | const XML_CONTEXT_PATTERN = /\n\n<(?:current_note|editor_selection|edito...
  function formatCurrentNote (line 20) | function formatCurrentNote(notePath: string): string {
  function appendCurrentNote (line 24) | function appendCurrentNote(prompt: string, notePath: string): string {
  function stripCurrentNoteContext (line 32) | function stripCurrentNoteContext(prompt: string): string {
  function extractContentBeforeXmlContext (line 48) | function extractContentBeforeXmlContext(text: string): string | undefined {
  function extractUserQuery (line 74) | function extractUserQuery(prompt: string): string {
  function formatContextFilesLine (line 94) | function formatContextFilesLine(files: string[]): string {
  function appendContextFiles (line 98) | function appendContextFiles(prompt: string, files: string[]): string {

FILE: src/utils/contextMentionResolver.ts
  type MentionLookupMatch (line 4) | interface MentionLookupMatch {
  constant TRAILING_PUNCTUATION_REGEX (line 10) | const TRAILING_PUNCTUATION_REGEX = /[),.!?:;]+$/;
  constant BOUNDARY_PUNCTUATION (line 11) | const BOUNDARY_PUNCTUATION = new Set([',', ')', '!', '?', ':', ';']);
  function isWhitespace (line 13) | function isWhitespace(char: string): boolean {
  function collectMentionEndCandidates (line 17) | function collectMentionEndCandidates(text: string, pathStart: number): n...
  function isMentionStart (line 36) | function isMentionStart(text: string, index: number): boolean {
  function normalizeMentionPath (line 42) | function normalizeMentionPath(pathText: string): string {
  function normalizeForPlatformLookup (line 50) | function normalizeForPlatformLookup(value: string): string {
  function buildExternalContextLookup (line 54) | function buildExternalContextLookup(
  function resolveExternalMentionAtIndex (line 69) | function resolveExternalMentionAtIndex(
  function findBestMentionLookupMatch (line 106) | function findBestMentionLookupMatch(
  function createExternalContextLookupGetter (line 141) | function createExternalContextLookupGetter(

FILE: src/utils/date.ts
  function getTodayDate (line 8) | function getTodayDate(): string {
  function formatDurationMmSs (line 21) | function formatDurationMmSs(seconds: number): string {

FILE: src/utils/diff.ts
  function structuredPatchToDiffLines (line 8) | function structuredPatchToDiffLines(hunks: StructuredPatchHunk[]): DiffL...
  function countLineChanges (line 32) | function countLineChanges(diffLines: DiffLine[]): DiffStats {
  function extractDiffData (line 50) | function extractDiffData(toolUseResult: unknown, toolCall: ToolCallInfo)...
  function diffFromToolInput (line 72) | function diffFromToolInput(toolCall: ToolCallInfo, filePath: string): To...

FILE: src/utils/editor.ts
  function getEditorView (line 14) | function getEditorView(editor: Editor): EditorView | undefined {
  type CursorContext (line 18) | interface CursorContext {
  type EditorSelectionContext (line 26) | interface EditorSelectionContext {
  function findNearestNonEmptyLine (line 35) | function findNearestNonEmptyLine(
  function buildCursorContext (line 52) | function buildCursorContext(
  function formatEditorContext (line 78) | function formatEditorContext(context: EditorSelectionContext): string {
  function appendEditorContext (line 101) | function appendEditorContext(prompt: string, context: EditorSelectionCon...

FILE: src/utils/env.ts
  constant PATH_SEPARATOR (line 15) | const PATH_SEPARATOR = isWindows ? ';' : ':';
  constant NODE_EXECUTABLE (line 16) | const NODE_EXECUTABLE = isWindows ? 'node.exe' : 'node';
  function getHomeDir (line 18) | function getHomeDir(): string {
  function getAppProvidedCliPaths (line 27) | function getAppProvidedCliPaths(): string[] {
  function getExtraBinaryPaths (line 44) | function getExtraBinaryPaths(): string[] {
  function findNodeDirectory (line 191) | function findNodeDirectory(additionalPaths?: string): string | null {
  function findNodeExecutable (line 217) | function findNodeExecutable(additionalPaths?: string): string | null {
  function cliPathRequiresNode (line 225) | function cliPathRequiresNode(cliPath: string): boolean {
  function getMissingNodeError (line 263) | function getMissingNodeError(cliPath: string, enhancedPath?: string): st...
  function getEnhancedPath (line 287) | function getEnhancedPath(additionalPaths?: string, cliPath?: string): st...
  constant CUSTOM_MODEL_ENV_KEYS (line 341) | const CUSTOM_MODEL_ENV_KEYS = [
  function getModelTypeFromEnvKey (line 348) | function getModelTypeFromEnvKey(envKey: string): string {
  function parseEnvironmentVariables (line 355) | function parseEnvironmentVariables(input: string): Record<string, string> {
  function getModelsFromEnvironment (line 379) | function getModelsFromEnvironment(envVars: Record<string, string>): { va...
  function getCurrentModelFromEnvironment (line 423) | function getCurrentModelFromEnvironment(envVars: Record<string, string>)...
  function getHostnameKey (line 440) | function getHostnameKey(): string {
  constant MIN_CONTEXT_LIMIT (line 444) | const MIN_CONTEXT_LIMIT = 1_000;
  constant MAX_CONTEXT_LIMIT (line 445) | const MAX_CONTEXT_LIMIT = 10_000_000;
  function getCustomModelIds (line 447) | function getCustomModelIds(envVars: Record<string, string>): Set<string> {
  function parseContextLimit (line 459) | function parseContextLimit(input: string): number | null {
  function formatContextLimit (line 481) | function formatContextLimit(tokens: number): string {

FILE: src/utils/externalContext.ts
  type PathConflict (line 11) | interface PathConflict {
  function normalizePathForComparison (line 23) | function normalizePathForComparison(p: string): string {
  function normalizePathForDisplay (line 27) | function normalizePathForDisplay(p: string): string {
  function findConflictingPath (line 32) | function findConflictingPath(
  function getFolderName (line 53) | function getFolderName(p: string): string {
  type ExternalContextDisplayEntry (line 59) | interface ExternalContextDisplayEntry {
  function getContextDisplayName (line 65) | function getContextDisplayName(
  function buildExternalContextDisplayEntries (line 81) | function buildExternalContextDisplayEntries(
  type DirectoryValidationResult (line 108) | interface DirectoryValidationResult {
  function validateDirectoryPath (line 113) | function validateDirectoryPath(p: string): DirectoryValidationResult {
  function isValidDirectoryPath (line 132) | function isValidDirectoryPath(p: string): boolean {
  function filterValidPaths (line 136) | function filterValidPaths(paths: string[]): string[] {
  function isDuplicatePath (line 140) | function isDuplicatePath(newPath: string, existingPaths: string[]): bool...

FILE: src/utils/externalContextScanner.ts
  type ExternalContextFile (line 13) | interface ExternalContextFile {
  type ScanCache (line 22) | interface ScanCache {
  constant CACHE_TTL_MS (line 27) | const CACHE_TTL_MS = 30000;
  constant MAX_FILES_PER_PATH (line 28) | const MAX_FILES_PER_PATH = 1000;
  constant MAX_DEPTH (line 29) | const MAX_DEPTH = 10;
  constant SKIP_DIRECTORIES (line 31) | const SKIP_DIRECTORIES = new Set([
  class ExternalContextScanner (line 49) | class ExternalContextScanner {
    method scanPaths (line 52) | scanPaths(externalContextPaths: string[]): ExternalContextFile[] {
    method scanDirectory (line 73) | private scanDirectory(
    method invalidateCache (line 125) | invalidateCache(): void {
    method invalidatePath (line 129) | invalidatePath(contextPath: string): void {

FILE: src/utils/fileLink.ts
  constant WIKILINK_PATTERN_SOURCE (line 21) | const WIKILINK_PATTERN_SOURCE = '(?<!!)\\[\\[([^\\]|#^]+)(?:#[^\\]|]+)?(...
  function createWikilinkPattern (line 24) | function createWikilinkPattern(): RegExp {
  type WikilinkMatch (line 28) | interface WikilinkMatch {
  function extractLinkTarget (line 36) | function extractLinkTarget(fullMatch: string): string {
  function findWikilinks (line 46) | function findWikilinks(app: App, text: string): WikilinkMatch[] {
  function fileExistsInVault (line 67) | function fileExistsInVault(app: App, linkPath: string): boolean {
  function createWikilink (line 92) | function createWikilink(
  function registerFileLinkHandler (line 109) | function registerFileLinkHandler(
  function buildFragmentWithLinks (line 129) | function buildFragmentWithLinks(text: string, matches: WikilinkMatch[]):...
  function processTextNode (line 157) | function processTextNode(app: App, node: Text): boolean {
  function processFileLinks (line 172) | function processFileLinks(app: App, container: HTMLElement): void {

FILE: src/utils/frontmatter.ts
  constant FRONTMATTER_PATTERN (line 3) | const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
  constant VALID_KEY_PATTERN (line 4) | const VALID_KEY_PATTERN = /^[\w-]+$/;
  function isValidKey (line 6) | function isValidKey(key: string): boolean {
  function unquote (line 10) | function unquote(value: string): string {
  function parseScalarValue (line 20) | function parseScalarValue(rawValue: string): unknown {
  function parseFrontmatterFallback (line 38) | function parseFrontmatterFallback(yamlContent: string): Record<string, u...
  function parseFrontmatter (line 101) | function parseFrontmatter(
  function extractString (line 128) | function extractString(
  function normalizeStringArray (line 140) | function normalizeStringArray(val: unknown): string[] | undefined {
  function extractStringArray (line 156) | function extractStringArray(
  function extractBoolean (line 163) | function extractBoolean(
  function isRecord (line 172) | function isRecord(value: unknown): value is Record<string, unknown> {
  constant MAX_SLUG_LENGTH (line 176) | const MAX_SLUG_LENGTH = 64;
  constant SLUG_PATTERN (line 177) | const SLUG_PATTERN = /^[a-z0-9-]+$/;
  constant YAML_RESERVED_WORDS (line 178) | const YAML_RESERVED_WORDS = new Set(['true', 'false', 'null', 'yes', 'no...
  function validateSlugName (line 180) | function validateSlugName(name: string, label: string): string | null {

FILE: src/utils/imageEmbed.ts
  constant IMAGE_EXTENSIONS (line 14) | const IMAGE_EXTENSIONS = new Set([
  constant IMAGE_EMBED_PATTERN (line 25) | const IMAGE_EMBED_PATTERN = /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
  function isImagePath (line 27) | function isImagePath(path: string): boolean {
  function resolveImageFile (line 32) | function resolveImageFile(
  function buildStyleAttribute (line 54) | function buildStyleAttribute(altText: string | undefined): string {
  function createImageHtml (line 69) | function createImageHtml(
  function createFallbackHtml (line 81) | function createFallbackHtml(wikilink: string): string {
  function replaceImageEmbedsWithHtml (line 89) | function replaceImageEmbedsWithHtml(

FILE: src/utils/inlineEdit.ts
  function normalizeInsertionText (line 10) | function normalizeInsertionText(text: string): string {
  function escapeHtml (line 15) | function escapeHtml(text: string): string {

FILE: src/utils/interrupt.ts
  constant INTERRUPT_MARKERS (line 1) | const INTERRUPT_MARKERS = new Set([
  constant COMPACTION_CANCELED_STDERR_PATTERN (line 6) | const COMPACTION_CANCELED_STDERR_PATTERN =
  function normalize (line 9) | function normalize(text: string): string {
  function isBracketInterruptText (line 13) | function isBracketInterruptText(text: string): boolean {
  function isCompactionCanceledStderr (line 17) | function isCompactionCanceledStderr(text: string): boolean {
  function isInterruptSignalText (line 21) | function isInterruptSignalText(text: string): boolean {

FILE: src/utils/markdown.ts
  function appendMarkdownSnippet (line 8) | function appendMarkdownSnippet(existingPrompt: string, snippet: string):...

FILE: src/utils/mcp.ts
  function extractMcpMentions (line 1) | function extractMcpMentions(text: string, validNames: Set<string>): Set<...
  function transformMcpMentions (line 20) | function transformMcpMentions(text: string, validNames: Set<string>): st...
  function escapeRegExp (line 42) | function escapeRegExp(str: string): string {
  function parseCommand (line 46) | function parseCommand(command: string, providedArgs?: string[]): { cmd: ...
  function splitCommandString (line 59) | function splitCommandString(cmdStr: string): string[] {

FILE: src/utils/path.ts
  function getVaultPath (line 16) | function getVaultPath(app: App): string | null {
  function getEnvValue (line 28) | function getEnvValue(key: string): string | undefined {
  function expandEnvironmentVariables (line 53) | function expandEnvironmentVariables(value: string): string {
  function expandHomePath (line 93) | function expandHomePath(p: string): string {
  function stripSurroundingQuotes (line 111) | function stripSurroundingQuotes(value: string): string {
  function parsePathEntries (line 121) | function parsePathEntries(pathValue?: string): string[] {
  function dedupePaths (line 139) | function dedupePaths(entries: string[]): string[] {
  function findFirstExistingPath (line 149) | function findFirstExistingPath(entries: string[], candidates: string[]):...
  function isExistingFile (line 162) | function isExistingFile(filePath: string): boolean {
  function resolveCliJsNearPathEntry (line 174) | function resolveCliJsNearPathEntry(entry: string, isWindows: boolean): s...
  function resolveCliJsFromPathEntries (line 194) | function resolveCliJsFromPathEntries(entries: string[], isWindows: boole...
  function resolveClaudeFromPathEntries (line 204) | function resolveClaudeFromPathEntries(
  function getNpmGlobalPrefix (line 230) | function getNpmGlobalPrefix(): string | null {
  function getNpmCliJsPaths (line 247) | function getNpmCliJsPaths(): string[] {
  constant NVM_LATEST_INSTALLED_ALIASES (line 296) | const NVM_LATEST_INSTALLED_ALIASES = new Set(['node', 'stable']);
  function isNvmBuiltInLatestAlias (line 298) | function isNvmBuiltInLatestAlias(alias: string): boolean {
  function findMatchingNvmVersion (line 302) | function findMatchingNvmVersion(entries: string[], resolvedAlias: string...
  function resolveNvmAlias (line 314) | function resolveNvmAlias(nvmDir: string, alias: string, depth = 0): stri...
  function resolveNvmDefaultBin (line 336) | function resolveNvmDefaultBin(home: string): string | null {
  function findClaudeCLIPath (line 364) | function findClaudeCLIPath(pathValue?: string): string | null {
  function resolveRealPath (line 462) | function resolveRealPath(p: string): string {
  function translateMsysPath (line 503) | function translateMsysPath(value: string): string {
  function normalizePathBeforeResolution (line 525) | function normalizePathBeforeResolution(p: string): string {
  function normalizeWindowsPathPrefix (line 532) | function normalizeWindowsPathPrefix(value: string): string {
  function normalizePathForFilesystem (line 555) | function normalizePathForFilesystem(value: string): string {
  function normalizePathForComparison (line 577) | function normalizePathForComparison(value: string): string {
  function isPathWithinVault (line 603) | function isPathWithinVault(candidatePath: string, vaultPath: string): bo...
  function normalizePathForVault (line 617) | function normalizePathForVault(
  function isPathInAllowedExportPaths (line 637) | function isPathInAllowedExportPaths(
  type PathAccessType (line 669) | type PathAccessType = 'vault' | 'readwrite' | 'context' | 'export' | 'no...
  function getPathAccessType (line 675) | function getPathAccessType(

FILE: src/utils/sdkSession.ts
  type SDKSessionReadResult (line 33) | interface SDKSessionReadResult {
  type SDKNativeMessage (line 40) | interface SDKNativeMessage {
  type SDKNativeContentBlock (line 71) | interface SDKNativeContentBlock {
  function encodeVaultPathForSDK (line 94) | function encodeVaultPathForSDK(vaultPath: string): string {
  function getSDKProjectsPath (line 99) | function getSDKProjectsPath(): string {
  function isValidAgentId (line 104) | function isValidAgentId(agentId: string): boolean {
  type SubagentToolEvent (line 114) | type SubagentToolEvent =
  function parseTimestampMs (line 130) | function parseTimestampMs(raw: unknown): number {
  function parseSubagentEvents (line 136) | function parseSubagentEvents(entry: unknown): SubagentToolEvent[] {
  function buildToolCallsFromSubagentEvents (line 193) | function buildToolCallsFromSubagentEvents(events: SubagentToolEvent[]): ...
  function getSubagentSidecarPath (line 258) | function getSubagentSidecarPath(
  function loadSubagentToolCalls (line 283) | async function loadSubagentToolCalls(
  function loadSubagentFinalResult (line 326) | async function loadSubagentFinalResult(
  function isValidSessionId (line 348) | function isValidSessionId(sessionId: string): boolean {
  function getSDKSessionPath (line 368) | function getSDKSessionPath(vaultPath: string, sessionId: string): string {
  function sdkSessionExists (line 377) | function sdkSessionExists(vaultPath: string, sessionId: string): boolean {
  function deleteSDKSession (line 386) | async function deleteSDKSession(vaultPath: string, sessionId: string): P...
  function readSDKSession (line 396) | async function readSDKSession(vaultPath: string, sessionId: string): Pro...
  function extractTextContent (line 424) | function extractTextContent(content: string | SDKNativeContentBlock[] | ...
  function isRebuiltContextContent (line 442) | function isRebuiltContextContent(textContent: string): boolean {
  function extractDisplayContent (line 451) | function extractDisplayContent(textContent: string): string | undefined {
  function extractImages (line 455) | function extractImages(content: string | SDKNativeContentBlock[] | undef...
  function extractToolCalls (line 483) | function extractToolCalls(
  function mapContentBlocks (line 522) | function mapContentBlocks(content: string | SDKNativeContentBlock[] | un...
  function parseSDKMessageToChat (line 567) | function parseSDKMessageToChat(
  function collectToolResults (line 633) | function collectToolResults(sdkMessages: SDKNativeMessage[]): Map<string...
  function collectStructuredPatchResults (line 654) | function collectStructuredPatchResults(sdkMessages: SDKNativeMessage[]):...
  type AsyncSubagentResult (line 673) | interface AsyncSubagentResult {
  function collectAsyncSubagentResults (line 687) | function collectAsyncSubagentResults(
  function extractXmlTag (line 712) | function extractXmlTag(content: string, tagName: string): string | null {
  function isSystemInjectedMessage (line 731) | function isSystemInjectedMessage(sdkMsg: SDKNativeMessage): boolean {
  function filterActiveBranch (line 760) | function filterActiveBranch(
  type SDKSessionLoadResult (line 1030) | interface SDKSessionLoadResult {
  function mergeAssistantMessage (line 1040) | function mergeAssistantMessage(target: ChatMessage, source: ChatMessage)...
  function extractAgentIdFromToolUseResult (line 1084) | function extractAgentIdFromToolUseResult(toolUseResult: unknown): string...
  type ResolvedAsyncStatus (line 1105) | type ResolvedAsyncStatus = Exclude<AsyncSubagentStatus, 'pending'>;
  function resolveToolUseResultStatus (line 1111) | function resolveToolUseResultStatus(
  function buildAsyncSubagentInfo (line 1132) | function buildAsyncSubagentInfo(
  function loadSDKSessionMessages (line 1172) | async function loadSDKSessionMessages(

FILE: src/utils/session.ts
  constant SESSION_ERROR_PATTERNS (line 14) | const SESSION_ERROR_PATTERNS = [
  constant SESSION_ERROR_COMPOUND_PATTERNS (line 22) | const SESSION_ERROR_COMPOUND_PATTERNS = [
  function isSessionExpiredError (line 28) | function isSessionExpiredError(error: unknown): boolean {
  function formatToolInput (line 54) | function formatToolInput(input: Record<string, unknown>, maxLength = 200...
  function formatToolCallForContext (line 88) | function formatToolCallForContext(toolCall: ToolCallInfo, maxErrorLength...
  function truncateToolResult (line 107) | function truncateToolResult(result: string, maxLength = 500): string {
  function formatContextLine (line 114) | function formatContextLine(message: ChatMessage): string | null {
  function formatThinkingBlocks (line 125) | function formatThinkingBlocks(message: ChatMessage): string[] {
  function buildContextFromHistory (line 144) | function buildContextFromHistory(messages: ChatMessage[]): string {
  function getLastUserMessage (line 200) | function getLastUserMessage(messages: ChatMessage[]): ChatMessage | unde...
  function buildPromptWithHistoryContext (line 213) | function buildPromptWithHistoryContext(

FILE: src/utils/slashCommand.ts
  type ParsedSlashCommandContent (line 11) | interface ParsedSlashCommandContent {
  function extractFirstParagraph (line 25) | function extractFirstParagraph(content: string): string | undefined {
  function validateCommandName (line 31) | function validateCommandName(name: string): string | null {
  function isSkill (line 35) | function isSkill(cmd: SlashCommand): boolean {
  function parsedToSlashCommand (line 39) | function parsedToSlashCommand(
  function parseSlashCommandContent (line 58) | function parseSlashCommandContent(content: string): ParsedSlashCommandCo...
  function normalizeArgumentHint (line 85) | function normalizeArgumentHint(hint: string): string {
  function yamlString (line 91) | function yamlString(value: string): string {
  function serializeCommand (line 101) | function serializeCommand(cmd: SlashCommand): string {
  function serializeSlashCommandMarkdown (line 107) | function serializeSlashCommandMarkdown(cmd: Partial<SlashCommand>, body:...

FILE: src/utils/subagentJsonl.ts
  function extractFinalResultFromSubagentJsonl (line 5) | function extractFinalResultFromSubagentJsonl(content: string): string | ...

FILE: tests/__mocks__/claude-agent-sdk.ts
  type HookCallbackMatcher (line 3) | interface HookCallbackMatcher {
  type SpawnOptions (line 8) | interface SpawnOptions {
  type SpawnedProcess (line 18) | interface SpawnedProcess {
  type Options (line 30) | interface Options {
  type AgentDefinition (line 58) | type AgentDefinition = {
  type AgentMcpServerSpec (line 70) | type AgentMcpServerSpec = string | Record<string, unknown>;
  type McpServerConfig (line 72) | type McpServerConfig = Record<string, unknown>;
  type PermissionBehavior (line 74) | type PermissionBehavior = 'allow' | 'deny' | 'ask';
  type PermissionRuleValue (line 76) | type PermissionRuleValue = {
  type PermissionUpdateDestination (line 81) | type PermissionUpdateDestination = 'userSettings' | 'projectSettings' | ...
  type PermissionMode (line 83) | type PermissionMode = 'acceptEdits' | 'bypassPermissions' | 'default' | ...
  type PermissionUpdate (line 85) | type PermissionUpdate =
  type CanUseTool (line 93) | type CanUseTool = (toolName: string, input: Record<string, unknown>, opt...
  type PermissionResult (line 102) | type PermissionResult =
  function setMockMessages (line 130) | function setMockMessages(messages: any[], options?: { appendResult?: boo...
  function resetMockMessages (line 135) | function resetMockMessages() {
  function simulateCrash (line 149) | function simulateCrash(afterChunks = 0) {
  function getQueryCallCount (line 157) | function getQueryCallCount(): number {
  function getLastOptions (line 161) | function getLastOptions(): Options | undefined {
  function getLastResponse (line 165) | function getLastResponse(): typeof lastResponse {
  function runPreToolUseHooks (line 170) | async function runPreToolUseHooks(
  function isAsyncIterable (line 199) | function isAsyncIterable(value: any): value is AsyncIterable<any> {
  function getMessagesForPrompt (line 203) | function getMessagesForPrompt(): any[] {
  function query (line 264) | function query({ prompt, options }: { prompt: any; options: Options }): ...

FILE: tests/__mocks__/obsidian.ts
  class Plugin (line 3) | class Plugin {
    method constructor (line 7) | constructor(app?: any, manifest?: any) {
  class PluginSettingTab (line 20) | class PluginSettingTab {
    method constructor (line 29) | constructor(app: any, plugin: any) {
    method display (line 34) | display() {}
  class ItemView (line 37) | class ItemView {
    method constructor (line 47) | constructor(leaf: any) {
    method getViewType (line 51) | getViewType(): string {
    method getDisplayText (line 55) | getDisplayText(): string {
    method getIcon (line 59) | getIcon(): string {
  class WorkspaceLeaf (line 64) | class WorkspaceLeaf {}
  class App (line 66) | class App {
  class MarkdownView (line 81) | class MarkdownView {
    method constructor (line 85) | constructor(editor?: any, file?: any) {
  class Setting (line 91) | class Setting {
    method constructor (line 92) | constructor(containerEl: any) {}
  class TextAreaComponent (line 99) | class TextAreaComponent {
    method constructor (line 103) | constructor(_container?: any) {
    method setValue (line 114) | setValue(value: string): this {
    method getValue (line 119) | getValue(): string {
  class Modal (line 124) | class Modal {
    method constructor (line 159) | constructor(app: any) {
  function unquoteYaml (line 178) | function unquoteYaml(value: string): string {
  function parseYamlValue (line 188) | function parseYamlValue(rawValue: string): unknown {
  function parseYaml (line 211) | function parseYaml(content: string): Record<string, unknown> {
  class TFile (line 320) | class TFile {
    method constructor (line 326) | constructor(path: string = '') {
  class TFolder (line 334) | class TFolder {
    method constructor (line 339) | constructor(path: string = '') {

FILE: tests/helpers/mockElement.ts
  type MockElement (line 1) | interface MockElement {
  function createMockEl (line 50) | function createMockEl(tag = 'div'): any {

FILE: tests/helpers/sdkMessages.ts
  constant TEST_UUID (line 15) | const TEST_UUID = '00000000-0000-4000-8000-000000000001';
  constant TEST_SESSION_ID (line 16) | const TEST_SESSION_ID = 'test-session';
  constant DEFAULT_RESULT_USAGE (line 18) | const DEFAULT_RESULT_USAGE = ({
  constant DEFAULT_MODEL_USAGE (line 25) | const DEFAULT_MODEL_USAGE: SDKResultSuccess['modelUsage'] = {
  type SystemInitMessageInput (line 38) | type SystemInitMessageInput = {
  type SystemStatusMessageInput (line 43) | type SystemStatusMessageInput = {
  type CompactBoundaryMessageInput (line 48) | type CompactBoundaryMessageInput = {
  type AssistantMessageInput (line 53) | type AssistantMessageInput = {
  type UserMessageInput (line 57) | type UserMessageInput = {
  type StreamEventMessageInput (line 63) | type StreamEventMessageInput = {
  type ResultSuccessMessageInput (line 67) | type ResultSuccessMessageInput = {
  type ResultErrorMessageInput (line 72) | type ResultErrorMessageInput = {
  type ToolProgressMessageInput (line 77) | type ToolProgressMessageInput = {
  type AuthStatusMessageInput (line 81) | type AuthStatusMessageInput = {
  type SDKTestMessageInput (line 85) | type SDKTestMessageInput =
  function buildSystemInitMessage (line 97) | function buildSystemInitMessage(overrides: Partial<Omit<SDKSystemMessage...
  function buildSystemStatusMessage (line 118) | function buildSystemStatusMessage(overrides: Partial<Omit<SDKStatusMessa...
  function buildCompactBoundaryMessage (line 129) | function buildCompactBoundaryMessage(
  function buildAssistantMessage (line 145) | function buildAssistantMessage(overrides: Partial<Omit<SDKAssistantMessa...
  function buildUserMessage (line 156) | function buildUserMessage(overrides: Partial<Omit<SDKUserMessage, 'type'...
  function buildStreamEventMessage (line 166) | function buildStreamEventMessage(
  function buildResultSuccessMessage (line 182) | function buildResultSuccessMessage(
  function buildResultErrorMessage (line 204) | function buildResultErrorMessage(
  function buildToolProgressMessage (line 228) | function buildToolProgressMessage(
  function buildAuthStatusMessage (line 243) | function buildAuthStatusMessage(
  function isResultErrorInput (line 256) | function isResultErrorInput(input: ResultSuccessMessageInput | ResultErr...
  function buildSDKMessage (line 260) | function buildSDKMessage(input: SDKTestMessageInput): SDKMessage {

FILE: tests/integration/core/agent/ClaudianService.test.ts
  function createAssistantWithToolUse (line 43) | function createAssistantWithToolUse(toolName: string, toolInput: Record<...
  function createUserWithToolResult (line 55) | function createUserWithToolResult(content: string, parentToolUseId = 'to...
  function createTextUserMessage (line 64) | function createTextUserMessage(content: string) {
  function createMockMcpManager (line 77) | function createMockMcpManager() {
  function createMockPlugin (line 90) | function createMockPlugin(settings: Record<string, unknown> = {}) {

FILE: tests/integration/core/mcp/mcp.test.ts
  function createMemoryStorage (line 43) | function createMemoryStorage(initialFile?: Record<string, unknown>): {
  function createManager (line 763) | function createManager(servers: ClaudianMcpServer[]): McpServerManager {

FILE: tests/unit/core/agent/ClaudianService.test.ts
  type MockMcpServerManager (line 19) | type MockMcpServerManager = jest.Mocked<McpServerManager>;
  function collectChunks (line 26) | async function collectChunks(gen: AsyncGenerator<any>): Promise<any[]> {
  method [Symbol.asyncIterator] (line 2650) | [Symbol.asyncIterator]() { return this; }
  method next (line 2651) | async next() {
  method return (line 2660) | async return() { return { done: true, value: undefined }; }
  method [Symbol.asyncIterator] (line 2688) | [Symbol.asyncIterator]() { return this; }
  method next (line 2689) | async next() {
  method return (line 2696) | async return() { return { done: true, value: undefined }; }
  method [Symbol.asyncIterator] (line 2749) | [Symbol.asyncIterator]() { return this; }
  method next (line 2750) | async next() {
  method return (line 2755) | async return() { return { done: true, value: undefined }; }
  method [Symbol.asyncIterator] (line 2800) | [Symbol.asyncIterator]() { return this; }
  method next (line 2801) | async next() {
  method return (line 2806) | async return() { return { done: true, value: undefined }; }
  method [Symbol.asyncIterator] (line 2855) | [Symbol.asyncIterator]() { return this; }
  method next (line 2856) | async next() {
  method return (line 2861) | async return() { return { done: true, value: undefined }; }

FILE: tests/unit/core/agent/MessageChannel.test.ts
  function createTextUserMessage (line 6) | function createTextUserMessage(content: string): SDKUserMessage {
  function createImageUserMessage (line 19) | function createImageUserMessage(data = 'image-data'): SDKUserMessage {

FILE: tests/unit/core/agent/QueryOptionsBuilder.test.ts
  function createMockMcpManager (line 7) | function createMockMcpManager() {
  function createMockPluginManager (line 20) | function createMockPluginManager() {
  function createMockSettings (line 37) | function createMockSettings(overrides: Partial<ClaudianSettings> = {}): ...
  function createMockPersistentQueryConfig (line 68) | function createMockPersistentQueryConfig(
  function createMockContext (line 90) | function createMockContext(overrides: Partial<QueryOptionsContext> = {})...

FILE: tests/unit/core/agents/AgentManager.test.ts
  function createMockPluginManager (line 16) | function createMockPluginManager(plugins: Array<{ name: string; enabled:...
  function createMockDirent (line 29) | function createMockDirent(name: string, isFile: boolean): fs.Dirent {
  constant VALID_AGENT_FILE (line 45) | const VALID_AGENT_FILE = `---
  constant MINIMAL_AGENT_FILE (line 54) | const MINIMAL_AGENT_FILE = `---
  constant PLUGIN_AGENT_FILE (line 60) | const PLUGIN_AGENT_FILE = `---
  constant INVALID_AGENT_FILE (line 68) | const INVALID_AGENT_FILE = `---

FILE: tests/unit/core/mcp/createNodeFetch.test.ts
  type ReceivedRequest (line 6) | interface ReceivedRequest {
  function createTestServer (line 13) | function createTestServer(handler?: (req: ReceivedRequest, res: http.Ser...

FILE: tests/unit/core/plugins/PluginManager.test.ts
  function createMockCCSettingsStorage (line 27) | function createMockCCSettingsStorage() {

FILE: tests/unit/core/storage/McpStorage.test.ts
  type MockAdapter (line 5) | type MockAdapter = VaultFileAdapter & { _store: Record<string, string> };
  function createMockAdapter (line 8) | function createMockAdapter(files: Record<string, string> = {}): MockAdap...

FILE: tests/unit/core/storage/SkillStorage.test.ts
  function createMockAdapter (line 4) | function createMockAdapter(files: Record<string, string> = {}): VaultFil...

FILE: tests/unit/core/storage/storage.test.ts
  type SessionMetaRecord (line 276) | interface SessionMetaRecord {
  type SessionMessageRecord (line 287) | interface SessionMessageRecord {
  type SessionRecord (line 292) | type SessionRecord = SessionMetaRecord | SessionMessageRecord;
  function parseJSONLHelper (line 294) | function parseJSONLHelper(content: string): Conversation | null {
  function serializeToJSONLHelper (line 328) | function serializeToJSONLHelper(conversation: Conversation): string {

FILE: tests/unit/core/storage/storageService.convenience.test.ts
  function createMockAdapter (line 6) | function createMockAdapter(initialFiles: Record<string, string> = {}) {
  function createMockPlugin (line 49) | function createMockPlugin(options: {

FILE: tests/unit/core/storage/storageService.migration.test.ts
  type AdapterOptions (line 6) | type AdapterOptions = {
  function createMockAdapter (line 10) | function createMockAdapter(
  function createMockPlugin (line 74) | function createMockPlugin(options: {

FILE: tests/unit/features/chat/controllers/BrowserSelectionController.test.ts
  function createMockIndicator (line 5) | function createMockIndicator() {
  function createMockContextRow (line 11) | function createMockContextRow(browserIndicator: HTMLElement) {
  function flushMicrotasks (line 30) | async function flushMicrotasks(): Promise<void> {

FILE: tests/unit/features/chat/controllers/CanvasSelectionController.test.ts
  function createMockIndicator (line 3) | function createMockIndicator() {
  function createMockContextRow (line 10) | function createMockContextRow() {
  function createMockCanvasNode (line 26) | function createMockCanvasNode(id: string) {

FILE: tests/unit/features/chat/controllers/ConversationController.test.ts
  function createMockDeps (line 14) | function createMockDeps(overrides: Partial<ConversationControllerDeps> =...

FILE: tests/unit/features/chat/controllers/InputController.test.ts
  function createMockInputEl (line 21) | function createMockInputEl() {
  function createMockWelcomeEl (line 28) | function createMockWelcomeEl() {
  function createMockFileContextManager (line 32) | function createMockFileContextManager() {
  function createMockImageContextManager (line 42) | function createMockImageContextManager() {
  function createMockAgentService (line 57) | function createMockAgentService() {
  function createMockInstructionRefineService (line 71) | function createMockInstructionRefineService(overrides: Record<string, je...
  function createMockInstructionModeManager (line 81) | function createMockInstructionModeManager() {
  function createMockDeps (line 85) | function createMockDeps(overrides: Partial<InputControllerDeps> = {}): I...
  function createSendableDeps (line 175) | function createSendableDeps(

FILE: tests/unit/features/chat/controllers/NavigationController.test.ts
  type Listener (line 3) | type Listener = (event: any) => void;
  class MockKeyboardEvent (line 6) | class MockKeyboardEvent {
    method constructor (line 18) | constructor(type: string, options: {
    method preventDefault (line 37) | preventDefault(): void {
    method stopPropagation (line 43) | stopPropagation(): void {
    method defaultPreventedValue (line 47) | get defaultPreventedValue(): boolean {
  class MockElement (line 58) | class MockElement {
    method constructor (line 66) | constructor(tagName = 'DIV') {
    method setAttribute (line 70) | setAttribute(name: string, value: string): void {
    method getAttribute (line 74) | getAttribute(name: string): string | null {
    method addClass (line 78) | addClass(cls: string): void {
    method removeClass (line 82) | removeClass(cls: string): void {
    method hasClass (line 86) | hasClass(cls: string): boolean {
    method addEventListener (line 90) | addEventListener(type: string, listener: Listener, options?: AddEventL...
    method removeEventListener (line 98) | removeEventListener(type: string, listener: Listener, options?: AddEve...
    method dispatchEvent (line 109) | dispatchEvent(event: KeyboardEvent): boolean {
    method focus (line 123) | focus(): void {
    method blur (line 127) | blur(): void {

FILE: tests/unit/features/chat/controllers/SelectionController.test.ts
  function createMockIndicator (line 9) | function createMockIndicator() {
  function createMockInput (line 16) | function createMockInput() {
  function createMockContextRow (line 33) | function createMockContextRow() {

FILE: tests/unit/features/chat/controllers/StreamController.test.ts
  function createMockDeps (line 56) | function createMockDeps(): StreamControllerDeps {
  function createTestMessage (line 114) | function createTestMessage(): ChatMessage {
  function createMockUsage (line 125) | function createMockUsage(overrides: Record<string, any> = {}) {

FILE: tests/unit/features/chat/controllers/contextRowVisibility.test.ts
  function createContextRow (line 3) | function createContextRow(browserIndicator: HTMLElement | null): HTMLEle...

FILE: tests/unit/features/chat/rendering/DiffRenderer.test.ts
  function countByClass (line 8) | function countByClass(el: any, cls: string): number {
  function makeInsertLines (line 15) | function makeInsertLines(n: number): DiffLine[] {

FILE: tests/unit/features/chat/rendering/InlineAskUserQuestion.test.ts
  function makeInput (line 14) | function makeInput(
  function renderWidget (line 25) | function renderWidget(
  function fireKeyDown (line 36) | function fireKeyDown(
  function findRoot (line 51) | function findRoot(container: any): any {
  function findItems (line 55) | function findItems(container: any): any[] {
  function renderImmediateWidget (line 564) | function renderImmediateWidget(

FILE: tests/unit/features/chat/rendering/InlineExitPlanMode.test.ts
  function fireKeyDown (line 16) | function fireKeyDown(root: any, key: string): void {
  function findRoot (line 25) | function findRoot(container: any): any {
  function findItems (line 29) | function findItems(root: any): any[] {

FILE: tests/unit/features/chat/rendering/MessageRenderer.test.ts
  function createMockComponent (line 32) | function createMockComponent() {
  function createRenderer (line 42) | function createRenderer(messagesEl?: any) {
  function setupDocumentMock (line 1197) | function setupDocumentMock() {

FILE: tests/unit/features/chat/rendering/ToolCallRenderer.test.ts
  function createToolCall (line 23) | function createToolCall(overrides: Partial<ToolCallInfo> = {}): ToolCall...

FILE: tests/unit/features/chat/rendering/WriteEditRenderer.test.ts
  function createToolCall (line 12) | function createToolCall(overrides: Partial<ToolCallInfo> = {}): ToolCall...
  function createDiffData (line 23) | function createDiffData(overrides: Partial<ToolDiffData> = {}): ToolDiff...
  function getTextContent (line 412) | function getTextContent(element: any): string {

FILE: tests/unit/features/chat/services/InstructionRefineService.test.ts
  function createMockPlugin (line 11) | function createMockPlugin(settings = {}) {

FILE: tests/unit/features/chat/services/SubagentManager.test.ts
  function setupRunningSubagent (line 1356) | function setupRunningSubagent(manager: SubagentManager) {

FILE: tests/unit/features/chat/services/TitleGenerationService.test.ts
  function createMockPlugin (line 9) | function createMockPlugin(settings = {}) {

FILE: tests/unit/features/chat/tabs/Tab.test.ts
  class MockResizeObserver (line 21) | class MockResizeObserver {
    method constructor (line 23) | constructor(callback: ResizeObserverCallback) {
  function createMockPlugin (line 305) | function createMockPlugin(overrides: Record<string, any> = {}): any {
  function createMockMcpManager (line 336) | function createMockMcpManager(): any {
  function createMockOptions (line 343) | function createMockOptions(overrides: Partial<TabCreateOptions> = {}): T...
  function setupForkTest (line 2134) | function setupForkTest(overrides: Record<string, any> = {}) {
  function setupForkAllTest (line 2420) | function setupForkAllTest(overrides: Record<string, any> = {}) {

FILE: tests/unit/features/chat/tabs/TabBar.test.ts
  function createMockCallbacks (line 7) | function createMockCallbacks(): TabBarCallbacks {
  function createTabBarItem (line 16) | function createTabBarItem(overrides: Partial<TabBarItem> = {}): TabBarIt...

FILE: tests/unit/features/chat/tabs/TabManager.test.ts
  function createMockPlugin (line 40) | function createMockPlugin(overrides: Record<string, any> = {}): any {
  function createMockMcpManager (line 58) | function createMockMcpManager(): any {
  function createMockView (line 62) | function createMockView(): any {
  function createMockTabData (line 69) | function createMockTabData(overrides: Record<string, any> = {}): any {
  function createManager (line 111) | function createManager(options: {
  function setupTitleTest (line 1600) | function setupTitleTest(existingTitles: string[] = []) {

FILE: tests/unit/features/chat/ui/BangBashModeManager.test.ts
  function createWrapper (line 3) | function createWrapper() {
  function createKeyEvent (line 10) | function createKeyEvent(key: string, options: { shiftKey?: boolean } = {...

FILE: tests/unit/features/chat/ui/ExternalContextSelector.test.ts
  function createMockCallbacks (line 18) | function createMockCallbacks() {

FILE: tests/unit/features/chat/ui/FileContextManager.test.ts
  function createMockTFile (line 18) | function createMockTFile(filePath: string): TFile {
  function findByClass (line 46) | function findByClass(root: MockElement, className: string): MockElement ...
  function findAllByClass (line 55) | function findAllByClass(root: MockElement, className: string): MockEleme...
  function createMockApp (line 67) | function createMockApp(options: {
  function createMockCallbacks (line 101) | function createMockCallbacks(options: {

FILE: tests/unit/features/chat/ui/ImageContext.test.ts
  function createMockCallbacks (line 25) | function createMockCallbacks() {
  function createContainerWithInputWrapper (line 31) | function createContainerWithInputWrapper(): { container: any; inputWrapp...
  function createMockTextArea (line 37) | function createMockTextArea(): any {
  function createImageAttachment (line 43) | function createImageAttachment(overrides: Partial<ImageAttachment> = {})...

FILE: tests/unit/features/chat/ui/InputToolbar.test.ts
  function makeUsage (line 18) | function makeUsage(overrides: Partial<UsageInfo> = {}): UsageInfo {
  function createMockCallbacks (line 30) | function createMockCallbacks(overrides: Record<string, any> = {}) {
  function createMockMcpManager (line 405) | function createMockMcpManager(servers: { name: string; enabled: boolean;...
  function createMockMcpManager (line 653) | function createMockMcpManager(servers: { name: string; enabled: boolean;...

FILE: tests/unit/features/chat/ui/InstructionModeManager.test.ts
  function createWrapper (line 3) | function createWrapper() {
  function createKeyEvent (line 10) | function createKeyEvent(key: string, options: { shiftKey?: boolean } = {...

FILE: tests/unit/features/chat/ui/NavigationSidebar.test.ts
  type Listener (line 10) | type Listener = (event: any) => void;
  class MockClassList (line 12) | class MockClassList {
    method add (line 15) | add(...items: string[]): void {
    method remove (line 19) | remove(...items: string[]): void {
    method contains (line 23) | contains(item: string): boolean {
    method toggle (line 27) | toggle(item: string, force?: boolean): void {
    method clear (line 43) | clear(): void {
    method toArray (line 47) | toArray(): string[] {
  class MockElement (line 52) | class MockElement {
    method constructor (line 69) | constructor(tagName: string) {
    method className (line 73) | set className(value: string) {
    method className (line 78) | get className(): string {
    method scrollHeight (line 82) | get scrollHeight(): number {
    method scrollHeight (line 86) | set scrollHeight(value: number) {
    method clientHeight (line 90) | get clientHeight(): number {
    method clientHeight (line 94) | set clientHeight(value: number) {
    method scrollTop (line 98) | get scrollTop(): number {
    method scrollTop (line 102) | set scrollTop(value: number) {
    method scrollTo (line 106) | scrollTo(options: { top: number; behavior: string }): void {
    method appendChild (line 111) | appendChild(child: MockElement): MockElement {
    method remove (line 117) | remove(): void {
    method setAttribute (line 123) | setAttribute(name: string, value: string): void {
    method getAttribute (line 127) | getAttribute(name: string): string | null {
    method addEventListener (line 131) | addEventListener(type: string, listener: Listener, _options?: any): vo...
    method removeEventListener (line 138) | removeEventListener(type: string, listener: Listener): void {
    method dispatchEvent (line 143) | dispatchEvent(event: any): void {
    method click (line 150) | click(): void {
    method empty (line 154) | empty(): void {
    method createDiv (line 159) | createDiv(options?: { cls?: string; text?: string; attr?: Record<strin...
    method querySelector (line 172) | querySelector(selector: string): MockElement | null {
    method querySelectorAll (line 176) | querySelectorAll(selector: string): MockElement[] {
  function addUserMessage (line 353) | function addUserMessage(el: MockElement, offset: number): MockElement {
  function addAssistantMessage (line 359) | function addAssistantMessage(el: MockElement, offset: number): MockEleme...
  function addConversation (line 365) | function addConversation(el: MockElement, userOffsets: number[], assista...
  function getButtons (line 377) | function getButtons(parent: MockElement) {

FILE: tests/unit/features/chat/ui/StatusPanel.test.ts
  type Listener (line 12) | type Listener = (event: any) => void;
  class MockClassList (line 14) | class MockClassList {
    method add (line 17) | add(...items: string[]): void {
    method remove (line 21) | remove(...items: string[]): void {
    method contains (line 25) | contains(item: string): boole
Condensed preview — 375 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,025K chars).
[
  {
    "path": ".eslintrc.cjs",
    "chars": 1904,
    "preview": "/** @type {import('eslint').Linter.Config} */\nmodule.exports = {\n  root: true,\n  ignorePatterns: ['dist/', 'node_modules"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1037,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  lint:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "chars": 1952,
    "preview": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file"
  },
  {
    "path": ".github/workflows/claude.yml",
    "chars": 1886,
    "preview": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issue"
  },
  {
    "path": ".github/workflows/duplicate-issues.yml",
    "chars": 591,
    "preview": "name: Potential Duplicates\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  check-duplicates:\n    runs-on: ubuntu-late"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1706,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      con"
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 630,
    "preview": "name: Close stale issues\n\non:\n  schedule:\n    - cron: '0 0 * * *' # Runs daily at midnight UTC\n  workflow_dispatch: # Al"
  },
  {
    "path": ".gitignore",
    "chars": 1019,
    "preview": "# Build output\nmain.js\nstyles.css\n*.js.map\ndist/\nbuild/\n\n# Dependencies\nnode_modules/\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": ".npmrc",
    "chars": 38,
    "preview": "tag-version-prefix=\"\"\nloglevel=silent\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 66,
    "preview": "## Agents\n\nRead CLAUDE.md for the agent overview and instructions."
  },
  {
    "path": "CLAUDE.md",
    "chars": 4362,
    "preview": "# CLAUDE.md\n\n## Project Overview\n\nClaudian - An Obsidian plugin that embeds Claude Code as a sidebar chat interface. The"
  },
  {
    "path": "LICENSE",
    "chars": 1055,
    "preview": "MIT License\n\nCopyright (c) 2025\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this so"
  },
  {
    "path": "README.md",
    "chars": 14300,
    "preview": "# Claudian\n\n![GitHub stars](https://img.shields.io/github/stars/YishenTu/claudian?style=social)\n![GitHub release](https:"
  },
  {
    "path": "esbuild.config.mjs",
    "chars": 2232,
    "preview": "import esbuild from 'esbuild';\nimport path from 'path';\nimport process from 'process';\nimport builtins from 'builtin-mod"
  },
  {
    "path": "jest.config.js",
    "chars": 1125,
    "preview": "/** @type {import('ts-jest').JestConfigWithTsJest} */\nconst baseConfig = {\n  preset: 'ts-jest',\n  testEnvironment: 'node"
  },
  {
    "path": "manifest.json",
    "chars": 418,
    "preview": "{\n  \"id\": \"claudian\",\n  \"name\": \"Claudian\",\n  \"version\": \"1.3.70\",\n  \"minAppVersion\": \"1.4.5\",\n  \"description\": \"Embeds "
  },
  {
    "path": "package.json",
    "chars": 1506,
    "preview": "{\n  \"name\": \"claudian\",\n  \"version\": \"1.3.70\",\n  \"description\": \"Claudian - Claude Code embedded in Obsidian sidebar\",\n "
  },
  {
    "path": "scripts/build-css.mjs",
    "chars": 3542,
    "preview": "#!/usr/bin/env node\n/**\n * CSS Build Script\n * Concatenates modular CSS files from src/style/ into root styles.css\n */\n\n"
  },
  {
    "path": "scripts/build.mjs",
    "chars": 593,
    "preview": "#!/usr/bin/env node\n/**\n * Combined build script - runs CSS build then esbuild\n * Avoids npm echoing commands\n */\n\nimpor"
  },
  {
    "path": "scripts/postinstall.mjs",
    "chars": 734,
    "preview": "#!/usr/bin/env node\n/**\n * Post-install script - copies .env.local.example to .env.local if it doesn't exist\n */\n\nimport"
  },
  {
    "path": "scripts/run-jest.js",
    "chars": 494,
    "preview": "const { spawnSync } = require('child_process');\nconst os = require('os');\nconst path = require('path');\n\nconst jestPath "
  },
  {
    "path": "scripts/sync-version.js",
    "chars": 533,
    "preview": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst packagePath = path.join(__dirname, '"
  },
  {
    "path": "src/core/CLAUDE.md",
    "chars": 3364,
    "preview": "# Core Infrastructure\n\nCore modules have **no feature dependencies**. Features depend on core, never the reverse.\n\n## Mo"
  },
  {
    "path": "src/core/agent/ClaudianService.ts",
    "chars": 63467,
    "preview": "/**\n * Claudian - Claude Agent SDK wrapper\n *\n * Handles communication with Claude via the Agent SDK. Manages streaming,"
  },
  {
    "path": "src/core/agent/MessageChannel.ts",
    "chars": 6834,
    "preview": "/**\n * Message Channel\n *\n * Queue-based async iterable for persistent queries.\n * Handles message queuing, turn managem"
  },
  {
    "path": "src/core/agent/QueryOptionsBuilder.ts",
    "chars": 12807,
    "preview": "/**\n * QueryOptionsBuilder - SDK Options Construction\n *\n * Extracts options-building logic from ClaudianService for:\n *"
  },
  {
    "path": "src/core/agent/SessionManager.ts",
    "chars": 3324,
    "preview": "/**\n * Session Manager\n *\n * Manages SDK session state including session ID, model tracking,\n * and interruption state.\n"
  },
  {
    "path": "src/core/agent/customSpawn.ts",
    "chars": 1879,
    "preview": "/**\n * Custom spawn logic for Claude Agent SDK.\n *\n * Provides a custom spawn function that resolves the full path to No"
  },
  {
    "path": "src/core/agent/index.ts",
    "chars": 523,
    "preview": "export { type ApprovalCallback, type ApprovalCallbackOptions, ClaudianService, type QueryOptions } from './ClaudianServi"
  },
  {
    "path": "src/core/agent/types.ts",
    "chars": 4738,
    "preview": "/**\n * Types and constants for the ClaudianService module.\n */\n\nimport type { SDKMessage, SDKUserMessage } from '@anthro"
  },
  {
    "path": "src/core/agents/AgentManager.ts",
    "chars": 6267,
    "preview": "/**\n * Agent load order (earlier sources take precedence for duplicate IDs):\n * 0. Built-in agents: dynamically provided"
  },
  {
    "path": "src/core/agents/AgentStorage.ts",
    "chars": 3400,
    "preview": "import { extractStringArray, isRecord, normalizeStringArray, parseFrontmatter } from '../../utils/frontmatter';\nimport {"
  },
  {
    "path": "src/core/agents/index.ts",
    "chars": 123,
    "preview": "export { AgentManager } from './AgentManager';\nexport { buildAgentFromFrontmatter, parseAgentFile } from './AgentStorage"
  },
  {
    "path": "src/core/commands/builtInCommands.ts",
    "chars": 2699,
    "preview": "/**\n * Claudian - Built-in slash commands\n *\n * System commands that perform actions (not prompt expansions).\n * These a"
  },
  {
    "path": "src/core/commands/index.ts",
    "chars": 196,
    "preview": "export {\n  BUILT_IN_COMMANDS,\n  type BuiltInCommand,\n  type BuiltInCommandAction,\n  type BuiltInCommandResult,\n  detectB"
  },
  {
    "path": "src/core/hooks/SecurityHooks.ts",
    "chars": 4981,
    "preview": "/**\n * Security Hooks\n *\n * PreToolUse hooks for enforcing blocklist and vault restriction.\n */\n\nimport type { HookCallb"
  },
  {
    "path": "src/core/hooks/SubagentHooks.ts",
    "chars": 824,
    "preview": "import type { HookCallbackMatcher } from '@anthropic-ai/claude-agent-sdk';\n\nexport interface SubagentHookState {\n  hasRu"
  },
  {
    "path": "src/core/hooks/index.ts",
    "chars": 232,
    "preview": "export {\n  type BlocklistContext,\n  createBlocklistHook,\n  createVaultRestrictionHook,\n  type VaultRestrictionContext,\n}"
  },
  {
    "path": "src/core/mcp/McpServerManager.ts",
    "chars": 3710,
    "preview": "/**\n * McpServerManager - Core MCP server configuration management.\n *\n * Infrastructure layer for loading, filtering, a"
  },
  {
    "path": "src/core/mcp/McpTester.ts",
    "chars": 8776,
    "preview": "import { Client } from '@modelcontextprotocol/sdk/client';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk"
  },
  {
    "path": "src/core/mcp/index.ts",
    "chars": 158,
    "preview": "export { McpServerManager, type McpStorageAdapter } from './McpServerManager';\nexport { type McpTestResult, type McpTool"
  },
  {
    "path": "src/core/plugins/PluginManager.ts",
    "chars": 5770,
    "preview": "/**\n * PluginManager - Discover and manage Claude Code plugins.\n *\n * Plugins are discovered from two sources:\n * - inst"
  },
  {
    "path": "src/core/plugins/index.ts",
    "chars": 49,
    "preview": "export { PluginManager } from './PluginManager';\n"
  },
  {
    "path": "src/core/prompts/inlineEdit.ts",
    "chars": 5328,
    "preview": "/**\n * Claudian - Inline Edit System Prompt\n *\n * Builds the system prompt for inline text editing (read-only tools).\n *"
  },
  {
    "path": "src/core/prompts/instructionRefine.ts",
    "chars": 2711,
    "preview": "/**\n * Claudian - Instruction Refine System Prompt\n *\n * Builds the system prompt for instruction refinement.\n */\n\nexpor"
  },
  {
    "path": "src/core/prompts/mainAgent.ts",
    "chars": 14681,
    "preview": "/**\n * Claudian - Main Agent System Prompt\n *\n * Builds the system prompt for the Claude Agent SDK including\n * Obsidian"
  },
  {
    "path": "src/core/prompts/titleGeneration.ts",
    "chars": 750,
    "preview": "/**\n * Claudian - Title Generation System Prompt\n *\n * System prompt for generating conversation titles.\n */\n\nexport con"
  },
  {
    "path": "src/core/sdk/index.ts",
    "chars": 255,
    "preview": "export type { TransformOptions } from './transformSDKMessage';\nexport { transformSDKMessage } from './transformSDKMessag"
  },
  {
    "path": "src/core/sdk/toolResultContent.ts",
    "chars": 1046,
    "preview": "interface ToolResultContentOptions {\n  fallbackIndent?: number;\n}\n\n/**\n * Agent/Subagent tool results can arrive as text"
  },
  {
    "path": "src/core/sdk/transformSDKMessage.ts",
    "chars": 9782,
    "preview": "import type { SDKMessage, SDKResultError } from '@anthropic-ai/claude-agent-sdk';\n\nimport type { SDKToolUseResult, Usage"
  },
  {
    "path": "src/core/sdk/typeGuards.ts",
    "chars": 360,
    "preview": "import type { StreamChunk } from '../types';\nimport type { SessionInitEvent, TransformEvent } from './types';\n\nexport fu"
  },
  {
    "path": "src/core/sdk/types.ts",
    "chars": 239,
    "preview": "import type { StreamChunk } from '../types';\n\nexport interface SessionInitEvent {\n  type: 'session_init';\n  sessionId: s"
  },
  {
    "path": "src/core/security/ApprovalManager.ts",
    "chars": 6135,
    "preview": "/** Permission utilities for tool action approval. */\n\nimport type { PermissionUpdate, PermissionUpdateDestination } fro"
  },
  {
    "path": "src/core/security/BashPathValidator.ts",
    "chars": 12008,
    "preview": "/**\n * Bash Path Validator\n *\n * Pure functions for parsing bash commands and validating path access.\n * Extracted from "
  },
  {
    "path": "src/core/security/BlocklistChecker.ts",
    "chars": 751,
    "preview": "/**\n * Blocklist Checker\n *\n * Checks bash commands against user-defined blocklist patterns.\n * Patterns are treated as "
  },
  {
    "path": "src/core/security/index.ts",
    "chars": 581,
    "preview": "export {\n  buildPermissionUpdates,\n  getActionDescription,\n  getActionPattern,\n  matchesRulePattern,\n} from './ApprovalM"
  },
  {
    "path": "src/core/storage/AgentVaultStorage.ts",
    "chars": 3009,
    "preview": "import { serializeAgent } from '../../utils/agent';\nimport { buildAgentFromFrontmatter, parseAgentFile } from '../agents"
  },
  {
    "path": "src/core/storage/CCSettingsStorage.ts",
    "chars": 8941,
    "preview": "/**\n * CCSettingsStorage - Handles CC-compatible settings.json read/write.\n *\n * Manages the .claude/settings.json file "
  },
  {
    "path": "src/core/storage/ClaudianSettingsStorage.ts",
    "chars": 6014,
    "preview": "/**\n * ClaudianSettingsStorage - Handles claudian-settings.json read/write.\n *\n * Manages the .claude/claudian-settings."
  },
  {
    "path": "src/core/storage/McpStorage.ts",
    "chars": 7805,
    "preview": "/**\n * McpStorage - Handles .claude/mcp.json read/write\n *\n * MCP server configurations are stored in Claude Code-compat"
  },
  {
    "path": "src/core/storage/SessionStorage.ts",
    "chars": 13088,
    "preview": "/**\n * SessionStorage - Handles chat session files in vault/.claude/sessions/\n *\n * Each conversation is stored as a JSO"
  },
  {
    "path": "src/core/storage/SkillStorage.ts",
    "chars": 1749,
    "preview": "import { parsedToSlashCommand, parseSlashCommandContent, serializeCommand } from '../../utils/slashCommand';\nimport type"
  },
  {
    "path": "src/core/storage/SlashCommandStorage.ts",
    "chars": 2807,
    "preview": "import { parsedToSlashCommand, parseSlashCommandContent, serializeCommand } from '../../utils/slashCommand';\nimport type"
  },
  {
    "path": "src/core/storage/StorageService.ts",
    "chars": 18291,
    "preview": "/**\n * StorageService - Main coordinator for distributed storage system.\n *\n * Manages:\n * - CC settings in .claude/sett"
  },
  {
    "path": "src/core/storage/VaultFileAdapter.ts",
    "chars": 3885,
    "preview": "/**\n * VaultFileAdapter - Wrapper around Obsidian Vault API for file operations.\n *\n * Provides a consistent interface f"
  },
  {
    "path": "src/core/storage/index.ts",
    "chars": 729,
    "preview": "export { AGENTS_PATH, AgentVaultStorage } from './AgentVaultStorage';\nexport { CC_SETTINGS_PATH, CCSettingsStorage, isLe"
  },
  {
    "path": "src/core/storage/migrationConstants.ts",
    "chars": 3786,
    "preview": "/**\n * Migration Constants - Shared constants for storage migration.\n *\n * Single source of truth for fields that need t"
  },
  {
    "path": "src/core/tools/index.ts",
    "chars": 1186,
    "preview": "export {\n  extractLastTodosFromMessages,\n  parseTodoInput,\n  type TodoItem,\n} from './todo';\nexport { getToolIcon, MCP_I"
  },
  {
    "path": "src/core/tools/todo.ts",
    "chars": 1907,
    "preview": "/**\n * Todo tool helpers.\n *\n * Parses TodoWrite tool input into typed todo items.\n */\n\nimport { TOOL_TODO_WRITE } from "
  },
  {
    "path": "src/core/tools/toolIcons.ts",
    "chars": 1557,
    "preview": "import {\n  TOOL_AGENT_OUTPUT,\n  TOOL_ASK_USER_QUESTION,\n  TOOL_BASH,\n  TOOL_BASH_OUTPUT,\n  TOOL_EDIT,\n  TOOL_ENTER_PLAN_"
  },
  {
    "path": "src/core/tools/toolInput.ts",
    "chars": 3352,
    "preview": "/**\n * Tool input helpers.\n *\n * Keeps parsing of common tool inputs consistent across services.\n */\n\nimport type { AskU"
  },
  {
    "path": "src/core/tools/toolNames.ts",
    "chars": 3919,
    "preview": "export const TOOL_AGENT_OUTPUT = 'TaskOutput' as const;\nexport const TOOL_ASK_USER_QUESTION = 'AskUserQuestion' as const"
  },
  {
    "path": "src/core/types/agent.ts",
    "chars": 2075,
    "preview": "export const AGENT_PERMISSION_MODES = ['default', 'acceptEdits', 'dontAsk', 'bypassPermissions', 'plan', 'delegate'] as "
  },
  {
    "path": "src/core/types/chat.ts",
    "chars": 7472,
    "preview": "/**\n * Chat and conversation type definitions.\n */\n\nimport type { SDKToolUseResult } from './diff';\nimport type { Subage"
  },
  {
    "path": "src/core/types/diff.ts",
    "chars": 641,
    "preview": "/**\n * Diff-related type definitions.\n */\n\nexport interface DiffLine {\n  type: 'equal' | 'insert' | 'delete';\n  text: st"
  },
  {
    "path": "src/core/types/index.ts",
    "chars": 2722,
    "preview": "// Chat types\nexport {\n  type ChatMessage,\n  type ContentBlock,\n  type Conversation,\n  type ConversationMeta,\n  type For"
  },
  {
    "path": "src/core/types/mcp.ts",
    "chars": 2850,
    "preview": "/**\n * Claudian - MCP (Model Context Protocol) type definitions\n *\n * Types for configuring and managing MCP servers tha"
  },
  {
    "path": "src/core/types/models.ts",
    "chars": 3606,
    "preview": "/**\n * Model type definitions and constants.\n */\n\n/** Model identifier (string to support custom models via environment "
  },
  {
    "path": "src/core/types/plugins.ts",
    "chars": 536,
    "preview": "export type PluginScope = 'user' | 'project';\n\nexport interface ClaudianPlugin {\n  /** e.g., \"plugin-name@source\" */\n  i"
  },
  {
    "path": "src/core/types/sdk.ts",
    "chars": 571,
    "preview": "import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';\n\nexport type { SDKMessage } from '@anthropic-ai/cl"
  },
  {
    "path": "src/core/types/settings.ts",
    "chars": 14313,
    "preview": "/**\n * Settings type definitions.\n */\n\nimport type { Locale } from '../../i18n/types';\nimport type { ClaudeModel, Effort"
  },
  {
    "path": "src/core/types/tools.ts",
    "chars": 2048,
    "preview": "/**\n * Tool-related type definitions.\n */\n\nimport type { DiffLine, DiffStats } from './diff';\n\n/** Diff data for Write/E"
  },
  {
    "path": "src/features/chat/CLAUDE.md",
    "chars": 6475,
    "preview": "# Chat Feature\n\nMain sidebar chat interface. `ClaudianView` is a thin shell; logic lives in controllers and services.\n\n#"
  },
  {
    "path": "src/features/chat/ClaudianView.ts",
    "chars": 22448,
    "preview": "import type { EventRef, WorkspaceLeaf } from 'obsidian';\nimport { ItemView, Notice, Scope, setIcon } from 'obsidian';\n\ni"
  },
  {
    "path": "src/features/chat/constants.ts",
    "chars": 3803,
    "preview": "export const LOGO_SVG = {\n  viewBox: '0 -.01 39.5 39.53',\n  width: '18',\n  height: '18',\n  path: 'm7.75 26.27 7.77-4.36."
  },
  {
    "path": "src/features/chat/controllers/BrowserSelectionController.ts",
    "chars": 9662,
    "preview": "import type { App, ItemView } from 'obsidian';\n\nimport type { BrowserSelectionContext } from '../../../utils/browser';\ni"
  },
  {
    "path": "src/features/chat/controllers/CanvasSelectionController.ts",
    "chars": 3734,
    "preview": "import type { App, ItemView } from 'obsidian';\n\nimport type { CanvasSelectionContext } from '../../../utils/canvas';\nimp"
  },
  {
    "path": "src/features/chat/controllers/ConversationController.ts",
    "chars": 33212,
    "preview": "import { Notice, setIcon } from 'obsidian';\n\nimport type { ClaudianService } from '../../../core/agent';\nimport type { C"
  },
  {
    "path": "src/features/chat/controllers/InputController.ts",
    "chars": 38394,
    "preview": "import { Notice } from 'obsidian';\n\nimport type { ApprovalCallbackOptions, ClaudianService } from '../../../core/agent';"
  },
  {
    "path": "src/features/chat/controllers/NavigationController.ts",
    "chars": 6088,
    "preview": "import type { KeyboardNavigationSettings } from '../../../core/types';\n\n/** Scroll speed in pixels per frame (~60fps = 4"
  },
  {
    "path": "src/features/chat/controllers/SelectionController.ts",
    "chars": 7821,
    "preview": "import type { App } from 'obsidian';\nimport { MarkdownView } from 'obsidian';\n\nimport { hideSelectionHighlight, showSele"
  },
  {
    "path": "src/features/chat/controllers/StreamController.ts",
    "chars": 34962,
    "preview": "import { TFile } from 'obsidian';\n\nimport type { ClaudianService } from '../../../core/agent';\nimport { extractResolvedA"
  },
  {
    "path": "src/features/chat/controllers/contextRowVisibility.ts",
    "chars": 1152,
    "preview": "export function updateContextRowHasContent(contextRowEl: HTMLElement): void {\n  const editorIndicator = contextRowEl.que"
  },
  {
    "path": "src/features/chat/controllers/index.ts",
    "chars": 592,
    "preview": "export { BrowserSelectionController } from './BrowserSelectionController';\nexport { CanvasSelectionController } from './"
  },
  {
    "path": "src/features/chat/rendering/DiffRenderer.ts",
    "chars": 3904,
    "preview": "import type { DiffLine } from '../../../core/types/diff';\n\nexport interface DiffHunk {\n  lines: DiffLine[];\n  oldStart: "
  },
  {
    "path": "src/features/chat/rendering/InlineAskUserQuestion.ts",
    "chars": 21807,
    "preview": "import type { AskUserQuestionItem, AskUserQuestionOption } from '../../../core/types/tools';\n\nconst HINTS_TEXT = 'Enter "
  },
  {
    "path": "src/features/chat/rendering/InlineExitPlanMode.ts",
    "chars": 9313,
    "preview": "import * as nodePath from 'path';\n\nimport type { ExitPlanModeDecision } from '../../../core/types/tools';\nimport type { "
  },
  {
    "path": "src/features/chat/rendering/MessageRenderer.ts",
    "chars": 24613,
    "preview": "import type { App, Component } from 'obsidian';\nimport { MarkdownRenderer, Notice } from 'obsidian';\n\nimport { isSubagen"
  },
  {
    "path": "src/features/chat/rendering/SubagentRenderer.ts",
    "chars": 21271,
    "preview": "import { setIcon } from 'obsidian';\n\nimport { getToolIcon, TOOL_TASK } from '../../../core/tools';\nimport type { Subagen"
  },
  {
    "path": "src/features/chat/rendering/ThinkingBlockRenderer.ts",
    "chars": 3924,
    "preview": "import { collapseElement, setupCollapsible } from './collapsible';\n\nexport type RenderContentFn = (el: HTMLElement, mark"
  },
  {
    "path": "src/features/chat/rendering/TodoListRenderer.ts",
    "chars": 111,
    "preview": "export {\n  extractLastTodosFromMessages,\n  parseTodoInput,\n  type TodoItem,\n} from '../../../core/tools/todo';\n"
  },
  {
    "path": "src/features/chat/rendering/ToolCallRenderer.ts",
    "chars": 21339,
    "preview": "import { setIcon } from 'obsidian';\n\nimport { extractResolvedAnswersFromResultText, type TodoItem } from '../../../core/"
  },
  {
    "path": "src/features/chat/rendering/WriteEditRenderer.ts",
    "chars": 7923,
    "preview": "import { setIcon } from 'obsidian';\n\nimport { getToolIcon } from '../../../core/tools';\nimport type { ToolCallInfo, Tool"
  },
  {
    "path": "src/features/chat/rendering/collapsible.ts",
    "chars": 2992,
    "preview": "export interface CollapsibleState {\n  isExpanded: boolean;\n}\n\nexport interface CollapsibleOptions {\n  /** Initial expand"
  },
  {
    "path": "src/features/chat/rendering/index.ts",
    "chars": 1084,
    "preview": "export { MessageRenderer } from './MessageRenderer';\nexport {\n  addSubagentToolCall,\n  type AsyncSubagentState,\n  create"
  },
  {
    "path": "src/features/chat/rendering/todoUtils.ts",
    "chars": 879,
    "preview": "import { setIcon } from 'obsidian';\n\nimport type { TodoItem } from '../../../core/tools';\n\nexport function getTodoStatus"
  },
  {
    "path": "src/features/chat/rewind.ts",
    "chars": 970,
    "preview": "import type { ChatMessage } from '../../core/types';\n\nexport interface RewindContext {\n  prevAssistantUuid: string | und"
  },
  {
    "path": "src/features/chat/services/BangBashService.ts",
    "chars": 1629,
    "preview": "import { exec } from 'child_process';\n\nexport interface BangBashResult {\n  command: string;\n  stdout: string;\n  stderr: "
  },
  {
    "path": "src/features/chat/services/InstructionRefineService.ts",
    "chars": 6124,
    "preview": "import type { Options } from '@anthropic-ai/claude-agent-sdk';\nimport { query as agentQuery } from '@anthropic-ai/claude"
  },
  {
    "path": "src/features/chat/services/SubagentManager.ts",
    "chars": 35569,
    "preview": "import { existsSync, readFileSync, realpathSync } from 'fs';\nimport { tmpdir } from 'os';\nimport { isAbsolute, sep } fro"
  },
  {
    "path": "src/features/chat/services/TitleGenerationService.ts",
    "chars": 6743,
    "preview": "import type { Options } from '@anthropic-ai/claude-agent-sdk';\nimport { query as agentQuery } from '@anthropic-ai/claude"
  },
  {
    "path": "src/features/chat/state/ChatState.ts",
    "chars": 10518,
    "preview": "import type { UsageInfo } from '../../../core/types';\nimport type {\n  ChatMessage,\n  ChatStateCallbacks,\n  ChatStateData"
  },
  {
    "path": "src/features/chat/state/index.ts",
    "chars": 316,
    "preview": "export { ChatState, createInitialState } from './ChatState';\nexport type {\n  ChatMessage,\n  ChatStateCallbacks,\n  ChatSt"
  },
  {
    "path": "src/features/chat/state/types.ts",
    "chars": 4341,
    "preview": "import type { EditorView } from '@codemirror/view';\n\nimport type { TodoItem } from '../../../core/tools';\nimport type {\n"
  },
  {
    "path": "src/features/chat/tabs/Tab.ts",
    "chars": 42913,
    "preview": "import type { Component } from 'obsidian';\nimport { Notice } from 'obsidian';\n\nimport { ClaudianService } from '../../.."
  },
  {
    "path": "src/features/chat/tabs/TabBar.ts",
    "chars": 2337,
    "preview": "import type { TabBarItem, TabId } from './types';\n\n/** Callbacks for TabBar interactions. */\nexport interface TabBarCall"
  },
  {
    "path": "src/features/chat/tabs/TabManager.ts",
    "chars": 19508,
    "preview": "import { Notice } from 'obsidian';\n\nimport type { ClaudianService } from '../../../core/agent';\nimport type { McpServerM"
  },
  {
    "path": "src/features/chat/tabs/index.ts",
    "chars": 104,
    "preview": "export * from './Tab';\nexport * from './TabBar';\nexport * from './TabManager';\nexport * from './types';\n"
  },
  {
    "path": "src/features/chat/tabs/types.ts",
    "chars": 7590,
    "preview": "import type { Component, WorkspaceLeaf } from 'obsidian';\n\nimport type { ClaudianService } from '../../../core/agent';\ni"
  },
  {
    "path": "src/features/chat/ui/BangBashModeManager.ts",
    "chars": 2889,
    "preview": "import { Notice } from 'obsidian';\n\nimport { t } from '../../../i18n';\n\nexport interface BangBashModeCallbacks {\n  onSub"
  },
  {
    "path": "src/features/chat/ui/FileContext.ts",
    "chars": 12557,
    "preview": "import type { App, EventRef } from 'obsidian';\nimport { Notice, TFile } from 'obsidian';\n\nimport type { AgentManager } f"
  },
  {
    "path": "src/features/chat/ui/ImageContext.ts",
    "chars": 10841,
    "preview": "import { Notice } from 'obsidian';\nimport * as path from 'path';\n\nimport type { ImageAttachment, ImageMediaType } from '"
  },
  {
    "path": "src/features/chat/ui/InputToolbar.ts",
    "chars": 32713,
    "preview": "import { Notice, setIcon } from 'obsidian';\nimport * as path from 'path';\n\nimport type { McpServerManager } from '../../"
  },
  {
    "path": "src/features/chat/ui/InstructionModeManager.ts",
    "chars": 4453,
    "preview": "export interface InstructionModeCallbacks {\n  onSubmit: (rawInstruction: string) => Promise<void>;\n  getInputWrapper: ()"
  },
  {
    "path": "src/features/chat/ui/NavigationSidebar.ts",
    "chars": 3801,
    "preview": "import { setIcon } from 'obsidian';\n\n/**\n * Floating sidebar for navigating chat history.\n * Provides quick access to to"
  },
  {
    "path": "src/features/chat/ui/StatusPanel.ts",
    "chars": 20219,
    "preview": "import { Notice, setIcon } from 'obsidian';\n\nimport type { TodoItem } from '../../../core/tools';\nimport { getToolIcon, "
  },
  {
    "path": "src/features/chat/ui/file-context/state/FileContextState.ts",
    "chars": 1875,
    "preview": "export class FileContextState {\n  private attachedFiles: Set<string> = new Set();\n  private sessionStarted = false;\n  pr"
  },
  {
    "path": "src/features/chat/ui/file-context/view/FileChipsView.ts",
    "chars": 2070,
    "preview": "import { setIcon } from 'obsidian';\n\nexport interface FileChipsViewCallbacks {\n  onRemoveAttachment: (path: string) => v"
  },
  {
    "path": "src/features/chat/ui/index.ts",
    "chars": 742,
    "preview": "export { type BangBashModeCallbacks, BangBashModeManager, type BangBashModeState } from './BangBashModeManager';\nexport "
  },
  {
    "path": "src/features/inline-edit/InlineEditService.ts",
    "chars": 11321,
    "preview": "import type { HookCallbackMatcher, Options } from '@anthropic-ai/claude-agent-sdk';\nimport { query as agentQuery } from "
  },
  {
    "path": "src/features/inline-edit/ui/InlineEditModal.ts",
    "chars": 23692,
    "preview": "import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';\nimport type { DecorationSet } from '@codem"
  },
  {
    "path": "src/features/settings/ClaudianSettings.ts",
    "chars": 30985,
    "preview": "import * as fs from 'fs';\nimport type { App } from 'obsidian';\nimport { Notice, PluginSettingTab, Setting } from 'obsidi"
  },
  {
    "path": "src/features/settings/keyboardNavigation.ts",
    "chars": 1785,
    "preview": "import type { KeyboardNavigationSettings } from '@/core/types/settings';\n\nconst NAV_ACTIONS = ['scrollUp', 'scrollDown',"
  },
  {
    "path": "src/features/settings/ui/AgentSettings.ts",
    "chars": 12184,
    "preview": "import type { App } from 'obsidian';\nimport { Modal, Notice, setIcon, Setting } from 'obsidian';\n\nimport type { AgentDef"
  },
  {
    "path": "src/features/settings/ui/EnvSnippetManager.ts",
    "chars": 11251,
    "preview": "import type { App } from 'obsidian';\nimport { Modal, Notice, setIcon, Setting } from 'obsidian';\n\nimport type { EnvSnipp"
  },
  {
    "path": "src/features/settings/ui/McpServerModal.ts",
    "chars": 10365,
    "preview": "import type { App } from 'obsidian';\nimport { Modal, Notice, Setting } from 'obsidian';\n\nimport type {\n  ClaudianMcpServ"
  },
  {
    "path": "src/features/settings/ui/McpSettingsManager.ts",
    "chars": 12901,
    "preview": "import { Notice, setIcon } from 'obsidian';\n\nimport { testMcpServer } from '../../../core/mcp/McpTester';\nimport { McpSt"
  },
  {
    "path": "src/features/settings/ui/McpTestModal.ts",
    "chars": 10874,
    "preview": "import type { App } from 'obsidian';\nimport { Modal, Notice, setIcon } from 'obsidian';\n\nimport type { McpTestResult, Mc"
  },
  {
    "path": "src/features/settings/ui/PluginSettingsManager.ts",
    "chars": 4591,
    "preview": "import { Notice, setIcon } from 'obsidian';\n\nimport type { ClaudianPlugin as ClaudianPluginType } from '../../../core/ty"
  },
  {
    "path": "src/features/settings/ui/SlashCommandSettings.ts",
    "chars": 15118,
    "preview": "import type { App, ToggleComponent } from 'obsidian';\nimport { Modal, Notice, setIcon, Setting } from 'obsidian';\n\nimpor"
  },
  {
    "path": "src/i18n/constants.ts",
    "chars": 1801,
    "preview": "/**\n * i18n Constants and Utilities\n *\n * Centralized constants for language management and UI display\n */\n\nimport type "
  },
  {
    "path": "src/i18n/i18n.ts",
    "chars": 3042,
    "preview": "/**\n * i18n - Internationalization service for Claudian\n *\n * Provides translation functionality for all UI strings.\n * "
  },
  {
    "path": "src/i18n/index.ts",
    "chars": 380,
    "preview": "// Types\nexport type { LocaleInfo } from './constants';\nexport type { Locale, TranslationKey } from './types';\n\n// Core "
  },
  {
    "path": "src/i18n/locales/de.json",
    "chars": 14087,
    "preview": "{\n  \"common\": {\n    \"save\": \"Speichern\",\n    \"cancel\": \"Abbrechen\",\n    \"delete\": \"Löschen\",\n    \"edit\": \"Bearbeiten\",\n "
  },
  {
    "path": "src/i18n/locales/en.json",
    "chars": 12549,
    "preview": "{\n  \"common\": {\n    \"save\": \"Save\",\n    \"cancel\": \"Cancel\",\n    \"delete\": \"Delete\",\n    \"edit\": \"Edit\",\n    \"add\": \"Add\""
  },
  {
    "path": "src/i18n/locales/es.json",
    "chars": 14076,
    "preview": "{\n  \"common\": {\n    \"save\": \"Guardar\",\n    \"cancel\": \"Cancelar\",\n    \"delete\": \"Eliminar\",\n    \"edit\": \"Editar\",\n    \"ad"
  },
  {
    "path": "src/i18n/locales/fr.json",
    "chars": 14419,
    "preview": "{\n  \"common\": {\n    \"save\": \"Enregistrer\",\n    \"cancel\": \"Annuler\",\n    \"delete\": \"Supprimer\",\n    \"edit\": \"Modifier\",\n "
  },
  {
    "path": "src/i18n/locales/ja.json",
    "chars": 9895,
    "preview": "{\n  \"common\": {\n    \"save\": \"保存\",\n    \"cancel\": \"キャンセル\",\n    \"delete\": \"削除\",\n    \"edit\": \"編集\",\n    \"add\": \"追加\",\n    \"rem"
  },
  {
    "path": "src/i18n/locales/ko.json",
    "chars": 9892,
    "preview": "{\n  \"common\": {\n    \"save\": \"저장\",\n    \"cancel\": \"취소\",\n    \"delete\": \"삭제\",\n    \"edit\": \"편집\",\n    \"add\": \"추가\",\n    \"remove"
  },
  {
    "path": "src/i18n/locales/pt.json",
    "chars": 13651,
    "preview": "{\n  \"common\": {\n    \"save\": \"Salvar\",\n    \"cancel\": \"Cancelar\",\n    \"delete\": \"Excluir\",\n    \"edit\": \"Editar\",\n    \"add\""
  },
  {
    "path": "src/i18n/locales/ru.json",
    "chars": 13828,
    "preview": "{\n  \"common\": {\n    \"save\": \"Сохранить\",\n    \"cancel\": \"Отмена\",\n    \"delete\": \"Удалить\",\n    \"edit\": \"Редактировать\",\n "
  },
  {
    "path": "src/i18n/locales/zh-CN.json",
    "chars": 8646,
    "preview": "{\n  \"common\": {\n    \"save\": \"保存\",\n    \"cancel\": \"取消\",\n    \"delete\": \"删除\",\n    \"edit\": \"编辑\",\n    \"add\": \"添加\",\n    \"remove"
  },
  {
    "path": "src/i18n/locales/zh-TW.json",
    "chars": 8679,
    "preview": "{\n  \"common\": {\n    \"save\": \"保存\",\n    \"cancel\": \"取消\",\n    \"delete\": \"刪除\",\n    \"edit\": \"編輯\",\n    \"add\": \"添加\",\n    \"remove"
  },
  {
    "path": "src/i18n/types.ts",
    "chars": 7757,
    "preview": "/**\n * i18n type definitions\n */\n\nexport type Locale = 'en' | 'zh-CN' | 'zh-TW' | 'ja' | 'ko' | 'de' | 'fr' | 'es' | 'ru"
  },
  {
    "path": "src/main.ts",
    "chars": 42567,
    "preview": "/**\n * Claudian - Obsidian plugin entry point\n *\n * Registers the sidebar chat view, settings tab, and commands.\n * Mana"
  },
  {
    "path": "src/shared/components/ResumeSessionDropdown.ts",
    "chars": 5626,
    "preview": "/**\n * Claudian - Resume session dropdown\n *\n * Dropup UI for selecting a previous conversation to resume.\n * Shown when"
  },
  {
    "path": "src/shared/components/SelectableDropdown.ts",
    "chars": 3757,
    "preview": "export interface SelectableDropdownOptions {\n  listClassName: string;\n  itemClassName: string;\n  emptyClassName: string;"
  },
  {
    "path": "src/shared/components/SelectionHighlight.ts",
    "chars": 2437,
    "preview": "/**\n * SelectionHighlight - Shared CM6 selection highlight for chat and inline edit\n *\n * Provides a reusable mechanism "
  },
  {
    "path": "src/shared/components/SlashCommandDropdown.ts",
    "chars": 11349,
    "preview": "/**\n * Claudian - Slash command dropdown\n *\n * Dropdown UI for selecting slash commands when typing /.\n * Follows the Fi"
  },
  {
    "path": "src/shared/icons.ts",
    "chars": 1228,
    "preview": "export const MCP_ICON_SVG = `<svg fill=\"currentColor\" fill-rule=\"evenodd\" height=\"1em\" viewBox=\"0 0 24 24\" width=\"1em\" x"
  },
  {
    "path": "src/shared/index.ts",
    "chars": 795,
    "preview": "export {\n  SelectableDropdown,\n  type SelectableDropdownOptions,\n  type SelectableDropdownRenderOptions,\n} from './compo"
  },
  {
    "path": "src/shared/mention/MentionDropdownController.ts",
    "chars": 20912,
    "preview": "import type { TFile } from 'obsidian';\nimport { setIcon } from 'obsidian';\n\nimport { buildExternalContextDisplayEntries "
  },
  {
    "path": "src/shared/mention/VaultMentionCache.ts",
    "chars": 2429,
    "preview": "import type { App, TFile } from 'obsidian';\nimport { TFolder } from 'obsidian';\n\nexport interface VaultFileCacheOptions "
  },
  {
    "path": "src/shared/mention/VaultMentionDataProvider.ts",
    "chars": 1264,
    "preview": "import type { App, TFile } from 'obsidian';\n\nimport { VaultFileCache, VaultFolderCache } from './VaultMentionCache';\n\nex"
  },
  {
    "path": "src/shared/mention/types.ts",
    "chars": 1373,
    "preview": "import type { TFile } from 'obsidian';\n\nexport interface FileMentionItem {\n  type: 'file';\n  name: string;\n  path: strin"
  },
  {
    "path": "src/shared/modals/ConfirmModal.ts",
    "chars": 1550,
    "preview": "import { type App,Modal, Setting } from 'obsidian';\n\nimport { t } from '../../i18n';\n\nexport function confirmDelete(app:"
  },
  {
    "path": "src/shared/modals/ForkTargetModal.ts",
    "chars": 1308,
    "preview": "import { type App, Modal } from 'obsidian';\n\nimport { t } from '../../i18n';\n\nexport type ForkTarget = 'new-tab' | 'curr"
  },
  {
    "path": "src/shared/modals/InstructionConfirmModal.ts",
    "chars": 9670,
    "preview": "/**\n * Claudian - Instruction modal\n *\n * Unified modal that handles all instruction mode states:\n * - Loading (initial "
  },
  {
    "path": "src/style/CLAUDE.md",
    "chars": 2375,
    "preview": "# CSS Style Guide\n\n## Structure\n\n```\nsrc/style/\n├── base/           # container, animations (@keyframes), variables\n├── "
  },
  {
    "path": "src/style/accessibility.css",
    "chars": 1300,
    "preview": "/* Accessibility - Focus Visible Styles */\n\n/* outline + offset + border-radius */\n.claudian-tool-header:focus-visible,\n"
  },
  {
    "path": "src/style/base/animations.css",
    "chars": 616,
    "preview": "@keyframes thinking-pulse {\n  0%,\n  100% {\n    opacity: 0.5;\n  }\n\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes spin {\n  to "
  },
  {
    "path": "src/style/base/container.css",
    "chars": 117,
    "preview": ".claudian-container {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  padding: 0;\n  overflow: hidden;\n}\n"
  },
  {
    "path": "src/style/base/variables.css",
    "chars": 224,
    "preview": "/* Brand & semantic color tokens */\n.claudian-container {\n  --claudian-brand: #D97757;\n  --claudian-brand-rgb: 217, 119,"
  },
  {
    "path": "src/style/components/code.css",
    "chars": 2406,
    "preview": "/* Code block wrapper - contains pre + button outside scroll area */\n.claudian-code-wrapper {\n  position: relative;\n  ma"
  },
  {
    "path": "src/style/components/context-footer.css",
    "chars": 1563,
    "preview": "/* Context usage meter (inline in toolbar) */\n\n.claudian-context-meter {\n  position: relative;\n  display: flex;\n  align-"
  },
  {
    "path": "src/style/components/header.css",
    "chars": 1414,
    "preview": "/* Header - logo, title/tabs slot, and actions */\n.claudian-header {\n  display: flex;\n  align-items: center;\n  padding: "
  },
  {
    "path": "src/style/components/history.css",
    "chars": 3806,
    "preview": ".claudian-history-container {\n  position: relative;\n}\n\n/* History dropup menu (opens upward since it's at bottom of view"
  },
  {
    "path": "src/style/components/input.css",
    "chars": 4878,
    "preview": "/* Input area */\n.claudian-input-container {\n  position: relative;\n  padding: 12px 0 0 0;\n}\n\n/* Input wrapper (border co"
  },
  {
    "path": "src/style/components/messages.css",
    "chars": 5001,
    "preview": "/* Messages wrapper (for scroll-to-bottom button positioning) */\n.claudian-messages-wrapper {\n  position: relative;\n  fl"
  },
  {
    "path": "src/style/components/nav-sidebar.css",
    "chars": 1161,
    "preview": "/* Navigation Sidebar */\n.claudian-nav-sidebar {\n    position: absolute;\n    right: 2px;\n    top: 50%;\n    transform: tr"
  },
  {
    "path": "src/style/components/status-panel.css",
    "chars": 4185,
    "preview": "/* Status Panel - persistent bottom panel for todos and command output */\n\n.claudian-status-panel-container {\n  flex-shr"
  },
  {
    "path": "src/style/components/subagent.css",
    "chars": 4717,
    "preview": ".claudian-subagent-list {\n  margin: 8px 0;\n}\n\n.claudian-text-block+.claudian-subagent-list {\n  margin-top: 8px;\n}\n\n.clau"
  },
  {
    "path": "src/style/components/tabs.css",
    "chars": 1329,
    "preview": ".claudian-tab-bar-container {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.claudian-tab-badges {\n  display: f"
  },
  {
    "path": "src/style/components/thinking.css",
    "chars": 1671,
    "preview": ".claudian-thinking {\n  color: var(--claudian-brand);\n  font-style: italic;\n  padding: 4px 0;\n  text-align: start;\n  anim"
  },
  {
    "path": "src/style/components/toolcalls.css",
    "chars": 4798,
    "preview": ".claudian-tool-call {\n  margin: 8px 0;\n}\n\n.claudian-tool-header {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  "
  },
  {
    "path": "src/style/features/ask-user-question.css",
    "chars": 5842,
    "preview": "/* AskUserQuestion - inline widget rendered in chat panel */\n\n.claudian-ask-question-inline {\n  font-family: var(--font-"
  },
  {
    "path": "src/style/features/diff.css",
    "chars": 3696,
    "preview": "/* Write/Edit Diff Block - Subagent style */\n.claudian-write-edit-block {\n  margin: 4px 0;\n  background: transparent;\n  "
  },
  {
    "path": "src/style/features/file-context.css",
    "chars": 3820,
    "preview": "/* @ Mention dropdown */\n.claudian-mention-dropdown {\n  display: none;\n  position: absolute;\n  bottom: 100%;\n  left: 0;\n"
  },
  {
    "path": "src/style/features/file-link.css",
    "chars": 467,
    "preview": "/* Clickable file links that open files in Obsidian */\n.claudian-file-link {\n  color: var(--text-accent);\n  text-decorat"
  },
  {
    "path": "src/style/features/image-context.css",
    "chars": 3470,
    "preview": "/* Image Context - Preview & Attachments */\n\n/* Image preview container (in input area) */\n.claudian-image-preview {\n  d"
  },
  {
    "path": "src/style/features/image-embed.css",
    "chars": 911,
    "preview": "/* Image embed styles - displays ![[image.png]] wikilinks as actual images */\n\n.claudian-embedded-image {\n  display: inl"
  },
  {
    "path": "src/style/features/image-modal.css",
    "chars": 1065,
    "preview": "/* Full-size Image Modal */\n.claudian-image-modal-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom"
  },
  {
    "path": "src/style/features/inline-edit.css",
    "chars": 3443,
    "preview": "/* Inline Edit (CM6 decorations) */\n\n/* Selection highlight (shared by inline edit and chat) */\n.cm-line .claudian-selec"
  },
  {
    "path": "src/style/features/plan-mode.css",
    "chars": 2102,
    "preview": "/* Plan Mode - inline cards for EnterPlanMode / ExitPlanMode */\n\n.claudian-plan-approval-inline {\n  font-family: var(--f"
  },
  {
    "path": "src/style/features/resume-session.css",
    "chars": 2378,
    "preview": "/* Resume Session Dropdown */\n.claudian-resume-dropdown {\n  display: none;\n  position: absolute;\n  bottom: 100%;\n  left:"
  },
  {
    "path": "src/style/features/slash-commands.css",
    "chars": 1745,
    "preview": "/* Slash Command Dropdown */\n.claudian-slash-dropdown {\n  display: none;\n  position: absolute;\n  bottom: 100%;\n  left: 0"
  },
  {
    "path": "src/style/index.css",
    "chars": 1781,
    "preview": "/* CSS module order. scripts/build-css.mjs reads these @import lines. */\n/* Add new modules here to include them in the "
  },
  {
    "path": "src/style/modals/fork-target.css",
    "chars": 384,
    "preview": "/* Fork Target Modal */\n.claudian-fork-target-modal {\n  max-width: 340px;\n}\n\n.claudian-fork-target-list {\n  display: fle"
  },
  {
    "path": "src/style/modals/instruction.css",
    "chars": 3438,
    "preview": "/* Instruction Mode */\n\n/* Instruction Confirm Modal */\n.claudian-instruction-modal {\n  max-width: 500px;\n}\n\n.claudian-i"
  },
  {
    "path": "src/style/modals/mcp-modal.css",
    "chars": 4422,
    "preview": "/* MCP Server Modal */\n.claudian-mcp-modal .modal-content {\n  width: 480px;\n  max-width: 90vw;\n}\n\n.claudian-mcp-type-fie"
  }
]

// ... and 175 more files (download for full content)

About this extraction

This page contains the full source code of the YishenTu/claudian GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 375 files (3.7 MB), approximately 985.8k tokens, and a symbol index with 2313 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!