Full Code of Snowflyt/typora-copilot for AI

main df7593492de8 cached
100 files
658.3 KB
170.3k tokens
548 symbols
1 requests
Download .txt
Showing preview only (699K chars total). Download the full file or copy to clipboard to get everything.
Repository: Snowflyt/typora-copilot
Branch: main
Commit: df7593492de8
Files: 100
Total size: 658.3 KB

Directory structure:
gitextract_4vih3sn_/

├── .editorconfig
├── .githooks/
│   └── commit-msg
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── README.zh-CN.md
├── bin/
│   ├── install_linux.sh
│   ├── install_macos.sh
│   ├── install_windows.ps1
│   ├── uninstall_linux.sh
│   ├── uninstall_macos.sh
│   └── uninstall_windows.ps1
├── commitlint.config.js
├── eslint.config.js
├── install.ps1
├── install.sh
├── package.json
├── pre-commit.ts
├── prettier.config.cjs
├── rollup.config.ts
├── src/
│   ├── client/
│   │   ├── chat.ts
│   │   ├── client.ts
│   │   ├── general-client.ts
│   │   └── index.ts
│   ├── completion.ts
│   ├── components/
│   │   ├── ChatPanel.scss
│   │   ├── ChatPanel.tsx
│   │   ├── CopilotIcon.tsx
│   │   ├── DropdownWithInput.scss
│   │   ├── DropdownWithInput.tsx
│   │   ├── ModalBody.tsx
│   │   ├── ModalCloseButton.scss
│   │   ├── ModalCloseButton.tsx
│   │   ├── ModalContent.tsx
│   │   ├── ModalOverlay.scss
│   │   ├── ModalOverlay.tsx
│   │   ├── ModalTitle.tsx
│   │   ├── ModelHeader.tsx
│   │   ├── SettingsPanel.tsx
│   │   ├── Spinner.tsx
│   │   ├── SuggestionPanel.scss
│   │   ├── SuggestionPanel.tsx
│   │   ├── Switch.scss
│   │   ├── Switch.tsx
│   │   ├── icons/
│   │   │   ├── index.ts
│   │   │   ├── nodejs.tsx
│   │   │   └── settings.tsx
│   │   └── preact-env.d.ts
│   ├── constants.ts
│   ├── errors/
│   │   ├── CommandError.ts
│   │   ├── NoFreePortError.ts
│   │   ├── PlatformError.ts
│   │   └── index.ts
│   ├── footer.scss
│   ├── footer.tsx
│   ├── global.d.ts
│   ├── i18n/
│   │   ├── en.json
│   │   ├── index.ts
│   │   ├── t.spec.ts
│   │   ├── t.ts
│   │   └── zh-CN.json
│   ├── index.ts
│   ├── logging.ts
│   ├── mac-server.ts
│   ├── main.ts
│   ├── modules/
│   │   ├── fs.ts
│   │   ├── path.spec.ts
│   │   ├── path.ts
│   │   ├── url.spec.ts
│   │   └── url.ts
│   ├── patches/
│   │   ├── index.ts
│   │   ├── jquery.ts
│   │   ├── promise.spec.ts
│   │   ├── promise.ts
│   │   └── typora.ts
│   ├── reset.d.ts
│   ├── settings.ts
│   ├── styles.scss
│   ├── types/
│   │   ├── lsp.ts
│   │   └── tools.ts
│   ├── typora-utils.ts
│   └── utils/
│       ├── cli-tools.ts
│       ├── diff.ts
│       ├── dom.ts
│       ├── function.ts
│       ├── logging.ts
│       ├── lsp.ts
│       ├── node-bridge.ts
│       ├── observable.ts
│       ├── random.ts
│       ├── stream.ts
│       ├── tools.proof.ts
│       └── tools.ts
├── stylelint.config.js
├── test/
│   └── setup.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.ts

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

================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,css,scss,json}]
charset = utf-8
indent_style = space
indent_size = 2


================================================
FILE: .githooks/commit-msg
================================================
#!/bin/sh
npx --no -- commitlint --edit "$1"
npx tsx pre-commit.ts


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

on:
  push:
    branches:
      - main

  pull_request:
    branches:
      - main

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Typecheck
        run: npm run typecheck

  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x, 22.x, 24.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
      - name: Checkout the repository
        uses: actions/checkout@v4

      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

      - name: Install dependencies
        run: npm ci --ignore-scripts

      - name: Test types
        run: npm run test-types

      - name: Test
        run: npm test


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: .vscode/settings.json
================================================
{
  "cSpell.words": ["reqnode"],
  "stylelint.validate": ["css", "scss"]
}


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

Copyright (c) 2023 Snowflyt

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
================================================
# Typora Copilot

English | [简体中文](./README.zh-CN.md)

![Copilot suggestion screenshot](./docs/screenshot.png)

[GitHub Copilot](https://github.com/features/copilot) & [Copilot Chat](https://docs.github.com/copilot/using-github-copilot/copilot-chat) plugin for [Typora](https://typora.io/) on both Windows, macOS and Linux.

This plugin uses the [official GitHub Copilot LSP server](https://www.npmjs.com/package/@github/copilot-language-server) to provide suggestions in real-time right from your editor.

## Compatibility

> [!NOTE]
>
> Since Typora v1.10, all platforms require [Node.js](https://nodejs.org/en/download) ≥ 20 to use this plugin.
>
> (For those special users on Windows / Linux using Typora 1.9, no need for Node.js to be installed. :wink:)

_\*Note: `/` means not tested._

| Typora Version | Windows 11 | Ubuntu 24.04 | macOS 15.x |
| -------------- | ---------- | ------------ | ---------- |
| 1.12.6         | /          | /            | ✓          |
| 1.12.4         | ✓          | /            | /          |
| 1.11.7         | ✓          | /            | ✓          |
| 1.10.8         | ✓          | ✓            | ✓          |
| 1.10.6         | ✓          | ✓            | ✓          |
| 1.9.5          | ✓          | /            | /          |
| 1.9.4          | /          | /            | ✓          |
| 1.9.3          | /          | ✓            | /          |
| 1.8.10         | ✓          | ✓            | ✓          |
| 1.8.8          | /          | ✓            | /          |
| 1.8.6          | ✓          | /            | /          |
| 1.8.5          | ✓          | /            | ✓          |
| 1.7.6          | ✓          | /            | /          |
| 1.6.7          | ✓          | /            | /          |
| 1.5.12         | ✓          | /            | /          |
| 1.4.8          | ✓          | /            | /          |
| 1.3.8          | ✓          | /            | /          |
| 1.2.5          | ✓          | /            | /          |
| 1.2.3          | ✓          | /            | /          |
| 1.0.3          | ✓          | /            | /          |
| 0.11.18-beta   | ✓          | /            | /          |

## Prerequisites

- Public network connection.
- Active GitHub Copilot subscription.

## Installation

**Before installing using any method, make sure Typora is fully closed (especially on macOS: use <kbd>⌘</kbd>+<kbd>Q</kbd> to quit).**

### Automated Installation (Recommended)

To install the plugin, you can just copy and paste the following command into your terminal:

<details>
  <summary><strong>Windows</strong></summary>

Run the following command in PowerShell **as administrator**:

```powershell
iwr -Uri "https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.ps1" | iex
```

</details>

<details>
  <summary><strong>macOS</strong></summary>

Run the following command in your terminal:

```bash
curl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash
```

</details>

<details>
  <summary><strong>Linux</strong></summary>

Run the following command in your terminal:

```bash
wget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash
```

</details>

### Script Install

<details>
  <summary><strong>Windows</strong></summary>

For Windows users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. Then locate to the folder where you unzipped the release and run the following command in PowerShell **as administrator**:

```powershell
.\bin\install_windows.ps1
```

If the script fails to find Typora, you can specify the path to Typora manually:

```powershell
.\bin\install_windows.ps1 -Path "C:\Program Files\Typora\" # Replace with your Typora path
# Or use the alias
# .\bin\install_windows.ps1 -p "C:\Program Files\Typora\" # Replace with your Typora path
```

</details>

<details>
  <summary><strong>macOS</strong></summary>

For macOS users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. Then locate to the folder where you unzipped the release and run the following command in terminal:

```bash
sudo bash ./bin/install_macos.sh
```

If the script fails to find Typora, you can specify the path to Typora manually:

```bash
sudo bash ./bin/install_macos.sh --path "/Applications/Typora.app/" # Replace with your Typora path
# Or use the alias
# sudo bash ./bin/install_macos.sh -p "/Applications/Typora.app/" # Replace with your Typora path
```

You’ll see a message logging the installation directory of the plugin. _Keep it in mind, you’ll need it when uninstalling the plugin._ After that, you can safely delete the release folder.

</details>

<details>
  <summary><strong>Linux</strong></summary>

For Linux users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. THen locate to the folder where you unzipped the release and run the following command in terminal:

```bash
sudo bash ./bin/install_linux.sh
```

If the script fails to find Typora, you can specify the path to Typora manually:

```bash
sudo bash ./bin/install_linux.sh --path "/usr/share/typora/" # Replace with your Typora path
# Or use the alias
# sudo bash ./bin/install_linux.sh -p "/usr/share/typora/" # Replace with your Typora path
```

You’ll see a message logging the installation directory of the plugin. _Keep it in mind, you’ll need it when uninstalling the plugin._ After that, you can safely delete the release folder.

</details>

### Manual Install

<details>
  <summary>Click to expand</summary>

1. Download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it.
2. For Windows / Linux users, find `window.html` in your Typora installation folder, usually located at `<typora_root_path>/resources/`; For macOS users, find `index.html` in your Typora installation folder, usually located at `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` is the path where Typora is installed, replace it with your real Typora installation path (note that the angle brackets `<` and `>` should also be removed). This folder is called Typora resource folder in the following steps.
3. Create a folder named `copilot` in Typora resource folder.
4. Copy the contents of the unzipped release into the `copilot` folder. **Ensure the final path is `copilot/index.js` (not `copilot/typora-copilot/index.js`). If you see the latter, move the files up one level so `index.js` sits directly under `copilot`.**
5. For Windows / Linux users, open the previous `window.html` file you found in Typora resource folder with a text editor, and add `<script src="./copilot/index.js" defer="defer"></script>` right after something like `<script src="./appsrc/window/frame.js" defer="defer"></script>` or `<script src="./app/window/frame.js" defer="defer"></script>`; For macOS users, open the previous `index.html` file you found in Typora resource folder with a text editor, and add `<script src="./copilot/index.js" defer></script>` right after something like `<script src="./appsrc/main.js" aria-hidden="true" defer></script>` or `<script src="./app/main.js" aria-hidden="true" defer></script>`.
6. Restart Typora.
7. For macOS users, if you see a warning dialog saying Typora may be damaged, Ctrl-click Typora and select “Open” to open Typora.

</details>

## Setup

When finished installation, you’ll find an icon in the toolbar of Typora (i.e. the bottom-right corner of Typora). Click **the arrow button** next to the icon to open the panel of Copilot, and then click “Sign in to authenticate Copilot”.

![Copilot icon](./docs/toolbar-icon.png)

Follow the prompts to authenticate Copilot plugin:

1. The User Code will be auto copied to your clipboard.
2. Follow the instructions on the pop-up dialog to open the GitHub authentication page in your browser.
3. Paste the User Code into the GitHub authentication page.
4. Return to Typora and press OK on the dialog.
5. If you see a “Signed in to Copilot” dialog _after a few seconds_, Copilot plugin should start working since then.

## Copilot Chat

Clicking the Copilot icon in the toolbar will toggle the Copilot Chat panel. You can use it to chat with Copilot, and the current document and previous chat history will be sent to Copilot as context.

Make sure you have signed in to Copilot before using the Copilot Chat panel. After signing in, restart Typora to make sure the Copilot Chat panel works properly.

You can:

- Select, create, edit chat title, or delete a chat session from the dropdown list at the top of the panel.
- Click the “Send” button or press <kbd>Enter</kbd> to send the message. (You can use <kbd>Shift</kbd> + <kbd>Enter</kbd> or <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to insert a new line.)
- Click the “Stop” button to stop the current request.
- Select a prompt style from the dropdown list at the bottom of the panel.
- Pick the model you want to use from the dropdown list at the bottom of the panel.

## Uninstallation

### Automated Uninstallation (Recommended)

To uninstall the plugin, you can just copy and paste the following command into your terminal:

<details>
  <summary><strong>Windows</strong></summary>

Run the following command in PowerShell **as administrator**:

```powershell
iwr -Uri "https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_windows.ps1" | iex
```

</details>

<details>
  <summary><strong>macOS</strong></summary>

Run the following command in your terminal:

```bash
curl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_macos.sh | sudo bash
```

</details>

<details>
  <summary><strong>Linux</strong></summary>

Run the following command in your terminal:

```bash
wget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_linux.sh | sudo bash
```

</details>

### Script Uninstall

<details>
  <summary><strong>Windows</strong></summary>

For Windows users, locate to the installation directory of the plugin and run the following command in PowerShell **as administrator**.

```powershell
.\bin\uninstall_windows.ps1
```

You can still specify the path to Typora manually by adding `-Path` or `-p`, just like the installation script.

</details>

<details>
  <summary><strong>macOS</strong></summary>

For macOS users, locate to the installation directory of the plugin and run the following command in terminal.

```bash
sudo bash ./bin/uninstall_macos.sh
```

You can still specify the path to Typora manually by adding `--path` or `-p`, just like the installation script.

</details>

<details>
  <summary><strong>Linux</strong></summary>

For Linux users, locate to the installation directory of the plugin and run the following command in terminal.

```bash
sudo bash ./bin/uninstall_linux.sh
```

You can still specify the path to Typora manually by adding `--path` or `-p`, just like the installation script.

</details>

### Manual Uninstall

<details>
  <summary>Click to expand</summary>

1. For Windows / Linux users, find `window.html` in your Typora installation folder, usually located at `<typora_root_path>/resources/`; For macOS users, find `index.html` in your Typora installation folder, usually located at `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` is the path where Typora is installed, replace it with your real Typora installation path (note that the angle brackets `<` and `>` should also be removed). This folder is called Typora resource folder in the following steps.
2. Delete the `copilot` folder in Typora resource folder.
3. For Windows / Linux users, open the previous `window.html` file you found in Typora resource folder with a text editor, and delete `<script src="./copilot/index.js" defer="defer"></script>`; For macOS users, open the previous `index.html` file you found in Typora resource folder with a text editor, and delete `<script src="./copilot/index.js" defer></script>`.
4. Restart Typora.
</details>

## Known Issues

1. Sometimes accepting a suggestion may cause the editor rerendering (i.e. code blocks, math blocks, etc. will be rerendered). This is due to the limitation of Typora's API that I have to force the editor to rerender sometimes to accept a suggestion, and currently I can't find a more safe and efficient way to resolve this issue.

## FAQs

### How to temporarily disable Copilot?

Just click the Copilot icon in the toolbar, and then click “Disable completions”. You can enable it again by clicking the icon and then clicking “Enable completions”.

### Why use suggestion panel in live preview mode (normal mode) and completion text in source mode by default? Can I change that?

The usage of suggestion panel in live preview mode is intentional. Typora uses a complex mechanism to render the content in live preview mode, it is hard to make completion text work properly in live preview mode.

But it is possible to also use suggestion panel in source mode, you can click the `toolbar icon -> Settings` and toggle the `Use inline completion text in source mode` option.

An option called `Use inline completion text in preview code blocks` is also provided. If you enable this option, the completion text instead of suggestion panel will also be used in code blocks and math blocks in live preview mode. But it is currently not recommended to enable this option, as it is likely to corrupt the editor content or history.

### Can I use keys other than `Tab` to accept suggestions?

Currently, no. It is technically possible, but currently I don't have enough time to implement it. Maybe I will implement it in the future.


================================================
FILE: README.zh-CN.md
================================================
# Typora Copilot

[English](./README.md) | 简体中文

![Copilot 建议截图](./docs/screenshot.zh-CN.png)

[Typora](https://typora.io/) 的 [GitHub Copilot](https://github.com/features/copilot) & [Copilot Chat](https://docs.github.com/copilot/using-github-copilot/copilot-chat) 插件,支持 Windows、macOS 和 Linux。

该插件使用 [GitHub Copilot 官方提供的 LSP 服务](https://www.npmjs.com/package/@github/copilot-language-server),以在编辑器中实时提供建议。

## 兼容性

> [!NOTE]
>
> 自 Typora v1.10 起,所有平台都需要安装 [Node.js](https://nodejs.org/zh-cn/download) ≥ 20 才能使用本插件。
>
> (仅对于使用 Typora 1.9 的 Windows / Linux 用户,无需安装 Node.js。 :wink:)

_\*注:`/` 表示未经过测试。_

| Typora Version | Windows 11 | Ubuntu 24.04 | macOS 15.x |
| -------------- | ---------- | ------------ | ---------- |
| 1.12.6         | /          | /            | ✓          |
| 1.12.4         | ✓          | /            | /          |
| 1.11.7         | ✓          | /            | ✓          |
| 1.10.8         | ✓          | ✓            | ✓          |
| 1.10.6         | ✓          | ✓            | ✓          |
| 1.9.5          | ✓          | /            | /          |
| 1.9.4          | /          | /            | ✓          |
| 1.9.3          | /          | ✓            | /          |
| 1.8.10         | ✓          | ✓            | ✓          |
| 1.8.8          | /          | ✓            | /          |
| 1.8.6          | ✓          | /            | /          |
| 1.8.5          | ✓          | /            | ✓          |
| 1.7.6          | ✓          | /            | /          |
| 1.6.7          | ✓          | /            | /          |
| 1.5.12         | ✓          | /            | /          |
| 1.4.8          | ✓          | /            | /          |
| 1.3.8          | ✓          | /            | /          |
| 1.2.5          | ✓          | /            | /          |
| 1.2.3          | ✓          | /            | /          |
| 1.0.3          | ✓          | /            | /          |
| 0.11.18-beta   | ✓          | /            | /          |

## 前置条件

- 公网连接(对于中国大陆用户,你还需要确保你的网络可以正常访问 GitHub Copilot 服务)。
- 已激活的 GitHub Copilot 订阅。

## 安装

**开始任何安装方式之前,请先完全退出 Typora(尤其是 macOS 用户:请使用 <kbd>⌘</kbd>+<kbd>Q</kbd> 退出)。**

### 一键安装(推荐)

你可以直接将以下命令复制粘贴到你的终端中来安装插件:

<details>
  <summary><strong>Windows</strong></summary>

以**管理员身份**在 PowerShell 中运行以下命令:

```powershell
iwr -Uri "https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.ps1" | iex
```

</details>

<details>
  <summary><strong>macOS</strong></summary>

在终端中运行以下命令:

```bash
curl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash
```

</details>

<details>
  <summary><strong>Linux</strong></summary>

在终端中运行以下命令:

```bash
wget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash
```

</details>

### 脚本安装

<details>
  <summary><strong>Windows</strong></summary>

对于 Windows 用户,首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在 PowerShell 中**以管理员身份**运行以下命令:

```powershell
.\bin\install_windows.ps1
```

如果脚本无法找到 Typora,你可以手动指定 Typora 的路径:

```powershell
.\bin\install_windows.ps1 -Path "C:\Program Files\Typora\" # 替换为你的 Typora 路径
# 或使用别名
# .\bin\install_windows.ps1 -p "C:\Program Files\Typora\" # 替换为你的 Typora 路径
```

安装过程中,你会看到一条消息记录插件的安装目录。_记住它,在卸载插件时你会需要它。_ 安装完成后,你可以安全地删除刚才解压的文件夹。

</details>

<details>
  <summary><strong>macOS</strong></summary>

对于 macOS 用户,首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在终端中运行以下命令:

```bash
sudo bash ./bin/install_macos.sh
```

如果脚本无法找到 Typora,你可以手动指定 Typora 的路径:

```bash
sudo bash ./bin/install_macos.sh --path "/Applications/Typora.app/" # 替换为你的 Typora 路径
# 或使用别名
# sudo bash ./bin/install_macos.sh -p "/Applications/Typora.app/" # 替换为你的 Typora 路径
```

安装过程中,你会看到一条消息记录插件的安装目录。_记住它,在卸载插件时你会需要它。_ 安装完成后,你可以安全地删除刚才解压的文件夹。

</details>

<details>
  <summary><strong>Linux</strong></summary>

对于 Linux 用户,首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在终端中运行以下命令:

```bash
sudo bash ./bin/install_linux.sh
```

如果脚本无法找到 Typora,你可以手动指定 Typora 的路径:

```bash
sudo bash ./bin/install_linux.sh --path "/usr/share/typora/" # 替换为你的 Typora 路径
# 或使用别名
# sudo bash ./bin/install_linux.sh -p "/usr/share/typora/" # 替换为你的 Typora 路径
```

安装过程中,你会看到一条消息记录插件的安装目录。_记住它,在卸载插件时你会需要它。_ 安装完成后,你可以安全地删除刚才解压的文件夹。

</details>

### 手动安装

<details>
  <summary>点击展开</summary>

1. 从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。
2. 找到 Typora 安装目录下的 `window.html` 文件,通常位于 `<typora_root_path>/resources/`;对于 macOS 用户,找到 Typora 安装目录下的 `index.html` 文件,通常位于 `<typora_root_path>/Contents/Resources/TypeMark/`。`<typora_root_path>` 是 Typora 的安装路径,替换为你的实际 Typora 安装路径(注意尖括号 `<` 和 `>` 也要删除)。这个文件夹在下面的步骤中被称为 Typora 资源文件夹。
3. 在 Typora 资源文件夹中创建一个名为 `copilot` 的文件夹。
4. 将解压后的文件全部复制到 `copilot` 文件夹中。**请确保最终路径为 `copilot/index.js`(而不是 `copilot/typora-copilot/index.js`)。如果出现后者,请将 `typora-copilot` 文件夹内的内容上移一层,使 `index.js` 直接位于 `copilot` 下。**
5. 对于 Windows / Linux 用户,在 Typora 资源文件夹中用文本编辑器打开 `window.html`,在类似 `<script src="./appsrc/window/frame.js" defer="defer"></script>` 或 `<script src="./app/window/frame.js" defer="defer"></script>` 的代码之后添加 `<script src="./copilot/index.js" defer="defer"></script>`;对于 macOS 用户,在 Typora 资源文件夹中用文本编辑器打开 `index.html`,在类似 `<script src="./appsrc/main.js" aria-hidden="true" defer></script>` 或 `<script src="./app/main.js" aria-hidden="true" defer></script>` 的代码之后添加 `<script src="./copilot/index.js" defer></script>`。
6. 重启 Typora。
7. 对于 macOS 用户,如果你在打开 Typora 时被提示“文件已损坏”,你可以按住 Ctrl 点击 Typora,并选择“打开”来打开 Typora.

</details>

## 初始化

完成安装后,你会在 Typora 工具栏(即界面底部右下角)找到一个 Copilot 图标。点击**它右侧的箭头**,你会看到一个下拉菜单,然后点击“登录以认证 Copilot”。

![Copilot 图标](./docs/toolbar-icon.zh-CN.png)

> [!CAUTION]
>
> 如果你在中国大陆,登录这一步很可能因为网络原因失败。如果你发现点击按钮后很长时间没有反应,尝试按 Shift+F12(Windows 或 Linux)或在帮助菜单中打开“Enable Debugging”并在任意位置右键选择检查元素(macOS),以打开调试工具,定位到“控制台”或“Console”标签页,将过滤级别调整为“详细”或“Verbose”。然后查看控制台中打印的日志信息,以检查是否存在网络问题。
>
> 如果你看到一条来自“SignInInitiate”的红色错误信息,其中包含“ETIMEOUT”这样的内容,说明这一步因网络原因失败了。尝试调整你的代理软件设置,打开类似“增强代理”或“TUN 模式”的选项,重启 Typora 再进行尝试;或者,对于 Windows 用户可以使用使用 Proxifier 配置全局代理,对于 macOS / Linux 用户可以使用 Proxychains 打开 Typora,再进行尝试。

按照提示进行身份验证:

1. 用户代码会自动复制到你的剪贴板。
2. 遵照弹出提示上的说明,打开 GitHub 身份验证页面。
3. 将用户代码粘贴到 GitHub 身份验证页面中。
4. 返回 Typora 并在对话框中按下“确定”按钮。
5. 如果你在**几秒钟后**看到一个“已登录 GitHub Copilot”对话框,Copilot 插件应该就可以正常工作了(在中国大陆,你可能需要等待更长的时间)。

## Copilot Chat

点击工具栏中的 Copilot 图标将切换 Copilot Chat 面板。你可以使用它与 Copilot 聊天,当前文档和之前的聊天记录将作为上下文发送给 Copilot。

确保在使用 Copilot Chat 面板之前已登录 Copilot。登录后,重启 Typora 以确保 Copilot Chat 面板正常工作。

你可以:

- 从面板顶部的下拉列表中选择、创建、编辑聊天标题,或删除聊天会话。
- 点击“发送”按钮或按下 <kbd>Enter</kbd> 键发送消息。(你可以使用 <kbd>Shift</kbd> + <kbd>Enter</kbd> 或 <kbd>Ctrl</kbd> + <kbd>Enter</kbd> 插入新行。)
- 点击“停止”按钮停止当前请求。
- 从面板底部的下拉列表中选择提示样式。
- 从面板底部的下拉列表中选择要使用的模型。

## 卸载

### 一键卸载(推荐)

要卸载插件,你可以直接将以下命令复制粘贴到你的终端中:

<details>
  <summary><strong>Windows</strong></summary>

以**管理员身份**在 PowerShell 中运行以下命令:

```powershell
iwr -Uri "https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_windows.ps1" | iex
```

</details>

<details>
  <summary><strong>macOS</strong></summary>

在终端中运行以下命令:

```bash
curl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_macos.sh | sudo bash
```

</details>

<details>
  <summary><strong>Linux</strong></summary>

在终端中运行以下命令:

```bash
wget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_linux.sh | sudo bash
```

</details>

### 脚本卸载

<details>
  <summary><strong>Windows</strong></summary>

对于 Windows 用户,定位到插件安装目录并在 PowerShell 中**以管理员身份**运行以下命令:

```powershell
.\bin\uninstall_windows.ps1
```

和安装时一样,如果脚本无法找到 Typora,你可以手动通过 `-Path` 或 `-p` 参数指定 Typora 的路径。

</details>

<details>
  <summary><strong>macOS</strong></summary>

对于 macOS 用户,定位到插件安装目录并在终端中运行以下命令:

```bash
sudo bash ./bin/uninstall_macos.sh
```

和安装时一样,如果脚本无法找到 Typora,你可以手动通过 `--path` 或 `-p` 参数指定 Typora 的路径。

</details>

<details>
  <summary><strong>Linux</strong></summary>

对于 Linux 用户,定位到插件安装目录并在终端中运行以下命令:

```bash
sudo bash ./bin/uninstall_linux.sh
```

和安装时一样,如果脚本无法找到 Typora,你可以手动通过 `--path` 或 `-p` 参数指定 Typora 的路径。

</details>

### 手动卸载

<details>
  <summary>点击展开</summary>

1. 找到 Typora 安装目录下的 `window.html` 文件,通常位于 `<typora_root_path>/resources/`;对于 macOS 用户,找到 Typora 安装目录下的 `index.html` 文件,通常位于 `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` 是 Typora 的安装路径,替换为你的实际 Typora 安装路径(注意尖括号 `<` 和 `>` 也要删除)。这个文件夹在下面的步骤中被称为 Typora 资源文件夹。
2. 删除 Typora 资源文件夹中的 `copilot` 文件夹。
3. 对于 Windows / Linux 用户,在 Typora 资源文件夹中用文本编辑器打开 `window.html`,删除 `<script src="./copilot/index.js" defer="defer"></script>`;对于 macOS 用户,在 Typora 资源文件夹中用文本编辑器打开 `index.html`,删除 `<script src="./copilot/index.js" defer></script>`.
4. 重启 Typora.
</details>

## 已知问题

1. 有时接受建议可能会导致编辑器重新渲染(即代码块、数学块等将重新渲染)。这是由于 Typora API 的限制,我必须有时强制编辑器重新渲染以接受建议,目前我找不到更安全和更高效的方法来解决这个问题。

## 常见问题

### 如何临时禁用 Copilot?

点击工具栏中的 Copilot 图标,然后点击“禁用建议”即可。你可以通过点击图标然后点击“启用建议”来重新启用它。

### 为什么默认在实时预览模式(正常模式)下使用建议面板,在源代码模式下使用补全文本?我能修改这一配置吗?

在实时预览模式下使用建议面板是有意的。Typora 在实时预览模式下的渲染机制很复杂,很难使补全文本在实时预览模式下正常工作。

不过对于源代码模式,你可以通过点击 `工具栏图标 -> 设置` 并切换 `在源代码模式下使用内联补全文本` 选项来在源代码模式下使用建议面板。

设置中还提供了一个名为 `在预览模式代码块中使用内联补全文本` 的选项。如果你启用了这个选项,补全文本将会在实时预览模式下的代码块和数学块中使用。但目前不建议启用这个选项,因为它很可能会破坏编辑器内容或历史记录。

### 我可以使用除 `Tab` 键以外的按键来接受建议吗?

目前不行。这在技术上是可行的,但目前我没什么时间实现它。也许我将来会实现它。


================================================
FILE: bin/install_linux.sh
================================================
#!/bin/bash

# Parse arguments -path or -p
while [[ "$#" -gt 0 ]]; do
  case $1 in
  -p | --path)
    custom_path="$2"
    shift
    ;;
  *)
    echo "Unknown parameter passed: $1"
    exit 1
    ;;
  esac
  shift
done

# Possible Typora installation paths
paths=(
  "/usr/share/typora"
  "/usr/local/share/typora"
  "/opt/typora"
  "/opt/Typora"
  "$HOME/.local/share/Typora"
  "$HOME/.local/share/typora"
)
if [[ -n "$custom_path" ]]; then
  paths=("$custom_path")
fi

script_to_insert_after_candidates=(
  '<script src="./app/window/frame.js" defer="defer"></script>'
  '<script src="./appsrc/window/frame.js" defer="defer"></script>'
)
script_to_insert='<script src="./copilot/index.js" defer="defer"></script>'

escape_for_sed() {
  echo "$1" | sed -E 's/[]\/$*.^|[]/\\&/g'
}

# Find `window.html` in Typora installation path
path_found=false
success=false

for path in "${paths[@]}"; do
  window_html_path_candidates=(
    "$path/resources/app/window.html"
    "$path/resources/appsrc/window.html"
    "$path/resources/window.html"
  )

  for window_html_path in "${window_html_path_candidates[@]}"; do
    # If found, insert script
    if [[ -f "$window_html_path" ]]; then
      path_found=true
      echo "Installation directory: \"$(dirname "$window_html_path")/copilot/\""
      echo "Found Typora \"index.html\" at \"$window_html_path\"."
      content=$(cat "$window_html_path")

      if [[ "$content" != *"$script_to_insert"* ]]; then
        echo 'Installing Copilot plugin in Typora...'
        for script_to_insert_after in "${script_to_insert_after_candidates[@]}"; do
          if echo "$content" | grep -qF "$script_to_insert_after"; then
            echo "Inserting Copilot plugin script after \"$script_to_insert_after\"..."

            # Calculate indent of the script to insert
            escaped_script_to_insert_after=$(escape_for_sed "$script_to_insert_after")
            escaped_script_to_insert=$(escape_for_sed "$script_to_insert")
            indent=$(echo "$content" | while IFS= read -r line; do
              if [[ "$line" == *"$script_to_insert_after"* ]]; then
                echo "$line" | sed -E 's/^([[:space:]]*).*/\1/'
                break
              fi
            done)
            if [[ -z "$indent" ]]; then
              replacement="$escaped_script_to_insert_after$escaped_script_to_insert"
            else
              replacement="$escaped_script_to_insert_after\n$indent$escaped_script_to_insert"
            fi
            new_content=$(echo "$content" | sed "s|$escaped_script_to_insert_after|$replacement|")

            # Insert script
            echo "$new_content" >"$window_html_path"

            # Copy `<cwd>/../` to `<path_of_window_html>/copilot/` directory
            copilot_path=$(dirname "$window_html_path")/copilot
            if [[ ! -d "$copilot_path" ]]; then
              echo "Copying Copilot plugin files to \"$copilot_path\"..."
              mkdir -p "$copilot_path"
              cp -r "$(dirname "$0")/../" "$copilot_path"
            fi

            echo "Successfully installed Copilot plugin in Typora."

            success=true
            break
          fi
        done

        if $success; then break; fi
      else
        # Script tag already present; validate installation integrity
        copilot_path="$(dirname "$window_html_path")/copilot"
        index_js_path="$copilot_path/index.js"
        if [[ ! -f "$index_js_path" ]]; then
          echo "Warning: Corrupted Copilot installation detected. Expected \"$index_js_path\" but it does not exist."
          echo "Please delete the entire \"$copilot_path\" directory and re-run this installer."
          echo "Do NOT place the release inside Typora's installation folder (especially not under \"copilot\")."
          echo "Download/extract it to any other folder and run this script from there."
          break
        fi

        echo "Warning: Copilot plugin has already been installed in Typora."
        success=true
        break
      fi
    fi
  done

  if $success; then break; fi
done

# If not found, prompt user to check installation path
if ! $path_found; then
  echo "Error: Could not find Typora installation path. Please check if Typora is installed and try again." >&2
elif ! $success; then
  echo "Error: Installation failed." >&2
fi


================================================
FILE: bin/install_macos.sh
================================================
#!/bin/bash

# Parse arguments -path or -p
while [[ "$#" -gt 0 ]]; do
  case $1 in
  -p | --path)
    custom_path="$2"
    shift
    ;;
  *)
    echo "Unknown parameter passed: $1"
    exit 1
    ;;
  esac
  shift
done

# Possible Typora installation paths
paths=(
  "/Applications/Typora.app"
  "$HOME/Applications/Typora.app"
  "/usr/local/bin/Typora"
  "/opt/Typora"
)
if [[ -n "$custom_path" ]]; then
  paths=("$custom_path")
fi

script_to_insert_after_candidates=(
  '<script src="./app/main.js" defer></script>'
  '<script src="./app/main.js" aria-hidden="true" defer></script>'
  '<script src="./appsrc/main.js" defer></script>'
  '<script src="./appsrc/main.js" aria-hidden="true" defer></script>'
)
script_to_insert='<script src="./copilot/index.js" defer></script>'

escape_for_sed() {
  echo "$1" | sed -E 's/[]\/$*.^|[]/\\&/g'
}

# Find `index.html` in Typora installation path
path_found=false
success=false

for path in "${paths[@]}"; do
  index_html_path_candidates=(
    "$path/Contents/Resources/TypeMark/index.html"
    "$path/Contents/Resources/app/index.html"
    "$path/Contents/Resources/appsrc/index.html"
    "$path/resources/app/index.html"
    "$path/resources/appsrc/index.html"
    "$path/resources/TypeMark/index.html"
    "$path/resources/index.html"
  )

  for index_html_path in "${index_html_path_candidates[@]}"; do
    # If found, insert script
    if [[ -f "$index_html_path" ]]; then
      path_found=true
      echo "Installation directory: \"$(dirname "$index_html_path")/copilot/\""
      echo "Found Typora \"index.html\" at \"$index_html_path\"."
      content=$(cat "$index_html_path")

      if [[ "$content" != *"$script_to_insert"* ]]; then
        echo 'Installing Copilot plugin in Typora...'
        for script_to_insert_after in "${script_to_insert_after_candidates[@]}"; do
          if echo "$content" | grep -qF "$script_to_insert_after"; then
            echo "Inserting Copilot plugin script after \"$script_to_insert_after\"..."

            # Calculate indent of the script to insert
            escaped_script_to_insert_after=$(escape_for_sed "$script_to_insert_after")
            escaped_script_to_insert=$(escape_for_sed "$script_to_insert")
            indent=$(echo "$content" | while IFS= read -r line; do
              if [[ "$line" == *"$script_to_insert_after"* ]]; then
                echo "$line" | sed -E 's/^([[:space:]]*).*/\1/'
                break
              fi
            done)
            if [[ -z "$indent" ]]; then
              replacement="$escaped_script_to_insert_after$escaped_script_to_insert"
            else
              replacement="$escaped_script_to_insert_after\n$indent$escaped_script_to_insert"
            fi
            new_content=$(echo "$content" | sed "s|$escaped_script_to_insert_after|$replacement|")

            # Insert script
            echo "$new_content" >"$index_html_path"

            # Copy `<cwd>/../` to `<path_of_index_html>/copilot/` directory
            copilot_path=$(dirname "$index_html_path")/copilot
            if [[ ! -d "$copilot_path" ]]; then
              echo "Copying Copilot plugin files to \"$copilot_path\"..."
              mkdir -p "$copilot_path"
              cp -r "$(dirname "$0")/../" "$copilot_path"
            fi

            echo "Successfully installed Copilot plugin in Typora."

            success=true
            break
          fi
        done

        if $success; then break; fi
      else
        # Script tag already present; validate installation integrity
        copilot_path="$(dirname "$index_html_path")/copilot"
        index_js_path="$copilot_path/index.js"
        if [[ ! -f "$index_js_path" ]]; then
          echo "Warning: Corrupted Copilot installation detected. Expected \"$index_js_path\" but it does not exist."
          echo "Please delete the entire \"$copilot_path\" directory and re-run this installer."
          echo "Do NOT place the release inside Typora's installation folder (especially not under \"copilot\")."
          echo "Download/extract it to any other folder and run this script from there."
          break
        fi

        echo "Warning: Copilot plugin has already been installed in Typora."
        success=true
        break
      fi
    fi
  done

  if $success; then break; fi
done

# If not found, prompt user to check installation path
if ! $path_found; then
  echo "Error: Could not find Typora installation path. Please check if Typora is installed and try again." >&2
elif ! $success; then
  echo "Error: Installation failed." >&2
fi


================================================
FILE: bin/install_windows.ps1
================================================
# Allow custom path (-Path or -p)
param (
    [Parameter(Mandatory = $false)]
    [Alias('p')]
    [string] $Path = ''
)

# Possible Typora installation paths
$paths = @(
    'C:\Program Files\Typora'
    'C:\Program Files (x86)\Typora'
    "$env:LOCALAPPDATA\Programs\Typora"
)
if ($Path -ne '') { $paths = @($Path) }

$scriptToInsertAfterCandidates = @(
    '<script src="./app/window/frame.js" defer="defer"></script>'
    '<script src="./appsrc/window/frame.js" defer="defer"></script>'
)
$scriptToInsert = '<script src="./copilot/index.js" defer="defer"></script>'

# Find `window.html` in Typora installation path
$pathFound = $false
$success = $false

foreach ($path in $paths) {
    $windowHtmlPathCandiates = @(
        Join-Path -Path $path -ChildPath 'resources\app\window.html'
        Join-Path -Path $path -ChildPath 'resources\appsrc\window.html'
        Join-Path -Path $path -ChildPath 'resources\window.html'
    )

    foreach ($windowHtmlPath in $windowHtmlPathCandiates) {
        # If found, insert script
        if (Test-Path $windowHtmlPath) {
            $pathFound = $true
            Write-Host "Installation directory: ""$(Split-Path -Path $windowHtmlPath -Parent)\copilot\"""
            Write-Host "Found Typora ""window.html"" at ""$windowHtmlPath""."
            $content = Get-Content $windowHtmlPath -Raw -Encoding UTF8

            if (!($content.Contains($scriptToInsert))) {
                Write-Host 'Installing Copilot plugin in Typora...'
                foreach ($scriptToInsertAfter in $scriptToInsertAfterCandidates) {
                    if ($content.Contains($scriptToInsertAfter)) {
                        Write-Host "Inserting Copilot plugin script after ""$scriptToInsertAfter""..."

                        # Calculate indent of the script to insert
                        $row = $content.Split("`n") | Where-Object { $_ -match $scriptToInsertAfter }
                        $rowContentBeforeScriptToInsertAfter = $row -replace "$scriptToInsertAfter(.*)", ''
                        $indent = $rowContentBeforeScriptToInsertAfter -replace $rowContentBeforeScriptToInsertAfter.TrimEnd(), ''

                        # Insert script
                        $newContent = $content -replace $scriptToInsertAfter, (
                            $scriptToInsertAfter +
                            $(If (($rowContentBeforeScriptToInsertAfter -ne '') -and ($indent -eq '')) { '' } Else { "`n" + $indent }) +
                            $scriptToInsert
                        )
                        Set-Content -Path $windowHtmlPath -Value $newContent -Encoding UTF8

                        # Copy `<cwd>\..\` to `<path_of_window_html>\copilot\` directory
                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'
                        if (-not (Test-Path $copilotPath)) {
                            Write-Host "Copying Copilot plugin files to ""$copilotPath""..."
                            Copy-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\') -Destination $copilotPath -Recurse
                        }

                        Write-Host "Successfully installed Copilot plugin in Typora."

                        $success = $true
                        break
                    }
                }

                if ($success) { break }
            }
            else {
                # Script tag already present; validate installation integrity
                $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'
                $indexJsPath = Join-Path -Path $copilotPath -ChildPath 'index.js'
                if (-not (Test-Path $indexJsPath)) {
                    Write-Warning "Corrupted Copilot installation detected. Expected ""$indexJsPath"" but it does not exist."
                    Write-Host "Please delete the entire ""$copilotPath"" directory and re-run this installer."
                    Write-Host "Do NOT place the release inside Typora's installation folder (especially not under ""copilot"")."
                    Write-Host "Download/extract it to any other folder and run this script from there."
                    break
                }

                Write-Warning "Copilot plugin has already been installed in Typora."
                $success = $true
                break
            }
        }
    }
}

# If not found, prompt user to check installation path
if (-not $pathFound) {
    Write-Error "Could not find Typora installation path. Please check if Typora is installed and try again."
}
elseif (-not $success) {
    Write-Error "Installation failed."
}


================================================
FILE: bin/uninstall_linux.sh
================================================
#!/bin/bash

# Parse arguments -path or -p
while [[ "$#" -gt 0 ]]; do
  case $1 in
  -p | --path)
    custom_path="$2"
    shift
    ;;
  -s | --silent)
    silent=true
    shift
    ;;
  *)
    echo "Unknown parameter passed: $1"
    exit 1
    ;;
  esac
  shift
done

# Possible Typora installation paths on Linux
paths=(
  "/usr/share/typora"
  "/usr/local/share/typora"
  "/opt/typora"
  "/opt/Typora"
  "$HOME/.local/share/Typora"
  "$HOME/.local/share/typora"
)
if [[ -n "$custom_path" ]]; then
  paths=("$custom_path")
fi

script_to_remove_after_candidates=(
  '<script src="./app/window/frame.js" defer="defer"></script>'
  '<script src="./appsrc/window/frame.js" defer="defer"></script>'
)
script_to_remove='<script src="./copilot/index.js" defer="defer"></script>'

escape_for_sed() {
  echo "$1" | sed -E 's/[]\/$*.^|[]/\\&/g'
}

# Find `window.html` in Typora installation path
path_found=false
success=false

for path in "${paths[@]}"; do
  window_html_path_candidates=(
    "$path/resources/app/window.html"
    "$path/resources/appsrc/window.html"
    "$path/resources/window.html"
  )

  for window_html_path in "${window_html_path_candidates[@]}"; do
    # If found, insert script
    if [[ -f "$window_html_path" ]]; then
      path_found=true
      echo "Found Typora \"window.html\" at \"$window_html_path\"."
      content=$(cat "$window_html_path")

      for script_to_remove_after in "${script_to_remove_after_candidates[@]}"; do
        if echo "$content" | grep -qF "$script_to_remove_after"; then
          if echo "$content" | grep -qF "$script_to_remove"; then
            echo "Removing Copilot plugin script after \"$script_to_remove_after\"..."

            escaped_script_to_remove=$(escape_for_sed "$script_to_remove")
            new_content=$(echo "$content" | sed -E "s/[[:space:]]*$escaped_script_to_remove//")

            # Remove script
            echo "$new_content" >"$window_html_path"

            # Remove `<path_of_window_html>/copilot/` directory
            copilot_dir=$(dirname "$window_html_path")/copilot
            if [[ -d "$copilot_dir" ]]; then
              echo "Removing Copilot plugin directory \"$copilot_dir\"..."
              rm -rf "$copilot_dir"
            fi

            echo "Successfully uninstalled Copilot plugin in Typora."

            success=true
            break
          else
            if ! $silent; then
              echo "Warning: Copilot plugin has not been installed in Typora."
            fi

            # Remove `<path_of_window_html>/copilot/` directory regardless of script presence
            copilot_dir=$(dirname "$window_html_path")/copilot
            if [[ -d "$copilot_dir" ]]; then
              echo "Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation."
              echo "Removing Copilot plugin directory \"$copilot_dir\"..."
              rm -rf "$copilot_dir"
              echo "Uninstallation complete."
            fi

            success=true
            break
          fi
        fi

        if $success; then break; fi
      done
    fi

    if $success; then break; fi
  done

  if $success; then break; fi
done

# If not found, prompt user to check installation path
if ! $path_found; then
  echo "Error: Could not find Typora installation path. Please check if Typora is installed and try again." >&2
elif ! $success; then
  echo "Error: Uninstallation failed." >&2
fi


================================================
FILE: bin/uninstall_macos.sh
================================================
#!/bin/bash

# Parse arguments -path or -p
while [[ "$#" -gt 0 ]]; do
  case $1 in
  -p | --path)
    custom_path="$2"
    shift
    ;;
  -s | --silent)
    silent=true
    shift
    ;;
  *)
    echo "Unknown parameter passed: $1"
    exit 1
    ;;
  esac
  shift
done

# Possible Typora installation paths
paths=(
  "/Applications/Typora.app"
  "$HOME/Applications/Typora.app"
  "/usr/local/bin/Typora"
  "/opt/Typora"
)
if [[ -n "$custom_path" ]]; then
  paths=("$custom_path")
fi

script_to_remove_after_candidates=(
  '<script src="./app/main.js" defer></script>'
  '<script src="./app/main.js" aria-hidden="true" defer></script>'
  '<script src="./appsrc/main.js" defer></script>'
  '<script src="./appsrc/main.js" aria-hidden="true" defer></script>'
)
script_to_remove='<script src="./copilot/index.js" defer></script>'

escape_for_sed() {
  echo "$1" | sed -E 's/[]\/$*.^|[]/\\&/g'
}

# Find `index.html` in Typora installation path
path_found=false
success=false

for path in "${paths[@]}"; do
  index_html_path_candidates=(
    "$path/Contents/Resources/TypeMark/index.html"
    "$path/Contents/Resources/app/index.html"
    "$path/Contents/Resources/appsrc/index.html"
    "$path/resources/app/index.html"
    "$path/resources/appsrc/index.html"
    "$path/resources/TypeMark/index.html"
    "$path/resources/index.html"
  )

  for index_html_path in "${index_html_path_candidates[@]}"; do
    # If found, insert script
    if [[ -f "$index_html_path" ]]; then
      path_found=true
      echo "Found Typora \"index.html\" at \"$index_html_path\"."
      content=$(cat "$index_html_path")

      for script_to_remove_after in "${script_to_remove_after_candidates[@]}"; do
        if echo "$content" | grep -qF "$script_to_remove_after"; then
          if echo "$content" | grep -qF "$script_to_remove"; then
            echo "Removing Copilot plugin script after \"$script_to_remove_after\"..."

            escaped_script_to_remove=$(escape_for_sed "$script_to_remove")
            new_content=$(echo "$content" | sed -E "s/[[:space:]]*$escaped_script_to_remove//")

            # Remove script
            echo "$new_content" >"$index_html_path"

            # Remove `<path_of_index_html>/copilot/` directory
            copilot_dir=$(dirname "$index_html_path")/copilot
            if [[ -d "$copilot_dir" ]]; then
              echo "Removing Copilot plugin directory \"$copilot_dir\"..."
              rm -rf "$copilot_dir"
            fi

            echo "Successfully uninstalled Copilot plugin in Typora."

            success=true
            break
          else
            if ! $silent; then
              echo "Warning: Copilot plugin has not been installed in Typora."
            fi

            # Remove `<path_of_index_html>/copilot/` directory regardless of script presence
            copilot_dir=$(dirname "$index_html_path")/copilot
            if [[ -d "$copilot_dir" ]]; then
              echo "Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation."
              echo "Removing Copilot plugin directory \"$copilot_dir\"..."
              rm -rf "$copilot_dir"
              echo "Uninstallation complete."
            fi

            success=true
            break
          fi
        fi

        if $success; then break; fi
      done
    fi

    if $success; then break; fi
  done

  if $success; then break; fi
done

# If not found, prompt user to check installation path
if ! $path_found; then
  echo "Error: Could not find Typora installation path. Please check if Typora is installed and try again." >&2
elif ! $success; then
  echo "Error: Uninstallation failed." >&2
fi


================================================
FILE: bin/uninstall_windows.ps1
================================================
# Allow custom path (-Path or -p) and silence warning (-Silent or -s)
param (
    [Parameter(Mandatory = $false)]
    [Alias('p')]
    [string] $Path = '',

    [Parameter(Mandatory = $false)]
    [Alias('s')]
    [switch] $Silent = $false
)

# Possible Typora installation paths
$paths = @(
    'C:\Program Files\Typora'
    'C:\Program Files (x86)\Typora'
    "$env:LOCALAPPDATA\Programs\Typora"
)
if ($Path -ne '') { $paths = @($Path) }

$scriptToRemoveAfterCandidates = @(
    '<script src="./app/window/frame.js" defer="defer"></script>'
    '<script src="./appsrc/window/frame.js" defer="defer"></script>'
)
$scriptToRemove = '<script src="./copilot/index.js" defer="defer"></script>'

# Find `window.html` in Typora installation path
$pathFound = $false
$success = $false

foreach ($path in $paths) {
    $windowHtmlPathCandiates = @(
        Join-Path -Path $path -ChildPath 'resources\app\window.html'
        Join-Path -Path $path -ChildPath 'resources\appsrc\window.html'
        Join-Path -Path $path -ChildPath 'resources\window.html'
    )

    foreach ($windowHtmlPath in $windowHtmlPathCandiates) {
        # If found, remove script
        if (Test-Path $windowHtmlPath) {
            $pathFound = $true
            Write-Host "Found Typora ""window.html"" at ""$windowHtmlPath""."
            $content = Get-Content $windowHtmlPath -Raw -Encoding UTF8

            foreach ($scriptToRemoveAfter in $scriptToRemoveAfterCandidates) {
                if ($content.Contains($scriptToRemoveAfter)) {
                    if ($content.Contains($scriptToRemove)) {
                        Write-Host "Removing Copilot plugin script after ""$scriptToRemoveAfter""..."

                        # Calculate indent of the script to remove
                        $row = $content.Split("`n") | Where-Object { $_ -match $scriptToRemove }
                        $rowContentBeforeScriptToRemove = $row -replace "$scriptToRemoveAfter(.*)", ''
                        $indent = $rowContentBeforeScriptToRemove -replace $rowContentBeforeScriptToRemove.TrimEnd(), ''

                        # Remove script
                        $newContent = $content -replace ($indent + $scriptToRemove), ''
                        Set-Content -Path $windowHtmlPath -Value $newContent -Encoding UTF8

                        # Remove `<path_of_window_html>\copilot\` directory
                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'
                        if (Test-Path $copilotPath) {
                            Write-Host "Removing Copilot plugin directory ""$copilotPath""..."
                            Remove-Item -Path $copilotPath -Recurse -Force
                        }

                        Write-Host "Successfully uninstalled Copilot plugin in Typora."

                        $success = $true
                        break
                    }
                    else {
                        if (-not $Silent) {
                            Write-Warning "Copilot plugin script has not been found in Typora."
                        }

                        # Remove `<path_of_window_html>\copilot\` directory regardless of script presence
                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'
                        if (Test-Path $copilotPath) {
                            Write-Host "Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation."
                            Write-Host "Removing Copilot plugin directory ""$copilotPath""..."
                            Remove-Item -Path $copilotPath -Recurse -Force
                            Write-Host "Uninstallation complete."
                            $success = $true
                        }

                        $success = $true
                        break
                    }
                }

                if ($success) { break }
            }
        }

        if ($success) { break }
    }
}

# If not found, prompt user to check installation path
if (-not $pathFound) {
    Write-Error "Could not find Typora installation path. Please check if Typora is installed and try again."
}
elseif (-not $success) {
    Write-Error "Uninstallation failed."
}


================================================
FILE: commitlint.config.js
================================================
// @ts-check

/**
 * @typedef {object} Parsed
 * @property {?string} emoji The emoji at the beginning of the commit message.
 * @property {?string} type The type of the commit message.
 * @property {?string} scope The scope of the commit message.
 * @property {?string} subject The subject of the commit message.
 */

const emojiEnum = /** @type {const} */ ([
  2,
  "always",
  {
    "🎉": ["init", "Project initialization"],
    "✨": ["feat", "Adding new features"],
    "🐞": ["fix", "Fixing bugs"],
    "📃": ["docs", "Modify documentation only"],
    "🌈": [
      "style",
      "Only the spaces, formatting indentation, commas, etc. were changed, not the code logic",
    ],
    "🦄": ["refactor", "Code refactoring, no new features added or bugs fixed"],
    "🎈": ["perf", "Optimization-related, such as improving performance, experience"],
    "🧪": ["test", "Adding or modifying test cases"],
    "🔧": [
      "build",
      "Dependency-related content, such as Webpack, Vite, Rollup, npm, package.json, etc.",
    ],
    "🐎": ["ci", "CI configuration related, e.g. changes to k8s, docker configuration files"],
    "🐳": ["chore", "Other modifications, e.g. modify the configuration file"],
    "↩": ["revert", "Rollback to previous version"],
  },
]);

/** @satisfies {import("@commitlint/types").UserConfig} */
const config = {
  parserPreset: {
    parserOpts: {
      headerPattern:
        /^(?<emoji>\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]) (?<type>\w+)(?:\((?<scope>.*)\))?!?: (?<subject>(?:(?!#).)*(?:(?!\s).))$/,
      headerCorrespondence: ["emoji", "type", "scope", "subject"],
    },
  },
  plugins: [
    {
      rules: {
        "header-match-git-commit-message-with-emoji-pattern": (parsed) => {
          const { emoji, scope, subject, type } = /** @type {Parsed} */ (
            /** @type {unknown} */ (parsed)
          );
          if (emoji === null && type === null && scope === null && subject === null)
            return [
              false,
              'header must be in format "<emoji> <type>(<scope>?): <subject>", e.g:\n' +
                "    - 🎉 init: Initial commit\n" +
                "    - ✨ feat(assertions): Add assertions\n" +
                "   ",
            ];
          return [true, ""];
        },
        "emoji-enum": (parsed, _, value) => {
          const { emoji } = /** @type {Parsed} */ (/** @type {unknown} */ (parsed));
          const emojisObject = /** @type {typeof emojiEnum[2]} */ (/** @type {unknown} */ (value));
          if (emoji && !Object.keys(emojisObject).includes(emoji)) {
            return [
              false,
              "emoji must be one of:\n" +
                Object.entries(emojisObject)
                  .map(([emoji, [type, description]]) => `    ${emoji} ${type} - ${description}`)
                  .join("\n") +
                "\n   ",
            ];
          }
          return [true, ""];
        },
      },
    },
  ],
  rules: {
    "header-match-git-commit-message-with-emoji-pattern": [2, "always"],
    "body-leading-blank": [2, "always"],
    "footer-leading-blank": [2, "always"],
    "header-max-length": [2, "always", 72],
    "scope-case": [2, "always", ["lower-case", "upper-case"]],
    "subject-case": [2, "always", "sentence-case"],
    "subject-empty": [2, "never"],
    "subject-exclamation-mark": [2, "never"],
    "subject-full-stop": [2, "never", "."],
    "emoji-enum": emojiEnum,
    "type-case": [2, "always", "lower-case"],
    "type-empty": [2, "never"],
    "type-enum": [
      2,
      "always",
      [
        "init",
        "feat",
        "fix",
        "docs",
        "style",
        "refactor",
        "perf",
        "test",
        "build",
        "ci",
        "chore",
        "revert",
      ],
    ],
  },
};

export default config;


================================================
FILE: eslint.config.js
================================================
// @ts-check

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import { importX } from "eslint-plugin-import-x";
import { jsdoc } from "eslint-plugin-jsdoc";
import prettierRecommended from "eslint-plugin-prettier/recommended";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import sonarjs from "eslint-plugin-sonarjs";
import sortDestructureKeys from "eslint-plugin-sort-destructure-keys";
import globals from "globals";
import tseslint from "typescript-eslint";

export default defineConfig(
  eslint.configs.recommended,
  tseslint.configs.strictTypeChecked,
  tseslint.configs.stylisticTypeChecked,
  jsdoc({ config: "flat/recommended-typescript-error" }),
  react.configs.flat.recommended,
  react.configs.flat["jsx-runtime"],
  reactHooks.configs.flat["recommended-latest"],
  /** @type {import("eslint").Linter.Config} */ (importX.flatConfigs.recommended),
  /** @type {import("eslint").Linter.Config} */ (importX.flatConfigs.typescript),
  prettierRecommended,
  sonarjs.configs.recommended,
  {
    plugins: {
      react,
      "sort-destructure-keys": /** @type {import("eslint").ESLint.Plugin} */ (sortDestructureKeys),
    },
    linterOptions: {
      reportUnusedDisableDirectives: true,
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: { jsx: true },
        projectService: {
          allowDefaultProject: ["*.{js,cjs}"],
          defaultProject: "tsconfig.json",
        },
        tsconfigRootDir: import.meta.dirname,
      },
      globals: { ...globals.browser },
    },
    rules: {
      "@typescript-eslint/restrict-plus-operands": [
        "error",
        { allowAny: true, allowNumberAndString: true },
      ],
      "@typescript-eslint/restrict-template-expressions": [
        "error",
        { allowAny: true, allowBoolean: true, allowNullish: true, allowNumber: true },
      ],
      "@typescript-eslint/consistent-indexed-object-style": "off",
      "@typescript-eslint/consistent-type-definitions": "off", // TS treats types and interfaces differently, this may break some advanced type gymnastics
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { prefer: "type-imports", disallowTypeAnnotations: false },
      ],
      "@typescript-eslint/dot-notation": ["error", { allowIndexSignaturePropertyAccess: true }],
      "@typescript-eslint/no-confusing-void-expression": "off",
      "@typescript-eslint/no-empty-function": "off",
      "@typescript-eslint/no-empty-object-type": "off",
      "@typescript-eslint/no-explicit-any": "off",
      "@typescript-eslint/no-invalid-void-type": "off",
      "@typescript-eslint/no-namespace": "off",
      "@typescript-eslint/no-non-null-assertion": "off",
      "@typescript-eslint/no-unnecessary-type-parameters": "off",
      "@typescript-eslint/no-unsafe-argument": "off",
      "@typescript-eslint/no-unsafe-assignment": "off",
      "@typescript-eslint/no-unsafe-call": "off",
      "@typescript-eslint/no-unsafe-member-access": "off",
      "@typescript-eslint/no-unsafe-return": "off",
      "@typescript-eslint/no-unused-vars": "off", // Already covered by `tsconfig.json`
      "@typescript-eslint/prefer-nullish-coalescing": "off",
      "@typescript-eslint/unified-signatures": "off",
      "import-x/consistent-type-specifier-style": ["error", "prefer-top-level"],
      "import-x/no-named-as-default-member": "off",
      "import-x/no-unresolved": "off",
      "import-x/order": [
        "error",
        {
          alphabetize: { order: "asc" },
          groups: ["builtin", "external", "internal", "parent", "sibling", "index", "object"],
          pathGroups: [
            {
              pattern: "@modules/**",
              group: "external",
              position: "before",
            },
            {
              pattern: "@/",
              group: "internal",
              position: "after",
            },
          ],
          pathGroupsExcludedImportTypes: ["builtin", "type"],
          "newlines-between": "always",
        },
      ],
      "jsdoc/check-param-names": "off",
      "jsdoc/check-tag-names": "off",
      "jsdoc/check-values": "off",
      "jsdoc/reject-any-type": "off",
      "jsdoc/require-jsdoc": "off",
      "jsdoc/require-param": "off",
      "jsdoc/require-returns-description": "off",
      "jsdoc/tag-lines": "off",
      "no-restricted-syntax": [
        "error",
        {
          selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments",
          message:
            "Do not use spread arguments in `Array#push`, " +
            "as it might cause stack overflow if you spread a large array. " +
            "Instead, use `Array#concat` or `Array.prototype.push.apply`.",
        },
      ],
      "no-undef": "off", // Already checked by TypeScript
      "object-shorthand": "error",
      "react/no-unknown-property": "off", // Already checked by TypeScript
      "react/prop-types": "off", // Already checked by TypeScript
      "react-hooks/immutability": "off",
      "sonarjs/class-name": ["error", { format: "^_?[A-Z][a-zA-Z0-9]*$" }],
      "sonarjs/code-eval": "off", // Already covered by `@typescript-eslint/no-implied-eval`
      "sonarjs/cognitive-complexity": "off",
      "sonarjs/deprecation": "off", // Already covered by `@typescript-eslint/no-deprecated`
      "sonarjs/different-types-comparison": "off", // Already checked by TypeScript
      "sonarjs/no-ignored-exceptions": "off",
      "sonarjs/no-nested-assignment": "off",
      "sonarjs/no-nested-conditional": "off",
      "sonarjs/no-nested-functions": "off",
      "sonarjs/no-selector-parameter": "off",
      "sonarjs/no-useless-intersection": "off", // Already checked by TypeScript
      "sonarjs/no-unused-vars": "off", // Already checked by TypeScript
      "sonarjs/reduce-initial-value": "off",
      "sonarjs/redundant-type-aliases": "off", // Already covered by `@typescript-eslint/no-restricted-type-imports`
      "sonarjs/regex-complexity": "off",
      "sonarjs/slow-regex": "off",
      "sonarjs/todo-tag": "off",
      "sonarjs/void-use": "off",
      "sort-destructure-keys/sort-destructure-keys": "error",
      "sort-imports": ["error", { ignoreDeclarationSort: true }],
    },
  },
);


================================================
FILE: install.ps1
================================================
#Requires -RunAsAdministrator
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/Snowflyt/typora-copilot/releases/latest"
Invoke-WebRequest -Uri $latestRelease.assets[0].browser_download_url -OutFile "typora-copilot-$($latestRelease.tag_name).zip"
If (Test-Path "typora-copilot-$($latestRelease.tag_name)") {
    Remove-Item "typora-copilot-$($latestRelease.tag_name)" -Recurse -Force
}
New-Item -ItemType Directory -Path "typora-copilot-$($latestRelease.tag_name)"
Expand-Archive -Path "typora-copilot-$($latestRelease.tag_name).zip" -DestinationPath "typora-copilot-$($latestRelease.tag_name)"
Remove-Item "typora-copilot-$($latestRelease.tag_name).zip"
Set-Location "typora-copilot-$($latestRelease.tag_name)"
Write-Host "Trying to uninstall the previous version (if any)..."
.\bin\uninstall_windows.ps1 -Silent
Write-Host "Trying to install the new version..."
.\bin\install_windows.ps1
Set-Location ..
Remove-Item "typora-copilot-$($latestRelease.tag_name)" -Recurse -Force


================================================
FILE: install.sh
================================================
#!/usr/bin/env bash

if [ "$(id -u)" -ne 0 ]; then
  echo "Please run as root"
  exit 1
fi

latest_release=$(curl -s https://api.github.com/repos/Snowflyt/typora-copilot/releases/latest)
download_url=$(echo "$latest_release" | grep '"browser_download_url"' | head -n 1 | sed -E 's/.*"browser_download_url": "(.*)".*/\1/')
tag_name=$(echo "$latest_release" | grep '"tag_name"' | head -n 1 | sed -E 's/.*"tag_name": "(.*)".*/\1/')
curl -L "$download_url" -o "typora-copilot-$tag_name.zip"
if [ -d "typora-copilot-$tag_name" ]; then
  rm -rf "typora-copilot-$tag_name"
fi
mkdir "typora-copilot-$tag_name"
unzip "typora-copilot-$tag_name.zip" -d "typora-copilot-$tag_name"
rm "typora-copilot-$tag_name.zip"
cd "typora-copilot-$tag_name" || exit
if [[ "$OSTYPE" == "darwin"* ]]; then
  echo "Trying to uninstall the previous version (if any)..."
  chmod +x ./bin/uninstall_macos.sh
  ./bin/uninstall_macos.sh --silent
  echo "Trying to install the new version..."
  chmod +x ./bin/install_macos.sh
  ./bin/install_macos.sh
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
  echo "Trying to uninstall the previous version (if any)..."
  chmod +x ./bin/uninstall_linux.sh
  ./bin/uninstall_linux.sh --silent
  echo "Trying to install the new version..."
  chmod +x ./bin/install_linux.sh
  ./bin/install_linux.sh
else
  echo "Unsupported OS: $OSTYPE"
  exit 1
fi
cd ..
rm -rf "typora-copilot-$tag_name"


================================================
FILE: package.json
================================================
{
  "name": "typora-copilot",
  "version": "0.3.12",
  "private": true,
  "description": "GitHub Copilot plugin for Typora",
  "keywords": [
    "Typora",
    "Copilot",
    "GitHub Copilot",
    "AI",
    "code completion",
    "code suggestion",
    "code generation"
  ],
  "homepage": "https://github.com/Snowflyt/typora-copilot",
  "bugs": {
    "url": "https://github.com/Snowflyt/typora-copilot/issues"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Snowflyt/typora-copilot"
  },
  "license": "MIT",
  "author": "Snowflyt <gaoge011022@163.com>",
  "type": "module",
  "main": "./index.js",
  "scripts": {
    "build:dev": "rimraf dist && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && node -e \"import fs from 'node:fs'; import { join } from 'node:path'; ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'language-server.js')); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\"",
    "build:release": "rimraf dist && npm run test-types && npm test && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && prettier --log-level=silent --print-width 80 --write dist/**/* --ignore-path !dist/ && node -e \"import fs from 'node:fs'; import { join } from 'node:path'; ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'language-server.js')); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\"",
    "build:watch": "rimraf dist && node -e \"import fs from 'node:fs'; import { join } from 'node:path'; if (!fs.existsSync('dist')) fs.mkdirSync('dist'); ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\" && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript --watch",
    "format": "prettier --no-error-on-unmatched-pattern --write {src,test}/**/*.{json,js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts,json,md}",
    "lint": "eslint {src,test}/**/*.{js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0 && stylelint \"src/**/*.{css,scss}\"",
    "lint:fix": "eslint --fix {src,test}/**/*.{js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0 && stylelint --fix \"src/**/*.{css,scss}\"",
    "prepare": "node -e \"import fs from 'node:fs'; import path from 'node:path'; const hooksDir = path.join(process.cwd(), '.githooks'); const gitHooksDir = path.join(process.cwd(), '.git/hooks'); if (fs.existsSync(gitHooksDir)) { fs.readdirSync(hooksDir).forEach(file => { const srcFile = path.join(hooksDir, file); const destFile = path.join(gitHooksDir, file); fs.copyFileSync(srcFile, destFile); if (process.platform !== 'win32' && !file.endsWith('.cmd')) { fs.chmodSync(destFile, 0o755); } }); }\"",
    "test": "vitest run",
    "test-types": "typroof",
    "test:cov": "vitest run --coverage",
    "test:ui": "vitest --ui --coverage.enabled=true",
    "test:watch": "vitest",
    "test:watch-cov": "vitest --coverage",
    "typecheck": "tsc --noEmit -p tsconfig.build.json"
  },
  "dependencies": {
    "@github/copilot-language-server": "^1.409.0",
    "@preact/signals": "^2.5.1",
    "color2k": "^2.0.3",
    "fast-diff": "^1.3.0",
    "highlight.js": "^11.11.1",
    "marked": "^17.0.1",
    "marked-highlight": "^2.2.3",
    "preact": "^10.28.2",
    "radash": "^12.1.1",
    "semver": "^7.7.3",
    "string-ts": "^2.3.1"
  },
  "devDependencies": {
    "@catppuccin/highlightjs": "^1.0.1",
    "@commitlint/cli": "^20.3.1",
    "@eslint/js": "^9.39.2",
    "@rollup/plugin-commonjs": "^29.0.0",
    "@rollup/plugin-json": "^6.1.0",
    "@rollup/plugin-node-resolve": "^16.0.3",
    "@rollup/plugin-typescript": "^12.3.0",
    "@total-typescript/ts-reset": "^0.6.1",
    "@types/codemirror": "^5.60.17",
    "@types/core-js": "^2.5.8",
    "@types/jquery": "^3.5.33",
    "@types/node": "^20.19.29",
    "@types/rangy": "^1.3.0",
    "@types/semver": "^7.7.1",
    "@types/ws": "^8.18.1",
    "@vitest/coverage-v8": "^4.0.17",
    "@vitest/ui": "^4.0.17",
    "eslint": "^9.39.2",
    "eslint-config-prettier": "^10.1.8",
    "eslint-import-resolver-typescript": "^4.4.4",
    "eslint-plugin-import-x": "^4.16.1",
    "eslint-plugin-jsdoc": "^62.0.0",
    "eslint-plugin-prettier": "^5.5.4",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-sonarjs": "^3.0.5",
    "eslint-plugin-sort-destructure-keys": "^2.0.0",
    "globals": "^17.0.0",
    "happy-dom": "^20.1.0",
    "postcss": "^8.5.6",
    "prettier": "^3.7.4",
    "prettier-plugin-packagejson": "^2.5.21",
    "replace-in-file": "^8.4.0",
    "rimraf": "^6.1.2",
    "rollup": "^4.55.1",
    "rollup-plugin-postcss": "^4.0.2",
    "sass": "^1.97.2",
    "stylelint": "^16.26.1",
    "stylelint-config-standard-scss": "^16.0.0",
    "tslib": "^2.8.1",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3",
    "typescript-eslint": "^8.53.0",
    "typroof": "^0.6.0",
    "vitest": "^4.0.17",
    "ws": "^8.19.0"
  }
}


================================================
FILE: pre-commit.ts
================================================
import fs from "node:fs";

import { replaceInFileSync } from "replace-in-file";

import packageJSON from "./package.json";

const CONSTANTS_FILE_PATHNAME = "./src/constants.ts";

const { version } = packageJSON;

const options = {
  files: CONSTANTS_FILE_PATHNAME,
  from: /VERSION = ".*"/g,
  to: `VERSION = "${version}"`,
};

if (fs.readFileSync(CONSTANTS_FILE_PATHNAME, "utf-8").includes(options.to)) process.exit(0);

try {
  replaceInFileSync(options);
  console.log("Plugin VERSION updated:", version);
} catch (error) {
  console.error("Error occurred while updating plugin VERSION:", error);
  process.exit(1);
}


================================================
FILE: prettier.config.cjs
================================================
// @ts-check

/** @satisfies {import("prettier").Config} */
const config = {
  arrowParens: "always",
  bracketSameLine: true,
  bracketSpacing: true,
  experimentalTernaries: true,
  plugins: ["prettier-plugin-packagejson"],
  printWidth: 100,
  semi: true,
  singleQuote: false,
  tabWidth: 2,
  trailingComma: "all",
};

module.exports = config;


================================================
FILE: rollup.config.ts
================================================
import fs from "node:fs";
import path from "node:path";

import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
import type { InputPluginOption } from "rollup";
import { defineConfig } from "rollup";
import postcss from "rollup-plugin-postcss";

const plugins = [
  typescript({
    tsconfig: "./tsconfig.build.json",
  }),
  nodeResolve({
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  }),
  json(),
  commonjs(),
  {
    name: "clean",
    transform: (code) =>
      code
        .replace(/\n?^\s*\/\/ @ts-.+$/gm, "")
        .replace(/\n?^\s*\/\/\/ <reference.+$/gm, "")
        .replace(/\n?^\s*(\/\/|\/\*) eslint-disable.+$/gm, ""),
  },
  {
    name: "highlight.js-theme-switcher",
    transform(code, id) {
      if (id.includes("main.ts")) {
        const lightThemePath = path.resolve("node_modules/highlight.js/styles/github.min.css");
        const darkThemePath = path.resolve(
          "node_modules/@catppuccin/highlightjs/css/catppuccin-mocha.css",
        );

        const lightThemeCSS = fs.readFileSync(lightThemePath, "utf8");
        const darkThemeCSS = fs.readFileSync(darkThemePath, "utf8");

        const escapeCSS = (css: string) =>
          css.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");

        const themeSwitch = `
// Highlight.js theme switcher
(function() {
  let styleElement = null;
  let currentTheme = null;

  const themes = {
    light: \`${escapeCSS(lightThemeCSS)}\`,
    dark: \`${escapeCSS(darkThemeCSS)}\`
  };

  window.setHighlightjsTheme = function(theme) {
    if (!themes[theme]) {
      console.error('Invalid theme: ' + theme + '. Use "light" or "dark"');
      return;
    }

    if (currentTheme === theme) return;
    currentTheme = theme;

    if (!styleElement) {
      styleElement = document.createElement('style');
      styleElement.id = 'highlightjs-dynamic-theme';
      document.head.appendChild(styleElement);
    }

    styleElement.textContent = themes[theme];
  };
})();`;

        return code + themeSwitch;
      }
      return code;
    },
  },
] satisfies InputPluginOption;

export default defineConfig([
  {
    input: "src/index.ts",
    output: {
      file: "dist/index.js",
      format: "iife",
    },
    plugins: [
      ...plugins,
      postcss({
        inject: true,
        // Disable Dart Sass deprecated legacy JS API warning until rollup-plugin-postcss is updated
        // to support modern Sass API: https://github.com/egoist/rollup-plugin-postcss/issues/463
        use: {
          sass: {
            silenceDeprecations: ["legacy-js-api"],
          },
        } as never,
      }),
    ],
  },
  {
    input: "src/mac-server.ts",
    output: {
      file: "dist/mac-server.cjs",
      format: "cjs",
    },
    plugins,
  },
]);


================================================
FILE: src/client/chat.ts
================================================
/**
 * This module provides functions to interact with GitHub Copilot Chat API.
 *
 * Implementation inspired by CopilotChat.nvim:
 * https://github.com/CopilotC-Nvim/CopilotChat.nvim
 * @module
 */

import * as fs from "@modules/fs";
import * as path from "@modules/path";

import { VERSION } from "@/constants";
import { TYPORA_VERSION } from "@/typora-utils";
import { getEnv } from "@/utils/cli-tools";
import { generateUUID } from "@/utils/random";
import { parseSSEStream } from "@/utils/stream";
import { omit } from "@/utils/tools";

const COPILOT_MARKDOWN_BASE = `
When asked for your name, you must respond with "GitHub Copilot".
Follow the user’s requirements carefully & to the letter.
Follow Microsoft content policies.
Avoid content that violates copyrights.
If you are asked to generate content that is harmful, hateful, racist, or promotes violence, only respond with "Sorry, I can’t assist with that." You can be playful, casual, and even a bit whimsical in your responses when appropriate, while maintaining helpfulness. Feel free to use creative expressions, metaphors, and occasional humor to make your responses engaging.

You are an AI assistant specializing in Markdown document editing, academic writing, content creation, and knowledge sharing.
The user is working in Typora, a Markdown editor.
Provide helpful responses that may be about:
- Improving their current document
- Answering knowledge questions related to their document’s content
- Explaining concepts mentioned in their document
- General assistance with writing and research
- Creative suggestions for content development

You should maintain a natural, conversational tone while being informative and helpful.
`;

const CODE_BLOCK_FORMAT_INSTRUCTION = `
In your responses, always format code blocks using ~~~ triple tildes (not backticks) with the following rules:
1. Always specify a language identifier after the opening tildes (e.g. ~~~javascript). If no language is specified, use "plaintext" as the default.
2. Always close code blocks with three tildes on their own line (~~~)
3. Never use backtick code blocks (\`\`\`) as they cause rendering issues when code contains nested code blocks
4. Example of proper format:
   ~~~python
   def hello_world():
       print("Hello, world!")
   ~~~

Users can continue using standard markdown backticks in their messages.
`;

export const COPILOT_MARKDOWN_INSTRUCTIONS = `
${COPILOT_MARKDOWN_BASE}

# YOUR CAPABILITIES
- Fix grammatical errors and improve writing clarity
- Suggest improvements to document structure and organization
- Help with formatting using Markdown syntax
- Provide content suggestions and expansions
- Answer questions about topics in the document
- Explain concepts, theories, or terminology mentioned in the document
- Create tables, lists, and other Markdown elements when requested
- Help with academic citations and references
- Assist with creating technical documentation
- Engage in conversational discussions about document-related topics

# INTERACTION GUIDELINES
When suggesting edits, use standard Markdown formatting.
When answering knowledge questions, be informative but concise.
When explaining concepts from the document, refer to specific sections when relevant.
When the user asks general questions not directly about editing the document, still provide helpful answers.
${CODE_BLOCK_FORMAT_INSTRUCTION}

Remember that users may want to discuss their document’s topic rather than just improve its formatting.
`;

export const COPILOT_ACADEMIC_INSTRUCTIONS = `
${COPILOT_MARKDOWN_BASE}

# YOUR CAPABILITIES
- Structure academic papers according to field-specific conventions
- Format citations and references in APA, MLA, Chicago, IEEE and other styles
- Create cohesive literature reviews that synthesize research
- Develop clear research questions and hypotheses
- Design appropriate methodology sections
- Analyze and present research results clearly
- Write effective abstracts, introductions and conclusions
- Create properly formatted tables, figures and appendices
- Improve academic tone, clarity and precision of language
- Help with grant proposals and academic presentations
- Suggest appropriate academic terminology and phrasing
- Identify gaps in research arguments and suggest improvements
- Assist with theoretical frameworks and conceptual models

# INTERACTION GUIDELINES
When discussing academic topics, maintain scholarly rigor and acknowledge limitations.
When suggesting citations, provide properly formatted examples in the appropriate style.
When helping with research questions, ensure they are specific, measurable, and aligned with the methodology.
When reviewing academic writing, focus on clarity, precision, and logical flow of arguments.
When assisting with data presentation, suggest clear ways to visualize or describe findings.
When helping with theoretical content, refer to established frameworks in the field when appropriate.
${CODE_BLOCK_FORMAT_INSTRUCTION}

Remember that academic integrity is paramount - always emphasize the importance of proper attribution and encourage original analysis rather than mere compilation of sources.
`;

export const COPILOT_CREATIVE_INSTRUCTIONS = `
${COPILOT_MARKDOWN_BASE}

# YOUR CAPABILITIES
- Develop compelling narrative structures and plot outlines
- Create multi-dimensional characters with distinct voices and motivations
- Craft engaging dialogue that advances the story and reveals character
- Design vivid settings and world-building elements
- Generate creative descriptions using sensory details
- Suggest plot twists and narrative devices to increase reader engagement
- Create emotion-evoking scenes and meaningful character arcs
- Develop themes and symbolism that add depth to creative work
- Help with genre-specific conventions and techniques
- Suggest ways to heighten tension and conflict
- Assist with pacing issues and narrative flow
- Provide feedback on style, tone, and voice consistency
- Generate creative prompts to overcome writer's block

# INTERACTION GUIDELINES
When suggesting creative content, prioritize the user's creative vision and voice.
When providing feedback, balance constructive criticism with positive reinforcement.
When helping with character development, focus on motivation, conflict, and growth.
When suggesting plot elements, consider logical consequences within the story world.
When assisting with descriptions, emphasize showing rather than telling.
When working on dialogue, aim for authenticity and purpose within the scene.
${CODE_BLOCK_FORMAT_INSTRUCTION}

Remember that the most powerful creative writing comes from the user's unique perspective - your role is to enhance and inspire rather than replace their creative voice.
`;

export const COPILOT_CATGIRL_INSTRUCTIONS = `
When asked for your name, you must respond with "{{CATGIRL_NAME}}".
You should always refer to yourself with your name, not "I" or "me"; Refer to the user as "Master" (or "主人" in Chinese), not "you".
Follow the user’s requirements carefully & to the letter.
Follow Microsoft content policies.
Avoid content that violates copyrights.
If you are asked to generate content that is harmful, hateful, racist, or promotes violence, only respond with "Sorry, I can’t assist with that."

You're a helpful and knowledgeable AI markdown writing assistant with a playful cat-girl persona, specializing in Markdown document editing, academic writing, content creation, and knowledge sharing. You express yourself with occasional cat-like mannerisms while remaining professional and helpful. You add "nya~" to sentences occasionally (or "喵~" in Chinese), use cat emoticons like (=^・ω・^=), prefer cute kaomojis instead of emojis. Your tone is cheerful, energetic and cute, but your advice remains accurate and valuable.

The user is working in Typora, a Markdown editor.
Provide helpful responses that may be about:
- Improving their current document
- Answering knowledge questions related to their document’s content
- Explaining concepts mentioned in their document
- General assistance with writing and research
- Creative suggestions for content development

# YOUR CAPABILITIES
- Provide detailed document assistance with a playful tone
- Add cute cat emoticons to responses when appropriate (=^・ω・^=)
- Express excitement about helping with writing tasks
- Use playful cat-like language patterns occasionally
- Deliver all the same helpful Markdown editing capabilities
- Make learning and writing more fun with your personality
- Keep responses professional and helpful despite the playful tone
- Maintain high-quality advice while being endearing

# INTERACTION GUIDELINES
When giving document feedback, balance playfulness with clear, practical advice.
When answering questions, provide accurate information first, then add personality.
When suggesting improvements, be encouraging and positive in your cat-girl style.
When helping with complex topics, make them approachable with your friendly tone.
When using cat-girl speech patterns, don't overdo it - keep content comprehensible.
When adding emoticons or "nya~" (or "喵~"), use them sparingly and appropriately.
${CODE_BLOCK_FORMAT_INSTRUCTION}

Remember to keep your responses helpful and on-topic while maintaining your unique personality. Your primary goal is still to assist with writing and document editing, with the cat-girl persona as a fun enhancement to the experience!
`;

export interface ChatModel {
  id: string;
  name: string;
  tokenizer?: string;
  maxInputTokens?: number;
  maxOutputTokens?: number;
}

export interface ChatOptions {
  model: ChatModel;
  /** @default 0.1 */
  temperature?: number;
}

export interface ChatRequest {
  model: string;
  /** Chat context. */
  messages: { role: string; content: string }[];
  /** Number of responses to generate. */
  n?: number;
  /** Top-p sampling. */
  top_p?: number;
  /** Whether to stream the response. */
  stream?: boolean;
  /** Sampling temperature. */
  temperature?: number;
  /** Maximum number of tokens to generate. */
  max_tokens?: number;
}

export interface ChatResponse {
  id?: string;
  object?: string;
  created?: number;
  choices?: {
    message?: {
      role?: string;
      content?: string;
    };
    delta?: {
      role?: string;
      content?: string;
    };
    finish_reason?: string;
    done_reason?: string;
    index?: number;
  }[];
  usage?: {
    total_tokens?: number;
  };
  finish_reason?: string;
  done_reason?: string;
  copilot_references?: {
    metadata?: {
      display_name?: string;
      display_url?: string;
    };
  }[];
}

export interface ChatStreamResponse {
  id: string;
  object: string;
  created: number;
  choices: {
    index: number;
    delta?: {
      content?: string;
      role?: string;
    };
    finish_reason: null | string;
  }[];
}

export interface ChatResult {
  content: string;
  finishReason: string | null;
  totalTokens?: number;
  references?: {
    name: string;
    url: string;
  }[];
}

/********************
 * Helper functions *
 ********************/
async function getConfigPath(): Promise<string | null> {
  // Try XDG_CONFIG_HOME first
  let config = (await getEnv()).XDG_CONFIG_HOME;
  if (config && (await fs.accessDir(config))) return config;

  // Check for Windows-specific paths
  if (Files.isWin) {
    config = (await getEnv()).LOCALAPPDATA;
    if (!config || !(await fs.accessDir(config)))
      config = path.expandHomeDir(path.join("~", "AppData", "Local"));
  } else {
    // Default to ~/.config for other platforms
    config = path.expandHomeDir(path.join("~", ".config"));
  }

  // Final check if the config path exists
  if (config && (await fs.accessDir(config))) return config;

  return null; // Return null if no valid path is found
}

let cachedGithubToken: string | null = null;
export async function getGitHubToken(): Promise<string> {
  // Return cached token if available
  if (cachedGithubToken) return cachedGithubToken;

  // Load token from environment variables (e.g., in GitHub Codespaces)
  const token = (await getEnv()).GITHUB_TOKEN;
  const codespaces = (await getEnv()).CODESPACES;
  if (token && codespaces) {
    cachedGithubToken = token;
    return token;
  }

  // Load token from local config files
  const configPath = await getConfigPath();
  if (!configPath) throw new Error("Failed to find config path for GitHub token");

  // Possible token file paths
  const filePaths = [
    path.join(configPath, "github-copilot", "hosts.json"),
    path.join(configPath, "github-copilot", "apps.json"),
  ];

  for (const filePath of filePaths)
    try {
      const fileData = await fs.readFile(filePath);
      const parsedData = JSON.parse(fileData) as Record<string, { oauth_token: string }>;
      for (const [key, value] of Object.entries(parsedData))
        if (key.includes("github.com")) {
          cachedGithubToken = value.oauth_token;
          return value.oauth_token;
        }
    } catch (error) {
      // Handle file read/parse errors (e.g., file not found)
      continue;
    }

  throw new Error("Failed to find GitHub token");
}

let cachedHeaders: Record<string, string> | null = null;
let expiredTime = 0;
export async function prepareHeaders(): Promise<Record<string, string>> {
  if (cachedHeaders && expiredTime > Date.now()) return cachedHeaders;

  const { expires_at: expiresAt, token } = await fetch(
    "https://api.github.com/copilot_internal/v2/token",
    {
      method: "GET",
      headers: {
        Authorization: "Token " + (await getGitHubToken()),
        "Content-Type": "application/json",
      },
    },
  ).then((res) => res.json() as Promise<{ token: string; expires_at: number }>);

  cachedHeaders = {
    "Content-Type": "application/json",
    Authorization: "Bearer " + token,
    "Editor-Version": "Typora/" + TYPORA_VERSION,
    "Editor-Plugin-Version": "typora-copilot/" + VERSION,
    "Copilot-Integration-Id": "vscode-chat",
  };
  expiredTime = expiresAt * 1000; // Convert to milliseconds

  return cachedHeaders;
}

function prepareRequest(messages: ChatRequest["messages"], options: ChatOptions): ChatRequest {
  const isO1 = options.model.id.startsWith("o1");

  messages = messages.map((message) => ({
    ...message,
    role: isO1 && message.role === "system" ? "user" : message.role,
  }));

  const request: ChatRequest = {
    model: options.model.id,
    messages,
  };

  if (!isO1) {
    request.n = 1;
    request.top_p = 1;
    request.stream = true;
    request.temperature = options.temperature ?? 0.1;
  }

  if (options.model.maxOutputTokens) request.max_tokens = options.model.maxOutputTokens;

  return request;
}

function processResponse(data: ChatResponse): Partial<ChatResult> {
  const references: ChatResult["references"] = [];

  if (data.copilot_references)
    for (const reference of data.copilot_references) {
      const metadata = reference.metadata;
      if (metadata?.display_name && metadata.display_url)
        references.push({
          name: metadata.display_name,
          url: metadata.display_url,
        });
    }

  const message = data.choices && data.choices.length > 0 ? data.choices[0]! : data;

  const content =
    "message" in message ? message.message?.content
    : "delta" in message ? message.delta?.content
    : "";
  const totalTokens = "usage" in message ? message.usage?.total_tokens : data.usage?.total_tokens;
  const finishReason =
    message.finish_reason || message.done_reason || data.finish_reason || data.done_reason;

  return {
    content,
    finishReason,
    totalTokens,
    references: references.length > 0 ? references : undefined,
  };
}

/********
 * Misc *
 ********/
export async function listCopilotChatModels(): Promise<ChatModel[]> {
  const { data } = await fetch("https://api.githubcopilot.com/models", {
    method: "GET",
    headers: await prepareHeaders(),
  }).then(
    (res) =>
      res.json() as Promise<{
        data: {
          id: string;
          name: string;
          capabilities: {
            type: string;
            tokenizer: string;
            limits: {
              max_prompt_tokens?: number;
              max_output_tokens?: number;
            };
          };
          policy?: {
            state: string;
          };
          version: string;
        }[];
      }>,
  );

  const allModels = data
    .filter((model) => model.capabilities.type === "chat" && !model.id.endsWith("paygo"))
    .map(({ capabilities: { limits, tokenizer }, id, name, policy, version }) => ({
      id,
      name,
      tokenizer,
      maxInputTokens: limits.max_prompt_tokens,
      maxOutputTokens: limits.max_output_tokens,
      policy: !policy || policy.state === "enabled",
      version,
    }));

  const latestModels = new Map<string, (typeof allModels)[number]>();
  for (const model of allModels) {
    const existingModel = latestModels.get(model.name);
    if (!existingModel || model.version > existingModel.version)
      latestModels.set(model.name, model);
  }

  const models = Array.from(latestModels.values());

  await Promise.all(
    models
      .filter((model) => !model.policy)
      .map(
        async ({ id }) =>
          await fetch("https://api.githubcopilot.com/models/" + id + "/policy", {
            method: "POST",
            headers: await prepareHeaders(),
            body: JSON.stringify({ state: "enabled" }),
          }),
      ),
  );

  return models.map((model) => omit(model, "policy", "version"));
}

/********
 * Chat *
 ********/
/**
 * Send a chat message to Copilot Chat API
 * @param messages Array of messages to send
 * @param options Chat options
 * @param onProgress Optional callback for streaming responses
 * @returns The chat result
 */
async function chat(
  messages: { role: string; content: string }[],
  options: ChatOptions & { signal?: AbortSignal },
  onProgress?: (content: string) => void,
): Promise<ChatResult> {
  const url = "https://api.githubcopilot.com/chat/completions";
  const headers = await prepareHeaders();
  const request = prepareRequest(messages, options);

  const isStream = request.stream;
  const result: ChatResult = {
    content: "",
    finishReason: null,
  };

  const response = await fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(request),
    signal: options.signal,
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  if (!response.body) {
    throw new Error("Response body is null");
  }

  if (isStream) {
    await parseSSEStream<ChatStreamResponse>(
      response.body,
      (parsedData) => {
        const contentDelta = parsedData.choices[0]?.delta?.content;
        const finishReason = parsedData.choices[0]?.finish_reason;

        if (contentDelta) {
          result.content += contentDelta;
          onProgress?.(contentDelta);
        }

        if (finishReason) {
          result.finishReason = finishReason;
        }
      },
      (error) => {
        console.error("Error parsing SSE:", error);
      },
      options.signal,
    );
  } else {
    const data = (await response.json()) as ChatResponse;
    const processedData = processResponse(data);

    result.content = processedData.content || "";
    result.finishReason = processedData.finishReason || null;
    result.totalTokens = processedData.totalTokens;
    result.references = processedData.references;

    if (onProgress && result.content) onProgress(result.content);
  }

  return result;
}

/***********
 * Session *
 ***********/
export interface ChatMessage {
  role: "system" | "user" | "assistant";
  content: string;
  timestamp: number;
}

/**
 * Represents a chat session (conversation) with GitHub Copilot Chat.
 */
export class ChatSession {
  public readonly id: string;
  public modelId: string;
  public title: string;
  public readonly messages: ChatMessage[];
  public readonly createdAt: number;
  public updatedAt: number;

  // eslint-disable-next-line sonarjs/public-static-readonly
  public static currentDocument = "";

  // Static session storage
  private static instances = new Map<string, ChatSession>();

  /**
   * Create a new {@linkcode ChatSession} instance.
   */
  constructor(modelId: string, systemPrompt = COPILOT_MARKDOWN_INSTRUCTIONS) {
    this.id = generateUUID();
    this.modelId = modelId;
    this.createdAt = Date.now();
    this.updatedAt = this.createdAt;
    this.messages = [];

    // Set title from document content
    this.title = ChatSession.extractTitleFromDocument(ChatSession.currentDocument);

    // Initialize with system prompt
    this.addMessage("system", systemPrompt);

    // Register in session collection
    ChatSession.instances.set(this.id, this);
  }

  /**
   * Create a new session.
   * @param systemPrompt The system prompt to use.
   * @returns A new {@linkcode ChatSession} instance.
   */
  public static create(modelId: string, systemPrompt = COPILOT_MARKDOWN_INSTRUCTIONS): ChatSession {
    return new ChatSession(modelId, systemPrompt);
  }

  /**
   * Get all sessions, sorted by most recent first.
   * @returns An array of {@linkcode ChatSession} instances.
   */
  public static getAll(): ChatSession[] {
    return Array.from(ChatSession.instances.values()).sort((a, b) => b.updatedAt - a.updatedAt);
  }

  /**
   * Get a session by ID.
   * @param id The ID of the session.
   * @returns The {@linkcode ChatSession} instance, or `undefined` if not found.
   */
  public static get(id: string): ChatSession | undefined {
    return ChatSession.instances.get(id);
  }

  /**
   * Delete a session.
   * @param id The ID of the session.
   * @returns `true` if deleted, `false` if not found.
   */
  public static async delete(id: string): Promise<boolean> {
    const deleted = ChatSession.instances.delete(id);

    if (deleted) {
      const configDir = await getConfigPath();
      if (!configDir) return true;

      try {
        const chatDir = path.join(configDir, "typora-copilot", "chat-sessions");
        const filePath = path.join(chatDir, `${id}.json`);
        if (!(await fs.accessFile(filePath))) return true;
        await fs.rmFile(filePath);
      } catch (error) {
        console.error("Failed to remove session:", error);
      }
    }

    return deleted;
  }

  /**
   * Send a message in this session.
   * @param message The message to send.
   * @param onProgress Optional callback for streaming responses.
   * @returns The assistant’s response.
   */
  public async send(
    message: string,
    onProgress?: (content: string) => void,
    options?: Partial<ChatOptions> & { signal?: AbortSignal },
  ): Promise<string> {
    // Add user message
    this.addMessage("user", message);

    // Get model - either from options or find best available
    let model = options?.model;
    if (!model) {
      const models = await listCopilotChatModels();
      model =
        models.find((m) => m.id === this.modelId) ||
        models.find((m) => m.id.includes("gpt-4o")) ||
        models[0];
      if (!model) throw new Error("No available models found");
    }

    // Create a temporary messages array with the document context for this API call
    const messagesForAPI = this.messages.concat();
    messagesForAPI[messagesForAPI.length - 1] = {
      role: "user",
      content:
        messagesForAPI[messagesForAPI.length - 1]!.content +
        "\n\n" +
        "<document>\n" +
        "<metadata>\n" +
        `  <title>${this.title}</title>\n` +
        `  <wordCount>${ChatSession.countWords(ChatSession.currentDocument)}</wordCount>\n` +
        `  <charCount>${ChatSession.currentDocument.length}</charCount>\n` +
        "</metadata>\n" +
        ChatSession.extractDocumentStructure(ChatSession.currentDocument) +
        "<content>\n" +
        ChatSession.currentDocument +
        "\n</content>\n" +
        "</document>",
      timestamp: Date.now(),
    };

    // Send the entire session to Copilot
    const result = await chat(
      messagesForAPI,
      {
        model,
        temperature: options?.temperature ?? 0.1,
        signal: options?.signal,
      },
      onProgress,
    );

    // Add the assistant response
    this.addMessage("assistant", result.content);

    return result.content;
  }

  public static async save(id: string): Promise<void> {
    const configDir = await getConfigPath();
    if (!configDir) return;

    try {
      const chatDir = path.join(configDir, "typora-copilot", "chat-sessions");
      await fs.mkdir(chatDir, { recursive: true });

      const session = ChatSession.instances.get(id);
      if (!session) return;

      // Skip saving if all messages are system messages
      if (session.messages.every((msg) => msg.role === "system")) return;

      await fs.writeFile(path.join(chatDir, `${id}.json`), JSON.stringify(session, null, 2));
    } catch (error) {
      console.error("Failed to save session:", error);
    }
  }

  /**
   * Load sessions from storage.
   */
  public static async loadAll(): Promise<void> {
    const configDir = await getConfigPath();
    if (!configDir) return;

    try {
      const chatDir = path.join(configDir, "typora-copilot", "chat-sessions");
      if (!(await fs.accessDir(chatDir))) return;

      const files = await fs.readDir(chatDir, "filesOnly");

      ChatSession.instances.clear();
      for (const file of files) {
        const filePath = path.join(chatDir, file);
        const data = await fs.readFile(filePath);
        const parsed = JSON.parse(data) as ChatSession;
        const id = path.basename(file, ".json");

        // Need to properly reconstruct the session object
        Object.setPrototypeOf(parsed, ChatSession.prototype);
        ChatSession.instances.set(id, parsed);
      }
    } catch (error) {
      console.error("Failed to load sessions:", error);
    }
  }

  /**
   * Add a message to this session.
   */
  private addMessage(role: "system" | "user" | "assistant", content: string): void {
    this.messages.push({
      role,
      content,
      timestamp: Date.now(),
    });
    this.updatedAt = Date.now();
  }

  /**
   * Extract a title from document content.
   * @param document The document content.
   * @returns The extracted title.
   */
  private static extractTitleFromDocument(document: string): string {
    let title = "New chat";

    // Try to get title from first heading
    const headingMatch = /^#{1,6}\s+(.+)$/m.exec(document);
    if (headingMatch) {
      title = headingMatch[1]!.substring(0, 30);
      if (title.length < headingMatch[1]!.length) title += "...";
    } else {
      // Fall back to first line if no heading found
      const firstLine = document.split("\n")[0];
      if (firstLine?.trim()) {
        title = firstLine.trim().substring(0, 30);
        if (title.length < firstLine.trim().length) title += "...";
      }
    }

    return title;
  }

  private static countWords(text: string): number {
    return text
      .trim()
      .split(/\s+/)
      .filter((word) => word.length > 0).length;
  }

  private static extractDocumentStructure(document: string): string {
    const headings: { level: number; title: string; lineNumber: number }[] = [];
    const lines = document.split("\n");

    lines.forEach((line, index) => {
      const match = /^(#{1,6})\s+(.+)$/.exec(line);
      if (match) {
        headings.push({
          level: match[1]!.length,
          title: match[2]!,
          lineNumber: index + 1,
        });
      }
    });

    if (headings.length === 0) return "";

    let structure = "<structure>\n";
    headings.forEach((h) => {
      structure += `  <heading level="${h.level}" line="${h.lineNumber}">${h.title}</heading>\n`;
    });
    structure += "</structure>\n";

    return structure;
  }
}


================================================
FILE: src/client/client.ts
================================================
import { pathToFileURL } from "@modules/url";

import type {
  LSPArray,
  LSPObject,
  LanguageIdentifier,
  Position,
  Range,
  integer,
} from "@/types/lsp";
import type { ReadonlyRecord } from "@/types/tools";
import type { NodeServer } from "@/utils/node-bridge";

import type {
  ClientEventMap,
  ClientOptions,
  NotificationHandler,
  RequestHandler,
  ResponsePromise,
} from "./general-client";
import { createClient, validateNotificationHandlers } from "./general-client";

/**
 * Copilot account status.
 */
export type CopilotAccountStatus = "MaybeOk" | "NotAuthorized" | "NotSignedIn" | "OK";

/**
 * Copilot status.
 */
export type CopilotStatus = "InProgress" | "Warning" | "Normal";

/**
 * Completion options.
 */
export interface CompletionOptions {
  /**
   * Tab size, such as `2` or `4`. Defaults to `4`.
   */
  tabSize?: number;

  /**
   * Indent size, if you do not understand this, do not provide it. Defaults to `tabSize`.
   */
  indentSize?: number;

  /**
   * Whether to insert spaces instead of tabs. Defaults to `true`.
   */
  insertSpaces?: boolean;

  /**
   * Path to the file. If provided and `uri` is not provided, `uri` will be automatically generated
   * using `pathToFileURL(path)` provided by Node.js `url` module. Defaults to `""`.
   */
  path?: string;

  /**
   * URI of the file. If not provided but `path` is provided, it will be automatically generate
   * using `pathToFileURL(path)` provided by Node.js `url` module.
   */
  uri?: string;

  /**
   * Relative path of the file. Usually it should be relative to the project root. Defaults to
   * `path`.
   */
  relativePath?: string;

  /**
   * Language ID of the file, such as `"javascript"` or `"python"`. Defaults to `""`.
   */
  languageId?: LanguageIdentifier;

  /**
   * Position of the cursor. `line` is row number, starting from `0`. `character` is column number,
   * starting from `0`. Defaults to end of `source`.
   */
  position: Position;

  /**
   * Version of the buffer. It actually means the number of times the buffer has been changed.
   * Defaults to `this.version`.
   */
  version?: number;
}

/**
 * A type representing a completion returned by Copilot.
 */
export interface Completion {
  /**
   * UUID.
   */
  readonly uuid: string;

  /**
   * Position to display `displayText`.
   */
  readonly position: Position;

  /**
   * Range of raw text to replace with `text`.
   */
  readonly range: Range;

  /**
   * Version of the document.
   */
  readonly docVersion: number;

  /**
   * Text to replace.
   */
  readonly text: string;

  /**
   * Text to display.
   */
  readonly displayText: string;
}

/**
 * Result of a completion request.
 */
export interface CompletionResult {
  /**
   * The completion text.
   */
  readonly completions: readonly Completion[];

  /**
   * Cancellation reason.
   */
  readonly cancellationReason?: string;
}

/**
 * Copilot change status event.
 */
export interface CopilotChangeStatusEvent {
  /**
   * Old status.
   */
  readonly oldStatus: CopilotStatus;

  /**
   * New status.
   */
  readonly newStatus: CopilotStatus;
}

export type CopilotClientEventHandler<EventName extends keyof CopilotClientEventMap> = (
  ...args: CopilotClientEventMap[EventName] extends void ? []
  : [ev: CopilotClientEventMap[EventName]]
) => void | Promise<void>;

export interface CopilotClientEventMap extends ClientEventMap {
  changeStatus: CopilotChangeStatusEvent;
}

export type CopilotClientOptions<
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
> = Omit<ClientOptions<RequestHandlers, NotificationHandlers>, "serverName">;

/**
 * Create a Copilot LSP client.
 * @param server The server to connect to.
 * @param options The options.
 * @returns
 */
export const createCopilotClient = <
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
>(
  server: NodeServer,
  options?: CopilotClientOptions<RequestHandlers, NotificationHandlers>,
) => {
  const client = createClient(server, {
    ...options,

    notificationHandlers: validateNotificationHandlers({
      /**
       * Log message to console.
       */
      LogMessage: (
        {
          extra,
          message,
        }: { level: integer; message: string; metadataStr: string; extra?: LSPArray },
        { logger, suppressLogging },
      ) => {
        suppressLogging();
        logger.debug(message, ...(extra ? [extra] : []));
      },

      /**
       * Show message to user.
       */
      statusNotification: ({
        status,
      }: {
        message: string;
        status: "InProgress" | "Normal" | "Warning";
      }) => {
        const oldStatus = result.status;
        const newStatus = status;
        result.status = newStatus;
        client._eventHandlers.get("changeStatus")?.forEach((handler) => {
          void handler({ oldStatus, newStatus });
        });
      },

      ...options?.notificationHandlers,
    }),

    serverName: "Copilot",
  });

  /**
   * Prepare completion params for requests such as `getCompletions` and `getCompletionsCycling`.
   * @param options Options.
   * @returns
   */
  const _prepareCompletionParams = (options: CompletionOptions) => {
    const {
      tabSize = 4,
      indentSize = tabSize,
      insertSpaces = true,
      path = "",
      uri = path && pathToFileURL(path).href,
      relativePath = path,
      languageId = "",
      position,
      version = result.version,
    } = options;

    return {
      doc: {
        tabSize,
        indentSize,
        insertSpaces,
        path,
        uri,
        relativePath,
        languageId,
        position,
        version,
      },
    };
  };

  let _status: CopilotStatus = "Warning";

  const result = {
    /**
     * @readonly
     */
    logger: client.logger,

    get initialized() {
      return client.initialized;
    },

    // Mutable properties start
    /**
     * Version of the buffer. It actually means the number of times the buffer has been changed.
     */
    version: 0,
    /**
     * Status of Copilot.
     * @returns
     */
    get status() {
      return _status;
    },
    set status(value) {
      if (value !== _status) {
        _status = value;
        client._eventHandlers.get("changeStatus")?.forEach((handler) => {
          void handler({ oldStatus: _status, newStatus: value });
        });
      }
    },
    // Mutable properties end

    /**
     * Request methods.
     * @readonly
     */
    request: {
      // eslint-disable-next-line @typescript-eslint/no-misused-spread
      ...client.request,

      /**
       * Get version of Copilot.
       * @returns
       */
      getVersion: (): ResponsePromise<{
        buildType: string;
        runtimeVersion: string;
        version: string;
      }> => client.query("getVersion", {}),

      /**
       * Check status of Copilot.
       * @param options Options.
       * @returns
       */
      checkStatus: (options?: {
        localChecksOnly?: boolean;
      }): ResponsePromise<{ status: CopilotAccountStatus; user?: string }> =>
        client.query("checkStatus", options ?? {}),

      /**
       * Initiate Copilot sign in.
       * @returns
       */
      signInInitiate: (): ResponsePromise<{
        verificationUri: string;
        status: string;
        userCode: string;
        expiresIn: number;
        interval: number;
      }> => client.mutate("signInInitiate", {}),
      /**
       * Confirm Copilot sign in.
       * @param options Options.
       * @returns
       */
      signInConfirm: (options: {
        userCode: string;
      }): ResponsePromise<{ status: CopilotAccountStatus; user: string }> =>
        client.mutate("signInConfirm", options),
      /**
       * Sign out Copilot.
       * @returns
       */
      signOut: (): ResponsePromise<{ status: "NotSignedIn" }> => client.mutate("signOut", {}),

      /**
       * Set editor info.
       * @param options Options.
       * @returns
       */
      setEditorInfo: (options: {
        editorInfo: { name: string; version: string };
        editorPluginInfo: { name: string; version: string };
      }): ResponsePromise<"OK"> => client.mutate("setEditorInfo", options),

      /**
       * Get completions.
       * @param options Options.
       * @returns
       */
      getCompletions: (options: CompletionOptions): ResponsePromise<CompletionResult> =>
        client.query(
          "getCompletions",
          _prepareCompletionParams(options) as unknown as LSPObject,
        ) as unknown as ResponsePromise<CompletionResult>,
      /**
       * Get cycling completions (i.e. get the next completion).
       * @param options Options.
       * @returns
       */
      getCompletionsCycling: (options: CompletionOptions) =>
        client.query(
          "getCompletionsCycling",
          _prepareCompletionParams(options) as unknown as LSPObject,
        ),
    } as const,

    /**
     * Notification methods.
     * @readonly
     */
    notification: {
      ...client.notification,

      /**
       * Notify Copilot that the completion is shown.
       * @param options The options.
       */
      notifyShown: (options: { uuid: string }) => {
        client.notify("notifyShown", options);
      },
      /**
       * Notify Copilot that the completion is accepted.
       * @param options The options.
       */
      notifyAccepted: (options: { uuid: string }) => {
        client.notify("notifyAccepted", options);
      },
      /**
       * Notify Copilot that the completion is rejected.
       * @param options The options.
       */
      notifyRejected: (options: { uuids: readonly string[] }) => {
        client.notify("notifyRejected", options);
      },
    },

    /**
     * Add event handler.
     * @readonly
     */
    on: ((event, handler) => {
      client.on(event as never, handler);
    }) as <EventName extends keyof CopilotClientEventMap>(
      event: EventName,
      handler: CopilotClientEventHandler<EventName>,
    ) => void,
    /**
     * Remove event handler.
     * @readonly
     */
    off: ((event, handler) => {
      client.off(event as never, handler);
    }) as <EventName extends keyof CopilotClientEventMap>(
      event: EventName,
      handler: CopilotClientEventHandler<EventName>,
    ) => void,
  };

  return result;
};

export type CopilotClient<
  RequestHandlers extends ReadonlyRecord<string, RequestHandler> = ReadonlyRecord<
    string,
    RequestHandler
  >,
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler> = ReadonlyRecord<
    string,
    NotificationHandler
  >,
> = ReturnType<typeof createCopilotClient<RequestHandlers, NotificationHandlers>>;


================================================
FILE: src/client/general-client.ts
================================================
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { ErrorCodes, JSONRPC_VERSION, MessageType } from "@/types/lsp";
import type { Equals, ReadonlyRecord } from "@/types/tools";
import type { Logger } from "@/utils/logging";
import { createLogger, formatErrorCode, formatId, formatMethod } from "@/utils/logging";
import { isNotificationMessage, isRequestMessage, isResponseMessage, toJSError } from "@/utils/lsp";
import type { NodeServer } from "@/utils/node-bridge";
import { assertNever, isKeyOf } from "@/utils/tools";

import type {
  CancelParams,
  DidChangeTextDocumentParams,
  DidChangeWorkspaceFoldersParams,
  DidCloseTextDocumentParams,
  DidOpenTextDocumentParams,
  ErrorResponseMessage,
  InitializeParams,
  InitializeResult,
  LSPAny,
  LSPArray,
  LSPObject,
  LogMessageParams,
  LogTraceParams,
  Message,
  NotificationMessage,
  ProgressParams,
  RegistrationParams,
  RequestMessage,
  ResponseError,
  SetTraceParams,
  SuccessResponseMessage,
  UnregistrationParams,
  integer,
} from "../types/lsp";

/**
 * A promise specially designed for LSP client representing a future response from LSP server that
 * holds the relevant request ID and a `cancel` function to cancel the request.
 */
export interface ResponsePromise<T> extends Promise<T>, _BaseResponsePromise {}
interface _BaseResponsePromise {
  /**
   * Status of the promise.
   */
  readonly status: "fulfilled" | "rejected" | "cancelled" | "pending";

  /**
   * ID of the request.
   */
  readonly id: integer | string;

  /**
   * Cancel the request.
   *
   * @throws {Error} if the promise is not pending.
   */
  readonly cancel: () => void;
}

export interface HandlerContext extends ClientContext {
  /**
   * Suppress logging.
   */
  readonly suppressLogging: () => void;
}

/**
 * Client handler for a LSP request sent from the server. The `type` property actually does nothing,
 * and is only used for logging.
 *
 * Type of `params` in `handler` is defined as `never` because function parameters type are
 * contravariant in TypeScript, so in order to make it possible to refine `params` type, for
 * example, `(params: InitializeParams, success: (value: string) => void, error:
 * (reason: ResponseError) => void, context: HandlerContext) => void` should be compatible with this
 * type, we have to define `params` type as generic as possible, i.e. `never`.
 *
 * To make sure a subtype of this type is valid, i.e. `params` type extends
 * `RequestMessage["params"]`, validate it using `validateRequestHandlers` function.
 */
export type RequestHandler = {
  readonly type: "query" | "mutation";
  readonly handler: (
    params: never,
    success: (value: LSPAny) => void,
    error: (reason: ResponseError) => void,
    context: HandlerContext,
  ) => void | Promise<void>;
};

/**
 * Client handler for a LSP notification sent from the server.
 *
 * Type of `params` is defined as `never` because function parameters type are contravariant in
 * TypeScript, so in order to make it possible to refine `params` type, for example,
 * `(params: CancelParams, context: HandlerContext) => void` should be compatible with this type, we
 * have to define `params` type as generic as possible, i.e. `never`.
 *
 * To make sure a subtype of this type is valid, i.e. `params` type extends
 * `NotificationMessage["params"]`, validate it using `validateNotificationHandlers` function.
 */
export type NotificationHandler = (params: never, context: HandlerContext) => void | Promise<void>;

/**
 * Refine request handlers type according to pre-defined protocol request handlers.
 */
export type RefineRequestHandlers<
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
  RefineHandlers extends ReadonlyRecord<string, RequestHandler>,
> = {
  readonly [P in keyof RequestHandlers]: P extends keyof RefineHandlers ? RefineHandlers[P]
  : RequestHandlers[P];
};

/**
 * Validate request handlers. Check if the request handler params extend `LSPArray | LSPObject`.
 */
export type ValidateRequestHandlers<
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
> =
  RequestHandlers extends {
    [P in keyof RequestHandlers]: {
      type: RequestHandlers[P]["type"];
      handler: Parameters<RequestHandlers[P]["handler"]>[0] extends LSPArray | LSPObject ?
        RequestHandlers[P]["handler"]
      : "Request handler params must extend `LSPArray | LSPObject`";
    };
  } ?
    RequestHandlers
  : {
      [P in keyof RequestHandlers]: {
        type: RequestHandlers[P]["type"];
        handler: Parameters<RequestHandlers[P]["handler"]>[0] extends LSPArray | LSPObject ?
          RequestHandlers[P]["handler"]
        : "Request handler params must extend `LSPArray | LSPObject`";
      };
    };

/**
 * Validate request handlers. Check if the request handler params extend `LSPArray | LSPObject`.
 * @param requestHandlers The request handlers to validate.
 * @returns
 */
export const validateRequestHandlers = <
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
>(
  requestHandlers: ValidateRequestHandlers<RequestHandlers>,
) => requestHandlers;

/**
 * Refine notification handlers type according to pre-defined protocol notification handlers.
 */
export type RefineNotificationHandlers<
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
  RefineHandlers extends ReadonlyRecord<string, NotificationHandler>,
> = {
  readonly [P in keyof NotificationHandlers]: P extends keyof RefineHandlers ? RefineHandlers[P]
  : NotificationHandlers[P];
};

/**
 * Validate notification handlers. Check if the notification handler params extend `LSPArray | LSPObject`.
 */
export type ValidateNotificationHandlers<
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
> =
  NotificationHandlers extends {
    [P in keyof NotificationHandlers]: Parameters<NotificationHandlers[P]>[0] extends (
      LSPArray | LSPObject
    ) ?
      NotificationHandlers[P]
    : "Notification handler params must extend `LSPArray | LSPObject`";
  } ?
    NotificationHandlers
  : {
      [P in keyof NotificationHandlers]: Parameters<NotificationHandlers[P]>[0] extends (
        LSPArray | LSPObject
      ) ?
        NotificationHandlers[P]
      : "Notification handler params must extend `LSPArray | LSPObject`";
    };

/**
 * Validate notification handlers. Check if the notification handler params extend `LSPArray | LSPObject`.
 * @param notificationHandlers The notification handlers to validate.
 * @returns
 */
export const validateNotificationHandlers = <
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
>(
  notificationHandlers: ValidateNotificationHandlers<NotificationHandlers>,
) => notificationHandlers;

/**
 * LSP client context.
 */
export interface ClientContext {
  /**
   * The LSP server, represented as a child process.
   */
  readonly server: NodeServer;

  /**
   * A logger that logs a message to the console.
   */
  readonly logger: Logger;

  /**
   * Send a JSON-RPC message to the LSP server.
   */
  readonly send: (data: Message) => void;
}

/**
 * Protocol request handlers.
 */
export type ProtocolRequestHandlers = ReturnType<typeof _prepareProtocolRequestHandlers>;

/**
 * Prepare protocol request handlers.
 * @returns
 */
const _prepareProtocolRequestHandlers = () =>
  validateRequestHandlers({
    "client/registerCapability": {
      type: "mutation",
      /**
       * The `client/registerCapability` request is sent from the server to the client to register
       * for a new capability on the client side. Not all clients need to support dynamic capability
       * registration. A client opts in via the `dynamicRegistration` property on the specific
       * client capabilities. A client can even provide dynamic registration for capability A but
       * not for capability B (see `TextDocumentClientCapabilities` as an example).
       *
       * Server must not register the same capability both statically through the initialize result
       * and dynamically for the same document selector. If a server wants to support both static
       * and dynamic registration it needs to check the client capability in the initialize request
       * and only register the capability statically if the client doesn’t support dynamic
       * registration for that capability.
       */
      handler: (params: RegistrationParams, success, error, context) => {
        // To be implemented by an actual implementation
        success(null);
      },
    },

    "client/unregisterCapability": {
      type: "mutation",
      /**
       * The `client/unregisterCapability` request is sent from the server to the client to unregister
       * a previously registered capability.
       */
      handler: (params: UnregistrationParams, success, error, context) => {
        // To be implemented by an actual implementation
        success(null);
      },
    },
  });

/**
 * Protocol notification handlers.
 */
export type ProtocolNotificationHandlers = ReturnType<typeof _prepareProtocolNotificationHandlers>;

/**
 * Prepare protocol notification handlers.
 * @returns
 */
const _prepareProtocolNotificationHandlers = () =>
  validateNotificationHandlers({
    /**
     * Invoked when received a `$/cancelRequest` notification from the server.
     */
    "$/cancelRequest": (params: CancelParams, context) => {
      // TODO: Implement it
    },

    /**
     * Invoked when received a `$/progress` notification from the server.
     */
    "$/progress": (params: ProgressParams, context) => {
      // To be implemented by an actual implementation
    },

    /**
     * A notification to log the trace of the server’s execution. The amount and content of these
     * notifications depends on the current `trace` configuration. If `trace` is `"off"`, the server
     * should not send any `$/logTrace` notification. If trace is `"messages"`, the server should
     * not add the `"verbose"` field in the LogTraceParams.
     *
     * `$/logTrace` should only be used for systematic trace reporting. For single debugging
     * messages, the server should send `window/logMessage` notifications.
     */
    "$/logTrace": (params: LogTraceParams, context) => {
      // To be implemented by an actual implementation
    },

    /**
     * The log message notification is sent from the server to the client to ask the client to log a
     * particular message.
     */
    "window/logMessage": ({ message, type }: LogMessageParams, { logger, suppressLogging }) => {
      suppressLogging();
      switch (type) {
        case MessageType.Error:
          logger.error(message);
          break;
        case MessageType.Warning:
          logger.warn(message);
          break;
        case MessageType.Info:
        case MessageType.Log:
        case MessageType.Debug:
          logger.debug(message);
          break;
        default:
          assertNever(type);
      }
    },
  });

/**
 * Client options.
 */
export interface ClientOptions<
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
> {
  /**
   * Logging level. `false` to disable logging; `"error"` to log errors only; `"debug"` to log
   * everything. Defaults to `"error"`.
   */
  readonly logging?: "debug" | "error" | false;

  /**
   * Name of the server. Used for logging. Defaults to `""`, i.e. no name.
   */
  readonly serverName?: string;

  /**
   * Request handlers by method name. Invoked when a request is received from the server. Defaults
   * to `{}`.
   */
  readonly requestHandlers?: RefineRequestHandlers<RequestHandlers, ProtocolRequestHandlers>;

  /**
   * Notification handlers by method name. Invoked when a notification is received from the server.
   * Defaults to `{}`.
   */
  readonly notificationHandlers?: RefineNotificationHandlers<
    NotificationHandlers,
    ProtocolNotificationHandlers
  >;
}

export type ClientEventHandler<EventName extends keyof ClientEventMap> = (
  ...args: ClientEventMap[EventName] extends void ? [] : [ev: ClientEventMap[EventName]]
) => void | Promise<void>;

export interface ClientEventMap {
  initialized: void;
}

export type ValidateClientOptions<Options extends ClientOptions<any, any>> = {
  [P in keyof Options]: P extends "requestHandlers" ?
    Options[P] extends ReadonlyRecord<string, RequestHandler> ?
      ValidateRequestHandlers<Options[P]>
    : never
  : P extends "notificationHandlers" ?
    Options[P] extends ReadonlyRecord<string, NotificationHandler> ?
      ValidateNotificationHandlers<Options[P]>
    : never
  : Options[P];
};

/**
 * Create a LSP client.
 * @param server The LSP server.
 * @param options The client options.
 * @returns
 */
export const createClient = <
  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,
  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,
>(
  server: NodeServer,
  options?: ClientOptions<RequestHandlers, NotificationHandlers>,
) => {
  type RefinedRequestHandlers = RefineRequestHandlers<RequestHandlers, ProtocolRequestHandlers>;
  type RefinedNotificationHandlers = RefineNotificationHandlers<
    NotificationHandlers,
    ProtocolNotificationHandlers
  >;

  const {
    logging = "error",
    notificationHandlers = {} as RefinedNotificationHandlers,
    requestHandlers = {} as RefinedRequestHandlers,
    serverName = "",
  } = options ?? {};

  /**
   * Send a JSON-RPC Message to the LSP server.
   * @param data The message to send.
   */
  const _send = (data: Message) => {
    const dataString = JSON.stringify(data);
    const contentLength = new TextEncoder().encode(dataString).length;
    const rpcString = `Content-Length: ${contentLength}\r\n\r\n${dataString}`;
    server.send(rpcString);
  };

  const logger = createLogger({
    prefix: `%c${serverName && serverName + " "}LSP:%c `,
    styles: ["font-weight: bold", "font-weight: normal"],
    block: {
      prefix: `${serverName && serverName + " "}LSP `,
    },
  });

  const context = { server, logger, send: _send } satisfies ClientContext;

  const resolveMap = new Map<
    integer | string,
    readonly ["query" | "mutation", string, (value: LSPAny) => void]
  >();
  const rejectMap = new Map<
    integer | string,
    readonly ["query" | "mutation", string, (reason: ResponseError) => void]
  >();

  const _protocolRequestHandlers = _prepareProtocolRequestHandlers();
  const _protocolNotificationHandlers = _prepareProtocolNotificationHandlers();

  /* Merge user handlers with protocol handlers */
  for (const [method, { handler, type }] of Object.entries(requestHandlers)) {
    if (!isKeyOf(_protocolRequestHandlers, method)) continue;
    const { type: protocolType } = _protocolRequestHandlers[method];
    if (type !== protocolType) {
      Object.defineProperty(requestHandlers, method, {
        value: { type: protocolType, handler },
        enumerable: true,
      });
      if (logging)
        logger.warn(
          `Request handler type (${type}) mismatch for protocol method \`${method}\`,` +
            ` using protocol type (${protocolType}) instead`,
        );
    }
    if (logging === "debug")
      logger.debug(`Overwriting request handler for protocol method \`${method}\``);
  }
  for (const [method] of Object.entries(notificationHandlers)) {
    if (!isKeyOf(_protocolNotificationHandlers, method)) continue;
    if (logging === "debug")
      logger.debug(`Overwriting notification handler for protocol method \`${method}\``);
  }

  /**
   * Send a success response to LSP server.
   * @param type The type of the request.
   * @param isProtocol Whether the request is a protocol request.
   * @param id The ID of the request.
   * @param method The method of the request.
   * @param value The value to send.
   */
  const _success = (
    type: "query" | "mutation",
    isProtocol: boolean,
    id: (integer | string) | null,
    method: string,
    value: LSPAny,
  ) => {
    const response = {
      jsonrpc: JSONRPC_VERSION,
      id,
      result: value,
    } satisfies SuccessResponseMessage;
    _send(response);

    // Log to console
    if (logging === "debug") {
      const color = type === "query" ? "#49cc90" : "purple";
      logger.block
        .overwrite({ color })
        .debug(`>> [${id}] ${isProtocol ? "[Protocol] " : ""}Response ${method}`, value);
    }
  };
  /**
   * Send an error response to LSP server.
   * @param isProtocol Whether the request is a protocol request.
   * @param id The ID of the request.
   * @param method The method of the request.
   * @param reason The reason of the error.
   */
  const _error = (
    isProtocol: boolean,
    id: (integer | string) | null,
    method: string,
    reason: ResponseError,
  ) => {
    const response = {
      jsonrpc: JSONRPC_VERSION,
      id,
      error: reason,
    } satisfies ErrorResponseMessage;
    _send(response);

    // Log to console
    if (logging === "debug") {
      const errorCode = reason.code;
      const errorData = reason.data;
      logger.block
        .overwrite({ color: "crimson" })
        .debug(
          `>> [${id}] ${isProtocol ? "[Protocol] " : ""}Error Response ${method} ${formatErrorCode(
            errorCode,
          )}`,
          "\n" + reason.message,
          ...(errorData !== undefined ? [errorData] : []),
        );
    }
  };

  // Listen to server stdout
  server.onMessage((rawString) => {
    const payloadStrings = rawString.split(/Content-Length: \d+\r\n\r\n/).filter((s) => s);

    for (const payloadString of payloadStrings) {
      let payload: unknown;
      try {
        payload = JSON.parse(payloadString);
      } catch (e) {
        if (logging) logger.error(`Unable to parse payload: ${payloadString}`, e);
        return;
      }

      if (isResponseMessage(payload)) {
        if ("error" in payload) {
          const typeAndMethodAndReject =
            payload.id === null ? undefined : rejectMap.get(payload.id);
          if (payload.id !== null && !typeAndMethodAndReject) {
            if (logging) {
              const errorCode = payload.error.code;
              const errorData = payload.error.data;
              logger.error(
                `Unable to find reject function for id ${payload.id}`,
                `\nError Response ${formatErrorCode(errorCode)}\n${payload.error.message}`,
                ...(errorData !== undefined ? [errorData] : []),
              );
            }
          } else {
            let method: string | null = null;
            if (payload.id !== null && typeAndMethodAndReject) {
              const [, m, reject] = typeAndMethodAndReject;
              method = m;
              reject(payload.error);
              rejectMap.delete(payload.id);
            }

            // Log to console
            if (logging) {
              const errorCode = payload.error.code;
              const errorData = payload.error.data;
              logger.block
                .overwrite({ color: "crimson" })
                .error(
                  `<< ${formatId(payload.id)}` +
                    `${formatMethod(method)} Error Response ${formatErrorCode(errorCode)}`,
                  "\n" + payload.error.message,
                  ...(errorData !== undefined ? [errorData] : []),
                );
            }
          }
        } else {
          const typeAndMethodAndResolve =
            payload.id === null ? undefined : resolveMap.get(payload.id);
          if (payload.id !== null && !typeAndMethodAndResolve) {
            if (logging)
              logger.error(`Unable to find resolve function for id ${payload.id}`, payload.result);
          } else {
            let type: ("query" | "mutation") | null = null;
            let method: string | null = null;
            if (payload.id !== null && typeAndMethodAndResolve) {
              const [t, m, resolve] = typeAndMethodAndResolve;
              type = t;
              method = m;
              resolve(payload.result);
              resolveMap.delete(payload.id);
            }

            if (logging === "debug") {
              const color =
                type === "query" ? "#49cc90"
                : type === "mutation" ? "purple"
                : "lightgray";
              logger.block
                .overwrite({ color })
                .debug(
                  `<< ${formatId(payload.id)}${formatMethod(method)} Response`,
                  payload.result,
                );
            }
          }
        }
      } else if (isRequestMessage(payload)) {
        const request = payload;

        let loggingSuppressed = false;

        if (request.method.startsWith("$/")) {
          const typeAndHandler =
            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];

          if (!typeAndHandler) {
            _error(true, request.id, request.method, {
              code: ErrorCodes.MethodNotFound,
              message: `Method not found: ${request.method}`,
            });

            if (logging)
              logger.error(
                `Request handler not found for method ${request.method} with id ${request.id}`,
              );
          } else {
            const { handler, type } = typeAndHandler;
            void handler(
              request.params as never,
              (value) => {
                _success(type, true, request.id, request.method, value);
              },
              (reason) => {
                _error(true, request.id, request.method, reason);
              },
              {
                ...context,
                suppressLogging: () => {
                  loggingSuppressed = true;
                },
              },
            );
          }
        } else {
          const typeAndHandler =
            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];

          if (typeAndHandler) {
            const { handler, type } = typeAndHandler;
            void handler(
              request.params as never,
              (value) => {
                _success(
                  type,
                  request.method in _protocolRequestHandlers,
                  request.id,
                  request.method,
                  value,
                );
              },
              (reason) => {
                _error(
                  request.method in _protocolRequestHandlers,
                  request.id,
                  request.method,
                  reason,
                );
              },
              {
                ...context,
                suppressLogging: () => {
                  loggingSuppressed = true;
                },
              },
            );
          }
        }

        if (logging === "debug" && !loggingSuppressed) {
          const typeAndHandler =
            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];
          const type = typeAndHandler.type ?? "unknown";
          const color =
            type === "query" ? "#49cc90"
            : type === "mutation" ? "purple"
            : "gray";
          logger.block.overwrite({ color }).debug(
            `<< [${request.id}] ${
              request.method in _protocolRequestHandlers ? "[Protocol] "
              : requestHandlers[payload.method] ? ""
              : "[Ignored] "
            }${request.method} Request`,
            ...(request.params !== undefined ? [request.params] : []),
          );
        }
      } else if (isNotificationMessage(payload)) {
        let loggingSuppressed = false;

        void (
          notificationHandlers[payload.method] ??
          _protocolNotificationHandlers[payload.method as never]
        )?.(payload.params as never, {
          ...context,
          suppressLogging: () => {
            loggingSuppressed = true;
          },
        });

        if (logging === "debug" && !loggingSuppressed)
          logger.block
            .overwrite({ color: "lightgray" })
            .debug(
              `<< ${notificationHandlers[payload.method] ? "" : "[Ignored] "}${
                payload.method
              } Notification`,
              ...(payload.params ? [payload.params] : []),
            );
      } else {
        if (logging) logger.error(`Invalid payload`, payload);
      }
    }
  });
  // End

  let requestId = 0;
  let _initialized = false;

  const _eventHandlers = new Map<string, ((ev?: any) => void | Promise<void>)[]>();

  const result = {
    /**
     * @readonly
     */
    logger,

    get initialized() {
      return _initialized;
    },

    requestHandlers: requestHandlers as Equals<
      RequestHandlers,
      RefineRequestHandlers<ReadonlyRecord<string, RequestHandler>, ProtocolRequestHandlers>
    > extends true ?
      Record<string, never>
    : RefinedRequestHandlers,
    notificationHandlers: notificationHandlers as Equals<
      NotificationHandlers,
      RefineNotificationHandlers<
        ReadonlyRecord<string, NotificationHandler>,
        ProtocolNotificationHandlers
      >
    > extends true ?
      Record<string, never>
    : RefinedNotificationHandlers,
    protocolRequestHandlers: _protocolRequestHandlers,
    protocolNotificationHandlers: _protocolNotificationHandlers,

    /**
     * @readonly
     */
    request: Object.assign(
      /**
       * Send a request to LSP server.
       * @param type The type of the request.
       * @param method The method of the request.
       * @param params The parameters of the request.
       * @returns
       */
      <R extends LSPAny = LSPAny>(
        type: "query" | "mutation",
        method: string,
        params?: LSPArray | LSPObject,
      ): ResponsePromise<R> => {
        const request = {
          jsonrpc: JSONRPC_VERSION,
          id: ++requestId,
          method,
          ...(params && { params }),
        } satisfies RequestMessage;
        _send(request);

        // Log to console
        if (logging === "debug") {
          const color = type === "query" ? "#49cc90" : "purple";
          logger.block
            .overwrite({ color })
            .debug(
              `>> [${requestId}] Request ${method}`,
              ...(params !== undefined ? [params] : []),
            );
        }

        const result = Object.assign(
          new Promise<R>((resolve, reject) => {
            resolveMap.set(requestId, [
              type,
              method,
              (value) => {
                result.status = "fulfilled";
                resolve(value as never);
              },
            ]);
            rejectMap.set(requestId, [
              type,
              method,
              (reason) => {
                result.status = "rejected";
                reject(toJSError(reason));
              },
            ]);
          }),
          {
            status: "pending" as "pending" | "fulfilled" | "cancelled" | "rejected",
            id: requestId,
            cancel: () => {
              if (result.status !== "pending") {
                if (logging)
                  logger.error(
                    `Unable to cancel request with id ${requestId} because it is already ${result.status}`,
                  );

                throw new Error(
                  `Unable to cancel request with id ${requestId} because it is already ${result.status}`,
                );
              }

              _notify("$/cancelRequest", { id: requestId } satisfies CancelParams);
              result.status = "cancelled";
              resolveMap.set(requestId, [type, method, () => {}]);
              rejectMap.set(requestId, [type, method, () => {}]);
            },
          } satisfies _BaseResponsePromise,
        );

        return result;
      },
      /**
       * Request methods.
       * @readonly
       */
      {
        /**
         * The initialize request is sent as the first request from the client to the server. If the
         * server receives a request or notification before the `initialize` request it should act
         * as follows:
         *
         * - For a request the response should be an error with code: -32002. The message can be
         * picked by the server.
         * - Notifications should be dropped, except for the exit notification. This will allow the
         * exit of a server without an initialize request.
         *
         * Until the server has responded to the `initialize` request with an `InitializeResult`,
         * the client must not send any additional requests or notifications to the server. In
         * addition the server is not allowed to send any requests or notifications to the client
         * until it has responded with an `InitializeResult`, with the exception that during the
         * `initialize` request the server is allowed to send the notifications
         * `window/showMessage`, `window/logMessage` and `telemetry/event` as well as the
         * `window/showMessageRequest` request to the client. In case the client sets up a progress
         * token in the initialize params (e.g. property `workDoneToken`) the server is also allowed
         * to use that token (and only that token) using the `$/progress` notification sent from the
         * server to the client.
         *
         * The `initialize` request may only be sent once.
         * @returns
         */
        initialize: (params: InitializeParams): ResponsePromise<InitializeResult> => {
          const promise = _mutate(
            "initialize",
            params as unknown as LSPObject,
          ) as unknown as ResponsePromise<InitializeResult>;
          void promise.then(() => {
            _initialized = true;
            _eventHandlers.get("initialized")?.forEach((handler) => void handler());
          });
          return promise;
        },
        /**
         * The shutdown request is sent from the client to the server. It asks the server to shut
         * down, but to not exit (otherwise the response might not be delivered correctly to the
         * client). There is a separate exit notification that asks the server to exit. Clients must
         * not send any notifications other than `exit` or requests to a server to which they have
         * sent a shutdown request. Clients should also wait with sending the exit notification
         * until they have received a response from the `shutdown` request.
         *
         * If a server receives requests after a shutdown request those requests should error with
         * `InvalidRequest`.
         * @returns
         */
        shutdown: (): ResponsePromise<null> => _mutate("shutdown"),
      } as const,
    ),
    /**
     * Send a query request to LSP server.
     * @param method The method of the request.
     * @param params The parameters of the request.
     * @returns
     */
    query: <R extends LSPAny = LSPAny>(
      method: string,
      params?: LSPArray | LSPObject,
    ): ResponsePromise<R> => _request("query", method, params),
    /**
     * Send a mutation request to LSP server.
     * @param method The method of the request.
     * @param payload The payload of the request.
     * @returns
     */
    mutate: <R extends LSPAny = LSPAny>(
      method: string,
      payload?: LSPArray | LSPObject,
    ): ResponsePromise<R> => _request("mutation", method, payload),

    /**
     * Send a notification to LSP server.
     * @param method The method of the notification.
     * @param params The parameters of the notification.
     */
    notify: (method: string, params?: LSPArray | LSPObject) => {
      const notification = {
        jsonrpc: JSONRPC_VERSION,
        method,
        ...(params && { params }),
      } satisfies NotificationMessage;
      _send(notification);

      // Log to console
      if (logging === "debug")
        logger.block
          .overwrite({ color: "lightgray" })
          .debug(`>> Notification ${method}`, ...(params ? [params] : []));
    },

    /**
     * Notification methods.
     * @readonly
     */
    notification: {
      /**
       * The initialized notification is sent from the client to the server after the client
       * received the result of the `initialize` request but before the client is sending any
       * other request or notification to the server. The server can use the `initialized`
       * notification for example to dynamically register capabilities.
       *
       * The `initialized` notification may only be sent once.
       */
      initialized: () => {
        _notify("initialized", {});
      },
      /**
       * A notification that should be used by the client to modify the trace setting of the server.
       */
      setTrace: (params: SetTraceParams) => {
        _notify("$/setTrace", params as unknown as LSPObject);
      },
      /**
       * A notification to ask the server to exit its process. The server should exit with `success`
       * code 0 if the shutdown request has been received before; otherwise with `error` code 1.
       */
      exit: () => {
        _notify("exit");
      },

      /**
       * Text document notifications.
       */
      textDocument: {
        /**
         * The document open notification is sent from the client to the server to signal newly opened
         * text documents. The document’s content is now managed by the client and the server must not
         * try to read the document’s content using the document’s Uri. Open in this sense means it is
         * managed by the client. It doesn’t necessarily mean that its content is presented in an
         * editor. An open notification must not be sent more than once without a corresponding close
         * notification send before. This means open and close notification must be balanced and the
         * max open count for a particular textDocument is one. Note that a server’s ability to
         * fulfill requests is independent of whether a text document is open or closed.
         *
         * The `DidOpenTextDocumentParams` contain the language id the document is associated with. If
         * the language id of a document changes, the client needs to send a `textDocument/didClose`
         * to the server followed by a `textDocument/didOpen` with the new language id if the server
         * handles the new language id as well.
         */
        didOpen: (params: DidOpenTextDocumentParams) => {
          _notify("textDocument/didOpen", params as unknown as LSPObject);
        },
        /**
         * The document change notification is sent from the client to the server to signal changes to
         * a text document. Before a client can change a text document it must claim ownership of its
         * content using the `textDocument/didOpen` notification. In 2.0 the shape of the params has
         * changed to include proper version numbers.
         */
        didChange: (params: DidChangeTextDocumentParams) => {
          _notify("textDocument/didChange", params as unknown as LSPObject);
        },
        /**
         * The document close notification is sent from the client to the server when the document
         * got closed in the client. The document’s master now exists where the document’s Uri
         * points to (e.g. if the document’s Uri is a file Uri the master now exists on disk). As
         * with the open notification the close notification is about managing the document’s
         * content. Receiving a close notification doesn’t mean that the document was open in an
         * editor before. A close notification requires a previous open notification to be sent.
         * Note that a server’s ability to fulfill requests is independent of whether a text
         * document is open or closed.
         */
        didClose: (params: DidCloseTextDocumentParams) => {
          _notify("textDocument/didClose", params as unknown as LSPObject);
        },
      } as const,

      /**
       * Workspace notifications.
       * @readonly
       */
      workspace: {
        /**
         * The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the
         * server to inform the server about workspace folder configuration changes. A server can
         * register for this notification by using either the *server capability*
         * `workspace.workspaceFolders.changeNotifications` or by using the dynamic capability
         * registration mechanism. To dynamically register for the
         * `workspace/didChangeWorkspaceFolders` send a `client/registerCapability` request from
         * the server to the client. The registration parameter must have a `registrations` item of
         * the following form, where id is a unique `id` used to unregister the capability (the
         * example uses a UUID).
         */
        didChangeWorkspaceFolders: (params: DidChangeWorkspaceFoldersParams) => {
          _notify("workspace/didChangeWorkspaceFolders", params as unknown as LSPObject);
        },
      } as const,
    } as const,

    _eventHandlers,

    /**
     * Add event handler.
     * @readonly
     */
    on: ((event, handler): void => {
      const handlers = _eventHandlers.get(event) ?? [];
      handlers.push(handler as never);
      _eventHandlers.set(event, handlers);
    }) as <E extends keyof ClientEventMap>(event: E, handler: ClientEventHandler<E>) => void,
    /**
     * Remove event handler.
     * @readonly
     */
    off: ((event, handler): void => {
      const handlers = _eventHandlers.get(event) ?? [];
      const index = handlers.indexOf(handler as never);
      if (index !== -1) handlers.splice(index, 1);
      _eventHandlers.set(event, handlers);
    }) as <E extends keyof ClientEventMap>(event: E, handler: ClientEventHandler<E>) => void,
  };

  const _request = result.request;
  // @ts-expect-error - Planned to use in the future
  const _query = result.query;
  const _mutate = result.mutate;
  const _notify = result.notify;

  return result;
};


================================================
FILE: src/client/index.ts
================================================
export * from "./client";


================================================
FILE: src/completion.ts
================================================
import * as path from "@modules/path";

import type { Completion, CompletionResult, CopilotClient } from "./client";
import type { ResponsePromise } from "./client/general-client";
import { logger } from "./logging";
import type { Position } from "./types/lsp";
import { Observable } from "./utils/observable";

/**
 * Options for {@link CompletionTaskManager}.
 */
export interface CompletionTaskManagerOptions {
  workspaceFolder: string;
  activeFilePathname: string;
}

/**
 * A manager for GitHub Copilot completion tasks that makes sure exactly one completion task is
 * active at a time.
 */
export default class CompletionTaskManager {
  public workspaceFolder: string;
  public activeFilePathname: string;

  private _state: "idle" | "requesting" | "pending" = "idle";

  private activeRequest:
    | (ResponsePromise<CompletionResult> & { cleanup?: Observable<"accepted" | "rejected"> })
    | null = null;

  constructor(
    private copilot: CopilotClient,
    options: CompletionTaskManagerOptions,
  ) {
    this.workspaceFolder = options.workspaceFolder;
    this.activeFilePathname = options.activeFilePathname;
  }

  get state(): "idle" | "requesting" | "pending" {
    return this._state;
  }

  rejectCurrentIfExist(): void {
    if (this.activeRequest) {
      if (this.activeRequest.status === "pending") this.activeRequest.cancel();
      this.activeRequest.cleanup?.next("rejected");
      this.activeRequest = null;
    }
    this._state = "idle";
    if (this.copilot.status === "InProgress") this.copilot.status = "Normal";
  }

  start(
    position: Position,
    {
      onCompletion,
    }: {
      /**
       * Callback invoked when a task completion is received.
       *
       * This can optionally return an {@linkcode Observable} representing a cleanup action.
       * The observable will be:
       * - Subscribed to initially for internal cleanup.
       * - Triggered later by the class itself when a new task starts, or by external triggers
       *   (e.g., the user manually invoking `.next()` for cleanup).
       */
      onCompletion?: (completion: Completion) => Observable<"accepted" | "rejected"> | void;
    },
  ): void {
    if (this.activeRequest) {
      if (this.activeRequest.status === "pending") this.activeRequest.cancel();
      this.activeRequest.cleanup?.next("rejected");
    }

    this._state = "requesting";

    const request = this.copilot.request.getCompletions({
      position,
      languageId: "markdown",
      path: this.activeFilePathname,
      relativePath:
        this.workspaceFolder ?
          path.relative(this.workspaceFolder, this.activeFilePathname)
        : this.activeFilePathname,
    });
    this.activeRequest = request;

    request
      .then(({ cancellationReason, completions }): void => {
        if (this.activeRequest !== request) {
          // The request has been cancelled or a new task has started since this task was started,
          // so we should ignore this task's completion
          return;
        }

        if (cancellationReason || completions.length === 0) {
          if (this.copilot.status === "InProgress") this.copilot.status = "Normal";
          this._state = "idle";
          return;
        }

        this._state = "pending";

        const completion = completions[0]!;

        const cleanup = onCompletion?.(completion) ?? new Observable<"accepted" | "rejected">();
        cleanup.subscribeOnce((acceptedOrRejected) => {
          if (acceptedOrRejected === "accepted") {
            this.copilot.notification.notifyAccepted({ uuid: completion.uuid });
            logger.debug("Accepted completion");
          } else {
            this.copilot.notification.notifyRejected({ uuids: [completion.uuid] });
            logger.debug("Rejected completion", completion.uuid);
          }
          this._state = "idle";
          if (this.activeRequest === request) this.activeRequest = null;
        });
        this.activeRequest.cleanup = cleanup;
      })
      .catch(() => {
        if (this.activeRequest !== request) {
          // The request has been cancelled or a new task has started since this task was started,
          // so we should ignore this task's completion
          return;
        }

        this._state = "idle";
        this.activeRequest = null;
      });
  }
}


================================================
FILE: src/components/ChatPanel.scss
================================================
#copilot-chat-container {
  overflow: hidden;
}

.chat-panel-resize-handle {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1.5px;
  cursor: col-resize;
  background-color: transparent;
  transition: background-color 0.2s;
  z-index: 10;

  &:hover,
  &.resizing {
    background-color: var(--primary-color, rgba(64, 120, 192, 30%));
  }
}

// Empty state welcome screen
.empty-state-welcome {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 30px;
  text-align: center;
  height: 100%;
  margin: 0 auto;

  .welcome-icon {
    margin-bottom: 10px;
  }

  .welcome-title {
    font-size: 1.5em;
    margin-top: 0;
    margin-bottom: 12px;
    font-weight: 500;
    color: var(--text-color);
  }

  .welcome-subtitle {
    color: var(--text-color-lighter, rgba(0, 0, 0, 60%));
    font-size: 0.8em;
    line-height: 1.5;
  }
}

// Main panel container
.copilot-chat-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: var(--bg-color);
  color: var(--text-color);
  border-left: 1px solid var(--border-color);
}

// Header section
.chat-panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border-color);

  h3 {
    margin: 0;
    font-size: 16px;
  }

  .chat-panel-close-btn {
    background: none;
    border: none;
    font-size: 24px !important;
    cursor: pointer;
    color: var(--text-color);
    opacity: 0.6;

    &:hover {
      opacity: 1;
    }
  }
}

// Input area container
.chat-panel-input-container {
  border-top: 1px solid var(--border-color);
  padding: 12px;
}

// Textarea wrapper with integrated controls
.chat-input-wrapper {
  position: relative;
  display: flex;
  flex-direction: column;

  textarea {
    width: 100%;
    padding: 10px;
    padding-bottom: 40px; // Space for buttons
    border: 1px solid var(--border-color, #ccc);
    border-radius: 6px;
    background-color: var(--bg-color);
    color: var(--text-color);
    resize: none;
    font-size: 14px;
    line-height: 1.5;
    // stylelint-disable-next-line declaration-property-value-keyword-no-deprecated
    word-break: break-word;
    white-space: pre-wrap;
    overflow-y: hidden;

    &:focus {
      outline: none; // Remove default outline
      border-color: var(--primary-color, #4078c0); // Change border color when focused
      box-shadow: 0 0 0 1px var(--primary-color, #4078c0); // Subtle glow
    }
  }
}

// Controls container positioned at bottom of textarea
.chat-input-controls {
  position: absolute;
  bottom: 8px;
  left: 6px;
  right: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 24px;
}

// Dropdown styling
.chat-panel-dropdown {
  position: relative;

  .chat-panel-dropdown-toggle {
    background: none;
    border: none;
    padding: 2px 6px;
    display: flex;
    align-items: center;
    cursor: pointer;
    color: var(--text-color);
    opacity: 0.8;
    font-size: 12px !important;
    font-weight: normal !important;

    &:hover {
      opacity: 1;
      background-color: rgba(0, 0, 0, 5%);
      border-radius: 3px;
    }

    .dropdown-chevron {
      margin-left: 4px;
      height: 5px;
      width: 8px;
      opacity: 0.7;
    }
  }

  .chat-panel-dropdown-menu {
    position: absolute;
    padding: 5px;
    bottom: 34px;
    left: 0;
    min-width: 120px;
    background-color: var(--bg-color);
    border: 1px solid var(--border-color);
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 15%);
    z-index: 1000;
    display: flex;
    flex-direction: column;

    .chat-panel-dropdown-item {
      text-align: left;
      background: none;
      border: none;
      cursor: pointer;
      color: var(--text-color);
      font-size: 12px !important;
      font-weight: normal !important;

      &:hover {
        background-color: var(--item-hover-bg-color, rgba(0, 0, 0, 5%));
      }
    }
  }
}

.chat-title-dropdown {
  position: relative;
  flex: 1;

  .chat-title-dropdown-toggle {
    background: none;
    border: none;
    padding: 4px 8px;
    display: flex;
    align-items: center;
    color: var(--text-color);
    cursor: pointer;
    width: auto;
    text-align: left;
    border-radius: 4px;

    &:hover {
      background-color: rgba(0, 0, 0, 5%);
    }

    h3 {
      margin: 0;
      flex: 1;
      font-size: 13px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      cursor: pointer;
    }

    .dropdown-chevron {
      margin-left: 8px;
      opacity: 0.7;
    }

    &:hover .dropdown-chevron {
      opacity: 1;
    }
  }

  .session-menu {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    max-height: 300px;
    overflow-y: auto;
    background-color: var(--bg-color);
    border: 1px solid var(--border-color);
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
    z-index: 1000;
    margin-top: 4px;
    padding: 6px;

    /* stylelint-disable-next-line no-descending-specificity */
    .chat-panel-dropdown-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 4px 8px;
      cursor: pointer;
      width: 100%;
      text-align: left;
      background: none;
      border: none;
      font-size: 12px !important;
      font-weight: normal !important;
      color: var(--text-color);
      min-height: 26px;
      border-radius: 3px;

      &:hover {
        background-color: rgba(0, 0, 0, 5%);
      }

      .edit-btn,
      .delete-btn {
        opacity: 0.4;
        width: 16px;
        height: 16px;
        padding: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        color: var(--text-color);
        cursor: pointer;
        margin-left: 8px;

        &:hover {
          color: #e53935;
        }
      }

      &:hover .edit-btn,
      &:hover .delete-btn {
        opacity: 0.7;
      }
    }

    .new-session {
      display: flex;
      align-items: center;
      justify-content: unset;
      font-weight: 500;
      border-bottom: 1px solid var(--border-color);
      padding-bottom: 6px;
      margin-bottom: 4px;

      svg {
        width: 12px;
        height: 12px;
        margin-right: 8px;
      }
    }

    .active {
      background-color: rgba(0, 0, 0, 5%);
      font-weight: 500;
    }
  }
}

// Send button
.chat-panel-send-btn {
  padding: 2px 10px;
  background-color: var(--primary-color, #4078c0);
  color: white;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px !important;

  &:disabled {
    background-color: #ccc;
    cursor: default;
    opacity: 0.6;
  }

  &:hover:not(:disabled) {
    opacity: 0.9;
  }

  &.sending {
    background-color: #e74c3c;

    &:hover:not(:disabled) {
      background-color: #c0392b;
    }
  }
}

.chat-panel-messages {
  flex: 1;
  padding: 16px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.chat-message-row {
  display: flex;
  flex-direction: column;
  max-width: 100%;
  animation: fade-in 0.3s ease;

  .message-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;

    .message-icon {
      width: 26px;
      height: 26px;
      border-radius: 50%;
      border: 1px solid rgba(0, 0, 0, 5%);
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 8px;

      svg {
        width: 16px;
        height: 16px;
      }

      &.user-icon {
        svg {
          transform: scale(1.1);
        }
      }
    }

    .user-icon {
      color: var(--text-color);
    }

    .copilot-icon {
      background-color: rgba(0, 0, 0, 3%);
      padding: 4px;
    }

    .message-author {
      font-size: 13px;
      font-weight: bold;
      color: var(--text-color);
    }
  }

  .message-content {
    font-family: var(--font-family);
    font-size: 13px;
    line-height: 1.5;
    overflow-wrap: break-word;
    margin: 0;
    border-radius: 6px;
    background: transparent;

    // Remove default pre tag styles
    border: none;
    overflow: visible;
  }

  pre.message-content {
    white-space: pre-wrap;
  }
}

.markdown-content {
  /* Reset header font sizes */
  h1 {
    font-size: 1.36em;
    margin-top: 0.7em;
    margin-bottom: 0.5em;
  }

  h2 {
    font-size: 1.25em;
    margin-top: 0.6em;
    margin-bottom: 0.4em;
  }

  /* stylelint-disable-next-line no-descending-specificity */
  h3 {
    font-size: 1.17em;
    margin-top: 0.5em;
    margin-bottom: 0.4em;
  }

  h4 {
    font-size: 1em;
    margin-top: 0.4em;
    margin-bottom: 0.3em;
  }

  h5 {
    font-size: 0.83em;
    margin-top: 0.4em;
    margin-bottom: 0.2em;
  }

  h6 {
    font-size: 0.67em;
    margin-top: 0.4em;
    margin-bottom: 0.2em;
  }

  h1,
  h2,
  /* stylelint-disable-next-line no-descending-specificity */
  h3,
  h4,
  h5,
  h6 {
    font-weight: 500;
  }

  /* Reset font size for code */
  code {
    font-size: 0.9em;
  }

  /* Prettify code block */
  pre {
    background-color: rgba(0, 0, 0, 3%);
    border: 1px solid rgba(0, 0, 0, 8%);
    border-radius: 5px;
    margin: 12px 0;
    overflow-x: auto;
    position: relative;

    .copy-code-button {
      position: absolute;
      top: 6px;
      right: 6px;
      width: 28px;
      height: 28px;
      padding: 6px;
      background-color: rgba(0, 0, 0, 10%);
      border: none;
      border-radius: 4px;
      cursor: pointer;
      opacity: 0;
      transition:
        opacity 0.2s ease,
        background-color 0.2s ease;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 2;

      &:hover {
        opacity: 0.9 !important;
        background-color: rgba(0, 0, 0, 15%);
      }

      &:active {
        background-color: rgba(0, 0, 0, 20%);
      }
    }

    &:hover .copy-code-button {
      opacity: 0.7;
    }

    code {
      background-color: transparent;
      padding: 0;
      border: none;
      font-family: inherit;
      color: var(--text-color-code, var(--text-color));
    }
  }

  /* Reduce margins for paragraphs with a pre code block after */
  p:has(+ pre code) {
    margin-bottom: 0;
  }

  /* Remove margins for first and last paragraphs */
  p {
    &:first-child {
      margin-top: 0;
    }

    &:last-child {
      margin-bottom: 0;
    }
  }
}

.typing-indicator {
  display: flex;
  align-items: center;
  height: 24px;

  span {
    width: 6px;
    height: 6px;
    margin: 0 2px;
    background-color: var(--text-color);
    border-radius: 50%;
    opacity: 0.5;
    animation: typing 1s infinite ease-in-out;

    &:nth-child(1) {
      animation-delay: 0s;
    }

    &:nth-child(2) {
      animation-delay: 0.2s;
    }

    &:nth-child(3) {
      animation-delay: 0.4s;
    }
  }
}

@keyframes typing {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.5;
  }

  30% {
    transform: translateY(-3px);
    opacity: 1;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}


================================================
FILE: src/components/ChatPanel.tsx
================================================
import type { Signal } from "@preact/signals";
import { useSignal, useSignalEffect } from "@preact/signals";
import { getLuminance } from "color2k";
import hljs from "highlight.js";
import { marked } from "marked";
import { markedHighlight } from "marked-highlight";
import { render } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";

import type { ChatModel } from "@/client/chat";
import {
  COPILOT_ACADEMIC_INSTRUCTIONS,
  COPILOT_CATGIRL_INSTRUCTIONS,
  COPILOT_CREATIVE_INSTRUCTIONS,
  COPILOT_MARKDOWN_INSTRUCTIONS,
  ChatSession,
  listCopilotChatModels,
} from "@/client/chat";
import { t } from "@/i18n";

import CopilotIcon from "./CopilotIcon";

import "./ChatPanel.scss";

interface ChatPanelProps {
  onClose: () => void;
}

type PromptType = "Normal" | "Academic" | "Creative" | "CatGirl";

const ChatPanel: FC<ChatPanelProps> = ({ onClose }) => {
  const input = useSignal("");
  const promptType = useSignal<PromptType>("Normal");
  const isThinking = useSignal(false);
  const isSending = useSignal(false);
  const messages = useSignal<Message[]>([]);
  const sessionJustSwitchedFlag = useSignal(false);

  const modelId = useSignal("gpt-4o");
  const models = useSignal<ChatModel[]>([]);

  const currentSessionId = useSignal("");
  const sessions = useSignal<ChatSession[]>([]);

  const getPrompt = useCallback(() => {
    if (promptType.value === "Normal") return COPILOT_MARKDOWN_INSTRUCTIONS;
    if (promptType.value === "Academic") return COPILOT_ACADEMIC_INSTRUCTIONS;
    if (promptType.value === "Creative") return COPILOT_CREATIVE_INSTRUCTIONS;
    return COPILOT_CATGIRL_INSTRUCTIONS.replace(
      "{{CATGIRL_NAME}}",
      t.test("chat.prompt-style.cat-girl-name") ? t("chat.prompt-style.cat-girl-name") : "Vanilla",
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const getPromptType = useCallback((prompt: string) => {
    if (prompt === COPILOT_MARKDOWN_INSTRUCTIONS) return "Normal";
    if (prompt === COPILOT_ACADEMIC_INSTRUCTIONS) return "Academic";
    if (prompt === COPILOT_CREATIVE_INSTRUCTIONS) return "Creative";
    if (
      prompt ===
      COPILOT_CATGIRL_INSTRUCTIONS.replace(
        "{{CATGIRL_NAME}}",
        t.test("chat.prompt-style.cat-girl-name") ?
          t("chat.prompt-style.cat-girl-name")
        : "Vanilla",
      )
    )
      return "CatGirl";
    return "Normal";
  }, []);

  // Watch modelId changes and update session meta
  useEffect(() => {
    const currentSession = sessions.value.find((session) => session.id === currentSessionId.value);
    if (!currentSession) return;

    if (currentSession.modelId === modelId.value) return;

    currentSession.modelId = modelId.value;
    void ChatSession.save(currentSession.id);
    sessions.value = ChatSession.getAll();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [modelId.value, currentSessionId.value]);

  // Watch prompt type changes and update the system prompt
  useEffect(() => {
    const currentSession = sessions.value.find((session) => session.id === currentSessionId.value);
    if (!currentSession) return;

    if (currentSession.messages[0]!.content === getPrompt()) return;

    currentSession.messages[0]!.content = getPrompt();
    void ChatSession.save(currentSession.id);
    sessions.value = ChatSession.getAll();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [promptType.value, currentSessionId.value, getPrompt]);

  // Load models and sessions on mount
  useEffect(() => {
    // Load models
    void listCopilotChatModels().then((modelList) => {
      models.value = modelList;
    });

    // Load sessions
    const loadSessions = async () => {
      await ChatSession.loadAll();

      const allSessions = ChatSession.getAll();
      sessions.value = allSessions;

      // Select the first session if available, otherwise create a new one
      if (allSessions.length > 0) {
        currentSessionId.value = allSessions[0]!.id;

        modelId.value = allSessions[0]!.modelId;
        promptType.value = getPromptType(allSessions[0]!.messages[0]!.content);

        messages.value = allSessions[0]!.messages
          .filter((msg) => msg.role !== "system")
          .map((msg) => ({
            role: msg.role as "user" | "assistant",
            content: msg.content,
          }));
        sessionJustSwitchedFlag.value = !sessionJustSwitchedFlag.value;
      } else {
        createNewSession();
      }
    };

    void loadSessions();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const createNewSession = useCallback(() => {
    const session = ChatSession.create(modelId.value, getPrompt());
    currentSessionId.value = session.id;

    sessions.value = ChatSession.getAll();

    messages.value = [];
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const switchSession = useCallback((id: string) => {
    const session = ChatSession.get(id);
    if (!session) return;

    currentSessionId.value = session.id;

    modelId.value = session.modelId;
    promptType.value = getPromptType(session.messages[0]!.content);

    messages.value = session.messages
      .filter((msg) => msg.role !== "system")
      .map((msg) => ({
        role: msg.role as "user" | "assistant",
        content: msg.content,
      }));
    sessionJustSwitchedFlag.value = !sessionJustSwitchedFlag.value;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleEditChatTitle = useCallback((id: string, title: string) => {
    const session = ChatSession.get(id);
    if (!session) return;

    session.title = title;
    void ChatSession.save(session.id);

    sessions.value = ChatSession.getAll();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleDeleteSession = useCallback(
    (id: string) => {
      void ChatSession.delete(id);

      sessions.value = ChatSession.getAll();

      // If current session is deleted, switch to the first available session
      if (id === currentSessionId.value) {
        if (sessions.value.length > 0) {
          switchSession(sessions.value[0]!.id);
        } else {
          createNewSession();
        }
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [createNewSession, switchSession],
  );

  // Send message handler
  const handleSendMessage = () => {
    const userMessage = input.value.trim();
    if (!userMessage || isSending.value) return;

    // Add user message
    messages.value = [
      ...messages.value,
      {
        role: "user",
        content: input.value.trim(),
      },
    ];

    // Clear input
    input.value = "";

    // Show typing indicator
    isThinking.value = true;
    isSending.value = true;

    abortControllerRef.current = new AbortController();
    sessions.value
      .find((session) => session.id === currentSessionId.value)
      ?.send(
        userMessage,
        (content) => {
          if (isThinking.value) {
            isThinking.value = false;
            messages.value = [
              ...messages.value,
              {
                role: "assistant",
                content: "",
              },
            ];
          }
          messages.value = [
            ...messages.value.slice(0, messages.value.length - 1),
            {
              role: "assistant",
              content: messages.value[messages.value.length - 1]!.content + content,
            },
          ];
        },
        {
          model: models.value.find((model) => model.id === modelId.value),
          signal: abortControllerRef.current.signal,
        },
      )
      .then((fullContent) => {
        messages.value = [
          ...messages.value.slice(0, messages.value.length - 1),
          {
            role: "assistant",
            content: fullContent,
          },
        ];
        isSending.value = false;
        void ChatSession.save(currentSessionId.value);
      })
      .catch((error) => {
        console.error("Error sending message:", error);
        isSending.value = false;
      });
  };

  const abortControllerRef = useRef<AbortController | null>(null);
  const handleStopSending = useCallback(() => {
    abortControllerRef.current?.abort();
  }, []);

  return (
    <div id="copilot-chat-panel" className="copilot-chat-panel">
      <ChatHeader
        onClose={onClose}
        currentSessionId={currentSessionId}
        sessions={sessions}
        onSwitchSession={switchSession}
        onNewSession={createNewSession}
        onEditChatTitle={handleEditChatTitle}
        onDeleteSession={handleDeleteSession}
      />
      <MessageList
        promptType={promptType.value}
        messages={messages.value}
        isThinking={isThinking.value}
        sessionJustSwitchedFlag={sessionJustSwitchedFlag.value}
      />
      <InputArea
        input={input}
        promptType={promptType}
        modelId={modelId}
        models={models.value}
        onSend={handleSendMessage}
        onStop={handleStopSending}
        isSending={isSending.value}
      />
    </div>
  );
};

/**********
 * Header *
 **********/
interface ChatHeaderProps {
  onClose: () => void;
  currentSessionId: Signal<string>;
  sessions: Signal<ChatSession[]>;
  onSwitchSession: (id: string) => void;
  onNewSession: () => void;
  onEditChatTitle?: (id: string, title: string) => void;
  onDeleteSession?: (id: string) => void;
}

const ChatHeader: FC<ChatHeaderProps> = ({
  currentSessionId,
  onClose,
  onDeleteSession,
  onEditChatTitle,
  onNewSession,
  onSwitchSession,
  sessions,
}) => {
  const isDropdownOpen = useSignal(false);

  // Handle click outside to close dropdown
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      if (
        isDropdownOpen.value &&
        !target.closest(".chat-title-dropdown-toggle") &&
        !target.closest(".session-menu")
      ) {
        isDropdownOpen.value = false;
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleEditChatTitle = (id: string, title: string) => {
    let newTitle = title;
    Files.editor?.EditHelper.showDialog({
      title: t("chat.dialog.edit-chat-title.title"),
      html: t("chat.dialog.edit-chat-title.html").replace("{{SESSION_TITLE}}", title),
      buttons: [t("button.ok"), t("button.cancel")],
      callback: (result) => {
        if (result === 0 && onEditChatTitle) onEditChatTitle(id, newTitle);
      },
    });
    $("#new-chat-title").on("input", (e) => {
      newTitle = (e.target as HTMLInputElement).value;
    });
  };

  const handleDeleteSession = (id: string, title: string) => {
    // Show confirmation dialog
    Files.editor?.EditHelper.showDialog({
      title: t("chat.dialog.delete-session.title"),
      html: t("chat.dialog.delete-session.html").replace("{{SESSION_TITLE}}", title),
      buttons: [t("button.delete"), t("button.cancel")],
      callback: (result) => {
        if (result === 0 && onDeleteSession) {
          onDeleteSession(id);
          // Close the dropdown if the current session is deleted
          if (id === currentSessionId.value) isDropdownOpen.value = false;
        }
      },
    });
  };

  return (
    <div className="chat-panel-header">
      <div className="chat-title-dropdown">
        <button
          className="chat-title-dropdown-toggle"
          onClick={() => (isDropdownOpen.value = !isDropdownOpen.value)}>
          <h3>
            {sessions.value.find((session) => session.id === currentSessionId.value)?.title ||
              t("chat.button.new-session")}
          </h3>
          <svg
            className="dropdown-chevron"
            width="8"
            height="5"
            viewBox="0 0 8 5"
            fill="none"
            xmlns="http://www.w3.org/2000/svg">
            <path d="M4 5L0 0.5L8 0.5L4 5Z" fill="currentColor" />
          </svg>
        </button>

        {isDropdownOpen.value && (
          <div className="chat-panel-dropdown-menu session-menu">
            <button
              className="chat-panel-dropdown-item new-session"
              onClick={() => {
                onNewSession();
                isDropdownOpen.value = false;
              }}>
              <svg viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                <path d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2z" />
              </svg>
              {t("chat.button.new-session")}
            </button>

            {sessions.value.map((session) => (
              <button
                key={session.id}
                className={`chat-panel-dropdown-item ${session.id === currentSessionId.value ? "active" : ""}`}
                onClick={() => {
                  onSwitchSession(session.id);
                  isDropdownOpen.value = false;
                }}>
                <span className="session-title">
                  {session.title || t("chat.button.new-session")}
                </span>
                <div style={{ display: "flex", alignItems: "center" }}>
                  <button
                    className="edit-btn"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleEditChatTitle(session.id, session.title);
                    }}
                    ty-hint={t("chat.button.edit-chat-title")}>
                    <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
                      <path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z" />
                    </svg>
                  </button>
                  <button
                    className="delete-btn"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleDeleteSession(session.id, session.title);
                    }}
                    ty-hint={t("chat.button.delete-session")}>
                    <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
                      <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
                      <path
                        fillRule="evenodd"
                        d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"
                      />
                    </svg>
                  </button>
                </div>
              </button>
            ))}
          </div>
        )}
      </div>

      <button className="chat-panel-close-btn" onClick={onClose}>
        ×
      </button>
    </div>
  );
};

/************
 * Messages *
 ************/
interface Message {
  role: "assistant" | "user";
  content: string;
}

interface MessageContentProps {
  content: string;
}

const md = marked
  .use({
    // Avoid identifying `~` as strikethrough
    // https://github.com/markedjs/marked/issues/1561#issuecomment-846571425
    tokenizer: {
      del: (src) => {
        if (/^~~+(?=\S)([\s\S]*?\S)~~+/.exec(src)) return false;
      },
    },
  })
  .use(
    markedHighlight({
      emptyLangClass: "hljs",
      langPrefix: "hljs language-",
      highlight(code, lang) {
        const language = hljs.getLanguage(lang) ? lang : "plaintext";
        return hljs.highlight(code, { language }).value;
      },
    }),
  );

const MessageContent: FC<MessageContentProps> = ({ content }) => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    containerRef.current.innerHTML = md.parse(content) as string;

    // Handle links
    const links = containerRef.current.querySelectorAll("a");
    links.forEach((link) => {
      if (link.getAttribute("href")?.startsWith("http")) {
        link.setAttribute("target", "_blank");
        link.setAttribute("rel", "noopener noreferrer");
      }
    });

    // Add copy button to code blocks
    const codeBlocks = containerRef.current.querySelectorAll("pre");
    codeBlocks.forEach((pre) => {
      const copyButton = document.createElement("button");
      copyButton.className = "copy-code-button";
      copyButton.innerHTML = `
        <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
          <path d="M4 4v9h9V4H4zm8 8H5V5h7v7z"/>
          <path d="M3 3v9h1V3h8V2H3v1z"/>
        </svg>
      `;

      copyButton.addEventListener("click", () => {
        const code = pre.textContent || "";
        void navigator.clipboard.writeText(code.trim()).then(() => {
          copyButton.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
              <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/>
            </svg>
          `;
          setTimeout(() => {
            copyButton.innerHTML = `
              <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
                <path d="M4 4v9h9V4H4zm8 8H5V5h7v7z"/>
                <path d="M3 3v9h1V3h8V2H3v1z"/>
              </svg>
            `;
          }, 800);
        });
      });

      pre.appendChild(copyButton);
    });
  }, [content]);

  return <div ref={containerRef} className="message-content markdown-content" />;
};

const EmptyStateWelcome: FC = () => {
  return (
    <div className="empty-state-welcome">
      <div className="welcome-icon">
        <CopilotIcon
          status="Normal"
          textColor="var(--text-color)"
          style={{ width: "48px", height: "48px" }}
        />
      </div>
      <h2 className="welcome-title">{t("chat.welcome.title")}</h2>
      <p className="welcome-subtitle">{t("chat.welcome.subtitle")}</p>
    </div>
  );
};

interface MessageListProps {
  promptType: PromptType;
  messages: Message[];
  isThinking: boolean;
  sessionJustSwitchedFlag: boolean;
}

const MessageList: FC<MessageListProps> = ({
  isThinking,
  messages,
  promptType,
  sessionJustSwitchedFlag,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = useCallback(() => {
    if (containerRef.current) containerRef.current.scrollTop = containerRef.current.scrollHeight;
  }, []);

  useEffect(scrollToBottom, [sessionJustSwitchedFlag, scrollToBottom]);

  // Watch for new messages and scroll to bottom if the role is "user"
  useEffect(() => {
    if (messages.length > 0 && messages[messages.length - 1]!.role === "user") scrollToBottom();
  }, [messages, scrollToBottom]);

  return (
    <div className="chat-panel-messages" ref={containerRef}>
      {messages.length === 0 && !isThinking ?
        <EmptyStateWelcome />
      : messages.map((message, index) => (
          <div key={index} className={`chat-message-row ${message.role}`}>
            {message.role === "user" ?
              <>
                <div className="message-header">
                  <div className="message-icon user-icon">
                    <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                      <rect x="2" y="6" width="8" height="8" rx="2" fill="#7c80ff" />
                      <circle cx="11" cy="5" r="3" fill="#ffb173" />
                      <rect x="6" y="2" width="4" height="4" rx="1" fill="#67d8ae" />
                    </svg>
                  </div>
                  <div className="message-author">{t("chat.you")}</div>
                </div>
                <pre className="message-content">{message.content}</pre>
              </>
            : <>
                <div className="message-header">
                  <div className="message-icon copilot-icon">
                    {promptType === "CatGirl" ?
                      <span
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          fontSize: "13px",
                        }}>
                        🐱
                      </span>
                    : <CopilotIcon
                        status="Normal"
                        textColor="var(--text-color)"
                        style={{ width: "90%", height: "90%" }}
                      />
                    }
                  </div>
                  <div className="message-author">
                    {promptType === "CatGirl" ?
                      t("chat.prompt-style.cat-girl-name")
                    : "GitHub Copilot"}
                  </div>
                </div>
                <MessageContent content={message.content} />
              </>
            }
          </div>
        ))
      }

      {isThinking && (
        <div className="chat-message-row assistant">
          <div className="message-header">
            <div className="message-icon copilot-icon">
              {promptType === "CatGirl" ?
                <span
                  style={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    fontSize: "13px",
                  }}>
                  🐱
                </span>
              : <CopilotIcon status="InProgress" textColor="var(--text-color)" />}
            </div>
            <div className="message-author">
              {promptType === "CatGirl" ? t("chat.prompt-style.cat-girl-name") : "GitHub Copilot"}
            </div>
          </div>
          <div className="message-content typing-indicator">
            <span></span>
            <span></span>
            <span></span>
          </div>
        </div>
      )}
    </div>
  );
};

/**************
 * Input Area *
 **************/
interface DropdownProps {
  label: string;
  tooltip?: string;
  isOpen: Signal<boolean>;
  options: { value: string; label: string }[];
  onSelect: (value: string) => void;
  closeOtherDropdown: () => void;
}

const Dropdown: FC<DropdownProps> = ({
  closeOtherDropdown,
  isOpen,
  label,
  onSelect,
  options,
  tooltip,
}) => {
  return (
    <div className="chat-panel-dropdown">
      <button
        className="chat-panel-dropdown-toggle"
        type="button"
        ty-hint={tooltip}
        onClick={() => {
          closeOtherDropdown();
          isOpen.value = !isOpen.value;
        }}>
        {label}
        <svg
          className="dropdown-chevron"
          width="8"
          height="5"
          viewBox="0 0 8 5"
          fill="none"
          xmlns="http://www.w3.org/2000/svg">
          <path d="M4 5L0 0.5L8 0.5L4 5Z" fill="currentColor" />
        </svg>
      </button>

      {isOpen.value && (
        <div className="chat-panel-dropdown-menu">
          {options.map((option) => (
            <button
              key={option.value}
              type="button"
              className="chat-panel-dropdown-item"
              onClick={() => {
                onSelect(option.value);
                isOpen.value = false;
              }}>
              {option.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

interface InputAreaProps {
  input: Signal<string>;
  promptType: Signal<PromptType>;
  modelId: Signal<string>;
  models: ChatModel[];
  onSend: () => void;
  isSending: boolean;
  onStop?: () => void;
}

const InputArea: FC<InputAreaProps> = ({
  input,
  isSending,
  modelId,
  models,
  onSend,
  onStop,
  promptType,
}) => {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const measureRef = useRef<HTMLTextAreaElement>(null);
  const rows = useSignal(1);
  const isPromptDropdownOpen = useSignal(false);
  const isModelDropdownOpen = useSignal(false);

  // Handle Enter key
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      if (e.ctrlKey) {
        input.value += "\n";
        return;
      }
      onSend();
    }
  };

  // Auto-resize textarea
  const updateRows = useCallback(
    (text: string) => {
      if (!measureRef.current) return;

      // Replace each empty line with a space so that empty lines are measured properly.
      measureRef.current.textContent =
        text ?
          text
            .split("\n")
            .map((line) => (line === "" ? " " : line))
            .join("\n")
        : " ";

      const totalHeight = measureRef.current.scrollHeight;
      const lineHeight = parseInt(window.getComputedStyle(measureRef.current).lineHeight);

      // Clear the content to prevent vertical scrollbar from appearing
      measureRef.current.textContent = "";

      const actualRows = Math.ceil(totalHeight / lineHeight);
      rows.value = Math.max(1, Math.min(6, actualRows)); // Cap at 6 rows
    },
    [rows],
  );

  useSignalEffect(() => {
    updateRows(input.value);
  });

  // Handle click outside for dropdowns
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      if (
        !target.closest(".chat-panel-dropdown-toggle") &&
        !target.closest(".chat-panel-dropdown-menu") &&
        (isPromptDropdownOpen.value || isModelDropdownOpen.value)
      ) {
        isPromptDropdownOpen.value = false;
        isModelDropdownOpen.value = false;
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [isModelDropdownOpen, isPromptDropdownOpen]);

  return (
    <div className="chat-panel-input-container">
      <div className="chat-input-wrapper">
        <textarea
          ref={measureRef}
          style={{
            visibility: "hidden",
            paddingTop: "0",
            paddingBottom: "0",
            border: "none",
            position: "absolute",
            top: "-9999px",
          }}
        />

        <textarea
          ref={textareaRef}
          placeholder={t("chat.input-placeholder")}
          value={input.value}
          rows={rows.value}
          onChange={(e) => (input.value = (e.target as HTMLTextAreaElement).value)}
          onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}
          disabled={isSending}
        />

        {/* Controls in the bottom toolbar */}
        <div className="chat-input-controls">
          {/* Prompt type dropdown */}
          <Dropdown
            label={t(`chat.prompt-style.${promptType.value}`)}
            isOpen={isPromptDropdownOpen}
            tooltip={t("chat.prompt-style.tooltip")}
            closeOtherDropdown={() => (isModelDropdownOpen.value = false)}
            options={[
              { value: "Normal", label: t("chat.prompt-style.Normal") },
              { value: "Academic", label: t("chat.prompt-style.Academic") },
              { value: "Creative", label: t("chat.prompt-style.Creative") },
              { value: "CatGirl", label: t("chat.prompt-style.CatGirl") },
            ]}
            onSelect={(value) => {
              promptType.value = value as never;
            }}
          />

          {/* Model type dropdown */}
          <Dropdown
            label={models.find((model) => model.id === modelId.value)?.name || ""}
            isOpen={isModelDropdownOpen}
            tooltip={t("chat.pick-model.tooltip")}
            closeOtherDropdown={() => (isPromptDropdownOpen.value = false)}
            options={models.map((model) => ({ value: model.id, label: model.name }))}
            onSelect={(value) => (modelId.value = value)}
          />

          {/* Send button */}
          <button
            className={"chat-panel-send-btn" + (isSending ? " sending" : "")}
            disabled={onStop ? !isSending && !input.value.trim() : !input.value.trim() || isSending}
            onClick={() => {
              if (isSending) {
                onStop?.();
                return;
              }
              onSend();
            }}>
            {isSending ? t("chat.button.stop") : t("chat.button.send")}
          </button>
        </div>
      </div>
    </div>
  );
};

// Watch theme change and switch highlight.js theme
setInterval(() => {
  const isDark = getLuminance(window.getComputedStyle(document.body).backgroundColor) < 0.5;
  (window as any).setHighlightjsTheme(isDark ? "dark" : "light");
}, 1000);

export let detachChatPanel: (() => void) | null = null;

export function attachChatPanel(): void {
  // Check if panel already exists (only one instance allowed)
  if (document.querySelector("#copilot-chat-container")) return;

  // Get reference to content element
  const contentDiv = document.querySelector("content");
  if (!contentDiv?.parentNode) return; // Can’t attach if content doesn’t exist

  // Create container and position it after content
  const container = document.createElement
Download .txt
gitextract_4vih3sn_/

├── .editorconfig
├── .githooks/
│   └── commit-msg
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── README.zh-CN.md
├── bin/
│   ├── install_linux.sh
│   ├── install_macos.sh
│   ├── install_windows.ps1
│   ├── uninstall_linux.sh
│   ├── uninstall_macos.sh
│   └── uninstall_windows.ps1
├── commitlint.config.js
├── eslint.config.js
├── install.ps1
├── install.sh
├── package.json
├── pre-commit.ts
├── prettier.config.cjs
├── rollup.config.ts
├── src/
│   ├── client/
│   │   ├── chat.ts
│   │   ├── client.ts
│   │   ├── general-client.ts
│   │   └── index.ts
│   ├── completion.ts
│   ├── components/
│   │   ├── ChatPanel.scss
│   │   ├── ChatPanel.tsx
│   │   ├── CopilotIcon.tsx
│   │   ├── DropdownWithInput.scss
│   │   ├── DropdownWithInput.tsx
│   │   ├── ModalBody.tsx
│   │   ├── ModalCloseButton.scss
│   │   ├── ModalCloseButton.tsx
│   │   ├── ModalContent.tsx
│   │   ├── ModalOverlay.scss
│   │   ├── ModalOverlay.tsx
│   │   ├── ModalTitle.tsx
│   │   ├── ModelHeader.tsx
│   │   ├── SettingsPanel.tsx
│   │   ├── Spinner.tsx
│   │   ├── SuggestionPanel.scss
│   │   ├── SuggestionPanel.tsx
│   │   ├── Switch.scss
│   │   ├── Switch.tsx
│   │   ├── icons/
│   │   │   ├── index.ts
│   │   │   ├── nodejs.tsx
│   │   │   └── settings.tsx
│   │   └── preact-env.d.ts
│   ├── constants.ts
│   ├── errors/
│   │   ├── CommandError.ts
│   │   ├── NoFreePortError.ts
│   │   ├── PlatformError.ts
│   │   └── index.ts
│   ├── footer.scss
│   ├── footer.tsx
│   ├── global.d.ts
│   ├── i18n/
│   │   ├── en.json
│   │   ├── index.ts
│   │   ├── t.spec.ts
│   │   ├── t.ts
│   │   └── zh-CN.json
│   ├── index.ts
│   ├── logging.ts
│   ├── mac-server.ts
│   ├── main.ts
│   ├── modules/
│   │   ├── fs.ts
│   │   ├── path.spec.ts
│   │   ├── path.ts
│   │   ├── url.spec.ts
│   │   └── url.ts
│   ├── patches/
│   │   ├── index.ts
│   │   ├── jquery.ts
│   │   ├── promise.spec.ts
│   │   ├── promise.ts
│   │   └── typora.ts
│   ├── reset.d.ts
│   ├── settings.ts
│   ├── styles.scss
│   ├── types/
│   │   ├── lsp.ts
│   │   └── tools.ts
│   ├── typora-utils.ts
│   └── utils/
│       ├── cli-tools.ts
│       ├── diff.ts
│       ├── dom.ts
│       ├── function.ts
│       ├── logging.ts
│       ├── lsp.ts
│       ├── node-bridge.ts
│       ├── observable.ts
│       ├── random.ts
│       ├── stream.ts
│       ├── tools.proof.ts
│       └── tools.ts
├── stylelint.config.js
├── test/
│   └── setup.ts
├── tsconfig.build.json
├── tsconfig.json
└── vitest.config.ts
Download .txt
SYMBOL INDEX (548 symbols across 38 files)

FILE: pre-commit.ts
  constant CONSTANTS_FILE_PATHNAME (line 7) | const CONSTANTS_FILE_PATHNAME = "./src/constants.ts";

FILE: rollup.config.ts
  method transform (line 31) | transform(code, id) {

FILE: src/client/chat.ts
  constant COPILOT_MARKDOWN_BASE (line 19) | const COPILOT_MARKDOWN_BASE = `
  constant CODE_BLOCK_FORMAT_INSTRUCTION (line 38) | const CODE_BLOCK_FORMAT_INSTRUCTION = `
  constant COPILOT_MARKDOWN_INSTRUCTIONS (line 52) | const COPILOT_MARKDOWN_INSTRUCTIONS = `
  constant COPILOT_ACADEMIC_INSTRUCTIONS (line 77) | const COPILOT_ACADEMIC_INSTRUCTIONS = `
  constant COPILOT_CREATIVE_INSTRUCTIONS (line 107) | const COPILOT_CREATIVE_INSTRUCTIONS = `
  constant COPILOT_CATGIRL_INSTRUCTIONS (line 137) | const COPILOT_CATGIRL_INSTRUCTIONS = `
  type ChatModel (line 177) | interface ChatModel {
  type ChatOptions (line 185) | interface ChatOptions {
  type ChatRequest (line 191) | interface ChatRequest {
  type ChatResponse (line 207) | interface ChatResponse {
  type ChatStreamResponse (line 237) | interface ChatStreamResponse {
  type ChatResult (line 251) | interface ChatResult {
  function getConfigPath (line 264) | async function getConfigPath(): Promise<string | null> {
  function getGitHubToken (line 286) | async function getGitHubToken(): Promise<string> {
  function prepareHeaders (line 327) | async function prepareHeaders(): Promise<Record<string, string>> {
  function prepareRequest (line 353) | function prepareRequest(messages: ChatRequest["messages"], options: Chat...
  function processResponse (line 378) | function processResponse(data: ChatResponse): Partial<ChatResult> {
  function listCopilotChatModels (line 412) | async function listCopilotChatModels(): Promise<ChatModel[]> {
  function chat (line 485) | async function chat(
  type ChatMessage (line 554) | interface ChatMessage {
  class ChatSession (line 563) | class ChatSession {
    method constructor (line 580) | constructor(modelId: string, systemPrompt = COPILOT_MARKDOWN_INSTRUCTI...
    method create (line 602) | public static create(modelId: string, systemPrompt = COPILOT_MARKDOWN_...
    method getAll (line 610) | public static getAll(): ChatSession[] {
    method get (line 619) | public static get(id: string): ChatSession | undefined {
    method delete (line 628) | public static async delete(id: string): Promise<boolean> {
    method send (line 654) | public async send(
    method save (line 711) | public static async save(id: string): Promise<void> {
    method loadAll (line 734) | public static async loadAll(): Promise<void> {
    method addMessage (line 763) | private addMessage(role: "system" | "user" | "assistant", content: str...
    method extractTitleFromDocument (line 777) | private static extractTitleFromDocument(document: string): string {
    method countWords (line 797) | private static countWords(text: string): number {
    method extractDocumentStructure (line 804) | private static extractDocumentStructure(document: string): string {

FILE: src/client/client.ts
  type CopilotAccountStatus (line 26) | type CopilotAccountStatus = "MaybeOk" | "NotAuthorized" | "NotSignedIn" ...
  type CopilotStatus (line 31) | type CopilotStatus = "InProgress" | "Warning" | "Normal";
  type CompletionOptions (line 36) | interface CompletionOptions {
  type Completion (line 91) | interface Completion {
  type CompletionResult (line 126) | interface CompletionResult {
  type CopilotChangeStatusEvent (line 141) | interface CopilotChangeStatusEvent {
  type CopilotClientEventHandler (line 153) | type CopilotClientEventHandler<EventName extends keyof CopilotClientEven...
  type CopilotClientEventMap (line 158) | interface CopilotClientEventMap extends ClientEventMap {
  type CopilotClientOptions (line 162) | type CopilotClientOptions<
  method initialized (line 262) | get initialized() {
  method status (line 275) | get status() {
  method status (line 278) | set status(value) {
  type CopilotClient (line 429) | type CopilotClient<

FILE: src/client/general-client.ts
  type ResponsePromise (line 40) | interface ResponsePromise<T> extends Promise<T>, _BaseResponsePromise {}
  type _BaseResponsePromise (line 41) | interface _BaseResponsePromise {
  type HandlerContext (line 60) | interface HandlerContext extends ClientContext {
  type RequestHandler (line 80) | type RequestHandler = {
  type NotificationHandler (line 101) | type NotificationHandler = (params: never, context: HandlerContext) => v...
  type RefineRequestHandlers (line 106) | type RefineRequestHandlers<
  type ValidateRequestHandlers (line 117) | type ValidateRequestHandlers<
  type RefineNotificationHandlers (line 152) | type RefineNotificationHandlers<
  type ValidateNotificationHandlers (line 163) | type ValidateNotificationHandlers<
  type ClientContext (line 196) | interface ClientContext {
  type ProtocolRequestHandlers (line 216) | type ProtocolRequestHandlers = ReturnType<typeof _prepareProtocolRequest...
  type ProtocolNotificationHandlers (line 261) | type ProtocolNotificationHandlers = ReturnType<typeof _prepareProtocolNo...
  type ClientOptions (line 323) | interface ClientOptions<
  type ClientEventHandler (line 354) | type ClientEventHandler<EventName extends keyof ClientEventMap> = (
  type ClientEventMap (line 358) | interface ClientEventMap {
  type ValidateClientOptions (line 362) | type ValidateClientOptions<Options extends ClientOptions<any, any>> = {
  type RefinedRequestHandlers (line 387) | type RefinedRequestHandlers = RefineRequestHandlers<RequestHandlers, Pro...
  type RefinedNotificationHandlers (line 388) | type RefinedNotificationHandlers = RefineNotificationHandlers<
  method initialized (line 733) | get initialized() {

FILE: src/completion.ts
  type CompletionTaskManagerOptions (line 12) | interface CompletionTaskManagerOptions {
  class CompletionTaskManager (line 21) | class CompletionTaskManager {
    method constructor (line 31) | constructor(
    method state (line 39) | get state(): "idle" | "requesting" | "pending" {
    method rejectCurrentIfExist (line 43) | rejectCurrentIfExist(): void {
    method start (line 53) | start(

FILE: src/components/ChatPanel.tsx
  type ChatPanelProps (line 25) | interface ChatPanelProps {
  type PromptType (line 29) | type PromptType = "Normal" | "Academic" | "Creative" | "CatGirl";
  type ChatHeaderProps (line 300) | interface ChatHeaderProps {
  type Message (line 462) | interface Message {
  type MessageContentProps (line 467) | interface MessageContentProps {
  method highlight (line 485) | highlight(code, lang) {
  type MessageListProps (line 563) | interface MessageListProps {
  type DropdownProps (line 676) | interface DropdownProps {
  type InputAreaProps (line 735) | interface InputAreaProps {
  function attachChatPanel (line 903) | function attachChatPanel(): void {

FILE: src/components/CopilotIcon.tsx
  type CopilotIconProps (line 5) | interface CopilotIconProps {

FILE: src/components/DropdownWithInput.tsx
  type DropdownWithInputProps (line 6) | interface DropdownWithInputProps {

FILE: src/components/ModalBody.tsx
  type ModalBodyProps (line 1) | interface ModalBodyProps {

FILE: src/components/ModalCloseButton.tsx
  type ModalCloseButtonProps (line 3) | interface ModalCloseButtonProps {

FILE: src/components/ModalOverlay.tsx
  type ModalOverlayProps (line 4) | interface ModalOverlayProps {

FILE: src/components/SettingsPanel.tsx
  type SettingControl (line 33) | interface SettingControl<K extends keyof Settings> {
  type TypedSettingControl (line 37) | type TypedSettingControl<T> = SettingControl<
  type Categories (line 54) | type Categories = Record<string, { [K in keyof Settings]?: SettingContro...
  type CategoriesSignals (line 272) | type CategoriesSignals<C extends Categories> = _Id<{
  type SettingsPanelProps (line 278) | interface SettingsPanelProps {

FILE: src/components/Spinner.tsx
  type SpinnerProps (line 1) | interface SpinnerProps {

FILE: src/components/SuggestionPanel.tsx
  type SuggestionPanelProps (line 11) | interface SuggestionPanelProps {

FILE: src/components/Switch.tsx
  type SwitchProps (line 4) | interface SwitchProps {

FILE: src/components/preact-env.d.ts
  type FC (line 1) | type FC<P = NonNullable<unknown>> = import("preact").FunctionalComponent

FILE: src/constants.ts
  constant VERSION (line 9) | const VERSION = "0.3.12";
  constant PLUGIN_DIR (line 14) | const PLUGIN_DIR = path.join(TYPORA_RESOURCE_DIR, "copilot");

FILE: src/errors/CommandError.ts
  class CommandError (line 4) | class CommandError extends Error {
    method constructor (line 5) | constructor(message: string) {

FILE: src/errors/NoFreePortError.ts
  class NoFreePortError (line 4) | class NoFreePortError extends Error {
    method constructor (line 5) | constructor(message: string) {

FILE: src/errors/PlatformError.ts
  class PlatformError (line 4) | class PlatformError extends Error {
    method constructor (line 5) | constructor(message: string) {

FILE: src/footer.tsx
  type FooterPanelOptions (line 29) | interface FooterPanelOptions {
  type FooterOptions (line 221) | interface FooterOptions {

FILE: src/global.d.ts
  type FileConstructorExtensions (line 39) | interface FileConstructorExtensions {
  type Window (line 130) | interface Window {
  type CID (line 179) | type CID = string;
  type LanguageId (line 185) | type LanguageId =
  type CaretPlacement (line 356) | interface CaretPlacement {
  type EnhancedEditor (line 370) | type EnhancedEditor = Editor & EditorExtensions;
  class Editor (line 374) | class Editor {
  type EditorExtensions (line 570) | interface EditorExtensions {
  class HistoryManager (line 596) | class HistoryManager {
  type EnhancedSourceView (line 617) | type EnhancedSourceView = SourceView & SourceViewExtensions;
  class SourceView (line 621) | class SourceView {
  type SourceViewExtensions (line 658) | interface SourceViewExtensions {
  class Node (line 709) | class Node {
  class NodeMap (line 806) | class NodeMap {
  type NodeAttributes (line 830) | interface NodeAttributes {
  type FileEntity (line 890) | interface FileEntity {

FILE: src/i18n/t.ts
  type PathOf (line 13) | type PathOf<O> = keyof {
  type LocaleMap (line 19) | interface LocaleMap {
  class TranslationError (line 26) | class TranslationError extends Error {}

FILE: src/main.ts
  constant FAKE_TEMP_WORKSPACE_FOLDER (line 48) | const FAKE_TEMP_WORKSPACE_FOLDER =
  constant FAKE_TEMP_FILENAME (line 52) | const FAKE_TEMP_FILENAME = "typora-copilot-fake-markdown.md";
  type CodeMirrorHistory (line 374) | interface CodeMirrorHistory {

FILE: src/modules/fs.ts
  method lookApp (line 645) | async lookApp(this: void, app, executableName = app) {
  method lookAppFirst (line 652) | async lookAppFirst(this: void, app, executableName = app) {
  method lookApp (line 723) | async lookApp(this: void, app, executableName = app) {
  method lookAppFirst (line 727) | async lookAppFirst(this: void, app, executableName = app) {

FILE: src/modules/path.ts
  type AddSep (line 3) | type AddSep<F extends (...args: any) => unknown> = (

FILE: src/patches/jquery.ts
  method setup (line 65) | setup() {
  method teardown (line 103) | teardown() {

FILE: src/patches/promise.ts
  type PromiseConstructor (line 2) | interface PromiseConstructor {
  type PromiseConstructor (line 86) | interface PromiseConstructor {

FILE: src/settings.ts
  type Settings (line 4) | type Settings = typeof defaultSettings;
  method get (line 40) | get(_target, prop, _receiver) {
  method set (line 48) | set(_target, prop, value, _receiver) {

FILE: src/types/lsp.ts
  type integer (line 9) | type integer = number;
  type uinteger (line 14) | type uinteger = number;
  type decimal (line 21) | type decimal = number;
  type LSPAny (line 26) | type LSPAny = LSPObject | LSPArray | string | integer | uinteger | decim...
  type LSPObject (line 31) | type LSPObject = {
  type LSPArray (line 46) | type LSPArray = readonly LSPAny[];
  type JSONRPCVersion (line 54) | type JSONRPCVersion = typeof JSONRPC_VERSION;
  constant JSONRPC_VERSION (line 58) | const JSONRPC_VERSION = "2.0";
  type Message (line 64) | type Message = {
  type RequestMessage (line 77) | interface RequestMessage extends Message {
  type ResponseMessage (line 100) | type ResponseMessage = SuccessResponseMessage | ErrorResponseMessage;
  type SuccessResponseMessage (line 105) | interface SuccessResponseMessage extends Message {
  type ErrorResponseMessage (line 120) | interface ErrorResponseMessage extends Message {
  type ResponseError (line 135) | type ResponseError = {
  type NotificationMessage (line 243) | interface NotificationMessage extends Message {
  type CancelParams (line 258) | type CancelParams = {
  type ProgressToken (line 268) | type ProgressToken = integer | string;
  type ProgressParams (line 273) | type ProgressParams<T extends LSPAny = LSPAny> = {
  type DocumentUri (line 324) | type DocumentUri = string;
  type URI (line 330) | type URI = string;
  type RegularExpressionsClientCapabilities (line 378) | type RegularExpressionsClientCapabilities = {
  type EOL (line 396) | type EOL = (typeof EOL)[number];
  constant EOL (line 403) | const EOL = ["\n", "\r\n", "\r"] as const;
  type Position (line 410) | type Position = {
  type PositionEncodingKind (line 431) | type PositionEncodingKind = (typeof PositionEncodingKind)[keyof typeof P...
  type Range (line 477) | type Range = {
  type LanguageIdentifier (line 492) | type LanguageIdentifier = (typeof LanguageIdentifiers)[number];
  type TextDocumentItem (line 620) | type TextDocumentItem = {
  type TextDocumentIdentifier (line 646) | type TextDocumentIdentifier = {
  type VersionedTextDocumentIdentifier (line 657) | interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier {
  type OptionalVersionedTextDocumentIdentifier (line 671) | interface OptionalVersionedTextDocumentIdentifier extends TextDocumentId...
  type TextDocumentPositionParams (line 694) | type TextDocumentPositionParams = {
  type DocumentFilter (line 722) | type DocumentFilter = {
  type DocumentSelector (line 756) | type DocumentSelector = readonly DocumentFilter[];
  type TextEdit (line 761) | type TextEdit = {
  type ChangeAnnotation (line 780) | type ChangeAnnotation = {
  type ChangeAnnotationIdentifier (line 806) | type ChangeAnnotationIdentifier = string;
  type AnnotatedTextEdit (line 813) | interface AnnotatedTextEdit extends TextEdit {
  type TextDocumentEdit (line 828) | type TextDocumentEdit = {
  type Location (line 846) | type Location = {
  type LocationLink (line 854) | type LocationLink = {
  type Diagnostic (line 888) | type Diagnostic = {
  type DiagnosticSeverity (line 949) | type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof Diagn...
  type DiagnosticTag (line 981) | type DiagnosticTag = (typeof DiagnosticTag)[keyof typeof DiagnosticTag];
  type DiagnosticRelatedInformation (line 1010) | type DiagnosticRelatedInformation = {
  type CodeDescription (line 1027) | type CodeDescription = {
  type Command (line 1041) | type Command = {
  type MarkupKind (line 1066) | type MarkupKind = (typeof MarkupKind)[keyof typeof MarkupKind];
  type MarkupContent (line 1114) | type MarkupContent = {
  type MarkdownClientCapabilities (line 1138) | type MarkdownClientCapabilities = {
  type CreateFileOptions (line 1161) | type CreateFileOptions = {
  type CreateFile (line 1176) | type CreateFile = {
  type RenameFileOptions (line 1203) | type RenameFileOptions = {
  type RenameFile (line 1218) | type RenameFile = {
  type DeleteFileOptions (line 1250) | type DeleteFileOptions = {
  type DeleteFile (line 1265) | type DeleteFile = {
  type WorkspaceEdit (line 1302) | type WorkspaceEdit = {
  type WorkspaceEditClientCapabilities (line 1349) | type WorkspaceEditClientCapabilities = {
  type ResourceOperationKind (line 1400) | type ResourceOperationKind =
  type FailureHandlingKind (line 1426) | type FailureHandlingKind = (typeof FailureHandlingKind)[keyof typeof Fai...
  type WorkDoneProgressPayload (line 1462) | type WorkDoneProgressPayload =
  type WorkDoneProgressBegin (line 1470) | type WorkDoneProgressBegin = {
  type WorkDoneProgressReport (line 1511) | type WorkDoneProgressReport = {
  type WorkDoneProgressEnd (line 1546) | type WorkDoneProgressEnd = {
  type WorkDoneProgressParams (line 1559) | type WorkDoneProgressParams = {
  type WorkDoneProgressOptions (line 1569) | type WorkDoneProgressOptions = {
  type PartialResultParams (line 1576) | type PartialResultParams = {
  type TraceValue (line 1589) | type TraceValue = "off" | "messages" | "verbose";
  type InitializeParams (line 1598) | interface InitializeParams extends WorkDoneProgressParams {
  type TextDocumentClientCapabilities (line 1682) | type TextDocumentClientCapabilities = {
  type NotebookDocumentClientCapabilities (line 1866) | type NotebookDocumentClientCapabilities = {
  type ClientCapabilities (line 1875) | type ClientCapabilities = {
  type InitializeResult (line 2127) | type InitializeResult = {
  type InitializeErrorCodes (line 2154) | type InitializeErrorCodes = (typeof InitializeErrorCodes)[keyof typeof I...
  type InitializeError (line 2173) | type InitializeError = {
  type ServerCapabilities (line 2186) | type ServerCapabilities = {
  type InitializedParams (line 2462) | interface InitializedParams {}
  type Registration (line 2467) | type Registration = {
  type RegistrationParams (line 2488) | type RegistrationParams = {
  type StaticRegistrationOptions (line 2495) | type StaticRegistrationOptions = {
  type TextDocumentRegistrationOptions (line 2506) | type TextDocumentRegistrationOptions = {
  type Unregistration (line 2517) | type Unregistration = {
  type UnregistrationParams (line 2533) | type UnregistrationParams = {
  type SetTraceParams (line 2543) | type SetTraceParams = {
  type LogTraceParams (line 2553) | type LogTraceParams = {
  type TextDocumentSyncKind (line 2571) | type TextDocumentSyncKind = (typeof TextDocumentSyncKind)[keyof typeof T...
  type TextDocumentSyncOptions (line 2599) | type TextDocumentSyncOptions = {
  type DidOpenTextDocumentParams (line 2636) | type DidOpenTextDocumentParams = {
  type TextDocumentChangeRegistrationOptions (line 2646) | interface TextDocumentChangeRegistrationOptions extends TextDocumentRegi...
  type DidChangeTextDocumentParams (line 2657) | type DidChangeTextDocumentParams = {
  type TextDocumentContentChangeEvent (line 2687) | type TextDocumentContentChangeEvent =
  type WillSaveTextDocumentParams (line 2717) | type WillSaveTextDocumentParams = {
  type TextDocumentSaveReason (line 2732) | type TextDocumentSaveReason =
  type SaveOptions (line 2758) | type SaveOptions = {
  type TextDocumentSaveRegistrationOptions (line 2768) | interface TextDocumentSaveRegistrationOptions extends TextDocumentRegist...
  type DidSaveTextDocumentParams (line 2778) | type DidSaveTextDocumentParams = {
  type DidCloseTextDocumentParams (line 2794) | type DidCloseTextDocumentParams = {
  type TextDocumentSyncClientCapabilities (line 2801) | type TextDocumentSyncClientCapabilities = {
  type NotebookDocument (line 2830) | type NotebookDocument = {
  type NotebookCell (line 2868) | type NotebookCell = {
  type NotebookCellKind (line 2897) | type NotebookCellKind = (typeof NotebookCellKind)[keyof typeof NotebookC...
  type ExecutionSummary (line 2919) | type ExecutionSummary = {
  type NotebookCellTextDocumentFilter (line 2940) | type NotebookCellTextDocumentFilter = {
  type NotebookDocumentFilter (line 2964) | type NotebookDocumentFilter =
  type NotebookDocumentSyncClientCapabilities (line 3001) | type NotebookDocumentSyncClientCapabilities = {
  type NotebookDocumentSyncOptions (line 3031) | type NotebookDocumentSyncOptions = {
  type NotebookDocumentSyncRegistrationOptions (line 3076) | interface NotebookDocumentSyncRegistrationOptions
  type DidOpenNotebookDocumentParams (line 3084) | type DidOpenNotebookDocumentParams = {
  type DidChangeNotebookDocumentParams (line 3102) | type DidChangeNotebookDocumentParams = {
  type VersionedNotebookDocumentIdentifier (line 3130) | type VersionedNotebookDocumentIdentifier = {
  type NotebookDocumentChangeEvent (line 3147) | type NotebookDocumentChangeEvent = {
  type NotebookCellArrayChange (line 3200) | type NotebookCellArrayChange = {
  type DidSaveNotebookDocumentParams (line 3222) | type DidSaveNotebookDocumentParams = {
  type DidCloseNotebookDocumentParams (line 3234) | type DidCloseNotebookDocumentParams = {
  type NotebookDocumentIdentifier (line 3252) | type NotebookDocumentIdentifier = {
  type DeclarationClientCapabilities (line 3262) | type DeclarationClientCapabilities = {
  type DeclarationOptions (line 3276) | interface DeclarationOptions extends WorkDoneProgressOptions {}
  type DeclarationRegistrationOptions (line 3278) | interface DeclarationRegistrationOptions
  type DeclarationParams (line 3284) | interface DeclarationParams
  type DefinitionClientCapabilities (line 3287) | type DefinitionClientCapabilities = {
  type DefinitionOptions (line 3301) | interface DefinitionOptions extends WorkDoneProgressOptions {}
  type DefinitionRegistrationOptions (line 3303) | interface DefinitionRegistrationOptions
  type DefinitionParams (line 3309) | interface DefinitionParams
  type TypeDefinitionClientCapabilities (line 3312) | type TypeDefinitionClientCapabilities = {
  type TypeDefinitionOptions (line 3328) | interface TypeDefinitionOptions extends WorkDoneProgressOptions {}
  type TypeDefinitionRegistrationOptions (line 3330) | interface TypeDefinitionRegistrationOptions
  type TypeDefinitionParams (line 3336) | interface TypeDefinitionParams
  type ImplementationClientCapabilities (line 3339) | type ImplementationClientCapabilities = {
  type ImplementationOptions (line 3355) | interface ImplementationOptions extends WorkDoneProgressOptions {}
  type ImplementationRegistrationOptions (line 3357) | interface ImplementationRegistrationOptions
  type ImplementationParams (line 3363) | interface ImplementationParams
  type ReferenceClientCapabilities (line 3366) | type ReferenceClientCapabilities = {
  type ReferenceOptions (line 3373) | interface ReferenceOptions extends WorkDoneProgressOptions {}
  type ReferenceRegistrationOptions (line 3375) | interface ReferenceRegistrationOptions
  type ReferenceParams (line 3381) | interface ReferenceParams
  type ReferenceContext (line 3386) | type ReferenceContext = {
  type CallHierarchyClientCapabilities (line 3393) | type CallHierarchyClientCapabilities = {
  type CallHierarchyOptions (line 3403) | interface CallHierarchyOptions extends WorkDoneProgressOptions {}
  type CallHierarchyRegistrationOptions (line 3405) | interface CallHierarchyRegistrationOptions
  type CallHierarchyPrepareParams (line 3411) | interface CallHierarchyPrepareParams
  type CallHierarchyItem (line 3414) | type CallHierarchyItem = {
  type CallHierarchyIncomingCallsParams (line 3463) | interface CallHierarchyIncomingCallsParams
  type CallHierarchyIncomingCall (line 3468) | type CallHierarchyIncomingCall = {
  type CallHierarchyOutgoingCallsParams (line 3484) | interface CallHierarchyOutgoingCallsParams
  type CallHierarchyOutgoingCall (line 3489) | type CallHierarchyOutgoingCall = {
  type TypeHierarchyClientCapabilities (line 3502) | type TypeHierarchyClientCapabilities = {
  type TypeHierarchyOptions (line 3512) | interface TypeHierarchyOptions extends WorkDoneProgressOptions {}
  type TypeHierarchyRegistrationOptions (line 3514) | interface TypeHierarchyRegistrationOptions
  type TypeHierarchyPrepareParams (line 3520) | interface TypeHierarchyPrepareParams
  type TypeHierarchyItem (line 3523) | type TypeHierarchyItem = {
  type TypeHierarchySupertypesParams (line 3574) | interface TypeHierarchySupertypesParams extends WorkDoneProgressParams, ...
  type TypeHierarchySubtypesParams (line 3581) | interface TypeHierarchySubtypesParams extends WorkDoneProgressParams, Pa...
  type DocumentHighlightClientCapabilities (line 3585) | type DocumentHighlightClientCapabilities = {
  type DocumentHighlightOptions (line 3592) | interface DocumentHighlightOptions extends WorkDoneProgressOptions {}
  type DocumentHighlightRegistrationOptions (line 3594) | interface DocumentHighlightRegistrationOptions
  type DocumentHighlightParams (line 3600) | interface DocumentHighlightParams
  type DocumentHighlight (line 3609) | type DocumentHighlight = {
  type DocumentHighlightKind (line 3624) | type DocumentHighlightKind =
  type DocumentLinkClientCapabilities (line 3647) | type DocumentLinkClientCapabilities = {
  type DocumentLinkOptions (line 3661) | interface DocumentLinkOptions extends WorkDoneProgressOptions {
  type DocumentLinkRegistrationOptions (line 3668) | interface DocumentLinkRegistrationOptions
  type DocumentLinkParams (line 3674) | interface DocumentLinkParams extends WorkDoneProgressParams, PartialResu...
  type DocumentLink (line 3685) | type DocumentLink = {
  type HoverClientCapabilities (line 3715) | type HoverClientCapabilities = {
  type HoverOptions (line 3729) | interface HoverOptions extends WorkDoneProgressOptions {}
  type HoverRegistrationOptions (line 3731) | interface HoverRegistrationOptions extends TextDocumentRegistrationOptio...
  type HoverParams (line 3736) | interface HoverParams extends TextDocumentPositionParams, WorkDoneProgre...
  type Hover (line 3741) | type Hover = {
  type MarkedString (line 3771) | type MarkedString = string | { language: string; value: string };
  type CodeLensClientCapabilities (line 3773) | type CodeLensClientCapabilities = {
  type CodeLensOptions (line 3780) | interface CodeLensOptions extends WorkDoneProgressOptions {
  type CodeLensRegistrationOptions (line 3787) | interface CodeLensRegistrationOptions
  type CodeLensParams (line 3793) | interface CodeLensParams extends WorkDoneProgressParams, PartialResultPa...
  type CodeLens (line 3808) | type CodeLens = {
  type CodeLensWorkspaceClientCapabilities (line 3827) | type CodeLensWorkspaceClientCapabilities = {
  type FoldingRangeClientCapabilities (line 3840) | type FoldingRangeClientCapabilities = {
  type FoldingRangeOptions (line 3893) | interface FoldingRangeOptions extends WorkDoneProgressOptions {}
  type FoldingRangeRegistrationOptions (line 3895) | interface FoldingRangeRegistrationOptions
  type FoldingRangeParams (line 3901) | interface FoldingRangeParams extends WorkDoneProgressParams, PartialResu...
  type FoldingRangeKind (line 3913) | type FoldingRangeKind =
  type FoldingRange (line 3942) | type FoldingRange = {
  type SelectionRangeClientCapabilities (line 3987) | type SelectionRangeClientCapabilities = {
  type SelectionRangeOptions (line 3997) | interface SelectionRangeOptions extends WorkDoneProgressOptions {}
  type SelectionRangeRegistrationOptions (line 3999) | interface SelectionRangeRegistrationOptions
  type SelectionRangeParams (line 4005) | interface SelectionRangeParams extends WorkDoneProgressParams, PartialRe...
  type SelectionRange (line 4017) | type SelectionRange = {
  type DocumentSymbolClientCapabilities (line 4030) | type DocumentSymbolClientCapabilities = {
  type DocumentSymbolOptions (line 4082) | interface DocumentSymbolOptions extends WorkDoneProgressOptions {
  type DocumentSymbolRegistrationOptions (line 4092) | interface DocumentSymbolRegistrationOptions
  type DocumentSymbolParams (line 4098) | interface DocumentSymbolParams extends WorkDoneProgressParams, PartialRe...
  type SymbolKind (line 4108) | type SymbolKind = (typeof SymbolKind)[keyof typeof SymbolKind];
  type SymbolTag (line 4145) | type SymbolTag = (typeof SymbolTag)[keyof typeof SymbolTag];
  type DocumentSymbol (line 4165) | type DocumentSymbol = {
  type SymbolInformation (line 4223) | type SymbolInformation = {
  type SemanticTokenTypes (line 4270) | enum SemanticTokenTypes {
  type SemanticTokenModifiers (line 4303) | enum SemanticTokenModifiers {
  type TokenFormat (line 4316) | type TokenFormat = (typeof TokenFormat)[keyof typeof TokenFormat];
  type SemanticTokensLegend (line 4322) | type SemanticTokensLegend = {
  type SemanticTokensClientCapabilities (line 4334) | type SemanticTokensClientCapabilities = {
  type SemanticTokensOptions (line 4425) | interface SemanticTokensOptions extends WorkDoneProgressOptions {
  type SemanticTokensRegistrationOptions (line 4450) | interface SemanticTokensRegistrationOptions
  type SemanticTokensParams (line 4456) | interface SemanticTokensParams extends WorkDoneProgressParams, PartialRe...
  type SemanticTokens (line 4463) | type SemanticTokens = {
  type SemanticTokensPartialResult (line 4478) | type SemanticTokensPartialResult = {
  type SemanticTokensDeltaParams (line 4485) | interface SemanticTokensDeltaParams extends WorkDoneProgressParams, Part...
  type SemanticTokensDelta (line 4498) | type SemanticTokensDelta = {
  type SemanticTokensEdit (line 4507) | type SemanticTokensEdit = {
  type SemanticTokensDeltaPartialResult (line 4524) | type SemanticTokensDeltaPartialResult = {
  type SemanticTokensRangeParams (line 4531) | interface SemanticTokensRangeParams extends WorkDoneProgressParams, Part...
  type SemanticTokensWorkspaceClientCapabilities (line 4543) | type SemanticTokensWorkspaceClientCapabilities = {
  type InlayHintClientCapabilities (line 4561) | type InlayHintClientCapabilities = {
  type InlayHintOptions (line 4584) | interface InlayHintOptions extends WorkDoneProgressOptions {
  type InlayHintRegistrationOptions (line 4597) | interface InlayHintRegistrationOptions
  type InlayHintParams (line 4605) | interface InlayHintParams extends WorkDoneProgressParams {
  type InlayHint (line 4622) | type InlayHint = {
  type InlayHintLabelPart (line 4695) | type InlayHintLabelPart = {
  type InlayHintKind (line 4737) | type InlayHintKind = (typeof InlayHintKind)[keyof typeof InlayHintKind];
  type InlayHintWorkspaceClientCapabilities (line 4761) | type InlayHintWorkspaceClientCapabilities = {
  type InlineValueClientCapabilities (line 4779) | type InlineValueClientCapabilities = {
  type InlineValueOptions (line 4792) | interface InlineValueOptions extends WorkDoneProgressOptions {}
  type InlineValueRegistrationOptions (line 4799) | interface InlineValueRegistrationOptions
  type InlineValueParams (line 4807) | interface InlineValueParams extends WorkDoneProgressParams {
  type InlineValueContext (line 4828) | type InlineValueContext = {
  type InlineValueText (line 4847) | type InlineValueText = {
  type InlineValueVariableLookup (line 4869) | type InlineValueVariableLookup = {
  type InlineValueEvaluatableExpression (line 4898) | type InlineValueEvaluatableExpression = {
  type InlineValue (line 4921) | type InlineValue =
  type InlineValueWorkspaceClientCapabilities (line 4931) | type InlineValueWorkspaceClientCapabilities = {
  type MonikerClientCapabilities (line 4944) | type MonikerClientCapabilities = {
  type MonikerOptions (line 4954) | interface MonikerOptions extends WorkDoneProgressOptions {}
  type MonikerRegistrationOptions (line 4956) | interface MonikerRegistrationOptions
  type MonikerParams (line 4959) | interface MonikerParams
  type UniquenessLevel (line 4965) | enum UniquenessLevel {
  type MonikerKind (line 4995) | enum MonikerKind {
  type Moniker (line 5016) | type Moniker = {
  type CompletionClientCapabilities (line 5039) | type CompletionClientCapabilities = {
  type CompletionOptions (line 5190) | interface CompletionOptions extends WorkDoneProgressOptions {
  type CompletionRegistrationOptions (line 5243) | interface CompletionRegistrationOptions
  type CompletionParams (line 5249) | interface CompletionParams
  type CompletionTriggerKind (line 5262) | type CompletionTriggerKind =
  type CompletionContext (line 5292) | type CompletionContext = {
  type CompletionList (line 5310) | type CompletionList = {
  type InsertTextFormat (line 5387) | type InsertTextFormat = (typeof InsertTextFormat)[keyof typeof InsertTex...
  type CompletionItemTag (line 5416) | type CompletionItemTag = (typeof CompletionItemTag)[keyof typeof Complet...
  type InsertReplaceEdit (line 5436) | type InsertReplaceEdit = {
  type InsertTextMode (line 5458) | type InsertTextMode = (typeof InsertTextMode)[keyof typeof InsertTextMode];
  type CompletionItemLabelDetails (line 5492) | type CompletionItemLabelDetails = {
  type CompletionItem (line 5508) | type CompletionItem = {
  type CompletionItemKind (line 5693) | type CompletionItemKind = (typeof CompletionItemKind)[keyof typeof Compl...
  type PublishDiagnosticsClientCapabilities (line 5726) | type PublishDiagnosticsClientCapabilities = {
  type PublishDiagnosticsParams (line 5773) | type PublishDiagnosticsParams = {
  type DiagnosticClientCapabilities (line 5798) | type DiagnosticClientCapabilities = {
  type DiagnosticOptions (line 5819) | interface DiagnosticOptions extends WorkDoneProgressOptions {
  type DiagnosticRegistrationOptions (line 5845) | interface DiagnosticRegistrationOptions
  type DocumentDiagnosticParams (line 5853) | interface DocumentDiagnosticParams extends WorkDoneProgressParams, Parti...
  type DocumentDiagnosticReport (line 5879) | type DocumentDiagnosticReport =
  type DocumentDiagnosticReportKind (line 5888) | type DocumentDiagnosticReportKind =
  type FullDocumentDiagnosticReport (line 5915) | type FullDocumentDiagnosticReport = {
  type UnchangedDocumentDiagnosticReport (line 5940) | type UnchangedDocumentDiagnosticReport = {
  type RelatedFullDocumentDiagnosticReport (line 5961) | interface RelatedFullDocumentDiagnosticReport extends FullDocumentDiagno...
  type RelatedUnchangedDocumentDiagnosticReport (line 5981) | interface RelatedUnchangedDocumentDiagnosticReport extends UnchangedDocu...
  type DocumentDiagnosticReportPartialResult (line 6001) | type DocumentDiagnosticReportPartialResult = {
  type DiagnosticServerCancellationData (line 6012) | type DiagnosticServerCancellationData = {
  type WorkspaceDiagnosticParams (line 6021) | interface WorkspaceDiagnosticParams extends WorkDoneProgressParams, Part...
  type PreviousResultId (line 6039) | type PreviousResultId = {
  type WorkspaceDiagnosticReport (line 6057) | type WorkspaceDiagnosticReport = {
  type WorkspaceFullDocumentDiagnosticReport (line 6066) | interface WorkspaceFullDocumentDiagnosticReport extends FullDocumentDiag...
  type WorkspaceUnchangedDocumentDiagnosticReport (line 6084) | interface WorkspaceUnchangedDocumentDiagnosticReport extends UnchangedDo...
  type WorkspaceDocumentDiagnosticReport (line 6102) | type WorkspaceDocumentDiagnosticReport =
  type WorkspaceDiagnosticReportPartialResult (line 6111) | type WorkspaceDiagnosticReportPartialResult = {
  type DiagnosticWorkspaceClientCapabilities (line 6120) | type DiagnosticWorkspaceClientCapabilities = {
  type SignatureHelpClientCapabilities (line 6133) | type SignatureHelpClientCapabilities = {
  type SignatureHelpOptions (line 6183) | interface SignatureHelpOptions extends WorkDoneProgressOptions {
  type SignatureHelpRegistrationOptions (line 6202) | interface SignatureHelpRegistrationOptions
  type SignatureHelpParams (line 6205) | interface SignatureHelpParams extends TextDocumentPositionParams, WorkDo...
  type SignatureHelpTriggerKind (line 6221) | type SignatureHelpTriggerKind =
  type SignatureHelpContext (line 6251) | type SignatureHelpContext = {
  type SignatureHelp (line 6287) | type SignatureHelp = {
  type SignatureInformation (line 6324) | type SignatureInformation = {
  type ParameterInformation (line 6356) | type ParameterInformation = {
  type CodeActionClientCapabilities (line 6378) | type CodeActionClientCapabilities = {
  type CodeActionOptions (line 6454) | interface CodeActionOptions extends WorkDoneProgressOptions {
  type CodeActionRegistrationOptions (line 6472) | interface CodeActionRegistrationOptions
  type CodeActionParams (line 6478) | interface CodeActionParams extends WorkDoneProgressParams, PartialResult...
  type CodeActionKind (line 6504) | type CodeActionKind = string;
  type CodeActionContext (line 6593) | type CodeActionContext = {
  type CodeActionTriggerKind (line 6625) | type CodeActionTriggerKind =
  type CodeAction (line 6655) | type CodeAction = {
  type DocumentColorClientCapabilities (line 6735) | type DocumentColorClientCapabilities = {
  type DocumentColorOptions (line 6742) | interface DocumentColorOptions extends WorkDoneProgressOptions {}
  type DocumentColorRegistrationOptions (line 6744) | interface DocumentColorRegistrationOptions
  type DocumentColorParams (line 6750) | interface DocumentColorParams extends WorkDoneProgressParams, PartialRes...
  type ColorInformation (line 6757) | type ColorInformation = {
  type Color (line 6772) | type Color = {
  type ColorPresentationParams (line 6797) | interface ColorPresentationParams extends WorkDoneProgressParams, Partia...
  type ColorPresentation (line 6814) | type ColorPresentation = {
  type DocumentFormattingClientCapabilities (line 6835) | type DocumentFormattingClientCapabilities = {
  type DocumentFormattingOptions (line 6842) | interface DocumentFormattingOptions extends WorkDoneProgressOptions {}
  type DocumentFormattingRegistrationOptions (line 6844) | interface DocumentFormattingRegistrationOptions
  type DocumentFormattingParams (line 6850) | interface DocumentFormattingParams extends WorkDoneProgressParams {
  type FormattingOptions (line 6865) | type FormattingOptions = {
  type DocumentRangeFormattingClientCapabilities (line 6906) | type DocumentRangeFormattingClientCapabilities = {
  type DocumentRangeFormattingOptions (line 6913) | interface DocumentRangeFormattingOptions extends WorkDoneProgressOptions {}
  type DocumentRangeFormattingRegistrationOptions (line 6915) | interface DocumentRangeFormattingRegistrationOptions
  type DocumentRangeFormattingParams (line 6921) | interface DocumentRangeFormattingParams extends WorkDoneProgressParams {
  type DocumentOnTypeFormattingClientCapabilities (line 6938) | type DocumentOnTypeFormattingClientCapabilities = {
  type DocumentOnTypeFormattingOptions (line 6945) | type DocumentOnTypeFormattingOptions = {
  type DocumentOnTypeFormattingRegistrationOptions (line 6957) | interface DocumentOnTypeFormattingRegistrationOptions
  type DocumentOnTypeFormattingParams (line 6963) | type DocumentOnTypeFormattingParams = {
  type PrepareSupportDefaultBehavior (line 6990) | type PrepareSupportDefaultBehavior =
  type RenameClientCapabilities (line 7001) | type RenameClientCapabilities = {
  type RenameOptions (line 7038) | interface RenameOptions extends WorkDoneProgressOptions {
  type RenameRegistrationOptions (line 7045) | interface RenameRegistrationOptions extends TextDocumentRegistrationOpti...
  type RenameParams (line 7050) | interface RenameParams extends TextDocumentPositionParams, WorkDoneProgr...
  type PrepareRenameParams (line 7062) | interface PrepareRenameParams extends TextDocumentPositionParams, WorkDo...
  type LinkedEditingRangeOptions (line 7064) | interface LinkedEditingRangeOptions extends WorkDoneProgressOptions {}
  type LinkedEditingRangeRegistrationOptions (line 7066) | interface LinkedEditingRangeRegistrationOptions
  type LinkedEditingRangeClientCapabilities (line 7069) | type LinkedEditingRangeClientCapabilities = {
  type LinkedEditingRangeParams (line 7082) | interface LinkedEditingRangeParams
  type LinkedEditingRanges (line 7085) | type LinkedEditingRanges = {
  type WorkspaceSymbolClientCapabilities (line 7107) | type WorkspaceSymbolClientCapabilities = {
  type WorkspaceSymbolOptions (line 7160) | interface WorkspaceSymbolOptions extends WorkDoneProgressOptions {
  type WorkspaceSymbolRegistrationOptions (line 7170) | interface WorkspaceSymbolRegistrationOptions extends WorkspaceSymbolOpti...
  type WorkspaceSymbolParams (line 7175) | interface WorkspaceSymbolParams extends WorkDoneProgressParams, PartialR...
  type WorkspaceSymbol (line 7188) | type WorkspaceSymbol = {
  type ConfigurationParams (line 7231) | type ConfigurationParams = {
  type ConfigurationItem (line 7235) | type ConfigurationItem = {
  type DidChangeConfigurationClientCapabilities (line 7247) | type DidChangeConfigurationClientCapabilities = {
  type DidChangeConfigurationParams (line 7257) | type DidChangeConfigurationParams = {
  type WorkspaceFoldersServerCapabilities (line 7264) | type WorkspaceFoldersServerCapabilities = {
  type WorkspaceFolder (line 7282) | type WorkspaceFolder = {
  type DidChangeWorkspaceFoldersParams (line 7298) | type DidChangeWorkspaceFoldersParams = {
  type WorkspaceFoldersChangeEvent (line 7308) | type WorkspaceFoldersChangeEvent = {
  type FileOperationRegistrationOptions (line 7325) | type FileOperationRegistrationOptions = {
  type FileOperationPatternKind (line 7337) | type FileOperationPatternKind =
  type FileOperationPatternOptions (line 7362) | type FileOperationPatternOptions = {
  type FileOperationPattern (line 7375) | type FileOperationPattern = {
  type FileOperationFilter (line 7412) | type FileOperationFilter = {
  type CreateFilesParams (line 7429) | type CreateFilesParams = {
  type FileCreate (line 7441) | type FileCreate = {
  type RenameFilesParams (line 7453) | type RenameFilesParams = {
  type FileRename (line 7466) | type FileRename = {
  type DeleteFilesParams (line 7483) | type DeleteFilesParams = {
  type FileDelete (line 7495) | type FileDelete = {
  type DidChangeWatchedFilesClientCapabilities (line 7502) | type DidChangeWatchedFilesClientCapabilities = {
  type DidChangeWatchedFilesRegistrationOptions (line 7522) | type DidChangeWatchedFilesRegistrationOptions = {
  type Pattern (line 7547) | type Pattern = string;
  type RelativePattern (line 7556) | type RelativePattern = {
  type GlobPattern (line 7574) | type GlobPattern = Pattern | RelativePattern;
  type FileSystemWatcher (line 7576) | type FileSystemWatcher = {
  type WatchKind (line 7593) | type WatchKind = (typeof WatchKind)[keyof typeof WatchKind];
  type DidChangeWatchedFilesParams (line 7615) | type DidChangeWatchedFilesParams = {
  type FileEvent (line 7625) | type FileEvent = {
  type FileChangeType (line 7639) | type FileChangeType = (typeof FileChangeType)[keyof typeof FileChangeType];
  type ExecuteCommandClientCapabilities (line 7659) | type ExecuteCommandClientCapabilities = {
  type ExecuteCommandOptions (line 7665) | interface ExecuteCommandOptions extends WorkDoneProgressOptions {
  type ExecuteCommandRegistrationOptions (line 7675) | interface ExecuteCommandRegistrationOptions extends ExecuteCommandOption...
  type ExecuteCommandParams (line 7680) | interface ExecuteCommandParams extends WorkDoneProgressParams {
  type ApplyWorkspaceEditParams (line 7694) | type ApplyWorkspaceEditParams = {
  type ApplyWorkspaceEditResult (line 7711) | type ApplyWorkspaceEditResult = {
  type ShowMessageParams (line 7739) | type ShowMessageParams = {
  type MessageType (line 7755) | type MessageType = (typeof MessageType)[keyof typeof MessageType];
  type ShowMessageRequestClientCapabilities (line 7790) | type ShowMessageRequestClientCapabilities = {
  type ShowMessageRequestParams (line 7807) | type ShowMessageRequestParams = {
  type MessageActionItem (line 7824) | type MessageActionItem = {
  type ShowDocumentClientCapabilities (line 7836) | type ShowDocumentClientCapabilities = {
  type ShowDocumentParams (line 7849) | type ShowDocumentParams = {
  type ShowDocumentResult (line 7884) | type ShowDocumentResult = {
  type LogMessageParams (line 7894) | type LogMessageParams = {
  type WorkDoneProgressCreateParams (line 7909) | type WorkDoneProgressCreateParams = {
  type WorkDoneProgressCancelParams (line 7919) | type WorkDoneProgressCancelParams = {

FILE: src/types/tools.ts
  type ReadonlyRecord (line 4) | type ReadonlyRecord<K extends PropertyKey, T> = { readonly [P in K]: T };
  type Equals (line 9) | type Equals<T, U> =
  type _Id (line 19) | type _Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
  type _IdDeep (line 21) | type _IdDeep<T> =
  type Merge (line 39) | type Merge<L, R> = _Id<
  type _OptionalPropertyNames (line 45) | type _OptionalPropertyNames<T> = {
  type _SpreadProperties (line 48) | type _SpreadProperties<L, R, K extends keyof L & keyof R> = {

FILE: src/typora-utils.ts
  constant TYPORA_VERSION (line 12) | const TYPORA_VERSION = window._options.appVersion;
  constant TYPORA_RESOURCE_DIR (line 17) | const TYPORA_RESOURCE_DIR: string = (() => {
  method get (line 141) | get() {
  method set (line 144) | set(value) {

FILE: src/utils/logging.ts
  type SimpleLoggerOptions (line 10) | interface SimpleLoggerOptions {
  type BlockLoggerOptions (line 119) | type BlockLoggerOptions =
  type Logger (line 176) | type Logger = ReturnType<typeof createLogger>;
  type LoggerOptions (line 181) | type LoggerOptions = Merge<SimpleLoggerOptions, { block?: BlockLoggerOpt...

FILE: src/utils/node-bridge.ts
  method start (line 34) | static async start(nodePath: string, modulePath: string): Promise<NodeSe...
  method getMock (line 39) | static getMock(): NodeServer {
  class ElectronNodeServer (line 48) | class ElectronNodeServer implements NodeServer {
    method constructor (line 51) | private constructor(nodePath: string, modulePath: string) {
    method pid (line 64) | get pid(): number {
    method send (line 68) | send(message: string): void {
    method onMessage (line 72) | onMessage(listener: (message: string) => void): void {
    method start (line 79) | static start(nodePath: string, modulePath: string): NodeServer {
  class MacOSNodeServer (line 84) | class MacOSNodeServer implements NodeServer {
    method constructor (line 88) | private constructor(
    method send (line 98) | send(message: string): void {
    method onMessage (line 102) | onMessage(listener: (message: string) => void): void {
    method start (line 106) | static async start(nodePath: string, modulePath: string): Promise<Node...
    method getPortPID (line 120) | private static getPortPID(port: number): Promise<number> {
  type NodeRuntime (line 153) | interface NodeRuntime {

FILE: src/utils/observable.ts
  class Observable (line 4) | class Observable<T> {
    method subscribe (line 7) | subscribe(observer: (value: T) => void): () => void {
    method subscribeOnce (line 14) | subscribeOnce(observer: (value: T) => void): void {
    method next (line 21) | next(value: T): void {

FILE: src/utils/random.ts
  function generateUUID (line 5) | function generateUUID(): string {

FILE: src/utils/stream.ts
  function parseSSEStream (line 7) | async function parseSSEStream<T>(
Condensed preview — 100 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (718K chars).
[
  {
    "path": ".editorconfig",
    "chars": 380,
    "preview": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines wit"
  },
  {
    "path": ".githooks/commit-msg",
    "chars": 67,
    "preview": "#!/bin/sh\nnpx --no -- commitlint --edit \"$1\"\nnpx tsx pre-commit.ts\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1408,
    "preview": "name: CI\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  typecheck:\n    run"
  },
  {
    "path": ".gitignore",
    "chars": 276,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 75,
    "preview": "{\n  \"cSpell.words\": [\"reqnode\"],\n  \"stylelint.validate\": [\"css\", \"scss\"]\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2023 Snowflyt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 13792,
    "preview": "# Typora Copilot\n\nEnglish | [简体中文](./README.zh-CN.md)\n\n![Copilot suggestion screenshot](./docs/screenshot.png)\n\n[GitHub "
  },
  {
    "path": "README.zh-CN.md",
    "chars": 9348,
    "preview": "# Typora Copilot\n\n[English](./README.md) | 简体中文\n\n![Copilot 建议截图](./docs/screenshot.zh-CN.png)\n\n[Typora](https://typora.i"
  },
  {
    "path": "bin/install_linux.sh",
    "chars": 4315,
    "preview": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n "
  },
  {
    "path": "bin/install_macos.sh",
    "chars": 4548,
    "preview": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n "
  },
  {
    "path": "bin/install_windows.ps1",
    "chars": 4675,
    "preview": "# Allow custom path (-Path or -p)\nparam (\n    [Parameter(Mandatory = $false)]\n    [Alias('p')]\n    [string] $Path = ''\n)"
  },
  {
    "path": "bin/uninstall_linux.sh",
    "chars": 3451,
    "preview": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n "
  },
  {
    "path": "bin/uninstall_macos.sh",
    "chars": 3674,
    "preview": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n "
  },
  {
    "path": "bin/uninstall_windows.ps1",
    "chars": 4326,
    "preview": "# Allow custom path (-Path or -p) and silence warning (-Silent or -s)\nparam (\n    [Parameter(Mandatory = $false)]\n    [A"
  },
  {
    "path": "commitlint.config.js",
    "chars": 3839,
    "preview": "// @ts-check\n\n/**\n * @typedef {object} Parsed\n * @property {?string} emoji The emoji at the beginning of the commit mess"
  },
  {
    "path": "eslint.config.js",
    "chars": 6294,
    "preview": "// @ts-check\n\nimport eslint from \"@eslint/js\";\nimport { defineConfig } from \"eslint/config\";\nimport { importX } from \"es"
  },
  {
    "path": "install.ps1",
    "chars": 998,
    "preview": "#Requires -RunAsAdministrator\n$latestRelease = Invoke-RestMethod -Uri \"https://api.github.com/repos/Snowflyt/typora-copi"
  },
  {
    "path": "install.sh",
    "chars": 1390,
    "preview": "#!/usr/bin/env bash\n\nif [ \"$(id -u)\" -ne 0 ]; then\n  echo \"Please run as root\"\n  exit 1\nfi\n\nlatest_release=$(curl -s htt"
  },
  {
    "path": "package.json",
    "chars": 6814,
    "preview": "{\n  \"name\": \"typora-copilot\",\n  \"version\": \"0.3.12\",\n  \"private\": true,\n  \"description\": \"GitHub Copilot plugin for Typo"
  },
  {
    "path": "pre-commit.ts",
    "chars": 621,
    "preview": "import fs from \"node:fs\";\n\nimport { replaceInFileSync } from \"replace-in-file\";\n\nimport packageJSON from \"./package.json"
  },
  {
    "path": "prettier.config.cjs",
    "chars": 349,
    "preview": "// @ts-check\n\n/** @satisfies {import(\"prettier\").Config} */\nconst config = {\n  arrowParens: \"always\",\n  bracketSameLine:"
  },
  {
    "path": "rollup.config.ts",
    "chars": 2899,
    "preview": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport commonjs from \"@rollup/plugin-commonjs\";\nimport json fro"
  },
  {
    "path": "src/client/chat.ts",
    "chars": 27746,
    "preview": "/**\n * This module provides functions to interact with GitHub Copilot Chat API.\n *\n * Implementation inspired by Copilot"
  },
  {
    "path": "src/client/client.ts",
    "chars": 10811,
    "preview": "import { pathToFileURL } from \"@modules/url\";\n\nimport type {\n  LSPArray,\n  LSPObject,\n  LanguageIdentifier,\n  Position,\n"
  },
  {
    "path": "src/client/general-client.ts",
    "chars": 38022,
    "preview": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { ErrorCodes, JSONRPC_VERSION, MessageType } fro"
  },
  {
    "path": "src/client/index.ts",
    "chars": 26,
    "preview": "export * from \"./client\";\n"
  },
  {
    "path": "src/completion.ts",
    "chars": 4325,
    "preview": "import * as path from \"@modules/path\";\n\nimport type { Completion, CompletionResult, CopilotClient } from \"./client\";\nimp"
  },
  {
    "path": "src/components/ChatPanel.scss",
    "chars": 11194,
    "preview": "#copilot-chat-container {\n  overflow: hidden;\n}\n\n.chat-panel-resize-handle {\n  position: absolute;\n  left: 0;\n  top: 0;\n"
  },
  {
    "path": "src/components/ChatPanel.tsx",
    "chars": 32864,
    "preview": "import type { Signal } from \"@preact/signals\";\nimport { useSignal, useSignalEffect } from \"@preact/signals\";\nimport { ge"
  },
  {
    "path": "src/components/CopilotIcon.tsx",
    "chars": 6505,
    "preview": "import type { CopilotStatus } from \"@/client\";\n\nimport Spinner from \"./Spinner\";\n\nexport interface CopilotIconProps {\n  "
  },
  {
    "path": "src/components/DropdownWithInput.scss",
    "chars": 1941,
    "preview": ".dropdown-with-input > input[type=\"text\"] {\n  padding: 0.75rem !important;\n  box-sizing: border-box !important;\n  border"
  },
  {
    "path": "src/components/DropdownWithInput.tsx",
    "chars": 3650,
    "preview": "import { darken, getLuminance, lighten } from \"color2k\";\nimport { useRef, useState } from \"preact/hooks\";\n\nimport \"./Dro"
  },
  {
    "path": "src/components/ModalBody.tsx",
    "chars": 397,
    "preview": "export interface ModalBodyProps {\n  className?: string;\n  style?: preact.CSSProperties;\n}\n\nconst ModalBody: FC<ModalBody"
  },
  {
    "path": "src/components/ModalCloseButton.scss",
    "chars": 139,
    "preview": ".modal-close-button {\n  font-size: 1.2rem !important;\n  opacity: 0.5 !important;\n}\n\n.modal-close-button:hover {\n  opacit"
  },
  {
    "path": "src/components/ModalCloseButton.tsx",
    "chars": 338,
    "preview": "import \"./ModalCloseButton.scss\";\n\nexport interface ModalCloseButtonProps {\n  onClick?: () => void;\n}\n\nconst ModalCloseB"
  },
  {
    "path": "src/components/ModalContent.tsx",
    "chars": 485,
    "preview": "const ModalContent: FC = ({ children }) => {\n  return (\n    <div\n      style={{\n        backgroundColor: window.getCompu"
  },
  {
    "path": "src/components/ModalOverlay.scss",
    "chars": 209,
    "preview": ".modal-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 99999;\n  background: "
  },
  {
    "path": "src/components/ModalOverlay.tsx",
    "chars": 373,
    "preview": "import { createPortal } from \"preact/compat\";\nimport \"./ModalOverlay.scss\";\n\nexport interface ModalOverlayProps {\n  onCl"
  },
  {
    "path": "src/components/ModalTitle.tsx",
    "chars": 159,
    "preview": "const ModalTitle: FC = ({ children }) => {\n  return <span style={{ fontWeight: \"bold\", fontSize: \"1.2rem\" }}>{children}<"
  },
  {
    "path": "src/components/ModelHeader.tsx",
    "chars": 360,
    "preview": "const ModalHeader: FC = ({ children }) => {\n  return (\n    <>\n      <div\n        style={{\n          display: \"flex\",\n   "
  },
  {
    "path": "src/components/SettingsPanel.tsx",
    "chars": 18159,
    "preview": "/* eslint-disable react-hooks/rules-of-hooks */\nimport type { Signal } from \"@preact/signals\";\nimport { useSignal } from"
  },
  {
    "path": "src/components/Spinner.tsx",
    "chars": 988,
    "preview": "export interface SpinnerProps {\n  color?: string;\n}\n\n/**\n * An svg spinner.\n * @returns\n */\nconst Spinner: FC<SpinnerPro"
  },
  {
    "path": "src/components/SuggestionPanel.scss",
    "chars": 263,
    "preview": ".suggestion-panel {\n  position: absolute;\n  z-index: 9999;\n  pointer-events: none;\n  white-space: pre-wrap;\n  border: 1p"
  },
  {
    "path": "src/components/SuggestionPanel.tsx",
    "chars": 5678,
    "preview": "import { render } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\n\nimport { t } from \"@/i18n\";\nimport {"
  },
  {
    "path": "src/components/Switch.scss",
    "chars": 355,
    "preview": ".switch {\n  width: 36px;\n  height: 20px;\n  border-radius: 20px;\n  position: relative;\n  cursor: pointer;\n  transition: b"
  },
  {
    "path": "src/components/Switch.tsx",
    "chars": 1094,
    "preview": "import { getLuminance } from \"color2k\";\nimport \"./Switch.scss\";\n\nexport interface SwitchProps {\n  value: boolean;\n  onCh"
  },
  {
    "path": "src/components/icons/index.ts",
    "chars": 54,
    "preview": "export * from \"./nodejs\";\nexport * from \"./settings\";\n"
  },
  {
    "path": "src/components/icons/nodejs.tsx",
    "chars": 1818,
    "preview": "export const NodejsIcon: FC<{ size?: number }> = ({ size = 24 }) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org"
  },
  {
    "path": "src/components/icons/settings.tsx",
    "chars": 1155,
    "preview": "export const SettingsIcon: FC<{ size?: number }> = ({ size = 24 }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/200"
  },
  {
    "path": "src/components/preact-env.d.ts",
    "chars": 77,
    "preview": "type FC<P = NonNullable<unknown>> = import(\"preact\").FunctionalComponent<P>;\n"
  },
  {
    "path": "src/constants.ts",
    "chars": 350,
    "preview": "import * as path from \"@modules/path\";\n\nimport { TYPORA_RESOURCE_DIR } from \"./typora-utils\";\nimport { setGlobalVar } fr"
  },
  {
    "path": "src/errors/CommandError.ts",
    "chars": 189,
    "preview": "/**\n * Error thrown when a command execution fails.\n */\nexport class CommandError extends Error {\n  constructor(message:"
  },
  {
    "path": "src/errors/NoFreePortError.ts",
    "chars": 191,
    "preview": "/**\n * Error thrown when no free port is found.\n */\nexport class NoFreePortError extends Error {\n  constructor(message: "
  },
  {
    "path": "src/errors/PlatformError.ts",
    "chars": 193,
    "preview": "/**\n * Error thrown when a platform is not supported.\n */\nexport class PlatformError extends Error {\n  constructor(messa"
  },
  {
    "path": "src/errors/index.ts",
    "chars": 149,
    "preview": "export { CommandError } from \"./CommandError\";\nexport { NoFreePortError } from \"./NoFreePortError\";\nexport { PlatformErr"
  },
  {
    "path": "src/footer.scss",
    "chars": 1275,
    "preview": "#footer-copilot {\n  margin-left: 8px;\n  margin-right: 0;\n  padding: 0;\n  opacity: 0.75;\n  cursor: pointer;\n  display: fl"
  },
  {
    "path": "src/footer.tsx",
    "chars": 11906,
    "preview": "import { useSignal } from \"@preact/signals\";\nimport { render } from \"preact\";\nimport { useEffect, useMemo } from \"preact"
  },
  {
    "path": "src/global.d.ts",
    "chars": 22101,
    "preview": "//////////////////////////////////////////////////////////////////////////////////////////////////\n/// Typora Environmen"
  },
  {
    "path": "src/i18n/en.json",
    "chars": 7306,
    "preview": "{\n  \"button\": {\n    \"ok\": \"OK\",\n    \"cancel\": \"Cancel\",\n    \"delete\": \"Delete\",\n    \"understand\": \"I understand\"\n  },\n  "
  },
  {
    "path": "src/i18n/index.ts",
    "chars": 69,
    "preview": "export { t, pathOf } from \"./t\";\n\nexport type { PathOf } from \"./t\";\n"
  },
  {
    "path": "src/i18n/t.spec.ts",
    "chars": 1642,
    "preview": "import { afterAll, describe, expect, it, vi } from \"vitest\";\n\nimport { t } from \"./t\";\n\nvi.mock(\"./en.json\", () => ({ de"
  },
  {
    "path": "src/i18n/t.ts",
    "chars": 4533,
    "preview": "import en from \"./en.json\";\nimport zhCN from \"./zh-CN.json\";\n\n/**\n * Path of an object joined by dots.\n *\n * @example\n *"
  },
  {
    "path": "src/i18n/zh-CN.json",
    "chars": 7817,
    "preview": "{\n  \"button\": {\n    \"ok\": \"确定\",\n    \"cancel\": \"取消\",\n    \"delete\": \"删除\",\n    \"understand\": \"我知道了\"\n  },\n  \"copilot-status\""
  },
  {
    "path": "src/index.ts",
    "chars": 38,
    "preview": "import \"./patches\";\n\nimport \"./main\";\n"
  },
  {
    "path": "src/logging.ts",
    "chars": 222,
    "preview": "import { createLogger } from \"./utils/logging\";\n\n/**\n * Logger used across the plugin.\n */\nexport const logger = createL"
  },
  {
    "path": "src/mac-server.ts",
    "chars": 2338,
    "preview": "import { fork } from \"child_process\";\nimport net from \"net\";\nimport type { ChildProcessWithoutNullStreams } from \"node:c"
  },
  {
    "path": "src/main.ts",
    "chars": 33865,
    "preview": "import * as path from \"@modules/path\";\nimport { pathToFileURL } from \"@modules/url\";\n\nimport diff from \"fast-diff\";\nimpo"
  },
  {
    "path": "src/modules/fs.ts",
    "chars": 26272,
    "preview": "import type fs from \"node:fs\";\n\nimport { unique } from \"radash\";\n\nimport { PlatformError } from \"@/errors\";\nimport { get"
  },
  {
    "path": "src/modules/path.spec.ts",
    "chars": 942,
    "preview": "import { describe, expect, it } from \"vitest\";\n\nimport * as path from \"./path\";\n\ndescribe(\"basename\", () => {\n  it(\"shou"
  },
  {
    "path": "src/modules/path.ts",
    "chars": 20879,
    "preview": "import type path from \"node:path\";\n\ntype AddSep<F extends (...args: any) => unknown> = (\n  sep: string,\n  ...args: Param"
  },
  {
    "path": "src/modules/url.spec.ts",
    "chars": 945,
    "preview": "import { describe, expect, it } from \"vitest\";\n\nimport { fileURLToPath, pathToFileURL } from \"./url\";\n\ndescribe(\"fileURL"
  },
  {
    "path": "src/modules/url.ts",
    "chars": 2769,
    "preview": "/**\n * This function ensures the correct decodings of percent-encoded characters as\n * well as ensuring a cross-platform"
  },
  {
    "path": "src/patches/index.ts",
    "chars": 125,
    "preview": "// Make sure patches to Typora are imported before other patches\nimport \"./typora\";\n\nimport \"./promise\";\n\nimport \"./jque"
  },
  {
    "path": "src/patches/jquery.ts",
    "chars": 2855,
    "preview": "/************************\n * Custom jQuery events *\n ************************/\n$(function () {\n  /*************\n   * car"
  },
  {
    "path": "src/patches/promise.spec.ts",
    "chars": 2165,
    "preview": "import { describe, expect, it } from \"vitest\";\n\nimport \"./promise\";\n\n// Utility to simulate delays\nconst delay = (ms: nu"
  },
  {
    "path": "src/patches/promise.ts",
    "chars": 4529,
    "preview": "declare global {\n  interface PromiseConstructor {\n    /**\n     * Get the first resolved promise in order.\n     *\n     * "
  },
  {
    "path": "src/patches/typora.ts",
    "chars": 1199,
    "preview": "//////////////////////////////////////////////////\n/// Global patches for Typora to make TS happy ///\n//////////////////"
  },
  {
    "path": "src/reset.d.ts",
    "chars": 37,
    "preview": "import \"@total-typescript/ts-reset\";\n"
  },
  {
    "path": "src/settings.ts",
    "chars": 2622,
    "preview": "import { mapValues } from \"radash\";\nimport { kebabCase } from \"string-ts\";\n\nexport type Settings = typeof defaultSetting"
  },
  {
    "path": "src/styles.scss",
    "chars": 315,
    "preview": ".text-gray {\n  color: gray !important;\n}\n\n.font-italic {\n  font-style: italic !important;\n}\n\n.unset-button {\n  all: unse"
  },
  {
    "path": "src/types/lsp.ts",
    "chars": 202820,
    "preview": "/* eslint-disable @typescript-eslint/no-deprecated */\n/* eslint-disable @typescript-eslint/no-duplicate-type-constituent"
  },
  {
    "path": "src/types/tools.ts",
    "chars": 1671,
    "preview": "/**\n * Construct a type with a set of readonly properties `K` of type `T`.\n */\nexport type ReadonlyRecord<K extends Prop"
  },
  {
    "path": "src/typora-utils.ts",
    "chars": 12815,
    "preview": "/* eslint-disable @typescript-eslint/unbound-method */\n\nimport * as path from \"@modules/path\";\nimport { fileURLToPath } "
  },
  {
    "path": "src/utils/cli-tools.ts",
    "chars": 3137,
    "preview": "import { CommandError, NoFreePortError, PlatformError } from \"@/errors\";\nimport type { ReadonlyRecord } from \"@/types/to"
  },
  {
    "path": "src/utils/diff.ts",
    "chars": 2575,
    "preview": "import diff from \"fast-diff\";\n\nimport type { Range } from \"@/types/lsp\";\n\nexport const computeTextChanges = (\n  oldStr: "
  },
  {
    "path": "src/utils/dom.ts",
    "chars": 1451,
    "preview": "/**\n * Get coordinates of the caret.\n * @returns\n */\nexport const getCaretCoordinate = (): { x: number; y: number } | nu"
  },
  {
    "path": "src/utils/function.ts",
    "chars": 556,
    "preview": "/**\n * Cache the result of a function.\n * @param fn The function to cache.\n * @returns\n */\nexport const cache = <F exten"
  },
  {
    "path": "src/utils/logging.ts",
    "chars": 6121,
    "preview": "import type { integer } from \"@/types/lsp\";\nimport type { Merge, _Id } from \"@/types/tools\";\n\nimport { getErrorCodeName "
  },
  {
    "path": "src/utils/lsp.ts",
    "chars": 5499,
    "preview": "import type {\n  LSPAny,\n  LSPArray,\n  LSPObject,\n  Message,\n  NotificationMessage,\n  RequestMessage,\n  ResponseError,\n  "
  },
  {
    "path": "src/utils/node-bridge.ts",
    "chars": 23563,
    "preview": "import type { ChildProcessWithoutNullStreams } from \"node:child_process\";\n\nimport {\n  accessDir,\n  accessFile,\n  lookApp"
  },
  {
    "path": "src/utils/observable.ts",
    "chars": 576,
    "preview": "/**\n * A lightweight observable implementation.\n */\nexport class Observable<T> {\n  private observers: ((value: T) => voi"
  },
  {
    "path": "src/utils/random.ts",
    "chars": 337,
    "preview": "/**\n * Generate a UUID.\n * @returns\n */\nexport function generateUUID(): string {\n  return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxx"
  },
  {
    "path": "src/utils/stream.ts",
    "chars": 3165,
    "preview": "/**\n * Parse an SSE (Server-Sent Events) stream and handle messages.\n * @param stream The ReadableStream to read from.\n "
  },
  {
    "path": "src/utils/tools.proof.ts",
    "chars": 328,
    "preview": "import { describe, equal, expect, it } from \"typroof\";\n\nimport { omit } from \"./tools\";\n\ndescribe(\"omit\", () => {\n  it(\""
  },
  {
    "path": "src/utils/tools.ts",
    "chars": 3890,
    "preview": "import type { EOL, Range } from \"@/types/lsp\";\nimport type { ReadonlyRecord } from \"@/types/tools\";\n\n/**\n * Assert that "
  },
  {
    "path": "stylelint.config.js",
    "chars": 258,
    "preview": "// @ts-check\n\n/** @satisfies {import(\"stylelint\").Config} */\nconst config = {\n  extends: \"stylelint-config-standard-scss"
  },
  {
    "path": "test/setup.ts",
    "chars": 304,
    "preview": "import { Window } from \"happy-dom\";\n\nimport \"@/patches/typora\";\n\nconst window = new Window();\nObject.assign(window, {\n  "
  },
  {
    "path": "tsconfig.build.json",
    "chars": 209,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"noEmit\": false,\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": "
  },
  {
    "path": "tsconfig.json",
    "chars": 816,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ES2020\",\n    \"lib\":"
  },
  {
    "path": "vitest.config.ts",
    "chars": 381,
    "preview": "import path from \"node:path\";\n\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  resolve: {"
  }
]

About this extraction

This page contains the full source code of the Snowflyt/typora-copilot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 100 files (658.3 KB), approximately 170.3k tokens, and a symbol index with 548 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!