mirror of
https://github.com/cline/cline.git
synced 2026-09-04 20:02:30 +08:00
Compare commits
275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd6fabe8ae | |||
| f289b924d1 | |||
| 7b9796cc75 | |||
| a8c7978b5c | |||
| ce3b53e237 | |||
| d837712366 | |||
| d966dbf25b | |||
| 97fd423583 | |||
| 4344eab56a | |||
| ef943d49ff | |||
| 154cce1f71 | |||
| 3994ac4159 | |||
| 1e5af11df2 | |||
| 9a4fec5775 | |||
| 33f5d5ff99 | |||
| bcb5457c04 | |||
| 8dc3fe673d | |||
| d4a71eb269 | |||
| 9f98e0d9d2 | |||
| 1938ca5744 | |||
| 0c12f5220c | |||
| 0fe207c018 | |||
| 3ed2097c28 | |||
| 64d00e1344 | |||
| cdeca5fc50 | |||
| 75d5b96927 | |||
| 6a99ed7337 | |||
| f516b2d355 | |||
| 546fb1f229 | |||
| 95a4b910c8 | |||
| 53a5433e72 | |||
| 842a68d5d7 | |||
| c42add2e1e | |||
| 55e2c1a772 | |||
| ab8eaa00d0 | |||
| 6af7624142 | |||
| e2661ffacc | |||
| 28f6fb016e | |||
| 5720f272a0 | |||
| 22c2ec7570 | |||
| f68fbbaf84 | |||
| 76d36c4797 | |||
| 796a6b90c9 | |||
| a4ba38a776 | |||
| bea291576d | |||
| a72c07a5bf | |||
| 97333c1f9e | |||
| 66f7bca93e | |||
| 7e44407344 | |||
| acd9fe5c6a | |||
| f27a5648f6 | |||
| 84591294b7 | |||
| f291104aad | |||
| 46ebcf548d | |||
| 4bd8c3d422 | |||
| 8d318dec47 | |||
| 7e86ec8766 | |||
| d3f3901549 | |||
| 397910a957 | |||
| 2e0c975e98 | |||
| 519946a14c | |||
| fd0402b2fc | |||
| 0ab4deb91b | |||
| 1dcddfada4 | |||
| 9cbed22c84 | |||
| 674a02a6b9 | |||
| 4afa9d3ea3 | |||
| 22f300c945 | |||
| 655917bd3a | |||
| 3f39d856be | |||
| 68c37dc52d | |||
| e57663aef9 | |||
| a508e4dbab | |||
| 1cb691a496 | |||
| e4340039ea | |||
| d35c4ba893 | |||
| 91abcea979 | |||
| 2df16d91e2 | |||
| 1ef9542b85 | |||
| 1cb41432a4 | |||
| f0f08a4205 | |||
| 4c248208e8 | |||
| 446507ed06 | |||
| ca56aa9bfe | |||
| 219eab9372 | |||
| dbd8604bd4 | |||
| 6f6fbdaabd | |||
| 5dc3bbde78 | |||
| 10197b038d | |||
| 9390d3f933 | |||
| 5df470bf48 | |||
| 034c4342d1 | |||
| 349295ab2c | |||
| 841402c178 | |||
| e52a052c81 | |||
| 7a91a9be2e | |||
| 93a494d009 | |||
| 2bd21f8a45 | |||
| 2f33f71ebd | |||
| dec10aaec3 | |||
| c09705a45f | |||
| 73058c871a | |||
| 03211f1364 | |||
| 65e9727c65 | |||
| 884fbfb21e | |||
| 0c880c7bb1 | |||
| a915122c6d | |||
| 835ed94736 | |||
| 00ca3d13fa | |||
| 3f3a87aed9 | |||
| 7a0f11837e | |||
| 03d2d01eed | |||
| db1b1c45bd | |||
| a4131e57d8 | |||
| 683dd9a741 | |||
| 0d437f71b0 | |||
| fc77c0faea | |||
| 12c85c4233 | |||
| d6b7a1ab41 | |||
| 3926e7b404 | |||
| 5ba4314b9a | |||
| 9ccad7f764 | |||
| 3562f54dbf | |||
| d427d5d76a | |||
| 072e2887b0 | |||
| 9c71a6f021 | |||
| 7627a382aa | |||
| 978c633c90 | |||
| e807b520e0 | |||
| f3a3f30db5 | |||
| f1c7934064 | |||
| 1d42da5248 | |||
| 7f3974a827 | |||
| d40ab56aff | |||
| bbdf445db7 | |||
| d992a3bf21 | |||
| ace95988f8 | |||
| 54726f1677 | |||
| 6308fef0a9 | |||
| b3fc79b8ce | |||
| ff05ec3bbe | |||
| bde7049c01 | |||
| 7cd06744ad | |||
| c88d3238cf | |||
| 852ca2348f | |||
| 7091ccf2c7 | |||
| ad6c33ac5b | |||
| 71e312e92a | |||
| 5903840f79 | |||
| 0c677b63db | |||
| b741135f85 | |||
| 2eab216815 | |||
| e57174eec8 | |||
| ad4631c682 | |||
| aae23e8685 | |||
| 0ff27591d5 | |||
| 23127f22de | |||
| 57ed14d1b1 | |||
| a769585406 | |||
| c44b29b002 | |||
| bb4e397a51 | |||
| 9824d8d476 | |||
| a46c5288ca | |||
| 91b947de69 | |||
| 7b25a21b26 | |||
| 1d1071dcf5 | |||
| 8d07b7d6cb | |||
| aacc69a558 | |||
| a27bedffb6 | |||
| bbeecefe84 | |||
| 50b57f472f | |||
| 0e7e0099cd | |||
| 44cbfe9d9c | |||
| 4ecccf5105 | |||
| 6d29bc6551 | |||
| 820057308a | |||
| 7355f7e3b9 | |||
| 156f18f7b2 | |||
| 6129caa423 | |||
| ffa4785f43 | |||
| 92ec126dca | |||
| c4c8b16afb | |||
| 2eed79dcd7 | |||
| d39a53feed | |||
| cae9bff416 | |||
| 36618fb327 | |||
| a335b5cb3e | |||
| bce71b4448 | |||
| f6d2b4d9ac | |||
| 911fcfda47 | |||
| 55569efb74 | |||
| 18561ed59d | |||
| fa7265fa33 | |||
| bf96223639 | |||
| c489f79508 | |||
| 6ccd662a1f | |||
| a2fa4d3b92 | |||
| c8f81374a3 | |||
| eb25634bd4 | |||
| 1753d9dc15 | |||
| a562868cb9 | |||
| bf5265758f | |||
| 469752a201 | |||
| 8fb2e8a3aa | |||
| aef32f52f9 | |||
| 2baf966db7 | |||
| 152ba674da | |||
| 12f5dc2e9e | |||
| 28d806a4e4 | |||
| e92c7de8c0 | |||
| 718e5b53f6 | |||
| e484534bcb | |||
| 2822723495 | |||
| b5c0cc18d8 | |||
| fb04a9bd15 | |||
| 6def83a5d9 | |||
| fc3d986d05 | |||
| 520ddb83bb | |||
| d96c5d4b40 | |||
| e6cbae0edc | |||
| 76bd0926e6 | |||
| 819f9ce00d | |||
| fd8cecddd5 | |||
| a502bd8653 | |||
| d48d5ee74d | |||
| cd320ea01f | |||
| 60485277c4 | |||
| 0a8f1ef248 | |||
| 4b2619daf7 | |||
| 913cf4b74d | |||
| 8e5be3f648 | |||
| 6c519ff6e5 | |||
| c61f9a9394 | |||
| fa53f301a4 | |||
| c36e375af5 | |||
| 94b02bf052 | |||
| f10b6f39be | |||
| 42e6a24d0f | |||
| 452733c3fd | |||
| 7fadcfaa3f | |||
| 9989225b69 | |||
| 8a73f63189 | |||
| 31e8c85f0a | |||
| 5b9916866d | |||
| f001e735f8 | |||
| 32893ee343 | |||
| 0d4e47e5c3 | |||
| c1a43482e7 | |||
| ea65383e16 | |||
| b97d1487a7 | |||
| 091cf945e4 | |||
| df4f551ba7 | |||
| 88da4ddf89 | |||
| 93eb607e6d | |||
| 810b5b78f5 | |||
| 38ea422f6a | |||
| a7a35c0138 | |||
| 01547ba1f4 | |||
| 7f1632f09f | |||
| 1b0ab3d01b | |||
| dce0902596 | |||
| 9e7a30bd34 | |||
| 2b1b1d1cf2 | |||
| fcf3792f63 | |||
| 0e833ade82 | |||
| 4455db5198 | |||
| 03ab2968a6 | |||
| a0d52d4d59 | |||
| 75fbeb4aad | |||
| 70db6bde34 | |||
| 02c2601e0e | |||
| 94692b5091 | |||
| a2794c680f | |||
| 6d3f8e1d5d | |||
| 3e5847890b |
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: create-pull-request
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, PR template usage, and PR creation using the gh CLI tool.
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
@@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
changesDir: .changes
|
||||
unreleasedDir: unreleased
|
||||
headerPath: header.tpl.md
|
||||
changelogPath: CHANGELOG.md
|
||||
versionExt: md
|
||||
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||
kindFormat: "### {{.Kind}}"
|
||||
changeFormat: "* {{.Body}}"
|
||||
kinds:
|
||||
- label: Added
|
||||
auto: minor
|
||||
- label: Changed
|
||||
auto: major
|
||||
- label: Deprecated
|
||||
auto: minor
|
||||
- label: Removed
|
||||
auto: major
|
||||
- label: Fixed
|
||||
auto: patch
|
||||
- label: Security
|
||||
auto: patch
|
||||
newlines:
|
||||
afterChangelogHeader: 1
|
||||
beforeChangelogVersion: 1
|
||||
endOfVersion: 1
|
||||
envPrefix: CHANGIE_
|
||||
@@ -0,0 +1,51 @@
|
||||
# Debug Harness
|
||||
|
||||
HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/server.ts`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Build extension first if needed (protos + esbuild):
|
||||
npm run protos && IS_DEV=true node esbuild.mjs
|
||||
|
||||
# Launch (skip-build if already built):
|
||||
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
|
||||
# In another terminal:
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
```
|
||||
|
||||
## Key commands
|
||||
|
||||
All via `POST localhost:19229/api` with `{"method":"...", "params":{...}}`:
|
||||
|
||||
- **`launch`** / **`shutdown`** — lifecycle
|
||||
- **`ui.screenshot`** — screenshot to `/tmp/cline-debug/`; returns `{path}` — **use `read_file` on the path to examine, do NOT `open` the file** (Preview.app covers the VSCode window)
|
||||
- **`ui.open_sidebar`** — open the Cline sidebar
|
||||
- **`ext.set_breakpoint`** `{file, line, condition?}` — breakpoint by source file (sourcemap-resolved)
|
||||
- **`ext.evaluate`** `{expression, callFrameId?}` — eval in extension host
|
||||
- **`ext.resume`** / **`ext.step_over`** / **`ext.step_into`** — stepping
|
||||
- **`ext.call_stack`** — inspect when paused
|
||||
- **`web.evaluate`** `{expression}` — eval in webview
|
||||
- **`web.post_message`** `{message}` — send postMessage to extension host via exposed vsCodeApi
|
||||
- **`wait_for_pause`** `{timeout?}` — block until breakpoint hit
|
||||
- **`ui.locator`** `{role?, testId?, text?, action?, frame?}` — Playwright locator (auto-retries on stale sidebar frame)
|
||||
- **`ui.react_input`** `{text, selector?, clear?, submit?}` — set React textarea value via `execCommand('insertText')`; works reliably across multiple tasks
|
||||
- **`ui.send_message`** `{text, images?, files?, responseType?}` — send chat message bypassing the textarea entirely (via gRPC postMessage)
|
||||
- **`ui.command_palette`** `{command}` — run VSCode command
|
||||
|
||||
## Caveats
|
||||
|
||||
- **⚠️ Dismiss "Introducing Cline Kanban" overlay FIRST**: On fresh launches a full-screen promo overlay blocks the sidebar. **Dismiss it immediately after `ui.open_sidebar`**, before any other interaction or screenshot. Most reliable method:
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}}'
|
||||
```
|
||||
- **Screenshots — don't open the file**: `ui.screenshot` and `ui.sidebar_screenshot` save PNGs to `/tmp/cline-debug/` and return the `{path}`. Use `read_file` on the path to examine screenshots. Running `open <path>` launches Preview.app on macOS which covers the VSCode window.
|
||||
- **Scripts count = 0 after launch**: CDP connects after extension host starts, so scripts parsed during startup aren't tracked. Breakpoints still work via sourcemap resolution.
|
||||
- **Port 9230**: Extension host inspector. If another VSCode instance uses this port, the harness will fail to connect. Kill other debug instances first.
|
||||
- **macOS only** for now (Playwright Electron launch behavior).
|
||||
- **Webview CDP**: `connect_webview` may fail depending on Electron version. `web.evaluate` still works via Playwright's `frame.evaluate()` fallback.
|
||||
- **Sourcemap paths**: esbuild outputs relative paths like `../src/extension.ts` in the sourcemap. The resolver handles this, but if a file isn't found, use `ext.source_files` to see exact paths.
|
||||
|
||||
See `src/dev/debug-harness/README.md` for full API reference.
|
||||
@@ -14,7 +14,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
|
||||
|
||||
## Miscellaneous
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
|
||||
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
|
||||
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
|
||||
- Additional instructions about making requests: @.clinerules/network.md
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PostToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PreToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskCancel hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskResume hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskStart hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "UserPromptSubmit hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -19,7 +19,7 @@ Review and address all comments on the current branch's PR.
|
||||
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
|
||||
- General comments: `gh pr view {pr_number} --json comments,reviews`
|
||||
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (release automation, CI status, etc.).
|
||||
|
||||
5. **Wait for my approval** before proceeding.
|
||||
|
||||
|
||||
@@ -1,549 +0,0 @@
|
||||
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
|
||||
|
||||
|
||||
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
|
||||
|
||||
|
||||
- 3.14
|
||||
<changeset>
|
||||
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
Releases
|
||||
claude-dev@3.14.0
|
||||
Minor Changes
|
||||
77c9863: create clinerules folder if its currently a file and creating new rule
|
||||
0ffb7dd: disabling shift hint for now & improving tooltip behavior
|
||||
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
|
||||
eb6e481: Full support for LaTeX rendering
|
||||
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
|
||||
e4d26be: allow cursorrules and windsurfrules
|
||||
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
|
||||
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
|
||||
aed152b: add truncation notice when truncating manually
|
||||
2fe2405: Migrate Cline Tools Section to new docs
|
||||
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
|
||||
03d4410: Added copy button to code blocks.
|
||||
c78fe23: addressed race condition in terminal command usage
|
||||
91e222f: add checkpoints after more messages
|
||||
14230e7: add newrule slash command
|
||||
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
|
||||
4196c14: add cache ui for open router and cline provider
|
||||
d97424f: showing expanded task by default
|
||||
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
|
||||
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
|
||||
4b697d8: Migrate the addRemoteServer to protobus
|
||||
Patch Changes
|
||||
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
|
||||
459adf0: Add markdown copy to chat
|
||||
74ec823: Minor UX improvement to drag and drop ux
|
||||
b0961f4: Remove linear pull request action
|
||||
e9ce384: searchCommits protobus migration
|
||||
5802b68: createRuleFile protobus migration
|
||||
df7f9fc: Add dependsOn to more blocks in the tasks.json
|
||||
41ae732: Fix for git commit mentions in repos with no git commits
|
||||
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
|
||||
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
|
||||
65243ad: Introduce UI library for future UI development
|
||||
4565e06: checkIsImageURL migrated to protobus
|
||||
5a8e9d8: protobus migration for openImage
|
||||
deeda6e: Lowering Gemini cache TTL time
|
||||
db0b022: Adding UI to show openrouter balance next to provider
|
||||
4650ffa: deleteRuleFile protobus migration
|
||||
d4bd755: fix cost calculation
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.14.0]
|
||||
|
||||
- Add UI to show openrouter balance next to provider
|
||||
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
|
||||
- Add more robust caching & cache tracking for gemini & vertex providers
|
||||
- Add support for LaTeX rendering
|
||||
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
|
||||
- Add truncation notice when truncating manually
|
||||
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
|
||||
- Add copy button to code blocks
|
||||
- Add copy button to markdown blocks (Thanks @weshoke!)
|
||||
- Add checkpoints to more messages
|
||||
- Add slash command to create a new rules file (/newrule)
|
||||
- Add cache ui for open router and cline provider
|
||||
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
|
||||
- Add support for cursorrules and windsurfrules
|
||||
- Add support for batch history deletion (Thanks @danix800!)
|
||||
- Improve Drag & Drop experience
|
||||
- Create clinerules folder creating new rule if it's needed
|
||||
- Enable pricing calculation for gemini and vertex providers
|
||||
- Refactor message handling to not show the MCP View of the server modal
|
||||
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
|
||||
- Update task header to be expanded by default
|
||||
- Update Gemini cache TTL time to 15 minutes
|
||||
- Fix race condition in terminal command usage
|
||||
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
|
||||
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
|
||||
- Fix for git commit mentions in repos with no git commits
|
||||
- Fix cost calculation (Thanks @BarreiroT!)
|
||||
</changelog>
|
||||
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
|
||||
Gemini models.
|
||||
</li>
|
||||
<li>
|
||||
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
|
||||
easily.
|
||||
</li>
|
||||
<li>
|
||||
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
|
||||
workflow.
|
||||
</li>
|
||||
<li>
|
||||
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
|
||||
</li>
|
||||
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
|
||||
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label="Previous Updates"
|
||||
title="Previous Updates:"
|
||||
classNames={{
|
||||
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
|
||||
title: "font-bold text-(--vscode-foreground)",
|
||||
indicator:
|
||||
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
|
||||
projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
|
||||
to plug and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
|
||||
new task (more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
|
||||
restore your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
- 3.13
|
||||
|
||||
<changeset>
|
||||
Minor Changes
|
||||
2964388: Added copy button to MermaidBlock component
|
||||
75143a7: Add the ability to fetch from global cline rules files
|
||||
Patch Changes
|
||||
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
|
||||
ab59bd9: Add stream options back to xai provider
|
||||
7276f50: Icons to indicate an action is occuring outside of the users workspace
|
||||
0b19ba6: update to NEW model
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.13.0]
|
||||
|
||||
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
|
||||
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
|
||||
- Add ability to edit past messages, with options to restore your workspace back to that point
|
||||
- Allow sending a message when selecting an option provided by the question or plan tool
|
||||
- Add command to jump to Cline's chat input
|
||||
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
|
||||
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
|
||||
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
|
||||
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
|
||||
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
|
||||
- Add detection of Ctrl+C termination in terminal, improving output reading issues
|
||||
- Fix issue where some commands with large output would cause UI to freeze
|
||||
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
|
||||
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
|
||||
</changelog>
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
|
||||
and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
|
||||
(more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
|
||||
your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
|
||||
quick access!
|
||||
</li>
|
||||
<li>
|
||||
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
|
||||
showing the number of edits Cline makes.
|
||||
</li>
|
||||
<li>
|
||||
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
|
||||
</li>
|
||||
</ul>
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
|
||||
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
|
||||
|
||||
The Changeset PR description looks something like this:
|
||||
|
||||
<changeset-pr-description>
|
||||
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
|
||||
# Releases
|
||||
## claude-dev@3.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
|
||||
- aabe4ae: Add detection for new users to display special components
|
||||
- 6c18d51: adds global endpoint for vertex ai users
|
||||
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
|
||||
- 5147e28: new workflow feature
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c0b3c69: fix eternal loading states when the last message is a checkpoint
|
||||
- 570ece3: selectImages protos migration
|
||||
- 8d8452e: askResponse protobus migration
|
||||
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
</changeset-pr-description>
|
||||
|
||||
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
|
||||
|
||||
I have the `gh` command line tool set up and authenticated, so you have everything you need.
|
||||
|
||||
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
|
||||
|
||||
To handle this process effectively, do the following:
|
||||
|
||||
For each of the automatically generated bullet points in the Changelog.md, you should
|
||||
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
|
||||
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
|
||||
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
|
||||
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
|
||||
5. Update the `CHANGELOG.md` accordingly
|
||||
|
||||
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
|
||||
|
||||
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
|
||||
|
||||
<keepachangelog-pinciples-for-good-changelogs>
|
||||
### Guiding Principles
|
||||
- Changelogs are for humans, not machines.
|
||||
- There should be an entry for every single version.
|
||||
- The same types of changes should be grouped.
|
||||
- The latest version comes first.
|
||||
|
||||
### Bullet points in the changelog should follow these principles:
|
||||
- Types of changes
|
||||
- Added for new features.
|
||||
- Changed for changes in existing functionality.
|
||||
- Deprecated for soon-to-be removed features.
|
||||
- Removed for now removed features.
|
||||
- Fixed for any bug fixes.
|
||||
- Security in case of vulnerabilities.
|
||||
</keepachangelog-pinciples-for-good-changelogs>
|
||||
|
||||
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
|
||||
|
||||
1. Patch
|
||||
2. Minor
|
||||
3. Major
|
||||
|
||||
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
|
||||
|
||||
<important_note>
|
||||
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
|
||||
|
||||
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
|
||||
|
||||
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
|
||||
</important_note>
|
||||
|
||||
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
|
||||
|
||||
|
||||
<detailed_sequence_of_steps>
|
||||
# Cline Release Process - Detailed Sequence of Steps
|
||||
|
||||
## Before Starting
|
||||
1. First, examine the changeset PR without checking it out:
|
||||
```bash
|
||||
gh pr view changeset-release/main
|
||||
```
|
||||
|
||||
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
|
||||
```bash
|
||||
gh pr diff changeset-release/main > changeset-diff.txt
|
||||
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
3. Once you're ready to start, checkout and update the changeset release branch:
|
||||
```bash
|
||||
git checkout changeset-release/main
|
||||
git pull origin changeset-release/main
|
||||
```
|
||||
|
||||
## Analyzing Each Change
|
||||
4. For each commit hash in the auto-generated changelog entries:
|
||||
|
||||
a. Find the PR number associated with a commit hash:
|
||||
```bash
|
||||
gh pr list --search "<commit-hash>" --state merged
|
||||
```
|
||||
|
||||
b. Get PR details for better context:
|
||||
```bash
|
||||
gh pr view <PR-number>
|
||||
```
|
||||
|
||||
c. Check if the contributor is external to determine if attribution is needed:
|
||||
```bash
|
||||
# Extract username from PR
|
||||
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
|
||||
|
||||
# Check if user is a member of the Cline organization
|
||||
# this command is a bit finnicky, but it 100% works.
|
||||
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
|
||||
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
|
||||
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
|
||||
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
|
||||
```
|
||||
|
||||
d. View the full PR diff to understand code changes:
|
||||
```bash
|
||||
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
|
||||
cat pr-diff-<PR-number>.txt
|
||||
```
|
||||
|
||||
## Updating the Changelog
|
||||
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
|
||||
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
|
||||
- Group by feature type (Added, Changed, Fixed)
|
||||
- Put most exciting features at the top
|
||||
- Move bug fixes and small improvements to the bottom
|
||||
- Use clear, end-user focused language
|
||||
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
|
||||
|
||||
## Version Number Verification
|
||||
6. Confirm the version bump is appropriate:
|
||||
- Check package.json to verify the auto-generated version number:
|
||||
```bash
|
||||
cat package.json | grep "\"version\""
|
||||
```
|
||||
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
|
||||
|
||||
7. Ensure the version in CHANGELOG.md has brackets around it:
|
||||
```
|
||||
## [3.16.0]
|
||||
```
|
||||
|
||||
## Creating the Announcement (for minor/major versions only)
|
||||
8. If this is a minor version bump, create/update the announcement component:
|
||||
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
|
||||
- Update the highlights based on key features
|
||||
- Move previous version highlights to the "Previous Updates" section
|
||||
- Use the previous announcement components as reference for structure
|
||||
|
||||
## Finalizing the Release
|
||||
9. Update dependencies with the new version number:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
|
||||
10. Commit your changes:
|
||||
```bash
|
||||
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
|
||||
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
|
||||
```
|
||||
|
||||
11. Push your changes to the changeset branch:
|
||||
```bash
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
12. Check that your changes pushed successfully:
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
</detailed_sequence_of_steps>
|
||||
@@ -89,16 +89,9 @@ On the main branch, create a commit that updates:
|
||||
|
||||
2. **package.json** - Update the version field to the new version
|
||||
|
||||
3. **Delete changesets** for the commits being included in the hotfix. This prevents the changeset bot from including duplicate entries in the next regular release.
|
||||
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
|
||||
|
||||
Find and delete the changeset files associated with the selected commits:
|
||||
```bash
|
||||
ls .changeset/
|
||||
```
|
||||
|
||||
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
|
||||
|
||||
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
|
||||
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
@@ -107,7 +100,7 @@ In the commit body, mention:
|
||||
- List the cherry-picked commits that will be included
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md package.json .changeset/
|
||||
git add CHANGELOG.md package.json
|
||||
git commit -m "v3.40.1 Release Notes (hotfix)
|
||||
|
||||
Hotfix release including:
|
||||
|
||||
@@ -347,8 +347,6 @@ A few notes:
|
||||
So until the settings page is update, and this is added to settings in a way that's clean and doesn't confuse new users, I don't think we can merge this. Please bear with us.
|
||||
</request_changes_comment>
|
||||
<request_changes_comment>
|
||||
Also, don't forget to add a changeset since this fixes a user-facing bug.
|
||||
|
||||
The architectural change is solid - moving the focus logic to the command handlers makes sense. Just don't want to introduce subtle timing issues by removing those timeouts.
|
||||
</request_changes_comment>
|
||||
</example_comments_that_i_have_written_before>
|
||||
|
||||
@@ -1,232 +1,64 @@
|
||||
# Release
|
||||
|
||||
Prepare and publish a release from the open changeset PR.
|
||||
Prepare and publish a release directly from `main`.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
1. Find and checkout the open changeset PR
|
||||
2. Clean up the changelog (fix version format, wordsmith entries)
|
||||
3. Push changes back to the PR branch
|
||||
4. Merge with proper commit message format
|
||||
5. Tag and push the release (after verifying the commit)
|
||||
6. Trigger the publish workflow
|
||||
7. Update GitHub release notes
|
||||
8. Provide final summary with Slack announcement
|
||||
1. Select/confirm the target version
|
||||
2. Curate `CHANGELOG.md` entries manually for end users
|
||||
3. Ensure `package.json` version matches the changelog
|
||||
4. Create and push a release commit + tag
|
||||
5. Trigger publish workflow
|
||||
6. Update GitHub release notes and share a summary
|
||||
|
||||
## Step 1: Find the Changeset PR
|
||||
## Process
|
||||
|
||||
Look for the open changeset PR:
|
||||
|
||||
```bash
|
||||
gh pr list --search "Changeset version bump" --state open --json number,title,headRefName,url
|
||||
```
|
||||
|
||||
If no PR is found, inform the user there's no changeset PR ready. They may need to:
|
||||
- Merge PRs with changesets to main first
|
||||
- Manually trigger the Changeset Converter workflow at: https://github.com/cline/cline/actions/workflows/changeset-converter.yml
|
||||
|
||||
## Step 2: Gather PR Information
|
||||
|
||||
Get the PR details:
|
||||
|
||||
```bash
|
||||
PR_NUMBER=<number from step 1>
|
||||
gh pr view $PR_NUMBER --json body,files,headRefName
|
||||
```
|
||||
|
||||
Checkout the PR branch:
|
||||
|
||||
```bash
|
||||
git fetch origin changeset-release/main
|
||||
git checkout changeset-release/main
|
||||
```
|
||||
|
||||
If the branch has diverged from remote, reset to the remote version:
|
||||
|
||||
```bash
|
||||
git reset --hard origin/changeset-release/main
|
||||
```
|
||||
|
||||
## Step 3: Analyze the Changes
|
||||
|
||||
Read the current CHANGELOG.md to see what the automation generated:
|
||||
|
||||
```bash
|
||||
head -50 CHANGELOG.md
|
||||
```
|
||||
|
||||
Get the version from package.json:
|
||||
|
||||
```bash
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
**Present to the user:**
|
||||
- The version number that will be released
|
||||
- The raw changelog entries from the changeset PR
|
||||
- Whether this is a patch, minor, or major release
|
||||
|
||||
## Step 4: Clean Up the Changelog
|
||||
|
||||
The changelog needs these fixes:
|
||||
|
||||
1. **Add brackets to version number**: Change `## 3.44.1` to `## [3.44.1]`
|
||||
|
||||
2. **No category headers**: Don't use `### Added`, `### Fixed`, etc. Just a flat list of bullet points.
|
||||
|
||||
3. **Order entries from most important to least important**:
|
||||
- Lead with major new features or significant fixes users care about
|
||||
- End with minor fixes or internal changes
|
||||
|
||||
4. **Write user-friendly descriptions**:
|
||||
- This is for end users, not developers—explain what changed in plain language
|
||||
- Remove commit hashes from the beginning of lines (the automation adds these)
|
||||
- Look at the actual commit diffs (`git show <hash>`) and PRs to understand what changed
|
||||
- Write colorful descriptions that explain the value and impact, not just technical details
|
||||
- Consolidate related changes into single entries when appropriate
|
||||
|
||||
**Ask the user** to review the proposed changelog changes before applying them. Show them:
|
||||
- Current (raw) changelog section
|
||||
- Proposed (cleaned) changelog section
|
||||
|
||||
Once approved, apply the changes to CHANGELOG.md.
|
||||
|
||||
## Step 5: Commit and Push Changes
|
||||
|
||||
After making changelog edits:
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Clean up changelog formatting"
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
## Step 6: Merge the PR
|
||||
|
||||
**Ask the user to confirm** they're ready to merge.
|
||||
|
||||
Merge the PR with the proper commit message format:
|
||||
|
||||
```bash
|
||||
VERSION=<version from package.json>
|
||||
gh pr merge $PR_NUMBER --squash --subject "v${VERSION} Release Notes" --body ""
|
||||
```
|
||||
|
||||
**If merge is blocked by branch protection:**
|
||||
- Users with admin privileges can add the `--admin` flag to bypass
|
||||
- Users without admin privileges need to get the PR approved through normal review first before merging
|
||||
|
||||
## Step 7: Tag the Release
|
||||
|
||||
After the merge completes, checkout main and pull:
|
||||
### 1) Sync and determine version
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
**IMPORTANT: Verify the latest commit is the release commit before tagging:**
|
||||
Confirm the release version with the maintainer (patch/minor/major).
|
||||
|
||||
### 2) Curate changelog and version
|
||||
|
||||
- Edit `CHANGELOG.md` for the target version using human-friendly release notes.
|
||||
- Ensure version headers use bracket format, e.g. `## [3.66.1]`.
|
||||
- Update `package.json` version to the same value.
|
||||
|
||||
### 3) Commit and tag
|
||||
|
||||
```bash
|
||||
git log -1 --oneline
|
||||
git add CHANGELOG.md package.json package-lock.json
|
||||
git commit -m "v<version> Release Notes"
|
||||
git push origin main
|
||||
git tag v<version>
|
||||
git push origin v<version>
|
||||
```
|
||||
|
||||
Confirm the commit message matches `v{VERSION} Release Notes` (e.g., `v3.44.1 Release Notes`). Do NOT blindly tag HEAD without verification.
|
||||
### 4) Trigger publish workflow
|
||||
|
||||
Once verified, tag and push:
|
||||
Tell the maintainer to run:
|
||||
https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
|
||||
Use `v<version>` as the release tag.
|
||||
|
||||
### 5) Update GitHub release notes
|
||||
|
||||
After publish completes:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
git tag v${VERSION}
|
||||
git push origin v${VERSION}
|
||||
gh release view v<version> --json body --jq '.body'
|
||||
gh release edit v<version> --notes "<final curated release notes>"
|
||||
```
|
||||
|
||||
## Step 8: Trigger Publish Workflow
|
||||
### 6) Final summary
|
||||
|
||||
**Copy the tag to clipboard** so the user can easily paste it into the GitHub Actions workflow:
|
||||
|
||||
```bash
|
||||
echo -n "v{VERSION}" | pbcopy
|
||||
```
|
||||
|
||||
**Tell the user to trigger the publish workflow:**
|
||||
1. Go to: https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
2. Select **"release"** for release-type
|
||||
3. Paste **`v{VERSION}`** as the tag (already in clipboard)
|
||||
|
||||
**Wait for the user** to confirm the publish workflow has completed before proceeding.
|
||||
|
||||
## Step 9: Update GitHub Release Notes
|
||||
|
||||
Once the user confirms the publish workflow is done, fetch the auto-generated release content:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
gh release view v${VERSION} --json body --jq '.body'
|
||||
```
|
||||
|
||||
The auto-generated release has:
|
||||
- `## What's Changed` - PR list (we'll replace this with our changelog)
|
||||
- `## New Contributors` - First-time contributors (keep this if present)
|
||||
- `**Full Changelog**` - Comparison link (keep this)
|
||||
|
||||
Build the new release body:
|
||||
1. Start with `## What's Changed` header
|
||||
2. Add our changelog content (from CHANGELOG.md for this version)
|
||||
3. Keep the `## New Contributors` section if it exists
|
||||
4. Keep the `**Full Changelog**` link
|
||||
|
||||
Update the release:
|
||||
|
||||
```bash
|
||||
gh release edit v${VERSION} --notes "<new body content>"
|
||||
```
|
||||
|
||||
Verify the release was updated:
|
||||
|
||||
```bash
|
||||
gh release view v${VERSION}
|
||||
```
|
||||
|
||||
## Step 10: Final Summary
|
||||
|
||||
**Copy a Slack announcement message to clipboard** (include the full changelog, not just highlights):
|
||||
|
||||
```bash
|
||||
echo "VS Code v{VERSION} Released
|
||||
|
||||
- Changelog entry 1
|
||||
- Changelog entry 2
|
||||
- Changelog entry 3" | pbcopy
|
||||
```
|
||||
|
||||
**Present a final summary:**
|
||||
- Version released: v{VERSION}
|
||||
- PR merged: #{PR_NUMBER}
|
||||
- Tag pushed: v{VERSION}
|
||||
- Release: https://github.com/cline/cline/releases/tag/v{VERSION}
|
||||
- Slack message copied to clipboard
|
||||
|
||||
**Final reminder:**
|
||||
Post the Slack message to announce the release
|
||||
|
||||
## Handling Edge Cases
|
||||
|
||||
### No changesets found
|
||||
If the changeset PR body shows no changes, inform the user they need to merge PRs with changesets first.
|
||||
|
||||
### Merge conflicts
|
||||
If there are conflicts on the changeset branch, help the user resolve them:
|
||||
```bash
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
# resolve conflicts
|
||||
git push origin changeset-release/main --force-with-lease
|
||||
```
|
||||
|
||||
### User wants to add more changes
|
||||
If the user wants to include additional PRs before releasing:
|
||||
1. Ask them to merge those PRs to main first
|
||||
2. The changeset automation will update the PR automatically
|
||||
3. Re-run this workflow after the PR is updated
|
||||
Provide:
|
||||
- Released version/tag
|
||||
- Link to release page
|
||||
- Summary of top end-user changes
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
webview-ui/src/assets/cline_kanban_demo.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copilot Instructions for Cline
|
||||
|
||||
This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge and nuanced patterns.
|
||||
|
||||
## Architecture
|
||||
- **Core** (`src/`): `extension.ts` → `WebviewProvider` → `Controller` (single source of truth) → `Task` (agent loop).
|
||||
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
|
||||
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
|
||||
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
|
||||
- **MCP**: `src/services/mcp/McpHub.ts`.
|
||||
|
||||
## Build & Test (Critical — non-obvious commands)
|
||||
- **Build**: `npm run compile` — NOT `npm run build`.
|
||||
- **Watch**: `npm run watch` (extension + webview).
|
||||
- **Protos**: `npm run protos` — run **immediately** after any `.proto` change. Generates into `src/shared/proto/`, `src/generated/`.
|
||||
- **Tests**: `npm run test:unit`. After prompt/tool changes: `UPDATE_SNAPSHOTS=true npm run test:unit`.
|
||||
|
||||
## Protobuf RPC Workflow (4 steps)
|
||||
1. **Define** in `proto/cline/*.proto`. Naming: `PascalCaseService`, `camelCase` RPCs, `PascalCase` Messages. Use `common.proto` shared types for simple data.
|
||||
2. **Generate**: `npm run protos`.
|
||||
3. **Backend handler**: `src/core/controller/<domain>/`.
|
||||
4. **Frontend call**: `UiServiceClient.myMethod(Request.create({...}))`.
|
||||
- Adding enums (e.g. `ClineSay`) → also update `src/shared/proto-conversions/cline-message.ts`.
|
||||
|
||||
## Adding API Providers (silent failure risk)
|
||||
Three proto conversion updates are **required** or the provider silently resets to Anthropic:
|
||||
1. `proto/cline/models.proto` — add to `ApiProvider` enum.
|
||||
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
|
||||
3. `convertProtoToApiProvider()` in the same file.
|
||||
|
||||
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
|
||||
|
||||
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
|
||||
|
||||
## Adding Tools to System Prompt (5+ file chain)
|
||||
1. Add enum to `ClineDefaultTool` in `src/shared/tools.ts`.
|
||||
2. Create definition in `src/core/prompts/system-prompt/tools/` (export `[GENERIC]` minimum).
|
||||
3. Register in `src/core/prompts/system-prompt/tools/init.ts`.
|
||||
4. Whitelist in `src/core/prompts/system-prompt/variants/*/config.ts` for each model family.
|
||||
5. Handler in `src/core/task/tools/handlers/`, wire in `ToolExecutor.ts`.
|
||||
6. If tool has UI: add `ClineSay` enum in proto → `ExtensionMessage.ts` → `cline-message.ts` → `ChatRow.tsx`.
|
||||
7. Regenerate snapshots: `UPDATE_SNAPSHOTS=true npm run test:unit`.
|
||||
|
||||
## Modifying System Prompt
|
||||
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
|
||||
|
||||
## Global State Keys (silent failure risk)
|
||||
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
|
||||
|
||||
## Slash Commands (3 places)
|
||||
- `src/core/slash-commands/index.ts` — definitions.
|
||||
- `src/core/prompts/commands.ts` — system prompt integration.
|
||||
- `webview-ui/src/utils/slash-commands.ts` — webview autocomplete.
|
||||
|
||||
## Conventions
|
||||
- **Paths**: Always use `src/utils/path` helpers (`toPosixString`) for cross-platform compatibility.
|
||||
- **Logging**: `src/shared/services/Logger.ts`.
|
||||
- **Feature flags**: See PR #7566 as reference pattern.
|
||||
@@ -60,7 +60,6 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
|
||||
|
||||
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
|
||||
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
|
||||
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
|
||||
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
This script updates a specific version's release notes section in CHANGELOG.md with new content
|
||||
or reformats existing content.
|
||||
|
||||
The script:
|
||||
1. Takes a version number, changelog path, and optionally new content as input from environment variables
|
||||
2. Finds the section in the changelog for the specified version
|
||||
3. Either:
|
||||
a) Replaces the content with new content if provided, or
|
||||
b) Reformats existing content by:
|
||||
- Removing the first two lines of the changeset format
|
||||
- Ensuring version numbers are wrapped in square brackets
|
||||
4. Writes the updated changelog back to the file
|
||||
|
||||
Environment Variables:
|
||||
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
|
||||
VERSION: The version number to update/format
|
||||
PREV_VERSION: The previous version number (used to locate section boundaries)
|
||||
NEW_CONTENT: Optional new content to insert for this version
|
||||
"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
|
||||
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
|
||||
VERSION = os.environ['VERSION']
|
||||
PREV_VERSION = os.environ.get("PREV_VERSION", "")
|
||||
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
|
||||
|
||||
def overwrite_changelog_section(changelog_text: str, new_content: str):
|
||||
# Find the section for the specified version
|
||||
version_pattern = f"## {VERSION}\n"
|
||||
unformmatted_prev_version_pattern = f"## {PREV_VERSION}\n"
|
||||
prev_version_pattern = f"## [{PREV_VERSION}]\n"
|
||||
print(f"latest version: {VERSION}")
|
||||
print(f"prev_version: {PREV_VERSION}")
|
||||
|
||||
notes_start_index = changelog_text.find(version_pattern) + len(version_pattern)
|
||||
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and (prev_version_pattern in changelog_text or unformmatted_prev_version_pattern in changelog_text) else len(changelog_text)
|
||||
|
||||
if new_content:
|
||||
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
|
||||
else:
|
||||
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
|
||||
filtered_lines = []
|
||||
for line in changeset_lines:
|
||||
# If the previous line is a changeset format
|
||||
if len(filtered_lines) > 1 and filtered_lines[-1].startswith("### "):
|
||||
# Remove the last two lines from the filted_lines
|
||||
filtered_lines.pop()
|
||||
filtered_lines.pop()
|
||||
else:
|
||||
filtered_lines.append(line.strip())
|
||||
|
||||
# Prepend a new line to the first line of filtered_lines
|
||||
if filtered_lines:
|
||||
filtered_lines[0] = "\n" + filtered_lines[0]
|
||||
|
||||
# Print filted_lines wiht a "\n" at the end of each line
|
||||
for line in filtered_lines:
|
||||
print(line.strip())
|
||||
|
||||
parsed_lines = "\n".join(line for line in filtered_lines)
|
||||
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
|
||||
return updated_changelog
|
||||
|
||||
with open(CHANGELOG_PATH, 'r') as f:
|
||||
changelog_content = f.read()
|
||||
|
||||
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
|
||||
# print("----------------------------------------------------------------------------------")
|
||||
# print(new_changelog)
|
||||
# print("----------------------------------------------------------------------------------")
|
||||
# Write back to CHANGELOG.md
|
||||
with open(CHANGELOG_PATH, 'w') as f:
|
||||
f.write(new_changelog)
|
||||
|
||||
print(f"{CHANGELOG_PATH} updated successfully!")
|
||||
@@ -1,113 +0,0 @@
|
||||
name: Changeset Converter
|
||||
run-name: Changeset Conversion
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
REPO_PATH: ${{ github.repository }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
|
||||
NODE_VERSION: 20.18.1
|
||||
|
||||
jobs:
|
||||
# Job 1: Create version bump PR when changesets are merged to main
|
||||
changeset-pr-version-bump:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.actor != 'github-actions'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check user for team affiliation
|
||||
id: team_check
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
org: ${{ github.repository_owner }}
|
||||
team: "deployer"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if user is authorized
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ "${{ steps.team_check.outputs.authorized }}" != "true" ]; then
|
||||
echo "User is not authorized to run this workflow."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.GIT_REF }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
# Check if there are any new changesets to process
|
||||
- name: Check for changesets
|
||||
id: check-changesets
|
||||
run: |
|
||||
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
|
||||
echo "Changesets diff with previous version: $NEW_CHANGESETS"
|
||||
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create version bump PR using changesets/action if there are new changesets
|
||||
- name: Create Changeset Pull Request
|
||||
if: steps.check-changesets.outputs.new_changesets != '0'
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: "changeset version bump"
|
||||
title: "Changeset version bump"
|
||||
version: npm run version-packages # This performs the changeset version bump
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Get current and previous versions to edit changelog entry
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(git show HEAD:package.json | jq -r '.version')
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
|
||||
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION"
|
||||
echo "prev_version=$PREV_VERSION"
|
||||
|
||||
# Update CHANGELOG.md with proper format
|
||||
- name: Update Changelog Format
|
||||
env:
|
||||
VERSION: ${{ steps.get_version.outputs.version }}
|
||||
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
|
||||
run: python .github/scripts/overwrite_changeset_changelog.py
|
||||
|
||||
# Commit and push changelog updates
|
||||
- name: Push Changelog updates to Pull Request
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email github-actions@github.com
|
||||
echo "Running git add and commit..."
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Updating CHANGELOG.md format"
|
||||
git status
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
echo "Pushing to remote..."
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
git push origin $CURRENT_BRANCH
|
||||
@@ -0,0 +1,83 @@
|
||||
name: CLI TUI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cli-tui-tests:
|
||||
name: CLI TUI Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build CLI
|
||||
run: npm run cli:build
|
||||
|
||||
- name: Run TUI Tests
|
||||
id: tui_tests
|
||||
run: |
|
||||
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
exit $exit_code
|
||||
|
||||
- name: Write failure summary
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
run: |
|
||||
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
if [ -f tui-test-output.log ]; then
|
||||
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload TUI traces
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tui-test-traces
|
||||
path: tests/e2e/cli/tui-traces/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload test log
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tui-test-log
|
||||
path: tui-test-output.log
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
@@ -55,7 +55,22 @@ jobs:
|
||||
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
|
||||
run: |
|
||||
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
|
||||
npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel
|
||||
max_attempts=3
|
||||
for attempt in $(seq 1 $max_attempts); do
|
||||
echo "::group::Attempt $attempt of $max_attempts"
|
||||
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
|
||||
echo "::endgroup::"
|
||||
echo "Smoke tests passed on attempt $attempt"
|
||||
exit 0
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "::error::Smoke tests failed after $max_attempts attempts"
|
||||
exit 1
|
||||
|
||||
- name: Generate summary
|
||||
if: always()
|
||||
|
||||
@@ -91,3 +91,21 @@ jobs:
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Post release to Slack
|
||||
uses: slackapi/slack-github-action@v3.0.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: "C0APVKGGZFC"
|
||||
text: "Cline CLI v${{ steps.version.outputs.version }}"
|
||||
blocks:
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: "*Cline CLI v${{ steps.version.outputs.version }}*"
|
||||
- type: "context"
|
||||
elements:
|
||||
- type: "mrkdwn"
|
||||
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>"
|
||||
|
||||
@@ -30,7 +30,11 @@ permissions:
|
||||
pull-requests: write # Required by nested reusable test workflow
|
||||
|
||||
jobs:
|
||||
cli-tui-tests:
|
||||
uses: ./.github/workflows/cli-tui-tests.yml
|
||||
|
||||
publish-main:
|
||||
needs: cli-tui-tests
|
||||
if: |
|
||||
github.repository == 'cline/cline' && (
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
@@ -44,6 +48,7 @@ jobs:
|
||||
confirm_publish: ${{ github.event.inputs.confirm_publish }}
|
||||
|
||||
publish-nightly:
|
||||
needs: cli-tui-tests
|
||||
if: |
|
||||
github.repository == 'cline/cline' && (
|
||||
github.event_name == 'schedule' ||
|
||||
|
||||
@@ -24,6 +24,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Check for recent commits
|
||||
run: |
|
||||
@@ -36,7 +38,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
@@ -47,6 +51,15 @@ jobs:
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Verify LFS media assets are resolved
|
||||
run: |
|
||||
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
|
||||
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
|
||||
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Publish Extension as Pre-release
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
|
||||
+107
-16
@@ -11,8 +11,13 @@ on:
|
||||
options:
|
||||
- pre-release
|
||||
- release
|
||||
auto_create_tag_from_main:
|
||||
description: "Auto-create and push the provided tag from the tested main commit (recommended)"
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
tag:
|
||||
description: "Enter existing tag to publish (e.g., v3.1.2)"
|
||||
description: "Tag to publish (required in both modes, e.g., v3.1.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -35,14 +40,74 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
lfs: true
|
||||
|
||||
- name: Resolve Release Tag
|
||||
id: resolve_tag
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
|
||||
TESTED_SHA="${{ github.sha }}"
|
||||
WORKFLOW_REF="${{ github.ref }}"
|
||||
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "Error: tag input is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
|
||||
echo "Error: tag must match vX.Y.Z (optionally with -suffix or .suffix)"
|
||||
exit 1
|
||||
fi
|
||||
TAG_REF="refs/tags/$TAG"
|
||||
|
||||
git fetch origin main --tags
|
||||
|
||||
if [[ "$AUTO_CREATE" == "true" ]]; then
|
||||
if [[ "$WORKFLOW_REF" != "refs/heads/main" ]]; then
|
||||
echo "Error: auto-create mode requires dispatching from main (current ref: $WORKFLOW_REF)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Auto-create enabled. Using tested workflow SHA: $TESTED_SHA"
|
||||
|
||||
if ! git merge-base --is-ancestor "$TESTED_SHA" origin/main; then
|
||||
echo "Error: tested SHA $TESTED_SHA is not on origin/main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git show-ref --verify --quiet "$TAG_REF"; then
|
||||
TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}")
|
||||
if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then
|
||||
echo "Error: tag '$TAG' already exists at $TAG_SHA, not at tested SHA ($TESTED_SHA)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag '$TAG' already exists at tested SHA. Continuing."
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$TAG" "$TESTED_SHA"
|
||||
git push origin "$TAG_REF"
|
||||
echo "Created and pushed tag '$TAG' from tested SHA $TESTED_SHA."
|
||||
fi
|
||||
else
|
||||
if ! git show-ref --verify --quiet "$TAG_REF"; then
|
||||
echo "Error: tag '$TAG' does not exist in the repository"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using existing tag '$TAG'."
|
||||
fi
|
||||
|
||||
git checkout --detach "$TAG_REF^{commit}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm install --include=optional
|
||||
@@ -59,20 +124,24 @@ jobs:
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Validate Tag
|
||||
id: validate_tag
|
||||
- name: Verify Tag Matches Package Version
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using existing tag: $TAG"
|
||||
|
||||
# Verify the tag exists
|
||||
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Error: Tag '$TAG' does not exist in the repository"
|
||||
TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
VERSION="v${{ steps.get_version.outputs.version }}"
|
||||
if [[ "$TAG" != "$VERSION" ]]; then
|
||||
echo "Error: tag '$TAG' does not match package version '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag and package version match: $TAG"
|
||||
|
||||
echo "Tag '$TAG' validated successfully"
|
||||
- name: Verify LFS media assets are resolved
|
||||
run: |
|
||||
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
|
||||
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
|
||||
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Package and Publish Extension
|
||||
env:
|
||||
@@ -103,7 +172,7 @@ jobs:
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.validate_tag.outputs.tag }}"
|
||||
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -119,12 +188,34 @@ jobs:
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.validate_tag.outputs.tag }}
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
files: "*.vsix"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.validate_tag.outputs.tag }}
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}
|
||||
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Post release to Slack
|
||||
uses: slackapi/slack-github-action@v3.0.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: "C0APVKGGZFC"
|
||||
text: "Cline ${{ steps.resolve_tag.outputs.tag }}"
|
||||
blocks:
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: "*Cline ${{ steps.resolve_tag.outputs.tag }}*"
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: ${{ toJSON(steps.changelog.outputs.content) }}
|
||||
- type: "context"
|
||||
elements:
|
||||
- type: "mrkdwn"
|
||||
text: "Full Changelog: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}"
|
||||
|
||||
@@ -20,7 +20,8 @@ jobs:
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/test-jetbrains'))
|
||||
contains(github.event.comment.body, '/test-jetbrains') &&
|
||||
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
|
||||
@@ -51,3 +51,8 @@ test-results
|
||||
|
||||
# Smoke test results (generated)
|
||||
evals/smoke-tests/results/
|
||||
|
||||
.tui-test
|
||||
secrets.json
|
||||
tui-traces
|
||||
tests/**/cache
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
-1
@@ -3,7 +3,11 @@
|
||||
"ts"
|
||||
],
|
||||
"spec": [
|
||||
"src/**/__tests__/*.ts"
|
||||
"src/**/__tests__/*.ts",
|
||||
"src/test/services/**/*.test.ts"
|
||||
],
|
||||
"ignore": [
|
||||
"src/sdk/**"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
|
||||
Vendored
+2
-1
@@ -16,7 +16,8 @@
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
"${workspaceFolder}",
|
||||
"--disable-extensions"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
|
||||
@@ -35,11 +35,9 @@ cli/**
|
||||
eslint-rules/
|
||||
old_docs/
|
||||
evals/
|
||||
.changie.yaml
|
||||
.codespellrc
|
||||
.mocharc.json
|
||||
buf.yaml
|
||||
.changeset/
|
||||
.clinerules/
|
||||
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
|
||||
+940
@@ -0,0 +1,940 @@
|
||||
# SDK Migration — Architecture & Design
|
||||
|
||||
Evergreen reference for the Cline SDK migration project. For the
|
||||
living implementation plan, see `migration.md`.
|
||||
|
||||
## References
|
||||
|
||||
### Code References
|
||||
|
||||
Cline SDK is at ~/clients/cline/sdk-wip
|
||||
Cline (core, classic VSCode extension, CLI) is at ~/clients/cline/cline
|
||||
JetBrains Plugin is at ~/clients/cline/intellij-plugin
|
||||
IntelliJ open source reference is at ~/clients/cline/intellij-community
|
||||
JCEF (Java-Chromium embedded framework reference) is at ~/clients/cline/jcef
|
||||
|
||||
You can use kb_search with these identifiers to understand the
|
||||
existing code and the SDK code:
|
||||
|
||||
cline - Cline core, classic VSCode extension, CLI
|
||||
sdk - Cline SDK
|
||||
plugin - JetBrains plugin
|
||||
vscode - Visual Studio Code opens source
|
||||
ij - IntelliJ open source
|
||||
jcef - JCEF IntelliJ's embedded Chromium layer
|
||||
|
||||
Prototype VSCode extension on SDK is at ~/clients/cline/sdk-vscode-sample
|
||||
Prototype JetBrains plugin on SDK is at ~/clients/cline/sdk-intellij-plugin-sample
|
||||
|
||||
These are prototypes with features missing and added, so refer to them
|
||||
as examples, but don't overindex on them.
|
||||
|
||||
### Documentation references
|
||||
|
||||
See the ~/clients/cline/cline/docs for extension product documentation
|
||||
and ~/clients/cline/sdk-wip/*.md for SDK documentation.
|
||||
|
||||
### Background on the products & architecture
|
||||
|
||||
There's a VSCode extension in cline/src. A large part of its UI is a
|
||||
React-based webview in cline/webview-ui.
|
||||
|
||||
There's a JetBrains plugin in the intellij-plugin repo. It packages
|
||||
the core of the VSCode extension, including the webview, and
|
||||
communicates with it with protobufs. There's a bunch of stuff in the
|
||||
cline repo called "standalone" which is what JetBrains communicates
|
||||
with.
|
||||
|
||||
In cline/cli there's a CLI and Kanban tool. Those use the SDK/are
|
||||
being ported separately to the SDK, so you don't need to worry about
|
||||
them. It is OK if you have to break them. Just ignore them.
|
||||
|
||||
Note, there was an earlier, failed attempt at a cli in go. The go cli
|
||||
used to use "standalone" like JetBrains. If there's any old go support
|
||||
cluttering up the repo, it is fine to delete it and clean it up.
|
||||
|
||||
The source code and docs mentioned above are the best reference to the
|
||||
product architecture, behavior, etc. Feel free to ask clarifying
|
||||
questions when necessary.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Features to remove
|
||||
|
||||
Terminal integration: There is legacy code in the VSCode extension,
|
||||
and stubs in the RPC system for JetBrains, which used the IDE's
|
||||
integrated terminals. We have decided we don't need these old modes
|
||||
and they should be removed. Instead we will rely on "background
|
||||
terminal". This literally means the node code forks and execs a shell
|
||||
and uses pipes to communicate with it.
|
||||
|
||||
Browser automation: Remove the system that uses Playwright to automate
|
||||
browsers. These use cases are now well served by third-party MCP
|
||||
tools.
|
||||
|
||||
"Shadow git" checkpointing system. This is too slow, especially on
|
||||
Windows. The way the Kanban project uses the existing git repo to
|
||||
store references, and only after each user message, is better. So we
|
||||
will drop the "shadow copy" git checkpointing system.
|
||||
|
||||
Memory bank, structured context: multi-file documentation
|
||||
(projectbrief, productContext, activeContext, systemPatterns,
|
||||
techContext, progress.)
|
||||
|
||||
Memory bank, persistence: cross-session context preservation.
|
||||
|
||||
Focus chain, task tracking: Auto-generated to-do list with real-time
|
||||
progress indicators.
|
||||
|
||||
Focus chain, integration: Editable focus chain integration with deep
|
||||
planning and reminders.
|
||||
|
||||
Deep planning exploration, output and the /deep-planning command.
|
||||
|
||||
Workflows (definition, natural language + XML tool syntax, MCP tools
|
||||
and user input prompts) ... these have been superceded by SKILLS
|
||||
moving forward.
|
||||
|
||||
Slash commands no longer necessary:
|
||||
|
||||
/deep-planning (codebase investigation + plan... we have plan/act mode)
|
||||
/reportbug (bug reporting with diagnostics)
|
||||
Custom workflows (/workflow.md for user-defined workflows... we have "skills" now.)
|
||||
|
||||
### Core features
|
||||
|
||||
These must work:
|
||||
|
||||
File operations: read, write, search, replace, list files, inspect
|
||||
code definitions (functions, classes, methods)
|
||||
|
||||
Terminal integration: "Background terminal" must work. The agent
|
||||
relies on this to run npm, git, docker, etc.
|
||||
|
||||
Multi-provider AI models: 30+ providers with seamless
|
||||
switching. There's one provider of note: VSCode has a provider which
|
||||
hooks up to Copilot using the VSCode LM Provider API. It would be good
|
||||
to support this *if possible.*
|
||||
|
||||
Auto-approve & YOLO mode
|
||||
- Granular per-tool permission controls
|
||||
- YOLO mode for maximum automation
|
||||
- Monitoring, notifications for long-running commands
|
||||
|
||||
Auto-compaction
|
||||
- Summarization automatically compresses conversations when context fills
|
||||
- Model support for Claude, Gemini, GPT-5, Grok, etc.
|
||||
|
||||
Subagents
|
||||
- Parallel execution of independent research agents
|
||||
- Isolation with separate context windows
|
||||
- Cost tracking for task usage per subagent
|
||||
|
||||
Web search and web fetch
|
||||
|
||||
Worktrees
|
||||
- git worktrees for parallel sessions
|
||||
- branch management and .worktreeinclude support
|
||||
- conflict resolution and merging
|
||||
|
||||
Workspaces
|
||||
- "multi-root" workspaces/projects with multiple root folders
|
||||
- @workspace:path scoped references
|
||||
|
||||
Jupyter Notebooks
|
||||
- Generate, explain, and improve notebook cells
|
||||
|
||||
Cline Rules
|
||||
- project-specific .cline/rules and global instructions.
|
||||
- Conditional logic: Path-based activation of rules.
|
||||
- Compatibility: Works with Cursor Rules, Windsurf rules, AGENTS.md
|
||||
|
||||
Skills
|
||||
- SKILL.md format with YAML frontmatter
|
||||
- Loading levels: Metadata, instructions and resources
|
||||
- Scope: Global and project-specific; toggleable
|
||||
|
||||
Hooks
|
||||
- Events: Task lifecycle + tool events (TaskStart, PreToolUse, etc.)
|
||||
- Runtimes: bash, powershell for Windows
|
||||
- IO: JSON
|
||||
- Context injection: Be able to modify or inject context dynamically
|
||||
|
||||
.clineignore
|
||||
- Exclusion rules, gitignore-style file/directory exclusion
|
||||
- Exceptions: ! prefix for overrides
|
||||
- Override behavior: Explicit @ mentions bypass ignore rules
|
||||
|
||||
MCP (Model Context Protocol)
|
||||
- Server management: Discovery, enable/disable, restart, config editing
|
||||
- Transport: stdio (local) and SSE (remote)
|
||||
- ...all the typical use cases for MCP: APIs, browser automation, db queries, etc.
|
||||
|
||||
### Core workflows
|
||||
|
||||
These must work:
|
||||
|
||||
Task Management
|
||||
|
||||
Task lifecycle - create and resume tasks; view task history
|
||||
Cost tracking - token using and cost monitoring per task
|
||||
|
||||
Plan & Act Mode
|
||||
Plan mode - Explore and investigate without modifying files
|
||||
Act mode - Implementation with approval gates
|
||||
Model config - Separate model configuration for plan and act mode if
|
||||
the user desires
|
||||
State persistence - Mode switching, task switching preserves history
|
||||
|
||||
File context (@-mentions)
|
||||
Context referencing - Reference files, folders, terminal output, git
|
||||
changes, URLs, commits via @
|
||||
|
||||
Slash commands
|
||||
/newtask (new task)
|
||||
/smol (compress history)
|
||||
/newrule (create rules)
|
||||
|
||||
### Model Configuration
|
||||
|
||||
We want to continue supporting our 30+ providers (Anthropic, OpenAI,
|
||||
OpenAI Codex, OpenRouter, Google Gemini, AWS Bedrock, DeepSeek,
|
||||
Cerebras, Qwen, Mistral, Groq, Fireworks, Together, xAI Grok,
|
||||
Moonshot, Nebius, HuggingFace, LiteLLM, Ollama, LM Studio, and more.)
|
||||
|
||||
THE MOST IMPORTANT REQUIREMENT HERE, after continuing to support them,
|
||||
is to USE THE CREDENTIALS, MODEL NAMES, CONFIGS, etc. WE HAVE
|
||||
SAVED. Logging people out of their providers is really annoying to
|
||||
users; regenerating API keys is painful for them.
|
||||
|
||||
VSCode LM API provider may be an interesting/unusual provider out of
|
||||
this set; it only works in VSCode by calling a specific API.
|
||||
|
||||
We must continue supporting local models like Ollama and LM Studio.
|
||||
|
||||
We must support the Cline provider with unified auth (open a
|
||||
webbrowser, handle the SSO redirect), built-in billing and credit
|
||||
display, banners advertising new or free models, stealth/early access
|
||||
models, organization switching.
|
||||
|
||||
### Enterprise Features
|
||||
|
||||
Security and governance
|
||||
- Client-side execution only (no data transmission outside of limited
|
||||
Telemetry and inference; no remote codebase indexing)
|
||||
- SSO role-based access control (member, admin, owner)
|
||||
- Model and tool controls per team
|
||||
- Remote configuration downloaded and applied by the extension
|
||||
|
||||
Observability
|
||||
- OpenTelemetry, Datadog, Grafana, Splunk integrations
|
||||
- Real-time analytics, cost breakdown by team, selective audit logging
|
||||
|
||||
Infrastructure
|
||||
- AWS Bedrock, Google Vertex AI, Azure OpenAI integration
|
||||
- Bring-your-own-inference with custom endpoints
|
||||
|
||||
### Priority "P1" (mid priority) items
|
||||
|
||||
Checkpoints - automatic file snapshots after each change. Note, the
|
||||
snapshot system in the VSCode extension and JetBrains plugin which
|
||||
copies the whole repository is slow, *especially on Windows*, so we
|
||||
should replace it with one that writes refs directly into the local
|
||||
git repo. Look at the way the kanban project does it; this is
|
||||
preferred (and maybe we should extract and share this code.)
|
||||
|
||||
Diffing - compare changes between checkpoints
|
||||
|
||||
Restore - restore files, task to a point, or both
|
||||
|
||||
MCP Marketplace - we could get rid of this, but ultimately we do want
|
||||
this feature with major improvements like allowing remote install. For
|
||||
now, consider removing it, but if it is easier to keep it around let's
|
||||
do that to lay the groundwork for improvements.
|
||||
|
||||
### Priority "P2" (lower priority) items
|
||||
|
||||
Task organization - favorites for task grouping and management
|
||||
|
||||
File context - Drag and drop files to add to context
|
||||
File context - actions - context menus to add to Cline, fix, explain, improve
|
||||
|
||||
Slash commands
|
||||
/explain-changes (git diff explanation)
|
||||
|
||||
---
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### Naming: "Sdk..." considered harmful
|
||||
|
||||
Do not name types "SdkFoo" or folders "sdk". If you need to use SdkFoo
|
||||
as a way to keep two classes around in parallel while you're porting,
|
||||
that's OK, but when this project is done we want to have one clean,
|
||||
simple codebase; the SDK backing is an implementation detail so just
|
||||
use simple noun phrases for classes, etc. and don't litter "Sdk" all
|
||||
over identifiers and folders.
|
||||
|
||||
### Proto deprecation and removal
|
||||
|
||||
We don't need proto files to describe webview messages. The webview
|
||||
and extension backend are both in TypeScript and are versioned and
|
||||
shipped together. We just need to use shared TypeScript interfaces
|
||||
between them.
|
||||
|
||||
We *also* don't need proto to describe JetBrains <--> node
|
||||
exchanges. We just need something typed and in sync between Kotlin and
|
||||
TypeScript that we can serialize. JSON probably makes sense. Protos
|
||||
are OK but we have had problems with that setup creating a ton of GC
|
||||
pressure on the Kotlin side, hit maximum message size limits, etc.
|
||||
|
||||
There are proto build steps which we can remove, as we use proto less.
|
||||
|
||||
protos are useful for state which is serialized. If there are files
|
||||
that are persisted described by protos, it is ok/good to keep
|
||||
them. Don't expand the use of protos to places protos are not already
|
||||
used.
|
||||
|
||||
### Web View UI
|
||||
|
||||
The Webview UI is very dependent on state arising from implementation
|
||||
details of the pre-SDK implementation. At the same time, we don't want
|
||||
to build a new UI from scratch right now because it may be forcing too
|
||||
many changes upon our users at once. So we aim to reuse the existing
|
||||
webview, but with radical simplificiation in its state management now
|
||||
that we will have a cleaner architecture in the extension "backend"
|
||||
with the layering enforced by the SDK.
|
||||
|
||||
The webview UI had defects like showing the wrong keybindings for
|
||||
JetBrains, or using tons of memory or CPU cycles by spamming state
|
||||
updates really rapidly or sending n^2 state updates as they streamed
|
||||
in. The first principle of this migration to the SDK is not get worse,
|
||||
but at the same time, we expect the state clean-up necessary in the
|
||||
webview will lead to radical simplifications which make some
|
||||
low-hanging fruit available in performance. It's great to go make
|
||||
those improvements where they are available.
|
||||
|
||||
We don't need this UI to be pixel perfect identical. We need it to be
|
||||
FAMILIAR, NOT WORSE and preferably BETTER than the status quo.
|
||||
|
||||
### Data formats, settings
|
||||
|
||||
We MUST pick up existing on-disk state for settings, etc. We don't
|
||||
want to log users out of their inference providers as we make this
|
||||
change to the SDK.
|
||||
|
||||
The CLI, VSCode extension and JetBrains extension largely share state
|
||||
on disk. We should continue that situation. If data migrations are
|
||||
necessary, that's fine, but design them with care. In particular, we
|
||||
want the long term to be fast, so we should write breadcrumbs
|
||||
indicating when migration is done. In addition, users can upgrade and
|
||||
downgrade their extension versions, etc. and we want to be robust to
|
||||
that in addition to all kinds of failures. For example, in the past we
|
||||
had problems where we overwrote a JSON settings file, perhaps racily,
|
||||
and left trailing }s in the file and this caused the product to
|
||||
totally fail. That's a very serious issue for our users so pay extra
|
||||
effort and attention to what is happening on disk. (We want PRACTICAL
|
||||
solutions and robustness and not performative solutions that just add
|
||||
tons of code and complexity with no real benefit.)
|
||||
|
||||
Invalidating old checkpoints is acceptable, unless it is particularly
|
||||
cheap to support the classic checkpoints. We won't be authoring those
|
||||
checkpoints any more, and it would be heavy to migrate them.
|
||||
|
||||
We want to move from .clinerules (old style) to .cline/rules (new style.)
|
||||
|
||||
### Telemetry
|
||||
|
||||
We generally want to continue sending the same Telemetry events. If
|
||||
that is hard, make a detailed report and we can follow up with our
|
||||
backend team. Note some enterprise features depend on OTEL
|
||||
observability.
|
||||
|
||||
### Code Sharing
|
||||
|
||||
In general we should share code between IDEs where there are benefits
|
||||
to do so. However trivial tools, or tools specific to a given IDE, can
|
||||
be wired up directly from the extension through to the SDK. (This is
|
||||
something that was hard to do in the old architecture and we would
|
||||
like to make easier.)
|
||||
|
||||
---
|
||||
|
||||
## Research Findings
|
||||
|
||||
### SDK Session Backend Extensibility
|
||||
|
||||
**Question**: Does the SDK's `SessionBackend` interface support
|
||||
storing arbitrary per-task data (e.g., tool settings, hook
|
||||
configuration, auto-approve preferences per task)?
|
||||
|
||||
**Answer**: Partially. The `SessionRow` has a `metadata:
|
||||
Record<string, unknown> | null` field that can store arbitrary
|
||||
key-value data per session. This is sufficient for per-task settings
|
||||
like auto-approve preferences, tool configuration, etc.
|
||||
|
||||
The SDK supports three backend implementations:
|
||||
1. `SqliteSessionStore` — SQLite-backed (default, preferred)
|
||||
2. `FileSessionService` — JSON file-backed (fallback when SQLite
|
||||
unavailable)
|
||||
3. `RpcCoreSessionService` — delegates to an RPC server
|
||||
|
||||
For our migration, we'll use either `FileSessionService` or provide a
|
||||
custom `SessionPersistenceAdapter` that reads/writes our existing task
|
||||
history format. The `ClineCoreOptions.sessionService` field accepts
|
||||
any backend implementing `CoreSessionService | RpcCoreSessionService |
|
||||
FileSessionService`.
|
||||
|
||||
**Key finding**: The `SessionPersistenceAdapter` interface is the
|
||||
cleanest extension point. It requires implementing: `ensureSessionsDir`,
|
||||
`upsertSession`, `getSession`, `listSessions`, `updateSession`,
|
||||
`deleteSession`, `enqueueSpawnRequest`, `claimSpawnRequest`. Our
|
||||
`LegacySessionBackend` adapter wraps the existing
|
||||
`~/.cline/data/tasks/` directory and `taskHistory` JSON array in
|
||||
`globalState.json`, mapping between `SessionRow` fields and our
|
||||
`HistoryItem` type:
|
||||
|
||||
```
|
||||
HistoryItem.id → SessionRow.sessionId
|
||||
HistoryItem.ts → SessionRow.startedAt (ISO string)
|
||||
HistoryItem.task → SessionRow.prompt
|
||||
HistoryItem.tokensIn → metadata.tokensIn
|
||||
HistoryItem.tokensOut → metadata.tokensOut
|
||||
HistoryItem.totalCost → metadata.totalCost
|
||||
HistoryItem.modelId → SessionRow.model
|
||||
HistoryItem.isFavorited → metadata.isFavorited
|
||||
```
|
||||
|
||||
Per-task files (`api_conversation_history.json`, `ui_messages.json`)
|
||||
map to `SessionRow.messagesPath` and `SessionRow.transcriptPath`.
|
||||
|
||||
**Decision**: We will provide a custom `SessionPersistenceAdapter`
|
||||
that translates between our existing format and the SDK's interface.
|
||||
No need for a separate sidecar storage layer. The `metadata` field
|
||||
handles all per-task extensions.
|
||||
|
||||
### Telemetry Event Mapping
|
||||
|
||||
The extension currently emits telemetry events via a PostHog-based
|
||||
`TelemetryService`. The SDK has its own `TelemetryService` with
|
||||
pluggable adapters (`OpenTelemetryAdapter`, `LoggerTelemetryAdapter`).
|
||||
|
||||
**Mapping of current extension events → SDK events:**
|
||||
|
||||
| Extension Event | SDK CORE_TELEMETRY_EVENTS | Notes |
|
||||
|---|---|---|
|
||||
| `user.extension_activated` | `CLIENT.STARTED` ("extension.activated") | ✅ Same event name |
|
||||
| `user.auth_started` | `USER.AUTH_STARTED` | ✅ Direct match |
|
||||
| `user.auth_succeeded` | `USER.AUTH_SUCCEEDED` | ✅ Direct match |
|
||||
| `user.auth_failed` | `USER.AUTH_FAILED` | ✅ Direct match |
|
||||
| `user.auth_logged_out` | `USER.AUTH_LOGGED_OUT` | ✅ Direct match |
|
||||
| `task.created` | `TASK.CREATED` | ✅ Direct match |
|
||||
| `task.restarted` | `TASK.RESTARTED` | ✅ Direct match |
|
||||
| `task.completed` | `TASK.COMPLETED` | ✅ Direct match |
|
||||
| `task.conversation_turn` | `TASK.CONVERSATION_TURN` | ✅ Direct match |
|
||||
| `task.tokens` | `TASK.TOKEN_USAGE` | ✅ Direct match |
|
||||
| `task.mode` | `TASK.MODE_SWITCH` | ✅ Direct match |
|
||||
| `task.tool_used` | `TASK.TOOL_USED` | ✅ Direct match |
|
||||
| `task.skill_used` | `TASK.SKILL_USED` | ✅ Direct match |
|
||||
| `task.diff_edit_failed` | `TASK.DIFF_EDIT_FAILED` | ✅ Direct match |
|
||||
| `task.provider_api_error` | `TASK.PROVIDER_API_ERROR` | ✅ Direct match |
|
||||
| `task.mention_used` | `TASK.MENTION_USED` | ✅ Direct match |
|
||||
| `task.mention_failed` | `TASK.MENTION_FAILED` | ✅ Direct match |
|
||||
| `task.mention_search_results` | `TASK.MENTION_SEARCH_RESULTS` | ✅ Direct match |
|
||||
| `task.subagent_started` | `TASK.SUBAGENT_STARTED` | ✅ Direct match |
|
||||
| `task.subagent_completed` | `TASK.SUBAGENT_COMPLETED` | ✅ Direct match |
|
||||
| `hooks.discovery_completed` | `HOOKS.DISCOVERY_COMPLETED` | ✅ Direct match |
|
||||
| `session.started` | `SESSION.STARTED` | ✅ Direct match |
|
||||
| `session.ended` | `SESSION.ENDED` | ✅ Direct match |
|
||||
|
||||
**Extension events with NO SDK equivalent (need adapter-layer emit):**
|
||||
|
||||
| Extension Event | Action |
|
||||
|---|---|
|
||||
| `user.opt_out` / `user.opt_in` | Emit via SDK's `captureRequired()` |
|
||||
| `user.telemetry_enabled` | Emit via SDK's `capture()` |
|
||||
| `user.extension_storage_error` | Emit via SDK's `capture()` |
|
||||
| `user.onboarding_progress` | Emit via SDK's `capture()` |
|
||||
| `workspace.*` (initialized, vcs_detected, etc.) | Emit via SDK's `capture()` |
|
||||
| `task.feedback` | Emit via SDK's `capture()` |
|
||||
| `task.option_selected` / `task.options_ignored` | Emit via SDK's `capture()` |
|
||||
| `task.checkpoint_used` | Emit via SDK's `capture()` |
|
||||
| `task.mcp_tool_called` | Emit via SDK's `capture()` |
|
||||
| `task.historical_loaded` | Emit via SDK's `capture()` |
|
||||
| `task.retry_clicked` | Emit via SDK's `capture()` |
|
||||
| `task.slash_command_used` | Emit via SDK's `capture()` |
|
||||
| `task.feature_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.rule_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.auto_condense_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.yolo_mode_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.terminal_*` (execution, output_failure, hang) | Emit via SDK's `capture()` |
|
||||
| `task.initialization` | Emit via SDK's `capture()` |
|
||||
| `task.summarize_task` | Emit via SDK's `capture()` |
|
||||
| `ui.*` (model_selected, button_clicked, etc.) | Emit via SDK's `capture()` |
|
||||
| `hooks.enabled` / `hooks.disabled` | Emit via SDK's `capture()` |
|
||||
| `hooks.cancel_requested` | Emit via SDK's `capture()` |
|
||||
| `hooks.context_modified` | Emit via SDK's `capture()` |
|
||||
| `worktree.*` | Emit via SDK's `capture()` |
|
||||
| `host.detected` | Emit via SDK's `capture()` |
|
||||
|
||||
**Extension events being REMOVED (features deleted):**
|
||||
|
||||
| Extension Event | Reason |
|
||||
|---|---|
|
||||
| `task.browser_tool_start/end/error` | Browser automation removed |
|
||||
| `task.focus_chain_*` (6 events) | Focus chain removed |
|
||||
| `task.workspace_search_pattern` | Folded into SDK search |
|
||||
| `task.subagent_enabled/disabled` | Toggle events; SDK manages directly |
|
||||
| `task.cline_web_tools_toggled` | Feature simplified |
|
||||
| `cline.grpc.response.size_bytes` | gRPC being removed |
|
||||
|
||||
**Metrics (OTEL counters/histograms):**
|
||||
|
||||
The extension has ~30 OTEL metrics (`cline.turns.total`,
|
||||
`cline.tokens.input.total`, `cline.api.ttft.seconds`, etc.). The SDK
|
||||
telemetry service supports `recordCounter`, `recordHistogram`, and
|
||||
`recordGauge`. We will emit these same metrics from the adapter layer
|
||||
using `telemetry.recordCounter()` / `telemetry.recordHistogram()`.
|
||||
The metric names can stay the same.
|
||||
|
||||
**Decision**: The SDK's `ITelemetryService.capture()` is a generic
|
||||
event emitter — we can emit ALL extension events through it. The
|
||||
adapter layer will create a thin telemetry wrapper that provides
|
||||
the same `captureTaskCreated()`, `captureToolUsage()`, etc. methods
|
||||
but delegates to the SDK's telemetry service. Events where the SDK
|
||||
already has a helper function (listed in the first table) use those
|
||||
directly. Others use `capture({ event, properties })`. No backend
|
||||
team coordination needed for the initial migration.
|
||||
|
||||
### JetBrains IPC Design
|
||||
|
||||
#### Current Architecture
|
||||
|
||||
```
|
||||
┌── Kotlin Plugin ──────────────────────────────┐
|
||||
│ │
|
||||
│ CoreProcessManager │
|
||||
│ └─ launches Node.js process (cline-core) │
|
||||
│ └─ communicates via gRPC ProtoBus │
|
||||
│ (port 26040-26340) │
|
||||
│ │
|
||||
│ HostBridgeService (gRPC server, port 26041) │
|
||||
│ ├─ DiffService │
|
||||
│ ├─ WindowService (show file, open dialog) │
|
||||
│ ├─ WorkspaceService (paths, diagnostics) │
|
||||
│ ├─ EnvService │
|
||||
│ └─ TestingService (get webview HTML) │
|
||||
│ │
|
||||
│ ProtoBusProxyService │
|
||||
│ └─ proxies webview ↔ cline-core gRPC │
|
||||
│ │
|
||||
│ JsPostMessageHandler │
|
||||
│ └─ injects JS bridge into JCEF webview │
|
||||
│ └─ converts postMessage → gRPC request │
|
||||
│ │
|
||||
│ WebViewManager │
|
||||
│ └─ loads webview HTML in JCEF │
|
||||
│ └─ receives gRPC responses → postMessage │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Problems with this architecture:
|
||||
- **Proto size limits**: gRPC messages hit 256MB limits with large
|
||||
conversations. The `ProtoBusProxyService` logs warnings at 10MB+.
|
||||
- **Java heap pressure**: Serializing/deserializing large proto
|
||||
messages stresses the JVM heap.
|
||||
- **Build complexity**: Proto compilation required for both
|
||||
TypeScript and Java/Kotlin.
|
||||
- **Stateless-in-theory**: The design is somewhat stateless but we
|
||||
haven't leveraged restart-for-reliability because state
|
||||
reconstruction is expensive.
|
||||
|
||||
#### Target Architecture
|
||||
|
||||
```
|
||||
┌── Kotlin Plugin ──────────────────────────────┐
|
||||
│ │
|
||||
│ CoreProcessManager │
|
||||
│ └─ launches SDK sidecar (Node.js) │
|
||||
│ └─ communicates via JSON-RPC over stdio │
|
||||
│ │
|
||||
│ HostCallbackService (JSON-RPC server) │
|
||||
│ ├─ showTextDocument, openDialog │
|
||||
│ ├─ getWorkspacePaths, getDiagnostics │
|
||||
│ ├─ getEnvVars, clipboard │
|
||||
│ └─ (extensible for PSI, run configs, etc.) │
|
||||
│ │
|
||||
│ WebviewBridge │
|
||||
│ └─ receives JSON messages from sidecar │
|
||||
│ └─ forwards to JCEF via executeJavaScript │
|
||||
│ └─ receives postMessage from JCEF │
|
||||
│ └─ forwards to sidecar via stdio │
|
||||
│ │
|
||||
│ WebViewManager │
|
||||
│ └─ loads adapted webview in JCEF │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
┌── SDK Sidecar (Node.js) ──────────────────────┐
|
||||
│ │
|
||||
│ SidecarMain │
|
||||
│ └─ JSON-RPC over stdio (bidirectional) │
|
||||
│ └─ imports @clinebot/core │
|
||||
│ └─ shares SDK adapter layer with VSCode │
|
||||
│ │
|
||||
│ ClineCore instance │
|
||||
│ └─ session management │
|
||||
│ └─ tool execution │
|
||||
│ └─ provider handling │
|
||||
│ │
|
||||
│ HostCallbackClient │
|
||||
│ └─ calls back to Kotlin for IDE ops │
|
||||
│ └─ registered as tool executors in SDK │
|
||||
│ │
|
||||
│ WebviewBridge │
|
||||
│ └─ translates SDK events → webview messages │
|
||||
│ └─ same code as VSCode adapter │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### IPC Mechanism: JSON-RPC over stdio
|
||||
|
||||
**Why JSON-RPC over stdio instead of gRPC:**
|
||||
- **No message size limits**: JSON over stdio has no inherent size
|
||||
cap. Conversations with 100K+ tokens serialize to ~5-20MB JSON
|
||||
which flows fine over pipes.
|
||||
- **No heap pressure**: No proto serialization on the Java side.
|
||||
Kotlin reads/writes JSON strings directly. JCEF already works
|
||||
with JSON.
|
||||
- **No proto compilation**: Eliminates the Java protobuf dependency
|
||||
and the dual TypeScript/Java proto generation step.
|
||||
- **Simple**: Well-understood protocol. Easy to debug (just read
|
||||
the pipe).
|
||||
|
||||
**Protocol**: JSON-RPC 2.0 over stdin/stdout with newline-delimited
|
||||
JSON messages. The sidecar reads from stdin and writes to stdout.
|
||||
Stderr is reserved for logging.
|
||||
|
||||
```
|
||||
→ {"jsonrpc":"2.0","method":"session/start","params":{...},"id":1}
|
||||
← {"jsonrpc":"2.0","result":{"sessionId":"..."},"id":1}
|
||||
← {"jsonrpc":"2.0","method":"webview/message","params":{"type":"assistant_delta","text":"..."}}
|
||||
```
|
||||
|
||||
**Notifications** (no `id`) are used for streaming events
|
||||
(assistant deltas, tool events, state updates). The Kotlin plugin
|
||||
processes these and forwards them to the JCEF webview.
|
||||
|
||||
**Callbacks** from sidecar → Kotlin (host operations) use
|
||||
JSON-RPC requests in the reverse direction:
|
||||
|
||||
```
|
||||
← {"jsonrpc":"2.0","method":"host/showTextDocument","params":{"path":"..."},"id":100}
|
||||
→ {"jsonrpc":"2.0","result":{"success":true},"id":100}
|
||||
```
|
||||
|
||||
#### Code Sharing Between VSCode, JetBrains, and CLI
|
||||
|
||||
The shared SDK adapter layer contains:
|
||||
|
||||
```
|
||||
src/sdk-adapter/
|
||||
index.ts — ClineSdkHost (creates ClineCore instance)
|
||||
session-backend.ts — LegacySessionBackend adapter
|
||||
webview-bridge.ts — SDK events → webview message translation
|
||||
provider-migration.ts — Legacy provider settings migration
|
||||
approval-adapter.ts — Auto-approve settings → SDK tool policies
|
||||
telemetry-adapter.ts — Extension telemetry → SDK telemetry
|
||||
types.ts — WebviewInbound, WebviewOutbound types
|
||||
```
|
||||
|
||||
Each host then has a thin integration layer:
|
||||
|
||||
- **VSCode** (`src/hosts/vscode/sdk-extension.ts`): In-process.
|
||||
Creates `ClineSdkHost`, registers VSCode LM handler, manages
|
||||
webview lifecycle. Uses `postMessage` for webview communication.
|
||||
|
||||
- **JetBrains** (`src/sidecar/main.ts`): Separate process. Creates
|
||||
`ClineSdkHost`, reads/writes JSON-RPC on stdio. Registers
|
||||
`HostCallbackClient` for IDE operations. The webview bridge code
|
||||
is identical — it just sends messages over stdio instead of
|
||||
`postMessage`.
|
||||
|
||||
#### Statefulness and Reliability
|
||||
|
||||
The sidecar is **stateful** — it holds the `ClineCore` instance with
|
||||
active sessions in memory. However, it is designed for **graceful
|
||||
restart**:
|
||||
|
||||
- **Session persistence**: All session state is written to disk
|
||||
after each turn (messages, manifest, transcript). On restart, the
|
||||
sidecar re-reads the session index and can resume.
|
||||
- **Crash detection**: The Kotlin plugin monitors the sidecar
|
||||
process. If it exits unexpectedly, the plugin restarts it after a
|
||||
brief delay (same as current `CoreProcessManager.RESTART_DELAY`).
|
||||
- **Smaller messages**: Because the protocol is JSON-RPC with
|
||||
incremental streaming (notifications for each delta), the
|
||||
messages are much smaller than the current gRPC approach which
|
||||
sends full state snapshots. This eliminates the heap pressure
|
||||
that made the current system unreliable.
|
||||
- **Interrupted operations**: If the sidecar crashes mid-turn, the
|
||||
next startup detects the unfinished session (status = "running"
|
||||
but no live process) and marks it as interrupted, just like the
|
||||
current task resumption flow.
|
||||
|
||||
#### JetBrains-Specific Tools
|
||||
|
||||
The HostCallback pattern makes it easy to add JetBrains-specific
|
||||
capabilities without changing shared code:
|
||||
|
||||
1. **Registration**: The sidecar's `HostCallbackClient` declares
|
||||
what capabilities the host supports (e.g., `"psi"`, `"runConfigs"`).
|
||||
2. **Tool Executors**: JetBrains-specific tool executors are
|
||||
registered in `ClineCoreOptions.defaultToolExecutors` when the
|
||||
sidecar starts. For example, a `getDiagnostics` executor that
|
||||
calls `host/getDiagnostics` via JSON-RPC to get IntelliJ's PSI
|
||||
analysis results.
|
||||
3. **No shared code changes**: Adding a new JetBrains capability
|
||||
requires:
|
||||
- Implementing the handler in Kotlin (`HostCallbackService`)
|
||||
- Adding a JSON-RPC method in the sidecar's `HostCallbackClient`
|
||||
- Optionally registering a custom tool executor
|
||||
|
||||
Example for exposing JetBrains PSI:
|
||||
```kotlin
|
||||
// Kotlin side
|
||||
"host/getPsiStructure" -> {
|
||||
val file = PsiManager.getInstance(project).findFile(virtualFile)
|
||||
// ... extract structure
|
||||
respondWithJson(result)
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
// Sidecar side - registered as a custom tool executor
|
||||
defaultToolExecutors: {
|
||||
list_code_definition_names: async (args) => {
|
||||
// Call back to JetBrains for richer PSI-based results
|
||||
const result = await hostCallback.call("host/getPsiStructure", { path: args.path });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Current Architecture
|
||||
```
|
||||
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
|
||||
│ WebviewProvider │ │ Kotlin Plugin │ │ React Ink │
|
||||
│ Controller │ │ CoreProcessManager │ │ ClineAgent │
|
||||
│ Task │ │ ProtoBusProxy │ │ │
|
||||
│ API providers (30+) │ │ JCEF WebView │ │ │
|
||||
│ McpHub │ │ │ │ │
|
||||
│ Webview (React) │ │ ↓ gRPC │ │ │
|
||||
│ │ │ cline-core │ │ │
|
||||
│ proto/cline/*.proto │ │ (standalone Node) │ │ │
|
||||
└────────────────────────┘ └──────────────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Target Architecture
|
||||
```
|
||||
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
|
||||
│ SDK Adapter Layer │ │ Kotlin Plugin │ │ React Ink │
|
||||
│ @clinebot/core │ │ │ │ TUI │
|
||||
│ (in-process) │ │ ↓ JSON-RPC/stdio │ │ │
|
||||
│ Webview (adapted) │ │ SDK sidecar (Node) │ │ │
|
||||
│ │ │ @clinebot/core │ │ │
|
||||
│ registerHandler │ │ │ │ │
|
||||
│ ("vscode-lm", ...) │ │ Webview (adapted) │ │ │
|
||||
└────────────────────────┘ └──────────────────────┘ └─────────────┘
|
||||
|
||||
All clients backed by:
|
||||
@clinebot/core → @clinebot/agents → @clinebot/llms
|
||||
↓ ↓ ↓
|
||||
Sessions Tools/Hooks Providers
|
||||
Storage MCP Bridge Model Catalog
|
||||
Telemetry Teams/Spawn Handler Registry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What the SDK Already Provides
|
||||
|
||||
These capabilities exist in the SDK and do not need to be rebuilt:
|
||||
|
||||
1. **Legacy provider settings migration** —
|
||||
`migrateLegacyProviderSettings()` reads `globalState.json` +
|
||||
`secrets.json`, writes to `providers.json`. Handles Anthropic,
|
||||
OpenAI, OpenAI Codex OAuth, OpenRouter, Bedrock, custom
|
||||
OpenAI-compatible endpoints, etc. Existing providers are never
|
||||
overwritten. Migrated entries are tagged `tokenSource: "migration"`.
|
||||
|
||||
2. **30+ provider handlers** — Anthropic, OpenAI (chat + responses
|
||||
API), Google Gemini, AWS Bedrock, Vertex AI, DeepSeek, Ollama,
|
||||
LM Studio, Mistral, Groq, Fireworks, Together, xAI, Cerebras,
|
||||
LiteLLM, Nebius, HuggingFace, and more.
|
||||
|
||||
3. **Custom handler registry** — `registerHandler(id, factory)` and
|
||||
`registerAsyncHandler(id, factory)` for providers that need
|
||||
host-specific dependencies (e.g., VSCode LM API).
|
||||
|
||||
4. **MCP management** — `InMemoryMcpManager` with stdio, SSE, and
|
||||
streamableHttp transports. Config loader reads from
|
||||
`~/.cline/data/settings/mcp.json` with Zod validation. Supports
|
||||
legacy format migration.
|
||||
|
||||
5. **Tool framework** — 8 built-in tools: `read_files`,
|
||||
`search_codebase`, `run_commands`, `editor`, `apply_patch`,
|
||||
`fetch_web_content`, `skills`, `ask_question`. Preset system with
|
||||
`development` (act mode) and `readonly` (plan mode) presets.
|
||||
Per-tool enable/disable. Policy-based approval (auto-approve,
|
||||
require-approval, per-tool overrides). Model-aware tool routing
|
||||
(e.g., OpenAI models use `apply_patch` instead of `editor`).
|
||||
|
||||
6. **Session lifecycle** — `ClineCore.create()` → `host.start()` /
|
||||
`host.send()` / `host.abort()` / `host.stop()` / `host.subscribe()`
|
||||
Interactive mode with prompt queueing (`queue`/`steer` delivery).
|
||||
Event subscription for streaming.
|
||||
|
||||
7. **Telemetry** — `TelemetryService` with pluggable adapters:
|
||||
`OpenTelemetryAdapter` (for enterprise OTEL), `LoggerTelemetryAdapter`.
|
||||
Standard events: `session.started`, `session.ended`,
|
||||
`task.created`, `task.conversation_turn`, `task.tool_used`, etc.
|
||||
See "Telemetry Event Mapping" above for full mapping.
|
||||
|
||||
8. **Rules & Skills** — Discovery from `.clinerules/`,
|
||||
`~/Documents/Cline/Rules`, `~/.cline/data/settings/rules/`.
|
||||
SKILL.md format with YAML frontmatter. Global and project scopes.
|
||||
|
||||
9. **Hooks** — `HookEngine` with lifecycle events. Node subprocess
|
||||
hook helpers for bash/powershell execution.
|
||||
|
||||
10. **Subagents/Teams** — `AgentTeamsRuntime`, spawn tools, team
|
||||
coordination with concurrent teammate agents.
|
||||
|
||||
11. **System prompt generation** — `getClineDefaultSystemPrompt()`
|
||||
with platform-aware customization.
|
||||
|
||||
12. **OAuth token management** — `RuntimeOAuthTokenManager` handles
|
||||
automatic token refresh during sessions for OAuth providers
|
||||
(Cline, OpenAI Codex).
|
||||
|
||||
13. **Storage isolation** — `CLINE_DIR`, `CLINE_DATA_DIR`,
|
||||
`CLINE_SESSION_DATA_DIR` environment variables plus
|
||||
`setClineDir()` / `setHomeDir()` APIs for test isolation.
|
||||
|
||||
---
|
||||
|
||||
## Test Strategy
|
||||
|
||||
See `migration.md` for the phase-by-phase test plan. This section
|
||||
covers the evergreen test infrastructure and classification.
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
**Extension unit tests** use Mocha with a custom `requires.ts` that
|
||||
mocks `vscode` and `@integrations/checkpoints` modules. Config in
|
||||
`.mocharc.json`. These tests run without VSCode.
|
||||
|
||||
**Webview tests** use Vitest with React Testing Library. Independent
|
||||
from the extension — they test React components in isolation.
|
||||
|
||||
**E2E tests** use Playwright to drive a real VSCode instance with
|
||||
the extension loaded. They test chat, auth, diff editing, and editor
|
||||
integration against a mock API server on localhost:7777.
|
||||
|
||||
**SDK adapter tests** use Vitest (simpler setup, better TypeScript
|
||||
support, no need for vscode-mock since adapter layer is
|
||||
VSCode-independent). Config in `vitest.config.sdk.ts`.
|
||||
|
||||
### SDK Storage Isolation for Tests
|
||||
|
||||
The SDK fully supports isolated test environments via environment
|
||||
variables and API calls:
|
||||
|
||||
```typescript
|
||||
import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
|
||||
const tempHome = mkdtempSync(join(tmpdir(), "test-home-"));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.CLINE_DIR = join(tempHome, ".cline");
|
||||
process.env.CLINE_DATA_DIR = join(tempHome, ".cline", "data");
|
||||
setHomeDir(tempHome);
|
||||
setClineDir(process.env.CLINE_DIR);
|
||||
```
|
||||
|
||||
### Test Framework Decisions
|
||||
|
||||
- **Keep Mocha** for extension unit tests (existing infrastructure)
|
||||
- **Keep Vitest** for webview and CLI tests
|
||||
- **Keep Playwright** for VSCode E2E tests
|
||||
- **Add Vitest** for new SDK adapter tests
|
||||
- **Isolated home directories** for all new tests touching storage
|
||||
|
||||
---
|
||||
|
||||
## Manual QA Guide
|
||||
|
||||
This section is for the QA team. It describes what has been removed
|
||||
(so you don't file bugs for missing features) and what areas carry
|
||||
the most regression risk after the SDK migration.
|
||||
|
||||
### Removed Features — Do Not File Bugs
|
||||
|
||||
- **Browser automation** — The built-in Playwright browser tool is
|
||||
gone. Users should use third-party MCP browser tools instead.
|
||||
- **IDE terminal integration** — Commands now run exclusively in a
|
||||
"background terminal" (headless shell). No terminal tab opens.
|
||||
- **Shadow-git checkpoints** — Existing checkpoints are invalidated.
|
||||
- **Memory bank / structured context** — All memory bank files and
|
||||
UI removed.
|
||||
- **Focus chain** — No focus chain panel or inline indicators.
|
||||
- **Deep planning / `/deep-planning`** — Plan/Act mode remains as
|
||||
the replacement.
|
||||
- **`/reportbug`** — Removed.
|
||||
- **Workflows** — Skills (SKILL.md format) are the replacement.
|
||||
- **Custom workflow slash commands** — Skills replace this.
|
||||
|
||||
### Risk Areas — VSCode Extension
|
||||
|
||||
1. **Provider credentials & model selection** — Verify existing API
|
||||
keys survive the upgrade and downgrade.
|
||||
2. **Cline provider OAuth / SSO** — Test sign-in, sign-out, token
|
||||
refresh, org switching.
|
||||
3. **Chat streaming & message display** — Watch for missing/duplicated
|
||||
messages, broken streaming, performance regressions.
|
||||
4. **Tool approval flow** — Verify auto-approve, YOLO mode, per-tool
|
||||
permissions.
|
||||
5. **Plan/Act mode** — Verify toggling, separate model configs,
|
||||
state persistence.
|
||||
6. **Task history & resume** — Old tasks appear, can be resumed; new
|
||||
tasks are saved.
|
||||
7. **MCP servers** — Existing configs picked up, tools work.
|
||||
8. **VSCode LM provider (Copilot)** — Verify it still works.
|
||||
9. **Settings UI** — All toggles and inputs persist correctly.
|
||||
10. **Webview performance** — Long conversations should not cause
|
||||
sluggishness.
|
||||
|
||||
### Risk Areas — JetBrains Extension
|
||||
|
||||
1. **Sidecar process lifecycle** — Starts reliably, auto-restarts,
|
||||
shuts down cleanly.
|
||||
2. **Webview communication** — Messages arrive, state is fresh.
|
||||
3. **Large conversations** — 100K+ tokens without OOM.
|
||||
4. **Host operations** — Open file, diagnostics, clipboard all work.
|
||||
5. **Keybindings** — Correct for the JetBrains platform.
|
||||
|
||||
### Risk Areas — CLI
|
||||
|
||||
1. **Agent backend replacement** — Core loop works in TUI and
|
||||
headless modes.
|
||||
2. **Provider & model picker** — All providers appear, defaults
|
||||
correct.
|
||||
3. **Shared state with IDE clients** — Credentials and history
|
||||
visible across clients.
|
||||
4. **Slash commands** — Removed commands don't appear; remaining
|
||||
ones work.
|
||||
5. **Worktrees & `--cwd`** — Function correctly.
|
||||
6. **ACP (Agent Communication Protocol)** — Programmatic usage works.
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# SDK Migration Caveats & Known Issues
|
||||
|
||||
Tracking issues found during the migration from the legacy inference system to the ClineCore SDK.
|
||||
|
||||
## Status Legend
|
||||
- 🔴 **Blocker** — prevents core functionality
|
||||
- 🟡 **Minor** — cosmetic or UX annoyance
|
||||
- 🟢 **Fixed** — resolved
|
||||
|
||||
NOTE:
|
||||
|
||||
1. Use your debugging tool (DEBUG-HARNESS.md) to reproduce issues.
|
||||
2. Use your debugging tool to validate your fixes.
|
||||
3. Commit one verified change together.
|
||||
4. Work on these in any order you prefer.
|
||||
|
||||
---
|
||||
|
||||
🟢 Under accounts, the when logged in the "current balance" is ---- and
|
||||
the reload button does nothing. **Fixed:** getUserCredits handler fetches
|
||||
balance from Cline API using stored auth token.
|
||||
|
||||
🟢 Under accounts, the "cline environment" dropdown doesn't change from
|
||||
production when you select "staging" or "local". **Fixed:** state-builder
|
||||
now reads `clineEnv` from globalState and maps to Environment enum;
|
||||
updateSettings handler persists clineEnv and clears auth on change.
|
||||
|
||||
🟢 Under accounts, the logout button does nothing. **Fixed:**
|
||||
accountLogoutClicked handler clears auth credentials from disk.
|
||||
|
||||
🟢 Reportedly under accounts you can't sign in. **Fixed:**
|
||||
accountLoginClicked handler (was STUB) now opens the Cline login page
|
||||
in the browser.
|
||||
|
||||
🔴 When you have a low credit balance, even after you change accounts
|
||||
(for example from one "org" to another) or refreshing you keep getting
|
||||
an error "Insufficient balance. Your Cline Credits balance ..."
|
||||
|
||||
🔴 In chat, a chat response has a "copy" button that is
|
||||
obscured/partially obscured by the last generated code block. In the
|
||||
classic extension, this appears with enough space around it to be
|
||||
visible.
|
||||
|
||||
🟢 Changing the model during a conversation does not, *apparently*,
|
||||
change the model used for inference. **Fixed:** updateSettings now
|
||||
updates the in-memory apiConfiguration (not just disk) so
|
||||
model/provider changes take effect immediately for subsequent sessions.
|
||||
|
||||
🔴 The OpenAI compatible provider produces "404 404 page not found"
|
||||
errors.
|
||||
|
||||
🔴 When running tools (for example, prompt the agent to use kb_status)
|
||||
output rectangles appear but they are blank.
|
||||
|
||||
🔴 When prompted with multiple-step work (like 1. Do this 2. Do that)
|
||||
the chat displays "0/0 TODOs".
|
||||
|
||||
🟡 Checkpoints appear in options, but checkpoints don't appear in
|
||||
chats; we need to overhaul the checkpoints system anyway see
|
||||
ARCHITECTURE.md.
|
||||
|
||||
🟢 In the history section, you can't mark chats as favorites.
|
||||
|
||||
🟡 Banners (for example "Try Claude Sonnet 4.6") can be dismissed, but
|
||||
there are no < and > buttons visible to page between them.
|
||||
|
||||
🟢 "Add to Cline" right click menu (use the command to trigger it)
|
||||
does not do anything. **Fixed:** sendAddToInputEvent now falls back to
|
||||
the SDK bridge's pushAddToInput when no classic gRPC subscriptions are
|
||||
active; WebviewGrpcBridge handles subscribeToAddToInput streaming and
|
||||
sends via both gRPC response and typed message.
|
||||
|
||||
🟢 When a task is cancelled, you can't enter a new chat and send that
|
||||
chat in addition. (The repro is: Run a task, click cancel relatively
|
||||
quickly, type a new prompt, try to hit enter/click the arrow.) **Fixed:**
|
||||
cancelTask now clears currentSession after abort so subsequent
|
||||
askResponse calls start a new task instead of sending to the aborted
|
||||
session.
|
||||
|
||||
🟢 MCP Servers tab never finishes loading (may be a workos: token
|
||||
prefix problem?) **Fixed:** subscribeToMcpServers now sends initial
|
||||
server data as a typed message (mcpServers) instead of only via gRPC
|
||||
streaming response, which the webview's dual-listen pattern picks up.
|
||||
|
||||
🟢 Attached images (via drag and drop or the + icon to attach an image
|
||||
file) aren't submitted to models. **Fixed:** newTask and askResponse
|
||||
now include the images array in ClineMessage objects so attached images
|
||||
appear in the chat UI and are passed to the SDK session.
|
||||
|
||||
🔴 Changing the account profile in the accounts tab (for example from
|
||||
Cline External, which has budget, to Cline Internal Testing Org, which
|
||||
doesn't) doesn't switch to that profile for inference.
|
||||
|
||||
🟡 Account panel may show logged-out state on launch despite being
|
||||
logged in. Inference still works. The `subscribeToAuthStatusUpdate`
|
||||
streaming subscription in the webview may not be established before
|
||||
the bridge pushes initial auth data, causing a race condition. On most
|
||||
launches the auth state loads correctly (verified via debug harness),
|
||||
but the user reports intermittent occurrences.
|
||||
|
||||
🔴 "Sign up with Cline" button does not do the IDE login flow — it
|
||||
opens the dashboard (`https://app.cline.bot/login`) instead. In
|
||||
origin/main, `accountLoginClicked` calls
|
||||
`AuthService.createAuthRequest()` which starts a local HTTP server for
|
||||
the OAuth callback, calls the Cline API auth endpoint with the
|
||||
callback URL, and opens the resulting OAuth redirect URL. The SDK does
|
||||
not have access to `AuthService` or `HostProvider.getCallbackUrl()`,
|
||||
so it falls back to opening the dashboard URL directly. Users who are
|
||||
not logged in cannot authenticate through the extension UI.
|
||||
**Requires:** SDK support for OAuth callback flows (see
|
||||
SDK-FEATURE-REQUESTS.md).
|
||||
|
||||
🔴 Buttons in the MCP Servers popup do nothing. The restart (🔄),
|
||||
enable/disable toggle, and delete (🔴) buttons on individual MCP
|
||||
servers are all no-ops. The gRPC handler stubs these methods:
|
||||
`restartMcpServer`, `deleteMcpServer`, `toggleMcpServer`,
|
||||
`toggleToolAutoApprove`, `authenticateMcpServer`, `updateMcpTimeout`.
|
||||
In origin/main these go through `controller.mcpHub` which manages live
|
||||
MCP server connections. The SDK reads MCP settings from disk but does
|
||||
not expose server lifecycle management to the webview. See SDK-MCP.md
|
||||
for details, we need to implement much more elaborate MCP support
|
||||
client side to work with the SDK, via a custom RuntimeBuilder and tool
|
||||
client factory that supports streamable HTTP; watches the file for
|
||||
changes and either restarts a session or causes the tool definitions
|
||||
to change; etc.
|
||||
|
||||
🔴 Buttons in the MCP Servers → Configure tab do nothing. Same root
|
||||
cause as above — the configure tab shows servers (e.g. "linear",
|
||||
"kamibiki") with restart/toggle/delete controls, but all interactions
|
||||
are stubbed. The "Configure MCP Servers" and "Advanced MCP Settings"
|
||||
links also depend on `openMcpSettings` which may or may not be wired.
|
||||
|
||||
🟡 MCP Marketplace never loads. The Marketplace tab shows "No MCP
|
||||
servers found in the marketplace". **Partial fix applied:**
|
||||
`subscribeToMcpMarketplaceCatalog` in `webview-grpc-bridge.ts` now
|
||||
reads from the disk cache (`~/.cline/data/cache/mcp_marketplace_catalog.json`)
|
||||
via `readMcpMarketplaceCatalogFromCache()` and pushes the catalog to
|
||||
the webview as a streaming response. This works if the cache was
|
||||
previously populated by the classic extension. However,
|
||||
`refreshMcpMarketplace` (which fetches fresh data from the API) is
|
||||
still stubbed because it requires an authenticated API call to
|
||||
`https://api.cline.bot/v1/mcp/marketplace`. If no cache file exists
|
||||
(fresh install), the marketplace will remain empty.
|
||||
**Note:** Could not validate with debug harness since it runs the
|
||||
classic extension, not the SDK adapter.
|
||||
+207
@@ -1,5 +1,212 @@
|
||||
# Changelog
|
||||
|
||||
## [3.77.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add "Lazy Teammate Mode" experimental toggle
|
||||
- `read_file` tool now supports chunked reading for targeted file access
|
||||
|
||||
### Fixed
|
||||
|
||||
- Exclude `new_task` tool from system prompt in yolo/headless mode
|
||||
- Fix Kanban demo video formatting
|
||||
|
||||
### Changed
|
||||
|
||||
- Polish `Notification` hook functionality
|
||||
|
||||
## [3.76.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Cline Kanban launch modal in webview; CLI now launches Kanban by default with a migration view
|
||||
- Add toggle to disable feature tips in chat
|
||||
- Add repeated tool call loop detection to prevent infinite loops wasting tokens
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CLI Kanban spawn on Windows by enabling shell mode for `npx.cmd`
|
||||
|
||||
## [3.75.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Latency improvements for remote workspaces
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stabilize flaky hooks tests
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove example hooks in favor of reading the docs
|
||||
|
||||
## [3.74.0]
|
||||
|
||||
### Added
|
||||
- Implement dynamic free model detection for Cline API
|
||||
- Add file read deduplication cache to prevent repeated reads
|
||||
- Add feature tips tooltip during thinking state
|
||||
|
||||
### Fixed
|
||||
- Replace error message when not logged in to Cline
|
||||
- Align ClineRulesToggleModal padding with ServersToggleModal
|
||||
- Skip WebP for GLM and Devstral models running through llama.cpp
|
||||
- Respect user-configured context window in LiteLLM getModel()
|
||||
- Honor explicit model IDs outside static catalog in W&B provider
|
||||
- Add missing Fireworks serverless models and pricing
|
||||
|
||||
## [3.73.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added W&B Inference by CoreWeave as a new API provider with 17 models
|
||||
- Improved parallel tool calling support for OpenRouter and Cline providers
|
||||
|
||||
### Fixed
|
||||
|
||||
- Claude Code Provider: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
|
||||
- Tool handlers (`read_file`, `list_files`, `list_code_definition_names`, `search_files`) now return graceful errors instead of crashing
|
||||
|
||||
## [3.72.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Anthropic Opus 4.6 fast mode variants
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolved native tool placeholder interpolation in prompts
|
||||
- Gemini: capped Flash output tokens to 8192 across providers
|
||||
- Fixed Windows unit test path normalization
|
||||
- Fixed flaky hooks tests on Windows
|
||||
- Bedrock: handle thinking and redacted_thinking blocks correctly in message conversion and streaming
|
||||
- Prevent crash when `list_files` or `list_code_definition_names` receives a file path
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Jupyter Notebook GIFs
|
||||
- Markdown image loading now requires user consent
|
||||
- Added `.github/copilot-instructions.md` for coding agents
|
||||
- Hooks: reintroduced feature toggle
|
||||
|
||||
## [3.71.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added GPT-5.4 models for ChatGPT subscription users
|
||||
- Hooks: Added a `Notification` hook for attention and completion boundaries
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handle streamable HTTP MCP reconnects more reliably after disconnects
|
||||
|
||||
## [3.70.0]
|
||||
|
||||
### Added
|
||||
|
||||
- New Cline API docs: Getting Started, Auth, Chat Completions, Models, Errors, and SDK Examples
|
||||
- Hook payloads now include `model.provider` and `model.slug`
|
||||
- Token/cost updates now happen immediately as usage chunks arrive, not after tool execution
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve subagent context compaction logic
|
||||
- Subagent stream retry delay increased to reduce noise from transient failures
|
||||
- State serialization errors are now caught and logged instead of crashing
|
||||
- Removed incorrect `max_tokens` from OpenRouter requests
|
||||
|
||||
### Changed
|
||||
|
||||
- Windows test cleanup now retries on locked files and applies per-test timeouts
|
||||
- Updated hooks docs
|
||||
|
||||
|
||||
## [3.69.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add `User-Agent` header to requests sent to the Cline backend
|
||||
- Add default auto-tag workflow for publish release flow
|
||||
- Show Cline SDK docs on the Cline page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Retry nested git restore and prevent silent `.git_disabled` leftovers in checkpoints
|
||||
- Prevent Chinese filename escaping in diff view
|
||||
- Trigger auto-compaction on OpenRouter context overflow errors
|
||||
- Restore GPT-OSS native file editing on OpenAI-compatible models
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Cline SDK docs
|
||||
- Improve hooks support for Windows PowerShell
|
||||
|
||||
## [3.68.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add dynamic Cline provider model fetching from Cline endpoint
|
||||
- Add additional Markdown formatting in CLI
|
||||
- Add focus indicator on action buttons in extension
|
||||
|
||||
### Fixed
|
||||
|
||||
- Clear all OCA secrets on auth refresh failure to prevent re-auth loops
|
||||
- Resolve "Could not find the file context" error in Explain Changes
|
||||
- Use `JSON_SCHEMA` for `yaml.load` to prevent unsafe deserialization
|
||||
- Fetch model info from API in CLI headless auth for Cline and Vercel providers
|
||||
- Generate commit message from staged changes only when staging exists
|
||||
- Update stale `maxTokens` values for Claude 3.7+ models across Anthropic, Bedrock, Vertex, and SAP AI Core
|
||||
- Use `model.info.maxTokens` for OpenRouter instead of hardcoded `8192`
|
||||
|
||||
### Changed
|
||||
|
||||
- Increase timeout for a flaky test to reduce short-term test instability
|
||||
|
||||
## [3.67.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
|
||||
- Added Codex 5.3 model support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OpenAI Codex by setting `store` to `false`
|
||||
- Use `isLocatedInPath()` instead of string matching for path containment checks
|
||||
|
||||
## [3.67.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for skills and optional modelId in subagent configuration
|
||||
- Add AgentConfigLoader for file-based agent configs
|
||||
- Add Responses API support for OpenAI native provider
|
||||
- Preconnect websocket to reduce response latency
|
||||
- Fetch featured models from backend with local fallback
|
||||
- Add /q command to quit CLI
|
||||
- Add MCP enterprise configuration details
|
||||
- Pull Cline's recommended models from internal endpoint
|
||||
- Add dynamic flag to adjust banner cache duration
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix reasoning delta crash on usage-only stream chunks
|
||||
- Fix OpenAI tool ID transformation restricted to native provider only
|
||||
- Fix auth check for ACP mode
|
||||
- Fix CLI yolo mode to not persist yolo setting to disk
|
||||
- Fix inline focus-chain slider within its feature row
|
||||
- Fix Gemini 3.1 Pro compatibility
|
||||
- Fix Cline auth with ACP flag
|
||||
|
||||
### Changed
|
||||
|
||||
- Move PR skill to .agents/skills
|
||||
- SambaNova provider: update models list
|
||||
- Remove changeset-converter GitHub Action and npm run changeset
|
||||
|
||||
## [3.66.0]
|
||||
|
||||
### Added
|
||||
|
||||
+6
-25
@@ -57,25 +57,11 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
### Creating a Pull Request
|
||||
|
||||
1. Before creating a PR, generate a changeset entry:
|
||||
```bash
|
||||
npm run changeset
|
||||
```
|
||||
This will prompt you for:
|
||||
- Type of change (major, minor, patch)
|
||||
- `major` → breaking changes (1.0.0 → 2.0.0)
|
||||
- `minor` → new features (1.0.0 → 1.1.0)
|
||||
- `patch` → bug fixes (1.0.0 → 1.0.1)
|
||||
- Description of your changes
|
||||
1. Commit your changes.
|
||||
|
||||
2. Commit your changes and the generated `.changeset` file
|
||||
|
||||
3. Push your branch and create a PR on GitHub. Our CI will:
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
- Changesetbot will create a comment showing the version impact
|
||||
- When merged to main, changesetbot will create a Version Packages PR
|
||||
- When the Version Packages PR is merged, a new release will be published
|
||||
4. Testing
|
||||
3. Testing
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
@@ -192,15 +178,10 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
- Temporary workspaces with test fixtures
|
||||
- Video recording for failed tests
|
||||
|
||||
4. **Version Management with Changesets**
|
||||
4. **Versioning & Changelog Notes**
|
||||
|
||||
- Create a changeset for any user-facing changes using `npm run changeset`
|
||||
- Choose the appropriate version bump:
|
||||
- `major` for breaking changes (1.0.0 → 2.0.0)
|
||||
- `minor` for new features (1.0.0 → 1.1.0)
|
||||
- `patch` for bug fixes (1.0.0 → 1.0.1)
|
||||
- Write clear, descriptive changeset messages that explain the impact
|
||||
- Documentation-only changes don't require changesets
|
||||
- Contributors do not need to create changelog-entry files as part of PRs.
|
||||
- Maintainers handle release versioning and changelog curation during the release process.
|
||||
|
||||
5. **Commit Guidelines**
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# E2E Debugging Visibility
|
||||
|
||||
This is a sub-project of the project described in migration.md. We are
|
||||
engaging in a big change to the VSCode extension. You gain visibility
|
||||
into the extension through tests, but sometimes that is not enough.
|
||||
|
||||
Your goal is to create way where you can launch the VSCode extension
|
||||
and have access to the node debugger and webview debugger so that you
|
||||
can set break points, evaluate expressions, inject input (consider
|
||||
Microsoft's work with Playwright in VSCode, but anything that works is
|
||||
fine) step, etc. so that you can observe execution and find and fix
|
||||
problems without the tedious cycle off adding print statements,
|
||||
running a test which may hang, fixing something, removing the print
|
||||
statements, etc.
|
||||
|
||||
For this step to be complete, you need to demonstrate you have the
|
||||
ability to:
|
||||
|
||||
1. Build and run the VSCode extension in an unminified form, including
|
||||
the webview unminified.
|
||||
|
||||
2. Add and remove breakpoints, including conditional breakpoints, on
|
||||
the extension side.
|
||||
|
||||
3. Add and remove breakpoints, including conditional breakpoints, on
|
||||
the webview side.
|
||||
|
||||
4. Evaluate expressions at breakpoints. You should be able to refer to
|
||||
local variables, that is, the extension code should be
|
||||
unminified. (Concatenated is fine as long as you can find your way
|
||||
around.)
|
||||
|
||||
5. Run, step at breakpoints.
|
||||
|
||||
6. Generate UI actions like opening the Cline sidebar, focusing
|
||||
elements, typing, etc.
|
||||
|
||||
7. Take screenshots that you can view.
|
||||
|
||||
You need to use this tool inside your agentic loop, that is, you will
|
||||
need to drive both of these debugees simultaneously from one loop, so
|
||||
you may need to write yourself a tool which blocks until one of the
|
||||
debugees hits a breakpoint; can use a timeout and let you break and
|
||||
examine isolates and stacks; things of that nature.
|
||||
|
||||
We are working on macOS, it is fine if this tool just works on macOS
|
||||
for now.
|
||||
|
||||
## Caveats
|
||||
|
||||
- **`CLINE_DIR` environment variable**: Because the debug harness
|
||||
spawns a fresh VSCode instance that runs the Cline extension, and
|
||||
because *you* (the agent) share state with that extension (API keys,
|
||||
provider settings, task history), you **must** set `CLINE_DIR=~/.cline`
|
||||
when launching the harness. Without it the debugee uses an isolated
|
||||
data directory and won't have your API keys or provider configuration,
|
||||
causing inference to fail silently (requests hang or error).
|
||||
```bash
|
||||
CLINE_DIR=~/.cline npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
```
|
||||
|
||||
- **⚠️ "Introducing Cline Kanban" promotion — DISMISS FIRST**: On
|
||||
fresh launches, a full-screen promotional overlay ("Introducing Cline
|
||||
Kanban") may appear in the sidebar webview. It obscures all other UI
|
||||
elements, so screenshots will show only the promo and interactions
|
||||
with the chat input, settings buttons, etc. will fail. **You must
|
||||
dismiss it immediately after opening the sidebar, before doing
|
||||
anything else.** This is easy to forget — if your screenshots look
|
||||
wrong or interactions fail, this is almost certainly why.
|
||||
|
||||
**Method 1 — Click the close button via DOM** (most reliable):
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "web.evaluate",
|
||||
"params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}
|
||||
}'
|
||||
```
|
||||
This finds the `<span class="sr-only">Close</span>` element and
|
||||
clicks its parent `<button>`.
|
||||
|
||||
**Method 2 — Press ESC** (simpler but less reliable):
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "web.evaluate",
|
||||
"params": {"expression": "document.activeElement.dispatchEvent(new KeyboardEvent(\"keydown\", {key: \"Escape\", code: \"Escape\", keyCode: 27, bubbles: true}))"}
|
||||
}'
|
||||
```
|
||||
|
||||
If neither works, take a screenshot (`ui.screenshot`) to see what's
|
||||
on screen and identify the current dismiss control.
|
||||
|
||||
- **Screenshots**: `ui.screenshot` and `ui.sidebar_screenshot` save
|
||||
PNG files to `/tmp/cline-debug/` and return `{path}` in the JSON
|
||||
response. **Do NOT open the screenshot file with `open`** — on macOS
|
||||
this launches Preview.app which covers the VSCode window you're
|
||||
debugging. Use `read_file` on the returned path to examine the image
|
||||
without disrupting the debuggee.
|
||||
|
||||
- **Debuggee launch delay**: The post-launch activation delay is 1
|
||||
second. If the extension hasn't fully loaded by the time you interact
|
||||
with it, just retry — `ui.open_sidebar` and `findSidebar` have their
|
||||
own internal polling with timeouts.
|
||||
|
||||
- **Top toolbar buttons**: The "new task", "mcp servers", "history",
|
||||
"accounts", and "settings" toolbar buttons are not in the webview DOM
|
||||
(they're VSCode UI chrome). Instead of trying to click them, run the
|
||||
associated VSCode commands directly:
|
||||
- New Task: `cline.plusButtonClicked`
|
||||
- MCP Servers: `cline.showMcpServers`
|
||||
- History: `cline.showHistory`
|
||||
- Accounts: `cline.showAccount`
|
||||
- Settings: `cline.openSettings`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "ui.command_palette",
|
||||
"params": {"command": "cline.openSettings"}
|
||||
}'
|
||||
```
|
||||
|
||||
- **CDP disconnects after window reload**: If you use
|
||||
`workbench.action.reloadWindow` (e.g., to pick up a rebuilt
|
||||
webview), the extension host CDP connection drops. You must do a full
|
||||
`shutdown` + relaunch of the debug harness to reconnect.
|
||||
|
||||
## References
|
||||
|
||||
You can see the vscode source code in ~/clients/cline/vscode and
|
||||
search it with kb_search vscode
|
||||
|
||||
You can search the cline source code (snapshot) with kb_search cline.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
These are issues the agent claims to have fixed, but should be verified:
|
||||
|
||||
## Fixed
|
||||
|
||||
### 1. 🟢 Remote inference fails with ECONNREFUSED (localhost:4000)
|
||||
**File:** `src/sdk/cline-session-factory.ts`
|
||||
**Symptom:** Sending any message with the Cline provider fails after 6 retries with "Cannot connect to API" / ECONNREFUSED to `http://localhost:4000/v1/chat/completions`.
|
||||
**Root cause:** Base URL overrides (`openAiBaseUrl`, `openRouterBaseUrl`, `liteLlmBaseUrl`) were applied unconditionally to ALL providers, clobbering the Cline API URL. A stale `openAiBaseUrl: "http://localhost:4000/v1"` in `~/.cline/data/globalState.json` overwrote `https://api.cline.bot/api/v1`.
|
||||
**Fix:** Guard each base URL override with a provider check so it only applies to its respective provider.
|
||||
|
||||
---
|
||||
|
||||
### 2. 🟢 Task history not persisted after completion
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** After a task completes successfully, the task did NOT appear in the RECENT section when returning to the home screen.
|
||||
**Root cause:** `newTask()` created a `currentTaskItem` but never pushed it to `this.taskHistory[]`. `clearTask()` discarded it without saving. No disk persistence implementation existed.
|
||||
**Fix:** `SdkController` now persists tasks on three paths: (1) `done` event updates `currentTaskItem` with final usage and calls `persistCurrentTask()`, (2) `clearTask()` calls `persistCurrentTask()` before resetting, (3) `cancelTask()` persists the in-progress task. `LegacyStateReader` gained `saveTaskHistory()`, `saveUiMessages()`, and `deleteTaskDirectory()` methods for disk I/O.
|
||||
|
||||
### 3. 🟢 Task resumption not implemented
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** Cannot resume a previous task from history.
|
||||
**Fix:** `showTaskWithId()` now finds the task in history, loads saved UI messages via `legacyState.readUiMessages()`, restores them into the translator, and sets `currentTaskItem`. The task view renders with full message history.
|
||||
|
||||
### 6. 🟢 Settings persistence is best-effort / incomplete
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** `updateSettings()` was a no-op stub with a TODO comment.
|
||||
**Fix:** `updateSettings()` now persists settings to `globalState.json` via `legacyState.saveApiConfiguration()`. `updateAutoApprovalSettings()` also persists via the same mechanism.
|
||||
|
||||
### 7. 🟢 Completed task not appearing in RECENT section
|
||||
**Where:** Home screen → RECENT section
|
||||
**Symptom:** After completing a task and clicking "New Task", the completed task did not appear in the RECENT history list.
|
||||
**Fix:** Resolved by issue #2 fix — tasks are now persisted to `taskHistory` on completion, so they appear in RECENT.
|
||||
|
||||
### 8. 🟢 Top bar buttons are non-functional
|
||||
**Where:** Header bar — accounts, settings, new chat, history buttons
|
||||
**Symptom:** Clicking any of the top bar buttons (accounts icon, settings gear, new chat +, task history) does nothing. No navigation occurs, no panels open.
|
||||
**Root cause:** gRPC stub. The webview subscribes to `subscribeToSettingsButtonClicked`, etc. — these are event streams pushed from the extension host when VSCode title bar buttons are clicked.
|
||||
**Fix:** Extension.ts button commands now send typed `navigate` messages via `WebviewGrpcBridge.navigate()`, bypassing gRPC streaming subscriptions. Plus button also calls `clearSdkTask()` to reset the SDK session.
|
||||
|
||||
### 9. 🟢 @ mentions / autocomplete not working
|
||||
**Where:** Chat input textarea
|
||||
**Symptom:** Typing `@` in the chat input does not trigger any autocomplete dropdown. No filename suggestions, no context items offered.
|
||||
**Root cause:** gRPC stub. The `@` autocomplete calls `FileServiceClient.searchFiles()` to get matching file paths.
|
||||
**Fix:** Implemented `searchFiles` handler in grpc-handler.ts that delegates to `SdkController.searchFiles()`, which does a real filesystem walk of the workspace directory (max depth 8, skips node_modules/.git/etc). Returns results with `mentionsRequestId` for proper request correlation.
|
||||
|
||||
### 10. 🟢 Add files/images button (+) does nothing
|
||||
**Where:** Bottom bar, "+" button next to chat input
|
||||
**Symptom:** Clicking the "+" button to add files and images produces no response — no file picker, no dropdown, no action.
|
||||
**Root cause:** gRPC stub. The button calls `FileServiceClient.selectFiles()` which opens a native file picker dialog.
|
||||
**Fix:** Implemented `selectFiles` handler that returns `StringArrays` format (`values1` = image data URLs, `values2` = file paths). VscodeWebviewProvider callback reads image files as base64 data URLs and returns relative paths for non-images.
|
||||
|
||||
### 11. 🟢 Cannot switch from Plan mode back to Act mode
|
||||
**Where:** Bottom bar Plan/Act toggle
|
||||
**Symptom:** Clicking "Plan" successfully switches to Plan mode. However, clicking "Act" after that does NOT switch back to Act mode.
|
||||
**Root cause:** Proto enum conversion bug — the webview sends numeric enum values (0=PLAN, 1=ACT) but the handler expected string values.
|
||||
**Fix:** `handleTogglePlanActMode` now converts proto enum values: `0/"PLAN" → "plan"`, `1/"ACT" → "act"`, with fallback for already-converted string values.
|
||||
|
||||
### 12. 🟢 "Manage cline rules and workflows" still mentions workflows
|
||||
**Where:** ClineRulesToggleModal tooltip and aria-label
|
||||
**Symptom:** The tooltip and aria-label still said "Manage Cline Rules & Workflows".
|
||||
**Fix:** Updated tooltip to "Manage Cline Rules" and aria-label to "Show/Hide Cline Rules". Also simplified chat placeholder text to remove "workflows" mention.
|
||||
|
||||
### 4. 🟢 Input text not cleared immediately on send
|
||||
**Where:** Webview chat input
|
||||
**Symptom:** After typing a message and pressing send/enter, the text remains visible in the input field briefly before clearing. Creates a feeling of lag.
|
||||
**Root cause:** In `useMessageHandlers.ts`, `setInputValue("")` was called AFTER `await TaskServiceClient.newTask(...)` or `await TaskServiceClient.askResponse(...)` completed. The network round-trip caused visible delay before the input cleared.
|
||||
**Fix:** Moved `setInputValue("")`, `setActiveQuote(null)`, `setSelectedImages([])`, `setSelectedFiles([])` to execute immediately when `hasContent` is true, before any async gRPC calls. React schedules a re-render synchronously, clearing the input before the network round-trip.
|
||||
|
||||
### 5. 🟢 api_req_started fires with zeroed token counts
|
||||
**Where:** Message stream / ChatRow rendering
|
||||
**Symptom:** An `api_req_started` partial message fires with `{"tokensIn":0,"tokensOut":0,"cost":0}` before real counts arrive, causing a brief flash of "0 / 200.0k" in the token usage bar.
|
||||
**Root cause:** `ContextWindow.tsx` rendered the token bar whenever `tokenData` existed (i.e., when `contextWindow > 0`), regardless of whether `lastApiReqTotalTokens` was 0.
|
||||
**Fix:** Added `tokenData.used === 0` guard to the null-return check in `ContextWindow.tsx`. The token bar now only renders when real (non-zero) token data is available.
|
||||
|
||||
### 14. 🟢 "Delete chat" button shows placeholder size
|
||||
**Where:** Task history → delete button tooltip / label
|
||||
**Symptom:** The "Delete chat" button tooltip displays `Delete Task (size: --)` when task size data is unavailable.
|
||||
**Root cause:** `DeleteTaskButton.tsx` unconditionally rendered `(size: ${taskSize ? formatSize(taskSize) : "--"})`, showing "--" when `taskSize` is undefined.
|
||||
**Fix:** Changed to conditionally include size: `taskSize ? \`Delete Task (${formatSize(taskSize)})\` : "Delete Task"`. The tooltip now shows just "Delete Task" when size is unavailable, or "Delete Task (12.4 KB)" when it is.
|
||||
|
||||
### 13. 🟢 Terminal settings navigates to blank/stuck webview
|
||||
**Where:** Settings → Terminal tab
|
||||
**Symptom:** Opening terminal settings causes React to crash, leaving a blank webview.
|
||||
**Root cause:** `getAvailableTerminalProfiles` was a gRPC stub returning `{data:{}}`. The webview called `setAvailableTerminalProfiles(response.profiles)` where `response.profiles` was `undefined`, overwriting the default `[]`. Then `TerminalSettingsSection` called `profilesToShow.map()` on `undefined`, crashing React.
|
||||
**Fix:** Implemented real `handleGetAvailableTerminalProfiles()` handler in grpc-handler.ts that calls `getAvailableTerminalProfiles()` from `utils/shell.ts`, returning platform-specific profiles (Default, zsh, bash on macOS). Also added `availableTerminalProfiles: []` to state-builder.ts as a safety net, and wired `scrollToSettings` to fire `navigate("settings", { targetSection })` via the bridge.
|
||||
**Verified:** Debug harness confirmed handler returns `{data:{profiles:[{id:"default",...},{id:"zsh",...},{id:"bash",...}]}}`, Settings → Terminal tab renders "Default Terminal Profile" dropdown with all 3 options, shell integration timeout, and terminal reuse settings.
|
||||
|
||||
### 16. 🟢 Cline Rules popup still has a "Workflows" tab
|
||||
**Where:** Scales-of-justice icon → Cline Rules modal
|
||||
**Symptom:** The "Manage Cline Rules" popup contains a "Workflows" tab. Issue #12 fixed the tooltip text, but the tab itself still exists inside the modal.
|
||||
**Root cause:** The `ClineRulesToggleModal` component had a full "Workflows" tab with toggle lists for global, local, and remote workflows, plus a description section. Workflows are no longer a feature.
|
||||
**Fix:** Removed the Workflows tab button, workflows description text, workflows content section (remote/global/local workflow toggle lists), and the remote workflows banner condition from `ClineRulesToggleModal.tsx`. The `currentView` state type was narrowed from `"rules" | "workflows" | "hooks" | "skills"` to `"rules" | "hooks" | "skills"`.
|
||||
|
||||
### 17. 🟢 Account pane shows "Sign up with Cline" despite being logged in
|
||||
**Where:** Account panel / pane
|
||||
**Symptom:** Even when the user is already authenticated and logged in, the account pane still displays "Sign up with Cline" and other sign-up prompts as if the user were not authenticated.
|
||||
**Root cause:** `subscribeToAuthStatusUpdate` is a streaming subscription. The bridge's `handleStreamingRequest()` fell into the `default` no-op case, so auth state was never pushed to the webview.
|
||||
**Fix:** Added explicit `subscribeToAuthStatusUpdate` case in `handleStreamingRequest()` that reads auth credentials from disk and pushes them. Added `roles` to org data and null safety in `isAdminOrOwner()`.
|
||||
|
||||
### 18. 🟢 "Sign up with Cline" button does nothing (moot)
|
||||
**Where:** Account pane → Sign up button
|
||||
**Fix:** Resolved by #17 — the sign-up button is no longer shown when the user is already authenticated.
|
||||
|
||||
### 19. 🟢 Terminal settings still shows "Terminal Execution Mode" option
|
||||
**Where:** Settings → Terminal
|
||||
**Fix:** Removed the Terminal Execution Mode dropdown, its handler, and unused imports from `TerminalSettingsSection.tsx`.
|
||||
|
||||
### 20. 🟢 Cline provider model type-ahead search does not work
|
||||
**Where:** Settings → Model selector (Cline provider)
|
||||
**Root cause:** `refreshClineModelsRpc` was a gRPC stub returning `{}`. The webview never received any model data.
|
||||
**Fix:** Implemented `handleRefreshClineModels()` in grpc-handler.ts that reads from disk cache first, then falls back to fetching from the Cline API using `globalThis.fetch`. Converts API response to `ModelInfo` records and returns in protobuf format.
|
||||
|
||||
### 21. 🟢 Cline provider recommends possibly outdated model
|
||||
**Where:** Settings → Model selector (Cline provider)
|
||||
**Fix:** Updated fallback recommendation text in `ClineModelPicker.tsx` from `anthropic/claude-sonnet-4.5` to `anthropic/claude-sonnet-4.6`.
|
||||
|
||||
### 22. 🟢 "Use different models for Plan and Act" checkbox immediately unchecks
|
||||
**Where:** Settings → Model configuration
|
||||
**Root cause:** `updateSettings()` was writing raw settings instead of merging individual known keys.
|
||||
**Fix:** `updateSettings()` now iterates known settings keys and writes each one individually. `buildExtensionState()` reads `planActSeparateModels` from `globalState`.
|
||||
|
||||
### 23. 🟢 MCP settings Configure tab crashes React
|
||||
**Where:** MCP Servers → Configure tab
|
||||
**Root cause:** `refreshMcpMarketplace` stub returns `{}`, replacing the default `{ items: [] }` state, causing `items.find()` to crash.
|
||||
**Fix:** Added optional chaining (`?.items?.find`) in `getMcpServerDisplayName()`.
|
||||
|
||||
### 24. 🟢 History tab is empty and search does nothing
|
||||
**Where:** History tab (task history list)
|
||||
**Root cause:** `handleGetTaskHistory()` returned `{ data: { history } }` but webview reads `response.tasks`.
|
||||
**Fix:** Changed return to `{ data: { tasks, totalCount } }`. Implemented server-side filtering/sorting.
|
||||
|
||||
### 25. 🟢 Auto-approve options immediately uncheck when toggled
|
||||
**Where:** Auto-approve options flyout
|
||||
**Root cause:** Same as #22 — `updateAutoApprovalSettings()` was not persisting properly.
|
||||
**Fix:** Fixed alongside #22.
|
||||
|
||||
---
|
||||
|
||||
### 15. 🟢 MCP tools are missing / not visible to the agent
|
||||
**Where:** Agent tool execution
|
||||
**Symptom:** MCP tools that should be available to the agent are not discovered or listed. The agent cannot see or use any MCP-provided tools during task execution.
|
||||
**Root cause:** `ClineCoreSession` in `cline-session-factory.ts` didn't pass MCP configuration through `coreConfig` when calling `host.start()`. The MCP settings file existed at `~/.cline/data/settings/cline_mcp_settings.json` but the session factory never wired MCP servers into the ClineCore session.
|
||||
**Fix:** Added `getOrCreateMcpManager()` to `cline-session-factory.ts` that reads MCP server registrations via `resolveMcpServerRegistrations()`, creates an `InMemoryMcpManager` with a client factory using `@modelcontextprotocol/sdk` (supporting stdio, streamableHttp, and SSE transports), connects to all non-disabled servers, generates `Tool[]` via `createMcpTools()`, and passes them as `extraTools` in `coreConfig`. The MCP manager is cached across sessions (servers are long-lived processes). Connection has a 30s timeout to avoid blocking session start. Individual server connection failures are logged but don't prevent other servers or the session from starting.
|
||||
**Verified:** Debug harness confirmed agent lists `kamibiki__kb_search`, `kamibiki__kb_status`, `kamibiki__kb_index` from the kamibiki MCP server, and successfully invoked `kamibiki__kb_status` returning real indexing data (6 repos, 1M+ embeddings).
|
||||
|
||||
---
|
||||
|
||||
### 27. 🟢 Banners (e.g., "Try Claude Sonnet 4.6") can't be dismissed
|
||||
**Where:** Home screen → banner carousel
|
||||
**Symptom:** Clicking the X dismiss button on any banner does nothing — the banner remains visible and reappears on reload.
|
||||
**Root cause:** Two issues: (1) `state-builder.ts` hardcoded `dismissedBanners: undefined` instead of reading from globalState, so dismissed banners were never communicated to the webview. (2) `grpc-handler.ts` wrote dismissed banner IDs as plain strings instead of the `{ bannerId, dismissedAt }` objects the webview expects.
|
||||
**Fix:** State builder now reads `dismissedBanners` from globalState with `normalizeDismissedBanners()` that handles both legacy plain strings and new objects. Handler writes proper `{ bannerId, dismissedAt }` objects and normalizes legacy entries on read.
|
||||
**Verified:** Debug harness confirmed banners dismiss correctly — carousel shrinks as each banner is dismissed and stays dismissed across reloads.
|
||||
|
||||
### 28. 🟢 Can't mark chats as favorites in history
|
||||
**Where:** History tab → star button on task items
|
||||
**Symptom:** Clicking the star icon on a history item does nothing — the favorite state never changes.
|
||||
**Root cause:** Proto field name mismatch in `handleToggleTaskFavorite()`: handler read `request.params?.id` and `request.params?.isFavorite`, but the webview sends `taskId` and `isFavorited` (proto field names from `TaskFavoriteRequest`).
|
||||
**Fix:** Handler now reads both proto names (`taskId`/`isFavorited`) with fallback to legacy names (`id`/`isFavorite`).
|
||||
|
||||
### 29. 🟢 Copy button obscured by last code block in chat
|
||||
**Where:** Chat response text with code blocks
|
||||
**Symptom:** The response copy button overlaps with the last code block, making it hard to see and click.
|
||||
**Root cause:** `CopyButton.tsx` positioned the bottom-right copy button at `bottom-1` (4px from bottom edge), which overlapped with code block content. No padding existed between the markdown content and the button.
|
||||
**Fix:** Changed position to `bottom-2.5` (10px clearance) and added `pb-4` padding to the chat text content wrapper for code block clearance.
|
||||
|
||||
### 30. 🟢 Current balance shows "----" / reload button does nothing
|
||||
**Where:** Account pane → credit balance display
|
||||
**Symptom:** The current balance always shows "----" and the reload button has no effect.
|
||||
**Root cause:** `getUserCredits` handler returned `{ credits: undefined }` instead of calling the Cline API. The webview reads `response.balance.currentBalance` which was always undefined.
|
||||
**Fix:** `getUserCredits` and `getOrganizationCredits` now fetch real balance data from the Cline API using the stored auth token (`Bearer` header). Includes 10s timeout and error handling.
|
||||
**Tested:** Integration tests with mock HTTP server verify real balance data flows through correctly (7 tests).
|
||||
|
||||
### 31. 🟢 Logout button does nothing
|
||||
**Where:** Account pane → logout button
|
||||
**Symptom:** Clicking logout has no effect — the user remains logged in.
|
||||
**Root cause:** `accountLogoutClicked` was a STUB (silent no-op) in grpc-handler.
|
||||
**Fix:** Implemented `handleAccountLogout()` that calls `LegacyStateReader.clearClineAuthInfo()` to remove `cline:clineAccountId` from secrets.json, then pushes state update so the webview shows the sign-in view.
|
||||
**Tested:** Integration test verifies credentials are cleared from disk and auth status shows unauthenticated after logout.
|
||||
|
||||
### 32. 🟢 Low credit balance persists after account switching
|
||||
**Where:** Account pane after switching organizations
|
||||
**Symptom:** After switching from a low-balance org to a high-balance org, the "Insufficient balance" error persists.
|
||||
**Root cause:** `setUserOrganization` was a STUB. The active org was never updated on disk, so credit queries always returned the same org's data.
|
||||
**Fix:** Implemented `handleSetUserOrganization()` that calls `LegacyStateReader.setActiveOrganization()` to update the `active` flag on orgs in stored credentials. Each credit fetch is a fresh API call keyed by org ID, so switching orgs correctly fetches the new org's balance.
|
||||
**Tested:** Integration test with mock server verifies: switch from low-balance org → high-balance org returns correct (high) balance, not stale (low) balance.
|
||||
|
||||
## Open Issues
|
||||
|
||||
---
|
||||
|
||||
### 26. 🟢 Clicking history items does not open them
|
||||
**Where:** History tab → clicking any task item; also RECENT section on home screen
|
||||
**Symptom:** History items display correctly in the History view, but clicking on them does nothing — the view stays on the History tab instead of navigating to the chat view with the loaded task.
|
||||
**Root cause:** `handleShowTaskWithId()` in `grpc-handler.ts` called `this.delegate.showTaskWithId(id)` to load the task data and push state, but never fired `this.onNavigateCallback?.("chat")` to tell the webview to navigate from the History view to the Chat view.
|
||||
**Fix:** Added `this.onNavigateCallback?.("chat")` after `showTaskWithId()` completes in `grpc-handler.ts`. This sends a typed `navigate` message to the webview, which triggers `navigateToChat()` — hiding the History view and revealing the Chat view with the loaded task.
|
||||
**Verified:** Debug harness confirmed clicking items in both the History tab and RECENT section on the home screen now navigates to the chat view with full message history loaded.
|
||||
|
||||
---
|
||||
|
||||
## Observations (not bugs, just notes)
|
||||
|
||||
### UI Rendering — Task Completion View
|
||||
The completed task view renders correctly:
|
||||
- Task header with cost badge (e.g. "$0.0072")
|
||||
- Token usage bar (e.g. "1.4k / 200.0k")
|
||||
- Response text displayed properly
|
||||
- "Task Completed" card with green checkmark and the result
|
||||
- "Start New Task" button appears below the chat
|
||||
- Input area changes to "Type a message..." (follow-up mode)
|
||||
|
||||
### Feature Card Carousel
|
||||
The home screen shows a rotating feature card carousel (1/4 through 4/4) promoting:
|
||||
- Claude Sonnet 4.6
|
||||
- MiniMax M2.5
|
||||
- ChatGPT integration
|
||||
- Jupyter Notebooks
|
||||
Each with a dismiss (X) button per-card.
|
||||
|
||||
### Model Selector
|
||||
Bottom bar correctly shows `cline:anthropic/claud...` (truncated) with Plan/Act toggle. Act mode is the default.
|
||||
|
||||
### Debug Harness: `ui.send_message` and `ui.react_input(submit:true)` Don't Start Tasks
|
||||
Both `ui.send_message` (gRPC postMessage) and `ui.react_input` with `submit:true` report success but don't actually start a new task—the webview stays on the home screen. The workaround is to use `ui.react_input` (without submit) to set the text, then dispatch a KeyboardEvent via `web.evaluate`:
|
||||
```
|
||||
curl -s localhost:19229/api -d '{"method":"ui.react_input","params":{"text":"your message","clear":true}}'
|
||||
curl -s localhost:19229/api -d '{"method":"web.evaluate","params":{"expression":"(() => { const ta = document.querySelector(\"textarea\"); ta.focus(); ta.dispatchEvent(new KeyboardEvent(\"keydown\",{key:\"Enter\",code:\"Enter\",keyCode:13,which:13,bubbles:true})); return \"ok\"; })()"}}'
|
||||
```
|
||||
|
||||
### Debug Harness Limitations (Fixed)
|
||||
All three limitations below have been addressed:
|
||||
|
||||
- ~~Programmatic textarea input doesn't reliably trigger React state updates after the first task.~~ **Fixed**: Two new commands added:
|
||||
- `ui.react_input` — Uses `document.execCommand('insertText')` which fires real InputEvents that React's onChange handler processes correctly, even after multiple tasks.
|
||||
- `ui.send_message` — Bypasses the textarea entirely by sending gRPC requests via `postMessage` directly to the extension host.
|
||||
- ~~The `web.evaluate` context can't access the VS Code API.~~ **Fixed**: The webview now exposes the VS Code API as `window.__clineVsCodeApi`, and a new `web.post_message` command lets the harness send arbitrary messages to the extension host through it.
|
||||
- ~~The `ui.locator` Playwright actions don't reliably target elements inside the webview iframe.~~ **Fixed**: `findSidebar()` now validates cached frame references (checking for both detached and stale frames), `getTarget()` accepts a `forceRefresh` flag, and `ui.locator` automatically retries with frame re-discovery when targeting sidebar elements.
|
||||
@@ -129,9 +129,9 @@ Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol),
|
||||
|
||||
### Checkpoints: Compare and Restore
|
||||
|
||||
As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
|
||||
As Cline works through a task, the extension can take an internal snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
|
||||
|
||||
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
|
||||
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress while the current checkpoint system is available.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
|
||||
+436
@@ -0,0 +1,436 @@
|
||||
# Cline SDK — MCP Server Management Reference
|
||||
|
||||
This document describes how the Cline SDK handles MCP (Model Context Protocol) server lifecycle, configuration, and what is — and isn't — exposed for client integration. It is intended for client developers building UI around MCP server management.
|
||||
|
||||
---
|
||||
|
||||
## Summary: The Claim That "The SDK Lacks Hooks" Is Wrong
|
||||
|
||||
The SDK **does** provide a full MCP manager with lifecycle operations. The `McpManager` interface in `@clinebot/core` supports:
|
||||
|
||||
- `registerServer()` / `unregisterServer()` — add or remove servers
|
||||
- `connectServer()` / `disconnectServer()` — start or stop connections
|
||||
- `setServerDisabled()` — toggle enable/disable
|
||||
- `listServers()` — get snapshots of all servers with status
|
||||
- `refreshTools()` — force-refresh tool lists from a server
|
||||
- `dispose()` — shut down all servers
|
||||
|
||||
The **actual gap** is narrower: there is no built-in file-watcher that auto-reloads `cline_mcp_settings.json` when it changes, and the RPC layer (`@clinebot/rpc`) does not currently expose MCP management endpoints. This means clients that manage MCP settings through the settings file must bridge the gap between file edits and runtime state themselves.
|
||||
|
||||
---
|
||||
|
||||
## SDK Architecture for MCP
|
||||
|
||||
### Layer 1: Settings File (`cline_mcp_settings.json`)
|
||||
|
||||
The SDK reads MCP server configuration from a JSON settings file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"docs": {
|
||||
"transport": { "type": "stdio", "command": "node", "args": ["./mcp.js"] }
|
||||
},
|
||||
"remote": {
|
||||
"transport": { "type": "streamableHttp", "url": "https://mcp.example.com" },
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Location resolution** (`resolveDefaultMcpSettingsPath()`):
|
||||
- `CLINE_MCP_SETTINGS_PATH` env var (if set)
|
||||
- Otherwise defaults to the platform-specific Cline data directory
|
||||
|
||||
**SDK utilities** for this file (all exported from `@clinebot/core`):
|
||||
- `resolveDefaultMcpSettingsPath()` — get the path
|
||||
- `hasMcpSettingsFile()` — check if it exists
|
||||
- `loadMcpSettingsFile()` — parse and validate with Zod
|
||||
- `resolveMcpServerRegistrations()` — parse file → `McpServerRegistration[]`
|
||||
- `registerMcpServersFromSettingsFile(manager)` — parse file and register all servers into a manager
|
||||
|
||||
### Layer 2: McpManager (`InMemoryMcpManager`)
|
||||
|
||||
`packages/core/src/extensions/mcp/manager.ts` — the runtime MCP lifecycle manager.
|
||||
|
||||
```typescript
|
||||
interface McpManager extends McpToolProvider {
|
||||
registerServer(registration: McpServerRegistration): Promise<void>;
|
||||
unregisterServer(serverName: string): Promise<void>;
|
||||
connectServer(serverName: string): Promise<void>;
|
||||
disconnectServer(serverName: string): Promise<void>;
|
||||
setServerDisabled(serverName: string, disabled: boolean): Promise<void>;
|
||||
listServers(): readonly McpServerSnapshot[];
|
||||
refreshTools(serverName: string): Promise<readonly McpToolDescriptor[]>;
|
||||
callTool(request: McpToolCallRequest): Promise<McpToolCallResult>;
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- **Lazy connection**: Servers are registered in disconnected state; connection happens on first `listTools()` or `callTool()`.
|
||||
- **Transport change detection**: If `registerServer()` is called with a changed transport config, the existing connection is torn down and the client is recreated.
|
||||
- **Exclusive locking**: Per-server operation locks prevent concurrent connect/disconnect races.
|
||||
- **Tool caching**: `listTools()` caches results for `toolsCacheTtlMs` (default 5 seconds). Use `refreshTools()` to force a refresh.
|
||||
- **Disable = disconnect**: Calling `setServerDisabled(name, true)` immediately disconnects the server.
|
||||
|
||||
### Layer 3: McpServerClient (Transport Layer)
|
||||
|
||||
`packages/core/src/extensions/mcp/client.ts` — the actual MCP protocol client.
|
||||
|
||||
The default factory (`createDefaultMcpServerClientFactory()`) creates `StdioMcpClient` instances:
|
||||
- Spawns child processes for `stdio` transport
|
||||
- Implements MCP JSON-RPC protocol (both newline-delimited and framed modes)
|
||||
- Auto-negotiates protocol mode by trying newline first, then framed
|
||||
- Protocol version: `2024-11-05`
|
||||
- Connect timeout: 1.5s, request timeout: 5s
|
||||
|
||||
```typescript
|
||||
interface McpServerClient {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTools(): Promise<readonly McpToolDescriptor[]>;
|
||||
callTool(request: { name: string; arguments?: Record<string, unknown> }): Promise<McpToolCallResult>;
|
||||
}
|
||||
|
||||
type McpServerClientFactory = (registration: McpServerRegistration) => Promise<McpServerClient> | McpServerClient;
|
||||
```
|
||||
|
||||
**Transport types supported**:
|
||||
| Type | Status |
|
||||
|---|---|
|
||||
| `stdio` | ✅ Fully implemented in `StdioMcpClient` |
|
||||
| `sse` | ⚠️ Type defined, but no built-in client (factory only creates `StdioMcpClient`) |
|
||||
| `streamableHttp` | ⚠️ Type defined, but no built-in client |
|
||||
|
||||
Clients needing SSE or StreamableHTTP support must provide a custom `McpServerClientFactory`.
|
||||
|
||||
### Layer 4: Tool Bridge
|
||||
|
||||
`packages/core/src/extensions/mcp/tools.ts` via `createMcpTools()` — converts MCP server tools into the SDK's `Tool` type for use in the agent loop.
|
||||
|
||||
```typescript
|
||||
interface CreateMcpToolsOptions {
|
||||
serverName: string;
|
||||
provider: McpToolProvider; // Usually the McpManager
|
||||
nameTransform?: McpToolNameTransform;
|
||||
timeoutMs?: number;
|
||||
retryable?: boolean;
|
||||
maxRetries?: number;
|
||||
}
|
||||
```
|
||||
|
||||
Default name transform: `{serverName}__{toolName}` (e.g. `docs__search`).
|
||||
|
||||
---
|
||||
|
||||
## How the Runtime Builder Uses MCP
|
||||
|
||||
`packages/core/src/runtime/runtime-builder.ts` → `loadConfiguredMcpTools()`:
|
||||
|
||||
1. Resolves the MCP settings file path
|
||||
2. Creates a fresh `InMemoryMcpManager`
|
||||
3. Calls `registerMcpServersFromSettingsFile()` to load all servers
|
||||
4. Creates `Tool[]` via `createMcpTools()` for each non-disabled server
|
||||
5. Returns the tools + a `shutdown()` callback that calls `manager.dispose()`
|
||||
|
||||
**Critical limitation**: This is done once at session build time. There is **no file watcher** that reloads MCP settings when they change during a session. If the settings file is edited mid-session, the running session won't see the changes.
|
||||
|
||||
---
|
||||
|
||||
## How Existing Apps Handle MCP Settings
|
||||
|
||||
### Tauri Apps (`apps/code`, `apps/desktop`)
|
||||
|
||||
Both Tauri apps implement MCP settings management **entirely in Rust** at the Tauri command level, bypassing the SDK's McpManager:
|
||||
|
||||
```rust
|
||||
// apps/code/src-tauri/src/main.rs (identical pattern in apps/desktop)
|
||||
fn list_mcp_servers() -> Result<McpServersResponse, String>
|
||||
fn set_mcp_server_disabled(name, disabled) -> Result<McpServersResponse, String>
|
||||
fn upsert_mcp_server(input) -> Result<McpServersResponse, String>
|
||||
fn delete_mcp_server(name) -> Result<McpServersResponse, String>
|
||||
```
|
||||
|
||||
These commands:
|
||||
- Read/write `cline_mcp_settings.json` directly
|
||||
- Return the full server list after each mutation
|
||||
- Do **not** interact with any running `McpManager` instance
|
||||
|
||||
The frontend (`apps/code/components/views/settings/mcp-view.tsx`) calls these Tauri commands:
|
||||
- `list_mcp_servers` — refresh the displayed server list
|
||||
- `set_mcp_server_disabled` — toggle enable/disable
|
||||
- `upsert_mcp_server` — add or edit a server
|
||||
- `delete_mcp_server` — remove a server
|
||||
|
||||
### Node.js Host (`apps/code/host/commands.ts`)
|
||||
|
||||
The Code app's Node.js host also implements MCP CRUD directly:
|
||||
```typescript
|
||||
// Direct file reads/writes, not using McpManager
|
||||
function readMcpServersResponse(): JsonRecord // reads cline_mcp_settings.json
|
||||
function writeMcpServersMap(servers: JsonRecord) // writes cline_mcp_settings.json
|
||||
function ensureMcpSettingsFile(): string // ensures file exists
|
||||
```
|
||||
|
||||
Commands: `list_mcp_servers`, `set_mcp_server_disabled`, `upsert_mcp_server`, `delete_mcp_server`, `ensure_mcp_settings_file`
|
||||
|
||||
### CLI (`apps/cli`)
|
||||
|
||||
The CLI has `clite config mcp` / `clite list mcp` for listing configured MCP servers. It uses the SDK's `resolveMcpServerRegistrations()` to read the settings file.
|
||||
|
||||
---
|
||||
|
||||
## What the SDK DOES Expose (Exported from `@clinebot/core`)
|
||||
|
||||
### Full Type & Implementation Exports
|
||||
|
||||
```typescript
|
||||
// Manager
|
||||
export { InMemoryMcpManager } from "./extensions/mcp";
|
||||
export type { McpManager, McpManagerOptions } from "./extensions/mcp";
|
||||
|
||||
// Client factory
|
||||
export { createDefaultMcpServerClientFactory } from "./extensions/mcp";
|
||||
export type { McpServerClient, McpServerClientFactory } from "./extensions/mcp";
|
||||
|
||||
// Config loading
|
||||
export { hasMcpSettingsFile, loadMcpSettingsFile, registerMcpServersFromSettingsFile,
|
||||
resolveDefaultMcpSettingsPath, resolveMcpServerRegistrations } from "./extensions/mcp";
|
||||
export type { LoadMcpSettingsOptions, McpSettingsFile, RegisterMcpServersFromSettingsOptions } from "./extensions/mcp";
|
||||
|
||||
// Types
|
||||
export type { McpServerRegistration, McpServerSnapshot, McpConnectionStatus,
|
||||
McpServerTransportConfig, McpStdioTransportConfig, McpSseTransportConfig,
|
||||
McpStreamableHttpTransportConfig } from "./extensions/mcp";
|
||||
|
||||
// Tool bridge
|
||||
export { createMcpTools } from "./extensions/mcp";
|
||||
export type { CreateMcpToolsOptions, McpToolCallRequest, McpToolCallResult,
|
||||
McpToolDescriptor, McpToolNameTransform, McpToolProvider } from "./extensions/mcp";
|
||||
|
||||
// Policies
|
||||
export { createDisabledMcpToolPolicies, createDisabledMcpToolPolicy } from "./extensions/mcp";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Missing / The Actual Gaps
|
||||
|
||||
### 1. No RPC Endpoints for MCP Management
|
||||
|
||||
`packages/rpc/src` has **zero** MCP-related code. There are no gRPC/RPC methods for:
|
||||
- Listing MCP servers
|
||||
- Registering/unregistering servers
|
||||
- Connecting/disconnecting servers
|
||||
- Toggling server disabled state
|
||||
- Refreshing tools
|
||||
|
||||
This means RPC-backed clients cannot manage MCP through the RPC layer.
|
||||
|
||||
### 2. No Settings File Watcher
|
||||
|
||||
The SDK has a `UnifiedConfigFileWatcher` for agents, skills, rules, and workflows — but **not for MCP settings**. When another client edits `cline_mcp_settings.json`, running sessions don't see the change.
|
||||
|
||||
### 3. No Live Manager Exposure to Clients
|
||||
|
||||
The runtime builder creates an `InMemoryMcpManager` internally during `loadConfiguredMcpTools()`, but it's encapsulated — only the resulting `Tool[]` and a `shutdown()` callback are returned. The manager itself is not exposed to the caller, so clients can't call `connectServer()`, `disconnectServer()`, etc. on a running session's MCP manager.
|
||||
|
||||
### 4. SSE/StreamableHTTP Client Not Implemented
|
||||
|
||||
The transport types are defined, but the default client factory only produces `StdioMcpClient`. Clients needing SSE or StreamableHTTP must provide their own `McpServerClientFactory`.
|
||||
|
||||
---
|
||||
|
||||
## What a Client Needs to Do Today
|
||||
|
||||
To implement full MCP server management UI:
|
||||
|
||||
### Settings CRUD (Works Now)
|
||||
Read and write `cline_mcp_settings.json` directly. The SDK provides:
|
||||
- `resolveDefaultMcpSettingsPath()` — find the file
|
||||
- `loadMcpSettingsFile()` — parse it
|
||||
- Write it yourself (it's just JSON with `{ mcpServers: { ... } }`)
|
||||
|
||||
This is exactly what the Tauri apps and Node.js host do today.
|
||||
|
||||
### Runtime Lifecycle (Partial)
|
||||
For a new session, MCP tools are automatically loaded from the settings file by the runtime builder.
|
||||
|
||||
For mid-session changes (restart, delete, toggle), clients currently have two options:
|
||||
1. **Edit the settings file and restart the session** — the next session build will pick up the changes
|
||||
2. **Create and manage your own `InMemoryMcpManager`** — the SDK exports everything needed:
|
||||
```typescript
|
||||
const manager = new InMemoryMcpManager({
|
||||
clientFactory: createDefaultMcpServerClientFactory(),
|
||||
});
|
||||
await manager.registerServer({ name: "docs", transport: { type: "stdio", command: "node", args: ["./mcp.js"] } });
|
||||
await manager.connectServer("docs");
|
||||
const tools = await manager.listTools("docs");
|
||||
await manager.disconnectServer("docs");
|
||||
await manager.unregisterServer("docs");
|
||||
```
|
||||
|
||||
### Cross-Client Sync (Not Built)
|
||||
If multiple clients share the same settings file, there is no notification mechanism. Clients would need their own file watcher (e.g. `fs.watch()` / `chokidar`) on `cline_mcp_settings.json`.
|
||||
|
||||
---
|
||||
|
||||
## How MCP Tools Become Visible to the Agent (and the Client)
|
||||
|
||||
### MCP Tools Are Injected as Regular SDK Tools
|
||||
|
||||
The `createMcpTools()` function converts each MCP tool descriptor into a standard `Tool` object (from `@clinebot/shared`). These tools are **indistinguishable** from built-in tools once created — they have a `name`, `description`, `inputSchema`, and an `execute` function.
|
||||
|
||||
```typescript
|
||||
// packages/core/src/extensions/mcp/tools.ts
|
||||
export async function createMcpTools(options: CreateMcpToolsOptions): Promise<Tool[]> {
|
||||
const descriptors = await options.provider.listTools(options.serverName);
|
||||
return descriptors.map((descriptor) => createTool({
|
||||
name: nameTransform({ serverName, toolName: descriptor.name }), // e.g. "docs__search"
|
||||
description: descriptor.description || `Execute MCP tool "${descriptor.name}" from server "${serverName}".`,
|
||||
inputSchema: descriptor.inputSchema,
|
||||
execute: async (input, context) => options.provider.callTool({
|
||||
serverName, toolName: descriptor.name, arguments: input, context,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### The Runtime Builder Merges MCP Tools with Built-in Tools
|
||||
|
||||
In `DefaultRuntimeBuilder.build()` (line ~460-476):
|
||||
```typescript
|
||||
if (normalized.enableTools) {
|
||||
tools.push(...createBuiltinToolsList(...)); // SDK built-in tools
|
||||
const mcpRuntime = await loadConfiguredMcpTools(); // MCP tools
|
||||
tools.push(...mcpRuntime.tools);
|
||||
mcpShutdown = mcpRuntime.shutdown;
|
||||
}
|
||||
```
|
||||
|
||||
The resulting `tools: Tool[]` array — containing **both** built-in and MCP tools — is returned in the `BuiltRuntime`:
|
||||
```typescript
|
||||
interface BuiltRuntime {
|
||||
tools: Tool[]; // ← includes MCP tools
|
||||
hooks?: AgentHooks;
|
||||
shutdown: (reason: string) => Promise<void> | void;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### The Agent Receives All Tools (Including MCP) Uniformly
|
||||
|
||||
The `Agent` (from `@clinebot/agents`) receives the merged `tools` array. It doesn't know or care which tools came from MCP vs built-in. The LLM sees all tools in its tool definitions and can call any of them.
|
||||
|
||||
### Client Control Over MCP Tools
|
||||
|
||||
Clients **can** control MCP tools through the same mechanisms they use for any tool:
|
||||
|
||||
1. **Tool Policies** — Enable/disable or require approval per tool name:
|
||||
```typescript
|
||||
toolPolicies: {
|
||||
"docs__search": { enabled: true, autoApprove: true },
|
||||
"docs__write": { enabled: true, autoApprove: false }, // requires approval
|
||||
"risky__delete": { enabled: false }, // completely disabled
|
||||
}
|
||||
```
|
||||
|
||||
2. **MCP-specific disable policies** — The SDK provides helpers to disable all tools from a specific MCP server:
|
||||
```typescript
|
||||
import { createDisabledMcpToolPolicies } from "@clinebot/core";
|
||||
const policies = createDisabledMcpToolPolicies({
|
||||
serverName: "risky-server",
|
||||
toolNames: ["delete", "modify", "drop"],
|
||||
});
|
||||
// → { "risky-server__delete": { enabled: false }, ... }
|
||||
```
|
||||
|
||||
3. **CLI flags** — `--tool-enable <name>` and `--tool-disable <name>` work for MCP tools too (they operate on the transformed name like `docs__search`).
|
||||
|
||||
4. **Tool approval callback** — When `autoApprove: false`, the agent calls `requestToolApproval()` before executing the tool, giving the client a chance to approve/reject each call.
|
||||
|
||||
5. **`enableTools: false`** — Disables ALL tools including MCP.
|
||||
|
||||
### What Clients Can See
|
||||
|
||||
The `BuiltRuntime.tools` array is visible to the caller of `runtimeBuilder.build()`. The session manager and host apps can inspect it to know exactly which tools (including MCP tools) are available.
|
||||
|
||||
MCP tools follow the naming convention `{serverName}__{toolName}` by default, so clients can identify which tools came from which MCP server by parsing the name prefix.
|
||||
|
||||
---
|
||||
|
||||
## Remote MCP Servers (StreamableHTTP / SSE)
|
||||
|
||||
### The SDK's Design Intent
|
||||
|
||||
The SDK clearly **intends** to support remote MCP servers. The evidence:
|
||||
|
||||
1. **Transport types are fully defined and validated**:
|
||||
```typescript
|
||||
interface McpStreamableHttpTransportConfig {
|
||||
type: "streamableHttp";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
interface McpSseTransportConfig {
|
||||
type: "sse";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Config loader validates all three transports** — The Zod schemas accept `stdio`, `sse`, and `streamableHttp` equally. Legacy formats (`url` without explicit type) default to `sse`; `transportType: "http"` maps to `streamableHttp`.
|
||||
|
||||
3. **Manager is transport-agnostic** — The `McpManager` uses `McpServerClientFactory` to create clients. It doesn't care about transport type; that's the factory's job.
|
||||
|
||||
4. **Tests use `streamableHttp` registrations** — The manager test suite registers servers with `transport: { type: "streamableHttp", url: "https://mcp.example.test" }` and they work fine (with a mock client factory).
|
||||
|
||||
5. **Settings file and UI accept all transports** — Both Tauri apps and the Code app UI offer `stdio`, `sse`, and `streamableHttp` as choices.
|
||||
|
||||
### What's Actually Implemented vs Not
|
||||
|
||||
| Concern | Status |
|
||||
|---|---|
|
||||
| Transport type definitions | ✅ Complete |
|
||||
| Settings file parsing for all transports | ✅ Complete |
|
||||
| Settings file CRUD (UI/Tauri/CLI) for all transports | ✅ Complete |
|
||||
| `McpManager` lifecycle for all transports | ✅ Complete (transport-agnostic) |
|
||||
| `StdioMcpClient` (spawns child process) | ✅ Complete |
|
||||
| HTTP/SSE client (connects to remote URL) | ❌ Not in default factory |
|
||||
|
||||
### The Gap for Remote Servers
|
||||
|
||||
The **only** thing missing is that `createDefaultMcpServerClientFactory()` returns a `StdioMcpClient` unconditionally — it doesn't check `registration.transport.type` and will fail for `sse` or `streamableHttp` transports.
|
||||
|
||||
A client can fix this by providing a custom factory:
|
||||
```typescript
|
||||
const manager = new InMemoryMcpManager({
|
||||
clientFactory: async (registration) => {
|
||||
if (registration.transport.type === "stdio") {
|
||||
return createDefaultMcpServerClientFactory()(registration);
|
||||
}
|
||||
if (registration.transport.type === "streamableHttp") {
|
||||
return new MyStreamableHttpMcpClient(registration);
|
||||
}
|
||||
if (registration.transport.type === "sse") {
|
||||
return new MySseMcpClient(registration);
|
||||
}
|
||||
throw new Error(`Unsupported transport: ${registration.transport.type}`);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The client just needs to implement the `McpServerClient` interface (4 methods: `connect`, `disconnect`, `listTools`, `callTool`).
|
||||
|
||||
---
|
||||
|
||||
## Recommendation for Improvement
|
||||
|
||||
To close the gap, the SDK could:
|
||||
|
||||
1. **Add MCP settings to the config watcher system** — create an `McpConfigDefinition` for `UnifiedConfigFileWatcher` to auto-detect changes to `cline_mcp_settings.json`
|
||||
2. **Expose the McpManager from the runtime builder** — return it alongside the tool list so clients can call lifecycle methods
|
||||
3. **Add MCP management to the RPC layer** — implement gRPC methods that proxy to `McpManager`
|
||||
4. **Implement SSE/StreamableHTTP clients** — extend the default client factory
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# Cline SDK — Provider Credentials & OAuth Reference
|
||||
|
||||
This document describes how the Cline SDK publishes inference provider metadata, handles credential resolution, and orchestrates OAuth authentication flows. It is intended for client developers integrating with `@clinebot/llms` and `@clinebot/core`.
|
||||
|
||||
---
|
||||
|
||||
## Provider Catalog
|
||||
|
||||
The SDK owns the canonical list of inference providers. It is **not** produced by clients.
|
||||
|
||||
### Where Providers Are Defined
|
||||
|
||||
| Layer | Location | What It Owns |
|
||||
|---|---|---|
|
||||
| `@clinebot/llms` | `packages/llms/src/gateway/builtins.ts` | `BUILTIN_SPECS` array — every built-in provider's `id`, `name`, `description`, `family`, `capabilities`, `apiKeyEnv`, `defaultModelId`, default `baseUrl` |
|
||||
| `@clinebot/llms` | `packages/llms/src/provider/ids.ts` | `BUILT_IN_PROVIDER` enum and `BUILT_IN_PROVIDER_IDS` array |
|
||||
| `@clinebot/shared` | `packages/shared/src/llms/gateway.ts` | `GatewayProviderManifest` type — the runtime shape clients receive |
|
||||
| `@clinebot/llms` | `packages/llms/src/gateway/provider-keys.ts` | Mapping from external `modelsDevKey` identifiers to runtime/generated provider IDs |
|
||||
|
||||
### BuiltinSpec Shape
|
||||
|
||||
Each built-in provider is declared as a `BuiltinSpec`:
|
||||
|
||||
```typescript
|
||||
interface BuiltinSpec {
|
||||
id: string; // e.g. "anthropic", "openai-native", "cline"
|
||||
name: string; // Human-readable name
|
||||
description: string;
|
||||
family: ProviderFamily; // Protocol family: "openai-compatible", "anthropic", "google", etc.
|
||||
protocol?: ProviderProtocol;
|
||||
client?: ProviderClient;
|
||||
capabilities?: ProviderCapability[]; // "reasoning" | "prompt-cache" | "tools" | "oauth" | "temperature" | "files"
|
||||
modelsProviderId?: string;
|
||||
defaultModelId?: string;
|
||||
modelsFactory?: () => Record<string, ModelInfo>;
|
||||
env?: readonly ("browser" | "node")[];
|
||||
apiKeyEnv?: readonly string[]; // Environment variable names for API key resolution
|
||||
docsUrl?: string;
|
||||
defaults?: GatewayProviderSettings; // Includes default baseUrl
|
||||
}
|
||||
```
|
||||
|
||||
### GatewayProviderManifest (Runtime Shape)
|
||||
|
||||
`toManifest()` converts a `BuiltinSpec` into a `GatewayProviderManifest`, which is what clients interact with at runtime:
|
||||
|
||||
```typescript
|
||||
interface GatewayProviderManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultModelId: string;
|
||||
models: readonly GatewayModelDefinition[];
|
||||
env?: readonly ("browser" | "node")[];
|
||||
api?: string; // Default base URL
|
||||
apiKeyEnv?: readonly string[]; // Env var names for credential resolution
|
||||
docsUrl?: string;
|
||||
metadata?: Record<string, JsonValue | undefined>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credential Resolution
|
||||
|
||||
### API Key Resolution Order
|
||||
|
||||
The SDK resolves credentials in `packages/llms/src/gateway/http.ts` via `resolveApiKey()`:
|
||||
|
||||
1. **Explicit `apiKey`** — passed directly in provider config
|
||||
2. **`apiKeyResolver()`** — async callback (e.g. fetch from keychain)
|
||||
3. **`apiKeyEnv`** — iterate environment variable names from the provider manifest; first non-empty value wins
|
||||
|
||||
If all fail, `getMissingApiKeyError()` produces a message naming the expected env vars:
|
||||
> `Missing API key for provider "anthropic". Set apiKey explicitly or one of: ANTHROPIC_API_KEY.`
|
||||
|
||||
### Per-Provider Credential Metadata Examples
|
||||
|
||||
| Provider | `apiKeyEnv` |
|
||||
|---|---|
|
||||
| `anthropic` | `["ANTHROPIC_API_KEY"]` |
|
||||
| `openai-native` | `["OPENAI_API_KEY"]` |
|
||||
| `gemini` | `["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"]` |
|
||||
| `vertex` | `["GCP_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS", "GEMINI_API_KEY", "GOOGLE_API_KEY"]` |
|
||||
| `bedrock` | `["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]` |
|
||||
| `sapaicore` | `["AICORE_SERVICE_KEY", "VCAP_SERVICES"]` |
|
||||
| `cline` | `["CLINE_API_KEY"]` |
|
||||
| `openrouter` | `["OPENROUTER_API_KEY"]` |
|
||||
| `deepseek` | `["DEEPSEEK_API_KEY"]` |
|
||||
|
||||
Most OpenAI-compatible providers follow the pattern `["{PROVIDER}_API_KEY"]`.
|
||||
|
||||
### Provider-Specific Settings
|
||||
|
||||
Some providers need additional configuration beyond an API key:
|
||||
|
||||
- **Vertex/GCP**: `gcpProjectId`, `gcpRegion`
|
||||
- **Bedrock/AWS**: `awsAuthentication` (`"iam" | "api-key" | "profile"`), `awsRegion`, `awsAccessKey`, `awsSecretKey`, `awsSessionToken`, `awsProfile`
|
||||
|
||||
These are passed through `ProviderSelectionConfig.settings` rather than `apiKeyEnv`.
|
||||
|
||||
---
|
||||
|
||||
## OAuth Authentication
|
||||
|
||||
### Which Providers Support OAuth
|
||||
|
||||
Only providers with `"oauth"` in their `capabilities` array support OAuth:
|
||||
|
||||
| Provider | OAuth Implementation |
|
||||
|---|---|
|
||||
| `cline` | `packages/core/src/auth/cline.ts` — Cline API OAuth |
|
||||
| `openai-codex` | `packages/core/src/auth/codex.ts` — ChatGPT/OpenAI Codex OAuth with PKCE |
|
||||
| `oca` | `packages/core/src/auth/oca.ts` — Oracle Code Assist OAuth with PKCE |
|
||||
|
||||
The CLI confirms this in `apps/cli/src/commands/auth.ts`:
|
||||
```typescript
|
||||
// Only these three providers support CLI OAuth flow
|
||||
if (providerId === "cline") return oauthApi.loginClineOAuth(...)
|
||||
if (providerId === "oca") return oauthApi.loginOcaOAuth(...)
|
||||
if (providerId === "openai-codex") return oauthApi.loginOpenAICodex(...)
|
||||
throw new Error(`Provider "${providerId}" does not support CLI OAuth flow`)
|
||||
```
|
||||
|
||||
### Responsibility Split: SDK vs Client
|
||||
|
||||
| Concern | Owner | Details |
|
||||
|---|---|---|
|
||||
| Spawn local callback server | **SDK** | `startLocalOAuthServer()` in `packages/core/src/auth/server.ts` |
|
||||
| Build authorization URL | **SDK** | Each auth module constructs the URL with redirect_uri, state, etc. |
|
||||
| Open browser / present URL | **Client** | SDK calls `callbacks.onAuth({ url, instructions })` — client decides how to handle |
|
||||
| Collect redirect code | **SDK** | Local HTTP server parses `?code=&state=` from redirect |
|
||||
| Exchange code for tokens | **SDK** | Each auth module handles the token exchange |
|
||||
| Prompt for manual code input | **Client** | SDK calls `callbacks.onPrompt()` or `callbacks.onManualCodeInput()` as fallback |
|
||||
|
||||
### The SDK Does NOT Open Browsers
|
||||
|
||||
The SDK never calls `open()` or launches a browser. It uses a callback-based interface:
|
||||
|
||||
```typescript
|
||||
// packages/core/src/auth/types.ts
|
||||
interface OAuthLoginCallbacks {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void; // SDK emits URL here
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>; // SDK asks for input here
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>; // Fallback if redirect fails
|
||||
}
|
||||
```
|
||||
|
||||
### The SDK DOES Spawn the Local Callback Server
|
||||
|
||||
`packages/core/src/auth/server.ts` exports `startLocalOAuthServer()`:
|
||||
|
||||
- Creates a `node:http` server on `127.0.0.1`
|
||||
- Tries a list of candidate ports in order, skipping `EADDRINUSE`
|
||||
- Listens on a configured callback path (e.g. `/callback`)
|
||||
- Extracts `code`, `state`, `provider`, `error` from the redirect URL query params
|
||||
- Returns a success HTML page to the browser ("Authentication Successful — You can close this window")
|
||||
- Auto-closes after 3 seconds via embedded `<script>`
|
||||
- Times out after 5 minutes by default
|
||||
|
||||
```typescript
|
||||
interface LocalOAuthServer {
|
||||
callbackUrl: string; // e.g. "http://127.0.0.1:54321/callback"
|
||||
waitForCallback: () => Promise<OAuthCallbackPayload>; // Resolves when redirect arrives
|
||||
cancelWait: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Redirect URLs Are NOT in Provider Metadata
|
||||
|
||||
Redirect/callback URLs are **dynamically constructed at runtime**, not published in the provider manifest:
|
||||
|
||||
- **Cline**: Dynamic port → `http://localhost:{port}/callback`
|
||||
- **OpenAI Codex**: Hardcoded `http://localhost:1455/auth/callback` (fixed port, fixed client ID `app_EMoamEEZ73f0CkXaXp7hrann`)
|
||||
- **OCA**: Dynamic port with configurable path, default `/oauth/callback`
|
||||
|
||||
### Client Integration Helper
|
||||
|
||||
`packages/core/src/auth/client.ts` provides a convenience adapter:
|
||||
|
||||
```typescript
|
||||
interface OAuthClientCallbacksOptions {
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onOutput?: (message: string) => void;
|
||||
openUrl?: (url: string) => void | Promise<void>; // Client provides browser-open function
|
||||
onOpenUrlError?: (context: { url: string; error: unknown }) => void;
|
||||
}
|
||||
|
||||
function createOAuthClientCallbacks(options): OAuthLoginCallbacks
|
||||
```
|
||||
|
||||
The `openUrl` field is where a CLI passes its `open` implementation, a Tauri app passes shell open, etc.
|
||||
|
||||
### End-to-End OAuth Flow
|
||||
|
||||
```
|
||||
1. Client calls SDK login function (e.g. loginClineOAuth)
|
||||
2. SDK → startLocalOAuthServer() → binds to 127.0.0.1:{port}
|
||||
3. SDK → builds authorization URL with redirect_uri = callback server URL
|
||||
4. SDK → callbacks.onAuth({ url, instructions })
|
||||
5. Client → opens browser (or displays URL to user)
|
||||
6. User → authenticates in browser
|
||||
7. Provider → redirects to http://127.0.0.1:{port}/callback?code=...&state=...
|
||||
8. SDK's local server → captures code and state, renders success page
|
||||
9. SDK → exchanges authorization code for tokens (provider-specific)
|
||||
10. SDK → returns OAuthCredentials { access, refresh, expires, accountId?, email? }
|
||||
```
|
||||
|
||||
If the local server redirect times out or fails, the SDK falls back to:
|
||||
- `onManualCodeInput()` — ask user to paste code
|
||||
- `onPrompt({ message: "Paste the authorization code (or full redirect URL):" })` — final fallback
|
||||
- `parseAuthorizationInput()` can handle both raw codes and full URLs with query params
|
||||
|
||||
### OAuthCredentials Shape
|
||||
|
||||
```typescript
|
||||
interface OAuthCredentials {
|
||||
access: string; // Access token
|
||||
refresh: string; // Refresh token
|
||||
expires: number; // Expiration timestamp (ms since epoch)
|
||||
accountId?: string; // Provider-specific account ID
|
||||
email?: string; // For display/telemetry
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### Provider-Specific OAuth Details
|
||||
|
||||
**Cline OAuth** (`packages/core/src/auth/cline.ts`):
|
||||
- Authorization URL: `{apiBaseUrl}/auth/authorize?client_type=extension&callback_url=...&state=...`
|
||||
- Token endpoint: `{apiBaseUrl}/auth/token`
|
||||
- Default API base: `https://api.cline.bot`
|
||||
- Supports provider passthrough (callback can include `?provider=google` etc.)
|
||||
|
||||
**OpenAI Codex OAuth** (`packages/core/src/auth/codex.ts`):
|
||||
- Uses PKCE (code challenge + verifier)
|
||||
- Authorization: `https://auth.openai.com/oauth/authorize`
|
||||
- Token: `https://auth.openai.com/oauth/token`
|
||||
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
|
||||
- Fixed redirect: `http://localhost:1455/auth/callback`
|
||||
- Scopes: `openid profile email offline_access`
|
||||
- JWT claim path: `https://api.openai.com/auth`
|
||||
|
||||
**OCA OAuth** (`packages/core/src/auth/oca.ts`):
|
||||
- Uses PKCE (S256 code challenge)
|
||||
- Supports `internal` and `external` mode with separate IDCS URLs and client IDs
|
||||
- Authorization: `{idcsUrl}/oauth2/v1/authorize`
|
||||
- Token: `{idcsUrl}/oauth2/v1/token`
|
||||
- Configurable callback ports and path
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 512 535">
|
||||
<!-- Generator: Adobe Illustrator 29.8.5, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M500.6,300.5c-9-20.7-17.9-41.4-26.9-62.1-.7-2-.3-4.4-.3-6.4.4-9,1.1-18,1.4-27,2.8-28.4-6.5-58-25.2-79.6-15.1-18.1-36.6-30.7-59.6-35.5-8.1-1.8-16.6-1.6-25-2.1-10-.7-20-1-30-1.7,2-11.9,1-24.1-3.7-35.3-5.8-14.1-16.8-25.9-30.6-32.5-14.4-7-31.5-8.2-46.7-3.1-16,5.2-29.5,17-36.8,32.1-4.9,10-6.8,21.2-6.1,32.2-19.7-1-39.4-2.2-59.1-3.1-26.8.5-53,11.7-72,30.6-20.2,19.5-31.7,47-32.3,75-.5,9.3-1,18.7-1.5,28-.2,2.1,0,4.1-1.2,6-9.8,16.8-19.5,33.7-29.4,50.6-2.2,4.1-4.9,8-6.6,12.3-2,5.7-1.2,12.2,1.3,17.6,8.9,19.5,17.6,39.2,26.5,58.7.8,1.9,1.5,3.7,1.3,5.8-.6,10.3-1.1,20.7-1.7,31-1.5,21.2,3,42.6,13.5,61.1,8.8,15.8,21.6,29.4,37.1,38.9,13.9,8.7,29.7,13.9,46,15.4,72,3.9,144,7.7,216,11.5,20.1,1.8,40.8-2.8,58.5-12.5,18.8-10.1,34.2-26,44.1-44.9,6.5-12.6,10.5-26.4,11.7-40.5.7-12.4,1.2-24.7,2-37.1,0-3.3,1.9-5.5,3.3-8.2,6.6-11.8,13.5-23.4,20.1-35.2,3.7-6.9,8.1-13.4,11.6-20.4,3.2-6.1,3.2-13.5.3-19.7ZM218.5,316.5c-9.7,7.1-21.3,12.3-33.5,12.5-17.6,1-35.1-5.3-49-16-4.6-3.2-8.1-7.5-9.6-13,0-1.8-.7-3.6,1.7-3.8,4,1,7.9,2.6,12,3.5,22.8,5.6,47.6,5.9,71,4.8,6.5-.2,13-1.3,19.5-.9-2.7,5.6-7.1,9.2-12,12.9ZM276,449.7c-14,.5-28,.1-42-.2-2.1,0-4.3,0-6.4-.4-.9-2.1.6-3.2,1.7-4.8,4.8-5.9,11-11,18.7-12.4,8.4-1.6,16.5,1.2,23.5,5.5,4.7,3,9.2,6.3,12.6,10.8-2.6,1.1-5.3,1.4-8.1,1.4ZM390.4,319.4c-16.4,14.2-38.8,21.8-60.4,18.4-13.2-1.6-24.7-8.6-34.1-17.7-3-3-6.2-6.5-8.1-10.4.5-1,1.2-1.6,2.2-1.6,2.8-.2,5.7.7,8.5,1.1,16,2.9,32.3,4.9,48.5,5.5,14.3.4,28.2-.2,42.2-3.6,2.2-.6,3.7-.3,5.8.5-1.1,2.9-2.2,5.7-4.6,7.8Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
+152
-1
@@ -1,6 +1,157 @@
|
||||
# cline
|
||||
|
||||
## 2.4.2
|
||||
## [2.12.0]
|
||||
|
||||
### Added
|
||||
|
||||
- `read_file` tool now supports chunked reading for targeted file access
|
||||
|
||||
### Fixed
|
||||
|
||||
- Exclude `new_task` tool from system prompt in yolo/headless mode
|
||||
|
||||
### Changed
|
||||
|
||||
- Polish `Notification` hook functionality
|
||||
|
||||
## [2.9.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Latency improvements for remote workspaces
|
||||
|
||||
## [2.8.2]
|
||||
|
||||
### Fixed
|
||||
- Use `kanban@latest` in `cline kanban` to always fetch the newest version
|
||||
|
||||
## [2.8.1]
|
||||
|
||||
### Added
|
||||
- Implement dynamic free model detection for Cline API
|
||||
- Add file read deduplication cache to prevent repeated reads
|
||||
- Add feature tips tooltip during thinking state
|
||||
|
||||
### Fixed
|
||||
- Fix flaky CLI Enter-key handling across Windows/test environments
|
||||
- Replace error message when not logged in to Cline
|
||||
- Align ClineRulesToggleModal padding with ServersToggleModal
|
||||
- Skip WebP for GLM and Devstral models running through llama.cpp
|
||||
- Respect user-configured context window in LiteLLM getModel()
|
||||
- Honor explicit model IDs outside static catalog in W&B provider
|
||||
- Add missing Fireworks serverless models and pricing
|
||||
|
||||
## [2.8.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added W&B Inference by CoreWeave as a new API provider with 17 models including DeepSeek-V3.1, Llama 4, and Qwen3-Coder
|
||||
- Added CLI TUI end-to-end test suite
|
||||
|
||||
### Fixed
|
||||
|
||||
- Claude Code: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
|
||||
- CLI: `/q` and `/exit` slash commands now execute immediately on Enter without requiring the slash menu to be visible
|
||||
- CLI: slash command filtering now prioritizes exact and prefix matches over fuzzy matches
|
||||
|
||||
## [2.7.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added MCP add shortcuts for stdio and HTTP servers
|
||||
- Added `--continue` for the current directory
|
||||
- Added `--auto-condense` flag for AI-powered context compaction
|
||||
- Added `--hooks-dir` flag for runtime hook injection
|
||||
- Enabled error autocapture
|
||||
- Prompt rules now include test verification guidance and make `CLI_RULES` language-agnostic
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed remount behavior so TUI remounts only on width resize
|
||||
- Fixed startup prompt replay on resize remount
|
||||
- Fixed task flags so they are applied before the welcome TUI mounts
|
||||
|
||||
### Changed
|
||||
|
||||
- Hooks: reintroduced feature toggle
|
||||
|
||||
## [2.6.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Added GPT-5.4 models for ChatGPT subscription users
|
||||
- Hooks: Added a `Notification` hook for attention and completion boundaries
|
||||
- Added `--hooks-dir` CLI flag for runtime hook injection
|
||||
- Added `--auto-approve-all` CLI flag for interactive mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handle streamable HTTP MCP reconnects more reliably
|
||||
|
||||
## [2.6.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Hook payloads now include `model.provider` and `model.slug`
|
||||
- Token/cost updates now happen immediately as usage chunks arrive, not after tool execution
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve subagent context compaction logic
|
||||
- Subagent stream retry delay increased to reduce noise from transient failures
|
||||
- State serialization errors are now caught and logged instead of crashing
|
||||
- Removed incorrect `max_tokens` from OpenRouter requests
|
||||
|
||||
## [2.5.2]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Windows PowerShell support for hooks (execution, resolution, and management), improving hook behavior on Windows for CLI and shared core workflows.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restored GPT-OSS native file editing for OpenAI-compatible models used through shared core tooling.
|
||||
- Improved OpenRouter context overflow error handling so auto-compaction triggers correctly for wrapped 400 errors.
|
||||
- Hardened checkpoint recovery by retrying nested git restore and preventing silent `.git_disabled` leftovers.
|
||||
- Added a User-Agent header for requests to the Cline back-end to improve request handling consistency.
|
||||
|
||||
## [2.5.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Expanded CLI markdown rendering support (headings, lists, blockquotes, fenced code blocks, links, and nested lists).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed CLI headless auth provider model metadata loading for Cline and Vercel AI Gateway by fetching model info from API with cache fallback.
|
||||
- Increased flaky CLI import test timeout on Windows CI to reduce intermittent test failures.
|
||||
|
||||
## [2.5.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
|
||||
- Added Codex 5.3 model support
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OpenAI Codex by setting `store` to `false`
|
||||
- Use `isLocatedInPath()` instead of string matching for path containment checks
|
||||
|
||||
## [2.4.3]
|
||||
|
||||
### Added
|
||||
|
||||
- Add /q command to quit CLI
|
||||
- Fetch featured models from backend with local fallback
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix auth check for ACP mode
|
||||
- Fix Cline auth with ACP flag
|
||||
- Fix yolo mode to not persist yolo setting to disk
|
||||
|
||||
## [2.4.2]
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+42
-10
@@ -186,6 +186,7 @@ const buildEnvVars: Record<string, string> = {
|
||||
const buildTimeEnvs = [
|
||||
"TELEMETRY_SERVICE_API_KEY",
|
||||
"ERROR_SERVICE_API_KEY",
|
||||
"ENABLE_ERROR_AUTOCAPTURE",
|
||||
"POSTHOG_TELEMETRY_ENABLED",
|
||||
"OTEL_TELEMETRY_ENABLED",
|
||||
"OTEL_LOGS_EXPORTER",
|
||||
@@ -208,8 +209,8 @@ if (production) {
|
||||
buildEnvVars["process.env.IS_DEV"] = "false"
|
||||
}
|
||||
|
||||
const config: esbuild.BuildOptions = {
|
||||
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
||||
// Shared build options
|
||||
const sharedOptions: Partial<esbuild.BuildOptions> = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
@@ -221,7 +222,6 @@ const config: esbuild.BuildOptions = {
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
outfile: path.join(__dirname, "dist", "cli.mjs"),
|
||||
// These modules need to load files from the module directory at runtime
|
||||
external: [
|
||||
"@grpc/reflection",
|
||||
@@ -237,6 +237,13 @@ const config: esbuild.BuildOptions = {
|
||||
"@vscode/ripgrep", // Uses __dirname to locate the binary
|
||||
],
|
||||
supported: { "top-level-await": true },
|
||||
}
|
||||
|
||||
// CLI executable configuration
|
||||
const cliConfig: esbuild.BuildOptions = {
|
||||
...sharedOptions,
|
||||
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
||||
outfile: path.join(__dirname, "dist", "cli.mjs"),
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
// Suppress all Node.js warnings (deprecation, experimental, etc.)
|
||||
@@ -250,19 +257,44 @@ const __dirname = _dirname(__filename);`,
|
||||
},
|
||||
}
|
||||
|
||||
// Library configuration for programmatic use
|
||||
const libConfig: esbuild.BuildOptions = {
|
||||
...sharedOptions,
|
||||
entryPoints: [path.join(__dirname, "src", "exports.ts")],
|
||||
outfile: path.join(__dirname, "dist", "lib.mjs"),
|
||||
banner: {
|
||||
js: `// Cline Library - Programmatic API
|
||||
import { createRequire as _createRequire } from 'module';
|
||||
import { fileURLToPath as _fileURLToPath } from 'url';
|
||||
import { dirname as _dirname } from 'path';
|
||||
const require = _createRequire(import.meta.url);
|
||||
const __filename = _fileURLToPath(import.meta.url);
|
||||
const __dirname = _dirname(__filename);`,
|
||||
},
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
// In watch mode, only watch the CLI (primary use case for development)
|
||||
const ctx = await esbuild.context(cliConfig)
|
||||
await ctx.watch()
|
||||
console.log("[cli] Watching for changes...")
|
||||
} else {
|
||||
await ctx.rebuild()
|
||||
await ctx.dispose()
|
||||
// Build both CLI and library
|
||||
console.log("[cli esbuild] Building CLI executable...")
|
||||
const cliCtx = await esbuild.context(cliConfig)
|
||||
await cliCtx.rebuild()
|
||||
await cliCtx.dispose()
|
||||
|
||||
// Make the output executable
|
||||
const outfile = path.join(__dirname, "dist", "cli.mjs")
|
||||
if (fs.existsSync(outfile)) {
|
||||
fs.chmodSync(outfile, "755")
|
||||
console.log("[cli esbuild] Building library bundle...")
|
||||
const libCtx = await esbuild.context(libConfig)
|
||||
await libCtx.rebuild()
|
||||
await libCtx.dispose()
|
||||
|
||||
// Make the CLI output executable
|
||||
const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
|
||||
if (fs.existsSync(cliOutfile)) {
|
||||
fs.chmodSync(cliOutfile, "755")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,8 @@ When running **cline** with just a prompt (no subcommand), these options are ava
|
||||
|
||||
**-T**, **\--taskId** *id* : Resume an existing task by ID instead of starting a new one. The prompt becomes an optional follow-up message.
|
||||
|
||||
**\--continue** : Resume the most recent task from the current working directory instead of starting a new one.
|
||||
|
||||
# JSON OUTPUT FORMAT
|
||||
|
||||
When using **\--json**, each message is output as a JSON object with these fields:
|
||||
@@ -268,6 +270,9 @@ cline -T abc123def
|
||||
# Resume a task with a follow-up message
|
||||
cline -T abc123def "Now add unit tests for the changes"
|
||||
|
||||
# Resume the most recent task from the current directory
|
||||
cline --continue
|
||||
|
||||
# Resume in plan mode to review before continuing
|
||||
cline -T abc123def -p "What's left to do?"
|
||||
|
||||
|
||||
+15
-5
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.4.2",
|
||||
"version": "2.13.0",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"main": "dist/lib.mjs",
|
||||
"types": "dist/lib.d.ts",
|
||||
"bin": {
|
||||
"cline": "./dist/cli.mjs"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/lib.mjs",
|
||||
"types": "./dist/lib.d.ts"
|
||||
}
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
@@ -23,8 +30,9 @@
|
||||
"scripts": {
|
||||
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
|
||||
"package": "npm pack --pack-destination ./dist",
|
||||
"build": "npm run typecheck && npx tsx esbuild.mts",
|
||||
"build:production": "npm run typecheck && npx tsx esbuild.mts --production",
|
||||
"build": "npm run typecheck && npx tsx esbuild.mts && npm run build:types",
|
||||
"build:production": "npm run typecheck && npx tsx esbuild.mts --production && npm run build:types",
|
||||
"build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && mkdir -p dist/agent && cp dist/types/cli/src/agent/ClineAgent.d.ts dist/types/cli/src/agent/ClineSessionEmitter.d.ts dist/types/cli/src/agent/public-types.d.ts dist/agent/ && rm -rf dist/types",
|
||||
"watch": "npx tsx esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
|
||||
"clean": "rimraf dist",
|
||||
@@ -62,6 +70,7 @@
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/node": "20.x",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^19.2.9",
|
||||
@@ -81,8 +90,9 @@
|
||||
"ink": "npm:@jrichman/ink@6.4.7",
|
||||
"ink-picture": "^1.3.3",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ora": "^8.0.1",
|
||||
"marked": "^17.0.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"ora": "^8.0.1",
|
||||
"pino": "^10.0.0",
|
||||
"pino-roll": "^4.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
|
||||
@@ -108,11 +108,7 @@ class ACPDiffServiceClient implements DiffServiceClientInterface {
|
||||
class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
private readonly version: string
|
||||
|
||||
constructor(
|
||||
_clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
_sessionIdResolver: SessionIdResolver,
|
||||
version: string = "1.0.0",
|
||||
) {
|
||||
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
|
||||
this.version = version
|
||||
}
|
||||
|
||||
@@ -402,7 +398,7 @@ export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
|
||||
clientCapabilities: acp.ClientCapabilities | undefined,
|
||||
sessionIdResolver: SessionIdResolver,
|
||||
cwdResolver: CwdResolver,
|
||||
version: string = "1.0.0",
|
||||
version: string,
|
||||
) {
|
||||
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
|
||||
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
|
||||
|
||||
+5
-21
@@ -15,7 +15,7 @@
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import { ClineAgent } from "../agent/ClineAgent.js"
|
||||
import type { AcpAgentOptions, SessionUpdateType } from "../agent/types.js"
|
||||
import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js"
|
||||
|
||||
/**
|
||||
* ACP Agent wrapper that bridges stdio connection to ClineAgent.
|
||||
@@ -39,37 +39,21 @@ export class AcpAgent implements acp.Agent {
|
||||
this.clineAgent = new ClineAgent(options)
|
||||
|
||||
// Wire up the permission handler to use the connection
|
||||
this.clineAgent.setPermissionHandler(async (request, resolve) => {
|
||||
this.clineAgent.setPermissionHandler(async (request) => {
|
||||
try {
|
||||
Logger.debug("[AcpAgent] Forwarding permission request to connection")
|
||||
const response = await this.connection.requestPermission({
|
||||
sessionId: this.getCurrentSessionId() ?? "",
|
||||
return await this.connection.requestPermission({
|
||||
sessionId: request.sessionId,
|
||||
toolCall: request.toolCall,
|
||||
options: request.options,
|
||||
})
|
||||
resolve(response)
|
||||
} catch (error) {
|
||||
Logger.debug("[AcpAgent] Error requesting permission:", error)
|
||||
resolve({ outcome: "rejected" as unknown as acp.RequestPermissionOutcome })
|
||||
return { outcome: { outcome: "cancelled" } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current active session ID from the ClineAgent.
|
||||
*/
|
||||
private getCurrentSessionId(): string | undefined {
|
||||
// Find the session that's currently processing
|
||||
for (const [sessionId, session] of this.clineAgent.sessions) {
|
||||
if (session.controller?.task) {
|
||||
return sessionId
|
||||
}
|
||||
}
|
||||
// Fall back to the first session if none is actively processing
|
||||
const firstSession = this.clineAgent.sessions.keys().next()
|
||||
return firstSession.done ? undefined : firstSession.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to session events and forward them to the connection.
|
||||
*/
|
||||
|
||||
@@ -15,22 +15,18 @@
|
||||
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { version as CLI_VERSION } from "../../../package.json"
|
||||
import { AcpAgent } from "./AcpAgent.js"
|
||||
import { nodeToWebReadable, nodeToWebWritable } from "./streamUtils.js"
|
||||
|
||||
// Re-export classes for programmatic use
|
||||
export { ClineAgent } from "../agent/ClineAgent.js"
|
||||
export { ClineSessionEmitter } from "../agent/ClineSessionEmitter.js"
|
||||
// Re-export types
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
ClineAcpSession,
|
||||
ClineAgentOptions,
|
||||
ClineSessionEvents,
|
||||
PermissionHandler,
|
||||
PermissionResolver,
|
||||
} from "../agent/types.js"
|
||||
export { AcpAgent } from "./AcpAgent.js"
|
||||
|
||||
@@ -73,6 +69,8 @@ export interface AcpModeOptions {
|
||||
config?: string
|
||||
/** Working directory (default: process.cwd()) */
|
||||
cwd?: string
|
||||
/** Additional runtime hooks directory */
|
||||
hooksDir?: string
|
||||
/** Enable verbose/debug logging to stderr */
|
||||
verbose?: boolean
|
||||
}
|
||||
@@ -99,8 +97,8 @@ export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
|
||||
|
||||
new AgentSideConnection((conn) => {
|
||||
agent = new AcpAgent(conn, {
|
||||
version: CLI_VERSION,
|
||||
debug: Boolean(options.verbose),
|
||||
hooksDir: options.hooksDir,
|
||||
})
|
||||
return agent
|
||||
}, stream)
|
||||
|
||||
+49
-65
@@ -38,11 +38,11 @@ import {
|
||||
} from "@shared/api"
|
||||
import type { ClineAsk, ClineMessage as ClineMessageType } from "@shared/ExtensionMessage"
|
||||
import { CLI_ONLY_COMMANDS, VSCODE_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { getProviderModelIdKey } from "@shared/storage/provider-keys"
|
||||
import { ClineEndpoint } from "@/config.js"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { getAvailableSlashCommands } from "@/core/controller/slash/getAvailableSlashCommands"
|
||||
import { setRuntimeHooksDir } from "@/core/storage/disk"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler.js"
|
||||
import { ExternalCommentReviewController } from "@/hosts/external/ExternalCommentReviewController.js"
|
||||
@@ -55,15 +55,19 @@ import { AuthService } from "@/services/auth/AuthService.js"
|
||||
import { Logger } from "@/shared/services/Logger.js"
|
||||
import type { Mode } from "@/shared/storage/types"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { version as AGENT_VERSION } from "../../package.json"
|
||||
import { ACPDiffViewProvider } from "../acp/ACPDiffViewProvider.js"
|
||||
import { ACPHostBridgeClientProvider } from "../acp/ACPHostBridgeClientProvider.js"
|
||||
import { AcpTerminalManager } from "../acp/AcpTerminalManager.js"
|
||||
import { isAuthConfigured } from "../utils/auth"
|
||||
import { fetchOpenRouterModels, usesOpenRouterModels } from "../utils/openrouter-models"
|
||||
import { CliContextResult, initializeCliContext } from "../vscode-context.js"
|
||||
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
|
||||
import { translateMessage } from "./messageTranslator.js"
|
||||
import { handlePermissionResponse } from "./permissionHandler.js"
|
||||
import type { AcpSessionState, ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./types.js"
|
||||
import type { ClineAcpSession, ClineAgentOptions, PermissionHandler } from "./public-types.js"
|
||||
import { AcpSessionStatus } from "./public-types.js"
|
||||
import { type AcpSessionState } from "./types.js"
|
||||
|
||||
// Map providers to their static model lists and defaults (copied from ModelPicker.tsx)
|
||||
const providerModels: Record<string, { models: Record<string, unknown>; defaultId: string }> = {
|
||||
@@ -104,7 +108,12 @@ function getModelList(provider: string): string[] {
|
||||
export class ClineAgent implements acp.Agent {
|
||||
private readonly options: ClineAgentOptions
|
||||
private readonly ctx: CliContextResult
|
||||
readonly sessions: Map<string, ClineAcpSession> = new Map()
|
||||
|
||||
/** Map of active sessions by session ID */
|
||||
public readonly sessions: Map<string, ClineAcpSession> = new Map()
|
||||
|
||||
/** WeakMap to associate ClineAcpSession with its Controller without exposing it to consumers */
|
||||
readonly #sessionControllers = new WeakMap<ClineAcpSession, Controller>()
|
||||
|
||||
/** Runtime state for active sessions */
|
||||
private readonly sessionStates: Map<string, AcpSessionState> = new Map()
|
||||
@@ -132,7 +141,8 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
constructor(options: ClineAgentOptions) {
|
||||
this.options = options
|
||||
this.ctx = initializeCliContext()
|
||||
setRuntimeHooksDir(options.hooksDir)
|
||||
this.ctx = initializeCliContext({ clineDir: options.clineDir })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +204,7 @@ export class ClineAgent implements acp.Agent {
|
||||
},
|
||||
agentInfo: {
|
||||
name: "cline",
|
||||
version: this.options.version,
|
||||
version: AGENT_VERSION,
|
||||
},
|
||||
authMethods: [
|
||||
{
|
||||
@@ -226,7 +236,7 @@ export class ClineAgent implements acp.Agent {
|
||||
clientCapabilities,
|
||||
() => this.currentActiveSessionId,
|
||||
() => this.sessions.get(this.currentActiveSessionId ?? "")?.cwd ?? process.cwd(),
|
||||
this.options.version,
|
||||
AGENT_VERSION,
|
||||
)
|
||||
|
||||
HostProvider.initialize(
|
||||
@@ -265,7 +275,7 @@ export class ClineAgent implements acp.Agent {
|
||||
*/
|
||||
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
|
||||
// Check if authentication is required
|
||||
const isAuthenticated = await this.isAuthConfigured()
|
||||
const isAuthenticated = await isAuthConfigured()
|
||||
if (!isAuthenticated) {
|
||||
throw RequestError.authRequired()
|
||||
}
|
||||
@@ -289,16 +299,16 @@ export class ClineAgent implements acp.Agent {
|
||||
mcpServers: params.mcpServers ?? [],
|
||||
createdAt: Date.now(),
|
||||
lastActivityAt: Date.now(),
|
||||
controller,
|
||||
}
|
||||
|
||||
this.#sessionControllers.set(session, controller)
|
||||
|
||||
this.sessions.set(sessionId, session)
|
||||
|
||||
// Initialize session state
|
||||
const sessionState: AcpSessionState = {
|
||||
sessionId,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
status: AcpSessionStatus.Idle,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
|
||||
@@ -435,11 +445,11 @@ export class ClineAgent implements acp.Agent {
|
||||
*
|
||||
* The prompt flow:
|
||||
* 1. Extract content from the ACP prompt (text, images, files)
|
||||
* 2. Set up state broadcasting (subscribe to controller updates)
|
||||
* 3. Initialize or continue task with Controller
|
||||
* 2. Set up internal cline state subsription
|
||||
* 3. Initialize or continue cline task
|
||||
* 4. Translate ClineMessages to ACP SessionUpdates
|
||||
* 5. Handle permission requests for tools/commands
|
||||
* 6. Return when task completes, is cancelled, or needs user input
|
||||
* 6. Return when cline task completes, is cancelled, or needs user input
|
||||
*/
|
||||
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
|
||||
const session = this.sessions.get(params.sessionId)
|
||||
@@ -449,11 +459,11 @@ export class ClineAgent implements acp.Agent {
|
||||
throw new Error(`Session not found: ${params.sessionId}`)
|
||||
}
|
||||
|
||||
if (sessionState.isProcessing) {
|
||||
if (sessionState.status === AcpSessionStatus.Processing) {
|
||||
throw new Error(`Session ${params.sessionId} is already processing a prompt`)
|
||||
}
|
||||
|
||||
const controller = session.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (!controller) {
|
||||
throw new Error("Controller not initialized for session. This is a bug in the ACP agent setup.")
|
||||
}
|
||||
@@ -464,8 +474,7 @@ export class ClineAgent implements acp.Agent {
|
||||
})
|
||||
|
||||
// Mark session as processing and set as current active session
|
||||
sessionState.isProcessing = true
|
||||
sessionState.cancelled = false
|
||||
sessionState.status = AcpSessionStatus.Processing
|
||||
session.lastActivityAt = Date.now()
|
||||
this.currentActiveSessionId = params.sessionId
|
||||
|
||||
@@ -586,7 +595,7 @@ export class ClineAgent implements acp.Agent {
|
||||
Logger.debug("[ClineAgent] Error during cleanup:", error)
|
||||
}
|
||||
}
|
||||
sessionState.isProcessing = false
|
||||
sessionState.status = AcpSessionStatus.Idle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,7 +657,13 @@ export class ClineAgent implements acp.Agent {
|
||||
permissionRequest: Omit<acp.RequestPermissionRequest, "sessionId">,
|
||||
): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
const controller = session?.controller
|
||||
|
||||
if (!session) {
|
||||
Logger.debug("[ClineAgent] No session found for permission request")
|
||||
return
|
||||
}
|
||||
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
|
||||
if (!controller?.task) {
|
||||
Logger.debug("[ClineAgent] No active task for permission request")
|
||||
@@ -829,7 +844,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
await this.emitSessionUpdate(sessionId, {
|
||||
sessionUpdate,
|
||||
content: { type: "text", text: needsNewline ? "\n" + textDelta : textDelta },
|
||||
content: { type: "text", text: needsNewline ? `\n${textDelta}` : textDelta },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -882,18 +897,22 @@ export class ClineAgent implements acp.Agent {
|
||||
*/
|
||||
async cancel(params: acp.CancelNotification): Promise<void> {
|
||||
const session = this.sessions.get(params.sessionId)
|
||||
if (!session) {
|
||||
Logger.debug("[ClineAgent] cancel called for non-existent session:", params.sessionId)
|
||||
return
|
||||
}
|
||||
const sessionState = this.sessionStates.get(params.sessionId)
|
||||
|
||||
Logger.debug("[ClineAgent] cancel called:", {
|
||||
sessionId: params.sessionId,
|
||||
isProcessing: sessionState?.isProcessing,
|
||||
status: sessionState?.status,
|
||||
})
|
||||
|
||||
if (sessionState) {
|
||||
sessionState.cancelled = true
|
||||
sessionState.status = AcpSessionStatus.Cancelled
|
||||
|
||||
// If we have an active controller task, cancel it
|
||||
const controller = session?.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (controller?.task) {
|
||||
try {
|
||||
await controller.cancelTask()
|
||||
@@ -934,7 +953,7 @@ export class ClineAgent implements acp.Agent {
|
||||
session.lastActivityAt = Date.now()
|
||||
|
||||
// Update Controller mode if active
|
||||
const controller = session.controller
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
if (controller) {
|
||||
controller.stateManager.setGlobalState("mode", session.mode)
|
||||
|
||||
@@ -1065,7 +1084,7 @@ export class ClineAgent implements acp.Agent {
|
||||
* @returns The permission response from the client
|
||||
*/
|
||||
protected async requestPermission(
|
||||
_sessionId: string,
|
||||
sessionId: string,
|
||||
toolCall: acp.ToolCallUpdate,
|
||||
options: acp.PermissionOption[],
|
||||
): Promise<acp.RequestPermissionResponse> {
|
||||
@@ -1080,17 +1099,15 @@ export class ClineAgent implements acp.Agent {
|
||||
return { outcome: "rejected" as unknown as acp.RequestPermissionOutcome }
|
||||
}
|
||||
|
||||
// Use the permission handler callback pattern
|
||||
return new Promise<acp.RequestPermissionResponse>((resolve) => {
|
||||
this.permissionHandler!({ toolCall, options }, resolve)
|
||||
})
|
||||
return await this.permissionHandler({ sessionId, toolCall, options })
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
await session.controller?.task?.abortTask()
|
||||
await session.controller?.stateManager.flushPendingState()
|
||||
await session.controller?.dispose()
|
||||
const controller = this.#sessionControllers.get(session)
|
||||
await controller?.task?.abortTask()
|
||||
await controller?.stateManager.flushPendingState()
|
||||
await controller?.dispose()
|
||||
this.sessions.delete(sessionId)
|
||||
this.sessionStates.delete(sessionId)
|
||||
}
|
||||
@@ -1146,39 +1163,6 @@ export class ClineAgent implements acp.Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has authentication configured.
|
||||
* Returns true if they have either:
|
||||
* - Cline provider with stored auth data
|
||||
* - OpenAI Codex provider with OAuth credentials
|
||||
* - BYO provider with an API key configured
|
||||
*/
|
||||
private async isAuthConfigured(): Promise<boolean> {
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") as string
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = (stateManager.getGlobalSettingsKey(providerKey) as string) || "cline"
|
||||
|
||||
if (currentProvider === "cline") {
|
||||
// For Cline provider, check if we have stored auth data
|
||||
return Boolean(stateManager.getSecretKey("clineApiKey") || stateManager.getSecretKey("clineAccountId"))
|
||||
}
|
||||
|
||||
// For OpenAI Codex provider, check OAuth credentials
|
||||
if (currentProvider === "openai-codex") {
|
||||
return await openAiCodexOAuthManager.isAuthenticated()
|
||||
}
|
||||
|
||||
// For BYO providers, check if the API key is configured
|
||||
const keyField = ProviderToApiKeyMap[currentProvider as keyof typeof ProviderToApiKeyMap]
|
||||
if (!keyField) {
|
||||
return false
|
||||
}
|
||||
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
return fields.some((key) => stateManager.getSecretKey(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OpenAI Codex OAuth authentication flow.
|
||||
*
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events"
|
||||
import type { ClineSessionEvents } from "./types.js"
|
||||
import type { ClineSessionEvents } from "./public-types.js"
|
||||
|
||||
/**
|
||||
* Type-safe EventEmitter for ClineAgent session events.
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { beforeEach, describe, expect, it } from "vitest"
|
||||
import { createSessionState, translateMessage, translateMessages } from "./messageTranslator"
|
||||
import type { AcpSessionState } from "./types"
|
||||
import { AcpSessionStatus } from "./types"
|
||||
|
||||
// =============================================================================
|
||||
// Test Helpers
|
||||
@@ -175,8 +176,7 @@ describe("createSessionState", () => {
|
||||
const state = createSessionState("my-session-123")
|
||||
|
||||
expect(state.sessionId).toBe("my-session-123")
|
||||
expect(state.isProcessing).toBe(false)
|
||||
expect(state.cancelled).toBe(false)
|
||||
expect(state.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state.pendingToolCalls).toBeInstanceOf(Map)
|
||||
expect(state.pendingToolCalls.size).toBe(0)
|
||||
expect(state.currentToolCallId).toBeUndefined()
|
||||
@@ -187,11 +187,11 @@ describe("createSessionState", () => {
|
||||
const state2 = createSessionState("session-2")
|
||||
|
||||
// Modify state1
|
||||
state1.isProcessing = true
|
||||
state1.status = AcpSessionStatus.Processing
|
||||
state1.pendingToolCalls.set("tool-1", {} as acp.ToolCall)
|
||||
|
||||
// state2 should be unaffected
|
||||
expect(state2.isProcessing).toBe(false)
|
||||
expect(state2.status).toBe(AcpSessionStatus.Idle)
|
||||
expect(state2.pendingToolCalls.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { ClineMessage, ClineSayBrowserAction, ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import type { AcpSessionState, TranslatedMessage } from "./types.js"
|
||||
import { AcpSessionStatus } from "./types.js"
|
||||
|
||||
/**
|
||||
* Maps Cline tool types to ACP ToolKind values.
|
||||
@@ -631,7 +632,6 @@ function translateAskMessage(
|
||||
case "new_task":
|
||||
case "condense":
|
||||
case "summarize_task":
|
||||
case "report_bug":
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "command_output":
|
||||
@@ -1019,8 +1019,7 @@ export function translateMessages(messages: ClineMessage[], sessionState: AcpSes
|
||||
export function createSessionState(sessionId: string): AcpSessionState {
|
||||
return {
|
||||
sessionId,
|
||||
isProcessing: false,
|
||||
cancelled: false,
|
||||
status: AcpSessionStatus.Idle,
|
||||
pendingToolCalls: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Public types for the Cline library API.
|
||||
*
|
||||
* This file contains types that are safe to export to library consumers.
|
||||
* It must NOT import any internal types (Controller, StateManager, etc.)
|
||||
* to keep the generated declaration files clean.
|
||||
*
|
||||
* Internal-only extensions of these types live in ./types.ts.
|
||||
*/
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
|
||||
// ============================================================
|
||||
// Session Update Type Utilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Different types of updates that can be sent during session processing.
|
||||
*
|
||||
* These updates provide real-time feedback about the agent's progress.
|
||||
*
|
||||
* See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
|
||||
*/
|
||||
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
|
||||
|
||||
/**
|
||||
* Different types of update payloads that can be sent during session processing.
|
||||
*
|
||||
* Each update type has a corresponding payload structure defined in the ACP SessionUpdate union.
|
||||
*/
|
||||
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
|
||||
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
|
||||
"sessionUpdate"
|
||||
>
|
||||
|
||||
// ============================================================
|
||||
// Permission Handler Callback Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Handler function for permission requests.
|
||||
* Called when the agent needs permission for a tool call.
|
||||
* The handler should present the request to the user and call resolve() with their response.
|
||||
*/
|
||||
export type PermissionHandler = (request: acp.RequestPermissionRequest) => Promise<acp.RequestPermissionResponse>
|
||||
|
||||
// ============================================================
|
||||
// Session Event Emitter Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Maps ACP SessionUpdate types to their event listener signatures.
|
||||
* Uses the sessionUpdate discriminator to derive event names and payload types.
|
||||
*/
|
||||
export type ClineSessionEvents = {
|
||||
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
|
||||
} & {
|
||||
/** Error event for session-level errors (not part of ACP SessionUpdate) */
|
||||
error: (error: Error) => void
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ClineAgent Options
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Options for creating a ClineAgent instance.
|
||||
*/
|
||||
export interface ClineAgentOptions {
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
/** Cline Config Directory (defaults to ~/.cline) */
|
||||
clineDir?: string
|
||||
/** Additional runtime hooks directory */
|
||||
hooksDir?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an ACP agent instance.
|
||||
*/
|
||||
export interface AcpAgentOptions {
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
/** Additional runtime hooks directory */
|
||||
hooksDir?: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Session Types
|
||||
// ============================================================
|
||||
export type SessionID = string
|
||||
|
||||
/**
|
||||
* Extended session data stored by Cline for ACP sessions.
|
||||
*/
|
||||
export interface ClineAcpSession {
|
||||
/** Unique session ID */
|
||||
sessionId: SessionID
|
||||
/** Working directory for the session */
|
||||
cwd: string
|
||||
/** Current mode (plan/act) */
|
||||
mode: "plan" | "act"
|
||||
/** MCP servers passed from the client */
|
||||
mcpServers: acp.McpServer[]
|
||||
/** Timestamp when session was created */
|
||||
createdAt: number
|
||||
/** Timestamp of last activity */
|
||||
lastActivityAt: number
|
||||
/** Whether this session was loaded from history (needs resume on first prompt) */
|
||||
isLoadedFromHistory?: boolean
|
||||
/** Model ID override for plan mode (format: "provider/modelId") */
|
||||
planModeModelId?: string
|
||||
/** Model ID override for act mode (format: "provider/modelId") */
|
||||
actModeModelId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle status of an ACP session.
|
||||
*
|
||||
* Represents the state machine:
|
||||
* Idle → Processing → Idle (normal completion)
|
||||
* Idle → Processing → Cancelled (cancellation, then back to Idle on next prompt)
|
||||
*/
|
||||
export enum AcpSessionStatus {
|
||||
/** Session is idle, waiting for a prompt */
|
||||
Idle = "idle",
|
||||
/** Session is actively processing a prompt */
|
||||
Processing = "processing",
|
||||
/** Session processing was cancelled */
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
/**
|
||||
* State tracking for an active ACP session within Cline.
|
||||
*/
|
||||
export interface AcpSessionState {
|
||||
/** Session ID */
|
||||
sessionId: SessionID
|
||||
/** Current lifecycle status of the session */
|
||||
status: AcpSessionStatus
|
||||
/** Current tool call ID being executed (if any) */
|
||||
currentToolCallId?: string
|
||||
/** Accumulated tool calls for permission batching */
|
||||
pendingToolCalls: Map<string, acp.ToolCall>
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Agent Capabilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Cline-specific agent capabilities extending the ACP base capabilities.
|
||||
*/
|
||||
export interface ClineAgentCapabilities {
|
||||
/** Support for loading sessions from disk */
|
||||
loadSession: boolean
|
||||
/** Prompt capabilities for the agent */
|
||||
promptCapabilities: {
|
||||
/** Support for image inputs */
|
||||
image: boolean
|
||||
/** Support for audio inputs */
|
||||
audio: boolean
|
||||
/** Support for embedded context (file resources) */
|
||||
embeddedContext: boolean
|
||||
}
|
||||
/** MCP server passthrough capabilities */
|
||||
mcpCapabilities: {
|
||||
/** Support for HTTP MCP servers */
|
||||
http: boolean
|
||||
/** Support for SSE MCP servers */
|
||||
sse: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cline agent info for ACP initialization response.
|
||||
*/
|
||||
export interface ClineAgentInfo {
|
||||
name: "cline"
|
||||
title: "Cline"
|
||||
version: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Permission Options
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Permission option as presented to the ACP client.
|
||||
*/
|
||||
export interface ClinePermissionOption {
|
||||
kind: acp.PermissionOptionKind
|
||||
name: string
|
||||
optionId: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Message Translation
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Result of translating a Cline message to ACP session update(s).
|
||||
* A single Cline message may produce multiple ACP updates.
|
||||
*/
|
||||
export interface TranslatedMessage {
|
||||
/** The session updates to send */
|
||||
updates: acp.SessionUpdate[]
|
||||
/** Whether this message requires a permission request */
|
||||
requiresPermission?: boolean
|
||||
/** Permission request details if required */
|
||||
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
|
||||
/** The toolCallId that was created/used (for tracking across streaming updates) */
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Re-exported ACP Types
|
||||
// ============================================================
|
||||
|
||||
export type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AudioContent,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionConfigOption,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SessionUpdate,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallStatus,
|
||||
ToolCallUpdate,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
+20
-199
@@ -1,76 +1,13 @@
|
||||
/**
|
||||
* Custom types and extensions for ACP integration with Cline CLI.
|
||||
* Internal types for ACP integration with Cline CLI.
|
||||
*
|
||||
* This file extends the base ACP types with Cline-specific functionality.
|
||||
* This file re-exports all public types from ./public-types.ts and adds
|
||||
* internal-only Types that reference core modules (Controller, etc.).
|
||||
*
|
||||
* Library consumers should never import from this file directly — they
|
||||
* get the public types via the library entrypoint (exports.ts).
|
||||
*/
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { Controller } from "@/core/controller"
|
||||
|
||||
// ============================================================
|
||||
// Session Update Type Utilities
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Extract the sessionUpdate discriminator value from a SessionUpdate variant.
|
||||
*/
|
||||
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
|
||||
|
||||
/**
|
||||
* Extract the payload type for a given sessionUpdate discriminator value.
|
||||
* This removes the `sessionUpdate` discriminator field from the type.
|
||||
*/
|
||||
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
|
||||
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
|
||||
"sessionUpdate"
|
||||
>
|
||||
|
||||
// ============================================================
|
||||
// Permission Handler Callback Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Callback to resolve a permission request with the user's response.
|
||||
*/
|
||||
export type PermissionResolver = (response: acp.RequestPermissionResponse) => void
|
||||
|
||||
/**
|
||||
* Handler function for permission requests.
|
||||
* Called when the agent needs permission for a tool call.
|
||||
* The handler should present the request to the user and call resolve() with their response.
|
||||
*/
|
||||
export type PermissionHandler = (request: Omit<acp.RequestPermissionRequest, "sessionId">, resolve: PermissionResolver) => void
|
||||
|
||||
// ============================================================
|
||||
// Session Event Emitter Types
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Maps ACP SessionUpdate types to their event listener signatures.
|
||||
* Uses the sessionUpdate discriminator to derive event names and payload types.
|
||||
*/
|
||||
export type ClineSessionEvents = {
|
||||
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
|
||||
} & {
|
||||
/** Error event for session-level errors (not part of ACP SessionUpdate) */
|
||||
error: (error: Error) => void
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ClineAgent Options (decoupled from connection)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Options for creating a ClineAgent instance (decoupled from connection).
|
||||
*/
|
||||
export interface ClineAgentOptions {
|
||||
/** CLI version string */
|
||||
version: string
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
// Re-export common ACP types for convenience
|
||||
export type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
@@ -114,134 +51,18 @@ export type {
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
/**
|
||||
* Cline-specific agent capabilities extending the ACP base capabilities.
|
||||
*/
|
||||
export interface ClineAgentCapabilities {
|
||||
/** Support for loading sessions from disk */
|
||||
loadSession: boolean
|
||||
/** Prompt capabilities for the agent */
|
||||
promptCapabilities: {
|
||||
/** Support for image inputs */
|
||||
image: boolean
|
||||
/** Support for audio inputs */
|
||||
audio: boolean
|
||||
/** Support for embedded context (file resources) */
|
||||
embeddedContext: boolean
|
||||
}
|
||||
/** MCP server passthrough capabilities */
|
||||
mcpCapabilities: {
|
||||
/** Support for HTTP MCP servers */
|
||||
http: boolean
|
||||
/** Support for SSE MCP servers */
|
||||
sse: boolean
|
||||
}
|
||||
}
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
ClineAgentCapabilities,
|
||||
ClineAgentInfo,
|
||||
ClineAgentOptions,
|
||||
ClinePermissionOption,
|
||||
ClineSessionEvents,
|
||||
PermissionHandler,
|
||||
SessionUpdatePayload,
|
||||
SessionUpdateType,
|
||||
TranslatedMessage,
|
||||
} from "./public-types.js"
|
||||
|
||||
/**
|
||||
* Cline agent info for ACP initialization response.
|
||||
*/
|
||||
export interface ClineAgentInfo {
|
||||
name: "cline"
|
||||
title: "Cline"
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended session data stored by Cline for ACP sessions.
|
||||
* Maps to Cline's task history structure.
|
||||
*/
|
||||
export interface ClineAcpSession {
|
||||
/** Unique session/task ID */
|
||||
sessionId: string
|
||||
/** Working directory for the session */
|
||||
cwd: string
|
||||
/** Current mode (plan/act) */
|
||||
mode: "plan" | "act"
|
||||
/** MCP servers passed from the client */
|
||||
mcpServers: acp.McpServer[]
|
||||
/** Timestamp when session was created */
|
||||
createdAt: number
|
||||
/** Timestamp of last activity */
|
||||
lastActivityAt: number
|
||||
/** Whether this session was loaded from history (needs resume on first prompt) */
|
||||
isLoadedFromHistory?: boolean
|
||||
/** Controller instance for this session (manages task execution) */
|
||||
controller?: Controller
|
||||
/** Model ID override for plan mode (format: "provider/modelId") */
|
||||
planModeModelId?: string
|
||||
/** Model ID override for act mode (format: "provider/modelId") */
|
||||
actModeModelId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission option as presented to the ACP client.
|
||||
*/
|
||||
export interface ClinePermissionOption {
|
||||
kind: acp.PermissionOptionKind
|
||||
name: string
|
||||
optionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of Cline message types to their ACP session update equivalents.
|
||||
*/
|
||||
export type ClineToAcpUpdateMapping = {
|
||||
/** Text messages from the agent */
|
||||
text: "agent_message_chunk"
|
||||
/** Reasoning/thinking from the agent */
|
||||
reasoning: "agent_thought_chunk"
|
||||
/** Markdown content from the agent */
|
||||
markdown: "agent_message_chunk"
|
||||
/** Tool execution */
|
||||
tool: "tool_call"
|
||||
/** Command execution */
|
||||
command: "tool_call"
|
||||
/** Command output */
|
||||
command_output: "tool_call_update"
|
||||
/** Task completion */
|
||||
completion_result: "end_turn"
|
||||
/** Error messages */
|
||||
error: "tool_call_update" | "error"
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an ACP agent instance.
|
||||
*/
|
||||
export interface AcpAgentOptions {
|
||||
/** CLI version string */
|
||||
version: string
|
||||
/** Whether debug logging is enabled */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of translating a Cline message to ACP session update(s).
|
||||
* A single Cline message may produce multiple ACP updates.
|
||||
*/
|
||||
export interface TranslatedMessage {
|
||||
/** The session updates to send */
|
||||
updates: acp.SessionUpdate[]
|
||||
/** Whether this message requires a permission request */
|
||||
requiresPermission?: boolean
|
||||
/** Permission request details if required */
|
||||
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
|
||||
/** The toolCallId that was created/used (for tracking across streaming updates) */
|
||||
toolCallId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* State tracking for an active ACP session within Cline.
|
||||
*/
|
||||
export interface AcpSessionState {
|
||||
/** Session ID */
|
||||
sessionId: string
|
||||
/** Whether the session is currently processing a prompt */
|
||||
isProcessing: boolean
|
||||
/** Current tool call ID being executed (if any) */
|
||||
currentToolCallId?: string
|
||||
/** Whether the session has been cancelled */
|
||||
cancelled: boolean
|
||||
/** Accumulated tool calls for permission batching */
|
||||
pendingToolCalls: Map<string, acp.ToolCall>
|
||||
}
|
||||
export { AcpSessionStatus } from "./public-types.js"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Box, Text, useInput } from "ink"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
|
||||
interface ApiKeyInputProps {
|
||||
providerName: string
|
||||
@@ -39,7 +39,7 @@ export const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
onSubmit(value)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Text } from "ink"
|
||||
import { render } from "ink-testing-library"
|
||||
import React from "react"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { App } from "./App"
|
||||
|
||||
const CLEAR_SEQUENCE = "\x1b[2J\x1b[3J\x1b[H"
|
||||
|
||||
function setTerminalSize(columns: number, rows: number) {
|
||||
Object.defineProperty(process.stdout, "columns", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: columns,
|
||||
})
|
||||
|
||||
Object.defineProperty(process.stdout, "rows", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rows,
|
||||
})
|
||||
}
|
||||
|
||||
function hasClearSequenceCall(calls: unknown[][]): boolean {
|
||||
return calls.some((call) => call[0] === CLEAR_SEQUENCE)
|
||||
}
|
||||
|
||||
vi.mock("./ChatView", () => ({
|
||||
ChatView: ({ controller, initialPrompt, initialImages }: any) => {
|
||||
React.useEffect(() => {
|
||||
if (initialPrompt || (initialImages && initialImages.length > 0)) {
|
||||
controller?.initTask(initialPrompt || "", initialImages)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return React.createElement(Text, null, "ChatView")
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("./TaskJsonView", () => ({
|
||||
TaskJsonView: () => React.createElement(Text, null, "TaskJsonView"),
|
||||
}))
|
||||
|
||||
vi.mock("./HistoryView", () => ({
|
||||
HistoryView: () => React.createElement(Text, null, "HistoryView"),
|
||||
}))
|
||||
|
||||
vi.mock("./ConfigView", () => ({
|
||||
ConfigView: () => React.createElement(Text, null, "ConfigView"),
|
||||
}))
|
||||
|
||||
vi.mock("./AuthView", () => ({
|
||||
AuthView: () => React.createElement(Text, null, "AuthView"),
|
||||
}))
|
||||
|
||||
vi.mock("../context/TaskContext", () => ({
|
||||
TaskContextProvider: ({ children }: any) => children,
|
||||
}))
|
||||
|
||||
vi.mock("../context/StdinContext", () => ({
|
||||
StdinProvider: ({ children }: any) => children,
|
||||
}))
|
||||
|
||||
describe("App startup prompt resize behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
delete (process.stdout as any).columns
|
||||
delete (process.stdout as any).rows
|
||||
})
|
||||
|
||||
it("does not replay initialPrompt after a width resize", async () => {
|
||||
const initTask = vi.fn()
|
||||
setTerminalSize(120, 40)
|
||||
|
||||
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
|
||||
const callback = args.find((arg) => typeof arg === "function")
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
return true
|
||||
}) as any)
|
||||
|
||||
const { unmount } = render(
|
||||
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(initTask).toHaveBeenCalledTimes(1)
|
||||
writeSpy.mockClear()
|
||||
|
||||
setTerminalSize(121, 40)
|
||||
process.stdout.emit("resize")
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(initTask).toHaveBeenCalledTimes(1)
|
||||
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(true)
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it("does not remount on height-only resize", async () => {
|
||||
const initTask = vi.fn()
|
||||
setTerminalSize(120, 40)
|
||||
|
||||
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
|
||||
const callback = args.find((arg) => typeof arg === "function")
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
return true
|
||||
}) as any)
|
||||
|
||||
const { unmount } = render(
|
||||
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(initTask).toHaveBeenCalledTimes(1)
|
||||
writeSpy.mockClear()
|
||||
|
||||
setTerminalSize(120, 45)
|
||||
process.stdout.emit("resize")
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(initTask).toHaveBeenCalledTimes(1)
|
||||
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(false)
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@@ -3,14 +3,15 @@
|
||||
* Routes between different views (task, history, config)
|
||||
*/
|
||||
|
||||
import { Box } from "ink"
|
||||
import React, { ReactNode, useCallback, useState } from "react"
|
||||
import { Box, useApp } from "ink"
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import { StdinProvider } from "../context/StdinContext"
|
||||
import { TaskContextProvider } from "../context/TaskContext"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { AuthView } from "./AuthView"
|
||||
import { ChatView } from "./ChatView"
|
||||
import { ConfigView } from "./ConfigView"
|
||||
import { ErrorBoundary } from "./ErrorBoundary"
|
||||
import { HistoryView } from "./HistoryView"
|
||||
import { TaskJsonView } from "./TaskJsonView"
|
||||
|
||||
@@ -90,7 +91,17 @@ interface AppProps {
|
||||
isRawModeSupported?: boolean
|
||||
}
|
||||
|
||||
export const App: React.FC<AppProps> = ({
|
||||
export const App: React.FC<AppProps> = (props) => {
|
||||
const { exit } = useApp()
|
||||
|
||||
return (
|
||||
<ErrorBoundary exit={exit}>
|
||||
<InternalApp {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const InternalApp: React.FC<AppProps> = ({
|
||||
view: initialView,
|
||||
taskId,
|
||||
verbose = false,
|
||||
@@ -135,6 +146,17 @@ export const App: React.FC<AppProps> = ({
|
||||
const { resizeKey } = useTerminalSize()
|
||||
const [currentView, setCurrentView] = useState<ViewType>(initialView)
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>(taskId)
|
||||
const [pendingInitialPrompt, setPendingInitialPrompt] = useState<string | undefined>(initialPrompt)
|
||||
const [pendingInitialImages, setPendingInitialImages] = useState<string[] | undefined>(initialImages)
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingInitialPrompt && (!pendingInitialImages || pendingInitialImages.length === 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
setPendingInitialPrompt(undefined)
|
||||
setPendingInitialImages(undefined)
|
||||
}, [pendingInitialPrompt, pendingInitialImages])
|
||||
|
||||
const handleSelectTask = useCallback((taskId: string) => {
|
||||
setSelectedTaskId(taskId)
|
||||
@@ -242,8 +264,8 @@ export const App: React.FC<AppProps> = ({
|
||||
) : (
|
||||
<ChatView
|
||||
controller={controller}
|
||||
initialImages={initialImages}
|
||||
initialPrompt={initialPrompt}
|
||||
initialImages={pendingInitialImages}
|
||||
initialPrompt={pendingInitialPrompt}
|
||||
onComplete={onComplete}
|
||||
onError={onError}
|
||||
onExit={onWelcomeExit}
|
||||
|
||||
@@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useTaskController } from "../context/TaskContext"
|
||||
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
|
||||
interface AskPromptProps {
|
||||
@@ -136,7 +136,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
} else if (promptType === "options") {
|
||||
// Number selection for options, or free text input
|
||||
const parts = jsonParseSafe(text, { options: [] as string[] })
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
// Submit free text on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -145,7 +145,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
setTextInput((prev) => prev.slice(0, -1))
|
||||
} else if (input && !key.ctrl && !key.meta) {
|
||||
// Check if it's a number for option selection (only when no text typed yet)
|
||||
const num = parseInt(input, 10)
|
||||
const num = Number.parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
|
||||
const selectedOption = parts.options[num - 1]
|
||||
sendResponse("messageResponse", selectedOption)
|
||||
@@ -156,7 +156,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
}
|
||||
} else if (promptType === "text") {
|
||||
// Text input mode
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
// Submit on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -169,7 +169,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
}
|
||||
} else if (promptType === "plan_mode_text") {
|
||||
// Plan mode text input - allows text response or toggle to Act mode
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
// Submit on Enter
|
||||
if (textInput.trim()) {
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -185,7 +185,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
}
|
||||
} else if (promptType === "completion") {
|
||||
// Task completed - allow follow-up question or exit
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
if (textInput.trim()) {
|
||||
// Send follow-up question
|
||||
sendResponse("messageResponse", textInput.trim())
|
||||
@@ -401,43 +401,42 @@ function getCliMessagePrefixIcon(message: ClineMessage): string {
|
||||
default:
|
||||
return "❔"
|
||||
}
|
||||
} else {
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️"
|
||||
case "generate_explanation":
|
||||
return "📝"
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️"
|
||||
case "generate_explanation":
|
||||
return "📝"
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ import { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { useScrollableList } from "../hooks/useScrollableList"
|
||||
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
import { useValidProviders } from "../utils/providers"
|
||||
import { ApiKeyInput } from "./ApiKeyInput"
|
||||
@@ -78,12 +79,12 @@ const Select: React.FC<{
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
useInput(
|
||||
(_, key) => {
|
||||
(input, key) => {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
onSelect(items[selectedIndex].value)
|
||||
}
|
||||
},
|
||||
@@ -129,7 +130,7 @@ const TextInput: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
onSubmit(value)
|
||||
} else if (key.backspace || key.delete) {
|
||||
onChange(value.slice(0, -1))
|
||||
@@ -172,6 +173,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [providerSearch, setProviderSearch] = useState("")
|
||||
const [providerIndex, setProviderIndex] = useState(0)
|
||||
const [clineModelIndex, setClineModelIndex] = useState(0)
|
||||
const featuredModels = useClineFeaturedModels()
|
||||
const [importSources, setImportSources] = useState<DetectedSources>({ codex: false, opencode: false })
|
||||
const [importSource, setImportSource] = useState<ImportSource | null>(null)
|
||||
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
|
||||
@@ -767,7 +769,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
<Box flexDirection="column">
|
||||
<Text color="white">Choose a model</Text>
|
||||
<Text> </Text>
|
||||
<FeaturedModelPicker selectedIndex={clineModelIndex} />
|
||||
<FeaturedModelPicker featuredModels={featuredModels} selectedIndex={clineModelIndex} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -851,7 +853,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setMenuIndex((prev) => (prev > 0 ? prev - 1 : mainMenuItems.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setMenuIndex((prev) => (prev < mainMenuItems.length - 1 ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
handleMainMenuSelect(mainMenuItems[menuIndex].value)
|
||||
}
|
||||
} else if (step === "provider") {
|
||||
@@ -859,7 +861,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setProviderIndex((prev) => (prev > 0 ? prev - 1 : providerItems.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setProviderIndex((prev) => (prev < providerItems.length - 1 ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
if (providerItems[providerIndex]) {
|
||||
handleProviderSelect(providerItems[providerIndex].value)
|
||||
}
|
||||
@@ -869,17 +871,17 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setProviderSearch((prev) => prev + input)
|
||||
}
|
||||
} else if (step === "cline_model") {
|
||||
const maxIndex = getFeaturedModelMaxIndex()
|
||||
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
|
||||
|
||||
if (key.upArrow) {
|
||||
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
|
||||
} else if (key.downArrow) {
|
||||
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
if (isBrowseAllSelected(clineModelIndex)) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
|
||||
setStep("modelid")
|
||||
} else {
|
||||
const selectedModel = getFeaturedModelAtIndex(clineModelIndex)
|
||||
const selectedModel = getFeaturedModelAtIndex(clineModelIndex, featuredModels)
|
||||
if (selectedModel) {
|
||||
handleClineModelSelect(selectedModel.id)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useState } from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
import { getModelList } from "./ModelPicker"
|
||||
import { SearchableList } from "./SearchableList"
|
||||
|
||||
@@ -43,7 +44,7 @@ export const BedrockCustomModelFlow: React.FC<BedrockCustomModelFlowProps> = ({
|
||||
if (step === "arn_input") {
|
||||
if (key.escape) {
|
||||
onCancel()
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
handleArnSubmit()
|
||||
} else if (key.backspace || key.delete) {
|
||||
setCustomArn((prev) => prev.slice(0, -1))
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { render } from "ink-testing-library"
|
||||
import React from "react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ChatMessage } from "./ChatMessage"
|
||||
|
||||
vi.mock("../hooks/useTerminalSize", () => ({
|
||||
useTerminalSize: () => ({
|
||||
columns: 120,
|
||||
rows: 40,
|
||||
resizeKey: 0,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("ChatMessage markdown rendering", () => {
|
||||
it("renders basic markdown elements correctly with appropriate styling", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: "# Heading 1\n\nThis is a **bold** and *italic* text with `inline code`.\n\n- List item 1\n- List item 2\n\n> Blockquote\n\n```javascript\nconst x = 1;\n```",
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
// Check for heading (bold)
|
||||
// \x1B[1m is the ANSI escape code for bold
|
||||
expect(frame).toMatch(/\x1B\[1mHeading 1\x1B\[22m/)
|
||||
|
||||
// Check for bold text
|
||||
expect(frame).toMatch(/\x1B\[1mbold\x1B\[22m/)
|
||||
|
||||
// Check for italic text
|
||||
// \x1B[3m is the ANSI escape code for italic
|
||||
expect(frame).toMatch(/\x1B\[3mitalic\x1B\[23m/)
|
||||
|
||||
// Check for inline code (no special styling in the current implementation, just text)
|
||||
expect(frame).toContain("inline code")
|
||||
|
||||
// Check for list items (gray bullet)
|
||||
// \x1B[90m is the ANSI escape code for gray
|
||||
expect(frame).toMatch(/\x1B\[90m• \x1B\[39mList item 1/)
|
||||
expect(frame).toMatch(/\x1B\[90m• \x1B\[39mList item 2/)
|
||||
|
||||
// Check for blockquote (gray pipe)
|
||||
expect(frame).toMatch(/\x1B\[90m│ \x1B\[39mBlockquote/)
|
||||
|
||||
// Check for code block (cyan text)
|
||||
// \x1B[36m is the ANSI escape code for cyan
|
||||
expect(frame).toMatch(/\x1B\[36mconst x = 1;\x1B\[39m/)
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
|
||||
import type { ClineAskUseMcpServer, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { Box, Text } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import { lexer, type Token, type Tokens } from "marked"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
@@ -20,13 +21,10 @@ import { DiffView } from "./DiffView"
|
||||
import { SubagentMessage } from "./SubagentMessage"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions.
|
||||
* Add "(Tab)" hint after "Act mode" mentions in plain text.
|
||||
* Case-insensitive, avoids double-adding if already present.
|
||||
* Matches just "Act mode" without requiring "to " prefix because markdown
|
||||
* processing may split "toggle to **Act mode**" into separate text chunks.
|
||||
*/
|
||||
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
const matches = text.match(actModeRegex)
|
||||
@@ -37,9 +35,7 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
parts.forEach((part, i) => {
|
||||
if (part) {
|
||||
nodes.push(part)
|
||||
}
|
||||
if (part) nodes.push(part)
|
||||
if (matches[i]) {
|
||||
nodes.push(
|
||||
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
|
||||
@@ -49,72 +45,146 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inline markdown: **bold**, *italic*, `code`
|
||||
* Also adds "(Tab)" hints after "Act mode" mentions.
|
||||
* Returns array of React nodes with appropriate styling
|
||||
* Render an array of marked tokens as Ink React nodes.
|
||||
* This is the entry point for recursive rendering — each token may
|
||||
* contain child tokens (e.g. a paragraph contains inline tokens,
|
||||
* a list contains items, etc.).
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
let hintCallIndex = 0
|
||||
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
|
||||
// Match **bold**, *italic*, or `code` - order matters (** before *)
|
||||
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
|
||||
let lastIndex = 0
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
// Add text before match (with Act Mode hint processing)
|
||||
if (match.index > lastIndex) {
|
||||
const beforeText = text.slice(lastIndex, match.index)
|
||||
nodes.push(...addHintedText(beforeText))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
const key = `md-${match.index}`
|
||||
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold - also process for Act Mode hints inside bold text
|
||||
const boldContent = fullMatch.slice(2, -2)
|
||||
const hintedContent = addHintedText(boldContent)
|
||||
nodes.push(
|
||||
<Text bold key={key}>
|
||||
{hintedContent}
|
||||
</Text>,
|
||||
)
|
||||
} else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
|
||||
// Italic
|
||||
nodes.push(
|
||||
<Text italic key={key}>
|
||||
{fullMatch.slice(1, -1)}
|
||||
</Text>,
|
||||
)
|
||||
} else if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
|
||||
// Inline code
|
||||
nodes.push(<Text key={key}>{fullMatch.slice(1, -1)}</Text>)
|
||||
}
|
||||
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
// Add remaining text (with Act Mode hint processing)
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(...addHintedText(text.slice(lastIndex)))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : addHintedText(text)
|
||||
function renderTokens(tokens: Token[], color?: string): React.ReactNode[] {
|
||||
return tokens.map((token, i) => renderToken(token, i, color))
|
||||
}
|
||||
|
||||
/**
|
||||
* Render text with inline markdown support
|
||||
* Render a single marked token (block or inline) as an Ink React node.
|
||||
* Handles both block-level tokens (heading, paragraph, list, code, etc.)
|
||||
* and inline tokens (strong, em, codespan, link, text).
|
||||
*/
|
||||
function renderToken(token: Token, key: number, color?: string): React.ReactNode {
|
||||
switch (token.type) {
|
||||
// --- Block tokens ---
|
||||
|
||||
case "heading": {
|
||||
const { depth, tokens } = token as Tokens.Heading
|
||||
return (
|
||||
<Box key={key} marginY={depth === 1 ? 1 : 0}>
|
||||
<Text bold color={color}>
|
||||
{renderTokens(tokens, color)}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case "paragraph":
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{renderTokens((token as Tokens.Paragraph).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<Box flexDirection="column" key={key} marginY={1}>
|
||||
{(token as Tokens.Code).text.split("\n").map((line, i) => (
|
||||
<Text color="cyan" key={i}>
|
||||
{line || " "}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "list": {
|
||||
const { ordered, start, items } = token as Tokens.List
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
{items.map((item, i) => (
|
||||
<Box flexDirection="row" key={i}>
|
||||
<Text color="gray">{ordered ? `${Number(start ?? 1) + i}. ` : "• "}</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{renderTokens(item.tokens, color)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case "blockquote":
|
||||
return (
|
||||
<Box flexDirection="row" key={key}>
|
||||
<Text color="gray">│ </Text>
|
||||
<Box flexDirection="column">{renderTokens((token as Tokens.Blockquote).tokens, color)}</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "space":
|
||||
return <Text key={key}> </Text>
|
||||
|
||||
// --- Inline tokens ---
|
||||
|
||||
case "strong":
|
||||
return (
|
||||
<Text bold color={color} key={key}>
|
||||
{renderTokens((token as Tokens.Strong).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "em":
|
||||
return (
|
||||
<Text color={color} italic key={key}>
|
||||
{renderTokens((token as Tokens.Em).tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
case "codespan":
|
||||
return <Text key={key}>{(token as Tokens.Codespan).text}</Text>
|
||||
|
||||
case "link": {
|
||||
const { text, href } = token as Tokens.Link
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{text && text !== href ? `${text} (${href})` : href}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
case "text": {
|
||||
const { text, tokens } = token as Tokens.Text
|
||||
if (tokens?.length) {
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{renderTokens(tokens, color)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text color={color} key={key}>
|
||||
{addActModeHint(text, `${key}`)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback for any unhandled token type
|
||||
default:
|
||||
return "raw" in token ? (
|
||||
<Text color={color} key={key}>
|
||||
{(token as { raw: string }).raw}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a markdown string as Ink components.
|
||||
* Uses marked's lexer to parse markdown into tokens, then renders
|
||||
* each token to the appropriate Ink component.
|
||||
*/
|
||||
const MarkdownText: React.FC<{ children: string; color?: string }> = ({ children, color }) => {
|
||||
const nodes = renderInlineMarkdown(children)
|
||||
return <Text color={color}>{nodes}</Text>
|
||||
const tokens = lexer(children)
|
||||
return <Box flexDirection="column">{renderTokens(tokens, color)}</Box>
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -774,22 +844,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
)
|
||||
}
|
||||
|
||||
// Report bug request
|
||||
if (type === "ask" && ask === "report_bug" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
Cline wants to create a Github issue:
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Text color="gray">{text}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Skip other message types
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ChatView } from "./ChatView"
|
||||
|
||||
// Helper to wait for async state updates
|
||||
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// Type for our exit mock function
|
||||
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
|
||||
@@ -126,12 +126,16 @@ vi.mock("../utils/file-search", () => ({
|
||||
searchWorkspaceFiles: vi.fn(async () => []),
|
||||
}))
|
||||
|
||||
vi.mock("../utils/slash-commands", () => ({
|
||||
extractSlashQuery: vi.fn(() => ({ inSlashMode: false, query: "", slashIndex: -1 })),
|
||||
filterCommands: vi.fn(() => []),
|
||||
insertSlashCommand: vi.fn((text: string) => text),
|
||||
sortCommandsWorkflowsFirst: vi.fn((cmds: unknown[]) => cmds),
|
||||
}))
|
||||
vi.mock("../utils/slash-commands", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../utils/slash-commands")>()
|
||||
return {
|
||||
...actual,
|
||||
extractSlashQuery: vi.fn(() => ({ inSlashMode: false, query: "", slashIndex: -1 })),
|
||||
filterCommands: vi.fn(() => []),
|
||||
insertSlashCommand: vi.fn((text: string) => text),
|
||||
sortCommandsWorkflowsFirst: vi.fn((cmds: unknown[]) => cmds),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("../utils/input", () => ({
|
||||
isMouseEscapeSequence: vi.fn(() => false),
|
||||
|
||||
+100
-71
@@ -108,7 +108,6 @@ import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { execSync } from "child_process"
|
||||
@@ -137,7 +136,14 @@ import {
|
||||
} from "../utils/file-search"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
|
||||
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
|
||||
import {
|
||||
createCliOnlySlashCommands,
|
||||
extractSlashQuery,
|
||||
filterCommands,
|
||||
getStandaloneSlashCommandToExecute,
|
||||
insertSlashCommand,
|
||||
sortCommandsWorkflowsFirst,
|
||||
} from "../utils/slash-commands"
|
||||
import { waitFor } from "../utils/timeout"
|
||||
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { shutdownEvent } from "../vscode-shim"
|
||||
@@ -403,7 +409,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const PASTE_UPDATE_DEBOUNCE_MS = 50 // Debounce visual updates to avoid flicker
|
||||
|
||||
// Slash command state
|
||||
const [availableCommands, setAvailableCommands] = useState<SlashCommandInfo[]>([])
|
||||
const [availableCommands, setAvailableCommands] = useState<SlashCommandInfo[]>(() => createCliOnlySlashCommands())
|
||||
const [selectedSlashIndex, setSelectedSlashIndex] = useState(0)
|
||||
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false)
|
||||
const lastSlashIndexRef = useRef<number>(-1)
|
||||
@@ -614,16 +620,15 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
try {
|
||||
const response = await getAvailableSlashCommands(ctrl, EmptyRequest.create())
|
||||
const cliCommands = response.commands.filter((cmd) => cmd.cliCompatible !== false)
|
||||
// Add CLI-only commands (like /settings) that are handled locally
|
||||
const cliOnlyCommands: SlashCommandInfo[] = CLI_ONLY_COMMANDS.map((cmd) => ({
|
||||
name: cmd.name,
|
||||
description: cmd.description || "",
|
||||
section: cmd.section || "default",
|
||||
cliCompatible: true,
|
||||
}))
|
||||
// Add CLI-only commands (like /settings) that are handled locally.
|
||||
// Seed these synchronously on first render so locally handled commands like
|
||||
// /q and /exit are immediately available, even before the async command
|
||||
// fetch completes. This avoids a race that can make the quit command tests
|
||||
// flaky on slower Windows CI runners.
|
||||
const cliOnlyCommands = createCliOnlySlashCommands()
|
||||
setAvailableCommands([...cliOnlyCommands, ...sortCommandsWorkflowsFirst(cliCommands)])
|
||||
} catch {
|
||||
// Fallback: commands will be empty, menu won't show
|
||||
// Keep CLI-only commands available even if backend command loading fails.
|
||||
}
|
||||
}
|
||||
loadCommands()
|
||||
@@ -843,6 +848,77 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}, 150)
|
||||
}, [inkExit, onExit])
|
||||
|
||||
const handleCliOnlySlashCommand = useCallback(
|
||||
(commandName: string): boolean => {
|
||||
if (commandName === "help") {
|
||||
setActivePanel({ type: "help" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "settings") {
|
||||
setActivePanel({ type: "settings" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "models") {
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
const provider =
|
||||
mode === "act"
|
||||
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
|
||||
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
|
||||
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
|
||||
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
|
||||
setActivePanel({ type: "settings", initialMode, initialModelKey })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "history") {
|
||||
setActivePanel({ type: "history" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "skills") {
|
||||
setActivePanel({ type: "skills" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "clear") {
|
||||
void clearViewAndResetTask()
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (commandName === "exit" || commandName === "q") {
|
||||
handleExit()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[clearViewAndResetTask, handleExit, mode, setCursorPos, setTextInput],
|
||||
)
|
||||
|
||||
// Get button config based on the last message state
|
||||
const buttonConfig = useMemo(() => {
|
||||
const lastMsg = messages[messages.length - 1] as ClineMessage | undefined
|
||||
@@ -1102,6 +1178,17 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
const inSlashMenu = slashInfo.inSlashMode && filteredCommands.length > 0 && !slashMenuDismissed
|
||||
const inFileMenu = mentionInfo.inMentionMode && fileResults.length > 0 && !inSlashMenu
|
||||
const standaloneSlashCommand = getStandaloneSlashCommandToExecute({
|
||||
prompt,
|
||||
inSlashMode: slashInfo.inSlashMode,
|
||||
hasSlashMenu: inSlashMenu,
|
||||
hasPendingAsk: !!pendingAsk,
|
||||
isSpinnerActive,
|
||||
})
|
||||
|
||||
if (key.return && standaloneSlashCommand && handleCliOnlySlashCommand(standaloneSlashCommand)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Slash command menu navigation (takes priority over file menu)
|
||||
if (inSlashMenu) {
|
||||
@@ -1116,64 +1203,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (key.tab || key.return) {
|
||||
const cmd = filteredCommands[selectedSlashIndex]
|
||||
if (cmd) {
|
||||
// Handle CLI-only commands locally
|
||||
if (cmd.name === "help") {
|
||||
setActivePanel({ type: "help" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "settings") {
|
||||
setActivePanel({ type: "settings" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "models") {
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
// Use current mode's provider to determine picker type
|
||||
const provider =
|
||||
mode === "act"
|
||||
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
|
||||
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
|
||||
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
|
||||
// Set model for current mode (plan or act)
|
||||
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
|
||||
setActivePanel({ type: "settings", initialMode, initialModelKey })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "history") {
|
||||
setActivePanel({ type: "history" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "skills") {
|
||||
setActivePanel({ type: "skills" })
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "clear") {
|
||||
clearViewAndResetTask()
|
||||
setSelectedSlashIndex(0)
|
||||
setSlashMenuDismissed(true)
|
||||
return
|
||||
}
|
||||
if (cmd.name === "exit") {
|
||||
handleExit()
|
||||
if (handleCliOnlySlashCommand(cmd.name)) {
|
||||
return
|
||||
}
|
||||
const newText = insertSlashCommand(textInput, slashInfo.slashIndex, cmd.name)
|
||||
@@ -1462,13 +1492,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (item.type === "header") {
|
||||
// Show static robot frame in header (first frame, looking straight ahead)
|
||||
return (
|
||||
<Box flexDirection="column" key="header">
|
||||
<Box flexDirection="column" key="header" marginBottom={1}>
|
||||
<StaticRobotFrame />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useState } from "react"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
|
||||
export type RestoreType = "task" | "workspace" | "taskAndWorkspace"
|
||||
|
||||
@@ -101,7 +102,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
|
||||
setSelectedCheckpoint((i) => Math.max(0, i - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedCheckpoint((i) => Math.min(checkpoints.length - 1, i + 1))
|
||||
} else if (key.return && checkpoints.length > 0) {
|
||||
} else if (isEnterKey(input, key) && checkpoints.length > 0) {
|
||||
setStage("restoreType")
|
||||
}
|
||||
} else if (stage === "restoreType") {
|
||||
@@ -109,7 +110,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
|
||||
setSelectedRestoreType((i) => Math.max(0, i - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedRestoreType((i) => Math.min(RESTORE_TYPE_OPTIONS.length - 1, i + 1))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
const checkpoint = checkpoints[selectedCheckpoint]
|
||||
const restoreType = RESTORE_TYPE_OPTIONS[selectedRestoreType]
|
||||
if (checkpoint && restoreType) {
|
||||
@@ -120,7 +121,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
|
||||
|
||||
// Quick number selection for checkpoints
|
||||
if (stage === "checkpoint") {
|
||||
const num = parseInt(input, 10)
|
||||
const num = Number.parseInt(input, 10)
|
||||
if (!Number.isNaN(num) && num >= 1 && num <= checkpoints.length) {
|
||||
setSelectedCheckpoint(num - 1)
|
||||
setStage("restoreType")
|
||||
|
||||
@@ -84,6 +84,16 @@ describe("ConfigView", () => {
|
||||
)
|
||||
expect(lastFrame()).toContain("Global Settings")
|
||||
})
|
||||
|
||||
it("hides Hooks tab when hooks are disabled", () => {
|
||||
const { lastFrame } = render(<ConfigView {...defaultProps} hooksEnabled={false} skillsEnabled={true} />)
|
||||
expect(lastFrame()).not.toContain("Hooks")
|
||||
})
|
||||
|
||||
it("shows Hooks tab when hooks are enabled", () => {
|
||||
const { lastFrame } = render(<ConfigView {...defaultProps} hooksEnabled={true} skillsEnabled={true} />)
|
||||
expect(lastFrame()).toContain("Hooks")
|
||||
})
|
||||
})
|
||||
|
||||
describe("value formatting", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Interactive config view component for displaying and editing configuration values
|
||||
* Supports tabs for Settings, Rules, Workflows, Hooks, and Skills
|
||||
* Supports tabs for Settings, Rules, legacy Workflows, Hooks, and Skills
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -515,7 +515,10 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
if (workflowEntries.length === 0) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color="gray">No workflows configured. Add workflow files to enable this feature.</Text>
|
||||
<Text color="gray">
|
||||
No legacy workflows configured. Add workflow files only if you still need `/file.md`
|
||||
compatibility.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -531,7 +534,11 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
<React.Fragment key={`${entry.source}-${entry.path}`}>
|
||||
{showHeader && (
|
||||
<SectionHeader
|
||||
title={entry.source === "global" ? "Global Workflows:" : "Workspace Workflows:"}
|
||||
title={
|
||||
entry.source === "global"
|
||||
? "Global Legacy Workflows:"
|
||||
: "Workspace Legacy Workflows:"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ToggleRow entry={entry} isSelected={actualIndex === selectedIndex} />
|
||||
|
||||
@@ -56,7 +56,13 @@ export interface ObjectEditorState {
|
||||
editValue: string
|
||||
}
|
||||
|
||||
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
|
||||
export const EXCLUDED_KEYS = new Set([
|
||||
"taskHistory",
|
||||
"primaryRootIndex",
|
||||
"welcomeViewCompleted",
|
||||
"isNewUser",
|
||||
"cliKanbanMigrationAnnouncementShown",
|
||||
])
|
||||
|
||||
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean", "object"])
|
||||
export const MAX_VISIBLE = 12
|
||||
@@ -65,7 +71,7 @@ export const SEPARATOR = "─".repeat(80)
|
||||
export const TABS: { key: TabView; label: string; requiresFlag?: "hooks" | "skills" }[] = [
|
||||
{ key: "settings", label: "Settings" },
|
||||
{ key: "rules", label: "Rules" },
|
||||
{ key: "workflows", label: "Workflows" },
|
||||
{ key: "workflows", label: "Legacy Workflows" },
|
||||
{ key: "hooks", label: "Hooks", requiresFlag: "hooks" },
|
||||
{ key: "skills", label: "Skills", requiresFlag: "skills" },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Box, Text } from "ink"
|
||||
import React from "react"
|
||||
import { ErrorService } from "@/services/error"
|
||||
import { StaticRobotFrame } from "./AsciiMotionCli"
|
||||
|
||||
type Props = React.PropsWithChildren<{ exit: (error?: Error) => void }>
|
||||
|
||||
async function onReactError(props: Props, error: Error, errorInfo: React.ErrorInfo) {
|
||||
try {
|
||||
await ErrorService.get().captureException(error, { context: "ErrorBoundary", errorInfo })
|
||||
await ErrorService.get().dispose()
|
||||
} catch {
|
||||
// Ignore errors
|
||||
} finally {
|
||||
props.exit(error)
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<Props, { hasError: boolean }> {
|
||||
override state = { hasError: false }
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
onReactError(this.props, error, errorInfo)
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Box flexDirection="column" height="100%" key="header" width="100%">
|
||||
<StaticRobotFrame />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
Something went wrong. We're sorry.
|
||||
</Text>
|
||||
<Text color="white">Please check the logs for more details.</Text>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Rotating feature tips shown during thinking/acting phases.
|
||||
* Appears after a brief delay and cycles through tips to educate users
|
||||
* about Cline features while they wait.
|
||||
*/
|
||||
|
||||
import { Box, Text } from "ink"
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
interface FeatureTipItem {
|
||||
text: string
|
||||
}
|
||||
|
||||
const FEATURE_TIPS: FeatureTipItem[] = [
|
||||
{
|
||||
text: 'Enable "Double-Check Completion" in settings to have Cline verify its work before finishing a task.',
|
||||
},
|
||||
{
|
||||
text: "Add a .clinerules file to your project root to give Cline project-specific instructions.",
|
||||
},
|
||||
{
|
||||
text: "Press Tab to switch between Plan and Act mode — plan an approach before Cline takes action.",
|
||||
},
|
||||
{
|
||||
text: "Use @ in the chat input to add files, folders, or URLs as context for your task.",
|
||||
},
|
||||
{
|
||||
text: "Set up MCP Servers to give Cline access to external tools and APIs.",
|
||||
},
|
||||
{
|
||||
text: "Cline creates checkpoints after changes — you can always restore to a previous state.",
|
||||
},
|
||||
{
|
||||
text: "Use /compact to condense long conversations and free up context window space.",
|
||||
},
|
||||
{
|
||||
text: "Enable auto-approve for read-only tools like file reads to speed up exploration.",
|
||||
},
|
||||
{
|
||||
text: "Use /settings to configure your API provider and model without leaving the terminal.",
|
||||
},
|
||||
{
|
||||
text: "You can pass images with --images flag or paste image file paths in the chat.",
|
||||
},
|
||||
{
|
||||
text: "Cline can browse websites — ask it to test your local dev server in the browser.",
|
||||
},
|
||||
{
|
||||
text: "Use /newtask to start a fresh task while keeping the important context from your current conversation.",
|
||||
},
|
||||
{
|
||||
text: "Try 'npm i -g cline' to manage tasks on a Kankan board — orchestrate coding agents across worktrees.",
|
||||
},
|
||||
{
|
||||
text: "Use Shift+Tab to toggle auto-approve all — let Cline work uninterrupted on trusted tasks.",
|
||||
},
|
||||
{
|
||||
text: "Press Up/Down arrows in an empty input to browse your previous task prompts.",
|
||||
},
|
||||
{
|
||||
text: "Type / to see all available commands — /history, /compact, /settings, and more.",
|
||||
},
|
||||
{
|
||||
text: "Use /skills to browse and attach reusable skill files that guide Cline's behavior.",
|
||||
},
|
||||
{
|
||||
text: 'You can disable these tips in /settings → Features → "Feature tips".',
|
||||
},
|
||||
]
|
||||
|
||||
const SHOW_DELAY_MS = 2000
|
||||
const CYCLE_INTERVAL_MS = 8000
|
||||
|
||||
/**
|
||||
* Shows rotating feature tips below the thinking indicator.
|
||||
* Appears after a brief delay and cycles through tips while Cline is thinking/acting.
|
||||
*/
|
||||
export const FeatureTip: React.FC = React.memo(() => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [tipIndex, setTipIndex] = useState(Math.floor(Math.random() * FEATURE_TIPS.length))
|
||||
const cycleTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const showTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const currentTip = FEATURE_TIPS[tipIndex]
|
||||
|
||||
const advanceTip = useCallback(() => {
|
||||
setTipIndex((prev) => (prev + 1) % FEATURE_TIPS.length)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
showTimerRef.current = setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
cycleTimerRef.current = setInterval(advanceTip, CYCLE_INTERVAL_MS)
|
||||
}, SHOW_DELAY_MS)
|
||||
|
||||
return () => {
|
||||
if (showTimerRef.current) {
|
||||
clearTimeout(showTimerRef.current)
|
||||
}
|
||||
if (cycleTimerRef.current) {
|
||||
clearInterval(cycleTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [advanceTip])
|
||||
|
||||
if (!isVisible) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingLeft={1}>
|
||||
<Text color="gray">
|
||||
💡 <Text bold>Tip:</Text> {currentTip.text}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@@ -7,13 +7,14 @@
|
||||
import { Box, Text } from "ink"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { type FeaturedModel, getAllFeaturedModels } from "../constants/featured-models"
|
||||
import type { FeaturedModel } from "../constants/featured-models"
|
||||
|
||||
interface FeaturedModelPickerProps {
|
||||
selectedIndex: number
|
||||
title?: string
|
||||
showBrowseAll?: boolean
|
||||
helpText?: string
|
||||
featuredModels: FeaturedModel[]
|
||||
}
|
||||
|
||||
export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
@@ -21,8 +22,9 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
title,
|
||||
showBrowseAll = true,
|
||||
helpText = "Arrows to navigate, Enter to select",
|
||||
featuredModels,
|
||||
}) => {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
const models = featuredModels
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -35,7 +37,7 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{featuredModels.map((model, i) => {
|
||||
{models.map((model, i) => {
|
||||
const isSelected = i === selectedIndex
|
||||
|
||||
return (
|
||||
@@ -64,8 +66,8 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
|
||||
{showBrowseAll && (
|
||||
<Box>
|
||||
<Text color={selectedIndex === featuredModels.length ? COLORS.primaryBlue : "white"}>
|
||||
{selectedIndex === featuredModels.length ? "❯ " : " "}
|
||||
<Text color={selectedIndex === models.length ? COLORS.primaryBlue : "white"}>
|
||||
{selectedIndex === models.length ? "❯ " : " "}
|
||||
Browse all models...
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -81,24 +83,21 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
* Get the maximum valid index for the featured model picker
|
||||
* (includes "Browse all" option if showBrowseAll is true)
|
||||
*/
|
||||
export function getFeaturedModelMaxIndex(showBrowseAll = true): number {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function getFeaturedModelMaxIndex(featuredModels: FeaturedModel[], showBrowseAll = true): number {
|
||||
return showBrowseAll ? featuredModels.length : featuredModels.length - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the selected index is the "Browse all" option
|
||||
*/
|
||||
export function isBrowseAllSelected(selectedIndex: number): boolean {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function isBrowseAllSelected(selectedIndex: number, featuredModels: FeaturedModel[]): boolean {
|
||||
return selectedIndex === featuredModels.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the featured model at the given index, or null if "Browse all" is selected
|
||||
*/
|
||||
export function getFeaturedModelAtIndex(index: number): FeaturedModel | null {
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
export function getFeaturedModelAtIndex(index: number, featuredModels: FeaturedModel[]): FeaturedModel | null {
|
||||
if (index >= 0 && index < featuredModels.length) {
|
||||
return featuredModels[index]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Displays a progress-tracked checklist of tasks
|
||||
*/
|
||||
|
||||
import { isCompletedFocusChainItem, isFocusChainItem, parseFocusChainItem } from "@shared/focus-chain-utils"
|
||||
import { isChecklistItem, isCompletedChecklistItem, parseChecklistItem } from "@shared/checklist-utils"
|
||||
import { Box, Text } from "ink"
|
||||
import React, { useMemo } from "react"
|
||||
|
||||
@@ -41,8 +41,8 @@ function parseCurrentTodoInfo(text: string): TodoInfo | null {
|
||||
const lines = text.split("\n")
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim()
|
||||
if (isFocusChainItem(line)) {
|
||||
const isCompleted = isCompletedFocusChainItem(line)
|
||||
if (isChecklistItem(line)) {
|
||||
const isCompleted = isCompletedChecklistItem(line)
|
||||
|
||||
if (isCompleted) {
|
||||
completedCount++
|
||||
@@ -80,7 +80,7 @@ function parseTodoItems(text: string): TodoItem[] {
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim()
|
||||
const parsed = parseFocusChainItem(line)
|
||||
const parsed = parseChecklistItem(line)
|
||||
if (parsed) {
|
||||
items.push(parsed)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ const Header: React.FC<{
|
||||
const isCompleted = completedCount === totalCount
|
||||
|
||||
const displayText = isCompleted ? "All tasks completed!" : currentTodo?.text || "To-Do list"
|
||||
const truncatedText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText
|
||||
const truncatedText = displayText.length > 50 ? `${displayText.substring(0, 47)}...` : displayText
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
@@ -136,8 +136,8 @@ const ExpandedList: React.FC<{
|
||||
}> = ({ items, isCompleted }) => {
|
||||
return (
|
||||
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
||||
{items.map((item, index) => (
|
||||
<Box key={index}>
|
||||
{items.map((item) => (
|
||||
<Box key={`${item.text}-${item.checked ? "done" : "todo"}`}>
|
||||
<Text color={item.checked ? "green" : "gray"}>{item.checked ? "✓" : "○"} </Text>
|
||||
<Text color={item.checked ? "green" : undefined} dimColor={item.checked}>
|
||||
{item.text}
|
||||
|
||||
@@ -88,6 +88,10 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
|
||||
{" "}
|
||||
<Text color="white">/clear</Text> - Start a fresh task
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">/q</Text> - Quit Cline
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { Panel } from "./Panel"
|
||||
|
||||
interface TaskHistoryItem {
|
||||
@@ -142,7 +142,7 @@ export const HistoryPanelContent: React.FC<HistoryPanelContentProps> = ({ onClos
|
||||
return
|
||||
}
|
||||
|
||||
if (key.return && items[selectedIndex]) {
|
||||
if (isEnterKey(input, key) && items[selectedIndex]) {
|
||||
handleSelect(items[selectedIndex])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
import { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
|
||||
interface TaskHistoryItem {
|
||||
id: string
|
||||
@@ -40,7 +41,7 @@ interface HistoryViewProps {
|
||||
/**
|
||||
* Format separator
|
||||
*/
|
||||
function formatSeparator(char: string = "─", width: number = 80): string {
|
||||
function formatSeparator(char = "─", width = 80): string {
|
||||
return char.repeat(Math.max(width, 10))
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
|
||||
setSelectedIndex((prev) => Math.max(0, prev - 1))
|
||||
} else if (key.downArrow || input === "j") {
|
||||
setSelectedIndex((prev) => Math.min(pageItems.length - 1, prev + 1))
|
||||
} else if (key.return && pageItems[selectedIndex]) {
|
||||
} else if (isEnterKey(input, key) && pageItems[selectedIndex]) {
|
||||
onSelect(pageItems[selectedIndex])
|
||||
} else if (key.leftArrow && hasPrevPage) {
|
||||
handlePageChange(currentPage - 1)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
importFromCodex,
|
||||
importFromOpenCode,
|
||||
} from "../utils/import-configs"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
import { applyProviderConfig } from "../utils/provider-config"
|
||||
|
||||
type ImportStep = "select" | "confirm" | "saving" | "error"
|
||||
@@ -95,13 +96,13 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : keys.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex((prev) => (prev < keys.length - 1 ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
setStep("confirm")
|
||||
}
|
||||
} else if (step === "confirm") {
|
||||
if (key.upArrow || key.downArrow) {
|
||||
setConfirmIndex((prev) => (prev === 0 ? 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(input, key)) {
|
||||
if (confirmIndex === 0) {
|
||||
handleConfirm()
|
||||
} else {
|
||||
@@ -109,7 +110,7 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
|
||||
}
|
||||
}
|
||||
} else if (step === "error") {
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { render } from "ink-testing-library"
|
||||
import { createElement } from "react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { KanbanMigrationView } from "./KanbanMigrationView"
|
||||
|
||||
describe("KanbanMigrationView", () => {
|
||||
it("renders the migration options", () => {
|
||||
const onSelect = vi.fn()
|
||||
const { lastFrame } = render(createElement(KanbanMigrationView, { isRawModeSupported: true, onSelect }))
|
||||
|
||||
expect(lastFrame()).toContain("Introducing Cline Kanban!")
|
||||
expect(lastFrame()).toContain("Open the new experience")
|
||||
expect(lastFrame()).toContain("Launch Cline Kanban and start there by default.")
|
||||
expect(lastFrame()).toContain("cline --tui")
|
||||
expect(lastFrame()).toContain("You can always run cline --tui for the terminal experience.")
|
||||
expect(lastFrame()).toContain("Exit")
|
||||
})
|
||||
|
||||
it("selects the highlighted option with Enter", () => {
|
||||
const onSelect = vi.fn()
|
||||
const { stdin } = render(createElement(KanbanMigrationView, { isRawModeSupported: true, onSelect }))
|
||||
|
||||
stdin.write("\r")
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("kanban")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { StdinProvider, useStdinContext } from "../context/StdinContext"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
import { type KanbanMigrationAction } from "../utils/kanban"
|
||||
import { StaticRobotFrame } from "./AsciiMotionCli"
|
||||
import { ErrorBoundary } from "./ErrorBoundary"
|
||||
|
||||
interface KanbanMigrationViewProps {
|
||||
isRawModeSupported: boolean
|
||||
onSelect: (action: KanbanMigrationAction) => void
|
||||
}
|
||||
|
||||
interface MigrationMenuItem {
|
||||
label: string
|
||||
description: string
|
||||
value: KanbanMigrationAction
|
||||
}
|
||||
|
||||
const InternalKanbanMigrationView: React.FC<Pick<KanbanMigrationViewProps, "onSelect">> = ({ onSelect }) => {
|
||||
const { exit } = useApp()
|
||||
const { isRawModeSupported } = useStdinContext()
|
||||
const items = useMemo<MigrationMenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: "Open the new experience",
|
||||
description: "Launch Cline Kanban and start there by default.",
|
||||
value: "kanban",
|
||||
},
|
||||
{
|
||||
label: "Exit",
|
||||
description: "You can always run cline --tui for the terminal experience.",
|
||||
value: "exit",
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.escape) {
|
||||
onSelect("exit")
|
||||
exit()
|
||||
} else if (key.upArrow) {
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
|
||||
} else if (isEnterKey(input, key)) {
|
||||
onSelect(items[selectedIndex].value)
|
||||
exit()
|
||||
}
|
||||
},
|
||||
{ isActive: isRawModeSupported },
|
||||
)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
<StaticRobotFrame />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
Introducing Cline Kanban!
|
||||
</Text>
|
||||
<Text color="gray">A board for orchestrating coding agents across worktrees, right from your browser.</Text>
|
||||
<Text> </Text>
|
||||
{items.map((item, index) => {
|
||||
const isSelected = index === selectedIndex
|
||||
return (
|
||||
<Box flexDirection="column" key={item.value} marginBottom={1}>
|
||||
<Text color={isSelected ? COLORS.primaryBlue : undefined}>
|
||||
{isSelected ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text color="gray"> {item.description}</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<Text> </Text>
|
||||
<Text color="gray">Use arrow keys to navigate, Enter to select, Esc or Ctrl+C to exit</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const KanbanMigrationView: React.FC<KanbanMigrationViewProps> = ({ isRawModeSupported, onSelect }) => {
|
||||
const { exit } = useApp()
|
||||
|
||||
return (
|
||||
<ErrorBoundary exit={exit}>
|
||||
<StdinProvider isRawModeSupported={isRawModeSupported}>
|
||||
<InternalKanbanMigrationView onSelect={onSelect} />
|
||||
</StdinProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@@ -62,6 +62,8 @@ import {
|
||||
sapAiCoreModels,
|
||||
vertexDefaultModelId,
|
||||
vertexModels,
|
||||
wandbDefaultModelId,
|
||||
wandbModels,
|
||||
xaiDefaultModelId,
|
||||
xaiModels,
|
||||
} from "@/shared/api"
|
||||
@@ -101,6 +103,7 @@ export const providerModels: Record<string, { models: Record<string, unknown>; d
|
||||
sambanova: { models: sambanovaModels, defaultId: sambanovaDefaultModelId },
|
||||
sapaicore: { models: sapAiCoreModels, defaultId: sapAiCoreDefaultModelId },
|
||||
vertex: { models: vertexModels, defaultId: vertexDefaultModelId },
|
||||
wandb: { models: wandbModels, defaultId: wandbDefaultModelId },
|
||||
xai: { models: xaiModels, defaultId: xaiDefaultModelId },
|
||||
zai: { models: internationalZAiModels, defaultId: internationalZAiDefaultModelId },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { filterCommands, getStandaloneSlashCommandName, getStandaloneSlashCommandToExecute } from "../utils/slash-commands"
|
||||
|
||||
const cliOnlySlashCommands = CLI_ONLY_COMMANDS.map((cmd) => ({
|
||||
name: cmd.name,
|
||||
description: cmd.description || "",
|
||||
section: cmd.section || "default",
|
||||
cliCompatible: true,
|
||||
}))
|
||||
|
||||
describe("Quit Command (/q and /exit)", () => {
|
||||
it("prioritizes /q as the selected slash command for an exact q query", () => {
|
||||
const result = filterCommands(cliOnlySlashCommands, "q")
|
||||
|
||||
expect(result[0]?.name).toBe("q")
|
||||
})
|
||||
|
||||
it("detects /q as a standalone slash command", () => {
|
||||
expect(getStandaloneSlashCommandName("/q")).toBe("q")
|
||||
})
|
||||
|
||||
it("detects /exit as a standalone slash command", () => {
|
||||
expect(getStandaloneSlashCommandName("/exit")).toBe("exit")
|
||||
})
|
||||
|
||||
it("resolves /q to direct execution when no slash menu is active", () => {
|
||||
expect(
|
||||
getStandaloneSlashCommandToExecute({
|
||||
prompt: "/q",
|
||||
inSlashMode: true,
|
||||
hasSlashMenu: false,
|
||||
hasPendingAsk: false,
|
||||
isSpinnerActive: false,
|
||||
}),
|
||||
).toBe("q")
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import { Box, Text, useInput } from "ink"
|
||||
import React, { useState } from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isEnterKey } from "../utils/input"
|
||||
|
||||
export interface SelectListItem {
|
||||
id: string
|
||||
@@ -31,7 +32,7 @@ export function SelectList<T extends SelectListItem>({ items, onSelect, isActive
|
||||
setSelectedIndex((i) => (i > 0 ? i - 1 : items.length - 1))
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex((i) => (i < items.length - 1 ? i + 1 : 0))
|
||||
} else if (key.return) {
|
||||
} else if (isEnterKey(_input, key)) {
|
||||
const item = items[selectedIndex]
|
||||
if (item) {
|
||||
onSelect(item)
|
||||
|
||||
@@ -25,6 +25,7 @@ import { supportsReasoningEffortForModel } from "@/utils/model-utils"
|
||||
import { version as CLI_VERSION } from "../../package.json"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
@@ -128,6 +129,12 @@ const FEATURE_SETTINGS = {
|
||||
label: "Double-check completion",
|
||||
description: "Reject first completion attempt and require re-verification",
|
||||
},
|
||||
showFeatureTips: {
|
||||
stateKey: "showFeatureTips",
|
||||
default: true,
|
||||
label: "Feature tips",
|
||||
description: "Show tips during thinking phases",
|
||||
},
|
||||
} as const
|
||||
|
||||
type FeatureKey = keyof typeof FEATURE_SETTINGS
|
||||
@@ -161,6 +168,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
)
|
||||
const [isPickingFeaturedModel, setIsPickingFeaturedModel] = useState(initialMode === "featured-models")
|
||||
const [featuredModelIndex, setFeaturedModelIndex] = useState(0)
|
||||
const featuredModels = useClineFeaturedModels()
|
||||
const [isPickingProvider, setIsPickingProvider] = useState(false)
|
||||
const [isPickingLanguage, setIsPickingLanguage] = useState(false)
|
||||
const [isEnteringApiKey, setIsEnteringApiKey] = useState(false)
|
||||
@@ -1292,7 +1300,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
|
||||
// Featured model picker mode (Cline provider)
|
||||
if (isPickingFeaturedModel) {
|
||||
const maxIndex = getFeaturedModelMaxIndex()
|
||||
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
|
||||
|
||||
if (key.escape) {
|
||||
setIsPickingFeaturedModel(false)
|
||||
@@ -1306,12 +1314,12 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
} else if (key.downArrow) {
|
||||
setFeaturedModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
|
||||
} else if (key.return) {
|
||||
if (isBrowseAllSelected(featuredModelIndex)) {
|
||||
if (isBrowseAllSelected(featuredModelIndex, featuredModels)) {
|
||||
// Switch to full ModelPicker
|
||||
setIsPickingFeaturedModel(false)
|
||||
setIsPickingModel(true)
|
||||
} else {
|
||||
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex)
|
||||
const selectedModel = getFeaturedModelAtIndex(featuredModelIndex, featuredModels)
|
||||
if (selectedModel && pickingModelKey) {
|
||||
handleModelSelect(selectedModel.id)
|
||||
setIsPickingFeaturedModel(false)
|
||||
@@ -1522,6 +1530,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const label = pickingModelKey === "actModelId" ? "Model ID (Act)" : "Model ID (Plan)"
|
||||
return (
|
||||
<FeaturedModelPicker
|
||||
featuredModels={featuredModels}
|
||||
helpText="Arrows to navigate, Enter to select, Esc to cancel"
|
||||
selectedIndex={featuredModelIndex}
|
||||
title={`Select: ${label}`}
|
||||
|
||||
@@ -120,7 +120,9 @@ describe("SkillsPanelContent", () => {
|
||||
await delay()
|
||||
|
||||
// Navigate down to marketplace (past the one skill)
|
||||
stdin.write("\x1B[B") // Down arrow
|
||||
// Use vim-style navigation here because it's more deterministic in the
|
||||
// full suite than raw arrow escape sequences on Windows.
|
||||
stdin.write("j")
|
||||
await delay()
|
||||
|
||||
stdin.write("\r") // Enter
|
||||
|
||||
@@ -12,7 +12,7 @@ import { refreshSkills } from "@/core/controller/file/refreshSkills"
|
||||
import { toggleSkill } from "@/core/controller/file/toggleSkill"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
|
||||
import { Panel } from "./Panel"
|
||||
|
||||
const SKILLS_MARKETPLACE_URL = "https://skills.sh/"
|
||||
@@ -143,7 +143,7 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
|
||||
}
|
||||
|
||||
// Actions
|
||||
if (key.return) {
|
||||
if (isEnterKey(input, key)) {
|
||||
if (isMarketplaceSelected) {
|
||||
openMarketplace()
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { FeatureTip } from "./FeatureTip"
|
||||
|
||||
interface ThinkingIndicatorProps {
|
||||
mode?: "act" | "plan"
|
||||
@@ -52,6 +54,7 @@ const ShimmerText: React.FC<{ text: string; color: string; shimmerPos: number }>
|
||||
}
|
||||
|
||||
export const ThinkingIndicator: React.FC<ThinkingIndicatorProps> = ({ mode = "act", startTime, onCancel }) => {
|
||||
const showFeatureTips = StateManager.get().getGlobalSettingsKey("showFeatureTips") ?? true
|
||||
const message = mode === "plan" ? "Planning" : "Acting"
|
||||
const color = mode === "plan" ? "yellow" : COLORS.primaryBlue
|
||||
|
||||
@@ -118,9 +121,12 @@ export const ThinkingIndicator: React.FC<ThinkingIndicatorProps> = ({ mode = "ac
|
||||
}, [startTime, elapsedMs])
|
||||
|
||||
return (
|
||||
<Box paddingLeft={1}>
|
||||
<ShimmerText color={color} shimmerPos={shimmerPos} text={fullText} />
|
||||
{elapsedStr && <Text color="gray"> ({elapsedStr} · esc to interrupt)</Text>}
|
||||
<Box flexDirection="column">
|
||||
<Box paddingLeft={1}>
|
||||
<ShimmerText color={color} shimmerPos={shimmerPos} text={fullText} />
|
||||
{elapsedStr && <Text color="gray"> ({elapsedStr} · esc to interrupt)</Text>}
|
||||
</Box>
|
||||
{showFeatureTips && <FeatureTip />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
/**
|
||||
* Welcome view component
|
||||
* Shows an interactive prompt when user starts cline without a command
|
||||
* Supports file mentions with @
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey, Mode, SettingsKey } from "@/shared/storage"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import {
|
||||
checkAndWarnRipgrepMissing,
|
||||
extractMentionQuery,
|
||||
type FileSearchResult,
|
||||
getRipgrepInstallInstructions,
|
||||
insertMention,
|
||||
searchWorkspaceFiles,
|
||||
} from "../utils/file-search"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { parseImagesFromInput } from "../utils/parser"
|
||||
import { AccountInfoView } from "./AccountInfoView"
|
||||
import { FileMentionMenu } from "./FileMentionMenu"
|
||||
|
||||
interface WelcomeViewProps {
|
||||
onSubmit: (prompt: string, imagePaths: string[]) => void
|
||||
onExit?: () => void
|
||||
controller?: any
|
||||
}
|
||||
|
||||
// ASCII art Cline logo
|
||||
const CLINE_LOGO = [
|
||||
" ::::::: ",
|
||||
" ::::::::: ",
|
||||
" ::::::::::::::::: ",
|
||||
" ::::::::::::::::::::::: ",
|
||||
" ::::::::::::::::::::::::: ",
|
||||
" ::::::::::::::::::::::::::: ",
|
||||
" ::::::: ::::::: ::::::: ",
|
||||
" ::::::: ::::: ::::::: ",
|
||||
":::::::: ::::: ::::::::",
|
||||
":::::::: ::::: ::::::::",
|
||||
" ::::::: ::::: ::::::: ",
|
||||
" ::::::: ::::::: ::::::: ",
|
||||
" ::::::::::::::::::::::::::: ",
|
||||
" ::::::::::::::::::::::::: ",
|
||||
" ::::::::::::::::::::::: ",
|
||||
" :::::::::::::::: ",
|
||||
]
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 150
|
||||
const RIPGREP_WARNING_DURATION_MS = 5000
|
||||
const MAX_SEARCH_RESULTS = 15
|
||||
|
||||
export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, controller }) => {
|
||||
const { isRawModeSupported } = useStdinContext()
|
||||
const [textInput, setTextInput] = useState("")
|
||||
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [showRipgrepWarning, setShowRipgrepWarning] = useState(false)
|
||||
const [escPressedOnce, setEscPressedOnce] = useState(false)
|
||||
const [mode, setMode] = useState<Mode>(() => {
|
||||
const stateManager = StateManager.get()
|
||||
return stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
})
|
||||
|
||||
const provider = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") as string
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as string
|
||||
return currentProvider || "cline"
|
||||
}, [controller])
|
||||
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelId = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (
|
||||
(stateManager.getGlobalSettingsKey(modelKey as SettingsKey) as string) ||
|
||||
getProviderDefaultModelId(provider as ApiProvider)
|
||||
)
|
||||
}, [mode, provider])
|
||||
|
||||
const toggleMode = useCallback(() => {
|
||||
const newMode: Mode = mode === "act" ? "plan" : "act"
|
||||
setMode(newMode)
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("mode", newMode)
|
||||
}, [mode])
|
||||
|
||||
const refs = useRef({
|
||||
searchTimeout: null as NodeJS.Timeout | null,
|
||||
lastQuery: "",
|
||||
hasCheckedRipgrep: false,
|
||||
})
|
||||
|
||||
const { prompt, imagePaths } = parseImagesFromInput(textInput)
|
||||
|
||||
const mentionInfo = useMemo(() => extractMentionQuery(textInput), [textInput])
|
||||
|
||||
const workspacePath = useMemo(() => {
|
||||
try {
|
||||
const root = controller?.getWorkspaceManagerSync?.()?.getPrimaryRoot?.()
|
||||
if (root?.path) {
|
||||
return root.path
|
||||
}
|
||||
} catch {
|
||||
// Fallback to cwd
|
||||
}
|
||||
return process.cwd()
|
||||
}, [controller])
|
||||
|
||||
// Search for files when in mention mode
|
||||
useEffect(() => {
|
||||
const { current: r } = refs
|
||||
|
||||
if (!mentionInfo.inMentionMode) {
|
||||
setFileResults([])
|
||||
setSelectedIndex(0)
|
||||
if (r.searchTimeout) {
|
||||
clearTimeout(r.searchTimeout)
|
||||
r.searchTimeout = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check for ripgrep on first mention trigger
|
||||
if (!r.hasCheckedRipgrep) {
|
||||
r.hasCheckedRipgrep = true
|
||||
if (checkAndWarnRipgrepMissing()) {
|
||||
setShowRipgrepWarning(true)
|
||||
setTimeout(() => setShowRipgrepWarning(false), RIPGREP_WARNING_DURATION_MS)
|
||||
}
|
||||
}
|
||||
|
||||
const { query } = mentionInfo
|
||||
if (query === r.lastQuery) {
|
||||
return
|
||||
}
|
||||
r.lastQuery = query
|
||||
|
||||
if (r.searchTimeout) {
|
||||
clearTimeout(r.searchTimeout)
|
||||
}
|
||||
setIsSearching(true)
|
||||
|
||||
r.searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const results = await searchWorkspaceFiles(query, workspacePath, MAX_SEARCH_RESULTS)
|
||||
setFileResults(results)
|
||||
setSelectedIndex(0)
|
||||
} catch {
|
||||
setFileResults([])
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS)
|
||||
|
||||
return () => {
|
||||
if (r.searchTimeout) {
|
||||
clearTimeout(r.searchTimeout)
|
||||
}
|
||||
}
|
||||
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
// Filter out mouse escape sequences
|
||||
if (isMouseEscapeSequence(input)) {
|
||||
return
|
||||
}
|
||||
|
||||
const inMenu = mentionInfo.inMentionMode && fileResults.length > 0
|
||||
|
||||
// Menu navigation
|
||||
if (inMenu) {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((i) => (i > 0 ? i - 1 : fileResults.length - 1))
|
||||
return
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setSelectedIndex((i) => (i < fileResults.length - 1 ? i + 1 : 0))
|
||||
return
|
||||
}
|
||||
if (key.tab || key.return) {
|
||||
const file = fileResults[selectedIndex]
|
||||
if (file) {
|
||||
setTextInput(insertMention(textInput, mentionInfo.atIndex, file.path))
|
||||
setFileResults([])
|
||||
setSelectedIndex(0)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key.escape) {
|
||||
setFileResults([])
|
||||
setSelectedIndex(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Normal input handling
|
||||
if (key.tab && !mentionInfo.inMentionMode) {
|
||||
toggleMode()
|
||||
return
|
||||
}
|
||||
if (key.return && !mentionInfo.inMentionMode) {
|
||||
if (prompt.trim() || imagePaths.length > 0) {
|
||||
onSubmit(prompt.trim(), imagePaths)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key.escape && !mentionInfo.inMentionMode) {
|
||||
if (escPressedOnce) {
|
||||
onExit?.()
|
||||
} else {
|
||||
setEscPressedOnce(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
setTextInput((prev) => prev.slice(0, -1))
|
||||
setEscPressedOnce(false)
|
||||
return
|
||||
}
|
||||
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
|
||||
setTextInput((prev) => prev + input)
|
||||
setEscPressedOnce(false)
|
||||
}
|
||||
},
|
||||
{ isActive: isRawModeSupported },
|
||||
)
|
||||
|
||||
const borderColor = mode === "act" ? "blue" : "yellow"
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Account/Provider info at top */}
|
||||
{controller && (
|
||||
<Box marginBottom={1}>
|
||||
<AccountInfoView controller={controller} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Cline logo - centered */}
|
||||
<Box alignItems="center" flexDirection="column">
|
||||
{CLINE_LOGO.map((line, idx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array that never changes
|
||||
<Text color="white" key={idx}>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Main prompt - centered, bold */}
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text bold color="white">
|
||||
What can I do for you?
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Ripgrep warning if needed */}
|
||||
{showRipgrepWarning && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="yellow">⚠ ripgrep not found - file search will be slower. </Text>
|
||||
<Text color="gray">Install: {getRipgrepInstallInstructions()}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Input field with border */}
|
||||
<Box
|
||||
borderColor={borderColor}
|
||||
borderStyle="round"
|
||||
flexDirection="row"
|
||||
marginTop={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
width="100%">
|
||||
<Text>{textInput}</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
|
||||
{/* Model ID and Mode toggle row */}
|
||||
<Box justifyContent="space-between" width="100%">
|
||||
{/* Model ID on left */}
|
||||
<Text color="gray">{modelId}</Text>
|
||||
|
||||
{/* Mode toggle on right */}
|
||||
<Box gap={1}>
|
||||
<Box>
|
||||
<Text bold={mode === "plan"} color={mode === "plan" ? "yellow" : "gray"}>
|
||||
{mode === "plan" ? "●" : "○"} Plan
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold={mode === "act"} color={mode === "act" ? "blue" : "gray"}>
|
||||
{mode === "act" ? "●" : "○"} Act
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color="gray">(Tab)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* File mention menu - below input */}
|
||||
{mentionInfo.inMentionMode && (
|
||||
<FileMentionMenu
|
||||
isLoading={isSearching}
|
||||
query={mentionInfo.query}
|
||||
results={fileResults}
|
||||
selectedIndex={selectedIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Attached images */}
|
||||
{imagePaths.length > 0 && (
|
||||
<Text color="magenta">
|
||||
📎 {imagePaths.length} image{imagePaths.length > 1 ? "s" : ""} attached
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Help text */}
|
||||
<Box>
|
||||
<Text color="gray">Enter to submit · @ to mention files · </Text>
|
||||
<Text bold={escPressedOnce} color={escPressedOnce ? "white" : "gray"}>
|
||||
{escPressedOnce ? "Press Esc again to exit" : "Esc to exit"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getAllFeaturedModels } from "./featured-models"
|
||||
import { getAllFeaturedModels, mapRecommendedModelsToFeaturedModels } from "./featured-models"
|
||||
|
||||
describe("featured models", () => {
|
||||
it("includes display names for all featured models", () => {
|
||||
@@ -9,4 +9,15 @@ describe("featured models", () => {
|
||||
expect(model.name).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it("fills free model metadata from fallback when upstream payload is sparse", () => {
|
||||
const models = mapRecommendedModelsToFeaturedModels({
|
||||
recommended: [],
|
||||
free: [{ id: "trinity-large-preview:free", name: "trinity-large-preview:free", description: "", tags: [] }],
|
||||
})
|
||||
|
||||
expect(models.free[0]?.name).toBe("Arcee AI Trinity Large Preview")
|
||||
expect(models.free[0]?.description).toBe("Arcee AI's advanced large preview model in the Trinity series")
|
||||
expect(models.free[0]?.labels).toContain("FREE")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Featured models shown in the Cline model picker during onboarding
|
||||
* These are curated models that work well with Cline
|
||||
*/
|
||||
import { CLINE_RECOMMENDED_MODELS_FALLBACK } from "@shared/cline/recommended-models"
|
||||
|
||||
export interface FeaturedModel {
|
||||
id: string
|
||||
@@ -10,61 +11,81 @@ export interface FeaturedModel {
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS: { recommended: FeaturedModel[]; free: FeaturedModel[] } = {
|
||||
recommended: [
|
||||
{
|
||||
id: "google/gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro Preview",
|
||||
description: "Latest Gemini release with 1m ctx window and strong coding performance",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4.6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
description: "Latest Sonnet release with strong coding and agent performance",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
name: "Claude Opus 4.6",
|
||||
description: "Most intelligent model for agents and coding",
|
||||
labels: ["BEST"],
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2-codex",
|
||||
name: "GPT 5.2 Codex",
|
||||
description: "OpenAI's latest with strong coding abilities",
|
||||
labels: ["HOT"],
|
||||
},
|
||||
],
|
||||
free: [
|
||||
{
|
||||
id: "minimax/minimax-m2.5",
|
||||
name: "MiniMax M2.5",
|
||||
description: "MiniMax-M2.5 is a lightweight, state-of-the-art LLM optimized for coding and agentic workflows",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "z-ai/glm-5",
|
||||
name: "Z-AI GLM5",
|
||||
description: "Z.AI's latest GLM 5 model with strong coding and agent performance",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "kwaipilot/kat-coder-pro",
|
||||
name: "KAT Coder Pro",
|
||||
description: "KwaiKAT's most advanced agentic coding model in the KAT-Coder series",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "arcee-ai/trinity-large-preview:free",
|
||||
name: "Trinity Large Preview",
|
||||
description: "Arcee AI's advanced large preview model in the Trinity series",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
],
|
||||
type RecommendedModelLike = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export function getAllFeaturedModels(): FeaturedModel[] {
|
||||
return [...FEATURED_MODELS.recommended, ...FEATURED_MODELS.free]
|
||||
export interface FeaturedModelsByTier {
|
||||
recommended: FeaturedModel[]
|
||||
free: FeaturedModel[]
|
||||
}
|
||||
|
||||
interface RecommendedModelsByTier {
|
||||
recommended: RecommendedModelLike[]
|
||||
free: RecommendedModelLike[]
|
||||
}
|
||||
|
||||
function toFeaturedModel(model: RecommendedModelLike): FeaturedModel {
|
||||
return {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
description: model.description,
|
||||
labels: model.tags,
|
||||
}
|
||||
}
|
||||
|
||||
function getModelIdSuffix(id: string): string {
|
||||
const lastSlashIndex = id.lastIndexOf("/")
|
||||
return lastSlashIndex >= 0 ? id.slice(lastSlashIndex + 1) : id
|
||||
}
|
||||
|
||||
function findFallbackFeaturedModelById(models: FeaturedModel[], id: string): FeaturedModel | undefined {
|
||||
const idSuffix = getModelIdSuffix(id)
|
||||
return models.find((model) => model.id === id || getModelIdSuffix(model.id) === idSuffix)
|
||||
}
|
||||
|
||||
function mapRecommendedModelToFeaturedModelWithFallback(
|
||||
model: RecommendedModelLike,
|
||||
fallbackModels: FeaturedModel[],
|
||||
defaultLabels: string[] = [],
|
||||
): FeaturedModel {
|
||||
const fallbackModel = findFallbackFeaturedModelById(fallbackModels, model.id)
|
||||
const upstreamNameLooksLikeFallback = model.name === model.id || model.name.trim().length === 0
|
||||
const name = upstreamNameLooksLikeFallback ? (fallbackModel?.name ?? model.name) : model.name
|
||||
const description = model.description.trim().length > 0 ? model.description : (fallbackModel?.description ?? "")
|
||||
const labels = model.tags.length > 0 ? model.tags : (fallbackModel?.labels ?? defaultLabels)
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
name,
|
||||
description,
|
||||
labels,
|
||||
}
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS: FeaturedModelsByTier = {
|
||||
recommended: CLINE_RECOMMENDED_MODELS_FALLBACK.recommended.map(toFeaturedModel),
|
||||
free: CLINE_RECOMMENDED_MODELS_FALLBACK.free.map(toFeaturedModel),
|
||||
}
|
||||
|
||||
export function getAllFeaturedModels(modelsByTier: FeaturedModelsByTier = FEATURED_MODELS): FeaturedModel[] {
|
||||
return [...modelsByTier.recommended, ...modelsByTier.free]
|
||||
}
|
||||
|
||||
export function mapRecommendedModelsToFeaturedModels(data: RecommendedModelsByTier): FeaturedModelsByTier {
|
||||
return {
|
||||
recommended: data.recommended.map((model) =>
|
||||
mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.recommended),
|
||||
),
|
||||
free: data.free.map((model) => mapRecommendedModelToFeaturedModelWithFallback(model, FEATURED_MODELS.free, ["FREE"])),
|
||||
}
|
||||
}
|
||||
|
||||
export function withFeaturedModelFallback(modelsByTier: FeaturedModelsByTier): FeaturedModelsByTier {
|
||||
const recommended = modelsByTier.recommended.length > 0 ? modelsByTier.recommended : FEATURED_MODELS.recommended
|
||||
const free = modelsByTier.free.length > 0 ? modelsByTier.free : FEATURED_MODELS.free
|
||||
return { recommended, free }
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class CliDiffServiceClient implements DiffServiceClientInterface {
|
||||
* CLI implementation of EnvService - handles environment operations
|
||||
*/
|
||||
export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
private clipboardContent: string = ""
|
||||
private clipboardContent = ""
|
||||
|
||||
private getTelemetrySetting(): proto.host.Setting {
|
||||
// Read from StateManager - defaults to ENABLED if not set or "unset"
|
||||
@@ -102,6 +102,8 @@ export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
version: CLI_VERSION,
|
||||
platform: "Cline CLI - Node.js",
|
||||
clineType: ClineClient.Cli,
|
||||
// remoteName is intentionally omitted — the CLI runs locally on the user's machine.
|
||||
// If CLI-in-container scenarios arise, populate this field to enable remote cadence tuning.
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Cline Library Exports
|
||||
*
|
||||
* This file exports the public API for programmatic use of Cline.
|
||||
* Use these classes and types to embed Cline into your applications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ClineAgent } from "cline"
|
||||
*
|
||||
* const agent = new ClineAgent()
|
||||
* await agent.initialize({ clientCapabilities: {} })
|
||||
* const session = await agent.newSession({ cwd: process.cwd() })
|
||||
* ```
|
||||
* @module cline
|
||||
*/
|
||||
|
||||
export { ClineAgent } from "./agent/ClineAgent.js"
|
||||
export { ClineSessionEmitter } from "./agent/ClineSessionEmitter.js"
|
||||
export type {
|
||||
AcpAgentOptions,
|
||||
AcpSessionState,
|
||||
AcpSessionStatus,
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AudioContent,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
ClineAcpSession,
|
||||
ClineAgentCapabilities,
|
||||
ClineAgentInfo,
|
||||
ClineAgentOptions,
|
||||
ClinePermissionOption,
|
||||
ClineSessionEvents,
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PermissionHandler,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionConfigOption,
|
||||
SessionModelState,
|
||||
SessionNotification,
|
||||
SessionUpdate,
|
||||
SessionUpdatePayload,
|
||||
SessionUpdateType,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallStatus,
|
||||
ToolCallUpdate,
|
||||
ToolKind,
|
||||
TranslatedMessage,
|
||||
} from "./agent/public-types.js"
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { refreshClineRecommendedModels } from "@/core/controller/models/refreshClineRecommendedModels"
|
||||
import {
|
||||
type FeaturedModel,
|
||||
getAllFeaturedModels,
|
||||
mapRecommendedModelsToFeaturedModels,
|
||||
withFeaturedModelFallback,
|
||||
} from "../constants/featured-models"
|
||||
|
||||
export function useClineFeaturedModels(): FeaturedModel[] {
|
||||
const [featuredModels, setFeaturedModels] = useState<FeaturedModel[]>(() => getAllFeaturedModels())
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
try {
|
||||
const recommendedModels = await refreshClineRecommendedModels()
|
||||
const mappedModels = mapRecommendedModelsToFeaturedModels(recommendedModels)
|
||||
const modelsWithFallback = withFeaturedModelFallback(mappedModels)
|
||||
if (!cancelled) {
|
||||
setFeaturedModels(getAllFeaturedModels(modelsWithFallback))
|
||||
}
|
||||
} catch {
|
||||
// Keep local fallback models on error.
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return featuredModels
|
||||
}
|
||||
@@ -26,6 +26,9 @@ import { useCallback, useEffect, useRef, useState } from "react"
|
||||
* to unmount and remount everything from scratch. This resets Ink's internal tracking
|
||||
* AND re-renders Static content since the components are brand new instances.
|
||||
*
|
||||
* We only run this full recovery when terminal width changes. Height-only resizes do not
|
||||
* affect wrapping in the same way and should not restart the task view.
|
||||
*
|
||||
* Gemini CLI does the same thing in AppContainer.tsx: debounce 300ms, then
|
||||
* stdout.write(ansiEscapes.clearTerminal) + setHistoryRemountKey(prev => prev + 1).
|
||||
*
|
||||
@@ -41,6 +44,8 @@ export function useTerminalSize() {
|
||||
})
|
||||
const [resizeKey, setResizeKey] = useState(0)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const previousColumnsRef = useRef(process.stdout.columns || 80)
|
||||
const pendingWidthRefreshRef = useRef(false)
|
||||
|
||||
const refreshAfterResize = useCallback(() => {
|
||||
// Clear terminal + scrollback to wipe stale content from old width
|
||||
@@ -56,17 +61,33 @@ export function useTerminalSize() {
|
||||
|
||||
useEffect(() => {
|
||||
function updateSize() {
|
||||
const nextColumns = process.stdout.columns || 80
|
||||
const nextRows = process.stdout.rows || 24
|
||||
const didWidthChange = nextColumns !== previousColumnsRef.current
|
||||
previousColumnsRef.current = nextColumns
|
||||
|
||||
setSize({
|
||||
columns: process.stdout.columns || 80,
|
||||
rows: process.stdout.rows || 24,
|
||||
columns: nextColumns,
|
||||
rows: nextRows,
|
||||
})
|
||||
|
||||
if (didWidthChange) {
|
||||
pendingWidthRefreshRef.current = true
|
||||
}
|
||||
|
||||
if (!pendingWidthRefreshRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Debounce: wait 300ms after last resize event to do full recovery
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
refreshAfterResize()
|
||||
if (pendingWidthRefreshRef.current) {
|
||||
refreshAfterResize()
|
||||
pendingWidthRefreshRef.current = false
|
||||
}
|
||||
debounceRef.current = null
|
||||
}, 300)
|
||||
}
|
||||
@@ -76,6 +97,7 @@ export function useTerminalSize() {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
pendingWidthRefreshRef.current = false
|
||||
}
|
||||
}, [refreshAfterResize])
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user