mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge branch 'dev' into question-tool-description
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
name: docs-build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- "packages/kilo-docs/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/kilo-docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build docs site
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build site
|
||||
run: bun run --filter @kilocode/kilo-docs build
|
||||
env:
|
||||
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||
|
||||
- name: Run tests
|
||||
run: bun run --filter @kilocode/kilo-docs test
|
||||
@@ -0,0 +1,32 @@
|
||||
name: test-vscode
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "packages/kilo-vscode/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/kilo-vscode/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: unit tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: packages/kilo-vscode
|
||||
run: bun run test:unit
|
||||
@@ -17,6 +17,7 @@ playground
|
||||
tmp
|
||||
dist
|
||||
ts-dist
|
||||
storybook-static
|
||||
.turbo
|
||||
**/.serena
|
||||
.serena/
|
||||
|
||||
@@ -6,6 +6,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
- The default branch in this repo is `dev`.
|
||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
||||
- You may be running in a git worktree. All changes must be made in your current working directory — never modify files in the main repo checkout.
|
||||
|
||||
## Build and Dev
|
||||
|
||||
|
||||
@@ -1,18 +1,53 @@
|
||||
<p align="center">
|
||||
<a href="https://kilo.ai">
|
||||
<img src="logo.png" alt="Kilo CLI logo">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">The open source AI coding agent.</p>
|
||||
<p align="center">
|
||||
<a href="https://kilo.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://x.com/kilocode"><img alt="X" src="https://img.shields.io/badge/X-@kilocode-000000?style=flat-square&logo=x&logoColor=white" /></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img alt="Reddit" src="https://img.shields.io/badge/Reddit-r%2Fkilocode-FF4500?style=flat-square&logo=reddit&logoColor=white" /></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/kilocode"><img src="https://img.shields.io/badge/kilocode-000000?style=flat&logo=x&logoColor=white" alt="X (Twitter)"></a>
|
||||
<a href="https://blog.kilo.ai"><img src="https://img.shields.io/badge/Blog-555?style=flat&logo=substack&logoColor=white" alt="Substack Blog"></a>
|
||||
<a href="https://kilo.ai/discord"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://www.reddit.com/r/kilocode/"><img src="https://img.shields.io/badge/Join%20r%2Fkilocode-D84315?style=flat&logo=reddit&logoColor=white" alt="Reddit"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
# 🚀 Kilo
|
||||
|
||||
### Installation
|
||||
> Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.
|
||||
> #1 on OpenRouter. 1.5M+ Kilo Coders. 25T+ tokens processed
|
||||
|
||||
- ✨ Generate code from natural language
|
||||
- ✅ Checks its own work
|
||||
- 🧪 Run terminal commands
|
||||
- 🌐 Automate the browser
|
||||
- ⚡ Inline autocomplete suggestions
|
||||
- 🤖 Latest AI models
|
||||
- 🎁 API keys optional
|
||||
- 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/Kilo-Org/kilocode/main/kilo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download)
|
||||
- Install CLI: `npm install -g @kilocode/cli`
|
||||
- [Official Kilo.ai Home page](https://kilo.ai) (learn more)
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Code Generation:** Kilo can generate code using natural language.
|
||||
- **Inline Autocomplete:** Get intelligent code completions as you type, powered by AI.
|
||||
- **Task Automation:** Kilo can automate repetitive coding tasks to save time.
|
||||
- **Automated Refactoring:** Kilo can refactor and improve existing code efficiently.
|
||||
- **MCP Server Marketplace**: Kilo can easily find, and use MCP servers to extend the agent capabilities.
|
||||
- **Multi Mode**: Plan with Architect, Code with Coder, and Debug with Debugger, and make your own custom modes.
|
||||
|
||||
## Get Started in Visual Studio Code
|
||||
|
||||
1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code).
|
||||
2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 – with transparent pricing that matches provider rates exactly.
|
||||
3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action:
|
||||
|
||||
[](https://youtu.be/pqGfYXgrhig)
|
||||
|
||||
## Get Started with the CLI
|
||||
|
||||
```bash
|
||||
# npm
|
||||
@@ -24,19 +59,6 @@ npx @kilocode/cli
|
||||
|
||||
Then run `kilo` in any project directory to start.
|
||||
|
||||
### Agents
|
||||
|
||||
Kilo CLI includes two built-in agents you can switch between using the `Tab` key:
|
||||
|
||||
- **build** - Default, full access agent for development work
|
||||
- **plan** - Read-only agent for analysis and code exploration
|
||||
- Denies file edits by default
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
|
||||
Also included is a **general** subagent for complex searches and multi-step tasks.
|
||||
This is used internally and can be invoked using `@general` in messages.
|
||||
|
||||
### Autonomous Mode (CI/CD)
|
||||
|
||||
Use the `--auto` flag with `kilo run` to enable fully autonomous operation without user interaction. This is ideal for CI/CD pipelines and automated workflows:
|
||||
@@ -47,44 +69,20 @@ kilo run --auto "run tests and fix any failures"
|
||||
|
||||
**Important:** The `--auto` flag disables all permission prompts and allows the agent to execute any action without confirmation. Only use this in trusted environments like CI/CD pipelines.
|
||||
|
||||
### Migrating from Kilo Code Extension
|
||||
## Contributing
|
||||
|
||||
If you're coming from the Kilo Code VS Code extension, your configurations are automatically migrated:
|
||||
We welcome contributions from developers, writers, and enthusiasts!
|
||||
To get started, please read our [Contributing Guide](/CONTRIBUTING.md). It includes details on setting up your environment, coding standards, types of contribution and how to submit pull requests.
|
||||
|
||||
| Kilo Code Feature | Kilo CLI Equivalent |
|
||||
| -------------------------------------------- | -------------------------------------------- |
|
||||
| Custom modes | Converted to agents |
|
||||
| Rules (`.kilocoderules`, `.kilocode/rules/`) | Added to `instructions` array |
|
||||
| Skills (`.kilocode/skills/`) | Auto-discovered alongside `.opencode/skill/` |
|
||||
| Workflows (`.kilocode/workflows/`) | Converted to commands |
|
||||
| MCP servers | Migrated to `mcp` config |
|
||||
## Code of Conduct
|
||||
|
||||
MCP servers are configured in **`~/.config/kilo/opencode.json`** (or `opencode.jsonc`; on Windows the config directory may be under `%USERPROFILE%` depending on your environment). Use a top-level `"mcp"` object: each key is a server name, value is `type: "local"` and `command: ["executable", "arg1", ...]`. Optional per-server: `environment`, `enabled`, `timeout`. Restart the CLI after editing for changes to take effect. (Path from [`packages/opencode/src/global/index.ts`](packages/opencode/src/global/index.ts); schema in [`config.ts`](packages/opencode/src/config/config.ts) `McpLocal`.)
|
||||
Our community is built on respect, inclusivity, and collaboration. Please review our [Code of Conduct](/CODE_OF_CONDUCT.md) to understand the expectations for all contributors and community members.
|
||||
|
||||
**Default mode mappings:**
|
||||
## License
|
||||
|
||||
- `code` → `build` agent
|
||||
- `architect` → `plan` agent
|
||||
This project is licensed under the MIT License.
|
||||
You’re free to use, modify, and distribute this code, including for commercial purposes as long as you include proper attribution and license notices. See [License](/LICENSE).
|
||||
|
||||
For detailed migration information, see:
|
||||
|
||||
- [Migration Overview](packages/opencode/src/kilocode/docs/migration.md)
|
||||
- [Rules Migration](packages/opencode/src/kilocode/docs/rules-migration.md)
|
||||
|
||||
### Documentation
|
||||
|
||||
For more info on how to configure Kilo CLI, [**head over to our docs**](https://kilo.ai/docs).
|
||||
|
||||
### Contributing
|
||||
|
||||
If you're interested in contributing, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### Where did Kilo CLI come from?
|
||||
### Where did Kilo CLI come from?
|
||||
|
||||
Kilo CLI is a fork of [OpenCode](https://github.com/anomalyco/opencode), enhanced to work within the Kilo agentic engineering platform.
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [Discord](https://kilo.ai/discord) | [X.com](https://x.com/kilocode) | [Reddit](https://reddit.com/r/kilocode)
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770073757,
|
||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
||||
"lastModified": 1771207753,
|
||||
"narHash": "sha256-b9uG8yN50DRQ6A7JdZBfzq718ryYrlmGgqkRm9OOwCE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
||||
"rev": "d1c15b7d5806069da59e819999d70e1cec0760bf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
unzip
|
||||
gnutar
|
||||
gzip
|
||||
ripgrep
|
||||
kilo-dev
|
||||
kilo-install-bin
|
||||
kilo-bin
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-230wfQK6tyxdD/u0UijoHVe19bcAgeg9qqIqvJdWLWs=",
|
||||
"aarch64-linux": "sha256-uFT5DTnRCcnBdIV5pdzpu9Qi0YJb0VvwrGpwsLBsHfo=",
|
||||
"aarch64-darwin": "sha256-czlJFKyo39D06NEw0TeSv8cTBbHzFBmZb5bFUpLBH+0=",
|
||||
"x86_64-darwin": "sha256-f/hvwcRjyo6xZ/bVzitCPlHdyisqHTN26iVVRXzHEM8="
|
||||
"x86_64-linux": "sha256-V1SML9naH7HShbdovoQIUc46rH6YNgFF9MSTw95k0fU=",
|
||||
"aarch64-linux": "sha256-wddgxfbK7v7O/zsdbx0e/TNYAw0KzzxyQbuUdwudf/Y=",
|
||||
"aarch64-darwin": "sha256-y7bMXWMSiHTHfKKiJ+c/2WRJmc7mCw2o601Ny8W5Zzs=",
|
||||
"x86_64-darwin": "sha256-hXdKlmVFWq2C0kJWgO1+g82B+/fvn9CG/JQyR31QtzY="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -102,6 +102,6 @@
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
},
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -57,7 +57,7 @@ function UiI18nBridge(props: ParentProps) {
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__KILO__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
|
||||
__KILO__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -18,6 +19,14 @@ export const DialogManageModels: Component = () => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
const providerRank = (id: string) => popularProviders.indexOf(id)
|
||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||
const providerVisible = (providerID: string) =>
|
||||
providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
|
||||
const setProviderVisibility = (providerID: string, checked: boolean) => {
|
||||
providerList(providerID).forEach((x) => {
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -36,7 +45,28 @@ export const DialogManageModels: Component = () => {
|
||||
items={local.model.list()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
groupBy={(x) => x.provider.id}
|
||||
groupHeader={(group) => {
|
||||
const provider = group.items[0].provider
|
||||
return (
|
||||
<>
|
||||
<span>{provider.name}</span>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
|
||||
>
|
||||
<Switch
|
||||
class="-mr-1"
|
||||
checked={providerVisible(provider.id)}
|
||||
onChange={(checked) => setProviderVisibility(provider.id, checked)}
|
||||
hideLabel
|
||||
>
|
||||
{provider.name}
|
||||
</Switch>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
sortGroupsBy={(a, b) => {
|
||||
const aRank = providerRank(a.items[0].provider.id)
|
||||
const bRank = providerRank(b.items[0].provider.id)
|
||||
|
||||
@@ -38,7 +38,12 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
||||
import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
} from "./prompt-input/history"
|
||||
import { createPromptSubmit } from "./prompt-input/submit"
|
||||
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
|
||||
import { PromptContextItems } from "./prompt-input/context-items"
|
||||
@@ -473,10 +478,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const prev = node.previousSibling
|
||||
const next = node.nextSibling
|
||||
const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
|
||||
const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
|
||||
if (!prevIsBr && !nextIsBr) return false
|
||||
if (nextIsBr && !prevIsBr && prev) return false
|
||||
return true
|
||||
return !!prevIsBr && !next
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||
const el = node as HTMLElement
|
||||
@@ -496,6 +498,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
editorRef.appendChild(createPill(part))
|
||||
}
|
||||
}
|
||||
|
||||
const last = editorRef.lastChild
|
||||
if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") {
|
||||
editorRef.appendChild(document.createTextNode("\u200B"))
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
@@ -729,7 +736,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
if (last.nodeType !== Node.TEXT_NODE) {
|
||||
range.setStartAfter(last)
|
||||
const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR"
|
||||
const next = last.nextSibling
|
||||
const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === ""
|
||||
if (isBreak && (!next || emptyText)) {
|
||||
const placeholder = next && emptyText ? next : document.createTextNode("\u200B")
|
||||
if (!next) last.parentNode?.insertBefore(placeholder, null)
|
||||
placeholder.textContent = "\u200B"
|
||||
range.setStart(placeholder, 0)
|
||||
} else {
|
||||
range.setStartAfter(last)
|
||||
}
|
||||
}
|
||||
}
|
||||
range.collapse(true)
|
||||
@@ -899,6 +916,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
.current()
|
||||
.map((part) => ("content" in part ? part.content : ""))
|
||||
.join("")
|
||||
const direction = event.key === "ArrowUp" ? "up" : "down"
|
||||
if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition)) return
|
||||
const isEmpty = textContent.trim() === "" || textLength <= 1
|
||||
const hasNewlines = textContent.includes("\n")
|
||||
const inHistory = store.historyIndex >= 0
|
||||
@@ -907,7 +926,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd)
|
||||
const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart)
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
if (direction === "up") {
|
||||
if (!allowUp) return
|
||||
if (navigateHistory("up")) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -2,17 +2,26 @@ import { describe, expect, test } from "bun:test"
|
||||
import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
|
||||
|
||||
describe("prompt-input editor dom", () => {
|
||||
test("createTextFragment preserves newlines with br and zero-width placeholders", () => {
|
||||
test("createTextFragment preserves newlines with consecutive br nodes", () => {
|
||||
const fragment = createTextFragment("foo\n\nbar")
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(fragment)
|
||||
|
||||
expect(container.childNodes.length).toBe(5)
|
||||
expect(container.childNodes.length).toBe(4)
|
||||
expect(container.childNodes[0]?.textContent).toBe("foo")
|
||||
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
|
||||
expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[3]?.textContent).toBe("bar")
|
||||
})
|
||||
|
||||
test("createTextFragment keeps trailing newline as terminal break", () => {
|
||||
const fragment = createTextFragment("foo\n")
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(fragment)
|
||||
|
||||
expect(container.childNodes.length).toBe(2)
|
||||
expect(container.childNodes[0]?.textContent).toBe("foo")
|
||||
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[2]?.textContent).toBe("\u200B")
|
||||
expect((container.childNodes[3] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[4]?.textContent).toBe("bar")
|
||||
})
|
||||
|
||||
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
|
||||
@@ -48,4 +57,21 @@ describe("prompt-input editor dom", () => {
|
||||
|
||||
container.remove()
|
||||
})
|
||||
|
||||
test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(document.createTextNode("a"))
|
||||
container.appendChild(document.createElement("br"))
|
||||
container.appendChild(document.createElement("br"))
|
||||
container.appendChild(document.createTextNode("b"))
|
||||
document.body.appendChild(container)
|
||||
|
||||
setCursorPosition(container, 2)
|
||||
expect(getCursorPosition(container)).toBe(2)
|
||||
|
||||
setCursorPosition(container, 3)
|
||||
expect(getCursorPosition(container)).toBe(3)
|
||||
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,8 +4,6 @@ export function createTextFragment(content: string): DocumentFragment {
|
||||
segments.forEach((segment, index) => {
|
||||
if (segment) {
|
||||
fragment.appendChild(document.createTextNode(segment))
|
||||
} else if (segments.length > 1) {
|
||||
fragment.appendChild(document.createTextNode("\u200B"))
|
||||
}
|
||||
if (index < segments.length - 1) {
|
||||
fragment.appendChild(document.createElement("br"))
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Prompt } from "@/context/prompt"
|
||||
import { clonePromptParts, navigatePromptHistory, prependHistoryEntry, promptLength } from "./history"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
clonePromptParts,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
} from "./history"
|
||||
|
||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
@@ -66,4 +72,20 @@ describe("prompt-input history", () => {
|
||||
if (original[1]?.type !== "file") throw new Error("expected file")
|
||||
expect(original[1].selection?.startLine).toBe(1)
|
||||
})
|
||||
|
||||
test("canNavigateHistoryAtCursor only allows multiline boundaries", () => {
|
||||
const value = "a\nb\nc"
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,13 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
export const MAX_HISTORY = 100
|
||||
|
||||
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number) {
|
||||
if (!text.includes("\n")) return true
|
||||
const position = Math.max(0, Math.min(cursor, text.length))
|
||||
if (direction === "up") return !text.slice(0, position).includes("\n")
|
||||
return !text.slice(position).includes("\n")
|
||||
}
|
||||
|
||||
export function clonePromptParts(prompt: Prompt): Prompt {
|
||||
return prompt.map((part) => {
|
||||
if (part.type === "text") return { ...part }
|
||||
|
||||
@@ -46,6 +46,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
type Queued = { directory: string; payload: Event }
|
||||
const FLUSH_FRAME_MS = 16
|
||||
const STREAM_YIELD_MS = 8
|
||||
const RECONNECT_DELAY_MS = 250
|
||||
|
||||
let queue: Queued[] = []
|
||||
let buffer: Queued[] = []
|
||||
@@ -91,50 +92,58 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
}
|
||||
|
||||
let streamErrorLogged = false
|
||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
void (async () => {
|
||||
const events = await eventSdk.global.event({
|
||||
onSseError: (error) => {
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream error", {
|
||||
url: server.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
while (!abort.signal.aborted) {
|
||||
try {
|
||||
const events = await eventSdk.global.event({
|
||||
onSseError: (error) => {
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream error", {
|
||||
url: server.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
let yielded = Date.now()
|
||||
for await (const event of events.stream) {
|
||||
const directory = event.directory ?? "global"
|
||||
const payload = event.payload
|
||||
const k = key(directory, payload)
|
||||
if (k) {
|
||||
const i = coalesced.get(k)
|
||||
if (i !== undefined) {
|
||||
queue[i] = { directory, payload }
|
||||
continue
|
||||
}
|
||||
coalesced.set(k, queue.length)
|
||||
}
|
||||
queue.push({ directory, payload })
|
||||
schedule()
|
||||
let yielded = Date.now()
|
||||
for await (const event of events.stream) {
|
||||
streamErrorLogged = false
|
||||
const directory = event.directory ?? "global"
|
||||
const payload = event.payload
|
||||
const k = key(directory, payload)
|
||||
if (k) {
|
||||
const i = coalesced.get(k)
|
||||
if (i !== undefined) {
|
||||
queue[i] = { directory, payload }
|
||||
continue
|
||||
}
|
||||
coalesced.set(k, queue.length)
|
||||
}
|
||||
queue.push({ directory, payload })
|
||||
schedule()
|
||||
|
||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||
yielded = Date.now()
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||
yielded = Date.now()
|
||||
await wait(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!streamErrorLogged) {
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream failed", {
|
||||
url: server.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (abort.signal.aborted) return
|
||||
await wait(RECONNECT_DELAY_MS)
|
||||
}
|
||||
})()
|
||||
.finally(flush)
|
||||
.catch((error) => {
|
||||
if (streamErrorLogged) return
|
||||
streamErrorLogged = true
|
||||
console.error("[global-sdk] event stream failed", {
|
||||
url: server.url,
|
||||
fetch: eventFetch ? "platform" : "webview",
|
||||
error,
|
||||
})
|
||||
})
|
||||
})().finally(flush)
|
||||
|
||||
onCleanup(() => {
|
||||
abort.abort()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createRoot, getOwner } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { State } from "./types"
|
||||
import { createChildStoreManager } from "./child-store"
|
||||
|
||||
const child = () => createStore({} as State)
|
||||
|
||||
describe("createChildStoreManager", () => {
|
||||
test("does not evict the active directory during mark", () => {
|
||||
const owner = createRoot((dispose) => {
|
||||
const current = getOwner()
|
||||
dispose()
|
||||
return current
|
||||
})
|
||||
if (!owner) throw new Error("owner required")
|
||||
|
||||
const manager = createChildStoreManager({
|
||||
owner,
|
||||
markStats() {},
|
||||
incrementEvictions() {},
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap() {},
|
||||
onDispose() {},
|
||||
})
|
||||
|
||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||
manager.children[directory] = child()
|
||||
manager.pin(directory)
|
||||
})
|
||||
|
||||
const directory = "/active"
|
||||
manager.children[directory] = child()
|
||||
manager.mark(directory)
|
||||
|
||||
expect(manager.children[directory]).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -36,7 +36,7 @@ export function createChildStoreManager(input: {
|
||||
const mark = (directory: string) => {
|
||||
if (!directory) return
|
||||
lifecycle.set(directory, { lastAccessAt: Date.now() })
|
||||
runEviction()
|
||||
runEviction(directory)
|
||||
}
|
||||
|
||||
const pin = (directory: string) => {
|
||||
@@ -106,7 +106,7 @@ export function createChildStoreManager(input: {
|
||||
return true
|
||||
}
|
||||
|
||||
function runEviction() {
|
||||
function runEviction(skip?: string) {
|
||||
const stores = Object.keys(children)
|
||||
if (stores.length === 0) return
|
||||
const list = pickDirectoriesToEvict({
|
||||
@@ -116,7 +116,7 @@ export function createChildStoreManager(input: {
|
||||
max: MAX_DIR_STORES,
|
||||
ttl: DIR_IDLE_TTL_MS,
|
||||
now: Date.now(),
|
||||
})
|
||||
}).filter((directory) => directory !== skip)
|
||||
if (list.length === 0) return
|
||||
for (const directory of list) {
|
||||
if (!disposeDirectory(directory)) continue
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppBaseProviders, AppInterface } from "@/app"
|
||||
import { Platform, PlatformProvider } from "@/context/platform"
|
||||
import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
import { handleNotificationClick } from "@/utils/notification-click"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
|
||||
@@ -68,11 +69,7 @@ const notify: Platform["notify"] = async (title, description, href) => {
|
||||
})
|
||||
|
||||
notification.onclick = () => {
|
||||
window.focus()
|
||||
if (href) {
|
||||
window.history.pushState(null, "", href)
|
||||
window.dispatchEvent(new PopStateEvent("popstate"))
|
||||
}
|
||||
handleNotificationClick(href)
|
||||
notification.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
|
||||
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
|
||||
"provider.connect.oauth.auto.visit.link": "هذا الرابط",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
|
||||
"provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
|
||||
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
|
||||
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
|
||||
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
|
||||
|
||||
@@ -109,6 +109,7 @@ export const dict = {
|
||||
"dialog.model.empty": "No model results",
|
||||
"dialog.model.manage": "Manage models",
|
||||
"dialog.model.manage.description": "Customize which models appear in the model selector.",
|
||||
"dialog.model.manage.provider.toggle": "Toggle all {{provider}} models",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "Free models provided by Kilo",
|
||||
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
|
||||
|
||||
@@ -385,8 +385,7 @@ export const dict = {
|
||||
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
"Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
|
||||
"toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
|
||||
"toast.update.action.installRestart": "Installer et redémarrer",
|
||||
"toast.update.action.notYet": "Pas encore",
|
||||
"error.page.title": "Quelque chose s'est mal passé",
|
||||
@@ -517,8 +516,7 @@ export const dict = {
|
||||
"sidebar.workspaces.enable": "Activer les espaces de travail",
|
||||
"sidebar.workspaces.disable": "Désactiver les espaces de travail",
|
||||
"sidebar.gettingStarted.title": "Commencer",
|
||||
"sidebar.gettingStarted.line1":
|
||||
"Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
|
||||
"sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
|
||||
"sidebar.gettingStarted.line2":
|
||||
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
|
||||
"sidebar.project.recentSessions": "Sessions récentes",
|
||||
|
||||
@@ -144,8 +144,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
|
||||
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
|
||||
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
|
||||
"provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
|
||||
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
|
||||
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
|
||||
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
|
||||
|
||||
@@ -145,8 +145,7 @@ export const dict = {
|
||||
"provider.connect.oauth.code.invalid": "授權碼無效",
|
||||
"provider.connect.oauth.auto.visit.prefix": "造訪 ",
|
||||
"provider.connect.oauth.auto.visit.link": "此連結",
|
||||
"provider.connect.oauth.auto.visit.suffix":
|
||||
" 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
|
||||
"provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
|
||||
"provider.connect.oauth.auto.confirmationCode": "確認碼",
|
||||
"provider.connect.toast.connected.title": "{{provider}} 已連線",
|
||||
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform"
|
||||
export { AppBaseProviders, AppInterface } from "./app"
|
||||
export { useCommand } from "./context/command"
|
||||
export { handleNotificationClick } from "./utils/notification-click"
|
||||
|
||||
@@ -32,9 +32,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
<div class="size-full rounded overflow-clip">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={
|
||||
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
|
||||
}
|
||||
src={props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full rounded"
|
||||
classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { handleNotificationClick } from "./notification-click"
|
||||
|
||||
describe("notification click", () => {
|
||||
test("focuses and navigates when href exists", () => {
|
||||
const calls: string[] = []
|
||||
handleNotificationClick("/abc/session/123", {
|
||||
focus: () => calls.push("focus"),
|
||||
location: {
|
||||
assign: (href) => calls.push(href),
|
||||
},
|
||||
})
|
||||
expect(calls).toEqual(["focus", "/abc/session/123"])
|
||||
})
|
||||
|
||||
test("only focuses when href is missing", () => {
|
||||
const calls: string[] = []
|
||||
handleNotificationClick(undefined, {
|
||||
focus: () => calls.push("focus"),
|
||||
location: {
|
||||
assign: (href) => calls.push(href),
|
||||
},
|
||||
})
|
||||
expect(calls).toEqual(["focus"])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
type WindowTarget = {
|
||||
focus: () => void
|
||||
location: {
|
||||
assign: (href: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
export const handleNotificationClick = (href?: string, target: WindowTarget = window) => {
|
||||
target.focus()
|
||||
if (!href) return
|
||||
target.location.assign(href)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -566,8 +566,8 @@ async fn initialize(app: AppHandle) {
|
||||
// come from any invocation of the sidecar CLI. The progress is captured by a stdout stream interceptor.
|
||||
// Then in the loading task, we wait for sqlite migration to complete before
|
||||
// starting our health check against the server, otherwise long migrations could result in a timeout.
|
||||
let sqlite_enabled = option_env!("KILO_SQLITE").is_some();
|
||||
let sqlite_done = (sqlite_enabled && !sqlite_file_exists()).then(|| {
|
||||
let needs_sqlite_migration = option_env!("KILO_SQLITE").is_some() && !sqlite_file_exists();
|
||||
let sqlite_done = needs_sqlite_migration.then(|| {
|
||||
tracing::info!(
|
||||
path = %opencode_db_path().expect("failed to get db path").display(),
|
||||
"Sqlite file not found, waiting for it to be generated"
|
||||
@@ -665,12 +665,14 @@ async fn initialize(app: AppHandle) {
|
||||
}
|
||||
|
||||
let _ = server_ready_rx.await;
|
||||
|
||||
tracing::info!("Loading task finished");
|
||||
}
|
||||
})
|
||||
.map_err(|_| ())
|
||||
.shared();
|
||||
|
||||
let loading_window = if sqlite_enabled
|
||||
let loading_window = if needs_sqlite_migration
|
||||
&& timeout(Duration::from_secs(1), loading_task.clone())
|
||||
.await
|
||||
.is_err()
|
||||
|
||||
@@ -10,10 +10,13 @@ export const commands = {
|
||||
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
|
||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||
getWslConfig: () => __TAURI_INVOKE<WslConfig>("get_wsl_config"),
|
||||
setWslConfig: (config: WslConfig) => __TAURI_INVOKE<null>("set_wsl_config", { config }),
|
||||
getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"),
|
||||
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
|
||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
||||
wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE<string>("wsl_path", { path, mode }),
|
||||
resolveAppPath: (appName: string) => __TAURI_INVOKE<string | null>("resolve_app_path", { appName }),
|
||||
};
|
||||
|
||||
|
||||
+344
-304
@@ -1,7 +1,14 @@
|
||||
// @refresh reload
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app"
|
||||
import {
|
||||
AppBaseProviders,
|
||||
AppInterface,
|
||||
PlatformProvider,
|
||||
Platform,
|
||||
useCommand,
|
||||
handleNotificationClick,
|
||||
} from "@opencode-ai/app"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
|
||||
@@ -9,7 +16,6 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import { AsyncStorage } from "@solid-primitives/storage"
|
||||
@@ -52,338 +58,371 @@ const listenForDeepLinks = async () => {
|
||||
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
|
||||
}
|
||||
|
||||
const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
platform: "desktop",
|
||||
os: (() => {
|
||||
const createPlatform = (password: Accessor<string | null>): Platform => {
|
||||
const os = (() => {
|
||||
const type = ostype()
|
||||
if (type === "macos" || type === "windows" || type === "linux") return type
|
||||
return undefined
|
||||
})(),
|
||||
version: pkg.version,
|
||||
})()
|
||||
|
||||
async openDirectoryPickerDialog(opts) {
|
||||
const result = await open({
|
||||
directory: true,
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFolder"),
|
||||
})
|
||||
return result
|
||||
},
|
||||
const wslHome = async () => {
|
||||
if (os !== "windows" || !window.__KILO__?.wsl) return undefined
|
||||
return commands.wslPath("~", "windows").catch(() => undefined)
|
||||
}
|
||||
|
||||
async openFilePickerDialog(opts) {
|
||||
const result = await open({
|
||||
directory: false,
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFile"),
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
async saveFilePickerDialog(opts) {
|
||||
const result = await save({
|
||||
title: opts?.title ?? t("desktop.dialog.saveFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
openLink(url: string) {
|
||||
void shellOpen(url).catch(() => undefined)
|
||||
},
|
||||
|
||||
async openPath(path: string, app?: string) {
|
||||
const os = ostype()
|
||||
if (os === "windows" && app) {
|
||||
const resolvedApp = await commands.resolveAppPath(app)
|
||||
return openerOpenPath(path, resolvedApp || app)
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
if (!result || !window.__KILO__?.wsl) return result
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => commands.wslPath(path, "linux").catch(() => path))) as any
|
||||
}
|
||||
return openerOpenPath(path, app)
|
||||
},
|
||||
return commands.wslPath(result, "linux").catch(() => result) as any
|
||||
}
|
||||
|
||||
back() {
|
||||
window.history.back()
|
||||
},
|
||||
return {
|
||||
platform: "desktop",
|
||||
os,
|
||||
version: pkg.version,
|
||||
|
||||
forward() {
|
||||
window.history.forward()
|
||||
},
|
||||
async openDirectoryPickerDialog(opts) {
|
||||
const defaultPath = await wslHome()
|
||||
const result = await open({
|
||||
directory: true,
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFolder"),
|
||||
defaultPath,
|
||||
})
|
||||
return await handleWslPicker(result)
|
||||
},
|
||||
|
||||
storage: (() => {
|
||||
type StoreLike = {
|
||||
get(key: string): Promise<string | null | undefined>
|
||||
set(key: string, value: string): Promise<unknown>
|
||||
delete(key: string): Promise<unknown>
|
||||
clear(): Promise<unknown>
|
||||
keys(): Promise<string[]>
|
||||
length(): Promise<number>
|
||||
}
|
||||
async openFilePickerDialog(opts) {
|
||||
const result = await open({
|
||||
directory: false,
|
||||
multiple: opts?.multiple ?? false,
|
||||
title: opts?.title ?? t("desktop.dialog.chooseFile"),
|
||||
})
|
||||
return handleWslPicker(result)
|
||||
},
|
||||
|
||||
const WRITE_DEBOUNCE_MS = 250
|
||||
async saveFilePickerDialog(opts) {
|
||||
const result = await save({
|
||||
title: opts?.title ?? t("desktop.dialog.saveFile"),
|
||||
defaultPath: opts?.defaultPath,
|
||||
})
|
||||
return handleWslPicker(result)
|
||||
},
|
||||
|
||||
const storeCache = new Map<string, Promise<StoreLike>>()
|
||||
const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
|
||||
const memoryCache = new Map<string, StoreLike>()
|
||||
openLink(url: string) {
|
||||
void shellOpen(url).catch(() => undefined)
|
||||
},
|
||||
async openPath(path: string, app?: string) {
|
||||
const os = ostype()
|
||||
if (os === "windows") {
|
||||
const resolvedApp = (app && (await commands.resolveAppPath(app))) || app
|
||||
const resolvedPath = await (async () => {
|
||||
if (window.__KILO__?.wsl) {
|
||||
const converted = await commands.wslPath(path, "windows").catch(() => null)
|
||||
if (converted) return converted
|
||||
}
|
||||
|
||||
const flushAll = async () => {
|
||||
const apis = Array.from(apiCache.values())
|
||||
await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
|
||||
}
|
||||
return path
|
||||
})()
|
||||
return openerOpenPath(resolvedPath, resolvedApp)
|
||||
}
|
||||
return openerOpenPath(path, app)
|
||||
},
|
||||
|
||||
if ("addEventListener" in globalThis) {
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState !== "hidden") return
|
||||
void flushAll()
|
||||
back() {
|
||||
window.history.back()
|
||||
},
|
||||
|
||||
forward() {
|
||||
window.history.forward()
|
||||
},
|
||||
|
||||
storage: (() => {
|
||||
type StoreLike = {
|
||||
get(key: string): Promise<string | null | undefined>
|
||||
set(key: string, value: string): Promise<unknown>
|
||||
delete(key: string): Promise<unknown>
|
||||
clear(): Promise<unknown>
|
||||
keys(): Promise<string[]>
|
||||
length(): Promise<number>
|
||||
}
|
||||
|
||||
window.addEventListener("pagehide", () => void flushAll())
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
const WRITE_DEBOUNCE_MS = 250
|
||||
|
||||
const createMemoryStore = () => {
|
||||
const data = new Map<string, string>()
|
||||
const store: StoreLike = {
|
||||
get: async (key) => data.get(key),
|
||||
set: async (key, value) => {
|
||||
data.set(key, value)
|
||||
},
|
||||
delete: async (key) => {
|
||||
data.delete(key)
|
||||
},
|
||||
clear: async () => {
|
||||
data.clear()
|
||||
},
|
||||
keys: async () => Array.from(data.keys()),
|
||||
length: async () => data.size,
|
||||
const storeCache = new Map<string, Promise<StoreLike>>()
|
||||
const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
|
||||
const memoryCache = new Map<string, StoreLike>()
|
||||
|
||||
const flushAll = async () => {
|
||||
const apis = Array.from(apiCache.values())
|
||||
await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
const getStore = (name: string) => {
|
||||
const cached = storeCache.get(name)
|
||||
if (cached) return cached
|
||||
if ("addEventListener" in globalThis) {
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState !== "hidden") return
|
||||
void flushAll()
|
||||
}
|
||||
|
||||
const store = Store.load(name).catch(() => {
|
||||
const cached = memoryCache.get(name)
|
||||
window.addEventListener("pagehide", () => void flushAll())
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
|
||||
const createMemoryStore = () => {
|
||||
const data = new Map<string, string>()
|
||||
const store: StoreLike = {
|
||||
get: async (key) => data.get(key),
|
||||
set: async (key, value) => {
|
||||
data.set(key, value)
|
||||
},
|
||||
delete: async (key) => {
|
||||
data.delete(key)
|
||||
},
|
||||
clear: async () => {
|
||||
data.clear()
|
||||
},
|
||||
keys: async () => Array.from(data.keys()),
|
||||
length: async () => data.size,
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
const getStore = (name: string) => {
|
||||
const cached = storeCache.get(name)
|
||||
if (cached) return cached
|
||||
|
||||
const memory = createMemoryStore()
|
||||
memoryCache.set(name, memory)
|
||||
return memory
|
||||
})
|
||||
const store = Store.load(name).catch(() => {
|
||||
const cached = memoryCache.get(name)
|
||||
if (cached) return cached
|
||||
|
||||
storeCache.set(name, store)
|
||||
return store
|
||||
}
|
||||
const memory = createMemoryStore()
|
||||
memoryCache.set(name, memory)
|
||||
return memory
|
||||
})
|
||||
|
||||
const createStorage = (name: string) => {
|
||||
const pending = new Map<string, string | null>()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let flushing: Promise<void> | undefined
|
||||
storeCache.set(name, store)
|
||||
return store
|
||||
}
|
||||
|
||||
const flush = async () => {
|
||||
if (flushing) return flushing
|
||||
const createStorage = (name: string) => {
|
||||
const pending = new Map<string, string | null>()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let flushing: Promise<void> | undefined
|
||||
|
||||
flushing = (async () => {
|
||||
const store = await getStore(name)
|
||||
while (pending.size > 0) {
|
||||
const batch = Array.from(pending.entries())
|
||||
pending.clear()
|
||||
for (const [key, value] of batch) {
|
||||
if (value === null) {
|
||||
await store.delete(key).catch(() => undefined)
|
||||
} else {
|
||||
await store.set(key, value).catch(() => undefined)
|
||||
const flush = async () => {
|
||||
if (flushing) return flushing
|
||||
|
||||
flushing = (async () => {
|
||||
const store = await getStore(name)
|
||||
while (pending.size > 0) {
|
||||
const batch = Array.from(pending.entries())
|
||||
pending.clear()
|
||||
for (const [key, value] of batch) {
|
||||
if (value === null) {
|
||||
await store.delete(key).catch(() => undefined)
|
||||
} else {
|
||||
await store.set(key, value).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
flushing = undefined
|
||||
})
|
||||
})().finally(() => {
|
||||
flushing = undefined
|
||||
})
|
||||
|
||||
return flushing
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined
|
||||
void flush()
|
||||
}, WRITE_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const api: AsyncStorage & { flush: () => Promise<void> } = {
|
||||
flush,
|
||||
getItem: async (key: string) => {
|
||||
const next = pending.get(key)
|
||||
if (next !== undefined) return next
|
||||
|
||||
const store = await getStore(name)
|
||||
const value = await store.get(key).catch(() => null)
|
||||
if (value === undefined) return null
|
||||
return value
|
||||
},
|
||||
setItem: async (key: string, value: string) => {
|
||||
pending.set(key, value)
|
||||
schedule()
|
||||
},
|
||||
removeItem: async (key: string) => {
|
||||
pending.set(key, null)
|
||||
schedule()
|
||||
},
|
||||
clear: async () => {
|
||||
pending.clear()
|
||||
const store = await getStore(name)
|
||||
await store.clear().catch(() => undefined)
|
||||
},
|
||||
key: async (index: number) => {
|
||||
const store = await getStore(name)
|
||||
return (await store.keys().catch(() => []))[index]
|
||||
},
|
||||
getLength: async () => {
|
||||
const store = await getStore(name)
|
||||
return await store.length().catch(() => 0)
|
||||
},
|
||||
get length() {
|
||||
return api.getLength()
|
||||
},
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
return (name = "default.dat") => {
|
||||
const cached = apiCache.get(name)
|
||||
if (cached) return cached
|
||||
|
||||
const api = createStorage(name)
|
||||
apiCache.set(name, api)
|
||||
return api
|
||||
}
|
||||
})(),
|
||||
|
||||
checkUpdate: async () => {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
const next = await check().catch(() => null)
|
||||
if (!next) return { updateAvailable: false }
|
||||
const ok = await next
|
||||
.download()
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!ok) return { updateAvailable: false }
|
||||
update = next
|
||||
return { updateAvailable: true, version: next.version }
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
if (!UPDATER_ENABLED || !update) return
|
||||
if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
|
||||
await update.install().catch(() => undefined)
|
||||
},
|
||||
|
||||
restart: async () => {
|
||||
await commands.killSidecar().catch(() => undefined)
|
||||
await relaunch()
|
||||
},
|
||||
|
||||
notify: async (title, description, href) => {
|
||||
const granted = await isPermissionGranted().catch(() => false)
|
||||
const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
|
||||
if (permission !== "granted") return
|
||||
|
||||
const win = getCurrentWindow()
|
||||
const focused = await win.isFocused().catch(() => document.hasFocus())
|
||||
if (focused) return
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96-v3.png",
|
||||
})
|
||||
notification.onclick = () => {
|
||||
const win = getCurrentWindow()
|
||||
void win.show().catch(() => undefined)
|
||||
void win.unminimize().catch(() => undefined)
|
||||
void win.setFocus().catch(() => undefined)
|
||||
if (href) {
|
||||
window.history.pushState(null, "", href)
|
||||
window.dispatchEvent(new PopStateEvent("popstate"))
|
||||
}
|
||||
notification.close()
|
||||
return flushing
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined
|
||||
void flush()
|
||||
}, WRITE_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const api: AsyncStorage & { flush: () => Promise<void> } = {
|
||||
flush,
|
||||
getItem: async (key: string) => {
|
||||
const next = pending.get(key)
|
||||
if (next !== undefined) return next
|
||||
|
||||
const store = await getStore(name)
|
||||
const value = await store.get(key).catch(() => null)
|
||||
if (value === undefined) return null
|
||||
return value
|
||||
},
|
||||
setItem: async (key: string, value: string) => {
|
||||
pending.set(key, value)
|
||||
schedule()
|
||||
},
|
||||
removeItem: async (key: string) => {
|
||||
pending.set(key, null)
|
||||
schedule()
|
||||
},
|
||||
clear: async () => {
|
||||
pending.clear()
|
||||
const store = await getStore(name)
|
||||
await store.clear().catch(() => undefined)
|
||||
},
|
||||
key: async (index: number) => {
|
||||
const store = await getStore(name)
|
||||
return (await store.keys().catch(() => []))[index]
|
||||
},
|
||||
getLength: async () => {
|
||||
const store = await getStore(name)
|
||||
return await store.length().catch(() => 0)
|
||||
},
|
||||
get length() {
|
||||
return api.getLength()
|
||||
},
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
return (name = "default.dat") => {
|
||||
const cached = apiCache.get(name)
|
||||
if (cached) return cached
|
||||
|
||||
const api = createStorage(name)
|
||||
apiCache.set(name, api)
|
||||
return api
|
||||
}
|
||||
})(),
|
||||
|
||||
checkUpdate: async () => {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
const next = await check().catch(() => null)
|
||||
if (!next) return { updateAvailable: false }
|
||||
const ok = await next
|
||||
.download()
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!ok) return { updateAvailable: false }
|
||||
update = next
|
||||
return { updateAvailable: true, version: next.version }
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
if (!UPDATER_ENABLED || !update) return
|
||||
if (ostype() === "windows") await commands.killSidecar().catch(() => undefined)
|
||||
await update.install().catch(() => undefined)
|
||||
},
|
||||
|
||||
restart: async () => {
|
||||
await commands.killSidecar().catch(() => undefined)
|
||||
await relaunch()
|
||||
},
|
||||
|
||||
notify: async (title, description, href) => {
|
||||
const granted = await isPermissionGranted().catch(() => false)
|
||||
const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
|
||||
if (permission !== "granted") return
|
||||
|
||||
const win = getCurrentWindow()
|
||||
const focused = await win.isFocused().catch(() => document.hasFocus())
|
||||
if (focused) return
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96-v3.png",
|
||||
})
|
||||
notification.onclick = () => {
|
||||
const win = getCurrentWindow()
|
||||
void win.show().catch(() => undefined)
|
||||
void win.unminimize().catch(() => undefined)
|
||||
void win.setFocus().catch(() => undefined)
|
||||
handleNotificationClick(href)
|
||||
notification.close()
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
|
||||
fetch: (input, init) => {
|
||||
const pw = password()
|
||||
|
||||
const addHeader = (headers: Headers, password: string) => {
|
||||
headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
|
||||
}
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (pw) addHeader(input.headers, pw)
|
||||
return tauriFetch(input)
|
||||
} else {
|
||||
const headers = new Headers(init?.headers)
|
||||
if (pw) addHeader(headers, pw)
|
||||
return tauriFetch(input, {
|
||||
...(init as any),
|
||||
headers: headers,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getWslEnabled: async () => {
|
||||
const next = await commands.getWslConfig().catch(() => null)
|
||||
if (next) return next.enabled
|
||||
return window.__KILO__!.wsl ?? false
|
||||
},
|
||||
|
||||
setWslEnabled: async (enabled) => {
|
||||
await commands.setWslConfig({ enabled })
|
||||
},
|
||||
|
||||
getDefaultServerUrl: async () => {
|
||||
const result = await commands.getDefaultServerUrl().catch(() => null)
|
||||
return result
|
||||
},
|
||||
|
||||
setDefaultServerUrl: async (url: string | null) => {
|
||||
await commands.setDefaultServerUrl(url)
|
||||
},
|
||||
|
||||
getDisplayBackend: async () => {
|
||||
const result = await commands.getDisplayBackend().catch(() => null)
|
||||
return result
|
||||
},
|
||||
|
||||
setDisplayBackend: async (backend) => {
|
||||
await commands.setDisplayBackend(backend)
|
||||
},
|
||||
|
||||
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
|
||||
|
||||
webviewZoom,
|
||||
|
||||
checkAppExists: async (appName: string) => {
|
||||
return commands.checkAppExists(appName)
|
||||
},
|
||||
|
||||
async readClipboardImage() {
|
||||
const image = await readImage().catch(() => null)
|
||||
if (!image) return null
|
||||
const bytes = await image.rgba().catch(() => null)
|
||||
if (!bytes || bytes.length === 0) return null
|
||||
const size = await image.size().catch(() => null)
|
||||
if (!size) return null
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = size.width
|
||||
canvas.height = size.height
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return null
|
||||
const imageData = ctx.createImageData(size.width, size.height)
|
||||
imageData.data.set(bytes)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
return new Promise<File | null>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return resolve(null)
|
||||
resolve(new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" }))
|
||||
}, "image/png")
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
|
||||
fetch: (input, init) => {
|
||||
const pw = password()
|
||||
|
||||
const addHeader = (headers: Headers, password: string) => {
|
||||
headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
|
||||
}
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (pw) addHeader(input.headers, pw)
|
||||
return tauriFetch(input)
|
||||
} else {
|
||||
const headers = new Headers(init?.headers)
|
||||
if (pw) addHeader(headers, pw)
|
||||
return tauriFetch(input, {
|
||||
...(init as any),
|
||||
headers: headers,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultServerUrl: async () => {
|
||||
const result = await commands.getDefaultServerUrl().catch(() => null)
|
||||
return result
|
||||
},
|
||||
|
||||
setDefaultServerUrl: async (url: string | null) => {
|
||||
await commands.setDefaultServerUrl(url)
|
||||
},
|
||||
|
||||
getDisplayBackend: async () => {
|
||||
const result = await commands.getDisplayBackend().catch(() => null)
|
||||
return result
|
||||
},
|
||||
|
||||
setDisplayBackend: async (backend) => {
|
||||
await invoke("set_display_backend", { backend }).catch(() => undefined)
|
||||
},
|
||||
|
||||
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
|
||||
|
||||
webviewZoom,
|
||||
|
||||
checkAppExists: async (appName: string) => {
|
||||
return commands.checkAppExists(appName)
|
||||
},
|
||||
|
||||
async readClipboardImage() {
|
||||
const image = await readImage().catch(() => null)
|
||||
if (!image) return null
|
||||
const bytes = await image.rgba().catch(() => null)
|
||||
if (!bytes || bytes.length === 0) return null
|
||||
const size = await image.size().catch(() => null)
|
||||
if (!size) return null
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = size.width
|
||||
canvas.height = size.height
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return null
|
||||
const imageData = ctx.createImageData(size.width, size.height)
|
||||
imageData.data.set(bytes)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
return new Promise<File | null>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return resolve(null)
|
||||
resolve(new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" }))
|
||||
}, "image/png")
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let menuTrigger = null as null | ((id: string) => void)
|
||||
createMenu((id) => {
|
||||
@@ -393,6 +432,7 @@ void listenForDeepLinks()
|
||||
|
||||
render(() => {
|
||||
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
||||
|
||||
const platform = createPlatform(() => serverPassword())
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import { Splash } from "@opencode-ai/ui/logo"
|
||||
import { Progress } from "@opencode-ai/ui/progress"
|
||||
import "./styles.css"
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { commands, events, InitStep } from "./bindings"
|
||||
import { Channel } from "@tauri-apps/api/core"
|
||||
|
||||
@@ -29,36 +29,20 @@ render(() => {
|
||||
channel.onmessage = (next) => setStep(next)
|
||||
commands.awaitInitialization(channel as any).catch(() => undefined)
|
||||
|
||||
createEffect(() => {
|
||||
if (phase() !== "sqlite_waiting") return
|
||||
|
||||
onMount(() => {
|
||||
setLine(0)
|
||||
setPercent(0)
|
||||
|
||||
const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms))
|
||||
|
||||
let stop: (() => void) | undefined
|
||||
let active = true
|
||||
|
||||
void events.sqliteMigrationProgress
|
||||
.listen((e) => {
|
||||
if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value)))
|
||||
if (e.payload.type === "Done") setPercent(100)
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (active) {
|
||||
stop = unlisten
|
||||
return
|
||||
}
|
||||
|
||||
unlisten()
|
||||
})
|
||||
.catch(() => undefined)
|
||||
const listener = events.sqliteMigrationProgress.listen((e) => {
|
||||
if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value)))
|
||||
if (e.payload.type === "Done") setPercent(100)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
listener.then((cb) => cb())
|
||||
timers.forEach(clearTimeout)
|
||||
stop?.()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.23"
|
||||
version = "1.0.24"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilo"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.23/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilo/releases/download/v1.0.24/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
NEXT_PUBLIC_ALGOLIA_APP_ID=ABC123
|
||||
NEXT_PUBLIC_ALGOLIA_API_KEY=zyz6789
|
||||
NEXT_PUBLIC_ALGOLIA_INDEX_NAME="Kilo Code Docs"
|
||||
NEXT_PUBLIC_ALGOLIA_ASSISTANT_ID=foo-bar
|
||||
NEXT_PUBLIC_POSTHOG_KEY=phc_QWERTY
|
||||
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/issues.jsonl merge=beads
|
||||
@@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.next/
|
||||
.vercel
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
screenshots/
|
||||
.beads/
|
||||
.env
|
||||
next-env.d.ts
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": { "DEFAULT_MINIMUM_TOKENS": "" },
|
||||
"alwaysAllow": ["resolve-library-id", "query-docs"],
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
## Project Overview
|
||||
|
||||
This is the Kilo Code documentation site. Kilo Code is the leading open source agentic engineering platform.
|
||||
|
||||
## Dev Server
|
||||
|
||||
The dev server is run with `bun dev` and runs on `http://localhost:3002`. Typically the user will be running it themselves, so always check if it is running FIRST before deciding to run it yourself to test something.
|
||||
|
||||
## Branch Naming Convention
|
||||
|
||||
When making changes _only_ to the documentation, create branches with the `docs/` prefix:
|
||||
|
||||
```bash
|
||||
git checkout -b docs/description-of-change
|
||||
```
|
||||
|
||||
This convention helps identify documentation-only PRs and keeps them organized.
|
||||
|
||||
## Markdoc Custom Tags
|
||||
|
||||
This project uses [Markdoc](https://markdoc.dev/) for rendering markdown with custom components. Custom tags allow you to embed React components directly in markdown files.
|
||||
|
||||
### Images
|
||||
|
||||
Use the Markdoc image tag format:
|
||||
|
||||
```markdown
|
||||
{% image src="/docs/img/kilo-provider/connected-accounts.png" alt="Connect account screen" width="800" caption="Connect account screen" /%}
|
||||
```
|
||||
|
||||
Note that this site is served under kilo.ai/docs so the `/docs` prefix **must** be present in every image path.
|
||||
|
||||
Image attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ------------------------------------------- |
|
||||
| `src` | String | Yes | The image source URL |
|
||||
| `alt` | String | Yes | Alternative text for the image |
|
||||
| `width` | String | No | Width of the image (e.g., '500px', '80%') |
|
||||
| `height` | String | No | Height of the image (e.g., '300px', 'auto') |
|
||||
| `caption` | String | No | Caption displayed below the image |
|
||||
|
||||
### Callouts
|
||||
|
||||
Use the Markdoc callout tag format:
|
||||
|
||||
```markdown
|
||||
{% callout type="info" %}
|
||||
You can report any bugs or feedback by chatting with us in our [Discord server](https://discord.gg/ovhcloud), in the AI Endpoints channel.
|
||||
{% /callout %}
|
||||
```
|
||||
|
||||
Callout attributes:
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
| ----------- | ------- | ------- | ------------------------------------------------- |
|
||||
| `title` | String | - | Optional custom title for the callout |
|
||||
| `type` | String | "note" | One of: generic, note, tip, info, warning, danger |
|
||||
| `collapsed` | Boolean | false | When true, the callout starts collapsed |
|
||||
|
||||
### Codicons
|
||||
|
||||
Use the Markdoc codicon tag format:
|
||||
|
||||
```markdown
|
||||
{% codicon name="gear" /%}
|
||||
```
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Adding New Pages
|
||||
|
||||
1. Create your page in the appropriate directory under `pages/`
|
||||
2. **Always update navigation**: Add the page to the corresponding navigation file in `lib/nav/`
|
||||
- Each section has its own nav file (e.g., `getting-started.ts`, `code-with-ai.ts`, `ai-providers.ts`)
|
||||
- Navigation structure is exported from `lib/nav/index.ts`
|
||||
- See `lib/types.ts` for the `NavSection` and `NavLink` interfaces
|
||||
|
||||
### Removing or Moving Pages
|
||||
|
||||
**Never remove a page without adding a redirect.** This prevents broken links from search engines, external references, and user bookmarks.
|
||||
|
||||
1. Add a redirect entry to `previous-docs-redirects.js`
|
||||
2. Redirect format:
|
||||
```javascript
|
||||
{
|
||||
source: "/docs/old-path",
|
||||
destination: "/docs/new-path",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
}
|
||||
```
|
||||
3. Update the navigation file to remove or update the link
|
||||
4. Redirects are loaded in `next.config.js`
|
||||
@@ -0,0 +1,55 @@
|
||||
# Full Next.js example
|
||||
|
||||
This is a full-featured boilerplate for a creating a documentation website using Markdoc and Next.js.
|
||||
|
||||
<img width="2032" alt="image" src="https://user-images.githubusercontent.com/62121649/174916143-16f18270-0463-402c-8b48-33c627ea7a7e.png">
|
||||
|
||||
## Setup
|
||||
|
||||
First, clone this repo and install the dependencies required:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `index.md`. The page auto-updates as you edit the file.
|
||||
|
||||
## Deploy
|
||||
|
||||
The quickest way to deploy your own version of this boilerplate is by deploying it with [Vercel](https://vercel.com) or [Netlify](https://www.netlify.com/) by clicking one of the buttons below.
|
||||
|
||||
### Deploy with Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/markdoc/next.js-starter)
|
||||
|
||||
### Deploy to Netlify
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/markdoc/next.js-starter)
|
||||
|
||||
## Contributing
|
||||
|
||||
When making documentation _only_ changes, use branch names prefixed with `docs/`:
|
||||
|
||||
```bash
|
||||
git checkout -b docs/your-change-description
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- `docs/add-mcp-tutorial`
|
||||
- `docs/fix-installation-steps`
|
||||
- `docs/update-api-reference`
|
||||
|
||||
For full contribution guidelines, see the [Contributing Guide](https://kilo.ai/docs/contributing).
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Tests for redirect loop detection in previous-docs-redirects.js
|
||||
*
|
||||
* This test suite verifies that the redirect configuration has no loops:
|
||||
* 1. Direct loops: A path redirecting to itself (source === destination)
|
||||
* 2. Indirect loops: A chain of redirects leading back to a starting point (A → B → C → A)
|
||||
*/
|
||||
|
||||
import { expect, describe, it } from "vitest"
|
||||
import redirects from "../previous-docs-redirects.js"
|
||||
|
||||
interface Redirect {
|
||||
source: string
|
||||
destination: string
|
||||
basePath?: boolean
|
||||
permanent?: boolean
|
||||
}
|
||||
|
||||
describe("previous-docs-redirects", () => {
|
||||
describe("direct loop detection", () => {
|
||||
it("should not have any redirects where source equals destination", () => {
|
||||
const directLoops: Redirect[] = []
|
||||
|
||||
for (const redirect of redirects as Redirect[]) {
|
||||
if (redirect.source === redirect.destination) {
|
||||
directLoops.push(redirect)
|
||||
}
|
||||
}
|
||||
|
||||
if (directLoops.length > 0) {
|
||||
const loopDetails = directLoops.map((r) => ` - "${r.source}" redirects to itself`).join("\n")
|
||||
expect.fail(`Found ${directLoops.length} direct redirect loop(s):\n${loopDetails}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("indirect loop detection", () => {
|
||||
it("should not have any redirect chains that form a cycle", () => {
|
||||
// Build a map of source -> destination for quick lookup
|
||||
// Note: We only consider exact path matches, not wildcard patterns like :path*
|
||||
// Also skip direct loops (source === destination) as they're caught by the direct loop test
|
||||
const redirectMap = new Map<string, string>()
|
||||
|
||||
for (const redirect of redirects as Redirect[]) {
|
||||
// Skip wildcard redirects as they don't form exact chains
|
||||
// Skip direct loops as they're caught by the direct loop test
|
||||
if (
|
||||
!redirect.source.includes(":") &&
|
||||
!redirect.source.includes("*") &&
|
||||
redirect.source !== redirect.destination
|
||||
) {
|
||||
redirectMap.set(redirect.source, redirect.destination)
|
||||
}
|
||||
}
|
||||
|
||||
const cycles: string[][] = []
|
||||
|
||||
/**
|
||||
* Detects if following redirects from a starting path leads back to any path in the chain.
|
||||
* Uses a visited set to track the current chain and detect cycles.
|
||||
*/
|
||||
function detectCycle(startPath: string): string[] | null {
|
||||
const visited = new Set<string>()
|
||||
const chain: string[] = [startPath]
|
||||
let currentPath = startPath
|
||||
|
||||
while (redirectMap.has(currentPath)) {
|
||||
const nextPath = redirectMap.get(currentPath)!
|
||||
|
||||
if (visited.has(nextPath)) {
|
||||
// Found a cycle - return the chain from the cycle start
|
||||
const cycleStartIndex = chain.indexOf(nextPath)
|
||||
if (cycleStartIndex !== -1) {
|
||||
return [...chain.slice(cycleStartIndex), nextPath]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
visited.add(currentPath)
|
||||
chain.push(nextPath)
|
||||
currentPath = nextPath
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Check each redirect source for potential cycles
|
||||
for (const source of redirectMap.keys()) {
|
||||
const cycle = detectCycle(source)
|
||||
if (cycle) {
|
||||
// Avoid duplicate cycle reports by checking if we've already found this cycle
|
||||
const cycleKey = [...cycle].sort().join(" -> ")
|
||||
const isDuplicate = cycles.some((existingCycle) => [...existingCycle].sort().join(" -> ") === cycleKey)
|
||||
if (!isDuplicate) {
|
||||
cycles.push(cycle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cycles.length > 0) {
|
||||
const cycleDetails = cycles.map((cycle) => ` - ${cycle.join(" → ")}`).join("\n")
|
||||
expect.fail(`Found ${cycles.length} indirect redirect cycle(s):\n${cycleDetails}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("redirect structure validation", () => {
|
||||
it("should have valid redirect objects with required properties", () => {
|
||||
const invalidRedirects: { index: number; issues: string[] }[] = []
|
||||
|
||||
;(redirects as Redirect[]).forEach((redirect, index) => {
|
||||
const issues: string[] = []
|
||||
|
||||
if (typeof redirect.source !== "string" || redirect.source.trim() === "") {
|
||||
issues.push("missing or invalid 'source' property")
|
||||
}
|
||||
|
||||
if (typeof redirect.destination !== "string" || redirect.destination.trim() === "") {
|
||||
issues.push("missing or invalid 'destination' property")
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
invalidRedirects.push({ index, issues })
|
||||
}
|
||||
})
|
||||
|
||||
if (invalidRedirects.length > 0) {
|
||||
const details = invalidRedirects
|
||||
.map((r) => ` - Redirect at index ${r.index}: ${r.issues.join(", ")}`)
|
||||
.join("\n")
|
||||
expect.fail(`Found ${invalidRedirects.length} invalid redirect(s):\n${details}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from "react"
|
||||
|
||||
type CalloutType = "generic" | "note" | "tip" | "info" | "warning" | "danger"
|
||||
|
||||
interface CalloutProps {
|
||||
type?: CalloutType
|
||||
title?: string
|
||||
collapsed?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const typeConfig: Record<
|
||||
CalloutType,
|
||||
{
|
||||
icon: string | null
|
||||
defaultTitle: string | null
|
||||
borderColor: string
|
||||
bgColor: string
|
||||
titleColor: string
|
||||
iconColor: string
|
||||
}
|
||||
> = {
|
||||
generic: {
|
||||
icon: null,
|
||||
defaultTitle: null,
|
||||
borderColor: "border-l-gray-300 dark:border-l-gray-600",
|
||||
bgColor: "bg-gray-50 dark:bg-gray-800/50",
|
||||
titleColor: "text-gray-700 dark:text-gray-300",
|
||||
iconColor: "text-gray-400",
|
||||
},
|
||||
note: {
|
||||
icon: "📝",
|
||||
defaultTitle: "Note",
|
||||
borderColor: "border-l-gray-500",
|
||||
bgColor: "bg-gray-50 dark:bg-gray-800/50",
|
||||
titleColor: "text-gray-700 dark:text-gray-300",
|
||||
iconColor: "text-gray-500",
|
||||
},
|
||||
tip: {
|
||||
icon: "💡",
|
||||
defaultTitle: "Tip",
|
||||
borderColor: "border-l-green-500",
|
||||
bgColor: "bg-green-50 dark:bg-green-900/20",
|
||||
titleColor: "text-green-700 dark:text-green-400",
|
||||
iconColor: "text-green-500",
|
||||
},
|
||||
info: {
|
||||
icon: "ℹ️",
|
||||
defaultTitle: "Info",
|
||||
borderColor: "border-l-blue-500",
|
||||
bgColor: "bg-blue-50 dark:bg-blue-900/20",
|
||||
titleColor: "text-blue-700 dark:text-blue-400",
|
||||
iconColor: "text-blue-500",
|
||||
},
|
||||
warning: {
|
||||
icon: "⚠️",
|
||||
defaultTitle: "Warning",
|
||||
borderColor: "border-l-yellow-500",
|
||||
bgColor: "bg-yellow-50 dark:bg-yellow-900/20",
|
||||
titleColor: "text-yellow-700 dark:text-yellow-400",
|
||||
iconColor: "text-yellow-500",
|
||||
},
|
||||
danger: {
|
||||
icon: "🚨",
|
||||
defaultTitle: "Danger",
|
||||
borderColor: "border-l-red-500",
|
||||
bgColor: "bg-red-50 dark:bg-red-900/20",
|
||||
titleColor: "text-red-700 dark:text-red-400",
|
||||
iconColor: "text-red-500",
|
||||
},
|
||||
}
|
||||
|
||||
export function Callout({ type = "note", title, collapsed = false, children }: CalloutProps) {
|
||||
const [isExpanded, setIsExpanded] = React.useState(!collapsed)
|
||||
const config = typeConfig[type]
|
||||
const displayTitle = title ?? config.defaultTitle
|
||||
const showHeader = displayTitle || config.icon
|
||||
|
||||
// If collapsed prop is set, make the header clickable
|
||||
if (collapsed) {
|
||||
return (
|
||||
<div className={`my-4 border-l-4 ${config.borderColor} ${config.bgColor} rounded-r-lg p-4`}>
|
||||
{showHeader && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={`flex items-center gap-2 font-semibold ${config.titleColor} w-full text-left cursor-pointer ${isExpanded ? "mb-2" : ""}`}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<span className={`transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}`} aria-hidden="true">
|
||||
▶
|
||||
</span>
|
||||
{config.icon && <span className={config.iconColor}>{config.icon}</span>}
|
||||
{displayTitle && <span className="uppercase text-sm tracking-wide">{displayTitle}</span>}
|
||||
</button>
|
||||
)}
|
||||
{isExpanded && <div className="text-gray-700 dark:text-gray-300 [&>p]:m-0">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default non-collapsible behavior
|
||||
return (
|
||||
<div className={`my-4 border-l-4 ${config.borderColor} ${config.bgColor} rounded-r-lg p-4`}>
|
||||
{showHeader && (
|
||||
<div className={`flex items-center gap-2 font-semibold ${config.titleColor} mb-2`}>
|
||||
{config.icon && <span className={config.iconColor}>{config.icon}</span>}
|
||||
{displayTitle && <span className="uppercase text-sm tracking-wide">{displayTitle}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-gray-700 dark:text-gray-300 [&>p]:m-0">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import Prism from "prismjs"
|
||||
|
||||
import * as React from "react"
|
||||
import { Codicon } from "./Codicon"
|
||||
|
||||
export function CodeBlock({ children, "data-language": language }) {
|
||||
const ref = React.useRef(null)
|
||||
const timeoutRef = React.useRef(null)
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) Prism.highlightElement(ref.current, false)
|
||||
}, [children])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCopy = async () => {
|
||||
const code = ref.current?.textContent || ""
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy code:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="code" aria-live="polite">
|
||||
<button
|
||||
type="button"
|
||||
className="copy-button"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy code to clipboard"
|
||||
title={copied ? "Copied!" : "Copy code"}
|
||||
>
|
||||
{copied ? <Codicon name="check" /> : <Codicon name="copy" />}
|
||||
</button>
|
||||
<pre ref={ref} className={`language-${language}`}>
|
||||
{children}
|
||||
</pre>
|
||||
<style jsx>
|
||||
{`
|
||||
.code {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:global(.dark) .copy-button {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
:global(.dark) .copy-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.copy-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Override Prism styles */
|
||||
.code :global(pre[class*="language-"]) {
|
||||
text-shadow: none;
|
||||
border-radius: 4px;
|
||||
padding-right: 3.5rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import "@vscode/codicons/dist/codicon.css"
|
||||
|
||||
interface CodiconProps {
|
||||
name: string
|
||||
size?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Codicon({ name, size = "1em", className = "" }: CodiconProps) {
|
||||
return (
|
||||
<i
|
||||
className={`codicon codicon-${name} ${className}`.trim()}
|
||||
style={{
|
||||
fontSize: size,
|
||||
verticalAlign: "middle",
|
||||
display: "inline",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
interface CopyPageButtonProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
const router = useRouter()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Reset copied/error state after 3 seconds
|
||||
useEffect(() => {
|
||||
if (copied || error) {
|
||||
const timer = setTimeout(() => {
|
||||
setCopied(false)
|
||||
setError(false)
|
||||
}, 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [copied, error])
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (copied || error || isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Fetch the raw markdown file based on current route
|
||||
// The route path maps to pages/<path>.md
|
||||
const path = router.asPath.split("#")[0].split("?")[0] // Remove hash and query params
|
||||
const mdPath = path === "/" ? "/index" : path
|
||||
|
||||
// Fetch the raw markdown content from the API route
|
||||
const response = await fetch(`/docs/api/raw-markdown?path=${encodeURIComponent(mdPath)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch markdown")
|
||||
}
|
||||
|
||||
const markdown = await response.text()
|
||||
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
setCopied(true)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy page:", err)
|
||||
setError(true)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={copied || error || isLoading}
|
||||
className={`copy-page-button ${copied ? "copied" : ""} ${error ? "errored" : ""} ${className || ""}`}
|
||||
aria-label={copied ? "Copied" : error ? "Copy failed" : "Copy page markdown"}
|
||||
title="Copy page as markdown for use with LLMs"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : error ? (
|
||||
<>
|
||||
<span>Copy failed</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon />
|
||||
<span>Copy page</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<style jsx>{`
|
||||
.copy-page-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-page-button:hover:not(:disabled) {
|
||||
background: var(--bg-tertiary, var(--bg-secondary));
|
||||
color: var(--text-brand);
|
||||
border-color: var(--text-brand);
|
||||
}
|
||||
|
||||
.copy-page-button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.copy-page-button.copied {
|
||||
color: var(--success-color, #22c55e);
|
||||
border-color: var(--success-color, #22c55e);
|
||||
background: var(--success-bg, rgba(34, 197, 94, 0.1));
|
||||
}
|
||||
|
||||
.copy-page-button.errored {
|
||||
color: var(--error-color, #ef4444);
|
||||
border-color: var(--error-color, #ef4444);
|
||||
background: var(--error-bg, rgba(239, 68, 68, 0.1));
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Copy icon (two overlapping rectangles)
|
||||
function CopyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
||||
<path d="M2 10V3.5A1.5 1.5 0 0 1 3.5 2H10" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Check icon for copied state
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 8.5L6.5 12L13 4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as React from "react"
|
||||
|
||||
export function Heading({ id = "", level = 1, children, className }) {
|
||||
return React.createElement(
|
||||
`h${level}`,
|
||||
{
|
||||
id,
|
||||
className: ["heading", className].filter(Boolean).join(" "),
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
|
||||
interface IconProps {
|
||||
src: string
|
||||
srcDark?: string
|
||||
alt?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
export function Icon({ src, srcDark, alt = "icon", size = "1.2em" }: IconProps) {
|
||||
const [isDark, setIsDark] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial dark mode state
|
||||
const checkDarkMode = () => {
|
||||
setIsDark(document.documentElement.classList.contains("dark"))
|
||||
}
|
||||
|
||||
checkDarkMode()
|
||||
|
||||
// Watch for dark mode changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === "class") {
|
||||
checkDarkMode()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
observer.observe(document.documentElement, { attributes: true })
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
const imageSrc = isDark && srcDark ? srcDark : src
|
||||
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
style={{
|
||||
height: size,
|
||||
width: "auto",
|
||||
verticalAlign: "middle",
|
||||
display: "inline",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react"
|
||||
|
||||
interface ImageProps {
|
||||
src: string
|
||||
alt: string
|
||||
width?: string
|
||||
height?: string
|
||||
caption?: string
|
||||
}
|
||||
|
||||
// Helper to add 'px' to numeric values that don't have units
|
||||
function addPxIfNeeded(value: string): string {
|
||||
// If the value is purely numeric, add 'px'
|
||||
if (/^\d+(\.\d+)?$/.test(value)) {
|
||||
return `${value}px`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function Image({ src, alt, width, height, caption }: ImageProps) {
|
||||
const imgStyle: React.CSSProperties = {
|
||||
maxWidth: "100%",
|
||||
height: "auto",
|
||||
}
|
||||
|
||||
if (width) imgStyle.width = addPxIfNeeded(width)
|
||||
if (height) imgStyle.height = addPxIfNeeded(height)
|
||||
|
||||
const figureStyle: React.CSSProperties = {
|
||||
margin: "1.5rem 0",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
}
|
||||
|
||||
// If width is specified, apply it to the figure to constrain caption width
|
||||
if (width) {
|
||||
figureStyle.width = addPxIfNeeded(width)
|
||||
figureStyle.maxWidth = "100%"
|
||||
}
|
||||
|
||||
return (
|
||||
<figure style={figureStyle}>
|
||||
<img src={src} alt={alt} style={imgStyle} />
|
||||
{caption && (
|
||||
<figcaption
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginTop: "0.5rem",
|
||||
color: "var(--gray-600, #6b7280)",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from "react"
|
||||
import { Icon } from "./Icon"
|
||||
|
||||
interface KiloCodeIconProps {
|
||||
size?: string
|
||||
}
|
||||
|
||||
export function KiloCodeIcon({ size = "1.2em" }: KiloCodeIconProps) {
|
||||
return <Icon src="/docs/img/kilo-v1.svg" srcDark="/docs/img/kilo-v1-white.svg" alt="Kilo Code Icon" size={size} />
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import Link from "next/link"
|
||||
import { SectionNav } from "../lib/types"
|
||||
import { Nav } from "../lib/nav"
|
||||
|
||||
// Define navigation items for each major section
|
||||
const sectionNavItems: SectionNav = {
|
||||
"getting-started": Nav.GettingStartedNav,
|
||||
"code-with-ai": Nav.CodeWithAiNav,
|
||||
customize: Nav.CustomizeNav,
|
||||
collaborate: Nav.CollaborateNav,
|
||||
"automate/tools": Nav.ToolsNav,
|
||||
automate: Nav.AutomateNav,
|
||||
"deploy-secure": Nav.DeploySecureNav,
|
||||
contributing: Nav.ContributingNav,
|
||||
"ai-providers": Nav.AiProvidersNav,
|
||||
gateway: Nav.GatewayNav,
|
||||
}
|
||||
|
||||
// Main nav items with their section keys
|
||||
const mainNavItems = [
|
||||
{ label: "Home", href: "/", sectionKey: null },
|
||||
{ label: "Get Started", href: "/getting-started", sectionKey: "getting-started" },
|
||||
{ label: "Code with AI", href: "/code-with-ai", sectionKey: "code-with-ai" },
|
||||
{ label: "Customize", href: "/customize", sectionKey: "customize" },
|
||||
{ label: "Collaborate", href: "/collaborate", sectionKey: "collaborate" },
|
||||
{ label: "Automate", href: "/automate", sectionKey: "automate" },
|
||||
{ label: "Deploy & Secure", href: "/deploy-secure", sectionKey: "deploy-secure" },
|
||||
{ label: "AI Gateway", href: "/gateway", sectionKey: "gateway" },
|
||||
{ label: "Contributing", href: "/contributing", sectionKey: "contributing" },
|
||||
]
|
||||
|
||||
function getCurrentSection(pathname: string): string | null {
|
||||
const sectionKeys = Object.keys(sectionNavItems)
|
||||
for (const section of sectionKeys) {
|
||||
if (pathname.startsWith(`/${section}`)) {
|
||||
return section
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getSectionLabel(sectionKey: string): string {
|
||||
const item = mainNavItems.find((nav) => nav.sectionKey === sectionKey)
|
||||
return item?.label || sectionKey
|
||||
}
|
||||
|
||||
// Chevron icons as components
|
||||
const ChevronRight = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
||||
<path d="M6 4L10 8L6 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChevronDown = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
||||
<path d="M4 6L8 10L12 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChevronLeft = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
|
||||
<path d="M10 4L6 8L10 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
interface SideNavProps {
|
||||
isMobileOpen?: boolean
|
||||
onMobileClose?: () => void
|
||||
}
|
||||
|
||||
export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
|
||||
const router = useRouter()
|
||||
const currentSection = getCurrentSection(router.pathname)
|
||||
|
||||
// Track which view is shown: 'main' or 'section'
|
||||
const [activeView, setActiveView] = useState<"main" | "section">(currentSection ? "section" : "main")
|
||||
|
||||
// Track which section is being viewed (for animation purposes)
|
||||
const [viewedSection, setViewedSection] = useState<string | null>(currentSection)
|
||||
|
||||
// Track which nav items are expanded (for nested navigation)
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set())
|
||||
|
||||
// Update view when route changes
|
||||
useEffect(() => {
|
||||
const newSection = getCurrentSection(router.pathname)
|
||||
if (newSection) {
|
||||
setViewedSection(newSection)
|
||||
setActiveView("section")
|
||||
} else {
|
||||
setActiveView("main")
|
||||
}
|
||||
}, [router.pathname])
|
||||
|
||||
// Auto-expand parent items when their child is active or when the parent itself is active
|
||||
useEffect(() => {
|
||||
const sectionItems = viewedSection ? sectionNavItems[viewedSection] || [] : []
|
||||
const newExpandedItems = new Set<string>()
|
||||
|
||||
sectionItems.forEach((group) => {
|
||||
group.links.forEach((link) => {
|
||||
if (link.subLinks) {
|
||||
const hasActiveChild = link.subLinks.some((subLink) => router.pathname === subLink.href)
|
||||
const isParentActive = router.pathname === link.href
|
||||
if (hasActiveChild || isParentActive) {
|
||||
newExpandedItems.add(link.href)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setExpandedItems(newExpandedItems)
|
||||
}, [router.pathname, viewedSection])
|
||||
|
||||
const handleLinkClick = () => {
|
||||
if (onMobileClose) {
|
||||
onMobileClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSectionClick = (sectionKey: string | null) => {
|
||||
if (sectionKey) {
|
||||
setViewedSection(sectionKey)
|
||||
setActiveView("section")
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackClick = () => {
|
||||
setActiveView("main")
|
||||
}
|
||||
|
||||
const toggleExpanded = (href: string) => {
|
||||
setExpandedItems((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(href)) {
|
||||
newSet.delete(href)
|
||||
} else {
|
||||
newSet.add(href)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const sectionItems = viewedSection ? sectionNavItems[viewedSection] || [] : []
|
||||
const sectionLabel = viewedSection ? getSectionLabel(viewedSection) : ""
|
||||
|
||||
// Main navigation panel
|
||||
const mainNavPanel = (
|
||||
<div className="nav-panel main-panel">
|
||||
{mainNavItems.map((item) => {
|
||||
const isActive = item.sectionKey ? router.pathname.startsWith(item.href) : router.pathname === item.href
|
||||
const hasSubItems = item.sectionKey && sectionNavItems[item.sectionKey]
|
||||
|
||||
if (hasSubItems) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={item.href}
|
||||
onClick={() => handleSectionClick(item.sectionKey)}
|
||||
className={`nav-item nav-item-button ${isActive ? "active" : ""}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-arrow">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={handleLinkClick}
|
||||
className={`nav-item ${isActive ? "active" : ""}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<style jsx>{`
|
||||
.nav-panel {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
border-radius: 0.5rem;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item-button {
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-arrow {
|
||||
opacity: 1;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Section navigation panel
|
||||
const sectionNavPanel = (
|
||||
<div className="nav-panel section-panel">
|
||||
<button type="button" className="back-button mobile-only" onClick={handleBackClick}>
|
||||
<span className="back-arrow">
|
||||
<ChevronLeft />
|
||||
</span>
|
||||
<span>{sectionLabel}</span>
|
||||
</button>
|
||||
|
||||
<div className="section-content">
|
||||
{sectionItems.map((group) => (
|
||||
<div key={group.title} className="nav-group">
|
||||
<span className="nav-group-title">{group.title}</span>
|
||||
<ul className="nav-links">
|
||||
{group.links.map((link) => {
|
||||
const hasSubLinks = link.subLinks && link.subLinks.length > 0
|
||||
const isExpanded = expandedItems.has(link.href)
|
||||
const hasActiveChild = link.subLinks?.some((subLink) => router.pathname === subLink.href)
|
||||
// Parent should only be active if it's the current page AND it has no active children
|
||||
const active = router.pathname === link.href && !hasActiveChild
|
||||
|
||||
return (
|
||||
<li key={link.href} className={`${active ? "active" : ""} ${hasSubLinks ? "has-children" : ""}`}>
|
||||
{hasSubLinks ? (
|
||||
<>
|
||||
<div className="nav-item-with-toggle">
|
||||
<Link href={link.href} onClick={handleLinkClick}>
|
||||
{link.children}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="toggle-button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
toggleExpanded(link.href)
|
||||
}}
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isExpanded ? <ChevronDown /> : <ChevronRight />}
|
||||
</button>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<ul className="sub-links">
|
||||
{link.subLinks.map((subLink) => {
|
||||
// Only mark sublink as active if it's an exact match
|
||||
const subActive = router.pathname === subLink.href
|
||||
return (
|
||||
<li key={subLink.href} className={subActive ? "active" : ""}>
|
||||
<Link href={subLink.href} onClick={handleLinkClick}>
|
||||
{subLink.children}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Link href={link.href} onClick={handleLinkClick}>
|
||||
{link.children}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.nav-panel {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
font-family: inherit;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.back-button.mobile-only {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-links li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-links li :global(a) {
|
||||
display: block;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0.375rem;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-links li :global(a:hover) {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-links li.active > :global(a),
|
||||
.nav-links li.active > :global(.nav-item-with-toggle) > :global(a) {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Nested navigation styles */
|
||||
.nav-links li.has-children {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-links :global(.nav-item-with-toggle) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-links :global(.nav-item-with-toggle a) {
|
||||
flex: 1;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-links :global(.toggle-button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-links :global(.toggle-button:hover) {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-links :global(.sub-links) {
|
||||
padding: 0;
|
||||
margin: 0.25rem 0 0.5rem 0;
|
||||
border-left: 2px solid var(--accent-color);
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-links :global(.sub-links li) {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-links :global(.sub-links a) {
|
||||
display: block;
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-links :global(.sub-links a:hover) {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-links :global(.sub-links li.active a) {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
const navContent = (
|
||||
<div className={`sliding-nav ${activeView === "section" ? "show-section" : "show-main"}`}>
|
||||
{mainNavPanel}
|
||||
{sectionNavPanel}
|
||||
|
||||
<style jsx>{`
|
||||
.sliding-nav {
|
||||
display: flex;
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sliding-nav.show-main {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sliding-nav.show-section {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* On desktop, always show section panel when in a section */
|
||||
@media (min-width: 769px) {
|
||||
.sliding-nav {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.sliding-nav.show-section {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<nav className="sidenav sidenav-desktop">{navContent}</nav>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
<div className={`sidenav-mobile-overlay ${isMobileOpen ? "open" : ""}`} onClick={onMobileClose} />
|
||||
|
||||
{/* Mobile sidebar drawer */}
|
||||
<nav className={`sidenav sidenav-mobile ${isMobileOpen ? "open" : ""}`}>{navContent}</nav>
|
||||
|
||||
<style jsx>{`
|
||||
.sidenav {
|
||||
background: var(--bg-color);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
.sidenav-desktop {
|
||||
position: sticky;
|
||||
top: var(--top-nav-height);
|
||||
height: calc(100vh - var(--top-nav-height));
|
||||
flex: 0 0 260px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Mobile overlay */
|
||||
.sidenav-mobile-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Mobile drawer */
|
||||
.sidenav-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 768px) {
|
||||
.sidenav-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidenav-mobile-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 40;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.sidenav-mobile-overlay.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.sidenav-mobile {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: var(--top-nav-height);
|
||||
left: 0;
|
||||
width: 300px;
|
||||
height: calc(100vh - var(--top-nav-height));
|
||||
z-index: 50;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidenav-mobile.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Global styles for elements that styled-jsx can't reach */}
|
||||
<style jsx global>{`
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
border-radius: 0.5rem;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item-button {
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react"
|
||||
|
||||
interface TableProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Table({ children }: TableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto my-6 border border-neutral-300 dark:border-neutral-700 rounded-lg">
|
||||
<table className="w-full border-collapse text-sm">{children}</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface THeadProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function THead({ children }: THeadProps) {
|
||||
return <thead className="bg-neutral-100 dark:bg-neutral-800/50">{children}</thead>
|
||||
}
|
||||
|
||||
interface TBodyProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function TBody({ children }: TBodyProps) {
|
||||
return <tbody className="bg-white dark:bg-neutral-900/50">{children}</tbody>
|
||||
}
|
||||
|
||||
interface TrProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Tr({ children }: TrProps) {
|
||||
return <tr className="border-b border-neutral-300 dark:border-neutral-700 last:border-b-0">{children}</tr>
|
||||
}
|
||||
|
||||
interface ThProps {
|
||||
children: React.ReactNode
|
||||
width?: string
|
||||
}
|
||||
|
||||
export function Th({ children, width }: ThProps) {
|
||||
return (
|
||||
<th className="px-4 py-2.5 text-left text-sm font-medium text-neutral-700 dark:text-neutral-300" style={{ width }}>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
interface TdProps {
|
||||
children: React.ReactNode
|
||||
colspan?: number
|
||||
rowspan?: number
|
||||
}
|
||||
|
||||
export function Td({ children, colspan, rowspan }: TdProps) {
|
||||
return (
|
||||
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400" colSpan={colspan} rowSpan={rowspan}>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
export function TableOfContents({ toc }) {
|
||||
const items = toc.filter((item) => item.id && (item.level === 2 || item.level === 3))
|
||||
const [activeHash, setActiveHash] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const updateHash = () => setActiveHash(window.location.hash)
|
||||
updateHash()
|
||||
window.addEventListener("hashchange", updateHash)
|
||||
return () => window.removeEventListener("hashchange", updateHash)
|
||||
}, [])
|
||||
|
||||
if (items.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="toc">
|
||||
<ul className="flex column">
|
||||
{items.map((item) => {
|
||||
const href = `#${item.id}`
|
||||
const active = activeHash === href
|
||||
return (
|
||||
<li
|
||||
key={item.title}
|
||||
className={[active ? "active" : undefined, item.level === 3 ? "padded" : undefined]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<Link href={href}>{item.title}</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<style jsx>
|
||||
{`
|
||||
nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-height: calc(100vh - var(--top-nav-height) - 6rem);
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem 0 0;
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: border-color 0.2s ease;
|
||||
overflow-y: auto;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
li {
|
||||
list-style-type: none;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
li :global(a) {
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
li :global(a:hover),
|
||||
li.active :global(a) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
li.padded {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
/* Hide on tablet and mobile */
|
||||
@media (max-width: 1024px) {
|
||||
nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useState, Children, isValidElement, ReactNode, ReactElement } from "react"
|
||||
|
||||
interface TabProps {
|
||||
label: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function Tab({ children }: TabProps) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export function Tabs({ children }: TabsProps) {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
const tabs = Children.toArray(children).filter(
|
||||
(child): child is ReactElement<TabProps> =>
|
||||
isValidElement(child) && (child.type === Tab || (child.props as any)?.label !== undefined),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="tabs-container my-6 border border-neutral-300 dark:border-neutral-700 rounded-lg overflow-hidden">
|
||||
<div className="tabs-header flex border-b border-neutral-300 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800/50 overflow-x-auto">
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 py-2 text-xs sm:px-4 sm:py-2.5 sm:text-sm font-medium transition-colors whitespace-nowrap ${
|
||||
activeIndex === index
|
||||
? "bg-white dark:bg-neutral-900 text-yellow-800 dark:text-yellow-300 border-b-2 border-yellow-800 dark:border-yellow-300 -mb-[1px]"
|
||||
: "text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-200 hover:bg-neutral-200 dark:hover:bg-neutral-700/50"
|
||||
}`}
|
||||
onClick={() => setActiveIndex(index)}
|
||||
>
|
||||
{tab.props.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="tabs-content p-3 sm:p-4 bg-white dark:bg-neutral-900/50">{tabs[activeIndex]}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
|
||||
type Theme = "light" | "dark" | "system"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<Theme>("system")
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// On mount, read the preference from localStorage or default to 'system'
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
const storedTheme = localStorage.getItem("theme") as Theme | null
|
||||
if (storedTheme) {
|
||||
setTheme(storedTheme)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Apply the theme to the document
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
const root = document.documentElement
|
||||
|
||||
if (theme === "system") {
|
||||
localStorage.removeItem("theme")
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
root.classList.toggle("dark", systemDark)
|
||||
} else {
|
||||
localStorage.setItem("theme", theme)
|
||||
root.classList.toggle("dark", theme === "dark")
|
||||
}
|
||||
}, [theme, mounted])
|
||||
|
||||
// Listen for system preference changes
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
if (theme === "system") {
|
||||
document.documentElement.classList.toggle("dark", e.matches)
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange)
|
||||
return () => mediaQuery.removeEventListener("change", handleChange)
|
||||
}, [theme, mounted])
|
||||
|
||||
const cycleTheme = () => {
|
||||
const themes: Theme[] = ["system", "light", "dark"]
|
||||
const currentIndex = themes.indexOf(theme)
|
||||
const nextIndex = (currentIndex + 1) % themes.length
|
||||
setTheme(themes[nextIndex])
|
||||
}
|
||||
|
||||
// Avoid hydration mismatch by not rendering until mounted
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button className="theme-toggle" aria-label="Toggle theme" style={{ width: "32px", height: "32px" }}>
|
||||
<span style={{ opacity: 0 }}>🌙</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
if (theme === "system") {
|
||||
return "💻"
|
||||
}
|
||||
if (theme === "dark") {
|
||||
return "🌙"
|
||||
}
|
||||
return "☀️"
|
||||
}
|
||||
|
||||
const getLabel = () => {
|
||||
if (theme === "system") {
|
||||
return "Using system theme"
|
||||
}
|
||||
if (theme === "dark") {
|
||||
return "Dark mode"
|
||||
}
|
||||
return "Light mode"
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={cycleTheme} className="theme-toggle" aria-label={getLabel()} title={getLabel()}>
|
||||
<span>{getIcon()}</span>
|
||||
</button>
|
||||
<style jsx>{`
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
.theme-toggle span {
|
||||
line-height: 1;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
import React, { useState, useRef, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/router"
|
||||
import docsearch from "@docsearch/js"
|
||||
import { ThemeToggle } from "./ThemeToggle"
|
||||
|
||||
interface NavItem {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
interface DropdownItem {
|
||||
label: string
|
||||
href: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface DropdownMenuProps {
|
||||
label: string
|
||||
items: DropdownItem[]
|
||||
isOpen: boolean
|
||||
onToggle: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{ label: "Get Started", href: "/getting-started" },
|
||||
{ label: "Code with AI", href: "/code-with-ai" },
|
||||
{ label: "Customize", href: "/customize" },
|
||||
{ label: "Collaborate", href: "/collaborate" },
|
||||
{ label: "Automate", href: "/automate" },
|
||||
{ label: "Deploy & Secure", href: "/deploy-secure" },
|
||||
{ label: "Kilo Gateway", href: "/gateway" },
|
||||
{ label: "Contributing", href: "/contributing" },
|
||||
]
|
||||
|
||||
const contributingItems: DropdownItem[] = [
|
||||
{ label: "Contributing Guide", href: "/contributing", description: "How to contribute to Kilo Code" },
|
||||
{
|
||||
label: "Code of Conduct",
|
||||
href: "https://github.com/Kilo-Org/kilocode?tab=coc-ov-file",
|
||||
description: "Our community guidelines",
|
||||
},
|
||||
{ label: "GitHub Repository", href: "https://github.com/Kilo-Org/", description: "View source and issues" },
|
||||
{ label: "Discord Community", href: "https://kilo.ai/discord", description: "Join our community" },
|
||||
]
|
||||
|
||||
const helpItems: DropdownItem[] = [
|
||||
{ label: "Documentation", href: "/", description: "Browse all documentation" },
|
||||
{ label: "FAQ", href: "/getting-started/faq", description: "Frequently asked questions" },
|
||||
{ label: "Community Projects", href: "/community", description: "Explore community resources" },
|
||||
{ label: "Support", href: "https://kilo.ai/support", description: "Get help from the team" },
|
||||
{
|
||||
label: "Changelog",
|
||||
href: "https://github.com/Kilo-Org/kilocode/releases",
|
||||
description: "Latest updates and releases",
|
||||
},
|
||||
]
|
||||
|
||||
function ChevronDownIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SparkleIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 1V3M8 13V15M3 8H1M15 8H13M12.5 3.5L11 5M5 11L3.5 12.5M12.5 12.5L11 11M5 5L3.5 3.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M19 19L14.65 14.65" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function HamburgerIcon({ isOpen }: { isOpen: boolean }) {
|
||||
return (
|
||||
<div className={`hamburger ${isOpen ? "open" : ""}`}>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenu({ label, items, isOpen, onToggle, onClose }: DropdownMenuProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [isOpen, onClose])
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex items-center gap-1 px-3 py-2 text-sm font-medium transition-colors hover:text-[var(--text-color)]"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
{label}
|
||||
<ChevronDownIcon className={`transition-transform ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-64 rounded-lg border shadow-lg z-50"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-color)",
|
||||
borderColor: "var(--border-color)",
|
||||
}}
|
||||
>
|
||||
<div className="py-2">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
className="block px-4 py-2.5 hover:bg-[var(--bg-secondary)] transition-colors"
|
||||
>
|
||||
<span className="block text-sm font-medium" style={{ color: "var(--text-color)" }}>
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description && (
|
||||
<span className="block text-xs mt-0.5" style={{ color: "var(--text-secondary)" }}>
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavTab({ item, isActive }: { item: NavItem; isActive: boolean }) {
|
||||
return (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`relative px-1 py-3 text-sm font-medium transition-colors ${
|
||||
isActive ? "text-indigo-600 dark:text-[#F8F675]" : "hover:text-[var(--text-color)]"
|
||||
}`}
|
||||
style={{ color: isActive ? "var(--text-brand)" : "var(--text-secondary)" }}
|
||||
>
|
||||
{item.label}
|
||||
{isActive && <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600 dark:bg-[#F8F675]" />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface TopNavProps {
|
||||
onMobileMenuToggle?: () => void
|
||||
isMobileMenuOpen?: boolean
|
||||
showMobileMenuButton?: boolean
|
||||
}
|
||||
|
||||
export function TopNav({ onMobileMenuToggle, isMobileMenuOpen = false, showMobileMenuButton = true }: TopNavProps) {
|
||||
const router = useRouter()
|
||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
|
||||
|
||||
const handleDropdownToggle = (name: string) => {
|
||||
setOpenDropdown(openDropdown === name ? null : name)
|
||||
}
|
||||
|
||||
const handleDropdownClose = () => {
|
||||
setOpenDropdown(null)
|
||||
}
|
||||
|
||||
const isActiveTab = (href: string) => {
|
||||
if (href === "/docs") {
|
||||
return router.pathname === "/docs" || router.pathname === "/docs/index"
|
||||
}
|
||||
return router.pathname.startsWith(href)
|
||||
}
|
||||
|
||||
// Open DocSearch modal programmatically
|
||||
const openDocSearch = () => {
|
||||
const searchButton = document.querySelector(".DocSearch-Button") as HTMLButtonElement
|
||||
if (searchButton) {
|
||||
searchButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize DocSearch
|
||||
useEffect(() => {
|
||||
docsearch({
|
||||
container: "#docsearch",
|
||||
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "PMZUYBQDAK",
|
||||
indexName: process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || "docsearch",
|
||||
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "24b09689d5b4223813d9b8e48563c8f6",
|
||||
askAi: process.env.NEXT_PUBLIC_ALGOLIA_ASSISTANT_ID || "askAIDemo",
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header className="top-header">
|
||||
{/* Top bar */}
|
||||
<div className="top-bar">
|
||||
{/* Mobile menu button */}
|
||||
{showMobileMenuButton && (
|
||||
<button className="mobile-menu-btn" onClick={onMobileMenuToggle} aria-label="Toggle menu">
|
||||
<HamburgerIcon isOpen={isMobileMenuOpen} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Link href="/" className="logo-link flex gap-2 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
className="logo-icon"
|
||||
aria-label="Kilo Code Logo"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0,0v100h100V0H0ZM92.5925926,92.5925926H7.4074074V7.4074074h85.1851852v85.1851852ZM61.1111044,71.9096084h9.2592593v7.4074074h-11.6402116l-5.026455-5.026455v-11.6402116h7.4074074v9.2592593ZM77.7777711,71.9096084h-7.4074074v-9.2592593h-9.2592593v-7.4074074h11.6402116l5.026455,5.026455v11.6402116ZM46.2962963,61.1114207h-7.4074074v-7.4074074h7.4074074v7.4074074ZM22.2222222,53.7040133h7.4074074v16.6666667h16.6666667v7.4074074h-19.047619l-5.026455-5.026455v-19.047619ZM77.7777711,38.8888889v7.4074074h-24.0740741v-7.4074074h8.2781918v-9.2592593h-8.2781918v-7.4074074h10.6591442l5.026455,5.026455v11.6402116h8.3884749ZM29.6296296,30.5555556h9.2592593l7.4074074,7.4074074v8.3333333h-7.4074074v-8.3333333h-9.2592593v8.3333333h-7.4074074v-24.0740741h7.4074074v8.3333333ZM46.2962963,30.5555556h-7.4074074v-8.3333333h7.4074074v8.3333333Z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<span className="logo-text font-brand">Kilo Code</span>
|
||||
<span className="docs-label">DOCS</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Center - Search and Ask AI (desktop only) */}
|
||||
<div className="search-container desktop-nav">
|
||||
<div id="docsearch" />
|
||||
|
||||
{/* <button className="ask-ai-btn">
|
||||
Ask AI
|
||||
<SparkleIcon className="w-4 h-4" />
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
<div className="right-actions">
|
||||
{/* Mobile search button */}
|
||||
<button className="mobile-search-btn mobile-only" onClick={openDocSearch} aria-label="Search documentation">
|
||||
<SearchIcon />
|
||||
</button>
|
||||
<ThemeToggle />
|
||||
<Link href="https://kilo.ai/github" className="github-link desktop-nav">
|
||||
GitHub
|
||||
</Link>
|
||||
<Link href="https://app.kilo.ai" className="signin-btn desktop-nav">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secondary nav bar (desktop only) */}
|
||||
<div className="secondary-bar desktop-nav">
|
||||
<nav className="main-nav">
|
||||
{mainNavItems.map((item) => (
|
||||
<NavTab key={item.href} item={item} isActive={isActiveTab(item.href)} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="dropdown-container">
|
||||
<DropdownMenu
|
||||
label="Contributing"
|
||||
items={contributingItems}
|
||||
isOpen={openDropdown === "contributing"}
|
||||
onToggle={() => handleDropdownToggle("contributing")}
|
||||
onClose={handleDropdownClose}
|
||||
/>
|
||||
<DropdownMenu
|
||||
label="Help"
|
||||
items={helpItems}
|
||||
isOpen={openDropdown === "help"}
|
||||
onToggle={() => handleDropdownToggle("help")}
|
||||
onClose={handleDropdownClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Announcement banner */}
|
||||
<div className="announcement-banner">
|
||||
<p>
|
||||
We're <Link href="https://blog.kilo.ai/p/kilo-cli">replatforming our extensions on the new Kilo CLI</Link>.
|
||||
Contribute to the new CLI and pre-release extensions at{" "}
|
||||
<Link href="https://github.com/Kilo-Org/kilo">Kilo-Org/kilo</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.top-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
background-color: var(--bg-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: var(--text-brand);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.docs-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
margin-left: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ask-ai-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.ask-ai-btn:hover {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.signin-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.signin-btn:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.secondary-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-search-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.mobile-search-btn:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 768px) {
|
||||
.top-bar {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-search-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docs-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-banner {
|
||||
background: #1a1a18;
|
||||
color: #a3a3a2;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid #3f3f3f;
|
||||
}
|
||||
|
||||
.announcement-banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.announcement-banner :global(a) {
|
||||
color: #f8f674;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.announcement-banner :global(a:hover) {
|
||||
color: #ffff8d;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.announcement-banner {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from "react"
|
||||
|
||||
interface YouTubeProps {
|
||||
url: string
|
||||
title?: string
|
||||
caption?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the YouTube video ID from various URL formats
|
||||
*/
|
||||
function extractVideoId(url: string): string | null {
|
||||
const patterns = [
|
||||
/(?:youtube\.com\/watch\?v=)([^&\s]+)/,
|
||||
/(?:youtube\.com\/embed\/)([^?\s]+)/,
|
||||
/(?:youtu\.be\/)([^?\s]+)/,
|
||||
/(?:youtube\.com\/v\/)([^?\s]+)/,
|
||||
]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = url.match(pattern)
|
||||
if (match) {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function YouTube({ url, title = "YouTube video", caption }: YouTubeProps) {
|
||||
const videoId = extractVideoId(url)
|
||||
|
||||
if (!videoId) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
backgroundColor: "var(--red-100, #fee2e2)",
|
||||
color: "var(--red-700, #b91c1c)",
|
||||
borderRadius: "0.5rem",
|
||||
margin: "1.5rem 0",
|
||||
}}
|
||||
>
|
||||
Invalid YouTube URL: {url}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "640px",
|
||||
margin: "1.5rem 0",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
paddingBottom: "56.25%", // 16:9 aspect ratio
|
||||
height: 0,
|
||||
overflow: "hidden",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
title={title}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
}}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
{caption && (
|
||||
<figcaption
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginTop: "0.5rem",
|
||||
color: "var(--gray-600, #6b7280)",
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</figcaption>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export * from "./Callout"
|
||||
export * from "./CodeBlock"
|
||||
export * from "./Codicon"
|
||||
export * from "./CopyPageButton"
|
||||
export * from "./Heading"
|
||||
export * from "./Icon"
|
||||
export * from "./Image"
|
||||
export * from "./KiloCodeIcon"
|
||||
export * from "./SideNav"
|
||||
export * from "./Table"
|
||||
export * from "./TableOfContents"
|
||||
export * from "./Tabs"
|
||||
export * from "./ThemeToggle"
|
||||
export * from "./TopNav"
|
||||
export * from "./YouTube"
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Dev Container Persistence
|
||||
description: How to preserve Kilo Code threads and settings in dev containers
|
||||
---
|
||||
|
||||
# Dev Container Persistence
|
||||
|
||||
When using Kilo Code in development containers (VS Code Dev Containers, GitHub Codespaces, etc.), your threads and settings can persist across container rebuilds by properly configuring volume mounts.
|
||||
|
||||
## Why Persistence Matters
|
||||
|
||||
Dev containers are ephemeral by default - when you rebuild the container, all data is lost unless explicitly persisted. Kilo Code stores important data including:
|
||||
|
||||
- **Conversation threads**: Your ongoing discussions with Kilo Code
|
||||
- **Settings**: API configurations, custom modes, and preferences
|
||||
- **Cache**: Vector store for code indexing and browser tool data
|
||||
|
||||
## Required Configuration
|
||||
|
||||
The Kilo Code dev container is pre-configured with named volumes to preserve your data. If you're setting up your own dev container, add these mounts to your `devcontainer.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Your Project",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"mounts": [
|
||||
{
|
||||
"source": "kilocode-global-storage",
|
||||
"target": "/root/.vscode-remote/data/User/globalStorage/kilocode.kilo-code",
|
||||
"type": "volume"
|
||||
},
|
||||
{
|
||||
"source": "kilocode-settings",
|
||||
"target": "/root/.vscode-remote/data/User/settings",
|
||||
"type": "volume"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Locations
|
||||
|
||||
| Data Type | Container Path |
|
||||
| ------------ | ------------------------------------------------------------------------- |
|
||||
| Threads | `/root/.vscode-remote/data/User/globalStorage/kilocode.kilo-code/tasks/` |
|
||||
| Settings | `/root/.vscode-remote/data/User/settings/` |
|
||||
| Cache | `/root/.vscode-remote/data/User/globalStorage/kilocode.kilo-code/cache/` |
|
||||
| Vector Store | `/root/.vscode-remote/data/User/globalStorage/kilocode.kilo-code/vector/` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Threads Don't Appear After Rebuild
|
||||
|
||||
1. **Check volume attachment**: Ensure the dev container has the volumes attached
|
||||
2. **Verify volume contents**: Check that the volume contains your data
|
||||
3. **Rebuild with volumes**: Use `devcontainer rebuild` instead of `devcontainer up --rebuild`
|
||||
|
||||
### Volumes Lost
|
||||
|
||||
If named volumes are accidentally deleted:
|
||||
|
||||
1. Threads cannot be automatically recovered
|
||||
2. Start new conversations with Kilo Code
|
||||
3. Consider implementing a backup strategy for important threads
|
||||
|
||||
### Manual Backup
|
||||
|
||||
To manually back up your threads:
|
||||
|
||||
```bash
|
||||
# Copy thread data from the container
|
||||
docker cp <container-name>:/root/.vscode-remote/data/User/globalStorage/kilocode.kilo-code ./kilocode-backup
|
||||
```
|
||||
|
||||
## Custom Storage Path
|
||||
|
||||
For advanced configurations, you can specify a custom storage path:
|
||||
|
||||
1. Add a bind mount to your `devcontainer.json`:
|
||||
|
||||
```json
|
||||
"mounts": [
|
||||
{
|
||||
"source": "${localWorkspaceFolder}/.kilocode-data",
|
||||
"target": "/home/vscode/kilocode-data",
|
||||
"type": "bind"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
2. Set the custom storage path in VS Code settings:
|
||||
- Open Settings (`Ctrl+,` or `Cmd+,`)
|
||||
- Search for "Kilo Code: Custom Storage Path"
|
||||
- Enter: `/home/vscode/kilocode-data`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use named volumes** for automatic persistence
|
||||
2. **Back up important threads** before major container changes
|
||||
3. **Avoid deleting volumes** during cleanup
|
||||
4. **Test persistence** by rebuilding and verifying threads remain
|
||||
|
||||
## GitHub Codespaces
|
||||
|
||||
GitHub Codespaces automatically persists your VS Code settings and extensions. For Kilo Code threads, the pre-configured dev container includes the necessary volume mounts.
|
||||
|
||||
If using a custom Codespace configuration, ensure the mounts from the Required Configuration section are included.
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
sidebar_label: Switching from Cline
|
||||
---
|
||||
|
||||
# Migrating from Cline to Kilo
|
||||
|
||||
A practical guide for developers switching from Cline to Kilo.
|
||||
|
||||
## Why Switch?
|
||||
|
||||
**Speed.** The Kilo Platform is designed to reduce friction across the entire development workflow. Beyond feature parity with Cline, Kilo includes Code Reviews, App Builder, Cloud Agents, and one-click Deploy.
|
||||
|
||||
**Multi-platform.** Work in VS Code, JetBrains IDEs, CLI, or the web. Your Sessions sync across all of them automatically.
|
||||
|
||||
**Specialized modes.** Instead of one agent doing everything, Kilo has five specialized modes optimized for different parts of your workflow.
|
||||
|
||||
**500+ models.** More providers, more options, better pricing flexibility. Switch models mid-conversation if you want.
|
||||
|
||||
---
|
||||
|
||||
## Understanding Kilo's Mode System
|
||||
|
||||
Cline uses a single agent with a Plan/Act toggle. Kilo takes a different approach: five specialized modes, each optimized for a specific part of development. You select modes from a dropdown in the interface in the Kilo Extension, or with slash commands in the CLI.
|
||||
|
||||
### Kilo's Five Modes
|
||||
|
||||
#### Code Mode (default)
|
||||
|
||||
- **What it does:** Implementation and refactoring
|
||||
- **Cline equivalent:** Act mode
|
||||
- **When to use:** Writing features, fixing bugs, making changes
|
||||
- **Example:** "Add user authentication to the API"
|
||||
|
||||
#### Ask Mode
|
||||
|
||||
- **What it does:** Answers questions, explains code
|
||||
- **Cline equivalent:** Plan mode (read-only exploration)
|
||||
- **When to use:** Understanding codebases, learning patterns
|
||||
- **Example:** "How does our caching layer work?"
|
||||
|
||||
#### Debug Mode
|
||||
|
||||
- **What it does:** Systematic troubleshooting and error diagnosis
|
||||
- **Cline equivalent:** Act mode focused on debugging
|
||||
- **When to use:** Tracking down bugs, fixing runtime issues
|
||||
- **Example:** "Why is this API endpoint returning 500?"
|
||||
|
||||
#### Architect Mode
|
||||
|
||||
- **What it does:** Planning, design, and technical leadership
|
||||
- **Cline equivalent:** Plan mode (exploration and planning)
|
||||
- **When to use:** Before complex refactors, designing new systems
|
||||
- **Example:** "Design a caching strategy for our API"
|
||||
|
||||
#### Orchestrator Mode
|
||||
|
||||
- **What it does:** Breaks down complex tasks, coordinates other modes
|
||||
- **Cline equivalent:** Complex multi-step workflows
|
||||
- **When to use:** Large features spanning multiple systems
|
||||
- **Example:** "Build a complete user management system"
|
||||
|
||||
### Mode Switching in Action
|
||||
|
||||
**Cline workflow:** Toggle Plan/Act → Submit task → Plan phase → Approve → Act phase → Checkpoint
|
||||
|
||||
**Kilo workflow:**
|
||||
Select mode from dropdown → Build → Switch modes as needed → Checkpoint → Session auto-saves
|
||||
|
||||
Or
|
||||
|
||||
Select Orchestrator mode → autonomously define subtasks → intelligently distribute subtasks across modes
|
||||
|
||||
#### Example: Refactoring authentication
|
||||
|
||||
**Cline:**
|
||||
|
||||
1. Enable Plan/Act mode
|
||||
2. "Refactor auth to use OAuth2"
|
||||
3. Wait for plan → Review → Approve
|
||||
4. Watch step-by-step execution
|
||||
5. Checkpoint when done
|
||||
|
||||
**Kilo:**
|
||||
|
||||
1. Select **Orchestrator** from the mode dropdown
|
||||
2. "Implement OAuth2 refactor for our authentication"
|
||||
3. Orchestrator creates subtasks for other modes automatically
|
||||
4. **Architect Mode** reviews the architecture
|
||||
5. **Code Mode** implements the OAuth2 client
|
||||
6. Session/checkpoints preserved automatically
|
||||
|
||||
#### Example: Understanding unfamiliar code
|
||||
|
||||
**Cline:** Use Plan mode (but risk accidentally switching to Act)
|
||||
|
||||
**Kilo:** Select **Ask** from the dropdown
|
||||
|
||||
- "Explain how the payment processing flow works"
|
||||
- "What external services does this integrate with?"
|
||||
- Ask mode never writes files, so exploration is always safe
|
||||
|
||||
**Why this matters:** In Cline, you might accidentally make changes while exploring. In Kilo, Ask and Architect modes can't write files, so you're safe to explore without worry. Kilo's **Orchestrator** mode lets you intelligently generate and execute subtasks that are optimized for each mode.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### VS Code / Cursor
|
||||
|
||||
1. Open Extensions (`Cmd/Ctrl + Shift + X`)
|
||||
2. Search "Kilo Code"
|
||||
3. Click Install
|
||||
4. Find the Kilo icon in your sidebar
|
||||
|
||||
### JetBrains IDEs
|
||||
|
||||
Supports IntelliJ, PyCharm, WebStorm, and all JetBrains IDEs.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- JetBrains Toolbox (required for auth)
|
||||
- Node.js LTS
|
||||
|
||||
**Install:**
|
||||
|
||||
1. Settings → Plugins → Marketplace
|
||||
2. Search "Kilo Code"
|
||||
3. Install and restart
|
||||
4. Find Kilo icon in right sidebar
|
||||
|
||||
### CLI
|
||||
|
||||
```shell
|
||||
npm install -g @kilocode/cli
|
||||
kilo
|
||||
```
|
||||
|
||||
### Web (Cloud Agents & App Builder)
|
||||
|
||||
Visit [app.kilo.ai](https://app.kilo.ai/) and sign in. This gives you access to:
|
||||
|
||||
- **Cloud Agents:** Run Kilo without a local machine
|
||||
- **App Builder:** Build and preview apps directly in your browser
|
||||
- **Kilo Deploy:** One-click deployments
|
||||
- **Code Reviews:** AI-powered PR analysis
|
||||
|
||||
---
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### Create account
|
||||
|
||||
1. Click "Try Kilo Code for Free" in the Kilo panel
|
||||
2. Sign in with OAuth at kilo.ai
|
||||
3. You'll be redirected back to your IDE
|
||||
|
||||
### Configure your provider
|
||||
|
||||
**Option 1: Use Kilo Gateway (easiest)**
|
||||
|
||||
1. Open settings (⚙️ icon)
|
||||
2. Select "Kilo Gateway" as provider
|
||||
3. Choose a model (such as Claude Opus 4.5, Gemini 3, MiniMax M2.1)
|
||||
|
||||
**Option 2: Bring your own API keys**
|
||||
|
||||
1. Select your provider (Anthropic, OpenAI, etc.)
|
||||
2. Enter your API key
|
||||
3. Choose your model
|
||||
|
||||
---
|
||||
|
||||
## Beyond the IDE: Kilo's Platform Features
|
||||
|
||||
One of the biggest differences from Cline is that Kilo isn't just an IDE extension. It's a platform with multiple interfaces that can all share your Sessions and context.
|
||||
|
||||
### Cloud Agents
|
||||
|
||||
Run Kilo from [app.kilo.ai/cloud](https://app.kilo.ai/cloud) without needing your local machine. Great for:
|
||||
|
||||
- Working from a tablet or phone
|
||||
- Offloading heavy tasks
|
||||
- Parallel execution without blocking your IDE
|
||||
|
||||
### Parallel Agents
|
||||
|
||||
Run multiple agents simultaneously without conflicts, in both the IDE and CLI. Start an agent working on tests while another handles documentation.
|
||||
|
||||
### Sessions
|
||||
|
||||
Your conversation history, context, and state sync across all interfaces automatically. Start a task in the CLI, continue in VS Code, check progress on mobile.
|
||||
|
||||
### App Builder
|
||||
|
||||
Build live apps and sites directly from the web with a real-time preview. Similar to Lovable, but integrated with your Kilo Sessions. Deploy with one click when you're ready.
|
||||
|
||||
### Kilo Deploy
|
||||
|
||||
One-click deployments from directly within Kilo. Go from code to production without leaving your workflow.
|
||||
|
||||
### Code Reviews
|
||||
|
||||
Automatically analyzes your PRs using your choice of AI model. Reviews happen the moment a PR is opened or updated, covering performance, security, style, and test coverage.
|
||||
|
||||
### Managed Indexing
|
||||
|
||||
Semantic search across your repositories using cloud-hosted embeddings. Kilo indexes your codebase to deliver more relevant, context-aware responses.
|
||||
|
||||
### Autocomplete
|
||||
|
||||
In-line ghost-text completions with tab to complete. Works alongside the agent modes for a complete coding experience.
|
||||
|
||||
---
|
||||
|
||||
## Complete Development Workflows
|
||||
|
||||
### New Feature Development
|
||||
|
||||
**Kilo approach:**
|
||||
|
||||
1. **Architect mode:** "Design a user notification system"
|
||||
2. Review architecture, discuss trade-offs
|
||||
3. **Code mode:** "Implement the notification service"
|
||||
4. Fast Apply builds it quickly
|
||||
5. **Debug mode:** "Email sends aren't working"
|
||||
6. Fix issues
|
||||
7. Session auto-saves as "Notifications-Complete"
|
||||
|
||||
### Debugging Production Issues
|
||||
|
||||
**Kilo approach:**
|
||||
|
||||
1. **Debug mode:** "Checkout fails with 'payment_intent_not_found'"
|
||||
2. Debug mode systematically checks logs, traces API calls
|
||||
3. **Code mode:** "Add idempotency key to prevent duplicates"
|
||||
4. Verify fix
|
||||
|
||||
### Large Refactoring
|
||||
|
||||
**Kilo approach:**
|
||||
|
||||
1. **Ask mode:** "Explain our current auth implementation"
|
||||
2. **Architect mode:** "Design migration to JWT tokens"
|
||||
3. Session saves as "Auth-Refactor-Plan"
|
||||
4. **Code mode:** Implement JWT generation, update middleware
|
||||
5. **Debug mode:** Fix failing tests
|
||||
|
||||
### Learning Unfamiliar Code
|
||||
|
||||
**Kilo approach:**
|
||||
|
||||
1. **Ask mode:** "Explain how payment processing works"
|
||||
2. "What happens when a payment fails?"
|
||||
3. "Show me the retry logic"
|
||||
4. Ask mode never writes, so exploration is completely safe
|
||||
5. When ready, switch to **Code mode** to make changes
|
||||
|
||||
---
|
||||
|
||||
## Feature Mapping
|
||||
|
||||
| Cline Feature | Kilo Equivalent | Notes |
|
||||
| ------------------ | ---------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Plan mode | Orchestrator, Architect, Ask modes | Architect plans, Ask explains, Orchestrate distributes tasks across other modes |
|
||||
| Act mode | Code mode | Implementation |
|
||||
| Plan/Act toggle | Mode dropdown | More granular control |
|
||||
| Checkpoints | Sessions + Checkpoints | Sessions preserve mode + context |
|
||||
| Background editing | Fast Apply | Sequential but instant |
|
||||
| Single agent | Five specialized modes | Purpose-built for each task |
|
||||
| Local only | Multi-platform | IDE, CLI, web, mobile |
|
||||
|
||||
---
|
||||
|
||||
## What You Gain
|
||||
|
||||
- **Specialized modes:** Purpose-built for different parts of development
|
||||
- **Fast Apply:** 5-10x faster code application
|
||||
- **Autocomplete:** Inline AI suggestions
|
||||
- **Multi-platform:** VS Code, JetBrains, CLI, web
|
||||
- **Session Persistance:** Sessions preserve mode + context across devices
|
||||
- **500+ models:** More provider options, switch anytime
|
||||
- **Cloud Agents:** Work without your local machine
|
||||
- **App Builder:** Build and preview apps in the browser
|
||||
- **One-click Deploy:** Ship directly from Kilo
|
||||
- **Code Reviews:** AI-powered PR analysis
|
||||
- **Parallel Agents:** Run multiple agents simultaneously
|
||||
|
||||
---
|
||||
|
||||
## Common Questions
|
||||
|
||||
**Q: Do I have to switch modes constantly?** No. Orchestrator does that automatically. Code mode handles most day-to-day work. Switch when you need specialized behavior.
|
||||
|
||||
**Q: What if I forget to switch modes?** Code mode is the default and handles most tasks. It'll still work, just might not be optimized for exploration or planning.
|
||||
|
||||
**Q: Can I customize what each mode does?** Yes. Add mode-specific instructions in settings, or create custom modes.
|
||||
|
||||
**Q: Can I use both Cline and Kilo side-by-side?** Yes. They're separate extensions.
|
||||
|
||||
**Q: What's the difference between Cloud Agents and the IDE extension?** Same capabilities, different interface. Cloud Agents run in the browser, so you can work from any device without your local machine.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Install Kilo in your primary IDE
|
||||
2. Try each mode with a small task:
|
||||
- **Code:** "Add a hello world endpoint"
|
||||
- **Ask:** "Explain what this file does"
|
||||
- **Debug:** "Why is this function returning undefined?"
|
||||
- **Architect:** "Design a logging system"
|
||||
- **Orchestrator:** "Build an app, end-to-end, that…"
|
||||
3. Try Cloud Agents at [app.kilo.ai](https://app.kilo.ai/)
|
||||
4. Install the Kilo CLI with `npm install -g @kilocode/cli`
|
||||
5. Enable Autocomplete for inline suggestions
|
||||
@@ -0,0 +1,10 @@
|
||||
import posthog from "posthog-js"
|
||||
|
||||
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
person_profiles: "identified_only",
|
||||
capture_pageview: false, // Handled manually in _app.tsx on routeChangeComplete
|
||||
capture_pageleave: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const AiProvidersNav: NavSection[] = [
|
||||
{
|
||||
title: "AI Providers",
|
||||
links: [
|
||||
{ href: "/ai-providers", children: "Overview" },
|
||||
{ href: "/ai-providers/kilocode", children: "Kilo Code (Default)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI Labs",
|
||||
links: [
|
||||
{ href: "/ai-providers/anthropic", children: "Anthropic" },
|
||||
{ href: "/ai-providers/claude-code", children: "Claude Code" },
|
||||
{ href: "/ai-providers/openai", children: "OpenAI" },
|
||||
{
|
||||
href: "/ai-providers/openai-chatgpt-plus-pro",
|
||||
children: "ChatGPT Plus/Pro",
|
||||
},
|
||||
{ href: "/ai-providers/gemini", children: "Google Gemini" },
|
||||
{ href: "/ai-providers/mistral", children: "Mistral AI" },
|
||||
{ href: "/ai-providers/deepseek", children: "DeepSeek" },
|
||||
{ href: "/ai-providers/xai", children: "xAI (Grok)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI Gateways",
|
||||
links: [
|
||||
{ href: "/ai-providers/openrouter", children: "OpenRouter" },
|
||||
{ href: "/ai-providers/glama", children: "Glama" },
|
||||
{ href: "/ai-providers/requesty", children: "Requesty" },
|
||||
{ href: "/ai-providers/unbound", children: "Unbound" },
|
||||
{
|
||||
href: "/ai-providers/vercel-ai-gateway",
|
||||
children: "Vercel AI Gateway",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cloud Providers",
|
||||
links: [
|
||||
{ href: "/ai-providers/vertex", children: "Google Vertex AI" },
|
||||
{ href: "/ai-providers/bedrock", children: "AWS Bedrock" },
|
||||
{ href: "/ai-providers/groq", children: "Groq" },
|
||||
{ href: "/ai-providers/cerebras", children: "Cerebras" },
|
||||
{ href: "/ai-providers/fireworks", children: "Fireworks AI" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Local & Self-Hosted",
|
||||
links: [
|
||||
{ href: "/ai-providers/ollama", children: "Ollama" },
|
||||
{ href: "/ai-providers/lmstudio", children: "LM Studio" },
|
||||
{ href: "/ai-providers/vscode-lm", children: "VS Code LM API" },
|
||||
{
|
||||
href: "/ai-providers/openai-compatible",
|
||||
children: "OpenAI Compatible",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Other Providers",
|
||||
links: [
|
||||
{ href: "/ai-providers/chutes-ai", children: "Chutes AI" },
|
||||
{ href: "/ai-providers/inception", children: "Inception" },
|
||||
{ href: "/ai-providers/minimax", children: "MiniMax" },
|
||||
{ href: "/ai-providers/moonshot", children: "Moonshot" },
|
||||
{ href: "/ai-providers/ovhcloud", children: "OVHcloud" },
|
||||
{ href: "/ai-providers/sap-ai-core", children: "SAP AI Core" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Special Modes",
|
||||
links: [
|
||||
{ href: "/ai-providers/v0", children: "v0" },
|
||||
{ href: "/ai-providers/human-relay", children: "Human Relay" },
|
||||
{ href: "/ai-providers/synthetic", children: "Synthetic Provider" },
|
||||
{
|
||||
href: "/ai-providers/virtual-quota-fallback",
|
||||
children: "Virtual Quota Fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const AutomateNav: NavSection[] = [
|
||||
{
|
||||
title: "Agents",
|
||||
links: [
|
||||
{ href: "/automate", children: "Overview" },
|
||||
{ href: "/automate/integrations", children: "Integrations" },
|
||||
{
|
||||
href: "/automate/code-reviews/overview",
|
||||
children: "Code Reviews",
|
||||
subLinks: [
|
||||
{ href: "/automate/code-reviews/overview", children: "Overview" },
|
||||
{ href: "/automate/code-reviews/github", children: "GitHub" },
|
||||
{ href: "/automate/code-reviews/gitlab", children: "GitLab" },
|
||||
],
|
||||
},
|
||||
{ href: "/automate/agent-manager", children: "Agent Manager" },
|
||||
{ href: "/automate/kiloclaw", children: "KiloClaw" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Extending Kilo",
|
||||
links: [
|
||||
{ href: "/automate/extending/local-models", children: "Local Models" },
|
||||
{
|
||||
href: "/automate/extending/shell-integration",
|
||||
children: "Shell Integration",
|
||||
},
|
||||
{
|
||||
href: "/automate/extending/auto-launch",
|
||||
children: "Auto-launch Configuration",
|
||||
},
|
||||
{
|
||||
href: "/automate/mcp/overview",
|
||||
children: "MCP",
|
||||
subLinks: [
|
||||
{ href: "/automate/mcp/overview", children: "MCP Overview" },
|
||||
{
|
||||
href: "/automate/mcp/using-in-kilo-code",
|
||||
children: "Using MCP in Kilo Code",
|
||||
},
|
||||
{ href: "/automate/mcp/using-in-cli", children: "Using MCP in CLI" },
|
||||
{ href: "/automate/mcp/what-is-mcp", children: "What is MCP" },
|
||||
{
|
||||
href: "/automate/mcp/server-transports",
|
||||
children: "Server Transports",
|
||||
},
|
||||
{ href: "/automate/mcp/mcp-vs-api", children: "MCP vs API" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tools",
|
||||
links: [
|
||||
{ href: "/automate/how-tools-work", children: "How Tools Work" },
|
||||
{ href: "/automate/tools", children: "Tools Details" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const CodeWithAiNav: NavSection[] = [
|
||||
{
|
||||
title: "Platforms",
|
||||
links: [
|
||||
{ href: "/code-with-ai", children: "Overview" },
|
||||
{ href: "/code-with-ai/platforms/vscode", children: "VS Code Extension" },
|
||||
{
|
||||
href: "/code-with-ai/platforms/jetbrains",
|
||||
children: "JetBrains Extension",
|
||||
},
|
||||
{ href: "/code-with-ai/platforms/cli", children: "CLI" },
|
||||
{ href: "/code-with-ai/platforms/cloud-agent", children: "Cloud Agent" },
|
||||
{ href: "/code-with-ai/platforms/mobile", children: "Mobile Apps" },
|
||||
{ href: "/code-with-ai/platforms/slack", children: "Slack" },
|
||||
{ href: "/code-with-ai/app-builder", children: "App Builder" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Chat & Context",
|
||||
links: [
|
||||
{
|
||||
href: "/code-with-ai/agents/chat-interface",
|
||||
children: "Chat Interface",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/context-mentions",
|
||||
children: "Context & Mentions",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/model-selection",
|
||||
children: "Model Selection",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/auto-model",
|
||||
children: "Auto Model",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/free-and-budget-models",
|
||||
children: "Free & Budget Models",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/agents/using-modes",
|
||||
children: "Modes",
|
||||
subLinks: [
|
||||
{ href: "/code-with-ai/agents/using-modes", children: "Using Modes" },
|
||||
{
|
||||
href: "/code-with-ai/agents/orchestrator-mode",
|
||||
children: "Orchestrator Mode",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Productivity Tools",
|
||||
links: [
|
||||
{
|
||||
href: "/code-with-ai/features/autocomplete",
|
||||
children: "Autocomplete",
|
||||
subLinks: [{ href: "/code-with-ai/features/autocomplete/mistral-setup", children: "Mistral Setup" }],
|
||||
},
|
||||
{ href: "/code-with-ai/features/code-actions", children: "Code Actions" },
|
||||
{
|
||||
href: "/code-with-ai/features/enhance-prompt",
|
||||
children: "Enhance Prompt",
|
||||
},
|
||||
{
|
||||
href: "/code-with-ai/features/git-commit-generation",
|
||||
children: "Git Commit Generation",
|
||||
},
|
||||
{ href: "/code-with-ai/features/speech-to-text", children: "Voice Transcription" },
|
||||
{
|
||||
href: "/code-with-ai/features/browser-use",
|
||||
children: "Agent Behavior",
|
||||
subLinks: [
|
||||
{ href: "/code-with-ai/features/browser-use", children: "Browser Use" },
|
||||
{ href: "/code-with-ai/features/fast-edits", children: "Fast Edits" },
|
||||
{
|
||||
href: "/code-with-ai/features/task-todo-list",
|
||||
children: "Task Todo List",
|
||||
},
|
||||
{ href: "/code-with-ai/features/checkpoints", children: "Checkpoints" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const CollaborateNav: NavSection[] = [
|
||||
{
|
||||
title: "Sharing",
|
||||
links: [
|
||||
{ href: "/collaborate", children: "Overview" },
|
||||
{ href: "/collaborate/sessions-sharing", children: "Sessions & Sharing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Kilo for Teams",
|
||||
links: [
|
||||
{ href: "/collaborate/teams/about-plans", children: "About Plans" },
|
||||
{
|
||||
href: "/collaborate/teams/getting-started",
|
||||
children: "Getting Started with Teams",
|
||||
},
|
||||
{ href: "/collaborate/teams/dashboard", children: "Dashboard" },
|
||||
{
|
||||
href: "/collaborate/teams/team-management",
|
||||
children: "Team Management",
|
||||
},
|
||||
{
|
||||
href: "/collaborate/teams/custom-modes-org",
|
||||
children: "Custom Modes (Org)",
|
||||
},
|
||||
{ href: "/collaborate/teams/billing", children: "Billing" },
|
||||
{ href: "/collaborate/teams/analytics", children: "Analytics" },
|
||||
{
|
||||
href: "/collaborate/adoption-dashboard/overview",
|
||||
children: "AI Adoption Dashboard",
|
||||
subLinks: [
|
||||
{
|
||||
href: "/collaborate/adoption-dashboard/overview",
|
||||
children: "Overview",
|
||||
},
|
||||
{
|
||||
href: "/collaborate/adoption-dashboard/understanding-your-score",
|
||||
children: "Understanding Your Score",
|
||||
},
|
||||
{
|
||||
href: "/collaborate/adoption-dashboard/improving-your-score",
|
||||
children: "Improving Your Score",
|
||||
},
|
||||
{
|
||||
href: "/collaborate/adoption-dashboard/for-team-leads",
|
||||
children: "For Team Leads",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Enterprise",
|
||||
links: [
|
||||
{ href: "/collaborate/enterprise/sso", children: "SSO" },
|
||||
{
|
||||
href: "/collaborate/enterprise/model-access-controls",
|
||||
children: "Model Access Controls",
|
||||
},
|
||||
{ href: "/collaborate/enterprise/audit-logs", children: "Audit Logs" },
|
||||
{ href: "/collaborate/enterprise/migration", children: "Migration" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const ContributingNav: NavSection[] = [
|
||||
{
|
||||
title: "Getting Started",
|
||||
links: [
|
||||
{ href: "/contributing", children: "Contributing Overview" },
|
||||
{
|
||||
href: "/contributing/development-environment",
|
||||
children: "Development Environment",
|
||||
},
|
||||
{
|
||||
href: "/contributing/ecosystem",
|
||||
children: "Ecosystem",
|
||||
},
|
||||
{
|
||||
href: "/contributing/cline-to-kilo-migration",
|
||||
children: "Cline to Kilo Migration",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Architecture",
|
||||
links: [
|
||||
{
|
||||
href: "/contributing/architecture",
|
||||
children: "Architecture Overview",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/features",
|
||||
children: "Features",
|
||||
subLinks: [
|
||||
{
|
||||
href: "/contributing/architecture/agent-observability",
|
||||
children: "Agent Observability",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/annual-billing",
|
||||
children: "Annual Billing",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/benchmarking",
|
||||
children: "Benchmarking",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/enterprise-mcp-controls",
|
||||
children: "Enterprise MCP Controls",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/mcp-oauth-authorization",
|
||||
children: "MCP OAuth Authorization",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/model-provider-blocklist",
|
||||
children: "Model/Provider Blocklist",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/onboarding-improvements",
|
||||
children: "Onboarding Improvements",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/organization-modes-library",
|
||||
children: "Organization Modes Library",
|
||||
},
|
||||
{
|
||||
href: "/deploy-secure/security-reviews",
|
||||
children: "Agentic Security Reviews",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/track-repo-url",
|
||||
children: "Track Repo URL",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/vercel-ai-gateway",
|
||||
children: "Vercel AI Gateway",
|
||||
},
|
||||
{
|
||||
href: "/contributing/architecture/voice-transcription",
|
||||
children: "Voice Transcription",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const CustomizeNav: NavSection[] = [
|
||||
{
|
||||
title: "Customization",
|
||||
links: [
|
||||
{ href: "/customize", children: "Overview" },
|
||||
{
|
||||
href: "/customize/custom-modes",
|
||||
children: "Custom Modes",
|
||||
},
|
||||
{
|
||||
href: "/customize/custom-rules",
|
||||
children: "Custom Rules",
|
||||
},
|
||||
{
|
||||
href: "/customize/custom-instructions",
|
||||
children: "Custom Instructions",
|
||||
},
|
||||
{ href: "/customize/agents-md", children: "agents.md" },
|
||||
{ href: "/customize/workflows", children: "Workflows" },
|
||||
{ href: "/customize/skills", children: "Skills" },
|
||||
{
|
||||
href: "/customize/prompt-engineering",
|
||||
children: "Prompt Engineering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Context & Indexing",
|
||||
links: [
|
||||
{
|
||||
href: "/customize/context/codebase-indexing",
|
||||
children: "Codebase Indexing",
|
||||
},
|
||||
{
|
||||
href: "/customize/context/context-condensing",
|
||||
children: "Context Condensing",
|
||||
},
|
||||
{
|
||||
href: "/customize/context/kilocodeignore",
|
||||
children: ".kilocodeignore",
|
||||
},
|
||||
{ href: "/customize/context/memory-bank", children: "Memory Bank" },
|
||||
{
|
||||
href: "/customize/context/large-projects",
|
||||
children: "Large Projects",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const DeploySecureNav: NavSection[] = [
|
||||
{
|
||||
title: "Deployment",
|
||||
links: [
|
||||
{ href: "/deploy-secure", children: "Overview" },
|
||||
{ href: "/deploy-secure/deploy", children: "Deploy" },
|
||||
{ href: "/deploy-secure/managed-indexing", children: "Managed Indexing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Security",
|
||||
links: [{ href: "/deploy-secure/security-reviews", children: "Security Reviews" }],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const GatewayNav: NavSection[] = [
|
||||
{
|
||||
title: "Introduction",
|
||||
links: [
|
||||
{ href: "/gateway", children: "Overview" },
|
||||
{ href: "/gateway/quickstart", children: "Quickstart" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
links: [
|
||||
{ href: "/gateway/authentication", children: "Authentication" },
|
||||
{ href: "/gateway/models-and-providers", children: "Models & Providers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Features",
|
||||
links: [
|
||||
{ href: "/gateway/streaming", children: "Streaming" },
|
||||
{ href: "/gateway/usage-and-billing", children: "Usage & Billing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Reference",
|
||||
links: [
|
||||
{ href: "/gateway/api-reference", children: "API Reference" },
|
||||
{ href: "/gateway/sdks-and-frameworks", children: "SDKs & Frameworks" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const GettingStartedNav: NavSection[] = [
|
||||
{
|
||||
title: "Introduction",
|
||||
links: [
|
||||
{ href: "/getting-started", children: "Overview" },
|
||||
{ href: "/getting-started/installing", children: "Installation" },
|
||||
{ href: "/getting-started/quickstart", children: "Quickstart" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
links: [
|
||||
{
|
||||
href: "/getting-started/setup-authentication",
|
||||
children: "Setup & Authentication",
|
||||
},
|
||||
{
|
||||
href: "/getting-started/using-kilo-for-free",
|
||||
children: "Using Kilo for Free",
|
||||
},
|
||||
{
|
||||
href: "/getting-started/byok",
|
||||
children: "Bring Your Own Key (BYOK)",
|
||||
},
|
||||
{ href: "/ai-providers", children: "AI Providers" },
|
||||
{
|
||||
href: "/getting-started/settings",
|
||||
children: "Settings",
|
||||
subLinks: [
|
||||
{ href: "/getting-started/settings/auto-approving-actions", children: "Auto-Approving Actions" },
|
||||
{ href: "/getting-started/settings/auto-cleanup", children: "Auto Cleanup" },
|
||||
{ href: "/getting-started/settings/system-notifications", children: "System Notifications" },
|
||||
],
|
||||
},
|
||||
{ href: "/getting-started/adding-credits", children: "Adding Credits" },
|
||||
{ href: "/getting-started/rate-limits-and-costs", children: "Rate Limits and Costs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Help",
|
||||
links: [
|
||||
{
|
||||
href: "/getting-started/faq",
|
||||
children: "FAQ",
|
||||
subLinks: [
|
||||
{ href: "/getting-started/faq/general", children: "General" },
|
||||
{ href: "/getting-started/faq/setup-and-installation", children: "Setup and Installation" },
|
||||
{ href: "/getting-started/faq/credits-and-billing", children: "Credits and Billing" },
|
||||
{ href: "/getting-started/faq/account-and-integration", children: "Account and Integration" },
|
||||
{ href: "/getting-started/faq/known-issues", children: "Known Issues" },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/getting-started/migrating",
|
||||
children: "Migrating from Cursor",
|
||||
},
|
||||
{
|
||||
href: "/getting-started/troubleshooting",
|
||||
children: "Troubleshooting",
|
||||
subLinks: [
|
||||
{
|
||||
href: "/getting-started/troubleshooting/troubleshooting-extension",
|
||||
children: "Extension Troubleshooting",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/getting-started/using-docs-with-agents",
|
||||
children: "Using Docs with Agents",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AiProvidersNav } from "./ai-providers"
|
||||
import { AutomateNav } from "./automate"
|
||||
import { CodeWithAiNav } from "./code-with-ai"
|
||||
import { CollaborateNav } from "./collaborate"
|
||||
import { ContributingNav } from "./contributing"
|
||||
import { CustomizeNav } from "./customize"
|
||||
import { DeploySecureNav } from "./deploy-secure"
|
||||
import { GatewayNav } from "./gateway"
|
||||
import { GettingStartedNav } from "./getting-started"
|
||||
import { ToolsNav } from "./tools"
|
||||
|
||||
export const Nav = {
|
||||
GettingStartedNav,
|
||||
CodeWithAiNav,
|
||||
CustomizeNav,
|
||||
CollaborateNav,
|
||||
AutomateNav,
|
||||
DeploySecureNav,
|
||||
ContributingNav,
|
||||
AiProvidersNav,
|
||||
GatewayNav,
|
||||
ToolsNav,
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NavSection } from "../types"
|
||||
|
||||
export const ToolsNav: NavSection[] = [
|
||||
{
|
||||
title: "Tools",
|
||||
links: [{ href: "/automate/tools", children: "Overview" }],
|
||||
},
|
||||
{
|
||||
title: "Read Tools",
|
||||
links: [
|
||||
{ href: "/automate/tools/read-file", children: "read_file" },
|
||||
{ href: "/automate/tools/search-files", children: "search_files" },
|
||||
{ href: "/automate/tools/list-files", children: "list_files" },
|
||||
{ href: "/automate/tools/list-code-definition-names", children: "list_code_definition_names" },
|
||||
{ href: "/automate/tools/codebase-search", children: "codebase_search" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Edit Tools",
|
||||
links: [
|
||||
{ href: "/automate/tools/apply-diff", children: "apply_diff" },
|
||||
{ href: "/automate/tools/delete-file", children: "delete_file" },
|
||||
{ href: "/automate/tools/write-to-file", children: "write_to_file" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Browser Tools",
|
||||
links: [{ href: "/automate/tools/browser-action", children: "browser_action" }],
|
||||
},
|
||||
{
|
||||
title: "Command Tools",
|
||||
links: [{ href: "/automate/tools/execute-command", children: "execute_command" }],
|
||||
},
|
||||
{
|
||||
title: "MCP Tools",
|
||||
links: [
|
||||
{ href: "/automate/tools/use-mcp-tool", children: "use_mcp_tool" },
|
||||
{ href: "/automate/tools/access-mcp-resource", children: "access_mcp_resource" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Workflow Tools",
|
||||
links: [
|
||||
{ href: "/automate/tools/switch-mode", children: "switch_mode" },
|
||||
{ href: "/automate/tools/new-task", children: "new_task" },
|
||||
{ href: "/automate/tools/ask-followup-question", children: "ask_followup_question" },
|
||||
{ href: "/automate/tools/attempt-completion", children: "attempt_completion" },
|
||||
{ href: "/automate/tools/update-todo-list", children: "update_todo_list" },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface NavLink {
|
||||
href: string
|
||||
children: string
|
||||
subLinks?: NavLink[] // Optional nested links for second-level navigation
|
||||
}
|
||||
|
||||
export interface NavSection {
|
||||
title: string
|
||||
links: NavLink[]
|
||||
}
|
||||
|
||||
export interface SectionNav {
|
||||
[key: string]: NavSection[]
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
## Mapping Existing Pages to New Structure
|
||||
|
||||
### Get Started
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||
| Introduction / Overview | `index`, `getting-started/concepts` |
|
||||
| Installation | `getting-started/installing` |
|
||||
| Quickstart | `getting-started/your-first-task` |
|
||||
| Setup & Authentication | `getting-started/setting-up`, `getting-started/connecting-api-provider` |
|
||||
| AI Providers | `basic-usage/connecting-providers`, `providers/*` (all of them) |
|
||||
| Settings | `basic-usage/settings-management` |
|
||||
| Adding Credits | `basic-usage/adding-credits` |
|
||||
| FAQ | Keep if it exists |
|
||||
| Migrating from Cursor/Windsurf | `advanced-usage/migrating-from-cursor-windsurf` |
|
||||
|
||||
---
|
||||
|
||||
### Code with AI
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **Platforms** (subheader) | |
|
||||
| VS Code Extension | Needs new page (or pull from install) |
|
||||
| JetBrains Extension | Needs new page |
|
||||
| CLI | `cli` |
|
||||
| Cloud Agent | `advanced-usage/cloud-agent` (partial) |
|
||||
| Mobile Apps | Needs new page |
|
||||
| Slack | `slack` |
|
||||
| **Working with Agents** (subheader) | |
|
||||
| The Chat Interface | `basic-usage/the-chat-interface` |
|
||||
| Context & Mentions | `basic-usage/context-mentions` |
|
||||
| Using Modes | `basic-usage/using-modes` |
|
||||
| Orchestrator Mode | `basic-usage/orchestrator-mode` |
|
||||
| Model Selection | `basic-usage/model-selection-guide` |
|
||||
| Free & Budget Models | `advanced-usage/free-and-budget-models` |
|
||||
| **Features** (subheader) | |
|
||||
| Autocomplete | `basic-usage/autocomplete/index`, `basic-usage/autocomplete/mistral-setup` |
|
||||
| Code Actions | `features/code-actions` |
|
||||
| Browser Use | `features/browser-use` |
|
||||
| Git Commit Generation | `basic-usage/git-commit-generation` |
|
||||
| Checkpoints | `features/checkpoints` |
|
||||
| Enhance Prompt | `features/enhance-prompt` |
|
||||
| Fast Edits | `features/fast-edits` |
|
||||
| Task Todo List | `basic-usage/task-todo-list` |
|
||||
| **Context & Indexing** (subheader) | |
|
||||
| Codebase Indexing | `features/codebase-indexing` |
|
||||
| Large Projects | `advanced-usage/large-projects` |
|
||||
| **Customization** (subheader) | |
|
||||
| Custom Modes | `agent-behavior/custom-modes` |
|
||||
| Custom Rules | `agent-behavior/custom-rules` |
|
||||
| Custom Instructions | `agent-behavior/custom-instructions` |
|
||||
| agents.md | `agent-behavior/agents-md` |
|
||||
| Workflows | `agent-behavior/workflows` |
|
||||
| Skills | `agent-behavior/skills` |
|
||||
| Prompt Engineering | `agent-behavior/prompt-engineering` |
|
||||
| **App Builder** (subheader) | |
|
||||
| App Builder | `advanced-usage/appbuilder` |
|
||||
|
||||
---
|
||||
|
||||
### Collaborate
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ------------------------------------- | --------------------------------------------------- |
|
||||
| Sessions & Sharing | `advanced-usage/sessions` |
|
||||
| **Kilo for Teams** (subheader) | |
|
||||
| About Plans | `plans/about` |
|
||||
| Getting Started with Teams | `plans/getting-started` |
|
||||
| Dashboard | `plans/dashboard` |
|
||||
| Team Management | `plans/team-management` |
|
||||
| Custom Modes (Org) | `plans/custom-modes` |
|
||||
| Billing | `plans/billing` |
|
||||
| Analytics | `plans/analytics` |
|
||||
| **AI Adoption Dashboard** (subheader) | |
|
||||
| Overview | `plans/adoption-dashboard/overview` |
|
||||
| Understanding Your Score | `plans/adoption-dashboard/understanding-your-score` |
|
||||
| Improving Your Score | `plans/adoption-dashboard/improving-your-score` |
|
||||
| For Team Leads | `plans/adoption-dashboard/for-team-leads` |
|
||||
| **Enterprise** (subheader) | |
|
||||
| SSO | `plans/enterprise/SSO` |
|
||||
| Model Access Controls | `plans/enterprise/model-access` |
|
||||
| Audit Logs | `plans/enterprise/audit-logs` |
|
||||
| Migration | `plans/migration` |
|
||||
|
||||
---
|
||||
|
||||
### Automate
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ------------------------------ | ------------------------------------- |
|
||||
| Integrations Overview | `advanced-usage/integrations` |
|
||||
| Code Reviews | `advanced-usage/code-reviews` |
|
||||
| Agent Manager | `advanced-usage/agent-manager` |
|
||||
| **Extending Kilo** (subheader) | |
|
||||
| Local Models | `advanced-usage/local-models` |
|
||||
| Shell Integration | `features/shell-integration` |
|
||||
| Auto-launch Configuration | `features/auto-launch-configuration` |
|
||||
| **MCP** (subheader) | |
|
||||
| MCP Overview | `features/mcp/overview` |
|
||||
| Using MCP in Kilo Code | `features/mcp/using-mcp-in-kilo-code` |
|
||||
| Using MCP in CLI | `features/mcp/using-mcp-in-cli` |
|
||||
| What is MCP | `features/mcp/what-is-mcp` |
|
||||
| Server Transports | `features/mcp/server-transports` |
|
||||
| MCP vs API | `features/mcp/mcp-vs-api` |
|
||||
| **Tools (subheader)** | |
|
||||
| How Tools Work | `basic-usage/how-tools-work` |
|
||||
| Tool Details | ALL of Tool reference |
|
||||
|
||||
---
|
||||
|
||||
### Deploy & Secure
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ---------------- | ----------------------------------------------------------------------- |
|
||||
| Deploy | `advanced-usage/deploy` |
|
||||
| Managed Indexing | `advanced-usage/managed-indexing` |
|
||||
| Security Reviews | `contributing/architecture/security-reviews` (move out of contributing) |
|
||||
|
||||
---
|
||||
|
||||
### Contributing
|
||||
|
||||
| New Item | Existing Page(s) |
|
||||
| ---------------------------- | -------------------------------------------------------------- |
|
||||
| Contributing Overview | `contributing/index` |
|
||||
| Development Environment | `contributing/development-environment` |
|
||||
| **Architecture** (subheader) | |
|
||||
| Architecture Overview | `contributing/architecture/index` |
|
||||
| Annual Billing | `contributing/architecture/annual-billing` |
|
||||
| Enterprise MCP Controls | `contributing/architecture/enterprise-mcp-controls` |
|
||||
| Onboarding Improvements | `contributing/architecture/onboarding-engagement-improvements` |
|
||||
| Organization Modes Library | `contributing/architecture/organization-modes-library` |
|
||||
| Track Repo URL | `contributing/architecture/track-repo-url` |
|
||||
| Vercel AI Gateway | `contributing/architecture/vercel-ai-gateway` |
|
||||
| Voice Transcription | `contributing/architecture/voice-transcription` |
|
||||
|
||||
---
|
||||
|
||||
## Pages to Add (Don't Exist Yet)
|
||||
|
||||
| Section | New Page Needed |
|
||||
| --------------- | --------------------------------------- |
|
||||
| Get Started | "Choosing Your Surface" (decision tree) |
|
||||
| Code with AI | VS Code dedicated page |
|
||||
| Code with AI | JetBrains dedicated page |
|
||||
| Code with AI | Web App dedicated page |
|
||||
| Code with AI | Mobile Apps (iOS/Android) |
|
||||
| Collaborate | Team Workspaces overview |
|
||||
| Collaborate | Permissions & Roles |
|
||||
| Automate | Triage Agent |
|
||||
| Automate | Auto-fix Agent |
|
||||
| Automate | GitHub Actions guide |
|
||||
| Automate | Webhooks & Triggers |
|
||||
| Deploy & Secure | Security Scanning Agent |
|
||||
| Deploy & Secure | Environment Configuration |
|
||||
| Deploy & Secure | Secrets Management |
|
||||
| Contributing | Roadmap |
|
||||
| Contributing | Community / Discord |
|
||||
|
||||
---
|
||||
|
||||
## Pages to Remove from Nav / Condense
|
||||
|
||||
| Page | Recommendation |
|
||||
| ------------------------------------- | -------------------------------------------------------- |
|
||||
| ☑️ `features/system-notifications` | Fold into Settings or remove |
|
||||
| ❎ `features/more-features` | Remove |
|
||||
| ☑️ `features/suggested-responses` | Fold into Chat Interface |
|
||||
| ☑️ `features/auto-approving-actions` | Fold into Settings |
|
||||
| ☑️ `advanced-usage/auto-cleanup` | Fold into Settings |
|
||||
| ❎ `features/model-temperature` | Remove |
|
||||
| ☑️ `advanced-usage/rate-limits-costs` | Fold into Adding Credits or AI Providers |
|
||||
| ❎ `features/footgun-prompting` | Remove |
|
||||
| ☑️ `tips-and-tricks` | Could become a blog post or fold relevant bits elsewhere |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Reusable as-is:** ~70% of your content maps cleanly
|
||||
|
||||
**Needs new pages:** ~12-15 new pages, mostly for newer cloud/platform features
|
||||
|
||||
**Condense/remove:** ~10-12 pages that are either too granular or should be folded into parent pages
|
||||
|
||||
**Tools Reference:** Decide if this stays in nav or becomes a "Reference" footer link - it's useful but clutters the IA
|
||||
|
||||
Want me to mock up the actual sidebar structure with the subheaders?
|
||||
@@ -0,0 +1,16 @@
|
||||
/* Use this file to export your Markdoc functions */
|
||||
|
||||
export const includes = {
|
||||
transform(parameters) {
|
||||
const [array, value] = Object.values(parameters)
|
||||
|
||||
return Array.isArray(array) ? array.includes(value) : false
|
||||
},
|
||||
}
|
||||
|
||||
export const upper = {
|
||||
transform(parameters) {
|
||||
const string = parameters[0]
|
||||
return typeof string === "string" ? string.toUpperCase() : string
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { nodes } from "@markdoc/markdoc"
|
||||
import { CodeBlock } from "../../components"
|
||||
|
||||
export const fence = {
|
||||
render: CodeBlock,
|
||||
attributes: nodes.fence.attributes,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Tag } from "@markdoc/markdoc"
|
||||
|
||||
import { Heading } from "../../components"
|
||||
|
||||
function generateID(children, attributes) {
|
||||
if (attributes.id && typeof attributes.id === "string") {
|
||||
return attributes.id
|
||||
}
|
||||
return children
|
||||
.filter((child) => typeof child === "string")
|
||||
.join(" ")
|
||||
.replace(/[?]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
export const heading = {
|
||||
render: Heading,
|
||||
children: ["inline"],
|
||||
attributes: {
|
||||
id: { type: String },
|
||||
level: { type: Number, required: true, default: 1 },
|
||||
className: { type: String },
|
||||
},
|
||||
transform(node, config) {
|
||||
const attributes = node.transformAttributes(config)
|
||||
const children = node.transformChildren(config)
|
||||
const id = generateID(children, attributes)
|
||||
|
||||
return new Tag(this.render, { ...attributes, id }, children)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/* Use this file to export your markdoc nodes */
|
||||
export * from "./fence.markdoc"
|
||||
export * from "./heading.markdoc"
|
||||
export * from "./table.markdoc"
|
||||
@@ -0,0 +1,41 @@
|
||||
import { nodes } from "@markdoc/markdoc"
|
||||
import { Table, THead, TBody, Tr, Th, Td } from "../../components/Table"
|
||||
|
||||
export const table = {
|
||||
...nodes.table,
|
||||
render: Table,
|
||||
}
|
||||
|
||||
export const thead = {
|
||||
...nodes.thead,
|
||||
render: THead,
|
||||
}
|
||||
|
||||
export const tbody = {
|
||||
...nodes.tbody,
|
||||
render: TBody,
|
||||
}
|
||||
|
||||
export const tr = {
|
||||
...nodes.tr,
|
||||
render: Tr,
|
||||
}
|
||||
|
||||
export const th = {
|
||||
...nodes.th,
|
||||
render: Th,
|
||||
attributes: {
|
||||
...nodes.th.attributes,
|
||||
width: { type: String },
|
||||
},
|
||||
}
|
||||
|
||||
export const td = {
|
||||
...nodes.td,
|
||||
render: Td,
|
||||
attributes: {
|
||||
...nodes.td.attributes,
|
||||
colspan: { type: Number },
|
||||
rowspan: { type: Number },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
Use Kilo Code directly from your terminal for maximum flexibility.
|
||||
|
||||
### Install via npm
|
||||
|
||||
```bash
|
||||
npm install -g @kilocode/cli
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
kilo --version
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
Kilo Code supports all JetBrains IDEs including IntelliJ IDEA, WebStorm, PyCharm, and more.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before installing the Kilo Code plugin, ensure you have:
|
||||
|
||||
1. **JetBrains Toolbox (Recommended):**
|
||||
- Download from [https://www.jetbrains.com/toolbox-app/](https://www.jetbrains.com/toolbox-app/)
|
||||
- Toolbox is required for authentication callbacks to work properly
|
||||
- Without Toolbox, you'll need to manually configure API keys
|
||||
|
||||
2. **Node.js:**
|
||||
- Download LTS version from [https://nodejs.org/](https://nodejs.org/)
|
||||
- Required for the extension's backend services
|
||||
|
||||
### Install directly
|
||||
|
||||
1. If you don't have a JetBrains IDE installed, download one from [jetbrains.com](https://www.jetbrains.com/)
|
||||
2. Then, you can click the button below to install Kilo Code directly from the JetBrains Marketplace:
|
||||
|
||||
[](https://plugins.jetbrains.com/plugin/28350-kilo-code)
|
||||
|
||||
### Install from JetBrains Marketplace
|
||||
|
||||
1. Open your JetBrains IDE
|
||||
2. Go to **Settings/Preferences → Plugins**
|
||||
3. Click **Marketplace** tab
|
||||
4. Search for "Kilo Code"
|
||||
5. Click **Install** and restart your IDE
|
||||
|
||||
### Supported IDEs
|
||||
|
||||
- IntelliJ IDEA
|
||||
- WebStorm
|
||||
- PyCharm
|
||||
- PhpStorm
|
||||
- GoLand
|
||||
- Rider
|
||||
- CLion
|
||||
- RubyMine
|
||||
- DataGrip
|
||||
|
||||
{% callout type="info" %}
|
||||
Both Community and Ultimate editions are supported. Some AI features may vary based on your JetBrains license.
|
||||
{% /callout %}
|
||||
@@ -0,0 +1,41 @@
|
||||
## Other IDEs & Editors
|
||||
|
||||
Kilo Code supports additional VS Code-based editors. Since these editors share VS Code's extension architecture, installation follows a similar process.
|
||||
|
||||
### Cursor
|
||||
|
||||
1. Open Cursor
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click **Install**
|
||||
|
||||
### VS Code Insiders
|
||||
|
||||
[VS Code Insiders](https://code.visualstudio.com/insiders/) is the pre-release version of VS Code with the latest features.
|
||||
|
||||
1. Open VS Code Insiders
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click **Install**
|
||||
|
||||
{% callout type="note" %}
|
||||
Extensions installed in VS Code Insiders are separate from your regular VS Code installation.
|
||||
{% /callout %}
|
||||
|
||||
### Windsurf
|
||||
|
||||
1. Open Windsurf
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click **Install**
|
||||
|
||||
### Trae AI
|
||||
|
||||
1. Open Trae AI
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click **Install**
|
||||
|
||||
{% callout type="info" %}
|
||||
All VS Code-based editors support [VSIX installation](#via-vsix) if the extension isn't available directly in their marketplace.
|
||||
{% /callout %}
|
||||
@@ -0,0 +1,21 @@
|
||||
Bring Kilo Code directly into your team's Slack workspace.
|
||||
|
||||
### Install the Slack App
|
||||
|
||||
1. Click the button below to install Kilo for Slack
|
||||
1. Select your workspace
|
||||
1. Authorize the required permissions
|
||||
|
||||
[](https://app.kilo.ai/get-started/slack)
|
||||
|
||||
{% callout type="info" %}
|
||||
You'll need app installation permissions to install the Slack app in your workspace.
|
||||
{% /callout %}
|
||||
|
||||
### Usage
|
||||
|
||||
Once installed, mention the bot in a channel or send a direct message:
|
||||
|
||||
```
|
||||
@Kilo Can you update the documentation for the new API endpoint in the backend repository?
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
### Install directly
|
||||
|
||||
1. If you don't have VS Code installed, download it from [code.visualstudio.com](https://code.visualstudio.com/)
|
||||
2. Then, you can click the button below to install Kilo Code directly from the VS Code Marketplace:
|
||||
|
||||
[](vscode:extension/kilocode.kilo-code)
|
||||
|
||||
### Install from VS Code Marketplace
|
||||
|
||||
1. Open VS Code
|
||||
2. Press `Ctrl+Shift+X` (Windows/Linux) or `Cmd+Shift+X` (macOS) to open Extensions
|
||||
3. Search for "Kilo Code"
|
||||
4. Click **Install**
|
||||
|
||||
### Install via Command Line
|
||||
|
||||
```bash
|
||||
code --install-extension kilocode.kilo-code
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
After installation, you should see the Kilo Code icon ({% kiloCodeIcon /%}) in the Activity Bar on the left side of VS Code. Click it to open the Kilo Code panel.
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Callout } from "../../components"
|
||||
|
||||
export const callout = {
|
||||
render: Callout,
|
||||
children: ["paragraph", "tag", "list"],
|
||||
attributes: {
|
||||
title: {
|
||||
type: String,
|
||||
description: "Optional custom title for the callout",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "note",
|
||||
matches: ["generic", "note", "tip", "info", "warning", "danger"],
|
||||
description: "The type of callout: generic (no icon/title), note, tip, info, warning, or danger",
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
description: "When true, the callout starts collapsed and can be expanded by clicking the header",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Codicon } from "../../components"
|
||||
|
||||
export const codicon = {
|
||||
render: Codicon,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "The name of the VS Code codicon (e.g., 'send', 'check', 'warning')",
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "1em",
|
||||
description: "The size of the icon (CSS font-size value)",
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: "",
|
||||
description: "Additional CSS classes to apply to the icon",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Icon } from "../../components"
|
||||
|
||||
export const icon = {
|
||||
render: Icon,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
srcDark: {
|
||||
type: String,
|
||||
description: "Optional image source for dark mode. Falls back to src if not provided.",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "1.2em",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Image } from "../../components"
|
||||
|
||||
export const image = {
|
||||
render: Image,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "The image source URL",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "Alternative text for the image",
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
description: "Width of the image (e.g., '500px', '80%')",
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
description: "Height of the image (e.g., '300px', 'auto')",
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
description: "Optional caption displayed below the image",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Use this file to export your markdoc tags */
|
||||
export * from "./callout.markdoc"
|
||||
export * from "./codicon.markdoc"
|
||||
export * from "./tabs.markdoc"
|
||||
export * from "./icon.markdoc"
|
||||
export * from "./image.markdoc"
|
||||
export * from "./kilo-code-icon.markdoc"
|
||||
export * from "./youtube.markdoc"
|
||||
@@ -0,0 +1,13 @@
|
||||
import { KiloCodeIcon } from "../../components"
|
||||
|
||||
export const kiloCodeIcon = {
|
||||
render: KiloCodeIcon,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
size: {
|
||||
type: String,
|
||||
default: "1.2em",
|
||||
description: "Size of the icon (CSS height value)",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Tabs, Tab } from "../../components"
|
||||
|
||||
export const tabs = {
|
||||
render: Tabs,
|
||||
children: ["tab"],
|
||||
attributes: {},
|
||||
}
|
||||
|
||||
export const tab = {
|
||||
render: Tab,
|
||||
children: ["paragraph", "tag", "list", "fence", "heading"],
|
||||
attributes: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "The label shown on the tab button",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { YouTube } from "../../components"
|
||||
|
||||
export const youtube = {
|
||||
render: YouTube,
|
||||
selfClosing: true,
|
||||
attributes: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: "The YouTube video URL",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
description: "Accessible title for the video iframe",
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
description: "Optional caption to display below the video",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const withMarkdoc = require("@markdoc/next.js")
|
||||
const previousDocsRedirects = require("./previous-docs-redirects")
|
||||
|
||||
module.exports = withMarkdoc(/* config: https://markdoc.io/docs/nextjs#options */)({
|
||||
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdoc"],
|
||||
basePath: "/docs",
|
||||
turbopack: {},
|
||||
async redirects() {
|
||||
return [...previousDocsRedirects]
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
// Rewrite /docs/llms.txt to the API endpoint (internal to basePath)
|
||||
source: "/llms.txt",
|
||||
destination: "/api/llms.txt",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "1.0.24",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
"@markdoc/markdoc": "^0.5.4",
|
||||
"@markdoc/next.js": "^0.5.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"@vscode/codicons": "^0.0.44",
|
||||
"next": "^16.1.5",
|
||||
"posthog-js": "^1.335.3",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import React from "react"
|
||||
import Link from "next/link"
|
||||
import Head from "next/head"
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>404 - Page Not Found | Kilo Code Documentation</title>
|
||||
<meta name="description" content="The requested page could not be found." />
|
||||
</Head>
|
||||
<div className="not-found-page">
|
||||
<div className="not-found-container">
|
||||
{/* Terminal Window */}
|
||||
<div className="terminal-window">
|
||||
<div className="terminal-header">
|
||||
<div className="terminal-dots">
|
||||
<span className="dot red"></span>
|
||||
<span className="dot yellow"></span>
|
||||
<span className="dot green"></span>
|
||||
</div>
|
||||
<span className="terminal-title">terminal</span>
|
||||
</div>
|
||||
<div className="terminal-body">
|
||||
<div className="terminal-line">
|
||||
<span className="prompt">$</span>
|
||||
<span className="command"> GET /requested-path</span>
|
||||
</div>
|
||||
<div className="terminal-line error">
|
||||
<span className="error-text">Error 404: Page not found</span>
|
||||
</div>
|
||||
<div className="error-code">
|
||||
<div className="error-bar"></div>
|
||||
<span className="code-404">404</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="message-section">
|
||||
<h1 className="message-title">Page not found</h1>
|
||||
<p className="message-subtitle">The page you requested does not exist or has been moved.</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="actions">
|
||||
<Link href="/" className="btn btn-primary">
|
||||
Return to Documentation
|
||||
</Link>
|
||||
<Link href="/getting-started" className="btn btn-secondary">
|
||||
Getting Started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.not-found-page {
|
||||
min-height: calc(100vh - var(--top-nav-height));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.not-found-container {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Terminal Window */
|
||||
.terminal-window {
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #3d3d3d;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.terminal-dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
|
||||
.dot.green {
|
||||
background: #27ca40;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
color: #888;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #f8f674;
|
||||
}
|
||||
|
||||
.command {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.error-bar {
|
||||
width: 4px;
|
||||
height: 80px;
|
||||
background: #f8f674;
|
||||
margin-right: 1.5rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.code-404 {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 4.5rem;
|
||||
font-weight: 700;
|
||||
color: #f8f674;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Message Section */
|
||||
.message-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-header);
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.message-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.not-found-page {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.code-404 {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
.error-bar {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions :global(.btn) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// This tells Next.js to use a different layout approach
|
||||
Custom404.is404 = true
|
||||
@@ -0,0 +1,272 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
import posthog from "posthog-js"
|
||||
|
||||
import { CopyPageButton, SideNav, TableOfContents, TopNav } from "../components"
|
||||
|
||||
import "prismjs"
|
||||
import "prismjs/components/prism-bash.min"
|
||||
import "prismjs/components/prism-javascript.min"
|
||||
import "prismjs/components/prism-typescript.min"
|
||||
import "prismjs/components/prism-python.min"
|
||||
import "prismjs/components/prism-json.min"
|
||||
import "prismjs/components/prism-yaml.min"
|
||||
import "prismjs/components/prism-go.min"
|
||||
import "prismjs/components/prism-ruby.min"
|
||||
import "prismjs/components/prism-markdown.min"
|
||||
import "prismjs/components/prism-powershell.min"
|
||||
import "prismjs/themes/prism.css"
|
||||
|
||||
import "@docsearch/css"
|
||||
|
||||
import "../public/globals.css"
|
||||
|
||||
import type { AppProps } from "next/app"
|
||||
import type { MarkdocNextJsPageProps } from "@markdoc/next.js"
|
||||
|
||||
const TITLE = "Kilo Code Documentation"
|
||||
const DESCRIPTION = "Build, ship, and iterate faster with the most popular open source coding agent."
|
||||
|
||||
function collectHeadings(node, sections = []) {
|
||||
if (node) {
|
||||
// Skip headings inside tabs - they're not always visible
|
||||
if (node.name === "Tabs") {
|
||||
return sections
|
||||
}
|
||||
|
||||
if (node.name === "Heading") {
|
||||
const title = node.children[0]
|
||||
|
||||
if (typeof title === "string") {
|
||||
sections.push({
|
||||
...node.attributes,
|
||||
title,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
collectHeadings(child, sections)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
export type MyAppProps = MarkdocNextJsPageProps
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps<MyAppProps>) {
|
||||
const { markdoc } = pageProps
|
||||
const router = useRouter()
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
|
||||
// Check if we're on the homepage or 404 page (full-page layouts)
|
||||
const isHomepage = router.pathname === "/"
|
||||
const is404 = router.pathname === "/404"
|
||||
const isFullPageLayout = isHomepage || is404
|
||||
|
||||
// Track initial pageview (routeChangeComplete doesn't fire on first load)
|
||||
useEffect(() => {
|
||||
posthog.capture("$pageview")
|
||||
}, [])
|
||||
|
||||
// Close mobile menu on route change and track pageviews
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
setIsMobileMenuOpen(false)
|
||||
}
|
||||
|
||||
const handleRouteComplete = () => {
|
||||
posthog.capture("$pageview")
|
||||
}
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange)
|
||||
router.events.on("routeChangeComplete", handleRouteComplete)
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange)
|
||||
router.events.off("routeChangeComplete", handleRouteComplete)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
// Prevent body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (isMobileMenuOpen) {
|
||||
document.body.style.overflow = "hidden"
|
||||
} else {
|
||||
document.body.style.overflow = ""
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = ""
|
||||
}
|
||||
}, [isMobileMenuOpen])
|
||||
|
||||
const handleMobileMenuToggle = () => {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen)
|
||||
}
|
||||
|
||||
const handleMobileMenuClose = () => {
|
||||
setIsMobileMenuOpen(false)
|
||||
}
|
||||
|
||||
let title = TITLE
|
||||
let description = DESCRIPTION
|
||||
if (markdoc) {
|
||||
if (markdoc.frontmatter.title) {
|
||||
title = markdoc.frontmatter.title
|
||||
}
|
||||
if (markdoc.frontmatter.description) {
|
||||
description = markdoc.frontmatter.description
|
||||
}
|
||||
}
|
||||
|
||||
const toc = pageProps.markdoc?.content ? collectHeadings(pageProps.markdoc.content) : []
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="strict-origin" />
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="shortcut icon" href="https://kilo.ai/favicon.ico" />
|
||||
<link rel="icon" href="https://kilo.ai/favicon.ico" sizes="48x48" type="image/x-icon" />
|
||||
<link rel="icon" href="https://kilo.ai/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="https://kilo.ai/apple-touch-icon.png" sizes="180x180" type="image/png" />
|
||||
<link rel="manifest" href="https://kilo.ai/site.webmanifest" />
|
||||
<link rel="icon" href="https://kilo.ai/android-chrome-192x192.png" type="image/png" sizes="192x192" />
|
||||
<link rel="icon" href="https://kilo.ai/android-chrome-512x512.png" type="image/png" sizes="512x512" />
|
||||
<meta name="theme-color" content="#617A91" />
|
||||
{/* Preconnect to Algolia for better performance */}
|
||||
<link rel="preconnect" href="https://PMZUYBQDAK-dsn.algolia.net" crossOrigin="anonymous" />
|
||||
{/* Script to prevent flash of wrong theme */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
<TopNav
|
||||
onMobileMenuToggle={handleMobileMenuToggle}
|
||||
isMobileMenuOpen={isMobileMenuOpen}
|
||||
showMobileMenuButton={!isFullPageLayout}
|
||||
/>
|
||||
{isFullPageLayout ? (
|
||||
<main className="fullpage-main">
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
) : (
|
||||
<div className="page">
|
||||
<SideNav isMobileOpen={isMobileMenuOpen} onMobileClose={handleMobileMenuClose} />
|
||||
<main className="main-content">
|
||||
<div className="content-wrapper">
|
||||
<div className="article-content flex column mt-5">
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
<div className="right-sidebar">
|
||||
{markdoc && <CopyPageButton />}
|
||||
<TableOfContents toc={toc} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
<style jsx>
|
||||
{`
|
||||
.fullpage-main {
|
||||
position: fixed;
|
||||
top: var(--top-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
background: var(--bg-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.page {
|
||||
position: fixed;
|
||||
top: var(--top-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - var(--top-nav-height));
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
background: var(--bg-color);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding-right: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.article-content {
|
||||
flex: 1 1 0;
|
||||
font-size: 16px;
|
||||
padding: 0 2rem 2rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.article-content > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
.article-content pre {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.right-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding-top: 2.5rem;
|
||||
flex: 0 0 240px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
/* Mobile responsive styles */
|
||||
@media (max-width: 1024px) {
|
||||
.right-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.content-wrapper {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.article-content {
|
||||
padding: 0 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
sidebar_label: Anthropic
|
||||
---
|
||||
|
||||
# Using Anthropic With Kilo Code
|
||||
|
||||
Anthropic is an AI safety and research company that builds reliable, interpretable, and steerable AI systems. Their Claude models are known for their strong reasoning abilities, helpfulness, and honesty.
|
||||
|
||||
**Website:** [https://www.anthropic.com/](https://www.anthropic.com/)
|
||||
|
||||
## Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [Anthropic Console](https://console.anthropic.com/). Create an account or sign in.
|
||||
2. **Navigate to API Keys:** Go to the [API keys](https://console.anthropic.com/settings/keys) section.
|
||||
3. **Create a Key:** Click "Create Key". Give your key a descriptive name (e.g., "Kilo Code").
|
||||
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
|
||||
|
||||
## Configuration in Kilo Code
|
||||
|
||||
1. **Open Kilo Code Settings:** Click the gear icon ({% codicon name="gear" /%}) in the Kilo Code panel.
|
||||
2. **Select Provider:** Choose "Anthropic" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Anthropic API key into the "Anthropic API Key" field.
|
||||
4. **Select Model:** Choose your desired Claude model from the "Model" dropdown.
|
||||
5. **(Optional) Custom Base URL:** If you need to use a custom base URL for the Anthropic API, check "Use custom base URL" and enter the URL. Most people won't need to adjust this.
|
||||
|
||||
## Tips and Notes
|
||||
|
||||
- **Prompt Caching:** Claude 3 models support [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching), which can significantly reduce costs and latency for repeated prompts.
|
||||
- **Context Window:** Claude models have large context windows (200,000 tokens), allowing you to include a significant amount of code and context in your prompts.
|
||||
- **Pricing:** Refer to the [Anthropic Pricing](https://www.anthropic.com/pricing) page for the latest pricing information.
|
||||
- **Rate Limits:** Anthropic has strict rate limits based on [usage tiers](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). If you're repeatedly hitting rate limits, consider contacting Anthropic sales or accessing Claude through a different provider like [OpenRouter](/docs/ai-providers/openrouter) or [Requesty](/docs/ai-providers/requesty).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user